Compare commits

...

12 Commits

Author SHA1 Message Date
f72d2c1b2b UI: Add Mii Edit Applet Locale (#311)
This allows the "Mii Edit Applet" dropdown to be localized ( I already
went ahead and localized French )
2024-11-25 13:43:01 -06:00
a18cecbc30 Korean "Show Changelog" translation (#313) 2024-11-25 13:40:39 -06:00
2e6794e69b Add custom refresh rate mode to VSync option (#238)
Rebased @jcm93's refreshinterval branch:
https://github.com/jcm93/Ryujinx/tree/refreshinterval

The option is placed under System/Hacks. Disabled, it's the default
Ryujinx behavior. Enabled, the behavior is shown in the attached
screenshots. If a framerate is too high or low, you can adjust the value
where you normally toggle VSync on and off. It will also cycle through
the default on/off toggles.

Also, in order to reduce clutter, I made an adjustment to remove the
target FPS and only show the percentage.

---------

Co-authored-by: jcm <6864788+jcm93@users.noreply.github.com>
2024-11-25 13:39:09 -06:00
7e16fccfc1 UI: Fix icons getting cutoff in the About window (#310)
Before:


![image](https://github.com/user-attachments/assets/c8d6b7d5-487b-4ab9-83e3-9489eaa0a076)

After:


![image](https://github.com/user-attachments/assets/18ea6360-f6ee-48e6-9a0a-cd8d88a0cf51)
2024-11-24 11:33:53 -06:00
a81212bbf1 Fix window decorations being too wide (#309) 2024-11-24 09:49:44 -06:00
e8d3ad4d8b UI: RPC: TSUKIHIME -A piece of blue glass moon- asset image 2024-11-23 13:10:53 -06:00
3b6731a351 infra: Undo packing native libraries into executable. 2024-11-22 17:51:44 -06:00
e653848a2c JIT Sparse Function Table (#250)
More up to date build of the JIT Sparse PR for continued development.
JIT Sparse Function Table was originally developed by riperiperi for the
original Ryujinx project, and decreased the amount of layers in the
Function Table structure, to decrease lookup times at the cost of
slightly higher RAM usage.
This PR rebalances the JIT Sparse Function Table to be a bit more RAM
intensive, but faster in workloads where the JIT Function Table is a
bottleneck. Faster RAM will see a bigger impact and slower RAM (DDR3 and
potentially slow DDR4) will see a slight performance decrease.
This PR also implements a base for a PPTC profile system that could
allow for PPTC with ExeFS mods enabled in the future.
This PR also potentially fixes a strange issue where Avalonia would time
out in some rare instances, e.g. when running ExeFS mods with TotK and a
strange controller configuration.

---------

Co-authored-by: Evan Husted <gr33m11@gmail.com>
2024-11-22 15:33:44 -06:00
5534001152 UI: Always save screenshots to the Ryujinx data directory. 2024-11-22 15:08:24 -06:00
e05875a079 UI: It's called "live testing." 2024-11-22 14:52:56 -06:00
49eeb26b6f UI: I may be stupid. Primary button result is Ok, not Yes. 2024-11-22 14:46:10 -06:00
f8d63f9a2f UI: Add a show changelog button in the Updater, for new updates & when you're on the latest version. 2024-11-22 14:38:58 -06:00
88 changed files with 1799 additions and 464 deletions

View File

@ -103,8 +103,8 @@ jobs:
- name: Publish
run: |
dotnet publish -c Release -r "${{ matrix.platform.name }}" -o ./publish_ava/publish -p:Version="${{ steps.version_info.outputs.build_version }}" -p:SourceRevisionId="${{ steps.version_info.outputs.git_short_hash }}" -p:DebugType=embedded src/Ryujinx --self-contained -p:IncludeNativeLibrariesForSelfExtract=true
dotnet publish -c Release -r "${{ matrix.platform.name }}" -o ./publish_sdl2_headless/publish -p:Version="${{ steps.version_info.outputs.build_version }}" -p:SourceRevisionId="${{ steps.version_info.outputs.git_short_hash }}" -p:DebugType=embedded src/Ryujinx.Headless.SDL2 --self-contained -p:IncludeNativeLibrariesForSelfExtract=true
dotnet publish -c Release -r "${{ matrix.platform.name }}" -o ./publish_ava/publish -p:Version="${{ steps.version_info.outputs.build_version }}" -p:SourceRevisionId="${{ steps.version_info.outputs.git_short_hash }}" -p:DebugType=embedded src/Ryujinx --self-contained
dotnet publish -c Release -r "${{ matrix.platform.name }}" -o ./publish_sdl2_headless/publish -p:Version="${{ steps.version_info.outputs.build_version }}" -p:SourceRevisionId="${{ steps.version_info.outputs.git_short_hash }}" -p:DebugType=embedded src/Ryujinx.Headless.SDL2 --self-contained
- name: Packing Windows builds
if: matrix.platform.os == 'windows-latest'

View File

@ -102,8 +102,8 @@ jobs:
- name: Publish
run: |
dotnet publish -c Release -r "${{ matrix.platform.name }}" -o ./publish -p:Version="${{ steps.version_info.outputs.build_version }}" -p:SourceRevisionId="${{ steps.version_info.outputs.git_short_hash }}" -p:DebugType=embedded src/Ryujinx --self-contained -p:IncludeNativeLibrariesForSelfExtract=true
dotnet publish -c Release -r "${{ matrix.platform.name }}" -o ./publish_sdl2_headless -p:Version="${{ steps.version_info.outputs.build_version }}" -p:SourceRevisionId="${{ steps.version_info.outputs.git_short_hash }}" -p:DebugType=embedded src/Ryujinx.Headless.SDL2 --self-contained -p:IncludeNativeLibrariesForSelfExtract=true
dotnet publish -c Release -r "${{ matrix.platform.name }}" -o ./publish -p:Version="${{ steps.version_info.outputs.build_version }}" -p:SourceRevisionId="${{ steps.version_info.outputs.git_short_hash }}" -p:DebugType=embedded src/Ryujinx --self-contained
dotnet publish -c Release -r "${{ matrix.platform.name }}" -o ./publish_sdl2_headless -p:Version="${{ steps.version_info.outputs.build_version }}" -p:SourceRevisionId="${{ steps.version_info.outputs.git_short_hash }}" -p:DebugType=embedded src/Ryujinx.Headless.SDL2 --self-contained
- name: Packing Windows builds
if: matrix.platform.os == 'windows-latest'

View File

@ -38,7 +38,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.6.5" />
<PackageVersion Include="Gommon" Version="2.6.6" />
<PackageVersion Include="securifybv.ShellLink" Version="0.1.0" />
<PackageVersion Include="shaderc.net" Version="0.1.0" />
<PackageVersion Include="SharpZipLib" Version="1.4.2" />
@ -52,4 +52,4 @@
<PackageVersion Include="System.Management" Version="8.0.0" />
<PackageVersion Include="UnicornEngine.Unicorn" Version="2.0.2-rc1-fb78016" />
</ItemGroup>
</Project>
</Project>

View File

@ -1,252 +0,0 @@
using ARMeilleure.Diagnostics;
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
namespace ARMeilleure.Common
{
/// <summary>
/// Represents a table of guest address to a value.
/// </summary>
/// <typeparam name="TEntry">Type of the value</typeparam>
public unsafe class AddressTable<TEntry> : IDisposable where TEntry : unmanaged
{
/// <summary>
/// Represents a level in an <see cref="AddressTable{TEntry}"/>.
/// </summary>
public readonly struct Level
{
/// <summary>
/// Gets the index of the <see cref="Level"/> in the guest address.
/// </summary>
public int Index { get; }
/// <summary>
/// Gets the length of the <see cref="Level"/> in the guest address.
/// </summary>
public int Length { get; }
/// <summary>
/// Gets the mask which masks the bits used by the <see cref="Level"/>.
/// </summary>
public ulong Mask => ((1ul << Length) - 1) << Index;
/// <summary>
/// Initializes a new instance of the <see cref="Level"/> structure with the specified
/// <paramref name="index"/> and <paramref name="length"/>.
/// </summary>
/// <param name="index">Index of the <see cref="Level"/></param>
/// <param name="length">Length of the <see cref="Level"/></param>
public Level(int index, int length)
{
(Index, Length) = (index, length);
}
/// <summary>
/// Gets the value of the <see cref="Level"/> from the specified guest <paramref name="address"/>.
/// </summary>
/// <param name="address">Guest address</param>
/// <returns>Value of the <see cref="Level"/> from the specified guest <paramref name="address"/></returns>
public int GetValue(ulong address)
{
return (int)((address & Mask) >> Index);
}
}
private bool _disposed;
private TEntry** _table;
private readonly List<nint> _pages;
/// <summary>
/// Gets the bits used by the <see cref="Levels"/> of the <see cref="AddressTable{TEntry}"/> instance.
/// </summary>
public ulong Mask { get; }
/// <summary>
/// Gets the <see cref="Level"/>s used by the <see cref="AddressTable{TEntry}"/> instance.
/// </summary>
public Level[] Levels { get; }
/// <summary>
/// Gets or sets the default fill value of newly created leaf pages.
/// </summary>
public TEntry Fill { get; set; }
/// <summary>
/// Gets the base address of the <see cref="EntryTable{TEntry}"/>.
/// </summary>
/// <exception cref="ObjectDisposedException"><see cref="EntryTable{TEntry}"/> instance was disposed</exception>
public nint Base
{
get
{
ObjectDisposedException.ThrowIf(_disposed, this);
lock (_pages)
{
return (nint)GetRootPage();
}
}
}
/// <summary>
/// Constructs a new instance of the <see cref="AddressTable{TEntry}"/> class with the specified list of
/// <see cref="Level"/>.
/// </summary>
/// <exception cref="ArgumentNullException"><paramref name="levels"/> is null</exception>
/// <exception cref="ArgumentException">Length of <paramref name="levels"/> is less than 2</exception>
public AddressTable(Level[] levels)
{
ArgumentNullException.ThrowIfNull(levels);
if (levels.Length < 2)
{
throw new ArgumentException("Table must be at least 2 levels deep.", nameof(levels));
}
_pages = new List<nint>(capacity: 16);
Levels = levels;
Mask = 0;
foreach (var level in Levels)
{
Mask |= level.Mask;
}
}
/// <summary>
/// Determines if the specified <paramref name="address"/> is in the range of the
/// <see cref="AddressTable{TEntry}"/>.
/// </summary>
/// <param name="address">Guest address</param>
/// <returns><see langword="true"/> if is valid; otherwise <see langword="false"/></returns>
public bool IsValid(ulong address)
{
return (address & ~Mask) == 0;
}
/// <summary>
/// Gets a reference to the value at the specified guest <paramref name="address"/>.
/// </summary>
/// <param name="address">Guest address</param>
/// <returns>Reference to the value at the specified guest <paramref name="address"/></returns>
/// <exception cref="ObjectDisposedException"><see cref="EntryTable{TEntry}"/> instance was disposed</exception>
/// <exception cref="ArgumentException"><paramref name="address"/> is not mapped</exception>
public ref TEntry GetValue(ulong address)
{
ObjectDisposedException.ThrowIf(_disposed, this);
if (!IsValid(address))
{
throw new ArgumentException($"Address 0x{address:X} is not mapped onto the table.", nameof(address));
}
lock (_pages)
{
return ref GetPage(address)[Levels[^1].GetValue(address)];
}
}
/// <summary>
/// Gets the leaf page for the specified guest <paramref name="address"/>.
/// </summary>
/// <param name="address">Guest address</param>
/// <returns>Leaf page for the specified guest <paramref name="address"/></returns>
private TEntry* GetPage(ulong address)
{
TEntry** page = GetRootPage();
for (int i = 0; i < Levels.Length - 1; i++)
{
ref Level level = ref Levels[i];
ref TEntry* nextPage = ref page[level.GetValue(address)];
if (nextPage == null)
{
ref Level nextLevel = ref Levels[i + 1];
nextPage = i == Levels.Length - 2 ?
(TEntry*)Allocate(1 << nextLevel.Length, Fill, leaf: true) :
(TEntry*)Allocate(1 << nextLevel.Length, nint.Zero, leaf: false);
}
page = (TEntry**)nextPage;
}
return (TEntry*)page;
}
/// <summary>
/// Lazily initialize and get the root page of the <see cref="AddressTable{TEntry}"/>.
/// </summary>
/// <returns>Root page of the <see cref="AddressTable{TEntry}"/></returns>
private TEntry** GetRootPage()
{
if (_table == null)
{
_table = (TEntry**)Allocate(1 << Levels[0].Length, fill: nint.Zero, leaf: false);
}
return _table;
}
/// <summary>
/// Allocates a block of memory of the specified type and length.
/// </summary>
/// <typeparam name="T">Type of elements</typeparam>
/// <param name="length">Number of elements</param>
/// <param name="fill">Fill value</param>
/// <param name="leaf"><see langword="true"/> if leaf; otherwise <see langword="false"/></param>
/// <returns>Allocated block</returns>
private nint Allocate<T>(int length, T fill, bool leaf) where T : unmanaged
{
var size = sizeof(T) * length;
var page = (nint)NativeAllocator.Instance.Allocate((uint)size);
var span = new Span<T>((void*)page, length);
span.Fill(fill);
_pages.Add(page);
TranslatorEventSource.Log.AddressTableAllocated(size, leaf);
return page;
}
/// <summary>
/// Releases all resources used by the <see cref="AddressTable{TEntry}"/> instance.
/// </summary>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// Releases all unmanaged and optionally managed resources used by the <see cref="AddressTable{TEntry}"/>
/// instance.
/// </summary>
/// <param name="disposing"><see langword="true"/> to dispose managed resources also; otherwise just unmanaged resouces</param>
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
foreach (var page in _pages)
{
Marshal.FreeHGlobal(page);
}
_disposed = true;
}
}
/// <summary>
/// Frees resources used by the <see cref="AddressTable{TEntry}"/> instance.
/// </summary>
~AddressTable()
{
Dispose(false);
}
}
}

View File

@ -0,0 +1,44 @@
namespace ARMeilleure.Common
{
/// <summary>
/// Represents a level in an <see cref="IAddressTable{TEntry}"/>.
/// </summary>
public readonly struct AddressTableLevel
{
/// <summary>
/// Gets the index of the <see cref="Level"/> in the guest address.
/// </summary>
public int Index { get; }
/// <summary>
/// Gets the length of the <see cref="AddressTableLevel"/> in the guest address.
/// </summary>
public int Length { get; }
/// <summary>
/// Gets the mask which masks the bits used by the <see cref="AddressTableLevel"/>.
/// </summary>
public ulong Mask => ((1ul << Length) - 1) << Index;
/// <summary>
/// Initializes a new instance of the <see cref="AddressTableLevel"/> structure with the specified
/// <paramref name="index"/> and <paramref name="length"/>.
/// </summary>
/// <param name="index">Index of the <see cref="AddressTableLevel"/></param>
/// <param name="length">Length of the <see cref="AddressTableLevel"/></param>
public AddressTableLevel(int index, int length)
{
(Index, Length) = (index, length);
}
/// <summary>
/// Gets the value of the <see cref="AddressTableLevel"/> from the specified guest <paramref name="address"/>.
/// </summary>
/// <param name="address">Guest address</param>
/// <returns>Value of the <see cref="AddressTableLevel"/> from the specified guest <paramref name="address"/></returns>
public int GetValue(ulong address)
{
return (int)((address & Mask) >> Index);
}
}
}

View File

@ -0,0 +1,75 @@
namespace ARMeilleure.Common
{
public static class AddressTablePresets
{
private static readonly AddressTableLevel[] _levels64Bit =
new AddressTableLevel[]
{
new(31, 17),
new(23, 8),
new(15, 8),
new( 7, 8),
new( 2, 5),
};
private static readonly AddressTableLevel[] _levels32Bit =
new AddressTableLevel[]
{
new(31, 17),
new(23, 8),
new(15, 8),
new( 7, 8),
new( 1, 6),
};
private static readonly AddressTableLevel[] _levels64BitSparseTiny =
new AddressTableLevel[]
{
new( 11, 28),
new( 2, 9),
};
private static readonly AddressTableLevel[] _levels32BitSparseTiny =
new AddressTableLevel[]
{
new( 10, 22),
new( 1, 9),
};
private static readonly AddressTableLevel[] _levels64BitSparseGiant =
new AddressTableLevel[]
{
new( 38, 1),
new( 2, 36),
};
private static readonly AddressTableLevel[] _levels32BitSparseGiant =
new AddressTableLevel[]
{
new( 31, 1),
new( 1, 30),
};
//high power will run worse on DDR3 systems and some DDR4 systems due to the higher ram utilization
//low power will never run worse than non-sparse, but for most systems it won't be necessary
//high power is always used, but I've left low power in here for future reference
public static AddressTableLevel[] GetArmPreset(bool for64Bits, bool sparse, bool lowPower = false)
{
if (sparse)
{
if (lowPower)
{
return for64Bits ? _levels64BitSparseTiny : _levels32BitSparseTiny;
}
else
{
return for64Bits ? _levels64BitSparseGiant : _levels32BitSparseGiant;
}
}
else
{
return for64Bits ? _levels64Bit : _levels32Bit;
}
}
}
}

View File

@ -2,7 +2,7 @@ using System;
namespace ARMeilleure.Common
{
unsafe abstract class Allocator : IDisposable
public unsafe abstract class Allocator : IDisposable
{
public T* Allocate<T>(ulong count = 1) where T : unmanaged
{

View File

@ -0,0 +1,51 @@
using System;
namespace ARMeilleure.Common
{
public interface IAddressTable<TEntry> : IDisposable where TEntry : unmanaged
{
/// <summary>
/// True if the address table's bottom level is sparsely mapped.
/// This also ensures the second bottom level is filled with a dummy page rather than 0.
/// </summary>
bool Sparse { get; }
/// <summary>
/// Gets the bits used by the <see cref="Levels"/> of the <see cref="IAddressTable{TEntry}"/> instance.
/// </summary>
ulong Mask { get; }
/// <summary>
/// Gets the <see cref="AddressTableLevel"/>s used by the <see cref="IAddressTable{TEntry}"/> instance.
/// </summary>
AddressTableLevel[] Levels { get; }
/// <summary>
/// Gets or sets the default fill value of newly created leaf pages.
/// </summary>
TEntry Fill { get; set; }
/// <summary>
/// Gets the base address of the <see cref="EntryTable{TEntry}"/>.
/// </summary>
/// <exception cref="ObjectDisposedException"><see cref="EntryTable{TEntry}"/> instance was disposed</exception>
nint Base { get; }
/// <summary>
/// Determines if the specified <paramref name="address"/> is in the range of the
/// <see cref="IAddressTable{TEntry}"/>.
/// </summary>
/// <param name="address">Guest address</param>
/// <returns><see langword="true"/> if is valid; otherwise <see langword="false"/></returns>
bool IsValid(ulong address);
/// <summary>
/// Gets a reference to the value at the specified guest <paramref name="address"/>.
/// </summary>
/// <param name="address">Guest address</param>
/// <returns>Reference to the value at the specified guest <paramref name="address"/></returns>
/// <exception cref="ObjectDisposedException"><see cref="EntryTable{TEntry}"/> instance was disposed</exception>
/// <exception cref="ArgumentException"><paramref name="address"/> is not mapped</exception>
ref TEntry GetValue(ulong address);
}
}

View File

@ -3,7 +3,7 @@ using System.Runtime.InteropServices;
namespace ARMeilleure.Common
{
unsafe sealed class NativeAllocator : Allocator
public unsafe sealed class NativeAllocator : Allocator
{
public static NativeAllocator Instance { get; } = new();

View File

@ -193,6 +193,8 @@ namespace ARMeilleure.Instructions
Operand hostAddress;
var table = context.FunctionTable;
// If address is mapped onto the function table, we can skip the table walk. Otherwise we fallback
// onto the dispatch stub.
if (guestAddress.Kind == OperandKind.Constant && context.FunctionTable.IsValid(guestAddress.Value))
@ -203,6 +205,30 @@ namespace ARMeilleure.Instructions
hostAddress = context.Load(OperandType.I64, hostAddressAddr);
}
else if (table.Sparse)
{
// Inline table lookup. Only enabled when the sparse function table is enabled with 2 levels.
// Deliberately attempts to avoid branches.
Operand tableBase = !context.HasPtc ?
Const(table.Base) :
Const(table.Base, Ptc.FunctionTableSymbol);
hostAddress = tableBase;
for (int i = 0; i < table.Levels.Length; i++)
{
var level = table.Levels[i];
int clearBits = 64 - (level.Index + level.Length);
Operand index = context.ShiftLeft(
context.ShiftRightUI(context.ShiftLeft(guestAddress, Const(clearBits)), Const(clearBits + level.Index)),
Const(3)
);
hostAddress = context.Load(OperandType.I64, context.Add(hostAddress, index));
}
}
else
{
hostAddress = !context.HasPtc ?

View File

@ -8,7 +8,7 @@ namespace ARMeilleure.Signal
{
public static class NativeSignalHandlerGenerator
{
public const int MaxTrackedRanges = 8;
public const int MaxTrackedRanges = 16;
private const int StructAddressOffset = 0;
private const int StructWriteOffset = 4;

View File

@ -46,7 +46,7 @@ namespace ARMeilleure.Translation
public IMemoryManager Memory { get; }
public EntryTable<uint> CountTable { get; }
public AddressTable<ulong> FunctionTable { get; }
public IAddressTable<ulong> FunctionTable { get; }
public TranslatorStubs Stubs { get; }
public ulong EntryAddress { get; }
@ -62,7 +62,7 @@ namespace ARMeilleure.Translation
public ArmEmitterContext(
IMemoryManager memory,
EntryTable<uint> countTable,
AddressTable<ulong> funcTable,
IAddressTable<ulong> funcTable,
TranslatorStubs stubs,
ulong entryAddress,
bool highCq,

View File

@ -30,7 +30,7 @@ namespace ARMeilleure.Translation.PTC
private const string OuterHeaderMagicString = "PTCohd\0\0";
private const string InnerHeaderMagicString = "PTCihd\0\0";
private const uint InternalVersion = 6950; //! To be incremented manually for each change to the ARMeilleure project.
private const uint InternalVersion = 6992; //! To be incremented manually for each change to the ARMeilleure project.
private const string ActualDir = "0";
private const string BackupDir = "1";
@ -41,6 +41,7 @@ namespace ARMeilleure.Translation.PTC
public static readonly Symbol PageTableSymbol = new(SymbolType.Special, 1);
public static readonly Symbol CountTableSymbol = new(SymbolType.Special, 2);
public static readonly Symbol DispatchStubSymbol = new(SymbolType.Special, 3);
public static readonly Symbol FunctionTableSymbol = new(SymbolType.Special, 4);
private const byte FillingByte = 0x00;
private const CompressionLevel SaveCompressionLevel = CompressionLevel.Fastest;
@ -101,7 +102,7 @@ namespace ARMeilleure.Translation.PTC
Disable();
}
public void Initialize(string titleIdText, string displayVersion, bool enabled, MemoryManagerType memoryMode)
public void Initialize(string titleIdText, string displayVersion, bool enabled, MemoryManagerType memoryMode, string cacheSelector)
{
Wait();
@ -127,6 +128,8 @@ namespace ARMeilleure.Translation.PTC
DisplayVersion = !string.IsNullOrEmpty(displayVersion) ? displayVersion : DisplayVersionDefault;
_memoryMode = memoryMode;
Logger.Info?.Print(LogClass.Ptc, $"PPTC (v{InternalVersion}) Profile: {DisplayVersion}-{cacheSelector}");
string workPathActual = Path.Combine(AppDataManager.GamesDirPath, TitleIdText, "cache", "cpu", ActualDir);
string workPathBackup = Path.Combine(AppDataManager.GamesDirPath, TitleIdText, "cache", "cpu", BackupDir);
@ -140,8 +143,8 @@ namespace ARMeilleure.Translation.PTC
Directory.CreateDirectory(workPathBackup);
}
CachePathActual = Path.Combine(workPathActual, DisplayVersion);
CachePathBackup = Path.Combine(workPathBackup, DisplayVersion);
CachePathActual = Path.Combine(workPathActual, DisplayVersion) + "-" + cacheSelector;
CachePathBackup = Path.Combine(workPathBackup, DisplayVersion) + "-" + cacheSelector;
PreLoad();
Profiler.PreLoad();
@ -706,6 +709,10 @@ namespace ARMeilleure.Translation.PTC
{
imm = translator.Stubs.DispatchStub;
}
else if (symbol == FunctionTableSymbol)
{
imm = translator.FunctionTable.Base;
}
if (imm == null)
{

View File

@ -22,33 +22,13 @@ namespace ARMeilleure.Translation
{
public class Translator
{
private static readonly AddressTable<ulong>.Level[] _levels64Bit =
new AddressTable<ulong>.Level[]
{
new(31, 17),
new(23, 8),
new(15, 8),
new( 7, 8),
new( 2, 5),
};
private static readonly AddressTable<ulong>.Level[] _levels32Bit =
new AddressTable<ulong>.Level[]
{
new(31, 17),
new(23, 8),
new(15, 8),
new( 7, 8),
new( 1, 6),
};
private readonly IJitMemoryAllocator _allocator;
private readonly ConcurrentQueue<KeyValuePair<ulong, TranslatedFunction>> _oldFuncs;
private readonly Ptc _ptc;
internal TranslatorCache<TranslatedFunction> Functions { get; }
internal AddressTable<ulong> FunctionTable { get; }
internal IAddressTable<ulong> FunctionTable { get; }
internal EntryTable<uint> CountTable { get; }
internal TranslatorStubs Stubs { get; }
internal TranslatorQueue Queue { get; }
@ -57,7 +37,7 @@ namespace ARMeilleure.Translation
private Thread[] _backgroundTranslationThreads;
private volatile int _threadCount;
public Translator(IJitMemoryAllocator allocator, IMemoryManager memory, bool for64Bits)
public Translator(IJitMemoryAllocator allocator, IMemoryManager memory, IAddressTable<ulong> functionTable)
{
_allocator = allocator;
Memory = memory;
@ -72,15 +52,15 @@ namespace ARMeilleure.Translation
CountTable = new EntryTable<uint>();
Functions = new TranslatorCache<TranslatedFunction>();
FunctionTable = new AddressTable<ulong>(for64Bits ? _levels64Bit : _levels32Bit);
FunctionTable = functionTable;
Stubs = new TranslatorStubs(FunctionTable);
FunctionTable.Fill = (ulong)Stubs.SlowDispatchStub;
}
public IPtcLoadState LoadDiskCache(string titleIdText, string displayVersion, bool enabled)
public IPtcLoadState LoadDiskCache(string titleIdText, string displayVersion, bool enabled, string cacheSelector)
{
_ptc.Initialize(titleIdText, displayVersion, enabled, Memory.Type);
_ptc.Initialize(titleIdText, displayVersion, enabled, Memory.Type, cacheSelector);
return _ptc;
}

View File

@ -19,7 +19,7 @@ namespace ARMeilleure.Translation
private bool _disposed;
private readonly AddressTable<ulong> _functionTable;
private readonly IAddressTable<ulong> _functionTable;
private readonly Lazy<nint> _dispatchStub;
private readonly Lazy<DispatcherFunction> _dispatchLoop;
private readonly Lazy<WrapperFunction> _contextWrapper;
@ -86,7 +86,7 @@ namespace ARMeilleure.Translation
/// </summary>
/// <param name="functionTable">Function table used to store pointers to the functions that the guest code will call</param>
/// <exception cref="ArgumentNullException"><paramref name="translator"/> is null</exception>
public TranslatorStubs(AddressTable<ulong> functionTable)
public TranslatorStubs(IAddressTable<ulong> functionTable)
{
ArgumentNullException.ThrowIfNull(functionTable);

View File

@ -2,7 +2,7 @@ namespace Ryujinx.Common.Configuration.Hid
{
public class KeyboardHotkeys
{
public Key ToggleVsync { get; set; }
public Key ToggleVSyncMode { get; set; }
public Key Screenshot { get; set; }
public Key ShowUI { get; set; }
public Key Pause { get; set; }
@ -11,5 +11,7 @@ namespace Ryujinx.Common.Configuration.Hid
public Key ResScaleDown { get; set; }
public Key VolumeUp { get; set; }
public Key VolumeDown { get; set; }
public Key CustomVSyncIntervalIncrement { get; set; }
public Key CustomVSyncIntervalDecrement { get; set; }
}
}

View File

@ -0,0 +1,9 @@
namespace Ryujinx.Common.Configuration
{
public enum VSyncMode
{
Switch,
Unbounded,
Custom
}
}

View File

@ -1,3 +1,4 @@
using System;
using System.Reflection;
namespace Ryujinx.Common
@ -35,5 +36,13 @@ namespace Ryujinx.Common
public static bool IsReleaseBuild => IsValid && ReleaseChannelName.Equals(ReleaseChannel);
public static string Version => IsValid ? BuildVersion : Assembly.GetEntryAssembly()!.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
public static string GetChangelogUrl(Version currentVersion, Version newVersion) =>
IsCanaryBuild
? $"https://github.com/{ReleaseChannelOwner}/{ReleaseChannelSourceRepo}/compare/Canary-{currentVersion}...Canary-{newVersion}"
: $"https://github.com/{ReleaseChannelOwner}/{ReleaseChannelSourceRepo}/releases/tag/{newVersion}";
public static string GetChangelogForVersion(Version version) =>
$"https://github.com/{ReleaseChannelOwner}/{ReleaseChannelRepo}/releases/tag/{version}";
}
}

View File

@ -0,0 +1,482 @@
using ARMeilleure.Memory;
using Ryujinx.Common;
using Ryujinx.Cpu.Signal;
using Ryujinx.Memory;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using static Ryujinx.Cpu.MemoryEhMeilleure;
namespace ARMeilleure.Common
{
/// <summary>
/// Represents a table of guest address to a value.
/// </summary>
/// <typeparam name="TEntry">Type of the value</typeparam>
public unsafe class AddressTable<TEntry> : IAddressTable<TEntry> where TEntry : unmanaged
{
/// <summary>
/// Represents a page of the address table.
/// </summary>
private readonly struct AddressTablePage
{
/// <summary>
/// True if the allocation belongs to a sparse block, false otherwise.
/// </summary>
public readonly bool IsSparse;
/// <summary>
/// Base address for the page.
/// </summary>
public readonly IntPtr Address;
public AddressTablePage(bool isSparse, IntPtr address)
{
IsSparse = isSparse;
Address = address;
}
}
/// <summary>
/// A sparsely mapped block of memory with a signal handler to map pages as they're accessed.
/// </summary>
private readonly struct TableSparseBlock : IDisposable
{
public readonly SparseMemoryBlock Block;
private readonly TrackingEventDelegate _trackingEvent;
public TableSparseBlock(ulong size, Action<IntPtr> ensureMapped, PageInitDelegate pageInit)
{
var block = new SparseMemoryBlock(size, pageInit, null);
_trackingEvent = (ulong address, ulong size, bool write) =>
{
ulong pointer = (ulong)block.Block.Pointer + address;
ensureMapped((IntPtr)pointer);
return pointer;
};
bool added = NativeSignalHandler.AddTrackedRegion(
(nuint)block.Block.Pointer,
(nuint)(block.Block.Pointer + (IntPtr)block.Block.Size),
Marshal.GetFunctionPointerForDelegate(_trackingEvent));
if (!added)
{
throw new InvalidOperationException("Number of allowed tracked regions exceeded.");
}
Block = block;
}
public void Dispose()
{
NativeSignalHandler.RemoveTrackedRegion((nuint)Block.Block.Pointer);
Block.Dispose();
}
}
private bool _disposed;
private TEntry** _table;
private readonly List<AddressTablePage> _pages;
private TEntry _fill;
private readonly MemoryBlock _sparseFill;
private readonly SparseMemoryBlock _fillBottomLevel;
private readonly TEntry* _fillBottomLevelPtr;
private readonly List<TableSparseBlock> _sparseReserved;
private readonly ReaderWriterLockSlim _sparseLock;
private ulong _sparseBlockSize;
private ulong _sparseReservedOffset;
public bool Sparse { get; }
/// <inheritdoc/>
public ulong Mask { get; }
/// <inheritdoc/>
public AddressTableLevel[] Levels { get; }
/// <inheritdoc/>
public TEntry Fill
{
get
{
return _fill;
}
set
{
UpdateFill(value);
}
}
/// <inheritdoc/>
public IntPtr Base
{
get
{
ObjectDisposedException.ThrowIf(_disposed, this);
lock (_pages)
{
return (IntPtr)GetRootPage();
}
}
}
/// <summary>
/// Constructs a new instance of the <see cref="AddressTable{TEntry}"/> class with the specified list of
/// <see cref="Level"/>.
/// </summary>
/// <param name="levels">Levels for the address table</param>
/// <param name="sparse">True if the bottom page should be sparsely mapped</param>
/// <exception cref="ArgumentNullException"><paramref name="levels"/> is null</exception>
/// <exception cref="ArgumentException">Length of <paramref name="levels"/> is less than 2</exception>
public AddressTable(AddressTableLevel[] levels, bool sparse)
{
ArgumentNullException.ThrowIfNull(levels);
_pages = new List<AddressTablePage>(capacity: 16);
Levels = levels;
Mask = 0;
foreach (var level in Levels)
{
Mask |= level.Mask;
}
Sparse = sparse;
if (sparse)
{
// If the address table is sparse, allocate a fill block
_sparseFill = new MemoryBlock(268435456ul, MemoryAllocationFlags.Mirrorable); //low Power TC uses size: 65536ul
ulong bottomLevelSize = (1ul << levels.Last().Length) * (ulong)sizeof(TEntry);
_fillBottomLevel = new SparseMemoryBlock(bottomLevelSize, null, _sparseFill);
_fillBottomLevelPtr = (TEntry*)_fillBottomLevel.Block.Pointer;
_sparseReserved = new List<TableSparseBlock>();
_sparseLock = new ReaderWriterLockSlim();
_sparseBlockSize = bottomLevelSize;
}
}
/// <summary>
/// Create an <see cref="AddressTable{TEntry}"/> instance for an ARM function table.
/// Selects the best table structure for A32/A64, taking into account the selected memory manager type.
/// </summary>
/// <param name="for64Bits">True if the guest is A64, false otherwise</param>
/// <param name="type">Memory manager type</param>
/// <returns>An <see cref="AddressTable{TEntry}"/> for ARM function lookup</returns>
public static AddressTable<TEntry> CreateForArm(bool for64Bits, MemoryManagerType type)
{
// Assume software memory means that we don't want to use any signal handlers.
bool sparse = type != MemoryManagerType.SoftwareMmu && type != MemoryManagerType.SoftwarePageTable;
return new AddressTable<TEntry>(AddressTablePresets.GetArmPreset(for64Bits, sparse), sparse);
}
/// <summary>
/// Update the fill value for the bottom level of the table.
/// </summary>
/// <param name="fillValue">New fill value</param>
private void UpdateFill(TEntry fillValue)
{
if (_sparseFill != null)
{
Span<byte> span = _sparseFill.GetSpan(0, (int)_sparseFill.Size);
MemoryMarshal.Cast<byte, TEntry>(span).Fill(fillValue);
}
_fill = fillValue;
}
/// <summary>
/// Signal that the given code range exists.
/// </summary>
/// <param name="address"></param>
/// <param name="size"></param>
public void SignalCodeRange(ulong address, ulong size)
{
AddressTableLevel bottom = Levels.Last();
ulong bottomLevelEntries = 1ul << bottom.Length;
ulong entryIndex = address >> bottom.Index;
ulong entries = size >> bottom.Index;
entries += entryIndex - BitUtils.AlignDown(entryIndex, bottomLevelEntries);
_sparseBlockSize = Math.Max(_sparseBlockSize, BitUtils.AlignUp(entries, bottomLevelEntries) * (ulong)sizeof(TEntry));
}
/// <inheritdoc/>
public bool IsValid(ulong address)
{
return (address & ~Mask) == 0;
}
/// <inheritdoc/>
public ref TEntry GetValue(ulong address)
{
ObjectDisposedException.ThrowIf(_disposed, this);
if (!IsValid(address))
{
throw new ArgumentException($"Address 0x{address:X} is not mapped onto the table.", nameof(address));
}
lock (_pages)
{
TEntry* page = GetPage(address);
int index = Levels[^1].GetValue(address);
EnsureMapped((IntPtr)(page + index));
return ref page[index];
}
}
/// <summary>
/// Gets the leaf page for the specified guest <paramref name="address"/>.
/// </summary>
/// <param name="address">Guest address</param>
/// <returns>Leaf page for the specified guest <paramref name="address"/></returns>
private TEntry* GetPage(ulong address)
{
TEntry** page = GetRootPage();
for (int i = 0; i < Levels.Length - 1; i++)
{
ref AddressTableLevel level = ref Levels[i];
ref TEntry* nextPage = ref page[level.GetValue(address)];
if (nextPage == null || nextPage == _fillBottomLevelPtr)
{
ref AddressTableLevel nextLevel = ref Levels[i + 1];
if (i == Levels.Length - 2)
{
nextPage = (TEntry*)Allocate(1 << nextLevel.Length, Fill, leaf: true);
}
else
{
nextPage = (TEntry*)Allocate(1 << nextLevel.Length, GetFillValue(i), leaf: false);
}
}
page = (TEntry**)nextPage;
}
return (TEntry*)page;
}
/// <summary>
/// Ensure the given pointer is mapped in any overlapping sparse reservations.
/// </summary>
/// <param name="ptr">Pointer to be mapped</param>
private void EnsureMapped(IntPtr ptr)
{
if (Sparse)
{
// Check sparse allocations to see if the pointer is in any of them.
// Ensure the page is committed if there's a match.
_sparseLock.EnterReadLock();
try
{
foreach (TableSparseBlock reserved in _sparseReserved)
{
SparseMemoryBlock sparse = reserved.Block;
if (ptr >= sparse.Block.Pointer && ptr < sparse.Block.Pointer + (IntPtr)sparse.Block.Size)
{
sparse.EnsureMapped((ulong)(ptr - sparse.Block.Pointer));
break;
}
}
}
finally
{
_sparseLock.ExitReadLock();
}
}
}
/// <summary>
/// Get the fill value for a non-leaf level of the table.
/// </summary>
/// <param name="level">Level to get the fill value for</param>
/// <returns>The fill value</returns>
private IntPtr GetFillValue(int level)
{
if (_fillBottomLevel != null && level == Levels.Length - 2)
{
return (IntPtr)_fillBottomLevelPtr;
}
else
{
return IntPtr.Zero;
}
}
/// <summary>
/// Lazily initialize and get the root page of the <see cref="AddressTable{TEntry}"/>.
/// </summary>
/// <returns>Root page of the <see cref="AddressTable{TEntry}"/></returns>
private TEntry** GetRootPage()
{
if (_table == null)
{
if (Levels.Length == 1)
_table = (TEntry**)Allocate(1 << Levels[0].Length, Fill, leaf: true);
else
_table = (TEntry**)Allocate(1 << Levels[0].Length, GetFillValue(0), leaf: false);
}
return _table;
}
/// <summary>
/// Initialize a leaf page with the fill value.
/// </summary>
/// <param name="page">Page to initialize</param>
private void InitLeafPage(Span<byte> page)
{
MemoryMarshal.Cast<byte, TEntry>(page).Fill(_fill);
}
/// <summary>
/// Reserve a new sparse block, and add it to the list.
/// </summary>
/// <returns>The new sparse block that was added</returns>
private TableSparseBlock ReserveNewSparseBlock()
{
var block = new TableSparseBlock(_sparseBlockSize, EnsureMapped, InitLeafPage);
_sparseReserved.Add(block);
_sparseReservedOffset = 0;
return block;
}
/// <summary>
/// Allocates a block of memory of the specified type and length.
/// </summary>
/// <typeparam name="T">Type of elements</typeparam>
/// <param name="length">Number of elements</param>
/// <param name="fill">Fill value</param>
/// <param name="leaf"><see langword="true"/> if leaf; otherwise <see langword="false"/></param>
/// <returns>Allocated block</returns>
private IntPtr Allocate<T>(int length, T fill, bool leaf) where T : unmanaged
{
var size = sizeof(T) * length;
AddressTablePage page;
if (Sparse && leaf)
{
_sparseLock.EnterWriteLock();
SparseMemoryBlock block;
if (_sparseReserved.Count == 0)
{
block = ReserveNewSparseBlock().Block;
}
else
{
block = _sparseReserved.Last().Block;
if (_sparseReservedOffset == block.Block.Size)
{
block = ReserveNewSparseBlock().Block;
}
}
page = new AddressTablePage(true, block.Block.Pointer + (IntPtr)_sparseReservedOffset);
_sparseReservedOffset += (ulong)size;
_sparseLock.ExitWriteLock();
}
else
{
var address = (IntPtr)NativeAllocator.Instance.Allocate((uint)size);
page = new AddressTablePage(false, address);
var span = new Span<T>((void*)page.Address, length);
span.Fill(fill);
}
_pages.Add(page);
//TranslatorEventSource.Log.AddressTableAllocated(size, leaf);
return page.Address;
}
/// <summary>
/// Releases all resources used by the <see cref="AddressTable{TEntry}"/> instance.
/// </summary>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// Releases all unmanaged and optionally managed resources used by the <see cref="AddressTable{TEntry}"/>
/// instance.
/// </summary>
/// <param name="disposing"><see langword="true"/> to dispose managed resources also; otherwise just unmanaged resouces</param>
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
foreach (var page in _pages)
{
if (!page.IsSparse)
{
Marshal.FreeHGlobal(page.Address);
}
}
if (Sparse)
{
foreach (TableSparseBlock block in _sparseReserved)
{
block.Dispose();
}
_sparseReserved.Clear();
_fillBottomLevel.Dispose();
_sparseFill.Dispose();
_sparseLock.Dispose();
}
_disposed = true;
}
}
/// <summary>
/// Frees resources used by the <see cref="AddressTable{TEntry}"/> instance.
/// </summary>
~AddressTable()
{
Dispose(false);
}
}
}

View File

@ -32,7 +32,7 @@ namespace Ryujinx.Cpu.AppleHv
{
}
public IDiskCacheLoadState LoadDiskCache(string titleIdText, string displayVersion, bool enabled)
public IDiskCacheLoadState LoadDiskCache(string titleIdText, string displayVersion, bool enabled, string cacheSelector)
{
return new DummyDiskCacheLoadState();
}

View File

@ -48,7 +48,7 @@ namespace Ryujinx.Cpu
/// <param name="displayVersion">Version of the application</param>
/// <param name="enabled">True if the cache should be loaded from disk if it exists, false otherwise</param>
/// <returns>Disk cache load progress reporter and manager</returns>
IDiskCacheLoadState LoadDiskCache(string titleIdText, string displayVersion, bool enabled);
IDiskCacheLoadState LoadDiskCache(string titleIdText, string displayVersion, bool enabled, string cacheSelector);
/// <summary>
/// Indicates that code has been loaded into guest memory, and that it might be executed in the future.

View File

@ -1,3 +1,4 @@
using ARMeilleure.Common;
using ARMeilleure.Memory;
using ARMeilleure.Translation;
using Ryujinx.Cpu.Signal;
@ -9,11 +10,13 @@ namespace Ryujinx.Cpu.Jit
{
private readonly ITickSource _tickSource;
private readonly Translator _translator;
private readonly AddressTable<ulong> _functionTable;
public JitCpuContext(ITickSource tickSource, IMemoryManager memory, bool for64Bit)
{
_tickSource = tickSource;
_translator = new Translator(new JitMemoryAllocator(forJit: true), memory, for64Bit);
_functionTable = AddressTable<ulong>.CreateForArm(for64Bit, memory.Type);
_translator = new Translator(new JitMemoryAllocator(forJit: true), memory, _functionTable);
if (memory.Type.IsHostMappedOrTracked())
{
@ -47,14 +50,15 @@ namespace Ryujinx.Cpu.Jit
}
/// <inheritdoc/>
public IDiskCacheLoadState LoadDiskCache(string titleIdText, string displayVersion, bool enabled)
public IDiskCacheLoadState LoadDiskCache(string titleIdText, string displayVersion, bool enabled, string cacheSelector)
{
return new JitDiskCacheLoadState(_translator.LoadDiskCache(titleIdText, displayVersion, enabled));
return new JitDiskCacheLoadState(_translator.LoadDiskCache(titleIdText, displayVersion, enabled, cacheSelector));
}
/// <inheritdoc/>
public void PrepareCodeRange(ulong address, ulong size)
{
_functionTable.SignalCodeRange(address, size);
_translator.PrepareCodeRange(address, size);
}

View File

@ -140,6 +140,10 @@ namespace Ryujinx.Cpu.LightningJit.Arm32.Target.Arm64
bool isTail = false)
{
int tempRegister;
int tempGuestAddress = -1;
bool inlineLookup = guestAddress.Kind != OperandKind.Constant &&
funcTable is { Sparse: true };
if (guestAddress.Kind == OperandKind.Constant)
{
@ -153,9 +157,16 @@ namespace Ryujinx.Cpu.LightningJit.Arm32.Target.Arm64
else
{
asm.StrRiUn(guestAddress, Register(regAlloc.FixedContextRegister), NativeContextOffsets.DispatchAddressOffset);
if (inlineLookup && guestAddress.Value == 0)
{
// X0 will be overwritten. Move the address to a temp register.
tempGuestAddress = regAlloc.AllocateTempGprRegister();
asm.Mov(Register(tempGuestAddress), guestAddress);
}
}
tempRegister = regAlloc.FixedContextRegister == 1 ? 2 : 1;
tempRegister = NextFreeRegister(1, tempGuestAddress);
if (!isTail)
{
@ -176,6 +187,40 @@ namespace Ryujinx.Cpu.LightningJit.Arm32.Target.Arm64
asm.Mov(rn, funcPtrLoc & ~0xfffUL);
asm.LdrRiUn(rn, rn, (int)(funcPtrLoc & 0xfffUL));
}
else if (inlineLookup)
{
// Inline table lookup. Only enabled when the sparse function table is enabled with 2 levels.
Operand indexReg = Register(NextFreeRegister(tempRegister + 1, tempGuestAddress));
if (tempGuestAddress != -1)
{
guestAddress = Register(tempGuestAddress);
}
ulong tableBase = (ulong)funcTable.Base;
// Index into the table.
asm.Mov(rn, tableBase);
for (int i = 0; i < funcTable.Levels.Length; i++)
{
var level = funcTable.Levels[i];
asm.Ubfx(indexReg, guestAddress, level.Index, level.Length);
asm.Lsl(indexReg, indexReg, Const(3));
// Index into the page.
asm.Add(rn, rn, indexReg);
// Load the page address.
asm.LdrRiUn(rn, rn, 0);
}
if (tempGuestAddress != -1)
{
regAlloc.FreeTempGprRegister(tempGuestAddress);
}
}
else
{
asm.Mov(rn, (ulong)funcPtr);
@ -252,5 +297,20 @@ namespace Ryujinx.Cpu.LightningJit.Arm32.Target.Arm64
{
return new Operand(register, RegisterType.Integer, type);
}
private static Operand Const(long value, OperandType type = OperandType.I64)
{
return new Operand(type, (ulong)value);
}
private static int NextFreeRegister(int start, int avoid)
{
if (start == avoid)
{
start++;
}
return start;
}
}
}

View File

@ -305,6 +305,10 @@ namespace Ryujinx.Cpu.LightningJit.Arm64.Target.Arm64
bool isTail = false)
{
int tempRegister;
int tempGuestAddress = -1;
bool inlineLookup = guestAddress.Kind != OperandKind.Constant &&
funcTable is { Sparse: true };
if (guestAddress.Kind == OperandKind.Constant)
{
@ -318,9 +322,16 @@ namespace Ryujinx.Cpu.LightningJit.Arm64.Target.Arm64
else
{
asm.StrRiUn(guestAddress, Register(regAlloc.FixedContextRegister), NativeContextOffsets.DispatchAddressOffset);
if (inlineLookup && guestAddress.Value == 0)
{
// X0 will be overwritten. Move the address to a temp register.
tempGuestAddress = regAlloc.AllocateTempGprRegister();
asm.Mov(Register(tempGuestAddress), guestAddress);
}
}
tempRegister = regAlloc.FixedContextRegister == 1 ? 2 : 1;
tempRegister = NextFreeRegister(1, tempGuestAddress);
if (!isTail)
{
@ -341,6 +352,40 @@ namespace Ryujinx.Cpu.LightningJit.Arm64.Target.Arm64
asm.Mov(rn, funcPtrLoc & ~0xfffUL);
asm.LdrRiUn(rn, rn, (int)(funcPtrLoc & 0xfffUL));
}
else if (inlineLookup)
{
// Inline table lookup. Only enabled when the sparse function table is enabled with 2 levels.
Operand indexReg = Register(NextFreeRegister(tempRegister + 1, tempGuestAddress));
if (tempGuestAddress != -1)
{
guestAddress = Register(tempGuestAddress);
}
ulong tableBase = (ulong)funcTable.Base;
// Index into the table.
asm.Mov(rn, tableBase);
for (int i = 0; i < funcTable.Levels.Length; i++)
{
var level = funcTable.Levels[i];
asm.Ubfx(indexReg, guestAddress, level.Index, level.Length);
asm.Lsl(indexReg, indexReg, Const(3));
// Index into the page.
asm.Add(rn, rn, indexReg);
// Load the page address.
asm.LdrRiUn(rn, rn, 0);
}
if (tempGuestAddress != -1)
{
regAlloc.FreeTempGprRegister(tempGuestAddress);
}
}
else
{
asm.Mov(rn, (ulong)funcPtr);
@ -613,5 +658,20 @@ namespace Ryujinx.Cpu.LightningJit.Arm64.Target.Arm64
{
return new Operand(register, RegisterType.Integer, type);
}
private static Operand Const(long value, OperandType type = OperandType.I64)
{
return new Operand(type, (ulong)value);
}
private static int NextFreeRegister(int start, int avoid)
{
if (start == avoid)
{
start++;
}
return start;
}
}
}

View File

@ -1,3 +1,4 @@
using ARMeilleure.Common;
using ARMeilleure.Memory;
using Ryujinx.Cpu.Jit;
using Ryujinx.Cpu.LightningJit.State;
@ -8,11 +9,16 @@ namespace Ryujinx.Cpu.LightningJit
{
private readonly ITickSource _tickSource;
private readonly Translator _translator;
private readonly AddressTable<ulong> _functionTable;
public LightningJitCpuContext(ITickSource tickSource, IMemoryManager memory, bool for64Bit)
{
_tickSource = tickSource;
_translator = new Translator(memory, for64Bit);
_functionTable = AddressTable<ulong>.CreateForArm(for64Bit, memory.Type);
_translator = new Translator(memory, _functionTable);
memory.UnmapEvent += UnmapHandler;
}
@ -40,7 +46,7 @@ namespace Ryujinx.Cpu.LightningJit
}
/// <inheritdoc/>
public IDiskCacheLoadState LoadDiskCache(string titleIdText, string displayVersion, bool enabled)
public IDiskCacheLoadState LoadDiskCache(string titleIdText, string displayVersion, bool enabled, string cacheSelector)
{
return new DummyDiskCacheLoadState();
}
@ -48,6 +54,7 @@ namespace Ryujinx.Cpu.LightningJit
/// <inheritdoc/>
public void PrepareCodeRange(ulong address, ulong size)
{
_functionTable.SignalCodeRange(address, size);
}
public void Dispose()

View File

@ -19,25 +19,6 @@ namespace Ryujinx.Cpu.LightningJit
// Should be enabled on platforms that enforce W^X.
private static bool IsNoWxPlatform => false;
private static readonly AddressTable<ulong>.Level[] _levels64Bit =
new AddressTable<ulong>.Level[]
{
new(31, 17),
new(23, 8),
new(15, 8),
new( 7, 8),
new( 2, 5),
};
private static readonly AddressTable<ulong>.Level[] _levels32Bit =
new AddressTable<ulong>.Level[]
{
new(23, 9),
new(15, 8),
new( 7, 8),
new( 1, 6),
};
private readonly ConcurrentQueue<KeyValuePair<ulong, TranslatedFunction>> _oldFuncs;
private readonly NoWxCache _noWxCache;
private bool _disposed;
@ -47,7 +28,7 @@ namespace Ryujinx.Cpu.LightningJit
internal TranslatorStubs Stubs { get; }
internal IMemoryManager Memory { get; }
public Translator(IMemoryManager memory, bool for64Bits)
public Translator(IMemoryManager memory, AddressTable<ulong> functionTable)
{
Memory = memory;
@ -63,7 +44,7 @@ namespace Ryujinx.Cpu.LightningJit
}
Functions = new TranslatorCache<TranslatedFunction>();
FunctionTable = new AddressTable<ulong>(for64Bits ? _levels64Bit : _levels32Bit);
FunctionTable = functionTable;
Stubs = new TranslatorStubs(FunctionTable, _noWxCache);
FunctionTable.Fill = (ulong)Stubs.SlowDispatchStub;

View File

@ -23,7 +23,7 @@ namespace Ryujinx.Cpu.LightningJit
private bool _disposed;
private readonly AddressTable<ulong> _functionTable;
private readonly IAddressTable<ulong> _functionTable;
private readonly NoWxCache _noWxCache;
private readonly GetFunctionAddressDelegate _getFunctionAddressRef;
private readonly nint _getFunctionAddress;
@ -79,7 +79,7 @@ namespace Ryujinx.Cpu.LightningJit
/// <param name="functionTable">Function table used to store pointers to the functions that the guest code will call</param>
/// <param name="noWxCache">Cache used on platforms that enforce W^X, otherwise should be null</param>
/// <exception cref="ArgumentNullException"><paramref name="translator"/> is null</exception>
public TranslatorStubs(AddressTable<ulong> functionTable, NoWxCache noWxCache)
public TranslatorStubs(IAddressTable<ulong> functionTable, NoWxCache noWxCache)
{
ArgumentNullException.ThrowIfNull(functionTable);

View File

@ -8,7 +8,7 @@ namespace Ryujinx.Graphics.GAL
void SetSize(int width, int height);
void ChangeVSyncMode(bool vsyncEnabled);
void ChangeVSyncMode(VSyncMode vSyncMode);
void SetAntiAliasing(AntiAliasing antialiasing);
void SetScalingFilter(ScalingFilter type);

View File

@ -31,7 +31,7 @@ namespace Ryujinx.Graphics.GAL.Multithreading
_impl.Window.SetSize(width, height);
}
public void ChangeVSyncMode(bool vsyncEnabled) { }
public void ChangeVSyncMode(VSyncMode vSyncMode) { }
public void SetAntiAliasing(AntiAliasing effect) { }

View File

@ -0,0 +1,9 @@
namespace Ryujinx.Graphics.GAL
{
public enum VSyncMode
{
Switch,
Unbounded,
Custom
}
}

View File

@ -54,7 +54,7 @@ namespace Ryujinx.Graphics.OpenGL
GL.PixelStore(PixelStoreParameter.UnpackAlignment, 4);
}
public void ChangeVSyncMode(bool vsyncEnabled) { }
public void ChangeVSyncMode(VSyncMode vSyncMode) { }
public void SetSize(int width, int height)
{

View File

@ -29,7 +29,7 @@ namespace Ryujinx.Graphics.Vulkan
private int _width;
private int _height;
private bool _vsyncEnabled;
private VSyncMode _vSyncMode;
private bool _swapchainIsDirty;
private VkFormat _format;
private AntiAliasing _currentAntiAliasing;
@ -139,7 +139,7 @@ namespace Ryujinx.Graphics.Vulkan
ImageArrayLayers = 1,
PreTransform = capabilities.CurrentTransform,
CompositeAlpha = ChooseCompositeAlpha(capabilities.SupportedCompositeAlpha),
PresentMode = ChooseSwapPresentMode(presentModes, _vsyncEnabled),
PresentMode = ChooseSwapPresentMode(presentModes, _vSyncMode),
Clipped = true,
};
@ -279,9 +279,9 @@ namespace Ryujinx.Graphics.Vulkan
}
}
private static PresentModeKHR ChooseSwapPresentMode(PresentModeKHR[] availablePresentModes, bool vsyncEnabled)
private static PresentModeKHR ChooseSwapPresentMode(PresentModeKHR[] availablePresentModes, VSyncMode vSyncMode)
{
if (!vsyncEnabled && availablePresentModes.Contains(PresentModeKHR.ImmediateKhr))
if (vSyncMode == VSyncMode.Unbounded && availablePresentModes.Contains(PresentModeKHR.ImmediateKhr))
{
return PresentModeKHR.ImmediateKhr;
}
@ -634,9 +634,10 @@ namespace Ryujinx.Graphics.Vulkan
_swapchainIsDirty = true;
}
public override void ChangeVSyncMode(bool vsyncEnabled)
public override void ChangeVSyncMode(VSyncMode vSyncMode)
{
_vsyncEnabled = vsyncEnabled;
_vSyncMode = vSyncMode;
//present mode may change, so mark the swapchain for recreation
_swapchainIsDirty = true;
}

View File

@ -10,7 +10,7 @@ namespace Ryujinx.Graphics.Vulkan
public abstract void Dispose();
public abstract void Present(ITexture texture, ImageCrop crop, Action swapBuffersCallback);
public abstract void SetSize(int width, int height);
public abstract void ChangeVSyncMode(bool vsyncEnabled);
public abstract void ChangeVSyncMode(VSyncMode vSyncMode);
public abstract void SetAntiAliasing(AntiAliasing effect);
public abstract void SetScalingFilter(ScalingFilter scalerType);
public abstract void SetScalingFilterLevel(float scale);

View File

@ -9,6 +9,7 @@ using Ryujinx.HLE.HOS.Services.Account.Acc;
using Ryujinx.HLE.HOS.SystemState;
using Ryujinx.HLE.UI;
using System;
using VSyncMode = Ryujinx.Common.Configuration.VSyncMode;
namespace Ryujinx.HLE
{
@ -84,9 +85,14 @@ namespace Ryujinx.HLE
internal readonly RegionCode Region;
/// <summary>
/// Control the initial state of the vertical sync in the SurfaceFlinger service.
/// Control the initial state of the present interval in the SurfaceFlinger service (previously Vsync).
/// </summary>
internal readonly bool EnableVsync;
internal readonly VSyncMode VSyncMode;
/// <summary>
/// Control the custom VSync interval, if enabled and active.
/// </summary>
internal readonly int CustomVSyncInterval;
/// <summary>
/// Control the initial state of the docked mode.
@ -195,7 +201,7 @@ namespace Ryujinx.HLE
IHostUIHandler hostUIHandler,
SystemLanguage systemLanguage,
RegionCode region,
bool enableVsync,
VSyncMode vSyncMode,
bool enableDockedMode,
bool enablePtc,
bool enableInternetAccess,
@ -212,7 +218,8 @@ namespace Ryujinx.HLE
MultiplayerMode multiplayerMode,
bool multiplayerDisableP2p,
string multiplayerLdnPassphrase,
string multiplayerLdnServer)
string multiplayerLdnServer,
int customVSyncInterval)
{
VirtualFileSystem = virtualFileSystem;
LibHacHorizonManager = libHacHorizonManager;
@ -225,7 +232,8 @@ namespace Ryujinx.HLE
HostUIHandler = hostUIHandler;
SystemLanguage = systemLanguage;
Region = region;
EnableVsync = enableVsync;
VSyncMode = vSyncMode;
CustomVSyncInterval = customVSyncInterval;
EnableDockedMode = enableDockedMode;
EnablePtc = enablePtc;
EnableInternetAccess = enableInternetAccess;

View File

@ -13,7 +13,8 @@ namespace Ryujinx.HLE.HOS
string displayVersion,
bool diskCacheEnabled,
ulong codeAddress,
ulong codeSize);
ulong codeSize,
string cacheSelector);
}
class ArmProcessContext<T> : IArmProcessContext where T : class, IVirtualMemoryManagerTracked, IMemoryManager
@ -67,10 +68,11 @@ namespace Ryujinx.HLE.HOS
string displayVersion,
bool diskCacheEnabled,
ulong codeAddress,
ulong codeSize)
ulong codeSize,
string cacheSelector)
{
_cpuContext.PrepareCodeRange(codeAddress, codeSize);
return _cpuContext.LoadDiskCache(titleIdText, displayVersion, diskCacheEnabled);
return _cpuContext.LoadDiskCache(titleIdText, displayVersion, diskCacheEnabled, cacheSelector);
}
public void InvalidateCacheRegion(ulong address, ulong size)

View File

@ -114,7 +114,7 @@ namespace Ryujinx.HLE.HOS
}
}
DiskCacheLoadState = processContext.Initialize(_titleIdText, _displayVersion, _diskCacheEnabled, _codeAddress, _codeSize);
DiskCacheLoadState = processContext.Initialize(_titleIdText, _displayVersion, _diskCacheEnabled, _codeAddress, _codeSize, "default"); //Ready for exefs profiles
return processContext;
}

View File

@ -10,13 +10,12 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using VSyncMode = Ryujinx.Common.Configuration.VSyncMode;
namespace Ryujinx.HLE.HOS.Services.SurfaceFlinger
{
class SurfaceFlinger : IConsumerListener, IDisposable
{
private const int TargetFps = 60;
private readonly Switch _device;
private readonly Dictionary<long, Layer> _layers;
@ -32,6 +31,9 @@ namespace Ryujinx.HLE.HOS.Services.SurfaceFlinger
private readonly long _spinTicks;
private readonly long _1msTicks;
private VSyncMode _vSyncMode;
private long _targetVSyncInterval;
private int _swapInterval;
private int _swapIntervalDelay;
@ -88,7 +90,8 @@ namespace Ryujinx.HLE.HOS.Services.SurfaceFlinger
}
else
{
_ticksPerFrame = Stopwatch.Frequency / TargetFps;
_ticksPerFrame = Stopwatch.Frequency / _device.TargetVSyncInterval;
_targetVSyncInterval = _device.TargetVSyncInterval;
}
}
@ -370,15 +373,20 @@ namespace Ryujinx.HLE.HOS.Services.SurfaceFlinger
if (acquireStatus == Status.Success)
{
// If device vsync is disabled, reflect the change.
if (!_device.EnableDeviceVsync)
if (_device.VSyncMode == VSyncMode.Unbounded)
{
if (_swapInterval != 0)
{
UpdateSwapInterval(0);
_vSyncMode = _device.VSyncMode;
}
}
else if (item.SwapInterval != _swapInterval)
else if (_device.VSyncMode != _vSyncMode)
{
UpdateSwapInterval(_device.VSyncMode == VSyncMode.Unbounded ? 0 : item.SwapInterval);
_vSyncMode = _device.VSyncMode;
}
else if (item.SwapInterval != _swapInterval || _device.TargetVSyncInterval != _targetVSyncInterval)
{
UpdateSwapInterval(item.SwapInterval);
}

View File

@ -27,7 +27,11 @@ namespace Ryujinx.HLE
public TamperMachine TamperMachine { get; }
public IHostUIHandler UIHandler { get; }
public bool EnableDeviceVsync { get; set; }
public VSyncMode VSyncMode { get; set; } = VSyncMode.Switch;
public bool CustomVSyncIntervalEnabled { get; set; } = false;
public int CustomVSyncInterval { get; set; }
public long TargetVSyncInterval { get; set; } = 60;
public bool IsFrameAvailable => Gpu.Window.IsFrameAvailable;
@ -59,12 +63,14 @@ namespace Ryujinx.HLE
System.State.SetLanguage(Configuration.SystemLanguage);
System.State.SetRegion(Configuration.Region);
EnableDeviceVsync = Configuration.EnableVsync;
VSyncMode = Configuration.VSyncMode;
CustomVSyncInterval = Configuration.CustomVSyncInterval;
System.State.DockedMode = Configuration.EnableDockedMode;
System.PerformanceState.PerformanceMode = System.State.DockedMode ? PerformanceMode.Boost : PerformanceMode.Default;
System.EnablePtc = Configuration.EnablePtc;
System.FsIntegrityCheckLevel = Configuration.FsIntegrityCheckLevel;
System.GlobalAccessLogMode = Configuration.FsGlobalAccessLogMode;
UpdateVSyncInterval();
#pragma warning restore IDE0055
}
@ -75,6 +81,34 @@ namespace Ryujinx.HLE
Gpu.GPFifo.DispatchCalls();
}
public void IncrementCustomVSyncInterval()
{
CustomVSyncInterval += 1;
UpdateVSyncInterval();
}
public void DecrementCustomVSyncInterval()
{
CustomVSyncInterval -= 1;
UpdateVSyncInterval();
}
public void UpdateVSyncInterval()
{
switch (VSyncMode)
{
case VSyncMode.Custom:
TargetVSyncInterval = CustomVSyncInterval;
break;
case VSyncMode.Switch:
TargetVSyncInterval = 60;
break;
case VSyncMode.Unbounded:
TargetVSyncInterval = 1;
break;
}
}
public bool LoadCart(string exeFsDir, string romFsFile = null) => Processes.LoadUnpackedNca(exeFsDir, romFsFile);
public bool LoadXci(string xciFile, ulong applicationId = 0) => Processes.LoadXci(xciFile, applicationId);
public bool LoadNca(string ncaFile) => Processes.LoadNca(ncaFile);

View File

@ -115,8 +115,11 @@ namespace Ryujinx.Headless.SDL2
[Option("fs-global-access-log-mode", Required = false, Default = 0, HelpText = "Enables FS access log output to the console.")]
public int FsGlobalAccessLogMode { get; set; }
[Option("disable-vsync", Required = false, HelpText = "Disables Vertical Sync.")]
public bool DisableVSync { get; set; }
[Option("vsync-mode", Required = false, Default = VSyncMode.Switch, HelpText = "Sets the emulated VSync mode (Switch, Unbounded, or Custom).")]
public VSyncMode VSyncMode { get; set; }
[Option("custom-refresh-rate", Required = false, Default = 90, HelpText = "Sets the custom refresh rate target value (integer).")]
public int CustomVSyncInterval { get; set; }
[Option("disable-shader-cache", Required = false, HelpText = "Disables Shader cache.")]
public bool DisableShaderCache { get; set; }

View File

@ -563,7 +563,7 @@ namespace Ryujinx.Headless.SDL2
window,
options.SystemLanguage,
options.SystemRegion,
!options.DisableVSync,
options.VSyncMode,
!options.DisableDockedMode,
!options.DisablePTC,
options.EnableInternetAccess,
@ -580,7 +580,8 @@ namespace Ryujinx.Headless.SDL2
Common.Configuration.Multiplayer.MultiplayerMode.Disabled,
false,
"",
"");
"",
options.CustomVSyncInterval);
return new Switch(configuration);
}

