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.
|
||||
context.BranchIfFalse(nextLabel, inRange, BasicBlockFrequency.Cold);
|
||||
|
||||
context.Copy(inRegionLocal, Const(1));
|
||||
Operand offset = context.BitwiseAnd(context.Subtract(faultAddress, rangeAddress), Const(~PageMask));
|
||||
|
||||
// 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));
|
||||
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);
|
||||
|
||||
|
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.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace ARMeilleure.Signal
|
||||
{
|
||||
class WindowsSignalHandlerRegistration
|
||||
unsafe class WindowsSignalHandlerRegistration
|
||||
{
|
||||
[DllImport("kernel32.dll")]
|
||||
private static extern IntPtr AddVectoredExceptionHandler(uint first, IntPtr handler);
|
||||
@ -11,6 +12,14 @@ namespace ARMeilleure.Signal
|
||||
[DllImport("kernel32.dll")]
|
||||
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)
|
||||
{
|
||||
return AddVectoredExceptionHandler(1, action);
|
||||
@ -20,5 +29,17 @@ namespace ARMeilleure.Signal
|
||||
{
|
||||
return RemoveVectoredExceptionHandler(handle) != 0;
|
||||
}
|
||||
|
||||
public static IntPtr GetCurrentThreadIdFunc()
|
||||
{
|
||||
if (_getCurrentThreadIdPtr == IntPtr.Zero)
|
||||
{
|
||||
IntPtr handle = LoadLibrary("kernel32.dll");
|
||||
|
||||
_getCurrentThreadIdPtr = GetProcAddress(handle, "GetCurrentThreadId");
|
||||
}
|
||||
|
||||
return _getCurrentThreadIdPtr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -81,7 +81,7 @@ namespace Ryujinx.Audio.Backends.CompatLayer
|
||||
{
|
||||
BufferTag = buffer.BufferTag,
|
||||
DataPointer = buffer.DataPointer,
|
||||
DataSize = (ulong)downmixedBuffer.Length
|
||||
DataSize = (ulong)downmixedBuffer.Length
|
||||
};
|
||||
|
||||
bool result = _realSession.RegisterBuffer(fakeBuffer, downmixedBuffer);
|
||||
|
@ -24,11 +24,11 @@ namespace Ryujinx.Audio.Backends.CompatLayer
|
||||
public short Right;
|
||||
}
|
||||
|
||||
private const int Q15Bits = 16;
|
||||
private const int RawQ15One = 1 << Q15Bits;
|
||||
private const int RawQ15HalfOne = (int)(0.5f * RawQ15One);
|
||||
private const int Minus3dBInQ15 = (int)(0.707f * RawQ15One);
|
||||
private const int Minus6dBInQ15 = (int)(0.501f * RawQ15One);
|
||||
private const int Q15Bits = 16;
|
||||
private const int RawQ15One = 1 << Q15Bits;
|
||||
private const int RawQ15HalfOne = (int)(0.5f * RawQ15One);
|
||||
private const int Minus3dBInQ15 = (int)(0.707f * RawQ15One);
|
||||
private const int Minus6dBInQ15 = (int)(0.501f * RawQ15One);
|
||||
private const int Minus12dBInQ15 = (int)(0.251f * RawQ15One);
|
||||
|
||||
private static readonly int[] DefaultSurroundToStereoCoefficients = new int[4]
|
||||
@ -46,8 +46,8 @@ namespace Ryujinx.Audio.Backends.CompatLayer
|
||||
};
|
||||
|
||||
private const int SurroundChannelCount = 6;
|
||||
private const int StereoChannelCount = 2;
|
||||
private const int MonoChannelCount = 1;
|
||||
private const int StereoChannelCount = 2;
|
||||
private const int MonoChannelCount = 1;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static ReadOnlySpan<Channel51FormatPCM16> GetSurroundBuffer(ReadOnlySpan<short> data)
|
||||
@ -86,7 +86,7 @@ namespace Ryujinx.Audio.Backends.CompatLayer
|
||||
{
|
||||
Channel51FormatPCM16 channel = channels[i];
|
||||
|
||||
downmixedBuffer[i * 2] = DownMixSurroundToStereo(coefficients, channel.BackLeft, channel.LowFrequency, channel.FrontCenter, channel.FrontLeft);
|
||||
downmixedBuffer[i * 2] = DownMixSurroundToStereo(coefficients, channel.BackLeft, channel.LowFrequency, channel.FrontCenter, channel.FrontLeft);
|
||||
downmixedBuffer[i * 2 + 1] = DownMixSurroundToStereo(coefficients, channel.BackRight, channel.LowFrequency, channel.FrontCenter, channel.FrontRight);
|
||||
}
|
||||
|
||||
|
@ -214,9 +214,9 @@ namespace Ryujinx.Audio.Input
|
||||
outputDeviceName = audioIn.DeviceName;
|
||||
outputConfiguration = new AudioOutputConfiguration
|
||||
{
|
||||
ChannelCount = audioIn.ChannelCount,
|
||||
SampleFormat = audioIn.SampleFormat,
|
||||
SampleRate = audioIn.SampleRate,
|
||||
ChannelCount = audioIn.ChannelCount,
|
||||
SampleFormat = audioIn.SampleFormat,
|
||||
SampleRate = audioIn.SampleRate,
|
||||
AudioOutState = audioIn.GetState(),
|
||||
};
|
||||
|
||||
|
@ -32,8 +32,8 @@ namespace Ryujinx.Audio.Integration
|
||||
_session.QueueBuffer(new AudioBuffer
|
||||
{
|
||||
DataPointer = _currentBufferTag++,
|
||||
Data = _buffer,
|
||||
DataSize = (ulong)_buffer.Length,
|
||||
Data = _buffer,
|
||||
DataSize = (ulong)_buffer.Length,
|
||||
});
|
||||
|
||||
_currentBufferTag = _currentBufferTag % 4;
|
||||
|
@ -209,9 +209,9 @@ namespace Ryujinx.Audio.Output
|
||||
outputDeviceName = audioOut.DeviceName;
|
||||
outputConfiguration = new AudioOutputConfiguration
|
||||
{
|
||||
ChannelCount = audioOut.ChannelCount,
|
||||
SampleFormat = audioOut.SampleFormat,
|
||||
SampleRate = audioOut.SampleRate,
|
||||
ChannelCount = audioOut.ChannelCount,
|
||||
SampleFormat = audioOut.SampleFormat,
|
||||
SampleRate = audioOut.SampleRate,
|
||||
AudioOutState = audioOut.GetState(),
|
||||
};
|
||||
|
||||
|
@ -169,7 +169,7 @@ namespace Ryujinx.Audio.Output
|
||||
}
|
||||
|
||||
SampleFormat = sampleFormat;
|
||||
SampleRate = Constants.TargetSampleRate;
|
||||
SampleRate = Constants.TargetSampleRate;
|
||||
}
|
||||
|
||||
return result;
|
||||
@ -187,9 +187,9 @@ namespace Ryujinx.Audio.Output
|
||||
{
|
||||
AudioBuffer buffer = new AudioBuffer
|
||||
{
|
||||
BufferTag = bufferTag,
|
||||
BufferTag = bufferTag,
|
||||
DataPointer = userBuffer.Data,
|
||||
DataSize = userBuffer.DataSize
|
||||
DataSize = userBuffer.DataSize
|
||||
};
|
||||
|
||||
if (_session.AppendBuffer(buffer))
|
||||
@ -291,7 +291,7 @@ namespace Ryujinx.Audio.Output
|
||||
{
|
||||
lock (_parentLock)
|
||||
{
|
||||
_session.SetVolume(volume);
|
||||
_session.SetVolume(volume);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -41,7 +41,7 @@ namespace Ryujinx.Audio.Renderer.Common
|
||||
return Memory<byte>.Empty;
|
||||
}
|
||||
|
||||
public Memory<T> Allocate<T>(ulong count, int align) where T: unmanaged
|
||||
public Memory<T> Allocate<T>(ulong count, int align) where T : unmanaged
|
||||
{
|
||||
Memory<byte> allocatedMemory = Allocate((ulong)Unsafe.SizeOf<T>() * count, align);
|
||||
|
||||
@ -53,7 +53,7 @@ namespace Ryujinx.Audio.Renderer.Common
|
||||
return SpanMemoryManager<T>.Cast(allocatedMemory);
|
||||
}
|
||||
|
||||
public static ulong GetTargetSize<T>(ulong currentSize, ulong count, int align) where T: unmanaged
|
||||
public static ulong GetTargetSize<T>(ulong currentSize, ulong count, int align) where T : unmanaged
|
||||
{
|
||||
return BitUtils.AlignUp(currentSize, align) + (ulong)Unsafe.SizeOf<T>() * count;
|
||||
}
|
||||
|
@ -87,7 +87,7 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
|
||||
float dryGain = FixedPointHelper.ToFloat(Parameter.DryGain, FixedPointPrecision);
|
||||
float outGain = FixedPointHelper.ToFloat(Parameter.OutGain, FixedPointPrecision);
|
||||
|
||||
Matrix2x2 delayFeedback = new Matrix2x2(delayFeedbackBaseGain , delayFeedbackCrossGain,
|
||||
Matrix2x2 delayFeedback = new Matrix2x2(delayFeedbackBaseGain, delayFeedbackCrossGain,
|
||||
delayFeedbackCrossGain, delayFeedbackBaseGain);
|
||||
|
||||
for (int i = 0; i < sampleCount; i++)
|
||||
@ -124,10 +124,10 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
|
||||
float dryGain = FixedPointHelper.ToFloat(Parameter.DryGain, FixedPointPrecision);
|
||||
float outGain = FixedPointHelper.ToFloat(Parameter.OutGain, FixedPointPrecision);
|
||||
|
||||
Matrix4x4 delayFeedback = new Matrix4x4(delayFeedbackBaseGain , delayFeedbackCrossGain, delayFeedbackCrossGain, 0.0f,
|
||||
delayFeedbackCrossGain, delayFeedbackBaseGain , 0.0f , delayFeedbackCrossGain,
|
||||
delayFeedbackCrossGain, 0.0f , delayFeedbackBaseGain , delayFeedbackCrossGain,
|
||||
0.0f , delayFeedbackCrossGain, delayFeedbackCrossGain, delayFeedbackBaseGain);
|
||||
Matrix4x4 delayFeedback = new Matrix4x4(delayFeedbackBaseGain, delayFeedbackCrossGain, delayFeedbackCrossGain, 0.0f,
|
||||
delayFeedbackCrossGain, delayFeedbackBaseGain, 0.0f, delayFeedbackCrossGain,
|
||||
delayFeedbackCrossGain, 0.0f, delayFeedbackBaseGain, delayFeedbackCrossGain,
|
||||
0.0f, delayFeedbackCrossGain, delayFeedbackCrossGain, delayFeedbackBaseGain);
|
||||
|
||||
|
||||
for (int i = 0; i < sampleCount; i++)
|
||||
@ -171,12 +171,12 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
|
||||
float dryGain = FixedPointHelper.ToFloat(Parameter.DryGain, FixedPointPrecision);
|
||||
float outGain = FixedPointHelper.ToFloat(Parameter.OutGain, FixedPointPrecision);
|
||||
|
||||
Matrix6x6 delayFeedback = new Matrix6x6(delayFeedbackBaseGain , 0.0f , 0.0f , 0.0f , delayFeedbackCrossGain, delayFeedbackCrossGain,
|
||||
0.0f , delayFeedbackBaseGain , 0.0f , delayFeedbackCrossGain, delayFeedbackCrossGain, 0.0f ,
|
||||
delayFeedbackCrossGain, 0.0f , delayFeedbackBaseGain , delayFeedbackCrossGain, 0.0f , 0.0f ,
|
||||
0.0f , delayFeedbackCrossGain, delayFeedbackCrossGain, delayFeedbackBaseGain , 0.0f , 0.0f ,
|
||||
delayFeedbackCrossGain, delayFeedbackCrossGain, 0.0f , 0.0f , delayFeedbackBaseGain , 0.0f ,
|
||||
0.0f , 0.0f , 0.0f , 0.0f , 0.0f , feedbackGain);
|
||||
Matrix6x6 delayFeedback = new Matrix6x6(delayFeedbackBaseGain, 0.0f, 0.0f, 0.0f, delayFeedbackCrossGain, delayFeedbackCrossGain,
|
||||
0.0f, delayFeedbackBaseGain, 0.0f, delayFeedbackCrossGain, delayFeedbackCrossGain, 0.0f,
|
||||
delayFeedbackCrossGain, 0.0f, delayFeedbackBaseGain, delayFeedbackCrossGain, 0.0f, 0.0f,
|
||||
0.0f, delayFeedbackCrossGain, delayFeedbackCrossGain, delayFeedbackBaseGain, 0.0f, 0.0f,
|
||||
delayFeedbackCrossGain, delayFeedbackCrossGain, 0.0f, 0.0f, delayFeedbackBaseGain, 0.0f,
|
||||
0.0f, 0.0f, 0.0f, 0.0f, 0.0f, feedbackGain);
|
||||
|
||||
for (int i = 0; i < sampleCount; i++)
|
||||
{
|
||||
|
@ -25,6 +25,6 @@ namespace Ryujinx.Audio.Renderer.Dsp.State
|
||||
UpdateParameter(ref parameter);
|
||||
}
|
||||
|
||||
public void UpdateParameter(ref LimiterParameter parameter) {}
|
||||
public void UpdateParameter(ref LimiterParameter parameter) { }
|
||||
}
|
||||
}
|
@ -29,34 +29,34 @@ namespace Ryujinx.Audio.Renderer.Server
|
||||
private object _lock = new object();
|
||||
|
||||
private AudioRendererExecutionMode _executionMode;
|
||||
private IWritableEvent _systemEvent;
|
||||
private ManualResetEvent _terminationEvent;
|
||||
private MemoryPoolState _dspMemoryPoolState;
|
||||
private VoiceContext _voiceContext;
|
||||
private MixContext _mixContext;
|
||||
private SinkContext _sinkContext;
|
||||
private SplitterContext _splitterContext;
|
||||
private EffectContext _effectContext;
|
||||
private PerformanceManager _performanceManager;
|
||||
private UpsamplerManager _upsamplerManager;
|
||||
private bool _isActive;
|
||||
private BehaviourContext _behaviourContext;
|
||||
private ulong _totalElapsedTicksUpdating;
|
||||
private ulong _totalElapsedTicks;
|
||||
private int _sessionId;
|
||||
private Memory<MemoryPoolState> _memoryPools;
|
||||
private IWritableEvent _systemEvent;
|
||||
private ManualResetEvent _terminationEvent;
|
||||
private MemoryPoolState _dspMemoryPoolState;
|
||||
private VoiceContext _voiceContext;
|
||||
private MixContext _mixContext;
|
||||
private SinkContext _sinkContext;
|
||||
private SplitterContext _splitterContext;
|
||||
private EffectContext _effectContext;
|
||||
private PerformanceManager _performanceManager;
|
||||
private UpsamplerManager _upsamplerManager;
|
||||
private bool _isActive;
|
||||
private BehaviourContext _behaviourContext;
|
||||
private ulong _totalElapsedTicksUpdating;
|
||||
private ulong _totalElapsedTicks;
|
||||
private int _sessionId;
|
||||
private Memory<MemoryPoolState> _memoryPools;
|
||||
|
||||
private uint _sampleRate;
|
||||
private uint _sampleCount;
|
||||
private uint _mixBufferCount;
|
||||
private uint _voiceChannelCountMax;
|
||||
private uint _upsamplerCount;
|
||||
private uint _memoryPoolCount;
|
||||
private uint _processHandle;
|
||||
private uint _sampleRate;
|
||||
private uint _sampleCount;
|
||||
private uint _mixBufferCount;
|
||||
private uint _voiceChannelCountMax;
|
||||
private uint _upsamplerCount;
|
||||
private uint _memoryPoolCount;
|
||||
private uint _processHandle;
|
||||
private ulong _appletResourceId;
|
||||
|
||||
private WritableRegion _workBufferRegion;
|
||||
private MemoryHandle _workBufferMemoryPin;
|
||||
private MemoryHandle _workBufferMemoryPin;
|
||||
|
||||
private Memory<float> _mixBuffer;
|
||||
private Memory<float> _depopBuffer;
|
||||
@ -81,21 +81,21 @@ namespace Ryujinx.Audio.Renderer.Server
|
||||
|
||||
public AudioRenderSystem(AudioRendererManager manager, IWritableEvent systemEvent)
|
||||
{
|
||||
_manager = manager;
|
||||
_terminationEvent = new ManualResetEvent(false);
|
||||
_manager = manager;
|
||||
_terminationEvent = new ManualResetEvent(false);
|
||||
_dspMemoryPoolState = MemoryPoolState.Create(MemoryPoolState.LocationType.Dsp);
|
||||
_voiceContext = new VoiceContext();
|
||||
_mixContext = new MixContext();
|
||||
_sinkContext = new SinkContext();
|
||||
_splitterContext = new SplitterContext();
|
||||
_effectContext = new EffectContext();
|
||||
_voiceContext = new VoiceContext();
|
||||
_mixContext = new MixContext();
|
||||
_sinkContext = new SinkContext();
|
||||
_splitterContext = new SplitterContext();
|
||||
_effectContext = new EffectContext();
|
||||
|
||||
_commandProcessingTimeEstimator = null;
|
||||
_systemEvent = systemEvent;
|
||||
_behaviourContext = new BehaviourContext();
|
||||
|
||||
_totalElapsedTicksUpdating = 0;
|
||||
_sessionId = 0;
|
||||
_sessionId = 0;
|
||||
}
|
||||
|
||||
public ResultCode Initialize(ref AudioRendererConfiguration parameter, uint processHandle, CpuAddress workBuffer, ulong workBufferSize, int sessionId, ulong appletResourceId, IVirtualMemoryManager memoryManager)
|
||||
@ -116,7 +116,7 @@ namespace Ryujinx.Audio.Renderer.Server
|
||||
|
||||
_behaviourContext.SetUserRevision(parameter.Revision);
|
||||
|
||||
_sampleRate = parameter.SampleRate;
|
||||
_sampleRate = parameter.SampleRate;
|
||||
_sampleCount = parameter.SampleCount;
|
||||
_mixBufferCount = parameter.MixBufferCount;
|
||||
_voiceChannelCountMax = Constants.VoiceChannelCountMax;
|
||||
@ -203,7 +203,7 @@ namespace Ryujinx.Audio.Renderer.Server
|
||||
{
|
||||
ref VoiceChannelResource voiceChannelResource = ref voiceChannelResources.Span[(int)id];
|
||||
|
||||
voiceChannelResource.Id = id;
|
||||
voiceChannelResource.Id = id;
|
||||
voiceChannelResource.IsUsed = false;
|
||||
}
|
||||
|
||||
|
@ -141,8 +141,8 @@ namespace Ryujinx.Audio.Renderer.Server
|
||||
public BehaviourContext()
|
||||
{
|
||||
UserRevision = 0;
|
||||
_errorInfos = new ErrorInfo[Constants.MaxErrorInfos];
|
||||
_errorIndex = 0;
|
||||
_errorInfos = new ErrorInfo[Constants.MaxErrorInfos];
|
||||
_errorIndex = 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -81,7 +81,7 @@ namespace Ryujinx.Audio.Renderer.Server.Effect
|
||||
/// </summary>
|
||||
/// <param name="parameter">The user parameter.</param>
|
||||
/// <returns>Returns true if the <see cref="EffectType"/> sent by the user matches the internal <see cref="EffectType"/>.</returns>
|
||||
public bool IsTypeValid<T>(ref T parameter) where T: unmanaged, IEffectInParameter
|
||||
public bool IsTypeValid<T>(ref T parameter) where T : unmanaged, IEffectInParameter
|
||||
{
|
||||
return parameter.Type == TargetEffectType;
|
||||
}
|
||||
@ -140,14 +140,14 @@ namespace Ryujinx.Audio.Renderer.Server.Effect
|
||||
/// Initialize the given <paramref name="state"/> result state.
|
||||
/// </summary>
|
||||
/// <param name="state">The state to initalize</param>
|
||||
public virtual void InitializeResultState(ref EffectResultState state) {}
|
||||
public virtual void InitializeResultState(ref EffectResultState state) { }
|
||||
|
||||
/// <summary>
|
||||
/// Update the <paramref name="destState"/> result state with <paramref name="srcState"/>.
|
||||
/// </summary>
|
||||
/// <param name="destState">The destination result state</param>
|
||||
/// <param name="srcState">The source result state</param>
|
||||
public virtual void UpdateResultState(ref EffectResultState destState, ref EffectResultState srcState) {}
|
||||
public virtual void UpdateResultState(ref EffectResultState destState, ref EffectResultState srcState) { }
|
||||
|
||||
/// <summary>
|
||||
/// Update the internal state from a user version 1 parameter.
|
||||
@ -215,7 +215,7 @@ namespace Ryujinx.Audio.Renderer.Server.Effect
|
||||
/// </summary>
|
||||
/// <param name="outStatus">The given user output.</param>
|
||||
/// <param name="isAudioRendererActive">If set to true, the <see cref="AudioRenderSystem"/> is active.</param>
|
||||
public void StoreStatus<T>(ref T outStatus, bool isAudioRendererActive) where T: unmanaged, IEffectOutStatus
|
||||
public void StoreStatus<T>(ref T outStatus, bool isAudioRendererActive) where T : unmanaged, IEffectOutStatus
|
||||
{
|
||||
if (isAudioRendererActive)
|
||||
{
|
||||
|
@ -46,7 +46,7 @@ namespace Ryujinx.Audio.Renderer.Server.Effect
|
||||
Update(out updateErrorInfo, ref parameter, mapper);
|
||||
}
|
||||
|
||||
public void Update<T>(out BehaviourParameter.ErrorInfo updateErrorInfo, ref T parameter, PoolMapper mapper) where T: unmanaged, IEffectInParameter
|
||||
public void Update<T>(out BehaviourParameter.ErrorInfo updateErrorInfo, ref T parameter, PoolMapper mapper) where T : unmanaged, IEffectInParameter
|
||||
{
|
||||
Debug.Assert(IsTypeValid(ref parameter));
|
||||
|
||||
|
@ -1,8 +1,7 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
using DspAddress = System.UInt64;
|
||||
using CpuAddress = System.UInt64;
|
||||
using DspAddress = System.UInt64;
|
||||
|
||||
namespace Ryujinx.Audio.Renderer.Server.MemoryPool
|
||||
{
|
||||
@ -53,9 +52,9 @@ namespace Ryujinx.Audio.Renderer.Server.MemoryPool
|
||||
{
|
||||
return new AddressInfo
|
||||
{
|
||||
CpuAddress = cpuAddress,
|
||||
_memoryPools = MemoryPoolState.Null,
|
||||
Size = size,
|
||||
CpuAddress = cpuAddress,
|
||||
_memoryPools = MemoryPoolState.Null,
|
||||
Size = size,
|
||||
ForceMappedDspAddress = 0
|
||||
};
|
||||
}
|
||||
@ -68,8 +67,8 @@ namespace Ryujinx.Audio.Renderer.Server.MemoryPool
|
||||
/// <param name="size">The size of the region.</param>
|
||||
public void Setup(CpuAddress cpuAddress, ulong size)
|
||||
{
|
||||
CpuAddress = cpuAddress;
|
||||
Size = size;
|
||||
CpuAddress = cpuAddress;
|
||||
Size = size;
|
||||
ForceMappedDspAddress = 0;
|
||||
|
||||
unsafe
|
||||
|
@ -1,8 +1,7 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
using DspAddress = System.UInt64;
|
||||
using CpuAddress = System.UInt64;
|
||||
using DspAddress = System.UInt64;
|
||||
|
||||
namespace Ryujinx.Audio.Renderer.Server.MemoryPool
|
||||
{
|
||||
@ -69,8 +68,8 @@ namespace Ryujinx.Audio.Renderer.Server.MemoryPool
|
||||
{
|
||||
CpuAddress = 0,
|
||||
DspAddress = 0,
|
||||
Size = 0,
|
||||
Location = location
|
||||
Size = 0,
|
||||
Location = location
|
||||
};
|
||||
}
|
||||
|
||||
@ -82,7 +81,7 @@ namespace Ryujinx.Audio.Renderer.Server.MemoryPool
|
||||
public void SetCpuAddress(CpuAddress cpuAddress, ulong size)
|
||||
{
|
||||
CpuAddress = cpuAddress;
|
||||
Size = size;
|
||||
Size = size;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -229,14 +229,14 @@ namespace Ryujinx.Audio.Renderer.Server.MemoryPool
|
||||
|
||||
if (AssignDspAddress(ref addressInfo))
|
||||
{
|
||||
errorInfo.ErrorCode = 0x0;
|
||||
errorInfo.ErrorCode = 0x0;
|
||||
errorInfo.ExtraErrorInfo = 0x0;
|
||||
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
errorInfo.ErrorCode = ResultCode.InvalidAddressInfo;
|
||||
errorInfo.ErrorCode = ResultCode.InvalidAddressInfo;
|
||||
errorInfo.ExtraErrorInfo = addressInfo.CpuAddress;
|
||||
|
||||
return _isForceMapEnabled;
|
||||
|
@ -13,7 +13,7 @@ namespace Ryujinx.Audio.Renderer.Server.Performance
|
||||
/// <typeparam name="THeader">The header implementation of the performance frame.</typeparam>
|
||||
/// <typeparam name="TEntry">The entry implementation of the performance frame.</typeparam>
|
||||
/// <typeparam name="TEntryDetail">A detailed implementation of the performance frame.</typeparam>
|
||||
public class PerformanceManagerGeneric<THeader, TEntry, TEntryDetail> : PerformanceManager where THeader: unmanaged, IPerformanceHeader where TEntry : unmanaged, IPerformanceEntry where TEntryDetail: unmanaged, IPerformanceDetailEntry
|
||||
public class PerformanceManagerGeneric<THeader, TEntry, TEntryDetail> : PerformanceManager where THeader : unmanaged, IPerformanceHeader where TEntry : unmanaged, IPerformanceEntry where TEntryDetail : unmanaged, IPerformanceDetailEntry
|
||||
{
|
||||
/// <summary>
|
||||
/// The magic used for the <see cref="THeader"/>.
|
||||
|
@ -172,7 +172,7 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
fixed (SplitterDestination *nextPtr = &next)
|
||||
fixed (SplitterDestination* nextPtr = &next)
|
||||
{
|
||||
_next = nextPtr;
|
||||
}
|
||||
|
@ -21,7 +21,7 @@ namespace Ryujinx.Audio.Renderer.Server
|
||||
public class StateUpdater
|
||||
{
|
||||
private readonly ReadOnlyMemory<byte> _inputOrigin;
|
||||
private ReadOnlyMemory <byte> _outputOrigin;
|
||||
private ReadOnlyMemory<byte> _outputOrigin;
|
||||
private ReadOnlyMemory<byte> _input;
|
||||
|
||||
private Memory<byte> _output;
|
||||
@ -207,7 +207,7 @@ namespace Ryujinx.Audio.Renderer.Server
|
||||
return ResultCode.Success;
|
||||
}
|
||||
|
||||
private static void ResetEffect<T>(ref BaseEffect effect, ref T parameter, PoolMapper mapper) where T: unmanaged, IEffectInParameter
|
||||
private static void ResetEffect<T>(ref BaseEffect effect, ref T parameter, PoolMapper mapper) where T : unmanaged, IEffectInParameter
|
||||
{
|
||||
effect.ForceUnmapBuffers(mapper);
|
||||
|
||||
|
@ -200,7 +200,7 @@ namespace Ryujinx.Audio.Renderer.Server.Voice
|
||||
SampleFormat = SampleFormat.Invalid;
|
||||
ChannelsCount = 0;
|
||||
Pitch = 0.0f;
|
||||
Volume= 0.0f;
|
||||
Volume = 0.0f;
|
||||
PreviousVolume = 0.0f;
|
||||
BiquadFilters.ToSpan().Fill(new BiquadFilterParameter());
|
||||
WaveBuffersCount = 0;
|
||||
@ -260,7 +260,7 @@ namespace Ryujinx.Audio.Renderer.Server.Voice
|
||||
}
|
||||
|
||||
return DataSourceStateAddressInfo.CpuAddress != parameter.DataSourceStateAddress ||
|
||||
DataSourceStateAddressInfo.Size != parameter.DataSourceStateSize ||
|
||||
DataSourceStateAddressInfo.Size != parameter.DataSourceStateSize ||
|
||||
DataSourceStateUnmapped;
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,6 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
using DspAddress = System.UInt64;
|
||||
using CpuAddress = System.UInt64;
|
||||
using DspAddress = System.UInt64;
|
||||
|
||||
namespace Ryujinx.Audio.Renderer.Utils
|
||||
{
|
||||
|
@ -2,20 +2,20 @@ namespace Ryujinx.Audio
|
||||
{
|
||||
public enum ResultCode
|
||||
{
|
||||
ModuleId = 153,
|
||||
ModuleId = 153,
|
||||
ErrorCodeShift = 9,
|
||||
|
||||
Success = 0,
|
||||
|
||||
DeviceNotFound = (1 << ErrorCodeShift) | ModuleId,
|
||||
OperationFailed = (2 << ErrorCodeShift) | ModuleId,
|
||||
UnsupportedSampleRate = (3 << ErrorCodeShift) | ModuleId,
|
||||
WorkBufferTooSmall = (4 << ErrorCodeShift) | ModuleId,
|
||||
BufferRingFull = (8 << ErrorCodeShift) | ModuleId,
|
||||
DeviceNotFound = (1 << ErrorCodeShift) | ModuleId,
|
||||
OperationFailed = (2 << ErrorCodeShift) | ModuleId,
|
||||
UnsupportedSampleRate = (3 << ErrorCodeShift) | ModuleId,
|
||||
WorkBufferTooSmall = (4 << ErrorCodeShift) | ModuleId,
|
||||
BufferRingFull = (8 << ErrorCodeShift) | ModuleId,
|
||||
UnsupportedChannelConfiguration = (10 << ErrorCodeShift) | ModuleId,
|
||||
InvalidUpdateInfo = (41 << ErrorCodeShift) | ModuleId,
|
||||
InvalidAddressInfo = (42 << ErrorCodeShift) | ModuleId,
|
||||
InvalidMixSorting = (43 << ErrorCodeShift) | ModuleId,
|
||||
UnsupportedOperation = (513 << ErrorCodeShift) | ModuleId,
|
||||
InvalidUpdateInfo = (41 << ErrorCodeShift) | ModuleId,
|
||||
InvalidAddressInfo = (42 << ErrorCodeShift) | ModuleId,
|
||||
InvalidMixSorting = (43 << ErrorCodeShift) | ModuleId,
|
||||
UnsupportedOperation = (513 << ErrorCodeShift) | ModuleId,
|
||||
}
|
||||
}
|
@ -53,6 +53,7 @@ namespace Ryujinx.Ava
|
||||
internal class AppHost
|
||||
{
|
||||
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);
|
||||
|
||||
@ -976,6 +977,13 @@ namespace Ryujinx.Ava
|
||||
|
||||
_parent.ViewModel.Volume = Device.GetVolume();
|
||||
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:
|
||||
(_keyboardInterface as AvaloniaKeyboard).Clear();
|
||||
break;
|
||||
@ -1033,6 +1041,14 @@ namespace Ryujinx.Ava
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
@ -118,7 +118,7 @@
|
||||
"SettingsTabSystemAudioBackendSoundIO": "SoundIO",
|
||||
"SettingsTabSystemAudioBackendSDL2": "SDL2",
|
||||
"SettingsTabSystemHacks": "Hacks",
|
||||
"SettingsTabSystemHacksNote": " - Können Fehler verursachen",
|
||||
"SettingsTabSystemHacksNote": " (Können Fehler verursachen)",
|
||||
"SettingsTabSystemExpandDramSize": "Erweitere DRAM Größe auf 6GB",
|
||||
"SettingsTabSystemIgnoreMissingServices": "Ignoriere fehlende Dienste",
|
||||
"SettingsTabGraphics": "Grafik",
|
||||
@ -556,5 +556,7 @@
|
||||
"SettingsSelectThemeFileDialogTitle" : "Wähle ein benutzerdefiniertes Thema",
|
||||
"SettingsXamlThemeFile" : "Xaml Thema-Datei",
|
||||
"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",
|
||||
"SettingsTabSystemAudioBackendSDL2": "SDL2",
|
||||
"SettingsTabSystemHacks": "Μικροδιορθώσεις",
|
||||
"SettingsTabSystemHacksNote": " - Μπορεί να προκαλέσουν αστάθεια",
|
||||
"SettingsTabSystemHacksNote": " (Μπορεί να προκαλέσουν αστάθεια)",
|
||||
"SettingsTabSystemExpandDramSize": "Επέκταση μεγέθους DRAM στα 6GB",
|
||||
"SettingsTabSystemIgnoreMissingServices": "Αγνόηση υπηρεσιών που λείπουν",
|
||||
"SettingsTabGraphics": "Γραφικά",
|
||||
|
@ -575,5 +575,9 @@
|
||||
"UserProfilesSetProfileImage": "Set Profile Image",
|
||||
"UserProfileEmptyNameError": "Name is required",
|
||||
"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",
|
||||
"SettingsTabSystemAudioBackendSDL2": "SDL2",
|
||||
"SettingsTabSystemHacks": "Hacks",
|
||||
"SettingsTabSystemHacksNote": " - Pueden causar inestabilidad",
|
||||
"SettingsTabSystemHacksNote": " (Pueden causar inestabilidad)",
|
||||
"SettingsTabSystemExpandDramSize": "Expandir DRAM a 6GB",
|
||||
"SettingsTabSystemIgnoreMissingServices": "Ignorar servicios no implementados",
|
||||
"SettingsTabGraphics": "Gráficos",
|
||||
@ -568,5 +568,7 @@
|
||||
"UpdateWindowTitle": "Administrar actualizaciones",
|
||||
"CheatWindowHeading": "Cheats disponibles 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",
|
||||
"SettingsTabSystemAudioBackendSDL2": "SDL2",
|
||||
"SettingsTabSystemHacks": "Hacks",
|
||||
"SettingsTabSystemHacksNote": " - Cela peut causer des instabilitées",
|
||||
"SettingsTabSystemHacksNote": " (Cela peut causer des instabilitées)",
|
||||
"SettingsTabSystemExpandDramSize": "Augmenter la taille de la DRAM à 6GB",
|
||||
"SettingsTabSystemIgnoreMissingServices": "Ignorer les services manquant",
|
||||
"SettingsTabGraphics": "Graphique",
|
||||
|
@ -118,7 +118,7 @@
|
||||
"SettingsTabSystemAudioBackendSoundIO": "SoundIO",
|
||||
"SettingsTabSystemAudioBackendSDL2": "SDL2",
|
||||
"SettingsTabSystemHacks": "Hacks",
|
||||
"SettingsTabSystemHacksNote": " - Possono causare instabilità",
|
||||
"SettingsTabSystemHacksNote": " (Possono causare instabilità)",
|
||||
"SettingsTabSystemExpandDramSize": "Espandi dimensione DRAM a 6GB",
|
||||
"SettingsTabSystemIgnoreMissingServices": "Ignora servizi mancanti",
|
||||
"SettingsTabGraphics": "Grafica",
|
||||
@ -554,5 +554,7 @@
|
||||
"ControllerMotionTitle": "Impostazioni dei sensori di movimento",
|
||||
"ControllerRumbleTitle": "Impostazioni di vibrazione",
|
||||
"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",
|
||||
"SettingsTabSystemAudioBackendSDL2": "SDL2",
|
||||
"SettingsTabSystemHacks": "해킹",
|
||||
"SettingsTabSystemHacksNote": " - 불안정을 일으킬 수 있음",
|
||||
"SettingsTabSystemHacksNote": " (불안정을 일으킬 수 있음)",
|
||||
"SettingsTabSystemExpandDramSize": "DRAM 크기를 6GB로 확장",
|
||||
"SettingsTabSystemIgnoreMissingServices": "누락된 서비스 무시",
|
||||
"SettingsTabGraphics": "제도법",
|
||||
|
@ -118,7 +118,7 @@
|
||||
"SettingsTabSystemAudioBackendSoundIO": "SoundIO",
|
||||
"SettingsTabSystemAudioBackendSDL2": "SDL2",
|
||||
"SettingsTabSystemHacks": "Hacks",
|
||||
"SettingsTabSystemHacksNote": " - Pode causar instabilidade",
|
||||
"SettingsTabSystemHacksNote": " (Pode causar instabilidade)",
|
||||
"SettingsTabSystemExpandDramSize": "Expandir memória para 6GB",
|
||||
"SettingsTabSystemIgnoreMissingServices": "Ignorar serviços não implementados",
|
||||
"SettingsTabGraphics": "Gráficos",
|
||||
@ -554,5 +554,7 @@
|
||||
"ControllerMotionTitle": "Configurações do controle de movimento",
|
||||
"ControllerRumbleTitle": "Configurações de vibração",
|
||||
"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",
|
||||
"SettingsTabSystemAudioBackendSDL2": "SDL2",
|
||||
"SettingsTabSystemHacks": "Хаки",
|
||||
"SettingsTabSystemHacksNote": " - Эти многие настройки вызывают нестабильность",
|
||||
"SettingsTabSystemHacksNote": " (Эти многие настройки вызывают нестабильность)",
|
||||
"SettingsTabSystemExpandDramSize": "Увеличение размера DRAM до 6GB",
|
||||
"SettingsTabSystemIgnoreMissingServices": "Игнорировать отсутствующие службы",
|
||||
"SettingsTabGraphics": "Графика",
|
||||
|
@ -118,7 +118,7 @@
|
||||
"SettingsTabSystemAudioBackendSoundIO": "SoundIO",
|
||||
"SettingsTabSystemAudioBackendSDL2": "SDL2",
|
||||
"SettingsTabSystemHacks": "Hacklar",
|
||||
"SettingsTabSystemHacksNote": " - Bunlar birçok dengesizlik oluşturabilir",
|
||||
"SettingsTabSystemHacksNote": " (Bunlar birçok dengesizlik oluşturabilir)",
|
||||
"SettingsTabSystemExpandDramSize": "DRAM boyutunu 6GB'a genişlet",
|
||||
"SettingsTabSystemIgnoreMissingServices": "Eksik Servisleri Görmezden Gel",
|
||||
"SettingsTabGraphics": "Grafikler",
|
||||
@ -554,5 +554,7 @@
|
||||
"ControllerMotionTitle": "Hareket Kontrol Seçenekleri",
|
||||
"ControllerRumbleTitle": "Titreşim Seçenekleri",
|
||||
"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",
|
||||
"SettingsTabSystemAudioBackendSDL2": "SDL2",
|
||||
"SettingsTabSystemHacks": "修正",
|
||||
"SettingsTabSystemHacksNote": " - 会引起模拟器不稳定",
|
||||
"SettingsTabSystemHacksNote": " (会引起模拟器不稳定)",
|
||||
"SettingsTabSystemExpandDramSize": "将模拟RAM大小扩展到 6GB",
|
||||
"SettingsTabSystemIgnoreMissingServices": "忽略缺少的服务",
|
||||
"SettingsTabGraphics": "图像",
|
||||
@ -568,5 +568,7 @@
|
||||
"UpdateWindowTitle": "管理游戏更新",
|
||||
"CheatWindowHeading": "适用于 {0} [{1}] 的金手指",
|
||||
"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,
|
||||
ShowUi,
|
||||
Pause,
|
||||
ToggleMute
|
||||
ToggleMute,
|
||||
ResScaleUp,
|
||||
ResScaleDown
|
||||
}
|
||||
}
|
@ -119,6 +119,7 @@
|
||||
<None Remove="Assets\Locales\ru_RU.json" />
|
||||
<None Remove="Assets\Locales\tr_TR.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\BaseDark.xaml" />
|
||||
<None Remove="Assets\Styles\BaseLight.xaml" />
|
||||
@ -136,6 +137,7 @@
|
||||
<EmbeddedResource Include="Assets\Locales\ru_RU.json" />
|
||||
<EmbeddedResource Include="Assets\Locales\tr_TR.json" />
|
||||
<EmbeddedResource Include="Assets\Locales\zh_CN.json" />
|
||||
<EmbeddedResource Include="Assets\Locales\zh_TW.json" />
|
||||
<EmbeddedResource Include="Assets\Styles\Styles.xaml" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
@ -37,7 +37,7 @@
|
||||
Header="{locale:Locale GameListContextMenuManageTitleUpdates}"
|
||||
ToolTip.Tip="{locale:Locale GameListContextMenuManageTitleUpdatesToolTip}" />
|
||||
<MenuItem
|
||||
Command="{Binding OpenDlcManager}"
|
||||
Command="{Binding OpenDownloadableContentManager}"
|
||||
Header="{locale:Locale GameListContextMenuManageDlc}"
|
||||
ToolTip.Tip="{locale:Locale GameListContextMenuManageDlcToolTip}" />
|
||||
<MenuItem
|
||||
|
@ -37,7 +37,7 @@
|
||||
Header="{locale:Locale GameListContextMenuManageTitleUpdates}"
|
||||
ToolTip.Tip="{locale:Locale GameListContextMenuManageTitleUpdatesToolTip}" />
|
||||
<MenuItem
|
||||
Command="{Binding OpenDlcManager}"
|
||||
Command="{Binding OpenDownloadableContentManager}"
|
||||
Header="{locale:Locale GameListContextMenuManageDlc}"
|
||||
ToolTip.Tip="{locale:Locale GameListContextMenuManageDlcToolTip}" />
|
||||
<MenuItem
|
||||
|
@ -17,9 +17,6 @@ namespace Ryujinx.Ava.Ui.Controls
|
||||
public UpdateWaitWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
#if DEBUG
|
||||
this.AttachDevTools();
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
@ -1,55 +1,87 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
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"
|
||||
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:controls="clr-namespace:Ryujinx.Ava.Ui.Controls"
|
||||
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
|
||||
x:Class="Ryujinx.Ava.Ui.Controls.UserEditor">
|
||||
<UserControl
|
||||
x:Class="Ryujinx.Ava.Ui.Controls.UserEditor"
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:models="clr-namespace:Ryujinx.Ava.Ui.Models"
|
||||
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
|
||||
xmlns:viewModels="clr-namespace:Ryujinx.Ava.Ui.ViewModels"
|
||||
Margin="0"
|
||||
Padding="0"
|
||||
mc:Ignorable="d">
|
||||
<UserControl.Resources>
|
||||
<controls:BitmapArrayValueConverter x:Key="ByteImage" />
|
||||
</UserControl.Resources>
|
||||
<Grid Margin="0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition/>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<StackPanel Orientation="Vertical" VerticalAlignment="Stretch" HorizontalAlignment="Left">
|
||||
<StackPanel
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Stretch"
|
||||
Orientation="Vertical">
|
||||
<Image
|
||||
Name="ProfileImage"
|
||||
Width="96"
|
||||
Height="96"
|
||||
Margin="0"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Top"
|
||||
Height="96" Width="96"
|
||||
Name="ProfileImage"
|
||||
Source="{Binding Image, Converter={StaticResource ByteImage}}" />
|
||||
<Button Margin="5" Content="{Locale:Locale UserProfilesChangeProfileImage}"
|
||||
Name="ChangePictureButton"
|
||||
Click="ChangePictureButton_Click"
|
||||
HorizontalAlignment="Stretch"/>
|
||||
<Button Margin="5" Content="{Locale:Locale UserProfilesSetProfileImage}"
|
||||
Name="AddPictureButton"
|
||||
Click="ChangePictureButton_Click"
|
||||
HorizontalAlignment="Stretch"/>
|
||||
<Button
|
||||
Name="ChangePictureButton"
|
||||
Margin="5"
|
||||
HorizontalAlignment="Stretch"
|
||||
Click="ChangePictureButton_Click"
|
||||
Content="{Locale:Locale UserProfilesChangeProfileImage}" />
|
||||
<Button
|
||||
Name="AddPictureButton"
|
||||
Margin="5"
|
||||
HorizontalAlignment="Stretch"
|
||||
Click="ChangePictureButton_Click"
|
||||
Content="{Locale:Locale UserProfilesSetProfileImage}" />
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Row="0" Orientation="Vertical" HorizontalAlignment="Stretch" Grid.Column="1" Spacing="10"
|
||||
Margin="5, 10">
|
||||
<TextBox Name="NameBox" Width="300" Text="{Binding Name}" MaxLength="{Binding MaxProfileNameLength}"
|
||||
HorizontalAlignment="Stretch" />
|
||||
<TextBlock Text="{Binding UserId}" Name="IdLabel" />
|
||||
<StackPanel
|
||||
Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
Margin="5,10"
|
||||
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 Grid.Column="0" Grid.ColumnSpan="2" Grid.Row="1" Orientation="Horizontal" Spacing="10" HorizontalAlignment="Right">
|
||||
<Button Content="{Locale:Locale Save}" Name="SaveButton" Click="SaveButton_Click"/>
|
||||
<Button HorizontalAlignment="Right" Content="{Locale:Locale Discard}"
|
||||
Name="CloseButton" Click="CloseButton_Click"/>
|
||||
<StackPanel
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
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>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
|
@ -63,7 +63,7 @@ namespace Ryujinx.Ava.Ui.Controls
|
||||
_parent?.GoBack();
|
||||
}
|
||||
|
||||
private void SaveButton_Click(object sender, RoutedEventArgs e)
|
||||
private async void SaveButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
DataValidationErrors.ClearErrors(NameBox);
|
||||
bool isInvalid = false;
|
||||
@ -77,7 +77,7 @@ namespace Ryujinx.Ava.Ui.Controls
|
||||
|
||||
if (TempProfile.Image == null)
|
||||
{
|
||||
ContentDialogHelper.CreateWarningDialog(LocaleManager.Instance["UserProfileNoImageError"], "");
|
||||
await ContentDialogHelper.CreateWarningDialog(LocaleManager.Instance["UserProfileNoImageError"], "");
|
||||
|
||||
isInvalid = true;
|
||||
}
|
||||
|
@ -1,29 +1,35 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
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:viewModels="clr-namespace:Ryujinx.Ava.Ui.ViewModels"
|
||||
xmlns:controls="clr-namespace:Ryujinx.Ava.Ui.Controls"
|
||||
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="Ryujinx.Ava.Ui.Controls.UserSelector">
|
||||
<UserControl
|
||||
x:Class="Ryujinx.Ava.Ui.Controls.UserSelector"
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
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: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:viewModels="clr-namespace:Ryujinx.Ava.Ui.ViewModels"
|
||||
d:DesignHeight="450"
|
||||
d:DesignWidth="800"
|
||||
mc:Ignorable="d">
|
||||
<UserControl.Resources>
|
||||
<controls:BitmapArrayValueConverter x:Key="ByteImage" />
|
||||
</UserControl.Resources>
|
||||
<Design.DataContext>
|
||||
<viewModels:UserProfileViewModel />
|
||||
</Design.DataContext>
|
||||
<Grid HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch">
|
||||
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<ListBox HorizontalAlignment="Stretch" VerticalAlignment="Center" Margin="5" Items="{Binding Profiles}"
|
||||
DoubleTapped="ProfilesList_DoubleTapped"
|
||||
SelectionChanged="SelectingItemsControl_SelectionChanged">
|
||||
<ListBox
|
||||
Margin="5"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Center"
|
||||
DoubleTapped="ProfilesList_DoubleTapped"
|
||||
Items="{Binding Profiles}"
|
||||
SelectionChanged="SelectingItemsControl_SelectionChanged">
|
||||
<ListBox.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<flex:FlexPanel
|
||||
@ -49,10 +55,11 @@
|
||||
</Grid.RowDefinitions>
|
||||
<Image
|
||||
Grid.Row="0"
|
||||
Width="96"
|
||||
Height="96"
|
||||
Margin="0"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Top"
|
||||
Height="96" Width="96"
|
||||
Source="{Binding Image, Converter={StaticResource ByteImage}}" />
|
||||
<StackPanel
|
||||
Grid.Row="1"
|
||||
@ -68,23 +75,34 @@
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
<Border HorizontalAlignment="Left" VerticalAlignment="Top"
|
||||
IsVisible="{Binding IsOpened}"
|
||||
Background="LimeGreen"
|
||||
Width="10"
|
||||
Height="10"
|
||||
Margin="5"
|
||||
CornerRadius="5" />
|
||||
<Border
|
||||
Width="10"
|
||||
Height="10"
|
||||
Margin="5"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Top"
|
||||
Background="LimeGreen"
|
||||
CornerRadius="5"
|
||||
IsVisible="{Binding IsOpened}" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
<StackPanel Grid.Row="1" Orientation="Horizontal" Margin="10,0" Spacing="10" HorizontalAlignment="Center">
|
||||
<Button Content="{Locale:Locale UserProfilesAddNewProfile}" Command="{Binding AddUser}" />
|
||||
<Button IsEnabled="{Binding IsSelectedProfiledEditable}"
|
||||
Content="{Locale:Locale UserProfilesEditProfile}" Command="{Binding EditUser}" />
|
||||
<Button IsEnabled="{Binding IsSelectedProfileDeletable}"
|
||||
Content="{Locale:Locale UserProfilesDeleteSelectedProfile}" Command="{Binding DeleteUser}" />
|
||||
<StackPanel
|
||||
Grid.Row="1"
|
||||
Margin="10,0"
|
||||
HorizontalAlignment="Center"
|
||||
Orientation="Horizontal"
|
||||
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>
|
||||
</Grid>
|
||||
</UserControl>
|
@ -21,7 +21,7 @@ namespace Ryujinx.Ava.Ui.Controls
|
||||
AddHandler(Frame.NavigatedToEvent, (s, e) =>
|
||||
{
|
||||
NavigatedTo(e);
|
||||
}, Avalonia.Interactivity.RoutingStrategies.Direct);
|
||||
}, RoutingStrategies.Direct);
|
||||
}
|
||||
}
|
||||
|
||||
@ -29,12 +29,10 @@ namespace Ryujinx.Ava.Ui.Controls
|
||||
{
|
||||
if (Program.PreviewerDetached)
|
||||
{
|
||||
switch (arg.NavigationMode)
|
||||
if (arg.NavigationMode == NavigationMode.New)
|
||||
{
|
||||
case NavigationMode.New:
|
||||
_parent = (NavigationDialogHost)arg.Parameter;
|
||||
ViewModel = _parent.ViewModel;
|
||||
break;
|
||||
_parent = (NavigationDialogHost)arg.Parameter;
|
||||
ViewModel = _parent.ViewModel;
|
||||
}
|
||||
|
||||
DataContext = ViewModel;
|
||||
|
@ -11,8 +11,8 @@ namespace Ryujinx.Ava.Ui.Models
|
||||
|
||||
public CheatModel(string name, string buildId, bool isEnabled)
|
||||
{
|
||||
Name = name;
|
||||
BuildId = buildId;
|
||||
Name = name;
|
||||
BuildId = buildId;
|
||||
IsEnabled = isEnabled;
|
||||
}
|
||||
|
||||
@ -22,7 +22,9 @@ namespace Ryujinx.Ava.Ui.Models
|
||||
set
|
||||
{
|
||||
_isEnabled = value;
|
||||
|
||||
EnableToggled?.Invoke(this, _isEnabled);
|
||||
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
@ -30,6 +32,7 @@ namespace Ryujinx.Ava.Ui.Models
|
||||
public string BuildId { get; }
|
||||
|
||||
public string BuildIdKey => $"{BuildId}-{Name}";
|
||||
|
||||
public string Name { get; }
|
||||
|
||||
public string CleanName => Name.Substring(1, Name.Length - 8);
|
||||
|
@ -10,26 +10,13 @@ namespace Ryujinx.Ava.Ui.Models
|
||||
public CheatsList(string buildId, string path)
|
||||
{
|
||||
BuildId = buildId;
|
||||
Path = path;
|
||||
Path = path;
|
||||
|
||||
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 Path { get; }
|
||||
public string Path { get; }
|
||||
|
||||
public bool IsEnabled
|
||||
{
|
||||
@ -47,5 +34,18 @@ namespace Ryujinx.Ava.Ui.Models
|
||||
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();
|
||||
|
||||
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;
|
||||
|
@ -1261,15 +1261,15 @@ namespace Ryujinx.Ava.Ui.ViewModels
|
||||
}
|
||||
}
|
||||
|
||||
public async void OpenDlcManager()
|
||||
public async void OpenDownloadableContentManager()
|
||||
{
|
||||
var selection = SelectedApplication;
|
||||
|
||||
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 =>
|
||||
_highlightedProfile != null;
|
||||
public bool IsHighlightedProfileEditable => _highlightedProfile != null;
|
||||
|
||||
public bool IsHighlightedProfileDeletable =>
|
||||
_highlightedProfile != null && _highlightedProfile.UserId != AccountManager.DefaultUserId;
|
||||
public bool IsHighlightedProfileDeletable => _highlightedProfile != null && _highlightedProfile.UserId != AccountManager.DefaultUserId;
|
||||
|
||||
public UserProfile HighlightedProfile
|
||||
{
|
||||
@ -62,16 +60,13 @@ namespace Ryujinx.Ava.Ui.ViewModels
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
public void Dispose() { }
|
||||
|
||||
public void LoadProfiles()
|
||||
{
|
||||
Profiles.Clear();
|
||||
|
||||
var profiles = _owner.AccountManager.GetAllUsers()
|
||||
.OrderByDescending(x => x.AccountState == AccountState.Open);
|
||||
var profiles = _owner.AccountManager.GetAllUsers().OrderByDescending(x => x.AccountState == AccountState.Open);
|
||||
|
||||
foreach (var profile in profiles)
|
||||
{
|
||||
@ -94,6 +89,7 @@ namespace Ryujinx.Ava.Ui.ViewModels
|
||||
public void AddUser()
|
||||
{
|
||||
UserProfile userProfile = null;
|
||||
|
||||
_owner.Navigate(typeof(UserEditor), (this._owner, userProfile, true));
|
||||
}
|
||||
|
||||
|
@ -2,7 +2,6 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.Threading;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
using Ryujinx.Common.Utilities;
|
||||
@ -27,9 +26,6 @@ namespace Ryujinx.Ava.Ui.Windows
|
||||
DataContext = this;
|
||||
|
||||
InitializeComponent();
|
||||
#if DEBUG
|
||||
this.AttachDevTools();
|
||||
#endif
|
||||
|
||||
_ = DownloadPatronsJson();
|
||||
}
|
||||
|
@ -1,6 +1,5 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
using Ryujinx.Ava.Ui.Models;
|
||||
using Ryujinx.Ava.Ui.ViewModels;
|
||||
@ -18,9 +17,7 @@ namespace Ryujinx.Ava.Ui.Windows
|
||||
DataContext = ViewModel;
|
||||
|
||||
InitializeComponent();
|
||||
#if DEBUG
|
||||
this.AttachDevTools();
|
||||
#endif
|
||||
|
||||
Title = $"Ryujinx {Program.Version} - " + LocaleManager.Instance["Amiibo"];
|
||||
}
|
||||
|
||||
@ -31,9 +28,7 @@ namespace Ryujinx.Ava.Ui.Windows
|
||||
DataContext = ViewModel;
|
||||
|
||||
InitializeComponent();
|
||||
#if DEBUG
|
||||
this.AttachDevTools();
|
||||
#endif
|
||||
|
||||
if (Program.PreviewerDetached)
|
||||
{
|
||||
Title = $"Ryujinx {Program.Version} - " + LocaleManager.Instance["Amiibo"];
|
||||
|
@ -1,21 +1,24 @@
|
||||
<window:StyleableWindow x:Class="Ryujinx.Ava.Ui.Windows.CheatWindow"
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
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:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
|
||||
xmlns:model="clr-namespace:Ryujinx.Ava.Ui.Models"
|
||||
xmlns:window="clr-namespace:Ryujinx.Ava.Ui.Windows"
|
||||
mc:Ignorable="d"
|
||||
Width="500" MinHeight="500" Height="500"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
MinWidth="500">
|
||||
<window:StyleableWindow
|
||||
x:Class="Ryujinx.Ava.Ui.Windows.CheatWindow"
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
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:window="clr-namespace:Ryujinx.Ava.Ui.Windows"
|
||||
Width="500"
|
||||
Height="500"
|
||||
MinWidth="500"
|
||||
MinHeight="500"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
mc:Ignorable="d">
|
||||
<Window.Styles>
|
||||
<Style Selector="TreeViewItem">
|
||||
<Setter Property="IsExpanded" Value="True" />
|
||||
</Style>
|
||||
</Window.Styles>
|
||||
<Grid Name="DlcGrid" Margin="15">
|
||||
<Grid Name="CheatGrid" Margin="15">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
@ -24,14 +27,14 @@
|
||||
</Grid.RowDefinitions>
|
||||
<TextBlock
|
||||
Grid.Row="1"
|
||||
MaxWidth="500"
|
||||
Margin="20,15,20,20"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
MaxWidth="500"
|
||||
LineHeight="18"
|
||||
TextWrapping="Wrap"
|
||||
Text="{Binding Heading}"
|
||||
TextAlignment="Center" />
|
||||
TextAlignment="Center"
|
||||
TextWrapping="Wrap" />
|
||||
<Border
|
||||
Grid.Row="2"
|
||||
Margin="5"
|
||||
@ -39,32 +42,38 @@
|
||||
VerticalAlignment="Stretch"
|
||||
BorderBrush="Gray"
|
||||
BorderThickness="1">
|
||||
<TreeView Items="{Binding LoadedCheats}"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
Name="CheatsView"
|
||||
MinHeight="300">
|
||||
<TreeView
|
||||
Name="CheatsView"
|
||||
MinHeight="300"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
Items="{Binding LoadedCheats}">
|
||||
<TreeView.Styles>
|
||||
<Styles>
|
||||
<Style Selector="TreeViewItem:empty /template/ ItemsPresenter">
|
||||
<Setter Property="IsVisible" Value="False"/>
|
||||
<Setter Property="IsVisible" Value="False" />
|
||||
</Style>
|
||||
</Styles>
|
||||
</TreeView.Styles>
|
||||
<TreeView.DataTemplates>
|
||||
<TreeDataTemplate DataType="model:CheatsList" ItemsSource="{Binding}">
|
||||
<StackPanel HorizontalAlignment="Left" Orientation="Horizontal">
|
||||
<CheckBox IsChecked="{Binding IsEnabled}" MinWidth="20" />
|
||||
<TextBlock Width="150"
|
||||
Text="{Binding BuildId}" />
|
||||
<TextBlock
|
||||
Text="{Binding Path}" />
|
||||
<CheckBox MinWidth="20" IsChecked="{Binding IsEnabled}" />
|
||||
<TextBlock Width="150" Text="{Binding BuildId}" />
|
||||
<TextBlock Text="{Binding Path}" />
|
||||
</StackPanel>
|
||||
</TreeDataTemplate>
|
||||
<DataTemplate x:DataType="model:CheatModel">
|
||||
<StackPanel Orientation="Horizontal" Margin="0" HorizontalAlignment="Left">
|
||||
<CheckBox IsChecked="{Binding IsEnabled}" Padding="0" Margin="5,0" MinWidth="20" />
|
||||
<TextBlock Text="{Binding CleanName}" VerticalAlignment="Center" />
|
||||
<StackPanel
|
||||
Margin="0"
|
||||
HorizontalAlignment="Left"
|
||||
Orientation="Horizontal">
|
||||
<CheckBox
|
||||
MinWidth="20"
|
||||
Margin="5,0"
|
||||
Padding="0"
|
||||
IsChecked="{Binding IsEnabled}" />
|
||||
<TextBlock VerticalAlignment="Center" Text="{Binding CleanName}" />
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</TreeView.DataTemplates>
|
||||
@ -79,8 +88,8 @@
|
||||
Name="SaveButton"
|
||||
MinWidth="90"
|
||||
Margin="5"
|
||||
IsVisible="{Binding !NoCheatsFound}"
|
||||
Command="{Binding Save}">
|
||||
Command="{Binding Save}"
|
||||
IsVisible="{Binding !NoCheatsFound}">
|
||||
<TextBlock Text="{locale:Locale SettingsButtonSave}" />
|
||||
</Button>
|
||||
<Button
|
||||
|
@ -1,6 +1,5 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Collections;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
using Ryujinx.Ava.Ui.Models;
|
||||
using Ryujinx.HLE.FileSystem;
|
||||
@ -26,7 +25,6 @@ namespace Ryujinx.Ava.Ui.Windows
|
||||
DataContext = this;
|
||||
|
||||
InitializeComponent();
|
||||
AttachDebugDevTools();
|
||||
|
||||
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());
|
||||
|
||||
InitializeComponent();
|
||||
#if DEBUG
|
||||
this.AttachDevTools();
|
||||
#endif
|
||||
|
||||
string modsBasePath = virtualFileSystem.ModLoader.GetModsBasePath();
|
||||
string titleModsPath = virtualFileSystem.ModLoader.GetTitleDir(modsBasePath, titleId);
|
||||
@ -96,12 +91,6 @@ namespace Ryujinx.Ava.Ui.Windows
|
||||
Title = $"Ryujinx {Program.Version} - " + LocaleManager.Instance["CheatWindowTitle"];
|
||||
}
|
||||
|
||||
[Conditional("DEBUG")]
|
||||
private void AttachDebugDevTools()
|
||||
{
|
||||
this.AttachDevTools();
|
||||
}
|
||||
|
||||
public void Save()
|
||||
{
|
||||
if (NoCheatsFound)
|
||||
|
@ -3,24 +3,14 @@ using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.LogicalTree;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.Threading;
|
||||
using Avalonia.VisualTree;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
using Ryujinx.Ava.Ui.Controls;
|
||||
using Ryujinx.Ava.Ui.Models;
|
||||
using Ryujinx.Ava.Ui.ViewModels;
|
||||
using Ryujinx.Common.Configuration.Hid;
|
||||
using Ryujinx.Common.Configuration.Hid.Controller;
|
||||
using Ryujinx.Input;
|
||||
using Ryujinx.Input.Assigner;
|
||||
using Ryujinx.Ui.Common.Configuration;
|
||||
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
|
||||
{
|
||||
|
@ -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
|
||||
x:Class="Ryujinx.Ava.Ui.Windows.DlcManagerWindow"
|
||||
x:Class="Ryujinx.Ava.Ui.Windows.DownloadableContentManagerWindow"
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
@ -11,7 +11,7 @@
|
||||
WindowStartupLocation="CenterOwner"
|
||||
MinWidth="600"
|
||||
mc:Ignorable="d">
|
||||
<Grid Name="DlcGrid" Margin="15">
|
||||
<Grid Name="DownloadableContentGrid" Margin="15">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
@ -40,7 +40,7 @@
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
HorizontalScrollBarVisibility="Auto"
|
||||
Items="{Binding Dlcs}"
|
||||
Items="{Binding DownloadableContents}"
|
||||
VerticalScrollBarVisibility="Auto">
|
||||
<DataGrid.Columns>
|
||||
<DataGridTemplateColumn Width="90">
|
||||
@ -50,7 +50,7 @@
|
||||
Width="50"
|
||||
MinWidth="40"
|
||||
HorizontalAlignment="Right"
|
||||
IsChecked="{Binding IsEnabled}" />
|
||||
IsChecked="{Binding Enabled}" />
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
<DataGridTemplateColumn.Header>
|
||||
@ -116,7 +116,7 @@
|
||||
Name="SaveButton"
|
||||
MinWidth="90"
|
||||
Margin="5"
|
||||
Command="{Binding Save}">
|
||||
Command="{Binding SaveAndClose}">
|
||||
<TextBlock Text="{locale:Locale SettingsButtonSave}" />
|
||||
</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}"
|
||||
Header="{locale:Locale MenuBarOptionsToggleFullscreen}"
|
||||
InputGesture="F11" />
|
||||
<MenuItem Header="{locale:Locale MenuBarOptionsStartGamesInFullscreen}">
|
||||
<MenuItem>
|
||||
<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>
|
||||
<MenuItem Header="{locale:Locale MenuBarOptionsShowConsole}" IsVisible="{Binding ShowConsoleVisible}">
|
||||
<MenuItem IsVisible="{Binding ShowConsoleVisible}">
|
||||
<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>
|
||||
<Separator />
|
||||
@ -167,6 +173,10 @@
|
||||
Command="{ReflectionBinding ChangeLanguage}"
|
||||
CommandParameter="zh_CN"
|
||||
Header="Simplified Chinese" />
|
||||
<MenuItem
|
||||
Command="{ReflectionBinding ChangeLanguage}"
|
||||
CommandParameter="zh_TW"
|
||||
Header="Traditional Chinese (Taiwan)" />
|
||||
</MenuItem>
|
||||
<Separator />
|
||||
<MenuItem
|
||||
@ -247,7 +257,7 @@
|
||||
</DockPanel>
|
||||
</StackPanel>
|
||||
<ContentControl
|
||||
Name="Content"
|
||||
Name="MainContent"
|
||||
Grid.Row="1"
|
||||
Padding="0"
|
||||
HorizontalAlignment="Stretch"
|
||||
|
@ -2,10 +2,8 @@ using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Threading;
|
||||
using Avalonia.Win32;
|
||||
using FluentAvalonia.UI.Controls;
|
||||
using Ryujinx.Ava.Common;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
@ -33,7 +31,7 @@ using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using InputManager = Ryujinx.Input.HLE.InputManager;
|
||||
using ProgressBar = Avalonia.Controls.ProgressBar;
|
||||
|
||||
namespace Ryujinx.Ava.Ui.Windows
|
||||
{
|
||||
public partial class MainWindow : StyleableWindow
|
||||
@ -87,7 +85,6 @@ namespace Ryujinx.Ava.Ui.Windows
|
||||
|
||||
InitializeComponent();
|
||||
Load();
|
||||
AttachDebugDevTools();
|
||||
|
||||
UiHandler = new AvaHostUiHandler(this);
|
||||
|
||||
@ -110,12 +107,6 @@ namespace Ryujinx.Ava.Ui.Windows
|
||||
_rendererWaitEvent = new AutoResetEvent(false);
|
||||
}
|
||||
|
||||
[Conditional("DEBUG")]
|
||||
private void AttachDebugDevTools()
|
||||
{
|
||||
this.AttachDevTools();
|
||||
}
|
||||
|
||||
public void LoadGameList()
|
||||
{
|
||||
if (_isLoading)
|
||||
@ -244,7 +235,7 @@ namespace Ryujinx.Ava.Ui.Windows
|
||||
|
||||
PrepareLoadScreen();
|
||||
|
||||
_mainViewContent = Content.Content as Control;
|
||||
_mainViewContent = MainContent.Content as Control;
|
||||
|
||||
GlRenderer = new RendererControl(3, 3, ConfigurationState.Instance.Logger.GraphicsDebugLevel);
|
||||
AppHost = new AppHost(GlRenderer, InputManager, path, VirtualFileSystem, ContentManager, AccountManager, _userChannelPersistence, this);
|
||||
@ -311,7 +302,7 @@ namespace Ryujinx.Ava.Ui.Windows
|
||||
|
||||
Dispatcher.UIThread.InvokeAsync(() =>
|
||||
{
|
||||
Content.Content = GlRenderer;
|
||||
MainContent.Content = GlRenderer;
|
||||
|
||||
if (startFullscreen && WindowState != WindowState.FullScreen)
|
||||
{
|
||||
@ -355,9 +346,9 @@ namespace Ryujinx.Ava.Ui.Windows
|
||||
|
||||
Dispatcher.UIThread.InvokeAsync(() =>
|
||||
{
|
||||
if (Content.Content != _mainViewContent)
|
||||
if (MainContent.Content != _mainViewContent)
|
||||
{
|
||||
Content.Content = _mainViewContent;
|
||||
MainContent.Content = _mainViewContent;
|
||||
}
|
||||
|
||||
ViewModel.ShowMenuAndStatusBar = true;
|
||||
|
@ -1,5 +1,4 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using FluentAvalonia.UI.Controls;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
using Ryujinx.Ava.Ui.Models;
|
||||
|
@ -1,5 +1,4 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using FluentAvalonia.UI.Controls;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
using Ryujinx.Ava.Ui.Models;
|
||||
|
@ -241,6 +241,22 @@
|
||||
TextAlignment="Center" />
|
||||
</ToggleButton>
|
||||
</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>
|
||||
</Border>
|
||||
</ScrollViewer>
|
||||
|
@ -1,19 +1,14 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Presenters;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Data;
|
||||
using Avalonia.Data.Converters;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.LogicalTree;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.Threading;
|
||||
using FluentAvalonia.Core;
|
||||
using FluentAvalonia.UI.Controls;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
using Ryujinx.Ava.Ui.Controls;
|
||||
using Ryujinx.Ava.Ui.Models;
|
||||
using Ryujinx.Ava.Ui.ViewModels;
|
||||
using Ryujinx.HLE.FileSystem;
|
||||
using Ryujinx.Input;
|
||||
@ -23,8 +18,6 @@ using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using TimeZone = Ryujinx.Ava.Ui.Models.TimeZone;
|
||||
|
||||
namespace Ryujinx.Ava.Ui.Windows
|
||||
@ -44,7 +37,6 @@ namespace Ryujinx.Ava.Ui.Windows
|
||||
|
||||
InitializeComponent();
|
||||
Load();
|
||||
AttachDebugDevTools();
|
||||
|
||||
FuncMultiValueConverter<string, string> converter = new(parts => string.Format("{0} {1} {2}", parts.ToArray()));
|
||||
MultiBinding tzMultiBinding = new() { Converter = converter };
|
||||
@ -62,13 +54,6 @@ namespace Ryujinx.Ava.Ui.Windows
|
||||
|
||||
InitializeComponent();
|
||||
Load();
|
||||
AttachDebugDevTools();
|
||||
}
|
||||
|
||||
[Conditional("DEBUG")]
|
||||
private void AttachDebugDevTools()
|
||||
{
|
||||
this.AttachDevTools();
|
||||
}
|
||||
|
||||
private void Load()
|
||||
|
@ -2,7 +2,6 @@
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Media.Imaging;
|
||||
using Avalonia.Platform;
|
||||
using FluentAvalonia.UI.Controls;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
|
@ -1,13 +1,14 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Collections;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.Threading;
|
||||
using LibHac.Common;
|
||||
using LibHac.Fs;
|
||||
using LibHac.Fs.Fsa;
|
||||
using LibHac.FsSystem;
|
||||
using LibHac.Tools.FsSystem.NcaUtils;
|
||||
using LibHac.Ns;
|
||||
using LibHac.Tools.FsSystem;
|
||||
using LibHac.Tools.FsSystem.NcaUtils;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
using Ryujinx.Ava.Ui.Controls;
|
||||
using Ryujinx.Ava.Ui.Models;
|
||||
@ -23,14 +24,12 @@ using System.Linq;
|
||||
using System.Text;
|
||||
using Path = System.IO.Path;
|
||||
using SpanHelpers = LibHac.Common.SpanHelpers;
|
||||
using LibHac.Tools.FsSystem;
|
||||
using Avalonia.Threading;
|
||||
|
||||
namespace Ryujinx.Ava.Ui.Windows
|
||||
{
|
||||
public partial class TitleUpdateWindow : StyleableWindow
|
||||
{
|
||||
private readonly string _updateJsonPath;
|
||||
private readonly string _titleUpdateJsonPath;
|
||||
private TitleUpdateMetadata _titleUpdateWindowData;
|
||||
|
||||
public VirtualFileSystem VirtualFileSystem { get; }
|
||||
@ -46,7 +45,6 @@ namespace Ryujinx.Ava.Ui.Windows
|
||||
DataContext = this;
|
||||
|
||||
InitializeComponent();
|
||||
AttachDebugDevTools();
|
||||
|
||||
Title = $"Ryujinx {Program.Version} - " + LocaleManager.Instance["UpdateWindowTitle"];
|
||||
}
|
||||
@ -54,36 +52,33 @@ namespace Ryujinx.Ava.Ui.Windows
|
||||
public TitleUpdateWindow(VirtualFileSystem virtualFileSystem, string titleId, string titleName)
|
||||
{
|
||||
VirtualFileSystem = virtualFileSystem;
|
||||
TitleId = titleId;
|
||||
TitleName = titleName;
|
||||
TitleId = titleId;
|
||||
TitleName = titleName;
|
||||
|
||||
_updateJsonPath = Path.Combine(AppDataManager.GamesDirPath, titleId, "updates.json");
|
||||
_titleUpdateJsonPath = Path.Combine(AppDataManager.GamesDirPath, titleId, "updates.json");
|
||||
|
||||
try
|
||||
{
|
||||
_titleUpdateWindowData = JsonHelper.DeserializeFromFile<TitleUpdateMetadata>(_updateJsonPath);
|
||||
_titleUpdateWindowData = JsonHelper.DeserializeFromFile<TitleUpdateMetadata>(_titleUpdateJsonPath);
|
||||
}
|
||||
catch
|
||||
{
|
||||
_titleUpdateWindowData = new TitleUpdateMetadata {Selected = "", Paths = new List<string>()};
|
||||
_titleUpdateWindowData = new TitleUpdateMetadata
|
||||
{
|
||||
Selected = "",
|
||||
Paths = new List<string>()
|
||||
};
|
||||
}
|
||||
|
||||
DataContext = this;
|
||||
|
||||
InitializeComponent();
|
||||
AttachDebugDevTools();
|
||||
|
||||
Title = $"Ryujinx {Program.Version} - " + LocaleManager.Instance["UpdateWindowTitle"];
|
||||
|
||||
LoadUpdates();
|
||||
}
|
||||
|
||||
[Conditional("DEBUG")]
|
||||
private void AttachDebugDevTools()
|
||||
{
|
||||
this.AttachDevTools();
|
||||
}
|
||||
|
||||
private void LoadUpdates()
|
||||
{
|
||||
TitleUpdates.Add(new TitleUpdateModel(default, string.Empty, true));
|
||||
@ -99,8 +94,8 @@ namespace Ryujinx.Ava.Ui.Windows
|
||||
}
|
||||
else
|
||||
{
|
||||
TitleUpdateModel selected = TitleUpdates.FirstOrDefault(x => x.Path == _titleUpdateWindowData.Selected);
|
||||
List<TitleUpdateModel> enabled = TitleUpdates.Where(x => x.IsEnabled).ToList();
|
||||
TitleUpdateModel selected = TitleUpdates.FirstOrDefault(x => x.Path == _titleUpdateWindowData.Selected);
|
||||
List<TitleUpdateModel> enabled = TitleUpdates.Where(x => x.IsEnabled).ToList();
|
||||
|
||||
foreach (TitleUpdateModel update in enabled)
|
||||
{
|
||||
@ -126,8 +121,7 @@ namespace Ryujinx.Ava.Ui.Windows
|
||||
|
||||
try
|
||||
{
|
||||
(Nca patchNca, Nca controlNca) =
|
||||
ApplicationLoader.GetGameUpdateDataFromPartition(VirtualFileSystem, nsp, TitleId, 0);
|
||||
(Nca patchNca, Nca controlNca) = ApplicationLoader.GetGameUpdateDataFromPartition(VirtualFileSystem, nsp, TitleId, 0);
|
||||
|
||||
if (controlNca != null && patchNca != null)
|
||||
{
|
||||
@ -135,11 +129,8 @@ namespace Ryujinx.Ava.Ui.Windows
|
||||
|
||||
using var nacpFile = new UniqueRef<IFile>();
|
||||
|
||||
controlNca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None)
|
||||
.OpenFile(ref nacpFile.Ref(), "/control.nacp".ToU8Span(), OpenMode.Read)
|
||||
.ThrowIfFailure();
|
||||
nacpFile.Get.Read(out _, 0, SpanHelpers.AsByteSpan(ref controlData), ReadOption.None)
|
||||
.ThrowIfFailure();
|
||||
controlNca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None).OpenFile(ref nacpFile.Ref(), "/control.nacp".ToU8Span(), OpenMode.Read).ThrowIfFailure();
|
||||
nacpFile.Get.Read(out _, 0, SpanHelpers.AsByteSpan(ref controlData), ReadOption.None).ThrowIfFailure();
|
||||
|
||||
TitleUpdates.Add(new TitleUpdateModel(controlData, path));
|
||||
}
|
||||
@ -190,9 +181,17 @@ namespace Ryujinx.Ava.Ui.Windows
|
||||
|
||||
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);
|
||||
|
||||
@ -222,12 +221,10 @@ namespace Ryujinx.Ava.Ui.Windows
|
||||
return 1;
|
||||
}
|
||||
|
||||
return Version.Parse(first.Control.DisplayVersionString.ToString())
|
||||
.CompareTo(Version.Parse(second.Control.DisplayVersionString.ToString())) * -1;
|
||||
return Version.Parse(first.Control.DisplayVersionString.ToString()).CompareTo(Version.Parse(second.Control.DisplayVersionString.ToString())) * -1;
|
||||
});
|
||||
|
||||
TitleUpdates.Clear();
|
||||
|
||||
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)
|
||||
|
@ -1,6 +1,5 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
using Ryujinx.Modules;
|
||||
using System;
|
||||
@ -23,9 +22,7 @@ namespace Ryujinx.Ava.Ui.Windows
|
||||
DataContext = this;
|
||||
|
||||
InitializeComponent();
|
||||
#if DEBUG
|
||||
this.AttachDevTools();
|
||||
#endif
|
||||
|
||||
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 Pause { 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;
|
||||
|
||||
_addressSpace = new MemoryBlock(asSize, asFlags);
|
||||
_addressSpaceMirror = new MemoryBlock(asSize, asFlags | MemoryAllocationFlags.ForceWindows4KBViewMapping);
|
||||
_addressSpaceMirror = new MemoryBlock(asSize, asFlags);
|
||||
|
||||
Tracking = new MemoryTracking(this, PageSize, invalidAccessHandler);
|
||||
_memoryEh = new MemoryEhMeilleure(_addressSpace, Tracking);
|
||||
_memoryEh = new MemoryEhMeilleure(_addressSpace, _addressSpaceMirror, Tracking);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -6,36 +6,57 @@ using System.Runtime.InteropServices;
|
||||
|
||||
namespace Ryujinx.Cpu
|
||||
{
|
||||
class MemoryEhMeilleure : IDisposable
|
||||
public class MemoryEhMeilleure : IDisposable
|
||||
{
|
||||
private delegate bool TrackingEventDelegate(ulong address, ulong size, bool write, bool precise = false);
|
||||
|
||||
private readonly MemoryBlock _addressSpace;
|
||||
private readonly MemoryTracking _tracking;
|
||||
private readonly TrackingEventDelegate _trackingEvent;
|
||||
|
||||
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;
|
||||
|
||||
_baseAddress = (ulong)_addressSpace.Pointer;
|
||||
_baseAddress = (ulong)addressSpace.Pointer;
|
||||
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));
|
||||
|
||||
if (!added)
|
||||
{
|
||||
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()
|
||||
{
|
||||
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(
|
||||
memoryManager,
|
||||
dst,
|
||||
dstGpuVa,
|
||||
dstBpp,
|
||||
dstStride,
|
||||
dst.Height,
|
||||
xCount,
|
||||
yCount,
|
||||
dstLinear);
|
||||
dstLinear,
|
||||
dst.MemoryLayout);
|
||||
|
||||
if (target != null)
|
||||
{
|
||||
|
@ -59,9 +59,24 @@ namespace Ryujinx.Graphics.Gpu
|
||||
{
|
||||
oldMemoryManager.Physical.BufferCache.NotifyBuffersModified -= BufferManager.Rebind;
|
||||
oldMemoryManager.Physical.DecrementReferenceCount();
|
||||
oldMemoryManager.MemoryUnmapped -= MemoryUnmappedHandler;
|
||||
}
|
||||
|
||||
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>
|
||||
|
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 System.Collections.Generic;
|
||||
|
||||
namespace Ryujinx.Graphics.Gpu.Image
|
||||
{
|
||||
/// <summary>
|
||||
/// Sampler pool.
|
||||
/// </summary>
|
||||
class SamplerPool : Pool<Sampler, SamplerDescriptor>
|
||||
class SamplerPool : Pool<Sampler, SamplerDescriptor>, IPool<SamplerPool>
|
||||
{
|
||||
private float _forcedAnisotropy;
|
||||
|
||||
/// <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>
|
||||
/// <param name="context">GPU context that the sampler pool belongs to</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>
|
||||
/// Texture bindings manager.
|
||||
/// </summary>
|
||||
class TextureBindingsManager : IDisposable
|
||||
class TextureBindingsManager
|
||||
{
|
||||
private const int InitialTextureStateSize = 32;
|
||||
private const int InitialImageStateSize = 8;
|
||||
@ -22,15 +22,17 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
|
||||
private readonly bool _isCompute;
|
||||
|
||||
private SamplerPool _samplerPool;
|
||||
|
||||
private ulong _texturePoolGpuVa;
|
||||
private int _texturePoolMaximumId;
|
||||
private TexturePool _texturePool;
|
||||
private ulong _samplerPoolGpuVa;
|
||||
private int _samplerPoolMaximumId;
|
||||
private SamplerIndex _samplerIndex;
|
||||
|
||||
private ulong _texturePoolAddress;
|
||||
private int _texturePoolMaximumId;
|
||||
private SamplerPool _samplerPool;
|
||||
|
||||
private readonly GpuChannel _channel;
|
||||
private readonly TexturePoolCache _texturePoolCache;
|
||||
private readonly SamplerPoolCache _samplerPoolCache;
|
||||
|
||||
private TexturePool _cachedTexturePool;
|
||||
private SamplerPool _cachedSamplerPool;
|
||||
@ -72,16 +74,25 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
/// </summary>
|
||||
/// <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="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="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;
|
||||
_channel = channel;
|
||||
_texturePoolCache = poolCache;
|
||||
_scales = scales;
|
||||
_isCompute = isCompute;
|
||||
_context = context;
|
||||
_channel = channel;
|
||||
_texturePoolCache = texturePoolCache;
|
||||
_samplerPoolCache = samplerPoolCache;
|
||||
|
||||
_scales = scales;
|
||||
_isCompute = isCompute;
|
||||
|
||||
int stages = isCompute ? 1 : Constants.ShaderStages;
|
||||
|
||||
@ -173,25 +184,10 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
/// <param name="samplerIndex">Type of the sampler pool indexing used for bound samplers</param>
|
||||
public void SetSamplerPool(ulong gpuVa, int maximumId, SamplerIndex samplerIndex)
|
||||
{
|
||||
if (gpuVa != 0)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
_samplerPoolGpuVa = gpuVa;
|
||||
_samplerPoolMaximumId = maximumId;
|
||||
_samplerIndex = samplerIndex;
|
||||
_samplerPool = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -201,18 +197,9 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
/// <param name="maximumId">Maximum ID of the pool (total count minus one)</param>
|
||||
public void SetTexturePool(ulong gpuVa, int maximumId)
|
||||
{
|
||||
if (gpuVa != 0)
|
||||
{
|
||||
ulong address = _channel.MemoryManager.Translate(gpuVa);
|
||||
|
||||
_texturePoolAddress = address;
|
||||
_texturePoolMaximumId = maximumId;
|
||||
}
|
||||
else
|
||||
{
|
||||
_texturePoolAddress = 0;
|
||||
_texturePoolMaximumId = 0;
|
||||
}
|
||||
_texturePoolGpuVa = gpuVa;
|
||||
_texturePoolMaximumId = maximumId;
|
||||
_texturePool = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -222,13 +209,9 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
/// <param name="samplerId">ID of the sampler</param>
|
||||
public (Texture, Sampler) GetTextureAndSampler(int textureId, int samplerId)
|
||||
{
|
||||
ulong texturePoolAddress = _texturePoolAddress;
|
||||
(TexturePool texturePool, SamplerPool samplerPool) = GetPools();
|
||||
|
||||
TexturePool texturePool = texturePoolAddress != 0
|
||||
? _texturePoolCache.FindOrCreate(_channel, texturePoolAddress, _texturePoolMaximumId)
|
||||
: null;
|
||||
|
||||
return (texturePool.Get(textureId), _samplerPool.Get(samplerId));
|
||||
return (texturePool.Get(textureId), samplerPool.Get(samplerId));
|
||||
}
|
||||
|
||||
/// <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>
|
||||
public bool CommitBindings(ShaderSpecializationState specState)
|
||||
{
|
||||
ulong texturePoolAddress = _texturePoolAddress;
|
||||
|
||||
TexturePool texturePool = texturePoolAddress != 0
|
||||
? _texturePoolCache.FindOrCreate(_channel, texturePoolAddress, _texturePoolMaximumId)
|
||||
: null;
|
||||
|
||||
SamplerPool samplerPool = _samplerPool;
|
||||
(TexturePool texturePool, SamplerPool samplerPool) = GetPools();
|
||||
|
||||
// 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.
|
||||
@ -381,7 +358,7 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
|
||||
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);
|
||||
}
|
||||
else
|
||||
@ -390,7 +367,7 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -447,13 +424,20 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
/// Ensures that the texture bindings are visible to the host GPU.
|
||||
/// Note: this actually performs the binding using the host graphics API.
|
||||
/// </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="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="specState">Specialization state for the bound shader</param>
|
||||
/// <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];
|
||||
if (textureCount == 0)
|
||||
@ -461,9 +445,7 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
return true;
|
||||
}
|
||||
|
||||
var samplerPool = _samplerPool;
|
||||
|
||||
if (pool == null)
|
||||
if (texturePool == null)
|
||||
{
|
||||
Logger.Error?.Print(LogClass.Gpu, $"Shader stage \"{stage}\" uses textures, but texture pool was not set.");
|
||||
return true;
|
||||
@ -528,7 +510,7 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
state.TextureHandle = textureId;
|
||||
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);
|
||||
|
||||
@ -819,6 +801,54 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
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>
|
||||
/// Force all bound textures and images to be rebound the next time CommitBindings is called.
|
||||
/// </summary>
|
||||
@ -827,13 +857,5 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
Array.Clear(_textureState);
|
||||
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.
|
||||
/// </summary>
|
||||
/// <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="bpp">Bytes per pixel</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="yCount">Number of lines to be copied</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>
|
||||
public Texture FindTexture(
|
||||
MemoryManager memoryManager,
|
||||
DmaTexture tex,
|
||||
ulong gpuVa,
|
||||
int bpp,
|
||||
int stride,
|
||||
int height,
|
||||
int xCount,
|
||||
int yCount,
|
||||
bool linear)
|
||||
bool linear,
|
||||
MemoryLayout memoryLayout)
|
||||
{
|
||||
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.
|
||||
|
||||
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
|
||||
{
|
||||
@ -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.
|
||||
// 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 &&
|
||||
texture.Info.GobBlocksInY == tex.MemoryLayout.UnpackGobBlocksInY() &&
|
||||
texture.Info.GobBlocksInZ == tex.MemoryLayout.UnpackGobBlocksInZ();
|
||||
texture.Info.GobBlocksInY == memoryLayout.UnpackGobBlocksInY() &&
|
||||
texture.Info.GobBlocksInZ == memoryLayout.UnpackGobBlocksInZ();
|
||||
|
||||
match = sizeMatch && formatMatch;
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
private readonly TextureBindingsManager _cpBindingsManager;
|
||||
private readonly TextureBindingsManager _gpBindingsManager;
|
||||
private readonly TexturePoolCache _texturePoolCache;
|
||||
private readonly SamplerPoolCache _samplerPoolCache;
|
||||
|
||||
private readonly Texture[] _rtColors;
|
||||
private readonly ITexture[] _rtHostColors;
|
||||
@ -41,13 +42,15 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
_channel = channel;
|
||||
|
||||
TexturePoolCache texturePoolCache = new TexturePoolCache(context);
|
||||
SamplerPoolCache samplerPoolCache = new SamplerPoolCache(context);
|
||||
|
||||
float[] scales = new float[64];
|
||||
new Span<float>(scales).Fill(1f);
|
||||
|
||||
_cpBindingsManager = new TextureBindingsManager(context, channel, texturePoolCache, scales, isCompute: true);
|
||||
_gpBindingsManager = new TextureBindingsManager(context, channel, texturePoolCache, scales, isCompute: false);
|
||||
_cpBindingsManager = new TextureBindingsManager(context, channel, texturePoolCache, samplerPoolCache, scales, isCompute: true);
|
||||
_gpBindingsManager = new TextureBindingsManager(context, channel, texturePoolCache, samplerPoolCache, scales, isCompute: false);
|
||||
_texturePoolCache = texturePoolCache;
|
||||
_samplerPoolCache = samplerPoolCache;
|
||||
|
||||
_rtColors = new Texture[Constants.TotalRenderTargets];
|
||||
_rtHostColors = new ITexture[Constants.TotalRenderTargets];
|
||||
@ -368,6 +371,10 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
// we must rebind everything.
|
||||
// Since compute work happens less often, we always do that
|
||||
// before and after the compute dispatch.
|
||||
|
||||
_texturePoolCache.Tick();
|
||||
_samplerPoolCache.Tick();
|
||||
|
||||
_cpBindingsManager.Rebind();
|
||||
bool result = _cpBindingsManager.CommitBindings(specState);
|
||||
_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>
|
||||
public bool CommitGraphicsBindings(ShaderSpecializationState specState)
|
||||
{
|
||||
_texturePoolCache.Tick();
|
||||
_samplerPoolCache.Tick();
|
||||
|
||||
bool result = _gpBindingsManager.CommitBindings(specState);
|
||||
|
||||
UpdateRenderTargets();
|
||||
@ -501,6 +511,15 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
_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>
|
||||
/// Forces all textures, samplers, images and render targets to be rebound the next time
|
||||
/// CommitGraphicsBindings is called.
|
||||
@ -523,8 +542,8 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
_cpBindingsManager.Dispose();
|
||||
_gpBindingsManager.Dispose();
|
||||
// Textures are owned by the texture cache, so we shouldn't dispose the texture pool cache.
|
||||
_samplerPoolCache.Dispose();
|
||||
|
||||
for (int i = 0; i < _rtColors.Length; i++)
|
||||
{
|
||||
|
@ -10,19 +10,24 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
/// <summary>
|
||||
/// Texture pool.
|
||||
/// </summary>
|
||||
class TexturePool : Pool<Texture, TextureDescriptor>
|
||||
class TexturePool : Pool<Texture, TextureDescriptor>, IPool<TexturePool>
|
||||
{
|
||||
private readonly GpuChannel _channel;
|
||||
private readonly ConcurrentQueue<Texture> _dereferenceQueue = new ConcurrentQueue<Texture>();
|
||||
private TextureDescriptor _defaultDescriptor;
|
||||
|
||||
/// <summary>
|
||||
/// Intrusive linked list node used on the texture pool cache.
|
||||
/// Linked list node used on the texture pool cache.
|
||||
/// </summary>
|
||||
public LinkedListNode<TexturePool> CacheNode { get; set; }
|
||||
|
||||
/// <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>
|
||||
/// <param name="context">GPU context 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
|
||||
{
|
||||
/// <summary>
|
||||
@ -8,69 +5,26 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
/// This can keep multiple texture pools, and return the current one as needed.
|
||||
/// It is useful for applications that uses multiple texture pools.
|
||||
/// </summary>
|
||||
class TexturePoolCache
|
||||
class TexturePoolCache : PoolCache<TexturePool>
|
||||
{
|
||||
private const int MaxCapacity = 4;
|
||||
|
||||
private readonly GpuContext _context;
|
||||
private readonly LinkedList<TexturePool> _pools;
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a new instance of the texture pool.
|
||||
/// </summary>
|
||||
/// <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>
|
||||
/// Finds a cache texture pool, or creates a new one if not found.
|
||||
/// Creates a new instance of the texture pool.
|
||||
/// </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 TexturePool FindOrCreate(GpuChannel channel, ulong address, int maximumId)
|
||||
/// <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="address">Address of the texture pool in guest memory</param>
|
||||
/// <param name="maximumId">Maximum texture ID of the texture pool (equal to maximum textures minus one)</param>
|
||||
protected override TexturePool CreatePool(GpuContext context, GpuChannel channel, ulong address, int maximumId)
|
||||
{
|
||||
TexturePool pool;
|
||||
|
||||
// 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;
|
||||
return new TexturePool(context, channel, address, maximumId);
|
||||
}
|
||||
}
|
||||
}
|
@ -21,7 +21,7 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
|
||||
private const ushort FileFormatVersionMajor = 1;
|
||||
private const ushort FileFormatVersionMinor = 1;
|
||||
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 SharedDataFileName = "shared.data";
|
||||
|
@ -104,12 +104,23 @@ namespace Ryujinx.Graphics.Shader.Instructions
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Figure out how this is supposed to work in the
|
||||
// presence of other condition codes.
|
||||
if (op.Ccc == Ccc.T)
|
||||
{
|
||||
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)
|
||||
@ -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.
|
||||
switch (cond)
|
||||
@ -263,7 +274,7 @@ namespace Ryujinx.Graphics.Shader.Instructions
|
||||
return context.BitwiseNot(GetZF());
|
||||
}
|
||||
|
||||
return Const(IrConsts.True);
|
||||
return Const(defaultCond);
|
||||
}
|
||||
}
|
||||
}
|
@ -422,19 +422,19 @@ namespace Ryujinx.HLE.HOS
|
||||
|
||||
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
|
||||
{
|
||||
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
|
||||
{
|
||||
class MockVirtualMemoryManager : IVirtualMemoryManager
|
||||
public class MockVirtualMemoryManager : IVirtualMemoryManager
|
||||
{
|
||||
public bool NoMappings = false;
|
||||
|
||||
|
@ -38,9 +38,15 @@ namespace Ryujinx.Memory.Tests
|
||||
Assert.AreEqual(Marshal.ReadInt32(_memoryBlock.Pointer, 0x2040), 0xbadc0de);
|
||||
}
|
||||
|
||||
[Test, Explicit]
|
||||
[Test]
|
||||
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 toAlias = new MemoryBlock(0x10000, MemoryAllocationFlags.Reserve | MemoryAllocationFlags.ViewCompatible);
|
||||
|
||||
@ -51,9 +57,15 @@ namespace Ryujinx.Memory.Tests
|
||||
Assert.AreEqual(Marshal.ReadInt32(backing.Pointer, 0x1000), 0xbadc0de);
|
||||
}
|
||||
|
||||
[Test, Explicit]
|
||||
[Test]
|
||||
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 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.
|
||||
/// The block that is to have their views mapped should be created with the <see cref="Mirrorable"/> flag.
|
||||
/// </summary>
|
||||
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
|
||||
ViewCompatible = 1 << 3
|
||||
}
|
||||
}
|
||||
|
@ -13,14 +13,11 @@ namespace Ryujinx.Memory
|
||||
private readonly bool _usesSharedMemory;
|
||||
private readonly bool _isMirror;
|
||||
private readonly bool _viewCompatible;
|
||||
private readonly bool _forceWindows4KBView;
|
||||
private IntPtr _sharedMemory;
|
||||
private IntPtr _pointer;
|
||||
private ConcurrentDictionary<MemoryBlock, byte> _viewStorages;
|
||||
private int _viewCount;
|
||||
|
||||
internal bool ForceWindows4KBView => _forceWindows4KBView;
|
||||
|
||||
/// <summary>
|
||||
/// Pointer to the memory block data.
|
||||
/// </summary>
|
||||
@ -49,8 +46,7 @@ namespace Ryujinx.Memory
|
||||
else if (flags.HasFlag(MemoryAllocationFlags.Reserve))
|
||||
{
|
||||
_viewCompatible = flags.HasFlag(MemoryAllocationFlags.ViewCompatible);
|
||||
_forceWindows4KBView = flags.HasFlag(MemoryAllocationFlags.ForceWindows4KBViewMapping);
|
||||
_pointer = MemoryManagement.Reserve(size, _viewCompatible, _forceWindows4KBView);
|
||||
_pointer = MemoryManagement.Reserve(size, _viewCompatible);
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -173,7 +169,7 @@ namespace Ryujinx.Memory
|
||||
/// <exception cref="MemoryProtectionException">Throw when <paramref name="permission"/> is invalid</exception>
|
||||
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>
|
||||
@ -406,7 +402,7 @@ namespace Ryujinx.Memory
|
||||
}
|
||||
else
|
||||
{
|
||||
MemoryManagement.Free(ptr, Size, _forceWindows4KBView);
|
||||
MemoryManagement.Free(ptr, Size);
|
||||
}
|
||||
|
||||
foreach (MemoryBlock viewStorage in _viewStorages.Keys)
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user