Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
14ce9e1567 | ||
|
|
952d013c67 | ||
|
|
46c8129bf5 | ||
|
|
8cfec5de4b | ||
|
|
37b6e081da | ||
|
|
3c3bcd82fe | ||
|
|
a00c59a46c | ||
|
|
1825bd87b4 | ||
|
|
62f8ceb60b | ||
|
|
1a888ae087 | ||
|
|
84d0ca5645 | ||
|
|
31b8d413d5 |
@@ -197,12 +197,29 @@ namespace ARMeilleure.Signal
|
|||||||
// Only call tracking if in range.
|
// Only call tracking if in range.
|
||||||
context.BranchIfFalse(nextLabel, inRange, BasicBlockFrequency.Cold);
|
context.BranchIfFalse(nextLabel, inRange, BasicBlockFrequency.Cold);
|
||||||
|
|
||||||
context.Copy(inRegionLocal, Const(1));
|
|
||||||
Operand offset = context.BitwiseAnd(context.Subtract(faultAddress, rangeAddress), Const(~PageMask));
|
Operand offset = context.BitwiseAnd(context.Subtract(faultAddress, rangeAddress), Const(~PageMask));
|
||||||
|
|
||||||
// Call the tracking action, with the pointer's relative offset to the base address.
|
// Call the tracking action, with the pointer's relative offset to the base address.
|
||||||
Operand trackingActionPtr = context.Load(OperandType.I64, Const((ulong)signalStructPtr + rangeBaseOffset + 20));
|
Operand trackingActionPtr = context.Load(OperandType.I64, Const((ulong)signalStructPtr + rangeBaseOffset + 20));
|
||||||
context.Call(trackingActionPtr, OperandType.I32, offset, Const(PageSize), isWrite, Const(0));
|
|
||||||
|
context.Copy(inRegionLocal, Const(0));
|
||||||
|
|
||||||
|
Operand skipActionLabel = Label();
|
||||||
|
|
||||||
|
// Tracking action should be non-null to call it, otherwise assume false return.
|
||||||
|
context.BranchIfFalse(skipActionLabel, trackingActionPtr);
|
||||||
|
Operand result = context.Call(trackingActionPtr, OperandType.I32, offset, Const(PageSize), isWrite, Const(0));
|
||||||
|
context.Copy(inRegionLocal, result);
|
||||||
|
|
||||||
|
context.MarkLabel(skipActionLabel);
|
||||||
|
|
||||||
|
// If the tracking action returns false or does not exist, it might be an invalid access due to a partial overlap on Windows.
|
||||||
|
if (OperatingSystem.IsWindows())
|
||||||
|
{
|
||||||
|
context.BranchIfTrue(endLabel, inRegionLocal);
|
||||||
|
|
||||||
|
context.Copy(inRegionLocal, WindowsPartialUnmapHandler.EmitRetryFromAccessViolation(context));
|
||||||
|
}
|
||||||
|
|
||||||
context.Branch(endLabel);
|
context.Branch(endLabel);
|
||||||
|
|
||||||
|
|||||||
84
ARMeilleure/Signal/TestMethods.cs
Normal file
84
ARMeilleure/Signal/TestMethods.cs
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
using ARMeilleure.IntermediateRepresentation;
|
||||||
|
using ARMeilleure.Translation;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
using static ARMeilleure.IntermediateRepresentation.Operand.Factory;
|
||||||
|
|
||||||
|
namespace ARMeilleure.Signal
|
||||||
|
{
|
||||||
|
public struct NativeWriteLoopState
|
||||||
|
{
|
||||||
|
public int Running;
|
||||||
|
public int Error;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class TestMethods
|
||||||
|
{
|
||||||
|
public delegate bool DebugPartialUnmap();
|
||||||
|
public delegate int DebugThreadLocalMapGetOrReserve(int threadId, int initialState);
|
||||||
|
public delegate void DebugNativeWriteLoop(IntPtr nativeWriteLoopPtr, IntPtr writePtr);
|
||||||
|
|
||||||
|
public static DebugPartialUnmap GenerateDebugPartialUnmap()
|
||||||
|
{
|
||||||
|
EmitterContext context = new EmitterContext();
|
||||||
|
|
||||||
|
var result = WindowsPartialUnmapHandler.EmitRetryFromAccessViolation(context);
|
||||||
|
|
||||||
|
context.Return(result);
|
||||||
|
|
||||||
|
// Compile and return the function.
|
||||||
|
|
||||||
|
ControlFlowGraph cfg = context.GetControlFlowGraph();
|
||||||
|
|
||||||
|
OperandType[] argTypes = new OperandType[] { OperandType.I64 };
|
||||||
|
|
||||||
|
return Compiler.Compile(cfg, argTypes, OperandType.I32, CompilerOptions.HighCq).Map<DebugPartialUnmap>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static DebugThreadLocalMapGetOrReserve GenerateDebugThreadLocalMapGetOrReserve(IntPtr structPtr)
|
||||||
|
{
|
||||||
|
EmitterContext context = new EmitterContext();
|
||||||
|
|
||||||
|
var result = WindowsPartialUnmapHandler.EmitThreadLocalMapIntGetOrReserve(context, structPtr, context.LoadArgument(OperandType.I32, 0), context.LoadArgument(OperandType.I32, 1));
|
||||||
|
|
||||||
|
context.Return(result);
|
||||||
|
|
||||||
|
// Compile and return the function.
|
||||||
|
|
||||||
|
ControlFlowGraph cfg = context.GetControlFlowGraph();
|
||||||
|
|
||||||
|
OperandType[] argTypes = new OperandType[] { OperandType.I64 };
|
||||||
|
|
||||||
|
return Compiler.Compile(cfg, argTypes, OperandType.I32, CompilerOptions.HighCq).Map<DebugThreadLocalMapGetOrReserve>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static DebugNativeWriteLoop GenerateDebugNativeWriteLoop()
|
||||||
|
{
|
||||||
|
EmitterContext context = new EmitterContext();
|
||||||
|
|
||||||
|
// Loop a write to the target address until "running" is false.
|
||||||
|
|
||||||
|
Operand structPtr = context.Copy(context.LoadArgument(OperandType.I64, 0));
|
||||||
|
Operand writePtr = context.Copy(context.LoadArgument(OperandType.I64, 1));
|
||||||
|
|
||||||
|
Operand loopLabel = Label();
|
||||||
|
context.MarkLabel(loopLabel);
|
||||||
|
|
||||||
|
context.Store(writePtr, Const(12345));
|
||||||
|
|
||||||
|
Operand running = context.Load(OperandType.I32, structPtr);
|
||||||
|
|
||||||
|
context.BranchIfTrue(loopLabel, running);
|
||||||
|
|
||||||
|
context.Return();
|
||||||
|
|
||||||
|
// Compile and return the function.
|
||||||
|
|
||||||
|
ControlFlowGraph cfg = context.GetControlFlowGraph();
|
||||||
|
|
||||||
|
OperandType[] argTypes = new OperandType[] { OperandType.I64 };
|
||||||
|
|
||||||
|
return Compiler.Compile(cfg, argTypes, OperandType.None, CompilerOptions.HighCq).Map<DebugNativeWriteLoop>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
186
ARMeilleure/Signal/WindowsPartialUnmapHandler.cs
Normal file
186
ARMeilleure/Signal/WindowsPartialUnmapHandler.cs
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
using ARMeilleure.IntermediateRepresentation;
|
||||||
|
using ARMeilleure.Translation;
|
||||||
|
using Ryujinx.Common.Memory.PartialUnmaps;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
using static ARMeilleure.IntermediateRepresentation.Operand.Factory;
|
||||||
|
|
||||||
|
namespace ARMeilleure.Signal
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Methods to handle signals caused by partial unmaps. See the structs for C# implementations of the methods.
|
||||||
|
/// </summary>
|
||||||
|
internal static class WindowsPartialUnmapHandler
|
||||||
|
{
|
||||||
|
public static Operand EmitRetryFromAccessViolation(EmitterContext context)
|
||||||
|
{
|
||||||
|
IntPtr partialRemapStatePtr = PartialUnmapState.GlobalState;
|
||||||
|
IntPtr localCountsPtr = IntPtr.Add(partialRemapStatePtr, PartialUnmapState.LocalCountsOffset);
|
||||||
|
|
||||||
|
// Get the lock first.
|
||||||
|
EmitNativeReaderLockAcquire(context, IntPtr.Add(partialRemapStatePtr, PartialUnmapState.PartialUnmapLockOffset));
|
||||||
|
|
||||||
|
IntPtr getCurrentThreadId = WindowsSignalHandlerRegistration.GetCurrentThreadIdFunc();
|
||||||
|
Operand threadId = context.Call(Const((ulong)getCurrentThreadId), OperandType.I32);
|
||||||
|
Operand threadIndex = EmitThreadLocalMapIntGetOrReserve(context, localCountsPtr, threadId, Const(0));
|
||||||
|
|
||||||
|
Operand endLabel = Label();
|
||||||
|
Operand retry = context.AllocateLocal(OperandType.I32);
|
||||||
|
Operand threadIndexValidLabel = Label();
|
||||||
|
|
||||||
|
context.BranchIfFalse(threadIndexValidLabel, context.ICompareEqual(threadIndex, Const(-1)));
|
||||||
|
|
||||||
|
context.Copy(retry, Const(1)); // Always retry when thread local cannot be allocated.
|
||||||
|
|
||||||
|
context.Branch(endLabel);
|
||||||
|
|
||||||
|
context.MarkLabel(threadIndexValidLabel);
|
||||||
|
|
||||||
|
Operand threadLocalPartialUnmapsPtr = EmitThreadLocalMapIntGetValuePtr(context, localCountsPtr, threadIndex);
|
||||||
|
Operand threadLocalPartialUnmaps = context.Load(OperandType.I32, threadLocalPartialUnmapsPtr);
|
||||||
|
Operand partialUnmapsCount = context.Load(OperandType.I32, Const((ulong)IntPtr.Add(partialRemapStatePtr, PartialUnmapState.PartialUnmapsCountOffset)));
|
||||||
|
|
||||||
|
context.Copy(retry, context.ICompareNotEqual(threadLocalPartialUnmaps, partialUnmapsCount));
|
||||||
|
|
||||||
|
Operand noRetryLabel = Label();
|
||||||
|
|
||||||
|
context.BranchIfFalse(noRetryLabel, retry);
|
||||||
|
|
||||||
|
// if (retry) {
|
||||||
|
|
||||||
|
context.Store(threadLocalPartialUnmapsPtr, partialUnmapsCount);
|
||||||
|
|
||||||
|
context.Branch(endLabel);
|
||||||
|
|
||||||
|
context.MarkLabel(noRetryLabel);
|
||||||
|
|
||||||
|
// }
|
||||||
|
|
||||||
|
context.MarkLabel(endLabel);
|
||||||
|
|
||||||
|
// Finally, release the lock and return the retry value.
|
||||||
|
EmitNativeReaderLockRelease(context, IntPtr.Add(partialRemapStatePtr, PartialUnmapState.PartialUnmapLockOffset));
|
||||||
|
|
||||||
|
return retry;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Operand EmitThreadLocalMapIntGetOrReserve(EmitterContext context, IntPtr threadLocalMapPtr, Operand threadId, Operand initialState)
|
||||||
|
{
|
||||||
|
Operand idsPtr = Const((ulong)IntPtr.Add(threadLocalMapPtr, ThreadLocalMap<int>.ThreadIdsOffset));
|
||||||
|
|
||||||
|
Operand i = context.AllocateLocal(OperandType.I32);
|
||||||
|
|
||||||
|
context.Copy(i, Const(0));
|
||||||
|
|
||||||
|
// (Loop 1) Check all slots for a matching Thread ID (while also trying to allocate)
|
||||||
|
|
||||||
|
Operand endLabel = Label();
|
||||||
|
|
||||||
|
Operand loopLabel = Label();
|
||||||
|
context.MarkLabel(loopLabel);
|
||||||
|
|
||||||
|
Operand offset = context.Multiply(i, Const(sizeof(int)));
|
||||||
|
Operand idPtr = context.Add(idsPtr, context.SignExtend32(OperandType.I64, offset));
|
||||||
|
|
||||||
|
// Check that this slot has the thread ID.
|
||||||
|
Operand existingId = context.CompareAndSwap(idPtr, threadId, threadId);
|
||||||
|
|
||||||
|
// If it was already the thread ID, then we just need to return i.
|
||||||
|
context.BranchIfTrue(endLabel, context.ICompareEqual(existingId, threadId));
|
||||||
|
|
||||||
|
context.Copy(i, context.Add(i, Const(1)));
|
||||||
|
|
||||||
|
context.BranchIfTrue(loopLabel, context.ICompareLess(i, Const(ThreadLocalMap<int>.MapSize)));
|
||||||
|
|
||||||
|
// (Loop 2) Try take a slot that is 0 with our Thread ID.
|
||||||
|
|
||||||
|
context.Copy(i, Const(0)); // Reset i.
|
||||||
|
|
||||||
|
Operand loop2Label = Label();
|
||||||
|
context.MarkLabel(loop2Label);
|
||||||
|
|
||||||
|
Operand offset2 = context.Multiply(i, Const(sizeof(int)));
|
||||||
|
Operand idPtr2 = context.Add(idsPtr, context.SignExtend32(OperandType.I64, offset2));
|
||||||
|
|
||||||
|
// Try and swap in the thread id on top of 0.
|
||||||
|
Operand existingId2 = context.CompareAndSwap(idPtr2, Const(0), threadId);
|
||||||
|
|
||||||
|
Operand idNot0Label = Label();
|
||||||
|
|
||||||
|
// If it was 0, then we need to initialize the struct entry and return i.
|
||||||
|
context.BranchIfFalse(idNot0Label, context.ICompareEqual(existingId2, Const(0)));
|
||||||
|
|
||||||
|
Operand structsPtr = Const((ulong)IntPtr.Add(threadLocalMapPtr, ThreadLocalMap<int>.StructsOffset));
|
||||||
|
Operand structPtr = context.Add(structsPtr, context.SignExtend32(OperandType.I64, offset2));
|
||||||
|
context.Store(structPtr, initialState);
|
||||||
|
|
||||||
|
context.Branch(endLabel);
|
||||||
|
|
||||||
|
context.MarkLabel(idNot0Label);
|
||||||
|
|
||||||
|
context.Copy(i, context.Add(i, Const(1)));
|
||||||
|
|
||||||
|
context.BranchIfTrue(loop2Label, context.ICompareLess(i, Const(ThreadLocalMap<int>.MapSize)));
|
||||||
|
|
||||||
|
context.Copy(i, Const(-1)); // Could not place the thread in the list.
|
||||||
|
|
||||||
|
context.MarkLabel(endLabel);
|
||||||
|
|
||||||
|
return context.Copy(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Operand EmitThreadLocalMapIntGetValuePtr(EmitterContext context, IntPtr threadLocalMapPtr, Operand index)
|
||||||
|
{
|
||||||
|
Operand offset = context.Multiply(index, Const(sizeof(int)));
|
||||||
|
Operand structsPtr = Const((ulong)IntPtr.Add(threadLocalMapPtr, ThreadLocalMap<int>.StructsOffset));
|
||||||
|
|
||||||
|
return context.Add(structsPtr, context.SignExtend32(OperandType.I64, offset));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void EmitThreadLocalMapIntRelease(EmitterContext context, IntPtr threadLocalMapPtr, Operand threadId, Operand index)
|
||||||
|
{
|
||||||
|
Operand offset = context.Multiply(index, Const(sizeof(int)));
|
||||||
|
Operand idsPtr = Const((ulong)IntPtr.Add(threadLocalMapPtr, ThreadLocalMap<int>.ThreadIdsOffset));
|
||||||
|
Operand idPtr = context.Add(idsPtr, context.SignExtend32(OperandType.I64, offset));
|
||||||
|
|
||||||
|
context.CompareAndSwap(idPtr, threadId, Const(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void EmitAtomicAddI32(EmitterContext context, Operand ptr, Operand additive)
|
||||||
|
{
|
||||||
|
Operand loop = Label();
|
||||||
|
context.MarkLabel(loop);
|
||||||
|
|
||||||
|
Operand initial = context.Load(OperandType.I32, ptr);
|
||||||
|
Operand newValue = context.Add(initial, additive);
|
||||||
|
|
||||||
|
Operand replaced = context.CompareAndSwap(ptr, initial, newValue);
|
||||||
|
|
||||||
|
context.BranchIfFalse(loop, context.ICompareEqual(initial, replaced));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void EmitNativeReaderLockAcquire(EmitterContext context, IntPtr nativeReaderLockPtr)
|
||||||
|
{
|
||||||
|
Operand writeLockPtr = Const((ulong)IntPtr.Add(nativeReaderLockPtr, NativeReaderWriterLock.WriteLockOffset));
|
||||||
|
|
||||||
|
// Spin until we can acquire the write lock.
|
||||||
|
Operand spinLabel = Label();
|
||||||
|
context.MarkLabel(spinLabel);
|
||||||
|
|
||||||
|
// Old value must be 0 to continue (we gained the write lock)
|
||||||
|
context.BranchIfTrue(spinLabel, context.CompareAndSwap(writeLockPtr, Const(0), Const(1)));
|
||||||
|
|
||||||
|
// Increment reader count.
|
||||||
|
EmitAtomicAddI32(context, Const((ulong)IntPtr.Add(nativeReaderLockPtr, NativeReaderWriterLock.ReaderCountOffset)), Const(1));
|
||||||
|
|
||||||
|
// Release write lock.
|
||||||
|
context.CompareAndSwap(writeLockPtr, Const(1), Const(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void EmitNativeReaderLockRelease(EmitterContext context, IntPtr nativeReaderLockPtr)
|
||||||
|
{
|
||||||
|
// Decrement reader count.
|
||||||
|
EmitAtomicAddI32(context, Const((ulong)IntPtr.Add(nativeReaderLockPtr, NativeReaderWriterLock.ReaderCountOffset)), Const(-1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
namespace ARMeilleure.Signal
|
namespace ARMeilleure.Signal
|
||||||
{
|
{
|
||||||
class WindowsSignalHandlerRegistration
|
unsafe class WindowsSignalHandlerRegistration
|
||||||
{
|
{
|
||||||
[DllImport("kernel32.dll")]
|
[DllImport("kernel32.dll")]
|
||||||
private static extern IntPtr AddVectoredExceptionHandler(uint first, IntPtr handler);
|
private static extern IntPtr AddVectoredExceptionHandler(uint first, IntPtr handler);
|
||||||
@@ -11,6 +12,14 @@ namespace ARMeilleure.Signal
|
|||||||
[DllImport("kernel32.dll")]
|
[DllImport("kernel32.dll")]
|
||||||
private static extern ulong RemoveVectoredExceptionHandler(IntPtr handle);
|
private static extern ulong RemoveVectoredExceptionHandler(IntPtr handle);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Ansi)]
|
||||||
|
static extern IntPtr LoadLibrary([MarshalAs(UnmanagedType.LPStr)] string lpFileName);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", CharSet = CharSet.Ansi, ExactSpelling = true, SetLastError = true)]
|
||||||
|
private static extern IntPtr GetProcAddress(IntPtr hModule, string procName);
|
||||||
|
|
||||||
|
private static IntPtr _getCurrentThreadIdPtr;
|
||||||
|
|
||||||
public static IntPtr RegisterExceptionHandler(IntPtr action)
|
public static IntPtr RegisterExceptionHandler(IntPtr action)
|
||||||
{
|
{
|
||||||
return AddVectoredExceptionHandler(1, action);
|
return AddVectoredExceptionHandler(1, action);
|
||||||
@@ -20,5 +29,17 @@ namespace ARMeilleure.Signal
|
|||||||
{
|
{
|
||||||
return RemoveVectoredExceptionHandler(handle) != 0;
|
return RemoveVectoredExceptionHandler(handle) != 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static IntPtr GetCurrentThreadIdFunc()
|
||||||
|
{
|
||||||
|
if (_getCurrentThreadIdPtr == IntPtr.Zero)
|
||||||
|
{
|
||||||
|
IntPtr handle = LoadLibrary("kernel32.dll");
|
||||||
|
|
||||||
|
_getCurrentThreadIdPtr = GetProcAddress(handle, "GetCurrentThreadId");
|
||||||
|
}
|
||||||
|
|
||||||
|
return _getCurrentThreadIdPtr;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
using DspAddress = System.UInt64;
|
|
||||||
using CpuAddress = System.UInt64;
|
using CpuAddress = System.UInt64;
|
||||||
|
using DspAddress = System.UInt64;
|
||||||
|
|
||||||
namespace Ryujinx.Audio.Renderer.Server.MemoryPool
|
namespace Ryujinx.Audio.Renderer.Server.MemoryPool
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
using DspAddress = System.UInt64;
|
|
||||||
using CpuAddress = System.UInt64;
|
using CpuAddress = System.UInt64;
|
||||||
|
using DspAddress = System.UInt64;
|
||||||
|
|
||||||
namespace Ryujinx.Audio.Renderer.Server.MemoryPool
|
namespace Ryujinx.Audio.Renderer.Server.MemoryPool
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
using DspAddress = System.UInt64;
|
|
||||||
using CpuAddress = System.UInt64;
|
using CpuAddress = System.UInt64;
|
||||||
|
using DspAddress = System.UInt64;
|
||||||
|
|
||||||
namespace Ryujinx.Audio.Renderer.Utils
|
namespace Ryujinx.Audio.Renderer.Utils
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ namespace Ryujinx.Ava
|
|||||||
internal class AppHost
|
internal class AppHost
|
||||||
{
|
{
|
||||||
private const int CursorHideIdleTime = 8; // Hide Cursor seconds
|
private const int CursorHideIdleTime = 8; // Hide Cursor seconds
|
||||||
|
private const float MaxResolutionScale = 4.0f; // Max resolution hotkeys can scale to before wrapping.
|
||||||
|
|
||||||
private static readonly Cursor InvisibleCursor = new Cursor(StandardCursorType.None);
|
private static readonly Cursor InvisibleCursor = new Cursor(StandardCursorType.None);
|
||||||
|
|
||||||
@@ -976,6 +977,13 @@ namespace Ryujinx.Ava
|
|||||||
|
|
||||||
_parent.ViewModel.Volume = Device.GetVolume();
|
_parent.ViewModel.Volume = Device.GetVolume();
|
||||||
break;
|
break;
|
||||||
|
case KeyboardHotkeyState.ResScaleUp:
|
||||||
|
GraphicsConfig.ResScale = GraphicsConfig.ResScale % MaxResolutionScale + 1;
|
||||||
|
break;
|
||||||
|
case KeyboardHotkeyState.ResScaleDown:
|
||||||
|
GraphicsConfig.ResScale =
|
||||||
|
(MaxResolutionScale + GraphicsConfig.ResScale - 2) % MaxResolutionScale + 1;
|
||||||
|
break;
|
||||||
case KeyboardHotkeyState.None:
|
case KeyboardHotkeyState.None:
|
||||||
(_keyboardInterface as AvaloniaKeyboard).Clear();
|
(_keyboardInterface as AvaloniaKeyboard).Clear();
|
||||||
break;
|
break;
|
||||||
@@ -1033,6 +1041,14 @@ namespace Ryujinx.Ava
|
|||||||
{
|
{
|
||||||
state = KeyboardHotkeyState.ToggleMute;
|
state = KeyboardHotkeyState.ToggleMute;
|
||||||
}
|
}
|
||||||
|
else if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.ResScaleUp))
|
||||||
|
{
|
||||||
|
state = KeyboardHotkeyState.ResScaleUp;
|
||||||
|
}
|
||||||
|
else if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.ResScaleDown))
|
||||||
|
{
|
||||||
|
state = KeyboardHotkeyState.ResScaleDown;
|
||||||
|
}
|
||||||
|
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -118,7 +118,7 @@
|
|||||||
"SettingsTabSystemAudioBackendSoundIO": "SoundIO",
|
"SettingsTabSystemAudioBackendSoundIO": "SoundIO",
|
||||||
"SettingsTabSystemAudioBackendSDL2": "SDL2",
|
"SettingsTabSystemAudioBackendSDL2": "SDL2",
|
||||||
"SettingsTabSystemHacks": "Hacks",
|
"SettingsTabSystemHacks": "Hacks",
|
||||||
"SettingsTabSystemHacksNote": " - Können Fehler verursachen",
|
"SettingsTabSystemHacksNote": " (Können Fehler verursachen)",
|
||||||
"SettingsTabSystemExpandDramSize": "Erweitere DRAM Größe auf 6GB",
|
"SettingsTabSystemExpandDramSize": "Erweitere DRAM Größe auf 6GB",
|
||||||
"SettingsTabSystemIgnoreMissingServices": "Ignoriere fehlende Dienste",
|
"SettingsTabSystemIgnoreMissingServices": "Ignoriere fehlende Dienste",
|
||||||
"SettingsTabGraphics": "Grafik",
|
"SettingsTabGraphics": "Grafik",
|
||||||
@@ -556,5 +556,7 @@
|
|||||||
"SettingsSelectThemeFileDialogTitle" : "Wähle ein benutzerdefiniertes Thema",
|
"SettingsSelectThemeFileDialogTitle" : "Wähle ein benutzerdefiniertes Thema",
|
||||||
"SettingsXamlThemeFile" : "Xaml Thema-Datei",
|
"SettingsXamlThemeFile" : "Xaml Thema-Datei",
|
||||||
"SettingsTabGraphicsBackend" : "Grafik-Backend",
|
"SettingsTabGraphicsBackend" : "Grafik-Backend",
|
||||||
"GraphicsBackendTooltip" : "Ändert das Grafik-Backend"
|
"GraphicsBackendTooltip" : "Ändert das Grafik-Backend",
|
||||||
|
"SettingsTabHotkeysResScaleUpHotkey": "Auflösung erhöhen:",
|
||||||
|
"SettingsTabHotkeysResScaleDownHotkey": "Auflösung vermindern:"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -118,7 +118,7 @@
|
|||||||
"SettingsTabSystemAudioBackendSoundIO": "SoundIO",
|
"SettingsTabSystemAudioBackendSoundIO": "SoundIO",
|
||||||
"SettingsTabSystemAudioBackendSDL2": "SDL2",
|
"SettingsTabSystemAudioBackendSDL2": "SDL2",
|
||||||
"SettingsTabSystemHacks": "Μικροδιορθώσεις",
|
"SettingsTabSystemHacks": "Μικροδιορθώσεις",
|
||||||
"SettingsTabSystemHacksNote": " - Μπορεί να προκαλέσουν αστάθεια",
|
"SettingsTabSystemHacksNote": " (Μπορεί να προκαλέσουν αστάθεια)",
|
||||||
"SettingsTabSystemExpandDramSize": "Επέκταση μεγέθους DRAM στα 6GB",
|
"SettingsTabSystemExpandDramSize": "Επέκταση μεγέθους DRAM στα 6GB",
|
||||||
"SettingsTabSystemIgnoreMissingServices": "Αγνόηση υπηρεσιών που λείπουν",
|
"SettingsTabSystemIgnoreMissingServices": "Αγνόηση υπηρεσιών που λείπουν",
|
||||||
"SettingsTabGraphics": "Γραφικά",
|
"SettingsTabGraphics": "Γραφικά",
|
||||||
|
|||||||
@@ -575,5 +575,9 @@
|
|||||||
"UserProfilesSetProfileImage": "Set Profile Image",
|
"UserProfilesSetProfileImage": "Set Profile Image",
|
||||||
"UserProfileEmptyNameError": "Name is required",
|
"UserProfileEmptyNameError": "Name is required",
|
||||||
"UserProfileNoImageError": "Profile image must be set",
|
"UserProfileNoImageError": "Profile image must be set",
|
||||||
"GameUpdateWindowHeading": "Updates Available for {0} [{1}]"
|
"GameUpdateWindowHeading": "Updates Available for {0} [{1}]",
|
||||||
|
"SettingsTabHotkeysResScaleUpHotkey": "Increase resolution:",
|
||||||
|
"SettingsTabHotkeysResScaleDownHotkey": "Decrease resolution:",
|
||||||
|
"UserProfilesName": "Name:",
|
||||||
|
"UserProfilesUserId" : "User Id:"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -118,7 +118,7 @@
|
|||||||
"SettingsTabSystemAudioBackendSoundIO": "SoundIO",
|
"SettingsTabSystemAudioBackendSoundIO": "SoundIO",
|
||||||
"SettingsTabSystemAudioBackendSDL2": "SDL2",
|
"SettingsTabSystemAudioBackendSDL2": "SDL2",
|
||||||
"SettingsTabSystemHacks": "Hacks",
|
"SettingsTabSystemHacks": "Hacks",
|
||||||
"SettingsTabSystemHacksNote": " - Pueden causar inestabilidad",
|
"SettingsTabSystemHacksNote": " (Pueden causar inestabilidad)",
|
||||||
"SettingsTabSystemExpandDramSize": "Expandir DRAM a 6GB",
|
"SettingsTabSystemExpandDramSize": "Expandir DRAM a 6GB",
|
||||||
"SettingsTabSystemIgnoreMissingServices": "Ignorar servicios no implementados",
|
"SettingsTabSystemIgnoreMissingServices": "Ignorar servicios no implementados",
|
||||||
"SettingsTabGraphics": "Gráficos",
|
"SettingsTabGraphics": "Gráficos",
|
||||||
@@ -568,5 +568,7 @@
|
|||||||
"UpdateWindowTitle": "Administrar actualizaciones",
|
"UpdateWindowTitle": "Administrar actualizaciones",
|
||||||
"CheatWindowHeading": "Cheats disponibles para {0} [{1}]",
|
"CheatWindowHeading": "Cheats disponibles para {0} [{1}]",
|
||||||
"DlcWindowHeading": "Contenido descargable disponible para {0} [{1}]",
|
"DlcWindowHeading": "Contenido descargable disponible para {0} [{1}]",
|
||||||
"GameUpdateWindowHeading": "Actualizaciones disponibles para {0} [{1}]"
|
"GameUpdateWindowHeading": "Actualizaciones disponibles para {0} [{1}]",
|
||||||
|
"SettingsTabHotkeysResScaleUpHotkey": "Aumentar la resolución:",
|
||||||
|
"SettingsTabHotkeysResScaleDownHotkey": "Disminuir la resolución:"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -111,7 +111,7 @@
|
|||||||
"SettingsTabSystemAudioBackendSoundIO": "SoundIO",
|
"SettingsTabSystemAudioBackendSoundIO": "SoundIO",
|
||||||
"SettingsTabSystemAudioBackendSDL2": "SDL2",
|
"SettingsTabSystemAudioBackendSDL2": "SDL2",
|
||||||
"SettingsTabSystemHacks": "Hacks",
|
"SettingsTabSystemHacks": "Hacks",
|
||||||
"SettingsTabSystemHacksNote": " - Cela peut causer des instabilitées",
|
"SettingsTabSystemHacksNote": " (Cela peut causer des instabilitées)",
|
||||||
"SettingsTabSystemExpandDramSize": "Augmenter la taille de la DRAM à 6GB",
|
"SettingsTabSystemExpandDramSize": "Augmenter la taille de la DRAM à 6GB",
|
||||||
"SettingsTabSystemIgnoreMissingServices": "Ignorer les services manquant",
|
"SettingsTabSystemIgnoreMissingServices": "Ignorer les services manquant",
|
||||||
"SettingsTabGraphics": "Graphique",
|
"SettingsTabGraphics": "Graphique",
|
||||||
|
|||||||
@@ -118,7 +118,7 @@
|
|||||||
"SettingsTabSystemAudioBackendSoundIO": "SoundIO",
|
"SettingsTabSystemAudioBackendSoundIO": "SoundIO",
|
||||||
"SettingsTabSystemAudioBackendSDL2": "SDL2",
|
"SettingsTabSystemAudioBackendSDL2": "SDL2",
|
||||||
"SettingsTabSystemHacks": "Hacks",
|
"SettingsTabSystemHacks": "Hacks",
|
||||||
"SettingsTabSystemHacksNote": " - Possono causare instabilità",
|
"SettingsTabSystemHacksNote": " (Possono causare instabilità)",
|
||||||
"SettingsTabSystemExpandDramSize": "Espandi dimensione DRAM a 6GB",
|
"SettingsTabSystemExpandDramSize": "Espandi dimensione DRAM a 6GB",
|
||||||
"SettingsTabSystemIgnoreMissingServices": "Ignora servizi mancanti",
|
"SettingsTabSystemIgnoreMissingServices": "Ignora servizi mancanti",
|
||||||
"SettingsTabGraphics": "Grafica",
|
"SettingsTabGraphics": "Grafica",
|
||||||
@@ -554,5 +554,7 @@
|
|||||||
"ControllerMotionTitle": "Impostazioni dei sensori di movimento",
|
"ControllerMotionTitle": "Impostazioni dei sensori di movimento",
|
||||||
"ControllerRumbleTitle": "Impostazioni di vibrazione",
|
"ControllerRumbleTitle": "Impostazioni di vibrazione",
|
||||||
"SettingsSelectThemeFileDialogTitle" : "Seleziona file del tema",
|
"SettingsSelectThemeFileDialogTitle" : "Seleziona file del tema",
|
||||||
"SettingsXamlThemeFile" : "File del tema xaml"
|
"SettingsXamlThemeFile" : "File del tema xaml",
|
||||||
|
"SettingsTabHotkeysResScaleUpHotkey": "Aumentare la risoluzione:",
|
||||||
|
"SettingsTabHotkeysResScaleDownHotkey": "Diminuire la risoluzione:"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -118,7 +118,7 @@
|
|||||||
"SettingsTabSystemAudioBackendSoundIO": "SoundIO",
|
"SettingsTabSystemAudioBackendSoundIO": "SoundIO",
|
||||||
"SettingsTabSystemAudioBackendSDL2": "SDL2",
|
"SettingsTabSystemAudioBackendSDL2": "SDL2",
|
||||||
"SettingsTabSystemHacks": "해킹",
|
"SettingsTabSystemHacks": "해킹",
|
||||||
"SettingsTabSystemHacksNote": " - 불안정을 일으킬 수 있음",
|
"SettingsTabSystemHacksNote": " (불안정을 일으킬 수 있음)",
|
||||||
"SettingsTabSystemExpandDramSize": "DRAM 크기를 6GB로 확장",
|
"SettingsTabSystemExpandDramSize": "DRAM 크기를 6GB로 확장",
|
||||||
"SettingsTabSystemIgnoreMissingServices": "누락된 서비스 무시",
|
"SettingsTabSystemIgnoreMissingServices": "누락된 서비스 무시",
|
||||||
"SettingsTabGraphics": "제도법",
|
"SettingsTabGraphics": "제도법",
|
||||||
|
|||||||
@@ -118,7 +118,7 @@
|
|||||||
"SettingsTabSystemAudioBackendSoundIO": "SoundIO",
|
"SettingsTabSystemAudioBackendSoundIO": "SoundIO",
|
||||||
"SettingsTabSystemAudioBackendSDL2": "SDL2",
|
"SettingsTabSystemAudioBackendSDL2": "SDL2",
|
||||||
"SettingsTabSystemHacks": "Hacks",
|
"SettingsTabSystemHacks": "Hacks",
|
||||||
"SettingsTabSystemHacksNote": " - Pode causar instabilidade",
|
"SettingsTabSystemHacksNote": " (Pode causar instabilidade)",
|
||||||
"SettingsTabSystemExpandDramSize": "Expandir memória para 6GB",
|
"SettingsTabSystemExpandDramSize": "Expandir memória para 6GB",
|
||||||
"SettingsTabSystemIgnoreMissingServices": "Ignorar serviços não implementados",
|
"SettingsTabSystemIgnoreMissingServices": "Ignorar serviços não implementados",
|
||||||
"SettingsTabGraphics": "Gráficos",
|
"SettingsTabGraphics": "Gráficos",
|
||||||
@@ -554,5 +554,7 @@
|
|||||||
"ControllerMotionTitle": "Configurações do controle de movimento",
|
"ControllerMotionTitle": "Configurações do controle de movimento",
|
||||||
"ControllerRumbleTitle": "Configurações de vibração",
|
"ControllerRumbleTitle": "Configurações de vibração",
|
||||||
"SettingsSelectThemeFileDialogTitle" : "Selecionar arquivo do tema",
|
"SettingsSelectThemeFileDialogTitle" : "Selecionar arquivo do tema",
|
||||||
"SettingsXamlThemeFile" : "Arquivo de tema Xaml"
|
"SettingsXamlThemeFile" : "Arquivo de tema Xaml",
|
||||||
|
"SettingsTabHotkeysResScaleUpHotkey": "Aumentar a resolução:",
|
||||||
|
"SettingsTabHotkeysResScaleDownHotkey": "Diminuir a resolução:"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -118,7 +118,7 @@
|
|||||||
"SettingsTabSystemAudioBackendSoundIO": "SoundIO",
|
"SettingsTabSystemAudioBackendSoundIO": "SoundIO",
|
||||||
"SettingsTabSystemAudioBackendSDL2": "SDL2",
|
"SettingsTabSystemAudioBackendSDL2": "SDL2",
|
||||||
"SettingsTabSystemHacks": "Хаки",
|
"SettingsTabSystemHacks": "Хаки",
|
||||||
"SettingsTabSystemHacksNote": " - Эти многие настройки вызывают нестабильность",
|
"SettingsTabSystemHacksNote": " (Эти многие настройки вызывают нестабильность)",
|
||||||
"SettingsTabSystemExpandDramSize": "Увеличение размера DRAM до 6GB",
|
"SettingsTabSystemExpandDramSize": "Увеличение размера DRAM до 6GB",
|
||||||
"SettingsTabSystemIgnoreMissingServices": "Игнорировать отсутствующие службы",
|
"SettingsTabSystemIgnoreMissingServices": "Игнорировать отсутствующие службы",
|
||||||
"SettingsTabGraphics": "Графика",
|
"SettingsTabGraphics": "Графика",
|
||||||
|
|||||||
@@ -118,7 +118,7 @@
|
|||||||
"SettingsTabSystemAudioBackendSoundIO": "SoundIO",
|
"SettingsTabSystemAudioBackendSoundIO": "SoundIO",
|
||||||
"SettingsTabSystemAudioBackendSDL2": "SDL2",
|
"SettingsTabSystemAudioBackendSDL2": "SDL2",
|
||||||
"SettingsTabSystemHacks": "Hacklar",
|
"SettingsTabSystemHacks": "Hacklar",
|
||||||
"SettingsTabSystemHacksNote": " - Bunlar birçok dengesizlik oluşturabilir",
|
"SettingsTabSystemHacksNote": " (Bunlar birçok dengesizlik oluşturabilir)",
|
||||||
"SettingsTabSystemExpandDramSize": "DRAM boyutunu 6GB'a genişlet",
|
"SettingsTabSystemExpandDramSize": "DRAM boyutunu 6GB'a genişlet",
|
||||||
"SettingsTabSystemIgnoreMissingServices": "Eksik Servisleri Görmezden Gel",
|
"SettingsTabSystemIgnoreMissingServices": "Eksik Servisleri Görmezden Gel",
|
||||||
"SettingsTabGraphics": "Grafikler",
|
"SettingsTabGraphics": "Grafikler",
|
||||||
@@ -554,5 +554,7 @@
|
|||||||
"ControllerMotionTitle": "Hareket Kontrol Seçenekleri",
|
"ControllerMotionTitle": "Hareket Kontrol Seçenekleri",
|
||||||
"ControllerRumbleTitle": "Titreşim Seçenekleri",
|
"ControllerRumbleTitle": "Titreşim Seçenekleri",
|
||||||
"SettingsSelectThemeFileDialogTitle" : "Tema Dosyası Seçin",
|
"SettingsSelectThemeFileDialogTitle" : "Tema Dosyası Seçin",
|
||||||
"SettingsXamlThemeFile" : "Xaml Tema Dosyası"
|
"SettingsXamlThemeFile" : "Xaml Tema Dosyası",
|
||||||
|
"SettingsTabHotkeysResScaleUpHotkey": "Çözünürlüğü artırın:",
|
||||||
|
"SettingsTabHotkeysResScaleDownHotkey": "Çözünürlüğü azaltın:"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -118,7 +118,7 @@
|
|||||||
"SettingsTabSystemAudioBackendSoundIO": "SoundIO",
|
"SettingsTabSystemAudioBackendSoundIO": "SoundIO",
|
||||||
"SettingsTabSystemAudioBackendSDL2": "SDL2",
|
"SettingsTabSystemAudioBackendSDL2": "SDL2",
|
||||||
"SettingsTabSystemHacks": "修正",
|
"SettingsTabSystemHacks": "修正",
|
||||||
"SettingsTabSystemHacksNote": " - 会引起模拟器不稳定",
|
"SettingsTabSystemHacksNote": " (会引起模拟器不稳定)",
|
||||||
"SettingsTabSystemExpandDramSize": "将模拟RAM大小扩展到 6GB",
|
"SettingsTabSystemExpandDramSize": "将模拟RAM大小扩展到 6GB",
|
||||||
"SettingsTabSystemIgnoreMissingServices": "忽略缺少的服务",
|
"SettingsTabSystemIgnoreMissingServices": "忽略缺少的服务",
|
||||||
"SettingsTabGraphics": "图像",
|
"SettingsTabGraphics": "图像",
|
||||||
@@ -568,5 +568,7 @@
|
|||||||
"UpdateWindowTitle": "管理游戏更新",
|
"UpdateWindowTitle": "管理游戏更新",
|
||||||
"CheatWindowHeading": "适用于 {0} [{1}] 的金手指",
|
"CheatWindowHeading": "适用于 {0} [{1}] 的金手指",
|
||||||
"DlcWindowHeading": "适用于 {0} [{1}] 的 DLC",
|
"DlcWindowHeading": "适用于 {0} [{1}] 的 DLC",
|
||||||
"GameUpdateWindowHeading": "适用于 {0} [{1}] 的更新"
|
"GameUpdateWindowHeading": "适用于 {0} [{1}] 的更新",
|
||||||
|
"SettingsTabHotkeysResScaleUpHotkey": "分辨率提高",
|
||||||
|
"SettingsTabHotkeysResScaleDownHotkey": "降低分辨率"
|
||||||
}
|
}
|
||||||
|
|||||||
572
Ryujinx.Ava/Assets/Locales/zh_TW.json
Normal file
572
Ryujinx.Ava/Assets/Locales/zh_TW.json
Normal file
@@ -0,0 +1,572 @@
|
|||||||
|
{
|
||||||
|
"MenuBarFileOpenApplet": "打開小程式",
|
||||||
|
"MenuBarFileOpenAppletOpenMiiAppletToolTip": "打開獨立的 Mii 小程式",
|
||||||
|
"SettingsTabInputDirectMouseAccess": "直通滑鼠操作",
|
||||||
|
"SettingsTabSystemMemoryManagerMode": "記憶體管理模式:",
|
||||||
|
"SettingsTabSystemMemoryManagerModeSoftware": "軟體",
|
||||||
|
"SettingsTabSystemMemoryManagerModeHost": "Host (快速)",
|
||||||
|
"SettingsTabSystemMemoryManagerModeHostUnchecked": "Host 略過檢查 (最快,但較不安全)",
|
||||||
|
"MenuBarFile": "_檔案",
|
||||||
|
"MenuBarFileOpenFromFile": "_載入檔案",
|
||||||
|
"MenuBarFileOpenUnpacked": "載入_解包後的遊戲",
|
||||||
|
"MenuBarFileOpenEmuFolder": "開啟 Ryujinx 資料夾",
|
||||||
|
"MenuBarFileOpenLogsFolder": "開啟日誌資料夾",
|
||||||
|
"MenuBarFileExit": "_退出",
|
||||||
|
"MenuBarOptions": "選項",
|
||||||
|
"MenuBarOptionsToggleFullscreen": "切換全螢幕模式",
|
||||||
|
"MenuBarOptionsStartGamesInFullscreen": "使用全螢幕模式啟動遊戲",
|
||||||
|
"MenuBarOptionsStopEmulation": "停止模擬",
|
||||||
|
"MenuBarOptionsSettings": "_設定",
|
||||||
|
"MenuBarOptionsManageUserProfiles": "_管理使用者帳號",
|
||||||
|
"MenuBarActions": "_動作",
|
||||||
|
"MenuBarOptionsSimulateWakeUpMessage": "模擬喚醒訊息",
|
||||||
|
"MenuBarActionsScanAmiibo": "掃描 Amiibo",
|
||||||
|
"MenuBarTools": "_工具",
|
||||||
|
"MenuBarToolsInstallFirmware": "安裝韌體",
|
||||||
|
"MenuBarFileToolsInstallFirmwareFromFile": "從 XCI 或 ZIP 安裝韌體",
|
||||||
|
"MenuBarFileToolsInstallFirmwareFromDirectory": "從資料夾安裝韌體",
|
||||||
|
"MenuBarHelp": "幫助",
|
||||||
|
"MenuBarHelpCheckForUpdates": "檢查更新",
|
||||||
|
"MenuBarHelpAbout": "關於",
|
||||||
|
"MenuSearch": "搜尋...",
|
||||||
|
"GameListHeaderFavorite": "收藏",
|
||||||
|
"GameListHeaderIcon": "圖示",
|
||||||
|
"GameListHeaderApplication": "名稱",
|
||||||
|
"GameListHeaderDeveloper": "開發商",
|
||||||
|
"GameListHeaderVersion": "版本",
|
||||||
|
"GameListHeaderTimePlayed": "遊玩時間",
|
||||||
|
"GameListHeaderLastPlayed": "上次遊玩",
|
||||||
|
"GameListHeaderFileExtension": "副檔名",
|
||||||
|
"GameListHeaderFileSize": "大小",
|
||||||
|
"GameListHeaderPath": "路徑",
|
||||||
|
"GameListContextMenuOpenUserSaveDirectory": "開啟使用者存檔資料夾",
|
||||||
|
"GameListContextMenuOpenUserSaveDirectoryToolTip": "開啟儲存遊戲存檔的資料夾",
|
||||||
|
"GameListContextMenuOpenUserDeviceDirectory": "開啟系統資料夾",
|
||||||
|
"GameListContextMenuOpenUserDeviceDirectoryToolTip": "開啟包含遊戲系統設定的資料夾",
|
||||||
|
"GameListContextMenuOpenUserBcatDirectory": "開啟 BCAT 資料夾",
|
||||||
|
"GameListContextMenuOpenUserBcatDirectoryToolTip": "開啟包含遊戲 BCAT 資料的資料夾",
|
||||||
|
"GameListContextMenuManageTitleUpdates": "管理遊戲更新",
|
||||||
|
"GameListContextMenuManageTitleUpdatesToolTip": "開啟更新管理視窗",
|
||||||
|
"GameListContextMenuManageDlc": "管理 DLC",
|
||||||
|
"GameListContextMenuManageDlcToolTip": "開啟 DLC 管理視窗",
|
||||||
|
"GameListContextMenuOpenModsDirectory": "開啟模組資料夾",
|
||||||
|
"GameListContextMenuOpenModsDirectoryToolTip": "開啟存放遊戲模組的資料夾",
|
||||||
|
"GameListContextMenuCacheManagement": "快取管理",
|
||||||
|
"GameListContextMenuCacheManagementPurgePptc": "清除 PPTC 快取",
|
||||||
|
"GameListContextMenuCacheManagementPurgePptcToolTip": "刪除遊戲的 PPTC 快取",
|
||||||
|
"GameListContextMenuCacheManagementPurgeShaderCache": "清除渲染器快取",
|
||||||
|
"GameListContextMenuCacheManagementPurgeShaderCacheToolTip": "刪除遊戲的渲染器快取",
|
||||||
|
"GameListContextMenuCacheManagementOpenPptcDirectory": "開啟 PPTC 資料夾",
|
||||||
|
"GameListContextMenuCacheManagementOpenPptcDirectoryToolTip": "開啟包含遊戲 PPTC 快取的資料夾",
|
||||||
|
"GameListContextMenuCacheManagementOpenShaderCacheDirectory": "開啟渲染器快取資料夾",
|
||||||
|
"GameListContextMenuCacheManagementOpenShaderCacheDirectoryToolTip": "開啟包含應用程式渲染器快取的資料夾",
|
||||||
|
"GameListContextMenuExtractData": "提取資料",
|
||||||
|
"GameListContextMenuExtractDataExeFS": "ExeFS",
|
||||||
|
"GameListContextMenuExtractDataExeFSToolTip": "從遊戲的目前狀態中提取 ExeFS 分區(包含更新)",
|
||||||
|
"GameListContextMenuExtractDataRomFS": "RomFS",
|
||||||
|
"GameListContextMenuExtractDataRomFSToolTip": "從遊戲的目前狀態中提取 RomFS 分區(包含更新)",
|
||||||
|
"GameListContextMenuExtractDataLogo": "圖示",
|
||||||
|
"GameListContextMenuExtractDataLogoToolTip": "從遊戲的目前狀態中提取圖示(包含更新)",
|
||||||
|
"StatusBarGamesLoaded": "{0}/{1} 遊戲載入完成",
|
||||||
|
"StatusBarSystemVersion": "系統版本: {0}",
|
||||||
|
"Settings": "設定",
|
||||||
|
"SettingsTabGeneral": "使用者介面",
|
||||||
|
"SettingsTabGeneralGeneral": "一般",
|
||||||
|
"SettingsTabGeneralEnableDiscordRichPresence": "啟用 Discord 動態狀態展示",
|
||||||
|
"SettingsTabGeneralCheckUpdatesOnLaunch": "自動檢查更新",
|
||||||
|
"SettingsTabGeneralShowConfirmExitDialog": "顯示 \"確認離開\" 對話框",
|
||||||
|
"SettingsTabGeneralHideCursorOnIdle": "自動隱藏滑鼠",
|
||||||
|
"SettingsTabGeneralGameDirectories": "遊戲資料夾",
|
||||||
|
"SettingsTabGeneralAdd": "新增",
|
||||||
|
"SettingsTabGeneralRemove": "刪除",
|
||||||
|
"SettingsTabSystem": "系統",
|
||||||
|
"SettingsTabSystemCore": "核心",
|
||||||
|
"SettingsTabSystemSystemRegion": "系統區域:",
|
||||||
|
"SettingsTabSystemSystemRegionJapan": "日本",
|
||||||
|
"SettingsTabSystemSystemRegionUSA": "美國",
|
||||||
|
"SettingsTabSystemSystemRegionEurope": "歐洲",
|
||||||
|
"SettingsTabSystemSystemRegionAustralia": "澳洲",
|
||||||
|
"SettingsTabSystemSystemRegionChina": "中國",
|
||||||
|
"SettingsTabSystemSystemRegionKorea": "韓國",
|
||||||
|
"SettingsTabSystemSystemRegionTaiwan": "台灣",
|
||||||
|
"SettingsTabSystemSystemLanguage": "系統語言:",
|
||||||
|
"SettingsTabSystemSystemLanguageJapanese": "日語",
|
||||||
|
"SettingsTabSystemSystemLanguageAmericanEnglish": "美式英語",
|
||||||
|
"SettingsTabSystemSystemLanguageFrench": "法語",
|
||||||
|
"SettingsTabSystemSystemLanguageGerman": "德語",
|
||||||
|
"SettingsTabSystemSystemLanguageItalian": "義大利語",
|
||||||
|
"SettingsTabSystemSystemLanguageSpanish": "西班牙語",
|
||||||
|
"SettingsTabSystemSystemLanguageChinese": "中文 (中國)",
|
||||||
|
"SettingsTabSystemSystemLanguageKorean": "韓語",
|
||||||
|
"SettingsTabSystemSystemLanguageDutch": "荷蘭語",
|
||||||
|
"SettingsTabSystemSystemLanguagePortuguese": "葡萄牙語",
|
||||||
|
"SettingsTabSystemSystemLanguageRussian": "俄語",
|
||||||
|
"SettingsTabSystemSystemLanguageTaiwanese": "中文 (台灣)",
|
||||||
|
"SettingsTabSystemSystemLanguageBritishEnglish": "英式英語",
|
||||||
|
"SettingsTabSystemSystemLanguageCanadianFrench": "加拿大法語",
|
||||||
|
"SettingsTabSystemSystemLanguageLatinAmericanSpanish": "拉美西班牙語",
|
||||||
|
"SettingsTabSystemSystemLanguageSimplifiedChinese": "簡體中文 (推薦)",
|
||||||
|
"SettingsTabSystemSystemLanguageTraditionalChinese": "繁體中文 (推薦)",
|
||||||
|
"SettingsTabSystemSystemTimeZone": "系統時區:",
|
||||||
|
"SettingsTabSystemSystemTime": "系統時鐘:",
|
||||||
|
"SettingsTabSystemEnableVsync": "開啟 VSync",
|
||||||
|
"SettingsTabSystemEnablePptc": "開啟 PPTC 快取",
|
||||||
|
"SettingsTabSystemEnableFsIntegrityChecks": "開啟檔案系統完整性檢查",
|
||||||
|
"SettingsTabSystemAudioBackend": "音訊後端:",
|
||||||
|
"SettingsTabSystemAudioBackendDummy": "無",
|
||||||
|
"SettingsTabSystemAudioBackendOpenAL": "OpenAL",
|
||||||
|
"SettingsTabSystemAudioBackendSoundIO": "SoundIO",
|
||||||
|
"SettingsTabSystemAudioBackendSDL2": "SDL2",
|
||||||
|
"SettingsTabSystemHacks": "修正",
|
||||||
|
"SettingsTabSystemHacksNote": " (會引起模擬器不穩定)",
|
||||||
|
"SettingsTabSystemExpandDramSize": "將模擬記憶體大小擴充至 6GB",
|
||||||
|
"SettingsTabSystemIgnoreMissingServices": "忽略缺少的服務",
|
||||||
|
"SettingsTabGraphics": "圖形",
|
||||||
|
"SettingsTabGraphicsEnhancements": "增強",
|
||||||
|
"SettingsTabGraphicsEnableShaderCache": "啟用渲染器快取",
|
||||||
|
"SettingsTabGraphicsAnisotropicFiltering": "各向異性過濾:",
|
||||||
|
"SettingsTabGraphicsAnisotropicFilteringAuto": "自動",
|
||||||
|
"SettingsTabGraphicsAnisotropicFiltering2x": "2x",
|
||||||
|
"SettingsTabGraphicsAnisotropicFiltering4x": "4x",
|
||||||
|
"SettingsTabGraphicsAnisotropicFiltering8x": "8x",
|
||||||
|
"SettingsTabGraphicsAnisotropicFiltering16x": "16x",
|
||||||
|
"SettingsTabGraphicsResolutionScale": "解析度縮放:",
|
||||||
|
"SettingsTabGraphicsResolutionScaleCustom": "自訂 (不推薦)",
|
||||||
|
"SettingsTabGraphicsResolutionScaleNative": "原生 (720p/1080p)",
|
||||||
|
"SettingsTabGraphicsResolutionScale2x": "2x (1440p/2160p)",
|
||||||
|
"SettingsTabGraphicsResolutionScale3x": "3x (2160p/3240p)",
|
||||||
|
"SettingsTabGraphicsResolutionScale4x": "4x (2880p/4320p)",
|
||||||
|
"SettingsTabGraphicsAspectRatio": "寬高比:",
|
||||||
|
"SettingsTabGraphicsAspectRatio4x3": "4:3",
|
||||||
|
"SettingsTabGraphicsAspectRatio16x9": "16:9",
|
||||||
|
"SettingsTabGraphicsAspectRatio16x10": "16:10",
|
||||||
|
"SettingsTabGraphicsAspectRatio21x9": "21:9",
|
||||||
|
"SettingsTabGraphicsAspectRatio32x9": "32:9",
|
||||||
|
"SettingsTabGraphicsAspectRatioStretch": "拉伸至螢幕大小",
|
||||||
|
"SettingsTabGraphicsDeveloperOptions": "開發者選項",
|
||||||
|
"SettingsTabGraphicsShaderDumpPath": "圖形渲染器轉儲路徑:",
|
||||||
|
"SettingsTabLogging": "日誌",
|
||||||
|
"SettingsTabLoggingLogging": "日誌",
|
||||||
|
"SettingsTabLoggingEnableLoggingToFile": "儲存日誌為檔案",
|
||||||
|
"SettingsTabLoggingEnableStubLogs": "記錄 Stub",
|
||||||
|
"SettingsTabLoggingEnableInfoLogs": "記錄資訊",
|
||||||
|
"SettingsTabLoggingEnableWarningLogs": "記錄警告",
|
||||||
|
"SettingsTabLoggingEnableErrorLogs": "記錄錯誤",
|
||||||
|
"SettingsTabLoggingEnableTraceLogs": "記錄 Trace",
|
||||||
|
"SettingsTabLoggingEnableGuestLogs": "記錄 Guest",
|
||||||
|
"SettingsTabLoggingEnableFsAccessLogs": "記錄檔案存取",
|
||||||
|
"SettingsTabLoggingFsGlobalAccessLogMode": "記錄全域檔案存取模式:",
|
||||||
|
"SettingsTabLoggingDeveloperOptions": "開發者選項 (警告: 會降低效能)",
|
||||||
|
"SettingsTabLoggingOpenglLogLevel": "OpenGL 日誌級別:",
|
||||||
|
"SettingsTabLoggingOpenglLogLevelNone": "無",
|
||||||
|
"SettingsTabLoggingOpenglLogLevelError": "錯誤",
|
||||||
|
"SettingsTabLoggingOpenglLogLevelPerformance": "減速",
|
||||||
|
"SettingsTabLoggingOpenglLogLevelAll": "全部",
|
||||||
|
"SettingsTabLoggingEnableDebugLogs": "啟用除錯日誌",
|
||||||
|
"SettingsTabInput": "輸入",
|
||||||
|
"SettingsTabInputEnableDockedMode": "Docked 模式",
|
||||||
|
"SettingsTabInputDirectKeyboardAccess": "直通鍵盤控制",
|
||||||
|
"SettingsButtonSave": "儲存",
|
||||||
|
"SettingsButtonClose": "關閉",
|
||||||
|
"SettingsButtonApply": "套用",
|
||||||
|
"ControllerSettingsPlayer": "玩家",
|
||||||
|
"ControllerSettingsPlayer1": "玩家 1",
|
||||||
|
"ControllerSettingsPlayer2": "玩家 2",
|
||||||
|
"ControllerSettingsPlayer3": "玩家 3",
|
||||||
|
"ControllerSettingsPlayer4": "玩家 4",
|
||||||
|
"ControllerSettingsPlayer5": "玩家 5",
|
||||||
|
"ControllerSettingsPlayer6": "玩家 6",
|
||||||
|
"ControllerSettingsPlayer7": "玩家 7",
|
||||||
|
"ControllerSettingsPlayer8": "玩家 8",
|
||||||
|
"ControllerSettingsHandheld": "掌機模式",
|
||||||
|
"ControllerSettingsInputDevice": "輸入設備",
|
||||||
|
"ControllerSettingsRefresh": "更新",
|
||||||
|
"ControllerSettingsDeviceDisabled": "關閉",
|
||||||
|
"ControllerSettingsControllerType": "手把類型",
|
||||||
|
"ControllerSettingsControllerTypeHandheld": "掌機",
|
||||||
|
"ControllerSettingsControllerTypeProController": "Pro 手把",
|
||||||
|
"ControllerSettingsControllerTypeJoyConPair": "JoyCon",
|
||||||
|
"ControllerSettingsControllerTypeJoyConLeft": "左 JoyCon",
|
||||||
|
"ControllerSettingsControllerTypeJoyConRight": "右 JoyCon",
|
||||||
|
"ControllerSettingsProfile": "預設",
|
||||||
|
"ControllerSettingsProfileDefault": "預設",
|
||||||
|
"ControllerSettingsLoad": "載入",
|
||||||
|
"ControllerSettingsAdd": "建立",
|
||||||
|
"ControllerSettingsRemove": "刪除",
|
||||||
|
"ControllerSettingsButtons": "按鈕",
|
||||||
|
"ControllerSettingsButtonA": "A",
|
||||||
|
"ControllerSettingsButtonB": "B",
|
||||||
|
"ControllerSettingsButtonX": "X",
|
||||||
|
"ControllerSettingsButtonY": "Y",
|
||||||
|
"ControllerSettingsButtonPlus": "+",
|
||||||
|
"ControllerSettingsButtonMinus": "-",
|
||||||
|
"ControllerSettingsDPad": "方向鍵",
|
||||||
|
"ControllerSettingsDPadUp": "上",
|
||||||
|
"ControllerSettingsDPadDown": "下",
|
||||||
|
"ControllerSettingsDPadLeft": "左",
|
||||||
|
"ControllerSettingsDPadRight": "右",
|
||||||
|
"ControllerSettingsLStick": "左搖桿",
|
||||||
|
"ControllerSettingsLStickButton": "按下",
|
||||||
|
"ControllerSettingsLStickUp": "上",
|
||||||
|
"ControllerSettingsLStickDown": "下",
|
||||||
|
"ControllerSettingsLStickLeft": "左",
|
||||||
|
"ControllerSettingsLStickRight": "右",
|
||||||
|
"ControllerSettingsLStickStick": "桿",
|
||||||
|
"ControllerSettingsLStickInvertXAxis": "反轉 X 方向",
|
||||||
|
"ControllerSettingsLStickInvertYAxis": "反轉 Y 方向",
|
||||||
|
"ControllerSettingsLStickDeadzone": "死區:",
|
||||||
|
"ControllerSettingsRStick": "右搖桿",
|
||||||
|
"ControllerSettingsRStickButton": "按下",
|
||||||
|
"ControllerSettingsRStickUp": "上",
|
||||||
|
"ControllerSettingsRStickDown": "下",
|
||||||
|
"ControllerSettingsRStickLeft": "左",
|
||||||
|
"ControllerSettingsRStickRight": "右",
|
||||||
|
"ControllerSettingsRStickStick": "桿",
|
||||||
|
"ControllerSettingsRStickInvertXAxis": "反轉 X 方向",
|
||||||
|
"ControllerSettingsRStickInvertYAxis": "反轉 Y 方向",
|
||||||
|
"ControllerSettingsRStickDeadzone": "死區:",
|
||||||
|
"ControllerSettingsTriggersLeft": "左 Triggers",
|
||||||
|
"ControllerSettingsTriggersRight": "右 Triggers",
|
||||||
|
"ControllerSettingsTriggersButtonsLeft": "左 Triggers 鍵",
|
||||||
|
"ControllerSettingsTriggersButtonsRight": "右 Triggers 鍵",
|
||||||
|
"ControllerSettingsTriggers": "Triggers",
|
||||||
|
"ControllerSettingsTriggerL": "L",
|
||||||
|
"ControllerSettingsTriggerR": "R",
|
||||||
|
"ControllerSettingsTriggerZL": "ZL",
|
||||||
|
"ControllerSettingsTriggerZR": "ZR",
|
||||||
|
"ControllerSettingsLeftSL": "SL",
|
||||||
|
"ControllerSettingsLeftSR": "SR",
|
||||||
|
"ControllerSettingsRightSL": "SL",
|
||||||
|
"ControllerSettingsRightSR": "SR",
|
||||||
|
"ControllerSettingsExtraButtonsLeft": "左按鍵",
|
||||||
|
"ControllerSettingsExtraButtonsRight": "右按鍵",
|
||||||
|
"ControllerSettingsMisc": "其他",
|
||||||
|
"ControllerSettingsTriggerThreshold": "Triggers 閾值:",
|
||||||
|
"ControllerSettingsMotion": "體感",
|
||||||
|
"ControllerSettingsMotionUseCemuhookCompatibleMotion": "使用 CemuHook 體感協議",
|
||||||
|
"ControllerSettingsMotionControllerSlot": "手把:",
|
||||||
|
"ControllerSettingsMotionMirrorInput": "鏡像操作",
|
||||||
|
"ControllerSettingsMotionRightJoyConSlot": "右 JoyCon:",
|
||||||
|
"ControllerSettingsMotionServerHost": "伺服器 Host:",
|
||||||
|
"ControllerSettingsMotionGyroSensitivity": "陀螺儀敏感度:",
|
||||||
|
"ControllerSettingsMotionGyroDeadzone": "陀螺儀死區:",
|
||||||
|
"ControllerSettingsSave": "儲存",
|
||||||
|
"ControllerSettingsClose": "關閉",
|
||||||
|
"UserProfilesSelectedUserProfile": "選擇使用者帳號:",
|
||||||
|
"UserProfilesSaveProfileName": "儲存帳號名稱",
|
||||||
|
"UserProfilesChangeProfileImage": "更換頭像",
|
||||||
|
"UserProfilesAvailableUserProfiles": "現有的帳號:",
|
||||||
|
"UserProfilesAddNewProfile": "建立帳號",
|
||||||
|
"UserProfilesDeleteSelectedProfile": "刪除選擇的帳號",
|
||||||
|
"UserProfilesClose": "關閉",
|
||||||
|
"ProfileImageSelectionTitle": "頭像選擇",
|
||||||
|
"ProfileImageSelectionHeader": "選擇合適的頭像圖片",
|
||||||
|
"ProfileImageSelectionNote": "您可以導入自訂頭像,或從系統中選擇頭像",
|
||||||
|
"ProfileImageSelectionImportImage": "導入圖片檔案",
|
||||||
|
"ProfileImageSelectionSelectAvatar": "選擇系統頭像",
|
||||||
|
"InputDialogTitle": "輸入對話框",
|
||||||
|
"InputDialogOk": "完成",
|
||||||
|
"InputDialogCancel": "取消",
|
||||||
|
"InputDialogAddNewProfileTitle": "選擇使用者名稱",
|
||||||
|
"InputDialogAddNewProfileHeader": "請輸入帳號名稱",
|
||||||
|
"InputDialogAddNewProfileSubtext": "(最大長度: {0})",
|
||||||
|
"AvatarChoose": "選擇",
|
||||||
|
"AvatarSetBackgroundColor": "設定背景顏色",
|
||||||
|
"AvatarClose": "關閉",
|
||||||
|
"ControllerSettingsLoadProfileToolTip": "載入預設",
|
||||||
|
"ControllerSettingsAddProfileToolTip": "新增預設",
|
||||||
|
"ControllerSettingsRemoveProfileToolTip": "刪除預設",
|
||||||
|
"ControllerSettingsSaveProfileToolTip": "儲存預設",
|
||||||
|
"MenuBarFileToolsTakeScreenshot": "儲存截圖",
|
||||||
|
"MenuBarFileToolsHideUi": "隱藏 UI",
|
||||||
|
"GameListContextMenuToggleFavorite": "標記為收藏",
|
||||||
|
"GameListContextMenuToggleFavoriteToolTip": "啟用或取消收藏標記",
|
||||||
|
"SettingsTabGeneralTheme": "主題",
|
||||||
|
"SettingsTabGeneralThemeCustomTheme": "自定主題路徑",
|
||||||
|
"SettingsTabGeneralThemeBaseStyle": "主題樣式",
|
||||||
|
"SettingsTabGeneralThemeBaseStyleDark": "深色模式",
|
||||||
|
"SettingsTabGeneralThemeBaseStyleLight": "淺色模式",
|
||||||
|
"SettingsTabGeneralThemeEnableCustomTheme": "使用自訂主題介面",
|
||||||
|
"ButtonBrowse": "瀏覽",
|
||||||
|
"ControllerSettingsConfigureGeneral": "配置",
|
||||||
|
"ControllerSettingsRumble": "震動",
|
||||||
|
"ControllerSettingsRumbleStrongMultiplier": "強震動調節",
|
||||||
|
"ControllerSettingsRumbleWeakMultiplier": "弱震動調節",
|
||||||
|
"DialogMessageSaveNotAvailableMessage": "沒有{0} [{1:x16}]的遊戲存檔",
|
||||||
|
"DialogMessageSaveNotAvailableCreateSaveMessage": "是否建立該遊戲的存檔資料夾?",
|
||||||
|
"DialogConfirmationTitle": "Ryujinx - 設定",
|
||||||
|
"DialogUpdaterTitle": "Ryujinx - 更新",
|
||||||
|
"DialogErrorTitle": "Ryujinx - 錯誤",
|
||||||
|
"DialogWarningTitle": "Ryujinx - 警告",
|
||||||
|
"DialogExitTitle": "Ryujinx - 關閉",
|
||||||
|
"DialogErrorMessage": "Ryujinx 遇到了錯誤",
|
||||||
|
"DialogExitMessage": "是否關閉 Ryujinx?",
|
||||||
|
"DialogExitSubMessage": "所有未儲存的進度會遺失!",
|
||||||
|
"DialogMessageCreateSaveErrorMessage": "建立特定的存檔時出錯: {0}",
|
||||||
|
"DialogMessageFindSaveErrorMessage": "查找特定的存檔時出錯: {0}",
|
||||||
|
"FolderDialogExtractTitle": "選擇要解壓到的資料夾",
|
||||||
|
"DialogNcaExtractionMessage": "提取{1}的{0}分區...",
|
||||||
|
"DialogNcaExtractionTitle": "Ryujinx - NCA分區提取",
|
||||||
|
"DialogNcaExtractionMainNcaNotFoundErrorMessage": "提取失敗。所選檔案中不含主NCA檔案",
|
||||||
|
"DialogNcaExtractionCheckLogErrorMessage": "提取失敗。請查看日誌檔案取得詳情。",
|
||||||
|
"DialogNcaExtractionSuccessMessage": "提取成功。",
|
||||||
|
"DialogUpdaterConvertFailedMessage": "無法轉換目前 Ryujinx 版本。",
|
||||||
|
"DialogUpdaterCancelUpdateMessage": "更新取消!",
|
||||||
|
"DialogUpdaterAlreadyOnLatestVersionMessage": "您使用的 Ryujinx 是最新版本。",
|
||||||
|
"DialogUpdaterFailedToGetVersionMessage": "嘗試從 Github 取得版本訊息時無效。可能是因為 GitHub Actions 正在編譯新版本。請過幾分鐘重試。",
|
||||||
|
"DialogUpdaterConvertFailedGithubMessage": "無法轉換從 Github 接收到的 Ryujinx 版本。",
|
||||||
|
"DialogUpdaterDownloadingMessage": "下載新版本中...",
|
||||||
|
"DialogUpdaterExtractionMessage": "正在提取更新...",
|
||||||
|
"DialogUpdaterRenamingMessage": "正在刪除舊檔案...",
|
||||||
|
"DialogUpdaterAddingFilesMessage": "安裝更新中...",
|
||||||
|
"DialogUpdaterCompleteMessage": "更新成功!",
|
||||||
|
"DialogUpdaterRestartMessage": "立即重啟 Ryujinx 完成更新?",
|
||||||
|
"DialogUpdaterArchNotSupportedMessage": "您執行的系統架構不受支援!",
|
||||||
|
"DialogUpdaterArchNotSupportedSubMessage": "(僅支援 x64 系統)",
|
||||||
|
"DialogUpdaterNoInternetMessage": "沒有連接到網路",
|
||||||
|
"DialogUpdaterNoInternetSubMessage": "請確保網路連接正常。",
|
||||||
|
"DialogUpdaterDirtyBuildMessage": "不能更新非官方版本的 Ryujinx!",
|
||||||
|
"DialogUpdaterDirtyBuildSubMessage": "如果希望使用受支援的版本,請您在 https://ryujinx.org/ 下載。",
|
||||||
|
"DialogRestartRequiredMessage": "需要重啟模擬器",
|
||||||
|
"DialogThemeRestartMessage": "主題設定已儲存。需要重新啟動才能生效。",
|
||||||
|
"DialogThemeRestartSubMessage": "您是否要重啟?",
|
||||||
|
"DialogFirmwareInstallEmbeddedMessage": "要安裝遊戲內建的韌體嗎?(韌體 {0})",
|
||||||
|
"DialogFirmwareInstallEmbeddedSuccessMessage": "未找到已安裝的韌體,但 Ryujinx 可以從現有的遊戲安裝韌體{0}.\\n模擬器現在可以執行。",
|
||||||
|
"DialogFirmwareNoFirmwareInstalledMessage": "未安裝韌體",
|
||||||
|
"DialogFirmwareInstalledMessage": "已安裝韌體{0}",
|
||||||
|
"DialogOpenSettingsWindowLabel": "打開設定視窗",
|
||||||
|
"DialogControllerAppletTitle": "控制器小視窗",
|
||||||
|
"DialogMessageDialogErrorExceptionMessage": "顯示訊息對話框時出錯: {0}",
|
||||||
|
"DialogSoftwareKeyboardErrorExceptionMessage": "顯示軟體鍵盤時出錯: {0}",
|
||||||
|
"DialogErrorAppletErrorExceptionMessage": "顯示錯誤對話框時出錯: {0}",
|
||||||
|
"DialogUserErrorDialogMessage": "{0}: {1}",
|
||||||
|
"DialogUserErrorDialogInfoMessage": "\n有關修復此錯誤的更多訊息,可以遵循我們的設定指南。",
|
||||||
|
"DialogUserErrorDialogTitle": "Ryujinx 錯誤 ({0})",
|
||||||
|
"DialogAmiiboApiTitle": "Amiibo API",
|
||||||
|
"DialogAmiiboApiFailFetchMessage": "從 API 取得訊息時出錯。",
|
||||||
|
"DialogAmiiboApiConnectErrorMessage": "無法連接到 Amiibo API 伺服器。伺服器可能已關閉,或者您沒有網路連接。",
|
||||||
|
"DialogProfileInvalidProfileErrorMessage": "預設{0} 與目前輸入配置系統不相容。",
|
||||||
|
"DialogProfileDefaultProfileOverwriteErrorMessage": "默認預設無法被覆蓋",
|
||||||
|
"DialogProfileDeleteProfileTitle": "刪除預設",
|
||||||
|
"DialogProfileDeleteProfileMessage": "刪除後不可恢復,確定嗎?",
|
||||||
|
"DialogWarning": "警告",
|
||||||
|
"DialogPPTCDeletionMessage": "您即將刪除:\n\n{0}的 PPTC 快取\n\n確定嗎?",
|
||||||
|
"DialogPPTCDeletionErrorMessage": "清除位於{0}的 PPTC 快取時出錯: {1}",
|
||||||
|
"DialogShaderDeletionMessage": "您即將刪除:\n\n{0}的渲染器快取\n\n確定嗎?",
|
||||||
|
"DialogShaderDeletionErrorMessage": "清除位於{0}的渲染器快取時出錯: {1}",
|
||||||
|
"DialogRyujinxErrorMessage": "Ryujinx 遇到錯誤",
|
||||||
|
"DialogInvalidTitleIdErrorMessage": "UI 錯誤:所選遊戲沒有有效的標題ID",
|
||||||
|
"DialogFirmwareInstallerFirmwareNotFoundErrorMessage": "路徑{0}找不到有效的系統韌體。",
|
||||||
|
"DialogFirmwareInstallerFirmwareInstallTitle": "安裝韌體{0}",
|
||||||
|
"DialogFirmwareInstallerFirmwareInstallMessage": "將安裝{0}版本的系統。",
|
||||||
|
"DialogFirmwareInstallerFirmwareInstallSubMessage": "\n\n這將替換目前系統版本{0}。",
|
||||||
|
"DialogFirmwareInstallerFirmwareInstallConfirmMessage": "\n\n確認進行?",
|
||||||
|
"DialogFirmwareInstallerFirmwareInstallWaitMessage": "安裝韌體中...",
|
||||||
|
"DialogFirmwareInstallerFirmwareInstallSuccessMessage": "成功安裝系統版本{0}。",
|
||||||
|
"DialogUserProfileDeletionWarningMessage": "刪除後將沒有可選擇的使用者帳號",
|
||||||
|
"DialogUserProfileDeletionConfirmMessage": "是否刪除選擇的帳號",
|
||||||
|
"DialogControllerSettingsModifiedConfirmMessage": "目前的輸入預設已更新",
|
||||||
|
"DialogControllerSettingsModifiedConfirmSubMessage": "要儲存嗎?",
|
||||||
|
"DialogDlcLoadNcaErrorMessage": "{0}. 錯誤的檔案: {1}",
|
||||||
|
"DialogDlcNoDlcErrorMessage": "選擇的檔案不包含所選遊戲的 DLC!",
|
||||||
|
"DialogPerformanceCheckLoggingEnabledMessage": "您啟用了跟蹤日誌,僅供開發人員使用。",
|
||||||
|
"DialogPerformanceCheckLoggingEnabledConfirmMessage": "為了獲得最佳效能,建議停用跟蹤日誌記錄。您是否要立即停用?",
|
||||||
|
"DialogPerformanceCheckShaderDumpEnabledMessage": "您啟用了渲染器轉儲,僅供開發人員使用。",
|
||||||
|
"DialogPerformanceCheckShaderDumpEnabledConfirmMessage": "為了獲得最佳效能,建議停用渲染器轉儲。您是否要立即停用?",
|
||||||
|
"DialogLoadAppGameAlreadyLoadedMessage": "目前已載入有遊戲",
|
||||||
|
"DialogLoadAppGameAlreadyLoadedSubMessage": "請停止模擬或關閉程式,再啟動另一個遊戲。",
|
||||||
|
"DialogUpdateAddUpdateErrorMessage": "選擇的檔案不包含所選遊戲的更新!",
|
||||||
|
"DialogSettingsBackendThreadingWarningTitle": "警告 - 後端多執行緒",
|
||||||
|
"DialogSettingsBackendThreadingWarningMessage": "改變此選項後必須重啟 Ryujinx 才能生效。根據您的硬體,您開啟該選項時,可能需要手動停用驅動程式本身的GL多執行緒。",
|
||||||
|
"SettingsTabGraphicsFeaturesOptions": "功能",
|
||||||
|
"SettingsTabGraphicsBackendMultithreading": "後端多執行緒:",
|
||||||
|
"CommonAuto": "自動(推薦)",
|
||||||
|
"CommonOff": "關閉",
|
||||||
|
"CommonOn": "打開",
|
||||||
|
"InputDialogYes": "是",
|
||||||
|
"InputDialogNo": "否",
|
||||||
|
"DialogProfileInvalidProfileNameErrorMessage": "檔案名包含無效字元,請重試。",
|
||||||
|
"MenuBarOptionsPauseEmulation": "暫停",
|
||||||
|
"MenuBarOptionsResumeEmulation": "繼續",
|
||||||
|
"AboutUrlTooltipMessage": "在瀏覽器中打開 Ryujinx 的官網。",
|
||||||
|
"AboutDisclaimerMessage": "Ryujinx 以任何方式與 Nintendo™ 及其合作伙伴都沒有任何關聯。",
|
||||||
|
"AboutAmiiboDisclaimerMessage": "我們的 Amiibo 模擬使用了\nAmiiboAPI (www.amiiboapi.com) ",
|
||||||
|
"AboutPatreonUrlTooltipMessage": "在瀏覽器中打開 Ryujinx 的 Patreon 贊助頁。",
|
||||||
|
"AboutGithubUrlTooltipMessage": "在瀏覽器中打開 Ryujinx 的 GitHub 儲存庫。",
|
||||||
|
"AboutDiscordUrlTooltipMessage": "在瀏覽器中打開 Ryujinx 的 Discord 伺服器邀請連結。",
|
||||||
|
"AboutTwitterUrlTooltipMessage": "在瀏覽器中打開 Ryujinx 的 Twitter 首頁。",
|
||||||
|
"AboutRyujinxAboutTitle": "關於:",
|
||||||
|
"AboutRyujinxAboutContent": "Ryujinx 是一款 Nintendo Switch™ 模擬器。\n您可以在 Patreon 上贊助 Ryujinx。\n關注 Twitter 或 Discord 可以取得模擬器最新動態。\n如果您對開發本軟體感興趣,歡迎來 GitHub 和 Discord 加入我們!",
|
||||||
|
"AboutRyujinxMaintainersTitle": "由以下作者維護:",
|
||||||
|
"AboutRyujinxMaintainersContentTooltipMessage": "在瀏覽器中打開貢獻者的網頁",
|
||||||
|
"AboutRyujinxSupprtersTitle": "感謝 Patreon 的贊助者:",
|
||||||
|
"AmiiboSeriesLabel": "Amiibo 系列",
|
||||||
|
"AmiiboCharacterLabel": "角色",
|
||||||
|
"AmiiboScanButtonLabel": "掃描",
|
||||||
|
"AmiiboOptionsShowAllLabel": "顯示所有 Amiibo",
|
||||||
|
"AmiiboOptionsUsRandomTagLabel": "修正: 使用隨機標記的 Uuid",
|
||||||
|
"DlcManagerTableHeadingEnabledLabel": "啟用",
|
||||||
|
"DlcManagerTableHeadingTitleIdLabel": "遊戲ID",
|
||||||
|
"DlcManagerTableHeadingContainerPathLabel": "資料夾路徑",
|
||||||
|
"DlcManagerTableHeadingFullPathLabel": "完整路徑",
|
||||||
|
"DlcManagerRemoveAllButton": "全部刪除",
|
||||||
|
"MenuBarOptionsChangeLanguage": "變更語言",
|
||||||
|
"CommonSort": "排序",
|
||||||
|
"CommonShowNames": "顯示名稱",
|
||||||
|
"CommonFavorite": "收藏",
|
||||||
|
"OrderAscending": "從小到大",
|
||||||
|
"OrderDescending": "從大到小",
|
||||||
|
"SettingsTabGraphicsFeatures": "額外功能",
|
||||||
|
"ErrorWindowTitle": "錯誤視窗",
|
||||||
|
"ToggleDiscordTooltip": "啟用或關閉 Discord 動態狀態展示",
|
||||||
|
"AddGameDirBoxTooltip": "輸入要添加的遊戲資料夾",
|
||||||
|
"AddGameDirTooltip": "添加遊戲資料夾到列表中",
|
||||||
|
"RemoveGameDirTooltip": "移除選中的資料夾",
|
||||||
|
"CustomThemeCheckTooltip": "啟用或關閉自訂主題",
|
||||||
|
"CustomThemePathTooltip": "自訂主題的資料夾",
|
||||||
|
"CustomThemeBrowseTooltip": "查找自訂主題",
|
||||||
|
"DockModeToggleTooltip": "是否開啟 Switch 的 Docked 模式",
|
||||||
|
"DirectKeyboardTooltip": "是否開啟\"直連鍵盤存取(HID) 支援\"\n(部分遊戲可以使用您的鍵盤輸入文字)",
|
||||||
|
"DirectMouseTooltip": "是否開啟\"直連滑鼠存取(HID) 支援\"\n(部分遊戲可以使用您的滑鼠導航)",
|
||||||
|
"RegionTooltip": "變更系統區域",
|
||||||
|
"LanguageTooltip": "變更系統語言",
|
||||||
|
"TimezoneTooltip": "變更系統時區",
|
||||||
|
"TimeTooltip": "變更系統時鐘",
|
||||||
|
"VSyncToggleTooltip": "關閉後,部分使用動態幀率的遊戲可以超過 60Hz 更新率",
|
||||||
|
"PptcToggleTooltip": "開啟以後減少遊戲啟動時間和卡頓",
|
||||||
|
"FsIntegrityToggleTooltip": "是否檢查遊戲檔案內容的完整性",
|
||||||
|
"AudioBackendTooltip": "默認推薦SDL,但每種音訊後端對各類遊戲相容性不同,遇到音訊問題可以切換後端",
|
||||||
|
"MemoryManagerTooltip": "改變 Switch 記憶體映射到電腦記憶體的方式,會影響CPU效能消耗",
|
||||||
|
"MemoryManagerSoftwareTooltip": "使用軟體記憶體頁管理,最精確但是速度最慢",
|
||||||
|
"MemoryManagerHostTooltip": "直接映射記憶體頁到電腦記憶體,JIT效率高",
|
||||||
|
"MemoryManagerUnsafeTooltip": "直接映射記憶體頁,但是不檢查記憶體溢出,JIT效率最高。\nRyujinx可以存取任何位置的記憶體,因而相對不安全。此模式下只應執行您信任的遊戲或軟體(即官方遊戲)",
|
||||||
|
"DRamTooltip": "擴展模擬的 Switch 記憶體為6GB,某些高畫質材質模組或 4K 模組需要此選項",
|
||||||
|
"IgnoreMissingServicesTooltip": "忽略某些未實現的系統服務,少部分遊戲需要此選項才能啟動",
|
||||||
|
"GraphicsBackendThreadingTooltip": "啟用後端多執行緒",
|
||||||
|
"GalThreadingTooltip": "使用模擬器自帶的多執行緒調度,減少渲染器編譯的卡頓,並提高驅動程式的效能(尤其是缺失多執行緒的AMD)。\nNVIDIA使用者需要重啟模擬器才能停用驅動本身的多執行緒,否則您需手動執行停用獲得最佳效能",
|
||||||
|
"ShaderCacheToggleTooltip": "開啟後快取渲染器到硬碟,減少遊戲卡頓",
|
||||||
|
"ResolutionScaleTooltip": "縮放渲染的解析度",
|
||||||
|
"ResolutionScaleEntryTooltip": "盡量使用如1.5的浮點倍數。非整數的倍率易引起錯誤",
|
||||||
|
"AnisotropyTooltip": "各向異性過濾等級。提高傾斜視角材質的清晰度\n('自動'使用遊戲默認指定的等級)",
|
||||||
|
"AspectRatioTooltip": "模擬器渲染視窗的寬高比",
|
||||||
|
"ShaderDumpPathTooltip": "轉儲圖形渲染器的路徑",
|
||||||
|
"FileLogTooltip": "是否儲存日誌檔案到硬碟",
|
||||||
|
"StubLogTooltip": "記錄 Stub 訊息",
|
||||||
|
"InfoLogTooltip": "記錄資訊訊息",
|
||||||
|
"WarnLogTooltip": "記錄警告訊息",
|
||||||
|
"ErrorLogTooltip": "記錄錯誤訊息",
|
||||||
|
"TraceLogTooltip": "記錄 Trace 訊息",
|
||||||
|
"GuestLogTooltip": "記錄 Guest 訊息",
|
||||||
|
"FileAccessLogTooltip": "記錄檔案存取訊息",
|
||||||
|
"FSAccessLogModeTooltip": "記錄 FS 存取訊息,輸出到控制台。可選的模式是0-3",
|
||||||
|
"DeveloperOptionTooltip": "使用請謹慎",
|
||||||
|
"OpenGlLogLevel": "需要打開適當的日誌等級",
|
||||||
|
"DebugLogTooltip": "記錄Debug訊息",
|
||||||
|
"LoadApplicationFileTooltip": "選擇 Switch 支援的遊戲格式並載入",
|
||||||
|
"LoadApplicationFolderTooltip": "選擇解包後的 Switch 遊戲並載入",
|
||||||
|
"OpenRyujinxFolderTooltip": "打開 Ryujinx 系統資料夾",
|
||||||
|
"OpenRyujinxLogsTooltip": "打開日誌存放的資料夾",
|
||||||
|
"ExitTooltip": "關閉 Ryujinx",
|
||||||
|
"OpenSettingsTooltip": "打開設定視窗",
|
||||||
|
"OpenProfileManagerTooltip": "打開使用者帳號管理器",
|
||||||
|
"StopEmulationTooltip": "停止執行目前遊戲並回到選擇界面",
|
||||||
|
"CheckUpdatesTooltip": "檢查 Ryujinx 新版本",
|
||||||
|
"OpenAboutTooltip": "開啟關於視窗",
|
||||||
|
"GridSize": "網格尺寸",
|
||||||
|
"GridSizeTooltip": "調整網格模式的大小",
|
||||||
|
"SettingsTabSystemSystemLanguageBrazilianPortuguese": "巴西葡萄牙語",
|
||||||
|
"AboutRyujinxContributorsButtonHeader": "查看所有參與者",
|
||||||
|
"SettingsTabSystemAudioVolume": "音量: ",
|
||||||
|
"AudioVolumeTooltip": "調節音量",
|
||||||
|
"SettingsTabSystemEnableInternetAccess": "啟用網路連接",
|
||||||
|
"EnableInternetAccessTooltip": "開啟網路存取。此選項打開後,效果類似於 Switch 連接到網路的狀態。注意即使此選項關閉,應用程式偶爾也有可能連接到網路",
|
||||||
|
"GameListContextMenuManageCheatToolTip": "管理金手指",
|
||||||
|
"GameListContextMenuManageCheat": "管理金手指",
|
||||||
|
"ControllerSettingsStickRange": "範圍",
|
||||||
|
"DialogStopEmulationTitle": "Ryujinx - 停止模擬",
|
||||||
|
"DialogStopEmulationMessage": "是否確定停止模擬?",
|
||||||
|
"SettingsTabCpu": "CPU",
|
||||||
|
"SettingsTabAudio": "音訊",
|
||||||
|
"SettingsTabNetwork": "網路",
|
||||||
|
"SettingsTabNetworkConnection": "網路連接",
|
||||||
|
"SettingsTabCpuCache": "CPU 快取",
|
||||||
|
"SettingsTabCpuMemory": "CPU 記憶體",
|
||||||
|
"DialogUpdaterFlatpakNotSupportedMessage": "請透過 Flathub 更新 Ryujinx。",
|
||||||
|
"UpdaterDisabledWarningTitle": "更新已停用!",
|
||||||
|
"GameListContextMenuOpenSdModsDirectory": "打開 Atmosphere 模組資料夾",
|
||||||
|
"GameListContextMenuOpenSdModsDirectoryToolTip": "打開包含應用程式模組的額外 Atmosphere SD卡資料夾",
|
||||||
|
"ControllerSettingsRotate90": "順時針旋轉 90°",
|
||||||
|
"IconSize": "圖示尺寸",
|
||||||
|
"IconSizeTooltip": "變更遊戲圖示大小",
|
||||||
|
"MenuBarOptionsShowConsole": "顯示控制台",
|
||||||
|
"ShaderCachePurgeError": "清除渲染器快取時出錯: {0}: {1}",
|
||||||
|
"UserErrorNoKeys": "找不到金鑰",
|
||||||
|
"UserErrorNoFirmware": "找不到韌體",
|
||||||
|
"UserErrorFirmwareParsingFailed": "韌體解析錯誤",
|
||||||
|
"UserErrorApplicationNotFound": "找不到應用程式",
|
||||||
|
"UserErrorUnknown": "未知錯誤",
|
||||||
|
"UserErrorUndefined": "未定義錯誤",
|
||||||
|
"UserErrorNoKeysDescription": "Ryujinx 找不到 『prod.keys』 檔案",
|
||||||
|
"UserErrorNoFirmwareDescription": "Ryujinx 找不到任何已安裝的韌體",
|
||||||
|
"UserErrorFirmwareParsingFailedDescription": "Ryujinx 無法解密選擇的韌體。這通常是由於金鑰過舊。",
|
||||||
|
"UserErrorApplicationNotFoundDescription": "Ryujinx 在選中路徑找不到有效的應用程式。",
|
||||||
|
"UserErrorUnknownDescription": "發生未知錯誤!",
|
||||||
|
"UserErrorUndefinedDescription": "發生了未定義錯誤!此類錯誤不應出現,請聯絡開發人員!",
|
||||||
|
"OpenSetupGuideMessage": "打開設定教學",
|
||||||
|
"NoUpdate": "沒有新版本",
|
||||||
|
"TitleUpdateVersionLabel": "版本 {0} - {1}",
|
||||||
|
"RyujinxInfo": "Ryujinx - 訊息",
|
||||||
|
"RyujinxConfirm": "Ryujinx - 確認",
|
||||||
|
"FileDialogAllTypes": "全部類型",
|
||||||
|
"Never": "從不",
|
||||||
|
"SwkbdMinCharacters": "至少應為 {0} 個字長",
|
||||||
|
"SwkbdMinRangeCharacters": "必須為 {0}-{1} 個字長",
|
||||||
|
"SoftwareKeyboard": "軟體鍵盤",
|
||||||
|
"DialogControllerAppletMessagePlayerRange": "遊戲需要 {0} 個玩家()持有:\n\nTYPES: {1}\n\nPLAYERS: {2}\n\n{3}請打開設定界面,配置手把;或者關閉視窗。",
|
||||||
|
"DialogControllerAppletMessage": "遊戲需要剛好 {0} 個玩家()持有 with:\n\nTYPES: {1}\n\nPLAYERS: {2}\n\n{3}請打開設定界面,配置手把;或者關閉視窗。",
|
||||||
|
"DialogControllerAppletDockModeSet": "現在處於主機模式,無法使用掌機操作方式\n\n",
|
||||||
|
"UpdaterRenaming": "正在刪除舊檔案...",
|
||||||
|
"UpdaterRenameFailed": "更新過程中無法重命名檔案: {0}",
|
||||||
|
"UpdaterAddingFiles": "安裝更新中...",
|
||||||
|
"UpdaterExtracting": "正在提取更新...",
|
||||||
|
"UpdaterDownloading": "下載新版本中...",
|
||||||
|
"Game": "遊戲",
|
||||||
|
"Docked": "主機模式",
|
||||||
|
"Handheld": "掌機模式",
|
||||||
|
"ConnectionError": "連接錯誤。",
|
||||||
|
"AboutPageDeveloperListMore": "{0} 等開發者...",
|
||||||
|
"ApiError": "API 錯誤",
|
||||||
|
"LoadingHeading": "正在啟動 {0}",
|
||||||
|
"CompilingPPTC": "編譯 PPTC 快取中",
|
||||||
|
"CompilingShaders": "編譯渲染器中",
|
||||||
|
"AllKeyboards": "所有鍵盤",
|
||||||
|
"OpenFileDialogTitle": "選擇支援的檔案格式",
|
||||||
|
"OpenFolderDialogTitle": "選擇一個包含解包遊戲的資料夾",
|
||||||
|
"AllSupportedFormats": "全部支援的格式",
|
||||||
|
"RyujinxUpdater": "Ryujinx 更新程式",
|
||||||
|
"SettingsTabHotkeys": "快捷鍵",
|
||||||
|
"SettingsTabHotkeysHotkeys": "鍵盤快捷鍵",
|
||||||
|
"SettingsTabHotkeysToggleVsyncHotkey": "切換垂直同步",
|
||||||
|
"SettingsTabHotkeysScreenshotHotkey": "截圖",
|
||||||
|
"SettingsTabHotkeysShowUiHotkey": "隱藏 UI",
|
||||||
|
"SettingsTabHotkeysPauseHotkey": "暫停",
|
||||||
|
"SettingsTabHotkeysToggleMuteHotkey": "靜音",
|
||||||
|
"ControllerMotionTitle": "體感操作設定",
|
||||||
|
"ControllerRumbleTitle": "震動設定",
|
||||||
|
"SettingsSelectThemeFileDialogTitle": "選擇主題檔案",
|
||||||
|
"SettingsXamlThemeFile": "Xaml 主題檔案",
|
||||||
|
"AvatarWindowTitle": "管理帳號 - 頭像",
|
||||||
|
"Amiibo": "Amiibo",
|
||||||
|
"Unknown": "未知",
|
||||||
|
"Usage": "用途",
|
||||||
|
"Writable": "可寫入",
|
||||||
|
"SelectDlcDialogTitle": "選擇 DLC 檔案",
|
||||||
|
"SelectUpdateDialogTitle": "選擇更新檔",
|
||||||
|
"UserProfileWindowTitle": "管理使用者設定檔",
|
||||||
|
"CheatWindowTitle": "管理遊戲金手指",
|
||||||
|
"DlcWindowTitle": "管理遊戲 DLC",
|
||||||
|
"UpdateWindowTitle": "管理遊戲更新",
|
||||||
|
"CheatWindowHeading": "金手指可用於 {0} [{1}]",
|
||||||
|
"DlcWindowHeading": "DLC 可用於 {0} [{1}]",
|
||||||
|
"GameUpdateWindowHeading": "更新可用於 {0} [{1}]"
|
||||||
|
}
|
||||||
@@ -7,6 +7,8 @@
|
|||||||
Screenshot,
|
Screenshot,
|
||||||
ShowUi,
|
ShowUi,
|
||||||
Pause,
|
Pause,
|
||||||
ToggleMute
|
ToggleMute,
|
||||||
|
ResScaleUp,
|
||||||
|
ResScaleDown
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -119,6 +119,7 @@
|
|||||||
<None Remove="Assets\Locales\ru_RU.json" />
|
<None Remove="Assets\Locales\ru_RU.json" />
|
||||||
<None Remove="Assets\Locales\tr_TR.json" />
|
<None Remove="Assets\Locales\tr_TR.json" />
|
||||||
<None Remove="Assets\Locales\zh_CN.json" />
|
<None Remove="Assets\Locales\zh_CN.json" />
|
||||||
|
<None Remove="Assets\Locales\zh_TW.json" />
|
||||||
<None Remove="Assets\Styles\Styles.xaml" />
|
<None Remove="Assets\Styles\Styles.xaml" />
|
||||||
<None Remove="Assets\Styles\BaseDark.xaml" />
|
<None Remove="Assets\Styles\BaseDark.xaml" />
|
||||||
<None Remove="Assets\Styles\BaseLight.xaml" />
|
<None Remove="Assets\Styles\BaseLight.xaml" />
|
||||||
@@ -136,6 +137,7 @@
|
|||||||
<EmbeddedResource Include="Assets\Locales\ru_RU.json" />
|
<EmbeddedResource Include="Assets\Locales\ru_RU.json" />
|
||||||
<EmbeddedResource Include="Assets\Locales\tr_TR.json" />
|
<EmbeddedResource Include="Assets\Locales\tr_TR.json" />
|
||||||
<EmbeddedResource Include="Assets\Locales\zh_CN.json" />
|
<EmbeddedResource Include="Assets\Locales\zh_CN.json" />
|
||||||
|
<EmbeddedResource Include="Assets\Locales\zh_TW.json" />
|
||||||
<EmbeddedResource Include="Assets\Styles\Styles.xaml" />
|
<EmbeddedResource Include="Assets\Styles\Styles.xaml" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -37,7 +37,7 @@
|
|||||||
Header="{locale:Locale GameListContextMenuManageTitleUpdates}"
|
Header="{locale:Locale GameListContextMenuManageTitleUpdates}"
|
||||||
ToolTip.Tip="{locale:Locale GameListContextMenuManageTitleUpdatesToolTip}" />
|
ToolTip.Tip="{locale:Locale GameListContextMenuManageTitleUpdatesToolTip}" />
|
||||||
<MenuItem
|
<MenuItem
|
||||||
Command="{Binding OpenDlcManager}"
|
Command="{Binding OpenDownloadableContentManager}"
|
||||||
Header="{locale:Locale GameListContextMenuManageDlc}"
|
Header="{locale:Locale GameListContextMenuManageDlc}"
|
||||||
ToolTip.Tip="{locale:Locale GameListContextMenuManageDlcToolTip}" />
|
ToolTip.Tip="{locale:Locale GameListContextMenuManageDlcToolTip}" />
|
||||||
<MenuItem
|
<MenuItem
|
||||||
|
|||||||
@@ -37,7 +37,7 @@
|
|||||||
Header="{locale:Locale GameListContextMenuManageTitleUpdates}"
|
Header="{locale:Locale GameListContextMenuManageTitleUpdates}"
|
||||||
ToolTip.Tip="{locale:Locale GameListContextMenuManageTitleUpdatesToolTip}" />
|
ToolTip.Tip="{locale:Locale GameListContextMenuManageTitleUpdatesToolTip}" />
|
||||||
<MenuItem
|
<MenuItem
|
||||||
Command="{Binding OpenDlcManager}"
|
Command="{Binding OpenDownloadableContentManager}"
|
||||||
Header="{locale:Locale GameListContextMenuManageDlc}"
|
Header="{locale:Locale GameListContextMenuManageDlc}"
|
||||||
ToolTip.Tip="{locale:Locale GameListContextMenuManageDlcToolTip}" />
|
ToolTip.Tip="{locale:Locale GameListContextMenuManageDlcToolTip}" />
|
||||||
<MenuItem
|
<MenuItem
|
||||||
|
|||||||
@@ -17,9 +17,6 @@ namespace Ryujinx.Ava.Ui.Controls
|
|||||||
public UpdateWaitWindow()
|
public UpdateWaitWindow()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
#if DEBUG
|
|
||||||
this.AttachDevTools();
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,16 +1,17 @@
|
|||||||
<UserControl xmlns="https://github.com/avaloniaui"
|
<UserControl
|
||||||
|
x:Class="Ryujinx.Ava.Ui.Controls.UserEditor"
|
||||||
|
xmlns="https://github.com/avaloniaui"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:Locale="clr-namespace:Ryujinx.Ava.Common.Locale"
|
||||||
|
xmlns:controls="clr-namespace:Ryujinx.Ava.Ui.Controls"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
mc:Ignorable="d"
|
|
||||||
Padding="0"
|
|
||||||
Margin="0"
|
|
||||||
xmlns:Locale="clr-namespace:Ryujinx.Ava.Common.Locale"
|
|
||||||
xmlns:viewModels="clr-namespace:Ryujinx.Ava.Ui.ViewModels"
|
|
||||||
xmlns:models="clr-namespace:Ryujinx.Ava.Ui.Models"
|
xmlns:models="clr-namespace:Ryujinx.Ava.Ui.Models"
|
||||||
xmlns:controls="clr-namespace:Ryujinx.Ava.Ui.Controls"
|
|
||||||
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
|
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
|
||||||
x:Class="Ryujinx.Ava.Ui.Controls.UserEditor">
|
xmlns:viewModels="clr-namespace:Ryujinx.Ava.Ui.ViewModels"
|
||||||
|
Margin="0"
|
||||||
|
Padding="0"
|
||||||
|
mc:Ignorable="d">
|
||||||
<UserControl.Resources>
|
<UserControl.Resources>
|
||||||
<controls:BitmapArrayValueConverter x:Key="ByteImage" />
|
<controls:BitmapArrayValueConverter x:Key="ByteImage" />
|
||||||
</UserControl.Resources>
|
</UserControl.Resources>
|
||||||
@@ -23,33 +24,64 @@
|
|||||||
<RowDefinition Height="*" />
|
<RowDefinition Height="*" />
|
||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
<StackPanel Orientation="Vertical" VerticalAlignment="Stretch" HorizontalAlignment="Left">
|
<StackPanel
|
||||||
|
HorizontalAlignment="Left"
|
||||||
|
VerticalAlignment="Stretch"
|
||||||
|
Orientation="Vertical">
|
||||||
<Image
|
<Image
|
||||||
|
Name="ProfileImage"
|
||||||
|
Width="96"
|
||||||
|
Height="96"
|
||||||
Margin="0"
|
Margin="0"
|
||||||
HorizontalAlignment="Stretch"
|
HorizontalAlignment="Stretch"
|
||||||
VerticalAlignment="Top"
|
VerticalAlignment="Top"
|
||||||
Height="96" Width="96"
|
|
||||||
Name="ProfileImage"
|
|
||||||
Source="{Binding Image, Converter={StaticResource ByteImage}}" />
|
Source="{Binding Image, Converter={StaticResource ByteImage}}" />
|
||||||
<Button Margin="5" Content="{Locale:Locale UserProfilesChangeProfileImage}"
|
<Button
|
||||||
Name="ChangePictureButton"
|
Name="ChangePictureButton"
|
||||||
|
Margin="5"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
Click="ChangePictureButton_Click"
|
Click="ChangePictureButton_Click"
|
||||||
HorizontalAlignment="Stretch"/>
|
Content="{Locale:Locale UserProfilesChangeProfileImage}" />
|
||||||
<Button Margin="5" Content="{Locale:Locale UserProfilesSetProfileImage}"
|
<Button
|
||||||
Name="AddPictureButton"
|
Name="AddPictureButton"
|
||||||
|
Margin="5"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
Click="ChangePictureButton_Click"
|
Click="ChangePictureButton_Click"
|
||||||
HorizontalAlignment="Stretch"/>
|
Content="{Locale:Locale UserProfilesSetProfileImage}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<StackPanel Grid.Row="0" Orientation="Vertical" HorizontalAlignment="Stretch" Grid.Column="1" Spacing="10"
|
<StackPanel
|
||||||
Margin="5, 10">
|
Grid.Row="0"
|
||||||
<TextBox Name="NameBox" Width="300" Text="{Binding Name}" MaxLength="{Binding MaxProfileNameLength}"
|
Grid.Column="1"
|
||||||
HorizontalAlignment="Stretch" />
|
Margin="5,10"
|
||||||
<TextBlock Text="{Binding UserId}" Name="IdLabel" />
|
HorizontalAlignment="Stretch"
|
||||||
|
Orientation="Vertical"
|
||||||
|
Spacing="10">
|
||||||
|
<TextBlock Text="{Locale:Locale UserProfilesName}" />
|
||||||
|
<TextBox
|
||||||
|
Name="NameBox"
|
||||||
|
Width="300"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
MaxLength="{Binding MaxProfileNameLength}"
|
||||||
|
Text="{Binding Name}" />
|
||||||
|
<TextBlock Text="{Locale:Locale UserProfilesUserId}" />
|
||||||
|
<TextBlock Name="IdLabel" Text="{Binding UserId}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<StackPanel Grid.Column="0" Grid.ColumnSpan="2" Grid.Row="1" Orientation="Horizontal" Spacing="10" HorizontalAlignment="Right">
|
<StackPanel
|
||||||
<Button Content="{Locale:Locale Save}" Name="SaveButton" Click="SaveButton_Click"/>
|
Grid.Row="1"
|
||||||
<Button HorizontalAlignment="Right" Content="{Locale:Locale Discard}"
|
Grid.Column="0"
|
||||||
Name="CloseButton" Click="CloseButton_Click"/>
|
Grid.ColumnSpan="2"
|
||||||
|
HorizontalAlignment="Right"
|
||||||
|
Orientation="Horizontal"
|
||||||
|
Spacing="10">
|
||||||
|
<Button
|
||||||
|
Name="SaveButton"
|
||||||
|
Click="SaveButton_Click"
|
||||||
|
Content="{Locale:Locale Save}" />
|
||||||
|
<Button
|
||||||
|
Name="CloseButton"
|
||||||
|
HorizontalAlignment="Right"
|
||||||
|
Click="CloseButton_Click"
|
||||||
|
Content="{Locale:Locale Discard}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
</UserControl>
|
</UserControl>
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ namespace Ryujinx.Ava.Ui.Controls
|
|||||||
_parent?.GoBack();
|
_parent?.GoBack();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SaveButton_Click(object sender, RoutedEventArgs e)
|
private async void SaveButton_Click(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
DataValidationErrors.ClearErrors(NameBox);
|
DataValidationErrors.ClearErrors(NameBox);
|
||||||
bool isInvalid = false;
|
bool isInvalid = false;
|
||||||
@@ -77,7 +77,7 @@ namespace Ryujinx.Ava.Ui.Controls
|
|||||||
|
|
||||||
if (TempProfile.Image == null)
|
if (TempProfile.Image == null)
|
||||||
{
|
{
|
||||||
ContentDialogHelper.CreateWarningDialog(LocaleManager.Instance["UserProfileNoImageError"], "");
|
await ContentDialogHelper.CreateWarningDialog(LocaleManager.Instance["UserProfileNoImageError"], "");
|
||||||
|
|
||||||
isInvalid = true;
|
isInvalid = true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,28 +1,34 @@
|
|||||||
<UserControl xmlns="https://github.com/avaloniaui"
|
<UserControl
|
||||||
|
x:Class="Ryujinx.Ava.Ui.Controls.UserSelector"
|
||||||
|
xmlns="https://github.com/avaloniaui"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
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:flex="clr-namespace:Avalonia.Flexbox;assembly=Avalonia.Flexbox"
|
|
||||||
xmlns:Locale="clr-namespace:Ryujinx.Ava.Common.Locale"
|
xmlns:Locale="clr-namespace:Ryujinx.Ava.Common.Locale"
|
||||||
xmlns:viewModels="clr-namespace:Ryujinx.Ava.Ui.ViewModels"
|
|
||||||
xmlns:controls="clr-namespace:Ryujinx.Ava.Ui.Controls"
|
xmlns:controls="clr-namespace:Ryujinx.Ava.Ui.Controls"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:flex="clr-namespace:Avalonia.Flexbox;assembly=Avalonia.Flexbox"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
|
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
|
||||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
xmlns:viewModels="clr-namespace:Ryujinx.Ava.Ui.ViewModels"
|
||||||
x:Class="Ryujinx.Ava.Ui.Controls.UserSelector">
|
d:DesignHeight="450"
|
||||||
|
d:DesignWidth="800"
|
||||||
|
mc:Ignorable="d">
|
||||||
<UserControl.Resources>
|
<UserControl.Resources>
|
||||||
<controls:BitmapArrayValueConverter x:Key="ByteImage" />
|
<controls:BitmapArrayValueConverter x:Key="ByteImage" />
|
||||||
</UserControl.Resources>
|
</UserControl.Resources>
|
||||||
<Design.DataContext>
|
<Design.DataContext>
|
||||||
<viewModels:UserProfileViewModel />
|
<viewModels:UserProfileViewModel />
|
||||||
</Design.DataContext>
|
</Design.DataContext>
|
||||||
<Grid HorizontalAlignment="Stretch"
|
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
|
||||||
VerticalAlignment="Stretch">
|
|
||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
<RowDefinition />
|
<RowDefinition />
|
||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
<ListBox HorizontalAlignment="Stretch" VerticalAlignment="Center" Margin="5" Items="{Binding Profiles}"
|
<ListBox
|
||||||
|
Margin="5"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
VerticalAlignment="Center"
|
||||||
DoubleTapped="ProfilesList_DoubleTapped"
|
DoubleTapped="ProfilesList_DoubleTapped"
|
||||||
|
Items="{Binding Profiles}"
|
||||||
SelectionChanged="SelectingItemsControl_SelectionChanged">
|
SelectionChanged="SelectingItemsControl_SelectionChanged">
|
||||||
<ListBox.ItemsPanel>
|
<ListBox.ItemsPanel>
|
||||||
<ItemsPanelTemplate>
|
<ItemsPanelTemplate>
|
||||||
@@ -49,10 +55,11 @@
|
|||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
<Image
|
<Image
|
||||||
Grid.Row="0"
|
Grid.Row="0"
|
||||||
|
Width="96"
|
||||||
|
Height="96"
|
||||||
Margin="0"
|
Margin="0"
|
||||||
HorizontalAlignment="Stretch"
|
HorizontalAlignment="Stretch"
|
||||||
VerticalAlignment="Top"
|
VerticalAlignment="Top"
|
||||||
Height="96" Width="96"
|
|
||||||
Source="{Binding Image, Converter={StaticResource ByteImage}}" />
|
Source="{Binding Image, Converter={StaticResource ByteImage}}" />
|
||||||
<StackPanel
|
<StackPanel
|
||||||
Grid.Row="1"
|
Grid.Row="1"
|
||||||
@@ -68,23 +75,34 @@
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
<Border HorizontalAlignment="Left" VerticalAlignment="Top"
|
<Border
|
||||||
IsVisible="{Binding IsOpened}"
|
|
||||||
Background="LimeGreen"
|
|
||||||
Width="10"
|
Width="10"
|
||||||
Height="10"
|
Height="10"
|
||||||
Margin="5"
|
Margin="5"
|
||||||
CornerRadius="5" />
|
HorizontalAlignment="Left"
|
||||||
|
VerticalAlignment="Top"
|
||||||
|
Background="LimeGreen"
|
||||||
|
CornerRadius="5"
|
||||||
|
IsVisible="{Binding IsOpened}" />
|
||||||
</Grid>
|
</Grid>
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</ListBox.ItemTemplate>
|
</ListBox.ItemTemplate>
|
||||||
</ListBox>
|
</ListBox>
|
||||||
<StackPanel Grid.Row="1" Orientation="Horizontal" Margin="10,0" Spacing="10" HorizontalAlignment="Center">
|
<StackPanel
|
||||||
<Button Content="{Locale:Locale UserProfilesAddNewProfile}" Command="{Binding AddUser}" />
|
Grid.Row="1"
|
||||||
<Button IsEnabled="{Binding IsSelectedProfiledEditable}"
|
Margin="10,0"
|
||||||
Content="{Locale:Locale UserProfilesEditProfile}" Command="{Binding EditUser}" />
|
HorizontalAlignment="Center"
|
||||||
<Button IsEnabled="{Binding IsSelectedProfileDeletable}"
|
Orientation="Horizontal"
|
||||||
Content="{Locale:Locale UserProfilesDeleteSelectedProfile}" Command="{Binding DeleteUser}" />
|
Spacing="10">
|
||||||
|
<Button Command="{Binding AddUser}" Content="{Locale:Locale UserProfilesAddNewProfile}" />
|
||||||
|
<Button
|
||||||
|
Command="{Binding EditUser}"
|
||||||
|
Content="{Locale:Locale UserProfilesEditProfile}"
|
||||||
|
IsEnabled="{Binding IsSelectedProfiledEditable}" />
|
||||||
|
<Button
|
||||||
|
Command="{Binding DeleteUser}"
|
||||||
|
Content="{Locale:Locale UserProfilesDeleteSelectedProfile}"
|
||||||
|
IsEnabled="{Binding IsSelectedProfileDeletable}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
</UserControl>
|
</UserControl>
|
||||||
@@ -21,7 +21,7 @@ namespace Ryujinx.Ava.Ui.Controls
|
|||||||
AddHandler(Frame.NavigatedToEvent, (s, e) =>
|
AddHandler(Frame.NavigatedToEvent, (s, e) =>
|
||||||
{
|
{
|
||||||
NavigatedTo(e);
|
NavigatedTo(e);
|
||||||
}, Avalonia.Interactivity.RoutingStrategies.Direct);
|
}, RoutingStrategies.Direct);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,12 +29,10 @@ namespace Ryujinx.Ava.Ui.Controls
|
|||||||
{
|
{
|
||||||
if (Program.PreviewerDetached)
|
if (Program.PreviewerDetached)
|
||||||
{
|
{
|
||||||
switch (arg.NavigationMode)
|
if (arg.NavigationMode == NavigationMode.New)
|
||||||
{
|
{
|
||||||
case NavigationMode.New:
|
|
||||||
_parent = (NavigationDialogHost)arg.Parameter;
|
_parent = (NavigationDialogHost)arg.Parameter;
|
||||||
ViewModel = _parent.ViewModel;
|
ViewModel = _parent.ViewModel;
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
DataContext = ViewModel;
|
DataContext = ViewModel;
|
||||||
|
|||||||
@@ -22,7 +22,9 @@ namespace Ryujinx.Ava.Ui.Models
|
|||||||
set
|
set
|
||||||
{
|
{
|
||||||
_isEnabled = value;
|
_isEnabled = value;
|
||||||
|
|
||||||
EnableToggled?.Invoke(this, _isEnabled);
|
EnableToggled?.Invoke(this, _isEnabled);
|
||||||
|
|
||||||
OnPropertyChanged();
|
OnPropertyChanged();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -30,6 +32,7 @@ namespace Ryujinx.Ava.Ui.Models
|
|||||||
public string BuildId { get; }
|
public string BuildId { get; }
|
||||||
|
|
||||||
public string BuildIdKey => $"{BuildId}-{Name}";
|
public string BuildIdKey => $"{BuildId}-{Name}";
|
||||||
|
|
||||||
public string Name { get; }
|
public string Name { get; }
|
||||||
|
|
||||||
public string CleanName => Name.Substring(1, Name.Length - 8);
|
public string CleanName => Name.Substring(1, Name.Length - 8);
|
||||||
|
|||||||
@@ -11,23 +11,10 @@ namespace Ryujinx.Ava.Ui.Models
|
|||||||
{
|
{
|
||||||
BuildId = buildId;
|
BuildId = buildId;
|
||||||
Path = path;
|
Path = path;
|
||||||
|
|
||||||
CollectionChanged += CheatsList_CollectionChanged;
|
CollectionChanged += CheatsList_CollectionChanged;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CheatsList_CollectionChanged(object sender,
|
|
||||||
NotifyCollectionChangedEventArgs e)
|
|
||||||
{
|
|
||||||
if (e.Action == NotifyCollectionChangedAction.Add)
|
|
||||||
{
|
|
||||||
(e.NewItems[0] as CheatModel).EnableToggled += Item_EnableToggled;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Item_EnableToggled(object sender, bool e)
|
|
||||||
{
|
|
||||||
OnPropertyChanged(new PropertyChangedEventArgs(nameof(IsEnabled)));
|
|
||||||
}
|
|
||||||
|
|
||||||
public string BuildId { get; }
|
public string BuildId { get; }
|
||||||
public string Path { get; }
|
public string Path { get; }
|
||||||
|
|
||||||
@@ -47,5 +34,18 @@ namespace Ryujinx.Ava.Ui.Models
|
|||||||
OnPropertyChanged(new PropertyChangedEventArgs(nameof(IsEnabled)));
|
OnPropertyChanged(new PropertyChangedEventArgs(nameof(IsEnabled)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void CheatsList_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.Action == NotifyCollectionChangedAction.Add)
|
||||||
|
{
|
||||||
|
(e.NewItems[0] as CheatModel).EnableToggled += Item_EnableToggled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Item_EnableToggled(object sender, bool e)
|
||||||
|
{
|
||||||
|
OnPropertyChanged(new PropertyChangedEventArgs(nameof(IsEnabled)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
namespace Ryujinx.Ava.Ui.Models
|
|
||||||
{
|
|
||||||
public class DlcModel
|
|
||||||
{
|
|
||||||
public bool IsEnabled { get; set; }
|
|
||||||
public string TitleId { get; }
|
|
||||||
public string ContainerPath { get; }
|
|
||||||
public string FullPath { get; }
|
|
||||||
|
|
||||||
public DlcModel(string titleId, string containerPath, string fullPath, bool isEnabled)
|
|
||||||
{
|
|
||||||
TitleId = titleId;
|
|
||||||
ContainerPath = containerPath;
|
|
||||||
FullPath = fullPath;
|
|
||||||
IsEnabled = isEnabled;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
18
Ryujinx.Ava/Ui/Models/DownloadableContentModel.cs
Normal file
18
Ryujinx.Ava/Ui/Models/DownloadableContentModel.cs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
namespace Ryujinx.Ava.Ui.Models
|
||||||
|
{
|
||||||
|
public class DownloadableContentModel
|
||||||
|
{
|
||||||
|
public bool Enabled { get; set; }
|
||||||
|
public string TitleId { get; }
|
||||||
|
public string ContainerPath { get; }
|
||||||
|
public string FullPath { get; }
|
||||||
|
|
||||||
|
public DownloadableContentModel(string titleId, string containerPath, string fullPath, bool enabled)
|
||||||
|
{
|
||||||
|
TitleId = titleId;
|
||||||
|
ContainerPath = containerPath;
|
||||||
|
FullPath = fullPath;
|
||||||
|
Enabled = enabled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -382,9 +382,9 @@ namespace Ryujinx.Ava.Ui.ViewModels
|
|||||||
{
|
{
|
||||||
string amiiboJsonString = await response.Content.ReadAsStringAsync();
|
string amiiboJsonString = await response.Content.ReadAsStringAsync();
|
||||||
|
|
||||||
using (FileStream dlcJsonStream = File.Create(_amiiboJsonPath, 4096, FileOptions.WriteThrough))
|
using (FileStream amiiboJsonStream = File.Create(_amiiboJsonPath, 4096, FileOptions.WriteThrough))
|
||||||
{
|
{
|
||||||
dlcJsonStream.Write(Encoding.UTF8.GetBytes(amiiboJsonString));
|
amiiboJsonStream.Write(Encoding.UTF8.GetBytes(amiiboJsonString));
|
||||||
}
|
}
|
||||||
|
|
||||||
return amiiboJsonString;
|
return amiiboJsonString;
|
||||||
|
|||||||
@@ -1261,15 +1261,15 @@ namespace Ryujinx.Ava.Ui.ViewModels
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async void OpenDlcManager()
|
public async void OpenDownloadableContentManager()
|
||||||
{
|
{
|
||||||
var selection = SelectedApplication;
|
var selection = SelectedApplication;
|
||||||
|
|
||||||
if (selection != null)
|
if (selection != null)
|
||||||
{
|
{
|
||||||
DlcManagerWindow dlcManager = new(_owner.VirtualFileSystem, ulong.Parse(selection.TitleId, NumberStyles.HexNumber), selection.TitleName);
|
DownloadableContentManagerWindow downloadableContentManager = new(_owner.VirtualFileSystem, ulong.Parse(selection.TitleId, NumberStyles.HexNumber), selection.TitleName);
|
||||||
|
|
||||||
await dlcManager.ShowDialog(_owner);
|
await downloadableContentManager.ShowDialog(_owner);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,11 +43,9 @@ namespace Ryujinx.Ava.Ui.ViewModels
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IsHighlightedProfileEditable =>
|
public bool IsHighlightedProfileEditable => _highlightedProfile != null;
|
||||||
_highlightedProfile != null;
|
|
||||||
|
|
||||||
public bool IsHighlightedProfileDeletable =>
|
public bool IsHighlightedProfileDeletable => _highlightedProfile != null && _highlightedProfile.UserId != AccountManager.DefaultUserId;
|
||||||
_highlightedProfile != null && _highlightedProfile.UserId != AccountManager.DefaultUserId;
|
|
||||||
|
|
||||||
public UserProfile HighlightedProfile
|
public UserProfile HighlightedProfile
|
||||||
{
|
{
|
||||||
@@ -62,16 +60,13 @@ namespace Ryujinx.Ava.Ui.ViewModels
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose() { }
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public void LoadProfiles()
|
public void LoadProfiles()
|
||||||
{
|
{
|
||||||
Profiles.Clear();
|
Profiles.Clear();
|
||||||
|
|
||||||
var profiles = _owner.AccountManager.GetAllUsers()
|
var profiles = _owner.AccountManager.GetAllUsers().OrderByDescending(x => x.AccountState == AccountState.Open);
|
||||||
.OrderByDescending(x => x.AccountState == AccountState.Open);
|
|
||||||
|
|
||||||
foreach (var profile in profiles)
|
foreach (var profile in profiles)
|
||||||
{
|
{
|
||||||
@@ -94,6 +89,7 @@ namespace Ryujinx.Ava.Ui.ViewModels
|
|||||||
public void AddUser()
|
public void AddUser()
|
||||||
{
|
{
|
||||||
UserProfile userProfile = null;
|
UserProfile userProfile = null;
|
||||||
|
|
||||||
_owner.Navigate(typeof(UserEditor), (this._owner, userProfile, true));
|
_owner.Navigate(typeof(UserEditor), (this._owner, userProfile, true));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Input;
|
using Avalonia.Input;
|
||||||
using Avalonia.Interactivity;
|
using Avalonia.Interactivity;
|
||||||
using Avalonia.Markup.Xaml;
|
|
||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
using Ryujinx.Ava.Common.Locale;
|
using Ryujinx.Ava.Common.Locale;
|
||||||
using Ryujinx.Common.Utilities;
|
using Ryujinx.Common.Utilities;
|
||||||
@@ -27,9 +26,6 @@ namespace Ryujinx.Ava.Ui.Windows
|
|||||||
DataContext = this;
|
DataContext = this;
|
||||||
|
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
#if DEBUG
|
|
||||||
this.AttachDevTools();
|
|
||||||
#endif
|
|
||||||
|
|
||||||
_ = DownloadPatronsJson();
|
_ = DownloadPatronsJson();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
using Avalonia;
|
using Avalonia;
|
||||||
using Avalonia.Interactivity;
|
using Avalonia.Interactivity;
|
||||||
using Avalonia.Markup.Xaml;
|
|
||||||
using Ryujinx.Ava.Common.Locale;
|
using Ryujinx.Ava.Common.Locale;
|
||||||
using Ryujinx.Ava.Ui.Models;
|
using Ryujinx.Ava.Ui.Models;
|
||||||
using Ryujinx.Ava.Ui.ViewModels;
|
using Ryujinx.Ava.Ui.ViewModels;
|
||||||
@@ -18,9 +17,7 @@ namespace Ryujinx.Ava.Ui.Windows
|
|||||||
DataContext = ViewModel;
|
DataContext = ViewModel;
|
||||||
|
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
#if DEBUG
|
|
||||||
this.AttachDevTools();
|
|
||||||
#endif
|
|
||||||
Title = $"Ryujinx {Program.Version} - " + LocaleManager.Instance["Amiibo"];
|
Title = $"Ryujinx {Program.Version} - " + LocaleManager.Instance["Amiibo"];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,9 +28,7 @@ namespace Ryujinx.Ava.Ui.Windows
|
|||||||
DataContext = ViewModel;
|
DataContext = ViewModel;
|
||||||
|
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
#if DEBUG
|
|
||||||
this.AttachDevTools();
|
|
||||||
#endif
|
|
||||||
if (Program.PreviewerDetached)
|
if (Program.PreviewerDetached)
|
||||||
{
|
{
|
||||||
Title = $"Ryujinx {Program.Version} - " + LocaleManager.Instance["Amiibo"];
|
Title = $"Ryujinx {Program.Version} - " + LocaleManager.Instance["Amiibo"];
|
||||||
|
|||||||
@@ -1,21 +1,24 @@
|
|||||||
<window:StyleableWindow x:Class="Ryujinx.Ava.Ui.Windows.CheatWindow"
|
<window:StyleableWindow
|
||||||
|
x:Class="Ryujinx.Ava.Ui.Windows.CheatWindow"
|
||||||
xmlns="https://github.com/avaloniaui"
|
xmlns="https://github.com/avaloniaui"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
|
||||||
xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
|
xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
xmlns:model="clr-namespace:Ryujinx.Ava.Ui.Models"
|
xmlns:model="clr-namespace:Ryujinx.Ava.Ui.Models"
|
||||||
xmlns:window="clr-namespace:Ryujinx.Ava.Ui.Windows"
|
xmlns:window="clr-namespace:Ryujinx.Ava.Ui.Windows"
|
||||||
mc:Ignorable="d"
|
Width="500"
|
||||||
Width="500" MinHeight="500" Height="500"
|
Height="500"
|
||||||
|
MinWidth="500"
|
||||||
|
MinHeight="500"
|
||||||
WindowStartupLocation="CenterOwner"
|
WindowStartupLocation="CenterOwner"
|
||||||
MinWidth="500">
|
mc:Ignorable="d">
|
||||||
<Window.Styles>
|
<Window.Styles>
|
||||||
<Style Selector="TreeViewItem">
|
<Style Selector="TreeViewItem">
|
||||||
<Setter Property="IsExpanded" Value="True" />
|
<Setter Property="IsExpanded" Value="True" />
|
||||||
</Style>
|
</Style>
|
||||||
</Window.Styles>
|
</Window.Styles>
|
||||||
<Grid Name="DlcGrid" Margin="15">
|
<Grid Name="CheatGrid" Margin="15">
|
||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
@@ -24,14 +27,14 @@
|
|||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
<TextBlock
|
<TextBlock
|
||||||
Grid.Row="1"
|
Grid.Row="1"
|
||||||
|
MaxWidth="500"
|
||||||
Margin="20,15,20,20"
|
Margin="20,15,20,20"
|
||||||
HorizontalAlignment="Center"
|
HorizontalAlignment="Center"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
MaxWidth="500"
|
|
||||||
LineHeight="18"
|
LineHeight="18"
|
||||||
TextWrapping="Wrap"
|
|
||||||
Text="{Binding Heading}"
|
Text="{Binding Heading}"
|
||||||
TextAlignment="Center" />
|
TextAlignment="Center"
|
||||||
|
TextWrapping="Wrap" />
|
||||||
<Border
|
<Border
|
||||||
Grid.Row="2"
|
Grid.Row="2"
|
||||||
Margin="5"
|
Margin="5"
|
||||||
@@ -39,11 +42,12 @@
|
|||||||
VerticalAlignment="Stretch"
|
VerticalAlignment="Stretch"
|
||||||
BorderBrush="Gray"
|
BorderBrush="Gray"
|
||||||
BorderThickness="1">
|
BorderThickness="1">
|
||||||
<TreeView Items="{Binding LoadedCheats}"
|
<TreeView
|
||||||
|
Name="CheatsView"
|
||||||
|
MinHeight="300"
|
||||||
HorizontalAlignment="Stretch"
|
HorizontalAlignment="Stretch"
|
||||||
VerticalAlignment="Stretch"
|
VerticalAlignment="Stretch"
|
||||||
Name="CheatsView"
|
Items="{Binding LoadedCheats}">
|
||||||
MinHeight="300">
|
|
||||||
<TreeView.Styles>
|
<TreeView.Styles>
|
||||||
<Styles>
|
<Styles>
|
||||||
<Style Selector="TreeViewItem:empty /template/ ItemsPresenter">
|
<Style Selector="TreeViewItem:empty /template/ ItemsPresenter">
|
||||||
@@ -54,17 +58,22 @@
|
|||||||
<TreeView.DataTemplates>
|
<TreeView.DataTemplates>
|
||||||
<TreeDataTemplate DataType="model:CheatsList" ItemsSource="{Binding}">
|
<TreeDataTemplate DataType="model:CheatsList" ItemsSource="{Binding}">
|
||||||
<StackPanel HorizontalAlignment="Left" Orientation="Horizontal">
|
<StackPanel HorizontalAlignment="Left" Orientation="Horizontal">
|
||||||
<CheckBox IsChecked="{Binding IsEnabled}" MinWidth="20" />
|
<CheckBox MinWidth="20" IsChecked="{Binding IsEnabled}" />
|
||||||
<TextBlock Width="150"
|
<TextBlock Width="150" Text="{Binding BuildId}" />
|
||||||
Text="{Binding BuildId}" />
|
<TextBlock Text="{Binding Path}" />
|
||||||
<TextBlock
|
|
||||||
Text="{Binding Path}" />
|
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</TreeDataTemplate>
|
</TreeDataTemplate>
|
||||||
<DataTemplate x:DataType="model:CheatModel">
|
<DataTemplate x:DataType="model:CheatModel">
|
||||||
<StackPanel Orientation="Horizontal" Margin="0" HorizontalAlignment="Left">
|
<StackPanel
|
||||||
<CheckBox IsChecked="{Binding IsEnabled}" Padding="0" Margin="5,0" MinWidth="20" />
|
Margin="0"
|
||||||
<TextBlock Text="{Binding CleanName}" VerticalAlignment="Center" />
|
HorizontalAlignment="Left"
|
||||||
|
Orientation="Horizontal">
|
||||||
|
<CheckBox
|
||||||
|
MinWidth="20"
|
||||||
|
Margin="5,0"
|
||||||
|
Padding="0"
|
||||||
|
IsChecked="{Binding IsEnabled}" />
|
||||||
|
<TextBlock VerticalAlignment="Center" Text="{Binding CleanName}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</TreeView.DataTemplates>
|
</TreeView.DataTemplates>
|
||||||
@@ -79,8 +88,8 @@
|
|||||||
Name="SaveButton"
|
Name="SaveButton"
|
||||||
MinWidth="90"
|
MinWidth="90"
|
||||||
Margin="5"
|
Margin="5"
|
||||||
IsVisible="{Binding !NoCheatsFound}"
|
Command="{Binding Save}"
|
||||||
Command="{Binding Save}">
|
IsVisible="{Binding !NoCheatsFound}">
|
||||||
<TextBlock Text="{locale:Locale SettingsButtonSave}" />
|
<TextBlock Text="{locale:Locale SettingsButtonSave}" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
using Avalonia;
|
using Avalonia;
|
||||||
using Avalonia.Collections;
|
using Avalonia.Collections;
|
||||||
using Avalonia.Markup.Xaml;
|
|
||||||
using Ryujinx.Ava.Common.Locale;
|
using Ryujinx.Ava.Common.Locale;
|
||||||
using Ryujinx.Ava.Ui.Models;
|
using Ryujinx.Ava.Ui.Models;
|
||||||
using Ryujinx.HLE.FileSystem;
|
using Ryujinx.HLE.FileSystem;
|
||||||
@@ -26,7 +25,6 @@ namespace Ryujinx.Ava.Ui.Windows
|
|||||||
DataContext = this;
|
DataContext = this;
|
||||||
|
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
AttachDebugDevTools();
|
|
||||||
|
|
||||||
Title = $"Ryujinx {Program.Version} - " + LocaleManager.Instance["CheatWindowTitle"];
|
Title = $"Ryujinx {Program.Version} - " + LocaleManager.Instance["CheatWindowTitle"];
|
||||||
}
|
}
|
||||||
@@ -38,9 +36,6 @@ namespace Ryujinx.Ava.Ui.Windows
|
|||||||
Heading = string.Format(LocaleManager.Instance["CheatWindowHeading"], titleName, titleId.ToUpper());
|
Heading = string.Format(LocaleManager.Instance["CheatWindowHeading"], titleName, titleId.ToUpper());
|
||||||
|
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
#if DEBUG
|
|
||||||
this.AttachDevTools();
|
|
||||||
#endif
|
|
||||||
|
|
||||||
string modsBasePath = virtualFileSystem.ModLoader.GetModsBasePath();
|
string modsBasePath = virtualFileSystem.ModLoader.GetModsBasePath();
|
||||||
string titleModsPath = virtualFileSystem.ModLoader.GetTitleDir(modsBasePath, titleId);
|
string titleModsPath = virtualFileSystem.ModLoader.GetTitleDir(modsBasePath, titleId);
|
||||||
@@ -96,12 +91,6 @@ namespace Ryujinx.Ava.Ui.Windows
|
|||||||
Title = $"Ryujinx {Program.Version} - " + LocaleManager.Instance["CheatWindowTitle"];
|
Title = $"Ryujinx {Program.Version} - " + LocaleManager.Instance["CheatWindowTitle"];
|
||||||
}
|
}
|
||||||
|
|
||||||
[Conditional("DEBUG")]
|
|
||||||
private void AttachDebugDevTools()
|
|
||||||
{
|
|
||||||
this.AttachDevTools();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Save()
|
public void Save()
|
||||||
{
|
{
|
||||||
if (NoCheatsFound)
|
if (NoCheatsFound)
|
||||||
|
|||||||
@@ -3,24 +3,14 @@ using Avalonia.Controls.Primitives;
|
|||||||
using Avalonia.Input;
|
using Avalonia.Input;
|
||||||
using Avalonia.Interactivity;
|
using Avalonia.Interactivity;
|
||||||
using Avalonia.LogicalTree;
|
using Avalonia.LogicalTree;
|
||||||
using Avalonia.Markup.Xaml;
|
|
||||||
using Avalonia.Threading;
|
|
||||||
using Avalonia.VisualTree;
|
|
||||||
using Ryujinx.Ava.Common.Locale;
|
using Ryujinx.Ava.Common.Locale;
|
||||||
using Ryujinx.Ava.Ui.Controls;
|
using Ryujinx.Ava.Ui.Controls;
|
||||||
using Ryujinx.Ava.Ui.Models;
|
using Ryujinx.Ava.Ui.Models;
|
||||||
using Ryujinx.Ava.Ui.ViewModels;
|
using Ryujinx.Ava.Ui.ViewModels;
|
||||||
using Ryujinx.Common.Configuration.Hid;
|
|
||||||
using Ryujinx.Common.Configuration.Hid.Controller;
|
using Ryujinx.Common.Configuration.Hid.Controller;
|
||||||
using Ryujinx.Input;
|
using Ryujinx.Input;
|
||||||
using Ryujinx.Input.Assigner;
|
using Ryujinx.Input.Assigner;
|
||||||
using Ryujinx.Ui.Common.Configuration;
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Key = Ryujinx.Input.Key;
|
|
||||||
|
|
||||||
namespace Ryujinx.Ava.Ui.Windows
|
namespace Ryujinx.Ava.Ui.Windows
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,254 +0,0 @@
|
|||||||
using Avalonia;
|
|
||||||
using Avalonia.Collections;
|
|
||||||
using Avalonia.Controls;
|
|
||||||
using Avalonia.Threading;
|
|
||||||
using LibHac.Common;
|
|
||||||
using LibHac.Fs;
|
|
||||||
using LibHac.Fs.Fsa;
|
|
||||||
using LibHac.FsSystem;
|
|
||||||
using LibHac.Tools.Fs;
|
|
||||||
using LibHac.Tools.FsSystem;
|
|
||||||
using LibHac.Tools.FsSystem.NcaUtils;
|
|
||||||
using Ryujinx.Ava.Common.Locale;
|
|
||||||
using Ryujinx.Ava.Ui.Controls;
|
|
||||||
using Ryujinx.Ava.Ui.Models;
|
|
||||||
using Ryujinx.Common.Configuration;
|
|
||||||
using Ryujinx.Common.Utilities;
|
|
||||||
using Ryujinx.HLE.FileSystem;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Path = System.IO.Path;
|
|
||||||
|
|
||||||
namespace Ryujinx.Ava.Ui.Windows
|
|
||||||
{
|
|
||||||
public partial class DlcManagerWindow : StyleableWindow
|
|
||||||
{
|
|
||||||
private readonly List<DlcContainer> _dlcContainerList;
|
|
||||||
private readonly string _dlcJsonPath;
|
|
||||||
|
|
||||||
public VirtualFileSystem VirtualFileSystem { get; }
|
|
||||||
|
|
||||||
public AvaloniaList<DlcModel> Dlcs { get; set; }
|
|
||||||
public ulong TitleId { get; }
|
|
||||||
public string TitleName { get; }
|
|
||||||
|
|
||||||
public string Heading => string.Format(LocaleManager.Instance["DlcWindowHeading"], TitleName, TitleId.ToString("X16"));
|
|
||||||
|
|
||||||
public DlcManagerWindow()
|
|
||||||
{
|
|
||||||
DataContext = this;
|
|
||||||
|
|
||||||
InitializeComponent();
|
|
||||||
AttachDebugDevTools();
|
|
||||||
|
|
||||||
Title = $"Ryujinx {Program.Version} - " + LocaleManager.Instance["DlcWindowTitle"];
|
|
||||||
}
|
|
||||||
|
|
||||||
public DlcManagerWindow(VirtualFileSystem virtualFileSystem, ulong titleId, string titleName)
|
|
||||||
{
|
|
||||||
VirtualFileSystem = virtualFileSystem;
|
|
||||||
TitleId = titleId;
|
|
||||||
TitleName = titleName;
|
|
||||||
|
|
||||||
_dlcJsonPath = Path.Combine(AppDataManager.GamesDirPath, titleId.ToString("x16"), "dlc.json");
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_dlcContainerList = JsonHelper.DeserializeFromFile<List<DlcContainer>>(_dlcJsonPath);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
_dlcContainerList = new List<DlcContainer>();
|
|
||||||
}
|
|
||||||
|
|
||||||
DataContext = this;
|
|
||||||
|
|
||||||
InitializeComponent();
|
|
||||||
AttachDebugDevTools();
|
|
||||||
|
|
||||||
Title = $"Ryujinx {Program.Version} - " + LocaleManager.Instance["DlcWindowTitle"];
|
|
||||||
|
|
||||||
LoadDlcs();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Conditional("DEBUG")]
|
|
||||||
private void AttachDebugDevTools()
|
|
||||||
{
|
|
||||||
this.AttachDevTools();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void LoadDlcs()
|
|
||||||
{
|
|
||||||
foreach (DlcContainer dlcContainer in _dlcContainerList)
|
|
||||||
{
|
|
||||||
using FileStream containerFile = File.OpenRead(dlcContainer.Path);
|
|
||||||
|
|
||||||
PartitionFileSystem pfs = new PartitionFileSystem(containerFile.AsStorage());
|
|
||||||
|
|
||||||
VirtualFileSystem.ImportTickets(pfs);
|
|
||||||
|
|
||||||
foreach (DlcNca dlcNca in dlcContainer.DlcNcaList)
|
|
||||||
{
|
|
||||||
using var ncaFile = new UniqueRef<IFile>();
|
|
||||||
pfs.OpenFile(ref ncaFile.Ref(), dlcNca.Path.ToU8Span(), OpenMode.Read).ThrowIfFailure();
|
|
||||||
|
|
||||||
Nca nca = TryCreateNca(ncaFile.Get.AsStorage(), dlcContainer.Path);
|
|
||||||
|
|
||||||
if (nca != null)
|
|
||||||
{
|
|
||||||
Dlcs.Add(new DlcModel(nca.Header.TitleId.ToString("X16"), dlcContainer.Path, dlcNca.Path,
|
|
||||||
dlcNca.Enabled));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Nca TryCreateNca(IStorage ncaStorage, string containerPath)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return new Nca(VirtualFileSystem.KeySet, ncaStorage);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Dispatcher.UIThread.InvokeAsync(async () =>
|
|
||||||
{
|
|
||||||
await ContentDialogHelper.CreateErrorDialog(string.Format(LocaleManager.Instance[
|
|
||||||
"DialogDlcLoadNcaErrorMessage"], ex.Message, containerPath));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task AddDlc(string path)
|
|
||||||
{
|
|
||||||
if (!File.Exists(path) || Dlcs.FirstOrDefault(x => x.ContainerPath == path) != null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
using (FileStream containerFile = File.OpenRead(path))
|
|
||||||
{
|
|
||||||
PartitionFileSystem pfs = new PartitionFileSystem(containerFile.AsStorage());
|
|
||||||
bool containsDlc = false;
|
|
||||||
|
|
||||||
VirtualFileSystem.ImportTickets(pfs);
|
|
||||||
|
|
||||||
foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca"))
|
|
||||||
{
|
|
||||||
using var ncaFile = new UniqueRef<IFile>();
|
|
||||||
|
|
||||||
pfs.OpenFile(ref ncaFile.Ref(), fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
|
|
||||||
|
|
||||||
Nca nca = TryCreateNca(ncaFile.Get.AsStorage(), path);
|
|
||||||
|
|
||||||
if (nca == null)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nca.Header.ContentType == NcaContentType.PublicData)
|
|
||||||
{
|
|
||||||
if ((nca.Header.TitleId & 0xFFFFFFFFFFFFE000) != TitleId)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
Dlcs.Add(new DlcModel(nca.Header.TitleId.ToString("X16"), path, fileEntry.FullPath, true));
|
|
||||||
|
|
||||||
containsDlc = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!containsDlc)
|
|
||||||
{
|
|
||||||
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance["DialogDlcNoDlcErrorMessage"]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void RemoveDlcs(bool removeSelectedOnly = false)
|
|
||||||
{
|
|
||||||
if (removeSelectedOnly)
|
|
||||||
{
|
|
||||||
Dlcs.RemoveAll(Dlcs.Where(x => x.IsEnabled).ToList());
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Dlcs.Clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void RemoveSelected()
|
|
||||||
{
|
|
||||||
RemoveDlcs(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void RemoveAll()
|
|
||||||
{
|
|
||||||
RemoveDlcs();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async void Add()
|
|
||||||
{
|
|
||||||
OpenFileDialog dialog = new OpenFileDialog() { Title = LocaleManager.Instance["SelectDlcDialogTitle"], AllowMultiple = true };
|
|
||||||
|
|
||||||
dialog.Filters.Add(new FileDialogFilter { Name = "NSP", Extensions = { "nsp" } });
|
|
||||||
|
|
||||||
string[] files = await dialog.ShowAsync(this);
|
|
||||||
|
|
||||||
if (files != null)
|
|
||||||
{
|
|
||||||
foreach (string file in files)
|
|
||||||
{
|
|
||||||
await AddDlc(file);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Save()
|
|
||||||
{
|
|
||||||
_dlcContainerList.Clear();
|
|
||||||
|
|
||||||
DlcContainer container = default;
|
|
||||||
|
|
||||||
foreach (DlcModel dlc in Dlcs)
|
|
||||||
{
|
|
||||||
if (container.Path != dlc.ContainerPath)
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrWhiteSpace(container.Path))
|
|
||||||
{
|
|
||||||
_dlcContainerList.Add(container);
|
|
||||||
}
|
|
||||||
|
|
||||||
container = new DlcContainer { Path = dlc.ContainerPath, DlcNcaList = new List<DlcNca>() };
|
|
||||||
}
|
|
||||||
|
|
||||||
container.DlcNcaList.Add(new DlcNca
|
|
||||||
{
|
|
||||||
Enabled = dlc.IsEnabled,
|
|
||||||
TitleId = Convert.ToUInt64(dlc.TitleId, 16),
|
|
||||||
Path = dlc.FullPath
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(container.Path))
|
|
||||||
{
|
|
||||||
_dlcContainerList.Add(container);
|
|
||||||
}
|
|
||||||
|
|
||||||
using (FileStream dlcJsonStream = File.Create(_dlcJsonPath, 4096, FileOptions.WriteThrough))
|
|
||||||
{
|
|
||||||
dlcJsonStream.Write(Encoding.UTF8.GetBytes(JsonHelper.Serialize(_dlcContainerList, true)));
|
|
||||||
}
|
|
||||||
|
|
||||||
Close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<window:StyleableWindow
|
<window:StyleableWindow
|
||||||
x:Class="Ryujinx.Ava.Ui.Windows.DlcManagerWindow"
|
x:Class="Ryujinx.Ava.Ui.Windows.DownloadableContentManagerWindow"
|
||||||
xmlns="https://github.com/avaloniaui"
|
xmlns="https://github.com/avaloniaui"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
WindowStartupLocation="CenterOwner"
|
WindowStartupLocation="CenterOwner"
|
||||||
MinWidth="600"
|
MinWidth="600"
|
||||||
mc:Ignorable="d">
|
mc:Ignorable="d">
|
||||||
<Grid Name="DlcGrid" Margin="15">
|
<Grid Name="DownloadableContentGrid" Margin="15">
|
||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
@@ -40,7 +40,7 @@
|
|||||||
HorizontalAlignment="Stretch"
|
HorizontalAlignment="Stretch"
|
||||||
VerticalAlignment="Stretch"
|
VerticalAlignment="Stretch"
|
||||||
HorizontalScrollBarVisibility="Auto"
|
HorizontalScrollBarVisibility="Auto"
|
||||||
Items="{Binding Dlcs}"
|
Items="{Binding DownloadableContents}"
|
||||||
VerticalScrollBarVisibility="Auto">
|
VerticalScrollBarVisibility="Auto">
|
||||||
<DataGrid.Columns>
|
<DataGrid.Columns>
|
||||||
<DataGridTemplateColumn Width="90">
|
<DataGridTemplateColumn Width="90">
|
||||||
@@ -50,7 +50,7 @@
|
|||||||
Width="50"
|
Width="50"
|
||||||
MinWidth="40"
|
MinWidth="40"
|
||||||
HorizontalAlignment="Right"
|
HorizontalAlignment="Right"
|
||||||
IsChecked="{Binding IsEnabled}" />
|
IsChecked="{Binding Enabled}" />
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</DataGridTemplateColumn.CellTemplate>
|
</DataGridTemplateColumn.CellTemplate>
|
||||||
<DataGridTemplateColumn.Header>
|
<DataGridTemplateColumn.Header>
|
||||||
@@ -116,7 +116,7 @@
|
|||||||
Name="SaveButton"
|
Name="SaveButton"
|
||||||
MinWidth="90"
|
MinWidth="90"
|
||||||
Margin="5"
|
Margin="5"
|
||||||
Command="{Binding Save}">
|
Command="{Binding SaveAndClose}">
|
||||||
<TextBlock Text="{locale:Locale SettingsButtonSave}" />
|
<TextBlock Text="{locale:Locale SettingsButtonSave}" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
266
Ryujinx.Ava/Ui/Windows/DownloadableContentManagerWindow.axaml.cs
Normal file
266
Ryujinx.Ava/Ui/Windows/DownloadableContentManagerWindow.axaml.cs
Normal file
@@ -0,0 +1,266 @@
|
|||||||
|
using Avalonia.Collections;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Threading;
|
||||||
|
using LibHac.Common;
|
||||||
|
using LibHac.Fs;
|
||||||
|
using LibHac.Fs.Fsa;
|
||||||
|
using LibHac.FsSystem;
|
||||||
|
using LibHac.Tools.Fs;
|
||||||
|
using LibHac.Tools.FsSystem;
|
||||||
|
using LibHac.Tools.FsSystem.NcaUtils;
|
||||||
|
using Ryujinx.Ava.Common.Locale;
|
||||||
|
using Ryujinx.Ava.Ui.Controls;
|
||||||
|
using Ryujinx.Ava.Ui.Models;
|
||||||
|
using Ryujinx.Common.Configuration;
|
||||||
|
using Ryujinx.Common.Utilities;
|
||||||
|
using Ryujinx.HLE.FileSystem;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Path = System.IO.Path;
|
||||||
|
|
||||||
|
namespace Ryujinx.Ava.Ui.Windows
|
||||||
|
{
|
||||||
|
public partial class DownloadableContentManagerWindow : StyleableWindow
|
||||||
|
{
|
||||||
|
private readonly List<DownloadableContentContainer> _downloadableContentContainerList;
|
||||||
|
private readonly string _downloadableContentJsonPath;
|
||||||
|
|
||||||
|
public VirtualFileSystem VirtualFileSystem { get; }
|
||||||
|
public AvaloniaList<DownloadableContentModel> DownloadableContents { get; set; } = new AvaloniaList<DownloadableContentModel>();
|
||||||
|
public ulong TitleId { get; }
|
||||||
|
public string TitleName { get; }
|
||||||
|
|
||||||
|
public string Heading => string.Format(LocaleManager.Instance["DlcWindowHeading"], TitleName, TitleId.ToString("X16"));
|
||||||
|
|
||||||
|
public DownloadableContentManagerWindow()
|
||||||
|
{
|
||||||
|
DataContext = this;
|
||||||
|
|
||||||
|
InitializeComponent();
|
||||||
|
|
||||||
|
Title = $"Ryujinx {Program.Version} - " + LocaleManager.Instance["DlcWindowTitle"];
|
||||||
|
}
|
||||||
|
|
||||||
|
public DownloadableContentManagerWindow(VirtualFileSystem virtualFileSystem, ulong titleId, string titleName)
|
||||||
|
{
|
||||||
|
VirtualFileSystem = virtualFileSystem;
|
||||||
|
TitleId = titleId;
|
||||||
|
TitleName = titleName;
|
||||||
|
|
||||||
|
_downloadableContentJsonPath = Path.Combine(AppDataManager.GamesDirPath, titleId.ToString("x16"), "dlc.json");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_downloadableContentContainerList = JsonHelper.DeserializeFromFile<List<DownloadableContentContainer>>(_downloadableContentJsonPath);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
_downloadableContentContainerList = new List<DownloadableContentContainer>();
|
||||||
|
}
|
||||||
|
|
||||||
|
DataContext = this;
|
||||||
|
|
||||||
|
InitializeComponent();
|
||||||
|
|
||||||
|
Title = $"Ryujinx {Program.Version} - " + LocaleManager.Instance["DlcWindowTitle"];
|
||||||
|
|
||||||
|
LoadDownloadableContents();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LoadDownloadableContents()
|
||||||
|
{
|
||||||
|
foreach (DownloadableContentContainer downloadableContentContainer in _downloadableContentContainerList)
|
||||||
|
{
|
||||||
|
if (File.Exists(downloadableContentContainer.ContainerPath))
|
||||||
|
{
|
||||||
|
using FileStream containerFile = File.OpenRead(downloadableContentContainer.ContainerPath);
|
||||||
|
|
||||||
|
PartitionFileSystem pfs = new PartitionFileSystem(containerFile.AsStorage());
|
||||||
|
|
||||||
|
VirtualFileSystem.ImportTickets(pfs);
|
||||||
|
|
||||||
|
foreach (DownloadableContentNca downloadableContentNca in downloadableContentContainer.DownloadableContentNcaList)
|
||||||
|
{
|
||||||
|
using var ncaFile = new UniqueRef<IFile>();
|
||||||
|
|
||||||
|
pfs.OpenFile(ref ncaFile.Ref(), downloadableContentNca.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
|
||||||
|
|
||||||
|
Nca nca = TryCreateNca(ncaFile.Get.AsStorage(), downloadableContentContainer.ContainerPath);
|
||||||
|
if (nca != null)
|
||||||
|
{
|
||||||
|
DownloadableContents.Add(new DownloadableContentModel(nca.Header.TitleId.ToString("X16"),
|
||||||
|
downloadableContentContainer.ContainerPath,
|
||||||
|
downloadableContentNca.FullPath,
|
||||||
|
downloadableContentNca.Enabled));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: Save the list again to remove leftovers.
|
||||||
|
Save();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Nca TryCreateNca(IStorage ncaStorage, string containerPath)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return new Nca(VirtualFileSystem.KeySet, ncaStorage);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Dispatcher.UIThread.InvokeAsync(async () =>
|
||||||
|
{
|
||||||
|
await ContentDialogHelper.CreateErrorDialog(string.Format(LocaleManager.Instance["DialogDlcLoadNcaErrorMessage"], ex.Message, containerPath));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task AddDownloadableContent(string path)
|
||||||
|
{
|
||||||
|
if (!File.Exists(path) || DownloadableContents.FirstOrDefault(x => x.ContainerPath == path) != null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
using (FileStream containerFile = File.OpenRead(path))
|
||||||
|
{
|
||||||
|
PartitionFileSystem pfs = new PartitionFileSystem(containerFile.AsStorage());
|
||||||
|
bool containsDownloadableContent = false;
|
||||||
|
|
||||||
|
VirtualFileSystem.ImportTickets(pfs);
|
||||||
|
|
||||||
|
foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca"))
|
||||||
|
{
|
||||||
|
using var ncaFile = new UniqueRef<IFile>();
|
||||||
|
|
||||||
|
pfs.OpenFile(ref ncaFile.Ref(), fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
|
||||||
|
|
||||||
|
Nca nca = TryCreateNca(ncaFile.Get.AsStorage(), path);
|
||||||
|
|
||||||
|
if (nca == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nca.Header.ContentType == NcaContentType.PublicData)
|
||||||
|
{
|
||||||
|
if ((nca.Header.TitleId & 0xFFFFFFFFFFFFE000) != TitleId)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
DownloadableContents.Add(new DownloadableContentModel(nca.Header.TitleId.ToString("X16"), path, fileEntry.FullPath, true));
|
||||||
|
|
||||||
|
containsDownloadableContent = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!containsDownloadableContent)
|
||||||
|
{
|
||||||
|
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance["DialogDlcNoDlcErrorMessage"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RemoveDownloadableContents(bool removeSelectedOnly = false)
|
||||||
|
{
|
||||||
|
if (removeSelectedOnly)
|
||||||
|
{
|
||||||
|
DownloadableContents.RemoveAll(DownloadableContents.Where(x => x.Enabled).ToList());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
DownloadableContents.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RemoveSelected()
|
||||||
|
{
|
||||||
|
RemoveDownloadableContents(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RemoveAll()
|
||||||
|
{
|
||||||
|
RemoveDownloadableContents();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async void Add()
|
||||||
|
{
|
||||||
|
OpenFileDialog dialog = new OpenFileDialog()
|
||||||
|
{
|
||||||
|
Title = LocaleManager.Instance["SelectDlcDialogTitle"],
|
||||||
|
AllowMultiple = true
|
||||||
|
};
|
||||||
|
|
||||||
|
dialog.Filters.Add(new FileDialogFilter
|
||||||
|
{
|
||||||
|
Name = "NSP",
|
||||||
|
Extensions = { "nsp" }
|
||||||
|
});
|
||||||
|
|
||||||
|
string[] files = await dialog.ShowAsync(this);
|
||||||
|
|
||||||
|
if (files != null)
|
||||||
|
{
|
||||||
|
foreach (string file in files)
|
||||||
|
{
|
||||||
|
await AddDownloadableContent(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Save()
|
||||||
|
{
|
||||||
|
_downloadableContentContainerList.Clear();
|
||||||
|
|
||||||
|
DownloadableContentContainer container = default;
|
||||||
|
|
||||||
|
foreach (DownloadableContentModel downloadableContent in DownloadableContents)
|
||||||
|
{
|
||||||
|
if (container.ContainerPath != downloadableContent.ContainerPath)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(container.ContainerPath))
|
||||||
|
{
|
||||||
|
_downloadableContentContainerList.Add(container);
|
||||||
|
}
|
||||||
|
|
||||||
|
container = new DownloadableContentContainer
|
||||||
|
{
|
||||||
|
ContainerPath = downloadableContent.ContainerPath,
|
||||||
|
DownloadableContentNcaList = new List<DownloadableContentNca>()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
container.DownloadableContentNcaList.Add(new DownloadableContentNca
|
||||||
|
{
|
||||||
|
Enabled = downloadableContent.Enabled,
|
||||||
|
TitleId = Convert.ToUInt64(downloadableContent.TitleId, 16),
|
||||||
|
FullPath = downloadableContent.FullPath
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(container.ContainerPath))
|
||||||
|
{
|
||||||
|
_downloadableContentContainerList.Add(container);
|
||||||
|
}
|
||||||
|
|
||||||
|
using (FileStream downloadableContentJsonStream = File.Create(_downloadableContentJsonPath, 4096, FileOptions.WriteThrough))
|
||||||
|
{
|
||||||
|
downloadableContentJsonStream.Write(Encoding.UTF8.GetBytes(JsonHelper.Serialize(_downloadableContentContainerList, true)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SaveAndClose()
|
||||||
|
{
|
||||||
|
Save();
|
||||||
|
Close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -111,14 +111,20 @@
|
|||||||
Command="{ReflectionBinding ToggleFullscreen}"
|
Command="{ReflectionBinding ToggleFullscreen}"
|
||||||
Header="{locale:Locale MenuBarOptionsToggleFullscreen}"
|
Header="{locale:Locale MenuBarOptionsToggleFullscreen}"
|
||||||
InputGesture="F11" />
|
InputGesture="F11" />
|
||||||
<MenuItem Header="{locale:Locale MenuBarOptionsStartGamesInFullscreen}">
|
<MenuItem>
|
||||||
<MenuItem.Icon>
|
<MenuItem.Icon>
|
||||||
<CheckBox IsChecked="{Binding StartGamesInFullscreen, Mode=TwoWay}" />
|
<CheckBox IsChecked="{Binding StartGamesInFullscreen, Mode=TwoWay}"
|
||||||
|
MinWidth="250">
|
||||||
|
<TextBlock Text="{locale:Locale MenuBarOptionsStartGamesInFullscreen}"/>
|
||||||
|
</CheckBox>
|
||||||
</MenuItem.Icon>
|
</MenuItem.Icon>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem Header="{locale:Locale MenuBarOptionsShowConsole}" IsVisible="{Binding ShowConsoleVisible}">
|
<MenuItem IsVisible="{Binding ShowConsoleVisible}">
|
||||||
<MenuItem.Icon>
|
<MenuItem.Icon>
|
||||||
<CheckBox IsChecked="{Binding ShowConsole, Mode=TwoWay}" />
|
<CheckBox IsChecked="{Binding ShowConsole, Mode=TwoWay}"
|
||||||
|
MinWidth="250">
|
||||||
|
<TextBlock Text="{locale:Locale MenuBarOptionsShowConsole}"/>
|
||||||
|
</CheckBox>
|
||||||
</MenuItem.Icon>
|
</MenuItem.Icon>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<Separator />
|
<Separator />
|
||||||
@@ -167,6 +173,10 @@
|
|||||||
Command="{ReflectionBinding ChangeLanguage}"
|
Command="{ReflectionBinding ChangeLanguage}"
|
||||||
CommandParameter="zh_CN"
|
CommandParameter="zh_CN"
|
||||||
Header="Simplified Chinese" />
|
Header="Simplified Chinese" />
|
||||||
|
<MenuItem
|
||||||
|
Command="{ReflectionBinding ChangeLanguage}"
|
||||||
|
CommandParameter="zh_TW"
|
||||||
|
Header="Traditional Chinese (Taiwan)" />
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<Separator />
|
<Separator />
|
||||||
<MenuItem
|
<MenuItem
|
||||||
@@ -247,7 +257,7 @@
|
|||||||
</DockPanel>
|
</DockPanel>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<ContentControl
|
<ContentControl
|
||||||
Name="Content"
|
Name="MainContent"
|
||||||
Grid.Row="1"
|
Grid.Row="1"
|
||||||
Padding="0"
|
Padding="0"
|
||||||
HorizontalAlignment="Stretch"
|
HorizontalAlignment="Stretch"
|
||||||
|
|||||||
@@ -2,10 +2,8 @@ using Avalonia;
|
|||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Input;
|
using Avalonia.Input;
|
||||||
using Avalonia.Interactivity;
|
using Avalonia.Interactivity;
|
||||||
using Avalonia.Markup.Xaml;
|
|
||||||
using Avalonia.Media;
|
using Avalonia.Media;
|
||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
using Avalonia.Win32;
|
|
||||||
using FluentAvalonia.UI.Controls;
|
using FluentAvalonia.UI.Controls;
|
||||||
using Ryujinx.Ava.Common;
|
using Ryujinx.Ava.Common;
|
||||||
using Ryujinx.Ava.Common.Locale;
|
using Ryujinx.Ava.Common.Locale;
|
||||||
@@ -33,7 +31,7 @@ using System.IO;
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using InputManager = Ryujinx.Input.HLE.InputManager;
|
using InputManager = Ryujinx.Input.HLE.InputManager;
|
||||||
using ProgressBar = Avalonia.Controls.ProgressBar;
|
|
||||||
namespace Ryujinx.Ava.Ui.Windows
|
namespace Ryujinx.Ava.Ui.Windows
|
||||||
{
|
{
|
||||||
public partial class MainWindow : StyleableWindow
|
public partial class MainWindow : StyleableWindow
|
||||||
@@ -87,7 +85,6 @@ namespace Ryujinx.Ava.Ui.Windows
|
|||||||
|
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
Load();
|
Load();
|
||||||
AttachDebugDevTools();
|
|
||||||
|
|
||||||
UiHandler = new AvaHostUiHandler(this);
|
UiHandler = new AvaHostUiHandler(this);
|
||||||
|
|
||||||
@@ -110,12 +107,6 @@ namespace Ryujinx.Ava.Ui.Windows
|
|||||||
_rendererWaitEvent = new AutoResetEvent(false);
|
_rendererWaitEvent = new AutoResetEvent(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Conditional("DEBUG")]
|
|
||||||
private void AttachDebugDevTools()
|
|
||||||
{
|
|
||||||
this.AttachDevTools();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void LoadGameList()
|
public void LoadGameList()
|
||||||
{
|
{
|
||||||
if (_isLoading)
|
if (_isLoading)
|
||||||
@@ -244,7 +235,7 @@ namespace Ryujinx.Ava.Ui.Windows
|
|||||||
|
|
||||||
PrepareLoadScreen();
|
PrepareLoadScreen();
|
||||||
|
|
||||||
_mainViewContent = Content.Content as Control;
|
_mainViewContent = MainContent.Content as Control;
|
||||||
|
|
||||||
GlRenderer = new RendererControl(3, 3, ConfigurationState.Instance.Logger.GraphicsDebugLevel);
|
GlRenderer = new RendererControl(3, 3, ConfigurationState.Instance.Logger.GraphicsDebugLevel);
|
||||||
AppHost = new AppHost(GlRenderer, InputManager, path, VirtualFileSystem, ContentManager, AccountManager, _userChannelPersistence, this);
|
AppHost = new AppHost(GlRenderer, InputManager, path, VirtualFileSystem, ContentManager, AccountManager, _userChannelPersistence, this);
|
||||||
@@ -311,7 +302,7 @@ namespace Ryujinx.Ava.Ui.Windows
|
|||||||
|
|
||||||
Dispatcher.UIThread.InvokeAsync(() =>
|
Dispatcher.UIThread.InvokeAsync(() =>
|
||||||
{
|
{
|
||||||
Content.Content = GlRenderer;
|
MainContent.Content = GlRenderer;
|
||||||
|
|
||||||
if (startFullscreen && WindowState != WindowState.FullScreen)
|
if (startFullscreen && WindowState != WindowState.FullScreen)
|
||||||
{
|
{
|
||||||
@@ -355,9 +346,9 @@ namespace Ryujinx.Ava.Ui.Windows
|
|||||||
|
|
||||||
Dispatcher.UIThread.InvokeAsync(() =>
|
Dispatcher.UIThread.InvokeAsync(() =>
|
||||||
{
|
{
|
||||||
if (Content.Content != _mainViewContent)
|
if (MainContent.Content != _mainViewContent)
|
||||||
{
|
{
|
||||||
Content.Content = _mainViewContent;
|
MainContent.Content = _mainViewContent;
|
||||||
}
|
}
|
||||||
|
|
||||||
ViewModel.ShowMenuAndStatusBar = true;
|
ViewModel.ShowMenuAndStatusBar = true;
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Markup.Xaml;
|
|
||||||
using FluentAvalonia.UI.Controls;
|
using FluentAvalonia.UI.Controls;
|
||||||
using Ryujinx.Ava.Common.Locale;
|
using Ryujinx.Ava.Common.Locale;
|
||||||
using Ryujinx.Ava.Ui.Models;
|
using Ryujinx.Ava.Ui.Models;
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Markup.Xaml;
|
|
||||||
using FluentAvalonia.UI.Controls;
|
using FluentAvalonia.UI.Controls;
|
||||||
using Ryujinx.Ava.Common.Locale;
|
using Ryujinx.Ava.Common.Locale;
|
||||||
using Ryujinx.Ava.Ui.Models;
|
using Ryujinx.Ava.Ui.Models;
|
||||||
|
|||||||
@@ -241,6 +241,22 @@
|
|||||||
TextAlignment="Center" />
|
TextAlignment="Center" />
|
||||||
</ToggleButton>
|
</ToggleButton>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
<StackPanel Margin="10,0,0,0" Orientation="Horizontal">
|
||||||
|
<TextBlock VerticalAlignment="Center" Text="{locale:Locale SettingsTabHotkeysResScaleUpHotkey}" Width="230" />
|
||||||
|
<ToggleButton Width="90" Height="27" Checked="Button_Checked" Unchecked="Button_Unchecked">
|
||||||
|
<TextBlock
|
||||||
|
Text="{Binding KeyboardHotkeys.ResScaleUp, Mode=TwoWay, Converter={StaticResource Key}}"
|
||||||
|
TextAlignment="Center" />
|
||||||
|
</ToggleButton>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Margin="10,0,0,0" Orientation="Horizontal">
|
||||||
|
<TextBlock VerticalAlignment="Center" Text="{locale:Locale SettingsTabHotkeysResScaleDownHotkey}" Width="230" />
|
||||||
|
<ToggleButton Width="90" Height="27" Checked="Button_Checked" Unchecked="Button_Unchecked">
|
||||||
|
<TextBlock
|
||||||
|
Text="{Binding KeyboardHotkeys.ResScaleDown, Mode=TwoWay, Converter={StaticResource Key}}"
|
||||||
|
TextAlignment="Center" />
|
||||||
|
</ToggleButton>
|
||||||
|
</StackPanel>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
|
|||||||
@@ -1,19 +1,14 @@
|
|||||||
using Avalonia;
|
using Avalonia;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Controls.Presenters;
|
|
||||||
using Avalonia.Controls.Primitives;
|
using Avalonia.Controls.Primitives;
|
||||||
using Avalonia.Data;
|
using Avalonia.Data;
|
||||||
using Avalonia.Data.Converters;
|
using Avalonia.Data.Converters;
|
||||||
using Avalonia.Input;
|
using Avalonia.Input;
|
||||||
using Avalonia.Interactivity;
|
using Avalonia.Interactivity;
|
||||||
using Avalonia.LogicalTree;
|
|
||||||
using Avalonia.Markup.Xaml;
|
|
||||||
using Avalonia.Threading;
|
|
||||||
using FluentAvalonia.Core;
|
using FluentAvalonia.Core;
|
||||||
using FluentAvalonia.UI.Controls;
|
using FluentAvalonia.UI.Controls;
|
||||||
using Ryujinx.Ava.Common.Locale;
|
using Ryujinx.Ava.Common.Locale;
|
||||||
using Ryujinx.Ava.Ui.Controls;
|
using Ryujinx.Ava.Ui.Controls;
|
||||||
using Ryujinx.Ava.Ui.Models;
|
|
||||||
using Ryujinx.Ava.Ui.ViewModels;
|
using Ryujinx.Ava.Ui.ViewModels;
|
||||||
using Ryujinx.HLE.FileSystem;
|
using Ryujinx.HLE.FileSystem;
|
||||||
using Ryujinx.Input;
|
using Ryujinx.Input;
|
||||||
@@ -23,8 +18,6 @@ using System.Collections.Generic;
|
|||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using TimeZone = Ryujinx.Ava.Ui.Models.TimeZone;
|
using TimeZone = Ryujinx.Ava.Ui.Models.TimeZone;
|
||||||
|
|
||||||
namespace Ryujinx.Ava.Ui.Windows
|
namespace Ryujinx.Ava.Ui.Windows
|
||||||
@@ -44,7 +37,6 @@ namespace Ryujinx.Ava.Ui.Windows
|
|||||||
|
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
Load();
|
Load();
|
||||||
AttachDebugDevTools();
|
|
||||||
|
|
||||||
FuncMultiValueConverter<string, string> converter = new(parts => string.Format("{0} {1} {2}", parts.ToArray()));
|
FuncMultiValueConverter<string, string> converter = new(parts => string.Format("{0} {1} {2}", parts.ToArray()));
|
||||||
MultiBinding tzMultiBinding = new() { Converter = converter };
|
MultiBinding tzMultiBinding = new() { Converter = converter };
|
||||||
@@ -62,13 +54,6 @@ namespace Ryujinx.Ava.Ui.Windows
|
|||||||
|
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
Load();
|
Load();
|
||||||
AttachDebugDevTools();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Conditional("DEBUG")]
|
|
||||||
private void AttachDebugDevTools()
|
|
||||||
{
|
|
||||||
this.AttachDevTools();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Load()
|
private void Load()
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
using Avalonia.Controls.Primitives;
|
using Avalonia.Controls.Primitives;
|
||||||
using Avalonia.Media.Imaging;
|
using Avalonia.Media.Imaging;
|
||||||
using Avalonia.Platform;
|
using Avalonia.Platform;
|
||||||
using FluentAvalonia.UI.Controls;
|
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
using Avalonia;
|
using Avalonia;
|
||||||
using Avalonia.Collections;
|
using Avalonia.Collections;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Markup.Xaml;
|
using Avalonia.Threading;
|
||||||
using LibHac.Common;
|
using LibHac.Common;
|
||||||
using LibHac.Fs;
|
using LibHac.Fs;
|
||||||
using LibHac.Fs.Fsa;
|
using LibHac.Fs.Fsa;
|
||||||
using LibHac.FsSystem;
|
using LibHac.FsSystem;
|
||||||
using LibHac.Tools.FsSystem.NcaUtils;
|
|
||||||
using LibHac.Ns;
|
using LibHac.Ns;
|
||||||
|
using LibHac.Tools.FsSystem;
|
||||||
|
using LibHac.Tools.FsSystem.NcaUtils;
|
||||||
using Ryujinx.Ava.Common.Locale;
|
using Ryujinx.Ava.Common.Locale;
|
||||||
using Ryujinx.Ava.Ui.Controls;
|
using Ryujinx.Ava.Ui.Controls;
|
||||||
using Ryujinx.Ava.Ui.Models;
|
using Ryujinx.Ava.Ui.Models;
|
||||||
@@ -23,14 +24,12 @@ using System.Linq;
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using Path = System.IO.Path;
|
using Path = System.IO.Path;
|
||||||
using SpanHelpers = LibHac.Common.SpanHelpers;
|
using SpanHelpers = LibHac.Common.SpanHelpers;
|
||||||
using LibHac.Tools.FsSystem;
|
|
||||||
using Avalonia.Threading;
|
|
||||||
|
|
||||||
namespace Ryujinx.Ava.Ui.Windows
|
namespace Ryujinx.Ava.Ui.Windows
|
||||||
{
|
{
|
||||||
public partial class TitleUpdateWindow : StyleableWindow
|
public partial class TitleUpdateWindow : StyleableWindow
|
||||||
{
|
{
|
||||||
private readonly string _updateJsonPath;
|
private readonly string _titleUpdateJsonPath;
|
||||||
private TitleUpdateMetadata _titleUpdateWindowData;
|
private TitleUpdateMetadata _titleUpdateWindowData;
|
||||||
|
|
||||||
public VirtualFileSystem VirtualFileSystem { get; }
|
public VirtualFileSystem VirtualFileSystem { get; }
|
||||||
@@ -46,7 +45,6 @@ namespace Ryujinx.Ava.Ui.Windows
|
|||||||
DataContext = this;
|
DataContext = this;
|
||||||
|
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
AttachDebugDevTools();
|
|
||||||
|
|
||||||
Title = $"Ryujinx {Program.Version} - " + LocaleManager.Instance["UpdateWindowTitle"];
|
Title = $"Ryujinx {Program.Version} - " + LocaleManager.Instance["UpdateWindowTitle"];
|
||||||
}
|
}
|
||||||
@@ -57,33 +55,30 @@ namespace Ryujinx.Ava.Ui.Windows
|
|||||||
TitleId = titleId;
|
TitleId = titleId;
|
||||||
TitleName = titleName;
|
TitleName = titleName;
|
||||||
|
|
||||||
_updateJsonPath = Path.Combine(AppDataManager.GamesDirPath, titleId, "updates.json");
|
_titleUpdateJsonPath = Path.Combine(AppDataManager.GamesDirPath, titleId, "updates.json");
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_titleUpdateWindowData = JsonHelper.DeserializeFromFile<TitleUpdateMetadata>(_updateJsonPath);
|
_titleUpdateWindowData = JsonHelper.DeserializeFromFile<TitleUpdateMetadata>(_titleUpdateJsonPath);
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
_titleUpdateWindowData = new TitleUpdateMetadata {Selected = "", Paths = new List<string>()};
|
_titleUpdateWindowData = new TitleUpdateMetadata
|
||||||
|
{
|
||||||
|
Selected = "",
|
||||||
|
Paths = new List<string>()
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
DataContext = this;
|
DataContext = this;
|
||||||
|
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
AttachDebugDevTools();
|
|
||||||
|
|
||||||
Title = $"Ryujinx {Program.Version} - " + LocaleManager.Instance["UpdateWindowTitle"];
|
Title = $"Ryujinx {Program.Version} - " + LocaleManager.Instance["UpdateWindowTitle"];
|
||||||
|
|
||||||
LoadUpdates();
|
LoadUpdates();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Conditional("DEBUG")]
|
|
||||||
private void AttachDebugDevTools()
|
|
||||||
{
|
|
||||||
this.AttachDevTools();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void LoadUpdates()
|
private void LoadUpdates()
|
||||||
{
|
{
|
||||||
TitleUpdates.Add(new TitleUpdateModel(default, string.Empty, true));
|
TitleUpdates.Add(new TitleUpdateModel(default, string.Empty, true));
|
||||||
@@ -126,8 +121,7 @@ namespace Ryujinx.Ava.Ui.Windows
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
(Nca patchNca, Nca controlNca) =
|
(Nca patchNca, Nca controlNca) = ApplicationLoader.GetGameUpdateDataFromPartition(VirtualFileSystem, nsp, TitleId, 0);
|
||||||
ApplicationLoader.GetGameUpdateDataFromPartition(VirtualFileSystem, nsp, TitleId, 0);
|
|
||||||
|
|
||||||
if (controlNca != null && patchNca != null)
|
if (controlNca != null && patchNca != null)
|
||||||
{
|
{
|
||||||
@@ -135,11 +129,8 @@ namespace Ryujinx.Ava.Ui.Windows
|
|||||||
|
|
||||||
using var nacpFile = new UniqueRef<IFile>();
|
using var nacpFile = new UniqueRef<IFile>();
|
||||||
|
|
||||||
controlNca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None)
|
controlNca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None).OpenFile(ref nacpFile.Ref(), "/control.nacp".ToU8Span(), OpenMode.Read).ThrowIfFailure();
|
||||||
.OpenFile(ref nacpFile.Ref(), "/control.nacp".ToU8Span(), OpenMode.Read)
|
nacpFile.Get.Read(out _, 0, SpanHelpers.AsByteSpan(ref controlData), ReadOption.None).ThrowIfFailure();
|
||||||
.ThrowIfFailure();
|
|
||||||
nacpFile.Get.Read(out _, 0, SpanHelpers.AsByteSpan(ref controlData), ReadOption.None)
|
|
||||||
.ThrowIfFailure();
|
|
||||||
|
|
||||||
TitleUpdates.Add(new TitleUpdateModel(controlData, path));
|
TitleUpdates.Add(new TitleUpdateModel(controlData, path));
|
||||||
}
|
}
|
||||||
@@ -190,9 +181,17 @@ namespace Ryujinx.Ava.Ui.Windows
|
|||||||
|
|
||||||
public async void Add()
|
public async void Add()
|
||||||
{
|
{
|
||||||
OpenFileDialog dialog = new OpenFileDialog() { Title = LocaleManager.Instance["SelectUpdateDialogTitle"], AllowMultiple = true };
|
OpenFileDialog dialog = new OpenFileDialog()
|
||||||
|
{
|
||||||
|
Title = LocaleManager.Instance["SelectUpdateDialogTitle"],
|
||||||
|
AllowMultiple = true
|
||||||
|
};
|
||||||
|
|
||||||
dialog.Filters.Add(new FileDialogFilter { Name = "NSP", Extensions = { "nsp" } });
|
dialog.Filters.Add(new FileDialogFilter
|
||||||
|
{
|
||||||
|
Name = "NSP",
|
||||||
|
Extensions = { "nsp" }
|
||||||
|
});
|
||||||
|
|
||||||
string[] files = await dialog.ShowAsync(this);
|
string[] files = await dialog.ShowAsync(this);
|
||||||
|
|
||||||
@@ -222,12 +221,10 @@ namespace Ryujinx.Ava.Ui.Windows
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Version.Parse(first.Control.DisplayVersionString.ToString())
|
return Version.Parse(first.Control.DisplayVersionString.ToString()).CompareTo(Version.Parse(second.Control.DisplayVersionString.ToString())) * -1;
|
||||||
.CompareTo(Version.Parse(second.Control.DisplayVersionString.ToString())) * -1;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
TitleUpdates.Clear();
|
TitleUpdates.Clear();
|
||||||
|
|
||||||
TitleUpdates.AddRange(list);
|
TitleUpdates.AddRange(list);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -247,9 +244,9 @@ namespace Ryujinx.Ava.Ui.Windows
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
using (FileStream dlcJsonStream = File.Create(_updateJsonPath, 4096, FileOptions.WriteThrough))
|
using (FileStream titleUpdateJsonStream = File.Create(_titleUpdateJsonPath, 4096, FileOptions.WriteThrough))
|
||||||
{
|
{
|
||||||
dlcJsonStream.Write(Encoding.UTF8.GetBytes(JsonHelper.Serialize(_titleUpdateWindowData, true)));
|
titleUpdateJsonStream.Write(Encoding.UTF8.GetBytes(JsonHelper.Serialize(_titleUpdateWindowData, true)));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Owner is MainWindow window)
|
if (Owner is MainWindow window)
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
using Avalonia;
|
using Avalonia;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Markup.Xaml;
|
|
||||||
using Ryujinx.Ava.Common.Locale;
|
using Ryujinx.Ava.Common.Locale;
|
||||||
using Ryujinx.Modules;
|
using Ryujinx.Modules;
|
||||||
using System;
|
using System;
|
||||||
@@ -23,9 +22,7 @@ namespace Ryujinx.Ava.Ui.Windows
|
|||||||
DataContext = this;
|
DataContext = this;
|
||||||
|
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
#if DEBUG
|
|
||||||
this.AttachDevTools();
|
|
||||||
#endif
|
|
||||||
Title = LocaleManager.Instance["RyujinxUpdater"];
|
Title = LocaleManager.Instance["RyujinxUpdater"];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace Ryujinx.Common.Configuration
|
|
||||||
{
|
|
||||||
public struct DlcContainer
|
|
||||||
{
|
|
||||||
public string Path { get; set; }
|
|
||||||
public List<DlcNca> DlcNcaList { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
namespace Ryujinx.Common.Configuration
|
|
||||||
{
|
|
||||||
public struct DlcNca
|
|
||||||
{
|
|
||||||
public string Path { get; set; }
|
|
||||||
public ulong TitleId { get; set; }
|
|
||||||
public bool Enabled { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
13
Ryujinx.Common/Configuration/DownloadableContentContainer.cs
Normal file
13
Ryujinx.Common/Configuration/DownloadableContentContainer.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Ryujinx.Common.Configuration
|
||||||
|
{
|
||||||
|
public struct DownloadableContentContainer
|
||||||
|
{
|
||||||
|
[JsonPropertyName("path")]
|
||||||
|
public string ContainerPath { get; set; }
|
||||||
|
[JsonPropertyName("dlc_nca_list")]
|
||||||
|
public List<DownloadableContentNca> DownloadableContentNcaList { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
14
Ryujinx.Common/Configuration/DownloadableContentNca.cs
Normal file
14
Ryujinx.Common/Configuration/DownloadableContentNca.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Ryujinx.Common.Configuration
|
||||||
|
{
|
||||||
|
public struct DownloadableContentNca
|
||||||
|
{
|
||||||
|
[JsonPropertyName("path")]
|
||||||
|
public string FullPath { get; set; }
|
||||||
|
[JsonPropertyName("title_id")]
|
||||||
|
public ulong TitleId { get; set; }
|
||||||
|
[JsonPropertyName("is_enabled")]
|
||||||
|
public bool Enabled { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,5 +7,7 @@
|
|||||||
public Key ShowUi { get; set; }
|
public Key ShowUi { get; set; }
|
||||||
public Key Pause { get; set; }
|
public Key Pause { get; set; }
|
||||||
public Key ToggleMute { get; set; }
|
public Key ToggleMute { get; set; }
|
||||||
|
public Key ResScaleUp { get; set; }
|
||||||
|
public Key ResScaleDown { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
|
using static Ryujinx.Common.Memory.PartialUnmaps.PartialUnmapHelpers;
|
||||||
|
|
||||||
|
namespace Ryujinx.Common.Memory.PartialUnmaps
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A simple implementation of a ReaderWriterLock which can be used from native code.
|
||||||
|
/// </summary>
|
||||||
|
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||||
|
public struct NativeReaderWriterLock
|
||||||
|
{
|
||||||
|
public int WriteLock;
|
||||||
|
public int ReaderCount;
|
||||||
|
|
||||||
|
public static int WriteLockOffset;
|
||||||
|
public static int ReaderCountOffset;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Populates the field offsets for use when emitting native code.
|
||||||
|
/// </summary>
|
||||||
|
static NativeReaderWriterLock()
|
||||||
|
{
|
||||||
|
NativeReaderWriterLock instance = new NativeReaderWriterLock();
|
||||||
|
|
||||||
|
WriteLockOffset = OffsetOf(ref instance, ref instance.WriteLock);
|
||||||
|
ReaderCountOffset = OffsetOf(ref instance, ref instance.ReaderCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Acquires the reader lock.
|
||||||
|
/// </summary>
|
||||||
|
public void AcquireReaderLock()
|
||||||
|
{
|
||||||
|
// Must take write lock for a very short time to become a reader.
|
||||||
|
|
||||||
|
while (Interlocked.CompareExchange(ref WriteLock, 1, 0) != 0) { }
|
||||||
|
|
||||||
|
Interlocked.Increment(ref ReaderCount);
|
||||||
|
|
||||||
|
Interlocked.Exchange(ref WriteLock, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Releases the reader lock.
|
||||||
|
/// </summary>
|
||||||
|
public void ReleaseReaderLock()
|
||||||
|
{
|
||||||
|
Interlocked.Decrement(ref ReaderCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Upgrades to a writer lock. The reader lock is temporarily released while obtaining the writer lock.
|
||||||
|
/// </summary>
|
||||||
|
public void UpgradeToWriterLock()
|
||||||
|
{
|
||||||
|
// Prevent any more threads from entering reader.
|
||||||
|
// If the write lock is already taken, wait for it to not be taken.
|
||||||
|
|
||||||
|
Interlocked.Decrement(ref ReaderCount);
|
||||||
|
|
||||||
|
while (Interlocked.CompareExchange(ref WriteLock, 1, 0) != 0) { }
|
||||||
|
|
||||||
|
// Wait for reader count to drop to 0, then take the lock again as the only reader.
|
||||||
|
|
||||||
|
while (Interlocked.CompareExchange(ref ReaderCount, 1, 0) != 0) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Downgrades from a writer lock, back to a reader one.
|
||||||
|
/// </summary>
|
||||||
|
public void DowngradeFromWriterLock()
|
||||||
|
{
|
||||||
|
// Release the WriteLock.
|
||||||
|
|
||||||
|
Interlocked.Exchange(ref WriteLock, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
20
Ryujinx.Common/Memory/PartialUnmaps/PartialUnmapHelpers.cs
Normal file
20
Ryujinx.Common/Memory/PartialUnmaps/PartialUnmapHelpers.cs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
|
namespace Ryujinx.Common.Memory.PartialUnmaps
|
||||||
|
{
|
||||||
|
static class PartialUnmapHelpers
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Calculates a byte offset of a given field within a struct.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">Struct type</typeparam>
|
||||||
|
/// <typeparam name="T2">Field type</typeparam>
|
||||||
|
/// <param name="storage">Parent struct</param>
|
||||||
|
/// <param name="target">Field</param>
|
||||||
|
/// <returns>The byte offset of the given field in the given struct</returns>
|
||||||
|
public static int OffsetOf<T, T2>(ref T2 storage, ref T target)
|
||||||
|
{
|
||||||
|
return (int)Unsafe.ByteOffset(ref Unsafe.As<T2, T>(ref storage), ref target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
160
Ryujinx.Common/Memory/PartialUnmaps/PartialUnmapState.cs
Normal file
160
Ryujinx.Common/Memory/PartialUnmaps/PartialUnmapState.cs
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
using System;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Runtime.Versioning;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
|
using static Ryujinx.Common.Memory.PartialUnmaps.PartialUnmapHelpers;
|
||||||
|
|
||||||
|
namespace Ryujinx.Common.Memory.PartialUnmaps
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// State for partial unmaps. Intended to be used on Windows.
|
||||||
|
/// </summary>
|
||||||
|
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||||
|
public struct PartialUnmapState
|
||||||
|
{
|
||||||
|
public NativeReaderWriterLock PartialUnmapLock;
|
||||||
|
public int PartialUnmapsCount;
|
||||||
|
public ThreadLocalMap<int> LocalCounts;
|
||||||
|
|
||||||
|
public readonly static int PartialUnmapLockOffset;
|
||||||
|
public readonly static int PartialUnmapsCountOffset;
|
||||||
|
public readonly static int LocalCountsOffset;
|
||||||
|
|
||||||
|
public readonly static IntPtr GlobalState;
|
||||||
|
|
||||||
|
[SupportedOSPlatform("windows")]
|
||||||
|
[DllImport("kernel32.dll")]
|
||||||
|
public static extern int GetCurrentThreadId();
|
||||||
|
|
||||||
|
[SupportedOSPlatform("windows")]
|
||||||
|
[DllImport("kernel32.dll", SetLastError = true)]
|
||||||
|
static extern IntPtr OpenThread(int dwDesiredAccess, bool bInheritHandle, uint dwThreadId);
|
||||||
|
|
||||||
|
[SupportedOSPlatform("windows")]
|
||||||
|
[DllImport("kernel32.dll", SetLastError = true)]
|
||||||
|
public static extern bool CloseHandle(IntPtr hObject);
|
||||||
|
|
||||||
|
[SupportedOSPlatform("windows")]
|
||||||
|
[DllImport("kernel32.dll", SetLastError = true)]
|
||||||
|
static extern bool GetExitCodeThread(IntPtr hThread, out uint lpExitCode);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a global static PartialUnmapState and populates the field offsets.
|
||||||
|
/// </summary>
|
||||||
|
static unsafe PartialUnmapState()
|
||||||
|
{
|
||||||
|
PartialUnmapState instance = new PartialUnmapState();
|
||||||
|
|
||||||
|
PartialUnmapLockOffset = OffsetOf(ref instance, ref instance.PartialUnmapLock);
|
||||||
|
PartialUnmapsCountOffset = OffsetOf(ref instance, ref instance.PartialUnmapsCount);
|
||||||
|
LocalCountsOffset = OffsetOf(ref instance, ref instance.LocalCounts);
|
||||||
|
|
||||||
|
int size = Unsafe.SizeOf<PartialUnmapState>();
|
||||||
|
GlobalState = Marshal.AllocHGlobal(size);
|
||||||
|
Unsafe.InitBlockUnaligned((void*)GlobalState, 0, (uint)size);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Resets the global state.
|
||||||
|
/// </summary>
|
||||||
|
public static unsafe void Reset()
|
||||||
|
{
|
||||||
|
int size = Unsafe.SizeOf<PartialUnmapState>();
|
||||||
|
Unsafe.InitBlockUnaligned((void*)GlobalState, 0, (uint)size);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a reference to the global state.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>A reference to the global state</returns>
|
||||||
|
public static unsafe ref PartialUnmapState GetRef()
|
||||||
|
{
|
||||||
|
return ref Unsafe.AsRef<PartialUnmapState>((void*)GlobalState);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if an access violation handler should retry execution due to a fault caused by partial unmap.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Due to Windows limitations, <see cref="UnmapView"/> might need to unmap more memory than requested.
|
||||||
|
/// The additional memory that was unmapped is later remapped, however this leaves a time gap where the
|
||||||
|
/// memory might be accessed but is unmapped. Users of the API must compensate for that by catching the
|
||||||
|
/// access violation and retrying if it happened between the unmap and remap operation.
|
||||||
|
/// This method can be used to decide if retrying in such cases is necessary or not.
|
||||||
|
///
|
||||||
|
/// This version of the function is not used, but serves as a reference for the native
|
||||||
|
/// implementation in ARMeilleure.
|
||||||
|
/// </remarks>
|
||||||
|
/// <returns>True if execution should be retried, false otherwise</returns>
|
||||||
|
[SupportedOSPlatform("windows")]
|
||||||
|
public bool RetryFromAccessViolation()
|
||||||
|
{
|
||||||
|
PartialUnmapLock.AcquireReaderLock();
|
||||||
|
|
||||||
|
int threadID = GetCurrentThreadId();
|
||||||
|
int threadIndex = LocalCounts.GetOrReserve(threadID, 0);
|
||||||
|
|
||||||
|
if (threadIndex == -1)
|
||||||
|
{
|
||||||
|
// Out of thread local space... try again later.
|
||||||
|
|
||||||
|
PartialUnmapLock.ReleaseReaderLock();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
ref int threadLocalPartialUnmapsCount = ref LocalCounts.GetValue(threadIndex);
|
||||||
|
|
||||||
|
bool retry = threadLocalPartialUnmapsCount != PartialUnmapsCount;
|
||||||
|
if (retry)
|
||||||
|
{
|
||||||
|
threadLocalPartialUnmapsCount = PartialUnmapsCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
PartialUnmapLock.ReleaseReaderLock();
|
||||||
|
|
||||||
|
return retry;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Iterates and trims threads in the thread -> count map that
|
||||||
|
/// are no longer active.
|
||||||
|
/// </summary>
|
||||||
|
[SupportedOSPlatform("windows")]
|
||||||
|
public void TrimThreads()
|
||||||
|
{
|
||||||
|
const uint ExitCodeStillActive = 259;
|
||||||
|
const int ThreadQueryInformation = 0x40;
|
||||||
|
|
||||||
|
Span<int> ids = LocalCounts.ThreadIds.ToSpan();
|
||||||
|
|
||||||
|
for (int i = 0; i < ids.Length; i++)
|
||||||
|
{
|
||||||
|
int id = ids[i];
|
||||||
|
|
||||||
|
if (id != 0)
|
||||||
|
{
|
||||||
|
IntPtr handle = OpenThread(ThreadQueryInformation, false, (uint)id);
|
||||||
|
|
||||||
|
if (handle == IntPtr.Zero)
|
||||||
|
{
|
||||||
|
Interlocked.CompareExchange(ref ids[i], 0, id);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
GetExitCodeThread(handle, out uint exitCode);
|
||||||
|
|
||||||
|
if (exitCode != ExitCodeStillActive)
|
||||||
|
{
|
||||||
|
Interlocked.CompareExchange(ref ids[i], 0, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
CloseHandle(handle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
92
Ryujinx.Common/Memory/PartialUnmaps/ThreadLocalMap.cs
Normal file
92
Ryujinx.Common/Memory/PartialUnmaps/ThreadLocalMap.cs
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
|
using static Ryujinx.Common.Memory.PartialUnmaps.PartialUnmapHelpers;
|
||||||
|
|
||||||
|
namespace Ryujinx.Common.Memory.PartialUnmaps
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A simple fixed size thread safe map that can be used from native code.
|
||||||
|
/// Integer thread IDs map to corresponding structs.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The value type for the map</typeparam>
|
||||||
|
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||||
|
public struct ThreadLocalMap<T> where T : unmanaged
|
||||||
|
{
|
||||||
|
public const int MapSize = 20;
|
||||||
|
|
||||||
|
public Array20<int> ThreadIds;
|
||||||
|
public Array20<T> Structs;
|
||||||
|
|
||||||
|
public static int ThreadIdsOffset;
|
||||||
|
public static int StructsOffset;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Populates the field offsets for use when emitting native code.
|
||||||
|
/// </summary>
|
||||||
|
static ThreadLocalMap()
|
||||||
|
{
|
||||||
|
ThreadLocalMap<T> instance = new ThreadLocalMap<T>();
|
||||||
|
|
||||||
|
ThreadIdsOffset = OffsetOf(ref instance, ref instance.ThreadIds);
|
||||||
|
StructsOffset = OffsetOf(ref instance, ref instance.Structs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the index of a given thread ID in the map, or reserves one.
|
||||||
|
/// When reserving a struct, its value is set to the given initial value.
|
||||||
|
/// Returns -1 when there is no space to reserve a new entry.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="threadId">Thread ID to use as a key</param>
|
||||||
|
/// <param name="initial">Initial value of the associated struct.</param>
|
||||||
|
/// <returns>The index of the entry, or -1 if none</returns>
|
||||||
|
public int GetOrReserve(int threadId, T initial)
|
||||||
|
{
|
||||||
|
// Try get a match first.
|
||||||
|
|
||||||
|
for (int i = 0; i < MapSize; i++)
|
||||||
|
{
|
||||||
|
int compare = Interlocked.CompareExchange(ref ThreadIds[i], threadId, threadId);
|
||||||
|
|
||||||
|
if (compare == threadId)
|
||||||
|
{
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try get a free entry. Since the id is assumed to be unique to this thread, we know it doesn't exist yet.
|
||||||
|
|
||||||
|
for (int i = 0; i < MapSize; i++)
|
||||||
|
{
|
||||||
|
int compare = Interlocked.CompareExchange(ref ThreadIds[i], threadId, 0);
|
||||||
|
|
||||||
|
if (compare == 0)
|
||||||
|
{
|
||||||
|
Structs[i] = initial;
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the struct value for a given map entry.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="index">Index of the entry</param>
|
||||||
|
/// <returns>A reference to the struct value</returns>
|
||||||
|
public ref T GetValue(int index)
|
||||||
|
{
|
||||||
|
return ref Structs[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Releases an entry from the map.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="index">Index of the entry to release</param>
|
||||||
|
public void Release(int index)
|
||||||
|
{
|
||||||
|
Interlocked.Exchange(ref ThreadIds[index], 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -89,10 +89,10 @@ namespace Ryujinx.Cpu.Jit
|
|||||||
MemoryAllocationFlags asFlags = MemoryAllocationFlags.Reserve | MemoryAllocationFlags.ViewCompatible;
|
MemoryAllocationFlags asFlags = MemoryAllocationFlags.Reserve | MemoryAllocationFlags.ViewCompatible;
|
||||||
|
|
||||||
_addressSpace = new MemoryBlock(asSize, asFlags);
|
_addressSpace = new MemoryBlock(asSize, asFlags);
|
||||||
_addressSpaceMirror = new MemoryBlock(asSize, asFlags | MemoryAllocationFlags.ForceWindows4KBViewMapping);
|
_addressSpaceMirror = new MemoryBlock(asSize, asFlags);
|
||||||
|
|
||||||
Tracking = new MemoryTracking(this, PageSize, invalidAccessHandler);
|
Tracking = new MemoryTracking(this, PageSize, invalidAccessHandler);
|
||||||
_memoryEh = new MemoryEhMeilleure(_addressSpace, Tracking);
|
_memoryEh = new MemoryEhMeilleure(_addressSpace, _addressSpaceMirror, Tracking);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -6,36 +6,57 @@ using System.Runtime.InteropServices;
|
|||||||
|
|
||||||
namespace Ryujinx.Cpu
|
namespace Ryujinx.Cpu
|
||||||
{
|
{
|
||||||
class MemoryEhMeilleure : IDisposable
|
public class MemoryEhMeilleure : IDisposable
|
||||||
{
|
{
|
||||||
private delegate bool TrackingEventDelegate(ulong address, ulong size, bool write, bool precise = false);
|
private delegate bool TrackingEventDelegate(ulong address, ulong size, bool write, bool precise = false);
|
||||||
|
|
||||||
private readonly MemoryBlock _addressSpace;
|
|
||||||
private readonly MemoryTracking _tracking;
|
private readonly MemoryTracking _tracking;
|
||||||
private readonly TrackingEventDelegate _trackingEvent;
|
private readonly TrackingEventDelegate _trackingEvent;
|
||||||
|
|
||||||
private readonly ulong _baseAddress;
|
private readonly ulong _baseAddress;
|
||||||
|
private readonly ulong _mirrorAddress;
|
||||||
|
|
||||||
public MemoryEhMeilleure(MemoryBlock addressSpace, MemoryTracking tracking)
|
public MemoryEhMeilleure(MemoryBlock addressSpace, MemoryBlock addressSpaceMirror, MemoryTracking tracking)
|
||||||
{
|
{
|
||||||
_addressSpace = addressSpace;
|
|
||||||
_tracking = tracking;
|
_tracking = tracking;
|
||||||
|
|
||||||
_baseAddress = (ulong)_addressSpace.Pointer;
|
_baseAddress = (ulong)addressSpace.Pointer;
|
||||||
ulong endAddress = _baseAddress + addressSpace.Size;
|
ulong endAddress = _baseAddress + addressSpace.Size;
|
||||||
|
|
||||||
_trackingEvent = new TrackingEventDelegate(tracking.VirtualMemoryEventEh);
|
_trackingEvent = new TrackingEventDelegate(tracking.VirtualMemoryEvent);
|
||||||
bool added = NativeSignalHandler.AddTrackedRegion((nuint)_baseAddress, (nuint)endAddress, Marshal.GetFunctionPointerForDelegate(_trackingEvent));
|
bool added = NativeSignalHandler.AddTrackedRegion((nuint)_baseAddress, (nuint)endAddress, Marshal.GetFunctionPointerForDelegate(_trackingEvent));
|
||||||
|
|
||||||
if (!added)
|
if (!added)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException("Number of allowed tracked regions exceeded.");
|
throw new InvalidOperationException("Number of allowed tracked regions exceeded.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (OperatingSystem.IsWindows())
|
||||||
|
{
|
||||||
|
// Add a tracking event with no signal handler for the mirror on Windows.
|
||||||
|
// The native handler has its own code to check for the partial overlap race when regions are protected by accident,
|
||||||
|
// and when there is no signal handler present.
|
||||||
|
|
||||||
|
_mirrorAddress = (ulong)addressSpaceMirror.Pointer;
|
||||||
|
ulong endAddressMirror = _mirrorAddress + addressSpace.Size;
|
||||||
|
|
||||||
|
bool addedMirror = NativeSignalHandler.AddTrackedRegion((nuint)_mirrorAddress, (nuint)endAddressMirror, IntPtr.Zero);
|
||||||
|
|
||||||
|
if (!addedMirror)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Number of allowed tracked regions exceeded.");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
NativeSignalHandler.RemoveTrackedRegion((nuint)_baseAddress);
|
NativeSignalHandler.RemoveTrackedRegion((nuint)_baseAddress);
|
||||||
|
|
||||||
|
if (_mirrorAddress != 0)
|
||||||
|
{
|
||||||
|
NativeSignalHandler.RemoveTrackedRegion((nuint)_mirrorAddress);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -216,13 +216,14 @@ namespace Ryujinx.Graphics.Gpu.Engine.Dma
|
|||||||
{
|
{
|
||||||
var target = memoryManager.Physical.TextureCache.FindTexture(
|
var target = memoryManager.Physical.TextureCache.FindTexture(
|
||||||
memoryManager,
|
memoryManager,
|
||||||
dst,
|
|
||||||
dstGpuVa,
|
dstGpuVa,
|
||||||
dstBpp,
|
dstBpp,
|
||||||
dstStride,
|
dstStride,
|
||||||
|
dst.Height,
|
||||||
xCount,
|
xCount,
|
||||||
yCount,
|
yCount,
|
||||||
dstLinear);
|
dstLinear,
|
||||||
|
dst.MemoryLayout);
|
||||||
|
|
||||||
if (target != null)
|
if (target != null)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -59,9 +59,24 @@ namespace Ryujinx.Graphics.Gpu
|
|||||||
{
|
{
|
||||||
oldMemoryManager.Physical.BufferCache.NotifyBuffersModified -= BufferManager.Rebind;
|
oldMemoryManager.Physical.BufferCache.NotifyBuffersModified -= BufferManager.Rebind;
|
||||||
oldMemoryManager.Physical.DecrementReferenceCount();
|
oldMemoryManager.Physical.DecrementReferenceCount();
|
||||||
|
oldMemoryManager.MemoryUnmapped -= MemoryUnmappedHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
memoryManager.Physical.BufferCache.NotifyBuffersModified += BufferManager.Rebind;
|
memoryManager.Physical.BufferCache.NotifyBuffersModified += BufferManager.Rebind;
|
||||||
|
memoryManager.MemoryUnmapped += MemoryUnmappedHandler;
|
||||||
|
|
||||||
|
// Since the memory manager changed, make sure we will get pools from addresses of the new memory manager.
|
||||||
|
TextureManager.ReloadPools();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Memory mappings change event handler.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="sender">Memory manager where the mappings changed</param>
|
||||||
|
/// <param name="e">Information about the region that is being changed</param>
|
||||||
|
private void MemoryUnmappedHandler(object sender, UnmapEventArgs e)
|
||||||
|
{
|
||||||
|
TextureManager.ReloadPools();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
129
Ryujinx.Graphics.Gpu/Image/PoolCache.cs
Normal file
129
Ryujinx.Graphics.Gpu/Image/PoolCache.cs
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Ryujinx.Graphics.Gpu.Image
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Resource pool interface.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">Resource pool type</typeparam>
|
||||||
|
interface IPool<T>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Start address of the pool in memory.
|
||||||
|
/// </summary>
|
||||||
|
ulong Address { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Linked list node used on the texture pool cache.
|
||||||
|
/// </summary>
|
||||||
|
LinkedListNode<T> CacheNode { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Timestamp set on the last use of the pool by the cache.
|
||||||
|
/// </summary>
|
||||||
|
ulong CacheTimestamp { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Pool cache.
|
||||||
|
/// This can keep multiple pools, and return the current one as needed.
|
||||||
|
/// </summary>
|
||||||
|
abstract class PoolCache<T> : IDisposable where T : IPool<T>, IDisposable
|
||||||
|
{
|
||||||
|
private const int MaxCapacity = 2;
|
||||||
|
private const ulong MinDeltaForRemoval = 20000;
|
||||||
|
|
||||||
|
private readonly GpuContext _context;
|
||||||
|
private readonly LinkedList<T> _pools;
|
||||||
|
private ulong _currentTimestamp;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Constructs a new instance of the pool.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context">GPU context that the texture pool belongs to</param>
|
||||||
|
public PoolCache(GpuContext context)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
_pools = new LinkedList<T>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Increments the internal timestamp of the cache that is used to decide when old resources will be deleted.
|
||||||
|
/// </summary>
|
||||||
|
public void Tick()
|
||||||
|
{
|
||||||
|
_currentTimestamp++;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Finds a cache texture pool, or creates a new one if not found.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="channel">GPU channel that the texture pool cache belongs to</param>
|
||||||
|
/// <param name="address">Start address of the texture pool</param>
|
||||||
|
/// <param name="maximumId">Maximum ID of the texture pool</param>
|
||||||
|
/// <returns>The found or newly created texture pool</returns>
|
||||||
|
public T FindOrCreate(GpuChannel channel, ulong address, int maximumId)
|
||||||
|
{
|
||||||
|
// Remove old entries from the cache, if possible.
|
||||||
|
while (_pools.Count > MaxCapacity && (_currentTimestamp - _pools.First.Value.CacheTimestamp) >= MinDeltaForRemoval)
|
||||||
|
{
|
||||||
|
T oldestPool = _pools.First.Value;
|
||||||
|
|
||||||
|
_pools.RemoveFirst();
|
||||||
|
oldestPool.Dispose();
|
||||||
|
oldestPool.CacheNode = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
T pool;
|
||||||
|
|
||||||
|
// Try to find the pool on the cache.
|
||||||
|
for (LinkedListNode<T> node = _pools.First; node != null; node = node.Next)
|
||||||
|
{
|
||||||
|
pool = node.Value;
|
||||||
|
|
||||||
|
if (pool.Address == address)
|
||||||
|
{
|
||||||
|
if (pool.CacheNode != _pools.Last)
|
||||||
|
{
|
||||||
|
_pools.Remove(pool.CacheNode);
|
||||||
|
|
||||||
|
pool.CacheNode = _pools.AddLast(pool);
|
||||||
|
}
|
||||||
|
|
||||||
|
pool.CacheTimestamp = _currentTimestamp;
|
||||||
|
|
||||||
|
return pool;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If not found, create a new one.
|
||||||
|
pool = CreatePool(_context, channel, address, maximumId);
|
||||||
|
|
||||||
|
pool.CacheNode = _pools.AddLast(pool);
|
||||||
|
pool.CacheTimestamp = _currentTimestamp;
|
||||||
|
|
||||||
|
return pool;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new instance of the pool.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context">GPU context that the pool belongs to</param>
|
||||||
|
/// <param name="channel">GPU channel that the pool belongs to</param>
|
||||||
|
/// <param name="address">Address of the pool in guest memory</param>
|
||||||
|
/// <param name="maximumId">Maximum ID of the pool (equal to maximum minus one)</param>
|
||||||
|
protected abstract T CreatePool(GpuContext context, GpuChannel channel, ulong address, int maximumId);
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
foreach (T pool in _pools)
|
||||||
|
{
|
||||||
|
pool.Dispose();
|
||||||
|
pool.CacheNode = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
_pools.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,16 +1,27 @@
|
|||||||
using Ryujinx.Graphics.Gpu.Memory;
|
using Ryujinx.Graphics.Gpu.Memory;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace Ryujinx.Graphics.Gpu.Image
|
namespace Ryujinx.Graphics.Gpu.Image
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Sampler pool.
|
/// Sampler pool.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
class SamplerPool : Pool<Sampler, SamplerDescriptor>
|
class SamplerPool : Pool<Sampler, SamplerDescriptor>, IPool<SamplerPool>
|
||||||
{
|
{
|
||||||
private float _forcedAnisotropy;
|
private float _forcedAnisotropy;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Constructs a new instance of the sampler pool.
|
/// Linked list node used on the sampler pool cache.
|
||||||
|
/// </summary>
|
||||||
|
public LinkedListNode<SamplerPool> CacheNode { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Timestamp used by the sampler pool cache, updated on every use of this sampler pool.
|
||||||
|
/// </summary>
|
||||||
|
public ulong CacheTimestamp { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new instance of the sampler pool.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="context">GPU context that the sampler pool belongs to</param>
|
/// <param name="context">GPU context that the sampler pool belongs to</param>
|
||||||
/// <param name="physicalMemory">Physical memory where the sampler descriptors are mapped</param>
|
/// <param name="physicalMemory">Physical memory where the sampler descriptors are mapped</param>
|
||||||
|
|||||||
30
Ryujinx.Graphics.Gpu/Image/SamplerPoolCache.cs
Normal file
30
Ryujinx.Graphics.Gpu/Image/SamplerPoolCache.cs
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
namespace Ryujinx.Graphics.Gpu.Image
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Sampler pool cache.
|
||||||
|
/// This can keep multiple sampler pools, and return the current one as needed.
|
||||||
|
/// It is useful for applications that uses multiple sampler pools.
|
||||||
|
/// </summary>
|
||||||
|
class SamplerPoolCache : PoolCache<SamplerPool>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Constructs a new instance of the texture pool.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context">GPU context that the texture pool belongs to</param>
|
||||||
|
public SamplerPoolCache(GpuContext context) : base(context)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new instance of the sampler pool.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context">GPU context that the sampler pool belongs to</param>
|
||||||
|
/// <param name="channel">GPU channel that the texture pool belongs to</param>
|
||||||
|
/// <param name="address">Address of the sampler pool in guest memory</param>
|
||||||
|
/// <param name="maximumId">Maximum sampler ID of the sampler pool (equal to maximum samplers minus one)</param>
|
||||||
|
protected override SamplerPool CreatePool(GpuContext context, GpuChannel channel, ulong address, int maximumId)
|
||||||
|
{
|
||||||
|
return new SamplerPool(context, channel.MemoryManager.Physical, address, maximumId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,7 +13,7 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Texture bindings manager.
|
/// Texture bindings manager.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
class TextureBindingsManager : IDisposable
|
class TextureBindingsManager
|
||||||
{
|
{
|
||||||
private const int InitialTextureStateSize = 32;
|
private const int InitialTextureStateSize = 32;
|
||||||
private const int InitialImageStateSize = 8;
|
private const int InitialImageStateSize = 8;
|
||||||
@@ -22,15 +22,17 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
|
|
||||||
private readonly bool _isCompute;
|
private readonly bool _isCompute;
|
||||||
|
|
||||||
private SamplerPool _samplerPool;
|
private ulong _texturePoolGpuVa;
|
||||||
|
|
||||||
private SamplerIndex _samplerIndex;
|
|
||||||
|
|
||||||
private ulong _texturePoolAddress;
|
|
||||||
private int _texturePoolMaximumId;
|
private int _texturePoolMaximumId;
|
||||||
|
private TexturePool _texturePool;
|
||||||
|
private ulong _samplerPoolGpuVa;
|
||||||
|
private int _samplerPoolMaximumId;
|
||||||
|
private SamplerIndex _samplerIndex;
|
||||||
|
private SamplerPool _samplerPool;
|
||||||
|
|
||||||
private readonly GpuChannel _channel;
|
private readonly GpuChannel _channel;
|
||||||
private readonly TexturePoolCache _texturePoolCache;
|
private readonly TexturePoolCache _texturePoolCache;
|
||||||
|
private readonly SamplerPoolCache _samplerPoolCache;
|
||||||
|
|
||||||
private TexturePool _cachedTexturePool;
|
private TexturePool _cachedTexturePool;
|
||||||
private SamplerPool _cachedSamplerPool;
|
private SamplerPool _cachedSamplerPool;
|
||||||
@@ -72,14 +74,23 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="context">The GPU context that the texture bindings manager belongs to</param>
|
/// <param name="context">The GPU context that the texture bindings manager belongs to</param>
|
||||||
/// <param name="channel">The GPU channel that the texture bindings manager belongs to</param>
|
/// <param name="channel">The GPU channel that the texture bindings manager belongs to</param>
|
||||||
/// <param name="poolCache">Texture pools cache used to get texture pools from</param>
|
/// <param name="texturePoolCache">Texture pools cache used to get texture pools from</param>
|
||||||
|
/// <param name="samplerPoolCache">Sampler pools cache used to get sampler pools from</param>
|
||||||
/// <param name="scales">Array where the scales for the currently bound textures are stored</param>
|
/// <param name="scales">Array where the scales for the currently bound textures are stored</param>
|
||||||
/// <param name="isCompute">True if the bindings manager is used for the compute engine</param>
|
/// <param name="isCompute">True if the bindings manager is used for the compute engine</param>
|
||||||
public TextureBindingsManager(GpuContext context, GpuChannel channel, TexturePoolCache poolCache, float[] scales, bool isCompute)
|
public TextureBindingsManager(
|
||||||
|
GpuContext context,
|
||||||
|
GpuChannel channel,
|
||||||
|
TexturePoolCache texturePoolCache,
|
||||||
|
SamplerPoolCache samplerPoolCache,
|
||||||
|
float[] scales,
|
||||||
|
bool isCompute)
|
||||||
{
|
{
|
||||||
_context = context;
|
_context = context;
|
||||||
_channel = channel;
|
_channel = channel;
|
||||||
_texturePoolCache = poolCache;
|
_texturePoolCache = texturePoolCache;
|
||||||
|
_samplerPoolCache = samplerPoolCache;
|
||||||
|
|
||||||
_scales = scales;
|
_scales = scales;
|
||||||
_isCompute = isCompute;
|
_isCompute = isCompute;
|
||||||
|
|
||||||
@@ -173,25 +184,10 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
/// <param name="samplerIndex">Type of the sampler pool indexing used for bound samplers</param>
|
/// <param name="samplerIndex">Type of the sampler pool indexing used for bound samplers</param>
|
||||||
public void SetSamplerPool(ulong gpuVa, int maximumId, SamplerIndex samplerIndex)
|
public void SetSamplerPool(ulong gpuVa, int maximumId, SamplerIndex samplerIndex)
|
||||||
{
|
{
|
||||||
if (gpuVa != 0)
|
_samplerPoolGpuVa = gpuVa;
|
||||||
{
|
_samplerPoolMaximumId = maximumId;
|
||||||
ulong address = _channel.MemoryManager.Translate(gpuVa);
|
|
||||||
|
|
||||||
if (_samplerPool != null && _samplerPool.Address == address && _samplerPool.MaximumId >= maximumId)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_samplerPool?.Dispose();
|
|
||||||
_samplerPool = new SamplerPool(_context, _channel.MemoryManager.Physical, address, maximumId);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_samplerPool?.Dispose();
|
|
||||||
_samplerPool = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
_samplerIndex = samplerIndex;
|
_samplerIndex = samplerIndex;
|
||||||
|
_samplerPool = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -201,18 +197,9 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
/// <param name="maximumId">Maximum ID of the pool (total count minus one)</param>
|
/// <param name="maximumId">Maximum ID of the pool (total count minus one)</param>
|
||||||
public void SetTexturePool(ulong gpuVa, int maximumId)
|
public void SetTexturePool(ulong gpuVa, int maximumId)
|
||||||
{
|
{
|
||||||
if (gpuVa != 0)
|
_texturePoolGpuVa = gpuVa;
|
||||||
{
|
|
||||||
ulong address = _channel.MemoryManager.Translate(gpuVa);
|
|
||||||
|
|
||||||
_texturePoolAddress = address;
|
|
||||||
_texturePoolMaximumId = maximumId;
|
_texturePoolMaximumId = maximumId;
|
||||||
}
|
_texturePool = null;
|
||||||
else
|
|
||||||
{
|
|
||||||
_texturePoolAddress = 0;
|
|
||||||
_texturePoolMaximumId = 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -222,13 +209,9 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
/// <param name="samplerId">ID of the sampler</param>
|
/// <param name="samplerId">ID of the sampler</param>
|
||||||
public (Texture, Sampler) GetTextureAndSampler(int textureId, int samplerId)
|
public (Texture, Sampler) GetTextureAndSampler(int textureId, int samplerId)
|
||||||
{
|
{
|
||||||
ulong texturePoolAddress = _texturePoolAddress;
|
(TexturePool texturePool, SamplerPool samplerPool) = GetPools();
|
||||||
|
|
||||||
TexturePool texturePool = texturePoolAddress != 0
|
return (texturePool.Get(textureId), samplerPool.Get(samplerId));
|
||||||
? _texturePoolCache.FindOrCreate(_channel, texturePoolAddress, _texturePoolMaximumId)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
return (texturePool.Get(textureId), _samplerPool.Get(samplerId));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -340,13 +323,7 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
/// <returns>True if all bound textures match the current shader specialiation state, false otherwise</returns>
|
/// <returns>True if all bound textures match the current shader specialiation state, false otherwise</returns>
|
||||||
public bool CommitBindings(ShaderSpecializationState specState)
|
public bool CommitBindings(ShaderSpecializationState specState)
|
||||||
{
|
{
|
||||||
ulong texturePoolAddress = _texturePoolAddress;
|
(TexturePool texturePool, SamplerPool samplerPool) = GetPools();
|
||||||
|
|
||||||
TexturePool texturePool = texturePoolAddress != 0
|
|
||||||
? _texturePoolCache.FindOrCreate(_channel, texturePoolAddress, _texturePoolMaximumId)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
SamplerPool samplerPool = _samplerPool;
|
|
||||||
|
|
||||||
// Check if the texture pool has been modified since bindings were last committed.
|
// Check if the texture pool has been modified since bindings were last committed.
|
||||||
// If it wasn't, then it's possible to avoid looking up textures again when the handle remains the same.
|
// If it wasn't, then it's possible to avoid looking up textures again when the handle remains the same.
|
||||||
@@ -381,7 +358,7 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
|
|
||||||
if (_isCompute)
|
if (_isCompute)
|
||||||
{
|
{
|
||||||
specStateMatches &= CommitTextureBindings(texturePool, ShaderStage.Compute, 0, poolModified, specState);
|
specStateMatches &= CommitTextureBindings(texturePool, samplerPool, ShaderStage.Compute, 0, poolModified, specState);
|
||||||
specStateMatches &= CommitImageBindings(texturePool, ShaderStage.Compute, 0, poolModified, specState);
|
specStateMatches &= CommitImageBindings(texturePool, ShaderStage.Compute, 0, poolModified, specState);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -390,7 +367,7 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
{
|
{
|
||||||
int stageIndex = (int)stage - 1;
|
int stageIndex = (int)stage - 1;
|
||||||
|
|
||||||
specStateMatches &= CommitTextureBindings(texturePool, stage, stageIndex, poolModified, specState);
|
specStateMatches &= CommitTextureBindings(texturePool, samplerPool, stage, stageIndex, poolModified, specState);
|
||||||
specStateMatches &= CommitImageBindings(texturePool, stage, stageIndex, poolModified, specState);
|
specStateMatches &= CommitImageBindings(texturePool, stage, stageIndex, poolModified, specState);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -447,13 +424,20 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
/// Ensures that the texture bindings are visible to the host GPU.
|
/// Ensures that the texture bindings are visible to the host GPU.
|
||||||
/// Note: this actually performs the binding using the host graphics API.
|
/// Note: this actually performs the binding using the host graphics API.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="pool">The current texture pool</param>
|
/// <param name="texturePool">The current texture pool</param>
|
||||||
|
/// <param name="samplerPool">The current sampler pool</param>
|
||||||
/// <param name="stage">The shader stage using the textures to be bound</param>
|
/// <param name="stage">The shader stage using the textures to be bound</param>
|
||||||
/// <param name="stageIndex">The stage number of the specified shader stage</param
|
/// <param name="stageIndex">The stage number of the specified shader stage</param
|
||||||
/// <param name="poolModified">True if either the texture or sampler pool was modified, false otherwise</param>
|
/// <param name="poolModified">True if either the texture or sampler pool was modified, false otherwise</param>
|
||||||
/// <param name="specState">Specialization state for the bound shader</param>
|
/// <param name="specState">Specialization state for the bound shader</param>
|
||||||
/// <returns>True if all bound textures match the current shader specialiation state, false otherwise</returns>
|
/// <returns>True if all bound textures match the current shader specialiation state, false otherwise</returns>
|
||||||
private bool CommitTextureBindings(TexturePool pool, ShaderStage stage, int stageIndex, bool poolModified, ShaderSpecializationState specState)
|
private bool CommitTextureBindings(
|
||||||
|
TexturePool texturePool,
|
||||||
|
SamplerPool samplerPool,
|
||||||
|
ShaderStage stage,
|
||||||
|
int stageIndex,
|
||||||
|
bool poolModified,
|
||||||
|
ShaderSpecializationState specState)
|
||||||
{
|
{
|
||||||
int textureCount = _textureBindingsCount[stageIndex];
|
int textureCount = _textureBindingsCount[stageIndex];
|
||||||
if (textureCount == 0)
|
if (textureCount == 0)
|
||||||
@@ -461,9 +445,7 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
var samplerPool = _samplerPool;
|
if (texturePool == null)
|
||||||
|
|
||||||
if (pool == null)
|
|
||||||
{
|
{
|
||||||
Logger.Error?.Print(LogClass.Gpu, $"Shader stage \"{stage}\" uses textures, but texture pool was not set.");
|
Logger.Error?.Print(LogClass.Gpu, $"Shader stage \"{stage}\" uses textures, but texture pool was not set.");
|
||||||
return true;
|
return true;
|
||||||
@@ -528,7 +510,7 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
state.TextureHandle = textureId;
|
state.TextureHandle = textureId;
|
||||||
state.SamplerHandle = samplerId;
|
state.SamplerHandle = samplerId;
|
||||||
|
|
||||||
ref readonly TextureDescriptor descriptor = ref pool.GetForBinding(textureId, out Texture texture);
|
ref readonly TextureDescriptor descriptor = ref texturePool.GetForBinding(textureId, out Texture texture);
|
||||||
|
|
||||||
specStateMatches &= specState.MatchesTexture(stage, index, descriptor);
|
specStateMatches &= specState.MatchesTexture(stage, index, descriptor);
|
||||||
|
|
||||||
@@ -819,6 +801,54 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
return handle;
|
return handle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the texture and sampler pool for the GPU virtual address that are currently set.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The texture and sampler pools</returns>
|
||||||
|
private (TexturePool, SamplerPool) GetPools()
|
||||||
|
{
|
||||||
|
MemoryManager memoryManager = _channel.MemoryManager;
|
||||||
|
|
||||||
|
TexturePool texturePool = _texturePool;
|
||||||
|
SamplerPool samplerPool = _samplerPool;
|
||||||
|
|
||||||
|
if (texturePool == null)
|
||||||
|
{
|
||||||
|
ulong poolAddress = memoryManager.Translate(_texturePoolGpuVa);
|
||||||
|
|
||||||
|
if (poolAddress != MemoryManager.PteUnmapped)
|
||||||
|
{
|
||||||
|
texturePool = _texturePoolCache.FindOrCreate(_channel, poolAddress, _texturePoolMaximumId);
|
||||||
|
_texturePool = texturePool;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (samplerPool == null)
|
||||||
|
{
|
||||||
|
ulong poolAddress = memoryManager.Translate(_samplerPoolGpuVa);
|
||||||
|
|
||||||
|
if (poolAddress != MemoryManager.PteUnmapped)
|
||||||
|
{
|
||||||
|
samplerPool = _samplerPoolCache.FindOrCreate(_channel, poolAddress, _samplerPoolMaximumId);
|
||||||
|
_samplerPool = samplerPool;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (texturePool, samplerPool);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Forces the texture and sampler pools to be re-loaded from the cache on next use.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This should be called if the memory mappings change, to ensure the correct pools are being used.
|
||||||
|
/// </remarks>
|
||||||
|
public void ReloadPools()
|
||||||
|
{
|
||||||
|
_samplerPool = null;
|
||||||
|
_texturePool = null;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Force all bound textures and images to be rebound the next time CommitBindings is called.
|
/// Force all bound textures and images to be rebound the next time CommitBindings is called.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -827,13 +857,5 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
Array.Clear(_textureState);
|
Array.Clear(_textureState);
|
||||||
Array.Clear(_imageState);
|
Array.Clear(_imageState);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Disposes all textures and samplers in the cache.
|
|
||||||
/// </summary>
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
_samplerPool?.Dispose();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -900,23 +900,25 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
/// Tries to find an existing texture matching the given buffer copy destination. If none is found, returns null.
|
/// Tries to find an existing texture matching the given buffer copy destination. If none is found, returns null.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="memoryManager">GPU memory manager where the texture is mapped</param>
|
/// <param name="memoryManager">GPU memory manager where the texture is mapped</param>
|
||||||
/// <param name="tex">The texture information</param>
|
|
||||||
/// <param name="gpuVa">GPU virtual address of the texture</param>
|
/// <param name="gpuVa">GPU virtual address of the texture</param>
|
||||||
/// <param name="bpp">Bytes per pixel</param>
|
/// <param name="bpp">Bytes per pixel</param>
|
||||||
/// <param name="stride">If <paramref name="linear"/> is true, should have the texture stride, otherwise ignored</param>
|
/// <param name="stride">If <paramref name="linear"/> is true, should have the texture stride, otherwise ignored</param>
|
||||||
|
/// <param name="height">If <paramref name="linear"/> is false, should have the texture height, otherwise ignored</param>
|
||||||
/// <param name="xCount">Number of pixels to be copied per line</param>
|
/// <param name="xCount">Number of pixels to be copied per line</param>
|
||||||
/// <param name="yCount">Number of lines to be copied</param>
|
/// <param name="yCount">Number of lines to be copied</param>
|
||||||
/// <param name="linear">True if the texture has a linear layout, false otherwise</param>
|
/// <param name="linear">True if the texture has a linear layout, false otherwise</param>
|
||||||
|
/// <param name="memoryLayout">If <paramref name="linear"/> is false, should have the memory layout, otherwise ignored</param>
|
||||||
/// <returns>A matching texture, or null if there is no match</returns>
|
/// <returns>A matching texture, or null if there is no match</returns>
|
||||||
public Texture FindTexture(
|
public Texture FindTexture(
|
||||||
MemoryManager memoryManager,
|
MemoryManager memoryManager,
|
||||||
DmaTexture tex,
|
|
||||||
ulong gpuVa,
|
ulong gpuVa,
|
||||||
int bpp,
|
int bpp,
|
||||||
int stride,
|
int stride,
|
||||||
|
int height,
|
||||||
int xCount,
|
int xCount,
|
||||||
int yCount,
|
int yCount,
|
||||||
bool linear)
|
bool linear,
|
||||||
|
MemoryLayout memoryLayout)
|
||||||
{
|
{
|
||||||
ulong address = memoryManager.Translate(gpuVa);
|
ulong address = memoryManager.Translate(gpuVa);
|
||||||
|
|
||||||
@@ -945,7 +947,7 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
{
|
{
|
||||||
// Size is not available for linear textures. Use the stride and end of the copy region instead.
|
// Size is not available for linear textures. Use the stride and end of the copy region instead.
|
||||||
|
|
||||||
match = texture.Info.IsLinear && texture.Info.Stride == stride && tex.RegionY + yCount <= texture.Info.Height;
|
match = texture.Info.IsLinear && texture.Info.Stride == stride && yCount == texture.Info.Height;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -953,10 +955,10 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
// Due to the way linear strided and block layouts work, widths can be multiplied by Bpp for comparison.
|
// Due to the way linear strided and block layouts work, widths can be multiplied by Bpp for comparison.
|
||||||
// Note: tex.Width is the aligned texture size. Prefer param.XCount, as the destination should be a texture with that exact size.
|
// Note: tex.Width is the aligned texture size. Prefer param.XCount, as the destination should be a texture with that exact size.
|
||||||
|
|
||||||
bool sizeMatch = xCount * bpp == texture.Info.Width * format.BytesPerPixel && tex.Height == texture.Info.Height;
|
bool sizeMatch = xCount * bpp == texture.Info.Width * format.BytesPerPixel && height == texture.Info.Height;
|
||||||
bool formatMatch = !texture.Info.IsLinear &&
|
bool formatMatch = !texture.Info.IsLinear &&
|
||||||
texture.Info.GobBlocksInY == tex.MemoryLayout.UnpackGobBlocksInY() &&
|
texture.Info.GobBlocksInY == memoryLayout.UnpackGobBlocksInY() &&
|
||||||
texture.Info.GobBlocksInZ == tex.MemoryLayout.UnpackGobBlocksInZ();
|
texture.Info.GobBlocksInZ == memoryLayout.UnpackGobBlocksInZ();
|
||||||
|
|
||||||
match = sizeMatch && formatMatch;
|
match = sizeMatch && formatMatch;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
private readonly TextureBindingsManager _cpBindingsManager;
|
private readonly TextureBindingsManager _cpBindingsManager;
|
||||||
private readonly TextureBindingsManager _gpBindingsManager;
|
private readonly TextureBindingsManager _gpBindingsManager;
|
||||||
private readonly TexturePoolCache _texturePoolCache;
|
private readonly TexturePoolCache _texturePoolCache;
|
||||||
|
private readonly SamplerPoolCache _samplerPoolCache;
|
||||||
|
|
||||||
private readonly Texture[] _rtColors;
|
private readonly Texture[] _rtColors;
|
||||||
private readonly ITexture[] _rtHostColors;
|
private readonly ITexture[] _rtHostColors;
|
||||||
@@ -41,13 +42,15 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
_channel = channel;
|
_channel = channel;
|
||||||
|
|
||||||
TexturePoolCache texturePoolCache = new TexturePoolCache(context);
|
TexturePoolCache texturePoolCache = new TexturePoolCache(context);
|
||||||
|
SamplerPoolCache samplerPoolCache = new SamplerPoolCache(context);
|
||||||
|
|
||||||
float[] scales = new float[64];
|
float[] scales = new float[64];
|
||||||
new Span<float>(scales).Fill(1f);
|
new Span<float>(scales).Fill(1f);
|
||||||
|
|
||||||
_cpBindingsManager = new TextureBindingsManager(context, channel, texturePoolCache, scales, isCompute: true);
|
_cpBindingsManager = new TextureBindingsManager(context, channel, texturePoolCache, samplerPoolCache, scales, isCompute: true);
|
||||||
_gpBindingsManager = new TextureBindingsManager(context, channel, texturePoolCache, scales, isCompute: false);
|
_gpBindingsManager = new TextureBindingsManager(context, channel, texturePoolCache, samplerPoolCache, scales, isCompute: false);
|
||||||
_texturePoolCache = texturePoolCache;
|
_texturePoolCache = texturePoolCache;
|
||||||
|
_samplerPoolCache = samplerPoolCache;
|
||||||
|
|
||||||
_rtColors = new Texture[Constants.TotalRenderTargets];
|
_rtColors = new Texture[Constants.TotalRenderTargets];
|
||||||
_rtHostColors = new ITexture[Constants.TotalRenderTargets];
|
_rtHostColors = new ITexture[Constants.TotalRenderTargets];
|
||||||
@@ -368,6 +371,10 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
// we must rebind everything.
|
// we must rebind everything.
|
||||||
// Since compute work happens less often, we always do that
|
// Since compute work happens less often, we always do that
|
||||||
// before and after the compute dispatch.
|
// before and after the compute dispatch.
|
||||||
|
|
||||||
|
_texturePoolCache.Tick();
|
||||||
|
_samplerPoolCache.Tick();
|
||||||
|
|
||||||
_cpBindingsManager.Rebind();
|
_cpBindingsManager.Rebind();
|
||||||
bool result = _cpBindingsManager.CommitBindings(specState);
|
bool result = _cpBindingsManager.CommitBindings(specState);
|
||||||
_gpBindingsManager.Rebind();
|
_gpBindingsManager.Rebind();
|
||||||
@@ -382,6 +389,9 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
/// <returns>True if all bound textures match the current shader specialization state, false otherwise</returns>
|
/// <returns>True if all bound textures match the current shader specialization state, false otherwise</returns>
|
||||||
public bool CommitGraphicsBindings(ShaderSpecializationState specState)
|
public bool CommitGraphicsBindings(ShaderSpecializationState specState)
|
||||||
{
|
{
|
||||||
|
_texturePoolCache.Tick();
|
||||||
|
_samplerPoolCache.Tick();
|
||||||
|
|
||||||
bool result = _gpBindingsManager.CommitBindings(specState);
|
bool result = _gpBindingsManager.CommitBindings(specState);
|
||||||
|
|
||||||
UpdateRenderTargets();
|
UpdateRenderTargets();
|
||||||
@@ -501,6 +511,15 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
_context.Renderer.Pipeline.SetRenderTargets(_rtHostColors, _rtHostDs);
|
_context.Renderer.Pipeline.SetRenderTargets(_rtHostColors, _rtHostDs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Forces the texture and sampler pools to be re-loaded from the cache on next use.
|
||||||
|
/// </summary>
|
||||||
|
public void ReloadPools()
|
||||||
|
{
|
||||||
|
_cpBindingsManager.ReloadPools();
|
||||||
|
_gpBindingsManager.ReloadPools();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Forces all textures, samplers, images and render targets to be rebound the next time
|
/// Forces all textures, samplers, images and render targets to be rebound the next time
|
||||||
/// CommitGraphicsBindings is called.
|
/// CommitGraphicsBindings is called.
|
||||||
@@ -523,8 +542,8 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
_cpBindingsManager.Dispose();
|
// Textures are owned by the texture cache, so we shouldn't dispose the texture pool cache.
|
||||||
_gpBindingsManager.Dispose();
|
_samplerPoolCache.Dispose();
|
||||||
|
|
||||||
for (int i = 0; i < _rtColors.Length; i++)
|
for (int i = 0; i < _rtColors.Length; i++)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -10,19 +10,24 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Texture pool.
|
/// Texture pool.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
class TexturePool : Pool<Texture, TextureDescriptor>
|
class TexturePool : Pool<Texture, TextureDescriptor>, IPool<TexturePool>
|
||||||
{
|
{
|
||||||
private readonly GpuChannel _channel;
|
private readonly GpuChannel _channel;
|
||||||
private readonly ConcurrentQueue<Texture> _dereferenceQueue = new ConcurrentQueue<Texture>();
|
private readonly ConcurrentQueue<Texture> _dereferenceQueue = new ConcurrentQueue<Texture>();
|
||||||
private TextureDescriptor _defaultDescriptor;
|
private TextureDescriptor _defaultDescriptor;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Intrusive linked list node used on the texture pool cache.
|
/// Linked list node used on the texture pool cache.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public LinkedListNode<TexturePool> CacheNode { get; set; }
|
public LinkedListNode<TexturePool> CacheNode { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Constructs a new instance of the texture pool.
|
/// Timestamp used by the texture pool cache, updated on every use of this texture pool.
|
||||||
|
/// </summary>
|
||||||
|
public ulong CacheTimestamp { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new instance of the texture pool.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="context">GPU context that the texture pool belongs to</param>
|
/// <param name="context">GPU context that the texture pool belongs to</param>
|
||||||
/// <param name="channel">GPU channel that the texture pool belongs to</param>
|
/// <param name="channel">GPU channel that the texture pool belongs to</param>
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace Ryujinx.Graphics.Gpu.Image
|
namespace Ryujinx.Graphics.Gpu.Image
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -8,69 +5,26 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
/// This can keep multiple texture pools, and return the current one as needed.
|
/// This can keep multiple texture pools, and return the current one as needed.
|
||||||
/// It is useful for applications that uses multiple texture pools.
|
/// It is useful for applications that uses multiple texture pools.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
class TexturePoolCache
|
class TexturePoolCache : PoolCache<TexturePool>
|
||||||
{
|
{
|
||||||
private const int MaxCapacity = 4;
|
|
||||||
|
|
||||||
private readonly GpuContext _context;
|
|
||||||
private readonly LinkedList<TexturePool> _pools;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Constructs a new instance of the texture pool.
|
/// Constructs a new instance of the texture pool.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="context">GPU context that the texture pool belongs to</param>
|
/// <param name="context">GPU context that the texture pool belongs to</param>
|
||||||
public TexturePoolCache(GpuContext context)
|
public TexturePoolCache(GpuContext context) : base(context)
|
||||||
{
|
{
|
||||||
_context = context;
|
|
||||||
_pools = new LinkedList<TexturePool>();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Finds a cache texture pool, or creates a new one if not found.
|
/// Creates a new instance of the texture pool.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="channel">GPU channel that the texture pool cache belongs to</param>
|
/// <param name="context">GPU context that the texture pool belongs to</param>
|
||||||
/// <param name="address">Start address of the texture pool</param>
|
/// <param name="channel">GPU channel that the texture pool belongs to</param>
|
||||||
/// <param name="maximumId">Maximum ID of the texture pool</param>
|
/// <param name="address">Address of the texture pool in guest memory</param>
|
||||||
/// <returns>The found or newly created texture pool</returns>
|
/// <param name="maximumId">Maximum texture ID of the texture pool (equal to maximum textures minus one)</param>
|
||||||
public TexturePool FindOrCreate(GpuChannel channel, ulong address, int maximumId)
|
protected override TexturePool CreatePool(GpuContext context, GpuChannel channel, ulong address, int maximumId)
|
||||||
{
|
{
|
||||||
TexturePool pool;
|
return new TexturePool(context, channel, address, maximumId);
|
||||||
|
|
||||||
// First we try to find the pool.
|
|
||||||
for (LinkedListNode<TexturePool> node = _pools.First; node != null; node = node.Next)
|
|
||||||
{
|
|
||||||
pool = node.Value;
|
|
||||||
|
|
||||||
if (pool.Address == address)
|
|
||||||
{
|
|
||||||
if (pool.CacheNode != _pools.Last)
|
|
||||||
{
|
|
||||||
_pools.Remove(pool.CacheNode);
|
|
||||||
|
|
||||||
pool.CacheNode = _pools.AddLast(pool);
|
|
||||||
}
|
|
||||||
|
|
||||||
return pool;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If not found, create a new one.
|
|
||||||
pool = new TexturePool(_context, channel, address, maximumId);
|
|
||||||
|
|
||||||
pool.CacheNode = _pools.AddLast(pool);
|
|
||||||
|
|
||||||
if (_pools.Count > MaxCapacity)
|
|
||||||
{
|
|
||||||
TexturePool oldestPool = _pools.First.Value;
|
|
||||||
|
|
||||||
_pools.RemoveFirst();
|
|
||||||
|
|
||||||
oldestPool.Dispose();
|
|
||||||
|
|
||||||
oldestPool.CacheNode = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return pool;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -21,7 +21,7 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
|
|||||||
private const ushort FileFormatVersionMajor = 1;
|
private const ushort FileFormatVersionMajor = 1;
|
||||||
private const ushort FileFormatVersionMinor = 1;
|
private const ushort FileFormatVersionMinor = 1;
|
||||||
private const uint FileFormatVersionPacked = ((uint)FileFormatVersionMajor << 16) | FileFormatVersionMinor;
|
private const uint FileFormatVersionPacked = ((uint)FileFormatVersionMajor << 16) | FileFormatVersionMinor;
|
||||||
private const uint CodeGenVersion = 3472;
|
private const uint CodeGenVersion = 3469;
|
||||||
|
|
||||||
private const string SharedTocFileName = "shared.toc";
|
private const string SharedTocFileName = "shared.toc";
|
||||||
private const string SharedDataFileName = "shared.data";
|
private const string SharedDataFileName = "shared.data";
|
||||||
|
|||||||
@@ -104,12 +104,23 @@ namespace Ryujinx.Graphics.Shader.Instructions
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Figure out how this is supposed to work in the
|
|
||||||
// presence of other condition codes.
|
|
||||||
if (op.Ccc == Ccc.T)
|
if (op.Ccc == Ccc.T)
|
||||||
{
|
{
|
||||||
context.Return();
|
context.Return();
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Operand cond = GetCondition(context, op.Ccc, IrConsts.False);
|
||||||
|
|
||||||
|
// If the condition is always false, we don't need to do anything.
|
||||||
|
if (cond.Type != OperandType.Constant || cond.Value != IrConsts.False)
|
||||||
|
{
|
||||||
|
Operand lblSkip = Label();
|
||||||
|
context.BranchIfFalse(lblSkip, cond);
|
||||||
|
context.Return();
|
||||||
|
context.MarkLabel(lblSkip);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void Kil(EmitterContext context)
|
public static void Kil(EmitterContext context)
|
||||||
@@ -250,7 +261,7 @@ namespace Ryujinx.Graphics.Shader.Instructions
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Operand GetCondition(EmitterContext context, Ccc cond)
|
private static Operand GetCondition(EmitterContext context, Ccc cond, int defaultCond = IrConsts.True)
|
||||||
{
|
{
|
||||||
// TODO: More condition codes, figure out how they work.
|
// TODO: More condition codes, figure out how they work.
|
||||||
switch (cond)
|
switch (cond)
|
||||||
@@ -263,7 +274,7 @@ namespace Ryujinx.Graphics.Shader.Instructions
|
|||||||
return context.BitwiseNot(GetZF());
|
return context.BitwiseNot(GetZF());
|
||||||
}
|
}
|
||||||
|
|
||||||
return Const(IrConsts.True);
|
return Const(defaultCond);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -422,19 +422,19 @@ namespace Ryujinx.HLE.HOS
|
|||||||
|
|
||||||
if (File.Exists(titleAocMetadataPath))
|
if (File.Exists(titleAocMetadataPath))
|
||||||
{
|
{
|
||||||
List<DlcContainer> dlcContainerList = JsonHelper.DeserializeFromFile<List<DlcContainer>>(titleAocMetadataPath);
|
List<DownloadableContentContainer> dlcContainerList = JsonHelper.DeserializeFromFile<List<DownloadableContentContainer>>(titleAocMetadataPath);
|
||||||
|
|
||||||
foreach (DlcContainer dlcContainer in dlcContainerList)
|
foreach (DownloadableContentContainer downloadableContentContainer in dlcContainerList)
|
||||||
{
|
{
|
||||||
foreach (DlcNca dlcNca in dlcContainer.DlcNcaList)
|
foreach (DownloadableContentNca downloadableContentNca in downloadableContentContainer.DownloadableContentNcaList)
|
||||||
{
|
{
|
||||||
if (File.Exists(dlcContainer.Path))
|
if (File.Exists(downloadableContentContainer.ContainerPath))
|
||||||
{
|
{
|
||||||
_device.Configuration.ContentManager.AddAocItem(dlcNca.TitleId, dlcContainer.Path, dlcNca.Path, dlcNca.Enabled);
|
_device.Configuration.ContentManager.AddAocItem(downloadableContentNca.TitleId, downloadableContentContainer.ContainerPath, downloadableContentNca.FullPath, downloadableContentNca.Enabled);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Logger.Warning?.Print(LogClass.Application, $"Cannot find AddOnContent file {dlcContainer.Path}. It may have been moved or renamed.");
|
Logger.Warning?.Print(LogClass.Application, $"Cannot find AddOnContent file {downloadableContentContainer.ContainerPath}. It may have been moved or renamed.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ using System.Collections.Generic;
|
|||||||
|
|
||||||
namespace Ryujinx.Memory.Tests
|
namespace Ryujinx.Memory.Tests
|
||||||
{
|
{
|
||||||
class MockVirtualMemoryManager : IVirtualMemoryManager
|
public class MockVirtualMemoryManager : IVirtualMemoryManager
|
||||||
{
|
{
|
||||||
public bool NoMappings = false;
|
public bool NoMappings = false;
|
||||||
|
|
||||||
|
|||||||
@@ -38,9 +38,15 @@ namespace Ryujinx.Memory.Tests
|
|||||||
Assert.AreEqual(Marshal.ReadInt32(_memoryBlock.Pointer, 0x2040), 0xbadc0de);
|
Assert.AreEqual(Marshal.ReadInt32(_memoryBlock.Pointer, 0x2040), 0xbadc0de);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test, Explicit]
|
[Test]
|
||||||
public void Test_Alias()
|
public void Test_Alias()
|
||||||
{
|
{
|
||||||
|
if (OperatingSystem.IsMacOS())
|
||||||
|
{
|
||||||
|
// Memory aliasing tests fail on CI at the moment.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
using MemoryBlock backing = new MemoryBlock(0x10000, MemoryAllocationFlags.Mirrorable);
|
using MemoryBlock backing = new MemoryBlock(0x10000, MemoryAllocationFlags.Mirrorable);
|
||||||
using MemoryBlock toAlias = new MemoryBlock(0x10000, MemoryAllocationFlags.Reserve | MemoryAllocationFlags.ViewCompatible);
|
using MemoryBlock toAlias = new MemoryBlock(0x10000, MemoryAllocationFlags.Reserve | MemoryAllocationFlags.ViewCompatible);
|
||||||
|
|
||||||
@@ -51,9 +57,15 @@ namespace Ryujinx.Memory.Tests
|
|||||||
Assert.AreEqual(Marshal.ReadInt32(backing.Pointer, 0x1000), 0xbadc0de);
|
Assert.AreEqual(Marshal.ReadInt32(backing.Pointer, 0x1000), 0xbadc0de);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test, Explicit]
|
[Test]
|
||||||
public void Test_AliasRandom()
|
public void Test_AliasRandom()
|
||||||
{
|
{
|
||||||
|
if (OperatingSystem.IsMacOS())
|
||||||
|
{
|
||||||
|
// Memory aliasing tests fail on CI at the moment.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
using MemoryBlock backing = new MemoryBlock(0x80000, MemoryAllocationFlags.Mirrorable);
|
using MemoryBlock backing = new MemoryBlock(0x80000, MemoryAllocationFlags.Mirrorable);
|
||||||
using MemoryBlock toAlias = new MemoryBlock(0x80000, MemoryAllocationFlags.Reserve | MemoryAllocationFlags.ViewCompatible);
|
using MemoryBlock toAlias = new MemoryBlock(0x80000, MemoryAllocationFlags.Reserve | MemoryAllocationFlags.ViewCompatible);
|
||||||
|
|
||||||
|
|||||||
@@ -35,12 +35,6 @@ namespace Ryujinx.Memory
|
|||||||
/// Indicates that the memory block should support mapping views of a mirrorable memory block.
|
/// Indicates that the memory block should support mapping views of a mirrorable memory block.
|
||||||
/// The block that is to have their views mapped should be created with the <see cref="Mirrorable"/> flag.
|
/// The block that is to have their views mapped should be created with the <see cref="Mirrorable"/> flag.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
ViewCompatible = 1 << 3,
|
ViewCompatible = 1 << 3
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Forces views to be mapped page by page on Windows. When partial unmaps are done, this avoids the need
|
|
||||||
/// to unmap the full range and remap sub-ranges, which creates a time window with incorrectly unmapped memory.
|
|
||||||
/// </summary>
|
|
||||||
ForceWindows4KBViewMapping = 1 << 4
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,14 +13,11 @@ namespace Ryujinx.Memory
|
|||||||
private readonly bool _usesSharedMemory;
|
private readonly bool _usesSharedMemory;
|
||||||
private readonly bool _isMirror;
|
private readonly bool _isMirror;
|
||||||
private readonly bool _viewCompatible;
|
private readonly bool _viewCompatible;
|
||||||
private readonly bool _forceWindows4KBView;
|
|
||||||
private IntPtr _sharedMemory;
|
private IntPtr _sharedMemory;
|
||||||
private IntPtr _pointer;
|
private IntPtr _pointer;
|
||||||
private ConcurrentDictionary<MemoryBlock, byte> _viewStorages;
|
private ConcurrentDictionary<MemoryBlock, byte> _viewStorages;
|
||||||
private int _viewCount;
|
private int _viewCount;
|
||||||
|
|
||||||
internal bool ForceWindows4KBView => _forceWindows4KBView;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Pointer to the memory block data.
|
/// Pointer to the memory block data.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -49,8 +46,7 @@ namespace Ryujinx.Memory
|
|||||||
else if (flags.HasFlag(MemoryAllocationFlags.Reserve))
|
else if (flags.HasFlag(MemoryAllocationFlags.Reserve))
|
||||||
{
|
{
|
||||||
_viewCompatible = flags.HasFlag(MemoryAllocationFlags.ViewCompatible);
|
_viewCompatible = flags.HasFlag(MemoryAllocationFlags.ViewCompatible);
|
||||||
_forceWindows4KBView = flags.HasFlag(MemoryAllocationFlags.ForceWindows4KBViewMapping);
|
_pointer = MemoryManagement.Reserve(size, _viewCompatible);
|
||||||
_pointer = MemoryManagement.Reserve(size, _viewCompatible, _forceWindows4KBView);
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -173,7 +169,7 @@ namespace Ryujinx.Memory
|
|||||||
/// <exception cref="MemoryProtectionException">Throw when <paramref name="permission"/> is invalid</exception>
|
/// <exception cref="MemoryProtectionException">Throw when <paramref name="permission"/> is invalid</exception>
|
||||||
public void Reprotect(ulong offset, ulong size, MemoryPermission permission, bool throwOnFail = true)
|
public void Reprotect(ulong offset, ulong size, MemoryPermission permission, bool throwOnFail = true)
|
||||||
{
|
{
|
||||||
MemoryManagement.Reprotect(GetPointerInternal(offset, size), size, permission, _viewCompatible, _forceWindows4KBView, throwOnFail);
|
MemoryManagement.Reprotect(GetPointerInternal(offset, size), size, permission, _viewCompatible, throwOnFail);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -406,7 +402,7 @@ namespace Ryujinx.Memory
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
MemoryManagement.Free(ptr, Size, _forceWindows4KBView);
|
MemoryManagement.Free(ptr, Size);
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (MemoryBlock viewStorage in _viewStorages.Keys)
|
foreach (MemoryBlock viewStorage in _viewStorages.Keys)
|
||||||
|
|||||||
@@ -20,11 +20,11 @@ namespace Ryujinx.Memory
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IntPtr Reserve(ulong size, bool viewCompatible, bool force4KBMap)
|
public static IntPtr Reserve(ulong size, bool viewCompatible)
|
||||||
{
|
{
|
||||||
if (OperatingSystem.IsWindows())
|
if (OperatingSystem.IsWindows())
|
||||||
{
|
{
|
||||||
return MemoryManagementWindows.Reserve((IntPtr)size, viewCompatible, force4KBMap);
|
return MemoryManagementWindows.Reserve((IntPtr)size, viewCompatible);
|
||||||
}
|
}
|
||||||
else if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS())
|
else if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS())
|
||||||
{
|
{
|
||||||
@@ -71,16 +71,9 @@ namespace Ryujinx.Memory
|
|||||||
public static void MapView(IntPtr sharedMemory, ulong srcOffset, IntPtr address, ulong size, MemoryBlock owner)
|
public static void MapView(IntPtr sharedMemory, ulong srcOffset, IntPtr address, ulong size, MemoryBlock owner)
|
||||||
{
|
{
|
||||||
if (OperatingSystem.IsWindows())
|
if (OperatingSystem.IsWindows())
|
||||||
{
|
|
||||||
if (owner.ForceWindows4KBView)
|
|
||||||
{
|
|
||||||
MemoryManagementWindows.MapView4KB(sharedMemory, srcOffset, address, (IntPtr)size);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
{
|
||||||
MemoryManagementWindows.MapView(sharedMemory, srcOffset, address, (IntPtr)size, owner);
|
MemoryManagementWindows.MapView(sharedMemory, srcOffset, address, (IntPtr)size, owner);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
else if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS())
|
else if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS())
|
||||||
{
|
{
|
||||||
MemoryManagementUnix.MapView(sharedMemory, srcOffset, address, size);
|
MemoryManagementUnix.MapView(sharedMemory, srcOffset, address, size);
|
||||||
@@ -94,16 +87,9 @@ namespace Ryujinx.Memory
|
|||||||
public static void UnmapView(IntPtr sharedMemory, IntPtr address, ulong size, MemoryBlock owner)
|
public static void UnmapView(IntPtr sharedMemory, IntPtr address, ulong size, MemoryBlock owner)
|
||||||
{
|
{
|
||||||
if (OperatingSystem.IsWindows())
|
if (OperatingSystem.IsWindows())
|
||||||
{
|
|
||||||
if (owner.ForceWindows4KBView)
|
|
||||||
{
|
|
||||||
MemoryManagementWindows.UnmapView4KB(address, (IntPtr)size);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
{
|
||||||
MemoryManagementWindows.UnmapView(sharedMemory, address, (IntPtr)size, owner);
|
MemoryManagementWindows.UnmapView(sharedMemory, address, (IntPtr)size, owner);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
else if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS())
|
else if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS())
|
||||||
{
|
{
|
||||||
MemoryManagementUnix.UnmapView(address, size);
|
MemoryManagementUnix.UnmapView(address, size);
|
||||||
@@ -114,21 +100,14 @@ namespace Ryujinx.Memory
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void Reprotect(IntPtr address, ulong size, MemoryPermission permission, bool forView, bool force4KBMap, bool throwOnFail)
|
public static void Reprotect(IntPtr address, ulong size, MemoryPermission permission, bool forView, bool throwOnFail)
|
||||||
{
|
{
|
||||||
bool result;
|
bool result;
|
||||||
|
|
||||||
if (OperatingSystem.IsWindows())
|
if (OperatingSystem.IsWindows())
|
||||||
{
|
|
||||||
if (forView && force4KBMap)
|
|
||||||
{
|
|
||||||
result = MemoryManagementWindows.Reprotect4KB(address, (IntPtr)size, permission, forView);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
{
|
||||||
result = MemoryManagementWindows.Reprotect(address, (IntPtr)size, permission, forView);
|
result = MemoryManagementWindows.Reprotect(address, (IntPtr)size, permission, forView);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
else if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS())
|
else if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS())
|
||||||
{
|
{
|
||||||
result = MemoryManagementUnix.Reprotect(address, size, permission);
|
result = MemoryManagementUnix.Reprotect(address, size, permission);
|
||||||
@@ -144,11 +123,11 @@ namespace Ryujinx.Memory
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool Free(IntPtr address, ulong size, bool force4KBMap)
|
public static bool Free(IntPtr address, ulong size)
|
||||||
{
|
{
|
||||||
if (OperatingSystem.IsWindows())
|
if (OperatingSystem.IsWindows())
|
||||||
{
|
{
|
||||||
return MemoryManagementWindows.Free(address, (IntPtr)size, force4KBMap);
|
return MemoryManagementWindows.Free(address, (IntPtr)size);
|
||||||
}
|
}
|
||||||
else if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS())
|
else if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS())
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -10,23 +10,19 @@ namespace Ryujinx.Memory
|
|||||||
public const int PageSize = 0x1000;
|
public const int PageSize = 0x1000;
|
||||||
|
|
||||||
private static readonly PlaceholderManager _placeholders = new PlaceholderManager();
|
private static readonly PlaceholderManager _placeholders = new PlaceholderManager();
|
||||||
private static readonly PlaceholderManager4KB _placeholders4KB = new PlaceholderManager4KB();
|
|
||||||
|
|
||||||
public static IntPtr Allocate(IntPtr size)
|
public static IntPtr Allocate(IntPtr size)
|
||||||
{
|
{
|
||||||
return AllocateInternal(size, AllocationType.Reserve | AllocationType.Commit);
|
return AllocateInternal(size, AllocationType.Reserve | AllocationType.Commit);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IntPtr Reserve(IntPtr size, bool viewCompatible, bool force4KBMap)
|
public static IntPtr Reserve(IntPtr size, bool viewCompatible)
|
||||||
{
|
{
|
||||||
if (viewCompatible)
|
if (viewCompatible)
|
||||||
{
|
{
|
||||||
IntPtr baseAddress = AllocateInternal2(size, AllocationType.Reserve | AllocationType.ReservePlaceholder);
|
IntPtr baseAddress = AllocateInternal2(size, AllocationType.Reserve | AllocationType.ReservePlaceholder);
|
||||||
|
|
||||||
if (!force4KBMap)
|
|
||||||
{
|
|
||||||
_placeholders.ReserveRange((ulong)baseAddress, (ulong)size);
|
_placeholders.ReserveRange((ulong)baseAddress, (ulong)size);
|
||||||
}
|
|
||||||
|
|
||||||
return baseAddress;
|
return baseAddress;
|
||||||
}
|
}
|
||||||
@@ -73,49 +69,11 @@ namespace Ryujinx.Memory
|
|||||||
_placeholders.MapView(sharedMemory, srcOffset, location, size, owner);
|
_placeholders.MapView(sharedMemory, srcOffset, location, size, owner);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void MapView4KB(IntPtr sharedMemory, ulong srcOffset, IntPtr location, IntPtr size)
|
|
||||||
{
|
|
||||||
_placeholders4KB.UnmapAndMarkRangeAsMapped(location, size);
|
|
||||||
|
|
||||||
ulong uaddress = (ulong)location;
|
|
||||||
ulong usize = (ulong)size;
|
|
||||||
IntPtr endLocation = (IntPtr)(uaddress + usize);
|
|
||||||
|
|
||||||
while (location != endLocation)
|
|
||||||
{
|
|
||||||
WindowsApi.VirtualFree(location, (IntPtr)PageSize, AllocationType.Release | AllocationType.PreservePlaceholder);
|
|
||||||
|
|
||||||
var ptr = WindowsApi.MapViewOfFile3(
|
|
||||||
sharedMemory,
|
|
||||||
WindowsApi.CurrentProcessHandle,
|
|
||||||
location,
|
|
||||||
srcOffset,
|
|
||||||
(IntPtr)PageSize,
|
|
||||||
0x4000,
|
|
||||||
MemoryProtection.ReadWrite,
|
|
||||||
IntPtr.Zero,
|
|
||||||
0);
|
|
||||||
|
|
||||||
if (ptr == IntPtr.Zero)
|
|
||||||
{
|
|
||||||
throw new WindowsApiException("MapViewOfFile3");
|
|
||||||
}
|
|
||||||
|
|
||||||
location += PageSize;
|
|
||||||
srcOffset += PageSize;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void UnmapView(IntPtr sharedMemory, IntPtr location, IntPtr size, MemoryBlock owner)
|
public static void UnmapView(IntPtr sharedMemory, IntPtr location, IntPtr size, MemoryBlock owner)
|
||||||
{
|
{
|
||||||
_placeholders.UnmapView(sharedMemory, location, size, owner);
|
_placeholders.UnmapView(sharedMemory, location, size, owner);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void UnmapView4KB(IntPtr location, IntPtr size)
|
|
||||||
{
|
|
||||||
_placeholders4KB.UnmapView(location, size);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool Reprotect(IntPtr address, IntPtr size, MemoryPermission permission, bool forView)
|
public static bool Reprotect(IntPtr address, IntPtr size, MemoryPermission permission, bool forView)
|
||||||
{
|
{
|
||||||
if (forView)
|
if (forView)
|
||||||
@@ -128,34 +86,9 @@ namespace Ryujinx.Memory
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool Reprotect4KB(IntPtr address, IntPtr size, MemoryPermission permission, bool forView)
|
public static bool Free(IntPtr address, IntPtr size)
|
||||||
{
|
|
||||||
ulong uaddress = (ulong)address;
|
|
||||||
ulong usize = (ulong)size;
|
|
||||||
while (usize > 0)
|
|
||||||
{
|
|
||||||
if (!WindowsApi.VirtualProtect((IntPtr)uaddress, (IntPtr)PageSize, WindowsApi.GetProtection(permission), out _))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
uaddress += PageSize;
|
|
||||||
usize -= PageSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool Free(IntPtr address, IntPtr size, bool force4KBMap)
|
|
||||||
{
|
|
||||||
if (force4KBMap)
|
|
||||||
{
|
|
||||||
_placeholders4KB.UnmapRange(address, size);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
{
|
||||||
_placeholders.UnreserveRange((ulong)address, (ulong)size);
|
_placeholders.UnreserveRange((ulong)address, (ulong)size);
|
||||||
}
|
|
||||||
|
|
||||||
return WindowsApi.VirtualFree(address, IntPtr.Zero, AllocationType.Release);
|
return WindowsApi.VirtualFree(address, IntPtr.Zero, AllocationType.Release);
|
||||||
}
|
}
|
||||||
@@ -207,10 +140,5 @@ namespace Ryujinx.Memory
|
|||||||
throw new ArgumentException("Invalid address.", nameof(address));
|
throw new ArgumentException("Invalid address.", nameof(address));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool RetryFromAccessViolation()
|
|
||||||
{
|
|
||||||
return _placeholders.RetryFromAccessViolation();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -188,30 +188,6 @@ namespace Ryujinx.Memory.Tracking
|
|||||||
return VirtualMemoryEvent(address, 1, write);
|
return VirtualMemoryEvent(address, 1, write);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Signal that a virtual memory event happened at the given location.
|
|
||||||
/// This is similar VirtualMemoryEvent, but on Windows, it might also return true after a partial unmap.
|
|
||||||
/// This should only be called from the exception handler.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="address">Virtual address accessed</param>
|
|
||||||
/// <param name="size">Size of the region affected in bytes</param>
|
|
||||||
/// <param name="write">Whether the region was written to or read</param>
|
|
||||||
/// <param name="precise">True if the access is precise, false otherwise</param>
|
|
||||||
/// <returns>True if the event triggered any tracking regions, false otherwise</returns>
|
|
||||||
public bool VirtualMemoryEventEh(ulong address, ulong size, bool write, bool precise = false)
|
|
||||||
{
|
|
||||||
// Windows has a limitation, it can't do partial unmaps.
|
|
||||||
// For this reason, we need to unmap the whole range and then remap the sub-ranges.
|
|
||||||
// When this happens, we might have caused a undesirable access violation from the time that the range was unmapped.
|
|
||||||
// In this case, try again as the memory might be mapped now.
|
|
||||||
if (OperatingSystem.IsWindows() && MemoryManagementWindows.RetryFromAccessViolation())
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return VirtualMemoryEvent(address, size, write, precise);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Signal that a virtual memory event happened at the given location.
|
/// Signal that a virtual memory event happened at the given location.
|
||||||
/// This can be flagged as a precise event, which will avoid reprotection and call special handlers if possible.
|
/// This can be flagged as a precise event, which will avoid reprotection and call special handlers if possible.
|
||||||
@@ -237,10 +213,12 @@ namespace Ryujinx.Memory.Tracking
|
|||||||
|
|
||||||
if (count == 0 && !precise)
|
if (count == 0 && !precise)
|
||||||
{
|
{
|
||||||
if (_memoryManager.IsMapped(address))
|
if (_memoryManager.IsRangeMapped(address, size))
|
||||||
{
|
{
|
||||||
|
// TODO: There is currently the possibility that a page can be protected after its virtual region is removed.
|
||||||
|
// This code handles that case when it happens, but it would be better to find out how this happens.
|
||||||
_memoryManager.TrackingReprotect(address & ~(ulong)(_pageSize - 1), (ulong)_pageSize, MemoryPermission.ReadAndWrite);
|
_memoryManager.TrackingReprotect(address & ~(ulong)(_pageSize - 1), (ulong)_pageSize, MemoryPermission.ReadAndWrite);
|
||||||
return false; // We can't handle this - it's probably a real invalid access.
|
return true; // This memory _should_ be mapped, so we need to try again.
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
|
using Ryujinx.Common.Memory.PartialUnmaps;
|
||||||
using System;
|
using System;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
using System.Runtime.Versioning;
|
using System.Runtime.Versioning;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
|
||||||
@@ -13,13 +15,10 @@ namespace Ryujinx.Memory.WindowsShared
|
|||||||
{
|
{
|
||||||
private const ulong MinimumPageSize = 0x1000;
|
private const ulong MinimumPageSize = 0x1000;
|
||||||
|
|
||||||
[ThreadStatic]
|
|
||||||
private static int _threadLocalPartialUnmapsCount;
|
|
||||||
|
|
||||||
private readonly IntervalTree<ulong, ulong> _mappings;
|
private readonly IntervalTree<ulong, ulong> _mappings;
|
||||||
private readonly IntervalTree<ulong, MemoryPermission> _protections;
|
private readonly IntervalTree<ulong, MemoryPermission> _protections;
|
||||||
private readonly ReaderWriterLock _partialUnmapLock;
|
private readonly IntPtr _partialUnmapStatePtr;
|
||||||
private int _partialUnmapsCount;
|
private readonly Thread _partialUnmapTrimThread;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a new instance of the Windows memory placeholder manager.
|
/// Creates a new instance of the Windows memory placeholder manager.
|
||||||
@@ -28,7 +27,35 @@ namespace Ryujinx.Memory.WindowsShared
|
|||||||
{
|
{
|
||||||
_mappings = new IntervalTree<ulong, ulong>();
|
_mappings = new IntervalTree<ulong, ulong>();
|
||||||
_protections = new IntervalTree<ulong, MemoryPermission>();
|
_protections = new IntervalTree<ulong, MemoryPermission>();
|
||||||
_partialUnmapLock = new ReaderWriterLock();
|
|
||||||
|
_partialUnmapStatePtr = PartialUnmapState.GlobalState;
|
||||||
|
|
||||||
|
_partialUnmapTrimThread = new Thread(TrimThreadLocalMapLoop);
|
||||||
|
_partialUnmapTrimThread.Name = "CPU.PartialUnmapTrimThread";
|
||||||
|
_partialUnmapTrimThread.IsBackground = true;
|
||||||
|
_partialUnmapTrimThread.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a reference to the partial unmap state struct.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>A reference to the partial unmap state struct</returns>
|
||||||
|
private unsafe ref PartialUnmapState GetPartialUnmapState()
|
||||||
|
{
|
||||||
|
return ref Unsafe.AsRef<PartialUnmapState>((void*)_partialUnmapStatePtr);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Trims inactive threads from the partial unmap state's thread mapping every few seconds.
|
||||||
|
/// Should be run in a Background thread so that it doesn't stop the program from closing.
|
||||||
|
/// </summary>
|
||||||
|
private void TrimThreadLocalMapLoop()
|
||||||
|
{
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
Thread.Sleep(2000);
|
||||||
|
GetPartialUnmapState().TrimThreads();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -98,7 +125,8 @@ namespace Ryujinx.Memory.WindowsShared
|
|||||||
/// <param name="owner">Memory block that owns the mapping</param>
|
/// <param name="owner">Memory block that owns the mapping</param>
|
||||||
public void MapView(IntPtr sharedMemory, ulong srcOffset, IntPtr location, IntPtr size, MemoryBlock owner)
|
public void MapView(IntPtr sharedMemory, ulong srcOffset, IntPtr location, IntPtr size, MemoryBlock owner)
|
||||||
{
|
{
|
||||||
_partialUnmapLock.AcquireReaderLock(Timeout.Infinite);
|
ref var partialUnmapLock = ref GetPartialUnmapState().PartialUnmapLock;
|
||||||
|
partialUnmapLock.AcquireReaderLock();
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -107,7 +135,7 @@ namespace Ryujinx.Memory.WindowsShared
|
|||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
_partialUnmapLock.ReleaseReaderLock();
|
partialUnmapLock.ReleaseReaderLock();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -221,7 +249,8 @@ namespace Ryujinx.Memory.WindowsShared
|
|||||||
/// <param name="owner">Memory block that owns the mapping</param>
|
/// <param name="owner">Memory block that owns the mapping</param>
|
||||||
public void UnmapView(IntPtr sharedMemory, IntPtr location, IntPtr size, MemoryBlock owner)
|
public void UnmapView(IntPtr sharedMemory, IntPtr location, IntPtr size, MemoryBlock owner)
|
||||||
{
|
{
|
||||||
_partialUnmapLock.AcquireReaderLock(Timeout.Infinite);
|
ref var partialUnmapLock = ref GetPartialUnmapState().PartialUnmapLock;
|
||||||
|
partialUnmapLock.AcquireReaderLock();
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -229,7 +258,7 @@ namespace Ryujinx.Memory.WindowsShared
|
|||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
_partialUnmapLock.ReleaseReaderLock();
|
partialUnmapLock.ReleaseReaderLock();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -265,11 +294,6 @@ namespace Ryujinx.Memory.WindowsShared
|
|||||||
|
|
||||||
if (IsMapped(overlap.Value))
|
if (IsMapped(overlap.Value))
|
||||||
{
|
{
|
||||||
if (!WindowsApi.UnmapViewOfFile2(WindowsApi.CurrentProcessHandle, (IntPtr)overlap.Start, 2))
|
|
||||||
{
|
|
||||||
throw new WindowsApiException("UnmapViewOfFile2");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tree operations might modify the node start/end values, so save a copy before we modify the tree.
|
// Tree operations might modify the node start/end values, so save a copy before we modify the tree.
|
||||||
ulong overlapStart = overlap.Start;
|
ulong overlapStart = overlap.Start;
|
||||||
ulong overlapEnd = overlap.End;
|
ulong overlapEnd = overlap.End;
|
||||||
@@ -291,9 +315,18 @@ namespace Ryujinx.Memory.WindowsShared
|
|||||||
// This is necessary because Windows does not support partial view unmaps.
|
// This is necessary because Windows does not support partial view unmaps.
|
||||||
// That is, you can only fully unmap a view that was previously mapped, you can't just unmap a chunck of it.
|
// That is, you can only fully unmap a view that was previously mapped, you can't just unmap a chunck of it.
|
||||||
|
|
||||||
LockCookie lockCookie = _partialUnmapLock.UpgradeToWriterLock(Timeout.Infinite);
|
ref var partialUnmapState = ref GetPartialUnmapState();
|
||||||
|
ref var partialUnmapLock = ref partialUnmapState.PartialUnmapLock;
|
||||||
|
partialUnmapLock.UpgradeToWriterLock();
|
||||||
|
|
||||||
_partialUnmapsCount++;
|
try
|
||||||
|
{
|
||||||
|
partialUnmapState.PartialUnmapsCount++;
|
||||||
|
|
||||||
|
if (!WindowsApi.UnmapViewOfFile2(WindowsApi.CurrentProcessHandle, (IntPtr)overlapStart, 2))
|
||||||
|
{
|
||||||
|
throw new WindowsApiException("UnmapViewOfFile2");
|
||||||
|
}
|
||||||
|
|
||||||
if (overlapStartsBefore)
|
if (overlapStartsBefore)
|
||||||
{
|
{
|
||||||
@@ -313,8 +346,15 @@ namespace Ryujinx.Memory.WindowsShared
|
|||||||
MapViewInternal(sharedMemory, remapBackingOffset, (IntPtr)remapAddress, (IntPtr)remapSize);
|
MapViewInternal(sharedMemory, remapBackingOffset, (IntPtr)remapAddress, (IntPtr)remapSize);
|
||||||
RestoreRangeProtection(remapAddress, remapSize);
|
RestoreRangeProtection(remapAddress, remapSize);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
_partialUnmapLock.DowngradeFromWriterLock(ref lockCookie);
|
finally
|
||||||
|
{
|
||||||
|
partialUnmapLock.DowngradeFromWriterLock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (!WindowsApi.UnmapViewOfFile2(WindowsApi.CurrentProcessHandle, (IntPtr)overlapStart, 2))
|
||||||
|
{
|
||||||
|
throw new WindowsApiException("UnmapViewOfFile2");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -394,7 +434,8 @@ namespace Ryujinx.Memory.WindowsShared
|
|||||||
/// <returns>True if the reprotection was successful, false otherwise</returns>
|
/// <returns>True if the reprotection was successful, false otherwise</returns>
|
||||||
public bool ReprotectView(IntPtr address, IntPtr size, MemoryPermission permission)
|
public bool ReprotectView(IntPtr address, IntPtr size, MemoryPermission permission)
|
||||||
{
|
{
|
||||||
_partialUnmapLock.AcquireReaderLock(Timeout.Infinite);
|
ref var partialUnmapLock = ref GetPartialUnmapState().PartialUnmapLock;
|
||||||
|
partialUnmapLock.AcquireReaderLock();
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -402,7 +443,7 @@ namespace Ryujinx.Memory.WindowsShared
|
|||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
_partialUnmapLock.ReleaseReaderLock();
|
partialUnmapLock.ReleaseReaderLock();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -659,31 +700,5 @@ namespace Ryujinx.Memory.WindowsShared
|
|||||||
ReprotectViewInternal((IntPtr)protAddress, (IntPtr)(protEndAddress - protAddress), protection.Value, true);
|
ReprotectViewInternal((IntPtr)protAddress, (IntPtr)(protEndAddress - protAddress), protection.Value, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Checks if an access violation handler should retry execution due to a fault caused by partial unmap.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// Due to Windows limitations, <see cref="UnmapView"/> might need to unmap more memory than requested.
|
|
||||||
/// The additional memory that was unmapped is later remapped, however this leaves a time gap where the
|
|
||||||
/// memory might be accessed but is unmapped. Users of the API must compensate for that by catching the
|
|
||||||
/// access violation and retrying if it happened between the unmap and remap operation.
|
|
||||||
/// This method can be used to decide if retrying in such cases is necessary or not.
|
|
||||||
/// </remarks>
|
|
||||||
/// <returns>True if execution should be retried, false otherwise</returns>
|
|
||||||
public bool RetryFromAccessViolation()
|
|
||||||
{
|
|
||||||
_partialUnmapLock.AcquireReaderLock(Timeout.Infinite);
|
|
||||||
|
|
||||||
bool retry = _threadLocalPartialUnmapsCount != _partialUnmapsCount;
|
|
||||||
if (retry)
|
|
||||||
{
|
|
||||||
_threadLocalPartialUnmapsCount = _partialUnmapsCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
_partialUnmapLock.ReleaseReaderLock();
|
|
||||||
|
|
||||||
return retry;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,170 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Runtime.Versioning;
|
|
||||||
|
|
||||||
namespace Ryujinx.Memory.WindowsShared
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Windows 4KB memory placeholder manager.
|
|
||||||
/// </summary>
|
|
||||||
[SupportedOSPlatform("windows")]
|
|
||||||
class PlaceholderManager4KB
|
|
||||||
{
|
|
||||||
private const int PageSize = MemoryManagementWindows.PageSize;
|
|
||||||
|
|
||||||
private readonly IntervalTree<ulong, byte> _mappings;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Creates a new instance of the Windows 4KB memory placeholder manager.
|
|
||||||
/// </summary>
|
|
||||||
public PlaceholderManager4KB()
|
|
||||||
{
|
|
||||||
_mappings = new IntervalTree<ulong, byte>();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Unmaps the specified range of memory and marks it as mapped internally.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// Since this marks the range as mapped, the expectation is that the range will be mapped after calling this method.
|
|
||||||
/// </remarks>
|
|
||||||
/// <param name="location">Memory address to unmap and mark as mapped</param>
|
|
||||||
/// <param name="size">Size of the range in bytes</param>
|
|
||||||
public void UnmapAndMarkRangeAsMapped(IntPtr location, IntPtr size)
|
|
||||||
{
|
|
||||||
ulong startAddress = (ulong)location;
|
|
||||||
ulong unmapSize = (ulong)size;
|
|
||||||
ulong endAddress = startAddress + unmapSize;
|
|
||||||
|
|
||||||
var overlaps = Array.Empty<IntervalTreeNode<ulong, byte>>();
|
|
||||||
int count = 0;
|
|
||||||
|
|
||||||
lock (_mappings)
|
|
||||||
{
|
|
||||||
count = _mappings.Get(startAddress, endAddress, ref overlaps);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int index = 0; index < count; index++)
|
|
||||||
{
|
|
||||||
var overlap = overlaps[index];
|
|
||||||
|
|
||||||
// Tree operations might modify the node start/end values, so save a copy before we modify the tree.
|
|
||||||
ulong overlapStart = overlap.Start;
|
|
||||||
ulong overlapEnd = overlap.End;
|
|
||||||
ulong overlapValue = overlap.Value;
|
|
||||||
|
|
||||||
_mappings.Remove(overlap);
|
|
||||||
|
|
||||||
ulong unmapStart = Math.Max(overlapStart, startAddress);
|
|
||||||
ulong unmapEnd = Math.Min(overlapEnd, endAddress);
|
|
||||||
|
|
||||||
if (overlapStart < startAddress)
|
|
||||||
{
|
|
||||||
startAddress = overlapStart;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (overlapEnd > endAddress)
|
|
||||||
{
|
|
||||||
endAddress = overlapEnd;
|
|
||||||
}
|
|
||||||
|
|
||||||
ulong currentAddress = unmapStart;
|
|
||||||
while (currentAddress < unmapEnd)
|
|
||||||
{
|
|
||||||
WindowsApi.UnmapViewOfFile2(WindowsApi.CurrentProcessHandle, (IntPtr)currentAddress, 2);
|
|
||||||
currentAddress += PageSize;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_mappings.Add(startAddress, endAddress, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Unmaps views at the specified memory range.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="location">Address of the range</param>
|
|
||||||
/// <param name="size">Size of the range in bytes</param>
|
|
||||||
public void UnmapView(IntPtr location, IntPtr size)
|
|
||||||
{
|
|
||||||
ulong startAddress = (ulong)location;
|
|
||||||
ulong unmapSize = (ulong)size;
|
|
||||||
ulong endAddress = startAddress + unmapSize;
|
|
||||||
|
|
||||||
var overlaps = Array.Empty<IntervalTreeNode<ulong, byte>>();
|
|
||||||
int count = 0;
|
|
||||||
|
|
||||||
lock (_mappings)
|
|
||||||
{
|
|
||||||
count = _mappings.Get(startAddress, endAddress, ref overlaps);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int index = 0; index < count; index++)
|
|
||||||
{
|
|
||||||
var overlap = overlaps[index];
|
|
||||||
|
|
||||||
// Tree operations might modify the node start/end values, so save a copy before we modify the tree.
|
|
||||||
ulong overlapStart = overlap.Start;
|
|
||||||
ulong overlapEnd = overlap.End;
|
|
||||||
|
|
||||||
_mappings.Remove(overlap);
|
|
||||||
|
|
||||||
if (overlapStart < startAddress)
|
|
||||||
{
|
|
||||||
_mappings.Add(overlapStart, startAddress, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (overlapEnd > endAddress)
|
|
||||||
{
|
|
||||||
_mappings.Add(endAddress, overlapEnd, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
ulong unmapStart = Math.Max(overlapStart, startAddress);
|
|
||||||
ulong unmapEnd = Math.Min(overlapEnd, endAddress);
|
|
||||||
|
|
||||||
ulong currentAddress = unmapStart;
|
|
||||||
while (currentAddress < unmapEnd)
|
|
||||||
{
|
|
||||||
WindowsApi.UnmapViewOfFile2(WindowsApi.CurrentProcessHandle, (IntPtr)currentAddress, 2);
|
|
||||||
currentAddress += PageSize;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Unmaps mapped memory at a given range.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="location">Address of the range</param>
|
|
||||||
/// <param name="size">Size of the range in bytes</param>
|
|
||||||
public void UnmapRange(IntPtr location, IntPtr size)
|
|
||||||
{
|
|
||||||
ulong startAddress = (ulong)location;
|
|
||||||
ulong unmapSize = (ulong)size;
|
|
||||||
ulong endAddress = startAddress + unmapSize;
|
|
||||||
|
|
||||||
var overlaps = Array.Empty<IntervalTreeNode<ulong, byte>>();
|
|
||||||
int count = 0;
|
|
||||||
|
|
||||||
lock (_mappings)
|
|
||||||
{
|
|
||||||
count = _mappings.Get(startAddress, endAddress, ref overlaps);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int index = 0; index < count; index++)
|
|
||||||
{
|
|
||||||
var overlap = overlaps[index];
|
|
||||||
|
|
||||||
// Tree operations might modify the node start/end values, so save a copy before we modify the tree.
|
|
||||||
ulong unmapStart = Math.Max(overlap.Start, startAddress);
|
|
||||||
ulong unmapEnd = Math.Min(overlap.End, endAddress);
|
|
||||||
|
|
||||||
_mappings.Remove(overlap);
|
|
||||||
|
|
||||||
ulong currentAddress = unmapStart;
|
|
||||||
while (currentAddress < unmapEnd)
|
|
||||||
{
|
|
||||||
WindowsApi.UnmapViewOfFile2(WindowsApi.CurrentProcessHandle, (IntPtr)currentAddress, 2);
|
|
||||||
currentAddress += PageSize;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -76,6 +76,9 @@ namespace Ryujinx.Memory.WindowsShared
|
|||||||
[DllImport("kernel32.dll")]
|
[DllImport("kernel32.dll")]
|
||||||
public static extern uint GetLastError();
|
public static extern uint GetLastError();
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll")]
|
||||||
|
public static extern int GetCurrentThreadId();
|
||||||
|
|
||||||
public static MemoryProtection GetProtection(MemoryPermission permission)
|
public static MemoryProtection GetProtection(MemoryPermission permission)
|
||||||
{
|
{
|
||||||
return permission switch
|
return permission switch
|
||||||
|
|||||||
8
Ryujinx.Tests/.runsettings
Normal file
8
Ryujinx.Tests/.runsettings
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<RunSettings>
|
||||||
|
<RunConfiguration>
|
||||||
|
<EnvironmentVariables>
|
||||||
|
<COMPlus_EnableAlternateStackCheck>1</COMPlus_EnableAlternateStackCheck>
|
||||||
|
</EnvironmentVariables>
|
||||||
|
</RunConfiguration>
|
||||||
|
</RunSettings>
|
||||||
53
Ryujinx.Tests/Memory/MockMemoryManager.cs
Normal file
53
Ryujinx.Tests/Memory/MockMemoryManager.cs
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
using ARMeilleure.Memory;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Ryujinx.Tests.Memory
|
||||||
|
{
|
||||||
|
internal class MockMemoryManager : IMemoryManager
|
||||||
|
{
|
||||||
|
public int AddressSpaceBits => throw new NotImplementedException();
|
||||||
|
|
||||||
|
public IntPtr PageTablePointer => throw new NotImplementedException();
|
||||||
|
|
||||||
|
public MemoryManagerType Type => MemoryManagerType.HostMappedUnsafe;
|
||||||
|
|
||||||
|
#pragma warning disable CS0067
|
||||||
|
public event Action<ulong, ulong> UnmapEvent;
|
||||||
|
#pragma warning restore CS0067
|
||||||
|
|
||||||
|
public ref T GetRef<T>(ulong va) where T : unmanaged
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ReadOnlySpan<byte> GetSpan(ulong va, int size, bool tracked = false)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsMapped(ulong va)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public T Read<T>(ulong va) where T : unmanaged
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public T ReadTracked<T>(ulong va) where T : unmanaged
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SignalMemoryTracking(ulong va, ulong size, bool write, bool precise = false)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Write<T>(ulong va, T value) where T : unmanaged
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
484
Ryujinx.Tests/Memory/PartialUnmaps.cs
Normal file
484
Ryujinx.Tests/Memory/PartialUnmaps.cs
Normal file
@@ -0,0 +1,484 @@
|
|||||||
|
using ARMeilleure.Signal;
|
||||||
|
using ARMeilleure.Translation;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using Ryujinx.Common.Memory.PartialUnmaps;
|
||||||
|
using Ryujinx.Cpu;
|
||||||
|
using Ryujinx.Cpu.Jit;
|
||||||
|
using Ryujinx.Memory;
|
||||||
|
using Ryujinx.Memory.Tests;
|
||||||
|
using Ryujinx.Memory.Tracking;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
|
namespace Ryujinx.Tests.Memory
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
internal class PartialUnmaps
|
||||||
|
{
|
||||||
|
private static Translator _translator;
|
||||||
|
|
||||||
|
private (MemoryBlock virt, MemoryBlock mirror, MemoryEhMeilleure exceptionHandler) GetVirtual(ulong asSize)
|
||||||
|
{
|
||||||
|
MemoryAllocationFlags asFlags = MemoryAllocationFlags.Reserve | MemoryAllocationFlags.ViewCompatible;
|
||||||
|
|
||||||
|
var addressSpace = new MemoryBlock(asSize, asFlags);
|
||||||
|
var addressSpaceMirror = new MemoryBlock(asSize, asFlags);
|
||||||
|
|
||||||
|
var tracking = new MemoryTracking(new MockVirtualMemoryManager(asSize, 0x1000), 0x1000);
|
||||||
|
var exceptionHandler = new MemoryEhMeilleure(addressSpace, addressSpaceMirror, tracking);
|
||||||
|
|
||||||
|
return (addressSpace, addressSpaceMirror, exceptionHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int CountThreads(ref PartialUnmapState state)
|
||||||
|
{
|
||||||
|
int count = 0;
|
||||||
|
|
||||||
|
ref var ids = ref state.LocalCounts.ThreadIds;
|
||||||
|
|
||||||
|
for (int i = 0; i < ids.Length; i++)
|
||||||
|
{
|
||||||
|
if (ids[i] != 0)
|
||||||
|
{
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
private 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void PartialUnmap([Values] bool readOnly)
|
||||||
|
{
|
||||||
|
if (OperatingSystem.IsMacOS())
|
||||||
|
{
|
||||||
|
// Memory aliasing tests fail on CI at the moment.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up an address space to test partial unmapping.
|
||||||
|
// Should register the signal handler to deal with this on Windows.
|
||||||
|
ulong vaSize = 0x100000;
|
||||||
|
|
||||||
|
// The first 0x100000 is mapped to start. It is replaced from the center with the 0x200000 mapping.
|
||||||
|
var backing = new MemoryBlock(vaSize * 2, MemoryAllocationFlags.Mirrorable);
|
||||||
|
|
||||||
|
(MemoryBlock unusedMainMemory, MemoryBlock memory, MemoryEhMeilleure exceptionHandler) = GetVirtual(vaSize * 2);
|
||||||
|
|
||||||
|
EnsureTranslator();
|
||||||
|
|
||||||
|
ref var state = ref PartialUnmapState.GetRef();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Globally reset the struct for handling partial unmap races.
|
||||||
|
PartialUnmapState.Reset();
|
||||||
|
bool shouldAccess = true;
|
||||||
|
bool error = false;
|
||||||
|
|
||||||
|
// Create a large mapping.
|
||||||
|
memory.MapView(backing, 0, 0, vaSize);
|
||||||
|
|
||||||
|
if (readOnly)
|
||||||
|
{
|
||||||
|
memory.Reprotect(0, vaSize, MemoryPermission.Read);
|
||||||
|
}
|
||||||
|
|
||||||
|
Thread testThread;
|
||||||
|
|
||||||
|
if (readOnly)
|
||||||
|
{
|
||||||
|
// Write a value to the physical memory, then try to read it repeately from virtual.
|
||||||
|
// It should not change.
|
||||||
|
testThread = new Thread(() =>
|
||||||
|
{
|
||||||
|
int i = 12345;
|
||||||
|
backing.Write(vaSize - 0x1000, i);
|
||||||
|
|
||||||
|
while (shouldAccess)
|
||||||
|
{
|
||||||
|
if (memory.Read<int>(vaSize - 0x1000) != i)
|
||||||
|
{
|
||||||
|
error = true;
|
||||||
|
shouldAccess = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Repeatedly write and check the value on the last page of the mapping on another thread.
|
||||||
|
testThread = new Thread(() =>
|
||||||
|
{
|
||||||
|
int i = 0;
|
||||||
|
while (shouldAccess)
|
||||||
|
{
|
||||||
|
memory.Write(vaSize - 0x1000, i);
|
||||||
|
if (memory.Read<int>(vaSize - 0x1000) != i)
|
||||||
|
{
|
||||||
|
error = true;
|
||||||
|
shouldAccess = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
testThread.Start();
|
||||||
|
|
||||||
|
// Create a smaller mapping, covering the larger mapping.
|
||||||
|
// Immediately try to write to the part of the larger mapping that did not change.
|
||||||
|
// Do this a lot, with the smaller mapping gradually increasing in size. Should not crash, data should not be lost.
|
||||||
|
|
||||||
|
ulong pageSize = 0x1000;
|
||||||
|
int mappingExpandCount = (int)(vaSize / (pageSize * 2)) - 1;
|
||||||
|
ulong vaCenter = vaSize / 2;
|
||||||
|
|
||||||
|
for (int i = 1; i <= mappingExpandCount; i++)
|
||||||
|
{
|
||||||
|
ulong start = vaCenter - (pageSize * (ulong)i);
|
||||||
|
ulong size = pageSize * (ulong)i * 2;
|
||||||
|
|
||||||
|
ulong startPa = start + vaSize;
|
||||||
|
|
||||||
|
memory.MapView(backing, startPa, start, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
// On Windows, this should put unmap counts on the thread local map.
|
||||||
|
if (OperatingSystem.IsWindows())
|
||||||
|
{
|
||||||
|
// One thread should be present on the thread local map. Trimming should remove it.
|
||||||
|
Assert.AreEqual(1, CountThreads(ref state));
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldAccess = false;
|
||||||
|
testThread.Join();
|
||||||
|
|
||||||
|
Assert.False(error);
|
||||||
|
|
||||||
|
string test = null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
test.IndexOf('1');
|
||||||
|
}
|
||||||
|
catch (NullReferenceException)
|
||||||
|
{
|
||||||
|
// This shouldn't freeze.
|
||||||
|
}
|
||||||
|
|
||||||
|
if (OperatingSystem.IsWindows())
|
||||||
|
{
|
||||||
|
state.TrimThreads();
|
||||||
|
|
||||||
|
Assert.AreEqual(0, CountThreads(ref state));
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Use this to test invalid access. Can't put this in the test suite unfortunately as invalid access crashes the test process.
|
||||||
|
* memory.Reprotect(vaSize - 0x1000, 0x1000, MemoryPermission.None);
|
||||||
|
* //memory.UnmapView(backing, vaSize - 0x1000, 0x1000);
|
||||||
|
* memory.Read<int>(vaSize - 0x1000);
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
exceptionHandler.Dispose();
|
||||||
|
unusedMainMemory.Dispose();
|
||||||
|
memory.Dispose();
|
||||||
|
backing.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public unsafe void PartialUnmapNative()
|
||||||
|
{
|
||||||
|
if (OperatingSystem.IsMacOS())
|
||||||
|
{
|
||||||
|
// Memory aliasing tests fail on CI at the moment.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up an address space to test partial unmapping.
|
||||||
|
// Should register the signal handler to deal with this on Windows.
|
||||||
|
ulong vaSize = 0x100000;
|
||||||
|
|
||||||
|
// The first 0x100000 is mapped to start. It is replaced from the center with the 0x200000 mapping.
|
||||||
|
var backing = new MemoryBlock(vaSize * 2, MemoryAllocationFlags.Mirrorable);
|
||||||
|
|
||||||
|
(MemoryBlock mainMemory, MemoryBlock unusedMirror, MemoryEhMeilleure exceptionHandler) = GetVirtual(vaSize * 2);
|
||||||
|
|
||||||
|
EnsureTranslator();
|
||||||
|
|
||||||
|
ref var state = ref PartialUnmapState.GetRef();
|
||||||
|
|
||||||
|
// Create some state to be used for managing the native writing loop.
|
||||||
|
int stateSize = Unsafe.SizeOf<NativeWriteLoopState>();
|
||||||
|
var statePtr = Marshal.AllocHGlobal(stateSize);
|
||||||
|
Unsafe.InitBlockUnaligned((void*)statePtr, 0, (uint)stateSize);
|
||||||
|
|
||||||
|
ref NativeWriteLoopState writeLoopState = ref Unsafe.AsRef<NativeWriteLoopState>((void*)statePtr);
|
||||||
|
writeLoopState.Running = 1;
|
||||||
|
writeLoopState.Error = 0;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Globally reset the struct for handling partial unmap races.
|
||||||
|
PartialUnmapState.Reset();
|
||||||
|
|
||||||
|
// Create a large mapping.
|
||||||
|
mainMemory.MapView(backing, 0, 0, vaSize);
|
||||||
|
|
||||||
|
var writeFunc = TestMethods.GenerateDebugNativeWriteLoop();
|
||||||
|
IntPtr writePtr = mainMemory.GetPointer(vaSize - 0x1000, 4);
|
||||||
|
|
||||||
|
Thread testThread = new Thread(() =>
|
||||||
|
{
|
||||||
|
writeFunc(statePtr, writePtr);
|
||||||
|
});
|
||||||
|
|
||||||
|
testThread.Start();
|
||||||
|
|
||||||
|
// Create a smaller mapping, covering the larger mapping.
|
||||||
|
// Immediately try to write to the part of the larger mapping that did not change.
|
||||||
|
// Do this a lot, with the smaller mapping gradually increasing in size. Should not crash, data should not be lost.
|
||||||
|
|
||||||
|
ulong pageSize = 0x1000;
|
||||||
|
int mappingExpandCount = (int)(vaSize / (pageSize * 2)) - 1;
|
||||||
|
ulong vaCenter = vaSize / 2;
|
||||||
|
|
||||||
|
for (int i = 1; i <= mappingExpandCount; i++)
|
||||||
|
{
|
||||||
|
ulong start = vaCenter - (pageSize * (ulong)i);
|
||||||
|
ulong size = pageSize * (ulong)i * 2;
|
||||||
|
|
||||||
|
ulong startPa = start + vaSize;
|
||||||
|
|
||||||
|
mainMemory.MapView(backing, startPa, start, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
writeLoopState.Running = 0;
|
||||||
|
testThread.Join();
|
||||||
|
|
||||||
|
Assert.False(writeLoopState.Error != 0);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Marshal.FreeHGlobal(statePtr);
|
||||||
|
|
||||||
|
exceptionHandler.Dispose();
|
||||||
|
mainMemory.Dispose();
|
||||||
|
unusedMirror.Dispose();
|
||||||
|
backing.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void ThreadLocalMap()
|
||||||
|
{
|
||||||
|
if (!OperatingSystem.IsWindows())
|
||||||
|
{
|
||||||
|
// Only test in Windows, as this is only used on Windows and uses Windows APIs for trimming.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
PartialUnmapState.Reset();
|
||||||
|
ref var state = ref PartialUnmapState.GetRef();
|
||||||
|
|
||||||
|
bool running = true;
|
||||||
|
var testThread = new Thread(() =>
|
||||||
|
{
|
||||||
|
if (!OperatingSystem.IsWindows())
|
||||||
|
{
|
||||||
|
// Need this here to avoid a warning.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
PartialUnmapState.GetRef().RetryFromAccessViolation();
|
||||||
|
while (running)
|
||||||
|
{
|
||||||
|
Thread.Sleep(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
testThread.Start();
|
||||||
|
Thread.Sleep(200);
|
||||||
|
|
||||||
|
Assert.AreEqual(1, CountThreads(ref state));
|
||||||
|
|
||||||
|
// Trimming should not remove the thread as it's still active.
|
||||||
|
state.TrimThreads();
|
||||||
|
Assert.AreEqual(1, CountThreads(ref state));
|
||||||
|
|
||||||
|
running = false;
|
||||||
|
|
||||||
|
testThread.Join();
|
||||||
|
|
||||||
|
// Should trim now that it's inactive.
|
||||||
|
state.TrimThreads();
|
||||||
|
Assert.AreEqual(0, CountThreads(ref state));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public unsafe void ThreadLocalMapNative()
|
||||||
|
{
|
||||||
|
if (!OperatingSystem.IsWindows())
|
||||||
|
{
|
||||||
|
// Only test in Windows, as this is only used on Windows and uses Windows APIs for trimming.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
EnsureTranslator();
|
||||||
|
|
||||||
|
PartialUnmapState.Reset();
|
||||||
|
|
||||||
|
ref var state = ref PartialUnmapState.GetRef();
|
||||||
|
|
||||||
|
fixed (void* localMap = &state.LocalCounts)
|
||||||
|
{
|
||||||
|
var getOrReserve = TestMethods.GenerateDebugThreadLocalMapGetOrReserve((IntPtr)localMap);
|
||||||
|
|
||||||
|
for (int i = 0; i < ThreadLocalMap<int>.MapSize; i++)
|
||||||
|
{
|
||||||
|
// Should obtain the index matching the call #.
|
||||||
|
Assert.AreEqual(i, getOrReserve(i + 1, i));
|
||||||
|
|
||||||
|
// Check that this and all previously reserved thread IDs and struct contents are intact.
|
||||||
|
for (int j = 0; j <= i; j++)
|
||||||
|
{
|
||||||
|
Assert.AreEqual(j + 1, state.LocalCounts.ThreadIds[j]);
|
||||||
|
Assert.AreEqual(j, state.LocalCounts.Structs[j]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trying to reserve again when the map is full should return -1.
|
||||||
|
Assert.AreEqual(-1, getOrReserve(200, 0));
|
||||||
|
|
||||||
|
for (int i = 0; i < ThreadLocalMap<int>.MapSize; i++)
|
||||||
|
{
|
||||||
|
// Should obtain the index matching the call #, as it already exists.
|
||||||
|
Assert.AreEqual(i, getOrReserve(i + 1, -1));
|
||||||
|
|
||||||
|
// The struct should not be reset to -1.
|
||||||
|
Assert.AreEqual(i, state.LocalCounts.Structs[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear one of the ids as if it were freed.
|
||||||
|
state.LocalCounts.ThreadIds[13] = 0;
|
||||||
|
|
||||||
|
// GetOrReserve should now obtain and return 13.
|
||||||
|
Assert.AreEqual(13, getOrReserve(300, 301));
|
||||||
|
Assert.AreEqual(300, state.LocalCounts.ThreadIds[13]);
|
||||||
|
Assert.AreEqual(301, state.LocalCounts.Structs[13]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void NativeReaderWriterLock()
|
||||||
|
{
|
||||||
|
var rwLock = new NativeReaderWriterLock();
|
||||||
|
var threads = new List<Thread>();
|
||||||
|
|
||||||
|
int value = 0;
|
||||||
|
|
||||||
|
bool running = true;
|
||||||
|
bool error = false;
|
||||||
|
int readersAllowed = 1;
|
||||||
|
|
||||||
|
for (int i = 0; i < 5; i++)
|
||||||
|
{
|
||||||
|
var readThread = new Thread(() =>
|
||||||
|
{
|
||||||
|
int count = 0;
|
||||||
|
while (running)
|
||||||
|
{
|
||||||
|
rwLock.AcquireReaderLock();
|
||||||
|
|
||||||
|
int originalValue = Thread.VolatileRead(ref value);
|
||||||
|
|
||||||
|
count++;
|
||||||
|
|
||||||
|
// Spin a bit.
|
||||||
|
for (int i = 0; i < 100; i++)
|
||||||
|
{
|
||||||
|
if (Thread.VolatileRead(ref readersAllowed) == 0)
|
||||||
|
{
|
||||||
|
error = true;
|
||||||
|
running = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should not change while the lock is held.
|
||||||
|
if (Thread.VolatileRead(ref value) != originalValue)
|
||||||
|
{
|
||||||
|
error = true;
|
||||||
|
running = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
rwLock.ReleaseReaderLock();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
threads.Add(readThread);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < 2; i++)
|
||||||
|
{
|
||||||
|
var writeThread = new Thread(() =>
|
||||||
|
{
|
||||||
|
int count = 0;
|
||||||
|
while (running)
|
||||||
|
{
|
||||||
|
rwLock.AcquireReaderLock();
|
||||||
|
rwLock.UpgradeToWriterLock();
|
||||||
|
|
||||||
|
Thread.Sleep(2);
|
||||||
|
count++;
|
||||||
|
|
||||||
|
Interlocked.Exchange(ref readersAllowed, 0);
|
||||||
|
|
||||||
|
for (int i = 0; i < 10; i++)
|
||||||
|
{
|
||||||
|
Interlocked.Increment(ref value);
|
||||||
|
}
|
||||||
|
|
||||||
|
Interlocked.Exchange(ref readersAllowed, 1);
|
||||||
|
|
||||||
|
rwLock.DowngradeFromWriterLock();
|
||||||
|
rwLock.ReleaseReaderLock();
|
||||||
|
|
||||||
|
Thread.Sleep(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
threads.Add(writeThread);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var thread in threads)
|
||||||
|
{
|
||||||
|
thread.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
Thread.Sleep(1000);
|
||||||
|
|
||||||
|
running = false;
|
||||||
|
|
||||||
|
foreach (var thread in threads)
|
||||||
|
{
|
||||||
|
thread.Join();
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.False(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net6.0</TargetFramework>
|
<TargetFramework>net6.0</TargetFramework>
|
||||||
@@ -9,10 +9,12 @@
|
|||||||
<TargetOS Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::OSX)))' == 'true'">osx</TargetOS>
|
<TargetOS Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::OSX)))' == 'true'">osx</TargetOS>
|
||||||
<TargetOS Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Linux)))' == 'true'">linux</TargetOS>
|
<TargetOS Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Linux)))' == 'true'">linux</TargetOS>
|
||||||
<Configurations>Debug;Release</Configurations>
|
<Configurations>Debug;Release</Configurations>
|
||||||
|
<RunSettingsFilePath>$(MSBuildProjectDirectory)\.runsettings</RunSettingsFilePath>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||||
|
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@@ -25,6 +27,7 @@
|
|||||||
<ProjectReference Include="..\Ryujinx.Audio\Ryujinx.Audio.csproj" />
|
<ProjectReference Include="..\Ryujinx.Audio\Ryujinx.Audio.csproj" />
|
||||||
<ProjectReference Include="..\Ryujinx.Cpu\Ryujinx.Cpu.csproj" />
|
<ProjectReference Include="..\Ryujinx.Cpu\Ryujinx.Cpu.csproj" />
|
||||||
<ProjectReference Include="..\Ryujinx.HLE\Ryujinx.HLE.csproj" />
|
<ProjectReference Include="..\Ryujinx.HLE\Ryujinx.HLE.csproj" />
|
||||||
|
<ProjectReference Include="..\Ryujinx.Memory.Tests\Ryujinx.Memory.Tests.csproj" />
|
||||||
<ProjectReference Include="..\Ryujinx.Memory\Ryujinx.Memory.csproj" />
|
<ProjectReference Include="..\Ryujinx.Memory\Ryujinx.Memory.csproj" />
|
||||||
<ProjectReference Include="..\Ryujinx.Tests.Unicorn\Ryujinx.Tests.Unicorn.csproj" />
|
<ProjectReference Include="..\Ryujinx.Tests.Unicorn\Ryujinx.Tests.Unicorn.csproj" />
|
||||||
<ProjectReference Include="..\ARMeilleure\ARMeilleure.csproj" />
|
<ProjectReference Include="..\ARMeilleure\ARMeilleure.csproj" />
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ namespace Ryujinx.Ui.Common.Configuration
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The current version of the file format
|
/// The current version of the file format
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const int CurrentVersion = 38;
|
public const int CurrentVersion = 39;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Version of the configuration file format
|
/// Version of the configuration file format
|
||||||
|
|||||||
@@ -648,7 +648,9 @@ namespace Ryujinx.Ui.Common.Configuration
|
|||||||
ToggleMute = Key.F2,
|
ToggleMute = Key.F2,
|
||||||
Screenshot = Key.F8,
|
Screenshot = Key.F8,
|
||||||
ShowUi = Key.F4,
|
ShowUi = Key.F4,
|
||||||
Pause = Key.F5
|
Pause = Key.F5,
|
||||||
|
ResScaleUp = Key.Unbound,
|
||||||
|
ResScaleDown = Key.Unbound
|
||||||
};
|
};
|
||||||
Hid.InputConfig.Value = new List<InputConfig>
|
Hid.InputConfig.Value = new List<InputConfig>
|
||||||
{
|
{
|
||||||
@@ -1096,6 +1098,22 @@ namespace Ryujinx.Ui.Common.Configuration
|
|||||||
configurationFileUpdated = true;
|
configurationFileUpdated = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (configurationFileFormat.Version < 39)
|
||||||
|
{
|
||||||
|
Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 39.");
|
||||||
|
|
||||||
|
configurationFileFormat.Hotkeys = new KeyboardHotkeys
|
||||||
|
{
|
||||||
|
ToggleVsync = configurationFileFormat.Hotkeys.ToggleVsync,
|
||||||
|
Screenshot = configurationFileFormat.Hotkeys.Screenshot,
|
||||||
|
ShowUi = configurationFileFormat.Hotkeys.ShowUi,
|
||||||
|
Pause = configurationFileFormat.Hotkeys.Pause,
|
||||||
|
ToggleMute = configurationFileFormat.Hotkeys.ToggleMute,
|
||||||
|
ResScaleUp = Key.Unbound,
|
||||||
|
ResScaleDown = Key.Unbound
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
Logger.EnableFileLog.Value = configurationFileFormat.EnableFileLog;
|
Logger.EnableFileLog.Value = configurationFileFormat.EnableFileLog;
|
||||||
Graphics.ResScale.Value = configurationFileFormat.ResScale;
|
Graphics.ResScale.Value = configurationFileFormat.ResScale;
|
||||||
Graphics.ResScaleCustom.Value = configurationFileFormat.ResScaleCustom;
|
Graphics.ResScaleCustom.Value = configurationFileFormat.ResScaleCustom;
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
<object class="GtkMenuItem" id="_loadApplicationFile">
|
<object class="GtkMenuItem" id="_loadApplicationFile">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can_focus">False</property>
|
<property name="can_focus">False</property>
|
||||||
<property name="tooltip_text" translatable="yes">Open a file chooser to chose a switch compatible file to load</property>
|
<property name="tooltip_text" translatable="yes">Open a file explorer to choose a Switch compatible file to load</property>
|
||||||
<property name="label" translatable="yes">Load Application from File</property>
|
<property name="label" translatable="yes">Load Application from File</property>
|
||||||
<property name="use_underline">True</property>
|
<property name="use_underline">True</property>
|
||||||
<signal name="activate" handler="Load_Application_File" swapped="no"/>
|
<signal name="activate" handler="Load_Application_File" swapped="no"/>
|
||||||
@@ -39,7 +39,7 @@
|
|||||||
<object class="GtkMenuItem" id="_loadApplicationFolder">
|
<object class="GtkMenuItem" id="_loadApplicationFolder">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can_focus">False</property>
|
<property name="can_focus">False</property>
|
||||||
<property name="tooltip_text" translatable="yes">Open a file chooser to chose a switch compatible, unpacked application to load</property>
|
<property name="tooltip_text" translatable="yes">Open a file explorer to choose a Switch compatible, unpacked application to load</property>
|
||||||
<property name="label" translatable="yes">Load Unpacked Game</property>
|
<property name="label" translatable="yes">Load Unpacked Game</property>
|
||||||
<property name="use_underline">True</property>
|
<property name="use_underline">True</property>
|
||||||
<signal name="activate" handler="Load_Application_Folder" swapped="no"/>
|
<signal name="activate" handler="Load_Application_Folder" swapped="no"/>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ using Ryujinx.Common;
|
|||||||
using Ryujinx.Common.Configuration;
|
using Ryujinx.Common.Configuration;
|
||||||
using Ryujinx.Common.Logging;
|
using Ryujinx.Common.Logging;
|
||||||
using Ryujinx.Ui.Common.Configuration;
|
using Ryujinx.Ui.Common.Configuration;
|
||||||
|
using Ryujinx.Graphics.Gpu;
|
||||||
using Ryujinx.Graphics.GAL;
|
using Ryujinx.Graphics.GAL;
|
||||||
using Ryujinx.Graphics.GAL.Multithreading;
|
using Ryujinx.Graphics.GAL.Multithreading;
|
||||||
using Ryujinx.Input;
|
using Ryujinx.Input;
|
||||||
@@ -33,6 +34,7 @@ namespace Ryujinx.Ui
|
|||||||
private const int SwitchPanelWidth = 1280;
|
private const int SwitchPanelWidth = 1280;
|
||||||
private const int SwitchPanelHeight = 720;
|
private const int SwitchPanelHeight = 720;
|
||||||
private const int TargetFps = 60;
|
private const int TargetFps = 60;
|
||||||
|
private const float MaxResolutionScale = 4.0f; // Max resolution hotkeys can scale to before wrapping.
|
||||||
|
|
||||||
public ManualResetEvent WaitEvent { get; set; }
|
public ManualResetEvent WaitEvent { get; set; }
|
||||||
public NpadManager NpadManager { get; }
|
public NpadManager NpadManager { get; }
|
||||||
@@ -618,6 +620,19 @@ namespace Ryujinx.Ui
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (currentHotkeyState.HasFlag(KeyboardHotkeyState.ResScaleUp) &&
|
||||||
|
!_prevHotkeyState.HasFlag(KeyboardHotkeyState.ResScaleUp))
|
||||||
|
{
|
||||||
|
GraphicsConfig.ResScale = GraphicsConfig.ResScale % MaxResolutionScale + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentHotkeyState.HasFlag(KeyboardHotkeyState.ResScaleDown) &&
|
||||||
|
!_prevHotkeyState.HasFlag(KeyboardHotkeyState.ResScaleDown))
|
||||||
|
{
|
||||||
|
GraphicsConfig.ResScale =
|
||||||
|
(MaxResolutionScale + GraphicsConfig.ResScale - 2) % MaxResolutionScale + 1;
|
||||||
|
}
|
||||||
|
|
||||||
_prevHotkeyState = currentHotkeyState;
|
_prevHotkeyState = currentHotkeyState;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -648,7 +663,9 @@ namespace Ryujinx.Ui
|
|||||||
Screenshot = 1 << 1,
|
Screenshot = 1 << 1,
|
||||||
ShowUi = 1 << 2,
|
ShowUi = 1 << 2,
|
||||||
Pause = 1 << 3,
|
Pause = 1 << 3,
|
||||||
ToggleMute = 1 << 4
|
ToggleMute = 1 << 4,
|
||||||
|
ResScaleUp = 1 << 5,
|
||||||
|
ResScaleDown = 1 << 6
|
||||||
}
|
}
|
||||||
|
|
||||||
private KeyboardHotkeyState GetHotkeyState()
|
private KeyboardHotkeyState GetHotkeyState()
|
||||||
@@ -680,6 +697,16 @@ namespace Ryujinx.Ui
|
|||||||
state |= KeyboardHotkeyState.ToggleMute;
|
state |= KeyboardHotkeyState.ToggleMute;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.ResScaleUp))
|
||||||
|
{
|
||||||
|
state |= KeyboardHotkeyState.ResScaleUp;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.ResScaleDown))
|
||||||
|
{
|
||||||
|
state |= KeyboardHotkeyState.ResScaleDown;
|
||||||
|
}
|
||||||
|
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ namespace Ryujinx.Ui.Windows
|
|||||||
private readonly VirtualFileSystem _virtualFileSystem;
|
private readonly VirtualFileSystem _virtualFileSystem;
|
||||||
private readonly string _titleId;
|
private readonly string _titleId;
|
||||||
private readonly string _dlcJsonPath;
|
private readonly string _dlcJsonPath;
|
||||||
private readonly List<DlcContainer> _dlcContainerList;
|
private readonly List<DownloadableContentContainer> _dlcContainerList;
|
||||||
|
|
||||||
#pragma warning disable CS0649, IDE0044
|
#pragma warning disable CS0649, IDE0044
|
||||||
[GUI] Label _baseTitleInfoLabel;
|
[GUI] Label _baseTitleInfoLabel;
|
||||||
@@ -45,11 +45,11 @@ namespace Ryujinx.Ui.Windows
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_dlcContainerList = JsonHelper.DeserializeFromFile<List<DlcContainer>>(_dlcJsonPath);
|
_dlcContainerList = JsonHelper.DeserializeFromFile<List<DownloadableContentContainer>>(_dlcJsonPath);
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
_dlcContainerList = new List<DlcContainer>();
|
_dlcContainerList = new List<DownloadableContentContainer>();
|
||||||
}
|
}
|
||||||
|
|
||||||
_dlcTreeView.Model = new TreeStore(typeof(bool), typeof(string), typeof(string));
|
_dlcTreeView.Model = new TreeStore(typeof(bool), typeof(string), typeof(string));
|
||||||
@@ -75,37 +75,37 @@ namespace Ryujinx.Ui.Windows
|
|||||||
_dlcTreeView.AppendColumn("TitleId", new CellRendererText(), "text", 1);
|
_dlcTreeView.AppendColumn("TitleId", new CellRendererText(), "text", 1);
|
||||||
_dlcTreeView.AppendColumn("Path", new CellRendererText(), "text", 2);
|
_dlcTreeView.AppendColumn("Path", new CellRendererText(), "text", 2);
|
||||||
|
|
||||||
foreach (DlcContainer dlcContainer in _dlcContainerList)
|
foreach (DownloadableContentContainer dlcContainer in _dlcContainerList)
|
||||||
{
|
{
|
||||||
if (File.Exists(dlcContainer.Path))
|
if (File.Exists(dlcContainer.ContainerPath))
|
||||||
{
|
{
|
||||||
// The parent tree item has its own "enabled" check box, but it's the actual
|
// The parent tree item has its own "enabled" check box, but it's the actual
|
||||||
// nca entries that store the enabled / disabled state. A bit of a UI inconsistency.
|
// nca entries that store the enabled / disabled state. A bit of a UI inconsistency.
|
||||||
// Maybe a tri-state check box would be better, but for now we check the parent
|
// Maybe a tri-state check box would be better, but for now we check the parent
|
||||||
// "enabled" box if all child NCAs are enabled. Usually fine since each nsp has only one nca.
|
// "enabled" box if all child NCAs are enabled. Usually fine since each nsp has only one nca.
|
||||||
bool areAllContentPacksEnabled = dlcContainer.DlcNcaList.TrueForAll((nca) => nca.Enabled);
|
bool areAllContentPacksEnabled = dlcContainer.DownloadableContentNcaList.TrueForAll((nca) => nca.Enabled);
|
||||||
TreeIter parentIter = ((TreeStore)_dlcTreeView.Model).AppendValues(areAllContentPacksEnabled, "", dlcContainer.Path);
|
TreeIter parentIter = ((TreeStore)_dlcTreeView.Model).AppendValues(areAllContentPacksEnabled, "", dlcContainer.ContainerPath);
|
||||||
using FileStream containerFile = File.OpenRead(dlcContainer.Path);
|
using FileStream containerFile = File.OpenRead(dlcContainer.ContainerPath);
|
||||||
PartitionFileSystem pfs = new PartitionFileSystem(containerFile.AsStorage());
|
PartitionFileSystem pfs = new PartitionFileSystem(containerFile.AsStorage());
|
||||||
_virtualFileSystem.ImportTickets(pfs);
|
_virtualFileSystem.ImportTickets(pfs);
|
||||||
|
|
||||||
foreach (DlcNca dlcNca in dlcContainer.DlcNcaList)
|
foreach (DownloadableContentNca dlcNca in dlcContainer.DownloadableContentNcaList)
|
||||||
{
|
{
|
||||||
using var ncaFile = new UniqueRef<IFile>();
|
using var ncaFile = new UniqueRef<IFile>();
|
||||||
|
|
||||||
pfs.OpenFile(ref ncaFile.Ref(), dlcNca.Path.ToU8Span(), OpenMode.Read).ThrowIfFailure();
|
pfs.OpenFile(ref ncaFile.Ref(), dlcNca.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
|
||||||
Nca nca = TryCreateNca(ncaFile.Get.AsStorage(), dlcContainer.Path);
|
Nca nca = TryCreateNca(ncaFile.Get.AsStorage(), dlcContainer.ContainerPath);
|
||||||
|
|
||||||
if (nca != null)
|
if (nca != null)
|
||||||
{
|
{
|
||||||
((TreeStore)_dlcTreeView.Model).AppendValues(parentIter, dlcNca.Enabled, nca.Header.TitleId.ToString("X16"), dlcNca.Path);
|
((TreeStore)_dlcTreeView.Model).AppendValues(parentIter, dlcNca.Enabled, nca.Header.TitleId.ToString("X16"), dlcNca.FullPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// DLC file moved or renamed. Allow the user to remove it without crashing the whole dialog.
|
// DLC file moved or renamed. Allow the user to remove it without crashing the whole dialog.
|
||||||
TreeIter parentIter = ((TreeStore)_dlcTreeView.Model).AppendValues(false, "", $"(MISSING) {dlcContainer.Path}");
|
TreeIter parentIter = ((TreeStore)_dlcTreeView.Model).AppendValues(false, "", $"(MISSING) {dlcContainer.ContainerPath}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -237,19 +237,19 @@ namespace Ryujinx.Ui.Windows
|
|||||||
{
|
{
|
||||||
if (_dlcTreeView.Model.IterChildren(out TreeIter childIter, parentIter))
|
if (_dlcTreeView.Model.IterChildren(out TreeIter childIter, parentIter))
|
||||||
{
|
{
|
||||||
DlcContainer dlcContainer = new DlcContainer
|
DownloadableContentContainer dlcContainer = new DownloadableContentContainer
|
||||||
{
|
{
|
||||||
Path = (string)_dlcTreeView.Model.GetValue(parentIter, 2),
|
ContainerPath = (string)_dlcTreeView.Model.GetValue(parentIter, 2),
|
||||||
DlcNcaList = new List<DlcNca>()
|
DownloadableContentNcaList = new List<DownloadableContentNca>()
|
||||||
};
|
};
|
||||||
|
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
dlcContainer.DlcNcaList.Add(new DlcNca
|
dlcContainer.DownloadableContentNcaList.Add(new DownloadableContentNca
|
||||||
{
|
{
|
||||||
Enabled = (bool)_dlcTreeView.Model.GetValue(childIter, 0),
|
Enabled = (bool)_dlcTreeView.Model.GetValue(childIter, 0),
|
||||||
TitleId = Convert.ToUInt64(_dlcTreeView.Model.GetValue(childIter, 1).ToString(), 16),
|
TitleId = Convert.ToUInt64(_dlcTreeView.Model.GetValue(childIter, 1).ToString(), 16),
|
||||||
Path = (string)_dlcTreeView.Model.GetValue(childIter, 2)
|
FullPath = (string)_dlcTreeView.Model.GetValue(childIter, 2)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
while (_dlcTreeView.Model.IterNext(ref childIter));
|
while (_dlcTreeView.Model.IterNext(ref childIter));
|
||||||
|
|||||||
@@ -110,7 +110,7 @@
|
|||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can-focus">True</property>
|
<property name="can-focus">True</property>
|
||||||
<property name="receives-default">False</property>
|
<property name="receives-default">False</property>
|
||||||
<property name="tooltip-text" translatable="yes">Enables or disables Discord Rich Presence</property>
|
<property name="tooltip-text" translatable="yes">Choose whether or not to display Ryujinx on your "currently playing" Discord activity</property>
|
||||||
<property name="halign">start</property>
|
<property name="halign">start</property>
|
||||||
<property name="draw-indicator">True</property>
|
<property name="draw-indicator">True</property>
|
||||||
</object>
|
</object>
|
||||||
@@ -264,7 +264,7 @@
|
|||||||
<object class="GtkEntry" id="_addGameDirBox">
|
<object class="GtkEntry" id="_addGameDirBox">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can-focus">True</property>
|
<property name="can-focus">True</property>
|
||||||
<property name="tooltip-text" translatable="yes">Enter a game directroy to add to the list</property>
|
<property name="tooltip-text" translatable="yes">Enter a game directory to add to the list</property>
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
<packing>
|
||||||
<property name="expand">True</property>
|
<property name="expand">True</property>
|
||||||
@@ -494,7 +494,7 @@
|
|||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can-focus">True</property>
|
<property name="can-focus">True</property>
|
||||||
<property name="receives-default">False</property>
|
<property name="receives-default">False</property>
|
||||||
<property name="tooltip-text" translatable="yes">Enable or disable Docked Mode</property>
|
<property name="tooltip-text" translatable="yes">Docked mode makes the emulated system behave as a docked Nintendo Switch. This improves graphical fidelity in most games. Conversely, disabling this will make the emulated system behave as a handheld Nintendo Switch, reducing graphics quality. Configure player 1 controls if planning to use docked mode; configure handheld controls if planning to use handheld mode. Leave ON if unsure.</property>
|
||||||
<property name="draw-indicator">True</property>
|
<property name="draw-indicator">True</property>
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
<packing>
|
||||||
@@ -510,7 +510,7 @@
|
|||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can-focus">True</property>
|
<property name="can-focus">True</property>
|
||||||
<property name="receives-default">False</property>
|
<property name="receives-default">False</property>
|
||||||
<property name="tooltip-text" translatable="yes">Enable or disable "direct keyboard access (HID) support" (Provides games access to your keyboard as a text entry device)</property>
|
<property name="tooltip-text" translatable="yes">Direct keyboard access (HID) support. Provides games access to your keyboard as a text entry device.</property>
|
||||||
<property name="draw-indicator">True</property>
|
<property name="draw-indicator">True</property>
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
<packing>
|
||||||
@@ -526,7 +526,7 @@
|
|||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can-focus">True</property>
|
<property name="can-focus">True</property>
|
||||||
<property name="receives-default">False</property>
|
<property name="receives-default">False</property>
|
||||||
<property name="tooltip-text" translatable="yes">Enable or disable "direct mouse access (HID) support" (Provides games access to your mouse as a pointing device)</property>
|
<property name="tooltip-text" translatable="yes">Direct mouse access (HID) support. Provides games access to your mouse as a pointing device.</property>
|
||||||
<property name="draw-indicator">True</property>
|
<property name="draw-indicator">True</property>
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
<packing>
|
||||||
@@ -1477,7 +1477,7 @@
|
|||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can-focus">True</property>
|
<property name="can-focus">True</property>
|
||||||
<property name="receives-default">False</property>
|
<property name="receives-default">False</property>
|
||||||
<property name="tooltip-text" translatable="yes">Enables or disables Vertical Sync</property>
|
<property name="tooltip-text" translatable="yes">Emulated console's Vertical Sync. Essentially a frame-limiter for the majority of games; disabling it may cause games to run at higher speed or make loading screens take longer or get stuck. Can be toggled in-game with a hotkey of your preference. We recommend doing this if you plan on disabling it. Leave ON if unsure.</property>
|
||||||
<property name="halign">start</property>
|
<property name="halign">start</property>
|
||||||
<property name="margin-top">5</property>
|
<property name="margin-top">5</property>
|
||||||
<property name="margin-bottom">5</property>
|
<property name="margin-bottom">5</property>
|
||||||
@@ -1495,7 +1495,7 @@
|
|||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can-focus">True</property>
|
<property name="can-focus">True</property>
|
||||||
<property name="receives-default">False</property>
|
<property name="receives-default">False</property>
|
||||||
<property name="tooltip-text" translatable="yes">Enables or disables profiled translation cache persistency</property>
|
<property name="tooltip-text" translatable="yes">Saves translated JIT functions so that they do not need to be translated every time the game loads. Reduces stuttering and significantly speeds up boot times after the first boot of a game. Leave ON if unsure.</property>
|
||||||
<property name="halign">start</property>
|
<property name="halign">start</property>
|
||||||
<property name="margin-top">5</property>
|
<property name="margin-top">5</property>
|
||||||
<property name="margin-bottom">5</property>
|
<property name="margin-bottom">5</property>
|
||||||
@@ -1513,7 +1513,7 @@
|
|||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can-focus">True</property>
|
<property name="can-focus">True</property>
|
||||||
<property name="receives-default">False</property>
|
<property name="receives-default">False</property>
|
||||||
<property name="tooltip-text" translatable="yes">Enables guest Internet access. If enabled, the application will behave as if the emulated Switch console was connected to the Internet. Note that in some cases, applications may still access the Internet even with this option disabled</property>
|
<property name="tooltip-text" translatable="yes">Allows the emulated application to connect to the Internet. Games with a LAN mode can connect to each other when this is enabled and the systems are connected to the same access point. This includes real consoles as well. Does NOT allow connecting to Nintendo servers. May cause crashing in certain games that try to connect to the Internet. Leave OFF if unsure.</property>
|
||||||
<property name="halign">start</property>
|
<property name="halign">start</property>
|
||||||
<property name="margin-top">5</property>
|
<property name="margin-top">5</property>
|
||||||
<property name="margin-bottom">5</property>
|
<property name="margin-bottom">5</property>
|
||||||
@@ -1531,7 +1531,7 @@
|
|||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can-focus">True</property>
|
<property name="can-focus">True</property>
|
||||||
<property name="receives-default">False</property>
|
<property name="receives-default">False</property>
|
||||||
<property name="tooltip-text" translatable="yes">Enables integrity checks on Game content files</property>
|
<property name="tooltip-text" translatable="yes">Checks for corrupt files when booting a game, and if corrupt files are detected, displays a hash error in the log. Has no impact on performance and is meant to help troubleshooting. Leave ON if unsure.</property>
|
||||||
<property name="halign">start</property>
|
<property name="halign">start</property>
|
||||||
<property name="margin-top">5</property>
|
<property name="margin-top">5</property>
|
||||||
<property name="margin-bottom">5</property>
|
<property name="margin-bottom">5</property>
|
||||||
@@ -1561,7 +1561,7 @@
|
|||||||
<object class="GtkLabel">
|
<object class="GtkLabel">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can-focus">False</property>
|
<property name="can-focus">False</property>
|
||||||
<property name="tooltip-text" translatable="yes">Change Audio Backend</property>
|
<property name="tooltip-text" translatable="yes">Changes the backend used to render audio. SDL2 is the preferred one, while OpenAL and SoundIO are used as fallbacks. Dummy will have no sound. Set to SDL2 if unsure.</property>
|
||||||
<property name="halign">end</property>
|
<property name="halign">end</property>
|
||||||
<property name="margin-right">5</property>
|
<property name="margin-right">5</property>
|
||||||
<property name="label" translatable="yes">Audio Backend: </property>
|
<property name="label" translatable="yes">Audio Backend: </property>
|
||||||
@@ -1592,7 +1592,7 @@
|
|||||||
<object class="GtkLabel">
|
<object class="GtkLabel">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can-focus">False</property>
|
<property name="can-focus">False</property>
|
||||||
<property name="tooltip-text" translatable="yes">Change how guest memory is mapped and accessed. Greatly affects emulated CPU performance.</property>
|
<property name="tooltip-text" translatable="yes">Change how guest memory is mapped and accessed. Greatly affects emulated CPU performance. Set to HOST UNCHECKED if unsure.</property>
|
||||||
<property name="halign">end</property>
|
<property name="halign">end</property>
|
||||||
<property name="margin-right">5</property>
|
<property name="margin-right">5</property>
|
||||||
<property name="label" translatable="yes">Memory Manager Mode: </property>
|
<property name="label" translatable="yes">Memory Manager Mode: </property>
|
||||||
@@ -1753,7 +1753,7 @@
|
|||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can-focus">True</property>
|
<property name="can-focus">True</property>
|
||||||
<property name="receives-default">False</property>
|
<property name="receives-default">False</property>
|
||||||
<property name="tooltip-text" translatable="yes">Expands the amount of memory on the emulated system from 4GB to 6GB</property>
|
<property name="tooltip-text" translatable="yes">Increases the amount of memory on the emulated system from 4GB to 6GB. This is only useful for higher-resolution texture packs or 4k resolution mods. Does NOT improve performance. Leave OFF if unsure.</property>
|
||||||
<property name="halign">start</property>
|
<property name="halign">start</property>
|
||||||
<property name="margin-top">5</property>
|
<property name="margin-top">5</property>
|
||||||
<property name="margin-bottom">5</property>
|
<property name="margin-bottom">5</property>
|
||||||
@@ -1771,7 +1771,7 @@
|
|||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can-focus">True</property>
|
<property name="can-focus">True</property>
|
||||||
<property name="receives-default">False</property>
|
<property name="receives-default">False</property>
|
||||||
<property name="tooltip-text" translatable="yes">Enable or disable ignoring missing services</property>
|
<property name="tooltip-text" translatable="yes">Ignores unimplemented Horizon OS services. This may help in bypassing crashes when booting certain games. Leave OFF if unsure.</property>
|
||||||
<property name="halign">start</property>
|
<property name="halign">start</property>
|
||||||
<property name="margin-top">5</property>
|
<property name="margin-top">5</property>
|
||||||
<property name="margin-bottom">5</property>
|
<property name="margin-bottom">5</property>
|
||||||
@@ -1864,7 +1864,7 @@
|
|||||||
<object class="GtkLabel">
|
<object class="GtkLabel">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can-focus">False</property>
|
<property name="can-focus">False</property>
|
||||||
<property name="tooltip-text" translatable="yes">Enable Graphics Backend Multithreading</property>
|
<property name="tooltip-text" translatable="yes">Executes graphics backend commands on a second thread. Speeds up shader compilation, reduces stuttering, and improves performance on GPU drivers without multithreading support of their own. Slightly better performance on drivers with multithreading. Set to AUTO if unsure.</property>
|
||||||
<property name="label" translatable="yes">Graphics Backend Multithreading:</property>
|
<property name="label" translatable="yes">Graphics Backend Multithreading:</property>
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
<packing>
|
||||||
@@ -1878,7 +1878,7 @@
|
|||||||
<object class="GtkComboBoxText" id="_galThreading">
|
<object class="GtkComboBoxText" id="_galThreading">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can-focus">False</property>
|
<property name="can-focus">False</property>
|
||||||
<property name="tooltip-text" translatable="yes">Executes graphics backend commands on a second thread. Allows runtime multithreading of shader compilation, reduces stuttering, and improves performance on drivers without multithreading support of their own. Slightly varying peak performance on drivers with multithreading. Ryujinx may need to be restarted to correctly disable driver built-in multithreading, or you may need to do it manually to get the best performance.</property>
|
<property name="tooltip-text" translatable="yes">Executes graphics backend commands on a second thread. Speeds up shader compilation, reduces stuttering, and improves performance on GPU drivers without multithreading support of their own. Slightly better performance on drivers with multithreading. Set to AUTO if unsure.</property>
|
||||||
<property name="active-id">-1</property>
|
<property name="active-id">-1</property>
|
||||||
<items>
|
<items>
|
||||||
<item id="Auto" translatable="yes">Auto</item>
|
<item id="Auto" translatable="yes">Auto</item>
|
||||||
@@ -1954,7 +1954,7 @@
|
|||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can-focus">True</property>
|
<property name="can-focus">True</property>
|
||||||
<property name="receives-default">False</property>
|
<property name="receives-default">False</property>
|
||||||
<property name="tooltip-text" translatable="yes">Enables or disables Shader Cache</property>
|
<property name="tooltip-text" translatable="yes">Saves a disk shader cache which reduces stuttering in subsequent runs. Leave ON if unsure.</property>
|
||||||
<property name="halign">start</property>
|
<property name="halign">start</property>
|
||||||
<property name="margin-top">5</property>
|
<property name="margin-top">5</property>
|
||||||
<property name="margin-bottom">5</property>
|
<property name="margin-bottom">5</property>
|
||||||
@@ -2306,7 +2306,7 @@
|
|||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can-focus">True</property>
|
<property name="can-focus">True</property>
|
||||||
<property name="receives-default">False</property>
|
<property name="receives-default">False</property>
|
||||||
<property name="tooltip-text" translatable="yes">Enables or disables logging to a file on disk</property>
|
<property name="tooltip-text" translatable="yes">Saves console logging to a log file on disk. Does not affect performance.</property>
|
||||||
<property name="halign">start</property>
|
<property name="halign">start</property>
|
||||||
<property name="margin-top">5</property>
|
<property name="margin-top">5</property>
|
||||||
<property name="margin-bottom">5</property>
|
<property name="margin-bottom">5</property>
|
||||||
@@ -2324,7 +2324,7 @@
|
|||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can-focus">True</property>
|
<property name="can-focus">True</property>
|
||||||
<property name="receives-default">False</property>
|
<property name="receives-default">False</property>
|
||||||
<property name="tooltip-text" translatable="yes">Enables printing stub log messages</property>
|
<property name="tooltip-text" translatable="yes">Prints stub log messages in the console. Does not affect performance.</property>
|
||||||
<property name="halign">start</property>
|
<property name="halign">start</property>
|
||||||
<property name="margin-top">5</property>
|
<property name="margin-top">5</property>
|
||||||
<property name="margin-bottom">5</property>
|
<property name="margin-bottom">5</property>
|
||||||
@@ -2342,7 +2342,7 @@
|
|||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can-focus">True</property>
|
<property name="can-focus">True</property>
|
||||||
<property name="receives-default">False</property>
|
<property name="receives-default">False</property>
|
||||||
<property name="tooltip-text" translatable="yes">Enables printing info log messages</property>
|
<property name="tooltip-text" translatable="yes">Prints info log messages in the console. Does not affect performance.</property>
|
||||||
<property name="halign">start</property>
|
<property name="halign">start</property>
|
||||||
<property name="margin-top">5</property>
|
<property name="margin-top">5</property>
|
||||||
<property name="margin-bottom">5</property>
|
<property name="margin-bottom">5</property>
|
||||||
@@ -2360,7 +2360,7 @@
|
|||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can-focus">True</property>
|
<property name="can-focus">True</property>
|
||||||
<property name="receives-default">False</property>
|
<property name="receives-default">False</property>
|
||||||
<property name="tooltip-text" translatable="yes">Enables printing warning log messages</property>
|
<property name="tooltip-text" translatable="yes">Prints warning log messages in the console. Does not affect performance.</property>
|
||||||
<property name="halign">start</property>
|
<property name="halign">start</property>
|
||||||
<property name="margin-top">5</property>
|
<property name="margin-top">5</property>
|
||||||
<property name="margin-bottom">5</property>
|
<property name="margin-bottom">5</property>
|
||||||
@@ -2378,7 +2378,7 @@
|
|||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can-focus">True</property>
|
<property name="can-focus">True</property>
|
||||||
<property name="receives-default">False</property>
|
<property name="receives-default">False</property>
|
||||||
<property name="tooltip-text" translatable="yes">Enables printing error log messages</property>
|
<property name="tooltip-text" translatable="yes">Prints error log messages in the console. Does not affect performance.</property>
|
||||||
<property name="halign">start</property>
|
<property name="halign">start</property>
|
||||||
<property name="margin-top">5</property>
|
<property name="margin-top">5</property>
|
||||||
<property name="margin-bottom">5</property>
|
<property name="margin-bottom">5</property>
|
||||||
@@ -2396,7 +2396,7 @@
|
|||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can-focus">True</property>
|
<property name="can-focus">True</property>
|
||||||
<property name="receives-default">False</property>
|
<property name="receives-default">False</property>
|
||||||
<property name="tooltip-text" translatable="yes">Enables printing guest log messages</property>
|
<property name="tooltip-text" translatable="yes">Prints guest log messages in the console. Does not affect performance.</property>
|
||||||
<property name="halign">start</property>
|
<property name="halign">start</property>
|
||||||
<property name="margin-top">5</property>
|
<property name="margin-top">5</property>
|
||||||
<property name="margin-bottom">5</property>
|
<property name="margin-bottom">5</property>
|
||||||
@@ -2414,7 +2414,7 @@
|
|||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can-focus">True</property>
|
<property name="can-focus">True</property>
|
||||||
<property name="receives-default">False</property>
|
<property name="receives-default">False</property>
|
||||||
<property name="tooltip-text" translatable="yes">Enables printing fs access log messages</property>
|
<property name="tooltip-text" translatable="yes">Enables FS access log output to the console. Possible modes are 0-3</property>
|
||||||
<property name="halign">start</property>
|
<property name="halign">start</property>
|
||||||
<property name="margin-top">5</property>
|
<property name="margin-top">5</property>
|
||||||
<property name="margin-bottom">5</property>
|
<property name="margin-bottom">5</property>
|
||||||
@@ -2560,7 +2560,7 @@
|
|||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can-focus">True</property>
|
<property name="can-focus">True</property>
|
||||||
<property name="receives-default">False</property>
|
<property name="receives-default">False</property>
|
||||||
<property name="tooltip-text" translatable="yes">Enables printing debug log messages</property>
|
<property name="tooltip-text" translatable="yes">Prints debug log messages in the console. Only use this if specifically instructed by a staff member, as it will make logs difficult to read and worsen emulator performance.</property>
|
||||||
<property name="halign">start</property>
|
<property name="halign">start</property>
|
||||||
<property name="margin-top">5</property>
|
<property name="margin-top">5</property>
|
||||||
<property name="margin-bottom">5</property>
|
<property name="margin-bottom">5</property>
|
||||||
@@ -2578,7 +2578,7 @@
|
|||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can-focus">True</property>
|
<property name="can-focus">True</property>
|
||||||
<property name="receives-default">False</property>
|
<property name="receives-default">False</property>
|
||||||
<property name="tooltip-text" translatable="yes">Enables printing trace log messages</property>
|
<property name="tooltip-text" translatable="yes">Prints trace log messages in the console. Does not affect performance.</property>
|
||||||
<property name="halign">start</property>
|
<property name="halign">start</property>
|
||||||
<property name="margin-top">5</property>
|
<property name="margin-top">5</property>
|
||||||
<property name="margin-bottom">5</property>
|
<property name="margin-bottom">5</property>
|
||||||
|
|||||||
Reference in New Issue
Block a user