View File

@ -3,7 +3,7 @@ using System;
namespace Ryujinx.Headless.SDL2
{
class StatusUpdatedEventArgs(
bool vSyncEnabled,
string vSyncMode,
string dockedMode,
string aspectRatio,
string gameStatus,
@ -11,7 +11,7 @@ namespace Ryujinx.Headless.SDL2
string gpuName)
: EventArgs
{
public bool VSyncEnabled = vSyncEnabled;
public string VSyncMode = vSyncMode;
public string DockedMode = dockedMode;
public string AspectRatio = aspectRatio;
public string GameStatus = gameStatus;

View File

@ -314,7 +314,7 @@ namespace Ryujinx.Headless.SDL2
}
StatusUpdatedEvent?.Invoke(this, new StatusUpdatedEventArgs(
Device.EnableDeviceVsync,
Device.VSyncMode.ToString(),
dockedMode,
Device.Configuration.AspectRatio.ToText(),
$"Game: {Device.Statistics.GetGameFrameRate():00.00} FPS ({Device.Statistics.GetGameFrameTime():00.00} ms)",

View File

@ -0,0 +1,125 @@
using Ryujinx.Common;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
namespace Ryujinx.Memory
{
public delegate void PageInitDelegate(Span<byte> page);
public class SparseMemoryBlock : IDisposable
{
private const ulong MapGranularity = 1UL << 17;
private readonly PageInitDelegate _pageInit;
private readonly object _lock = new object();
private readonly ulong _pageSize;
private readonly MemoryBlock _reservedBlock;
private readonly List<MemoryBlock> _mappedBlocks;
private ulong _mappedBlockUsage;
private readonly ulong[] _mappedPageBitmap;
public MemoryBlock Block => _reservedBlock;
public SparseMemoryBlock(ulong size, PageInitDelegate pageInit, MemoryBlock fill)
{
_pageSize = MemoryBlock.GetPageSize();
_reservedBlock = new MemoryBlock(size, MemoryAllocationFlags.Reserve | MemoryAllocationFlags.ViewCompatible);
_mappedBlocks = new List<MemoryBlock>();
_pageInit = pageInit;
int pages = (int)BitUtils.DivRoundUp(size, _pageSize);
int bitmapEntries = BitUtils.DivRoundUp(pages, 64);
_mappedPageBitmap = new ulong[bitmapEntries];
if (fill != null)
{
// Fill the block with mappings from the fill block.
if (fill.Size % _pageSize != 0)
{
throw new ArgumentException("Fill memory block should be page aligned.", nameof(fill));
}
int repeats = (int)BitUtils.DivRoundUp(size, fill.Size);
ulong offset = 0;
for (int i = 0; i < repeats; i++)
{
_reservedBlock.MapView(fill, 0, offset, Math.Min(fill.Size, size - offset));
offset += fill.Size;
}
}
// If a fill block isn't provided, the pages that aren't EnsureMapped are unmapped.
// The caller can rely on signal handler to fill empty pages instead.
}
private void MapPage(ulong pageOffset)
{
// Take a page from the latest mapped block.
MemoryBlock block = _mappedBlocks.LastOrDefault();
if (block == null || _mappedBlockUsage == MapGranularity)
{
// Need to map some more memory.
block = new MemoryBlock(MapGranularity, MemoryAllocationFlags.Mirrorable);
_mappedBlocks.Add(block);
_mappedBlockUsage = 0;
}
_pageInit(block.GetSpan(_mappedBlockUsage, (int)_pageSize));
_reservedBlock.MapView(block, _mappedBlockUsage, pageOffset, _pageSize);
_mappedBlockUsage += _pageSize;
}
public void EnsureMapped(ulong offset)
{
int pageIndex = (int)(offset / _pageSize);
int bitmapIndex = pageIndex >> 6;
ref ulong entry = ref _mappedPageBitmap[bitmapIndex];
ulong bit = 1UL << (pageIndex & 63);
if ((Volatile.Read(ref entry) & bit) == 0)
{
// Not mapped.
lock (_lock)
{
// Check the bit while locked to make sure that this only happens once.
ulong lockedEntry = Volatile.Read(ref entry);
if ((lockedEntry & bit) == 0)
{
MapPage(offset & ~(_pageSize - 1));
lockedEntry |= bit;
Interlocked.Exchange(ref entry, lockedEntry);
}
}
}
}
public void Dispose()
{
_reservedBlock.Dispose();
foreach (MemoryBlock block in _mappedBlocks)
{
block.Dispose();
}
GC.SuppressFinalize(this);
}
}
}

View File

@ -1,3 +1,4 @@
using ARMeilleure.Common;
using ARMeilleure.Memory;
using ARMeilleure.State;
using ARMeilleure.Translation;
@ -12,7 +13,7 @@ namespace Ryujinx.Tests.Cpu
public CpuContext(IMemoryManager memory, bool for64Bit)
{
_translator = new Translator(new JitMemoryAllocator(), memory, for64Bit);
_translator = new Translator(new JitMemoryAllocator(), memory, AddressTable<ulong>.CreateForArm(for64Bit, memory.Type));
memory.UnmapEvent += UnmapHandler;
}

View File

@ -1,3 +1,5 @@
using ARMeilleure.Common;
using ARMeilleure.Memory;
using ARMeilleure.Translation;
using NUnit.Framework;
using Ryujinx.Cpu.Jit;
@ -17,7 +19,10 @@ namespace Ryujinx.Tests.Cpu
private static void EnsureTranslator()
{
// Create a translator, as one is needed to register the signal handler or emit methods.
_translator ??= new Translator(new JitMemoryAllocator(), new MockMemoryManager(), true);
_translator ??= new Translator(
new JitMemoryAllocator(),
new MockMemoryManager(),
AddressTable<ulong>.CreateForArm(true, MemoryManagerType.SoftwarePageTable));
}
[MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)]

View File

@ -1,3 +1,5 @@
using ARMeilleure.Common;
using ARMeilleure.Memory;
using ARMeilleure.Signal;
using ARMeilleure.Translation;
using NUnit.Framework;
@ -53,7 +55,10 @@ namespace Ryujinx.Tests.Memory
private static void EnsureTranslator()
{
// Create a translator, as one is needed to register the signal handler or emit methods.
_translator ??= new Translator(new JitMemoryAllocator(), new MockMemoryManager(), true);
_translator ??= new Translator(
new JitMemoryAllocator(),
new MockMemoryManager(),
AddressTable<ulong>.CreateForArm(true, MemoryManagerType.SoftwarePageTable));
}
[Test]

View File

@ -1,3 +1,4 @@
using Ryujinx.Common;
using Ryujinx.Common.Configuration;
using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Common.Configuration.Multiplayer;
@ -16,7 +17,7 @@ namespace Ryujinx.UI.Common.Configuration
/// <summary>
/// The current version of the file format
/// </summary>
public const int CurrentVersion = 56;
public const int CurrentVersion = 57;
/// <summary>
/// Version of the configuration file format
@ -191,8 +192,25 @@ namespace Ryujinx.UI.Common.Configuration
/// <summary>
/// Enables or disables Vertical Sync
/// </summary>
/// <remarks>Kept for file format compatibility (to avoid possible failure when parsing configuration on old versions)</remarks>
/// TODO: Remove this when those older versions aren't in use anymore.
public bool EnableVsync { get; set; }
/// <summary>
/// Current VSync mode; 60 (Switch), unbounded ("Vsync off"), or custom
/// </summary>
public VSyncMode VSyncMode { get; set; }
/// <summary>
/// Enables or disables the custom present interval
/// </summary>
public bool EnableCustomVSyncInterval { get; set; }
/// <summary>
/// The custom present interval value
/// </summary>
public int CustomVSyncInterval { get; set; }
/// <summary>
/// Enables or disables Shader cache
/// </summary>

View File

@ -82,7 +82,7 @@ namespace Ryujinx.UI.Common.Configuration
configurationFileFormat.Hotkeys = new KeyboardHotkeys
{
ToggleVsync = Key.F1,
ToggleVSyncMode = Key.F1,
};
configurationFileUpdated = true;
@ -276,7 +276,7 @@ namespace Ryujinx.UI.Common.Configuration
configurationFileFormat.Hotkeys = new KeyboardHotkeys
{
ToggleVsync = Key.F1,
ToggleVSyncMode = Key.F1,
Screenshot = Key.F8,
};
@ -289,7 +289,7 @@ namespace Ryujinx.UI.Common.Configuration
configurationFileFormat.Hotkeys = new KeyboardHotkeys
{
ToggleVsync = Key.F1,
ToggleVSyncMode = Key.F1,
Screenshot = Key.F8,
ShowUI = Key.F4,
};
@ -332,7 +332,7 @@ namespace Ryujinx.UI.Common.Configuration
configurationFileFormat.Hotkeys = new KeyboardHotkeys
{
ToggleVsync = configurationFileFormat.Hotkeys.ToggleVsync,
ToggleVSyncMode = configurationFileFormat.Hotkeys.ToggleVSyncMode,
Screenshot = configurationFileFormat.Hotkeys.Screenshot,
ShowUI = configurationFileFormat.Hotkeys.ShowUI,
Pause = Key.F5,
@ -347,7 +347,7 @@ namespace Ryujinx.UI.Common.Configuration
configurationFileFormat.Hotkeys = new KeyboardHotkeys
{
ToggleVsync = configurationFileFormat.Hotkeys.ToggleVsync,
ToggleVSyncMode = configurationFileFormat.Hotkeys.ToggleVSyncMode,
Screenshot = configurationFileFormat.Hotkeys.Screenshot,
ShowUI = configurationFileFormat.Hotkeys.ShowUI,
Pause = configurationFileFormat.Hotkeys.Pause,
@ -421,7 +421,7 @@ namespace Ryujinx.UI.Common.Configuration
configurationFileFormat.Hotkeys = new KeyboardHotkeys
{
ToggleVsync = configurationFileFormat.Hotkeys.ToggleVsync,
ToggleVSyncMode = configurationFileFormat.Hotkeys.ToggleVSyncMode,
Screenshot = configurationFileFormat.Hotkeys.Screenshot,
ShowUI = configurationFileFormat.Hotkeys.ShowUI,
Pause = configurationFileFormat.Hotkeys.Pause,
@ -448,7 +448,7 @@ namespace Ryujinx.UI.Common.Configuration
configurationFileFormat.Hotkeys = new KeyboardHotkeys
{
ToggleVsync = configurationFileFormat.Hotkeys.ToggleVsync,
ToggleVSyncMode = configurationFileFormat.Hotkeys.ToggleVSyncMode,
Screenshot = configurationFileFormat.Hotkeys.Screenshot,
ShowUI = configurationFileFormat.Hotkeys.ShowUI,
Pause = configurationFileFormat.Hotkeys.Pause,
@ -611,6 +611,33 @@ namespace Ryujinx.UI.Common.Configuration
configurationFileUpdated = true;
}
if (configurationFileFormat.Version < 57)
{
Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 57.");
configurationFileFormat.VSyncMode = VSyncMode.Switch;
configurationFileFormat.EnableCustomVSyncInterval = false;
configurationFileFormat.Hotkeys = new KeyboardHotkeys
{
ToggleVSyncMode = Key.F1,
Screenshot = configurationFileFormat.Hotkeys.Screenshot,
ShowUI = configurationFileFormat.Hotkeys.ShowUI,
Pause = configurationFileFormat.Hotkeys.Pause,
ToggleMute = configurationFileFormat.Hotkeys.ToggleMute,
ResScaleUp = configurationFileFormat.Hotkeys.ResScaleUp,
ResScaleDown = configurationFileFormat.Hotkeys.ResScaleDown,
VolumeUp = configurationFileFormat.Hotkeys.VolumeUp,
VolumeDown = configurationFileFormat.Hotkeys.VolumeDown,
CustomVSyncIntervalIncrement = Key.Unbound,
CustomVSyncIntervalDecrement = Key.Unbound,
};
configurationFileFormat.CustomVSyncInterval = 120;
configurationFileUpdated = true;
}
Logger.EnableFileLog.Value = configurationFileFormat.EnableFileLog;
Graphics.ResScale.Value = configurationFileFormat.ResScale;
Graphics.ResScaleCustom.Value = configurationFileFormat.ResScaleCustom;
@ -646,7 +673,9 @@ namespace Ryujinx.UI.Common.Configuration
ShowTitleBar.Value = configurationFileFormat.ShowTitleBar;
EnableHardwareAcceleration.Value = configurationFileFormat.EnableHardwareAcceleration;
HideCursor.Value = configurationFileFormat.HideCursor;
Graphics.EnableVsync.Value = configurationFileFormat.EnableVsync;
Graphics.VSyncMode.Value = configurationFileFormat.VSyncMode;
Graphics.EnableCustomVSyncInterval.Value = configurationFileFormat.EnableCustomVSyncInterval;
Graphics.CustomVSyncInterval.Value = configurationFileFormat.CustomVSyncInterval;
Graphics.EnableShaderCache.Value = configurationFileFormat.EnableShaderCache;
Graphics.EnableTextureRecompression.Value = configurationFileFormat.EnableTextureRecompression;
Graphics.EnableMacroHLE.Value = configurationFileFormat.EnableMacroHLE;

View File

@ -1,4 +1,4 @@
using ARMeilleure;
using ARMeilleure;
using Ryujinx.Common;
using Ryujinx.Common.Configuration;
using Ryujinx.Common.Configuration.Hid;
@ -474,9 +474,19 @@ namespace Ryujinx.UI.Common.Configuration
public ReactiveObject<string> ShadersDumpPath { get; private set; }
/// <summary>
/// Enables or disables Vertical Sync
/// Toggles the present interval mode. Options are Switch (60Hz), Unbounded (previously Vsync off), and Custom, if enabled.
/// </summary>
public ReactiveObject<bool> EnableVsync { get; private set; }
public ReactiveObject<VSyncMode> VSyncMode { get; private set; }
/// <summary>
/// Enables or disables the custom present interval mode.
/// </summary>
public ReactiveObject<bool> EnableCustomVSyncInterval { get; private set; }
/// <summary>
/// Changes the custom present interval.
/// </summary>
public ReactiveObject<int> CustomVSyncInterval { get; private set; }
/// <summary>
/// Enables or disables Shader cache
@ -536,8 +546,12 @@ namespace Ryujinx.UI.Common.Configuration
AspectRatio = new ReactiveObject<AspectRatio>();
AspectRatio.LogChangesToValue(nameof(AspectRatio));
ShadersDumpPath = new ReactiveObject<string>();
EnableVsync = new ReactiveObject<bool>();
EnableVsync.LogChangesToValue(nameof(EnableVsync));
VSyncMode = new ReactiveObject<VSyncMode>();
VSyncMode.LogChangesToValue(nameof(VSyncMode));
EnableCustomVSyncInterval = new ReactiveObject<bool>();
EnableCustomVSyncInterval.LogChangesToValue(nameof(EnableCustomVSyncInterval));
CustomVSyncInterval = new ReactiveObject<int>();
CustomVSyncInterval.LogChangesToValue(nameof(CustomVSyncInterval));
EnableShaderCache = new ReactiveObject<bool>();
EnableShaderCache.LogChangesToValue(nameof(EnableShaderCache));
EnableTextureRecompression = new ReactiveObject<bool>();

View File

@ -64,7 +64,9 @@ namespace Ryujinx.UI.Common.Configuration
ShowTitleBar = ShowTitleBar,
EnableHardwareAcceleration = EnableHardwareAcceleration,
HideCursor = HideCursor,
EnableVsync = Graphics.EnableVsync,
VSyncMode = Graphics.VSyncMode,
EnableCustomVSyncInterval = Graphics.EnableCustomVSyncInterval,
CustomVSyncInterval = Graphics.CustomVSyncInterval,
EnableShaderCache = Graphics.EnableShaderCache,
EnableTextureRecompression = Graphics.EnableTextureRecompression,
EnableMacroHLE = Graphics.EnableMacroHLE,
@ -179,7 +181,9 @@ namespace Ryujinx.UI.Common.Configuration
ShowTitleBar.Value = !OperatingSystem.IsWindows();
EnableHardwareAcceleration.Value = true;
HideCursor.Value = HideCursorMode.OnIdle;
Graphics.EnableVsync.Value = true;
Graphics.VSyncMode.Value = VSyncMode.Switch;
Graphics.CustomVSyncInterval.Value = 120;
Graphics.EnableCustomVSyncInterval.Value = false;
Graphics.EnableShaderCache.Value = true;
Graphics.EnableTextureRecompression.Value = false;
Graphics.EnableMacroHLE.Value = true;
@ -240,7 +244,7 @@ namespace Ryujinx.UI.Common.Configuration
Hid.EnableMouse.Value = false;
Hid.Hotkeys.Value = new KeyboardHotkeys
{
ToggleVsync = Key.F1,
ToggleVSyncMode = Key.F1,
ToggleMute = Key.F2,
Screenshot = Key.F8,
ShowUI = Key.F4,

View File

@ -264,6 +264,7 @@ namespace Ryujinx.UI.Common
"0100800015926000", // Suika Game
"0100e46006708000", // Terraria
"01000a10041ea000", // The Elder Scrolls V: Skyrim
"010057a01e4d4000", // TSUKIHIME -A piece of blue glass moon-
"010080b00ad66000", // Undertale
];
}

View File

@ -57,6 +57,8 @@ using Key = Ryujinx.Input.Key;
using MouseButton = Ryujinx.Input.MouseButton;
using ScalingFilter = Ryujinx.Common.Configuration.ScalingFilter;
using Size = Avalonia.Size;
using Switch = Ryujinx.HLE.Switch;
using VSyncMode = Ryujinx.Common.Configuration.VSyncMode;
namespace Ryujinx.Ava
{
@ -203,6 +205,9 @@ namespace Ryujinx.Ava
ConfigurationState.Instance.Graphics.ScalingFilter.Event += UpdateScalingFilter;
ConfigurationState.Instance.Graphics.ScalingFilterLevel.Event += UpdateScalingFilterLevel;
ConfigurationState.Instance.Graphics.EnableColorSpacePassthrough.Event += UpdateColorSpacePassthrough;
ConfigurationState.Instance.Graphics.VSyncMode.Event += UpdateVSyncMode;
ConfigurationState.Instance.Graphics.CustomVSyncInterval.Event += UpdateCustomVSyncIntervalValue;
ConfigurationState.Instance.Graphics.EnableCustomVSyncInterval.Event += UpdateCustomVSyncIntervalEnabled;
ConfigurationState.Instance.System.EnableInternetAccess.Event += UpdateEnableInternetAccessState;
ConfigurationState.Instance.Multiplayer.LanInterfaceId.Event += UpdateLanInterfaceIdState;
@ -295,6 +300,66 @@ namespace Ryujinx.Ava
_renderer.Window?.SetColorSpacePassthrough((bool)ConfigurationState.Instance.Graphics.EnableColorSpacePassthrough.Value);
}
public void UpdateVSyncMode(object sender, ReactiveEventArgs<VSyncMode> e)
{
if (Device != null)
{
Device.VSyncMode = e.NewValue;
Device.UpdateVSyncInterval();
}
_renderer.Window?.ChangeVSyncMode((Ryujinx.Graphics.GAL.VSyncMode)e.NewValue);
_viewModel.ShowCustomVSyncIntervalPicker = (e.NewValue == VSyncMode.Custom);
}
public void VSyncModeToggle()
{
VSyncMode oldVSyncMode = Device.VSyncMode;
VSyncMode newVSyncMode = VSyncMode.Switch;
bool customVSyncIntervalEnabled = ConfigurationState.Instance.Graphics.EnableCustomVSyncInterval.Value;
switch (oldVSyncMode)
{
case VSyncMode.Switch:
newVSyncMode = VSyncMode.Unbounded;
break;
case VSyncMode.Unbounded:
if (customVSyncIntervalEnabled)
{
newVSyncMode = VSyncMode.Custom;
}
else
{
newVSyncMode = VSyncMode.Switch;
}
break;
case VSyncMode.Custom:
newVSyncMode = VSyncMode.Switch;
break;
}
UpdateVSyncMode(this, new ReactiveEventArgs<VSyncMode>(oldVSyncMode, newVSyncMode));
}
private void UpdateCustomVSyncIntervalValue(object sender, ReactiveEventArgs<int> e)
{
if (Device != null)
{
Device.TargetVSyncInterval = e.NewValue;
Device.UpdateVSyncInterval();
}
}
private void UpdateCustomVSyncIntervalEnabled(object sender, ReactiveEventArgs<bool> e)
{
if (Device != null)
{
Device.CustomVSyncIntervalEnabled = e.NewValue;
Device.UpdateVSyncInterval();
}
}
private void ShowCursor()
{
Dispatcher.UIThread.Post(() =>
@ -352,11 +417,7 @@ namespace Ryujinx.Ava
string filename = $"{sanitizedApplicationName}_{currentTime.Year}-{currentTime.Month:D2}-{currentTime.Day:D2}_{currentTime.Hour:D2}-{currentTime.Minute:D2}-{currentTime.Second:D2}.png";
string directory = AppDataManager.Mode switch
{
AppDataManager.LaunchMode.Portable or AppDataManager.LaunchMode.Custom => Path.Combine(AppDataManager.BaseDirPath, "screenshots"),
_ => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyPictures), "Ryujinx"),
};
string directory = Path.Combine(AppDataManager.BaseDirPath, "screenshots");
string path = Path.Combine(directory, filename);
@ -509,12 +570,6 @@ namespace Ryujinx.Ava
Device.Configuration.MultiplayerDisableP2p = e.NewValue;
}
public void ToggleVSync()
{
Device.EnableDeviceVsync = !Device.EnableDeviceVsync;
_renderer.Window.ChangeVSyncMode(Device.EnableDeviceVsync);
}
public void Stop()
{
_isActive = false;
@ -868,7 +923,7 @@ namespace Ryujinx.Ava
_viewModel.UiHandler,
(SystemLanguage)ConfigurationState.Instance.System.Language.Value,
(RegionCode)ConfigurationState.Instance.System.Region.Value,
ConfigurationState.Instance.Graphics.EnableVsync,
ConfigurationState.Instance.Graphics.VSyncMode,
ConfigurationState.Instance.System.EnableDockedMode,
ConfigurationState.Instance.System.EnablePtc,
ConfigurationState.Instance.System.EnableInternetAccess,
@ -885,7 +940,8 @@ namespace Ryujinx.Ava
ConfigurationState.Instance.Multiplayer.Mode,
ConfigurationState.Instance.Multiplayer.DisableP2p,
ConfigurationState.Instance.Multiplayer.LdnPassphrase,
ConfigurationState.Instance.Multiplayer.LdnServer));
ConfigurationState.Instance.Multiplayer.LdnServer,
ConfigurationState.Instance.Graphics.CustomVSyncInterval.Value));
}
private static IHardwareDeviceDriver InitializeAudio()
@ -1006,7 +1062,7 @@ namespace Ryujinx.Ava
Device.Gpu.SetGpuThread();
Device.Gpu.InitializeShaderCache(_gpuCancellationTokenSource.Token);
_renderer.Window.ChangeVSyncMode(Device.EnableDeviceVsync);
_renderer.Window.ChangeVSyncMode((Ryujinx.Graphics.GAL.VSyncMode)Device.VSyncMode);
while (_isActive)
{
@ -1067,6 +1123,7 @@ namespace Ryujinx.Ava
{
// Run a status update only when a frame is to be drawn. This prevents from updating the ui and wasting a render when no frame is queued.
string dockedMode = ConfigurationState.Instance.System.EnableDockedMode ? LocaleManager.Instance[LocaleKeys.Docked] : LocaleManager.Instance[LocaleKeys.Handheld];
string vSyncMode = Device.VSyncMode.ToString();
UpdateShaderCount();
@ -1076,7 +1133,7 @@ namespace Ryujinx.Ava
}
StatusUpdatedEvent?.Invoke(this, new StatusUpdatedEventArgs(
Device.EnableDeviceVsync,
vSyncMode,
LocaleManager.Instance[LocaleKeys.VolumeShort] + $": {(int)(Device.GetVolume() * 100)}%",
dockedMode,
ConfigurationState.Instance.Graphics.AspectRatio.Value.ToText(),
@ -1179,8 +1236,16 @@ namespace Ryujinx.Ava
{
switch (currentHotkeyState)
{
case KeyboardHotkeyState.ToggleVSync:
ToggleVSync();
case KeyboardHotkeyState.ToggleVSyncMode:
VSyncModeToggle();
break;
case KeyboardHotkeyState.CustomVSyncIntervalDecrement:
Device.DecrementCustomVSyncInterval();
_viewModel.CustomVSyncInterval -= 1;
break;
case KeyboardHotkeyState.CustomVSyncIntervalIncrement:
Device.IncrementCustomVSyncInterval();
_viewModel.CustomVSyncInterval += 1;
break;
case KeyboardHotkeyState.Screenshot:
ScreenshotRequested = true;
@ -1267,9 +1332,9 @@ namespace Ryujinx.Ava
{
KeyboardHotkeyState state = KeyboardHotkeyState.None;
if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.ToggleVsync))
if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.ToggleVSyncMode))
{
state = KeyboardHotkeyState.ToggleVSync;
state = KeyboardHotkeyState.ToggleVSyncMode;
}
else if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.Screenshot))
{
@ -1303,6 +1368,14 @@ namespace Ryujinx.Ava
{
state = KeyboardHotkeyState.VolumeDown;
}
else if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.CustomVSyncIntervalIncrement))
{
state = KeyboardHotkeyState.CustomVSyncIntervalIncrement;
}
else if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.CustomVSyncIntervalDecrement))
{
state = KeyboardHotkeyState.CustomVSyncIntervalDecrement;
}
return state;
}

View File

@ -1,6 +1,7 @@
{
"Language": "اَلْعَرَبِيَّةُ",
"MenuBarFileOpenApplet": "فتح التطبيق المصغر",
"MenuBarFileOpenAppletOpenMiiApplet": "Mii Edit Applet",
"MenuBarFileOpenAppletOpenMiiAppletToolTip": "‫افتح تطبيق تحرير Mii في الوضع المستقل",
"SettingsTabInputDirectMouseAccess": "الوصول المباشر للفأرة",
"SettingsTabSystemMemoryManagerMode": "وضع إدارة الذاكرة:",
@ -457,6 +458,7 @@
"DialogUpdaterExtractionMessage": "جاري استخراج التحديث...",
"DialogUpdaterRenamingMessage": "إعادة تسمية التحديث...",
"DialogUpdaterAddingFilesMessage": "إضافة تحديث جديد...",
"DialogUpdaterShowChangelogMessage": "Show Changelog",
"DialogUpdaterCompleteMessage": "اكتمل التحديث",
"DialogUpdaterRestartMessage": "هل تريد إعادة تشغيل ريوجينكس الآن؟",
"DialogUpdaterNoInternetMessage": "أنت غير متصل بالإنترنت.",

View File

@ -1,6 +1,7 @@
{
"Language": "Deutsch",
"MenuBarFileOpenApplet": "Öffne Anwendung",
"MenuBarFileOpenAppletOpenMiiApplet": "Mii Edit Applet",
"MenuBarFileOpenAppletOpenMiiAppletToolTip": "Öffnet das Mii-Editor-Applet im Standalone-Modus",
"SettingsTabInputDirectMouseAccess": "Direkter Mauszugriff",
"SettingsTabSystemMemoryManagerMode": "Speichermanagermodus:",
@ -457,6 +458,7 @@
"DialogUpdaterExtractionMessage": "Update wird entpackt...",
"DialogUpdaterRenamingMessage": "Update wird umbenannt...",
"DialogUpdaterAddingFilesMessage": "Update wird hinzugefügt...",
"DialogUpdaterShowChangelogMessage": "Show Changelog",
"DialogUpdaterCompleteMessage": "Update abgeschlossen!",
"DialogUpdaterRestartMessage": "Ryujinx jetzt neu starten?",
"DialogUpdaterNoInternetMessage": "Es besteht keine Verbindung mit dem Internet!",

View File

@ -1,6 +1,7 @@
{
"Language": "Ελληνικά",
"MenuBarFileOpenApplet": "Άνοιγμα Applet",
"MenuBarFileOpenAppletOpenMiiApplet": "Mii Edit Applet",
"MenuBarFileOpenAppletOpenMiiAppletToolTip": "Άνοιγμα του Mii Editor Applet σε Αυτόνομη λειτουργία",
"SettingsTabInputDirectMouseAccess": "Άμεση Πρόσβαση Ποντικιού",
"SettingsTabSystemMemoryManagerMode": "Λειτουργία Διαχείρισης Μνήμης:",
@ -457,6 +458,7 @@
"DialogUpdaterExtractionMessage": "Εξαγωγή Ενημέρωσης...",
"DialogUpdaterRenamingMessage": "Μετονομασία Ενημέρωσης...",
"DialogUpdaterAddingFilesMessage": "Προσθήκη Νέας Ενημέρωσης...",
"DialogUpdaterShowChangelogMessage": "Show Changelog",
"DialogUpdaterCompleteMessage": "Η Ενημέρωση Ολοκληρώθηκε!",
"DialogUpdaterRestartMessage": "Θέλετε να επανεκκινήσετε το Ryujinx τώρα;",
"DialogUpdaterNoInternetMessage": "Δεν είστε συνδεδεμένοι στο Διαδίκτυο!",

View File

@ -1,6 +1,7 @@
{
"Language": "English (US)",
"MenuBarFileOpenApplet": "Open Applet",
"MenuBarFileOpenAppletOpenMiiApplet": "Mii Edit Applet",
"MenuBarFileOpenAppletOpenMiiAppletToolTip": "Open Mii Editor Applet in Standalone mode",
"SettingsTabInputDirectMouseAccess": "Direct Mouse Access",
"SettingsTabSystemMemoryManagerMode": "Memory Manager Mode:",
@ -142,9 +143,20 @@
"SettingsTabSystemSystemLanguageLatinAmericanSpanish": "Latin American Spanish",
"SettingsTabSystemSystemLanguageSimplifiedChinese": "Simplified Chinese",
"SettingsTabSystemSystemLanguageTraditionalChinese": "Traditional Chinese",
"SettingsTabSystemSystemTimeZone": "System TimeZone:",
"SettingsTabSystemSystemTimeZone": "System Time Zone:",
"SettingsTabSystemSystemTime": "System Time:",
"SettingsTabSystemEnableVsync": "VSync",
"SettingsTabSystemVSyncMode": "VSync:",
"SettingsTabSystemEnableCustomVSyncInterval": "Enable custom refresh rate (Experimental)",
"SettingsTabSystemVSyncModeSwitch": "Switch",
"SettingsTabSystemVSyncModeUnbounded": "Unbounded",
"SettingsTabSystemVSyncModeCustom": "Custom Refresh Rate",
"SettingsTabSystemVSyncModeTooltip": "Emulated Vertical Sync. 'Switch' emulates the Switch's refresh rate of 60Hz. 'Unbounded' is an unbounded refresh rate.",
"SettingsTabSystemVSyncModeTooltipCustom": "Emulated Vertical Sync. 'Switch' emulates the Switch's refresh rate of 60Hz. 'Unbounded' is an unbounded refresh rate. 'Custom' emulates the specified custom refresh rate.",
"SettingsTabSystemEnableCustomVSyncIntervalTooltip": "Allows the user to specify an emulated refresh rate. In some titles, this may speed up or slow down the rate of gameplay logic. In other titles, it may allow for capping FPS at some multiple of the refresh rate, or lead to unpredictable behavior. This is an experimental feature, with no guarantees for how gameplay will be affected. \n\nLeave OFF if unsure.",
"SettingsTabSystemCustomVSyncIntervalValueTooltip": "The custom refresh rate target value.",
"SettingsTabSystemCustomVSyncIntervalSliderTooltip": "The custom refresh rate, as a percentage of the normal Switch refresh rate.",
"SettingsTabSystemCustomVSyncIntervalPercentage": "Custom Refresh Rate %:",
"SettingsTabSystemCustomVSyncIntervalValue": "Custom Refresh Rate Value:",
"SettingsTabSystemEnablePptc": "PPTC (Profiled Persistent Translation Cache)",
"SettingsTabSystemEnableLowPowerPptc": "Low-power PPTC cache",
"SettingsTabSystemEnableFsIntegrityChecks": "FS Integrity Checks",
@ -153,6 +165,7 @@
"SettingsTabSystemAudioBackendOpenAL": "OpenAL",
"SettingsTabSystemAudioBackendSoundIO": "SoundIO",
"SettingsTabSystemAudioBackendSDL2": "SDL2",
"SettingsTabSystemCustomVSyncInterval": "Interval",
"SettingsTabSystemHacks": "Hacks",
"SettingsTabSystemHacksNote": "May cause instability",
"SettingsTabSystemDramSize": "DRAM size:",
@ -457,6 +470,7 @@
"DialogUpdaterExtractionMessage": "Extracting Update...",
"DialogUpdaterRenamingMessage": "Renaming Update...",
"DialogUpdaterAddingFilesMessage": "Adding New Update...",
"DialogUpdaterShowChangelogMessage": "Show Changelog",
"DialogUpdaterCompleteMessage": "Update Complete!",
"DialogUpdaterRestartMessage": "Do you want to restart Ryujinx now?",
"DialogUpdaterNoInternetMessage": "You are not connected to the Internet!",
@ -719,11 +733,13 @@
"RyujinxUpdater": "Ryujinx Updater",
"SettingsTabHotkeys": "Keyboard Hotkeys",
"SettingsTabHotkeysHotkeys": "Keyboard Hotkeys",
"SettingsTabHotkeysToggleVsyncHotkey": "Toggle VSync:",
"SettingsTabHotkeysToggleVSyncModeHotkey": "Toggle VSync mode:",
"SettingsTabHotkeysScreenshotHotkey": "Screenshot:",
"SettingsTabHotkeysShowUiHotkey": "Show UI:",
"SettingsTabHotkeysPauseHotkey": "Pause:",
"SettingsTabHotkeysToggleMuteHotkey": "Mute:",
"SettingsTabHotkeysIncrementCustomVSyncIntervalHotkey": "Raise custom refresh rate",
"SettingsTabHotkeysDecrementCustomVSyncIntervalHotkey": "Lower custom refresh rate",
"ControllerMotionTitle": "Motion Control Settings",
"ControllerRumbleTitle": "Rumble Settings",
"SettingsSelectThemeFileDialogTitle": "Select Theme File",

View File

@ -1,6 +1,7 @@
{
"Language": "Español (ES)",
"MenuBarFileOpenApplet": "Abrir applet",
"MenuBarFileOpenAppletOpenMiiApplet": "Mii Edit Applet",
"MenuBarFileOpenAppletOpenMiiAppletToolTip": "Abre el editor de Mii en modo autónomo",
"SettingsTabInputDirectMouseAccess": "Acceso directo al ratón",
"SettingsTabSystemMemoryManagerMode": "Modo del administrador de memoria:",
@ -457,6 +458,7 @@
"DialogUpdaterExtractionMessage": "Extrayendo actualización...",
"DialogUpdaterRenamingMessage": "Renombrando actualización...",
"DialogUpdaterAddingFilesMessage": "Aplicando actualización...",
"DialogUpdaterShowChangelogMessage": "Show Changelog",
"DialogUpdaterCompleteMessage": "¡Actualización completa!",
"DialogUpdaterRestartMessage": "¿Quieres reiniciar Ryujinx?",
"DialogUpdaterNoInternetMessage": "¡No estás conectado a internet!",

View File

@ -1,6 +1,7 @@
{
"Language": "Français",
"MenuBarFileOpenApplet": "Ouvrir un programme",
"MenuBarFileOpenAppletOpenMiiApplet": "Éditeur de Mii",
"MenuBarFileOpenAppletOpenMiiAppletToolTip": "Ouvrir l'éditeur Mii en mode Standalone",
"SettingsTabInputDirectMouseAccess": "Accès direct à la souris",
"SettingsTabSystemMemoryManagerMode": "Mode de gestion de la mémoire :",
@ -457,6 +458,7 @@
"DialogUpdaterExtractionMessage": "Extraction de la mise à jour…",
"DialogUpdaterRenamingMessage": "Renommage de la mise à jour...",
"DialogUpdaterAddingFilesMessage": "Ajout d'une nouvelle mise à jour...",
"DialogUpdaterShowChangelogMessage": "Show Changelog",
"DialogUpdaterCompleteMessage": "Mise à jour terminée !",
"DialogUpdaterRestartMessage": "Voulez-vous redémarrer Ryujinx maintenant ?",
"DialogUpdaterNoInternetMessage": "Vous n'êtes pas connecté à Internet !",

View File

@ -1,6 +1,7 @@
{
"Language": "עִברִית",
"MenuBarFileOpenApplet": "פתח יישומון",
"MenuBarFileOpenAppletOpenMiiApplet": "Mii Edit Applet",
"MenuBarFileOpenAppletOpenMiiAppletToolTip": "פתח את יישומון עורך ה- Mii במצב עצמאי",
"SettingsTabInputDirectMouseAccess": "גישה ישירה לעכבר",
"SettingsTabSystemMemoryManagerMode": "מצב מנהל זיכרון:",
@ -457,6 +458,7 @@
"DialogUpdaterExtractionMessage": "מחלץ עדכון...",
"DialogUpdaterRenamingMessage": "משנה את שם העדכון...",
"DialogUpdaterAddingFilesMessage": "מוסיף עדכון חדש...",
"DialogUpdaterShowChangelogMessage": "Show Changelog",
"DialogUpdaterCompleteMessage": "העדכון הושלם!",
"DialogUpdaterRestartMessage": "האם אתם רוצים להפעיל מחדש את ריוג'ינקס עכשיו?",
"DialogUpdaterNoInternetMessage": "אתם לא מחוברים לאינטרנט!",

View File

@ -1,6 +1,7 @@
{
"Language": "Italiano",
"MenuBarFileOpenApplet": "Apri applet",
"MenuBarFileOpenAppletOpenMiiApplet": "Mii Edit Applet",
"MenuBarFileOpenAppletOpenMiiAppletToolTip": "Apri l'applet Mii Editor in modalità Standalone",
"SettingsTabInputDirectMouseAccess": "Accesso diretto al mouse",
"SettingsTabSystemMemoryManagerMode": "Modalità di gestione della memoria:",
@ -457,6 +458,7 @@
"DialogUpdaterExtractionMessage": "Estrazione dell'aggiornamento...",
"DialogUpdaterRenamingMessage": "Rinominazione dell'aggiornamento...",
"DialogUpdaterAddingFilesMessage": "Aggiunta del nuovo aggiornamento...",
"DialogUpdaterShowChangelogMessage": "Show Changelog",
"DialogUpdaterCompleteMessage": "Aggiornamento completato!",
"DialogUpdaterRestartMessage": "Vuoi riavviare Ryujinx adesso?",
"DialogUpdaterNoInternetMessage": "Non sei connesso ad Internet!",

View File

@ -1,6 +1,7 @@
{
"Language": "日本語",
"MenuBarFileOpenApplet": "アプレットを開く",
"MenuBarFileOpenAppletOpenMiiApplet": "Mii Edit Applet",
"MenuBarFileOpenAppletOpenMiiAppletToolTip": "スタンドアロンモードで Mii エディタアプレットを開きます",
"SettingsTabInputDirectMouseAccess": "マウス直接アクセス",
"SettingsTabSystemMemoryManagerMode": "メモリ管理モード:",
@ -457,6 +458,7 @@
"DialogUpdaterExtractionMessage": "アップデートを展開中...",
"DialogUpdaterRenamingMessage": "アップデートをリネーム中...",
"DialogUpdaterAddingFilesMessage": "新規アップデートを追加中...",
"DialogUpdaterShowChangelogMessage": "Show Changelog",
"DialogUpdaterCompleteMessage": "アップデート完了!",
"DialogUpdaterRestartMessage": "すぐに Ryujinx を再起動しますか?",
"DialogUpdaterNoInternetMessage": "インターネットに接続されていません!",

View File

@ -1,6 +1,7 @@
{
"Language": "한국어",
"MenuBarFileOpenApplet": "애플릿 열기",
"MenuBarFileOpenAppletOpenMiiApplet": "Mii Edit Applet",
"MenuBarFileOpenAppletOpenMiiAppletToolTip": "독립 실행형 모드로 Mii 편집기 애플릿 열기",
"SettingsTabInputDirectMouseAccess": "마우스 직접 접근",
"SettingsTabSystemMemoryManagerMode": "메모리 관리자 모드 :",
@ -457,6 +458,7 @@
"DialogUpdaterExtractionMessage": "업데이트 추출 중...",
"DialogUpdaterRenamingMessage": "이름 변경 업데이트...",
"DialogUpdaterAddingFilesMessage": "새 업데이트 추가 중...",
"DialogUpdaterShowChangelogMessage": "변경 로그 보기",
"DialogUpdaterCompleteMessage": "업데이트가 완료되었습니다!",
"DialogUpdaterRestartMessage": "지금 Ryujinx를 다시 시작하시겠습니까?",
"DialogUpdaterNoInternetMessage": "인터넷에 연결되어 있지 않습니다!",

View File

@ -1,6 +1,7 @@
{
"Language": "Polski",
"MenuBarFileOpenApplet": "Otwórz Aplet",
"MenuBarFileOpenAppletOpenMiiApplet": "Mii Edit Applet",
"MenuBarFileOpenAppletOpenMiiAppletToolTip": "Otwórz aplet Mii Editor w trybie indywidualnym",
"SettingsTabInputDirectMouseAccess": "Bezpośredni dostęp do myszy",
"SettingsTabSystemMemoryManagerMode": "Tryb menedżera pamięci:",
@ -457,6 +458,7 @@
"DialogUpdaterExtractionMessage": "Wypakowywanie Aktualizacji...",
"DialogUpdaterRenamingMessage": "Zmiana Nazwy Aktualizacji...",
"DialogUpdaterAddingFilesMessage": "Dodawanie Nowej Aktualizacji...",
"DialogUpdaterShowChangelogMessage": "Show Changelog",
"DialogUpdaterCompleteMessage": "Aktualizacja Zakończona!",
"DialogUpdaterRestartMessage": "Czy chcesz teraz zrestartować Ryujinx?",
"DialogUpdaterNoInternetMessage": "Nie masz połączenia z Internetem!",

View File

@ -1,6 +1,7 @@
{
"Language": "Português (BR)",
"MenuBarFileOpenApplet": "Abrir Applet",
"MenuBarFileOpenAppletOpenMiiApplet": "Mii Edit Applet",
"MenuBarFileOpenAppletOpenMiiAppletToolTip": "Abrir editor Mii em modo avulso",
"SettingsTabInputDirectMouseAccess": "Acesso direto ao mouse",
"SettingsTabSystemMemoryManagerMode": "Modo de gerenciamento de memória:",
@ -457,6 +458,7 @@
"DialogUpdaterExtractionMessage": "Extraindo atualização...",
"DialogUpdaterRenamingMessage": "Renomeando atualização...",
"DialogUpdaterAddingFilesMessage": "Adicionando nova atualização...",
"DialogUpdaterShowChangelogMessage": "Show Changelog",
"DialogUpdaterCompleteMessage": "Atualização concluída!",
"DialogUpdaterRestartMessage": "Deseja reiniciar o Ryujinx agora?",
"DialogUpdaterNoInternetMessage": "Você não está conectado à Internet!",

View File

@ -1,6 +1,7 @@
{
"Language": "Русский (RU)",
"MenuBarFileOpenApplet": "Открыть апплет",
"MenuBarFileOpenAppletOpenMiiApplet": "Mii Edit Applet",
"MenuBarFileOpenAppletOpenMiiAppletToolTip": "Открывает апплет Mii Editor в автономном режиме",
"SettingsTabInputDirectMouseAccess": "Прямой ввод мыши",
"SettingsTabSystemMemoryManagerMode": "Режим менеджера памяти:",
@ -457,6 +458,7 @@
"DialogUpdaterExtractionMessage": "Извлечение обновления...",
"DialogUpdaterRenamingMessage": "Переименование обновления...",
"DialogUpdaterAddingFilesMessage": "Добавление нового обновления...",
"DialogUpdaterShowChangelogMessage": "Show Changelog",
"DialogUpdaterCompleteMessage": "Обновление завершено",
"DialogUpdaterRestartMessage": "Перезапустить Ryujinx?",
"DialogUpdaterNoInternetMessage": "Вы не подключены к интернету",

View File

@ -1,6 +1,7 @@
{
"Language": "ภาษาไทย",
"MenuBarFileOpenApplet": "เปิด Applet",
"MenuBarFileOpenAppletOpenMiiApplet": "Mii Edit Applet",
"MenuBarFileOpenAppletOpenMiiAppletToolTip": "เปิดโปรแกรม Mii Editor Applet",
"SettingsTabInputDirectMouseAccess": "เข้าถึงเมาส์ได้โดยตรง",
"SettingsTabSystemMemoryManagerMode": "โหมดจัดการหน่วยความจำ:",
@ -457,6 +458,7 @@
"DialogUpdaterExtractionMessage": "กำลังแตกไฟล์อัปเดต...",
"DialogUpdaterRenamingMessage": "กำลังลบไฟล์เก่า...",
"DialogUpdaterAddingFilesMessage": "กำลังเพิ่มไฟล์อัปเดตใหม่...",
"DialogUpdaterShowChangelogMessage": "Show Changelog",
"DialogUpdaterCompleteMessage": "อัปเดตเสร็จสมบูรณ์แล้ว!",
"DialogUpdaterRestartMessage": "คุณต้องการรีสตาร์ท Ryujinx ตอนนี้หรือไม่?",
"DialogUpdaterNoInternetMessage": "คุณไม่ได้เชื่อมต่อกับอินเทอร์เน็ต!",

View File

@ -1,6 +1,7 @@
{
"Language": "Türkçe",
"MenuBarFileOpenApplet": "Applet'i Aç",
"MenuBarFileOpenAppletOpenMiiApplet": "Mii Edit Applet",
"MenuBarFileOpenAppletOpenMiiAppletToolTip": "Mii Editör Applet'ini Bağımsız Mod'da Aç",
"SettingsTabInputDirectMouseAccess": "Doğrudan Mouse Erişimi",
"SettingsTabSystemMemoryManagerMode": "Hafıza Yönetim Modu:",
@ -457,6 +458,7 @@
"DialogUpdaterExtractionMessage": "Güncelleme Ayıklanıyor...",
"DialogUpdaterRenamingMessage": "Güncelleme Yeniden Adlandırılıyor...",
"DialogUpdaterAddingFilesMessage": "Yeni Güncelleme Ekleniyor...",
"DialogUpdaterShowChangelogMessage": "Show Changelog",
"DialogUpdaterCompleteMessage": "Güncelleme Tamamlandı!",
"DialogUpdaterRestartMessage": "Ryujinx'i şimdi yeniden başlatmak istiyor musunuz?",
"DialogUpdaterNoInternetMessage": "İnternete bağlı değilsiniz!",

View File

@ -1,6 +1,7 @@
{
"Language": "Українська",
"MenuBarFileOpenApplet": "Відкрити аплет",
"MenuBarFileOpenAppletOpenMiiApplet": "Mii Edit Applet",
"MenuBarFileOpenAppletOpenMiiAppletToolTip": "Відкрити аплет Mii Editor в автономному режимі",
"SettingsTabInputDirectMouseAccess": "Прямий доступ мишею",
"SettingsTabSystemMemoryManagerMode": "Режим диспетчера пам’яті:",
@ -457,6 +458,7 @@
"DialogUpdaterExtractionMessage": "Видобування оновлення...",
"DialogUpdaterRenamingMessage": "Перейменування оновлення...",
"DialogUpdaterAddingFilesMessage": "Додавання нового оновлення...",
"DialogUpdaterShowChangelogMessage": "Show Changelog",
"DialogUpdaterCompleteMessage": "Оновлення завершено!",
"DialogUpdaterRestartMessage": "Перезапустити Ryujinx зараз?",
"DialogUpdaterNoInternetMessage": "Ви не підключені до Інтернету!",

View File

@ -1,6 +1,7 @@
{
"Language": "简体中文",
"MenuBarFileOpenApplet": "打开小程序",
"MenuBarFileOpenAppletOpenMiiApplet": "Mii Edit Applet",
"MenuBarFileOpenAppletOpenMiiAppletToolTip": "打开独立的 Mii 小程序",
"SettingsTabInputDirectMouseAccess": "直通鼠标操作",
"SettingsTabSystemMemoryManagerMode": "内存管理模式:",
@ -457,6 +458,7 @@
"DialogUpdaterExtractionMessage": "正在提取更新...",
"DialogUpdaterRenamingMessage": "正在重命名更新...",
"DialogUpdaterAddingFilesMessage": "安装更新中...",
"DialogUpdaterShowChangelogMessage": "Show Changelog",
"DialogUpdaterCompleteMessage": "更新成功!",
"DialogUpdaterRestartMessage": "是否立即重启 Ryujinx 模拟器?",
"DialogUpdaterNoInternetMessage": "没有连接到网络",

View File

@ -1,6 +1,7 @@
{
"Language": "繁體中文 (台灣)",
"MenuBarFileOpenApplet": "開啟小程式",
"MenuBarFileOpenAppletOpenMiiApplet": "Mii Edit Applet",
"MenuBarFileOpenAppletOpenMiiAppletToolTip": "在獨立模式下開啟 Mii 編輯器小程式",
"SettingsTabInputDirectMouseAccess": "滑鼠直接存取",
"SettingsTabSystemMemoryManagerMode": "記憶體管理員模式:",
@ -457,6 +458,7 @@
"DialogUpdaterExtractionMessage": "正在提取更新...",
"DialogUpdaterRenamingMessage": "重新命名更新...",
"DialogUpdaterAddingFilesMessage": "加入新更新...",
"DialogUpdaterShowChangelogMessage": "Show Changelog",
"DialogUpdaterCompleteMessage": "更新成功!",
"DialogUpdaterRestartMessage": "您現在要重新啟動 Ryujinx 嗎?",
"DialogUpdaterNoInternetMessage": "您沒有連線到網際網路!",

View File

@ -1,7 +1,8 @@
<Styles
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia">
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:windowing="clr-namespace:FluentAvalonia.UI.Windowing;assembly=FluentAvalonia">
<Design.PreviewWith>
<Border Height="2000"
Padding="20">
@ -231,7 +232,7 @@
<Setter Property="BorderBrush"
Value="{DynamicResource HighlightBrush}" />
</Style>
<Style Selector="Button">
<Style Selector="Button:not(windowing|MinMaxCloseControl Button)">
<Setter Property="MinWidth"
Value="80" />
</Style>

View File

@ -26,8 +26,9 @@
<Color x:Key="AppListBackgroundColor">#b3ffffff</Color>
<Color x:Key="AppListHoverBackgroundColor">#80cccccc</Color>
<Color x:Key="SecondaryTextColor">#A0000000</Color>
<Color x:Key="VsyncEnabled">#FF2EEAC9</Color>
<Color x:Key="VsyncDisabled">#FFFF4554</Color>
<Color x:Key="Switch">#FF2EEAC9</Color>
<Color x:Key="Unbounded">#FFFF4554</Color>
<Color x:Key="Custom">#6483F5</Color>
</ResourceDictionary>
<ResourceDictionary x:Key="Light">
<SolidColorBrush x:Key="DataGridSelectionBackgroundBrush"

View File

@ -3,7 +3,7 @@ namespace Ryujinx.Ava.Common
public enum KeyboardHotkeyState
{
None,
ToggleVSync,
ToggleVSyncMode,
Screenshot,
ShowUI,
Pause,
@ -12,5 +12,7 @@ namespace Ryujinx.Ava.Common
ResScaleDown,
VolumeUp,
VolumeDown,
CustomVSyncIntervalIncrement,
CustomVSyncIntervalDecrement,
}
}

View File

@ -261,6 +261,16 @@ namespace Ryujinx.Ava.UI.Helpers
string.Empty,
LocaleManager.Instance[LocaleKeys.InputDialogOk],
(int)Symbol.Important);
internal static async Task<UserResult> CreateUpdaterUpToDateInfoDialog(string primary, string secondaryText)
=> await ShowTextDialog(
LocaleManager.Instance[LocaleKeys.DialogUpdaterTitle],
primary,
secondaryText,
LocaleManager.Instance[LocaleKeys.DialogUpdaterShowChangelogMessage],
string.Empty,
LocaleManager.Instance[LocaleKeys.InputDialogOk],
(int)Symbol.Important);
internal static async Task CreateWarningDialog(string primary, string secondaryText)
=> await ShowTextDialog(
@ -309,6 +319,30 @@ namespace Ryujinx.Ava.UI.Helpers
return response == UserResult.Yes;
}
internal static async Task<UserResult> CreateUpdaterChoiceDialog(string title, string primary, string secondaryText)
{
if (_isChoiceDialogOpen)
{
return UserResult.Cancel;
}
_isChoiceDialogOpen = true;
UserResult response = await ShowTextDialog(
title,
primary,
secondaryText,
LocaleManager.Instance[LocaleKeys.InputDialogYes],
LocaleManager.Instance[LocaleKeys.DialogUpdaterShowChangelogMessage],
LocaleManager.Instance[LocaleKeys.InputDialogNo],
(int)Symbol.Help,
UserResult.Yes);
_isChoiceDialogOpen = false;
return response;
}
internal static async Task<bool> CreateExitDialog()
{

View File

@ -5,13 +5,13 @@ namespace Ryujinx.Ava.UI.Models.Input
{
public class HotkeyConfig : BaseModel
{
private Key _toggleVsync;
public Key ToggleVsync
private Key _toggleVSyncMode;
public Key ToggleVSyncMode
{
get => _toggleVsync;
get => _toggleVSyncMode;
set
{
_toggleVsync = value;
_toggleVSyncMode = value;
OnPropertyChanged();
}
}
@ -104,11 +104,33 @@ namespace Ryujinx.Ava.UI.Models.Input
}
}
private Key _customVSyncIntervalIncrement;
public Key CustomVSyncIntervalIncrement
{
get => _customVSyncIntervalIncrement;
set
{
_customVSyncIntervalIncrement = value;
OnPropertyChanged();
}
}
private Key _customVSyncIntervalDecrement;
public Key CustomVSyncIntervalDecrement
{
get => _customVSyncIntervalDecrement;
set
{
_customVSyncIntervalDecrement = value;
OnPropertyChanged();
}
}
public HotkeyConfig(KeyboardHotkeys config)
{
if (config != null)
{
ToggleVsync = config.ToggleVsync;
ToggleVSyncMode = config.ToggleVSyncMode;
Screenshot = config.Screenshot;
ShowUI = config.ShowUI;
Pause = config.Pause;
@ -117,6 +139,8 @@ namespace Ryujinx.Ava.UI.Models.Input
ResScaleDown = config.ResScaleDown;
VolumeUp = config.VolumeUp;
VolumeDown = config.VolumeDown;
CustomVSyncIntervalIncrement = config.CustomVSyncIntervalIncrement;
CustomVSyncIntervalDecrement = config.CustomVSyncIntervalDecrement;
}
}
@ -124,7 +148,7 @@ namespace Ryujinx.Ava.UI.Models.Input
{
var config = new KeyboardHotkeys
{
ToggleVsync = ToggleVsync,
ToggleVSyncMode = ToggleVSyncMode,
Screenshot = Screenshot,
ShowUI = ShowUI,
Pause = Pause,
@ -133,6 +157,8 @@ namespace Ryujinx.Ava.UI.Models.Input
ResScaleDown = ResScaleDown,
VolumeUp = VolumeUp,
VolumeDown = VolumeDown,
CustomVSyncIntervalIncrement = CustomVSyncIntervalIncrement,
CustomVSyncIntervalDecrement = CustomVSyncIntervalDecrement,
};
return config;

View File

@ -47,7 +47,7 @@ namespace Ryujinx.Ava.UI.Models
TitleId = info.ProgramId;
UserId = info.UserId;
var appData = App.MainWindow.ViewModel.Applications.FirstOrDefault(x => x.IdString.Equals(TitleIdString, StringComparison.OrdinalIgnoreCase));
var appData = MainWindow.MainWindowViewModel.Applications.FirstOrDefault(x => x.IdString.Equals(TitleIdString, StringComparison.OrdinalIgnoreCase));
InGameList = appData != null;

View File

@ -4,18 +4,17 @@ namespace Ryujinx.Ava.UI.Models
{
internal class StatusUpdatedEventArgs : EventArgs
{
public bool VSyncEnabled { get; }
public string VSyncMode { get; }
public string VolumeStatus { get; }
public string AspectRatio { get; }
public string DockedMode { get; }
public string FifoStatus { get; }
public string GameStatus { get; }
public uint ShaderCount { get; }
public StatusUpdatedEventArgs(bool vSyncEnabled, string volumeStatus, string dockedMode, string aspectRatio, string gameStatus, string fifoStatus, uint shaderCount)
public StatusUpdatedEventArgs(string vSyncMode, string volumeStatus, string dockedMode, string aspectRatio, string gameStatus, string fifoStatus, uint shaderCount)
{
VSyncEnabled = vSyncEnabled;
VSyncMode = vSyncMode;
VolumeStatus = volumeStatus;
DockedMode = dockedMode;
AspectRatio = aspectRatio;

View File

@ -63,6 +63,7 @@ namespace Ryujinx.Ava.UI.ViewModels
private string _searchText;
private Timer _searchTimer;
private string _dockedStatusText;
private string _vSyncModeText;
private string _fifoStatusText;
private string _gameStatusText;
private string _volumeStatusText;
@ -80,7 +81,7 @@ namespace Ryujinx.Ava.UI.ViewModels
private bool _showStatusSeparator;
private Brush _progressBarForegroundColor;
private Brush _progressBarBackgroundColor;
private Brush _vsyncColor;
private Brush _vSyncModeColor;
private byte[] _selectedIcon;
private bool _isAppletMenuActive;
private int _statusBarProgressMaximum;
@ -111,6 +112,8 @@ namespace Ryujinx.Ava.UI.ViewModels
private WindowState _windowState;
private double _windowWidth;
private double _windowHeight;
private int _customVSyncInterval;
private int _customVSyncIntervalPercentageProxy;
private bool _isActive;
private bool _isSubMenuOpen;
@ -145,6 +148,7 @@ namespace Ryujinx.Ava.UI.ViewModels
Volume = ConfigurationState.Instance.System.AudioVolume;
}
CustomVSyncInterval = ConfigurationState.Instance.Graphics.CustomVSyncInterval.Value;
}
public void Initialize(
@ -447,17 +451,87 @@ namespace Ryujinx.Ava.UI.ViewModels
}
}
public Brush VsyncColor
public Brush VSyncModeColor
{
get => _vsyncColor;
get => _vSyncModeColor;
set
{
_vsyncColor = value;
_vSyncModeColor = value;
OnPropertyChanged();
}
}
public bool ShowCustomVSyncIntervalPicker
{
get
{
if (_isGameRunning)
{
return AppHost.Device.VSyncMode ==
VSyncMode.Custom;
}
else
{
return false;
}
}
set
{
OnPropertyChanged();
}
}
public int CustomVSyncIntervalPercentageProxy
{
get => _customVSyncIntervalPercentageProxy;
set
{
int newInterval = (int)((value / 100f) * 60);
_customVSyncInterval = newInterval;
_customVSyncIntervalPercentageProxy = value;
if (_isGameRunning)
{
AppHost.Device.CustomVSyncInterval = newInterval;
AppHost.Device.UpdateVSyncInterval();
}
OnPropertyChanged((nameof(CustomVSyncInterval)));
OnPropertyChanged((nameof(CustomVSyncIntervalPercentageText)));
}
}
public string CustomVSyncIntervalPercentageText
{
get
{
string text = CustomVSyncIntervalPercentageProxy.ToString() + "%";
return text;
}
set
{
}
}
public int CustomVSyncInterval
{
get => _customVSyncInterval;
set
{
_customVSyncInterval = value;
int newPercent = (int)((value / 60f) * 100);
_customVSyncIntervalPercentageProxy = newPercent;
if (_isGameRunning)
{
AppHost.Device.CustomVSyncInterval = value;
AppHost.Device.UpdateVSyncInterval();
}
OnPropertyChanged(nameof(CustomVSyncIntervalPercentageProxy));
OnPropertyChanged(nameof(CustomVSyncIntervalPercentageText));
OnPropertyChanged();
}
}
public byte[] SelectedIcon
{
get => _selectedIcon;
@ -578,6 +652,17 @@ namespace Ryujinx.Ava.UI.ViewModels
}
}
public string VSyncModeText
{
get => _vSyncModeText;
set
{
_vSyncModeText = value;
OnPropertyChanged();
}
}
public string DockedStatusText
{
get => _dockedStatusText;
@ -1292,17 +1377,18 @@ namespace Ryujinx.Ava.UI.ViewModels
{
Dispatcher.UIThread.InvokeAsync(() =>
{
Application.Current!.Styles.TryGetResource(args.VSyncEnabled
? "VsyncEnabled"
: "VsyncDisabled",
Application.Current!.Styles.TryGetResource(args.VSyncMode,
Application.Current.ActualThemeVariant,
out object color);
if (color is Color clr)
{
VsyncColor = new SolidColorBrush(clr);
VSyncModeColor = new SolidColorBrush(clr);
}
VSyncModeText = args.VSyncMode == "Custom" ? "Custom" : "VSync";
ShowCustomVSyncIntervalPicker =
args.VSyncMode == VSyncMode.Custom.ToString();
DockedStatusText = args.DockedMode;
AspectRatioStatusText = args.AspectRatio;
GameStatusText = args.GameStatus;
@ -1495,6 +1581,27 @@ namespace Ryujinx.Ava.UI.ViewModels
}
}
public void ToggleVSyncMode()
{
AppHost.VSyncModeToggle();
OnPropertyChanged(nameof(ShowCustomVSyncIntervalPicker));
}
public void VSyncModeSettingChanged()
{
if (_isGameRunning)
{
AppHost.Device.CustomVSyncInterval = ConfigurationState.Instance.Graphics.CustomVSyncInterval.Value;
AppHost.Device.UpdateVSyncInterval();
}
CustomVSyncInterval = ConfigurationState.Instance.Graphics.CustomVSyncInterval.Value;
OnPropertyChanged(nameof(ShowCustomVSyncIntervalPicker));
OnPropertyChanged(nameof(CustomVSyncIntervalPercentageProxy));
OnPropertyChanged(nameof(CustomVSyncIntervalPercentageText));
OnPropertyChanged(nameof(CustomVSyncInterval));
}
public async Task ExitCurrentState()
{
if (WindowState is WindowState.FullScreen)

View File

@ -52,6 +52,10 @@ namespace Ryujinx.Ava.UI.ViewModels
private int _graphicsBackendIndex;
private int _scalingFilter;
private int _scalingFilterLevel;
private int _customVSyncInterval;
private bool _enableCustomVSyncInterval;
private int _customVSyncIntervalPercentageProxy;
private VSyncMode _vSyncMode;
public event Action CloseWindow;
public event Action SaveSettingsEvent;
@ -154,7 +158,74 @@ namespace Ryujinx.Ava.UI.ViewModels
public bool EnableDockedMode { get; set; }
public bool EnableKeyboard { get; set; }
public bool EnableMouse { get; set; }
public bool EnableVsync { get; set; }
public VSyncMode VSyncMode
{
get => _vSyncMode;
set
{
if (value == VSyncMode.Custom ||
value == VSyncMode.Switch ||
value == VSyncMode.Unbounded)
{
_vSyncMode = value;
OnPropertyChanged();
}
}
}
public int CustomVSyncIntervalPercentageProxy
{
get => _customVSyncIntervalPercentageProxy;
set
{
int newInterval = (int)((value / 100f) * 60);
_customVSyncInterval = newInterval;
_customVSyncIntervalPercentageProxy = value;
OnPropertyChanged((nameof(CustomVSyncInterval)));
OnPropertyChanged((nameof(CustomVSyncIntervalPercentageText)));
}
}
public string CustomVSyncIntervalPercentageText
{
get
{
string text = CustomVSyncIntervalPercentageProxy.ToString() + "%";
return text;
}
}
public bool EnableCustomVSyncInterval
{
get => _enableCustomVSyncInterval;
set
{
_enableCustomVSyncInterval = value;
if (_vSyncMode == VSyncMode.Custom && !value)
{
VSyncMode = VSyncMode.Switch;
}
else if (value)
{
VSyncMode = VSyncMode.Custom;
}
OnPropertyChanged();
}
}
public int CustomVSyncInterval
{
get => _customVSyncInterval;
set
{
_customVSyncInterval = value;
int newPercent = (int)((value / 60f) * 100);
_customVSyncIntervalPercentageProxy = newPercent;
OnPropertyChanged(nameof(CustomVSyncIntervalPercentageProxy));
OnPropertyChanged(nameof(CustomVSyncIntervalPercentageText));
OnPropertyChanged();
}
}
public bool EnablePptc { get; set; }
public bool EnableLowPowerPptc { get; set; }
public bool EnableInternetAccess { get; set; }
@ -484,7 +555,9 @@ namespace Ryujinx.Ava.UI.ViewModels
CurrentDate = currentDateTime.Date;
CurrentTime = currentDateTime.TimeOfDay;
EnableVsync = config.Graphics.EnableVsync;
EnableCustomVSyncInterval = config.Graphics.EnableCustomVSyncInterval.Value;
CustomVSyncInterval = config.Graphics.CustomVSyncInterval;
VSyncMode = config.Graphics.VSyncMode;
EnableFsIntegrityChecks = config.System.EnableFsIntegrityChecks;
DramSize = config.System.DramSize;
IgnoreMissingServices = config.System.IgnoreMissingServices;
@ -590,7 +663,9 @@ namespace Ryujinx.Ava.UI.ViewModels
}
config.System.SystemTimeOffset.Value = Convert.ToInt64((CurrentDate.ToUnixTimeSeconds() + CurrentTime.TotalSeconds) - DateTimeOffset.Now.ToUnixTimeSeconds());
config.Graphics.EnableVsync.Value = EnableVsync;
config.Graphics.VSyncMode.Value = VSyncMode;
config.Graphics.EnableCustomVSyncInterval.Value = EnableCustomVSyncInterval;
config.Graphics.CustomVSyncInterval.Value = CustomVSyncInterval;
config.System.EnableFsIntegrityChecks.Value = EnableFsIntegrityChecks;
config.System.DramSize.Value = DramSize;
config.System.IgnoreMissingServices.Value = IgnoreMissingServices;
@ -660,6 +735,7 @@ namespace Ryujinx.Ava.UI.ViewModels
config.ToFileFormat().SaveConfig(Program.ConfigurationPath);
MainWindow.UpdateGraphicsConfig();
MainWindow.MainWindowViewModel.VSyncModeSettingChanged();
SaveSettingsEvent?.Invoke();

View File

@ -58,7 +58,7 @@
<MenuItem Header="{ext:Locale MenuBarFileOpenApplet}" IsEnabled="{Binding IsAppletMenuActive}" Icon="{ext:Icon mdi-launch}">
<MenuItem
Click="OpenMiiApplet"
Header="Mii Edit Applet"
Header="{ext:Locale MenuBarFileOpenAppletOpenMiiApplet}"
Icon="{ext:Icon fa-solid fa-person}"
ToolTip.Tip="{ext:Locale MenuBarFileOpenAppletOpenMiiAppletToolTip}" />
</MenuItem>

View File

@ -79,15 +79,59 @@
MaxHeight="18"
Orientation="Horizontal">
<TextBlock
Name="VsyncStatus"
Name="VSyncMode"
Margin="5,0,5,0"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Foreground="{Binding VsyncColor}"
Foreground="{Binding VSyncModeColor}"
IsVisible="{Binding !ShowLoadProgress}"
PointerReleased="VsyncStatus_PointerReleased"
Text="VSync"
TextAlignment="Start" />
PointerReleased="VSyncMode_PointerReleased"
Text="{Binding VSyncModeText}"
TextAlignment="Start"/>
<Button MinWidth="0"
Width="20"
IsVisible="{Binding ShowCustomVSyncIntervalPicker}"
Margin="-5,0,5,0"
Background="Transparent"
BorderThickness="0">
<ui:SymbolIcon Symbol="Settings"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
Width="14"
Height="14"/>
<Button.Styles>
<Style Selector=":checked">
<Style Selector="^:checked ContentPresenter">
<Setter Property="Foreground" Value="{DynamicResource ThemeForegroundColor}" />
</Style>
</Style>
<Style Selector="Border#SeparatorBorder">
<Setter Property="Opacity" Value="0" />
</Style>
</Button.Styles>
<Button.Flyout>
<Flyout Placement="Top" ShowMode="TransientWithDismissOnPointerMoveAway">
<StackPanel Margin="0,0,0,0"
Orientation="Horizontal">
<Slider Value="{Binding CustomVSyncIntervalPercentageProxy}"
MinWidth="175"
Margin="0,-3,0,0"
Height="32"
Padding="0,-5"
TickFrequency="1"
IsSnapToTickEnabled="True"
LargeChange="10"
SmallChange="1"
VerticalAlignment="Center"
Minimum="10"
Maximum="400" />
<TextBlock Margin="5,0"
Width="40"
Text="{Binding CustomVSyncIntervalPercentageText}"/>
</StackPanel>
</Flyout>
</Button.Flyout>
</Button>
<Border
Width="2"
Height="12"

View File

@ -38,11 +38,10 @@ namespace Ryujinx.Ava.UI.Views.Main
}
}
private void VsyncStatus_PointerReleased(object sender, PointerReleasedEventArgs e)
private void VSyncMode_PointerReleased(object sender, PointerReleasedEventArgs e)
{
Window.ViewModel.AppHost.ToggleVSync();
Logger.Info?.Print(LogClass.Application, $"VSync toggled to: {Window.ViewModel.AppHost.Device.EnableDeviceVsync}");
Window.ViewModel.ToggleVSyncMode();
Logger.Info?.Print(LogClass.Application, $"VSync Mode toggled to: {Window.ViewModel.AppHost.Device.VSyncMode}");
}
private void DockedStatus_PointerReleased(object sender, PointerReleasedEventArgs e)

View File

@ -1,4 +1,4 @@
<UserControl
<UserControl
x:Class="Ryujinx.Ava.UI.Views.Settings.SettingsHotkeysView"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
@ -50,9 +50,9 @@
Classes="h1"
Text="{ext:Locale SettingsTabHotkeysHotkeys}" />
<StackPanel>
<TextBlock Text="{ext:Locale SettingsTabHotkeysToggleVsyncHotkey}" />
<ToggleButton Name="ToggleVsync">
<TextBlock Text="{Binding KeyboardHotkey.ToggleVsync, Converter={StaticResource Key}}" />
<TextBlock Text="{ext:Locale SettingsTabHotkeysToggleVSyncModeHotkey}" />
<ToggleButton Name="ToggleVSyncMode">
<TextBlock Text="{Binding KeyboardHotkey.ToggleVSyncMode, Converter={StaticResource Key}}" />
</ToggleButton>
</StackPanel>
<StackPanel>
@ -103,6 +103,18 @@
<TextBlock Text="{Binding KeyboardHotkey.VolumeDown, Converter={StaticResource Key}}" />
</ToggleButton>
</StackPanel>
<StackPanel Margin="10,0,0,0" Orientation="Horizontal">
<TextBlock Text="{ext:Locale SettingsTabHotkeysIncrementCustomVSyncIntervalHotkey}" />
<ToggleButton Name="CustomVSyncIntervalIncrement">
<TextBlock Text="{Binding KeyboardHotkey.CustomVSyncIntervalIncrement, Converter={StaticResource Key}}" />
</ToggleButton>
</StackPanel>
<StackPanel Margin="10,0,0,0" Orientation="Horizontal">
<TextBlock Text="{ext:Locale SettingsTabHotkeysDecrementCustomVSyncIntervalHotkey}" />
<ToggleButton Name="CustomVSyncIntervalDecrement">
<TextBlock Text="{Binding KeyboardHotkey.CustomVSyncIntervalDecrement, Converter={StaticResource Key}}" />
</ToggleButton>
</StackPanel>
</StackPanel>
</Border>
</ScrollViewer>

View File

@ -82,8 +82,8 @@ namespace Ryujinx.Ava.UI.Views.Settings
switch (button.Name)
{
case "ToggleVsync":
viewModel.KeyboardHotkey.ToggleVsync = buttonValue.AsHidType<Key>();
case "ToggleVSyncMode":
viewModel.KeyboardHotkey.ToggleVSyncMode = buttonValue.AsHidType<Key>();
break;
case "Screenshot":
viewModel.KeyboardHotkey.Screenshot = buttonValue.AsHidType<Key>();
@ -109,6 +109,12 @@ namespace Ryujinx.Ava.UI.Views.Settings
case "VolumeDown":
viewModel.KeyboardHotkey.VolumeDown = buttonValue.AsHidType<Key>();
break;
case "CustomVSyncIntervalIncrement":
viewModel.KeyboardHotkey.CustomVSyncIntervalIncrement = buttonValue.AsHidType<Key>();
break;
case "CustomVSyncIntervalDecrement":
viewModel.KeyboardHotkey.CustomVSyncIntervalDecrement = buttonValue.AsHidType<Key>();
break;
}
}
};

View File

@ -4,6 +4,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:ext="clr-namespace:Ryujinx.Ava.Common.Markup"
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers"
@ -181,11 +182,68 @@
Width="350"
ToolTip.Tip="{ext:Locale TimeTooltip}" />
</StackPanel>
<CheckBox IsChecked="{Binding EnableVsync}">
<StackPanel Margin="0,0,0,10"
Orientation="Horizontal">
<TextBlock
Text="{ext:Locale SettingsTabSystemEnableVsync}"
ToolTip.Tip="{ext:Locale VSyncToggleTooltip}" />
</CheckBox>
VerticalAlignment="Center"
Text="{ext:Locale SettingsTabSystemVSyncMode}"
ToolTip.Tip="{ext:Locale SettingsTabSystemVSyncModeTooltip}"
Width="250" />
<ComboBox
IsVisible="{Binding EnableCustomVSyncInterval}"
SelectedIndex="{Binding VSyncMode}"
ToolTip.Tip="{ext:Locale SettingsTabSystemVSyncModeTooltipCustom}"
HorizontalContentAlignment="Left"
Width="350">
<ComboBoxItem>
<TextBlock Text="{ext:Locale SettingsTabSystemVSyncModeSwitch}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{ext:Locale SettingsTabSystemVSyncModeUnbounded}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{ext:Locale SettingsTabSystemVSyncModeCustom}" />
</ComboBoxItem>
</ComboBox>
<ComboBox
IsVisible="{Binding !EnableCustomVSyncInterval}"
SelectedIndex="{Binding VSyncMode}"
ToolTip.Tip="{ext:Locale SettingsTabSystemVSyncModeTooltip}"
HorizontalContentAlignment="Left"
Width="350">
<ComboBoxItem>
<TextBlock Text="{ext:Locale SettingsTabSystemVSyncModeSwitch}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{ext:Locale SettingsTabSystemVSyncModeUnbounded}" />
</ComboBoxItem>
</ComboBox>
</StackPanel>
<StackPanel IsVisible="{Binding EnableCustomVSyncInterval}"
Margin="0,0,0,10"
Orientation="Horizontal">
<TextBlock
VerticalAlignment="Center"
Text="{ext:Locale SettingsTabSystemCustomVSyncIntervalPercentage}"
ToolTip.Tip="{ext:Locale SettingsTabSystemCustomVSyncIntervalValueTooltip}"
Width="250" />
<Slider Value="{Binding CustomVSyncIntervalPercentageProxy}"
ToolTip.Tip="{ext:Locale SettingsTabSystemCustomVSyncIntervalSliderTooltip}"
MinWidth="175"
Margin="10,-3,0,0"
Height="32"
Padding="0,-5"
TickFrequency="1"
IsSnapToTickEnabled="True"
LargeChange="10"
SmallChange="1"
VerticalAlignment="Center"
Minimum="10"
Maximum="400" />
<TextBlock Margin="5,0"
Width="40"
Text="{Binding CustomVSyncIntervalPercentageText}"/>
</StackPanel>
<CheckBox IsChecked="{Binding EnableFsIntegrityChecks}">
<TextBlock
Text="{ext:Locale SettingsTabSystemEnableFsIntegrityChecks}"
@ -244,6 +302,11 @@
ToolTip.Tip="{ext:Locale IgnoreAppletTooltip}">
<TextBlock Text="{ext:Locale SettingsTabSystemIgnoreApplet}" />
</CheckBox>
<CheckBox
IsChecked="{Binding EnableCustomVSyncInterval}"
ToolTip.Tip="{ext:Locale SettingsTabSystemEnableCustomVSyncIntervalTooltip}">
<TextBlock Text="{ext:Locale SettingsTabSystemEnableCustomVSyncInterval}" />
</CheckBox>
</StackPanel>
</StackPanel>
</Border>

View File

@ -6,8 +6,10 @@
xmlns:ext="clr-namespace:Ryujinx.Ava.Common.Markup"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:viewModel="clr-namespace:Ryujinx.Ava.UI.ViewModels"
Width="550"
Height="260"
MinWidth="550"
MinHeight="260"
MaxWidth="600"
MaxHeight="500"
Margin="0,-12,0,0"
d:DesignHeight="260"
d:DesignWidth="550"

View File

@ -38,6 +38,8 @@ namespace Ryujinx.Ava.UI.Windows
{
public partial class MainWindow : StyleableAppWindow
{
internal static MainWindowViewModel MainWindowViewModel { get; private set; }
public MainWindowViewModel ViewModel { get; }
internal readonly AvaHostUIHandler UiHandler;
@ -73,7 +75,7 @@ namespace Ryujinx.Ava.UI.Windows
public MainWindow()
{
DataContext = ViewModel = new MainWindowViewModel
DataContext = ViewModel = MainWindowViewModel = new MainWindowViewModel
{
Window = this
};

View File

@ -114,9 +114,14 @@ namespace Ryujinx.Ava
{
if (showVersionUpToDate)
{
await ContentDialogHelper.CreateUpdaterInfoDialog(
UserResult userResult = await ContentDialogHelper.CreateUpdaterUpToDateInfoDialog(
LocaleManager.Instance[LocaleKeys.DialogUpdaterAlreadyOnLatestVersionMessage],
string.Empty);
if (userResult is UserResult.Ok)
{
OpenHelper.OpenUrl(ReleaseInformation.GetChangelogForVersion(currentVersion));
}
}
_running = false;
@ -133,9 +138,14 @@ namespace Ryujinx.Ava
{
if (showVersionUpToDate)
{
await ContentDialogHelper.CreateUpdaterInfoDialog(
UserResult userResult = await ContentDialogHelper.CreateUpdaterUpToDateInfoDialog(
LocaleManager.Instance[LocaleKeys.DialogUpdaterAlreadyOnLatestVersionMessage],
string.Empty);
if (userResult is UserResult.Ok)
{
OpenHelper.OpenUrl(ReleaseInformation.GetChangelogForVersion(currentVersion));
}
}
_running = false;
@ -176,9 +186,14 @@ namespace Ryujinx.Ava
{
if (showVersionUpToDate)
{
await ContentDialogHelper.CreateUpdaterInfoDialog(
UserResult userResult = await ContentDialogHelper.CreateUpdaterUpToDateInfoDialog(
LocaleManager.Instance[LocaleKeys.DialogUpdaterAlreadyOnLatestVersionMessage],
string.Empty);
if (userResult is UserResult.Ok)
{
OpenHelper.OpenUrl(ReleaseInformation.GetChangelogForVersion(currentVersion));
}
}
_running = false;
@ -206,19 +221,29 @@ namespace Ryujinx.Ava
await Dispatcher.UIThread.InvokeAsync(async () =>
{
string newVersionString = ReleaseInformation.IsCanaryBuild
? $"Canary {currentVersion} -> Canary {newVersion}"
: $"{currentVersion} -> {newVersion}";
RequestUserToUpdate:
// Show a message asking the user if they want to update
var shouldUpdate = await ContentDialogHelper.CreateChoiceDialog(
UserResult shouldUpdate = await ContentDialogHelper.CreateUpdaterChoiceDialog(
LocaleManager.Instance[LocaleKeys.RyujinxUpdater],
LocaleManager.Instance[LocaleKeys.RyujinxUpdaterMessage],
$"{Program.Version} -> {newVersion}");
newVersionString);
if (shouldUpdate)
switch (shouldUpdate)
{
await UpdateRyujinx(mainWindow, _buildUrl);
}
else
{
_running = false;
case UserResult.Yes:
await UpdateRyujinx(mainWindow, _buildUrl);
break;
// Secondary button maps to no, which in this case is the show changelog button.
case UserResult.No:
OpenHelper.OpenUrl(ReleaseInformation.GetChangelogUrl(currentVersion, newVersion));
goto RequestUserToUpdate;
default:
_running = false;
break;
}
});
}