diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index 383bbb151..399aa039c 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -1,6 +1,7 @@ name: Feature Request description: Suggest a new feature for Ryujinx. title: "[Feature Request]" +labels: enhancement body: - type: textarea id: overview diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9e11302fd..221c7732e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -20,7 +20,7 @@ jobs: - { name: win-x64, os: windows-latest, zip_os_name: win_x64 } - { name: linux-x64, os: ubuntu-latest, zip_os_name: linux_x64 } - { name: linux-arm64, os: ubuntu-latest, zip_os_name: linux_arm64 } - - { name: osx-x64, os: macOS-latest, zip_os_name: osx_x64 } + - { name: osx-x64, os: macos-13, zip_os_name: osx_x64 } fail-fast: false steps: @@ -41,12 +41,12 @@ jobs: - name: Change config filename run: sed -r --in-place 's/\%\%RYUJINX_CONFIG_FILE_NAME\%\%/PRConfig\.json/g;' src/Ryujinx.Common/ReleaseInformation.cs shell: bash - if: github.event_name == 'pull_request' && matrix.platform.os != 'macOS-latest' + if: github.event_name == 'pull_request' && matrix.platform.os != 'macos-13' - name: Change config filename for macOS run: sed -r -i '' 's/\%\%RYUJINX_CONFIG_FILE_NAME\%\%/PRConfig\.json/g;' src/Ryujinx.Common/ReleaseInformation.cs shell: bash - if: github.event_name == 'pull_request' && matrix.platform.os == 'macOS-latest' + if: github.event_name == 'pull_request' && matrix.platform.os == 'macos-13' - name: Build run: dotnet build -c "${{ matrix.configuration }}" -p:Version="${{ env.RYUJINX_BASE_VERSION }}" -p:SourceRevisionId="${{ steps.git_short_hash.outputs.result }}" -p:ExtraDefineConstants=DISABLE_UPDATER @@ -61,15 +61,15 @@ jobs: - name: Publish Ryujinx run: dotnet publish -c "${{ matrix.configuration }}" -r "${{ matrix.platform.name }}" -o ./publish -p:Version="${{ env.RYUJINX_BASE_VERSION }}" -p:DebugType=embedded -p:SourceRevisionId="${{ steps.git_short_hash.outputs.result }}" -p:ExtraDefineConstants=DISABLE_UPDATER src/Ryujinx --self-contained true - if: github.event_name == 'pull_request' && matrix.platform.os != 'macOS-latest' + if: github.event_name == 'pull_request' && matrix.platform.os != 'macos-13' - name: Publish Ryujinx.Headless.SDL2 run: dotnet publish -c "${{ matrix.configuration }}" -r "${{ matrix.platform.name }}" -o ./publish_sdl2_headless -p:Version="${{ env.RYUJINX_BASE_VERSION }}" -p:DebugType=embedded -p:SourceRevisionId="${{ steps.git_short_hash.outputs.result }}" -p:ExtraDefineConstants=DISABLE_UPDATER src/Ryujinx.Headless.SDL2 --self-contained true - if: github.event_name == 'pull_request' && matrix.platform.os != 'macOS-latest' + if: github.event_name == 'pull_request' && matrix.platform.os != 'macos-13' - name: Publish Ryujinx.Gtk3 run: dotnet publish -c "${{ matrix.configuration }}" -r "${{ matrix.platform.name }}" -o ./publish_gtk -p:Version="${{ env.RYUJINX_BASE_VERSION }}" -p:DebugType=embedded -p:SourceRevisionId="${{ steps.git_short_hash.outputs.result }}" -p:ExtraDefineConstants=DISABLE_UPDATER src/Ryujinx.Gtk3 --self-contained true - if: github.event_name == 'pull_request' && matrix.platform.os != 'macOS-latest' + if: github.event_name == 'pull_request' && matrix.platform.os != 'macos-13' - name: Set executable bit run: | @@ -83,21 +83,21 @@ jobs: with: name: ryujinx-${{ matrix.configuration }}-${{ env.RYUJINX_BASE_VERSION }}+${{ steps.git_short_hash.outputs.result }}-${{ matrix.platform.zip_os_name }} path: publish - if: github.event_name == 'pull_request' && matrix.platform.os != 'macOS-latest' + if: github.event_name == 'pull_request' && matrix.platform.os != 'macos-13' - name: Upload Ryujinx.Headless.SDL2 artifact uses: actions/upload-artifact@v4 with: name: sdl2-ryujinx-headless-${{ matrix.configuration }}-${{ env.RYUJINX_BASE_VERSION }}+${{ steps.git_short_hash.outputs.result }}-${{ matrix.platform.zip_os_name }} path: publish_sdl2_headless - if: github.event_name == 'pull_request' && matrix.platform.os != 'macOS-latest' + if: github.event_name == 'pull_request' && matrix.platform.os != 'macos-13' - name: Upload Ryujinx.Gtk3 artifact uses: actions/upload-artifact@v4 with: name: gtk-ryujinx-${{ matrix.configuration }}-${{ env.RYUJINX_BASE_VERSION }}+${{ steps.git_short_hash.outputs.result }}-${{ matrix.platform.zip_os_name }} path: publish_gtk - if: github.event_name == 'pull_request' && matrix.platform.os != 'macOS-latest' + if: github.event_name == 'pull_request' && matrix.platform.os != 'macos-13' build_macos: name: macOS Universal (${{ matrix.configuration }}) diff --git a/Directory.Packages.props b/Directory.Packages.props index c3af18cee..c6cc01f9a 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -8,10 +8,10 @@ - - + + - + @@ -20,7 +20,7 @@ - + @@ -42,11 +42,11 @@ - + - + \ No newline at end of file diff --git a/README.md b/README.md index f2f3cb001..7f2294d31 100644 --- a/README.md +++ b/README.md @@ -36,8 +36,8 @@ ## Compatibility -As of October 2023, Ryujinx has been tested on approximately 4,200 titles; -over 4,150 boot past menus and into gameplay, with roughly 3,500 of those being considered playable. +As of May 2024, Ryujinx has been tested on approximately 4,300 titles; +over 4,100 boot past menus and into gameplay, with roughly 3,550 of those being considered playable. You can check out the compatibility list [here](https://github.com/Ryujinx/Ryujinx-Games-List/issues). diff --git a/Ryujinx.sln.DotSettings b/Ryujinx.sln.DotSettings index 049bdaf69..ed7f3e911 100644 --- a/Ryujinx.sln.DotSettings +++ b/Ryujinx.sln.DotSettings @@ -4,6 +4,8 @@ UseExplicitType UseExplicitType <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="I" Suffix="" Style="AaBb" /></Policy> + <Policy><Descriptor Staticness="Any" AccessRightKinds="Any" Description="Types and namespaces"><ElementKinds><Kind Name="NAMESPACE" /><Kind Name="CLASS" /><Kind Name="STRUCT" /><Kind Name="ENUM" /><Kind Name="DELEGATE" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="I" Suffix="" Style="AaBb" /></Policy></Policy> + True True True True diff --git a/src/ARMeilleure/CodeGen/Arm64/CodeGenContext.cs b/src/ARMeilleure/CodeGen/Arm64/CodeGenContext.cs index 12ebabddd..89b1e9e6b 100644 --- a/src/ARMeilleure/CodeGen/Arm64/CodeGenContext.cs +++ b/src/ARMeilleure/CodeGen/Arm64/CodeGenContext.cs @@ -237,7 +237,7 @@ namespace ARMeilleure.CodeGen.Arm64 long originalPosition = _stream.Position; _stream.Seek(0, SeekOrigin.Begin); - _stream.Read(code, 0, code.Length); + _stream.ReadExactly(code, 0, code.Length); _stream.Seek(originalPosition, SeekOrigin.Begin); RelocInfo relocInfo; diff --git a/src/ARMeilleure/CodeGen/X86/Assembler.cs b/src/ARMeilleure/CodeGen/X86/Assembler.cs index 55bf07248..96f4de049 100644 --- a/src/ARMeilleure/CodeGen/X86/Assembler.cs +++ b/src/ARMeilleure/CodeGen/X86/Assembler.cs @@ -1444,7 +1444,7 @@ namespace ARMeilleure.CodeGen.X86 Span buffer = new byte[jump.JumpPosition - _stream.Position]; - _stream.Read(buffer); + _stream.ReadExactly(buffer); _stream.Seek(ReservedBytesForJump, SeekOrigin.Current); codeStream.Write(buffer); diff --git a/src/ARMeilleure/Translation/PTC/Ptc.cs b/src/ARMeilleure/Translation/PTC/Ptc.cs index 58f065342..f56bdce1c 100644 --- a/src/ARMeilleure/Translation/PTC/Ptc.cs +++ b/src/ARMeilleure/Translation/PTC/Ptc.cs @@ -857,8 +857,14 @@ namespace ARMeilleure.Translation.PTC Stopwatch sw = Stopwatch.StartNew(); - threads.ForEach((thread) => thread.Start()); - threads.ForEach((thread) => thread.Join()); + foreach (var thread in threads) + { + thread.Start(); + } + foreach (var thread in threads) + { + thread.Join(); + } threads.Clear(); diff --git a/src/Ryujinx.Audio.Backends.SDL2/SDL2HardwareDeviceSession.cs b/src/Ryujinx.Audio.Backends.SDL2/SDL2HardwareDeviceSession.cs index 00188ba58..62fe5025d 100644 --- a/src/Ryujinx.Audio.Backends.SDL2/SDL2HardwareDeviceSession.cs +++ b/src/Ryujinx.Audio.Backends.SDL2/SDL2HardwareDeviceSession.cs @@ -1,8 +1,10 @@ using Ryujinx.Audio.Backends.Common; using Ryujinx.Audio.Common; using Ryujinx.Common.Logging; +using Ryujinx.Common.Memory; using Ryujinx.Memory; using System; +using System.Buffers; using System.Collections.Concurrent; using System.Threading; @@ -87,7 +89,9 @@ namespace Ryujinx.Audio.Backends.SDL2 return; } - byte[] samples = new byte[frameCount * _bytesPerFrame]; + using IMemoryOwner samplesOwner = ByteMemoryPool.Rent(frameCount * _bytesPerFrame); + + Span samples = samplesOwner.Memory.Span; _ringBuffer.Read(samples, 0, samples.Length); diff --git a/src/Ryujinx.Audio.Backends.SoundIo/SoundIoHardwareDeviceSession.cs b/src/Ryujinx.Audio.Backends.SoundIo/SoundIoHardwareDeviceSession.cs index f60982e30..4011a1214 100644 --- a/src/Ryujinx.Audio.Backends.SoundIo/SoundIoHardwareDeviceSession.cs +++ b/src/Ryujinx.Audio.Backends.SoundIo/SoundIoHardwareDeviceSession.cs @@ -1,8 +1,10 @@ using Ryujinx.Audio.Backends.Common; using Ryujinx.Audio.Backends.SoundIo.Native; using Ryujinx.Audio.Common; +using Ryujinx.Common.Memory; using Ryujinx.Memory; using System; +using System.Buffers; using System.Collections.Concurrent; using System.Runtime.CompilerServices; using System.Threading; @@ -37,7 +39,7 @@ namespace Ryujinx.Audio.Backends.SoundIo _outputStream = _driver.OpenStream(RequestedSampleFormat, RequestedSampleRate, RequestedChannelCount); _outputStream.WriteCallback += Update; _outputStream.Volume = requestedVolume; - // TODO: Setup other callbacks (errors, ect). + // TODO: Setup other callbacks (errors, etc.) _outputStream.Open(); } @@ -120,7 +122,9 @@ namespace Ryujinx.Audio.Backends.SoundIo int channelCount = areas.Length; - byte[] samples = new byte[frameCount * bytesPerFrame]; + using IMemoryOwner samplesOwner = ByteMemoryPool.Rent(frameCount * bytesPerFrame); + + Span samples = samplesOwner.Memory.Span; _ringBuffer.Read(samples, 0, samples.Length); diff --git a/src/Ryujinx.Audio/Renderer/Common/VoiceUpdateState.cs b/src/Ryujinx.Audio/Renderer/Common/VoiceUpdateState.cs index 608381af1..7f881373f 100644 --- a/src/Ryujinx.Audio/Renderer/Common/VoiceUpdateState.cs +++ b/src/Ryujinx.Audio/Renderer/Common/VoiceUpdateState.cs @@ -15,7 +15,6 @@ namespace Ryujinx.Audio.Renderer.Common { public const int Align = 0x10; public const int BiquadStateOffset = 0x0; - public const int BiquadStateSize = 0x10; /// /// The state of the biquad filters of this voice. diff --git a/src/Ryujinx.Audio/Renderer/Dsp/BiquadFilterHelper.cs b/src/Ryujinx.Audio/Renderer/Dsp/BiquadFilterHelper.cs index 1a51a1fbd..31f614d67 100644 --- a/src/Ryujinx.Audio/Renderer/Dsp/BiquadFilterHelper.cs +++ b/src/Ryujinx.Audio/Renderer/Dsp/BiquadFilterHelper.cs @@ -16,10 +16,15 @@ namespace Ryujinx.Audio.Renderer.Dsp /// The biquad filter parameter /// The biquad filter state /// The output buffer to write the result - /// The input buffer to write the result + /// The input buffer to read the samples from /// The count of samples to process [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void ProcessBiquadFilter(ref BiquadFilterParameter parameter, ref BiquadFilterState state, Span outputBuffer, ReadOnlySpan inputBuffer, uint sampleCount) + public static void ProcessBiquadFilter( + ref BiquadFilterParameter parameter, + ref BiquadFilterState state, + Span outputBuffer, + ReadOnlySpan inputBuffer, + uint sampleCount) { float a0 = FixedPointHelper.ToFloat(parameter.Numerator[0], FixedPointPrecisionForParameter); float a1 = FixedPointHelper.ToFloat(parameter.Numerator[1], FixedPointPrecisionForParameter); @@ -40,6 +45,96 @@ namespace Ryujinx.Audio.Renderer.Dsp } } + /// + /// Apply a single biquad filter and mix the result into the output buffer. + /// + /// This is implemented with a direct form 1. + /// The biquad filter parameter + /// The biquad filter state + /// The output buffer to write the result + /// The input buffer to read the samples from + /// The count of samples to process + /// Mix volume + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ProcessBiquadFilterAndMix( + ref BiquadFilterParameter parameter, + ref BiquadFilterState state, + Span outputBuffer, + ReadOnlySpan inputBuffer, + uint sampleCount, + float volume) + { + float a0 = FixedPointHelper.ToFloat(parameter.Numerator[0], FixedPointPrecisionForParameter); + float a1 = FixedPointHelper.ToFloat(parameter.Numerator[1], FixedPointPrecisionForParameter); + float a2 = FixedPointHelper.ToFloat(parameter.Numerator[2], FixedPointPrecisionForParameter); + + float b1 = FixedPointHelper.ToFloat(parameter.Denominator[0], FixedPointPrecisionForParameter); + float b2 = FixedPointHelper.ToFloat(parameter.Denominator[1], FixedPointPrecisionForParameter); + + for (int i = 0; i < sampleCount; i++) + { + float input = inputBuffer[i]; + float output = input * a0 + state.State0 * a1 + state.State1 * a2 + state.State2 * b1 + state.State3 * b2; + + state.State1 = state.State0; + state.State0 = input; + state.State3 = state.State2; + state.State2 = output; + + outputBuffer[i] += FloatingPointHelper.MultiplyRoundUp(output, volume); + } + } + + /// + /// Apply a single biquad filter and mix the result into the output buffer with volume ramp. + /// + /// This is implemented with a direct form 1. + /// The biquad filter parameter + /// The biquad filter state + /// The output buffer to write the result + /// The input buffer to read the samples from + /// The count of samples to process + /// Initial mix volume + /// Volume increment step + /// Last filtered sample value + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float ProcessBiquadFilterAndMixRamp( + ref BiquadFilterParameter parameter, + ref BiquadFilterState state, + Span outputBuffer, + ReadOnlySpan inputBuffer, + uint sampleCount, + float volume, + float ramp) + { + float a0 = FixedPointHelper.ToFloat(parameter.Numerator[0], FixedPointPrecisionForParameter); + float a1 = FixedPointHelper.ToFloat(parameter.Numerator[1], FixedPointPrecisionForParameter); + float a2 = FixedPointHelper.ToFloat(parameter.Numerator[2], FixedPointPrecisionForParameter); + + float b1 = FixedPointHelper.ToFloat(parameter.Denominator[0], FixedPointPrecisionForParameter); + float b2 = FixedPointHelper.ToFloat(parameter.Denominator[1], FixedPointPrecisionForParameter); + + float mixState = 0f; + + for (int i = 0; i < sampleCount; i++) + { + float input = inputBuffer[i]; + float output = input * a0 + state.State0 * a1 + state.State1 * a2 + state.State2 * b1 + state.State3 * b2; + + state.State1 = state.State0; + state.State0 = input; + state.State3 = state.State2; + state.State2 = output; + + mixState = FloatingPointHelper.MultiplyRoundUp(output, volume); + + outputBuffer[i] += mixState; + volume += ramp; + } + + return mixState; + } + /// /// Apply multiple biquad filter. /// @@ -47,10 +142,15 @@ namespace Ryujinx.Audio.Renderer.Dsp /// The biquad filter parameter /// The biquad filter state /// The output buffer to write the result - /// The input buffer to write the result + /// The input buffer to read the samples from /// The count of samples to process [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void ProcessBiquadFilter(ReadOnlySpan parameters, Span states, Span outputBuffer, ReadOnlySpan inputBuffer, uint sampleCount) + public static void ProcessBiquadFilter( + ReadOnlySpan parameters, + Span states, + Span outputBuffer, + ReadOnlySpan inputBuffer, + uint sampleCount) { for (int stageIndex = 0; stageIndex < parameters.Length; stageIndex++) { @@ -67,7 +167,7 @@ namespace Ryujinx.Audio.Renderer.Dsp for (int i = 0; i < sampleCount; i++) { - float input = inputBuffer[i]; + float input = stageIndex != 0 ? outputBuffer[i] : inputBuffer[i]; float output = input * a0 + state.State0 * a1 + state.State1 * a2 + state.State2 * b1 + state.State3 * b2; state.State1 = state.State0; @@ -79,5 +179,129 @@ namespace Ryujinx.Audio.Renderer.Dsp } } } + + /// + /// Apply double biquad filter and mix the result into the output buffer. + /// + /// This is implemented with a direct form 1. + /// The biquad filter parameter + /// The biquad filter state + /// The output buffer to write the result + /// The input buffer to read the samples from + /// The count of samples to process + /// Mix volume + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ProcessDoubleBiquadFilterAndMix( + ref BiquadFilterParameter parameter0, + ref BiquadFilterParameter parameter1, + ref BiquadFilterState state0, + ref BiquadFilterState state1, + Span outputBuffer, + ReadOnlySpan inputBuffer, + uint sampleCount, + float volume) + { + float a00 = FixedPointHelper.ToFloat(parameter0.Numerator[0], FixedPointPrecisionForParameter); + float a10 = FixedPointHelper.ToFloat(parameter0.Numerator[1], FixedPointPrecisionForParameter); + float a20 = FixedPointHelper.ToFloat(parameter0.Numerator[2], FixedPointPrecisionForParameter); + + float b10 = FixedPointHelper.ToFloat(parameter0.Denominator[0], FixedPointPrecisionForParameter); + float b20 = FixedPointHelper.ToFloat(parameter0.Denominator[1], FixedPointPrecisionForParameter); + + float a01 = FixedPointHelper.ToFloat(parameter1.Numerator[0], FixedPointPrecisionForParameter); + float a11 = FixedPointHelper.ToFloat(parameter1.Numerator[1], FixedPointPrecisionForParameter); + float a21 = FixedPointHelper.ToFloat(parameter1.Numerator[2], FixedPointPrecisionForParameter); + + float b11 = FixedPointHelper.ToFloat(parameter1.Denominator[0], FixedPointPrecisionForParameter); + float b21 = FixedPointHelper.ToFloat(parameter1.Denominator[1], FixedPointPrecisionForParameter); + + for (int i = 0; i < sampleCount; i++) + { + float input = inputBuffer[i]; + float output = input * a00 + state0.State0 * a10 + state0.State1 * a20 + state0.State2 * b10 + state0.State3 * b20; + + state0.State1 = state0.State0; + state0.State0 = input; + state0.State3 = state0.State2; + state0.State2 = output; + + input = output; + output = input * a01 + state1.State0 * a11 + state1.State1 * a21 + state1.State2 * b11 + state1.State3 * b21; + + state1.State1 = state1.State0; + state1.State0 = input; + state1.State3 = state1.State2; + state1.State2 = output; + + outputBuffer[i] += FloatingPointHelper.MultiplyRoundUp(output, volume); + } + } + + /// + /// Apply double biquad filter and mix the result into the output buffer with volume ramp. + /// + /// This is implemented with a direct form 1. + /// The biquad filter parameter + /// The biquad filter state + /// The output buffer to write the result + /// The input buffer to read the samples from + /// The count of samples to process + /// Initial mix volume + /// Volume increment step + /// Last filtered sample value + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float ProcessDoubleBiquadFilterAndMixRamp( + ref BiquadFilterParameter parameter0, + ref BiquadFilterParameter parameter1, + ref BiquadFilterState state0, + ref BiquadFilterState state1, + Span outputBuffer, + ReadOnlySpan inputBuffer, + uint sampleCount, + float volume, + float ramp) + { + float a00 = FixedPointHelper.ToFloat(parameter0.Numerator[0], FixedPointPrecisionForParameter); + float a10 = FixedPointHelper.ToFloat(parameter0.Numerator[1], FixedPointPrecisionForParameter); + float a20 = FixedPointHelper.ToFloat(parameter0.Numerator[2], FixedPointPrecisionForParameter); + + float b10 = FixedPointHelper.ToFloat(parameter0.Denominator[0], FixedPointPrecisionForParameter); + float b20 = FixedPointHelper.ToFloat(parameter0.Denominator[1], FixedPointPrecisionForParameter); + + float a01 = FixedPointHelper.ToFloat(parameter1.Numerator[0], FixedPointPrecisionForParameter); + float a11 = FixedPointHelper.ToFloat(parameter1.Numerator[1], FixedPointPrecisionForParameter); + float a21 = FixedPointHelper.ToFloat(parameter1.Numerator[2], FixedPointPrecisionForParameter); + + float b11 = FixedPointHelper.ToFloat(parameter1.Denominator[0], FixedPointPrecisionForParameter); + float b21 = FixedPointHelper.ToFloat(parameter1.Denominator[1], FixedPointPrecisionForParameter); + + float mixState = 0f; + + for (int i = 0; i < sampleCount; i++) + { + float input = inputBuffer[i]; + float output = input * a00 + state0.State0 * a10 + state0.State1 * a20 + state0.State2 * b10 + state0.State3 * b20; + + state0.State1 = state0.State0; + state0.State0 = input; + state0.State3 = state0.State2; + state0.State2 = output; + + input = output; + output = input * a01 + state1.State0 * a11 + state1.State1 * a21 + state1.State2 * b11 + state1.State3 * b21; + + state1.State1 = state1.State0; + state1.State0 = input; + state1.State3 = state1.State2; + state1.State2 = output; + + mixState = FloatingPointHelper.MultiplyRoundUp(output, volume); + + outputBuffer[i] += mixState; + volume += ramp; + } + + return mixState; + } } } diff --git a/src/Ryujinx.Audio/Renderer/Dsp/Command/BiquadFilterAndMixCommand.cs b/src/Ryujinx.Audio/Renderer/Dsp/Command/BiquadFilterAndMixCommand.cs new file mode 100644 index 000000000..106fc0357 --- /dev/null +++ b/src/Ryujinx.Audio/Renderer/Dsp/Command/BiquadFilterAndMixCommand.cs @@ -0,0 +1,123 @@ +using Ryujinx.Audio.Renderer.Common; +using Ryujinx.Audio.Renderer.Dsp.State; +using Ryujinx.Audio.Renderer.Parameter; +using System; + +namespace Ryujinx.Audio.Renderer.Dsp.Command +{ + public class BiquadFilterAndMixCommand : ICommand + { + public bool Enabled { get; set; } + + public int NodeId { get; } + + public CommandType CommandType => CommandType.BiquadFilterAndMix; + + public uint EstimatedProcessingTime { get; set; } + + public ushort InputBufferIndex { get; } + public ushort OutputBufferIndex { get; } + + private BiquadFilterParameter _parameter; + + public Memory BiquadFilterState { get; } + public Memory PreviousBiquadFilterState { get; } + + public Memory State { get; } + + public int LastSampleIndex { get; } + + public float Volume0 { get; } + public float Volume1 { get; } + + public bool NeedInitialization { get; } + public bool HasVolumeRamp { get; } + public bool IsFirstMixBuffer { get; } + + public BiquadFilterAndMixCommand( + float volume0, + float volume1, + uint inputBufferIndex, + uint outputBufferIndex, + int lastSampleIndex, + Memory state, + ref BiquadFilterParameter filter, + Memory biquadFilterState, + Memory previousBiquadFilterState, + bool needInitialization, + bool hasVolumeRamp, + bool isFirstMixBuffer, + int nodeId) + { + Enabled = true; + NodeId = nodeId; + + InputBufferIndex = (ushort)inputBufferIndex; + OutputBufferIndex = (ushort)outputBufferIndex; + + _parameter = filter; + BiquadFilterState = biquadFilterState; + PreviousBiquadFilterState = previousBiquadFilterState; + + State = state; + LastSampleIndex = lastSampleIndex; + + Volume0 = volume0; + Volume1 = volume1; + + NeedInitialization = needInitialization; + HasVolumeRamp = hasVolumeRamp; + IsFirstMixBuffer = isFirstMixBuffer; + } + + public void Process(CommandList context) + { + ReadOnlySpan inputBuffer = context.GetBuffer(InputBufferIndex); + Span outputBuffer = context.GetBuffer(OutputBufferIndex); + + if (NeedInitialization) + { + // If there is no previous state, initialize to zero. + + BiquadFilterState.Span[0] = new BiquadFilterState(); + } + else if (IsFirstMixBuffer) + { + // This is the first buffer, set previous state to current state. + + PreviousBiquadFilterState.Span[0] = BiquadFilterState.Span[0]; + } + else + { + // Rewind the current state by copying back the previous state. + + BiquadFilterState.Span[0] = PreviousBiquadFilterState.Span[0]; + } + + if (HasVolumeRamp) + { + float volume = Volume0; + float ramp = (Volume1 - Volume0) / (int)context.SampleCount; + + State.Span[0].LastSamples[LastSampleIndex] = BiquadFilterHelper.ProcessBiquadFilterAndMixRamp( + ref _parameter, + ref BiquadFilterState.Span[0], + outputBuffer, + inputBuffer, + context.SampleCount, + volume, + ramp); + } + else + { + BiquadFilterHelper.ProcessBiquadFilterAndMix( + ref _parameter, + ref BiquadFilterState.Span[0], + outputBuffer, + inputBuffer, + context.SampleCount, + Volume1); + } + } + } +} diff --git a/src/Ryujinx.Audio/Renderer/Dsp/Command/CommandType.cs b/src/Ryujinx.Audio/Renderer/Dsp/Command/CommandType.cs index 098a04a04..de5c0ea2c 100644 --- a/src/Ryujinx.Audio/Renderer/Dsp/Command/CommandType.cs +++ b/src/Ryujinx.Audio/Renderer/Dsp/Command/CommandType.cs @@ -30,8 +30,10 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command CopyMixBuffer, LimiterVersion1, LimiterVersion2, - GroupedBiquadFilter, + MultiTapBiquadFilter, CaptureBuffer, Compressor, + BiquadFilterAndMix, + MultiTapBiquadFilterAndMix, } } diff --git a/src/Ryujinx.Audio/Renderer/Dsp/Command/MixRampGroupedCommand.cs b/src/Ryujinx.Audio/Renderer/Dsp/Command/MixRampGroupedCommand.cs index 3c7dd63b2..41ac84c1a 100644 --- a/src/Ryujinx.Audio/Renderer/Dsp/Command/MixRampGroupedCommand.cs +++ b/src/Ryujinx.Audio/Renderer/Dsp/Command/MixRampGroupedCommand.cs @@ -24,7 +24,14 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command public Memory State { get; } - public MixRampGroupedCommand(uint mixBufferCount, uint inputBufferIndex, uint outputBufferIndex, Span volume0, Span volume1, Memory state, int nodeId) + public MixRampGroupedCommand( + uint mixBufferCount, + uint inputBufferIndex, + uint outputBufferIndex, + ReadOnlySpan volume0, + ReadOnlySpan volume1, + Memory state, + int nodeId) { Enabled = true; MixBufferCount = mixBufferCount; @@ -48,7 +55,12 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static float ProcessMixRampGrouped(Span outputBuffer, ReadOnlySpan inputBuffer, float volume0, float volume1, int sampleCount) + private static float ProcessMixRampGrouped( + Span outputBuffer, + ReadOnlySpan inputBuffer, + float volume0, + float volume1, + int sampleCount) { float ramp = (volume1 - volume0) / sampleCount; float volume = volume0; diff --git a/src/Ryujinx.Audio/Renderer/Dsp/Command/MultiTapBiquadFilterAndMixCommand.cs b/src/Ryujinx.Audio/Renderer/Dsp/Command/MultiTapBiquadFilterAndMixCommand.cs new file mode 100644 index 000000000..e359371b4 --- /dev/null +++ b/src/Ryujinx.Audio/Renderer/Dsp/Command/MultiTapBiquadFilterAndMixCommand.cs @@ -0,0 +1,145 @@ +using Ryujinx.Audio.Renderer.Common; +using Ryujinx.Audio.Renderer.Dsp.State; +using Ryujinx.Audio.Renderer.Parameter; +using System; + +namespace Ryujinx.Audio.Renderer.Dsp.Command +{ + public class MultiTapBiquadFilterAndMixCommand : ICommand + { + public bool Enabled { get; set; } + + public int NodeId { get; } + + public CommandType CommandType => CommandType.MultiTapBiquadFilterAndMix; + + public uint EstimatedProcessingTime { get; set; } + + public ushort InputBufferIndex { get; } + public ushort OutputBufferIndex { get; } + + private BiquadFilterParameter _parameter0; + private BiquadFilterParameter _parameter1; + + public Memory BiquadFilterState0 { get; } + public Memory BiquadFilterState1 { get; } + public Memory PreviousBiquadFilterState0 { get; } + public Memory PreviousBiquadFilterState1 { get; } + + public Memory State { get; } + + public int LastSampleIndex { get; } + + public float Volume0 { get; } + public float Volume1 { get; } + + public bool NeedInitialization0 { get; } + public bool NeedInitialization1 { get; } + public bool HasVolumeRamp { get; } + public bool IsFirstMixBuffer { get; } + + public MultiTapBiquadFilterAndMixCommand( + float volume0, + float volume1, + uint inputBufferIndex, + uint outputBufferIndex, + int lastSampleIndex, + Memory state, + ref BiquadFilterParameter filter0, + ref BiquadFilterParameter filter1, + Memory biquadFilterState0, + Memory biquadFilterState1, + Memory previousBiquadFilterState0, + Memory previousBiquadFilterState1, + bool needInitialization0, + bool needInitialization1, + bool hasVolumeRamp, + bool isFirstMixBuffer, + int nodeId) + { + Enabled = true; + NodeId = nodeId; + + InputBufferIndex = (ushort)inputBufferIndex; + OutputBufferIndex = (ushort)outputBufferIndex; + + _parameter0 = filter0; + _parameter1 = filter1; + BiquadFilterState0 = biquadFilterState0; + BiquadFilterState1 = biquadFilterState1; + PreviousBiquadFilterState0 = previousBiquadFilterState0; + PreviousBiquadFilterState1 = previousBiquadFilterState1; + + State = state; + LastSampleIndex = lastSampleIndex; + + Volume0 = volume0; + Volume1 = volume1; + + NeedInitialization0 = needInitialization0; + NeedInitialization1 = needInitialization1; + HasVolumeRamp = hasVolumeRamp; + IsFirstMixBuffer = isFirstMixBuffer; + } + + private void UpdateState(Memory state, Memory previousState, bool needInitialization) + { + if (needInitialization) + { + // If there is no previous state, initialize to zero. + + state.Span[0] = new BiquadFilterState(); + } + else if (IsFirstMixBuffer) + { + // This is the first buffer, set previous state to current state. + + previousState.Span[0] = state.Span[0]; + } + else + { + // Rewind the current state by copying back the previous state. + + state.Span[0] = previousState.Span[0]; + } + } + + public void Process(CommandList context) + { + ReadOnlySpan inputBuffer = context.GetBuffer(InputBufferIndex); + Span outputBuffer = context.GetBuffer(OutputBufferIndex); + + UpdateState(BiquadFilterState0, PreviousBiquadFilterState0, NeedInitialization0); + UpdateState(BiquadFilterState1, PreviousBiquadFilterState1, NeedInitialization1); + + if (HasVolumeRamp) + { + float volume = Volume0; + float ramp = (Volume1 - Volume0) / (int)context.SampleCount; + + State.Span[0].LastSamples[LastSampleIndex] = BiquadFilterHelper.ProcessDoubleBiquadFilterAndMixRamp( + ref _parameter0, + ref _parameter1, + ref BiquadFilterState0.Span[0], + ref BiquadFilterState1.Span[0], + outputBuffer, + inputBuffer, + context.SampleCount, + volume, + ramp); + } + else + { + BiquadFilterHelper.ProcessDoubleBiquadFilterAndMix( + ref _parameter0, + ref _parameter1, + ref BiquadFilterState0.Span[0], + ref BiquadFilterState1.Span[0], + outputBuffer, + inputBuffer, + context.SampleCount, + Volume1); + } + } + } +} diff --git a/src/Ryujinx.Audio/Renderer/Dsp/Command/GroupedBiquadFilterCommand.cs b/src/Ryujinx.Audio/Renderer/Dsp/Command/MultiTapBiquadFilterCommand.cs similarity index 84% rename from src/Ryujinx.Audio/Renderer/Dsp/Command/GroupedBiquadFilterCommand.cs rename to src/Ryujinx.Audio/Renderer/Dsp/Command/MultiTapBiquadFilterCommand.cs index 7af851bdc..e159f8ef7 100644 --- a/src/Ryujinx.Audio/Renderer/Dsp/Command/GroupedBiquadFilterCommand.cs +++ b/src/Ryujinx.Audio/Renderer/Dsp/Command/MultiTapBiquadFilterCommand.cs @@ -4,13 +4,13 @@ using System; namespace Ryujinx.Audio.Renderer.Dsp.Command { - public class GroupedBiquadFilterCommand : ICommand + public class MultiTapBiquadFilterCommand : ICommand { public bool Enabled { get; set; } public int NodeId { get; } - public CommandType CommandType => CommandType.GroupedBiquadFilter; + public CommandType CommandType => CommandType.MultiTapBiquadFilter; public uint EstimatedProcessingTime { get; set; } @@ -20,7 +20,7 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command private readonly int _outputBufferIndex; private readonly bool[] _isInitialized; - public GroupedBiquadFilterCommand(int baseIndex, ReadOnlySpan filters, Memory biquadFilterStateMemory, int inputBufferOffset, int outputBufferOffset, ReadOnlySpan isInitialized, int nodeId) + public MultiTapBiquadFilterCommand(int baseIndex, ReadOnlySpan filters, Memory biquadFilterStateMemory, int inputBufferOffset, int outputBufferOffset, ReadOnlySpan isInitialized, int nodeId) { _parameters = filters.ToArray(); _biquadFilterStates = biquadFilterStateMemory; diff --git a/src/Ryujinx.Audio/Renderer/Dsp/State/BiquadFilterState.cs b/src/Ryujinx.Audio/Renderer/Dsp/State/BiquadFilterState.cs index f9a32b3f9..58a2d9cce 100644 --- a/src/Ryujinx.Audio/Renderer/Dsp/State/BiquadFilterState.cs +++ b/src/Ryujinx.Audio/Renderer/Dsp/State/BiquadFilterState.cs @@ -2,12 +2,16 @@ using System.Runtime.InteropServices; namespace Ryujinx.Audio.Renderer.Dsp.State { - [StructLayout(LayoutKind.Sequential, Pack = 1, Size = 0x10)] + [StructLayout(LayoutKind.Sequential, Pack = 1, Size = 0x20)] public struct BiquadFilterState { public float State0; public float State1; public float State2; public float State3; + public float State4; + public float State5; + public float State6; + public float State7; } } diff --git a/src/Ryujinx.Audio/Renderer/Parameter/ISplitterDestinationInParameter.cs b/src/Ryujinx.Audio/Renderer/Parameter/ISplitterDestinationInParameter.cs new file mode 100644 index 000000000..807232f20 --- /dev/null +++ b/src/Ryujinx.Audio/Renderer/Parameter/ISplitterDestinationInParameter.cs @@ -0,0 +1,43 @@ +using Ryujinx.Common.Memory; +using System; + +namespace Ryujinx.Audio.Renderer.Parameter +{ + /// + /// Generic interface for the splitter destination parameters. + /// + public interface ISplitterDestinationInParameter + { + /// + /// Target splitter destination data id. + /// + int Id { get; } + + /// + /// The mix to output the result of the splitter. + /// + int DestinationId { get; } + + /// + /// Biquad filter parameters. + /// + Array2 BiquadFilters { get; } + + /// + /// Set to true if in use. + /// + bool IsUsed { get; } + + /// + /// Mix buffer volumes. + /// + /// Used when a splitter id is specified in the mix. + Span MixBufferVolume { get; } + + /// + /// Check if the magic is valid. + /// + /// Returns true if the magic is valid. + bool IsMagicValid(); + } +} diff --git a/src/Ryujinx.Audio/Renderer/Parameter/SplitterDestinationInParameter.cs b/src/Ryujinx.Audio/Renderer/Parameter/SplitterDestinationInParameterVersion1.cs similarity index 73% rename from src/Ryujinx.Audio/Renderer/Parameter/SplitterDestinationInParameter.cs rename to src/Ryujinx.Audio/Renderer/Parameter/SplitterDestinationInParameterVersion1.cs index b74b67be0..029c001ea 100644 --- a/src/Ryujinx.Audio/Renderer/Parameter/SplitterDestinationInParameter.cs +++ b/src/Ryujinx.Audio/Renderer/Parameter/SplitterDestinationInParameterVersion1.cs @@ -1,3 +1,4 @@ +using Ryujinx.Common.Memory; using Ryujinx.Common.Utilities; using System; using System.Runtime.InteropServices; @@ -5,10 +6,10 @@ using System.Runtime.InteropServices; namespace Ryujinx.Audio.Renderer.Parameter { /// - /// Input header for a splitter destination update. + /// Input header for a splitter destination version 1 update. /// [StructLayout(LayoutKind.Sequential, Pack = 1)] - public struct SplitterDestinationInParameter + public struct SplitterDestinationInParameterVersion1 : ISplitterDestinationInParameter { /// /// Magic of the input header. @@ -41,7 +42,7 @@ namespace Ryujinx.Audio.Renderer.Parameter /// private unsafe fixed byte _reserved[3]; - [StructLayout(LayoutKind.Sequential, Size = 4 * Constants.MixBufferCountMax, Pack = 1)] + [StructLayout(LayoutKind.Sequential, Size = sizeof(float) * Constants.MixBufferCountMax, Pack = 1)] private struct MixArray { } /// @@ -50,6 +51,14 @@ namespace Ryujinx.Audio.Renderer.Parameter /// Used when a splitter id is specified in the mix. public Span MixBufferVolume => SpanHelpers.AsSpan(ref _mixBufferVolume); + readonly int ISplitterDestinationInParameter.Id => Id; + + readonly int ISplitterDestinationInParameter.DestinationId => DestinationId; + + readonly Array2 ISplitterDestinationInParameter.BiquadFilters => default; + + readonly bool ISplitterDestinationInParameter.IsUsed => IsUsed; + /// /// The expected constant of any input header. /// diff --git a/src/Ryujinx.Audio/Renderer/Parameter/SplitterDestinationInParameterVersion2.cs b/src/Ryujinx.Audio/Renderer/Parameter/SplitterDestinationInParameterVersion2.cs new file mode 100644 index 000000000..312be8b70 --- /dev/null +++ b/src/Ryujinx.Audio/Renderer/Parameter/SplitterDestinationInParameterVersion2.cs @@ -0,0 +1,81 @@ +using Ryujinx.Common.Memory; +using Ryujinx.Common.Utilities; +using System; +using System.Runtime.InteropServices; + +namespace Ryujinx.Audio.Renderer.Parameter +{ + /// + /// Input header for a splitter destination version 2 update. + /// + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct SplitterDestinationInParameterVersion2 : ISplitterDestinationInParameter + { + /// + /// Magic of the input header. + /// + public uint Magic; + + /// + /// Target splitter destination data id. + /// + public int Id; + + /// + /// Mix buffer volumes storage. + /// + private MixArray _mixBufferVolume; + + /// + /// The mix to output the result of the splitter. + /// + public int DestinationId; + + /// + /// Biquad filter parameters. + /// + public Array2 BiquadFilters; + + /// + /// Set to true if in use. + /// + [MarshalAs(UnmanagedType.I1)] + public bool IsUsed; + + /// + /// Reserved/padding. + /// + private unsafe fixed byte _reserved[11]; + + [StructLayout(LayoutKind.Sequential, Size = sizeof(float) * Constants.MixBufferCountMax, Pack = 1)] + private struct MixArray { } + + /// + /// Mix buffer volumes. + /// + /// Used when a splitter id is specified in the mix. + public Span MixBufferVolume => SpanHelpers.AsSpan(ref _mixBufferVolume); + + readonly int ISplitterDestinationInParameter.Id => Id; + + readonly int ISplitterDestinationInParameter.DestinationId => DestinationId; + + readonly Array2 ISplitterDestinationInParameter.BiquadFilters => BiquadFilters; + + readonly bool ISplitterDestinationInParameter.IsUsed => IsUsed; + + /// + /// The expected constant of any input header. + /// + private const uint ValidMagic = 0x44444E53; + + /// + /// Check if the magic is valid. + /// + /// Returns true if the magic is valid. + public readonly bool IsMagicValid() + { + return Magic == ValidMagic; + } + } +} diff --git a/src/Ryujinx.Audio/Renderer/Server/AudioRenderSystem.cs b/src/Ryujinx.Audio/Renderer/Server/AudioRenderSystem.cs index 9b56f5cbd..246889c48 100644 --- a/src/Ryujinx.Audio/Renderer/Server/AudioRenderSystem.cs +++ b/src/Ryujinx.Audio/Renderer/Server/AudioRenderSystem.cs @@ -1,6 +1,7 @@ using Ryujinx.Audio.Integration; using Ryujinx.Audio.Renderer.Common; using Ryujinx.Audio.Renderer.Dsp.Command; +using Ryujinx.Audio.Renderer.Dsp.State; using Ryujinx.Audio.Renderer.Parameter; using Ryujinx.Audio.Renderer.Server.Effect; using Ryujinx.Audio.Renderer.Server.MemoryPool; @@ -173,6 +174,22 @@ namespace Ryujinx.Audio.Renderer.Server return ResultCode.WorkBufferTooSmall; } + Memory splitterBqfStates = Memory.Empty; + + if (_behaviourContext.IsBiquadFilterParameterForSplitterEnabled() && + parameter.SplitterCount > 0 && + parameter.SplitterDestinationCount > 0) + { + splitterBqfStates = workBufferAllocator.Allocate(parameter.SplitterDestinationCount * SplitterContext.BqfStatesPerDestination, 0x10); + + if (splitterBqfStates.IsEmpty) + { + return ResultCode.WorkBufferTooSmall; + } + + splitterBqfStates.Span.Clear(); + } + // Invalidate DSP cache on what was currently allocated with workBuffer. AudioProcessorMemoryManager.InvalidateDspCache(_dspMemoryPoolState.Translate(workBuffer, workBufferAllocator.Offset), workBufferAllocator.Offset); @@ -292,7 +309,7 @@ namespace Ryujinx.Audio.Renderer.Server state = MemoryPoolState.Create(MemoryPoolState.LocationType.Cpu); } - if (!_splitterContext.Initialize(ref _behaviourContext, ref parameter, workBufferAllocator)) + if (!_splitterContext.Initialize(ref _behaviourContext, ref parameter, workBufferAllocator, splitterBqfStates)) { return ResultCode.WorkBufferTooSmall; } @@ -775,6 +792,13 @@ namespace Ryujinx.Audio.Renderer.Server // Splitter size = SplitterContext.GetWorkBufferSize(size, ref behaviourContext, ref parameter); + if (behaviourContext.IsBiquadFilterParameterForSplitterEnabled() && + parameter.SplitterCount > 0 && + parameter.SplitterDestinationCount > 0) + { + size = WorkBufferAllocator.GetTargetSize(size, parameter.SplitterDestinationCount * SplitterContext.BqfStatesPerDestination, 0x10); + } + // DSP Voice size = WorkBufferAllocator.GetTargetSize(size, parameter.VoiceCount, VoiceUpdateState.Align); diff --git a/src/Ryujinx.Audio/Renderer/Server/BehaviourContext.cs b/src/Ryujinx.Audio/Renderer/Server/BehaviourContext.cs index 099d8f561..32c7de6cf 100644 --- a/src/Ryujinx.Audio/Renderer/Server/BehaviourContext.cs +++ b/src/Ryujinx.Audio/Renderer/Server/BehaviourContext.cs @@ -45,7 +45,6 @@ namespace Ryujinx.Audio.Renderer.Server /// was added to supply the count of update done sent to the DSP. /// A new version of the command estimator was added to address timing changes caused by the voice changes. /// Additionally, the rendering limit percent was incremented to 80%. - /// /// /// This was added in system update 6.0.0 public const int Revision5 = 5 << 24; @@ -101,10 +100,18 @@ namespace Ryujinx.Audio.Renderer.Server /// This was added in system update 14.0.0 but some changes were made in 15.0.0 public const int Revision11 = 11 << 24; + /// + /// REV12: + /// Two new commands were added to for biquad filtering and mixing (with optinal volume ramp) on the same command. + /// Splitter destinations can now specify up to two biquad filtering parameters, used for filtering the buffer before mixing. + /// + /// This was added in system update 17.0.0 + public const int Revision12 = 12 << 24; + /// /// Last revision supported by the implementation. /// - public const int LastRevision = Revision11; + public const int LastRevision = Revision12; /// /// Target revision magic supported by the implementation. @@ -212,7 +219,7 @@ namespace Ryujinx.Audio.Renderer.Server /// /// Check if the audio renderer should fix the GC-ADPCM context not being provided to the DSP. /// - /// True if if the audio renderer should fix it. + /// True if the audio renderer should fix it. public bool IsAdpcmLoopContextBugFixed() { return CheckFeatureSupported(UserRevision, BaseRevisionMagic + Revision2); @@ -354,7 +361,7 @@ namespace Ryujinx.Audio.Renderer.Server /// Check if the audio renderer should use an optimized Biquad Filter (Direct Form 1) in case of two biquad filters are defined on a voice. /// /// True if the audio renderer should use the optimization. - public bool IsBiquadFilterGroupedOptimizationSupported() + public bool UseMultiTapBiquadFilterProcessing() { return CheckFeatureSupported(UserRevision, BaseRevisionMagic + Revision10); } @@ -368,6 +375,15 @@ namespace Ryujinx.Audio.Renderer.Server return CheckFeatureSupported(UserRevision, BaseRevisionMagic + Revision11); } + /// + /// Check if the audio renderer should support biquad filter on splitter. + /// + /// True if the audio renderer support biquad filter on splitter + public bool IsBiquadFilterParameterForSplitterEnabled() + { + return CheckFeatureSupported(UserRevision, BaseRevisionMagic + Revision12); + } + /// /// Get the version of the . /// diff --git a/src/Ryujinx.Audio/Renderer/Server/CommandBuffer.cs b/src/Ryujinx.Audio/Renderer/Server/CommandBuffer.cs index f4174a913..702f05462 100644 --- a/src/Ryujinx.Audio/Renderer/Server/CommandBuffer.cs +++ b/src/Ryujinx.Audio/Renderer/Server/CommandBuffer.cs @@ -204,7 +204,7 @@ namespace Ryujinx.Audio.Renderer.Server } /// - /// Create a new . + /// Create a new . /// /// The base index of the input and output buffer. /// The biquad filter parameters. @@ -213,9 +213,9 @@ namespace Ryujinx.Audio.Renderer.Server /// The output buffer offset. /// Set to true if the biquad filter state is initialized. /// The node id associated to this command. - public void GenerateGroupedBiquadFilter(int baseIndex, ReadOnlySpan filters, Memory biquadFilterStatesMemory, int inputBufferOffset, int outputBufferOffset, ReadOnlySpan isInitialized, int nodeId) + public void GenerateMultiTapBiquadFilter(int baseIndex, ReadOnlySpan filters, Memory biquadFilterStatesMemory, int inputBufferOffset, int outputBufferOffset, ReadOnlySpan isInitialized, int nodeId) { - GroupedBiquadFilterCommand command = new(baseIndex, filters, biquadFilterStatesMemory, inputBufferOffset, outputBufferOffset, isInitialized, nodeId); + MultiTapBiquadFilterCommand command = new(baseIndex, filters, biquadFilterStatesMemory, inputBufferOffset, outputBufferOffset, isInitialized, nodeId); command.EstimatedProcessingTime = _commandProcessingTimeEstimator.Estimate(command); @@ -232,7 +232,7 @@ namespace Ryujinx.Audio.Renderer.Server /// The new volume. /// The to generate the command from. /// The node id associated to this command. - public void GenerateMixRampGrouped(uint mixBufferCount, uint inputBufferIndex, uint outputBufferIndex, Span previousVolume, Span volume, Memory state, int nodeId) + public void GenerateMixRampGrouped(uint mixBufferCount, uint inputBufferIndex, uint outputBufferIndex, ReadOnlySpan previousVolume, ReadOnlySpan volume, Memory state, int nodeId) { MixRampGroupedCommand command = new(mixBufferCount, inputBufferIndex, outputBufferIndex, previousVolume, volume, state, nodeId); @@ -260,6 +260,120 @@ namespace Ryujinx.Audio.Renderer.Server AddCommand(command); } + /// + /// Generate a new . + /// + /// The previous volume. + /// The new volume. + /// The input buffer index. + /// The output buffer index. + /// The index in the array to store the ramped sample. + /// The to generate the command from. + /// The biquad filter parameter. + /// The biquad state. + /// The previous biquad state. + /// Set to true if the biquad filter state needs to be initialized. + /// Set to true if the mix has volume ramp, and should be taken into account. + /// Set to true if the buffer is the first mix buffer. + /// The node id associated to this command. + public void GenerateBiquadFilterAndMix( + float previousVolume, + float volume, + uint inputBufferIndex, + uint outputBufferIndex, + int lastSampleIndex, + Memory state, + ref BiquadFilterParameter filter, + Memory biquadFilterState, + Memory previousBiquadFilterState, + bool needInitialization, + bool hasVolumeRamp, + bool isFirstMixBuffer, + int nodeId) + { + BiquadFilterAndMixCommand command = new( + previousVolume, + volume, + inputBufferIndex, + outputBufferIndex, + lastSampleIndex, + state, + ref filter, + biquadFilterState, + previousBiquadFilterState, + needInitialization, + hasVolumeRamp, + isFirstMixBuffer, + nodeId); + + command.EstimatedProcessingTime = _commandProcessingTimeEstimator.Estimate(command); + + AddCommand(command); + } + + /// + /// Generate a new . + /// + /// The previous volume. + /// The new volume. + /// The input buffer index. + /// The output buffer index. + /// The index in the array to store the ramped sample. + /// The to generate the command from. + /// First biquad filter parameter. + /// Second biquad filter parameter. + /// First biquad state. + /// Second biquad state. + /// First previous biquad state. + /// Second previous biquad state. + /// Set to true if the first biquad filter state needs to be initialized. + /// Set to true if the second biquad filter state needs to be initialized. + /// Set to true if the mix has volume ramp, and should be taken into account. + /// Set to true if the buffer is the first mix buffer. + /// The node id associated to this command. + public void GenerateMultiTapBiquadFilterAndMix( + float previousVolume, + float volume, + uint inputBufferIndex, + uint outputBufferIndex, + int lastSampleIndex, + Memory state, + ref BiquadFilterParameter filter0, + ref BiquadFilterParameter filter1, + Memory biquadFilterState0, + Memory biquadFilterState1, + Memory previousBiquadFilterState0, + Memory previousBiquadFilterState1, + bool needInitialization0, + bool needInitialization1, + bool hasVolumeRamp, + bool isFirstMixBuffer, + int nodeId) + { + MultiTapBiquadFilterAndMixCommand command = new( + previousVolume, + volume, + inputBufferIndex, + outputBufferIndex, + lastSampleIndex, + state, + ref filter0, + ref filter1, + biquadFilterState0, + biquadFilterState1, + previousBiquadFilterState0, + previousBiquadFilterState1, + needInitialization0, + needInitialization1, + hasVolumeRamp, + isFirstMixBuffer, + nodeId); + + command.EstimatedProcessingTime = _commandProcessingTimeEstimator.Estimate(command); + + AddCommand(command); + } + /// /// Generate a new . /// @@ -268,7 +382,7 @@ namespace Ryujinx.Audio.Renderer.Server /// The buffer count. /// The node id associated to this command. /// The target sample rate in use. - public void GenerateDepopForMixBuffersCommand(Memory depopBuffer, uint bufferOffset, uint bufferCount, int nodeId, uint sampleRate) + public void GenerateDepopForMixBuffers(Memory depopBuffer, uint bufferOffset, uint bufferCount, int nodeId, uint sampleRate) { DepopForMixBuffersCommand command = new(depopBuffer, bufferOffset, bufferCount, nodeId, sampleRate); diff --git a/src/Ryujinx.Audio/Renderer/Server/CommandGenerator.cs b/src/Ryujinx.Audio/Renderer/Server/CommandGenerator.cs index ae8f699f3..d798230c1 100644 --- a/src/Ryujinx.Audio/Renderer/Server/CommandGenerator.cs +++ b/src/Ryujinx.Audio/Renderer/Server/CommandGenerator.cs @@ -12,6 +12,7 @@ using Ryujinx.Audio.Renderer.Server.Voice; using Ryujinx.Audio.Renderer.Utils; using System; using System.Diagnostics; +using System.Runtime.CompilerServices; namespace Ryujinx.Audio.Renderer.Server { @@ -46,12 +47,13 @@ namespace Ryujinx.Audio.Renderer.Server { ref MixState mix = ref _mixContext.GetState(voiceState.MixId); - _commandBuffer.GenerateDepopPrepare(dspState, - _rendererContext.DepopBuffer, - mix.BufferCount, - mix.BufferOffset, - voiceState.NodeId, - voiceState.WasPlaying); + _commandBuffer.GenerateDepopPrepare( + dspState, + _rendererContext.DepopBuffer, + mix.BufferCount, + mix.BufferOffset, + voiceState.NodeId, + voiceState.WasPlaying); } else if (voiceState.SplitterId != Constants.UnusedSplitterId) { @@ -59,15 +61,13 @@ namespace Ryujinx.Audio.Renderer.Server while (true) { - Span destinationSpan = _splitterContext.GetDestination((int)voiceState.SplitterId, destinationId++); + SplitterDestination destination = _splitterContext.GetDestination((int)voiceState.SplitterId, destinationId++); - if (destinationSpan.IsEmpty) + if (destination.IsNull) { break; } - ref SplitterDestination destination = ref destinationSpan[0]; - if (destination.IsConfigured()) { int mixId = destination.DestinationId; @@ -76,12 +76,13 @@ namespace Ryujinx.Audio.Renderer.Server { ref MixState mix = ref _mixContext.GetState(mixId); - _commandBuffer.GenerateDepopPrepare(dspState, - _rendererContext.DepopBuffer, - mix.BufferCount, - mix.BufferOffset, - voiceState.NodeId, - voiceState.WasPlaying); + _commandBuffer.GenerateDepopPrepare( + dspState, + _rendererContext.DepopBuffer, + mix.BufferCount, + mix.BufferOffset, + voiceState.NodeId, + voiceState.WasPlaying); destination.MarkAsNeedToUpdateInternalState(); } @@ -95,35 +96,39 @@ namespace Ryujinx.Audio.Renderer.Server if (_rendererContext.BehaviourContext.IsWaveBufferVersion2Supported()) { - _commandBuffer.GenerateDataSourceVersion2(ref voiceState, - dspState, - (ushort)_rendererContext.MixBufferCount, - (ushort)channelIndex, - voiceState.NodeId); + _commandBuffer.GenerateDataSourceVersion2( + ref voiceState, + dspState, + (ushort)_rendererContext.MixBufferCount, + (ushort)channelIndex, + voiceState.NodeId); } else { switch (voiceState.SampleFormat) { case SampleFormat.PcmInt16: - _commandBuffer.GeneratePcmInt16DataSourceVersion1(ref voiceState, - dspState, - (ushort)_rendererContext.MixBufferCount, - (ushort)channelIndex, - voiceState.NodeId); + _commandBuffer.GeneratePcmInt16DataSourceVersion1( + ref voiceState, + dspState, + (ushort)_rendererContext.MixBufferCount, + (ushort)channelIndex, + voiceState.NodeId); break; case SampleFormat.PcmFloat: - _commandBuffer.GeneratePcmFloatDataSourceVersion1(ref voiceState, - dspState, - (ushort)_rendererContext.MixBufferCount, - (ushort)channelIndex, - voiceState.NodeId); + _commandBuffer.GeneratePcmFloatDataSourceVersion1( + ref voiceState, + dspState, + (ushort)_rendererContext.MixBufferCount, + (ushort)channelIndex, + voiceState.NodeId); break; case SampleFormat.Adpcm: - _commandBuffer.GenerateAdpcmDataSourceVersion1(ref voiceState, - dspState, - (ushort)_rendererContext.MixBufferCount, - voiceState.NodeId); + _commandBuffer.GenerateAdpcmDataSourceVersion1( + ref voiceState, + dspState, + (ushort)_rendererContext.MixBufferCount, + voiceState.NodeId); break; default: throw new NotImplementedException($"Unsupported data source {voiceState.SampleFormat}"); @@ -134,14 +139,14 @@ namespace Ryujinx.Audio.Renderer.Server private void GenerateBiquadFilterForVoice(ref VoiceState voiceState, Memory state, int baseIndex, int bufferOffset, int nodeId) { - bool supportsOptimizedPath = _rendererContext.BehaviourContext.IsBiquadFilterGroupedOptimizationSupported(); + bool supportsOptimizedPath = _rendererContext.BehaviourContext.UseMultiTapBiquadFilterProcessing(); if (supportsOptimizedPath && voiceState.BiquadFilters[0].Enable && voiceState.BiquadFilters[1].Enable) { - Memory biquadStateRawMemory = SpanMemoryManager.Cast(state)[..(VoiceUpdateState.BiquadStateSize * Constants.VoiceBiquadFilterCount)]; + Memory biquadStateRawMemory = SpanMemoryManager.Cast(state)[..(Unsafe.SizeOf() * Constants.VoiceBiquadFilterCount)]; Memory stateMemory = SpanMemoryManager.Cast(biquadStateRawMemory); - _commandBuffer.GenerateGroupedBiquadFilter(baseIndex, voiceState.BiquadFilters.AsSpan(), stateMemory, bufferOffset, bufferOffset, voiceState.BiquadFilterNeedInitialization, nodeId); + _commandBuffer.GenerateMultiTapBiquadFilter(baseIndex, voiceState.BiquadFilters.AsSpan(), stateMemory, bufferOffset, bufferOffset, voiceState.BiquadFilterNeedInitialization, nodeId); } else { @@ -151,33 +156,134 @@ namespace Ryujinx.Audio.Renderer.Server if (filter.Enable) { - Memory biquadStateRawMemory = SpanMemoryManager.Cast(state)[..(VoiceUpdateState.BiquadStateSize * Constants.VoiceBiquadFilterCount)]; - + Memory biquadStateRawMemory = SpanMemoryManager.Cast(state)[..(Unsafe.SizeOf() * Constants.VoiceBiquadFilterCount)]; Memory stateMemory = SpanMemoryManager.Cast(biquadStateRawMemory); - _commandBuffer.GenerateBiquadFilter(baseIndex, - ref filter, - stateMemory.Slice(i, 1), - bufferOffset, - bufferOffset, - !voiceState.BiquadFilterNeedInitialization[i], - nodeId); + _commandBuffer.GenerateBiquadFilter( + baseIndex, + ref filter, + stateMemory.Slice(i, 1), + bufferOffset, + bufferOffset, + !voiceState.BiquadFilterNeedInitialization[i], + nodeId); } } } } - private void GenerateVoiceMix(Span mixVolumes, Span previousMixVolumes, Memory state, uint bufferOffset, uint bufferCount, uint bufferIndex, int nodeId) + private void GenerateVoiceMixWithSplitter( + SplitterDestination destination, + Memory state, + uint bufferOffset, + uint bufferCount, + uint bufferIndex, + int nodeId) + { + ReadOnlySpan mixVolumes = destination.MixBufferVolume; + ReadOnlySpan previousMixVolumes = destination.PreviousMixBufferVolume; + + ref BiquadFilterParameter bqf0 = ref destination.GetBiquadFilterParameter(0); + ref BiquadFilterParameter bqf1 = ref destination.GetBiquadFilterParameter(1); + + Memory bqfState = _splitterContext.GetBiquadFilterState(destination); + + bool isFirstMixBuffer = true; + + for (int i = 0; i < bufferCount; i++) + { + float previousMixVolume = previousMixVolumes[i]; + float mixVolume = mixVolumes[i]; + + if (mixVolume != 0.0f || previousMixVolume != 0.0f) + { + if (bqf0.Enable && bqf1.Enable) + { + _commandBuffer.GenerateMultiTapBiquadFilterAndMix( + previousMixVolume, + mixVolume, + bufferIndex, + bufferOffset + (uint)i, + i, + state, + ref bqf0, + ref bqf1, + bqfState[..1], + bqfState.Slice(1, 1), + bqfState.Slice(2, 1), + bqfState.Slice(3, 1), + !destination.IsBiquadFilterEnabledPrev(), + !destination.IsBiquadFilterEnabledPrev(), + true, + isFirstMixBuffer, + nodeId); + + destination.UpdateBiquadFilterEnabledPrev(0); + destination.UpdateBiquadFilterEnabledPrev(1); + } + else if (bqf0.Enable) + { + _commandBuffer.GenerateBiquadFilterAndMix( + previousMixVolume, + mixVolume, + bufferIndex, + bufferOffset + (uint)i, + i, + state, + ref bqf0, + bqfState[..1], + bqfState.Slice(1, 1), + !destination.IsBiquadFilterEnabledPrev(), + true, + isFirstMixBuffer, + nodeId); + + destination.UpdateBiquadFilterEnabledPrev(0); + } + else if (bqf1.Enable) + { + _commandBuffer.GenerateBiquadFilterAndMix( + previousMixVolume, + mixVolume, + bufferIndex, + bufferOffset + (uint)i, + i, + state, + ref bqf1, + bqfState[..1], + bqfState.Slice(1, 1), + !destination.IsBiquadFilterEnabledPrev(), + true, + isFirstMixBuffer, + nodeId); + + destination.UpdateBiquadFilterEnabledPrev(1); + } + + isFirstMixBuffer = false; + } + } + } + + private void GenerateVoiceMix( + ReadOnlySpan mixVolumes, + ReadOnlySpan previousMixVolumes, + Memory state, + uint bufferOffset, + uint bufferCount, + uint bufferIndex, + int nodeId) { if (bufferCount > Constants.VoiceChannelCountMax) { - _commandBuffer.GenerateMixRampGrouped(bufferCount, - bufferIndex, - bufferOffset, - previousMixVolumes, - mixVolumes, - state, - nodeId); + _commandBuffer.GenerateMixRampGrouped( + bufferCount, + bufferIndex, + bufferOffset, + previousMixVolumes, + mixVolumes, + state, + nodeId); } else { @@ -188,13 +294,14 @@ namespace Ryujinx.Audio.Renderer.Server if (mixVolume != 0.0f || previousMixVolume != 0.0f) { - _commandBuffer.GenerateMixRamp(previousMixVolume, - mixVolume, - bufferIndex, - bufferOffset + (uint)i, - i, - state, - nodeId); + _commandBuffer.GenerateMixRamp( + previousMixVolume, + mixVolume, + bufferIndex, + bufferOffset + (uint)i, + i, + state, + nodeId); } } } @@ -271,10 +378,11 @@ namespace Ryujinx.Audio.Renderer.Server GeneratePerformance(ref performanceEntry, PerformanceCommand.Type.Start, nodeId); } - _commandBuffer.GenerateVolumeRamp(voiceState.PreviousVolume, - voiceState.Volume, - _rendererContext.MixBufferCount + (uint)channelIndex, - nodeId); + _commandBuffer.GenerateVolumeRamp( + voiceState.PreviousVolume, + voiceState.Volume, + _rendererContext.MixBufferCount + (uint)channelIndex, + nodeId); if (performanceInitialized) { @@ -291,15 +399,13 @@ namespace Ryujinx.Audio.Renderer.Server while (true) { - Span destinationSpan = _splitterContext.GetDestination((int)voiceState.SplitterId, destinationId); + SplitterDestination destination = _splitterContext.GetDestination((int)voiceState.SplitterId, destinationId); - if (destinationSpan.IsEmpty) + if (destination.IsNull) { break; } - ref SplitterDestination destination = ref destinationSpan[0]; - destinationId += (int)channelsCount; if (destination.IsConfigured()) @@ -310,13 +416,27 @@ namespace Ryujinx.Audio.Renderer.Server { ref MixState mix = ref _mixContext.GetState(mixId); - GenerateVoiceMix(destination.MixBufferVolume, - destination.PreviousMixBufferVolume, - dspStateMemory, - mix.BufferOffset, - mix.BufferCount, - _rendererContext.MixBufferCount + (uint)channelIndex, - nodeId); + if (destination.IsBiquadFilterEnabled()) + { + GenerateVoiceMixWithSplitter( + destination, + dspStateMemory, + mix.BufferOffset, + mix.BufferCount, + _rendererContext.MixBufferCount + (uint)channelIndex, + nodeId); + } + else + { + GenerateVoiceMix( + destination.MixBufferVolume, + destination.PreviousMixBufferVolume, + dspStateMemory, + mix.BufferOffset, + mix.BufferCount, + _rendererContext.MixBufferCount + (uint)channelIndex, + nodeId); + } destination.MarkAsNeedToUpdateInternalState(); } @@ -337,13 +457,14 @@ namespace Ryujinx.Audio.Renderer.Server GeneratePerformance(ref performanceEntry, PerformanceCommand.Type.Start, nodeId); } - GenerateVoiceMix(channelResource.Mix.AsSpan(), - channelResource.PreviousMix.AsSpan(), - dspStateMemory, - mix.BufferOffset, - mix.BufferCount, - _rendererContext.MixBufferCount + (uint)channelIndex, - nodeId); + GenerateVoiceMix( + channelResource.Mix.AsSpan(), + channelResource.PreviousMix.AsSpan(), + dspStateMemory, + mix.BufferOffset, + mix.BufferCount, + _rendererContext.MixBufferCount + (uint)channelIndex, + nodeId); if (performanceInitialized) { @@ -409,10 +530,11 @@ namespace Ryujinx.Audio.Renderer.Server { if (effect.Parameter.Volumes[i] != 0.0f) { - _commandBuffer.GenerateMix((uint)bufferOffset + effect.Parameter.Input[i], - (uint)bufferOffset + effect.Parameter.Output[i], - nodeId, - effect.Parameter.Volumes[i]); + _commandBuffer.GenerateMix( + (uint)bufferOffset + effect.Parameter.Input[i], + (uint)bufferOffset + effect.Parameter.Output[i], + nodeId, + effect.Parameter.Volumes[i]); } } } @@ -447,17 +569,18 @@ namespace Ryujinx.Audio.Renderer.Server updateCount = newUpdateCount; } - _commandBuffer.GenerateAuxEffect(bufferOffset, - effect.Parameter.Input[i], - effect.Parameter.Output[i], - ref effect.State, - effect.IsEnabled, - effect.Parameter.BufferStorageSize, - effect.State.SendBufferInfoBase, - effect.State.ReturnBufferInfoBase, - updateCount, - writeOffset, - nodeId); + _commandBuffer.GenerateAuxEffect( + bufferOffset, + effect.Parameter.Input[i], + effect.Parameter.Output[i], + ref effect.State, + effect.IsEnabled, + effect.Parameter.BufferStorageSize, + effect.State.SendBufferInfoBase, + effect.State.ReturnBufferInfoBase, + updateCount, + writeOffset, + nodeId); writeOffset = newUpdateCount; @@ -500,7 +623,7 @@ namespace Ryujinx.Audio.Renderer.Server if (effect.IsEnabled) { bool needInitialization = effect.Parameter.Status == UsageState.Invalid || - (effect.Parameter.Status == UsageState.New && !_rendererContext.BehaviourContext.IsBiquadFilterEffectStateClearBugFixed()); + (effect.Parameter.Status == UsageState.New && !_rendererContext.BehaviourContext.IsBiquadFilterEffectStateClearBugFixed()); BiquadFilterParameter parameter = new() { @@ -512,11 +635,14 @@ namespace Ryujinx.Audio.Renderer.Server for (int i = 0; i < effect.Parameter.ChannelCount; i++) { - _commandBuffer.GenerateBiquadFilter((int)bufferOffset, ref parameter, effect.State.Slice(i, 1), - effect.Parameter.Input[i], - effect.Parameter.Output[i], - needInitialization, - nodeId); + _commandBuffer.GenerateBiquadFilter( + (int)bufferOffset, + ref parameter, + effect.State.Slice(i, 1), + effect.Parameter.Input[i], + effect.Parameter.Output[i], + needInitialization, + nodeId); } } else @@ -591,15 +717,16 @@ namespace Ryujinx.Audio.Renderer.Server updateCount = newUpdateCount; } - _commandBuffer.GenerateCaptureEffect(bufferOffset, - effect.Parameter.Input[i], - effect.State.SendBufferInfo, - effect.IsEnabled, - effect.Parameter.BufferStorageSize, - effect.State.SendBufferInfoBase, - updateCount, - writeOffset, - nodeId); + _commandBuffer.GenerateCaptureEffect( + bufferOffset, + effect.Parameter.Input[i], + effect.State.SendBufferInfo, + effect.IsEnabled, + effect.Parameter.BufferStorageSize, + effect.State.SendBufferInfoBase, + updateCount, + writeOffset, + nodeId); writeOffset = newUpdateCount; @@ -612,11 +739,12 @@ namespace Ryujinx.Audio.Renderer.Server { Debug.Assert(effect.Type == EffectType.Compressor); - _commandBuffer.GenerateCompressorEffect(bufferOffset, - effect.Parameter, - effect.State, - effect.IsEnabled, - nodeId); + _commandBuffer.GenerateCompressorEffect( + bufferOffset, + effect.Parameter, + effect.State, + effect.IsEnabled, + nodeId); } private void GenerateEffect(ref MixState mix, int effectId, BaseEffect effect) @@ -629,8 +757,11 @@ namespace Ryujinx.Audio.Renderer.Server bool performanceInitialized = false; - if (_performanceManager != null && _performanceManager.GetNextEntry(out performanceEntry, effect.GetPerformanceDetailType(), - isFinalMix ? PerformanceEntryType.FinalMix : PerformanceEntryType.SubMix, nodeId)) + if (_performanceManager != null && _performanceManager.GetNextEntry( + out performanceEntry, + effect.GetPerformanceDetailType(), + isFinalMix ? PerformanceEntryType.FinalMix : PerformanceEntryType.SubMix, + nodeId)) { performanceInitialized = true; @@ -706,6 +837,85 @@ namespace Ryujinx.Audio.Renderer.Server } } + private void GenerateMixWithSplitter( + uint inputBufferIndex, + uint outputBufferIndex, + float volume, + SplitterDestination destination, + ref bool isFirstMixBuffer, + int nodeId) + { + ref BiquadFilterParameter bqf0 = ref destination.GetBiquadFilterParameter(0); + ref BiquadFilterParameter bqf1 = ref destination.GetBiquadFilterParameter(1); + + Memory bqfState = _splitterContext.GetBiquadFilterState(destination); + + if (bqf0.Enable && bqf1.Enable) + { + _commandBuffer.GenerateMultiTapBiquadFilterAndMix( + 0f, + volume, + inputBufferIndex, + outputBufferIndex, + 0, + Memory.Empty, + ref bqf0, + ref bqf1, + bqfState[..1], + bqfState.Slice(1, 1), + bqfState.Slice(2, 1), + bqfState.Slice(3, 1), + !destination.IsBiquadFilterEnabledPrev(), + !destination.IsBiquadFilterEnabledPrev(), + false, + isFirstMixBuffer, + nodeId); + + destination.UpdateBiquadFilterEnabledPrev(0); + destination.UpdateBiquadFilterEnabledPrev(1); + } + else if (bqf0.Enable) + { + _commandBuffer.GenerateBiquadFilterAndMix( + 0f, + volume, + inputBufferIndex, + outputBufferIndex, + 0, + Memory.Empty, + ref bqf0, + bqfState[..1], + bqfState.Slice(1, 1), + !destination.IsBiquadFilterEnabledPrev(), + false, + isFirstMixBuffer, + nodeId); + + destination.UpdateBiquadFilterEnabledPrev(0); + } + else if (bqf1.Enable) + { + _commandBuffer.GenerateBiquadFilterAndMix( + 0f, + volume, + inputBufferIndex, + outputBufferIndex, + 0, + Memory.Empty, + ref bqf1, + bqfState[..1], + bqfState.Slice(1, 1), + !destination.IsBiquadFilterEnabledPrev(), + false, + isFirstMixBuffer, + nodeId); + + destination.UpdateBiquadFilterEnabledPrev(1); + } + + isFirstMixBuffer = false; + } + private void GenerateMix(ref MixState mix) { if (mix.HasAnyDestination()) @@ -722,15 +932,13 @@ namespace Ryujinx.Audio.Renderer.Server { int destinationIndex = destinationId++; - Span destinationSpan = _splitterContext.GetDestination((int)mix.DestinationSplitterId, destinationIndex); + SplitterDestination destination = _splitterContext.GetDestination((int)mix.DestinationSplitterId, destinationIndex); - if (destinationSpan.IsEmpty) + if (destination.IsNull) { break; } - ref SplitterDestination destination = ref destinationSpan[0]; - if (destination.IsConfigured()) { int mixId = destination.DestinationId; @@ -741,16 +949,32 @@ namespace Ryujinx.Audio.Renderer.Server uint inputBufferIndex = mix.BufferOffset + ((uint)destinationIndex % mix.BufferCount); + bool isFirstMixBuffer = true; + for (uint bufferDestinationIndex = 0; bufferDestinationIndex < destinationMix.BufferCount; bufferDestinationIndex++) { float volume = mix.Volume * destination.GetMixVolume((int)bufferDestinationIndex); if (volume != 0.0f) { - _commandBuffer.GenerateMix(inputBufferIndex, - destinationMix.BufferOffset + bufferDestinationIndex, - mix.NodeId, - volume); + if (destination.IsBiquadFilterEnabled()) + { + GenerateMixWithSplitter( + inputBufferIndex, + destinationMix.BufferOffset + bufferDestinationIndex, + volume, + destination, + ref isFirstMixBuffer, + mix.NodeId); + } + else + { + _commandBuffer.GenerateMix( + inputBufferIndex, + destinationMix.BufferOffset + bufferDestinationIndex, + mix.NodeId, + volume); + } } } } @@ -770,10 +994,11 @@ namespace Ryujinx.Audio.Renderer.Server if (volume != 0.0f) { - _commandBuffer.GenerateMix(mix.BufferOffset + bufferIndex, - destinationMix.BufferOffset + bufferDestinationIndex, - mix.NodeId, - volume); + _commandBuffer.GenerateMix( + mix.BufferOffset + bufferIndex, + destinationMix.BufferOffset + bufferDestinationIndex, + mix.NodeId, + volume); } } } @@ -783,11 +1008,12 @@ namespace Ryujinx.Audio.Renderer.Server private void GenerateSubMix(ref MixState subMix) { - _commandBuffer.GenerateDepopForMixBuffersCommand(_rendererContext.DepopBuffer, - subMix.BufferOffset, - subMix.BufferCount, - subMix.NodeId, - subMix.SampleRate); + _commandBuffer.GenerateDepopForMixBuffers( + _rendererContext.DepopBuffer, + subMix.BufferOffset, + subMix.BufferCount, + subMix.NodeId, + subMix.SampleRate); GenerateEffects(ref subMix); @@ -847,11 +1073,12 @@ namespace Ryujinx.Audio.Renderer.Server { ref MixState finalMix = ref _mixContext.GetFinalState(); - _commandBuffer.GenerateDepopForMixBuffersCommand(_rendererContext.DepopBuffer, - finalMix.BufferOffset, - finalMix.BufferCount, - finalMix.NodeId, - finalMix.SampleRate); + _commandBuffer.GenerateDepopForMixBuffers( + _rendererContext.DepopBuffer, + finalMix.BufferOffset, + finalMix.BufferCount, + finalMix.NodeId, + finalMix.SampleRate); GenerateEffects(ref finalMix); @@ -882,9 +1109,10 @@ namespace Ryujinx.Audio.Renderer.Server GeneratePerformance(ref performanceEntry, PerformanceCommand.Type.Start, nodeId); } - _commandBuffer.GenerateVolume(finalMix.Volume, - finalMix.BufferOffset + bufferIndex, - nodeId); + _commandBuffer.GenerateVolume( + finalMix.Volume, + finalMix.BufferOffset + bufferIndex, + nodeId); if (performanceSubInitialized) { @@ -938,41 +1166,45 @@ namespace Ryujinx.Audio.Renderer.Server if (useCustomDownMixingCommand) { - _commandBuffer.GenerateDownMixSurroundToStereo(finalMix.BufferOffset, - sink.Parameter.Input.AsSpan(), - sink.Parameter.Input.AsSpan(), - sink.DownMixCoefficients, - Constants.InvalidNodeId); + _commandBuffer.GenerateDownMixSurroundToStereo( + finalMix.BufferOffset, + sink.Parameter.Input.AsSpan(), + sink.Parameter.Input.AsSpan(), + sink.DownMixCoefficients, + Constants.InvalidNodeId); } // NOTE: We do the downmixing at the DSP level as it's easier that way. else if (_rendererContext.ChannelCount == 2 && sink.Parameter.InputCount == 6) { - _commandBuffer.GenerateDownMixSurroundToStereo(finalMix.BufferOffset, - sink.Parameter.Input.AsSpan(), - sink.Parameter.Input.AsSpan(), - Constants.DefaultSurroundToStereoCoefficients, - Constants.InvalidNodeId); + _commandBuffer.GenerateDownMixSurroundToStereo( + finalMix.BufferOffset, + sink.Parameter.Input.AsSpan(), + sink.Parameter.Input.AsSpan(), + Constants.DefaultSurroundToStereoCoefficients, + Constants.InvalidNodeId); } CommandList commandList = _commandBuffer.CommandList; if (sink.UpsamplerState != null) { - _commandBuffer.GenerateUpsample(finalMix.BufferOffset, - sink.UpsamplerState, - sink.Parameter.InputCount, - sink.Parameter.Input.AsSpan(), - commandList.BufferCount, - commandList.SampleCount, - commandList.SampleRate, - Constants.InvalidNodeId); + _commandBuffer.GenerateUpsample( + finalMix.BufferOffset, + sink.UpsamplerState, + sink.Parameter.InputCount, + sink.Parameter.Input.AsSpan(), + commandList.BufferCount, + commandList.SampleCount, + commandList.SampleRate, + Constants.InvalidNodeId); } - _commandBuffer.GenerateDeviceSink(finalMix.BufferOffset, - sink, - _rendererContext.SessionId, - commandList.Buffers, - Constants.InvalidNodeId); + _commandBuffer.GenerateDeviceSink( + finalMix.BufferOffset, + sink, + _rendererContext.SessionId, + commandList.Buffers, + Constants.InvalidNodeId); } private void GenerateSink(BaseSink sink, ref MixState finalMix) diff --git a/src/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion1.cs b/src/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion1.cs index d95e9aa71..cff754b82 100644 --- a/src/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion1.cs +++ b/src/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion1.cs @@ -170,7 +170,7 @@ namespace Ryujinx.Audio.Renderer.Server return 0; } - public uint Estimate(GroupedBiquadFilterCommand command) + public uint Estimate(MultiTapBiquadFilterCommand command) { return 0; } @@ -184,5 +184,15 @@ namespace Ryujinx.Audio.Renderer.Server { return 0; } + + public uint Estimate(BiquadFilterAndMixCommand command) + { + return 0; + } + + public uint Estimate(MultiTapBiquadFilterAndMixCommand command) + { + return 0; + } } } diff --git a/src/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion2.cs b/src/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion2.cs index 929aaf383..ef1326924 100644 --- a/src/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion2.cs +++ b/src/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion2.cs @@ -462,7 +462,7 @@ namespace Ryujinx.Audio.Renderer.Server return 0; } - public uint Estimate(GroupedBiquadFilterCommand command) + public uint Estimate(MultiTapBiquadFilterCommand command) { return 0; } @@ -476,5 +476,15 @@ namespace Ryujinx.Audio.Renderer.Server { return 0; } + + public uint Estimate(BiquadFilterAndMixCommand command) + { + return 0; + } + + public uint Estimate(MultiTapBiquadFilterAndMixCommand command) + { + return 0; + } } } diff --git a/src/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion3.cs b/src/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion3.cs index 8ae4bc059..31a5347b4 100644 --- a/src/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion3.cs +++ b/src/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion3.cs @@ -632,7 +632,7 @@ namespace Ryujinx.Audio.Renderer.Server }; } - public virtual uint Estimate(GroupedBiquadFilterCommand command) + public virtual uint Estimate(MultiTapBiquadFilterCommand command) { return 0; } @@ -646,5 +646,15 @@ namespace Ryujinx.Audio.Renderer.Server { return 0; } + + public virtual uint Estimate(BiquadFilterAndMixCommand command) + { + return 0; + } + + public virtual uint Estimate(MultiTapBiquadFilterAndMixCommand command) + { + return 0; + } } } diff --git a/src/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion4.cs b/src/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion4.cs index 25bc67cd9..fb357120d 100644 --- a/src/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion4.cs +++ b/src/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion4.cs @@ -10,7 +10,7 @@ namespace Ryujinx.Audio.Renderer.Server { public CommandProcessingTimeEstimatorVersion4(uint sampleCount, uint bufferCount) : base(sampleCount, bufferCount) { } - public override uint Estimate(GroupedBiquadFilterCommand command) + public override uint Estimate(MultiTapBiquadFilterCommand command) { Debug.Assert(SampleCount == 160 || SampleCount == 240); diff --git a/src/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion5.cs b/src/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion5.cs index 7135c1c4f..06f135a88 100644 --- a/src/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion5.cs +++ b/src/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion5.cs @@ -210,5 +210,53 @@ namespace Ryujinx.Audio.Renderer.Server _ => throw new NotImplementedException($"{command.Parameter.ChannelCount}"), }; } + + public override uint Estimate(BiquadFilterAndMixCommand command) + { + Debug.Assert(SampleCount == 160 || SampleCount == 240); + + if (command.HasVolumeRamp) + { + if (SampleCount == 160) + { + return 5204; + } + + return 6683; + } + else + { + if (SampleCount == 160) + { + return 3427; + } + + return 4752; + } + } + + public override uint Estimate(MultiTapBiquadFilterAndMixCommand command) + { + Debug.Assert(SampleCount == 160 || SampleCount == 240); + + if (command.HasVolumeRamp) + { + if (SampleCount == 160) + { + return 7939; + } + + return 10669; + } + else + { + if (SampleCount == 160) + { + return 6256; + } + + return 8683; + } + } } } diff --git a/src/Ryujinx.Audio/Renderer/Server/ICommandProcessingTimeEstimator.cs b/src/Ryujinx.Audio/Renderer/Server/ICommandProcessingTimeEstimator.cs index 27b22363a..9c4312ad6 100644 --- a/src/Ryujinx.Audio/Renderer/Server/ICommandProcessingTimeEstimator.cs +++ b/src/Ryujinx.Audio/Renderer/Server/ICommandProcessingTimeEstimator.cs @@ -33,8 +33,10 @@ namespace Ryujinx.Audio.Renderer.Server uint Estimate(UpsampleCommand command); uint Estimate(LimiterCommandVersion1 command); uint Estimate(LimiterCommandVersion2 command); - uint Estimate(GroupedBiquadFilterCommand command); + uint Estimate(MultiTapBiquadFilterCommand command); uint Estimate(CaptureBufferCommand command); uint Estimate(CompressorCommand command); + uint Estimate(BiquadFilterAndMixCommand command); + uint Estimate(MultiTapBiquadFilterAndMixCommand command); } } diff --git a/src/Ryujinx.Audio/Renderer/Server/Mix/MixState.cs b/src/Ryujinx.Audio/Renderer/Server/Mix/MixState.cs index b90574da9..5ba58ea5b 100644 --- a/src/Ryujinx.Audio/Renderer/Server/Mix/MixState.cs +++ b/src/Ryujinx.Audio/Renderer/Server/Mix/MixState.cs @@ -225,11 +225,11 @@ namespace Ryujinx.Audio.Renderer.Server.Mix for (int i = 0; i < splitter.DestinationCount; i++) { - Span destination = splitter.GetData(i); + SplitterDestination destination = splitter.GetData(i); - if (!destination.IsEmpty) + if (!destination.IsNull) { - int destinationMixId = destination[0].DestinationId; + int destinationMixId = destination.DestinationId; if (destinationMixId != UnusedMixId) { diff --git a/src/Ryujinx.Audio/Renderer/Server/Splitter/SplitterContext.cs b/src/Ryujinx.Audio/Renderer/Server/Splitter/SplitterContext.cs index 3efa783c3..a7b82a6bd 100644 --- a/src/Ryujinx.Audio/Renderer/Server/Splitter/SplitterContext.cs +++ b/src/Ryujinx.Audio/Renderer/Server/Splitter/SplitterContext.cs @@ -1,4 +1,5 @@ using Ryujinx.Audio.Renderer.Common; +using Ryujinx.Audio.Renderer.Dsp.State; using Ryujinx.Audio.Renderer.Parameter; using Ryujinx.Audio.Renderer.Utils; using Ryujinx.Common; @@ -15,15 +16,35 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter /// public class SplitterContext { + /// + /// Amount of biquad filter states per splitter destination. + /// + public const int BqfStatesPerDestination = 4; + /// /// Storage for . /// private Memory _splitters; /// - /// Storage for . + /// Storage for . /// - private Memory _splitterDestinations; + private Memory _splitterDestinationsV1; + + /// + /// Storage for . + /// + private Memory _splitterDestinationsV2; + + /// + /// Splitter biquad filtering states. + /// + private Memory _splitterBqfStates; + + /// + /// Version of the splitter context that is being used, currently can be 1 or 2. + /// + public int Version { get; private set; } /// /// If set to true, trust the user destination count in . @@ -36,12 +57,17 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter /// The behaviour context. /// The audio renderer configuration. /// The . + /// Memory to store the biquad filtering state for splitters during processing. /// Return true if the initialization was successful. - public bool Initialize(ref BehaviourContext behaviourContext, ref AudioRendererConfiguration parameter, WorkBufferAllocator workBufferAllocator) + public bool Initialize( + ref BehaviourContext behaviourContext, + ref AudioRendererConfiguration parameter, + WorkBufferAllocator workBufferAllocator, + Memory splitterBqfStates) { if (!behaviourContext.IsSplitterSupported() || parameter.SplitterCount <= 0 || parameter.SplitterDestinationCount <= 0) { - Setup(Memory.Empty, Memory.Empty, false); + Setup(Memory.Empty, Memory.Empty, Memory.Empty, false); return true; } @@ -60,23 +86,62 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter splitter = new SplitterState(splitterId++); } - Memory splitterDestinations = workBufferAllocator.Allocate(parameter.SplitterDestinationCount, - SplitterDestination.Alignment); + Memory splitterDestinationsV1 = Memory.Empty; + Memory splitterDestinationsV2 = Memory.Empty; - if (splitterDestinations.IsEmpty) + if (!behaviourContext.IsBiquadFilterParameterForSplitterEnabled()) { - return false; + Version = 1; + + splitterDestinationsV1 = workBufferAllocator.Allocate(parameter.SplitterDestinationCount, + SplitterDestinationVersion1.Alignment); + + if (splitterDestinationsV1.IsEmpty) + { + return false; + } + + int splitterDestinationId = 0; + foreach (ref SplitterDestinationVersion1 data in splitterDestinationsV1.Span) + { + data = new SplitterDestinationVersion1(splitterDestinationId++); + } } - - int splitterDestinationId = 0; - foreach (ref SplitterDestination data in splitterDestinations.Span) + else { - data = new SplitterDestination(splitterDestinationId++); + Version = 2; + + splitterDestinationsV2 = workBufferAllocator.Allocate(parameter.SplitterDestinationCount, + SplitterDestinationVersion2.Alignment); + + if (splitterDestinationsV2.IsEmpty) + { + return false; + } + + int splitterDestinationId = 0; + foreach (ref SplitterDestinationVersion2 data in splitterDestinationsV2.Span) + { + data = new SplitterDestinationVersion2(splitterDestinationId++); + } + + if (parameter.SplitterDestinationCount > 0) + { + // Official code stores it in the SplitterDestinationVersion2 struct, + // but we don't to avoid using unsafe code. + + splitterBqfStates.Span.Clear(); + _splitterBqfStates = splitterBqfStates; + } + else + { + _splitterBqfStates = Memory.Empty; + } } SplitterState.InitializeSplitters(splitters.Span); - Setup(splitters, splitterDestinations, behaviourContext.IsSplitterBugFixed()); + Setup(splitters, splitterDestinationsV1, splitterDestinationsV2, behaviourContext.IsSplitterBugFixed()); return true; } @@ -93,7 +158,15 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter if (behaviourContext.IsSplitterSupported()) { size = WorkBufferAllocator.GetTargetSize(size, parameter.SplitterCount, SplitterState.Alignment); - size = WorkBufferAllocator.GetTargetSize(size, parameter.SplitterDestinationCount, SplitterDestination.Alignment); + + if (behaviourContext.IsBiquadFilterParameterForSplitterEnabled()) + { + size = WorkBufferAllocator.GetTargetSize(size, parameter.SplitterDestinationCount, SplitterDestinationVersion2.Alignment); + } + else + { + size = WorkBufferAllocator.GetTargetSize(size, parameter.SplitterDestinationCount, SplitterDestinationVersion1.Alignment); + } if (behaviourContext.IsSplitterBugFixed()) { @@ -110,12 +183,18 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter /// Setup the instance. /// /// The storage. - /// The storage. + /// The storage. + /// The storage. /// If set to true, trust the user destination count in . - private void Setup(Memory splitters, Memory splitterDestinations, bool isBugFixed) + private void Setup( + Memory splitters, + Memory splitterDestinationsV1, + Memory splitterDestinationsV2, + bool isBugFixed) { _splitters = splitters; - _splitterDestinations = splitterDestinations; + _splitterDestinationsV1 = splitterDestinationsV1; + _splitterDestinationsV2 = splitterDestinationsV2; IsBugFixed = isBugFixed; } @@ -141,7 +220,9 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter return 0; } - return _splitterDestinations.Length / _splitters.Length; + int length = _splitterDestinationsV2.IsEmpty ? _splitterDestinationsV1.Length : _splitterDestinationsV2.Length; + + return length / _splitters.Length; } /// @@ -178,7 +259,39 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter } /// - /// Update one or multiple from user parameters. + /// Update one splitter destination data from user parameters. + /// + /// The raw data after the splitter header. + /// True if the update was successful, false otherwise + private bool UpdateData(ref SequenceReader input) where T : unmanaged, ISplitterDestinationInParameter + { + ref readonly T parameter = ref input.GetRefOrRefToCopy(out _); + + Debug.Assert(parameter.IsMagicValid()); + + if (parameter.IsMagicValid()) + { + int length = _splitterDestinationsV2.IsEmpty ? _splitterDestinationsV1.Length : _splitterDestinationsV2.Length; + + if (parameter.Id >= 0 && parameter.Id < length) + { + SplitterDestination destination = GetDestination(parameter.Id); + + destination.Update(parameter); + } + + return true; + } + else + { + input.Rewind(Unsafe.SizeOf()); + + return false; + } + } + + /// + /// Update one or multiple splitter destination data from user parameters. /// /// The splitter header. /// The raw data after the splitter header. @@ -186,23 +299,23 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter { for (int i = 0; i < inputHeader.SplitterDestinationCount; i++) { - ref readonly SplitterDestinationInParameter parameter = ref input.GetRefOrRefToCopy(out _); - - Debug.Assert(parameter.IsMagicValid()); - - if (parameter.IsMagicValid()) + if (Version == 1) { - if (parameter.Id >= 0 && parameter.Id < _splitterDestinations.Length) + if (!UpdateData(ref input)) { - ref SplitterDestination destination = ref GetDestination(parameter.Id); - - destination.Update(parameter); + break; + } + } + else if (Version == 2) + { + if (!UpdateData(ref input)) + { + break; } } else { - input.Rewind(Unsafe.SizeOf()); - break; + Debug.Fail($"Invalid splitter context version {Version}."); } } } @@ -214,7 +327,7 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter /// Return true if the update was successful. public bool Update(ref SequenceReader input) { - if (_splitterDestinations.IsEmpty || _splitters.IsEmpty) + if (!UsingSplitter()) { return true; } @@ -251,45 +364,52 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter } /// - /// Get a reference to a at the given . + /// Get a reference to the splitter destination data at the given . /// /// The index to use. - /// A reference to a at the given . - public ref SplitterDestination GetDestination(int id) + /// A reference to the splitter destination data at the given . + public SplitterDestination GetDestination(int id) { - return ref SpanIOHelper.GetFromMemory(_splitterDestinations, id, (uint)_splitterDestinations.Length); + if (_splitterDestinationsV2.IsEmpty) + { + return new SplitterDestination(ref SpanIOHelper.GetFromMemory(_splitterDestinationsV1, id, (uint)_splitterDestinationsV1.Length)); + } + else + { + return new SplitterDestination(ref SpanIOHelper.GetFromMemory(_splitterDestinationsV2, id, (uint)_splitterDestinationsV2.Length)); + } } /// - /// Get a at the given . - /// - /// The index to use. - /// A at the given . - public Memory GetDestinationMemory(int id) - { - return SpanIOHelper.GetMemory(_splitterDestinations, id, (uint)_splitterDestinations.Length); - } - - /// - /// Get a in the at and pass to . + /// Get a in the at and pass to . /// /// The index to use to get the . /// The index of the . - /// A . - public Span GetDestination(int id, int destinationId) + /// A . + public SplitterDestination GetDestination(int id, int destinationId) { ref SplitterState splitter = ref GetState(id); return splitter.GetData(destinationId); } + /// + /// Gets the biquad filter state for a given splitter destination. + /// + /// The splitter destination. + /// Biquad filter state for the specified destination. + public Memory GetBiquadFilterState(SplitterDestination destination) + { + return _splitterBqfStates.Slice(destination.Id * BqfStatesPerDestination, BqfStatesPerDestination); + } + /// /// Return true if the audio renderer has any splitters. /// /// True if the audio renderer has any splitters. public bool UsingSplitter() { - return !_splitters.IsEmpty && !_splitterDestinations.IsEmpty; + return !_splitters.IsEmpty && (!_splitterDestinationsV1.IsEmpty || !_splitterDestinationsV2.IsEmpty); } /// diff --git a/src/Ryujinx.Audio/Renderer/Server/Splitter/SplitterDestination.cs b/src/Ryujinx.Audio/Renderer/Server/Splitter/SplitterDestination.cs index 1faf7921f..36dfa5e41 100644 --- a/src/Ryujinx.Audio/Renderer/Server/Splitter/SplitterDestination.cs +++ b/src/Ryujinx.Audio/Renderer/Server/Splitter/SplitterDestination.cs @@ -1,115 +1,198 @@ using Ryujinx.Audio.Renderer.Parameter; -using Ryujinx.Common.Utilities; using System; using System.Diagnostics; -using System.Runtime.InteropServices; +using System.Runtime.CompilerServices; namespace Ryujinx.Audio.Renderer.Server.Splitter { /// /// Server state for a splitter destination. /// - [StructLayout(LayoutKind.Sequential, Size = 0xE0, Pack = Alignment)] - public struct SplitterDestination + public ref struct SplitterDestination { - public const int Alignment = 0x10; + private ref SplitterDestinationVersion1 _v1; + private ref SplitterDestinationVersion2 _v2; /// - /// The unique id of this . + /// Checks if the splitter destination data reference is null. /// - public int Id; + public bool IsNull => Unsafe.IsNullRef(ref _v1) && Unsafe.IsNullRef(ref _v2); /// - /// The mix to output the result of the splitter. + /// The splitter unique id. /// - public int DestinationId; - - /// - /// Mix buffer volumes storage. - /// - private MixArray _mix; - private MixArray _previousMix; - - /// - /// Pointer to the next linked element. - /// - private unsafe SplitterDestination* _next; - - /// - /// Set to true if in use. - /// - [MarshalAs(UnmanagedType.I1)] - public bool IsUsed; - - /// - /// Set to true if the internal state need to be updated. - /// - [MarshalAs(UnmanagedType.I1)] - public bool NeedToUpdateInternalState; - - [StructLayout(LayoutKind.Sequential, Size = 4 * Constants.MixBufferCountMax, Pack = 1)] - private struct MixArray { } - - /// - /// Mix buffer volumes. - /// - /// Used when a splitter id is specified in the mix. - public Span MixBufferVolume => SpanHelpers.AsSpan(ref _mix); - - /// - /// Previous mix buffer volumes. - /// - /// Used when a splitter id is specified in the mix. - public Span PreviousMixBufferVolume => SpanHelpers.AsSpan(ref _previousMix); - - /// - /// Get the of the next element or if not present. - /// - public readonly Span Next + public int Id { get { - unsafe + if (Unsafe.IsNullRef(ref _v2)) { - return _next != null ? new Span(_next, 1) : Span.Empty; + if (Unsafe.IsNullRef(ref _v1)) + { + return 0; + } + else + { + return _v1.Id; + } + } + else + { + return _v2.Id; } } } /// - /// Create a new . + /// The mix to output the result of the splitter. /// - /// The unique id of this . - public SplitterDestination(int id) : this() + public int DestinationId { - Id = id; - DestinationId = Constants.UnusedMixId; - - ClearVolumes(); + get + { + if (Unsafe.IsNullRef(ref _v2)) + { + if (Unsafe.IsNullRef(ref _v1)) + { + return 0; + } + else + { + return _v1.DestinationId; + } + } + else + { + return _v2.DestinationId; + } + } } /// - /// Update the from user parameter. + /// Mix buffer volumes. + /// + /// Used when a splitter id is specified in the mix. + public Span MixBufferVolume + { + get + { + if (Unsafe.IsNullRef(ref _v2)) + { + if (Unsafe.IsNullRef(ref _v1)) + { + return Span.Empty; + } + else + { + return _v1.MixBufferVolume; + } + } + else + { + return _v2.MixBufferVolume; + } + } + } + + /// + /// Previous mix buffer volumes. + /// + /// Used when a splitter id is specified in the mix. + public Span PreviousMixBufferVolume + { + get + { + if (Unsafe.IsNullRef(ref _v2)) + { + if (Unsafe.IsNullRef(ref _v1)) + { + return Span.Empty; + } + else + { + return _v1.PreviousMixBufferVolume; + } + } + else + { + return _v2.PreviousMixBufferVolume; + } + } + } + + /// + /// Get the of the next element or null if not present. + /// + public readonly SplitterDestination Next + { + get + { + unsafe + { + if (Unsafe.IsNullRef(ref _v2)) + { + if (Unsafe.IsNullRef(ref _v1)) + { + return new SplitterDestination(); + } + else + { + return new SplitterDestination(ref _v1.Next); + } + } + else + { + return new SplitterDestination(ref _v2.Next); + } + } + } + } + + /// + /// Creates a new splitter destination wrapper for the version 1 splitter destination data. + /// + /// Version 1 splitter destination data + public SplitterDestination(ref SplitterDestinationVersion1 v1) + { + _v1 = ref v1; + _v2 = ref Unsafe.NullRef(); + } + + /// + /// Creates a new splitter destination wrapper for the version 2 splitter destination data. + /// + /// Version 2 splitter destination data + public SplitterDestination(ref SplitterDestinationVersion2 v2) + { + + _v1 = ref Unsafe.NullRef(); + _v2 = ref v2; + } + + /// + /// Creates a new splitter destination wrapper for the splitter destination data. + /// + /// Version 1 splitter destination data + /// Version 2 splitter destination data + public unsafe SplitterDestination(SplitterDestinationVersion1* v1, SplitterDestinationVersion2* v2) + { + _v1 = ref Unsafe.AsRef(v1); + _v2 = ref Unsafe.AsRef(v2); + } + + /// + /// Update the splitter destination data from user parameter. /// /// The user parameter. - public void Update(SplitterDestinationInParameter parameter) + public void Update(in T parameter) where T : ISplitterDestinationInParameter { - Debug.Assert(Id == parameter.Id); - - if (parameter.IsMagicValid() && Id == parameter.Id) + if (Unsafe.IsNullRef(ref _v2)) { - DestinationId = parameter.DestinationId; - - parameter.MixBufferVolume.CopyTo(MixBufferVolume); - - if (!IsUsed && parameter.IsUsed) - { - MixBufferVolume.CopyTo(PreviousMixBufferVolume); - - NeedToUpdateInternalState = false; - } - - IsUsed = parameter.IsUsed; + _v1.Update(parameter); + } + else + { + _v2.Update(parameter); } } @@ -118,12 +201,14 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter /// public void UpdateInternalState() { - if (IsUsed && NeedToUpdateInternalState) + if (Unsafe.IsNullRef(ref _v2)) { - MixBufferVolume.CopyTo(PreviousMixBufferVolume); + _v1.UpdateInternalState(); + } + else + { + _v2.UpdateInternalState(); } - - NeedToUpdateInternalState = false; } /// @@ -131,16 +216,23 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter /// public void MarkAsNeedToUpdateInternalState() { - NeedToUpdateInternalState = true; + if (Unsafe.IsNullRef(ref _v2)) + { + _v1.MarkAsNeedToUpdateInternalState(); + } + else + { + _v2.MarkAsNeedToUpdateInternalState(); + } } /// - /// Return true if the is used and has a destination. + /// Return true if the splitter destination is used and has a destination. /// - /// True if the is used and has a destination. + /// True if the splitter destination is used and has a destination. public readonly bool IsConfigured() { - return IsUsed && DestinationId != Constants.UnusedMixId; + return Unsafe.IsNullRef(ref _v2) ? _v1.IsConfigured() : _v2.IsConfigured(); } /// @@ -150,9 +242,17 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter /// The volume for the given destination. public float GetMixVolume(int destinationIndex) { - Debug.Assert(destinationIndex >= 0 && destinationIndex < Constants.MixBufferCountMax); + return Unsafe.IsNullRef(ref _v2) ? _v1.GetMixVolume(destinationIndex) : _v2.GetMixVolume(destinationIndex); + } - return MixBufferVolume[destinationIndex]; + /// + /// Get the previous volume for a given destination. + /// + /// The destination index to use. + /// The volume for the given destination. + public float GetMixVolumePrev(int destinationIndex) + { + return Unsafe.IsNullRef(ref _v2) ? _v1.GetMixVolumePrev(destinationIndex) : _v2.GetMixVolumePrev(destinationIndex); } /// @@ -160,22 +260,33 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter /// public void ClearVolumes() { - MixBufferVolume.Clear(); - PreviousMixBufferVolume.Clear(); + if (Unsafe.IsNullRef(ref _v2)) + { + _v1.ClearVolumes(); + } + else + { + _v2.ClearVolumes(); + } } /// - /// Link the next element to the given . + /// Link the next element to the given splitter destination. /// - /// The given to link. - public void Link(ref SplitterDestination next) + /// The given splitter destination to link. + public void Link(SplitterDestination next) { - unsafe + if (Unsafe.IsNullRef(ref _v2)) { - fixed (SplitterDestination* nextPtr = &next) - { - _next = nextPtr; - } + Debug.Assert(!Unsafe.IsNullRef(ref next._v1)); + + _v1.Link(ref next._v1); + } + else + { + Debug.Assert(!Unsafe.IsNullRef(ref next._v2)); + + _v2.Link(ref next._v2); } } @@ -184,10 +295,74 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter /// public void Unlink() { - unsafe + if (Unsafe.IsNullRef(ref _v2)) { - _next = null; + _v1.Unlink(); } + else + { + _v2.Unlink(); + } + } + + /// + /// Checks if any biquad filter is enabled. + /// + /// True if any biquad filter is enabled. + public bool IsBiquadFilterEnabled() + { + return !Unsafe.IsNullRef(ref _v2) && _v2.IsBiquadFilterEnabled(); + } + + /// + /// Checks if any biquad filter was previously enabled. + /// + /// True if any biquad filter was previously enabled. + public bool IsBiquadFilterEnabledPrev() + { + return !Unsafe.IsNullRef(ref _v2) && _v2.IsBiquadFilterEnabledPrev(); + } + + /// + /// Gets the biquad filter parameters. + /// + /// Biquad filter index (0 or 1). + /// Biquad filter parameters. + public ref BiquadFilterParameter GetBiquadFilterParameter(int index) + { + Debug.Assert(!Unsafe.IsNullRef(ref _v2)); + + return ref _v2.GetBiquadFilterParameter(index); + } + + /// + /// Checks if any biquad filter was previously enabled. + /// + /// Biquad filter index (0 or 1). + public void UpdateBiquadFilterEnabledPrev(int index) + { + if (!Unsafe.IsNullRef(ref _v2)) + { + _v2.UpdateBiquadFilterEnabledPrev(index); + } + } + + /// + /// Get the reference for the version 1 splitter destination data, or null if version 2 is being used or the destination is null. + /// + /// Reference for the version 1 splitter destination data. + public ref SplitterDestinationVersion1 GetV1RefOrNull() + { + return ref _v1; + } + + /// + /// Get the reference for the version 2 splitter destination data, or null if version 1 is being used or the destination is null. + /// + /// Reference for the version 2 splitter destination data. + public ref SplitterDestinationVersion2 GetV2RefOrNull() + { + return ref _v2; } } } diff --git a/src/Ryujinx.Audio/Renderer/Server/Splitter/SplitterDestinationVersion1.cs b/src/Ryujinx.Audio/Renderer/Server/Splitter/SplitterDestinationVersion1.cs new file mode 100644 index 000000000..5d2b8fb0f --- /dev/null +++ b/src/Ryujinx.Audio/Renderer/Server/Splitter/SplitterDestinationVersion1.cs @@ -0,0 +1,206 @@ +using Ryujinx.Audio.Renderer.Parameter; +using Ryujinx.Common.Utilities; +using System; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace Ryujinx.Audio.Renderer.Server.Splitter +{ + /// + /// Server state for a splitter destination (version 1). + /// + [StructLayout(LayoutKind.Sequential, Size = 0xE0, Pack = Alignment)] + public struct SplitterDestinationVersion1 + { + public const int Alignment = 0x10; + + /// + /// The unique id of this . + /// + public int Id; + + /// + /// The mix to output the result of the splitter. + /// + public int DestinationId; + + /// + /// Mix buffer volumes storage. + /// + private MixArray _mix; + private MixArray _previousMix; + + /// + /// Pointer to the next linked element. + /// + private unsafe SplitterDestinationVersion1* _next; + + /// + /// Set to true if in use. + /// + [MarshalAs(UnmanagedType.I1)] + public bool IsUsed; + + /// + /// Set to true if the internal state need to be updated. + /// + [MarshalAs(UnmanagedType.I1)] + public bool NeedToUpdateInternalState; + + [StructLayout(LayoutKind.Sequential, Size = sizeof(float) * Constants.MixBufferCountMax, Pack = 1)] + private struct MixArray { } + + /// + /// Mix buffer volumes. + /// + /// Used when a splitter id is specified in the mix. + public Span MixBufferVolume => SpanHelpers.AsSpan(ref _mix); + + /// + /// Previous mix buffer volumes. + /// + /// Used when a splitter id is specified in the mix. + public Span PreviousMixBufferVolume => SpanHelpers.AsSpan(ref _previousMix); + + /// + /// Get the reference of the next element or null if not present. + /// + public readonly ref SplitterDestinationVersion1 Next + { + get + { + unsafe + { + return ref Unsafe.AsRef(_next); + } + } + } + + /// + /// Create a new . + /// + /// The unique id of this . + public SplitterDestinationVersion1(int id) : this() + { + Id = id; + DestinationId = Constants.UnusedMixId; + + ClearVolumes(); + } + + /// + /// Update the from user parameter. + /// + /// The user parameter. + public void Update(in T parameter) where T : ISplitterDestinationInParameter + { + Debug.Assert(Id == parameter.Id); + + if (parameter.IsMagicValid() && Id == parameter.Id) + { + DestinationId = parameter.DestinationId; + + parameter.MixBufferVolume.CopyTo(MixBufferVolume); + + if (!IsUsed && parameter.IsUsed) + { + MixBufferVolume.CopyTo(PreviousMixBufferVolume); + + NeedToUpdateInternalState = false; + } + + IsUsed = parameter.IsUsed; + } + } + + /// + /// Update the internal state of the instance. + /// + public void UpdateInternalState() + { + if (IsUsed && NeedToUpdateInternalState) + { + MixBufferVolume.CopyTo(PreviousMixBufferVolume); + } + + NeedToUpdateInternalState = false; + } + + /// + /// Set the update internal state marker. + /// + public void MarkAsNeedToUpdateInternalState() + { + NeedToUpdateInternalState = true; + } + + /// + /// Return true if the is used and has a destination. + /// + /// True if the is used and has a destination. + public readonly bool IsConfigured() + { + return IsUsed && DestinationId != Constants.UnusedMixId; + } + + /// + /// Get the volume for a given destination. + /// + /// The destination index to use. + /// The volume for the given destination. + public float GetMixVolume(int destinationIndex) + { + Debug.Assert(destinationIndex >= 0 && destinationIndex < Constants.MixBufferCountMax); + + return MixBufferVolume[destinationIndex]; + } + + /// + /// Get the previous volume for a given destination. + /// + /// The destination index to use. + /// The volume for the given destination. + public float GetMixVolumePrev(int destinationIndex) + { + Debug.Assert(destinationIndex >= 0 && destinationIndex < Constants.MixBufferCountMax); + + return PreviousMixBufferVolume[destinationIndex]; + } + + /// + /// Clear the volumes. + /// + public void ClearVolumes() + { + MixBufferVolume.Clear(); + PreviousMixBufferVolume.Clear(); + } + + /// + /// Link the next element to the given . + /// + /// The given to link. + public void Link(ref SplitterDestinationVersion1 next) + { + unsafe + { + fixed (SplitterDestinationVersion1* nextPtr = &next) + { + _next = nextPtr; + } + } + } + + /// + /// Remove the link to the next element. + /// + public void Unlink() + { + unsafe + { + _next = null; + } + } + } +} diff --git a/src/Ryujinx.Audio/Renderer/Server/Splitter/SplitterDestinationVersion2.cs b/src/Ryujinx.Audio/Renderer/Server/Splitter/SplitterDestinationVersion2.cs new file mode 100644 index 000000000..f9487909d --- /dev/null +++ b/src/Ryujinx.Audio/Renderer/Server/Splitter/SplitterDestinationVersion2.cs @@ -0,0 +1,250 @@ +using Ryujinx.Audio.Renderer.Parameter; +using Ryujinx.Common.Memory; +using Ryujinx.Common.Utilities; +using System; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace Ryujinx.Audio.Renderer.Server.Splitter +{ + /// + /// Server state for a splitter destination (version 2). + /// + [StructLayout(LayoutKind.Sequential, Size = 0x110, Pack = Alignment)] + public struct SplitterDestinationVersion2 + { + public const int Alignment = 0x10; + + /// + /// The unique id of this . + /// + public int Id; + + /// + /// The mix to output the result of the splitter. + /// + public int DestinationId; + + /// + /// Mix buffer volumes storage. + /// + private MixArray _mix; + private MixArray _previousMix; + + /// + /// Pointer to the next linked element. + /// + private unsafe SplitterDestinationVersion2* _next; + + /// + /// Set to true if in use. + /// + [MarshalAs(UnmanagedType.I1)] + public bool IsUsed; + + /// + /// Set to true if the internal state need to be updated. + /// + [MarshalAs(UnmanagedType.I1)] + public bool NeedToUpdateInternalState; + + [StructLayout(LayoutKind.Sequential, Size = sizeof(float) * Constants.MixBufferCountMax, Pack = 1)] + private struct MixArray { } + + /// + /// Mix buffer volumes. + /// + /// Used when a splitter id is specified in the mix. + public Span MixBufferVolume => SpanHelpers.AsSpan(ref _mix); + + /// + /// Previous mix buffer volumes. + /// + /// Used when a splitter id is specified in the mix. + public Span PreviousMixBufferVolume => SpanHelpers.AsSpan(ref _previousMix); + + /// + /// Get the reference of the next element or null if not present. + /// + public readonly ref SplitterDestinationVersion2 Next + { + get + { + unsafe + { + return ref Unsafe.AsRef(_next); + } + } + } + + private Array2 _biquadFilters; + + private Array2 _isPreviousBiquadFilterEnabled; + + /// + /// Create a new . + /// + /// The unique id of this . + public SplitterDestinationVersion2(int id) : this() + { + Id = id; + DestinationId = Constants.UnusedMixId; + + ClearVolumes(); + } + + /// + /// Update the from user parameter. + /// + /// The user parameter. + public void Update(in T parameter) where T : ISplitterDestinationInParameter + { + Debug.Assert(Id == parameter.Id); + + if (parameter.IsMagicValid() && Id == parameter.Id) + { + DestinationId = parameter.DestinationId; + + parameter.MixBufferVolume.CopyTo(MixBufferVolume); + + _biquadFilters = parameter.BiquadFilters; + + if (!IsUsed && parameter.IsUsed) + { + MixBufferVolume.CopyTo(PreviousMixBufferVolume); + + NeedToUpdateInternalState = false; + } + + IsUsed = parameter.IsUsed; + } + } + + /// + /// Update the internal state of the instance. + /// + public void UpdateInternalState() + { + if (IsUsed && NeedToUpdateInternalState) + { + MixBufferVolume.CopyTo(PreviousMixBufferVolume); + } + + NeedToUpdateInternalState = false; + } + + /// + /// Set the update internal state marker. + /// + public void MarkAsNeedToUpdateInternalState() + { + NeedToUpdateInternalState = true; + } + + /// + /// Return true if the is used and has a destination. + /// + /// True if the is used and has a destination. + public readonly bool IsConfigured() + { + return IsUsed && DestinationId != Constants.UnusedMixId; + } + + /// + /// Get the volume for a given destination. + /// + /// The destination index to use. + /// The volume for the given destination. + public float GetMixVolume(int destinationIndex) + { + Debug.Assert(destinationIndex >= 0 && destinationIndex < Constants.MixBufferCountMax); + + return MixBufferVolume[destinationIndex]; + } + + /// + /// Get the previous volume for a given destination. + /// + /// The destination index to use. + /// The volume for the given destination. + public float GetMixVolumePrev(int destinationIndex) + { + Debug.Assert(destinationIndex >= 0 && destinationIndex < Constants.MixBufferCountMax); + + return PreviousMixBufferVolume[destinationIndex]; + } + + /// + /// Clear the volumes. + /// + public void ClearVolumes() + { + MixBufferVolume.Clear(); + PreviousMixBufferVolume.Clear(); + } + + /// + /// Link the next element to the given . + /// + /// The given to link. + public void Link(ref SplitterDestinationVersion2 next) + { + unsafe + { + fixed (SplitterDestinationVersion2* nextPtr = &next) + { + _next = nextPtr; + } + } + } + + /// + /// Remove the link to the next element. + /// + public void Unlink() + { + unsafe + { + _next = null; + } + } + + /// + /// Checks if any biquad filter is enabled. + /// + /// True if any biquad filter is enabled. + public bool IsBiquadFilterEnabled() + { + return _biquadFilters[0].Enable || _biquadFilters[1].Enable; + } + + /// + /// Checks if any biquad filter was previously enabled. + /// + /// True if any biquad filter was previously enabled. + public bool IsBiquadFilterEnabledPrev() + { + return _isPreviousBiquadFilterEnabled[0]; + } + + /// + /// Gets the biquad filter parameters. + /// + /// Biquad filter index (0 or 1). + /// Biquad filter parameters. + public ref BiquadFilterParameter GetBiquadFilterParameter(int index) + { + return ref _biquadFilters[index]; + } + + /// + /// Checks if any biquad filter was previously enabled. + /// + /// Biquad filter index (0 or 1). + public void UpdateBiquadFilterEnabledPrev(int index) + { + _isPreviousBiquadFilterEnabled[index] = _biquadFilters[index].Enable; + } + } +} diff --git a/src/Ryujinx.Audio/Renderer/Server/Splitter/SplitterState.cs b/src/Ryujinx.Audio/Renderer/Server/Splitter/SplitterState.cs index 944f092d2..3e7dce559 100644 --- a/src/Ryujinx.Audio/Renderer/Server/Splitter/SplitterState.cs +++ b/src/Ryujinx.Audio/Renderer/Server/Splitter/SplitterState.cs @@ -15,6 +15,8 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter { public const int Alignment = 0x10; + private delegate void SplitterDestinationAction(SplitterDestination destination, int index); + /// /// The unique id of this . /// @@ -26,7 +28,7 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter public uint SampleRate; /// - /// Count of splitter destinations (). + /// Count of splitter destinations. /// public int DestinationCount; @@ -37,20 +39,25 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter public bool HasNewConnection; /// - /// Linked list of . + /// Linked list of . /// - private unsafe SplitterDestination* _destinationsData; + private unsafe SplitterDestinationVersion1* _destinationDataV1; /// - /// Span to the first element of the linked list of . + /// Linked list of . /// - public readonly Span Destinations + private unsafe SplitterDestinationVersion2* _destinationDataV2; + + /// + /// First element of the linked list of splitter destinations data. + /// + public readonly SplitterDestination Destination { get { unsafe { - return (IntPtr)_destinationsData != IntPtr.Zero ? new Span(_destinationsData, 1) : Span.Empty; + return new SplitterDestination(_destinationDataV1, _destinationDataV2); } } } @@ -64,20 +71,20 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter Id = id; } - public readonly Span GetData(int index) + public readonly SplitterDestination GetData(int index) { int i = 0; - Span result = Destinations; + SplitterDestination result = Destination; while (i < index) { - if (result.IsEmpty) + if (result.IsNull) { break; } - result = result[0].Next; + result = result.Next; i++; } @@ -93,25 +100,25 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter } /// - /// Utility function to apply a given to all . + /// Utility function to apply an action to all . /// /// The action to execute on each elements. - private readonly void ForEachDestination(SpanAction action) + private readonly void ForEachDestination(SplitterDestinationAction action) { - Span temp = Destinations; + SplitterDestination temp = Destination; int i = 0; while (true) { - if (temp.IsEmpty) + if (temp.IsNull) { break; } - Span next = temp[0].Next; + SplitterDestination next = temp.Next; - action.Invoke(temp, i++); + action(temp, i++); temp = next; } @@ -142,9 +149,9 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter { input.ReadLittleEndian(out int destinationId); - Memory destination = context.GetDestinationMemory(destinationId); + SplitterDestination destination = context.GetDestination(destinationId); - SetDestination(ref destination.Span[0]); + SetDestination(destination); DestinationCount = destinationCount; @@ -152,9 +159,9 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter { input.ReadLittleEndian(out destinationId); - Memory nextDestination = context.GetDestinationMemory(destinationId); + SplitterDestination nextDestination = context.GetDestination(destinationId); - destination.Span[0].Link(ref nextDestination.Span[0]); + destination.Link(nextDestination); destination = nextDestination; } } @@ -174,16 +181,21 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter } /// - /// Set the head of the linked list of . + /// Set the head of the linked list of . /// - /// A reference to a . - public void SetDestination(ref SplitterDestination newValue) + /// New destination value. + public void SetDestination(SplitterDestination newValue) { unsafe { - fixed (SplitterDestination* newValuePtr = &newValue) + fixed (SplitterDestinationVersion1* newValuePtr = &newValue.GetV1RefOrNull()) { - _destinationsData = newValuePtr; + _destinationDataV1 = newValuePtr; + } + + fixed (SplitterDestinationVersion2* newValuePtr = &newValue.GetV2RefOrNull()) + { + _destinationDataV2 = newValuePtr; } } } @@ -193,19 +205,20 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter /// public readonly void UpdateInternalState() { - ForEachDestination((destination, _) => destination[0].UpdateInternalState()); + ForEachDestination((destination, _) => destination.UpdateInternalState()); } /// - /// Clear all links from the . + /// Clear all links from the . /// public void ClearLinks() { - ForEachDestination((destination, _) => destination[0].Unlink()); + ForEachDestination((destination, _) => destination.Unlink()); unsafe { - _destinationsData = (SplitterDestination*)IntPtr.Zero; + _destinationDataV1 = null; + _destinationDataV2 = null; } } @@ -219,7 +232,8 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter { unsafe { - splitter._destinationsData = (SplitterDestination*)IntPtr.Zero; + splitter._destinationDataV1 = null; + splitter._destinationDataV2 = null; } splitter.DestinationCount = 0; diff --git a/src/Ryujinx.Common/Configuration/Hid/KeyboardHotkeys.cs b/src/Ryujinx.Common/Configuration/Hid/KeyboardHotkeys.cs index af0c4616a..3ef6ee491 100644 --- a/src/Ryujinx.Common/Configuration/Hid/KeyboardHotkeys.cs +++ b/src/Ryujinx.Common/Configuration/Hid/KeyboardHotkeys.cs @@ -1,7 +1,5 @@ namespace Ryujinx.Common.Configuration.Hid { - // NOTE: Please don't change this to struct. - // This breaks Avalonia's TwoWay binding, which makes us unable to save new KeyboardHotkeys. public class KeyboardHotkeys { public Key ToggleVsync { get; set; } diff --git a/src/Ryujinx.Common/Extensions/SequenceReaderExtensions.cs b/src/Ryujinx.Common/Extensions/SequenceReaderExtensions.cs index 5403c87c0..79b5d743b 100644 --- a/src/Ryujinx.Common/Extensions/SequenceReaderExtensions.cs +++ b/src/Ryujinx.Common/Extensions/SequenceReaderExtensions.cs @@ -44,7 +44,7 @@ namespace Ryujinx.Common.Extensions /// /// DO NOT use after calling this method, as it will only /// contain a value if the value couldn't be referenced directly because it spans multiple segments. - /// To discourage use, it is recommended to to call this method like the following: + /// To discourage use, it is recommended to call this method like the following: /// /// ref readonly MyStruct value = ref sequenceReader.GetRefOrRefToCopy{MyStruct}(out _); /// diff --git a/src/Ryujinx.Common/Memory/MemoryOwner.cs b/src/Ryujinx.Common/Memory/MemoryOwner.cs new file mode 100644 index 000000000..5e567ab8d --- /dev/null +++ b/src/Ryujinx.Common/Memory/MemoryOwner.cs @@ -0,0 +1,140 @@ +#nullable enable +using System; +using System.Buffers; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Threading; + +namespace Ryujinx.Common.Memory +{ + /// + /// An implementation with an embedded length and fast + /// accessor, with memory allocated from . + /// + /// The type of item to store. + public sealed class MemoryOwner : IMemoryOwner + { + private readonly int _length; + private T[]? _array; + + /// + /// Initializes a new instance of the class with the specified parameters. + /// + /// The length of the new memory buffer to use + private MemoryOwner(int length) + { + _length = length; + _array = ArrayPool.Shared.Rent(length); + } + + /// + /// Creates a new instance with the specified length. + /// + /// The length of the new memory buffer to use + /// A instance of the requested length + /// Thrown when is not valid + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static MemoryOwner Rent(int length) => new(length); + + /// + /// Creates a new instance with the specified length and the content cleared. + /// + /// The length of the new memory buffer to use + /// A instance of the requested length and the content cleared + /// Thrown when is not valid + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static MemoryOwner RentCleared(int length) + { + MemoryOwner result = new(length); + + result._array.AsSpan(0, length).Clear(); + + return result; + } + + /// + /// Creates a new instance with the content copied from the specified buffer. + /// + /// The buffer to copy + /// A instance with the same length and content as + public static MemoryOwner RentCopy(ReadOnlySpan buffer) + { + MemoryOwner result = new(buffer.Length); + + buffer.CopyTo(result._array); + + return result; + } + + /// + /// Gets the number of items in the current instance. + /// + public int Length + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _length; + } + + /// + public Memory Memory + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + T[]? array = _array; + + if (array is null) + { + ThrowObjectDisposedException(); + } + + return new(array, 0, _length); + } + } + + /// + /// Gets a wrapping the memory belonging to the current instance. + /// + /// + /// Uses a trick made possible by the .NET 6+ runtime array layout. + /// + public Span Span + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + T[]? array = _array; + + if (array is null) + { + ThrowObjectDisposedException(); + } + + ref T firstElementRef = ref MemoryMarshal.GetArrayDataReference(array); + + return MemoryMarshal.CreateSpan(ref firstElementRef, _length); + } + } + + /// + public void Dispose() + { + T[]? array = Interlocked.Exchange(ref _array, null); + + if (array is not null) + { + ArrayPool.Shared.Return(array); + } + } + + /// + /// Throws an when is . + /// + [DoesNotReturn] + private static void ThrowObjectDisposedException() + { + throw new ObjectDisposedException(nameof(MemoryOwner), "The buffer has already been disposed."); + } + } +} diff --git a/src/Ryujinx.Common/Memory/SpanOwner.cs b/src/Ryujinx.Common/Memory/SpanOwner.cs new file mode 100644 index 000000000..a4b4adf32 --- /dev/null +++ b/src/Ryujinx.Common/Memory/SpanOwner.cs @@ -0,0 +1,114 @@ +using System; +using System.Buffers; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace Ryujinx.Common.Memory +{ + /// + /// A stack-only type that rents a buffer of a specified length from . + /// It does not implement to avoid being boxed, but should still be disposed. This + /// is easy since C# 8, which allows use of C# `using` constructs on any type that has a public Dispose() method. + /// To keep this type simple, fast, and read-only, it does not check or guard against multiple disposals. + /// For all these reasons, all usage should be with a `using` block or statement. + /// + /// The type of item to store. + public readonly ref struct SpanOwner + { + private readonly int _length; + private readonly T[] _array; + + /// + /// Initializes a new instance of the struct with the specified parameters. + /// + /// The length of the new memory buffer to use + private SpanOwner(int length) + { + _length = length; + _array = ArrayPool.Shared.Rent(length); + } + + /// + /// Gets an empty instance. + /// + public static SpanOwner Empty + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => new(0); + } + + /// + /// Creates a new instance with the specified length. + /// + /// The length of the new memory buffer to use + /// A instance of the requested length + /// Thrown when is not valid + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static SpanOwner Rent(int length) => new(length); + + /// + /// Creates a new instance with the length and the content cleared. + /// + /// The length of the new memory buffer to use + /// A instance of the requested length and the content cleared + /// Thrown when is not valid + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static SpanOwner RentCleared(int length) + { + SpanOwner result = new(length); + + result._array.AsSpan(0, length).Clear(); + + return result; + } + + /// + /// Creates a new instance with the content copied from the specified buffer. + /// + /// The buffer to copy + /// A instance with the same length and content as + public static SpanOwner RentCopy(ReadOnlySpan buffer) + { + SpanOwner result = new(buffer.Length); + + buffer.CopyTo(result._array); + + return result; + } + + /// + /// Gets the number of items in the current instance + /// + public int Length + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _length; + } + + /// + /// Gets a wrapping the memory belonging to the current instance. + /// + /// + /// Uses a trick made possible by the .NET 6+ runtime array layout. + /// + public Span Span + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + ref T firstElementRef = ref MemoryMarshal.GetArrayDataReference(_array); + + return MemoryMarshal.CreateSpan(ref firstElementRef, _length); + } + } + + /// + /// Implements the duck-typed method. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Dispose() + { + ArrayPool.Shared.Return(_array); + } + } +} diff --git a/src/Ryujinx.Cpu/Jit/MemoryManagerHostTracked.cs b/src/Ryujinx.Cpu/Jit/MemoryManagerHostTracked.cs index 0acb57be4..663d0aeb1 100644 --- a/src/Ryujinx.Cpu/Jit/MemoryManagerHostTracked.cs +++ b/src/Ryujinx.Cpu/Jit/MemoryManagerHostTracked.cs @@ -85,6 +85,70 @@ namespace Ryujinx.Cpu.Jit _addressSpace = new(Tracking, backingMemory, _nativePageTable, useProtectionMirrors); } + public override ReadOnlySequence GetReadOnlySequence(ulong va, int size, bool tracked = false) + { + if (size == 0) + { + return ReadOnlySequence.Empty; + } + + try + { + if (tracked) + { + SignalMemoryTracking(va, (ulong)size, false); + } + else + { + AssertValidAddressAndSize(va, (ulong)size); + } + + ulong endVa = va + (ulong)size; + int offset = 0; + + BytesReadOnlySequenceSegment first = null, last = null; + + while (va < endVa) + { + (MemoryBlock memory, ulong rangeOffset, ulong copySize) = GetMemoryOffsetAndSize(va, (ulong)(size - offset)); + + Memory physicalMemory = memory.GetMemory(rangeOffset, (int)copySize); + + if (first is null) + { + first = last = new BytesReadOnlySequenceSegment(physicalMemory); + } + else + { + if (last.IsContiguousWith(physicalMemory, out nuint contiguousStart, out int contiguousSize)) + { + Memory contiguousPhysicalMemory = new NativeMemoryManager(contiguousStart, contiguousSize).Memory; + + last.Replace(contiguousPhysicalMemory); + } + else + { + last = last.Append(physicalMemory); + } + } + + va += copySize; + offset += (int)copySize; + } + + return new ReadOnlySequence(first, 0, last, (int)(size - last.RunningIndex)); + } + catch (InvalidMemoryRegionException) + { + if (_invalidAccessHandler == null || !_invalidAccessHandler(va)) + { + throw; + } + + return ReadOnlySequence.Empty; + } + } + /// public void Map(ulong va, ulong pa, ulong size, MemoryMapFlags flags) { diff --git a/src/Ryujinx.Graphics.GAL/BufferAccess.cs b/src/Ryujinx.Graphics.GAL/BufferAccess.cs index faefa5188..1e7736f8f 100644 --- a/src/Ryujinx.Graphics.GAL/BufferAccess.cs +++ b/src/Ryujinx.Graphics.GAL/BufferAccess.cs @@ -6,8 +6,13 @@ namespace Ryujinx.Graphics.GAL public enum BufferAccess { Default = 0, - FlushPersistent = 1 << 0, - Stream = 1 << 1, - SparseCompatible = 1 << 2, + HostMemory = 1, + DeviceMemory = 2, + DeviceMemoryMapped = 3, + + MemoryTypeMask = 0xf, + + Stream = 1 << 4, + SparseCompatible = 1 << 5, } } diff --git a/src/Ryujinx.Graphics.GAL/Capabilities.cs b/src/Ryujinx.Graphics.GAL/Capabilities.cs index dc927eaba..a5c6eb5c8 100644 --- a/src/Ryujinx.Graphics.GAL/Capabilities.cs +++ b/src/Ryujinx.Graphics.GAL/Capabilities.cs @@ -6,6 +6,7 @@ namespace Ryujinx.Graphics.GAL { public readonly TargetApi Api; public readonly string VendorName; + public readonly SystemMemoryType MemoryType; public readonly bool HasFrontFacingBug; public readonly bool HasVectorIndexingBug; @@ -36,6 +37,8 @@ namespace Ryujinx.Graphics.GAL public readonly bool SupportsMismatchingViewFormat; public readonly bool SupportsCubemapView; public readonly bool SupportsNonConstantTextureOffset; + public readonly bool SupportsQuads; + public readonly bool SupportsSeparateSampler; public readonly bool SupportsShaderBallot; public readonly bool SupportsShaderBarrierDivergence; public readonly bool SupportsShaderFloat64; @@ -48,6 +51,13 @@ namespace Ryujinx.Graphics.GAL public readonly bool SupportsIndirectParameters; public readonly bool SupportsDepthClipControl; + public readonly int UniformBufferSetIndex; + public readonly int StorageBufferSetIndex; + public readonly int TextureSetIndex; + public readonly int ImageSetIndex; + public readonly int ExtraSetBaseIndex; + public readonly int MaximumExtraSets; + public readonly uint MaximumUniformBuffersPerStage; public readonly uint MaximumStorageBuffersPerStage; public readonly uint MaximumTexturesPerStage; @@ -64,6 +74,7 @@ namespace Ryujinx.Graphics.GAL public Capabilities( TargetApi api, string vendorName, + SystemMemoryType memoryType, bool hasFrontFacingBug, bool hasVectorIndexingBug, bool needsFragmentOutputSpecialization, @@ -92,6 +103,8 @@ namespace Ryujinx.Graphics.GAL bool supportsMismatchingViewFormat, bool supportsCubemapView, bool supportsNonConstantTextureOffset, + bool supportsQuads, + bool supportsSeparateSampler, bool supportsShaderBallot, bool supportsShaderBarrierDivergence, bool supportsShaderFloat64, @@ -103,6 +116,12 @@ namespace Ryujinx.Graphics.GAL bool supportsViewportSwizzle, bool supportsIndirectParameters, bool supportsDepthClipControl, + int uniformBufferSetIndex, + int storageBufferSetIndex, + int textureSetIndex, + int imageSetIndex, + int extraSetBaseIndex, + int maximumExtraSets, uint maximumUniformBuffersPerStage, uint maximumStorageBuffersPerStage, uint maximumTexturesPerStage, @@ -116,6 +135,7 @@ namespace Ryujinx.Graphics.GAL { Api = api; VendorName = vendorName; + MemoryType = memoryType; HasFrontFacingBug = hasFrontFacingBug; HasVectorIndexingBug = hasVectorIndexingBug; NeedsFragmentOutputSpecialization = needsFragmentOutputSpecialization; @@ -144,6 +164,8 @@ namespace Ryujinx.Graphics.GAL SupportsMismatchingViewFormat = supportsMismatchingViewFormat; SupportsCubemapView = supportsCubemapView; SupportsNonConstantTextureOffset = supportsNonConstantTextureOffset; + SupportsQuads = supportsQuads; + SupportsSeparateSampler = supportsSeparateSampler; SupportsShaderBallot = supportsShaderBallot; SupportsShaderBarrierDivergence = supportsShaderBarrierDivergence; SupportsShaderFloat64 = supportsShaderFloat64; @@ -155,6 +177,12 @@ namespace Ryujinx.Graphics.GAL SupportsViewportSwizzle = supportsViewportSwizzle; SupportsIndirectParameters = supportsIndirectParameters; SupportsDepthClipControl = supportsDepthClipControl; + UniformBufferSetIndex = uniformBufferSetIndex; + StorageBufferSetIndex = storageBufferSetIndex; + TextureSetIndex = textureSetIndex; + ImageSetIndex = imageSetIndex; + ExtraSetBaseIndex = extraSetBaseIndex; + MaximumExtraSets = maximumExtraSets; MaximumUniformBuffersPerStage = maximumUniformBuffersPerStage; MaximumStorageBuffersPerStage = maximumStorageBuffersPerStage; MaximumTexturesPerStage = maximumTexturesPerStage; diff --git a/src/Ryujinx.Graphics.GAL/Format.cs b/src/Ryujinx.Graphics.GAL/Format.cs index bd3b38a9a..17c42d2d4 100644 --- a/src/Ryujinx.Graphics.GAL/Format.cs +++ b/src/Ryujinx.Graphics.GAL/Format.cs @@ -711,5 +711,36 @@ namespace Ryujinx.Graphics.GAL { return format.IsUint() || format.IsSint(); } + + /// + /// Checks if the texture format is a float or sRGB color format. + /// + /// + /// Does not include normalized, compressed or depth formats. + /// Float and sRGB formats do not participate in logical operations. + /// + /// Texture format + /// True if the format is a float or sRGB color format, false otherwise + public static bool IsFloatOrSrgb(this Format format) + { + switch (format) + { + case Format.R8G8B8A8Srgb: + case Format.B8G8R8A8Srgb: + case Format.R16Float: + case Format.R16G16Float: + case Format.R16G16B16Float: + case Format.R16G16B16A16Float: + case Format.R32Float: + case Format.R32G32Float: + case Format.R32G32B32Float: + case Format.R32G32B32A32Float: + case Format.R11G11B10Float: + case Format.R9G9B9E5Float: + return true; + } + + return false; + } } } diff --git a/src/Ryujinx.Graphics.GAL/IPipeline.cs b/src/Ryujinx.Graphics.GAL/IPipeline.cs index 9efb9e3e8..cbf1bc3a2 100644 --- a/src/Ryujinx.Graphics.GAL/IPipeline.cs +++ b/src/Ryujinx.Graphics.GAL/IPipeline.cs @@ -60,6 +60,7 @@ namespace Ryujinx.Graphics.GAL void SetImage(ShaderStage stage, int binding, ITexture texture, Format imageFormat); void SetImageArray(ShaderStage stage, int binding, IImageArray array); + void SetImageArraySeparate(ShaderStage stage, int setIndex, IImageArray array); void SetLineParameters(float width, bool smooth); @@ -91,6 +92,7 @@ namespace Ryujinx.Graphics.GAL void SetTextureAndSampler(ShaderStage stage, int binding, ITexture texture, ISampler sampler); void SetTextureArray(ShaderStage stage, int binding, ITextureArray array); + void SetTextureArraySeparate(ShaderStage stage, int setIndex, ITextureArray array); void SetTransformFeedbackBuffers(ReadOnlySpan buffers); void SetUniformBuffers(ReadOnlySpan buffers); diff --git a/src/Ryujinx.Graphics.GAL/IRenderer.cs b/src/Ryujinx.Graphics.GAL/IRenderer.cs index a3466e396..85d0bd729 100644 --- a/src/Ryujinx.Graphics.GAL/IRenderer.cs +++ b/src/Ryujinx.Graphics.GAL/IRenderer.cs @@ -17,7 +17,6 @@ namespace Ryujinx.Graphics.GAL void BackgroundContextAction(Action action, bool alwaysBackground = false); BufferHandle CreateBuffer(int size, BufferAccess access = BufferAccess.Default); - BufferHandle CreateBuffer(int size, BufferAccess access, BufferHandle storageHint); BufferHandle CreateBuffer(nint pointer, int size); BufferHandle CreateBufferSparse(ReadOnlySpan storageBuffers); diff --git a/src/Ryujinx.Graphics.GAL/Multithreading/CommandHelper.cs b/src/Ryujinx.Graphics.GAL/Multithreading/CommandHelper.cs index fd2919be4..edaae3042 100644 --- a/src/Ryujinx.Graphics.GAL/Multithreading/CommandHelper.cs +++ b/src/Ryujinx.Graphics.GAL/Multithreading/CommandHelper.cs @@ -44,7 +44,6 @@ namespace Ryujinx.Graphics.GAL.Multithreading } Register(CommandType.Action); - Register(CommandType.CreateBuffer); Register(CommandType.CreateBufferAccess); Register(CommandType.CreateBufferSparse); Register(CommandType.CreateHostBuffer); @@ -125,6 +124,7 @@ namespace Ryujinx.Graphics.GAL.Multithreading Register(CommandType.SetUniformBuffers); Register(CommandType.SetImage); Register(CommandType.SetImageArray); + Register(CommandType.SetImageArraySeparate); Register(CommandType.SetIndexBuffer); Register(CommandType.SetLineParameters); Register(CommandType.SetLogicOpState); @@ -142,6 +142,7 @@ namespace Ryujinx.Graphics.GAL.Multithreading Register(CommandType.SetStencilTest); Register(CommandType.SetTextureAndSampler); Register(CommandType.SetTextureArray); + Register(CommandType.SetTextureArraySeparate); Register(CommandType.SetUserClipDistance); Register(CommandType.SetVertexAttribs); Register(CommandType.SetVertexBuffers); diff --git a/src/Ryujinx.Graphics.GAL/Multithreading/CommandType.cs b/src/Ryujinx.Graphics.GAL/Multithreading/CommandType.cs index a5e7336cd..758695352 100644 --- a/src/Ryujinx.Graphics.GAL/Multithreading/CommandType.cs +++ b/src/Ryujinx.Graphics.GAL/Multithreading/CommandType.cs @@ -3,7 +3,6 @@ namespace Ryujinx.Graphics.GAL.Multithreading enum CommandType : byte { Action, - CreateBuffer, CreateBufferAccess, CreateBufferSparse, CreateHostBuffer, @@ -85,6 +84,7 @@ namespace Ryujinx.Graphics.GAL.Multithreading SetUniformBuffers, SetImage, SetImageArray, + SetImageArraySeparate, SetIndexBuffer, SetLineParameters, SetLogicOpState, @@ -102,6 +102,7 @@ namespace Ryujinx.Graphics.GAL.Multithreading SetStencilTest, SetTextureAndSampler, SetTextureArray, + SetTextureArraySeparate, SetUserClipDistance, SetVertexAttribs, SetVertexBuffers, diff --git a/src/Ryujinx.Graphics.GAL/Multithreading/Commands/Renderer/CreateBufferCommand.cs b/src/Ryujinx.Graphics.GAL/Multithreading/Commands/Renderer/CreateBufferCommand.cs deleted file mode 100644 index 60a6e4bf4..000000000 --- a/src/Ryujinx.Graphics.GAL/Multithreading/Commands/Renderer/CreateBufferCommand.cs +++ /dev/null @@ -1,31 +0,0 @@ -namespace Ryujinx.Graphics.GAL.Multithreading.Commands.Renderer -{ - struct CreateBufferCommand : IGALCommand, IGALCommand - { - public readonly CommandType CommandType => CommandType.CreateBuffer; - private BufferHandle _threadedHandle; - private int _size; - private BufferAccess _access; - private BufferHandle _storageHint; - - public void Set(BufferHandle threadedHandle, int size, BufferAccess access, BufferHandle storageHint) - { - _threadedHandle = threadedHandle; - _size = size; - _access = access; - _storageHint = storageHint; - } - - public static void Run(ref CreateBufferCommand command, ThreadedRenderer threaded, IRenderer renderer) - { - BufferHandle hint = BufferHandle.Null; - - if (command._storageHint != BufferHandle.Null) - { - hint = threaded.Buffers.MapBuffer(command._storageHint); - } - - threaded.Buffers.AssignBuffer(command._threadedHandle, renderer.CreateBuffer(command._size, command._access, hint)); - } - } -} diff --git a/src/Ryujinx.Graphics.GAL/Multithreading/Commands/SetImageArraySeparateCommand.cs b/src/Ryujinx.Graphics.GAL/Multithreading/Commands/SetImageArraySeparateCommand.cs new file mode 100644 index 000000000..abeb58a06 --- /dev/null +++ b/src/Ryujinx.Graphics.GAL/Multithreading/Commands/SetImageArraySeparateCommand.cs @@ -0,0 +1,26 @@ +using Ryujinx.Graphics.GAL.Multithreading.Model; +using Ryujinx.Graphics.GAL.Multithreading.Resources; +using Ryujinx.Graphics.Shader; + +namespace Ryujinx.Graphics.GAL.Multithreading.Commands +{ + struct SetImageArraySeparateCommand : IGALCommand, IGALCommand + { + public readonly CommandType CommandType => CommandType.SetImageArraySeparate; + private ShaderStage _stage; + private int _setIndex; + private TableRef _array; + + public void Set(ShaderStage stage, int setIndex, TableRef array) + { + _stage = stage; + _setIndex = setIndex; + _array = array; + } + + public static void Run(ref SetImageArraySeparateCommand command, ThreadedRenderer threaded, IRenderer renderer) + { + renderer.Pipeline.SetImageArraySeparate(command._stage, command._setIndex, command._array.GetAs(threaded)?.Base); + } + } +} diff --git a/src/Ryujinx.Graphics.GAL/Multithreading/Commands/SetTextureArraySeparateCommand.cs b/src/Ryujinx.Graphics.GAL/Multithreading/Commands/SetTextureArraySeparateCommand.cs new file mode 100644 index 000000000..b179f2e70 --- /dev/null +++ b/src/Ryujinx.Graphics.GAL/Multithreading/Commands/SetTextureArraySeparateCommand.cs @@ -0,0 +1,26 @@ +using Ryujinx.Graphics.GAL.Multithreading.Model; +using Ryujinx.Graphics.GAL.Multithreading.Resources; +using Ryujinx.Graphics.Shader; + +namespace Ryujinx.Graphics.GAL.Multithreading.Commands +{ + struct SetTextureArraySeparateCommand : IGALCommand, IGALCommand + { + public readonly CommandType CommandType => CommandType.SetTextureArraySeparate; + private ShaderStage _stage; + private int _setIndex; + private TableRef _array; + + public void Set(ShaderStage stage, int setIndex, TableRef array) + { + _stage = stage; + _setIndex = setIndex; + _array = array; + } + + public static void Run(ref SetTextureArraySeparateCommand command, ThreadedRenderer threaded, IRenderer renderer) + { + renderer.Pipeline.SetTextureArraySeparate(command._stage, command._setIndex, command._array.GetAs(threaded)?.Base); + } + } +} diff --git a/src/Ryujinx.Graphics.GAL/Multithreading/ThreadedPipeline.cs b/src/Ryujinx.Graphics.GAL/Multithreading/ThreadedPipeline.cs index 697894eb5..edd79d8a0 100644 --- a/src/Ryujinx.Graphics.GAL/Multithreading/ThreadedPipeline.cs +++ b/src/Ryujinx.Graphics.GAL/Multithreading/ThreadedPipeline.cs @@ -189,6 +189,12 @@ namespace Ryujinx.Graphics.GAL.Multithreading _renderer.QueueCommand(); } + public void SetImageArraySeparate(ShaderStage stage, int setIndex, IImageArray array) + { + _renderer.New().Set(stage, setIndex, Ref(array)); + _renderer.QueueCommand(); + } + public void SetIndexBuffer(BufferRange buffer, IndexType type) { _renderer.New().Set(buffer, type); @@ -297,6 +303,12 @@ namespace Ryujinx.Graphics.GAL.Multithreading _renderer.QueueCommand(); } + public void SetTextureArraySeparate(ShaderStage stage, int setIndex, ITextureArray array) + { + _renderer.New().Set(stage, setIndex, Ref(array)); + _renderer.QueueCommand(); + } + public void SetTransformFeedbackBuffers(ReadOnlySpan buffers) { _renderer.New().Set(_renderer.CopySpan(buffers)); diff --git a/src/Ryujinx.Graphics.GAL/Multithreading/ThreadedRenderer.cs b/src/Ryujinx.Graphics.GAL/Multithreading/ThreadedRenderer.cs index 5e17bcd2c..cc3d2e5c1 100644 --- a/src/Ryujinx.Graphics.GAL/Multithreading/ThreadedRenderer.cs +++ b/src/Ryujinx.Graphics.GAL/Multithreading/ThreadedRenderer.cs @@ -272,15 +272,6 @@ namespace Ryujinx.Graphics.GAL.Multithreading return handle; } - public BufferHandle CreateBuffer(int size, BufferAccess access, BufferHandle storageHint) - { - BufferHandle handle = Buffers.CreateBufferHandle(); - New().Set(handle, size, access, storageHint); - QueueCommand(); - - return handle; - } - public BufferHandle CreateBuffer(nint pointer, int size) { BufferHandle handle = Buffers.CreateBufferHandle(); diff --git a/src/Ryujinx.Graphics.GAL/SystemMemoryType.cs b/src/Ryujinx.Graphics.GAL/SystemMemoryType.cs new file mode 100644 index 000000000..532921298 --- /dev/null +++ b/src/Ryujinx.Graphics.GAL/SystemMemoryType.cs @@ -0,0 +1,29 @@ +namespace Ryujinx.Graphics.GAL +{ + public enum SystemMemoryType + { + /// + /// The backend manages the ownership of memory. This mode never supports host imported memory. + /// + BackendManaged, + + /// + /// Device memory has similar performance to host memory, usually because it's shared between CPU/GPU. + /// Use host memory whenever possible. + /// + UnifiedMemory, + + /// + /// GPU storage to host memory goes though a slow interconnect, but it would still be preferable to use it if the data is flushed back often. + /// Assumes constant buffer access to host memory is rather fast. + /// + DedicatedMemory, + + /// + /// GPU storage to host memory goes though a slow interconnect, that is very slow when doing access from storage. + /// When frequently accessed, copy buffers to host memory using DMA. + /// Assumes constant buffer access to host memory is rather fast. + /// + DedicatedMemorySlowStorage + } +} diff --git a/src/Ryujinx.Graphics.Gpu/Engine/Compute/ComputeClass.cs b/src/Ryujinx.Graphics.Gpu/Engine/Compute/ComputeClass.cs index ccdbe4748..cd8144724 100644 --- a/src/Ryujinx.Graphics.Gpu/Engine/Compute/ComputeClass.cs +++ b/src/Ryujinx.Graphics.Gpu/Engine/Compute/ComputeClass.cs @@ -126,6 +126,8 @@ namespace Ryujinx.Graphics.Gpu.Engine.Compute ulong samplerPoolGpuVa = ((ulong)_state.State.SetTexSamplerPoolAOffsetUpper << 32) | _state.State.SetTexSamplerPoolB; ulong texturePoolGpuVa = ((ulong)_state.State.SetTexHeaderPoolAOffsetUpper << 32) | _state.State.SetTexHeaderPoolB; + int samplerPoolMaximumId = _state.State.SetTexSamplerPoolCMaximumIndex; + GpuChannelPoolState poolState = new( texturePoolGpuVa, _state.State.SetTexHeaderPoolCMaximumIndex, @@ -139,7 +141,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Compute sharedMemorySize, _channel.BufferManager.HasUnalignedStorageBuffers); - CachedShaderProgram cs = memoryManager.Physical.ShaderCache.GetComputeShader(_channel, poolState, computeState, shaderGpuVa); + CachedShaderProgram cs = memoryManager.Physical.ShaderCache.GetComputeShader(_channel, samplerPoolMaximumId, poolState, computeState, shaderGpuVa); _context.Renderer.Pipeline.SetProgram(cs.HostProgram); @@ -184,7 +186,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Compute sharedMemorySize, _channel.BufferManager.HasUnalignedStorageBuffers); - cs = memoryManager.Physical.ShaderCache.GetComputeShader(_channel, poolState, computeState, shaderGpuVa); + cs = memoryManager.Physical.ShaderCache.GetComputeShader(_channel, samplerPoolMaximumId, poolState, computeState, shaderGpuVa); _context.Renderer.Pipeline.SetProgram(cs.HostProgram); } diff --git a/src/Ryujinx.Graphics.Gpu/Engine/GPFifo/GPFifoClass.cs b/src/Ryujinx.Graphics.Gpu/Engine/GPFifo/GPFifoClass.cs index 5bd8ec728..cedd824a1 100644 --- a/src/Ryujinx.Graphics.Gpu/Engine/GPFifo/GPFifoClass.cs +++ b/src/Ryujinx.Graphics.Gpu/Engine/GPFifo/GPFifoClass.cs @@ -157,6 +157,9 @@ namespace Ryujinx.Graphics.Gpu.Engine.GPFifo } else if (operation == SyncpointbOperation.Incr) { + // "Unbind" render targets since a syncpoint increment might indicate future CPU access for the textures. + _parent.TextureManager.RefreshModifiedTextures(); + _context.CreateHostSyncIfNeeded(HostSyncFlags.StrictSyncpoint); _context.Synchronization.IncrementSyncpoint(syncpointId); } diff --git a/src/Ryujinx.Graphics.Gpu/Engine/GPFifo/GPFifoProcessor.cs b/src/Ryujinx.Graphics.Gpu/Engine/GPFifo/GPFifoProcessor.cs index b57109c7d..984a9cff8 100644 --- a/src/Ryujinx.Graphics.Gpu/Engine/GPFifo/GPFifoProcessor.cs +++ b/src/Ryujinx.Graphics.Gpu/Engine/GPFifo/GPFifoProcessor.cs @@ -4,6 +4,7 @@ using Ryujinx.Graphics.Gpu.Engine.Dma; using Ryujinx.Graphics.Gpu.Engine.InlineToMemory; using Ryujinx.Graphics.Gpu.Engine.Threed; using Ryujinx.Graphics.Gpu.Engine.Twod; +using Ryujinx.Graphics.Gpu.Image; using Ryujinx.Graphics.Gpu.Memory; using System; using System.Runtime.CompilerServices; @@ -28,6 +29,11 @@ namespace Ryujinx.Graphics.Gpu.Engine.GPFifo /// public MemoryManager MemoryManager => _channel.MemoryManager; + /// + /// Channel texture manager. + /// + public TextureManager TextureManager => _channel.TextureManager; + /// /// 3D Engine. /// diff --git a/src/Ryujinx.Graphics.Gpu/Engine/MME/MacroHLE.cs b/src/Ryujinx.Graphics.Gpu/Engine/MME/MacroHLE.cs index 7f3772f44..475d1ee4e 100644 --- a/src/Ryujinx.Graphics.Gpu/Engine/MME/MacroHLE.cs +++ b/src/Ryujinx.Graphics.Gpu/Engine/MME/MacroHLE.cs @@ -5,6 +5,7 @@ using Ryujinx.Graphics.GAL; using Ryujinx.Graphics.Gpu.Engine.GPFifo; using Ryujinx.Graphics.Gpu.Engine.Threed; using Ryujinx.Graphics.Gpu.Engine.Types; +using Ryujinx.Graphics.Gpu.Memory; using Ryujinx.Memory.Range; using System; using System.Collections.Generic; @@ -495,8 +496,8 @@ namespace Ryujinx.Graphics.Gpu.Engine.MME ulong indirectBufferSize = (ulong)maxDrawCount * (ulong)stride; - MultiRange indirectBufferRange = bufferCache.TranslateAndCreateMultiBuffers(_processor.MemoryManager, indirectBufferGpuVa, indirectBufferSize); - MultiRange parameterBufferRange = bufferCache.TranslateAndCreateMultiBuffers(_processor.MemoryManager, parameterBufferGpuVa, 4); + MultiRange indirectBufferRange = bufferCache.TranslateAndCreateMultiBuffers(_processor.MemoryManager, indirectBufferGpuVa, indirectBufferSize, BufferStage.Indirect); + MultiRange parameterBufferRange = bufferCache.TranslateAndCreateMultiBuffers(_processor.MemoryManager, parameterBufferGpuVa, 4, BufferStage.Indirect); _processor.ThreedClass.DrawIndirect( topology, diff --git a/src/Ryujinx.Graphics.Gpu/Engine/Threed/ComputeDraw/VtgAsComputeContext.cs b/src/Ryujinx.Graphics.Gpu/Engine/Threed/ComputeDraw/VtgAsComputeContext.cs index f9cb40b0d..6de50fb2e 100644 --- a/src/Ryujinx.Graphics.Gpu/Engine/Threed/ComputeDraw/VtgAsComputeContext.cs +++ b/src/Ryujinx.Graphics.Gpu/Engine/Threed/ComputeDraw/VtgAsComputeContext.cs @@ -438,7 +438,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed.ComputeDraw ReadOnlySpan dataBytes = MemoryMarshal.Cast(data); - BufferHandle buffer = _context.Renderer.CreateBuffer(dataBytes.Length); + BufferHandle buffer = _context.Renderer.CreateBuffer(dataBytes.Length, BufferAccess.DeviceMemory); _context.Renderer.SetBufferData(buffer, 0, dataBytes); return new IndexBuffer(buffer, count, dataBytes.Length); @@ -529,7 +529,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed.ComputeDraw { if (_dummyBuffer == BufferHandle.Null) { - _dummyBuffer = _context.Renderer.CreateBuffer(DummyBufferSize); + _dummyBuffer = _context.Renderer.CreateBuffer(DummyBufferSize, BufferAccess.DeviceMemory); _context.Renderer.Pipeline.ClearBuffer(_dummyBuffer, 0, DummyBufferSize, 0); } @@ -550,7 +550,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed.ComputeDraw _context.Renderer.DeleteBuffer(_sequentialIndexBuffer); } - _sequentialIndexBuffer = _context.Renderer.CreateBuffer(count * sizeof(uint)); + _sequentialIndexBuffer = _context.Renderer.CreateBuffer(count * sizeof(uint), BufferAccess.DeviceMemory); _sequentialIndexBufferCount = count; Span data = new int[count]; @@ -583,7 +583,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed.ComputeDraw _context.Renderer.DeleteBuffer(buffer.Handle); } - buffer.Handle = _context.Renderer.CreateBuffer(newSize); + buffer.Handle = _context.Renderer.CreateBuffer(newSize, BufferAccess.DeviceMemory); buffer.Size = newSize; } diff --git a/src/Ryujinx.Graphics.Gpu/Engine/Threed/ComputeDraw/VtgAsComputeState.cs b/src/Ryujinx.Graphics.Gpu/Engine/Threed/ComputeDraw/VtgAsComputeState.cs index 6324e6a15..73682866b 100644 --- a/src/Ryujinx.Graphics.Gpu/Engine/Threed/ComputeDraw/VtgAsComputeState.cs +++ b/src/Ryujinx.Graphics.Gpu/Engine/Threed/ComputeDraw/VtgAsComputeState.cs @@ -3,6 +3,7 @@ using Ryujinx.Common.Logging; using Ryujinx.Graphics.GAL; using Ryujinx.Graphics.Gpu.Engine.Types; using Ryujinx.Graphics.Gpu.Image; +using Ryujinx.Graphics.Gpu.Memory; using Ryujinx.Graphics.Gpu.Shader; using Ryujinx.Graphics.Shader; using Ryujinx.Graphics.Shader.Translation; @@ -370,7 +371,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed.ComputeDraw { var memoryManager = _channel.MemoryManager; - BufferRange range = memoryManager.Physical.BufferCache.GetBufferRange(memoryManager.GetPhysicalRegions(address, size)); + BufferRange range = memoryManager.Physical.BufferCache.GetBufferRange(memoryManager.GetPhysicalRegions(address, size), BufferStage.VertexBuffer); ITexture bufferTexture = _vacContext.EnsureBufferTexture(index + 2, format); bufferTexture.SetStorage(range); @@ -412,7 +413,9 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed.ComputeDraw var memoryManager = _channel.MemoryManager; ulong misalign = address & ((ulong)_context.Capabilities.TextureBufferOffsetAlignment - 1); - BufferRange range = memoryManager.Physical.BufferCache.GetBufferRange(memoryManager.GetPhysicalRegions(address + indexOffset - misalign, size + misalign)); + BufferRange range = memoryManager.Physical.BufferCache.GetBufferRange( + memoryManager.GetPhysicalRegions(address + indexOffset - misalign, size + misalign), + BufferStage.IndexBuffer); misalignedOffset = (int)misalign >> shift; SetIndexBufferTexture(reservations, range, format); diff --git a/src/Ryujinx.Graphics.Gpu/Engine/Threed/DrawManager.cs b/src/Ryujinx.Graphics.Gpu/Engine/Threed/DrawManager.cs index d8de14de0..56ef64c6e 100644 --- a/src/Ryujinx.Graphics.Gpu/Engine/Threed/DrawManager.cs +++ b/src/Ryujinx.Graphics.Gpu/Engine/Threed/DrawManager.cs @@ -684,8 +684,8 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed if (hasCount) { - var indirectBuffer = memory.BufferCache.GetBufferRange(indirectBufferRange); - var parameterBuffer = memory.BufferCache.GetBufferRange(parameterBufferRange); + var indirectBuffer = memory.BufferCache.GetBufferRange(indirectBufferRange, BufferStage.Indirect); + var parameterBuffer = memory.BufferCache.GetBufferRange(parameterBufferRange, BufferStage.Indirect); if (indexed) { @@ -698,7 +698,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed } else { - var indirectBuffer = memory.BufferCache.GetBufferRange(indirectBufferRange); + var indirectBuffer = memory.BufferCache.GetBufferRange(indirectBufferRange, BufferStage.Indirect); if (indexed) { diff --git a/src/Ryujinx.Graphics.Gpu/Engine/Threed/StateUpdater.cs b/src/Ryujinx.Graphics.Gpu/Engine/Threed/StateUpdater.cs index b3eb62185..1dc77b52d 100644 --- a/src/Ryujinx.Graphics.Gpu/Engine/Threed/StateUpdater.cs +++ b/src/Ryujinx.Graphics.Gpu/Engine/Threed/StateUpdater.cs @@ -1429,7 +1429,18 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed addressesSpan[index] = baseAddress + shader.Offset; } - CachedShaderProgram gs = shaderCache.GetGraphicsShader(ref _state.State, ref _pipeline, _channel, ref _currentSpecState.GetPoolState(), ref _currentSpecState.GetGraphicsState(), addresses); + int samplerPoolMaximumId = _state.State.SamplerIndex == SamplerIndex.ViaHeaderIndex + ? _state.State.TexturePoolState.MaximumId + : _state.State.SamplerPoolState.MaximumId; + + CachedShaderProgram gs = shaderCache.GetGraphicsShader( + ref _state.State, + ref _pipeline, + _channel, + samplerPoolMaximumId, + ref _currentSpecState.GetPoolState(), + ref _currentSpecState.GetGraphicsState(), + addresses); // Consume the modified flag for spec state so that it isn't checked again. _currentSpecState.SetShader(gs); diff --git a/src/Ryujinx.Graphics.Gpu/GpuContext.cs b/src/Ryujinx.Graphics.Gpu/GpuContext.cs index aa0084fdc..048d32fb7 100644 --- a/src/Ryujinx.Graphics.Gpu/GpuContext.cs +++ b/src/Ryujinx.Graphics.Gpu/GpuContext.cs @@ -393,10 +393,17 @@ namespace Ryujinx.Graphics.Gpu if (force || _pendingSync || (syncpoint && SyncpointActions.Count > 0)) { - Renderer.CreateSync(SyncNumber, strict); + foreach (var action in SyncActions) + { + action.SyncPreAction(syncpoint); + } - SyncActions.ForEach(action => action.SyncPreAction(syncpoint)); - SyncpointActions.ForEach(action => action.SyncPreAction(syncpoint)); + foreach (var action in SyncpointActions) + { + action.SyncPreAction(syncpoint); + } + + Renderer.CreateSync(SyncNumber, strict); SyncNumber++; diff --git a/src/Ryujinx.Graphics.Gpu/Image/PoolCache.cs b/src/Ryujinx.Graphics.Gpu/Image/PoolCache.cs index d9881f897..50872ab63 100644 --- a/src/Ryujinx.Graphics.Gpu/Image/PoolCache.cs +++ b/src/Ryujinx.Graphics.Gpu/Image/PoolCache.cs @@ -62,8 +62,9 @@ namespace Ryujinx.Graphics.Gpu.Image /// GPU channel that the texture pool cache belongs to /// Start address of the texture pool /// Maximum ID of the texture pool + /// Cache of texture array bindings /// The found or newly created texture pool - public T FindOrCreate(GpuChannel channel, ulong address, int maximumId) + public T FindOrCreate(GpuChannel channel, ulong address, int maximumId, TextureBindingsArrayCache bindingsArrayCache) { // Remove old entries from the cache, if possible. while (_pools.Count > MaxCapacity && (_currentTimestamp - _pools.First.Value.CacheTimestamp) >= MinDeltaForRemoval) @@ -73,6 +74,7 @@ namespace Ryujinx.Graphics.Gpu.Image _pools.RemoveFirst(); oldestPool.Dispose(); oldestPool.CacheNode = null; + bindingsArrayCache.RemoveAllWithPool(oldestPool); } T pool; @@ -87,8 +89,7 @@ namespace Ryujinx.Graphics.Gpu.Image if (pool.CacheNode != _pools.Last) { _pools.Remove(pool.CacheNode); - - pool.CacheNode = _pools.AddLast(pool); + _pools.AddLast(pool.CacheNode); } pool.CacheTimestamp = _currentTimestamp; diff --git a/src/Ryujinx.Graphics.Gpu/Image/Texture.cs b/src/Ryujinx.Graphics.Gpu/Image/Texture.cs index e67caea81..dde28dbd7 100644 --- a/src/Ryujinx.Graphics.Gpu/Image/Texture.cs +++ b/src/Ryujinx.Graphics.Gpu/Image/Texture.cs @@ -390,7 +390,7 @@ namespace Ryujinx.Graphics.Gpu.Image { _views.Remove(texture); - Group.RemoveView(texture); + Group.RemoveView(_views, texture); texture._viewStorage = texture; diff --git a/src/Ryujinx.Graphics.Gpu/Image/TextureBindingInfo.cs b/src/Ryujinx.Graphics.Gpu/Image/TextureBindingInfo.cs index 12a457dbc..31abc21e8 100644 --- a/src/Ryujinx.Graphics.Gpu/Image/TextureBindingInfo.cs +++ b/src/Ryujinx.Graphics.Gpu/Image/TextureBindingInfo.cs @@ -19,6 +19,11 @@ namespace Ryujinx.Graphics.Gpu.Image /// public Format Format { get; } + /// + /// Shader texture host set index. + /// + public int Set { get; } + /// /// Shader texture host binding point. /// @@ -44,20 +49,27 @@ namespace Ryujinx.Graphics.Gpu.Image /// public TextureUsageFlags Flags { get; } + /// + /// Indicates that the binding is for a sampler. + /// + public bool IsSamplerOnly { get; } + /// /// Constructs the texture binding information structure. /// /// The shader sampler target type /// Format of the image as declared on the shader + /// Shader texture host set index /// The shader texture binding point /// For array of textures, this indicates the length of the array. A value of one indicates it is not an array /// Constant buffer slot where the texture handle is located /// The shader texture handle (read index into the texture constant buffer) /// The texture's usage flags, indicating how it is used in the shader - public TextureBindingInfo(Target target, Format format, int binding, int arrayLength, int cbufSlot, int handle, TextureUsageFlags flags) + public TextureBindingInfo(Target target, Format format, int set, int binding, int arrayLength, int cbufSlot, int handle, TextureUsageFlags flags) { Target = target; Format = format; + Set = set; Binding = binding; ArrayLength = arrayLength; CbufSlot = cbufSlot; @@ -69,13 +81,24 @@ namespace Ryujinx.Graphics.Gpu.Image /// Constructs the texture binding information structure. /// /// The shader sampler target type + /// Shader texture host set index /// The shader texture binding point /// For array of textures, this indicates the length of the array. A value of one indicates it is not an array /// Constant buffer slot where the texture handle is located /// The shader texture handle (read index into the texture constant buffer) /// The texture's usage flags, indicating how it is used in the shader - public TextureBindingInfo(Target target, int binding, int arrayLength, int cbufSlot, int handle, TextureUsageFlags flags) : this(target, (Format)0, binding, arrayLength, cbufSlot, handle, flags) + /// Indicates that the binding is for a sampler + public TextureBindingInfo( + Target target, + int set, + int binding, + int arrayLength, + int cbufSlot, + int handle, + TextureUsageFlags flags, + bool isSamplerOnly) : this(target, 0, set, binding, arrayLength, cbufSlot, handle, flags) { + IsSamplerOnly = isSamplerOnly; } } } diff --git a/src/Ryujinx.Graphics.Gpu/Image/TextureBindingsArrayCache.cs b/src/Ryujinx.Graphics.Gpu/Image/TextureBindingsArrayCache.cs index 4645317c4..18e28b3dd 100644 --- a/src/Ryujinx.Graphics.Gpu/Image/TextureBindingsArrayCache.cs +++ b/src/Ryujinx.Graphics.Gpu/Image/TextureBindingsArrayCache.cs @@ -21,12 +21,98 @@ namespace Ryujinx.Graphics.Gpu.Image private readonly GpuContext _context; private readonly GpuChannel _channel; - private readonly bool _isCompute; /// /// Array cache entry key. /// - private readonly struct CacheEntryKey : IEquatable + private readonly struct CacheEntryFromPoolKey : IEquatable + { + /// + /// Whether the entry is for an image. + /// + public readonly bool IsImage; + + /// + /// Whether the entry is for a sampler. + /// + public readonly bool IsSampler; + + /// + /// Texture or image target type. + /// + public readonly Target Target; + + /// + /// Number of entries of the array. + /// + public readonly int ArrayLength; + + private readonly TexturePool _texturePool; + private readonly SamplerPool _samplerPool; + + /// + /// Creates a new array cache entry. + /// + /// Whether the entry is for an image + /// Binding information for the array + /// Texture pool where the array textures are located + /// Sampler pool where the array samplers are located + public CacheEntryFromPoolKey(bool isImage, TextureBindingInfo bindingInfo, TexturePool texturePool, SamplerPool samplerPool) + { + IsImage = isImage; + IsSampler = bindingInfo.IsSamplerOnly; + Target = bindingInfo.Target; + ArrayLength = bindingInfo.ArrayLength; + + _texturePool = texturePool; + _samplerPool = samplerPool; + } + + /// + /// Checks if the pool matches the cached pool. + /// + /// Texture or sampler pool instance + /// True if the pool matches, false otherwise + public bool MatchesPool(IPool pool) + { + return _texturePool == pool || _samplerPool == pool; + } + + /// + /// Checks if the texture and sampler pools matches the cached pools. + /// + /// Texture pool instance + /// Sampler pool instance + /// True if the pools match, false otherwise + private bool MatchesPools(TexturePool texturePool, SamplerPool samplerPool) + { + return _texturePool == texturePool && _samplerPool == samplerPool; + } + + public bool Equals(CacheEntryFromPoolKey other) + { + return IsImage == other.IsImage && + IsSampler == other.IsSampler && + Target == other.Target && + ArrayLength == other.ArrayLength && + MatchesPools(other._texturePool, other._samplerPool); + } + + public override bool Equals(object obj) + { + return obj is CacheEntryFromBufferKey other && Equals(other); + } + + public override int GetHashCode() + { + return HashCode.Combine(_texturePool, _samplerPool, IsSampler); + } + } + + /// + /// Array cache entry key. + /// + private readonly struct CacheEntryFromBufferKey : IEquatable { /// /// Whether the entry is for an image. @@ -61,7 +147,7 @@ namespace Ryujinx.Graphics.Gpu.Image /// Texture pool where the array textures are located /// Sampler pool where the array samplers are located /// Constant buffer bounds with the texture handles - public CacheEntryKey( + public CacheEntryFromBufferKey( bool isImage, TextureBindingInfo bindingInfo, TexturePool texturePool, @@ -100,7 +186,7 @@ namespace Ryujinx.Graphics.Gpu.Image return _textureBufferBounds.Equals(textureBufferBounds); } - public bool Equals(CacheEntryKey other) + public bool Equals(CacheEntryFromBufferKey other) { return IsImage == other.IsImage && Target == other.Target && @@ -112,7 +198,7 @@ namespace Ryujinx.Graphics.Gpu.Image public override bool Equals(object obj) { - return obj is CacheEntryKey other && Equals(other); + return obj is CacheEntryFromBufferKey other && Equals(other); } public override int GetHashCode() @@ -122,40 +208,15 @@ namespace Ryujinx.Graphics.Gpu.Image } /// - /// Array cache entry. + /// Array cache entry from pool. /// private class CacheEntry { - /// - /// Key for this entry on the cache. - /// - public readonly CacheEntryKey Key; - - /// - /// Linked list node used on the texture bindings array cache. - /// - public LinkedListNode CacheNode; - - /// - /// Timestamp set on the last use of the array by the cache. - /// - public int CacheTimestamp; - /// /// All cached textures, along with their invalidated sequence number as value. /// public readonly Dictionary Textures; - /// - /// All pool texture IDs along with their textures. - /// - public readonly Dictionary TextureIds; - - /// - /// All pool sampler IDs along with their samplers. - /// - public readonly Dictionary SamplerIds; - /// /// Backend texture array if the entry is for a texture, otherwise null. /// @@ -166,44 +227,39 @@ namespace Ryujinx.Graphics.Gpu.Image /// public readonly IImageArray ImageArray; - private readonly TexturePool _texturePool; - private readonly SamplerPool _samplerPool; + /// + /// Texture pool where the array textures are located. + /// + protected readonly TexturePool TexturePool; + + /// + /// Sampler pool where the array samplers are located. + /// + protected readonly SamplerPool SamplerPool; private int _texturePoolSequence; private int _samplerPoolSequence; - private int[] _cachedTextureBuffer; - private int[] _cachedSamplerBuffer; - - private int _lastSequenceNumber; - /// /// Creates a new array cache entry. /// - /// Key for this entry on the cache /// Texture pool where the array textures are located /// Sampler pool where the array samplers are located - private CacheEntry(ref CacheEntryKey key, TexturePool texturePool, SamplerPool samplerPool) + private CacheEntry(TexturePool texturePool, SamplerPool samplerPool) { - Key = key; Textures = new Dictionary(); - TextureIds = new Dictionary(); - SamplerIds = new Dictionary(); - _texturePool = texturePool; - _samplerPool = samplerPool; - - _lastSequenceNumber = -1; + TexturePool = texturePool; + SamplerPool = samplerPool; } /// /// Creates a new array cache entry. /// - /// Key for this entry on the cache /// Backend texture array /// Texture pool where the array textures are located /// Sampler pool where the array samplers are located - public CacheEntry(ref CacheEntryKey key, ITextureArray array, TexturePool texturePool, SamplerPool samplerPool) : this(ref key, texturePool, samplerPool) + public CacheEntry(ITextureArray array, TexturePool texturePool, SamplerPool samplerPool) : this(texturePool, samplerPool) { TextureArray = array; } @@ -211,11 +267,10 @@ namespace Ryujinx.Graphics.Gpu.Image /// /// Creates a new array cache entry. /// - /// Key for this entry on the cache /// Backend image array /// Texture pool where the array textures are located /// Sampler pool where the array samplers are located - public CacheEntry(ref CacheEntryKey key, IImageArray array, TexturePool texturePool, SamplerPool samplerPool) : this(ref key, texturePool, samplerPool) + public CacheEntry(IImageArray array, TexturePool texturePool, SamplerPool samplerPool) : this(texturePool, samplerPool) { ImageArray = array; } @@ -248,23 +303,9 @@ namespace Ryujinx.Graphics.Gpu.Image /// /// Clears all cached texture instances. /// - public void Reset() + public virtual void Reset() { Textures.Clear(); - TextureIds.Clear(); - SamplerIds.Clear(); - } - - /// - /// Updates the cached constant buffer data. - /// - /// Constant buffer data with the texture handles (and sampler handles, if they are combined) - /// Constant buffer data with the sampler handles - /// Whether and comes from different buffers - public void UpdateData(ReadOnlySpan cachedTextureBuffer, ReadOnlySpan cachedSamplerBuffer, bool separateSamplerBuffer) - { - _cachedTextureBuffer = cachedTextureBuffer.ToArray(); - _cachedSamplerBuffer = separateSamplerBuffer ? cachedSamplerBuffer.ToArray() : _cachedTextureBuffer; } /// @@ -287,39 +328,105 @@ namespace Ryujinx.Graphics.Gpu.Image /// /// Checks if the cached texture or sampler pool has been modified since the last call to this method. /// - /// True if any used entries of the pools might have been modified, false otherwise - public bool PoolsModified() + /// True if any used entries of the pool might have been modified, false otherwise + public bool TexturePoolModified() { - bool texturePoolModified = _texturePool.WasModified(ref _texturePoolSequence); - bool samplerPoolModified = _samplerPool.WasModified(ref _samplerPoolSequence); + return TexturePool.WasModified(ref _texturePoolSequence); + } - // If both pools were not modified since the last check, we have nothing else to check. - if (!texturePoolModified && !samplerPoolModified) - { - return false; - } + /// + /// Checks if the cached texture or sampler pool has been modified since the last call to this method. + /// + /// True if any used entries of the pool might have been modified, false otherwise + public bool SamplerPoolModified() + { + return SamplerPool.WasModified(ref _samplerPoolSequence); + } + } - // If the pools were modified, let's check if any of the entries we care about changed. + /// + /// Array cache entry from constant buffer. + /// + private class CacheEntryFromBuffer : CacheEntry + { + /// + /// Key for this entry on the cache. + /// + public readonly CacheEntryFromBufferKey Key; - // Check if any of our cached textures changed on the pool. - foreach ((int textureId, Texture texture) in TextureIds) - { - if (_texturePool.GetCachedItem(textureId) != texture) - { - return true; - } - } + /// + /// Linked list node used on the texture bindings array cache. + /// + public LinkedListNode CacheNode; - // Check if any of our cached samplers changed on the pool. - foreach ((int samplerId, Sampler sampler) in SamplerIds) - { - if (_samplerPool.GetCachedItem(samplerId) != sampler) - { - return true; - } - } + /// + /// Timestamp set on the last use of the array by the cache. + /// + public int CacheTimestamp; - return false; + /// + /// All pool texture IDs along with their textures. + /// + public readonly Dictionary TextureIds; + + /// + /// All pool sampler IDs along with their samplers. + /// + public readonly Dictionary SamplerIds; + + private int[] _cachedTextureBuffer; + private int[] _cachedSamplerBuffer; + + private int _lastSequenceNumber; + + /// + /// Creates a new array cache entry. + /// + /// Key for this entry on the cache + /// Backend texture array + /// Texture pool where the array textures are located + /// Sampler pool where the array samplers are located + public CacheEntryFromBuffer(ref CacheEntryFromBufferKey key, ITextureArray array, TexturePool texturePool, SamplerPool samplerPool) : base(array, texturePool, samplerPool) + { + Key = key; + _lastSequenceNumber = -1; + TextureIds = new Dictionary(); + SamplerIds = new Dictionary(); + } + + /// + /// Creates a new array cache entry. + /// + /// Key for this entry on the cache + /// Backend image array + /// Texture pool where the array textures are located + /// Sampler pool where the array samplers are located + public CacheEntryFromBuffer(ref CacheEntryFromBufferKey key, IImageArray array, TexturePool texturePool, SamplerPool samplerPool) : base(array, texturePool, samplerPool) + { + Key = key; + _lastSequenceNumber = -1; + TextureIds = new Dictionary(); + SamplerIds = new Dictionary(); + } + + /// + public override void Reset() + { + base.Reset(); + TextureIds.Clear(); + SamplerIds.Clear(); + } + + /// + /// Updates the cached constant buffer data. + /// + /// Constant buffer data with the texture handles (and sampler handles, if they are combined) + /// Constant buffer data with the sampler handles + /// Whether and comes from different buffers + public void UpdateData(ReadOnlySpan cachedTextureBuffer, ReadOnlySpan cachedSamplerBuffer, bool separateSamplerBuffer) + { + _cachedTextureBuffer = cachedTextureBuffer.ToArray(); + _cachedSamplerBuffer = separateSamplerBuffer ? cachedSamplerBuffer.ToArray() : _cachedTextureBuffer; } /// @@ -380,10 +487,51 @@ namespace Ryujinx.Graphics.Gpu.Image return true; } + + /// + /// Checks if the cached texture or sampler pool has been modified since the last call to this method. + /// + /// True if any used entries of the pools might have been modified, false otherwise + public bool PoolsModified() + { + bool texturePoolModified = TexturePoolModified(); + bool samplerPoolModified = SamplerPoolModified(); + + // If both pools were not modified since the last check, we have nothing else to check. + if (!texturePoolModified && !samplerPoolModified) + { + return false; + } + + // If the pools were modified, let's check if any of the entries we care about changed. + + // Check if any of our cached textures changed on the pool. + foreach ((int textureId, (Texture texture, TextureDescriptor descriptor)) in TextureIds) + { + if (TexturePool.GetCachedItem(textureId) != texture || + (texture == null && TexturePool.IsValidId(textureId) && !TexturePool.GetDescriptorRef(textureId).Equals(descriptor))) + { + return true; + } + } + + // Check if any of our cached samplers changed on the pool. + foreach ((int samplerId, (Sampler sampler, SamplerDescriptor descriptor)) in SamplerIds) + { + if (SamplerPool.GetCachedItem(samplerId) != sampler || + (sampler == null && SamplerPool.IsValidId(samplerId) && !SamplerPool.GetDescriptorRef(samplerId).Equals(descriptor))) + { + return true; + } + } + + return false; + } } - private readonly Dictionary _cache; - private readonly LinkedList _lruCache; + private readonly Dictionary _cacheFromBuffer; + private readonly Dictionary _cacheFromPool; + private readonly LinkedList _lruCache; private int _currentTimestamp; @@ -392,14 +540,13 @@ namespace Ryujinx.Graphics.Gpu.Image /// /// GPU context /// GPU channel - /// Whether the bindings will be used for compute or graphics pipelines - public TextureBindingsArrayCache(GpuContext context, GpuChannel channel, bool isCompute) + public TextureBindingsArrayCache(GpuContext context, GpuChannel channel) { _context = context; _channel = channel; - _isCompute = isCompute; - _cache = new Dictionary(); - _lruCache = new LinkedList(); + _cacheFromBuffer = new Dictionary(); + _cacheFromPool = new Dictionary(); + _lruCache = new LinkedList(); } /// @@ -419,7 +566,7 @@ namespace Ryujinx.Graphics.Gpu.Image int stageIndex, int textureBufferIndex, SamplerIndex samplerIndex, - TextureBindingInfo bindingInfo) + in TextureBindingInfo bindingInfo) { Update(texturePool, samplerPool, stage, stageIndex, textureBufferIndex, isImage: false, samplerIndex, bindingInfo); } @@ -432,7 +579,7 @@ namespace Ryujinx.Graphics.Gpu.Image /// Shader stage index where the array is used /// Texture constant buffer index /// Array binding information - public void UpdateImageArray(TexturePool texturePool, ShaderStage stage, int stageIndex, int textureBufferIndex, TextureBindingInfo bindingInfo) + public void UpdateImageArray(TexturePool texturePool, ShaderStage stage, int stageIndex, int textureBufferIndex, in TextureBindingInfo bindingInfo) { Update(texturePool, null, stage, stageIndex, textureBufferIndex, isImage: true, SamplerIndex.ViaHeaderIndex, bindingInfo); } @@ -456,16 +603,181 @@ namespace Ryujinx.Graphics.Gpu.Image int textureBufferIndex, bool isImage, SamplerIndex samplerIndex, - TextureBindingInfo bindingInfo) + in TextureBindingInfo bindingInfo) + { + if (IsDirectHandleType(bindingInfo.Handle)) + { + UpdateFromPool(texturePool, samplerPool, stage, isImage, bindingInfo); + } + else + { + UpdateFromBuffer(texturePool, samplerPool, stage, stageIndex, textureBufferIndex, isImage, samplerIndex, bindingInfo); + } + } + + /// + /// Updates a texture or image array bindings and textures from a texture or sampler pool. + /// + /// Texture pool + /// Sampler pool + /// Shader stage where the array is used + /// Whether the array is a image or texture array + /// Array binding information + private void UpdateFromPool(TexturePool texturePool, SamplerPool samplerPool, ShaderStage stage, bool isImage, in TextureBindingInfo bindingInfo) + { + CacheEntry entry = GetOrAddEntry(texturePool, samplerPool, bindingInfo, isImage, out bool isNewEntry); + + bool isSampler = bindingInfo.IsSamplerOnly; + bool poolModified = isSampler ? entry.SamplerPoolModified() : entry.TexturePoolModified(); + bool isStore = bindingInfo.Flags.HasFlag(TextureUsageFlags.ImageStore); + bool resScaleUnsupported = bindingInfo.Flags.HasFlag(TextureUsageFlags.ResScaleUnsupported); + + if (!poolModified && !isNewEntry && entry.ValidateTextures()) + { + entry.SynchronizeMemory(isStore, resScaleUnsupported); + + if (isImage) + { + SetImageArray(stage, bindingInfo, entry.ImageArray); + } + else + { + SetTextureArray(stage, bindingInfo, entry.TextureArray); + } + + return; + } + + if (!isNewEntry) + { + entry.Reset(); + } + + int length = (isSampler ? samplerPool.MaximumId : texturePool.MaximumId) + 1; + length = Math.Min(length, bindingInfo.ArrayLength); + + Format[] formats = isImage ? new Format[bindingInfo.ArrayLength] : null; + ISampler[] samplers = isImage ? null : new ISampler[bindingInfo.ArrayLength]; + ITexture[] textures = new ITexture[bindingInfo.ArrayLength]; + + for (int index = 0; index < length; index++) + { + Texture texture = null; + Sampler sampler = null; + + if (isSampler) + { + sampler = samplerPool?.Get(index); + } + else + { + ref readonly TextureDescriptor descriptor = ref texturePool.GetForBinding(index, out texture); + + if (texture != null) + { + entry.Textures[texture] = texture.InvalidatedSequence; + + if (isStore) + { + texture.SignalModified(); + } + + if (resScaleUnsupported && texture.ScaleMode != TextureScaleMode.Blacklisted) + { + // Scaling textures used on arrays is currently not supported. + + texture.BlacklistScale(); + } + } + } + + ITexture hostTexture = texture?.GetTargetTexture(bindingInfo.Target); + ISampler hostSampler = sampler?.GetHostSampler(texture); + + Format format = bindingInfo.Format; + + if (hostTexture != null && texture.Target == Target.TextureBuffer) + { + // Ensure that the buffer texture is using the correct buffer as storage. + // Buffers are frequently re-created to accommodate larger data, so we need to re-bind + // to ensure we're not using a old buffer that was already deleted. + if (isImage) + { + if (format == 0 && texture != null) + { + format = texture.Format; + } + + _channel.BufferManager.SetBufferTextureStorage(stage, entry.ImageArray, hostTexture, texture.Range, bindingInfo, index, format); + } + else + { + _channel.BufferManager.SetBufferTextureStorage(stage, entry.TextureArray, hostTexture, texture.Range, bindingInfo, index, format); + } + } + else if (isImage) + { + if (format == 0 && texture != null) + { + format = texture.Format; + } + + formats[index] = format; + textures[index] = hostTexture; + } + else + { + samplers[index] = hostSampler; + textures[index] = hostTexture; + } + } + + if (isImage) + { + entry.ImageArray.SetFormats(0, formats); + entry.ImageArray.SetImages(0, textures); + + SetImageArray(stage, bindingInfo, entry.ImageArray); + } + else + { + entry.TextureArray.SetSamplers(0, samplers); + entry.TextureArray.SetTextures(0, textures); + + SetTextureArray(stage, bindingInfo, entry.TextureArray); + } + } + + /// + /// Updates a texture or image array bindings and textures from constant buffer handles. + /// + /// Texture pool + /// Sampler pool + /// Shader stage where the array is used + /// Shader stage index where the array is used + /// Texture constant buffer index + /// Whether the array is a image or texture array + /// Sampler handles source + /// Array binding information + private void UpdateFromBuffer( + TexturePool texturePool, + SamplerPool samplerPool, + ShaderStage stage, + int stageIndex, + int textureBufferIndex, + bool isImage, + SamplerIndex samplerIndex, + in TextureBindingInfo bindingInfo) { (textureBufferIndex, int samplerBufferIndex) = TextureHandle.UnpackSlots(bindingInfo.CbufSlot, textureBufferIndex); bool separateSamplerBuffer = textureBufferIndex != samplerBufferIndex; + bool isCompute = stage == ShaderStage.Compute; - ref BufferBounds textureBufferBounds = ref _channel.BufferManager.GetUniformBufferBounds(_isCompute, stageIndex, textureBufferIndex); - ref BufferBounds samplerBufferBounds = ref _channel.BufferManager.GetUniformBufferBounds(_isCompute, stageIndex, samplerBufferIndex); + ref BufferBounds textureBufferBounds = ref _channel.BufferManager.GetUniformBufferBounds(isCompute, stageIndex, textureBufferIndex); + ref BufferBounds samplerBufferBounds = ref _channel.BufferManager.GetUniformBufferBounds(isCompute, stageIndex, samplerBufferIndex); - CacheEntry entry = GetOrAddEntry( + CacheEntryFromBuffer entry = GetOrAddEntry( texturePool, samplerPool, bindingInfo, @@ -488,11 +800,11 @@ namespace Ryujinx.Graphics.Gpu.Image if (isImage) { - _context.Renderer.Pipeline.SetImageArray(stage, bindingInfo.Binding, entry.ImageArray); + SetImageArray(stage, bindingInfo, entry.ImageArray); } else { - _context.Renderer.Pipeline.SetTextureArray(stage, bindingInfo.Binding, entry.TextureArray); + SetTextureArray(stage, bindingInfo, entry.TextureArray); } return; @@ -517,11 +829,11 @@ namespace Ryujinx.Graphics.Gpu.Image if (isImage) { - _context.Renderer.Pipeline.SetImageArray(stage, bindingInfo.Binding, entry.ImageArray); + SetImageArray(stage, bindingInfo, entry.ImageArray); } else { - _context.Renderer.Pipeline.SetTextureArray(stage, bindingInfo.Binding, entry.TextureArray); + SetTextureArray(stage, bindingInfo, entry.TextureArray); } return; @@ -589,8 +901,8 @@ namespace Ryujinx.Graphics.Gpu.Image Sampler sampler = samplerPool?.Get(samplerId); - entry.TextureIds[textureId] = texture; - entry.SamplerIds[samplerId] = sampler; + entry.TextureIds[textureId] = (texture, descriptor); + entry.SamplerIds[samplerId] = (sampler, samplerPool?.GetDescriptorRef(samplerId) ?? default); ITexture hostTexture = texture?.GetTargetTexture(bindingInfo.Target); ISampler hostSampler = sampler?.GetHostSampler(texture); @@ -609,11 +921,11 @@ namespace Ryujinx.Graphics.Gpu.Image format = texture.Format; } - _channel.BufferManager.SetBufferTextureStorage(entry.ImageArray, hostTexture, texture.Range, bindingInfo, index, format); + _channel.BufferManager.SetBufferTextureStorage(stage, entry.ImageArray, hostTexture, texture.Range, bindingInfo, index, format); } else { - _channel.BufferManager.SetBufferTextureStorage(entry.TextureArray, hostTexture, texture.Range, bindingInfo, index, format); + _channel.BufferManager.SetBufferTextureStorage(stage, entry.TextureArray, hostTexture, texture.Range, bindingInfo, index, format); } } else if (isImage) @@ -638,43 +950,72 @@ namespace Ryujinx.Graphics.Gpu.Image entry.ImageArray.SetFormats(0, formats); entry.ImageArray.SetImages(0, textures); - _context.Renderer.Pipeline.SetImageArray(stage, bindingInfo.Binding, entry.ImageArray); + SetImageArray(stage, bindingInfo, entry.ImageArray); } else { entry.TextureArray.SetSamplers(0, samplers); entry.TextureArray.SetTextures(0, textures); - _context.Renderer.Pipeline.SetTextureArray(stage, bindingInfo.Binding, entry.TextureArray); + SetTextureArray(stage, bindingInfo, entry.TextureArray); } } /// - /// Gets a cached texture entry, or creates a new one if not found. + /// Updates a texture array binding on the host. + /// + /// Shader stage where the array is used + /// Array binding information + /// Texture array + private void SetTextureArray(ShaderStage stage, in TextureBindingInfo bindingInfo, ITextureArray array) + { + if (bindingInfo.Set >= _context.Capabilities.ExtraSetBaseIndex && _context.Capabilities.MaximumExtraSets != 0) + { + _context.Renderer.Pipeline.SetTextureArraySeparate(stage, bindingInfo.Set, array); + } + else + { + _context.Renderer.Pipeline.SetTextureArray(stage, bindingInfo.Binding, array); + } + } + + /// + /// Updates a image array binding on the host. + /// + /// Shader stage where the array is used + /// Array binding information + /// Image array + private void SetImageArray(ShaderStage stage, in TextureBindingInfo bindingInfo, IImageArray array) + { + if (bindingInfo.Set >= _context.Capabilities.ExtraSetBaseIndex && _context.Capabilities.MaximumExtraSets != 0) + { + _context.Renderer.Pipeline.SetImageArraySeparate(stage, bindingInfo.Set, array); + } + else + { + _context.Renderer.Pipeline.SetImageArray(stage, bindingInfo.Binding, array); + } + } + + /// + /// Gets a cached texture entry from pool, or creates a new one if not found. /// /// Texture pool /// Sampler pool /// Array binding information /// Whether the array is a image or texture array - /// Constant buffer bounds with the texture handles /// Whether a new entry was created, or an existing one was returned /// Cache entry private CacheEntry GetOrAddEntry( TexturePool texturePool, SamplerPool samplerPool, - TextureBindingInfo bindingInfo, + in TextureBindingInfo bindingInfo, bool isImage, - ref BufferBounds textureBufferBounds, out bool isNew) { - CacheEntryKey key = new CacheEntryKey( - isImage, - bindingInfo, - texturePool, - samplerPool, - ref textureBufferBounds); + CacheEntryFromPoolKey key = new CacheEntryFromPoolKey(isImage, bindingInfo, texturePool, samplerPool); - isNew = !_cache.TryGetValue(key, out CacheEntry entry); + isNew = !_cacheFromPool.TryGetValue(key, out CacheEntry entry); if (isNew) { @@ -684,13 +1025,61 @@ namespace Ryujinx.Graphics.Gpu.Image { IImageArray array = _context.Renderer.CreateImageArray(arrayLength, bindingInfo.Target == Target.TextureBuffer); - _cache.Add(key, entry = new CacheEntry(ref key, array, texturePool, samplerPool)); + _cacheFromPool.Add(key, entry = new CacheEntry(array, texturePool, samplerPool)); } else { ITextureArray array = _context.Renderer.CreateTextureArray(arrayLength, bindingInfo.Target == Target.TextureBuffer); - _cache.Add(key, entry = new CacheEntry(ref key, array, texturePool, samplerPool)); + _cacheFromPool.Add(key, entry = new CacheEntry(array, texturePool, samplerPool)); + } + } + + return entry; + } + + /// + /// Gets a cached texture entry from constant buffer, or creates a new one if not found. + /// + /// Texture pool + /// Sampler pool + /// Array binding information + /// Whether the array is a image or texture array + /// Constant buffer bounds with the texture handles + /// Whether a new entry was created, or an existing one was returned + /// Cache entry + private CacheEntryFromBuffer GetOrAddEntry( + TexturePool texturePool, + SamplerPool samplerPool, + in TextureBindingInfo bindingInfo, + bool isImage, + ref BufferBounds textureBufferBounds, + out bool isNew) + { + CacheEntryFromBufferKey key = new CacheEntryFromBufferKey( + isImage, + bindingInfo, + texturePool, + samplerPool, + ref textureBufferBounds); + + isNew = !_cacheFromBuffer.TryGetValue(key, out CacheEntryFromBuffer entry); + + if (isNew) + { + int arrayLength = bindingInfo.ArrayLength; + + if (isImage) + { + IImageArray array = _context.Renderer.CreateImageArray(arrayLength, bindingInfo.Target == Target.TextureBuffer); + + _cacheFromBuffer.Add(key, entry = new CacheEntryFromBuffer(ref key, array, texturePool, samplerPool)); + } + else + { + ITextureArray array = _context.Renderer.CreateTextureArray(arrayLength, bindingInfo.Target == Target.TextureBuffer); + + _cacheFromBuffer.Add(key, entry = new CacheEntryFromBuffer(ref key, array, texturePool, samplerPool)); } } @@ -716,15 +1105,52 @@ namespace Ryujinx.Graphics.Gpu.Image /// private void RemoveLeastUsedEntries() { - LinkedListNode nextNode = _lruCache.First; + LinkedListNode nextNode = _lruCache.First; while (nextNode != null && _currentTimestamp - nextNode.Value.CacheTimestamp >= MinDeltaForRemoval) { - LinkedListNode toRemove = nextNode; + LinkedListNode toRemove = nextNode; nextNode = nextNode.Next; - _cache.Remove(toRemove.Value.Key); + _cacheFromBuffer.Remove(toRemove.Value.Key); _lruCache.Remove(toRemove); } } + + /// + /// Removes all cached texture arrays matching the specified texture pool. + /// + /// Texture pool + public void RemoveAllWithPool(IPool pool) + { + List keysToRemove = null; + + foreach (CacheEntryFromPoolKey key in _cacheFromPool.Keys) + { + if (key.MatchesPool(pool)) + { + (keysToRemove ??= new()).Add(key); + } + } + + if (keysToRemove != null) + { + foreach (CacheEntryFromPoolKey key in keysToRemove) + { + _cacheFromPool.Remove(key); + } + } + } + + /// + /// Checks if a handle indicates the binding should have all its textures sourced directly from a pool. + /// + /// Handle to check + /// True if the handle represents direct pool access, false otherwise + private static bool IsDirectHandleType(int handle) + { + (_, _, TextureHandleType type) = TextureHandle.UnpackOffsets(handle); + + return type == TextureHandleType.Direct; + } } } diff --git a/src/Ryujinx.Graphics.Gpu/Image/TextureBindingsManager.cs b/src/Ryujinx.Graphics.Gpu/Image/TextureBindingsManager.cs index 3c10c95e0..9f1f60d95 100644 --- a/src/Ryujinx.Graphics.Gpu/Image/TextureBindingsManager.cs +++ b/src/Ryujinx.Graphics.Gpu/Image/TextureBindingsManager.cs @@ -34,7 +34,7 @@ namespace Ryujinx.Graphics.Gpu.Image private readonly TexturePoolCache _texturePoolCache; private readonly SamplerPoolCache _samplerPoolCache; - private readonly TextureBindingsArrayCache _arrayBindingsCache; + private readonly TextureBindingsArrayCache _bindingsArrayCache; private TexturePool _cachedTexturePool; private SamplerPool _cachedSamplerPool; @@ -72,12 +72,14 @@ namespace Ryujinx.Graphics.Gpu.Image /// /// The GPU context that the texture bindings manager belongs to /// The GPU channel that the texture bindings manager belongs to + /// Cache of texture array bindings /// Texture pools cache used to get texture pools from /// Sampler pools cache used to get sampler pools from /// True if the bindings manager is used for the compute engine public TextureBindingsManager( GpuContext context, GpuChannel channel, + TextureBindingsArrayCache bindingsArrayCache, TexturePoolCache texturePoolCache, SamplerPoolCache samplerPoolCache, bool isCompute) @@ -89,7 +91,7 @@ namespace Ryujinx.Graphics.Gpu.Image _isCompute = isCompute; - _arrayBindingsCache = new TextureBindingsArrayCache(context, channel, isCompute); + _bindingsArrayCache = bindingsArrayCache; int stages = isCompute ? 1 : Constants.ShaderStages; @@ -456,7 +458,7 @@ namespace Ryujinx.Graphics.Gpu.Image if (bindingInfo.ArrayLength > 1) { - _arrayBindingsCache.UpdateTextureArray(texturePool, samplerPool, stage, stageIndex, _textureBufferIndex, _samplerIndex, bindingInfo); + _bindingsArrayCache.UpdateTextureArray(texturePool, samplerPool, stage, stageIndex, _textureBufferIndex, _samplerIndex, bindingInfo); continue; } @@ -594,7 +596,7 @@ namespace Ryujinx.Graphics.Gpu.Image if (bindingInfo.ArrayLength > 1) { - _arrayBindingsCache.UpdateImageArray(pool, stage, stageIndex, _textureBufferIndex, bindingInfo); + _bindingsArrayCache.UpdateImageArray(pool, stage, stageIndex, _textureBufferIndex, bindingInfo); continue; } @@ -732,7 +734,7 @@ namespace Ryujinx.Graphics.Gpu.Image ulong poolAddress = _channel.MemoryManager.Translate(poolGpuVa); - TexturePool texturePool = _texturePoolCache.FindOrCreate(_channel, poolAddress, maximumId); + TexturePool texturePool = _texturePoolCache.FindOrCreate(_channel, poolAddress, maximumId, _bindingsArrayCache); TextureDescriptor descriptor; @@ -770,7 +772,7 @@ namespace Ryujinx.Graphics.Gpu.Image ? _channel.BufferManager.GetComputeUniformBufferAddress(textureBufferIndex) : _channel.BufferManager.GetGraphicsUniformBufferAddress(stageIndex, textureBufferIndex); - int handle = textureBufferAddress != 0 + int handle = textureBufferAddress != MemoryManager.PteUnmapped ? _channel.MemoryManager.Physical.Read(textureBufferAddress + (uint)textureWordOffset * 4) : 0; @@ -790,7 +792,7 @@ namespace Ryujinx.Graphics.Gpu.Image ? _channel.BufferManager.GetComputeUniformBufferAddress(samplerBufferIndex) : _channel.BufferManager.GetGraphicsUniformBufferAddress(stageIndex, samplerBufferIndex); - samplerHandle = samplerBufferAddress != 0 + samplerHandle = samplerBufferAddress != MemoryManager.PteUnmapped ? _channel.MemoryManager.Physical.Read(samplerBufferAddress + (uint)samplerWordOffset * 4) : 0; } @@ -828,7 +830,7 @@ namespace Ryujinx.Graphics.Gpu.Image if (poolAddress != MemoryManager.PteUnmapped) { - texturePool = _texturePoolCache.FindOrCreate(_channel, poolAddress, _texturePoolMaximumId); + texturePool = _texturePoolCache.FindOrCreate(_channel, poolAddress, _texturePoolMaximumId, _bindingsArrayCache); _texturePool = texturePool; } } @@ -839,7 +841,7 @@ namespace Ryujinx.Graphics.Gpu.Image if (poolAddress != MemoryManager.PteUnmapped) { - samplerPool = _samplerPoolCache.FindOrCreate(_channel, poolAddress, _samplerPoolMaximumId); + samplerPool = _samplerPoolCache.FindOrCreate(_channel, poolAddress, _samplerPoolMaximumId, _bindingsArrayCache); _samplerPool = samplerPool; } } diff --git a/src/Ryujinx.Graphics.Gpu/Image/TextureCache.cs b/src/Ryujinx.Graphics.Gpu/Image/TextureCache.cs index 5743c89c0..b9ff060e2 100644 --- a/src/Ryujinx.Graphics.Gpu/Image/TextureCache.cs +++ b/src/Ryujinx.Graphics.Gpu/Image/TextureCache.cs @@ -8,6 +8,7 @@ using Ryujinx.Graphics.Texture; using Ryujinx.Memory.Range; using System; using System.Collections.Generic; +using System.Threading; namespace Ryujinx.Graphics.Gpu.Image { @@ -39,6 +40,8 @@ namespace Ryujinx.Graphics.Gpu.Image private readonly MultiRangeList _textures; private readonly HashSet _partiallyMappedTextures; + private readonly ReaderWriterLockSlim _texturesLock; + private Texture[] _textureOverlaps; private OverlapInfo[] _overlapInfo; @@ -57,6 +60,8 @@ namespace Ryujinx.Graphics.Gpu.Image _textures = new MultiRangeList(); _partiallyMappedTextures = new HashSet(); + _texturesLock = new ReaderWriterLockSlim(); + _textureOverlaps = new Texture[OverlapsBufferInitialCapacity]; _overlapInfo = new OverlapInfo[OverlapsBufferInitialCapacity]; @@ -75,10 +80,16 @@ namespace Ryujinx.Graphics.Gpu.Image MultiRange unmapped = ((MemoryManager)sender).GetPhysicalRegions(e.Address, e.Size); - lock (_textures) + _texturesLock.EnterReadLock(); + + try { overlapCount = _textures.FindOverlaps(unmapped, ref overlaps); } + finally + { + _texturesLock.ExitReadLock(); + } if (overlapCount > 0) { @@ -217,7 +228,18 @@ namespace Ryujinx.Graphics.Gpu.Image public bool UpdateMapping(Texture texture, MultiRange range) { // There cannot be an existing texture compatible with this mapping in the texture cache already. - int overlapCount = _textures.FindOverlaps(range, ref _textureOverlaps); + int overlapCount; + + _texturesLock.EnterReadLock(); + + try + { + overlapCount = _textures.FindOverlaps(range, ref _textureOverlaps); + } + finally + { + _texturesLock.ExitReadLock(); + } for (int i = 0; i < overlapCount; i++) { @@ -231,11 +253,20 @@ namespace Ryujinx.Graphics.Gpu.Image } } - _textures.Remove(texture); + _texturesLock.EnterWriteLock(); - texture.ReplaceRange(range); + try + { + _textures.Remove(texture); - _textures.Add(texture); + texture.ReplaceRange(range); + + _textures.Add(texture); + } + finally + { + _texturesLock.ExitWriteLock(); + } return true; } @@ -611,11 +642,17 @@ namespace Ryujinx.Graphics.Gpu.Image int sameAddressOverlapsCount; - lock (_textures) + _texturesLock.EnterReadLock(); + + try { // Try to find a perfect texture match, with the same address and parameters. sameAddressOverlapsCount = _textures.FindOverlaps(address, ref _textureOverlaps); } + finally + { + _texturesLock.ExitReadLock(); + } Texture texture = null; @@ -698,10 +735,16 @@ namespace Ryujinx.Graphics.Gpu.Image if (info.Target != Target.TextureBuffer) { - lock (_textures) + _texturesLock.EnterReadLock(); + + try { overlapsCount = _textures.FindOverlaps(range.Value, ref _textureOverlaps); } + finally + { + _texturesLock.ExitReadLock(); + } } if (_overlapInfo.Length != _textureOverlaps.Length) @@ -1025,10 +1068,16 @@ namespace Ryujinx.Graphics.Gpu.Image _cache.Add(texture); } - lock (_textures) + _texturesLock.EnterWriteLock(); + + try { _textures.Add(texture); } + finally + { + _texturesLock.ExitWriteLock(); + } if (partiallyMapped) { @@ -1091,7 +1140,19 @@ namespace Ryujinx.Graphics.Gpu.Image return null; } - int addressMatches = _textures.FindOverlaps(address, ref _textureOverlaps); + int addressMatches; + + _texturesLock.EnterReadLock(); + + try + { + addressMatches = _textures.FindOverlaps(address, ref _textureOverlaps); + } + finally + { + _texturesLock.ExitReadLock(); + } + Texture textureMatch = null; for (int i = 0; i < addressMatches; i++) @@ -1232,10 +1293,16 @@ namespace Ryujinx.Graphics.Gpu.Image /// The texture to be removed public void RemoveTextureFromCache(Texture texture) { - lock (_textures) + _texturesLock.EnterWriteLock(); + + try { _textures.Remove(texture); } + finally + { + _texturesLock.ExitWriteLock(); + } lock (_partiallyMappedTextures) { @@ -1324,13 +1391,19 @@ namespace Ryujinx.Graphics.Gpu.Image /// public void Dispose() { - lock (_textures) + _texturesLock.EnterReadLock(); + + try { foreach (Texture texture in _textures) { texture.Dispose(); } } + finally + { + _texturesLock.ExitReadLock(); + } } } } diff --git a/src/Ryujinx.Graphics.Gpu/Image/TextureGroup.cs b/src/Ryujinx.Graphics.Gpu/Image/TextureGroup.cs index de9c47c97..06ca2c599 100644 --- a/src/Ryujinx.Graphics.Gpu/Image/TextureGroup.cs +++ b/src/Ryujinx.Graphics.Gpu/Image/TextureGroup.cs @@ -88,9 +88,9 @@ namespace Ryujinx.Graphics.Gpu.Image private MultiRange TextureRange => Storage.Range; /// - /// The views list from the storage texture. + /// The views array from the storage texture. /// - private List _views; + private Texture[] _views; private TextureGroupHandle[] _handles; private bool[] _loadNeeded; @@ -645,7 +645,7 @@ namespace Ryujinx.Graphics.Gpu.Image } else { - _flushBuffer = _context.Renderer.CreateBuffer((int)Storage.Size, BufferAccess.FlushPersistent); + _flushBuffer = _context.Renderer.CreateBuffer((int)Storage.Size, BufferAccess.HostMemory); _flushBufferImported = false; } @@ -1074,7 +1074,7 @@ namespace Ryujinx.Graphics.Gpu.Image public void UpdateViews(List views, Texture texture) { // This is saved to calculate overlapping views for each handle. - _views = views; + _views = views.ToArray(); bool layerViews = _hasLayerViews; bool mipViews = _hasMipViews; @@ -1136,9 +1136,13 @@ namespace Ryujinx.Graphics.Gpu.Image /// /// Removes a view from the group, removing it from all overlap lists. /// + /// The views list of the storage texture /// View to remove from the group - public void RemoveView(Texture view) + public void RemoveView(List views, Texture view) { + // This is saved to calculate overlapping views for each handle. + _views = views.ToArray(); + int offset = FindOffset(view); foreach (TextureGroupHandle handle in _handles) @@ -1605,9 +1609,11 @@ namespace Ryujinx.Graphics.Gpu.Image Storage.SignalModifiedDirty(); - if (_views != null) + Texture[] views = _views; + + if (views != null) { - foreach (Texture texture in _views) + foreach (Texture texture in views) { texture.SignalModifiedDirty(); } diff --git a/src/Ryujinx.Graphics.Gpu/Image/TextureGroupHandle.cs b/src/Ryujinx.Graphics.Gpu/Image/TextureGroupHandle.cs index 0af6b7ca8..860922d59 100644 --- a/src/Ryujinx.Graphics.Gpu/Image/TextureGroupHandle.cs +++ b/src/Ryujinx.Graphics.Gpu/Image/TextureGroupHandle.cs @@ -121,7 +121,7 @@ namespace Ryujinx.Graphics.Gpu.Image public TextureGroupHandle(TextureGroup group, int offset, ulong size, - List views, + IEnumerable views, int firstLayer, int firstLevel, int baseSlice, @@ -201,8 +201,8 @@ namespace Ryujinx.Graphics.Gpu.Image /// Calculate a list of which views overlap this handle. /// /// The parent texture group, used to find a view's base CPU VA offset - /// The list of views to search for overlaps - public void RecalculateOverlaps(TextureGroup group, List views) + /// The views to search for overlaps + public void RecalculateOverlaps(TextureGroup group, IEnumerable views) { // Overlaps can be accessed from the memory tracking signal handler, so access must be atomic. lock (Overlaps) diff --git a/src/Ryujinx.Graphics.Gpu/Image/TextureManager.cs b/src/Ryujinx.Graphics.Gpu/Image/TextureManager.cs index 8c2a88727..db2921468 100644 --- a/src/Ryujinx.Graphics.Gpu/Image/TextureManager.cs +++ b/src/Ryujinx.Graphics.Gpu/Image/TextureManager.cs @@ -15,6 +15,7 @@ namespace Ryujinx.Graphics.Gpu.Image private readonly TextureBindingsManager _cpBindingsManager; private readonly TextureBindingsManager _gpBindingsManager; + private readonly TextureBindingsArrayCache _bindingsArrayCache; private readonly TexturePoolCache _texturePoolCache; private readonly SamplerPoolCache _samplerPoolCache; @@ -46,8 +47,9 @@ namespace Ryujinx.Graphics.Gpu.Image TexturePoolCache texturePoolCache = new(context); SamplerPoolCache samplerPoolCache = new(context); - _cpBindingsManager = new TextureBindingsManager(context, channel, texturePoolCache, samplerPoolCache, isCompute: true); - _gpBindingsManager = new TextureBindingsManager(context, channel, texturePoolCache, samplerPoolCache, isCompute: false); + _bindingsArrayCache = new TextureBindingsArrayCache(context, channel); + _cpBindingsManager = new TextureBindingsManager(context, channel, _bindingsArrayCache, texturePoolCache, samplerPoolCache, isCompute: true); + _gpBindingsManager = new TextureBindingsManager(context, channel, _bindingsArrayCache, texturePoolCache, samplerPoolCache, isCompute: false); _texturePoolCache = texturePoolCache; _samplerPoolCache = samplerPoolCache; @@ -384,7 +386,7 @@ namespace Ryujinx.Graphics.Gpu.Image { ulong poolAddress = _channel.MemoryManager.Translate(poolGpuVa); - TexturePool texturePool = _texturePoolCache.FindOrCreate(_channel, poolAddress, maximumId); + TexturePool texturePool = _texturePoolCache.FindOrCreate(_channel, poolAddress, maximumId, _bindingsArrayCache); return texturePool; } diff --git a/src/Ryujinx.Graphics.Gpu/Memory/Buffer.cs b/src/Ryujinx.Graphics.Gpu/Memory/Buffer.cs index d293060b5..e060e0b4f 100644 --- a/src/Ryujinx.Graphics.Gpu/Memory/Buffer.cs +++ b/src/Ryujinx.Graphics.Gpu/Memory/Buffer.cs @@ -10,6 +10,8 @@ using System.Threading; namespace Ryujinx.Graphics.Gpu.Memory { + delegate void BufferFlushAction(ulong address, ulong size, ulong syncNumber); + /// /// Buffer, used to store vertex and index data, uniform and storage buffers, and others. /// @@ -23,7 +25,7 @@ namespace Ryujinx.Graphics.Gpu.Memory /// /// Host buffer handle. /// - public BufferHandle Handle { get; } + public BufferHandle Handle { get; private set; } /// /// Start address of the buffer in guest memory. @@ -60,6 +62,17 @@ namespace Ryujinx.Graphics.Gpu.Memory /// private BufferModifiedRangeList _modifiedRanges = null; + /// + /// A structure that is used to flush buffer data back to a host mapped buffer for cached readback. + /// Only used if the buffer data is explicitly owned by device local memory. + /// + private BufferPreFlush _preFlush = null; + + /// + /// Usage tracking state that determines what type of backing the buffer should use. + /// + public BufferBackingState BackingState; + private readonly MultiRegionHandle _memoryTrackingGranular; private readonly RegionHandle _memoryTracking; @@ -87,6 +100,7 @@ namespace Ryujinx.Graphics.Gpu.Memory /// Physical memory where the buffer is mapped /// Start address of the buffer /// Size of the buffer in bytes + /// The type of usage that created the buffer /// Indicates if the buffer can be used in a sparse buffer mapping /// Buffers which this buffer contains, and will inherit tracking handles from public Buffer( @@ -94,6 +108,7 @@ namespace Ryujinx.Graphics.Gpu.Memory PhysicalMemory physicalMemory, ulong address, ulong size, + BufferStage stage, bool sparseCompatible, IEnumerable baseBuffers = null) { @@ -103,9 +118,11 @@ namespace Ryujinx.Graphics.Gpu.Memory Size = size; SparseCompatible = sparseCompatible; - BufferAccess access = sparseCompatible ? BufferAccess.SparseCompatible : BufferAccess.Default; + BackingState = new BufferBackingState(_context, this, stage, baseBuffers); - Handle = context.Renderer.CreateBuffer((int)size, access, baseBuffers?.MaxBy(x => x.Size).Handle ?? BufferHandle.Null); + BufferAccess access = BackingState.SwitchAccess(this); + + Handle = context.Renderer.CreateBuffer((int)size, access); _useGranular = size > GranularBufferThreshold; @@ -161,6 +178,29 @@ namespace Ryujinx.Graphics.Gpu.Memory _virtualDependenciesLock = new ReaderWriterLockSlim(); } + /// + /// Recreates the backing buffer based on the desired access type + /// reported by the backing state struct. + /// + private void ChangeBacking() + { + BufferAccess access = BackingState.SwitchAccess(this); + + BufferHandle newHandle = _context.Renderer.CreateBuffer((int)Size, access); + + _context.Renderer.Pipeline.CopyBuffer(Handle, newHandle, 0, 0, (int)Size); + + _modifiedRanges?.SelfMigration(); + + // If swtiching from device local to host mapped, pre-flushing data no longer makes sense. + // This is set to null and disposed when the migration fully completes. + _preFlush = null; + + Handle = newHandle; + + _physicalMemory.BufferCache.BufferBackingChanged(this); + } + /// /// Gets a sub-range from the buffer, from a start address til a page boundary after the given size. /// @@ -246,6 +286,7 @@ namespace Ryujinx.Graphics.Gpu.Memory } else { + BackingState.RecordSet(); _context.Renderer.SetBufferData(Handle, 0, _physicalMemory.GetSpan(Address, (int)Size)); CopyToDependantVirtualBuffers(); } @@ -283,15 +324,35 @@ namespace Ryujinx.Graphics.Gpu.Memory _modifiedRanges ??= new BufferModifiedRangeList(_context, this, Flush); } + /// + /// Checks if a backing change is deemed necessary from the given usage. + /// If it is, queues a backing change to happen on the next sync action. + /// + /// Buffer stage that can change backing type + private void TryQueueBackingChange(BufferStage stage) + { + if (BackingState.ShouldChangeBacking(stage)) + { + if (!_syncActionRegistered) + { + _context.RegisterSyncAction(this); + _syncActionRegistered = true; + } + } + } + /// /// Signal that the given region of the buffer has been modified. /// /// The start address of the modified region /// The size of the modified region - public void SignalModified(ulong address, ulong size) + /// Buffer stage that triggered the modification + public void SignalModified(ulong address, ulong size, BufferStage stage) { EnsureRangeList(); + TryQueueBackingChange(stage); + _modifiedRanges.SignalModified(address, size); if (!_syncActionRegistered) @@ -311,6 +372,37 @@ namespace Ryujinx.Graphics.Gpu.Memory _modifiedRanges?.Clear(address, size); } + /// + /// Action to be performed immediately before sync is created. + /// This will copy any buffer ranges designated for pre-flushing. + /// + /// True if the action is a guest syncpoint + public void SyncPreAction(bool syncpoint) + { + if (_referenceCount == 0) + { + return; + } + + if (BackingState.ShouldChangeBacking()) + { + ChangeBacking(); + } + + if (BackingState.IsDeviceLocal) + { + _preFlush ??= new BufferPreFlush(_context, this, FlushImpl); + + if (_preFlush.ShouldCopy) + { + _modifiedRanges?.GetRangesAtSync(Address, Size, _context.SyncNumber, (address, size) => + { + _preFlush.CopyModified(address, size); + }); + } + } + } + /// /// Action to be performed when a syncpoint is reached after modification. /// This will register read/write tracking to flush the buffer from GPU when its memory is used. @@ -466,6 +558,8 @@ namespace Ryujinx.Graphics.Gpu.Memory /// Size of the modified region private void LoadRegion(ulong mAddress, ulong mSize) { + BackingState.RecordSet(); + int offset = (int)(mAddress - Address); _context.Renderer.SetBufferData(Handle, offset, _physicalMemory.GetSpan(mAddress, (int)mSize)); @@ -539,18 +633,84 @@ namespace Ryujinx.Graphics.Gpu.Memory /// Flushes a range of the buffer. /// This writes the range data back into guest memory. /// + /// Buffer handle to flush data from /// Start address of the range /// Size in bytes of the range - public void Flush(ulong address, ulong size) + private void FlushImpl(BufferHandle handle, ulong address, ulong size) { int offset = (int)(address - Address); - using PinnedSpan data = _context.Renderer.GetBufferData(Handle, offset, (int)size); + using PinnedSpan data = _context.Renderer.GetBufferData(handle, offset, (int)size); // TODO: When write tracking shaders, they will need to be aware of changes in overlapping buffers. _physicalMemory.WriteUntracked(address, CopyFromDependantVirtualBuffers(data.Get(), address, size)); } + /// + /// Flushes a range of the buffer. + /// This writes the range data back into guest memory. + /// + /// Start address of the range + /// Size in bytes of the range + private void FlushImpl(ulong address, ulong size) + { + FlushImpl(Handle, address, size); + } + + /// + /// Flushes a range of the buffer from the most optimal source. + /// This writes the range data back into guest memory. + /// + /// Start address of the range + /// Size in bytes of the range + /// Sync number waited for before flushing the data + public void Flush(ulong address, ulong size, ulong syncNumber) + { + BackingState.RecordFlush(); + + BufferPreFlush preFlush = _preFlush; + + if (preFlush != null) + { + preFlush.FlushWithAction(address, size, syncNumber); + } + else + { + FlushImpl(address, size); + } + } + /// + /// Gets an action that disposes the backing buffer using its current handle. + /// Useful for deleting an old copy of the buffer after the handle changes. + /// + /// An action that flushes data from the specified range, using the buffer handle at the time the method is generated + public Action GetSnapshotDisposeAction() + { + BufferHandle handle = Handle; + BufferPreFlush preFlush = _preFlush; + + return () => + { + _context.Renderer.DeleteBuffer(handle); + preFlush?.Dispose(); + }; + } + + /// + /// Gets an action that flushes a range of the buffer using its current handle. + /// Useful for flushing data from old copies of the buffer after the handle changes. + /// + /// An action that flushes data from the specified range, using the buffer handle at the time the method is generated + public BufferFlushAction GetSnapshotFlushAction() + { + BufferHandle handle = Handle; + + return (ulong address, ulong size, ulong _) => + { + FlushImpl(handle, address, size); + }; + } + /// /// Align a given address and size region to page boundaries. /// @@ -857,6 +1017,8 @@ namespace Ryujinx.Graphics.Gpu.Memory _modifiedRanges?.Clear(); _context.Renderer.DeleteBuffer(Handle); + _preFlush?.Dispose(); + _preFlush = null; UnmappedSequence++; } diff --git a/src/Ryujinx.Graphics.Gpu/Memory/BufferBackingState.cs b/src/Ryujinx.Graphics.Gpu/Memory/BufferBackingState.cs new file mode 100644 index 000000000..3f65131e6 --- /dev/null +++ b/src/Ryujinx.Graphics.Gpu/Memory/BufferBackingState.cs @@ -0,0 +1,294 @@ +using Ryujinx.Graphics.GAL; +using System; +using System.Collections.Generic; + +namespace Ryujinx.Graphics.Gpu.Memory +{ + /// + /// Type of backing memory. + /// In ascending order of priority when merging multiple buffer backing states. + /// + internal enum BufferBackingType + { + HostMemory, + DeviceMemory, + DeviceMemoryWithFlush + } + + /// + /// Keeps track of buffer usage to decide what memory heap that buffer memory is placed on. + /// Dedicated GPUs prefer certain types of resources to be device local, + /// and if we need data to be read back, we might prefer that they're in host memory. + /// + /// The measurements recorded here compare to a set of heruristics (thresholds and conditions) + /// that appear to produce good performance in most software. + /// + internal struct BufferBackingState + { + private const int DeviceLocalSizeThreshold = 256 * 1024; // 256kb + + private const int SetCountThreshold = 100; + private const int WriteCountThreshold = 50; + private const int FlushCountThreshold = 5; + private const int DeviceLocalForceExpiry = 100; + + public readonly bool IsDeviceLocal => _activeType != BufferBackingType.HostMemory; + + private readonly SystemMemoryType _systemMemoryType; + private BufferBackingType _activeType; + private BufferBackingType _desiredType; + + private bool _canSwap; + + private int _setCount; + private int _writeCount; + private int _flushCount; + private int _flushTemp; + private int _lastFlushWrite; + private int _deviceLocalForceCount; + + private readonly int _size; + + /// + /// Initialize the buffer backing state for a given parent buffer. + /// + /// GPU context + /// Parent buffer + /// Initial buffer stage + /// Buffers to inherit state from + public BufferBackingState(GpuContext context, Buffer parent, BufferStage stage, IEnumerable baseBuffers = null) + { + _size = (int)parent.Size; + _systemMemoryType = context.Capabilities.MemoryType; + + // Backend managed is always auto, unified memory is always host. + _desiredType = BufferBackingType.HostMemory; + _canSwap = _systemMemoryType != SystemMemoryType.BackendManaged && _systemMemoryType != SystemMemoryType.UnifiedMemory; + + if (_canSwap) + { + // Might want to start certain buffers as being device local, + // and the usage might also lock those buffers into being device local. + + BufferStage storageFlags = stage & BufferStage.StorageMask; + + if (parent.Size > DeviceLocalSizeThreshold && baseBuffers == null) + { + _desiredType = BufferBackingType.DeviceMemory; + } + + if (storageFlags != 0) + { + // Storage buffer bindings may require special treatment. + + var rawStage = stage & BufferStage.StageMask; + + if (rawStage == BufferStage.Fragment) + { + // Fragment read should start device local. + + _desiredType = BufferBackingType.DeviceMemory; + + if (storageFlags != BufferStage.StorageRead) + { + // Fragment write should stay device local until the use doesn't happen anymore. + + _deviceLocalForceCount = DeviceLocalForceExpiry; + } + } + + // TODO: Might be nice to force atomic access to be device local for any stage. + } + + if (baseBuffers != null) + { + foreach (Buffer buffer in baseBuffers) + { + CombineState(buffer.BackingState); + } + } + } + } + + /// + /// Combine buffer backing types, selecting the one with highest priority. + /// + /// First buffer backing type + /// Second buffer backing type + /// Combined buffer backing type + private static BufferBackingType CombineTypes(BufferBackingType left, BufferBackingType right) + { + return (BufferBackingType)Math.Max((int)left, (int)right); + } + + /// + /// Combine the state from the given buffer backing state with this one, + /// so that the state isn't lost when migrating buffers. + /// + /// Buffer state to combine into this state + private void CombineState(BufferBackingState oldState) + { + _setCount += oldState._setCount; + _writeCount += oldState._writeCount; + _flushCount += oldState._flushCount; + _flushTemp += oldState._flushTemp; + _lastFlushWrite = -1; + _deviceLocalForceCount = Math.Max(_deviceLocalForceCount, oldState._deviceLocalForceCount); + + _canSwap &= oldState._canSwap; + + _desiredType = CombineTypes(_desiredType, oldState._desiredType); + } + + /// + /// Get the buffer access for the desired backing type, and record that type as now being active. + /// + /// Parent buffer + /// Buffer access + public BufferAccess SwitchAccess(Buffer parent) + { + BufferAccess access = parent.SparseCompatible ? BufferAccess.SparseCompatible : BufferAccess.Default; + + bool isBackendManaged = _systemMemoryType == SystemMemoryType.BackendManaged; + + if (!isBackendManaged) + { + switch (_desiredType) + { + case BufferBackingType.HostMemory: + access |= BufferAccess.HostMemory; + break; + case BufferBackingType.DeviceMemory: + access |= BufferAccess.DeviceMemory; + break; + case BufferBackingType.DeviceMemoryWithFlush: + access |= BufferAccess.DeviceMemoryMapped; + break; + } + } + + _activeType = _desiredType; + + return access; + } + + /// + /// Record when data has been uploaded to the buffer. + /// + public void RecordSet() + { + _setCount++; + + ConsiderUseCounts(); + } + + /// + /// Record when data has been flushed from the buffer. + /// + public void RecordFlush() + { + if (_lastFlushWrite != _writeCount) + { + // If it's on the same page as the last flush, ignore it. + _lastFlushWrite = _writeCount; + _flushCount++; + } + } + + /// + /// Determine if the buffer backing should be changed. + /// + /// True if the desired backing type is different from the current type + public readonly bool ShouldChangeBacking() + { + return _desiredType != _activeType; + } + + /// + /// Determine if the buffer backing should be changed, considering a new use with the given buffer stage. + /// + /// Buffer stage for the use + /// True if the desired backing type is different from the current type + public bool ShouldChangeBacking(BufferStage stage) + { + if (!_canSwap) + { + return false; + } + + BufferStage storageFlags = stage & BufferStage.StorageMask; + + if (storageFlags != 0) + { + if (storageFlags != BufferStage.StorageRead) + { + // Storage write. + _writeCount++; + + var rawStage = stage & BufferStage.StageMask; + + if (rawStage == BufferStage.Fragment) + { + // Switch to device memory, swap back only if this use disappears. + + _desiredType = CombineTypes(_desiredType, BufferBackingType.DeviceMemory); + _deviceLocalForceCount = DeviceLocalForceExpiry; + + // TODO: Might be nice to force atomic access to be device local for any stage. + } + } + + ConsiderUseCounts(); + } + + return _desiredType != _activeType; + } + + /// + /// Evaluate the current counts to determine what the buffer's desired backing type is. + /// This method depends on heuristics devised by testing a variety of software. + /// + private void ConsiderUseCounts() + { + if (_canSwap) + { + if (_writeCount >= WriteCountThreshold || _setCount >= SetCountThreshold || _flushCount >= FlushCountThreshold) + { + if (_deviceLocalForceCount > 0 && --_deviceLocalForceCount != 0) + { + // Some buffer usage demanded that the buffer stay device local. + // The desired type was selected when this counter was set. + } + else if (_flushCount > 0 || _flushTemp-- > 0) + { + // Buffers that flush should ideally be mapped in host address space for easy copies. + // If the buffer is large it will do better on GPU memory, as there will be more writes than data flushes (typically individual pages). + // If it is small, then it's likely most of the buffer will be flushed so we want it on host memory, as access is cached. + _desiredType = _size > DeviceLocalSizeThreshold ? BufferBackingType.DeviceMemoryWithFlush : BufferBackingType.HostMemory; + } + else if (_writeCount >= WriteCountThreshold) + { + // Buffers that are written often should ideally be in the device local heap. (Storage buffers) + _desiredType = BufferBackingType.DeviceMemory; + } + else if (_setCount > SetCountThreshold) + { + // Buffers that have their data set often should ideally be host mapped. (Constant buffers) + _desiredType = BufferBackingType.HostMemory; + } + + // It's harder for a buffer that is flushed to revert to another type of mapping. + if (_flushCount > 0) + { + _flushTemp = 1000; + } + + _lastFlushWrite = -1; + _flushCount = 0; + _writeCount = 0; + _setCount = 0; + } + } + } + } +} diff --git a/src/Ryujinx.Graphics.Gpu/Memory/BufferCache.cs b/src/Ryujinx.Graphics.Gpu/Memory/BufferCache.cs index c6284780d..66d2cdb62 100644 --- a/src/Ryujinx.Graphics.Gpu/Memory/BufferCache.cs +++ b/src/Ryujinx.Graphics.Gpu/Memory/BufferCache.cs @@ -107,8 +107,9 @@ namespace Ryujinx.Graphics.Gpu.Memory /// GPU memory manager where the buffer is mapped /// Start GPU virtual address of the buffer /// Size in bytes of the buffer + /// The type of usage that created the buffer /// Contiguous physical range of the buffer, after address translation - public MultiRange TranslateAndCreateBuffer(MemoryManager memoryManager, ulong gpuVa, ulong size) + public MultiRange TranslateAndCreateBuffer(MemoryManager memoryManager, ulong gpuVa, ulong size, BufferStage stage) { if (gpuVa == 0) { @@ -119,7 +120,7 @@ namespace Ryujinx.Graphics.Gpu.Memory if (address != MemoryManager.PteUnmapped) { - CreateBuffer(address, size); + CreateBuffer(address, size, stage); } return new MultiRange(address, size); @@ -132,8 +133,9 @@ namespace Ryujinx.Graphics.Gpu.Memory /// GPU memory manager where the buffer is mapped /// Start GPU virtual address of the buffer /// Size in bytes of the buffer + /// The type of usage that created the buffer /// Physical ranges of the buffer, after address translation - public MultiRange TranslateAndCreateMultiBuffers(MemoryManager memoryManager, ulong gpuVa, ulong size) + public MultiRange TranslateAndCreateMultiBuffers(MemoryManager memoryManager, ulong gpuVa, ulong size, BufferStage stage) { if (gpuVa == 0) { @@ -149,7 +151,7 @@ namespace Ryujinx.Graphics.Gpu.Memory return range; } - CreateBuffer(range); + CreateBuffer(range, stage); return range; } @@ -161,8 +163,9 @@ namespace Ryujinx.Graphics.Gpu.Memory /// GPU memory manager where the buffer is mapped /// Start GPU virtual address of the buffer /// Size in bytes of the buffer + /// The type of usage that created the buffer /// Physical ranges of the buffer, after address translation - public MultiRange TranslateAndCreateMultiBuffersPhysicalOnly(MemoryManager memoryManager, ulong gpuVa, ulong size) + public MultiRange TranslateAndCreateMultiBuffersPhysicalOnly(MemoryManager memoryManager, ulong gpuVa, ulong size, BufferStage stage) { if (gpuVa == 0) { @@ -186,11 +189,11 @@ namespace Ryujinx.Graphics.Gpu.Memory { if (range.Count > 1) { - CreateBuffer(subRange.Address, subRange.Size, SparseBufferAlignmentSize); + CreateBuffer(subRange.Address, subRange.Size, stage, SparseBufferAlignmentSize); } else { - CreateBuffer(subRange.Address, subRange.Size); + CreateBuffer(subRange.Address, subRange.Size, stage); } } } @@ -203,11 +206,12 @@ namespace Ryujinx.Graphics.Gpu.Memory /// This can be used to ensure the existance of a buffer. /// /// Physical ranges of memory where the buffer data is located - public void CreateBuffer(MultiRange range) + /// The type of usage that created the buffer + public void CreateBuffer(MultiRange range, BufferStage stage) { if (range.Count > 1) { - CreateMultiRangeBuffer(range); + CreateMultiRangeBuffer(range, stage); } else { @@ -215,7 +219,7 @@ namespace Ryujinx.Graphics.Gpu.Memory if (subRange.Address != MemoryManager.PteUnmapped) { - CreateBuffer(subRange.Address, subRange.Size); + CreateBuffer(subRange.Address, subRange.Size, stage); } } } @@ -226,7 +230,8 @@ namespace Ryujinx.Graphics.Gpu.Memory /// /// Address of the buffer in memory /// Size of the buffer in bytes - public void CreateBuffer(ulong address, ulong size) + /// The type of usage that created the buffer + public void CreateBuffer(ulong address, ulong size, BufferStage stage) { ulong endAddress = address + size; @@ -239,7 +244,7 @@ namespace Ryujinx.Graphics.Gpu.Memory alignedEndAddress += BufferAlignmentSize; } - CreateBufferAligned(alignedAddress, alignedEndAddress - alignedAddress); + CreateBufferAligned(alignedAddress, alignedEndAddress - alignedAddress, stage); } /// @@ -248,8 +253,9 @@ namespace Ryujinx.Graphics.Gpu.Memory /// /// Address of the buffer in memory /// Size of the buffer in bytes + /// The type of usage that created the buffer /// Alignment of the start address of the buffer in bytes - public void CreateBuffer(ulong address, ulong size, ulong alignment) + public void CreateBuffer(ulong address, ulong size, BufferStage stage, ulong alignment) { ulong alignmentMask = alignment - 1; ulong pageAlignmentMask = BufferAlignmentMask; @@ -264,7 +270,7 @@ namespace Ryujinx.Graphics.Gpu.Memory alignedEndAddress += pageAlignmentMask; } - CreateBufferAligned(alignedAddress, alignedEndAddress - alignedAddress, alignment); + CreateBufferAligned(alignedAddress, alignedEndAddress - alignedAddress, stage, alignment); } /// @@ -272,7 +278,8 @@ namespace Ryujinx.Graphics.Gpu.Memory /// if it does not exist yet. /// /// Physical ranges of memory - private void CreateMultiRangeBuffer(MultiRange range) + /// The type of usage that created the buffer + private void CreateMultiRangeBuffer(MultiRange range, BufferStage stage) { // Ensure all non-contiguous buffer we might use are sparse aligned. for (int i = 0; i < range.Count; i++) @@ -281,7 +288,7 @@ namespace Ryujinx.Graphics.Gpu.Memory if (subRange.Address != MemoryManager.PteUnmapped) { - CreateBuffer(subRange.Address, subRange.Size, SparseBufferAlignmentSize); + CreateBuffer(subRange.Address, subRange.Size, stage, SparseBufferAlignmentSize); } } @@ -431,9 +438,9 @@ namespace Ryujinx.Graphics.Gpu.Memory result.EndGpuAddress < gpuVa + size || result.UnmappedSequence != result.Buffer.UnmappedSequence) { - MultiRange range = TranslateAndCreateBuffer(memoryManager, gpuVa, size); + MultiRange range = TranslateAndCreateBuffer(memoryManager, gpuVa, size, BufferStage.Internal); ulong address = range.GetSubRange(0).Address; - result = new BufferCacheEntry(address, gpuVa, GetBuffer(address, size)); + result = new BufferCacheEntry(address, gpuVa, GetBuffer(address, size, BufferStage.Internal)); _dirtyCache[gpuVa] = result; } @@ -466,9 +473,9 @@ namespace Ryujinx.Graphics.Gpu.Memory result.EndGpuAddress < alignedEndGpuVa || result.UnmappedSequence != result.Buffer.UnmappedSequence) { - MultiRange range = TranslateAndCreateBuffer(memoryManager, alignedGpuVa, size); + MultiRange range = TranslateAndCreateBuffer(memoryManager, alignedGpuVa, size, BufferStage.None); ulong address = range.GetSubRange(0).Address; - result = new BufferCacheEntry(address, alignedGpuVa, GetBuffer(address, size)); + result = new BufferCacheEntry(address, alignedGpuVa, GetBuffer(address, size, BufferStage.None)); _modifiedCache[alignedGpuVa] = result; } @@ -485,7 +492,8 @@ namespace Ryujinx.Graphics.Gpu.Memory /// /// Address of the buffer in guest memory /// Size in bytes of the buffer - private void CreateBufferAligned(ulong address, ulong size) + /// The type of usage that created the buffer + private void CreateBufferAligned(ulong address, ulong size, BufferStage stage) { Buffer[] overlaps = _bufferOverlaps; int overlapsCount = _buffers.FindOverlapsNonOverlapping(address, size, ref overlaps); @@ -546,13 +554,13 @@ namespace Ryujinx.Graphics.Gpu.Memory ulong newSize = endAddress - address; - CreateBufferAligned(address, newSize, anySparseCompatible, overlaps, overlapsCount); + CreateBufferAligned(address, newSize, stage, anySparseCompatible, overlaps, overlapsCount); } } else { // No overlap, just create a new buffer. - Buffer buffer = new(_context, _physicalMemory, address, size, sparseCompatible: false); + Buffer buffer = new(_context, _physicalMemory, address, size, stage, sparseCompatible: false); lock (_buffers) { @@ -570,8 +578,9 @@ namespace Ryujinx.Graphics.Gpu.Memory /// /// Address of the buffer in guest memory /// Size in bytes of the buffer + /// The type of usage that created the buffer /// Alignment of the start address of the buffer - private void CreateBufferAligned(ulong address, ulong size, ulong alignment) + private void CreateBufferAligned(ulong address, ulong size, BufferStage stage, ulong alignment) { Buffer[] overlaps = _bufferOverlaps; int overlapsCount = _buffers.FindOverlapsNonOverlapping(address, size, ref overlaps); @@ -624,13 +633,13 @@ namespace Ryujinx.Graphics.Gpu.Memory ulong newSize = endAddress - address; - CreateBufferAligned(address, newSize, sparseAligned, overlaps, overlapsCount); + CreateBufferAligned(address, newSize, stage, sparseAligned, overlaps, overlapsCount); } } else { // No overlap, just create a new buffer. - Buffer buffer = new(_context, _physicalMemory, address, size, sparseAligned); + Buffer buffer = new(_context, _physicalMemory, address, size, stage, sparseAligned); lock (_buffers) { @@ -648,12 +657,13 @@ namespace Ryujinx.Graphics.Gpu.Memory /// /// Address of the buffer in guest memory /// Size in bytes of the buffer + /// The type of usage that created the buffer /// Indicates if the buffer can be used in a sparse buffer mapping /// Buffers overlapping the range /// Total of overlaps - private void CreateBufferAligned(ulong address, ulong size, bool sparseCompatible, Buffer[] overlaps, int overlapsCount) + private void CreateBufferAligned(ulong address, ulong size, BufferStage stage, bool sparseCompatible, Buffer[] overlaps, int overlapsCount) { - Buffer newBuffer = new Buffer(_context, _physicalMemory, address, size, sparseCompatible, overlaps.Take(overlapsCount)); + Buffer newBuffer = new Buffer(_context, _physicalMemory, address, size, stage, sparseCompatible, overlaps.Take(overlapsCount)); lock (_buffers) { @@ -704,7 +714,7 @@ namespace Ryujinx.Graphics.Gpu.Memory for (int index = 0; index < overlapCount; index++) { - CreateMultiRangeBuffer(overlaps[index].Range); + CreateMultiRangeBuffer(overlaps[index].Range, BufferStage.None); } } @@ -731,8 +741,8 @@ namespace Ryujinx.Graphics.Gpu.Memory /// Size in bytes of the copy public void CopyBuffer(MemoryManager memoryManager, ulong srcVa, ulong dstVa, ulong size) { - MultiRange srcRange = TranslateAndCreateMultiBuffersPhysicalOnly(memoryManager, srcVa, size); - MultiRange dstRange = TranslateAndCreateMultiBuffersPhysicalOnly(memoryManager, dstVa, size); + MultiRange srcRange = TranslateAndCreateMultiBuffersPhysicalOnly(memoryManager, srcVa, size, BufferStage.Copy); + MultiRange dstRange = TranslateAndCreateMultiBuffersPhysicalOnly(memoryManager, dstVa, size, BufferStage.Copy); if (srcRange.Count == 1 && dstRange.Count == 1) { @@ -788,8 +798,8 @@ namespace Ryujinx.Graphics.Gpu.Memory /// Size in bytes of the copy private void CopyBufferSingleRange(MemoryManager memoryManager, ulong srcAddress, ulong dstAddress, ulong size) { - Buffer srcBuffer = GetBuffer(srcAddress, size); - Buffer dstBuffer = GetBuffer(dstAddress, size); + Buffer srcBuffer = GetBuffer(srcAddress, size, BufferStage.Copy); + Buffer dstBuffer = GetBuffer(dstAddress, size, BufferStage.Copy); int srcOffset = (int)(srcAddress - srcBuffer.Address); int dstOffset = (int)(dstAddress - dstBuffer.Address); @@ -803,7 +813,7 @@ namespace Ryujinx.Graphics.Gpu.Memory if (srcBuffer.IsModified(srcAddress, size)) { - dstBuffer.SignalModified(dstAddress, size); + dstBuffer.SignalModified(dstAddress, size, BufferStage.Copy); } else { @@ -828,12 +838,12 @@ namespace Ryujinx.Graphics.Gpu.Memory /// Value to be written into the buffer public void ClearBuffer(MemoryManager memoryManager, ulong gpuVa, ulong size, uint value) { - MultiRange range = TranslateAndCreateMultiBuffersPhysicalOnly(memoryManager, gpuVa, size); + MultiRange range = TranslateAndCreateMultiBuffersPhysicalOnly(memoryManager, gpuVa, size, BufferStage.Copy); for (int index = 0; index < range.Count; index++) { MemoryRange subRange = range.GetSubRange(index); - Buffer buffer = GetBuffer(subRange.Address, subRange.Size); + Buffer buffer = GetBuffer(subRange.Address, subRange.Size, BufferStage.Copy); int offset = (int)(subRange.Address - buffer.Address); @@ -849,18 +859,19 @@ namespace Ryujinx.Graphics.Gpu.Memory /// Gets a buffer sub-range starting at a given memory address, aligned to the next page boundary. /// /// Physical regions of memory where the buffer is mapped + /// Buffer stage that triggered the access /// Whether the buffer will be written to by this use /// The buffer sub-range starting at the given memory address - public BufferRange GetBufferRangeAligned(MultiRange range, bool write = false) + public BufferRange GetBufferRangeAligned(MultiRange range, BufferStage stage, bool write = false) { if (range.Count > 1) { - return GetBuffer(range, write).GetRange(range); + return GetBuffer(range, stage, write).GetRange(range); } else { MemoryRange subRange = range.GetSubRange(0); - return GetBuffer(subRange.Address, subRange.Size, write).GetRangeAligned(subRange.Address, subRange.Size, write); + return GetBuffer(subRange.Address, subRange.Size, stage, write).GetRangeAligned(subRange.Address, subRange.Size, write); } } @@ -868,18 +879,19 @@ namespace Ryujinx.Graphics.Gpu.Memory /// Gets a buffer sub-range for a given memory range. /// /// Physical regions of memory where the buffer is mapped + /// Buffer stage that triggered the access /// Whether the buffer will be written to by this use /// The buffer sub-range for the given range - public BufferRange GetBufferRange(MultiRange range, bool write = false) + public BufferRange GetBufferRange(MultiRange range, BufferStage stage, bool write = false) { if (range.Count > 1) { - return GetBuffer(range, write).GetRange(range); + return GetBuffer(range, stage, write).GetRange(range); } else { MemoryRange subRange = range.GetSubRange(0); - return GetBuffer(subRange.Address, subRange.Size, write).GetRange(subRange.Address, subRange.Size, write); + return GetBuffer(subRange.Address, subRange.Size, stage, write).GetRange(subRange.Address, subRange.Size, write); } } @@ -888,9 +900,10 @@ namespace Ryujinx.Graphics.Gpu.Memory /// A buffer overlapping with the specified range is assumed to already exist on the cache. /// /// Physical regions of memory where the buffer is mapped + /// Buffer stage that triggered the access /// Whether the buffer will be written to by this use /// The buffer where the range is fully contained - private MultiRangeBuffer GetBuffer(MultiRange range, bool write = false) + private MultiRangeBuffer GetBuffer(MultiRange range, BufferStage stage, bool write = false) { for (int i = 0; i < range.Count; i++) { @@ -902,7 +915,7 @@ namespace Ryujinx.Graphics.Gpu.Memory if (write) { - subBuffer.SignalModified(subRange.Address, subRange.Size); + subBuffer.SignalModified(subRange.Address, subRange.Size, stage); } } @@ -935,9 +948,10 @@ namespace Ryujinx.Graphics.Gpu.Memory /// /// Start address of the memory range /// Size in bytes of the memory range + /// Buffer stage that triggered the access /// Whether the buffer will be written to by this use /// The buffer where the range is fully contained - private Buffer GetBuffer(ulong address, ulong size, bool write = false) + private Buffer GetBuffer(ulong address, ulong size, BufferStage stage, bool write = false) { Buffer buffer; @@ -950,7 +964,7 @@ namespace Ryujinx.Graphics.Gpu.Memory if (write) { - buffer.SignalModified(address, size); + buffer.SignalModified(address, size, stage); } } else @@ -1004,6 +1018,18 @@ namespace Ryujinx.Graphics.Gpu.Memory } } + /// + /// Signal that the given buffer's handle has changed, + /// forcing rebind and any overlapping multi-range buffers to be recreated. + /// + /// The buffer that has changed handle + public void BufferBackingChanged(Buffer buffer) + { + NotifyBuffersModified?.Invoke(); + + RecreateMultiRangeBuffers(buffer.Address, buffer.Size); + } + /// /// Prune any invalid entries from a quick access dictionary. /// diff --git a/src/Ryujinx.Graphics.Gpu/Memory/BufferManager.cs b/src/Ryujinx.Graphics.Gpu/Memory/BufferManager.cs index 8f2201e0a..26d9501c6 100644 --- a/src/Ryujinx.Graphics.Gpu/Memory/BufferManager.cs +++ b/src/Ryujinx.Graphics.Gpu/Memory/BufferManager.cs @@ -156,7 +156,7 @@ namespace Ryujinx.Graphics.Gpu.Memory /// Type of each index buffer element public void SetIndexBuffer(ulong gpuVa, ulong size, IndexType type) { - MultiRange range = _channel.MemoryManager.Physical.BufferCache.TranslateAndCreateBuffer(_channel.MemoryManager, gpuVa, size); + MultiRange range = _channel.MemoryManager.Physical.BufferCache.TranslateAndCreateBuffer(_channel.MemoryManager, gpuVa, size, BufferStage.IndexBuffer); _indexBuffer.Range = range; _indexBuffer.Type = type; @@ -186,7 +186,7 @@ namespace Ryujinx.Graphics.Gpu.Memory /// Vertex divisor of the buffer, for instanced draws public void SetVertexBuffer(int index, ulong gpuVa, ulong size, int stride, int divisor) { - MultiRange range = _channel.MemoryManager.Physical.BufferCache.TranslateAndCreateBuffer(_channel.MemoryManager, gpuVa, size); + MultiRange range = _channel.MemoryManager.Physical.BufferCache.TranslateAndCreateBuffer(_channel.MemoryManager, gpuVa, size, BufferStage.VertexBuffer); _vertexBuffers[index].Range = range; _vertexBuffers[index].Stride = stride; @@ -213,7 +213,7 @@ namespace Ryujinx.Graphics.Gpu.Memory /// Size in bytes of the transform feedback buffer public void SetTransformFeedbackBuffer(int index, ulong gpuVa, ulong size) { - MultiRange range = _channel.MemoryManager.Physical.BufferCache.TranslateAndCreateMultiBuffers(_channel.MemoryManager, gpuVa, size); + MultiRange range = _channel.MemoryManager.Physical.BufferCache.TranslateAndCreateMultiBuffers(_channel.MemoryManager, gpuVa, size, BufferStage.TransformFeedback); _transformFeedbackBuffers[index] = new BufferBounds(range); _transformFeedbackBuffersDirty = true; @@ -260,7 +260,7 @@ namespace Ryujinx.Graphics.Gpu.Memory gpuVa = BitUtils.AlignDown(gpuVa, (ulong)_context.Capabilities.StorageBufferOffsetAlignment); - MultiRange range = _channel.MemoryManager.Physical.BufferCache.TranslateAndCreateMultiBuffers(_channel.MemoryManager, gpuVa, size); + MultiRange range = _channel.MemoryManager.Physical.BufferCache.TranslateAndCreateMultiBuffers(_channel.MemoryManager, gpuVa, size, BufferStageUtils.ComputeStorage(flags)); _cpStorageBuffers.SetBounds(index, range, flags); } @@ -284,7 +284,7 @@ namespace Ryujinx.Graphics.Gpu.Memory gpuVa = BitUtils.AlignDown(gpuVa, (ulong)_context.Capabilities.StorageBufferOffsetAlignment); - MultiRange range = _channel.MemoryManager.Physical.BufferCache.TranslateAndCreateMultiBuffers(_channel.MemoryManager, gpuVa, size); + MultiRange range = _channel.MemoryManager.Physical.BufferCache.TranslateAndCreateMultiBuffers(_channel.MemoryManager, gpuVa, size, BufferStageUtils.GraphicsStorage(stage, flags)); if (!buffers.Buffers[index].Range.Equals(range)) { @@ -303,7 +303,7 @@ namespace Ryujinx.Graphics.Gpu.Memory /// Size in bytes of the storage buffer public void SetComputeUniformBuffer(int index, ulong gpuVa, ulong size) { - MultiRange range = _channel.MemoryManager.Physical.BufferCache.TranslateAndCreateBuffer(_channel.MemoryManager, gpuVa, size); + MultiRange range = _channel.MemoryManager.Physical.BufferCache.TranslateAndCreateBuffer(_channel.MemoryManager, gpuVa, size, BufferStage.Compute); _cpUniformBuffers.SetBounds(index, range); } @@ -318,7 +318,7 @@ namespace Ryujinx.Graphics.Gpu.Memory /// Size in bytes of the storage buffer public void SetGraphicsUniformBuffer(int stage, int index, ulong gpuVa, ulong size) { - MultiRange range = _channel.MemoryManager.Physical.BufferCache.TranslateAndCreateBuffer(_channel.MemoryManager, gpuVa, size); + MultiRange range = _channel.MemoryManager.Physical.BufferCache.TranslateAndCreateBuffer(_channel.MemoryManager, gpuVa, size, BufferStageUtils.FromShaderStage(stage)); _gpUniformBuffers[stage].SetBounds(index, range); _gpUniformBuffersDirty = true; @@ -502,7 +502,7 @@ namespace Ryujinx.Graphics.Gpu.Memory foreach (var binding in _bufferTextures) { var isStore = binding.BindingInfo.Flags.HasFlag(TextureUsageFlags.ImageStore); - var range = bufferCache.GetBufferRange(binding.Range, isStore); + var range = bufferCache.GetBufferRange(binding.Range, BufferStageUtils.TextureBuffer(binding.Stage, binding.BindingInfo.Flags), isStore); binding.Texture.SetStorage(range); // The texture must be rebound to use the new storage if it was updated. @@ -526,7 +526,7 @@ namespace Ryujinx.Graphics.Gpu.Memory foreach (var binding in _bufferTextureArrays) { - var range = bufferCache.GetBufferRange(binding.Range); + var range = bufferCache.GetBufferRange(binding.Range, BufferStage.None); binding.Texture.SetStorage(range); textureArray[0] = binding.Texture; @@ -536,7 +536,7 @@ namespace Ryujinx.Graphics.Gpu.Memory foreach (var binding in _bufferImageArrays) { var isStore = binding.BindingInfo.Flags.HasFlag(TextureUsageFlags.ImageStore); - var range = bufferCache.GetBufferRange(binding.Range, isStore); + var range = bufferCache.GetBufferRange(binding.Range, BufferStage.None, isStore); binding.Texture.SetStorage(range); textureArray[0] = binding.Texture; @@ -565,7 +565,7 @@ namespace Ryujinx.Graphics.Gpu.Memory if (!_indexBuffer.Range.IsUnmapped) { - BufferRange buffer = bufferCache.GetBufferRange(_indexBuffer.Range); + BufferRange buffer = bufferCache.GetBufferRange(_indexBuffer.Range, BufferStage.IndexBuffer); _context.Renderer.Pipeline.SetIndexBuffer(buffer, _indexBuffer.Type); } @@ -597,7 +597,7 @@ namespace Ryujinx.Graphics.Gpu.Memory continue; } - BufferRange buffer = bufferCache.GetBufferRange(vb.Range); + BufferRange buffer = bufferCache.GetBufferRange(vb.Range, BufferStage.VertexBuffer); vertexBuffers[index] = new VertexBufferDescriptor(buffer, vb.Stride, vb.Divisor); } @@ -637,7 +637,7 @@ namespace Ryujinx.Graphics.Gpu.Memory continue; } - tfbs[index] = bufferCache.GetBufferRange(tfb.Range, write: true); + tfbs[index] = bufferCache.GetBufferRange(tfb.Range, BufferStage.TransformFeedback, write: true); } _context.Renderer.Pipeline.SetTransformFeedbackBuffers(tfbs); @@ -684,7 +684,7 @@ namespace Ryujinx.Graphics.Gpu.Memory _context.SupportBufferUpdater.SetTfeOffset(index, tfeOffset); - buffers[index] = new BufferAssignment(index, bufferCache.GetBufferRange(range, write: true)); + buffers[index] = new BufferAssignment(index, bufferCache.GetBufferRange(range, BufferStage.TransformFeedback, write: true)); } } @@ -751,6 +751,7 @@ namespace Ryujinx.Graphics.Gpu.Memory for (ShaderStage stage = ShaderStage.Vertex; stage <= ShaderStage.Fragment; stage++) { ref var buffers = ref bindings[(int)stage - 1]; + BufferStage bufferStage = BufferStageUtils.FromShaderStage(stage); for (int index = 0; index < buffers.Count; index++) { @@ -762,8 +763,8 @@ namespace Ryujinx.Graphics.Gpu.Memory { var isWrite = bounds.Flags.HasFlag(BufferUsageFlags.Write); var range = isStorage - ? bufferCache.GetBufferRangeAligned(bounds.Range, isWrite) - : bufferCache.GetBufferRange(bounds.Range); + ? bufferCache.GetBufferRangeAligned(bounds.Range, bufferStage | BufferStageUtils.FromUsage(bounds.Flags), isWrite) + : bufferCache.GetBufferRange(bounds.Range, bufferStage); ranges[rangesCount++] = new BufferAssignment(bindingInfo.Binding, range); } @@ -799,8 +800,8 @@ namespace Ryujinx.Graphics.Gpu.Memory { var isWrite = bounds.Flags.HasFlag(BufferUsageFlags.Write); var range = isStorage - ? bufferCache.GetBufferRangeAligned(bounds.Range, isWrite) - : bufferCache.GetBufferRange(bounds.Range); + ? bufferCache.GetBufferRangeAligned(bounds.Range, BufferStageUtils.ComputeStorage(bounds.Flags), isWrite) + : bufferCache.GetBufferRange(bounds.Range, BufferStage.Compute); ranges[rangesCount++] = new BufferAssignment(bindingInfo.Binding, range); } @@ -875,7 +876,7 @@ namespace Ryujinx.Graphics.Gpu.Memory Format format, bool isImage) { - _channel.MemoryManager.Physical.BufferCache.CreateBuffer(range); + _channel.MemoryManager.Physical.BufferCache.CreateBuffer(range, BufferStageUtils.TextureBuffer(stage, bindingInfo.Flags)); _bufferTextures.Add(new BufferTextureBinding(stage, texture, range, bindingInfo, format, isImage)); } @@ -883,6 +884,7 @@ namespace Ryujinx.Graphics.Gpu.Memory /// /// Sets the buffer storage of a buffer texture array element. This will be bound when the buffer manager commits bindings. /// + /// Shader stage accessing the texture /// Texture array where the element will be inserted /// Buffer texture /// Physical ranges of memory where the buffer texture data is located @@ -890,6 +892,7 @@ namespace Ryujinx.Graphics.Gpu.Memory /// Index of the binding on the array /// Format of the buffer texture public void SetBufferTextureStorage( + ShaderStage stage, ITextureArray array, ITexture texture, MultiRange range, @@ -897,7 +900,7 @@ namespace Ryujinx.Graphics.Gpu.Memory int index, Format format) { - _channel.MemoryManager.Physical.BufferCache.CreateBuffer(range); + _channel.MemoryManager.Physical.BufferCache.CreateBuffer(range, BufferStageUtils.TextureBuffer(stage, bindingInfo.Flags)); _bufferTextureArrays.Add(new BufferTextureArrayBinding(array, texture, range, bindingInfo, index, format)); } @@ -905,6 +908,7 @@ namespace Ryujinx.Graphics.Gpu.Memory /// /// Sets the buffer storage of a buffer image array element. This will be bound when the buffer manager commits bindings. /// + /// Shader stage accessing the texture /// Image array where the element will be inserted /// Buffer texture /// Physical ranges of memory where the buffer texture data is located @@ -912,6 +916,7 @@ namespace Ryujinx.Graphics.Gpu.Memory /// Index of the binding on the array /// Format of the buffer texture public void SetBufferTextureStorage( + ShaderStage stage, IImageArray array, ITexture texture, MultiRange range, @@ -919,7 +924,7 @@ namespace Ryujinx.Graphics.Gpu.Memory int index, Format format) { - _channel.MemoryManager.Physical.BufferCache.CreateBuffer(range); + _channel.MemoryManager.Physical.BufferCache.CreateBuffer(range, BufferStageUtils.TextureBuffer(stage, bindingInfo.Flags)); _bufferImageArrays.Add(new BufferTextureArrayBinding(array, texture, range, bindingInfo, index, format)); } diff --git a/src/Ryujinx.Graphics.Gpu/Memory/BufferMigration.cs b/src/Ryujinx.Graphics.Gpu/Memory/BufferMigration.cs index 0a5268031..ce9985318 100644 --- a/src/Ryujinx.Graphics.Gpu/Memory/BufferMigration.cs +++ b/src/Ryujinx.Graphics.Gpu/Memory/BufferMigration.cs @@ -1,37 +1,21 @@ using System; +using System.Threading; namespace Ryujinx.Graphics.Gpu.Memory { /// - /// A record of when buffer data was copied from one buffer to another, along with the SyncNumber when the migration will be complete. - /// Keeps the source buffer alive for data flushes until the migration is complete. + /// A record of when buffer data was copied from multiple buffers to one migration target, + /// along with the SyncNumber when the migration will be complete. + /// Keeps the source buffers alive for data flushes until the migration is complete. + /// All spans cover the full range of the "destination" buffer. /// internal class BufferMigration : IDisposable { /// - /// The offset for the migrated region. + /// Ranges from source buffers that were copied as part of this migration. + /// Ordered by increasing base address. /// - private readonly ulong _offset; - - /// - /// The size for the migrated region. - /// - private readonly ulong _size; - - /// - /// The buffer that was migrated from. - /// - private readonly Buffer _buffer; - - /// - /// The source range action, to be called on overlap with an unreached sync number. - /// - private readonly Action _sourceRangeAction; - - /// - /// The source range list. - /// - private readonly BufferModifiedRangeList _source; + public BufferMigrationSpan[] Spans { get; private set; } /// /// The destination range list. This range list must be updated when flushing the source. @@ -43,55 +27,193 @@ namespace Ryujinx.Graphics.Gpu.Memory /// public readonly ulong SyncNumber; + /// + /// Number of active users there are traversing this migration's spans. + /// + private int _refCount; + + /// + /// Create a new buffer migration. + /// + /// Source spans for the migration + /// Destination buffer range list + /// Sync number where this migration will be complete + public BufferMigration(BufferMigrationSpan[] spans, BufferModifiedRangeList destination, ulong syncNumber) + { + Spans = spans; + Destination = destination; + SyncNumber = syncNumber; + } + + /// + /// Add a span to the migration. Allocates a new array with the target size, and replaces it. + /// + /// + /// The base address for the span is assumed to be higher than all other spans in the migration, + /// to keep the span array ordered. + /// + public void AddSpanToEnd(BufferMigrationSpan span) + { + BufferMigrationSpan[] oldSpans = Spans; + + BufferMigrationSpan[] newSpans = new BufferMigrationSpan[oldSpans.Length + 1]; + + oldSpans.CopyTo(newSpans, 0); + + newSpans[oldSpans.Length] = span; + + Spans = newSpans; + } + + /// + /// Performs the given range action, or one from a migration that overlaps and has not synced yet. + /// + /// The offset to pass to the action + /// The size to pass to the action + /// The sync number that has been reached + /// The action to perform + public void RangeActionWithMigration(ulong offset, ulong size, ulong syncNumber, BufferFlushAction rangeAction) + { + long syncDiff = (long)(syncNumber - SyncNumber); + + if (syncDiff >= 0) + { + // The migration has completed. Run the parent action. + rangeAction(offset, size, syncNumber); + } + else + { + Interlocked.Increment(ref _refCount); + + ulong prevAddress = offset; + ulong endAddress = offset + size; + + foreach (BufferMigrationSpan span in Spans) + { + if (!span.Overlaps(offset, size)) + { + continue; + } + + if (span.Address > prevAddress) + { + // There's a gap between this span and the last (or the start address). Flush the range using the parent action. + + rangeAction(prevAddress, span.Address - prevAddress, syncNumber); + } + + span.RangeActionWithMigration(offset, size, syncNumber); + + prevAddress = span.Address + span.Size; + } + + if (endAddress > prevAddress) + { + // There's a gap at the end of the range with no migration. Flush the range using the parent action. + rangeAction(prevAddress, endAddress - prevAddress, syncNumber); + } + + Interlocked.Decrement(ref _refCount); + } + } + + /// + /// Dispose the buffer migration. This removes the reference from the destination range list, + /// and runs all the dispose buffers for the migration spans. (typically disposes the source buffer) + /// + public void Dispose() + { + while (Volatile.Read(ref _refCount) > 0) + { + // Coming into this method, the sync for the migration will be met, so nothing can increment the ref count. + // However, an existing traversal of the spans for data flush could still be in progress. + // Spin if this is ever the case, so they don't get disposed before the operation is complete. + } + + Destination.RemoveMigration(this); + + foreach (BufferMigrationSpan span in Spans) + { + span.Dispose(); + } + } + } + + /// + /// A record of when buffer data was copied from one buffer to another, for a specific range in a source buffer. + /// Keeps the source buffer alive for data flushes until the migration is complete. + /// + internal readonly struct BufferMigrationSpan : IDisposable + { + /// + /// The offset for the migrated region. + /// + public readonly ulong Address; + + /// + /// The size for the migrated region. + /// + public readonly ulong Size; + + /// + /// The action to perform when the migration isn't needed anymore. + /// + private readonly Action _disposeAction; + + /// + /// The source range action, to be called on overlap with an unreached sync number. + /// + private readonly BufferFlushAction _sourceRangeAction; + + /// + /// Optional migration for the source data. Can chain together if many migrations happen in a short time. + /// If this is null, then _sourceRangeAction will always provide up to date data. + /// + private readonly BufferMigration _source; + /// /// Creates a record for a buffer migration. /// /// The source buffer for this migration + /// The action to perform when the migration isn't needed anymore /// The flush action for the source buffer - /// The modified range list for the source buffer - /// The modified range list for the destination buffer - /// The sync number for when the migration is complete - public BufferMigration( + /// Pending migration for the source buffer + public BufferMigrationSpan( Buffer buffer, - Action sourceRangeAction, - BufferModifiedRangeList source, - BufferModifiedRangeList dest, - ulong syncNumber) + Action disposeAction, + BufferFlushAction sourceRangeAction, + BufferMigration source) { - _offset = buffer.Address; - _size = buffer.Size; - _buffer = buffer; + Address = buffer.Address; + Size = buffer.Size; + _disposeAction = disposeAction; _sourceRangeAction = sourceRangeAction; _source = source; - Destination = dest; - SyncNumber = syncNumber; } + /// + /// Creates a record for a buffer migration, using the default buffer dispose action. + /// + /// The source buffer for this migration + /// The flush action for the source buffer + /// Pending migration for the source buffer + public BufferMigrationSpan( + Buffer buffer, + BufferFlushAction sourceRangeAction, + BufferMigration source) : this(buffer, buffer.DecrementReferenceCount, sourceRangeAction, source) { } + /// /// Determine if the given range overlaps this migration, and has not been completed yet. /// /// Start offset /// Range size - /// The sync number that was waited on /// True if overlapping and in progress, false otherwise - public bool Overlaps(ulong offset, ulong size, ulong syncNumber) + public bool Overlaps(ulong offset, ulong size) { ulong end = offset + size; - ulong destEnd = _offset + _size; - long syncDiff = (long)(syncNumber - SyncNumber); // syncNumber is less if the copy has not completed. + ulong destEnd = Address + Size; - return !(end <= _offset || offset >= destEnd) && syncDiff < 0; - } - - /// - /// Determine if the given range matches this migration. - /// - /// Start offset - /// Range size - /// True if the range exactly matches, false otherwise - public bool FullyMatches(ulong offset, ulong size) - { - return _offset == offset && _size == size; + return !(end <= Address || offset >= destEnd); } /// @@ -100,26 +222,30 @@ namespace Ryujinx.Graphics.Gpu.Memory /// Start offset /// Range size /// Current sync number - /// The modified range list that originally owned this range - public void RangeActionWithMigration(ulong offset, ulong size, ulong syncNumber, BufferModifiedRangeList parent) + public void RangeActionWithMigration(ulong offset, ulong size, ulong syncNumber) { ulong end = offset + size; - end = Math.Min(_offset + _size, end); - offset = Math.Max(_offset, offset); + end = Math.Min(Address + Size, end); + offset = Math.Max(Address, offset); size = end - offset; - _source.RangeActionWithMigration(offset, size, syncNumber, parent, _sourceRangeAction); + if (_source != null) + { + _source.RangeActionWithMigration(offset, size, syncNumber, _sourceRangeAction); + } + else + { + _sourceRangeAction(offset, size, syncNumber); + } } /// - /// Removes this reference to the range list, potentially allowing for the source buffer to be disposed. + /// Removes this migration span, potentially allowing for the source buffer to be disposed. /// public void Dispose() { - Destination.RemoveMigration(this); - - _buffer.DecrementReferenceCount(); + _disposeAction(); } } } diff --git a/src/Ryujinx.Graphics.Gpu/Memory/BufferModifiedRangeList.cs b/src/Ryujinx.Graphics.Gpu/Memory/BufferModifiedRangeList.cs index 6ada8a4b2..d330de638 100644 --- a/src/Ryujinx.Graphics.Gpu/Memory/BufferModifiedRangeList.cs +++ b/src/Ryujinx.Graphics.Gpu/Memory/BufferModifiedRangeList.cs @@ -1,7 +1,6 @@ using Ryujinx.Common.Pools; using Ryujinx.Memory.Range; using System; -using System.Collections.Generic; using System.Linq; namespace Ryujinx.Graphics.Gpu.Memory @@ -72,10 +71,10 @@ namespace Ryujinx.Graphics.Gpu.Memory private readonly GpuContext _context; private readonly Buffer _parent; - private readonly Action _flushAction; + private readonly BufferFlushAction _flushAction; - private List _sources; - private BufferMigration _migrationTarget; + private BufferMigration _source; + private BufferModifiedRangeList _migrationTarget; private readonly object _lock = new(); @@ -99,7 +98,7 @@ namespace Ryujinx.Graphics.Gpu.Memory /// GPU context that the buffer range list belongs to /// The parent buffer that owns this range list /// The flush action for the parent buffer - public BufferModifiedRangeList(GpuContext context, Buffer parent, Action flushAction) : base(BackingInitialSize) + public BufferModifiedRangeList(GpuContext context, Buffer parent, BufferFlushAction flushAction) : base(BackingInitialSize) { _context = context; _parent = parent; @@ -199,6 +198,36 @@ namespace Ryujinx.Graphics.Gpu.Memory } } + /// + /// Gets modified ranges within the specified region, and then fires the given action for each range individually. + /// + /// Start address to query + /// Size to query + /// Sync number required for a range to be signalled + /// The action to call for each modified range + public void GetRangesAtSync(ulong address, ulong size, ulong syncNumber, Action rangeAction) + { + int count = 0; + + ref var overlaps = ref ThreadStaticArray.Get(); + + // Range list must be consistent for this operation. + lock (_lock) + { + count = FindOverlapsNonOverlapping(address, size, ref overlaps); + } + + for (int i = 0; i < count; i++) + { + BufferModifiedRange overlap = overlaps[i]; + + if (overlap.SyncNumber == syncNumber) + { + rangeAction(overlap.Address, overlap.Size); + } + } + } + /// /// Gets modified ranges within the specified region, and then fires the given action for each range individually. /// @@ -245,41 +274,16 @@ namespace Ryujinx.Graphics.Gpu.Memory /// The offset to pass to the action /// The size to pass to the action /// The sync number that has been reached - /// The modified range list that originally owned this range /// The action to perform - public void RangeActionWithMigration(ulong offset, ulong size, ulong syncNumber, BufferModifiedRangeList parent, Action rangeAction) + public void RangeActionWithMigration(ulong offset, ulong size, ulong syncNumber, BufferFlushAction rangeAction) { - bool firstSource = true; - - if (parent != this) + if (_source != null) { - lock (_lock) - { - if (_sources != null) - { - foreach (BufferMigration source in _sources) - { - if (source.Overlaps(offset, size, syncNumber)) - { - if (firstSource && !source.FullyMatches(offset, size)) - { - // Perform this buffer's action first. The migrations will run after. - rangeAction(offset, size); - } - - source.RangeActionWithMigration(offset, size, syncNumber, parent); - - firstSource = false; - } - } - } - } + _source.RangeActionWithMigration(offset, size, syncNumber, rangeAction); } - - if (firstSource) + else { - // No overlapping migrations, or they are not meant for this range, flush the data using the given action. - rangeAction(offset, size); + rangeAction(offset, size, syncNumber); } } @@ -319,7 +323,7 @@ namespace Ryujinx.Graphics.Gpu.Memory ClearPart(overlap, clampAddress, clampEnd); - RangeActionWithMigration(clampAddress, clampEnd - clampAddress, waitSync, overlap.Parent, _flushAction); + RangeActionWithMigration(clampAddress, clampEnd - clampAddress, waitSync, _flushAction); } } @@ -329,7 +333,7 @@ namespace Ryujinx.Graphics.Gpu.Memory // There is a migration target to call instead. This can't be changed after set so accessing it outside the lock is fine. - _migrationTarget.Destination.RemoveRangesAndFlush(overlaps, rangeCount, highestDiff, currentSync, address, endAddress); + _migrationTarget.RemoveRangesAndFlush(overlaps, rangeCount, highestDiff, currentSync, address, endAddress); } /// @@ -367,7 +371,7 @@ namespace Ryujinx.Graphics.Gpu.Memory if (rangeCount == -1) { - _migrationTarget.Destination.WaitForAndFlushRanges(address, size); + _migrationTarget.WaitForAndFlushRanges(address, size); return; } @@ -407,6 +411,9 @@ namespace Ryujinx.Graphics.Gpu.Memory /// /// Inherit ranges from another modified range list. /// + /// + /// Assumes that ranges will be inherited in address ascending order. + /// /// The range list to inherit from /// The action to call for each modified range public void InheritRanges(BufferModifiedRangeList ranges, Action registerRangeAction) @@ -415,18 +422,31 @@ namespace Ryujinx.Graphics.Gpu.Memory lock (ranges._lock) { - BufferMigration migration = new(ranges._parent, ranges._flushAction, ranges, this, _context.SyncNumber); - - ranges._parent.IncrementReferenceCount(); - ranges._migrationTarget = migration; - - _context.RegisterBufferMigration(migration); - inheritRanges = ranges.ToArray(); lock (_lock) { - (_sources ??= new List()).Add(migration); + // Copy over the migration from the previous range list + + BufferMigration oldMigration = ranges._source; + + BufferMigrationSpan span = new BufferMigrationSpan(ranges._parent, ranges._flushAction, oldMigration); + ranges._parent.IncrementReferenceCount(); + + if (_source == null) + { + // Create a new migration. + _source = new BufferMigration(new BufferMigrationSpan[] { span }, this, _context.SyncNumber); + + _context.RegisterBufferMigration(_source); + } + else + { + // Extend the migration + _source.AddSpanToEnd(span); + } + + ranges._migrationTarget = this; foreach (BufferModifiedRange range in inheritRanges) { @@ -445,6 +465,27 @@ namespace Ryujinx.Graphics.Gpu.Memory } } + /// + /// Register a migration from previous buffer storage. This migration is from a snapshot of the buffer's + /// current handle to its handle in the future, and is assumed to be complete when the sync action completes. + /// When the migration completes, the handle is disposed. + /// + public void SelfMigration() + { + lock (_lock) + { + BufferMigrationSpan span = new(_parent, _parent.GetSnapshotDisposeAction(), _parent.GetSnapshotFlushAction(), _source); + BufferMigration migration = new(new BufferMigrationSpan[] { span }, this, _context.SyncNumber); + + // Migration target is used to redirect flush actions to the latest range list, + // so we don't need to set it here. (this range list is still the latest) + + _context.RegisterBufferMigration(migration); + + _source = migration; + } + } + /// /// Removes a source buffer migration, indicating its copy has completed. /// @@ -453,7 +494,10 @@ namespace Ryujinx.Graphics.Gpu.Memory { lock (_lock) { - _sources.Remove(migration); + if (_source == migration) + { + _source = null; + } } } diff --git a/src/Ryujinx.Graphics.Gpu/Memory/BufferPreFlush.cs b/src/Ryujinx.Graphics.Gpu/Memory/BufferPreFlush.cs new file mode 100644 index 000000000..d58b9ea66 --- /dev/null +++ b/src/Ryujinx.Graphics.Gpu/Memory/BufferPreFlush.cs @@ -0,0 +1,295 @@ +using Ryujinx.Common; +using Ryujinx.Graphics.GAL; +using System; + +namespace Ryujinx.Graphics.Gpu.Memory +{ + /// + /// Manages flushing ranges from buffers in advance for easy access, if they are flushed often. + /// Typically, from device local memory to a host mapped target for cached access. + /// + internal class BufferPreFlush : IDisposable + { + private const ulong PageSize = MemoryManager.PageSize; + + /// + /// Threshold for the number of copies without a flush required to disable preflush on a page. + /// + private const int DeactivateCopyThreshold = 200; + + /// + /// Value that indicates whether a page has been flushed or copied before. + /// + private enum PreFlushState + { + None, + HasFlushed, + HasCopied + } + + /// + /// Flush state for each page of the buffer. + /// Controls whether data should be copied to the flush buffer, what sync is expected + /// and unflushed copy counting for stopping copies that are no longer needed. + /// + private struct PreFlushPage + { + public PreFlushState State; + public ulong FirstActivatedSync; + public ulong LastCopiedSync; + public int CopyCount; + } + + /// + /// True if there are ranges that should copy to the flush buffer, false otherwise. + /// + public bool ShouldCopy { get; private set; } + + private readonly GpuContext _context; + private readonly Buffer _buffer; + private readonly PreFlushPage[] _pages; + private readonly ulong _address; + private readonly ulong _size; + private readonly ulong _misalignment; + private readonly Action _flushAction; + + private BufferHandle _flushBuffer; + + public BufferPreFlush(GpuContext context, Buffer parent, Action flushAction) + { + _context = context; + _buffer = parent; + _address = parent.Address; + _size = parent.Size; + _pages = new PreFlushPage[BitUtils.DivRoundUp(_size, PageSize)]; + _misalignment = _address & (PageSize - 1); + + _flushAction = flushAction; + } + + /// + /// Ensure that the flush buffer exists. + /// + private void EnsureFlushBuffer() + { + if (_flushBuffer == BufferHandle.Null) + { + _flushBuffer = _context.Renderer.CreateBuffer((int)_size, BufferAccess.HostMemory); + } + } + + /// + /// Gets a page range from an address and size byte range. + /// + /// Range address + /// Range size + /// A page index and count + private (int index, int count) GetPageRange(ulong address, ulong size) + { + ulong offset = address - _address; + ulong endOffset = offset + size; + + int basePage = (int)(offset / PageSize); + int endPage = (int)((endOffset - 1) / PageSize); + + return (basePage, 1 + endPage - basePage); + } + + /// + /// Gets an offset and size range in the parent buffer from a page index and count. + /// + /// Range start page + /// Range page count + /// Offset and size range + private (int offset, int size) GetOffset(int startPage, int count) + { + int offset = (int)((ulong)startPage * PageSize - _misalignment); + int endOffset = (int)((ulong)(startPage + count) * PageSize - _misalignment); + + offset = Math.Max(0, offset); + endOffset = Math.Min((int)_size, endOffset); + + return (offset, endOffset - offset); + } + + /// + /// Copy a range of pages from the parent buffer into the flush buffer. + /// + /// Range start page + /// Range page count + private void CopyPageRange(int startPage, int count) + { + (int offset, int size) = GetOffset(startPage, count); + + EnsureFlushBuffer(); + + _context.Renderer.Pipeline.CopyBuffer(_buffer.Handle, _flushBuffer, offset, offset, size); + } + + /// + /// Copy a modified range into the flush buffer if it's marked as flushed. + /// Any pages the range overlaps are copied, and copies aren't repeated in the same sync number. + /// + /// Range address + /// Range size + public void CopyModified(ulong address, ulong size) + { + (int baseIndex, int count) = GetPageRange(address, size); + ulong syncNumber = _context.SyncNumber; + + int startPage = -1; + + for (int i = 0; i < count; i++) + { + int pageIndex = baseIndex + i; + ref PreFlushPage page = ref _pages[pageIndex]; + + if (page.State > PreFlushState.None) + { + // Perform the copy, and update the state of each page. + if (startPage == -1) + { + startPage = pageIndex; + } + + if (page.State != PreFlushState.HasCopied) + { + page.FirstActivatedSync = syncNumber; + page.State = PreFlushState.HasCopied; + } + else if (page.CopyCount++ >= DeactivateCopyThreshold) + { + page.CopyCount = 0; + page.State = PreFlushState.None; + } + + if (page.LastCopiedSync != syncNumber) + { + page.LastCopiedSync = syncNumber; + } + } + else if (startPage != -1) + { + CopyPageRange(startPage, pageIndex - startPage); + + startPage = -1; + } + } + + if (startPage != -1) + { + CopyPageRange(startPage, (baseIndex + count) - startPage); + } + } + + /// + /// Flush the given page range back into guest memory, optionally using data from the flush buffer. + /// The actual flushed range is an intersection of the page range and the address range. + /// + /// Address range start + /// Address range size + /// Page range start + /// Page range count + /// True if the data should come from the flush buffer + private void FlushPageRange(ulong address, ulong size, int startPage, int count, bool preFlush) + { + (int pageOffset, int pageSize) = GetOffset(startPage, count); + + int offset = (int)(address - _address); + int end = offset + (int)size; + + offset = Math.Max(offset, pageOffset); + end = Math.Min(end, pageOffset + pageSize); + + if (end >= offset) + { + BufferHandle handle = preFlush ? _flushBuffer : _buffer.Handle; + _flushAction(handle, _address + (ulong)offset, (ulong)(end - offset)); + } + } + + /// + /// Flush the given address range back into guest memory, optionally using data from the flush buffer. + /// When a copy has been performed on or before the waited sync number, the data can come from the flush buffer. + /// Otherwise, it flushes the parent buffer directly. + /// + /// Range address + /// Range size + /// Sync number that has been waited for + public void FlushWithAction(ulong address, ulong size, ulong syncNumber) + { + // Copy the parts of the range that have pre-flush copies that have been completed. + // Run the flush action for ranges that don't have pre-flush copies. + + // If a range doesn't have a pre-flush copy, consider adding one. + + (int baseIndex, int count) = GetPageRange(address, size); + + bool rangePreFlushed = false; + int startPage = -1; + + for (int i = 0; i < count; i++) + { + int pageIndex = baseIndex + i; + ref PreFlushPage page = ref _pages[pageIndex]; + + bool flushPage = false; + page.CopyCount = 0; + + if (page.State == PreFlushState.HasCopied) + { + if (syncNumber >= page.FirstActivatedSync) + { + // After the range is first activated, its data will always be copied to the preflush buffer on each sync. + flushPage = true; + } + } + else if (page.State == PreFlushState.None) + { + page.State = PreFlushState.HasFlushed; + ShouldCopy = true; + } + + if (flushPage) + { + if (!rangePreFlushed || startPage == -1) + { + if (startPage != -1) + { + FlushPageRange(address, size, startPage, pageIndex - startPage, false); + } + + rangePreFlushed = true; + startPage = pageIndex; + } + } + else if (rangePreFlushed || startPage == -1) + { + if (startPage != -1) + { + FlushPageRange(address, size, startPage, pageIndex - startPage, true); + } + + rangePreFlushed = false; + startPage = pageIndex; + } + } + + if (startPage != -1) + { + FlushPageRange(address, size, startPage, (baseIndex + count) - startPage, rangePreFlushed); + } + } + + /// + /// Dispose the flush buffer, if present. + /// + public void Dispose() + { + if (_flushBuffer != BufferHandle.Null) + { + _context.Renderer.DeleteBuffer(_flushBuffer); + } + } + } +} diff --git a/src/Ryujinx.Graphics.Gpu/Memory/BufferStage.cs b/src/Ryujinx.Graphics.Gpu/Memory/BufferStage.cs new file mode 100644 index 000000000..d56abda28 --- /dev/null +++ b/src/Ryujinx.Graphics.Gpu/Memory/BufferStage.cs @@ -0,0 +1,99 @@ +using Ryujinx.Graphics.Shader; +using System.Runtime.CompilerServices; + +namespace Ryujinx.Graphics.Gpu.Memory +{ + /// + /// Pipeline stages that can modify buffer data, as well as flags indicating storage usage. + /// Must match ShaderStage for the shader stages, though anything after that can be in any order. + /// + internal enum BufferStage : byte + { + Compute, + Vertex, + TessellationControl, + TessellationEvaluation, + Geometry, + Fragment, + + Indirect, + VertexBuffer, + IndexBuffer, + Copy, + TransformFeedback, + Internal, + None, + + StageMask = 0x3f, + StorageMask = 0xc0, + + StorageRead = 0x40, + StorageWrite = 0x80, + +#pragma warning disable CA1069 // Enums values should not be duplicated + StorageAtomic = 0xc0 +#pragma warning restore CA1069 // Enums values should not be duplicated + } + + /// + /// Utility methods to convert shader stages and binding flags into buffer stages. + /// + internal static class BufferStageUtils + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static BufferStage FromShaderStage(ShaderStage stage) + { + return (BufferStage)stage; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static BufferStage FromShaderStage(int stageIndex) + { + return (BufferStage)(stageIndex + 1); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static BufferStage FromUsage(BufferUsageFlags flags) + { + if (flags.HasFlag(BufferUsageFlags.Write)) + { + return BufferStage.StorageWrite; + } + else + { + return BufferStage.StorageRead; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static BufferStage FromUsage(TextureUsageFlags flags) + { + if (flags.HasFlag(TextureUsageFlags.ImageStore)) + { + return BufferStage.StorageWrite; + } + else + { + return BufferStage.StorageRead; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static BufferStage TextureBuffer(ShaderStage shaderStage, TextureUsageFlags flags) + { + return FromShaderStage(shaderStage) | FromUsage(flags); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static BufferStage GraphicsStorage(int stageIndex, BufferUsageFlags flags) + { + return FromShaderStage(stageIndex) | FromUsage(flags); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static BufferStage ComputeStorage(BufferUsageFlags flags) + { + return BufferStage.Compute | FromUsage(flags); + } + } +} diff --git a/src/Ryujinx.Graphics.Gpu/Shader/CachedShaderBindings.cs b/src/Ryujinx.Graphics.Gpu/Shader/CachedShaderBindings.cs index 6e36753e8..51be00b6e 100644 --- a/src/Ryujinx.Graphics.Gpu/Shader/CachedShaderBindings.cs +++ b/src/Ryujinx.Graphics.Gpu/Shader/CachedShaderBindings.cs @@ -58,15 +58,17 @@ namespace Ryujinx.Graphics.Gpu.Shader TextureBindings[i] = stage.Info.Textures.Select(descriptor => { - Target target = ShaderTexture.GetTarget(descriptor.Type); + Target target = descriptor.Type != SamplerType.None ? ShaderTexture.GetTarget(descriptor.Type) : default; var result = new TextureBindingInfo( target, + descriptor.Set, descriptor.Binding, descriptor.ArrayLength, descriptor.CbufSlot, descriptor.HandleIndex, - descriptor.Flags); + descriptor.Flags, + descriptor.Type == SamplerType.None); if (descriptor.ArrayLength <= 1) { @@ -89,6 +91,7 @@ namespace Ryujinx.Graphics.Gpu.Shader var result = new TextureBindingInfo( target, format, + descriptor.Set, descriptor.Binding, descriptor.ArrayLength, descriptor.CbufSlot, diff --git a/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/BinarySerializer.cs b/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/BinarySerializer.cs index b08c44d67..3837092c9 100644 --- a/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/BinarySerializer.cs +++ b/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/BinarySerializer.cs @@ -125,9 +125,18 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache CompressionAlgorithm algorithm = CompressionAlgorithm.None; Read(ref algorithm); - if (algorithm == CompressionAlgorithm.Deflate) + switch (algorithm) { - _activeStream = new DeflateStream(_stream, CompressionMode.Decompress, true); + case CompressionAlgorithm.None: + break; + case CompressionAlgorithm.Deflate: + _activeStream = new DeflateStream(_stream, CompressionMode.Decompress, true); + break; + case CompressionAlgorithm.Brotli: + _activeStream = new BrotliStream(_stream, CompressionMode.Decompress, true); + break; + default: + throw new ArgumentException($"Invalid compression algorithm \"{algorithm}\""); } } @@ -139,9 +148,18 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache { Write(ref algorithm); - if (algorithm == CompressionAlgorithm.Deflate) + switch (algorithm) { - _activeStream = new DeflateStream(_stream, CompressionLevel.SmallestSize, true); + case CompressionAlgorithm.None: + break; + case CompressionAlgorithm.Deflate: + _activeStream = new DeflateStream(_stream, CompressionLevel.Fastest, true); + break; + case CompressionAlgorithm.Brotli: + _activeStream = new BrotliStream(_stream, CompressionLevel.Fastest, true); + break; + default: + throw new ArgumentException($"Invalid compression algorithm \"{algorithm}\""); } } @@ -177,7 +195,7 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache switch (algorithm) { case CompressionAlgorithm.None: - stream.Read(data); + stream.ReadExactly(data); break; case CompressionAlgorithm.Deflate: stream = new DeflateStream(stream, CompressionMode.Decompress, true); @@ -187,6 +205,14 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache } stream.Dispose(); break; + case CompressionAlgorithm.Brotli: + stream = new BrotliStream(stream, CompressionMode.Decompress, true); + for (int offset = 0; offset < data.Length;) + { + offset += stream.Read(data[offset..]); + } + stream.Dispose(); + break; } } @@ -206,7 +232,12 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache stream.Write(data); break; case CompressionAlgorithm.Deflate: - stream = new DeflateStream(stream, CompressionLevel.SmallestSize, true); + stream = new DeflateStream(stream, CompressionLevel.Fastest, true); + stream.Write(data); + stream.Dispose(); + break; + case CompressionAlgorithm.Brotli: + stream = new BrotliStream(stream, CompressionLevel.Fastest, true); stream.Write(data); stream.Dispose(); break; diff --git a/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/CompressionAlgorithm.cs b/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/CompressionAlgorithm.cs index 96ddbb513..86d3de07d 100644 --- a/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/CompressionAlgorithm.cs +++ b/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/CompressionAlgorithm.cs @@ -14,5 +14,10 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache /// Deflate compression (RFC 1951). /// Deflate, + + /// + /// Brotli compression (RFC 7932). + /// + Brotli, } } diff --git a/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheCommon.cs b/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheCommon.cs index c4ce0b870..cecfe9acf 100644 --- a/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheCommon.cs +++ b/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheCommon.cs @@ -51,7 +51,7 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache /// Compression algorithm public static CompressionAlgorithm GetCompressionAlgorithm() { - return CompressionAlgorithm.Deflate; + return CompressionAlgorithm.Brotli; } } } diff --git a/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheGpuAccessor.cs b/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheGpuAccessor.cs index 681838a9b..3c7664b77 100644 --- a/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheGpuAccessor.cs +++ b/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheGpuAccessor.cs @@ -18,6 +18,8 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache private readonly ShaderSpecializationState _newSpecState; private readonly int _stageIndex; private readonly bool _isVulkan; + private readonly bool _hasGeometryShader; + private readonly bool _supportsQuads; /// /// Creates a new instance of the cached GPU state accessor for shader translation. @@ -29,6 +31,7 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache /// Shader specialization state of the recompiled shader /// Resource counts shared across all shader stages /// Shader stage index + /// Indicates if a geometry shader is present public DiskCacheGpuAccessor( GpuContext context, ReadOnlyMemory data, @@ -36,7 +39,8 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache ShaderSpecializationState oldSpecState, ShaderSpecializationState newSpecState, ResourceCounts counts, - int stageIndex) : base(context, counts, stageIndex) + int stageIndex, + bool hasGeometryShader) : base(context, counts, stageIndex) { _data = data; _cb1Data = cb1Data; @@ -44,6 +48,8 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache _newSpecState = newSpecState; _stageIndex = stageIndex; _isVulkan = context.Capabilities.Api == TargetApi.Vulkan; + _hasGeometryShader = hasGeometryShader; + _supportsQuads = context.Capabilities.SupportsQuads; if (stageIndex == (int)ShaderStage.Geometry - 1) { @@ -100,7 +106,11 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache /// public GpuGraphicsState QueryGraphicsState() { - return _oldSpecState.GraphicsState.CreateShaderGraphicsState(!_isVulkan, _isVulkan || _oldSpecState.GraphicsState.YNegateEnabled); + return _oldSpecState.GraphicsState.CreateShaderGraphicsState( + !_isVulkan, + _supportsQuads, + _hasGeometryShader, + _isVulkan || _oldSpecState.GraphicsState.YNegateEnabled); } /// @@ -109,6 +119,13 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache return _oldSpecState.GraphicsState.HasConstantBufferDrawParameters; } + /// + /// Pool length is not available on the cache + public int QuerySamplerArrayLengthFromPool() + { + return QueryArrayLengthFromPool(isSampler: true); + } + /// public SamplerType QuerySamplerType(int handle, int cbufSlot) { @@ -117,6 +134,7 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache } /// + /// Constant buffer derived length is not available on the cache public int QueryTextureArrayLengthFromBuffer(int slot) { if (!_oldSpecState.TextureArrayFromBufferRegistered(_stageIndex, 0, slot)) @@ -130,6 +148,13 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache return arrayLength; } + /// + /// Pool length is not available on the cache + public int QueryTextureArrayLengthFromPool() + { + return QueryArrayLengthFromPool(isSampler: false); + } + /// public TextureFormat QueryTextureFormat(int handle, int cbufSlot) { @@ -170,6 +195,7 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache } /// + /// Texture information is not available on the cache public void RegisterTexture(int handle, int cbufSlot) { if (!_oldSpecState.TextureRegistered(_stageIndex, handle, cbufSlot)) @@ -182,5 +208,24 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache bool coordNormalized = _oldSpecState.GetCoordNormalized(_stageIndex, handle, cbufSlot); _newSpecState.RegisterTexture(_stageIndex, handle, cbufSlot, format, formatSrgb, target, coordNormalized); } + + /// + /// Gets the cached texture or sampler pool capacity. + /// + /// True to get sampler pool length, false for texture pool length + /// Pool length + /// Pool length is not available on the cache + private int QueryArrayLengthFromPool(bool isSampler) + { + if (!_oldSpecState.TextureArrayFromPoolRegistered(isSampler)) + { + throw new DiskCacheLoadException(DiskCacheLoadResult.MissingTextureArrayLength); + } + + int arrayLength = _oldSpecState.GetTextureArrayFromPoolLength(isSampler); + _newSpecState.RegisterTextureArrayLengthFromPool(isSampler, arrayLength); + + return arrayLength; + } } } diff --git a/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheGuestStorage.cs b/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheGuestStorage.cs index 59d2cfb3f..08cd3bb02 100644 --- a/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheGuestStorage.cs +++ b/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheGuestStorage.cs @@ -220,7 +220,7 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache } dataFileStream.Seek((long)entry.Offset, SeekOrigin.Begin); - dataFileStream.Read(cb1Data); + dataFileStream.ReadExactly(cb1Data); BinarySerializer.ReadCompressed(dataFileStream, guestCode); _cache[index] = (guestCode, cb1Data); @@ -279,7 +279,7 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache dataFileStream.Seek((long)entry.Offset, SeekOrigin.Begin); byte[] cachedCode = new byte[entry.CodeSize]; byte[] cachedCb1Data = new byte[entry.Cb1DataSize]; - dataFileStream.Read(cachedCb1Data); + dataFileStream.ReadExactly(cachedCb1Data); BinarySerializer.ReadCompressed(dataFileStream, cachedCode); if (data.SequenceEqual(cachedCode) && cb1Data.SequenceEqual(cachedCb1Data)) diff --git a/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheHostStorage.cs b/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheHostStorage.cs index b6a277a2a..fbf48f017 100644 --- a/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheHostStorage.cs +++ b/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheHostStorage.cs @@ -22,7 +22,7 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache private const ushort FileFormatVersionMajor = 1; private const ushort FileFormatVersionMinor = 2; private const uint FileFormatVersionPacked = ((uint)FileFormatVersionMajor << 16) | FileFormatVersionMinor; - private const uint CodeGenVersion = 6489; + private const uint CodeGenVersion = 6852; private const string SharedTocFileName = "shared.toc"; private const string SharedDataFileName = "shared.data"; diff --git a/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/ParallelDiskCacheLoader.cs b/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/ParallelDiskCacheLoader.cs index 153fc4427..20f96462e 100644 --- a/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/ParallelDiskCacheLoader.cs +++ b/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/ParallelDiskCacheLoader.cs @@ -601,6 +601,8 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache TargetApi api = _context.Capabilities.Api; + bool hasCachedGs = guestShaders[4].HasValue; + for (int stageIndex = Constants.ShaderStages - 1; stageIndex >= 0; stageIndex--) { if (guestShaders[stageIndex + 1].HasValue) @@ -610,7 +612,7 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache byte[] guestCode = shader.Code; byte[] cb1Data = shader.Cb1Data; - DiskCacheGpuAccessor gpuAccessor = new(_context, guestCode, cb1Data, specState, newSpecState, counts, stageIndex); + DiskCacheGpuAccessor gpuAccessor = new(_context, guestCode, cb1Data, specState, newSpecState, counts, stageIndex, hasCachedGs); TranslatorContext currentStage = DecodeGraphicsShader(gpuAccessor, api, DefaultFlags, 0); if (nextStage != null) @@ -623,7 +625,7 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache byte[] guestCodeA = guestShaders[0].Value.Code; byte[] cb1DataA = guestShaders[0].Value.Cb1Data; - DiskCacheGpuAccessor gpuAccessorA = new(_context, guestCodeA, cb1DataA, specState, newSpecState, counts, 0); + DiskCacheGpuAccessor gpuAccessorA = new(_context, guestCodeA, cb1DataA, specState, newSpecState, counts, 0, hasCachedGs); translatorContexts[0] = DecodeGraphicsShader(gpuAccessorA, api, DefaultFlags | TranslationFlags.VertexA, 0); } @@ -711,7 +713,7 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache GuestCodeAndCbData shader = guestShaders[0].Value; ResourceCounts counts = new(); ShaderSpecializationState newSpecState = new(ref specState.ComputeState); - DiskCacheGpuAccessor gpuAccessor = new(_context, shader.Code, shader.Cb1Data, specState, newSpecState, counts, 0); + DiskCacheGpuAccessor gpuAccessor = new(_context, shader.Code, shader.Cb1Data, specState, newSpecState, counts, 0, false); gpuAccessor.InitializeReservedCounts(tfEnabled: false, vertexAsCompute: false); TranslatorContext translatorContext = DecodeComputeShader(gpuAccessor, _context.Capabilities.Api, 0); diff --git a/src/Ryujinx.Graphics.Gpu/Shader/GpuAccessor.cs b/src/Ryujinx.Graphics.Gpu/Shader/GpuAccessor.cs index 1d22ab933..1be75f242 100644 --- a/src/Ryujinx.Graphics.Gpu/Shader/GpuAccessor.cs +++ b/src/Ryujinx.Graphics.Gpu/Shader/GpuAccessor.cs @@ -17,6 +17,8 @@ namespace Ryujinx.Graphics.Gpu.Shader private readonly int _stageIndex; private readonly bool _compute; private readonly bool _isVulkan; + private readonly bool _hasGeometryShader; + private readonly bool _supportsQuads; /// /// Creates a new instance of the GPU state accessor for graphics shader translation. @@ -25,12 +27,20 @@ namespace Ryujinx.Graphics.Gpu.Shader /// GPU channel /// Current GPU state /// Graphics shader stage index (0 = Vertex, 4 = Fragment) - public GpuAccessor(GpuContext context, GpuChannel channel, GpuAccessorState state, int stageIndex) : base(context, state.ResourceCounts, stageIndex) + /// Indicates if a geometry shader is present + public GpuAccessor( + GpuContext context, + GpuChannel channel, + GpuAccessorState state, + int stageIndex, + bool hasGeometryShader) : base(context, state.ResourceCounts, stageIndex) { - _isVulkan = context.Capabilities.Api == TargetApi.Vulkan; _channel = channel; _state = state; _stageIndex = stageIndex; + _isVulkan = context.Capabilities.Api == TargetApi.Vulkan; + _hasGeometryShader = hasGeometryShader; + _supportsQuads = context.Capabilities.SupportsQuads; if (stageIndex == (int)ShaderStage.Geometry - 1) { @@ -105,7 +115,11 @@ namespace Ryujinx.Graphics.Gpu.Shader /// public GpuGraphicsState QueryGraphicsState() { - return _state.GraphicsState.CreateShaderGraphicsState(!_isVulkan, _isVulkan || _state.GraphicsState.YNegateEnabled); + return _state.GraphicsState.CreateShaderGraphicsState( + !_isVulkan, + _supportsQuads, + _hasGeometryShader, + _isVulkan || _state.GraphicsState.YNegateEnabled); } /// @@ -120,6 +134,15 @@ namespace Ryujinx.Graphics.Gpu.Shader return _state.GraphicsState.HasUnalignedStorageBuffer || _state.ComputeState.HasUnalignedStorageBuffer; } + /// + public int QuerySamplerArrayLengthFromPool() + { + int length = _state.SamplerPoolMaximumId + 1; + _state.SpecializationState?.RegisterTextureArrayLengthFromPool(isSampler: true, length); + + return length; + } + /// public SamplerType QuerySamplerType(int handle, int cbufSlot) { @@ -141,6 +164,15 @@ namespace Ryujinx.Graphics.Gpu.Shader return arrayLength; } + /// + public int QueryTextureArrayLengthFromPool() + { + int length = _state.PoolState.TexturePoolMaximumId + 1; + _state.SpecializationState?.RegisterTextureArrayLengthFromPool(isSampler: false, length); + + return length; + } + //// public TextureFormat QueryTextureFormat(int handle, int cbufSlot) { diff --git a/src/Ryujinx.Graphics.Gpu/Shader/GpuAccessorBase.cs b/src/Ryujinx.Graphics.Gpu/Shader/GpuAccessorBase.cs index 06e5edf1e..d89eebabf 100644 --- a/src/Ryujinx.Graphics.Gpu/Shader/GpuAccessorBase.cs +++ b/src/Ryujinx.Graphics.Gpu/Shader/GpuAccessorBase.cs @@ -51,7 +51,7 @@ namespace Ryujinx.Graphics.Gpu.Shader _reservedImages = rrc.ReservedImages; } - public int CreateConstantBufferBinding(int index) + public SetBindingPair CreateConstantBufferBinding(int index) { int binding; @@ -64,10 +64,10 @@ namespace Ryujinx.Graphics.Gpu.Shader binding = _resourceCounts.UniformBuffersCount++; } - return binding + _reservedConstantBuffers; + return new SetBindingPair(_context.Capabilities.UniformBufferSetIndex, binding + _reservedConstantBuffers); } - public int CreateImageBinding(int count, bool isBuffer) + public SetBindingPair CreateImageBinding(int count, bool isBuffer) { int binding; @@ -96,10 +96,10 @@ namespace Ryujinx.Graphics.Gpu.Shader _resourceCounts.ImagesCount += count; } - return binding + _reservedImages; + return new SetBindingPair(_context.Capabilities.ImageSetIndex, binding + _reservedImages); } - public int CreateStorageBufferBinding(int index) + public SetBindingPair CreateStorageBufferBinding(int index) { int binding; @@ -112,10 +112,10 @@ namespace Ryujinx.Graphics.Gpu.Shader binding = _resourceCounts.StorageBuffersCount++; } - return binding + _reservedStorageBuffers; + return new SetBindingPair(_context.Capabilities.StorageBufferSetIndex, binding + _reservedStorageBuffers); } - public int CreateTextureBinding(int count, bool isBuffer) + public SetBindingPair CreateTextureBinding(int count, bool isBuffer) { int binding; @@ -144,7 +144,7 @@ namespace Ryujinx.Graphics.Gpu.Shader _resourceCounts.TexturesCount += count; } - return binding + _reservedTextures; + return new SetBindingPair(_context.Capabilities.TextureSetIndex, binding + _reservedTextures); } private int GetBindingFromIndex(int index, uint maxPerStage, string resourceName) @@ -183,6 +183,16 @@ namespace Ryujinx.Graphics.Gpu.Shader return maxPerStage * Constants.ShaderStages; } + public int CreateExtraSet() + { + if (_resourceCounts.SetsCount >= _context.Capabilities.MaximumExtraSets) + { + return -1; + } + + return _context.Capabilities.ExtraSetBaseIndex + _resourceCounts.SetsCount++; + } + public int QueryHostGatherBiasPrecision() => _context.Capabilities.GatherBiasPrecision; public bool QueryHostReducedPrecision() => _context.Capabilities.ReduceShaderPrecision; @@ -213,6 +223,8 @@ namespace Ryujinx.Graphics.Gpu.Shader public bool QueryHostSupportsScaledVertexFormats() => _context.Capabilities.SupportsScaledVertexFormats; + public bool QueryHostSupportsSeparateSampler() => _context.Capabilities.SupportsSeparateSampler; + public bool QueryHostSupportsShaderBallot() => _context.Capabilities.SupportsShaderBallot; public bool QueryHostSupportsShaderBarrierDivergence() => _context.Capabilities.SupportsShaderBarrierDivergence; diff --git a/src/Ryujinx.Graphics.Gpu/Shader/GpuAccessorState.cs b/src/Ryujinx.Graphics.Gpu/Shader/GpuAccessorState.cs index cfc4a2ccc..808bf1851 100644 --- a/src/Ryujinx.Graphics.Gpu/Shader/GpuAccessorState.cs +++ b/src/Ryujinx.Graphics.Gpu/Shader/GpuAccessorState.cs @@ -5,6 +5,11 @@ namespace Ryujinx.Graphics.Gpu.Shader /// class GpuAccessorState { + /// + /// Maximum ID that a sampler pool entry may have. + /// + public readonly int SamplerPoolMaximumId; + /// /// GPU texture pool state. /// @@ -38,18 +43,21 @@ namespace Ryujinx.Graphics.Gpu.Shader /// /// Creates a new GPU accessor state. /// + /// Maximum ID that a sampler pool entry may have /// GPU texture pool state /// GPU compute state, for compute shaders /// GPU graphics state, for vertex, tessellation, geometry and fragment shaders /// Shader specialization state (shared by all stages) /// Transform feedback information, if the shader uses transform feedback. Otherwise, should be null public GpuAccessorState( + int samplerPoolMaximumId, GpuChannelPoolState poolState, GpuChannelComputeState computeState, GpuChannelGraphicsState graphicsState, ShaderSpecializationState specializationState, TransformFeedbackDescriptor[] transformFeedbackDescriptors = null) { + SamplerPoolMaximumId = samplerPoolMaximumId; PoolState = poolState; GraphicsState = graphicsState; ComputeState = computeState; diff --git a/src/Ryujinx.Graphics.Gpu/Shader/GpuChannelGraphicsState.cs b/src/Ryujinx.Graphics.Gpu/Shader/GpuChannelGraphicsState.cs index b5bc4df3c..765bef7d4 100644 --- a/src/Ryujinx.Graphics.Gpu/Shader/GpuChannelGraphicsState.cs +++ b/src/Ryujinx.Graphics.Gpu/Shader/GpuChannelGraphicsState.cs @@ -106,8 +106,11 @@ namespace Ryujinx.Graphics.Gpu.Shader /// Creates a new graphics state from this state that can be used for shader generation. /// /// Indicates if the host API supports alpha test operations + /// Indicates if the host API supports quad primitives + /// Indicates if a geometry shader is used + /// If true, indicates that the fragment origin is the upper left corner of the viewport, otherwise it is the lower left corner /// GPU graphics state that can be used for shader translation - public readonly GpuGraphicsState CreateShaderGraphicsState(bool hostSupportsAlphaTest, bool originUpperLeft) + public readonly GpuGraphicsState CreateShaderGraphicsState(bool hostSupportsAlphaTest, bool hostSupportsQuads, bool hasGeometryShader, bool originUpperLeft) { AlphaTestOp alphaTestOp; @@ -130,6 +133,9 @@ namespace Ryujinx.Graphics.Gpu.Shader }; } + bool isQuad = Topology == PrimitiveTopology.Quads || Topology == PrimitiveTopology.QuadStrip; + bool halvePrimitiveId = !hostSupportsQuads && !hasGeometryShader && isQuad; + return new GpuGraphicsState( EarlyZForce, ConvertToInputTopology(Topology, TessellationMode), @@ -149,7 +155,8 @@ namespace Ryujinx.Graphics.Gpu.Shader in FragmentOutputTypes, DualSourceBlendEnable, YNegateEnabled, - originUpperLeft); + originUpperLeft, + halvePrimitiveId); } /// diff --git a/src/Ryujinx.Graphics.Gpu/Shader/GpuChannelPoolState.cs b/src/Ryujinx.Graphics.Gpu/Shader/GpuChannelPoolState.cs index ddb45152e..a2ab99335 100644 --- a/src/Ryujinx.Graphics.Gpu/Shader/GpuChannelPoolState.cs +++ b/src/Ryujinx.Graphics.Gpu/Shader/GpuChannelPoolState.cs @@ -2,7 +2,6 @@ using System; namespace Ryujinx.Graphics.Gpu.Shader { -#pragma warning disable CS0659 // Class overrides Object.Equals(object o) but does not override Object.GetHashCode() /// /// State used by the . /// @@ -52,6 +51,10 @@ namespace Ryujinx.Graphics.Gpu.Shader { return obj is GpuChannelPoolState state && Equals(state); } + + public override int GetHashCode() + { + return HashCode.Combine(TexturePoolGpuVa, TexturePoolMaximumId, TextureBufferIndex); + } } -#pragma warning restore CS0659 } diff --git a/src/Ryujinx.Graphics.Gpu/Shader/ResourceCounts.cs b/src/Ryujinx.Graphics.Gpu/Shader/ResourceCounts.cs index 126e3249c..59ab378cf 100644 --- a/src/Ryujinx.Graphics.Gpu/Shader/ResourceCounts.cs +++ b/src/Ryujinx.Graphics.Gpu/Shader/ResourceCounts.cs @@ -24,5 +24,10 @@ namespace Ryujinx.Graphics.Gpu.Shader /// Total of images used by the shaders. /// public int ImagesCount; + + /// + /// Total of extra sets used by the shaders. + /// + public int SetsCount; } } diff --git a/src/Ryujinx.Graphics.Gpu/Shader/ShaderCache.cs b/src/Ryujinx.Graphics.Gpu/Shader/ShaderCache.cs index 0b17af8b2..4fc66c4c0 100644 --- a/src/Ryujinx.Graphics.Gpu/Shader/ShaderCache.cs +++ b/src/Ryujinx.Graphics.Gpu/Shader/ShaderCache.cs @@ -192,12 +192,14 @@ namespace Ryujinx.Graphics.Gpu.Shader /// This automatically translates, compiles and adds the code to the cache if not present. /// /// GPU channel + /// Maximum ID that an entry in the sampler pool may have /// Texture pool state /// Compute engine state /// GPU virtual address of the binary shader code /// Compiled compute shader code public CachedShaderProgram GetComputeShader( GpuChannel channel, + int samplerPoolMaximumId, GpuChannelPoolState poolState, GpuChannelComputeState computeState, ulong gpuVa) @@ -214,7 +216,7 @@ namespace Ryujinx.Graphics.Gpu.Shader } ShaderSpecializationState specState = new(ref computeState); - GpuAccessorState gpuAccessorState = new(poolState, computeState, default, specState); + GpuAccessorState gpuAccessorState = new(samplerPoolMaximumId, poolState, computeState, default, specState); GpuAccessor gpuAccessor = new(_context, channel, gpuAccessorState); gpuAccessor.InitializeReservedCounts(tfEnabled: false, vertexAsCompute: false); @@ -291,6 +293,7 @@ namespace Ryujinx.Graphics.Gpu.Shader /// GPU state /// Pipeline state /// GPU channel + /// Maximum ID that an entry in the sampler pool may have /// Texture pool state /// 3D engine state /// Addresses of the shaders for each stage @@ -299,6 +302,7 @@ namespace Ryujinx.Graphics.Gpu.Shader ref ThreedClassState state, ref ProgramPipelineState pipeline, GpuChannel channel, + int samplerPoolMaximumId, ref GpuChannelPoolState poolState, ref GpuChannelGraphicsState graphicsState, ShaderAddresses addresses) @@ -319,7 +323,7 @@ namespace Ryujinx.Graphics.Gpu.Shader UpdatePipelineInfo(ref state, ref pipeline, graphicsState, channel); ShaderSpecializationState specState = new(ref graphicsState, ref pipeline, transformFeedbackDescriptors); - GpuAccessorState gpuAccessorState = new(poolState, default, graphicsState, specState, transformFeedbackDescriptors); + GpuAccessorState gpuAccessorState = new(samplerPoolMaximumId, poolState, default, graphicsState, specState, transformFeedbackDescriptors); ReadOnlySpan addressesSpan = addresses.AsSpan(); @@ -335,7 +339,7 @@ namespace Ryujinx.Graphics.Gpu.Shader if (gpuVa != 0) { - GpuAccessor gpuAccessor = new(_context, channel, gpuAccessorState, stageIndex); + GpuAccessor gpuAccessor = new(_context, channel, gpuAccessorState, stageIndex, addresses.Geometry != 0); TranslatorContext currentStage = DecodeGraphicsShader(gpuAccessor, api, DefaultFlags, gpuVa); if (nextStage != null) diff --git a/src/Ryujinx.Graphics.Gpu/Shader/ShaderInfoBuilder.cs b/src/Ryujinx.Graphics.Gpu/Shader/ShaderInfoBuilder.cs index ea8f164f1..42b2cbb59 100644 --- a/src/Ryujinx.Graphics.Gpu/Shader/ShaderInfoBuilder.cs +++ b/src/Ryujinx.Graphics.Gpu/Shader/ShaderInfoBuilder.cs @@ -1,5 +1,6 @@ using Ryujinx.Graphics.GAL; using Ryujinx.Graphics.Shader; +using System; using System.Collections.Generic; namespace Ryujinx.Graphics.Gpu.Shader @@ -9,13 +10,6 @@ namespace Ryujinx.Graphics.Gpu.Shader /// class ShaderInfoBuilder { - private const int TotalSets = 4; - - private const int UniformSetIndex = 0; - private const int StorageSetIndex = 1; - private const int TextureSetIndex = 2; - private const int ImageSetIndex = 3; - private const ResourceStages SupportBufferStages = ResourceStages.Compute | ResourceStages.Vertex | @@ -36,8 +30,8 @@ namespace Ryujinx.Graphics.Gpu.Shader private readonly int _reservedTextures; private readonly int _reservedImages; - private readonly List[] _resourceDescriptors; - private readonly List[] _resourceUsages; + private List[] _resourceDescriptors; + private List[] _resourceUsages; /// /// Creates a new shader info builder. @@ -51,17 +45,27 @@ namespace Ryujinx.Graphics.Gpu.Shader _fragmentOutputMap = -1; - _resourceDescriptors = new List[TotalSets]; - _resourceUsages = new List[TotalSets]; + int uniformSetIndex = context.Capabilities.UniformBufferSetIndex; + int storageSetIndex = context.Capabilities.StorageBufferSetIndex; + int textureSetIndex = context.Capabilities.TextureSetIndex; + int imageSetIndex = context.Capabilities.ImageSetIndex; - for (int index = 0; index < TotalSets; index++) + int totalSets = Math.Max(uniformSetIndex, storageSetIndex); + totalSets = Math.Max(totalSets, textureSetIndex); + totalSets = Math.Max(totalSets, imageSetIndex); + totalSets++; + + _resourceDescriptors = new List[totalSets]; + _resourceUsages = new List[totalSets]; + + for (int index = 0; index < totalSets; index++) { _resourceDescriptors[index] = new(); _resourceUsages[index] = new(); } - AddDescriptor(SupportBufferStages, ResourceType.UniformBuffer, UniformSetIndex, 0, 1); - AddUsage(SupportBufferStages, ResourceType.UniformBuffer, UniformSetIndex, 0, 1); + AddDescriptor(SupportBufferStages, ResourceType.UniformBuffer, uniformSetIndex, 0, 1); + AddUsage(SupportBufferStages, ResourceType.UniformBuffer, uniformSetIndex, 0, 1); ResourceReservationCounts rrc = new(!context.Capabilities.SupportsTransformFeedback && tfEnabled, vertexAsCompute); @@ -73,12 +77,20 @@ namespace Ryujinx.Graphics.Gpu.Shader // TODO: Handle that better? Maybe we should only set the binding that are really needed on each shader. ResourceStages stages = vertexAsCompute ? ResourceStages.Compute | ResourceStages.Vertex : VtgStages; - PopulateDescriptorAndUsages(stages, ResourceType.UniformBuffer, UniformSetIndex, 1, rrc.ReservedConstantBuffers - 1); - PopulateDescriptorAndUsages(stages, ResourceType.StorageBuffer, StorageSetIndex, 0, rrc.ReservedStorageBuffers); - PopulateDescriptorAndUsages(stages, ResourceType.BufferTexture, TextureSetIndex, 0, rrc.ReservedTextures); - PopulateDescriptorAndUsages(stages, ResourceType.BufferImage, ImageSetIndex, 0, rrc.ReservedImages); + PopulateDescriptorAndUsages(stages, ResourceType.UniformBuffer, uniformSetIndex, 1, rrc.ReservedConstantBuffers - 1); + PopulateDescriptorAndUsages(stages, ResourceType.StorageBuffer, storageSetIndex, 0, rrc.ReservedStorageBuffers); + PopulateDescriptorAndUsages(stages, ResourceType.BufferTexture, textureSetIndex, 0, rrc.ReservedTextures); + PopulateDescriptorAndUsages(stages, ResourceType.BufferImage, imageSetIndex, 0, rrc.ReservedImages); } + /// + /// Populates descriptors and usages for vertex as compute and transform feedback emulation reserved resources. + /// + /// Shader stages where the resources are used + /// Resource type + /// Resource set index where the resources are used + /// First binding number + /// Amount of bindings private void PopulateDescriptorAndUsages(ResourceStages stages, ResourceType type, int setIndex, int start, int count) { AddDescriptor(stages, type, setIndex, start, count); @@ -127,18 +139,23 @@ namespace Ryujinx.Graphics.Gpu.Shader int textureBinding = _reservedTextures + stageIndex * texturesPerStage * 2; int imageBinding = _reservedImages + stageIndex * imagesPerStage * 2; - AddDescriptor(stages, ResourceType.UniformBuffer, UniformSetIndex, uniformBinding, uniformsPerStage); - AddDescriptor(stages, ResourceType.StorageBuffer, StorageSetIndex, storageBinding, storagesPerStage); - AddDualDescriptor(stages, ResourceType.TextureAndSampler, ResourceType.BufferTexture, TextureSetIndex, textureBinding, texturesPerStage); - AddDualDescriptor(stages, ResourceType.Image, ResourceType.BufferImage, ImageSetIndex, imageBinding, imagesPerStage); + int uniformSetIndex = _context.Capabilities.UniformBufferSetIndex; + int storageSetIndex = _context.Capabilities.StorageBufferSetIndex; + int textureSetIndex = _context.Capabilities.TextureSetIndex; + int imageSetIndex = _context.Capabilities.ImageSetIndex; - AddArrayDescriptors(info.Textures, stages, TextureSetIndex, isImage: false); - AddArrayDescriptors(info.Images, stages, TextureSetIndex, isImage: true); + AddDescriptor(stages, ResourceType.UniformBuffer, uniformSetIndex, uniformBinding, uniformsPerStage); + AddDescriptor(stages, ResourceType.StorageBuffer, storageSetIndex, storageBinding, storagesPerStage); + AddDualDescriptor(stages, ResourceType.TextureAndSampler, ResourceType.BufferTexture, textureSetIndex, textureBinding, texturesPerStage); + AddDualDescriptor(stages, ResourceType.Image, ResourceType.BufferImage, imageSetIndex, imageBinding, imagesPerStage); - AddUsage(info.CBuffers, stages, UniformSetIndex, isStorage: false); - AddUsage(info.SBuffers, stages, StorageSetIndex, isStorage: true); - AddUsage(info.Textures, stages, TextureSetIndex, isImage: false); - AddUsage(info.Images, stages, ImageSetIndex, isImage: true); + AddArrayDescriptors(info.Textures, stages, isImage: false); + AddArrayDescriptors(info.Images, stages, isImage: true); + + AddUsage(info.CBuffers, stages, isStorage: false); + AddUsage(info.SBuffers, stages, isStorage: true); + AddUsage(info.Textures, stages, isImage: false); + AddUsage(info.Images, stages, isImage: true); } /// @@ -177,21 +194,16 @@ namespace Ryujinx.Graphics.Gpu.Shader /// /// Textures to be added /// Stages where the textures are used - /// Descriptor set index where the textures will be bound /// True for images, false for textures - private void AddArrayDescriptors(IEnumerable textures, ResourceStages stages, int setIndex, bool isImage) + private void AddArrayDescriptors(IEnumerable textures, ResourceStages stages, bool isImage) { foreach (TextureDescriptor texture in textures) { if (texture.ArrayLength > 1) { - bool isBuffer = (texture.Type & SamplerType.Mask) == SamplerType.TextureBuffer; + ResourceType type = GetTextureResourceType(texture, isImage); - ResourceType type = isBuffer - ? (isImage ? ResourceType.BufferImage : ResourceType.BufferTexture) - : (isImage ? ResourceType.Image : ResourceType.TextureAndSampler); - - _resourceDescriptors[setIndex].Add(new ResourceDescriptor(texture.Binding, texture.ArrayLength, type, stages)); + GetDescriptors(texture.Set).Add(new ResourceDescriptor(texture.Binding, texture.ArrayLength, type, stages)); } } } @@ -217,13 +229,12 @@ namespace Ryujinx.Graphics.Gpu.Shader /// /// Buffers to be added /// Stages where the buffers are used - /// Descriptor set index where the buffers will be bound /// True for storage buffers, false for uniform buffers - private void AddUsage(IEnumerable buffers, ResourceStages stages, int setIndex, bool isStorage) + private void AddUsage(IEnumerable buffers, ResourceStages stages, bool isStorage) { foreach (BufferDescriptor buffer in buffers) { - _resourceUsages[setIndex].Add(new ResourceUsage( + GetUsages(buffer.Set).Add(new ResourceUsage( buffer.Binding, 1, isStorage ? ResourceType.StorageBuffer : ResourceType.UniformBuffer, @@ -236,19 +247,88 @@ namespace Ryujinx.Graphics.Gpu.Shader /// /// Textures to be added /// Stages where the textures are used - /// Descriptor set index where the textures will be bound /// True for images, false for textures - private void AddUsage(IEnumerable textures, ResourceStages stages, int setIndex, bool isImage) + private void AddUsage(IEnumerable textures, ResourceStages stages, bool isImage) { foreach (TextureDescriptor texture in textures) { - bool isBuffer = (texture.Type & SamplerType.Mask) == SamplerType.TextureBuffer; + ResourceType type = GetTextureResourceType(texture, isImage); - ResourceType type = isBuffer - ? (isImage ? ResourceType.BufferImage : ResourceType.BufferTexture) - : (isImage ? ResourceType.Image : ResourceType.TextureAndSampler); + GetUsages(texture.Set).Add(new ResourceUsage(texture.Binding, texture.ArrayLength, type, stages)); + } + } - _resourceUsages[setIndex].Add(new ResourceUsage(texture.Binding, texture.ArrayLength, type, stages)); + /// + /// Gets the list of resource descriptors for a given set index. A new list will be created if needed. + /// + /// Resource set index + /// List of resource descriptors + private List GetDescriptors(int setIndex) + { + if (_resourceDescriptors.Length <= setIndex) + { + int oldLength = _resourceDescriptors.Length; + Array.Resize(ref _resourceDescriptors, setIndex + 1); + + for (int index = oldLength; index <= setIndex; index++) + { + _resourceDescriptors[index] = new(); + } + } + + return _resourceDescriptors[setIndex]; + } + + /// + /// Gets the list of resource usages for a given set index. A new list will be created if needed. + /// + /// Resource set index + /// List of resource usages + private List GetUsages(int setIndex) + { + if (_resourceUsages.Length <= setIndex) + { + int oldLength = _resourceUsages.Length; + Array.Resize(ref _resourceUsages, setIndex + 1); + + for (int index = oldLength; index <= setIndex; index++) + { + _resourceUsages[index] = new(); + } + } + + return _resourceUsages[setIndex]; + } + + /// + /// Gets a resource type from a texture descriptor. + /// + /// Texture descriptor + /// Whether the texture is a image texture (writable) or not (sampled) + /// Resource type + private static ResourceType GetTextureResourceType(TextureDescriptor texture, bool isImage) + { + bool isBuffer = (texture.Type & SamplerType.Mask) == SamplerType.TextureBuffer; + + if (isBuffer) + { + return isImage ? ResourceType.BufferImage : ResourceType.BufferTexture; + } + else if (isImage) + { + return ResourceType.Image; + } + else if (texture.Type == SamplerType.None) + { + return ResourceType.Sampler; + } + else if (texture.Separate) + { + return ResourceType.Texture; + } + else + { + return ResourceType.TextureAndSampler; } } @@ -260,10 +340,12 @@ namespace Ryujinx.Graphics.Gpu.Shader /// Shader information public ShaderInfo Build(ProgramPipelineState? pipeline, bool fromCache = false) { - var descriptors = new ResourceDescriptorCollection[TotalSets]; - var usages = new ResourceUsageCollection[TotalSets]; + int totalSets = _resourceDescriptors.Length; - for (int index = 0; index < TotalSets; index++) + var descriptors = new ResourceDescriptorCollection[totalSets]; + var usages = new ResourceUsageCollection[totalSets]; + + for (int index = 0; index < totalSets; index++) { descriptors[index] = new ResourceDescriptorCollection(_resourceDescriptors[index].ToArray().AsReadOnly()); usages[index] = new ResourceUsageCollection(_resourceUsages[index].ToArray().AsReadOnly()); diff --git a/src/Ryujinx.Graphics.Gpu/Shader/ShaderSpecializationState.cs b/src/Ryujinx.Graphics.Gpu/Shader/ShaderSpecializationState.cs index c90a0b8f4..98acb6f27 100644 --- a/src/Ryujinx.Graphics.Gpu/Shader/ShaderSpecializationState.cs +++ b/src/Ryujinx.Graphics.Gpu/Shader/ShaderSpecializationState.cs @@ -31,6 +31,7 @@ namespace Ryujinx.Graphics.Gpu.Shader PrimitiveTopology = 1 << 1, TransformFeedback = 1 << 3, TextureArrayFromBuffer = 1 << 4, + TextureArrayFromPool = 1 << 5, } private QueriedStateFlags _queriedState; @@ -154,7 +155,8 @@ namespace Ryujinx.Graphics.Gpu.Shader } private readonly Dictionary> _textureSpecialization; - private readonly Dictionary _textureArraySpecialization; + private readonly Dictionary _textureArrayFromBufferSpecialization; + private readonly Dictionary _textureArrayFromPoolSpecialization; private KeyValuePair>[] _allTextures; private Box[][] _textureByBinding; private Box[][] _imageByBinding; @@ -165,7 +167,8 @@ namespace Ryujinx.Graphics.Gpu.Shader private ShaderSpecializationState() { _textureSpecialization = new Dictionary>(); - _textureArraySpecialization = new Dictionary(); + _textureArrayFromBufferSpecialization = new Dictionary(); + _textureArrayFromPoolSpecialization = new Dictionary(); } /// @@ -327,7 +330,7 @@ namespace Ryujinx.Graphics.Gpu.Shader } /// - /// Indicates that the coordinate normalization state of a given texture was used during the shader translation process. + /// Registers the length of a texture array calculated from a constant buffer size. /// /// Shader stage where the texture is used /// Offset in words of the texture handle on the texture buffer @@ -335,10 +338,21 @@ namespace Ryujinx.Graphics.Gpu.Shader /// Number of elements in the texture array public void RegisterTextureArrayLengthFromBuffer(int stageIndex, int handle, int cbufSlot, int length) { - _textureArraySpecialization[new TextureKey(stageIndex, handle, cbufSlot)] = length; + _textureArrayFromBufferSpecialization[new TextureKey(stageIndex, handle, cbufSlot)] = length; _queriedState |= QueriedStateFlags.TextureArrayFromBuffer; } + /// + /// Registers the length of a texture array calculated from a texture or sampler pool capacity. + /// + /// True for sampler pool, false for texture pool + /// Number of elements in the texture array + public void RegisterTextureArrayLengthFromPool(bool isSampler, int length) + { + _textureArrayFromPoolSpecialization[isSampler] = length; + _queriedState |= QueriedStateFlags.TextureArrayFromPool; + } + /// /// Indicates that the format of a given texture was used during the shader translation process. /// @@ -385,7 +399,7 @@ namespace Ryujinx.Graphics.Gpu.Shader } /// - /// Checks if a given texture was registerd on this specialization state. + /// Checks if a given texture was registered on this specialization state. /// /// Shader stage where the texture is used /// Offset in words of the texture handle on the texture buffer @@ -396,14 +410,25 @@ namespace Ryujinx.Graphics.Gpu.Shader } /// - /// Checks if a given texture array (from constant buffer) was registerd on this specialization state. + /// Checks if a given texture array (from constant buffer) was registered on this specialization state. /// /// Shader stage where the texture is used /// Offset in words of the texture handle on the texture buffer /// Slot of the texture buffer constant buffer + /// True if the length for the given buffer and stage exists, false otherwise public bool TextureArrayFromBufferRegistered(int stageIndex, int handle, int cbufSlot) { - return _textureArraySpecialization.ContainsKey(new TextureKey(stageIndex, handle, cbufSlot)); + return _textureArrayFromBufferSpecialization.ContainsKey(new TextureKey(stageIndex, handle, cbufSlot)); + } + + /// + /// Checks if a given texture array (from a sampler pool or texture pool) was registered on this specialization state. + /// + /// True for sampler pool, false for texture pool + /// True if the length for the given pool, false otherwise + public bool TextureArrayFromPoolRegistered(bool isSampler) + { + return _textureArrayFromPoolSpecialization.ContainsKey(isSampler); } /// @@ -412,6 +437,7 @@ namespace Ryujinx.Graphics.Gpu.Shader /// Shader stage where the texture is used /// Offset in words of the texture handle on the texture buffer /// Slot of the texture buffer constant buffer + /// Format and sRGB tuple public (uint, bool) GetFormat(int stageIndex, int handle, int cbufSlot) { TextureSpecializationState state = GetTextureSpecState(stageIndex, handle, cbufSlot).Value; @@ -424,6 +450,7 @@ namespace Ryujinx.Graphics.Gpu.Shader /// Shader stage where the texture is used /// Offset in words of the texture handle on the texture buffer /// Slot of the texture buffer constant buffer + /// Texture target public TextureTarget GetTextureTarget(int stageIndex, int handle, int cbufSlot) { return GetTextureSpecState(stageIndex, handle, cbufSlot).Value.TextureTarget; @@ -435,6 +462,7 @@ namespace Ryujinx.Graphics.Gpu.Shader /// Shader stage where the texture is used /// Offset in words of the texture handle on the texture buffer /// Slot of the texture buffer constant buffer + /// True if coordinates are normalized, false otherwise public bool GetCoordNormalized(int stageIndex, int handle, int cbufSlot) { return GetTextureSpecState(stageIndex, handle, cbufSlot).Value.CoordNormalized; @@ -446,9 +474,20 @@ namespace Ryujinx.Graphics.Gpu.Shader /// Shader stage where the texture is used /// Offset in words of the texture handle on the texture buffer /// Slot of the texture buffer constant buffer + /// Texture array length public int GetTextureArrayFromBufferLength(int stageIndex, int handle, int cbufSlot) { - return _textureArraySpecialization[new TextureKey(stageIndex, handle, cbufSlot)]; + return _textureArrayFromBufferSpecialization[new TextureKey(stageIndex, handle, cbufSlot)]; + } + + /// + /// Gets the recorded length of a given texture array (from a sampler or texture pool). + /// + /// True to get the sampler pool length, false to get the texture pool length + /// Texture array length + public int GetTextureArrayFromPoolLength(bool isSampler) + { + return _textureArrayFromPoolSpecialization[isSampler]; } /// @@ -894,7 +933,23 @@ namespace Ryujinx.Graphics.Gpu.Shader dataReader.ReadWithMagicAndSize(ref textureKey, TexkMagic); dataReader.Read(ref length); - specState._textureArraySpecialization[textureKey] = length; + specState._textureArrayFromBufferSpecialization[textureKey] = length; + } + } + + if (specState._queriedState.HasFlag(QueriedStateFlags.TextureArrayFromPool)) + { + dataReader.Read(ref count); + + for (int index = 0; index < count; index++) + { + bool textureKey = default; + int length = 0; + + dataReader.ReadWithMagicAndSize(ref textureKey, TexkMagic); + dataReader.Read(ref length); + + specState._textureArrayFromPoolSpecialization[textureKey] = length; } } @@ -965,10 +1020,25 @@ namespace Ryujinx.Graphics.Gpu.Shader if (_queriedState.HasFlag(QueriedStateFlags.TextureArrayFromBuffer)) { - count = (ushort)_textureArraySpecialization.Count; + count = (ushort)_textureArrayFromBufferSpecialization.Count; dataWriter.Write(ref count); - foreach (var kv in _textureArraySpecialization) + foreach (var kv in _textureArrayFromBufferSpecialization) + { + var textureKey = kv.Key; + var length = kv.Value; + + dataWriter.WriteWithMagicAndSize(ref textureKey, TexkMagic); + dataWriter.Write(ref length); + } + } + + if (_queriedState.HasFlag(QueriedStateFlags.TextureArrayFromPool)) + { + count = (ushort)_textureArrayFromPoolSpecialization.Count; + dataWriter.Write(ref count); + + foreach (var kv in _textureArrayFromPoolSpecialization) { var textureKey = kv.Key; var length = kv.Value; diff --git a/src/Ryujinx.Graphics.OpenGL/OpenGLRenderer.cs b/src/Ryujinx.Graphics.OpenGL/OpenGLRenderer.cs index a945cbf20..ba9cd45c6 100644 --- a/src/Ryujinx.Graphics.OpenGL/OpenGLRenderer.cs +++ b/src/Ryujinx.Graphics.OpenGL/OpenGLRenderer.cs @@ -61,7 +61,9 @@ namespace Ryujinx.Graphics.OpenGL { BufferCount++; - if (access.HasFlag(GAL.BufferAccess.FlushPersistent)) + var memType = access & GAL.BufferAccess.MemoryTypeMask; + + if (memType == GAL.BufferAccess.HostMemory) { BufferHandle handle = Buffer.CreatePersistent(size); @@ -75,11 +77,6 @@ namespace Ryujinx.Graphics.OpenGL } } - public BufferHandle CreateBuffer(int size, GAL.BufferAccess access, BufferHandle storageHint) - { - return CreateBuffer(size, access); - } - public BufferHandle CreateBuffer(nint pointer, int size) { throw new NotSupportedException(); @@ -148,6 +145,7 @@ namespace Ryujinx.Graphics.OpenGL return new Capabilities( api: TargetApi.OpenGL, vendorName: GpuVendor, + memoryType: SystemMemoryType.BackendManaged, hasFrontFacingBug: intelWindows, hasVectorIndexingBug: amdWindows, needsFragmentOutputSpecialization: false, @@ -161,6 +159,7 @@ namespace Ryujinx.Graphics.OpenGL supportsBgraFormat: false, supportsR4G4Format: false, supportsR4G4B4A4Format: true, + supportsScaledVertexFormats: true, supportsSnormBufferTextureFormat: false, supports5BitComponentFormat: true, supportsSparseBuffer: false, @@ -175,7 +174,8 @@ namespace Ryujinx.Graphics.OpenGL supportsMismatchingViewFormat: HwCapabilities.SupportsMismatchingViewFormat, supportsCubemapView: true, supportsNonConstantTextureOffset: HwCapabilities.SupportsNonConstantTextureOffset, - supportsScaledVertexFormats: true, + supportsQuads: HwCapabilities.SupportsQuads, + supportsSeparateSampler: false, supportsShaderBallot: HwCapabilities.SupportsShaderBallot, supportsShaderBarrierDivergence: !(intelWindows || intelUnix), supportsShaderFloat64: true, @@ -187,6 +187,12 @@ namespace Ryujinx.Graphics.OpenGL supportsViewportSwizzle: HwCapabilities.SupportsViewportSwizzle, supportsIndirectParameters: HwCapabilities.SupportsIndirectParameters, supportsDepthClipControl: true, + uniformBufferSetIndex: 0, + storageBufferSetIndex: 1, + textureSetIndex: 2, + imageSetIndex: 3, + extraSetBaseIndex: 0, + maximumExtraSets: 0, maximumUniformBuffersPerStage: 13, // TODO: Avoid hardcoding those limits here and get from driver? maximumStorageBuffersPerStage: 16, maximumTexturesPerStage: 32, diff --git a/src/Ryujinx.Graphics.OpenGL/Pipeline.cs b/src/Ryujinx.Graphics.OpenGL/Pipeline.cs index 6d066bb67..54f6b3f7b 100644 --- a/src/Ryujinx.Graphics.OpenGL/Pipeline.cs +++ b/src/Ryujinx.Graphics.OpenGL/Pipeline.cs @@ -963,6 +963,11 @@ namespace Ryujinx.Graphics.OpenGL (array as ImageArray).Bind(binding); } + public void SetImageArraySeparate(ShaderStage stage, int setIndex, IImageArray array) + { + throw new NotSupportedException("OpenGL does not support descriptor sets."); + } + public void SetIndexBuffer(BufferRange buffer, IndexType type) { _elementsType = type.Convert(); @@ -1312,6 +1317,11 @@ namespace Ryujinx.Graphics.OpenGL (array as TextureArray).Bind(binding); } + public void SetTextureArraySeparate(ShaderStage stage, int setIndex, ITextureArray array) + { + throw new NotSupportedException("OpenGL does not support descriptor sets."); + } + public void SetTransformFeedbackBuffers(ReadOnlySpan buffers) { if (_tfEnabled) diff --git a/src/Ryujinx.Graphics.Shader/BufferDescriptor.cs b/src/Ryujinx.Graphics.Shader/BufferDescriptor.cs index ead1c5e67..11d4e3c11 100644 --- a/src/Ryujinx.Graphics.Shader/BufferDescriptor.cs +++ b/src/Ryujinx.Graphics.Shader/BufferDescriptor.cs @@ -4,14 +4,16 @@ namespace Ryujinx.Graphics.Shader { // New fields should be added to the end of the struct to keep disk shader cache compatibility. + public readonly int Set; public readonly int Binding; public readonly byte Slot; public readonly byte SbCbSlot; public readonly ushort SbCbOffset; public readonly BufferUsageFlags Flags; - public BufferDescriptor(int binding, int slot) + public BufferDescriptor(int set, int binding, int slot) { + Set = set; Binding = binding; Slot = (byte)slot; SbCbSlot = 0; @@ -19,8 +21,9 @@ namespace Ryujinx.Graphics.Shader Flags = BufferUsageFlags.None; } - public BufferDescriptor(int binding, int slot, int sbCbSlot, int sbCbOffset, BufferUsageFlags flags) + public BufferDescriptor(int set, int binding, int slot, int sbCbSlot, int sbCbOffset, BufferUsageFlags flags) { + Set = set; Binding = binding; Slot = (byte)slot; SbCbSlot = (byte)sbCbSlot; diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/Declarations.cs b/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/Declarations.cs index 763487dac..eb6c689b8 100644 --- a/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/Declarations.cs +++ b/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/Declarations.cs @@ -6,7 +6,6 @@ using System; using System.Collections.Generic; using System.Globalization; using System.Linq; -using System.Numerics; namespace Ryujinx.Graphics.Shader.CodeGen.Glsl { @@ -352,7 +351,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl arrayDecl = "[]"; } - string samplerTypeName = definition.Type.ToGlslSamplerType(); + string samplerTypeName = definition.Separate ? definition.Type.ToGlslTextureType() : definition.Type.ToGlslSamplerType(); string layout = string.Empty; diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/Instructions/InstGenMemory.cs b/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/Instructions/InstGenMemory.cs index b4773b819..4308b08f8 100644 --- a/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/Instructions/InstGenMemory.cs +++ b/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/Instructions/InstGenMemory.cs @@ -462,7 +462,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions } else { - context.Properties.Textures.TryGetValue(texOp.Binding, out TextureDefinition definition); + context.Properties.Textures.TryGetValue(texOp.GetTextureSetAndBinding(), out TextureDefinition definition); bool hasLod = !definition.Type.HasFlag(SamplerType.Multisample) && (definition.Type & SamplerType.Mask) != SamplerType.TextureBuffer; string texCall; @@ -639,20 +639,33 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions private static string GetSamplerName(CodeGenContext context, AstTextureOperation texOp, ref int srcIndex) { - TextureDefinition definition = context.Properties.Textures[texOp.Binding]; - string name = definition.Name; + TextureDefinition textureDefinition = context.Properties.Textures[texOp.GetTextureSetAndBinding()]; + string name = textureDefinition.Name; - if (definition.ArrayLength != 1) + if (textureDefinition.ArrayLength != 1) { name = $"{name}[{GetSourceExpr(context, texOp.GetSource(srcIndex++), AggregateType.S32)}]"; } + if (texOp.IsSeparate) + { + TextureDefinition samplerDefinition = context.Properties.Textures[texOp.GetSamplerSetAndBinding()]; + string samplerName = samplerDefinition.Name; + + if (samplerDefinition.ArrayLength != 1) + { + samplerName = $"{samplerName}[{GetSourceExpr(context, texOp.GetSource(srcIndex++), AggregateType.S32)}]"; + } + + name = $"{texOp.Type.ToGlslSamplerType()}({name}, {samplerName})"; + } + return name; } private static string GetImageName(CodeGenContext context, AstTextureOperation texOp, ref int srcIndex) { - TextureDefinition definition = context.Properties.Images[texOp.Binding]; + TextureDefinition definition = context.Properties.Images[texOp.GetTextureSetAndBinding()]; string name = definition.Name; if (definition.ArrayLength != 1) diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/CodeGenContext.cs b/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/CodeGenContext.cs index 2b1fdf44c..f3be29bb9 100644 --- a/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/CodeGenContext.cs +++ b/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/CodeGenContext.cs @@ -33,9 +33,9 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv public Dictionary LocalMemories { get; } = new(); public Dictionary SharedMemories { get; } = new(); - public Dictionary SamplersTypes { get; } = new(); - public Dictionary Samplers { get; } = new(); - public Dictionary Images { get; } = new(); + public Dictionary SamplersTypes { get; } = new(); + public Dictionary Samplers { get; } = new(); + public Dictionary Images { get; } = new(); public Dictionary Inputs { get; } = new(); public Dictionary Outputs { get; } = new(); diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/Declarations.cs b/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/Declarations.cs index 9633c522e..55d35bf0d 100644 --- a/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/Declarations.cs +++ b/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/Declarations.cs @@ -160,49 +160,61 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv { int setIndex = context.TargetApi == TargetApi.Vulkan ? sampler.Set : 0; - var dim = (sampler.Type & SamplerType.Mask) switch + SpvInstruction imageType; + SpvInstruction sampledImageType; + + if (sampler.Type != SamplerType.None) { - SamplerType.Texture1D => Dim.Dim1D, - SamplerType.Texture2D => Dim.Dim2D, - SamplerType.Texture3D => Dim.Dim3D, - SamplerType.TextureCube => Dim.Cube, - SamplerType.TextureBuffer => Dim.Buffer, - _ => throw new InvalidOperationException($"Invalid sampler type \"{sampler.Type & SamplerType.Mask}\"."), - }; + var dim = (sampler.Type & SamplerType.Mask) switch + { + SamplerType.Texture1D => Dim.Dim1D, + SamplerType.Texture2D => Dim.Dim2D, + SamplerType.Texture3D => Dim.Dim3D, + SamplerType.TextureCube => Dim.Cube, + SamplerType.TextureBuffer => Dim.Buffer, + _ => throw new InvalidOperationException($"Invalid sampler type \"{sampler.Type & SamplerType.Mask}\"."), + }; - var imageType = context.TypeImage( - context.TypeFP32(), - dim, - sampler.Type.HasFlag(SamplerType.Shadow), - sampler.Type.HasFlag(SamplerType.Array), - sampler.Type.HasFlag(SamplerType.Multisample), - 1, - ImageFormat.Unknown); + imageType = context.TypeImage( + context.TypeFP32(), + dim, + sampler.Type.HasFlag(SamplerType.Shadow), + sampler.Type.HasFlag(SamplerType.Array), + sampler.Type.HasFlag(SamplerType.Multisample), + 1, + ImageFormat.Unknown); - var sampledImageType = context.TypeSampledImage(imageType); - var sampledImagePointerType = context.TypePointer(StorageClass.UniformConstant, sampledImageType); + sampledImageType = context.TypeSampledImage(imageType); + } + else + { + imageType = sampledImageType = context.TypeSampler(); + } + + var sampledOrSeparateImageType = sampler.Separate ? imageType : sampledImageType; + var sampledImagePointerType = context.TypePointer(StorageClass.UniformConstant, sampledOrSeparateImageType); var sampledImageArrayPointerType = sampledImagePointerType; if (sampler.ArrayLength == 0) { - var sampledImageArrayType = context.TypeRuntimeArray(sampledImageType); + var sampledImageArrayType = context.TypeRuntimeArray(sampledOrSeparateImageType); sampledImageArrayPointerType = context.TypePointer(StorageClass.UniformConstant, sampledImageArrayType); } else if (sampler.ArrayLength != 1) { - var sampledImageArrayType = context.TypeArray(sampledImageType, context.Constant(context.TypeU32(), sampler.ArrayLength)); + var sampledImageArrayType = context.TypeArray(sampledOrSeparateImageType, context.Constant(context.TypeU32(), sampler.ArrayLength)); sampledImageArrayPointerType = context.TypePointer(StorageClass.UniformConstant, sampledImageArrayType); } var sampledImageVariable = context.Variable(sampledImageArrayPointerType, StorageClass.UniformConstant); - context.Samplers.Add(sampler.Binding, new SamplerDeclaration( + context.Samplers.Add(new(sampler.Set, sampler.Binding), new SamplerDeclaration( imageType, sampledImageType, sampledImagePointerType, sampledImageVariable, sampler.ArrayLength != 1)); - context.SamplersTypes.Add(sampler.Binding, sampler.Type); + context.SamplersTypes.Add(new(sampler.Set, sampler.Binding), sampler.Type); context.Name(sampledImageVariable, sampler.Name); context.Decorate(sampledImageVariable, Decoration.DescriptorSet, (LiteralInteger)setIndex); @@ -244,7 +256,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv var imageVariable = context.Variable(imageArrayPointerType, StorageClass.UniformConstant); - context.Images.Add(image.Binding, new ImageDeclaration(imageType, imagePointerType, imageVariable, image.ArrayLength != 1)); + context.Images.Add(new(image.Set, image.Binding), new ImageDeclaration(imageType, imagePointerType, imageVariable, image.ArrayLength != 1)); context.Name(imageVariable, image.Name); context.Decorate(imageVariable, Decoration.DescriptorSet, (LiteralInteger)setIndex); diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/Instructions.cs b/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/Instructions.cs index 409e466cd..6206985d8 100644 --- a/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/Instructions.cs +++ b/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/Instructions.cs @@ -602,7 +602,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv return context.Get(type, texOp.GetSource(srcIndex++)); } - ImageDeclaration declaration = context.Images[texOp.Binding]; + ImageDeclaration declaration = context.Images[texOp.GetTextureSetAndBinding()]; SpvInstruction image = declaration.Image; SpvInstruction resultType = context.GetType(componentType); @@ -681,7 +681,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv return context.Get(type, texOp.GetSource(srcIndex++)); } - ImageDeclaration declaration = context.Images[texOp.Binding]; + ImageDeclaration declaration = context.Images[texOp.GetTextureSetAndBinding()]; SpvInstruction image = declaration.Image; if (declaration.IsIndexed) @@ -738,7 +738,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv return context.Get(type, texOp.GetSource(srcIndex++)); } - ImageDeclaration declaration = context.Images[texOp.Binding]; + ImageDeclaration declaration = context.Images[texOp.GetTextureSetAndBinding()]; SpvInstruction image = declaration.Image; if (declaration.IsIndexed) @@ -837,17 +837,8 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv return context.Get(type, texOp.GetSource(srcIndex++)); } - SamplerDeclaration declaration = context.Samplers[texOp.Binding]; - SpvInstruction image = declaration.Image; - - if (declaration.IsIndexed) - { - SpvInstruction textureIndex = Src(AggregateType.S32); - - image = context.AccessChain(declaration.SampledImagePointerType, image, textureIndex); - } - - image = context.Load(declaration.SampledImageType, image); + SamplerDeclaration declaration = context.Samplers[texOp.GetTextureSetAndBinding()]; + SpvInstruction image = GenerateSampledImageLoad(context, texOp, declaration, ref srcIndex); int pCount = texOp.Type.GetDimensions(); @@ -1170,17 +1161,8 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv return context.Get(type, texOp.GetSource(srcIndex++)); } - SamplerDeclaration declaration = context.Samplers[texOp.Binding]; - SpvInstruction image = declaration.Image; - - if (declaration.IsIndexed) - { - SpvInstruction textureIndex = Src(AggregateType.S32); - - image = context.AccessChain(declaration.SampledImagePointerType, image, textureIndex); - } - - image = context.Load(declaration.SampledImageType, image); + SamplerDeclaration declaration = context.Samplers[texOp.GetTextureSetAndBinding()]; + SpvInstruction image = GenerateSampledImageLoad(context, texOp, declaration, ref srcIndex); int coordsCount = texOp.Type.GetDimensions(); @@ -1449,17 +1431,11 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv { AstTextureOperation texOp = (AstTextureOperation)operation; - SamplerDeclaration declaration = context.Samplers[texOp.Binding]; - SpvInstruction image = declaration.Image; + int srcIndex = 0; - if (declaration.IsIndexed) - { - SpvInstruction textureIndex = context.GetS32(texOp.GetSource(0)); + SamplerDeclaration declaration = context.Samplers[texOp.GetTextureSetAndBinding()]; + SpvInstruction image = GenerateSampledImageLoad(context, texOp, declaration, ref srcIndex); - image = context.AccessChain(declaration.SampledImagePointerType, image, textureIndex); - } - - image = context.Load(declaration.SampledImageType, image); image = context.Image(declaration.ImageType, image); SpvInstruction result = context.ImageQuerySamples(context.TypeS32(), image); @@ -1471,17 +1447,11 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv { AstTextureOperation texOp = (AstTextureOperation)operation; - SamplerDeclaration declaration = context.Samplers[texOp.Binding]; - SpvInstruction image = declaration.Image; + int srcIndex = 0; - if (declaration.IsIndexed) - { - SpvInstruction textureIndex = context.GetS32(texOp.GetSource(0)); + SamplerDeclaration declaration = context.Samplers[texOp.GetTextureSetAndBinding()]; + SpvInstruction image = GenerateSampledImageLoad(context, texOp, declaration, ref srcIndex); - image = context.AccessChain(declaration.SampledImagePointerType, image, textureIndex); - } - - image = context.Load(declaration.SampledImageType, image); image = context.Image(declaration.ImageType, image); if (texOp.Index == 3) @@ -1490,7 +1460,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv } else { - var type = context.SamplersTypes[texOp.Binding]; + var type = context.SamplersTypes[texOp.GetTextureSetAndBinding()]; bool hasLod = !type.HasFlag(SamplerType.Multisample) && type != SamplerType.TextureBuffer; int dimensions = (type & SamplerType.Mask) == SamplerType.TextureCube ? 2 : type.GetDimensions(); @@ -1506,8 +1476,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv if (hasLod) { - int lodSrcIndex = declaration.IsIndexed ? 1 : 0; - var lod = context.GetS32(operation.GetSource(lodSrcIndex)); + var lod = context.GetS32(operation.GetSource(srcIndex)); result = context.ImageQuerySizeLod(resultType, image, lod); } else @@ -1905,6 +1874,43 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv } } + private static SpvInstruction GenerateSampledImageLoad(CodeGenContext context, AstTextureOperation texOp, SamplerDeclaration declaration, ref int srcIndex) + { + SpvInstruction image = declaration.Image; + + if (declaration.IsIndexed) + { + SpvInstruction textureIndex = context.Get(AggregateType.S32, texOp.GetSource(srcIndex++)); + + image = context.AccessChain(declaration.SampledImagePointerType, image, textureIndex); + } + + if (texOp.IsSeparate) + { + image = context.Load(declaration.ImageType, image); + + SamplerDeclaration samplerDeclaration = context.Samplers[texOp.GetSamplerSetAndBinding()]; + + SpvInstruction sampler = samplerDeclaration.Image; + + if (samplerDeclaration.IsIndexed) + { + SpvInstruction samplerIndex = context.Get(AggregateType.S32, texOp.GetSource(srcIndex++)); + + sampler = context.AccessChain(samplerDeclaration.SampledImagePointerType, sampler, samplerIndex); + } + + sampler = context.Load(samplerDeclaration.ImageType, sampler); + image = context.SampledImage(declaration.SampledImageType, image, sampler); + } + else + { + image = context.Load(declaration.SampledImageType, image); + } + + return image; + } + private static OperationResult GenerateUnary( CodeGenContext context, AstOperation operation, diff --git a/src/Ryujinx.Graphics.Shader/GpuGraphicsState.cs b/src/Ryujinx.Graphics.Shader/GpuGraphicsState.cs index f16c71d55..38684002c 100644 --- a/src/Ryujinx.Graphics.Shader/GpuGraphicsState.cs +++ b/src/Ryujinx.Graphics.Shader/GpuGraphicsState.cs @@ -102,6 +102,11 @@ namespace Ryujinx.Graphics.Shader /// public readonly bool OriginUpperLeft; + /// + /// Indicates that the primitive ID values on the shader should be halved due to quad to triangles conversion. + /// + public readonly bool HalvePrimitiveId; + /// /// Creates a new GPU graphics state. /// @@ -124,6 +129,7 @@ namespace Ryujinx.Graphics.Shader /// Indicates whether dual source blend is enabled /// Indicates if negation of the viewport Y axis is enabled /// If true, indicates that the fragment origin is the upper left corner of the viewport, otherwise it is the lower left corner + /// Indicates that the primitive ID values on the shader should be halved due to quad to triangles conversion public GpuGraphicsState( bool earlyZForce, InputTopology topology, @@ -143,7 +149,8 @@ namespace Ryujinx.Graphics.Shader in Array8 fragmentOutputTypes, bool dualSourceBlendEnable, bool yNegateEnabled, - bool originUpperLeft) + bool originUpperLeft, + bool halvePrimitiveId) { EarlyZForce = earlyZForce; Topology = topology; @@ -164,6 +171,7 @@ namespace Ryujinx.Graphics.Shader DualSourceBlendEnable = dualSourceBlendEnable; YNegateEnabled = yNegateEnabled; OriginUpperLeft = originUpperLeft; + HalvePrimitiveId = halvePrimitiveId; } } } diff --git a/src/Ryujinx.Graphics.Shader/IGpuAccessor.cs b/src/Ryujinx.Graphics.Shader/IGpuAccessor.cs index 99366ad67..4e6d6edf9 100644 --- a/src/Ryujinx.Graphics.Shader/IGpuAccessor.cs +++ b/src/Ryujinx.Graphics.Shader/IGpuAccessor.cs @@ -27,41 +27,43 @@ namespace Ryujinx.Graphics.Shader ReadOnlySpan GetCode(ulong address, int minimumSize); /// - /// Gets the size in bytes of a bound constant buffer for the current shader stage. - /// - /// The number of the constant buffer to get the size from - /// Size in bytes - int QueryTextureArrayLengthFromBuffer(int slot); - - /// - /// Queries the binding number of a constant buffer. + /// Gets the binding number of a constant buffer. /// /// Constant buffer index /// Binding number - int CreateConstantBufferBinding(int index); + SetBindingPair CreateConstantBufferBinding(int index); /// - /// Queries the binding number of an image. + /// Gets the binding number of an image. /// /// For array of images, the number of elements of the array, otherwise it should be 1 /// Indicates if the image is a buffer image /// Binding number - int CreateImageBinding(int count, bool isBuffer); + SetBindingPair CreateImageBinding(int count, bool isBuffer); /// - /// Queries the binding number of a storage buffer. + /// Gets the binding number of a storage buffer. /// /// Storage buffer index /// Binding number - int CreateStorageBufferBinding(int index); + SetBindingPair CreateStorageBufferBinding(int index); /// - /// Queries the binding number of a texture. + /// Gets the binding number of a texture. /// /// For array of textures, the number of elements of the array, otherwise it should be 1 /// Indicates if the texture is a buffer texture /// Binding number - int CreateTextureBinding(int count, bool isBuffer); + SetBindingPair CreateTextureBinding(int count, bool isBuffer); + + /// + /// Gets the set index for an additional set, or -1 if there's no extra set available. + /// + /// Extra set index, or -1 if not available + int CreateExtraSet() + { + return -1; + } /// /// Queries Local Size X for compute shaders. @@ -142,6 +144,7 @@ namespace Ryujinx.Graphics.Shader default, false, false, + false, false); } @@ -298,6 +301,15 @@ namespace Ryujinx.Graphics.Shader return true; } + /// + /// Queries host API support for separate textures and samplers. + /// + /// True if the API supports samplers and textures to be combined on the shader, false otherwise + bool QueryHostSupportsSeparateSampler() + { + return true; + } + /// /// Queries host GPU shader ballot support. /// @@ -388,6 +400,12 @@ namespace Ryujinx.Graphics.Shader return true; } + /// + /// Gets the maximum number of samplers that the bound texture pool may have. + /// + /// Maximum amount of samplers that the pool may have + int QuerySamplerArrayLengthFromPool(); + /// /// Queries sampler type information. /// @@ -399,6 +417,19 @@ namespace Ryujinx.Graphics.Shader return SamplerType.Texture2D; } + /// + /// Gets the size in bytes of a bound constant buffer for the current shader stage. + /// + /// The number of the constant buffer to get the size from + /// Size in bytes + int QueryTextureArrayLengthFromBuffer(int slot); + + /// + /// Gets the maximum number of textures that the bound texture pool may have. + /// + /// Maximum amount of textures that the pool may have + int QueryTextureArrayLengthFromPool(); + /// /// Queries texture coordinate normalization information. /// diff --git a/src/Ryujinx.Graphics.Shader/Instructions/InstEmitAttribute.cs b/src/Ryujinx.Graphics.Shader/Instructions/InstEmitAttribute.cs index 63ce38e25..c704156bc 100644 --- a/src/Ryujinx.Graphics.Shader/Instructions/InstEmitAttribute.cs +++ b/src/Ryujinx.Graphics.Shader/Instructions/InstEmitAttribute.cs @@ -84,6 +84,10 @@ namespace Ryujinx.Graphics.Shader.Instructions value = context.IConvertU32ToFP32(value); } } + else if (offset == AttributeConsts.PrimitiveId && context.TranslatorContext.Definitions.HalvePrimitiveId) + { + value = context.ShiftRightS32(value, Const(1)); + } context.Copy(Register(rd), value); } @@ -187,6 +191,12 @@ namespace Ryujinx.Graphics.Shader.Instructions } } } + else if (op.Imm10 == AttributeConsts.PrimitiveId && context.TranslatorContext.Definitions.HalvePrimitiveId) + { + // If quads are used, but the host does not support them, they need to be converted to triangles. + // Since each quad becomes 2 triangles, we need to compensate here and divide primitive ID by 2. + res = context.ShiftRightS32(res, Const(1)); + } else if (op.Imm10 == AttributeConsts.FrontFacing && context.TranslatorContext.GpuAccessor.QueryHostHasFrontFacingBug()) { // gl_FrontFacing sometimes has incorrect (flipped) values depending how it is accessed on Intel GPUs. diff --git a/src/Ryujinx.Graphics.Shader/Instructions/InstEmitSurface.cs b/src/Ryujinx.Graphics.Shader/Instructions/InstEmitSurface.cs index 0aac0ffa8..383e82c69 100644 --- a/src/Ryujinx.Graphics.Shader/Instructions/InstEmitSurface.cs +++ b/src/Ryujinx.Graphics.Shader/Instructions/InstEmitSurface.cs @@ -278,7 +278,7 @@ namespace Ryujinx.Graphics.Shader.Instructions flags |= TextureFlags.Bindless; } - int binding = isBindless ? 0 : context.ResourceManager.GetTextureOrImageBinding( + SetBindingPair setAndBinding = isBindless ? default : context.ResourceManager.GetTextureOrImageBinding( Instruction.ImageAtomic, type, format, @@ -286,7 +286,7 @@ namespace Ryujinx.Graphics.Shader.Instructions TextureOperation.DefaultCbufSlot, imm); - Operand res = context.ImageAtomic(type, format, flags, binding, sources); + Operand res = context.ImageAtomic(type, format, flags, setAndBinding, sources); context.Copy(d, res); } @@ -389,7 +389,7 @@ namespace Ryujinx.Graphics.Shader.Instructions TextureFormat format = isBindless ? TextureFormat.Unknown : ShaderProperties.GetTextureFormat(context.TranslatorContext.GpuAccessor, handle); - int binding = isBindless ? 0 : context.ResourceManager.GetTextureOrImageBinding( + SetBindingPair setAndBinding = isBindless ? default : context.ResourceManager.GetTextureOrImageBinding( Instruction.ImageLoad, type, format, @@ -397,7 +397,7 @@ namespace Ryujinx.Graphics.Shader.Instructions TextureOperation.DefaultCbufSlot, handle); - context.ImageLoad(type, format, flags, binding, (int)componentMask, dests, sources); + context.ImageLoad(type, format, flags, setAndBinding, (int)componentMask, dests, sources); } else { @@ -432,7 +432,7 @@ namespace Ryujinx.Graphics.Shader.Instructions TextureFormat format = GetTextureFormat(size); - int binding = isBindless ? 0 : context.ResourceManager.GetTextureOrImageBinding( + SetBindingPair setAndBinding = isBindless ? default : context.ResourceManager.GetTextureOrImageBinding( Instruction.ImageLoad, type, format, @@ -440,7 +440,7 @@ namespace Ryujinx.Graphics.Shader.Instructions TextureOperation.DefaultCbufSlot, handle); - context.ImageLoad(type, format, flags, binding, compMask, dests, sources); + context.ImageLoad(type, format, flags, setAndBinding, compMask, dests, sources); switch (size) { @@ -552,7 +552,7 @@ namespace Ryujinx.Graphics.Shader.Instructions flags |= TextureFlags.Bindless; } - int binding = isBindless ? 0 : context.ResourceManager.GetTextureOrImageBinding( + SetBindingPair setAndBinding = isBindless ? default : context.ResourceManager.GetTextureOrImageBinding( Instruction.ImageAtomic, type, format, @@ -560,7 +560,7 @@ namespace Ryujinx.Graphics.Shader.Instructions TextureOperation.DefaultCbufSlot, imm); - context.ImageAtomic(type, format, flags, binding, sources); + context.ImageAtomic(type, format, flags, setAndBinding, sources); } private static void EmitSust( @@ -679,7 +679,7 @@ namespace Ryujinx.Graphics.Shader.Instructions flags |= TextureFlags.Coherent; } - int binding = isBindless ? 0 : context.ResourceManager.GetTextureOrImageBinding( + SetBindingPair setAndBinding = isBindless ? default : context.ResourceManager.GetTextureOrImageBinding( Instruction.ImageStore, type, format, @@ -687,7 +687,7 @@ namespace Ryujinx.Graphics.Shader.Instructions TextureOperation.DefaultCbufSlot, handle); - context.ImageStore(type, format, flags, binding, sources); + context.ImageStore(type, format, flags, setAndBinding, sources); } private static int GetComponentSizeInBytesLog2(SuatomSize size) diff --git a/src/Ryujinx.Graphics.Shader/Instructions/InstEmitTexture.cs b/src/Ryujinx.Graphics.Shader/Instructions/InstEmitTexture.cs index 06daa26a0..2076262da 100644 --- a/src/Ryujinx.Graphics.Shader/Instructions/InstEmitTexture.cs +++ b/src/Ryujinx.Graphics.Shader/Instructions/InstEmitTexture.cs @@ -885,7 +885,7 @@ namespace Ryujinx.Graphics.Shader.Instructions return Register(dest++, RegisterType.Gpr); } - int binding = isBindless ? 0 : context.ResourceManager.GetTextureOrImageBinding( + SetBindingPair setAndBinding = isBindless ? default : context.ResourceManager.GetTextureOrImageBinding( Instruction.Lod, type, TextureFormat.Unknown, @@ -913,7 +913,7 @@ namespace Ryujinx.Graphics.Shader.Instructions else { // The instruction component order is the inverse of GLSL's. - Operand res = context.Lod(type, flags, binding, compIndex ^ 1, sources); + Operand res = context.Lod(type, flags, setAndBinding, compIndex ^ 1, sources); res = context.FPMultiply(res, ConstF(256.0f)); @@ -1116,12 +1116,12 @@ namespace Ryujinx.Graphics.Shader.Instructions } TextureFlags flags = isBindless ? TextureFlags.Bindless : TextureFlags.None; - int binding; + SetBindingPair setAndBinding; switch (query) { case TexQuery.TexHeaderDimension: - binding = isBindless ? 0 : context.ResourceManager.GetTextureOrImageBinding( + setAndBinding = isBindless ? default : context.ResourceManager.GetTextureOrImageBinding( Instruction.TextureQuerySize, type, TextureFormat.Unknown, @@ -1140,13 +1140,13 @@ namespace Ryujinx.Graphics.Shader.Instructions break; } - context.Copy(d, context.TextureQuerySize(type, flags, binding, compIndex, sources)); + context.Copy(d, context.TextureQuerySize(type, flags, setAndBinding, compIndex, sources)); } } break; case TexQuery.TexHeaderTextureType: - binding = isBindless ? 0 : context.ResourceManager.GetTextureOrImageBinding( + setAndBinding = isBindless ? default : context.ResourceManager.GetTextureOrImageBinding( Instruction.TextureQuerySamples, type, TextureFormat.Unknown, @@ -1171,7 +1171,7 @@ namespace Ryujinx.Graphics.Shader.Instructions if (d != null) { - context.Copy(d, context.TextureQuerySamples(type, flags, binding, sources)); + context.Copy(d, context.TextureQuerySamples(type, flags, setAndBinding, sources)); } } break; @@ -1191,7 +1191,7 @@ namespace Ryujinx.Graphics.Shader.Instructions Operand[] dests, Operand[] sources) { - int binding = flags.HasFlag(TextureFlags.Bindless) ? 0 : context.ResourceManager.GetTextureOrImageBinding( + SetBindingPair setAndBinding = flags.HasFlag(TextureFlags.Bindless) ? default : context.ResourceManager.GetTextureOrImageBinding( Instruction.TextureSample, type, TextureFormat.Unknown, @@ -1199,7 +1199,7 @@ namespace Ryujinx.Graphics.Shader.Instructions TextureOperation.DefaultCbufSlot, handle); - context.TextureSample(type, flags, binding, componentMask, dests, sources); + context.TextureSample(type, flags, setAndBinding, componentMask, dests, sources); } private static SamplerType ConvertSamplerType(TexDim dimensions) diff --git a/src/Ryujinx.Graphics.Shader/IntermediateRepresentation/Operation.cs b/src/Ryujinx.Graphics.Shader/IntermediateRepresentation/Operation.cs index 0c1b2a3f3..713e8a4fb 100644 --- a/src/Ryujinx.Graphics.Shader/IntermediateRepresentation/Operation.cs +++ b/src/Ryujinx.Graphics.Shader/IntermediateRepresentation/Operation.cs @@ -216,6 +216,11 @@ namespace Ryujinx.Graphics.Shader.IntermediateRepresentation newSources[index] = source; + if (source != null && source.Type == OperandType.LocalVariable) + { + source.UseOps.Add(this); + } + _sources = newSources; } diff --git a/src/Ryujinx.Graphics.Shader/IntermediateRepresentation/TextureOperation.cs b/src/Ryujinx.Graphics.Shader/IntermediateRepresentation/TextureOperation.cs index 1b82e2945..7eee8f2e9 100644 --- a/src/Ryujinx.Graphics.Shader/IntermediateRepresentation/TextureOperation.cs +++ b/src/Ryujinx.Graphics.Shader/IntermediateRepresentation/TextureOperation.cs @@ -8,13 +8,17 @@ namespace Ryujinx.Graphics.Shader.IntermediateRepresentation public TextureFormat Format { get; set; } public TextureFlags Flags { get; private set; } + public int Set { get; private set; } public int Binding { get; private set; } + public int SamplerSet { get; private set; } + public int SamplerBinding { get; private set; } public TextureOperation( Instruction inst, SamplerType type, TextureFormat format, TextureFlags flags, + int set, int binding, int compIndex, Operand[] dests, @@ -23,16 +27,28 @@ namespace Ryujinx.Graphics.Shader.IntermediateRepresentation Type = type; Format = format; Flags = flags; + Set = set; Binding = binding; + SamplerSet = -1; + SamplerBinding = -1; } - public void TurnIntoArray(int binding) + public void TurnIntoArray(SetBindingPair setAndBinding) { Flags &= ~TextureFlags.Bindless; - Binding = binding; + Set = setAndBinding.SetIndex; + Binding = setAndBinding.Binding; } - public void SetBinding(int binding) + public void TurnIntoArray(SetBindingPair textureSetAndBinding, SetBindingPair samplerSetAndBinding) + { + TurnIntoArray(textureSetAndBinding); + + SamplerSet = samplerSetAndBinding.SetIndex; + SamplerBinding = samplerSetAndBinding.Binding; + } + + public void SetBinding(SetBindingPair setAndBinding) { if ((Flags & TextureFlags.Bindless) != 0) { @@ -41,7 +57,8 @@ namespace Ryujinx.Graphics.Shader.IntermediateRepresentation RemoveSource(0); } - Binding = binding; + Set = setAndBinding.SetIndex; + Binding = setAndBinding.Binding; } public void SetLodLevelFlag() diff --git a/src/Ryujinx.Graphics.Shader/SamplerType.cs b/src/Ryujinx.Graphics.Shader/SamplerType.cs index 66c748bf3..a693495fa 100644 --- a/src/Ryujinx.Graphics.Shader/SamplerType.cs +++ b/src/Ryujinx.Graphics.Shader/SamplerType.cs @@ -69,6 +69,7 @@ namespace Ryujinx.Graphics.Shader { string typeName = (type & SamplerType.Mask) switch { + SamplerType.None => "sampler", SamplerType.Texture1D => "sampler1D", SamplerType.TextureBuffer => "samplerBuffer", SamplerType.Texture2D => "sampler2D", @@ -95,6 +96,31 @@ namespace Ryujinx.Graphics.Shader return typeName; } + public static string ToGlslTextureType(this SamplerType type) + { + string typeName = (type & SamplerType.Mask) switch + { + SamplerType.Texture1D => "texture1D", + SamplerType.TextureBuffer => "textureBuffer", + SamplerType.Texture2D => "texture2D", + SamplerType.Texture3D => "texture3D", + SamplerType.TextureCube => "textureCube", + _ => throw new ArgumentException($"Invalid texture type \"{type}\"."), + }; + + if ((type & SamplerType.Multisample) != 0) + { + typeName += "MS"; + } + + if ((type & SamplerType.Array) != 0) + { + typeName += "Array"; + } + + return typeName; + } + public static string ToGlslImageType(this SamplerType type, AggregateType componentType) { string typeName = (type & SamplerType.Mask) switch diff --git a/src/Ryujinx.Graphics.Shader/SetBindingPair.cs b/src/Ryujinx.Graphics.Shader/SetBindingPair.cs new file mode 100644 index 000000000..1e8a4f9c6 --- /dev/null +++ b/src/Ryujinx.Graphics.Shader/SetBindingPair.cs @@ -0,0 +1,4 @@ +namespace Ryujinx.Graphics.Shader +{ + public readonly record struct SetBindingPair(int SetIndex, int Binding); +} diff --git a/src/Ryujinx.Graphics.Shader/StructuredIr/AstTextureOperation.cs b/src/Ryujinx.Graphics.Shader/StructuredIr/AstTextureOperation.cs index 3970df1e9..867cae853 100644 --- a/src/Ryujinx.Graphics.Shader/StructuredIr/AstTextureOperation.cs +++ b/src/Ryujinx.Graphics.Shader/StructuredIr/AstTextureOperation.cs @@ -8,21 +8,42 @@ namespace Ryujinx.Graphics.Shader.StructuredIr public TextureFormat Format { get; } public TextureFlags Flags { get; } + public int Set { get; } public int Binding { get; } + public int SamplerSet { get; } + public int SamplerBinding { get; } + + public bool IsSeparate => SamplerBinding >= 0; public AstTextureOperation( Instruction inst, SamplerType type, TextureFormat format, TextureFlags flags, + int set, int binding, + int samplerSet, + int samplerBinding, int index, params IAstNode[] sources) : base(inst, StorageKind.None, false, index, sources, sources.Length) { Type = type; Format = format; Flags = flags; + Set = set; Binding = binding; + SamplerSet = samplerSet; + SamplerBinding = samplerBinding; + } + + public SetBindingPair GetTextureSetAndBinding() + { + return new SetBindingPair(Set, Binding); + } + + public SetBindingPair GetSamplerSetAndBinding() + { + return new SetBindingPair(SamplerSet, SamplerBinding); } } } diff --git a/src/Ryujinx.Graphics.Shader/StructuredIr/ShaderProperties.cs b/src/Ryujinx.Graphics.Shader/StructuredIr/ShaderProperties.cs index 8c12c2aaf..53ed6bfcc 100644 --- a/src/Ryujinx.Graphics.Shader/StructuredIr/ShaderProperties.cs +++ b/src/Ryujinx.Graphics.Shader/StructuredIr/ShaderProperties.cs @@ -6,15 +6,15 @@ namespace Ryujinx.Graphics.Shader.StructuredIr { private readonly Dictionary _constantBuffers; private readonly Dictionary _storageBuffers; - private readonly Dictionary _textures; - private readonly Dictionary _images; + private readonly Dictionary _textures; + private readonly Dictionary _images; private readonly Dictionary _localMemories; private readonly Dictionary _sharedMemories; public IReadOnlyDictionary ConstantBuffers => _constantBuffers; public IReadOnlyDictionary StorageBuffers => _storageBuffers; - public IReadOnlyDictionary Textures => _textures; - public IReadOnlyDictionary Images => _images; + public IReadOnlyDictionary Textures => _textures; + public IReadOnlyDictionary Images => _images; public IReadOnlyDictionary LocalMemories => _localMemories; public IReadOnlyDictionary SharedMemories => _sharedMemories; @@ -22,8 +22,8 @@ namespace Ryujinx.Graphics.Shader.StructuredIr { _constantBuffers = new Dictionary(); _storageBuffers = new Dictionary(); - _textures = new Dictionary(); - _images = new Dictionary(); + _textures = new Dictionary(); + _images = new Dictionary(); _localMemories = new Dictionary(); _sharedMemories = new Dictionary(); } @@ -40,12 +40,12 @@ namespace Ryujinx.Graphics.Shader.StructuredIr public void AddOrUpdateTexture(TextureDefinition definition) { - _textures[definition.Binding] = definition; + _textures[new(definition.Set, definition.Binding)] = definition; } public void AddOrUpdateImage(TextureDefinition definition) { - _images[definition.Binding] = definition; + _images[new(definition.Set, definition.Binding)] = definition; } public int AddLocalMemory(MemoryDefinition definition) diff --git a/src/Ryujinx.Graphics.Shader/StructuredIr/StructuredProgram.cs b/src/Ryujinx.Graphics.Shader/StructuredIr/StructuredProgram.cs index 2e2df7546..88053658d 100644 --- a/src/Ryujinx.Graphics.Shader/StructuredIr/StructuredProgram.cs +++ b/src/Ryujinx.Graphics.Shader/StructuredIr/StructuredProgram.cs @@ -169,7 +169,17 @@ namespace Ryujinx.Graphics.Shader.StructuredIr AstTextureOperation GetAstTextureOperation(TextureOperation texOp) { - return new AstTextureOperation(inst, texOp.Type, texOp.Format, texOp.Flags, texOp.Binding, texOp.Index, sources); + return new AstTextureOperation( + inst, + texOp.Type, + texOp.Format, + texOp.Flags, + texOp.Set, + texOp.Binding, + texOp.SamplerSet, + texOp.SamplerBinding, + texOp.Index, + sources); } int componentsCount = BitOperations.PopCount((uint)operation.Index); diff --git a/src/Ryujinx.Graphics.Shader/StructuredIr/TextureDefinition.cs b/src/Ryujinx.Graphics.Shader/StructuredIr/TextureDefinition.cs index bdd3a2ed1..1021dff0e 100644 --- a/src/Ryujinx.Graphics.Shader/StructuredIr/TextureDefinition.cs +++ b/src/Ryujinx.Graphics.Shader/StructuredIr/TextureDefinition.cs @@ -5,25 +5,39 @@ namespace Ryujinx.Graphics.Shader public int Set { get; } public int Binding { get; } public int ArrayLength { get; } + public bool Separate { get; } public string Name { get; } public SamplerType Type { get; } public TextureFormat Format { get; } public TextureUsageFlags Flags { get; } - public TextureDefinition(int set, int binding, int arrayLength, string name, SamplerType type, TextureFormat format, TextureUsageFlags flags) + public TextureDefinition( + int set, + int binding, + int arrayLength, + bool separate, + string name, + SamplerType type, + TextureFormat format, + TextureUsageFlags flags) { Set = set; Binding = binding; ArrayLength = arrayLength; + Separate = separate; Name = name; Type = type; Format = format; Flags = flags; } + public TextureDefinition(int set, int binding, string name, SamplerType type) : this(set, binding, 1, false, name, type, TextureFormat.Unknown, TextureUsageFlags.None) + { + } + public TextureDefinition SetFlag(TextureUsageFlags flag) { - return new TextureDefinition(Set, Binding, ArrayLength, Name, Type, Format, Flags | flag); + return new TextureDefinition(Set, Binding, ArrayLength, Separate, Name, Type, Format, Flags | flag); } } } diff --git a/src/Ryujinx.Graphics.Shader/TextureDescriptor.cs b/src/Ryujinx.Graphics.Shader/TextureDescriptor.cs index 38834da72..1e387407d 100644 --- a/src/Ryujinx.Graphics.Shader/TextureDescriptor.cs +++ b/src/Ryujinx.Graphics.Shader/TextureDescriptor.cs @@ -4,6 +4,7 @@ namespace Ryujinx.Graphics.Shader { // New fields should be added to the end of the struct to keep disk shader cache compatibility. + public readonly int Set; public readonly int Binding; public readonly SamplerType Type; @@ -13,23 +14,29 @@ namespace Ryujinx.Graphics.Shader public readonly int HandleIndex; public readonly int ArrayLength; + public readonly bool Separate; + public readonly TextureUsageFlags Flags; public TextureDescriptor( + int set, int binding, SamplerType type, TextureFormat format, int cbufSlot, int handleIndex, int arrayLength, + bool separate, TextureUsageFlags flags) { + Set = set; Binding = binding; Type = type; Format = format; CbufSlot = cbufSlot; HandleIndex = handleIndex; ArrayLength = arrayLength; + Separate = separate; Flags = flags; } } diff --git a/src/Ryujinx.Graphics.Shader/TextureHandle.cs b/src/Ryujinx.Graphics.Shader/TextureHandle.cs index 7df9c8e47..3aaceac48 100644 --- a/src/Ryujinx.Graphics.Shader/TextureHandle.cs +++ b/src/Ryujinx.Graphics.Shader/TextureHandle.cs @@ -9,6 +9,7 @@ namespace Ryujinx.Graphics.Shader SeparateSamplerHandle = 1, SeparateSamplerId = 2, SeparateConstantSamplerHandle = 3, + Direct = 4, } public static class TextureHandle diff --git a/src/Ryujinx.Graphics.Shader/Translation/EmitterContext.cs b/src/Ryujinx.Graphics.Shader/Translation/EmitterContext.cs index e1157eea4..5e07b39f1 100644 --- a/src/Ryujinx.Graphics.Shader/Translation/EmitterContext.cs +++ b/src/Ryujinx.Graphics.Shader/Translation/EmitterContext.cs @@ -124,7 +124,7 @@ namespace Ryujinx.Graphics.Shader.Translation this.TextureSample( SamplerType.TextureBuffer, TextureFlags.IntCoords, - ResourceManager.Reservations.IndexBufferTextureBinding, + ResourceManager.Reservations.GetIndexBufferTextureSetAndBinding(), 1, new[] { vertexIndexVr }, new[] { this.IAdd(ibBaseOffset, outputVertexOffset) }); @@ -145,7 +145,7 @@ namespace Ryujinx.Graphics.Shader.Translation this.TextureSample( SamplerType.TextureBuffer, TextureFlags.IntCoords, - ResourceManager.Reservations.TopologyRemapBufferTextureBinding, + ResourceManager.Reservations.GetTopologyRemapBufferTextureSetAndBinding(), 1, new[] { vertexIndex }, new[] { this.IAdd(baseVertex, Const(index)) }); diff --git a/src/Ryujinx.Graphics.Shader/Translation/EmitterContextInsts.cs b/src/Ryujinx.Graphics.Shader/Translation/EmitterContextInsts.cs index 9e314c620..5bdbb0025 100644 --- a/src/Ryujinx.Graphics.Shader/Translation/EmitterContextInsts.cs +++ b/src/Ryujinx.Graphics.Shader/Translation/EmitterContextInsts.cs @@ -618,12 +618,21 @@ namespace Ryujinx.Graphics.Shader.Translation SamplerType type, TextureFormat format, TextureFlags flags, - int binding, + SetBindingPair setAndBinding, Operand[] sources) { Operand dest = Local(); - context.Add(new TextureOperation(Instruction.ImageAtomic, type, format, flags, binding, 0, new[] { dest }, sources)); + context.Add(new TextureOperation( + Instruction.ImageAtomic, + type, + format, + flags, + setAndBinding.SetIndex, + setAndBinding.Binding, + 0, + new[] { dest }, + sources)); return dest; } @@ -633,12 +642,21 @@ namespace Ryujinx.Graphics.Shader.Translation SamplerType type, TextureFormat format, TextureFlags flags, - int binding, + SetBindingPair setAndBinding, int compMask, Operand[] dests, Operand[] sources) { - context.Add(new TextureOperation(Instruction.ImageLoad, type, format, flags, binding, compMask, dests, sources)); + context.Add(new TextureOperation( + Instruction.ImageLoad, + type, + format, + flags, + setAndBinding.SetIndex, + setAndBinding.Binding, + compMask, + dests, + sources)); } public static void ImageStore( @@ -646,10 +664,19 @@ namespace Ryujinx.Graphics.Shader.Translation SamplerType type, TextureFormat format, TextureFlags flags, - int binding, + SetBindingPair setAndBinding, Operand[] sources) { - context.Add(new TextureOperation(Instruction.ImageStore, type, format, flags, binding, 0, null, sources)); + context.Add(new TextureOperation( + Instruction.ImageStore, + type, + format, + flags, + setAndBinding.SetIndex, + setAndBinding.Binding, + 0, + null, + sources)); } public static Operand IsNan(this EmitterContext context, Operand a, Instruction fpType = Instruction.FP32) @@ -718,13 +745,22 @@ namespace Ryujinx.Graphics.Shader.Translation this EmitterContext context, SamplerType type, TextureFlags flags, - int binding, + SetBindingPair setAndBinding, int compIndex, Operand[] sources) { Operand dest = Local(); - context.Add(new TextureOperation(Instruction.Lod, type, TextureFormat.Unknown, flags, binding, compIndex, new[] { dest }, sources)); + context.Add(new TextureOperation( + Instruction.Lod, + type, + TextureFormat.Unknown, + flags, + setAndBinding.SetIndex, + setAndBinding.Binding, + compIndex, + new[] { dest }, + sources)); return dest; } @@ -889,24 +925,42 @@ namespace Ryujinx.Graphics.Shader.Translation this EmitterContext context, SamplerType type, TextureFlags flags, - int binding, + SetBindingPair setAndBinding, int compMask, Operand[] dests, Operand[] sources) { - context.Add(new TextureOperation(Instruction.TextureSample, type, TextureFormat.Unknown, flags, binding, compMask, dests, sources)); + context.Add(new TextureOperation( + Instruction.TextureSample, + type, + TextureFormat.Unknown, + flags, + setAndBinding.SetIndex, + setAndBinding.Binding, + compMask, + dests, + sources)); } public static Operand TextureQuerySamples( this EmitterContext context, SamplerType type, TextureFlags flags, - int binding, + SetBindingPair setAndBinding, Operand[] sources) { Operand dest = Local(); - context.Add(new TextureOperation(Instruction.TextureQuerySamples, type, TextureFormat.Unknown, flags, binding, 0, new[] { dest }, sources)); + context.Add(new TextureOperation( + Instruction.TextureQuerySamples, + type, + TextureFormat.Unknown, + flags, + setAndBinding.SetIndex, + setAndBinding.Binding, + 0, + new[] { dest }, + sources)); return dest; } @@ -915,13 +969,22 @@ namespace Ryujinx.Graphics.Shader.Translation this EmitterContext context, SamplerType type, TextureFlags flags, - int binding, + SetBindingPair setAndBinding, int compIndex, Operand[] sources) { Operand dest = Local(); - context.Add(new TextureOperation(Instruction.TextureQuerySize, type, TextureFormat.Unknown, flags, binding, compIndex, new[] { dest }, sources)); + context.Add(new TextureOperation( + Instruction.TextureQuerySize, + type, + TextureFormat.Unknown, + flags, + setAndBinding.SetIndex, + setAndBinding.Binding, + compIndex, + new[] { dest }, + sources)); return dest; } diff --git a/src/Ryujinx.Graphics.Shader/Translation/Optimizations/BindlessElimination.cs b/src/Ryujinx.Graphics.Shader/Translation/Optimizations/BindlessElimination.cs index ad955278f..02a83fbe4 100644 --- a/src/Ryujinx.Graphics.Shader/Translation/Optimizations/BindlessElimination.cs +++ b/src/Ryujinx.Graphics.Shader/Translation/Optimizations/BindlessElimination.cs @@ -1,6 +1,7 @@ using Ryujinx.Graphics.Shader.Instructions; using Ryujinx.Graphics.Shader.IntermediateRepresentation; using Ryujinx.Graphics.Shader.StructuredIr; +using System; using System.Collections.Generic; namespace Ryujinx.Graphics.Shader.Translation.Optimizations @@ -31,11 +32,18 @@ namespace Ryujinx.Graphics.Shader.Translation.Optimizations continue; } - if (!TryConvertBindless(block, resourceManager, gpuAccessor, texOp)) + if (!TryConvertBindless(block, resourceManager, gpuAccessor, texOp) && + !GenerateBindlessAccess(block, resourceManager, gpuAccessor, texOp, node)) { // If we can't do bindless elimination, remove the texture operation. // Set any destination variables to zero. + string typeName = texOp.Inst.IsImage() + ? texOp.Type.ToGlslImageType(texOp.Format.GetComponentType()) + : texOp.Type.ToGlslTextureType(); + + gpuAccessor.Log($"Failed to find handle source for bindless access of type \"{typeName}\"."); + for (int destIndex = 0; destIndex < texOp.DestsCount; destIndex++) { block.Operations.AddBefore(node, new Operation(Instruction.Copy, texOp.GetDest(destIndex), OperandHelper.Const(0))); @@ -46,6 +54,117 @@ namespace Ryujinx.Graphics.Shader.Translation.Optimizations } } + private static bool GenerateBindlessAccess( + BasicBlock block, + ResourceManager resourceManager, + IGpuAccessor gpuAccessor, + TextureOperation texOp, + LinkedListNode node) + { + if (!gpuAccessor.QueryHostSupportsSeparateSampler()) + { + // We depend on combining samplers and textures in the shader being supported for this. + + return false; + } + + Operand bindlessHandle = texOp.GetSource(0); + + if (bindlessHandle.AsgOp is PhiNode phi) + { + for (int srcIndex = 0; srcIndex < phi.SourcesCount; srcIndex++) + { + Operand phiSource = phi.GetSource(srcIndex); + + if (phiSource.AsgOp is not PhiNode && !IsBindlessAccessAllowed(phiSource)) + { + return false; + } + } + } + else if (!IsBindlessAccessAllowed(bindlessHandle)) + { + return false; + } + + Operand textureHandle = OperandHelper.Local(); + Operand samplerHandle = OperandHelper.Local(); + Operand textureIndex = OperandHelper.Local(); + + block.Operations.AddBefore(node, new Operation(Instruction.BitwiseAnd, textureHandle, bindlessHandle, OperandHelper.Const(0xfffff))); + block.Operations.AddBefore(node, new Operation(Instruction.ShiftRightU32, samplerHandle, bindlessHandle, OperandHelper.Const(20))); + + int texturePoolLength = Math.Max(BindlessToArray.MinimumArrayLength, gpuAccessor.QueryTextureArrayLengthFromPool()); + + block.Operations.AddBefore(node, new Operation(Instruction.MinimumU32, textureIndex, textureHandle, OperandHelper.Const(texturePoolLength - 1))); + + texOp.SetSource(0, textureIndex); + + bool hasSampler = !texOp.Inst.IsImage(); + + SetBindingPair textureSetAndBinding = resourceManager.GetTextureOrImageBinding( + texOp.Inst, + texOp.Type, + texOp.Format, + texOp.Flags & ~TextureFlags.Bindless, + 0, + TextureHandle.PackOffsets(0, 0, TextureHandleType.Direct), + texturePoolLength, + hasSampler); + + if (hasSampler) + { + Operand samplerIndex = OperandHelper.Local(); + + int samplerPoolLength = Math.Max(BindlessToArray.MinimumArrayLength, gpuAccessor.QuerySamplerArrayLengthFromPool()); + + block.Operations.AddBefore(node, new Operation(Instruction.MinimumU32, samplerIndex, samplerHandle, OperandHelper.Const(samplerPoolLength - 1))); + + texOp.InsertSource(1, samplerIndex); + + SetBindingPair samplerSetAndBinding = resourceManager.GetTextureOrImageBinding( + texOp.Inst, + SamplerType.None, + texOp.Format, + TextureFlags.None, + 0, + TextureHandle.PackOffsets(0, 0, TextureHandleType.Direct), + samplerPoolLength); + + texOp.TurnIntoArray(textureSetAndBinding, samplerSetAndBinding); + } + else + { + texOp.TurnIntoArray(textureSetAndBinding); + } + + return true; + } + + private static bool IsBindlessAccessAllowed(Operand nvHandle) + { + if (nvHandle.Type == OperandType.ConstantBuffer) + { + // Bindless access with handles from constant buffer is allowed. + + return true; + } + + if (nvHandle.AsgOp is not Operation handleOp || + handleOp.Inst != Instruction.Load || + (handleOp.StorageKind != StorageKind.Input && handleOp.StorageKind != StorageKind.StorageBuffer)) + { + // Right now, we only allow bindless access when the handle comes from a shader input or storage buffer. + // This is an artificial limitation to prevent it from being used in cases where it + // would have a large performance impact of loading all textures in the pool. + // It might be removed in the future, if we can mitigate the performance impact. + + return false; + } + + return true; + } + private static bool TryConvertBindless(BasicBlock block, ResourceManager resourceManager, IGpuAccessor gpuAccessor, TextureOperation texOp) { if (texOp.Inst == Instruction.TextureSample || texOp.Inst.IsTextureQuery()) @@ -361,7 +480,7 @@ namespace Ryujinx.Graphics.Shader.Translation.Optimizations } } - int binding = resourceManager.GetTextureOrImageBinding( + SetBindingPair setAndBinding = resourceManager.GetTextureOrImageBinding( texOp.Inst, texOp.Type, texOp.Format, @@ -369,7 +488,7 @@ namespace Ryujinx.Graphics.Shader.Translation.Optimizations cbufSlot, cbufOffset); - texOp.SetBinding(binding); + texOp.SetBinding(setAndBinding); } } } diff --git a/src/Ryujinx.Graphics.Shader/Translation/Optimizations/BindlessToArray.cs b/src/Ryujinx.Graphics.Shader/Translation/Optimizations/BindlessToArray.cs index 7543d1c24..8eed139d6 100644 --- a/src/Ryujinx.Graphics.Shader/Translation/Optimizations/BindlessToArray.cs +++ b/src/Ryujinx.Graphics.Shader/Translation/Optimizations/BindlessToArray.cs @@ -11,7 +11,7 @@ namespace Ryujinx.Graphics.Shader.Translation.Optimizations private const int HardcodedArrayLengthOgl = 4; // 1 and 0 elements are not considered arrays anymore. - private const int MinimumArrayLength = 2; + public const int MinimumArrayLength = 2; public static void RunPassOgl(BasicBlock block, ResourceManager resourceManager) { @@ -221,7 +221,7 @@ namespace Ryujinx.Graphics.Shader.Translation.Optimizations private static void TurnIntoArray(ResourceManager resourceManager, TextureOperation texOp, int cbufSlot, int handleIndex, int length) { - int binding = resourceManager.GetTextureOrImageBinding( + SetBindingPair setAndBinding = resourceManager.GetTextureOrImageBinding( texOp.Inst, texOp.Type, texOp.Format, @@ -230,7 +230,7 @@ namespace Ryujinx.Graphics.Shader.Translation.Optimizations handleIndex, length); - texOp.TurnIntoArray(binding); + texOp.TurnIntoArray(setAndBinding); } } } diff --git a/src/Ryujinx.Graphics.Shader/Translation/ResourceManager.cs b/src/Ryujinx.Graphics.Shader/Translation/ResourceManager.cs index e9fe0b1ee..94691a5b4 100644 --- a/src/Ryujinx.Graphics.Shader/Translation/ResourceManager.cs +++ b/src/Ryujinx.Graphics.Shader/Translation/ResourceManager.cs @@ -20,8 +20,8 @@ namespace Ryujinx.Graphics.Shader.Translation private readonly ShaderStage _stage; private readonly string _stagePrefix; - private readonly int[] _cbSlotToBindingMap; - private readonly int[] _sbSlotToBindingMap; + private readonly SetBindingPair[] _cbSlotToBindingMap; + private readonly SetBindingPair[] _sbSlotToBindingMap; private uint _sbSlotWritten; private readonly Dictionary _sbSlots; @@ -29,10 +29,11 @@ namespace Ryujinx.Graphics.Shader.Translation private readonly HashSet _usedConstantBufferBindings; - private readonly record struct TextureInfo(int CbufSlot, int Handle, int ArrayLength, SamplerType Type, TextureFormat Format); + private readonly record struct TextureInfo(int CbufSlot, int Handle, int ArrayLength, bool Separate, SamplerType Type, TextureFormat Format); private struct TextureMeta { + public int Set; public int Binding; public bool AccurateType; public SamplerType Type; @@ -64,10 +65,10 @@ namespace Ryujinx.Graphics.Shader.Translation _stage = stage; _stagePrefix = GetShaderStagePrefix(stage); - _cbSlotToBindingMap = new int[18]; - _sbSlotToBindingMap = new int[16]; - _cbSlotToBindingMap.AsSpan().Fill(-1); - _sbSlotToBindingMap.AsSpan().Fill(-1); + _cbSlotToBindingMap = new SetBindingPair[18]; + _sbSlotToBindingMap = new SetBindingPair[16]; + _cbSlotToBindingMap.AsSpan().Fill(new(-1, -1)); + _sbSlotToBindingMap.AsSpan().Fill(new(-1, -1)); _sbSlots = new(); _sbSlotsReverse = new(); @@ -146,16 +147,16 @@ namespace Ryujinx.Graphics.Shader.Translation public int GetConstantBufferBinding(int slot) { - int binding = _cbSlotToBindingMap[slot]; - if (binding < 0) + SetBindingPair setAndBinding = _cbSlotToBindingMap[slot]; + if (setAndBinding.Binding < 0) { - binding = _gpuAccessor.CreateConstantBufferBinding(slot); - _cbSlotToBindingMap[slot] = binding; + setAndBinding = _gpuAccessor.CreateConstantBufferBinding(slot); + _cbSlotToBindingMap[slot] = setAndBinding; string slotNumber = slot.ToString(CultureInfo.InvariantCulture); - AddNewConstantBuffer(binding, $"{_stagePrefix}_c{slotNumber}"); + AddNewConstantBuffer(setAndBinding.SetIndex, setAndBinding.Binding, $"{_stagePrefix}_c{slotNumber}"); } - return binding; + return setAndBinding.Binding; } public bool TryGetStorageBufferBinding(int sbCbSlot, int sbCbOffset, bool write, out int binding) @@ -166,14 +167,14 @@ namespace Ryujinx.Graphics.Shader.Translation return false; } - binding = _sbSlotToBindingMap[slot]; + SetBindingPair setAndBinding = _sbSlotToBindingMap[slot]; - if (binding < 0) + if (setAndBinding.Binding < 0) { - binding = _gpuAccessor.CreateStorageBufferBinding(slot); - _sbSlotToBindingMap[slot] = binding; + setAndBinding = _gpuAccessor.CreateStorageBufferBinding(slot); + _sbSlotToBindingMap[slot] = setAndBinding; string slotNumber = slot.ToString(CultureInfo.InvariantCulture); - AddNewStorageBuffer(binding, $"{_stagePrefix}_s{slotNumber}"); + AddNewStorageBuffer(setAndBinding.SetIndex, setAndBinding.Binding, $"{_stagePrefix}_s{slotNumber}"); } if (write) @@ -181,6 +182,7 @@ namespace Ryujinx.Graphics.Shader.Translation _sbSlotWritten |= 1u << slot; } + binding = setAndBinding.Binding; return true; } @@ -208,7 +210,7 @@ namespace Ryujinx.Graphics.Shader.Translation { for (slot = 0; slot < _cbSlotToBindingMap.Length; slot++) { - if (_cbSlotToBindingMap[slot] == binding) + if (_cbSlotToBindingMap[slot].Binding == binding) { return true; } @@ -218,14 +220,15 @@ namespace Ryujinx.Graphics.Shader.Translation return false; } - public int GetTextureOrImageBinding( + public SetBindingPair GetTextureOrImageBinding( Instruction inst, SamplerType type, TextureFormat format, TextureFlags flags, int cbufSlot, int handle, - int arrayLength = 1) + int arrayLength = 1, + bool separate = false) { inst &= Instruction.Mask; bool isImage = inst.IsImage(); @@ -239,14 +242,25 @@ namespace Ryujinx.Graphics.Shader.Translation format = TextureFormat.Unknown; } - int binding = GetTextureOrImageBinding(cbufSlot, handle, arrayLength, type, format, isImage, intCoords, isWrite, accurateType, coherent); + SetBindingPair setAndBinding = GetTextureOrImageBinding( + cbufSlot, + handle, + arrayLength, + type, + format, + isImage, + intCoords, + isWrite, + accurateType, + coherent, + separate); _gpuAccessor.RegisterTexture(handle, cbufSlot); - return binding; + return setAndBinding; } - private int GetTextureOrImageBinding( + private SetBindingPair GetTextureOrImageBinding( int cbufSlot, int handle, int arrayLength, @@ -256,9 +270,10 @@ namespace Ryujinx.Graphics.Shader.Translation bool intCoords, bool write, bool accurateType, - bool coherent) + bool coherent, + bool separate) { - var dimensions = type.GetDimensions(); + var dimensions = type == SamplerType.None ? 0 : type.GetDimensions(); var dict = isImage ? _usedImages : _usedTextures; var usageFlags = TextureUsageFlags.None; @@ -290,7 +305,7 @@ namespace Ryujinx.Graphics.Shader.Translation // For array textures, we also want to use type as key, // since we may have texture handles stores in the same buffer, but for textures with different types. var keyType = arrayLength > 1 ? type : SamplerType.None; - var info = new TextureInfo(cbufSlot, handle, arrayLength, keyType, format); + var info = new TextureInfo(cbufSlot, handle, arrayLength, separate, keyType, format); var meta = new TextureMeta() { AccurateType = accurateType, @@ -298,21 +313,38 @@ namespace Ryujinx.Graphics.Shader.Translation UsageFlags = usageFlags, }; + int setIndex; int binding; if (dict.TryGetValue(info, out var existingMeta)) { dict[info] = MergeTextureMeta(meta, existingMeta); + setIndex = existingMeta.Set; binding = existingMeta.Binding; } else { - bool isBuffer = (type & SamplerType.Mask) == SamplerType.TextureBuffer; + if (arrayLength > 1 && (setIndex = _gpuAccessor.CreateExtraSet()) >= 0) + { + // We reserved an "extra set" for the array. + // In this case the binding is always the first one (0). + // Using separate sets for array is better as we need to do less descriptor set updates. - binding = isImage - ? _gpuAccessor.CreateImageBinding(arrayLength, isBuffer) - : _gpuAccessor.CreateTextureBinding(arrayLength, isBuffer); + binding = 0; + } + else + { + bool isBuffer = (type & SamplerType.Mask) == SamplerType.TextureBuffer; + SetBindingPair setAndBinding = isImage + ? _gpuAccessor.CreateImageBinding(arrayLength, isBuffer) + : _gpuAccessor.CreateTextureBinding(arrayLength, isBuffer); + + setIndex = setAndBinding.SetIndex; + binding = setAndBinding.Binding; + } + + meta.Set = setIndex; meta.Binding = binding; dict.Add(info, meta); @@ -332,15 +364,20 @@ namespace Ryujinx.Graphics.Shader.Translation ? $"{prefix}_tcb_{handle:X}_{format.ToGlslFormat()}" : $"{prefix}_cb{cbufSlot}_{handle:X}_{format.ToGlslFormat()}"; } + else if (type == SamplerType.None) + { + nameSuffix = cbufSlot < 0 ? $"s_tcb_{handle:X}" : $"s_cb{cbufSlot}_{handle:X}"; + } else { nameSuffix = cbufSlot < 0 ? $"{prefix}_tcb_{handle:X}" : $"{prefix}_cb{cbufSlot}_{handle:X}"; } var definition = new TextureDefinition( - isImage ? 3 : 2, + setIndex, binding, arrayLength, + separate, $"{_stagePrefix}_{nameSuffix}", meta.Type, info.Format, @@ -355,11 +392,12 @@ namespace Ryujinx.Graphics.Shader.Translation Properties.AddOrUpdateTexture(definition); } - return binding; + return new SetBindingPair(setIndex, binding); } private static TextureMeta MergeTextureMeta(TextureMeta meta, TextureMeta existingMeta) { + meta.Set = existingMeta.Set; meta.Binding = existingMeta.Binding; meta.UsageFlags |= existingMeta.UsageFlags; @@ -422,11 +460,11 @@ namespace Ryujinx.Graphics.Shader.Translation for (int slot = 0; slot < _cbSlotToBindingMap.Length; slot++) { - int binding = _cbSlotToBindingMap[slot]; + SetBindingPair setAndBinding = _cbSlotToBindingMap[slot]; - if (binding >= 0 && _usedConstantBufferBindings.Contains(binding)) + if (setAndBinding.Binding >= 0 && _usedConstantBufferBindings.Contains(setAndBinding.Binding)) { - descriptors[descriptorIndex++] = new BufferDescriptor(binding, slot); + descriptors[descriptorIndex++] = new BufferDescriptor(setAndBinding.SetIndex, setAndBinding.Binding, slot); } } @@ -446,13 +484,13 @@ namespace Ryujinx.Graphics.Shader.Translation foreach ((int key, int slot) in _sbSlots) { - int binding = _sbSlotToBindingMap[slot]; + SetBindingPair setAndBinding = _sbSlotToBindingMap[slot]; - if (binding >= 0) + if (setAndBinding.Binding >= 0) { (int sbCbSlot, int sbCbOffset) = UnpackSbCbInfo(key); BufferUsageFlags flags = (_sbSlotWritten & (1u << slot)) != 0 ? BufferUsageFlags.Write : BufferUsageFlags.None; - descriptors[descriptorIndex++] = new BufferDescriptor(binding, slot, sbCbSlot, sbCbOffset, flags); + descriptors[descriptorIndex++] = new BufferDescriptor(setAndBinding.SetIndex, setAndBinding.Binding, slot, sbCbSlot, sbCbOffset, flags); } } @@ -489,12 +527,14 @@ namespace Ryujinx.Graphics.Shader.Translation } descriptors.Add(new TextureDescriptor( + meta.Set, meta.Binding, meta.Type, info.Format, info.CbufSlot, info.Handle, info.ArrayLength, + info.Separate, meta.UsageFlags)); } @@ -508,12 +548,14 @@ namespace Ryujinx.Graphics.Shader.Translation } descriptors.Add(new TextureDescriptor( + meta.Set, meta.Binding, meta.Type, info.Format, info.CbufSlot, info.Handle, info.ArrayLength, + info.Separate, meta.UsageFlags)); } } @@ -567,24 +609,24 @@ namespace Ryujinx.Graphics.Shader.Translation return false; } - private void AddNewConstantBuffer(int binding, string name) + private void AddNewConstantBuffer(int setIndex, int binding, string name) { StructureType type = new(new[] { new StructureField(AggregateType.Array | AggregateType.Vector4 | AggregateType.FP32, "data", Constants.ConstantBufferSize / 16), }); - Properties.AddOrUpdateConstantBuffer(new(BufferLayout.Std140, 0, binding, name, type)); + Properties.AddOrUpdateConstantBuffer(new(BufferLayout.Std140, setIndex, binding, name, type)); } - private void AddNewStorageBuffer(int binding, string name) + private void AddNewStorageBuffer(int setIndex, int binding, string name) { StructureType type = new(new[] { new StructureField(AggregateType.Array | AggregateType.U32, "data", 0), }); - Properties.AddOrUpdateStorageBuffer(new(BufferLayout.Std430, 1, binding, name, type)); + Properties.AddOrUpdateStorageBuffer(new(BufferLayout.Std430, setIndex, binding, name, type)); } public static string GetShaderStagePrefix(ShaderStage stage) diff --git a/src/Ryujinx.Graphics.Shader/Translation/ResourceReservations.cs b/src/Ryujinx.Graphics.Shader/Translation/ResourceReservations.cs index d559f6699..c89c4d0b6 100644 --- a/src/Ryujinx.Graphics.Shader/Translation/ResourceReservations.cs +++ b/src/Ryujinx.Graphics.Shader/Translation/ResourceReservations.cs @@ -11,6 +11,8 @@ namespace Ryujinx.Graphics.Shader.Translation public const int MaxVertexBufferTextures = 32; + private const int TextureSetIndex = 2; // TODO: Get from GPU accessor. + public int VertexInfoConstantBufferBinding { get; } public int VertexOutputStorageBufferBinding { get; } public int GeometryVertexOutputStorageBufferBinding { get; } @@ -163,6 +165,21 @@ namespace Ryujinx.Graphics.Shader.Translation return _vertexBufferTextureBaseBinding + vaLocation; } + public SetBindingPair GetVertexBufferTextureSetAndBinding(int vaLocation) + { + return new SetBindingPair(TextureSetIndex, GetVertexBufferTextureBinding(vaLocation)); + } + + public SetBindingPair GetIndexBufferTextureSetAndBinding() + { + return new SetBindingPair(TextureSetIndex, IndexBufferTextureBinding); + } + + public SetBindingPair GetTopologyRemapBufferTextureSetAndBinding() + { + return new SetBindingPair(TextureSetIndex, TopologyRemapBufferTextureBinding); + } + internal bool TryGetOffset(StorageKind storageKind, int location, int component, out int offset) { return _offsets.TryGetValue(new IoDefinition(storageKind, IoVariable.UserDefined, location, component), out offset); diff --git a/src/Ryujinx.Graphics.Shader/Translation/ShaderDefinitions.cs b/src/Ryujinx.Graphics.Shader/Translation/ShaderDefinitions.cs index 3246e2594..f831ec940 100644 --- a/src/Ryujinx.Graphics.Shader/Translation/ShaderDefinitions.cs +++ b/src/Ryujinx.Graphics.Shader/Translation/ShaderDefinitions.cs @@ -45,6 +45,8 @@ namespace Ryujinx.Graphics.Shader.Translation public bool YNegateEnabled => _graphicsState.YNegateEnabled; public bool OriginUpperLeft => _graphicsState.OriginUpperLeft; + public bool HalvePrimitiveId => _graphicsState.HalvePrimitiveId; + public ImapPixelType[] ImapTypes { get; } public bool IaIndexing { get; private set; } public bool OaIndexing { get; private set; } diff --git a/src/Ryujinx.Graphics.Shader/Translation/Transforms/TexturePass.cs b/src/Ryujinx.Graphics.Shader/Translation/Transforms/TexturePass.cs index 072b45695..6ba8cb44a 100644 --- a/src/Ryujinx.Graphics.Shader/Translation/Transforms/TexturePass.cs +++ b/src/Ryujinx.Graphics.Shader/Translation/Transforms/TexturePass.cs @@ -182,6 +182,7 @@ namespace Ryujinx.Graphics.Shader.Translation.Transforms texOp.Type, texOp.Format, texOp.Flags, + texOp.Set, texOp.Binding, index, new[] { coordSize }, @@ -251,6 +252,7 @@ namespace Ryujinx.Graphics.Shader.Translation.Transforms texOp.Type, texOp.Format, texOp.Flags, + texOp.Set, texOp.Binding, index, new[] { coordSize }, @@ -471,6 +473,7 @@ namespace Ryujinx.Graphics.Shader.Translation.Transforms texOp.Type, texOp.Format, texOp.Flags & ~(TextureFlags.Offset | TextureFlags.Offsets), + texOp.Set, texOp.Binding, 1 << 3, // W component: i=0, j=0 new[] { dests[destIndex++] }, @@ -527,6 +530,7 @@ namespace Ryujinx.Graphics.Shader.Translation.Transforms texOp.Type, texOp.Format, texOp.Flags & ~(TextureFlags.Offset | TextureFlags.Offsets), + texOp.Set, texOp.Binding, componentIndex, dests, @@ -573,6 +577,7 @@ namespace Ryujinx.Graphics.Shader.Translation.Transforms texOp.Type, texOp.Format, texOp.Flags, + texOp.Set, texOp.Binding, index, new[] { texSizes[index] }, @@ -603,6 +608,7 @@ namespace Ryujinx.Graphics.Shader.Translation.Transforms texOp.Type, texOp.Format, texOp.Flags, + texOp.Set, texOp.Binding, 0, new[] { lod }, @@ -633,6 +639,7 @@ namespace Ryujinx.Graphics.Shader.Translation.Transforms texOp.Type, texOp.Format, texOp.Flags, + texOp.Set, texOp.Binding, index, new[] { texSizes[index] }, diff --git a/src/Ryujinx.Graphics.Shader/Translation/Transforms/VertexToCompute.cs b/src/Ryujinx.Graphics.Shader/Translation/Transforms/VertexToCompute.cs index d71ada865..ddd2134d2 100644 --- a/src/Ryujinx.Graphics.Shader/Translation/Transforms/VertexToCompute.cs +++ b/src/Ryujinx.Graphics.Shader/Translation/Transforms/VertexToCompute.cs @@ -54,6 +54,7 @@ namespace Ryujinx.Graphics.Shader.Translation.Transforms { bool needsSextNorm = context.Definitions.IsAttributePackedRgb10A2Signed(location); + SetBindingPair setAndBinding = context.ResourceManager.Reservations.GetVertexBufferTextureSetAndBinding(location); Operand temp = needsSextNorm ? Local() : dest; Operand vertexElemOffset = GenerateVertexOffset(context.ResourceManager, node, location, 0); @@ -62,7 +63,8 @@ namespace Ryujinx.Graphics.Shader.Translation.Transforms SamplerType.TextureBuffer, TextureFormat.Unknown, TextureFlags.IntCoords, - context.ResourceManager.Reservations.GetVertexBufferTextureBinding(location), + setAndBinding.SetIndex, + setAndBinding.Binding, 1 << component, new[] { temp }, new[] { vertexElemOffset })); @@ -75,6 +77,7 @@ namespace Ryujinx.Graphics.Shader.Translation.Transforms } else { + SetBindingPair setAndBinding = context.ResourceManager.Reservations.GetVertexBufferTextureSetAndBinding(location); Operand temp = component > 0 ? Local() : dest; Operand vertexElemOffset = GenerateVertexOffset(context.ResourceManager, node, location, component); @@ -83,7 +86,8 @@ namespace Ryujinx.Graphics.Shader.Translation.Transforms SamplerType.TextureBuffer, TextureFormat.Unknown, TextureFlags.IntCoords, - context.ResourceManager.Reservations.GetVertexBufferTextureBinding(location), + setAndBinding.SetIndex, + setAndBinding.Binding, 1, new[] { temp }, new[] { vertexElemOffset })); diff --git a/src/Ryujinx.Graphics.Shader/Translation/TranslatorContext.cs b/src/Ryujinx.Graphics.Shader/Translation/TranslatorContext.cs index 581f4372c..59914736e 100644 --- a/src/Ryujinx.Graphics.Shader/Translation/TranslatorContext.cs +++ b/src/Ryujinx.Graphics.Shader/Translation/TranslatorContext.cs @@ -412,8 +412,8 @@ namespace Ryujinx.Graphics.Shader.Translation if (Stage == ShaderStage.Vertex) { - int ibBinding = resourceManager.Reservations.IndexBufferTextureBinding; - TextureDefinition indexBuffer = new(2, ibBinding, 1, "ib_data", SamplerType.TextureBuffer, TextureFormat.Unknown, TextureUsageFlags.None); + SetBindingPair ibSetAndBinding = resourceManager.Reservations.GetIndexBufferTextureSetAndBinding(); + TextureDefinition indexBuffer = new(ibSetAndBinding.SetIndex, ibSetAndBinding.Binding, "ib_data", SamplerType.TextureBuffer); resourceManager.Properties.AddOrUpdateTexture(indexBuffer); int inputMap = _program.AttributeUsage.UsedInputAttributes; @@ -421,8 +421,8 @@ namespace Ryujinx.Graphics.Shader.Translation while (inputMap != 0) { int location = BitOperations.TrailingZeroCount(inputMap); - int binding = resourceManager.Reservations.GetVertexBufferTextureBinding(location); - TextureDefinition vaBuffer = new(2, binding, 1, $"vb_data{location}", SamplerType.TextureBuffer, TextureFormat.Unknown, TextureUsageFlags.None); + SetBindingPair setAndBinding = resourceManager.Reservations.GetVertexBufferTextureSetAndBinding(location); + TextureDefinition vaBuffer = new(setAndBinding.SetIndex, setAndBinding.Binding, $"vb_data{location}", SamplerType.TextureBuffer); resourceManager.Properties.AddOrUpdateTexture(vaBuffer); inputMap &= ~(1 << location); @@ -430,8 +430,8 @@ namespace Ryujinx.Graphics.Shader.Translation } else if (Stage == ShaderStage.Geometry) { - int trbBinding = resourceManager.Reservations.TopologyRemapBufferTextureBinding; - TextureDefinition remapBuffer = new(2, trbBinding, 1, "trb_data", SamplerType.TextureBuffer, TextureFormat.Unknown, TextureUsageFlags.None); + SetBindingPair trbSetAndBinding = resourceManager.Reservations.GetTopologyRemapBufferTextureSetAndBinding(); + TextureDefinition remapBuffer = new(trbSetAndBinding.SetIndex, trbSetAndBinding.Binding, "trb_data", SamplerType.TextureBuffer); resourceManager.Properties.AddOrUpdateTexture(remapBuffer); int geometryVbOutputSbBinding = resourceManager.Reservations.GeometryVertexOutputStorageBufferBinding; diff --git a/src/Ryujinx.Graphics.Vulkan/BufferHolder.cs b/src/Ryujinx.Graphics.Vulkan/BufferHolder.cs index 3673ee5a1..3dcbc3130 100644 --- a/src/Ryujinx.Graphics.Vulkan/BufferHolder.cs +++ b/src/Ryujinx.Graphics.Vulkan/BufferHolder.cs @@ -1,4 +1,3 @@ -using Ryujinx.Common.Logging; using Ryujinx.Graphics.GAL; using Silk.NET.Vulkan; using System; @@ -31,40 +30,29 @@ namespace Ryujinx.Graphics.Vulkan private readonly VulkanRenderer _gd; private readonly Device _device; - private MemoryAllocation _allocation; - private Auto _buffer; - private Auto _allocationAuto; + private readonly MemoryAllocation _allocation; + private readonly Auto _buffer; + private readonly Auto _allocationAuto; private readonly bool _allocationImported; - private ulong _bufferHandle; + private readonly ulong _bufferHandle; private CacheByRange _cachedConvertedBuffers; public int Size { get; } - private IntPtr _map; + private readonly IntPtr _map; - private MultiFenceHolder _waitable; + private readonly MultiFenceHolder _waitable; private bool _lastAccessIsWrite; - private BufferAllocationType _baseType; - private BufferAllocationType _currentType; - private bool _swapQueued; - - public BufferAllocationType DesiredType { get; private set; } - - private int _setCount; - private int _writeCount; - private int _flushCount; - private int _flushTemp; - private int _lastFlushWrite = -1; + private readonly BufferAllocationType _baseType; + private readonly BufferAllocationType _activeType; private readonly ReaderWriterLockSlim _flushLock; private FenceHolder _flushFence; private int _flushWaiting; - private List _swapActions; - private byte[] _pendingData; private BufferMirrorRangeList _pendingDataRanges; private Dictionary _mirrors; @@ -83,8 +71,7 @@ namespace Ryujinx.Graphics.Vulkan _map = allocation.HostPointer; _baseType = type; - _currentType = currentType; - DesiredType = currentType; + _activeType = currentType; _flushLock = new ReaderWriterLockSlim(); _useMirrors = gd.IsTBDR; @@ -104,8 +91,7 @@ namespace Ryujinx.Graphics.Vulkan _map = _allocation.HostPointer + offset; _baseType = type; - _currentType = currentType; - DesiredType = currentType; + _activeType = currentType; _flushLock = new ReaderWriterLockSlim(); } @@ -120,164 +106,11 @@ namespace Ryujinx.Graphics.Vulkan Size = size; _baseType = BufferAllocationType.Sparse; - _currentType = BufferAllocationType.Sparse; - DesiredType = BufferAllocationType.Sparse; + _activeType = BufferAllocationType.Sparse; _flushLock = new ReaderWriterLockSlim(); } - public bool TryBackingSwap(ref CommandBufferScoped? cbs) - { - if (_swapQueued && DesiredType != _currentType) - { - // Only swap if the buffer is not used in any queued command buffer. - bool isRented = _buffer.HasRentedCommandBufferDependency(_gd.CommandBufferPool); - - if (!isRented && _gd.CommandBufferPool.OwnedByCurrentThread && !_flushLock.IsReadLockHeld && (_pendingData == null || cbs != null)) - { - var currentAllocation = _allocationAuto; - var currentBuffer = _buffer; - IntPtr currentMap = _map; - - (VkBuffer buffer, MemoryAllocation allocation, BufferAllocationType resultType) = _gd.BufferManager.CreateBacking(_gd, Size, DesiredType, false, false, _currentType); - - if (buffer.Handle != 0) - { - if (cbs != null) - { - ClearMirrors(cbs.Value, 0, Size); - } - - _flushLock.EnterWriteLock(); - - ClearFlushFence(); - - _waitable = new MultiFenceHolder(Size); - - _allocation = allocation; - _allocationAuto = new Auto(allocation); - _buffer = new Auto(new DisposableBuffer(_gd.Api, _device, buffer), this, _waitable, _allocationAuto); - _bufferHandle = buffer.Handle; - _map = allocation.HostPointer; - - if (_map != IntPtr.Zero && currentMap != IntPtr.Zero) - { - // Copy data directly. Readbacks don't have to wait if this is done. - - unsafe - { - new Span((void*)currentMap, Size).CopyTo(new Span((void*)_map, Size)); - } - } - else - { - cbs ??= _gd.CommandBufferPool.Rent(); - - CommandBufferScoped cbsV = cbs.Value; - - Copy(_gd, cbsV, currentBuffer, _buffer, 0, 0, Size); - - // Need to wait for the data to reach the new buffer before data can be flushed. - - _flushFence = _gd.CommandBufferPool.GetFence(cbsV.CommandBufferIndex); - _flushFence.Get(); - } - - Logger.Debug?.PrintMsg(LogClass.Gpu, $"Converted {Size} buffer {_currentType} to {resultType}"); - - _currentType = resultType; - - if (_swapActions != null) - { - foreach (var action in _swapActions) - { - action(); - } - - _swapActions.Clear(); - } - - currentBuffer.Dispose(); - currentAllocation.Dispose(); - - _gd.PipelineInternal.SwapBuffer(currentBuffer, _buffer); - - _flushLock.ExitWriteLock(); - } - - _swapQueued = false; - - return true; - } - - return false; - } - - _swapQueued = false; - - return true; - } - - private void ConsiderBackingSwap() - { - if (_baseType == BufferAllocationType.Auto) - { - // When flushed, wait for a bit more info to make a decision. - bool wasFlushed = _flushTemp > 0; - int multiplier = wasFlushed ? 2 : 0; - if (_writeCount >= (WriteCountThreshold << multiplier) || _setCount >= (SetCountThreshold << multiplier) || _flushCount >= (FlushCountThreshold << multiplier)) - { - if (_flushCount > 0 || _flushTemp-- > 0) - { - // Buffers that flush should ideally be mapped in host address space for easy copies. - // If the buffer is large it will do better on GPU memory, as there will be more writes than data flushes (typically individual pages). - // If it is small, then it's likely most of the buffer will be flushed so we want it on host memory, as access is cached. - - bool hostMappingSensitive = _gd.Vendor == Vendor.Nvidia; - bool deviceLocalMapped = Size > DeviceLocalSizeThreshold || (wasFlushed && _writeCount > _flushCount * 10 && hostMappingSensitive) || _currentType == BufferAllocationType.DeviceLocalMapped; - - DesiredType = deviceLocalMapped ? BufferAllocationType.DeviceLocalMapped : BufferAllocationType.HostMapped; - - // It's harder for a buffer that is flushed to revert to another type of mapping. - if (_flushCount > 0) - { - _flushTemp = 1000; - } - } - else if (_writeCount >= (WriteCountThreshold << multiplier)) - { - // Buffers that are written often should ideally be in the device local heap. (Storage buffers) - DesiredType = BufferAllocationType.DeviceLocal; - } - else if (_setCount > (SetCountThreshold << multiplier)) - { - // Buffers that have their data set often should ideally be host mapped. (Constant buffers) - DesiredType = BufferAllocationType.HostMapped; - } - - _lastFlushWrite = -1; - _flushCount = 0; - _writeCount = 0; - _setCount = 0; - } - - if (!_swapQueued && DesiredType != _currentType) - { - _swapQueued = true; - - _gd.PipelineInternal.AddBackingSwap(this); - } - } - } - - public void Pin() - { - if (_baseType == BufferAllocationType.Auto) - { - _baseType = _currentType; - } - } - public unsafe Auto CreateView(VkFormat format, int offset, int size, Action invalidateView) { var bufferViewCreateInfo = new BufferViewCreateInfo @@ -291,19 +124,9 @@ namespace Ryujinx.Graphics.Vulkan _gd.Api.CreateBufferView(_device, bufferViewCreateInfo, null, out var bufferView).ThrowOnError(); - (_swapActions ??= new List()).Add(invalidateView); - return new Auto(new DisposableBufferView(_gd.Api, _device, bufferView), this, _waitable, _buffer); } - public void InheritMetrics(BufferHolder other) - { - _setCount = other._setCount; - _writeCount = other._writeCount; - _flushCount = other._flushCount; - _flushTemp = other._flushTemp; - } - public unsafe void InsertBarrier(CommandBuffer commandBuffer, bool isWrite) { // If the last access is write, we always need a barrier to be sure we will read or modify @@ -423,18 +246,8 @@ namespace Ryujinx.Graphics.Vulkan { if (isWrite) { - _writeCount++; - SignalWrite(0, Size); } - else if (isSSBO) - { - // Always consider SSBO access for swapping to device local memory. - - _writeCount++; - - ConsiderBackingSwap(); - } return _buffer; } @@ -443,8 +256,6 @@ namespace Ryujinx.Graphics.Vulkan { if (isWrite) { - _writeCount++; - SignalWrite(offset, size); } @@ -543,8 +354,6 @@ namespace Ryujinx.Graphics.Vulkan public void SignalWrite(int offset, int size) { - ConsiderBackingSwap(); - if (offset == 0 && size == Size) { _cachedConvertedBuffers.Clear(); @@ -624,13 +433,6 @@ namespace Ryujinx.Graphics.Vulkan WaitForFlushFence(); - if (_lastFlushWrite != _writeCount) - { - // If it's on the same page as the last flush, ignore it. - _lastFlushWrite = _writeCount; - _flushCount++; - } - Span result; if (_map != IntPtr.Zero) @@ -711,8 +513,7 @@ namespace Ryujinx.Graphics.Vulkan return; } - _setCount++; - bool allowMirror = _useMirrors && allowCbsWait && cbs != null && _currentType <= BufferAllocationType.HostMapped; + bool allowMirror = _useMirrors && allowCbsWait && cbs != null && _activeType <= BufferAllocationType.HostMapped; if (_map != IntPtr.Zero) { @@ -863,8 +664,6 @@ namespace Ryujinx.Graphics.Vulkan var dstBuffer = GetBuffer(cbs.CommandBuffer, dstOffset, data.Length, true).Get(cbs, dstOffset, data.Length, true).Value; - _writeCount--; - InsertBufferBarrier( _gd, cbs.CommandBuffer, @@ -1100,8 +899,6 @@ namespace Ryujinx.Graphics.Vulkan public void Dispose() { - _swapQueued = false; - _gd.PipelineInternal?.FlushCommandsIfWeightExceeding(_buffer, (ulong)Size); _buffer.Dispose(); diff --git a/src/Ryujinx.Graphics.Vulkan/BufferManager.cs b/src/Ryujinx.Graphics.Vulkan/BufferManager.cs index 33289a0e0..e73cde83c 100644 --- a/src/Ryujinx.Graphics.Vulkan/BufferManager.cs +++ b/src/Ryujinx.Graphics.Vulkan/BufferManager.cs @@ -165,10 +165,6 @@ namespace Ryujinx.Graphics.Vulkan if (TryGetBuffer(range.Handle, out var existingHolder)) { - // Since this buffer now also owns the memory from the referenced buffer, - // we pin it to ensure the memory location will not change. - existingHolder.Pin(); - (var memory, var offset) = existingHolder.GetDeviceMemoryAndOffset(); memoryBinds[index] = new SparseMemoryBind() @@ -235,10 +231,9 @@ namespace Ryujinx.Graphics.Vulkan int size, bool sparseCompatible = false, BufferAllocationType baseType = BufferAllocationType.HostMapped, - BufferHandle storageHint = default, bool forceMirrors = false) { - return CreateWithHandle(gd, size, out _, sparseCompatible, baseType, storageHint, forceMirrors); + return CreateWithHandle(gd, size, out _, sparseCompatible, baseType, forceMirrors); } public BufferHandle CreateWithHandle( @@ -247,10 +242,9 @@ namespace Ryujinx.Graphics.Vulkan out BufferHolder holder, bool sparseCompatible = false, BufferAllocationType baseType = BufferAllocationType.HostMapped, - BufferHandle storageHint = default, bool forceMirrors = false) { - holder = Create(gd, size, forConditionalRendering: false, sparseCompatible, baseType, storageHint); + holder = Create(gd, size, forConditionalRendering: false, sparseCompatible, baseType); if (holder == null) { return BufferHandle.Null; @@ -387,31 +381,13 @@ namespace Ryujinx.Graphics.Vulkan int size, bool forConditionalRendering = false, bool sparseCompatible = false, - BufferAllocationType baseType = BufferAllocationType.HostMapped, - BufferHandle storageHint = default) + BufferAllocationType baseType = BufferAllocationType.HostMapped) { BufferAllocationType type = baseType; - BufferHolder storageHintHolder = null; if (baseType == BufferAllocationType.Auto) { - if (gd.IsSharedMemory) - { - baseType = BufferAllocationType.HostMapped; - type = baseType; - } - else - { - type = size >= BufferHolder.DeviceLocalSizeThreshold ? BufferAllocationType.DeviceLocal : BufferAllocationType.HostMapped; - } - - if (storageHint != BufferHandle.Null) - { - if (TryGetBuffer(storageHint, out storageHintHolder)) - { - type = storageHintHolder.DesiredType; - } - } + type = BufferAllocationType.HostMapped; } (VkBuffer buffer, MemoryAllocation allocation, BufferAllocationType resultType) = @@ -421,11 +397,6 @@ namespace Ryujinx.Graphics.Vulkan { var holder = new BufferHolder(gd, _device, buffer, allocation, size, baseType, resultType); - if (storageHintHolder != null) - { - holder.InheritMetrics(storageHintHolder); - } - return holder; } diff --git a/src/Ryujinx.Graphics.Vulkan/CommandBufferPool.cs b/src/Ryujinx.Graphics.Vulkan/CommandBufferPool.cs index 61cfbb6ec..278dbecfa 100644 --- a/src/Ryujinx.Graphics.Vulkan/CommandBufferPool.cs +++ b/src/Ryujinx.Graphics.Vulkan/CommandBufferPool.cs @@ -302,12 +302,12 @@ namespace Ryujinx.Graphics.Vulkan SubmitInfo sInfo = new() { SType = StructureType.SubmitInfo, - WaitSemaphoreCount = waitSemaphores != null ? (uint)waitSemaphores.Length : 0, + WaitSemaphoreCount = !waitSemaphores.IsEmpty ? (uint)waitSemaphores.Length : 0, PWaitSemaphores = pWaitSemaphores, PWaitDstStageMask = pWaitDstStageMask, CommandBufferCount = 1, PCommandBuffers = &commandBuffer, - SignalSemaphoreCount = signalSemaphores != null ? (uint)signalSemaphores.Length : 0, + SignalSemaphoreCount = !signalSemaphores.IsEmpty ? (uint)signalSemaphores.Length : 0, PSignalSemaphores = pSignalSemaphores, }; diff --git a/src/Ryujinx.Graphics.Vulkan/DescriptorSetTemplate.cs b/src/Ryujinx.Graphics.Vulkan/DescriptorSetTemplate.cs index b9abd8fcd..117f79bb4 100644 --- a/src/Ryujinx.Graphics.Vulkan/DescriptorSetTemplate.cs +++ b/src/Ryujinx.Graphics.Vulkan/DescriptorSetTemplate.cs @@ -43,11 +43,11 @@ namespace Ryujinx.Graphics.Vulkan int binding = segment.Binding; int count = segment.Count; - if (setIndex == PipelineBase.UniformSetIndex) + if (IsBufferType(segment.Type)) { entries[seg] = new DescriptorUpdateTemplateEntry() { - DescriptorType = DescriptorType.UniformBuffer, + DescriptorType = segment.Type.Convert(), DstBinding = (uint)binding, DescriptorCount = (uint)count, Offset = structureOffset, @@ -56,76 +56,31 @@ namespace Ryujinx.Graphics.Vulkan structureOffset += (nuint)(Unsafe.SizeOf() * count); } - else if (setIndex == PipelineBase.StorageSetIndex) + else if (IsBufferTextureType(segment.Type)) { entries[seg] = new DescriptorUpdateTemplateEntry() { - DescriptorType = DescriptorType.StorageBuffer, + DescriptorType = segment.Type.Convert(), DstBinding = (uint)binding, DescriptorCount = (uint)count, Offset = structureOffset, - Stride = (nuint)Unsafe.SizeOf() + Stride = (nuint)Unsafe.SizeOf() }; - structureOffset += (nuint)(Unsafe.SizeOf() * count); + structureOffset += (nuint)(Unsafe.SizeOf() * count); } - else if (setIndex == PipelineBase.TextureSetIndex) + else { - if (segment.Type != ResourceType.BufferTexture) + entries[seg] = new DescriptorUpdateTemplateEntry() { - entries[seg] = new DescriptorUpdateTemplateEntry() - { - DescriptorType = DescriptorType.CombinedImageSampler, - DstBinding = (uint)binding, - DescriptorCount = (uint)count, - Offset = structureOffset, - Stride = (nuint)Unsafe.SizeOf() - }; + DescriptorType = segment.Type.Convert(), + DstBinding = (uint)binding, + DescriptorCount = (uint)count, + Offset = structureOffset, + Stride = (nuint)Unsafe.SizeOf() + }; - structureOffset += (nuint)(Unsafe.SizeOf() * count); - } - else - { - entries[seg] = new DescriptorUpdateTemplateEntry() - { - DescriptorType = DescriptorType.UniformTexelBuffer, - DstBinding = (uint)binding, - DescriptorCount = (uint)count, - Offset = structureOffset, - Stride = (nuint)Unsafe.SizeOf() - }; - - structureOffset += (nuint)(Unsafe.SizeOf() * count); - } - } - else if (setIndex == PipelineBase.ImageSetIndex) - { - if (segment.Type != ResourceType.BufferImage) - { - entries[seg] = new DescriptorUpdateTemplateEntry() - { - DescriptorType = DescriptorType.StorageImage, - DstBinding = (uint)binding, - DescriptorCount = (uint)count, - Offset = structureOffset, - Stride = (nuint)Unsafe.SizeOf() - }; - - structureOffset += (nuint)(Unsafe.SizeOf() * count); - } - else - { - entries[seg] = new DescriptorUpdateTemplateEntry() - { - DescriptorType = DescriptorType.StorageTexelBuffer, - DstBinding = (uint)binding, - DescriptorCount = (uint)count, - Offset = structureOffset, - Stride = (nuint)Unsafe.SizeOf() - }; - - structureOffset += (nuint)(Unsafe.SizeOf() * count); - } + structureOffset += (nuint)(Unsafe.SizeOf() * count); } } @@ -237,6 +192,16 @@ namespace Ryujinx.Graphics.Vulkan Template = result; } + private static bool IsBufferType(ResourceType type) + { + return type == ResourceType.UniformBuffer || type == ResourceType.StorageBuffer; + } + + private static bool IsBufferTextureType(ResourceType type) + { + return type == ResourceType.BufferTexture || type == ResourceType.BufferImage; + } + public unsafe void Dispose() { _gd.Api.DestroyDescriptorUpdateTemplate(_device, Template, null); diff --git a/src/Ryujinx.Graphics.Vulkan/DescriptorSetUpdater.cs b/src/Ryujinx.Graphics.Vulkan/DescriptorSetUpdater.cs index a0010e660..382f88d05 100644 --- a/src/Ryujinx.Graphics.Vulkan/DescriptorSetUpdater.cs +++ b/src/Ryujinx.Graphics.Vulkan/DescriptorSetUpdater.cs @@ -69,17 +69,7 @@ namespace Ryujinx.Graphics.Vulkan } } - private record struct ArrayRef - { - public ShaderStage Stage; - public T Array; - - public ArrayRef(ShaderStage stage, T array) - { - Stage = stage; - Array = array; - } - } + private readonly record struct ArrayRef(ShaderStage Stage, T Array); private readonly VulkanRenderer _gd; private readonly Device _device; @@ -97,6 +87,9 @@ namespace Ryujinx.Graphics.Vulkan private ArrayRef[] _textureArrayRefs; private ArrayRef[] _imageArrayRefs; + private ArrayRef[] _textureArrayExtraRefs; + private ArrayRef[] _imageArrayExtraRefs; + private readonly DescriptorBufferInfo[] _uniformBuffers; private readonly DescriptorBufferInfo[] _storageBuffers; private readonly DescriptorImageInfo[] _textures; @@ -152,6 +145,9 @@ namespace Ryujinx.Graphics.Vulkan _textureArrayRefs = Array.Empty>(); _imageArrayRefs = Array.Empty>(); + _textureArrayExtraRefs = Array.Empty>(); + _imageArrayExtraRefs = Array.Empty>(); + _uniformBuffers = new DescriptorBufferInfo[Constants.MaxUniformBufferBindings]; _storageBuffers = new DescriptorBufferInfo[Constants.MaxStorageBufferBindings]; _textures = new DescriptorImageInfo[Constants.MaxTexturesPerStage]; @@ -495,25 +491,39 @@ namespace Ryujinx.Graphics.Vulkan public void SetTextureArray(CommandBufferScoped cbs, ShaderStage stage, int binding, ITextureArray array) { - if (_textureArrayRefs.Length <= binding) - { - Array.Resize(ref _textureArrayRefs, binding + ArrayGrowthSize); - } + ref ArrayRef arrayRef = ref GetArrayRef(ref _textureArrayRefs, binding, ArrayGrowthSize); - if (_textureArrayRefs[binding].Stage != stage || _textureArrayRefs[binding].Array != array) + if (arrayRef.Stage != stage || arrayRef.Array != array) { - if (_textureArrayRefs[binding].Array != null) - { - _textureArrayRefs[binding].Array.Bound = false; - } + arrayRef.Array?.DecrementBindCount(); if (array is TextureArray textureArray) { - textureArray.Bound = true; + textureArray.IncrementBindCount(); textureArray.QueueWriteToReadBarriers(cbs, stage.ConvertToPipelineStageFlags()); } - _textureArrayRefs[binding] = new ArrayRef(stage, array as TextureArray); + arrayRef = new ArrayRef(stage, array as TextureArray); + + SignalDirty(DirtyFlags.Texture); + } + } + + public void SetTextureArraySeparate(CommandBufferScoped cbs, ShaderStage stage, int setIndex, ITextureArray array) + { + ref ArrayRef arrayRef = ref GetArrayRef(ref _textureArrayExtraRefs, setIndex - PipelineBase.DescriptorSetLayouts); + + if (arrayRef.Stage != stage || arrayRef.Array != array) + { + arrayRef.Array?.DecrementBindCount(); + + if (array is TextureArray textureArray) + { + textureArray.IncrementBindCount(); + textureArray.QueueWriteToReadBarriers(cbs, stage.ConvertToPipelineStageFlags()); + } + + arrayRef = new ArrayRef(stage, array as TextureArray); SignalDirty(DirtyFlags.Texture); } @@ -521,30 +531,56 @@ namespace Ryujinx.Graphics.Vulkan public void SetImageArray(CommandBufferScoped cbs, ShaderStage stage, int binding, IImageArray array) { - if (_imageArrayRefs.Length <= binding) - { - Array.Resize(ref _imageArrayRefs, binding + ArrayGrowthSize); - } + ref ArrayRef arrayRef = ref GetArrayRef(ref _imageArrayRefs, binding, ArrayGrowthSize); - if (_imageArrayRefs[binding].Stage != stage || _imageArrayRefs[binding].Array != array) + if (arrayRef.Stage != stage || arrayRef.Array != array) { - if (_imageArrayRefs[binding].Array != null) - { - _imageArrayRefs[binding].Array.Bound = false; - } + arrayRef.Array?.DecrementBindCount(); if (array is ImageArray imageArray) { - imageArray.Bound = true; + imageArray.IncrementBindCount(); imageArray.QueueWriteToReadBarriers(cbs, stage.ConvertToPipelineStageFlags()); } - _imageArrayRefs[binding] = new ArrayRef(stage, array as ImageArray); + arrayRef = new ArrayRef(stage, array as ImageArray); SignalDirty(DirtyFlags.Image); } } + public void SetImageArraySeparate(CommandBufferScoped cbs, ShaderStage stage, int setIndex, IImageArray array) + { + ref ArrayRef arrayRef = ref GetArrayRef(ref _imageArrayExtraRefs, setIndex - PipelineBase.DescriptorSetLayouts); + + if (arrayRef.Stage != stage || arrayRef.Array != array) + { + arrayRef.Array?.DecrementBindCount(); + + if (array is ImageArray imageArray) + { + imageArray.IncrementBindCount(); + imageArray.QueueWriteToReadBarriers(cbs, stage.ConvertToPipelineStageFlags()); + } + + arrayRef = new ArrayRef(stage, array as ImageArray); + + SignalDirty(DirtyFlags.Image); + } + } + + private static ref ArrayRef GetArrayRef(ref ArrayRef[] array, int index, int growthSize = 1) + { + ArgumentOutOfRangeException.ThrowIfNegative(index); + + if (array.Length <= index) + { + Array.Resize(ref array, index + growthSize); + } + + return ref array[index]; + } + public void SetUniformBuffers(CommandBuffer commandBuffer, ReadOnlySpan buffers) { for (int i = 0; i < buffers.Length; i++) @@ -594,31 +630,40 @@ namespace Ryujinx.Graphics.Vulkan return; } + var program = _program; + if (_dirty.HasFlag(DirtyFlags.Uniform)) { - if (_program.UsePushDescriptors) + if (program.UsePushDescriptors) { - UpdateAndBindUniformBufferPd(cbs, pbp); + UpdateAndBindUniformBufferPd(cbs); } else { - UpdateAndBind(cbs, PipelineBase.UniformSetIndex, pbp); + UpdateAndBind(cbs, program, PipelineBase.UniformSetIndex, pbp); } } if (_dirty.HasFlag(DirtyFlags.Storage)) { - UpdateAndBind(cbs, PipelineBase.StorageSetIndex, pbp); + UpdateAndBind(cbs, program, PipelineBase.StorageSetIndex, pbp); } if (_dirty.HasFlag(DirtyFlags.Texture)) { - UpdateAndBind(cbs, PipelineBase.TextureSetIndex, pbp); + UpdateAndBind(cbs, program, PipelineBase.TextureSetIndex, pbp); } if (_dirty.HasFlag(DirtyFlags.Image)) { - UpdateAndBind(cbs, PipelineBase.ImageSetIndex, pbp); + UpdateAndBind(cbs, program, PipelineBase.ImageSetIndex, pbp); + } + + if (program.BindingSegments.Length > PipelineBase.DescriptorSetLayouts) + { + // Program is using extra sets, we need to bind those too. + + BindExtraSets(cbs, program, pbp); } _dirty = DirtyFlags.None; @@ -658,9 +703,8 @@ namespace Ryujinx.Graphics.Vulkan } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void UpdateAndBind(CommandBufferScoped cbs, int setIndex, PipelineBindPoint pbp) + private void UpdateAndBind(CommandBufferScoped cbs, ShaderCollection program, int setIndex, PipelineBindPoint pbp) { - var program = _program; var bindingSegments = program.BindingSegments[setIndex]; if (bindingSegments.Length == 0) @@ -869,7 +913,7 @@ namespace Ryujinx.Graphics.Vulkan } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void UpdateAndBindUniformBufferPd(CommandBufferScoped cbs, PipelineBindPoint pbp) + private void UpdateAndBindUniformBufferPd(CommandBufferScoped cbs) { int sequence = _pdSequence; var bindingSegments = _program.BindingSegments[PipelineBase.UniformSetIndex]; @@ -933,6 +977,56 @@ namespace Ryujinx.Graphics.Vulkan } } + private void BindExtraSets(CommandBufferScoped cbs, ShaderCollection program, PipelineBindPoint pbp) + { + for (int setIndex = PipelineBase.DescriptorSetLayouts; setIndex < program.BindingSegments.Length; setIndex++) + { + var bindingSegments = program.BindingSegments[setIndex]; + + if (bindingSegments.Length == 0) + { + continue; + } + + ResourceBindingSegment segment = bindingSegments[0]; + + if (segment.IsArray) + { + DescriptorSet[] sets = null; + + if (segment.Type == ResourceType.Texture || + segment.Type == ResourceType.Sampler || + segment.Type == ResourceType.TextureAndSampler || + segment.Type == ResourceType.BufferTexture) + { + sets = _textureArrayExtraRefs[setIndex - PipelineBase.DescriptorSetLayouts].Array.GetDescriptorSets( + _device, + cbs, + _templateUpdater, + program, + setIndex, + _dummyTexture, + _dummySampler); + } + else if (segment.Type == ResourceType.Image || segment.Type == ResourceType.BufferImage) + { + sets = _imageArrayExtraRefs[setIndex - PipelineBase.DescriptorSetLayouts].Array.GetDescriptorSets( + _device, + cbs, + _templateUpdater, + program, + setIndex, + _dummyTexture); + } + + if (sets != null) + { + _gd.Api.CmdBindDescriptorSets(cbs.CommandBuffer, pbp, _program.PipelineLayout, (uint)setIndex, 1, sets, 0, ReadOnlySpan.Empty); + } + } + } + } + public void SignalCommandBufferChange() { _updateDescriptorCacheCbIndex = true; diff --git a/src/Ryujinx.Graphics.Vulkan/EnumConversion.cs b/src/Ryujinx.Graphics.Vulkan/EnumConversion.cs index f9243bf83..9d1fd9ffd 100644 --- a/src/Ryujinx.Graphics.Vulkan/EnumConversion.cs +++ b/src/Ryujinx.Graphics.Vulkan/EnumConversion.cs @@ -424,10 +424,20 @@ namespace Ryujinx.Graphics.Vulkan public static BufferAllocationType Convert(this BufferAccess access) { - if (access.HasFlag(BufferAccess.FlushPersistent) || access.HasFlag(BufferAccess.Stream)) + BufferAccess memType = access & BufferAccess.MemoryTypeMask; + + if (memType == BufferAccess.HostMemory || access.HasFlag(BufferAccess.Stream)) { return BufferAllocationType.HostMapped; } + else if (memType == BufferAccess.DeviceMemory) + { + return BufferAllocationType.DeviceLocal; + } + else if (memType == BufferAccess.DeviceMemoryMapped) + { + return BufferAllocationType.DeviceLocalMapped; + } return BufferAllocationType.Auto; } diff --git a/src/Ryujinx.Graphics.Vulkan/FramebufferParams.cs b/src/Ryujinx.Graphics.Vulkan/FramebufferParams.cs index 8079e5ff9..ea0fd42e5 100644 --- a/src/Ryujinx.Graphics.Vulkan/FramebufferParams.cs +++ b/src/Ryujinx.Graphics.Vulkan/FramebufferParams.cs @@ -24,6 +24,7 @@ namespace Ryujinx.Graphics.Vulkan public VkFormat[] AttachmentFormats { get; } public int[] AttachmentIndices { get; } public uint AttachmentIntegerFormatMask { get; } + public bool LogicOpsAllowed { get; } public int AttachmentsCount { get; } public int MaxColorAttachmentIndex => AttachmentIndices.Length > 0 ? AttachmentIndices[^1] : -1; @@ -32,7 +33,9 @@ namespace Ryujinx.Graphics.Vulkan public FramebufferParams(Device device, TextureView view, uint width, uint height) { - bool isDepthStencil = view.Info.Format.IsDepthOrStencil(); + var format = view.Info.Format; + + bool isDepthStencil = format.IsDepthOrStencil(); _device = device; _attachments = new[] { view.GetImageViewForAttachment() }; @@ -56,6 +59,8 @@ namespace Ryujinx.Graphics.Vulkan AttachmentSamples = new[] { (uint)view.Info.Samples }; AttachmentFormats = new[] { view.VkFormat }; AttachmentIndices = isDepthStencil ? Array.Empty() : new[] { 0 }; + AttachmentIntegerFormatMask = format.IsInteger() ? 1u : 0u; + LogicOpsAllowed = !format.IsFloatOrSrgb(); AttachmentsCount = 1; @@ -85,6 +90,7 @@ namespace Ryujinx.Graphics.Vulkan int index = 0; int bindIndex = 0; uint attachmentIntegerFormatMask = 0; + bool allFormatsFloatOrSrgb = colorsCount != 0; foreach (ITexture color in colors) { @@ -101,11 +107,15 @@ namespace Ryujinx.Graphics.Vulkan AttachmentFormats[index] = texture.VkFormat; AttachmentIndices[index] = bindIndex; - if (texture.Info.Format.IsInteger()) + var format = texture.Info.Format; + + if (format.IsInteger()) { attachmentIntegerFormatMask |= 1u << bindIndex; } + allFormatsFloatOrSrgb &= format.IsFloatOrSrgb(); + width = Math.Min(width, (uint)texture.Width); height = Math.Min(height, (uint)texture.Height); layers = Math.Min(layers, (uint)texture.Layers); @@ -120,6 +130,7 @@ namespace Ryujinx.Graphics.Vulkan } AttachmentIntegerFormatMask = attachmentIntegerFormatMask; + LogicOpsAllowed = !allFormatsFloatOrSrgb; if (depthStencil is TextureView dsTexture && dsTexture.Valid) { diff --git a/src/Ryujinx.Graphics.Vulkan/ImageArray.cs b/src/Ryujinx.Graphics.Vulkan/ImageArray.cs index 38a5b6b48..3c7f321ff 100644 --- a/src/Ryujinx.Graphics.Vulkan/ImageArray.cs +++ b/src/Ryujinx.Graphics.Vulkan/ImageArray.cs @@ -2,6 +2,7 @@ using Ryujinx.Graphics.GAL; using Silk.NET.Vulkan; using System; using System.Collections.Generic; +using System.Diagnostics; namespace Ryujinx.Graphics.Vulkan { @@ -24,12 +25,18 @@ namespace Ryujinx.Graphics.Vulkan private HashSet _storages; + private DescriptorSet[] _cachedDescriptorSets; + private int _cachedCommandBufferIndex; private int _cachedSubmissionCount; + private ShaderCollection _cachedDscProgram; + private int _cachedDscSetIndex; + private int _cachedDscIndex; + private readonly bool _isBuffer; - public bool Bound; + private int _bindCount; public ImageArray(VulkanRenderer gd, int size, bool isBuffer) { @@ -97,8 +104,12 @@ namespace Ryujinx.Graphics.Vulkan { _cachedCommandBufferIndex = -1; _storages = null; + _cachedDescriptorSets = null; - _gd.PipelineInternal.ForceImageDirty(); + if (_bindCount != 0) + { + _gd.PipelineInternal.ForceImageDirty(); + } } public void QueueWriteToReadBarriers(CommandBufferScoped cbs, PipelineStageFlags stageFlags) @@ -175,5 +186,65 @@ namespace Ryujinx.Graphics.Vulkan return bufferTextures; } + + public DescriptorSet[] GetDescriptorSets( + Device device, + CommandBufferScoped cbs, + DescriptorSetTemplateUpdater templateUpdater, + ShaderCollection program, + int setIndex, + TextureView dummyTexture) + { + if (_cachedDescriptorSets != null) + { + // We still need to ensure the current command buffer holds a reference to all used textures. + + if (!_isBuffer) + { + GetImageInfos(_gd, cbs, dummyTexture); + } + else + { + GetBufferViews(cbs); + } + + return _cachedDescriptorSets; + } + + _cachedDscProgram?.ReleaseManualDescriptorSetCollection(_cachedDscSetIndex, _cachedDscIndex); + var dsc = program.GetNewManualDescriptorSetCollection(cbs.CommandBufferIndex, setIndex, out _cachedDscIndex).Get(cbs); + + DescriptorSetTemplate template = program.Templates[setIndex]; + + DescriptorSetTemplateWriter tu = templateUpdater.Begin(template); + + if (!_isBuffer) + { + tu.Push(GetImageInfos(_gd, cbs, dummyTexture)); + } + else + { + tu.Push(GetBufferViews(cbs)); + } + + var sets = dsc.GetSets(); + templateUpdater.Commit(_gd, device, sets[0]); + _cachedDescriptorSets = sets; + _cachedDscProgram = program; + _cachedDscSetIndex = setIndex; + + return sets; + } + + public void IncrementBindCount() + { + _bindCount++; + } + + public void DecrementBindCount() + { + int newBindCount = --_bindCount; + Debug.Assert(newBindCount >= 0); + } } } diff --git a/src/Ryujinx.Graphics.Vulkan/MultiFenceHolder.cs b/src/Ryujinx.Graphics.Vulkan/MultiFenceHolder.cs index 0bce3b72d..806b872bc 100644 --- a/src/Ryujinx.Graphics.Vulkan/MultiFenceHolder.cs +++ b/src/Ryujinx.Graphics.Vulkan/MultiFenceHolder.cs @@ -1,3 +1,4 @@ +using Ryujinx.Common.Memory; using Silk.NET.Vulkan; using System; @@ -165,14 +166,15 @@ namespace Ryujinx.Graphics.Vulkan /// True if all fences were signaled before the timeout expired, false otherwise private bool WaitForFencesImpl(Vk api, Device device, int offset, int size, bool hasTimeout, ulong timeout) { - Span fenceHolders = new FenceHolder[CommandBufferPool.MaxCommandBuffers]; + using SpanOwner fenceHoldersOwner = SpanOwner.Rent(CommandBufferPool.MaxCommandBuffers); + Span fenceHolders = fenceHoldersOwner.Span; int count = size != 0 ? GetOverlappingFences(fenceHolders, offset, size) : GetFences(fenceHolders); Span fences = stackalloc Fence[count]; int fenceCount = 0; - for (int i = 0; i < count; i++) + for (int i = 0; i < fences.Length; i++) { if (fenceHolders[i].TryGet(out Fence fence)) { diff --git a/src/Ryujinx.Graphics.Vulkan/PipelineBase.cs b/src/Ryujinx.Graphics.Vulkan/PipelineBase.cs index 41ab84d94..918de59b7 100644 --- a/src/Ryujinx.Graphics.Vulkan/PipelineBase.cs +++ b/src/Ryujinx.Graphics.Vulkan/PipelineBase.cs @@ -751,14 +751,12 @@ namespace Ryujinx.Graphics.Vulkan _vertexBufferUpdater.Commit(Cbs); } -#pragma warning disable CA1822 // Mark member as static public void SetAlphaTest(bool enable, float reference, CompareOp op) { // This is currently handled using shader specialization, as Vulkan does not support alpha test. // In the future, we may want to use this to write the reference value into the support buffer, // to avoid creating one version of the shader per reference value used. } -#pragma warning restore CA1822 public void SetBlendState(AdvancedBlendDescriptor blend) { @@ -903,6 +901,11 @@ namespace Ryujinx.Graphics.Vulkan _descriptorSetUpdater.SetImageArray(Cbs, stage, binding, array); } + public void SetImageArraySeparate(ShaderStage stage, int setIndex, IImageArray array) + { + _descriptorSetUpdater.SetImageArraySeparate(Cbs, stage, setIndex, array); + } + public void SetIndexBuffer(BufferRange buffer, IndexType type) { if (buffer.Handle != BufferHandle.Null) @@ -945,7 +948,6 @@ namespace Ryujinx.Graphics.Vulkan // TODO: Default levels (likely needs emulation on shaders?) } -#pragma warning disable CA1822 // Mark member as static public void SetPointParameters(float size, bool isProgramPointSize, bool enablePointSprite, Origin origin) { // TODO. @@ -955,7 +957,6 @@ namespace Ryujinx.Graphics.Vulkan { // TODO. } -#pragma warning restore CA1822 public void SetPrimitiveRestart(bool enable, int index) { @@ -1156,6 +1157,11 @@ namespace Ryujinx.Graphics.Vulkan _descriptorSetUpdater.SetTextureArray(Cbs, stage, binding, array); } + public void SetTextureArraySeparate(ShaderStage stage, int setIndex, ITextureArray array) + { + _descriptorSetUpdater.SetTextureArraySeparate(Cbs, stage, setIndex, array); + } + public void SetTransformFeedbackBuffers(ReadOnlySpan buffers) { PauseTransformFeedbackInternal(); @@ -1186,12 +1192,10 @@ namespace Ryujinx.Graphics.Vulkan _descriptorSetUpdater.SetUniformBuffers(CommandBuffer, buffers); } -#pragma warning disable CA1822 // Mark member as static public void SetUserClipDistance(int index, bool enableClip) { // TODO. } -#pragma warning restore CA1822 public void SetVertexAttribs(ReadOnlySpan vertexAttribs) { @@ -1498,6 +1502,7 @@ namespace Ryujinx.Graphics.Vulkan var dstAttachmentFormats = _newState.Internal.AttachmentFormats.AsSpan(); FramebufferParams.AttachmentFormats.CopyTo(dstAttachmentFormats); _newState.Internal.AttachmentIntegerFormatMask = FramebufferParams.AttachmentIntegerFormatMask; + _newState.Internal.LogicOpsAllowed = FramebufferParams.LogicOpsAllowed; for (int i = FramebufferParams.AttachmentFormats.Length; i < dstAttachmentFormats.Length; i++) { diff --git a/src/Ryujinx.Graphics.Vulkan/PipelineConverter.cs b/src/Ryujinx.Graphics.Vulkan/PipelineConverter.cs index 95b480a5e..41618c736 100644 --- a/src/Ryujinx.Graphics.Vulkan/PipelineConverter.cs +++ b/src/Ryujinx.Graphics.Vulkan/PipelineConverter.cs @@ -302,6 +302,7 @@ namespace Ryujinx.Graphics.Vulkan int attachmentCount = 0; int maxColorAttachmentIndex = -1; uint attachmentIntegerFormatMask = 0; + bool allFormatsFloatOrSrgb = true; for (int i = 0; i < Constants.MaxRenderTargets; i++) { @@ -314,6 +315,8 @@ namespace Ryujinx.Graphics.Vulkan { attachmentIntegerFormatMask |= 1u << i; } + + allFormatsFloatOrSrgb &= state.AttachmentFormats[i].IsFloatOrSrgb(); } } @@ -325,6 +328,7 @@ namespace Ryujinx.Graphics.Vulkan pipeline.ColorBlendAttachmentStateCount = (uint)(maxColorAttachmentIndex + 1); pipeline.VertexAttributeDescriptionsCount = (uint)Math.Min(Constants.MaxVertexAttributes, state.VertexAttribCount); pipeline.Internal.AttachmentIntegerFormatMask = attachmentIntegerFormatMask; + pipeline.Internal.LogicOpsAllowed = attachmentCount == 0 || !allFormatsFloatOrSrgb; return pipeline; } diff --git a/src/Ryujinx.Graphics.Vulkan/PipelineFull.cs b/src/Ryujinx.Graphics.Vulkan/PipelineFull.cs index 4987548cd..357d517eb 100644 --- a/src/Ryujinx.Graphics.Vulkan/PipelineFull.cs +++ b/src/Ryujinx.Graphics.Vulkan/PipelineFull.cs @@ -222,20 +222,6 @@ namespace Ryujinx.Graphics.Vulkan } } - private void TryBackingSwaps() - { - CommandBufferScoped? cbs = null; - - _backingSwaps.RemoveAll(holder => holder.TryBackingSwap(ref cbs)); - - cbs?.Dispose(); - } - - public void AddBackingSwap(BufferHolder holder) - { - _backingSwaps.Add(holder); - } - public void Restore() { if (Pipeline != null) @@ -291,8 +277,6 @@ namespace Ryujinx.Graphics.Vulkan Gd.ResetCounterPool(); - TryBackingSwaps(); - Restore(); } diff --git a/src/Ryujinx.Graphics.Vulkan/PipelineLayoutCacheEntry.cs b/src/Ryujinx.Graphics.Vulkan/PipelineLayoutCacheEntry.cs index fb1f0a5ff..7d0948d6e 100644 --- a/src/Ryujinx.Graphics.Vulkan/PipelineLayoutCacheEntry.cs +++ b/src/Ryujinx.Graphics.Vulkan/PipelineLayoutCacheEntry.cs @@ -3,6 +3,7 @@ using Silk.NET.Vulkan; using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Runtime.InteropServices; namespace Ryujinx.Graphics.Vulkan { @@ -27,6 +28,24 @@ namespace Ryujinx.Graphics.Vulkan private int _dsLastCbIndex; private int _dsLastSubmissionCount; + private struct ManualDescriptorSetEntry + { + public Auto DescriptorSet; + public int CbIndex; + public int CbSubmissionCount; + public bool InUse; + + public ManualDescriptorSetEntry(Auto descriptorSet, int cbIndex, int cbSubmissionCount, bool inUse) + { + DescriptorSet = descriptorSet; + CbIndex = cbIndex; + CbSubmissionCount = cbSubmissionCount; + InUse = inUse; + } + } + + private readonly List[] _manualDsCache; + private readonly Dictionary _pdTemplates; private readonly ResourceDescriptorCollection _pdDescriptors; private long _lastPdUsage; @@ -50,6 +69,7 @@ namespace Ryujinx.Graphics.Vulkan } _dsCacheCursor = new int[setsCount]; + _manualDsCache = new List[setsCount]; } public PipelineLayoutCacheEntry( @@ -124,6 +144,51 @@ namespace Ryujinx.Graphics.Vulkan return list[index]; } + public Auto GetNewManualDescriptorSetCollection(int commandBufferIndex, int setIndex, out int cacheIndex) + { + int submissionCount = _gd.CommandBufferPool.GetSubmissionCount(commandBufferIndex); + + var list = _manualDsCache[setIndex] ??= new(); + var span = CollectionsMarshal.AsSpan(list); + + for (int index = 0; index < span.Length; index++) + { + ref ManualDescriptorSetEntry entry = ref span[index]; + + if (!entry.InUse && (entry.CbIndex != commandBufferIndex || entry.CbSubmissionCount != submissionCount)) + { + entry.InUse = true; + entry.CbIndex = commandBufferIndex; + entry.CbSubmissionCount = submissionCount; + + cacheIndex = index; + + return entry.DescriptorSet; + } + } + + var dsc = _descriptorSetManager.AllocateDescriptorSet( + _gd.Api, + DescriptorSetLayouts[setIndex], + _poolSizes[setIndex], + setIndex, + _consumedDescriptorsPerSet[setIndex], + false); + + cacheIndex = list.Count; + list.Add(new ManualDescriptorSetEntry(dsc, commandBufferIndex, submissionCount, inUse: true)); + + return dsc; + } + + public void ReleaseManualDescriptorSetCollection(int setIndex, int cacheIndex) + { + var list = _manualDsCache[setIndex]; + var span = CollectionsMarshal.AsSpan(list); + + span[cacheIndex].InUse = false; + } + private static Span GetDescriptorPoolSizes(Span output, ResourceDescriptorCollection setDescriptor, uint multiplier) { int count = 0; @@ -204,6 +269,21 @@ namespace Ryujinx.Graphics.Vulkan } } + for (int i = 0; i < _manualDsCache.Length; i++) + { + if (_manualDsCache[i] == null) + { + continue; + } + + for (int j = 0; j < _manualDsCache[i].Count; j++) + { + _manualDsCache[i][j].DescriptorSet.Dispose(); + } + + _manualDsCache[i].Clear(); + } + _gd.Api.DestroyPipelineLayout(_device, PipelineLayout, null); for (int i = 0; i < DescriptorSetLayouts.Length; i++) diff --git a/src/Ryujinx.Graphics.Vulkan/PipelineState.cs b/src/Ryujinx.Graphics.Vulkan/PipelineState.cs index 49c12b376..c38748936 100644 --- a/src/Ryujinx.Graphics.Vulkan/PipelineState.cs +++ b/src/Ryujinx.Graphics.Vulkan/PipelineState.cs @@ -560,10 +560,14 @@ namespace Ryujinx.Graphics.Vulkan } } + // Vendors other than NVIDIA have a bug where it enables logical operations even for float formats, + // so we need to force disable them here. + bool logicOpEnable = LogicOpEnable && (gd.Vendor == Vendor.Nvidia || Internal.LogicOpsAllowed); + var colorBlendState = new PipelineColorBlendStateCreateInfo { SType = StructureType.PipelineColorBlendStateCreateInfo, - LogicOpEnable = LogicOpEnable, + LogicOpEnable = logicOpEnable, LogicOp = LogicOp, AttachmentCount = ColorBlendAttachmentStateCount, PAttachments = pColorBlendAttachmentState, diff --git a/src/Ryujinx.Graphics.Vulkan/PipelineUid.cs b/src/Ryujinx.Graphics.Vulkan/PipelineUid.cs index 3448d9743..238f06e2a 100644 --- a/src/Ryujinx.Graphics.Vulkan/PipelineUid.cs +++ b/src/Ryujinx.Graphics.Vulkan/PipelineUid.cs @@ -34,6 +34,7 @@ namespace Ryujinx.Graphics.Vulkan public Array8 ColorBlendAttachmentState; public Array9 AttachmentFormats; public uint AttachmentIntegerFormatMask; + public bool LogicOpsAllowed; public readonly override bool Equals(object obj) { diff --git a/src/Ryujinx.Graphics.Vulkan/ShaderCollection.cs b/src/Ryujinx.Graphics.Vulkan/ShaderCollection.cs index 178546983..f2d648a51 100644 --- a/src/Ryujinx.Graphics.Vulkan/ShaderCollection.cs +++ b/src/Ryujinx.Graphics.Vulkan/ShaderCollection.cs @@ -604,6 +604,16 @@ namespace Ryujinx.Graphics.Vulkan return _plce.GetNewDescriptorSetCollection(setIndex, out isNew); } + public Auto GetNewManualDescriptorSetCollection(int commandBufferIndex, int setIndex, out int cacheIndex) + { + return _plce.GetNewManualDescriptorSetCollection(commandBufferIndex, setIndex, out cacheIndex); + } + + public void ReleaseManualDescriptorSetCollection(int setIndex, int cacheIndex) + { + _plce.ReleaseManualDescriptorSetCollection(setIndex, cacheIndex); + } + public bool HasSameLayout(ShaderCollection other) { return other != null && _plce == other._plce; diff --git a/src/Ryujinx.Graphics.Vulkan/TextureArray.cs b/src/Ryujinx.Graphics.Vulkan/TextureArray.cs index 6ef9087bc..fe834225c 100644 --- a/src/Ryujinx.Graphics.Vulkan/TextureArray.cs +++ b/src/Ryujinx.Graphics.Vulkan/TextureArray.cs @@ -2,6 +2,7 @@ using Ryujinx.Graphics.GAL; using Silk.NET.Vulkan; using System; using System.Collections.Generic; +using System.Diagnostics; namespace Ryujinx.Graphics.Vulkan { @@ -24,12 +25,18 @@ namespace Ryujinx.Graphics.Vulkan private HashSet _storages; + private DescriptorSet[] _cachedDescriptorSets; + private int _cachedCommandBufferIndex; private int _cachedSubmissionCount; + private ShaderCollection _cachedDscProgram; + private int _cachedDscSetIndex; + private int _cachedDscIndex; + private readonly bool _isBuffer; - public bool Bound; + private int _bindCount; public TextureArray(VulkanRenderer gd, int size, bool isBuffer) { @@ -106,8 +113,12 @@ namespace Ryujinx.Graphics.Vulkan { _cachedCommandBufferIndex = -1; _storages = null; + _cachedDescriptorSets = null; - _gd.PipelineInternal.ForceTextureDirty(); + if (_bindCount != 0) + { + _gd.PipelineInternal.ForceTextureDirty(); + } } public void QueueWriteToReadBarriers(CommandBufferScoped cbs, PipelineStageFlags stageFlags) @@ -190,5 +201,66 @@ namespace Ryujinx.Graphics.Vulkan return bufferTextures; } + + public DescriptorSet[] GetDescriptorSets( + Device device, + CommandBufferScoped cbs, + DescriptorSetTemplateUpdater templateUpdater, + ShaderCollection program, + int setIndex, + TextureView dummyTexture, + SamplerHolder dummySampler) + { + if (_cachedDescriptorSets != null) + { + // We still need to ensure the current command buffer holds a reference to all used textures. + + if (!_isBuffer) + { + GetImageInfos(_gd, cbs, dummyTexture, dummySampler); + } + else + { + GetBufferViews(cbs); + } + + return _cachedDescriptorSets; + } + + _cachedDscProgram?.ReleaseManualDescriptorSetCollection(_cachedDscSetIndex, _cachedDscIndex); + var dsc = program.GetNewManualDescriptorSetCollection(cbs.CommandBufferIndex, setIndex, out _cachedDscIndex).Get(cbs); + + DescriptorSetTemplate template = program.Templates[setIndex]; + + DescriptorSetTemplateWriter tu = templateUpdater.Begin(template); + + if (!_isBuffer) + { + tu.Push(GetImageInfos(_gd, cbs, dummyTexture, dummySampler)); + } + else + { + tu.Push(GetBufferViews(cbs)); + } + + var sets = dsc.GetSets(); + templateUpdater.Commit(_gd, device, sets[0]); + _cachedDescriptorSets = sets; + _cachedDscProgram = program; + _cachedDscSetIndex = setIndex; + + return sets; + } + + public void IncrementBindCount() + { + _bindCount++; + } + + public void DecrementBindCount() + { + int newBindCount = --_bindCount; + Debug.Assert(newBindCount >= 0); + } } } diff --git a/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs b/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs index e75e7f4b4..86a347e01 100644 --- a/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs +++ b/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs @@ -486,12 +486,7 @@ namespace Ryujinx.Graphics.Vulkan public BufferHandle CreateBuffer(int size, BufferAccess access) { - return BufferManager.CreateWithHandle(this, size, access.HasFlag(BufferAccess.SparseCompatible), access.Convert(), default, access == BufferAccess.Stream); - } - - public BufferHandle CreateBuffer(int size, BufferAccess access, BufferHandle storageHint) - { - return BufferManager.CreateWithHandle(this, size, access.HasFlag(BufferAccess.SparseCompatible), access.Convert(), storageHint); + return BufferManager.CreateWithHandle(this, size, access.HasFlag(BufferAccess.SparseCompatible), access.Convert(), access.HasFlag(BufferAccess.Stream)); } public BufferHandle CreateBuffer(nint pointer, int size) @@ -675,9 +670,23 @@ namespace Ryujinx.Graphics.Vulkan var limits = _physicalDevice.PhysicalDeviceProperties.Limits; var mainQueueProperties = _physicalDevice.QueueFamilyProperties[QueueFamilyIndex]; + SystemMemoryType memoryType; + + if (IsSharedMemory) + { + memoryType = SystemMemoryType.UnifiedMemory; + } + else + { + memoryType = Vendor == Vendor.Nvidia ? + SystemMemoryType.DedicatedMemorySlowStorage : + SystemMemoryType.DedicatedMemory; + } + return new Capabilities( api: TargetApi.Vulkan, GpuVendor, + memoryType: memoryType, hasFrontFacingBug: IsIntelWindows, hasVectorIndexingBug: Vendor == Vendor.Qualcomm, needsFragmentOutputSpecialization: IsMoltenVk, @@ -691,6 +700,7 @@ namespace Ryujinx.Graphics.Vulkan supportsBgraFormat: true, supportsR4G4Format: false, supportsR4G4B4A4Format: supportsR4G4B4A4Format, + supportsScaledVertexFormats: FormatCapabilities.SupportsScaledVertexFormats(), supportsSnormBufferTextureFormat: true, supports5BitComponentFormat: supports5BitComponentFormat, supportsSparseBuffer: features2.Features.SparseBinding && mainQueueProperties.QueueFlags.HasFlag(QueueFlags.SparseBindingBit), @@ -705,7 +715,8 @@ namespace Ryujinx.Graphics.Vulkan supportsMismatchingViewFormat: true, supportsCubemapView: !IsAmdGcn, supportsNonConstantTextureOffset: false, - supportsScaledVertexFormats: FormatCapabilities.SupportsScaledVertexFormats(), + supportsQuads: false, + supportsSeparateSampler: true, supportsShaderBallot: false, supportsShaderBarrierDivergence: Vendor != Vendor.Intel, supportsShaderFloat64: Capabilities.SupportsShaderFloat64, @@ -717,6 +728,12 @@ namespace Ryujinx.Graphics.Vulkan supportsViewportSwizzle: false, supportsIndirectParameters: true, supportsDepthClipControl: Capabilities.SupportsDepthClipControl, + uniformBufferSetIndex: PipelineBase.UniformSetIndex, + storageBufferSetIndex: PipelineBase.StorageSetIndex, + textureSetIndex: PipelineBase.TextureSetIndex, + imageSetIndex: PipelineBase.ImageSetIndex, + extraSetBaseIndex: PipelineBase.DescriptorSetLayouts, + maximumExtraSets: Math.Max(0, (int)limits.MaxBoundDescriptorSets - PipelineBase.DescriptorSetLayouts), maximumUniformBuffersPerStage: Constants.MaxUniformBuffersPerStage, maximumStorageBuffersPerStage: Constants.MaxStorageBuffersPerStage, maximumTexturesPerStage: Constants.MaxTexturesPerStage, diff --git a/src/Ryujinx.Gtk3/Input/GTK3/GTK3KeyboardDriver.cs b/src/Ryujinx.Gtk3/Input/GTK3/GTK3KeyboardDriver.cs index e502254be..bd71c7933 100644 --- a/src/Ryujinx.Gtk3/Input/GTK3/GTK3KeyboardDriver.cs +++ b/src/Ryujinx.Gtk3/Input/GTK3/GTK3KeyboardDriver.cs @@ -81,6 +81,11 @@ namespace Ryujinx.Input.GTK3 return _pressedKeys.Contains(nativeKey); } + public void Clear() + { + _pressedKeys.Clear(); + } + public IGamepad GetGamepad(string id) { if (!_keyboardIdentifers[0].Equals(id)) diff --git a/src/Ryujinx.Gtk3/Program.cs b/src/Ryujinx.Gtk3/Program.cs index 1845c512e..749cb6978 100644 --- a/src/Ryujinx.Gtk3/Program.cs +++ b/src/Ryujinx.Gtk3/Program.cs @@ -75,7 +75,7 @@ namespace Ryujinx if (OperatingSystem.IsWindows() && !OperatingSystem.IsWindowsVersionAtLeast(10, 0, 17134)) { - MessageBoxA(IntPtr.Zero, "You are running an outdated version of Windows.\n\nStarting on June 1st 2022, Ryujinx will only support Windows 10 1803 and newer.\n", $"Ryujinx {Version}", MbIconWarning); + MessageBoxA(IntPtr.Zero, "You are running an outdated version of Windows.\n\nRyujinx supports Windows 10 version 1803 and newer.\n", $"Ryujinx {Version}", MbIconWarning); } // Parse arguments @@ -181,21 +181,24 @@ namespace Ryujinx { // No configuration, we load the default values and save it to disk ConfigurationPath = appDataConfigurationPath; + Logger.Notice.Print(LogClass.Application, $"No configuration file found. Saving default configuration to: {ConfigurationPath}"); ConfigurationState.Instance.LoadDefault(); ConfigurationState.Instance.ToFileFormat().SaveConfig(ConfigurationPath); } else { + Logger.Notice.Print(LogClass.Application, $"Loading configuration from: {ConfigurationPath}"); + if (ConfigurationFileFormat.TryLoad(ConfigurationPath, out ConfigurationFileFormat configurationFileFormat)) { ConfigurationState.Instance.Load(configurationFileFormat, ConfigurationPath); } else { - ConfigurationState.Instance.LoadDefault(); + Logger.Warning?.PrintMsg(LogClass.Application, $"Failed to load config! Loading the default config instead.\nFailed config location: {ConfigurationPath}"); - Logger.Warning?.PrintMsg(LogClass.Application, $"Failed to load config! Loading the default config instead.\nFailed config location {ConfigurationPath}"); + ConfigurationState.Instance.LoadDefault(); } } diff --git a/src/Ryujinx.Gtk3/Ryujinx.Gtk3.csproj b/src/Ryujinx.Gtk3/Ryujinx.Gtk3.csproj index 68bf98981..b4453f9d7 100644 --- a/src/Ryujinx.Gtk3/Ryujinx.Gtk3.csproj +++ b/src/Ryujinx.Gtk3/Ryujinx.Gtk3.csproj @@ -63,7 +63,7 @@ - + Always diff --git a/src/Ryujinx.Gtk3/UI/Applet/GtkHostUIHandler.cs b/src/Ryujinx.Gtk3/UI/Applet/GtkHostUIHandler.cs index 1d918d21b..b3f509a09 100644 --- a/src/Ryujinx.Gtk3/UI/Applet/GtkHostUIHandler.cs +++ b/src/Ryujinx.Gtk3/UI/Applet/GtkHostUIHandler.cs @@ -107,6 +107,8 @@ namespace Ryujinx.UI.Applet swkbdDialog.SetInputLengthValidation(args.StringLengthMin, args.StringLengthMax); swkbdDialog.SetInputValidation(args.KeyboardMode); + ((MainWindow)_parent).RendererWidget.NpadManager.BlockInputUpdates(); + if (swkbdDialog.Run() == (int)ResponseType.Ok) { inputText = swkbdDialog.InputEntry.Text; @@ -128,6 +130,7 @@ namespace Ryujinx.UI.Applet }); dialogCloseEvent.WaitOne(); + ((MainWindow)_parent).RendererWidget.NpadManager.UnblockInputUpdates(); userText = error ? null : inputText; diff --git a/src/Ryujinx.Gtk3/UI/Helper/ButtonHelper.cs b/src/Ryujinx.Gtk3/UI/Helper/ButtonHelper.cs new file mode 100644 index 000000000..5a8ca96a1 --- /dev/null +++ b/src/Ryujinx.Gtk3/UI/Helper/ButtonHelper.cs @@ -0,0 +1,158 @@ +using Ryujinx.Common.Configuration.Hid.Controller; +using Ryujinx.Input; +using System; +using System.Collections.Generic; +using Key = Ryujinx.Common.Configuration.Hid.Key; +using StickInputId = Ryujinx.Common.Configuration.Hid.Controller.StickInputId; + +namespace Ryujinx.UI.Helper +{ + public static class ButtonHelper + { + private static readonly Dictionary _keysMap = new() + { + { Key.Unknown, "Unknown" }, + { Key.ShiftLeft, "ShiftLeft" }, + { Key.ShiftRight, "ShiftRight" }, + { Key.ControlLeft, "CtrlLeft" }, + { Key.ControlRight, "CtrlRight" }, + { Key.AltLeft, OperatingSystem.IsMacOS() ? "OptLeft" : "AltLeft" }, + { Key.AltRight, OperatingSystem.IsMacOS() ? "OptRight" : "AltRight" }, + { Key.WinLeft, OperatingSystem.IsMacOS() ? "CmdLeft" : "WinLeft" }, + { Key.WinRight, OperatingSystem.IsMacOS() ? "CmdRight" : "WinRight" }, + { Key.Up, "Up" }, + { Key.Down, "Down" }, + { Key.Left, "Left" }, + { Key.Right, "Right" }, + { Key.Enter, "Enter" }, + { Key.Escape, "Escape" }, + { Key.Space, "Space" }, + { Key.Tab, "Tab" }, + { Key.BackSpace, "Backspace" }, + { Key.Insert, "Insert" }, + { Key.Delete, "Delete" }, + { Key.PageUp, "PageUp" }, + { Key.PageDown, "PageDown" }, + { Key.Home, "Home" }, + { Key.End, "End" }, + { Key.CapsLock, "CapsLock" }, + { Key.ScrollLock, "ScrollLock" }, + { Key.PrintScreen, "PrintScreen" }, + { Key.Pause, "Pause" }, + { Key.NumLock, "NumLock" }, + { Key.Clear, "Clear" }, + { Key.Keypad0, "Keypad0" }, + { Key.Keypad1, "Keypad1" }, + { Key.Keypad2, "Keypad2" }, + { Key.Keypad3, "Keypad3" }, + { Key.Keypad4, "Keypad4" }, + { Key.Keypad5, "Keypad5" }, + { Key.Keypad6, "Keypad6" }, + { Key.Keypad7, "Keypad7" }, + { Key.Keypad8, "Keypad8" }, + { Key.Keypad9, "Keypad9" }, + { Key.KeypadDivide, "KeypadDivide" }, + { Key.KeypadMultiply, "KeypadMultiply" }, + { Key.KeypadSubtract, "KeypadSubtract" }, + { Key.KeypadAdd, "KeypadAdd" }, + { Key.KeypadDecimal, "KeypadDecimal" }, + { Key.KeypadEnter, "KeypadEnter" }, + { Key.Number0, "0" }, + { Key.Number1, "1" }, + { Key.Number2, "2" }, + { Key.Number3, "3" }, + { Key.Number4, "4" }, + { Key.Number5, "5" }, + { Key.Number6, "6" }, + { Key.Number7, "7" }, + { Key.Number8, "8" }, + { Key.Number9, "9" }, + { Key.Tilde, "~" }, + { Key.Grave, "`" }, + { Key.Minus, "-" }, + { Key.Plus, "+" }, + { Key.BracketLeft, "[" }, + { Key.BracketRight, "]" }, + { Key.Semicolon, ";" }, + { Key.Quote, "'" }, + { Key.Comma, "," }, + { Key.Period, "." }, + { Key.Slash, "/" }, + { Key.BackSlash, "\\" }, + { Key.Unbound, "Unbound" }, + }; + + private static readonly Dictionary _gamepadInputIdMap = new() + { + { GamepadInputId.LeftStick, "LeftStick" }, + { GamepadInputId.RightStick, "RightStick" }, + { GamepadInputId.LeftShoulder, "LeftShoulder" }, + { GamepadInputId.RightShoulder, "RightShoulder" }, + { GamepadInputId.LeftTrigger, "LeftTrigger" }, + { GamepadInputId.RightTrigger, "RightTrigger" }, + { GamepadInputId.DpadUp, "DpadUp" }, + { GamepadInputId.DpadDown, "DpadDown" }, + { GamepadInputId.DpadLeft, "DpadLeft" }, + { GamepadInputId.DpadRight, "DpadRight" }, + { GamepadInputId.Minus, "Minus" }, + { GamepadInputId.Plus, "Plus" }, + { GamepadInputId.Guide, "Guide" }, + { GamepadInputId.Misc1, "Misc1" }, + { GamepadInputId.Paddle1, "Paddle1" }, + { GamepadInputId.Paddle2, "Paddle2" }, + { GamepadInputId.Paddle3, "Paddle3" }, + { GamepadInputId.Paddle4, "Paddle4" }, + { GamepadInputId.Touchpad, "Touchpad" }, + { GamepadInputId.SingleLeftTrigger0, "SingleLeftTrigger0" }, + { GamepadInputId.SingleRightTrigger0, "SingleRightTrigger0" }, + { GamepadInputId.SingleLeftTrigger1, "SingleLeftTrigger1" }, + { GamepadInputId.SingleRightTrigger1, "SingleRightTrigger1" }, + { GamepadInputId.Unbound, "Unbound" }, + }; + + private static readonly Dictionary _stickInputIdMap = new() + { + { StickInputId.Left, "StickLeft" }, + { StickInputId.Right, "StickRight" }, + { StickInputId.Unbound, "Unbound" }, + }; + + public static string ToString(Button button) + { + string keyString = ""; + + switch (button.Type) + { + case ButtonType.Key: + var key = button.AsHidType(); + + if (!_keysMap.TryGetValue(button.AsHidType(), out keyString)) + { + keyString = key.ToString(); + } + + break; + case ButtonType.GamepadButtonInputId: + var gamepadButton = button.AsHidType(); + + if (!_gamepadInputIdMap.TryGetValue(button.AsHidType(), out keyString)) + { + keyString = gamepadButton.ToString(); + } + + break; + case ButtonType.StickId: + var stickInput = button.AsHidType(); + + if (!_stickInputIdMap.TryGetValue(button.AsHidType(), out keyString)) + { + keyString = stickInput.ToString(); + } + + break; + } + + return keyString; + } + } +} diff --git a/src/Ryujinx.Gtk3/UI/Windows/AvatarWindow.cs b/src/Ryujinx.Gtk3/UI/Windows/AvatarWindow.cs index 7cddc362b..d9ecd47b7 100644 --- a/src/Ryujinx.Gtk3/UI/Windows/AvatarWindow.cs +++ b/src/Ryujinx.Gtk3/UI/Windows/AvatarWindow.cs @@ -233,7 +233,7 @@ namespace Ryujinx.UI.Windows reader.ReadInt64(); // Padding byte[] input = new byte[stream.Length - stream.Position]; - stream.Read(input, 0, input.Length); + stream.ReadExactly(input, 0, input.Length); long inputOffset = 0; diff --git a/src/Ryujinx.Gtk3/UI/Windows/ControllerWindow.cs b/src/Ryujinx.Gtk3/UI/Windows/ControllerWindow.cs index 6712657f8..d0b8266f4 100644 --- a/src/Ryujinx.Gtk3/UI/Windows/ControllerWindow.cs +++ b/src/Ryujinx.Gtk3/UI/Windows/ControllerWindow.cs @@ -10,6 +10,7 @@ using Ryujinx.Input; using Ryujinx.Input.Assigner; using Ryujinx.Input.GTK3; using Ryujinx.UI.Common.Configuration; +using Ryujinx.UI.Helper; using Ryujinx.UI.Widgets; using System; using System.Collections.Generic; @@ -17,6 +18,7 @@ using System.IO; using System.Reflection; using System.Text.Json; using System.Threading; +using Button = Ryujinx.Input.Button; using ConfigGamepadInputId = Ryujinx.Common.Configuration.Hid.Controller.GamepadInputId; using ConfigStickInputId = Ryujinx.Common.Configuration.Hid.Controller.StickInputId; using GUI = Gtk.Builder.ObjectAttribute; @@ -887,13 +889,13 @@ namespace Ryujinx.UI.Windows Thread.Sleep(10); assigner.ReadInput(); - if (_mousePressed || keyboard.IsPressed(Ryujinx.Input.Key.Escape) || assigner.HasAnyButtonPressed() || assigner.ShouldCancel()) + if (_mousePressed || keyboard.IsPressed(Ryujinx.Input.Key.Escape) || assigner.IsAnyButtonPressed() || assigner.ShouldCancel()) { break; } } - string pressedButton = assigner.GetPressedButton(); + string pressedButton = ButtonHelper.ToString(assigner.GetPressedButton() ?? new Button(Input.Key.Unknown)); Application.Invoke(delegate { diff --git a/src/Ryujinx.HLE/HOS/Kernel/Ipc/KServerSession.cs b/src/Ryujinx.HLE/HOS/Kernel/Ipc/KServerSession.cs index 7e41a3f3a..3b4280855 100644 --- a/src/Ryujinx.HLE/HOS/Kernel/Ipc/KServerSession.cs +++ b/src/Ryujinx.HLE/HOS/Kernel/Ipc/KServerSession.cs @@ -570,7 +570,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Ipc } else { - serverProcess.CpuMemory.Write(copyDst, clientProcess.CpuMemory.GetSpan(copySrc, (int)copySize)); + serverProcess.CpuMemory.Write(copyDst, clientProcess.CpuMemory.GetReadOnlySequence(copySrc, (int)copySize)); } if (clientResult != Result.Success) @@ -858,7 +858,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Ipc } else { - clientProcess.CpuMemory.Write(copyDst, serverProcess.CpuMemory.GetSpan(copySrc, (int)copySize)); + clientProcess.CpuMemory.Write(copyDst, serverProcess.CpuMemory.GetReadOnlySequence(copySrc, (int)copySize)); } } diff --git a/src/Ryujinx.HLE/HOS/Kernel/Memory/KPageTable.cs b/src/Ryujinx.HLE/HOS/Kernel/Memory/KPageTable.cs index d262c159d..4ffa447dd 100644 --- a/src/Ryujinx.HLE/HOS/Kernel/Memory/KPageTable.cs +++ b/src/Ryujinx.HLE/HOS/Kernel/Memory/KPageTable.cs @@ -2,6 +2,7 @@ using Ryujinx.Horizon.Common; using Ryujinx.Memory; using Ryujinx.Memory.Range; using System; +using System.Buffers; using System.Collections.Generic; using System.Diagnostics; @@ -34,6 +35,12 @@ namespace Ryujinx.HLE.HOS.Kernel.Memory } } + /// + protected override ReadOnlySequence GetReadOnlySequence(ulong va, int size) + { + return _cpuMemory.GetReadOnlySequence(va, size); + } + /// protected override ReadOnlySpan GetSpan(ulong va, int size) { @@ -247,6 +254,12 @@ namespace Ryujinx.HLE.HOS.Kernel.Memory _cpuMemory.SignalMemoryTracking(va, size, write); } + /// + protected override void Write(ulong va, ReadOnlySequence data) + { + _cpuMemory.Write(va, data); + } + /// protected override void Write(ulong va, ReadOnlySpan data) { diff --git a/src/Ryujinx.HLE/HOS/Kernel/Memory/KPageTableBase.cs b/src/Ryujinx.HLE/HOS/Kernel/Memory/KPageTableBase.cs index ae99a434a..58bbc0dbf 100644 --- a/src/Ryujinx.HLE/HOS/Kernel/Memory/KPageTableBase.cs +++ b/src/Ryujinx.HLE/HOS/Kernel/Memory/KPageTableBase.cs @@ -5,6 +5,7 @@ using Ryujinx.Horizon.Common; using Ryujinx.Memory; using Ryujinx.Memory.Range; using System; +using System.Buffers; using System.Collections.Generic; using System.Diagnostics; @@ -1568,7 +1569,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Memory while (size > 0) { - ulong copySize = 0x100000; // Copy chunck size. Any value will do, moderate sizes are recommended. + ulong copySize = int.MaxValue; if (copySize > size) { @@ -1577,11 +1578,11 @@ namespace Ryujinx.HLE.HOS.Kernel.Memory if (toServer) { - currentProcess.CpuMemory.Write(serverAddress, GetSpan(clientAddress, (int)copySize)); + currentProcess.CpuMemory.Write(serverAddress, GetReadOnlySequence(clientAddress, (int)copySize)); } else { - Write(clientAddress, currentProcess.CpuMemory.GetSpan(serverAddress, (int)copySize)); + Write(clientAddress, currentProcess.CpuMemory.GetReadOnlySequence(serverAddress, (int)copySize)); } serverAddress += copySize; @@ -1911,9 +1912,9 @@ namespace Ryujinx.HLE.HOS.Kernel.Memory Context.Memory.Fill(GetDramAddressFromPa(dstFirstPagePa), unusedSizeBefore, (byte)_ipcFillValue); ulong copySize = addressRounded <= endAddr ? addressRounded - address : size; - var data = srcPageTable.GetSpan(addressTruncated + unusedSizeBefore, (int)copySize); + var data = srcPageTable.GetReadOnlySequence(addressTruncated + unusedSizeBefore, (int)copySize); - Context.Memory.Write(GetDramAddressFromPa(dstFirstPagePa + unusedSizeBefore), data); + ((IWritableBlock)Context.Memory).Write(GetDramAddressFromPa(dstFirstPagePa + unusedSizeBefore), data); firstPageFillAddress += unusedSizeBefore + copySize; @@ -1977,9 +1978,9 @@ namespace Ryujinx.HLE.HOS.Kernel.Memory if (send) { ulong copySize = endAddr - endAddrTruncated; - var data = srcPageTable.GetSpan(endAddrTruncated, (int)copySize); + var data = srcPageTable.GetReadOnlySequence(endAddrTruncated, (int)copySize); - Context.Memory.Write(GetDramAddressFromPa(dstLastPagePa), data); + ((IWritableBlock)Context.Memory).Write(GetDramAddressFromPa(dstLastPagePa), data); lastPageFillAddr += copySize; @@ -2943,6 +2944,18 @@ namespace Ryujinx.HLE.HOS.Kernel.Memory /// Page list where the ranges will be added protected abstract void GetPhysicalRegions(ulong va, ulong size, KPageList pageList); + /// + /// Gets a read-only sequence of data from CPU mapped memory. + /// + /// + /// Allows reading non-contiguous memory without first copying it to a newly allocated single contiguous block. + /// + /// Virtual address of the data + /// Size of the data + /// A read-only sequence of the data + /// Throw for unhandled invalid or unmapped memory accesses + protected abstract ReadOnlySequence GetReadOnlySequence(ulong va, int size); + /// /// Gets a read-only span of data from CPU mapped memory. /// @@ -2952,7 +2965,6 @@ namespace Ryujinx.HLE.HOS.Kernel.Memory /// /// Virtual address of the data /// Size of the data - /// True if read tracking is triggered on the span /// A read-only span of the data /// Throw for unhandled invalid or unmapped memory accesses protected abstract ReadOnlySpan GetSpan(ulong va, int size); @@ -3060,6 +3072,14 @@ namespace Ryujinx.HLE.HOS.Kernel.Memory /// Size of the region protected abstract void SignalMemoryTracking(ulong va, ulong size, bool write); + /// + /// Writes data to CPU mapped memory, with write tracking. + /// + /// Virtual address to write the data into + /// Data to be written + /// Throw for unhandled invalid or unmapped memory accesses + protected abstract void Write(ulong va, ReadOnlySequence data); + /// /// Writes data to CPU mapped memory, with write tracking. /// diff --git a/src/Ryujinx.HLE/HOS/Kernel/Threading/KScheduler.cs b/src/Ryujinx.HLE/HOS/Kernel/Threading/KScheduler.cs index 905c61d66..8ef77902c 100644 --- a/src/Ryujinx.HLE/HOS/Kernel/Threading/KScheduler.cs +++ b/src/Ryujinx.HLE/HOS/Kernel/Threading/KScheduler.cs @@ -28,42 +28,25 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading private SchedulingState _state; - private AutoResetEvent _idleInterruptEvent; - private readonly object _idleInterruptEventLock; - private KThread _previousThread; private KThread _currentThread; - private readonly KThread _idleThread; + + private int _coreIdleLock; + private bool _idleSignalled = true; + private bool _idleActive = true; + private long _idleTimeRunning; public KThread PreviousThread => _previousThread; public KThread CurrentThread => _currentThread; public long LastContextSwitchTime { get; private set; } - public long TotalIdleTimeTicks => _idleThread.TotalTimeRunning; + public long TotalIdleTimeTicks => _idleTimeRunning; public KScheduler(KernelContext context, int coreId) { _context = context; _coreId = coreId; - _idleInterruptEvent = new AutoResetEvent(false); - _idleInterruptEventLock = new object(); - - KThread idleThread = CreateIdleThread(context, coreId); - - _currentThread = idleThread; - _idleThread = idleThread; - - idleThread.StartHostThread(); - idleThread.SchedulerWaitEvent.Set(); - } - - private KThread CreateIdleThread(KernelContext context, int cpuCore) - { - KThread idleThread = new(context); - - idleThread.Initialize(0UL, 0UL, 0UL, PrioritiesCount, cpuCore, null, ThreadType.Dummy, IdleThreadLoop); - - return idleThread; + _currentThread = null; } public static ulong SelectThreads(KernelContext context) @@ -237,39 +220,64 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading KThread threadToSignal = context.Schedulers[coreToSignal]._currentThread; // Request the thread running on that core to stop and reschedule, if we have one. - if (threadToSignal != context.Schedulers[coreToSignal]._idleThread) - { - threadToSignal.Context.RequestInterrupt(); - } + threadToSignal?.Context.RequestInterrupt(); // If the core is idle, ensure that the idle thread is awaken. - context.Schedulers[coreToSignal]._idleInterruptEvent.Set(); + context.Schedulers[coreToSignal].NotifyIdleThread(); scheduledCoresMask &= ~(1UL << coreToSignal); } } - private void IdleThreadLoop() + private void ActivateIdleThread() { - while (_context.Running) + while (Interlocked.CompareExchange(ref _coreIdleLock, 1, 0) != 0) + { + Thread.SpinWait(1); + } + + Thread.MemoryBarrier(); + + // Signals that idle thread is now active on this core. + _idleActive = true; + + TryLeaveIdle(); + + Interlocked.Exchange(ref _coreIdleLock, 0); + } + + private void NotifyIdleThread() + { + while (Interlocked.CompareExchange(ref _coreIdleLock, 1, 0) != 0) + { + Thread.SpinWait(1); + } + + Thread.MemoryBarrier(); + + // Signals that the idle core may be able to exit idle. + _idleSignalled = true; + + TryLeaveIdle(); + + Interlocked.Exchange(ref _coreIdleLock, 0); + } + + public void TryLeaveIdle() + { + if (_idleSignalled && _idleActive) { _state.NeedsScheduling = false; Thread.MemoryBarrier(); - KThread nextThread = PickNextThread(_state.SelectedThread); + KThread nextThread = PickNextThread(null, _state.SelectedThread); - if (_idleThread != nextThread) + if (nextThread != null) { - _idleThread.SchedulerWaitEvent.Reset(); - WaitHandle.SignalAndWait(nextThread.SchedulerWaitEvent, _idleThread.SchedulerWaitEvent); + _idleActive = false; + nextThread.SchedulerWaitEvent.Set(); } - _idleInterruptEvent.WaitOne(); - } - - lock (_idleInterruptEventLock) - { - _idleInterruptEvent.Dispose(); - _idleInterruptEvent = null; + _idleSignalled = false; } } @@ -292,20 +300,37 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading // Wake all the threads that might be waiting until this thread context is unlocked. for (int core = 0; core < CpuCoresCount; core++) { - _context.Schedulers[core]._idleInterruptEvent.Set(); + _context.Schedulers[core].NotifyIdleThread(); } - KThread nextThread = PickNextThread(selectedThread); + KThread nextThread = PickNextThread(KernelStatic.GetCurrentThread(), selectedThread); if (currentThread.Context.Running) { // Wait until this thread is scheduled again, and allow the next thread to run. - WaitHandle.SignalAndWait(nextThread.SchedulerWaitEvent, currentThread.SchedulerWaitEvent); + + if (nextThread == null) + { + ActivateIdleThread(); + currentThread.SchedulerWaitEvent.WaitOne(); + } + else + { + WaitHandle.SignalAndWait(nextThread.SchedulerWaitEvent, currentThread.SchedulerWaitEvent); + } } else { // Allow the next thread to run. - nextThread.SchedulerWaitEvent.Set(); + + if (nextThread == null) + { + ActivateIdleThread(); + } + else + { + nextThread.SchedulerWaitEvent.Set(); + } // We don't need to wait since the thread is exiting, however we need to // make sure this thread will never call the scheduler again, since it is @@ -319,7 +344,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading } } - private KThread PickNextThread(KThread selectedThread) + private KThread PickNextThread(KThread currentThread, KThread selectedThread) { while (true) { @@ -335,7 +360,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading // on the core, as the scheduled thread will handle the next switch. if (selectedThread.ThreadContext.Lock()) { - SwitchTo(selectedThread); + SwitchTo(currentThread, selectedThread); if (!_state.NeedsScheduling) { @@ -346,15 +371,15 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading } else { - return _idleThread; + return null; } } else { // The core is idle now, make sure that the idle thread can run // and switch the core when a thread is available. - SwitchTo(null); - return _idleThread; + SwitchTo(currentThread, null); + return null; } _state.NeedsScheduling = false; @@ -363,12 +388,9 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading } } - private void SwitchTo(KThread nextThread) + private void SwitchTo(KThread currentThread, KThread nextThread) { - KProcess currentProcess = KernelStatic.GetCurrentProcess(); - KThread currentThread = KernelStatic.GetCurrentThread(); - - nextThread ??= _idleThread; + KProcess currentProcess = currentThread?.Owner; if (currentThread != nextThread) { @@ -376,7 +398,14 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading long currentTicks = PerformanceCounter.ElapsedTicks; long ticksDelta = currentTicks - previousTicks; - currentThread.AddCpuTime(ticksDelta); + if (currentThread == null) + { + Interlocked.Add(ref _idleTimeRunning, ticksDelta); + } + else + { + currentThread.AddCpuTime(ticksDelta); + } currentProcess?.AddCpuTime(ticksDelta); @@ -386,13 +415,13 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading { _previousThread = !currentThread.TerminationRequested && currentThread.ActiveCore == _coreId ? currentThread : null; } - else if (currentThread == _idleThread) + else if (currentThread == null) { _previousThread = null; } } - if (nextThread.CurrentCore != _coreId) + if (nextThread != null && nextThread.CurrentCore != _coreId) { nextThread.CurrentCore = _coreId; } @@ -645,11 +674,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading public void Dispose() { - // Ensure that the idle thread is not blocked and can exit. - lock (_idleInterruptEventLock) - { - _idleInterruptEvent?.Set(); - } + // No resources to dispose for now. } } } diff --git a/src/Ryujinx.HLE/HOS/Kernel/Threading/KThread.cs b/src/Ryujinx.HLE/HOS/Kernel/Threading/KThread.cs index 12383fb8a..835bf5d40 100644 --- a/src/Ryujinx.HLE/HOS/Kernel/Threading/KThread.cs +++ b/src/Ryujinx.HLE/HOS/Kernel/Threading/KThread.cs @@ -143,9 +143,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading PreferredCore = cpuCore; AffinityMask |= 1UL << cpuCore; - SchedFlags = type == ThreadType.Dummy - ? ThreadSchedState.Running - : ThreadSchedState.None; + SchedFlags = ThreadSchedState.None; ActiveCore = cpuCore; ObjSyncResult = KernelResult.ThreadNotStarted; @@ -1055,6 +1053,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading // If the thread is not schedulable, we want to just run or pause // it directly as we don't care about priority or the core it is // running on in this case. + if (SchedFlags == ThreadSchedState.Running) { _schedulerWaitEvent.Set(); diff --git a/src/Ryujinx.HLE/HOS/Kernel/Threading/ThreadType.cs b/src/Ryujinx.HLE/HOS/Kernel/Threading/ThreadType.cs index 83093570b..e2dfd2ffb 100644 --- a/src/Ryujinx.HLE/HOS/Kernel/Threading/ThreadType.cs +++ b/src/Ryujinx.HLE/HOS/Kernel/Threading/ThreadType.cs @@ -2,7 +2,6 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading { enum ThreadType { - Dummy, Kernel, Kernel2, User, diff --git a/src/Ryujinx.HLE/HOS/Services/Fatal/IService.cs b/src/Ryujinx.HLE/HOS/Services/Fatal/IService.cs index 21daf8758..155077745 100644 --- a/src/Ryujinx.HLE/HOS/Services/Fatal/IService.cs +++ b/src/Ryujinx.HLE/HOS/Services/Fatal/IService.cs @@ -60,7 +60,7 @@ namespace Ryujinx.HLE.HOS.Services.Fatal errorReport.AppendLine($"\tResultCode: {((int)resultCode & 0x1FF) + 2000}-{((int)resultCode >> 9) & 0x3FFF:d4}"); errorReport.AppendLine($"\tFatalPolicy: {fatalPolicy}"); - if (cpuContext != null) + if (!cpuContext.IsEmpty) { errorReport.AppendLine("CPU Context:"); diff --git a/src/Ryujinx.HLE/HOS/Services/Hid/HidDevices/MouseDevice.cs b/src/Ryujinx.HLE/HOS/Services/Hid/HidDevices/MouseDevice.cs index b2dd3feaf..2e62d206b 100644 --- a/src/Ryujinx.HLE/HOS/Services/Hid/HidDevices/MouseDevice.cs +++ b/src/Ryujinx.HLE/HOS/Services/Hid/HidDevices/MouseDevice.cs @@ -23,8 +23,8 @@ namespace Ryujinx.HLE.HOS.Services.Hid newState.Buttons = (MouseButton)buttons; newState.X = mouseX; newState.Y = mouseY; - newState.DeltaX = mouseX - previousEntry.DeltaX; - newState.DeltaY = mouseY - previousEntry.DeltaY; + newState.DeltaX = mouseX - previousEntry.X; + newState.DeltaY = mouseY - previousEntry.Y; newState.WheelDeltaX = scrollX; newState.WheelDeltaY = scrollY; newState.Attributes = connected ? MouseAttribute.IsConnected : MouseAttribute.None; diff --git a/src/Ryujinx.HLE/HOS/Services/Hid/IHidServer.cs b/src/Ryujinx.HLE/HOS/Services/Hid/IHidServer.cs index bcc87f53b..e3f505f37 100644 --- a/src/Ryujinx.HLE/HOS/Services/Hid/IHidServer.cs +++ b/src/Ryujinx.HLE/HOS/Services/Hid/IHidServer.cs @@ -22,6 +22,7 @@ namespace Ryujinx.HLE.HOS.Services.Hid private bool _sixAxisSensorFusionEnabled; private bool _unintendedHomeButtonInputProtectionEnabled; + private bool _npadAnalogStickCenterClampEnabled; private bool _vibrationPermitted; private bool _usbFullKeyControllerEnabled; private readonly bool _isFirmwareUpdateAvailableForSixAxisSensor; @@ -1107,6 +1108,19 @@ namespace Ryujinx.HLE.HOS.Services.Hid // If not, it returns nothing. } + [CommandCmif(134)] // 6.1.0+ + // SetNpadUseAnalogStickUseCenterClamp(bool Enable, nn::applet::AppletResourceUserId) + public ResultCode SetNpadUseAnalogStickUseCenterClamp(ServiceCtx context) + { + ulong pid = context.RequestData.ReadUInt64(); + _npadAnalogStickCenterClampEnabled = context.RequestData.ReadUInt32() != 0; + long appletResourceUserId = context.RequestData.ReadInt64(); + + Logger.Stub?.PrintStub(LogClass.ServiceHid, new { pid, appletResourceUserId, _npadAnalogStickCenterClampEnabled }); + + return ResultCode.Success; + } + [CommandCmif(200)] // GetVibrationDeviceInfo(nn::hid::VibrationDeviceHandle) -> nn::hid::VibrationDeviceInfo public ResultCode GetVibrationDeviceInfo(ServiceCtx context) diff --git a/src/Ryujinx.HLE/HOS/Services/SurfaceFlinger/IBinder.cs b/src/Ryujinx.HLE/HOS/Services/SurfaceFlinger/IBinder.cs index 0fb2dfd2e..54aac48ae 100644 --- a/src/Ryujinx.HLE/HOS/Services/SurfaceFlinger/IBinder.cs +++ b/src/Ryujinx.HLE/HOS/Services/SurfaceFlinger/IBinder.cs @@ -13,10 +13,10 @@ namespace Ryujinx.HLE.HOS.Services.SurfaceFlinger ResultCode OnTransact(uint code, uint flags, ReadOnlySpan inputParcel, Span outputParcel) { - Parcel inputParcelReader = new(inputParcel.ToArray()); + using Parcel inputParcelReader = new(inputParcel); // TODO: support objects? - Parcel outputParcelWriter = new((uint)(outputParcel.Length - Unsafe.SizeOf()), 0); + using Parcel outputParcelWriter = new((uint)(outputParcel.Length - Unsafe.SizeOf()), 0); string inputInterfaceToken = inputParcelReader.ReadInterfaceToken(); diff --git a/src/Ryujinx.HLE/HOS/Services/SurfaceFlinger/Parcel.cs b/src/Ryujinx.HLE/HOS/Services/SurfaceFlinger/Parcel.cs index 4ac0525ba..c6cd60d04 100644 --- a/src/Ryujinx.HLE/HOS/Services/SurfaceFlinger/Parcel.cs +++ b/src/Ryujinx.HLE/HOS/Services/SurfaceFlinger/Parcel.cs @@ -1,7 +1,9 @@ using Ryujinx.Common; +using Ryujinx.Common.Memory; using Ryujinx.Common.Utilities; using Ryujinx.HLE.HOS.Services.SurfaceFlinger.Types; using System; +using System.Buffers; using System.Diagnostics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -9,13 +11,13 @@ using System.Text; namespace Ryujinx.HLE.HOS.Services.SurfaceFlinger { - class Parcel + sealed class Parcel : IDisposable { - private readonly byte[] _rawData; + private readonly IMemoryOwner _rawDataOwner; - private Span Raw => new(_rawData); + private Span Raw => _rawDataOwner.Memory.Span; - private ref ParcelHeader Header => ref MemoryMarshal.Cast(_rawData)[0]; + private ref ParcelHeader Header => ref MemoryMarshal.Cast(Raw)[0]; private Span Payload => Raw.Slice((int)Header.PayloadOffset, (int)Header.PayloadSize); @@ -24,9 +26,11 @@ namespace Ryujinx.HLE.HOS.Services.SurfaceFlinger private int _payloadPosition; private int _objectPosition; - public Parcel(byte[] rawData) + private bool _isDisposed; + + public Parcel(ReadOnlySpan data) { - _rawData = rawData; + _rawDataOwner = ByteMemoryPool.RentCopy(data); _payloadPosition = 0; _objectPosition = 0; @@ -36,7 +40,7 @@ namespace Ryujinx.HLE.HOS.Services.SurfaceFlinger { uint headerSize = (uint)Unsafe.SizeOf(); - _rawData = new byte[BitUtils.AlignUp(headerSize + payloadSize + objectsSize, 4)]; + _rawDataOwner = ByteMemoryPool.RentCleared(BitUtils.AlignUp(headerSize + payloadSize + objectsSize, 4)); Header.PayloadSize = payloadSize; Header.ObjectsSize = objectsSize; @@ -132,7 +136,9 @@ namespace Ryujinx.HLE.HOS.Services.SurfaceFlinger // TODO: figure out what this value is - WriteInplaceObject(new byte[4] { 0, 0, 0, 0 }); + Span fourBytes = stackalloc byte[4]; + + WriteInplaceObject(fourBytes); } public AndroidStrongPointer ReadStrongPointer() where T : unmanaged, IFlattenable @@ -219,5 +225,15 @@ namespace Ryujinx.HLE.HOS.Services.SurfaceFlinger return Raw[..(int)(Header.PayloadSize + Header.ObjectsSize + Unsafe.SizeOf())]; } + + public void Dispose() + { + if (!_isDisposed) + { + _isDisposed = true; + + _rawDataOwner.Dispose(); + } + } } } diff --git a/src/Ryujinx.HLE/HOS/Services/Vi/RootService/IApplicationDisplayService.cs b/src/Ryujinx.HLE/HOS/Services/Vi/RootService/IApplicationDisplayService.cs index b6988f08d..a2b1fb524 100644 --- a/src/Ryujinx.HLE/HOS/Services/Vi/RootService/IApplicationDisplayService.cs +++ b/src/Ryujinx.HLE/HOS/Services/Vi/RootService/IApplicationDisplayService.cs @@ -250,7 +250,7 @@ namespace Ryujinx.HLE.HOS.Services.Vi.RootService context.Device.System.SurfaceFlinger.SetRenderLayer(layerId); - Parcel parcel = new(0x28, 0x4); + using Parcel parcel = new(0x28, 0x4); parcel.WriteObject(producer, "dispdrv\0"); @@ -288,7 +288,7 @@ namespace Ryujinx.HLE.HOS.Services.Vi.RootService context.Device.System.SurfaceFlinger.SetRenderLayer(layerId); - Parcel parcel = new(0x28, 0x4); + using Parcel parcel = new(0x28, 0x4); parcel.WriteObject(producer, "dispdrv\0"); diff --git a/src/Ryujinx.HLE/HOS/TamperMachine.cs b/src/Ryujinx.HLE/HOS/TamperMachine.cs index f234e540e..609221535 100644 --- a/src/Ryujinx.HLE/HOS/TamperMachine.cs +++ b/src/Ryujinx.HLE/HOS/TamperMachine.cs @@ -143,7 +143,7 @@ namespace Ryujinx.HLE.HOS try { - ControllerKeys pressedKeys = (ControllerKeys)Thread.VolatileRead(ref _pressedKeys); + ControllerKeys pressedKeys = (ControllerKeys)Volatile.Read(ref _pressedKeys); program.Process.TamperedCodeMemory = false; program.Execute(pressedKeys); @@ -175,14 +175,14 @@ namespace Ryujinx.HLE.HOS { if (input.PlayerId == PlayerIndex.Player1 || input.PlayerId == PlayerIndex.Handheld) { - Thread.VolatileWrite(ref _pressedKeys, (long)input.Buttons); + Volatile.Write(ref _pressedKeys, (long)input.Buttons); return; } } // Clear the input because player one is not conected. - Thread.VolatileWrite(ref _pressedKeys, 0); + Volatile.Write(ref _pressedKeys, 0); } } } diff --git a/src/Ryujinx.Headless.SDL2/Ryujinx.Headless.SDL2.csproj b/src/Ryujinx.Headless.SDL2/Ryujinx.Headless.SDL2.csproj index cc5a36518..610229544 100644 --- a/src/Ryujinx.Headless.SDL2/Ryujinx.Headless.SDL2.csproj +++ b/src/Ryujinx.Headless.SDL2/Ryujinx.Headless.SDL2.csproj @@ -48,7 +48,7 @@ - + Always diff --git a/src/Ryujinx.Horizon/Ryujinx.Horizon.csproj b/src/Ryujinx.Horizon/Ryujinx.Horizon.csproj index d1f572d5c..bf34ddd17 100644 --- a/src/Ryujinx.Horizon/Ryujinx.Horizon.csproj +++ b/src/Ryujinx.Horizon/Ryujinx.Horizon.csproj @@ -1,4 +1,4 @@ - + net8.0 @@ -16,10 +16,4 @@ - - - - NU1605 - - diff --git a/src/Ryujinx.Horizon/Sdk/Codec/Detail/HardwareOpusDecoder.cs b/src/Ryujinx.Horizon/Sdk/Codec/Detail/HardwareOpusDecoder.cs index 5d2798582..2146362df 100644 --- a/src/Ryujinx.Horizon/Sdk/Codec/Detail/HardwareOpusDecoder.cs +++ b/src/Ryujinx.Horizon/Sdk/Codec/Detail/HardwareOpusDecoder.cs @@ -14,6 +14,11 @@ namespace Ryujinx.Horizon.Sdk.Codec.Detail { partial class HardwareOpusDecoder : IHardwareOpusDecoder, IDisposable { + static HardwareOpusDecoder() + { + OpusCodecFactory.AttemptToUseNativeLibrary = false; + } + [StructLayout(LayoutKind.Sequential)] private struct OpusPacketHeader { @@ -30,60 +35,87 @@ namespace Ryujinx.Horizon.Sdk.Codec.Detail } } - private interface IDecoder + private interface IDecoder : IDisposable { int SampleRate { get; } int ChannelsCount { get; } - int Decode(byte[] inData, int inDataOffset, int len, short[] outPcm, int outPcmOffset, int frameSize); + int Decode(ReadOnlySpan inData, Span outPcm, int frameSize); void ResetState(); } private class Decoder : IDecoder { - private readonly OpusDecoder _decoder; + private readonly IOpusDecoder _decoder; public int SampleRate => _decoder.SampleRate; public int ChannelsCount => _decoder.NumChannels; public Decoder(int sampleRate, int channelsCount) { - _decoder = new OpusDecoder(sampleRate, channelsCount); + _decoder = OpusCodecFactory.CreateDecoder(sampleRate, channelsCount); } - public int Decode(byte[] inData, int inDataOffset, int len, short[] outPcm, int outPcmOffset, int frameSize) + public int Decode(ReadOnlySpan inData, Span outPcm, int frameSize) { - return _decoder.Decode(inData, inDataOffset, len, outPcm, outPcmOffset, frameSize); + return _decoder.Decode(inData, outPcm, frameSize); } public void ResetState() { _decoder.ResetState(); } + + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + _decoder?.Dispose(); + } + } } private class MultiSampleDecoder : IDecoder { - private readonly OpusMSDecoder _decoder; + private readonly IOpusMultiStreamDecoder _decoder; public int SampleRate => _decoder.SampleRate; - public int ChannelsCount { get; } + public int ChannelsCount => _decoder.NumChannels; public MultiSampleDecoder(int sampleRate, int channelsCount, int streams, int coupledStreams, byte[] mapping) { - ChannelsCount = channelsCount; - _decoder = new OpusMSDecoder(sampleRate, channelsCount, streams, coupledStreams, mapping); + _decoder = OpusCodecFactory.CreateMultiStreamDecoder(sampleRate, channelsCount, streams, coupledStreams, mapping); } - public int Decode(byte[] inData, int inDataOffset, int len, short[] outPcm, int outPcmOffset, int frameSize) + public int Decode(ReadOnlySpan inData, Span outPcm, int frameSize) { - return _decoder.DecodeMultistream(inData, inDataOffset, len, outPcm, outPcmOffset, frameSize, 0); + return _decoder.DecodeMultistream(inData, outPcm, frameSize, false); } public void ResetState() { _decoder.ResetState(); } + + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + _decoder?.Dispose(); + } + } } private readonly IDecoder _decoder; @@ -221,7 +253,8 @@ namespace Ryujinx.Horizon.Sdk.Codec.Detail { timeTaken = 0; - Result result = DecodeInterleaved(_decoder, reset, input, out short[] outPcmData, output.Length, out outConsumed, out outSamples); + Span outPcmSpace = MemoryMarshal.Cast(output); + Result result = DecodeInterleaved(_decoder, reset, input, outPcmSpace, output.Length, out outConsumed, out outSamples); if (withPerf) { @@ -229,14 +262,12 @@ namespace Ryujinx.Horizon.Sdk.Codec.Detail timeTaken = 0; } - MemoryMarshal.Cast(outPcmData).CopyTo(output[..(outPcmData.Length * sizeof(short))]); - return result; } - private static Result GetPacketNumSamples(IDecoder decoder, out int numSamples, byte[] packet) + private static Result GetPacketNumSamples(IDecoder decoder, out int numSamples, ReadOnlySpan packet) { - int result = OpusPacketInfo.GetNumSamples(packet, 0, packet.Length, decoder.SampleRate); + int result = OpusPacketInfo.GetNumSamples(packet, decoder.SampleRate); numSamples = result; @@ -256,12 +287,11 @@ namespace Ryujinx.Horizon.Sdk.Codec.Detail IDecoder decoder, bool reset, ReadOnlySpan input, - out short[] outPcmData, + Span outPcmData, int outputSize, out int outConsumed, out int outSamples) { - outPcmData = null; outConsumed = 0; outSamples = 0; @@ -281,7 +311,7 @@ namespace Ryujinx.Horizon.Sdk.Codec.Detail return CodecResult.InvalidLength; } - byte[] opusData = input.Slice(headerSize, (int)header.Length).ToArray(); + ReadOnlySpan opusData = input.Slice(headerSize, (int)header.Length); Result result = GetPacketNumSamples(decoder, out int numSamples, opusData); @@ -292,8 +322,6 @@ namespace Ryujinx.Horizon.Sdk.Codec.Detail return CodecResult.InvalidLength; } - outPcmData = new short[numSamples * decoder.ChannelsCount]; - if (reset) { decoder.ResetState(); @@ -301,13 +329,22 @@ namespace Ryujinx.Horizon.Sdk.Codec.Detail try { - outSamples = decoder.Decode(opusData, 0, opusData.Length, outPcmData, 0, outPcmData.Length / decoder.ChannelsCount); + outSamples = decoder.Decode(opusData, outPcmData, numSamples); outConsumed = (int)totalSize; } - catch (OpusException) + catch (OpusException e) { - // TODO: As OpusException doesn't return the exact error code, this is inaccurate in some cases... - return CodecResult.InvalidLength; + switch (e.OpusErrorCode) + { + case OpusError.OPUS_BUFFER_TOO_SMALL: + return CodecResult.InvalidLength; + case OpusError.OPUS_BAD_ARG: + return CodecResult.OpusBadArg; + case OpusError.OPUS_INVALID_PACKET: + return CodecResult.OpusInvalidPacket; + default: + return CodecResult.InvalidLength; + } } } @@ -324,6 +361,8 @@ namespace Ryujinx.Horizon.Sdk.Codec.Detail _workBufferHandle = 0; } + + _decoder?.Dispose(); } } diff --git a/src/Ryujinx.Input/Assigner/GamepadButtonAssigner.cs b/src/Ryujinx.Input/Assigner/GamepadButtonAssigner.cs index 388ebcc07..80fed2b82 100644 --- a/src/Ryujinx.Input/Assigner/GamepadButtonAssigner.cs +++ b/src/Ryujinx.Input/Assigner/GamepadButtonAssigner.cs @@ -49,9 +49,9 @@ namespace Ryujinx.Input.Assigner CollectButtonStats(); } - public bool HasAnyButtonPressed() + public bool IsAnyButtonPressed() { - return _detector.HasAnyButtonPressed(); + return _detector.IsAnyButtonPressed(); } public bool ShouldCancel() @@ -59,16 +59,11 @@ namespace Ryujinx.Input.Assigner return _gamepad == null || !_gamepad.IsConnected; } - public string GetPressedButton() + public Button? GetPressedButton() { IEnumerable pressedButtons = _detector.GetPressedButtons(); - if (pressedButtons.Any()) - { - return !_forStick ? pressedButtons.First().ToString() : ((StickInputId)pressedButtons.First()).ToString(); - } - - return ""; + return !_forStick ? new(pressedButtons.FirstOrDefault()) : new((StickInputId)pressedButtons.FirstOrDefault()); } private void CollectButtonStats() @@ -123,7 +118,7 @@ namespace Ryujinx.Input.Assigner _stats = new Dictionary(); } - public bool HasAnyButtonPressed() + public bool IsAnyButtonPressed() { return _stats.Values.Any(CheckButtonPressed); } diff --git a/src/Ryujinx.Input/Assigner/IButtonAssigner.cs b/src/Ryujinx.Input/Assigner/IButtonAssigner.cs index 76a9fece4..688fbddb4 100644 --- a/src/Ryujinx.Input/Assigner/IButtonAssigner.cs +++ b/src/Ryujinx.Input/Assigner/IButtonAssigner.cs @@ -19,7 +19,7 @@ namespace Ryujinx.Input.Assigner /// Check if a button was pressed. /// /// True if a button was pressed - bool HasAnyButtonPressed(); + bool IsAnyButtonPressed(); /// /// Indicate if the user of this API should cancel operations. This is triggered for example when a gamepad get disconnected or when a user cancel assignation operations. @@ -31,6 +31,6 @@ namespace Ryujinx.Input.Assigner /// Get the pressed button that was read in by the button assigner. /// /// The pressed button that was read - string GetPressedButton(); + Button? GetPressedButton(); } } diff --git a/src/Ryujinx.Input/Assigner/KeyboardKeyAssigner.cs b/src/Ryujinx.Input/Assigner/KeyboardKeyAssigner.cs index e52ef4a2c..3c011a63b 100644 --- a/src/Ryujinx.Input/Assigner/KeyboardKeyAssigner.cs +++ b/src/Ryujinx.Input/Assigner/KeyboardKeyAssigner.cs @@ -21,9 +21,9 @@ namespace Ryujinx.Input.Assigner _keyboardState = _keyboard.GetKeyboardStateSnapshot(); } - public bool HasAnyButtonPressed() + public bool IsAnyButtonPressed() { - return GetPressedButton().Length != 0; + return GetPressedButton() is not null; } public bool ShouldCancel() @@ -31,20 +31,20 @@ namespace Ryujinx.Input.Assigner return _keyboardState.IsPressed(Key.Escape); } - public string GetPressedButton() + public Button? GetPressedButton() { - string keyPressed = ""; + Button? keyPressed = null; for (Key key = Key.Unknown; key < Key.Count; key++) { if (_keyboardState.IsPressed(key)) { - keyPressed = key.ToString(); + keyPressed = new(key); break; } } - return !ShouldCancel() ? keyPressed : ""; + return !ShouldCancel() ? keyPressed : null; } } } diff --git a/src/Ryujinx.Input/Button.cs b/src/Ryujinx.Input/Button.cs new file mode 100644 index 000000000..4289901ce --- /dev/null +++ b/src/Ryujinx.Input/Button.cs @@ -0,0 +1,33 @@ +using System; + +namespace Ryujinx.Input +{ + public readonly struct Button + { + public readonly ButtonType Type; + private readonly uint _rawValue; + + public Button(Key key) + { + Type = ButtonType.Key; + _rawValue = (uint)key; + } + + public Button(GamepadButtonInputId gamepad) + { + Type = ButtonType.GamepadButtonInputId; + _rawValue = (uint)gamepad; + } + + public Button(StickInputId stick) + { + Type = ButtonType.StickId; + _rawValue = (uint)stick; + } + + public T AsHidType() where T : Enum + { + return (T)Enum.ToObject(typeof(T), _rawValue); + } + } +} diff --git a/src/Ryujinx.Input/ButtonType.cs b/src/Ryujinx.Input/ButtonType.cs new file mode 100644 index 000000000..25ef5eea8 --- /dev/null +++ b/src/Ryujinx.Input/ButtonType.cs @@ -0,0 +1,9 @@ +namespace Ryujinx.Input +{ + public enum ButtonType + { + Key, + GamepadButtonInputId, + StickId, + } +} diff --git a/src/Ryujinx.Input/HLE/NpadController.cs b/src/Ryujinx.Input/HLE/NpadController.cs index 8411c10a7..380745283 100644 --- a/src/Ryujinx.Input/HLE/NpadController.cs +++ b/src/Ryujinx.Input/HLE/NpadController.cs @@ -203,8 +203,6 @@ namespace Ryujinx.Input.HLE new(Key.NumLock, 10), }; - private bool _isValid; - private MotionInput _leftMotionInput; private MotionInput _rightMotionInput; @@ -222,7 +220,6 @@ namespace Ryujinx.Input.HLE { State = default; Id = null; - _isValid = false; _cemuHookClient = cemuHookClient; } @@ -234,11 +231,10 @@ namespace Ryujinx.Input.HLE Id = config.Id; _gamepad = GamepadDriver.GetGamepad(Id); - _isValid = _gamepad != null; UpdateUserConfiguration(config); - return _isValid; + return _gamepad != null; } public void UpdateUserConfiguration(InputConfig config) @@ -262,10 +258,7 @@ namespace Ryujinx.Input.HLE _config = config; - if (_isValid) - { - _gamepad.SetConfiguration(config); - } + _gamepad?.SetConfiguration(config); } private void UpdateMotionInput(MotionConfigController motionConfig) @@ -282,18 +275,21 @@ namespace Ryujinx.Input.HLE public void Update() { - if (_isValid && GamepadDriver != null) + // _gamepad may be altered by other threads + var gamepad = _gamepad; + + if (gamepad != null && GamepadDriver != null) { - State = _gamepad.GetMappedStateSnapshot(); + State = gamepad.GetMappedStateSnapshot(); if (_config is StandardControllerInputConfig controllerConfig && controllerConfig.Motion.EnableMotion) { if (controllerConfig.Motion.MotionBackend == MotionInputBackendType.GamepadDriver) { - if (_gamepad.Features.HasFlag(GamepadFeaturesFlag.Motion)) + if (gamepad.Features.HasFlag(GamepadFeaturesFlag.Motion)) { - Vector3 accelerometer = _gamepad.GetMotionData(MotionInputId.Accelerometer); - Vector3 gyroscope = _gamepad.GetMotionData(MotionInputId.Gyroscope); + Vector3 accelerometer = gamepad.GetMotionData(MotionInputId.Accelerometer); + Vector3 gyroscope = gamepad.GetMotionData(MotionInputId.Gyroscope); accelerometer = new Vector3(accelerometer.X, -accelerometer.Z, accelerometer.Y); gyroscope = new Vector3(gyroscope.X, -gyroscope.Z, gyroscope.Y); @@ -491,38 +487,35 @@ namespace Ryujinx.Input.HLE return value; } - public KeyboardInput? GetHLEKeyboardInput() + public static KeyboardInput GetHLEKeyboardInput(IGamepadDriver KeyboardDriver) { - if (_gamepad is IKeyboard keyboard) + var keyboard = KeyboardDriver.GetGamepad("0") as IKeyboard; + + KeyboardStateSnapshot keyboardState = keyboard.GetKeyboardStateSnapshot(); + + KeyboardInput hidKeyboard = new() { - KeyboardStateSnapshot keyboardState = keyboard.GetKeyboardStateSnapshot(); + Modifier = 0, + Keys = new ulong[0x4], + }; - KeyboardInput hidKeyboard = new() - { - Modifier = 0, - Keys = new ulong[0x4], - }; + foreach (HLEKeyboardMappingEntry entry in _keyMapping) + { + ulong value = keyboardState.IsPressed(entry.TargetKey) ? 1UL : 0UL; - foreach (HLEKeyboardMappingEntry entry in _keyMapping) - { - ulong value = keyboardState.IsPressed(entry.TargetKey) ? 1UL : 0UL; - - hidKeyboard.Keys[entry.Target / 0x40] |= (value << (entry.Target % 0x40)); - } - - foreach (HLEKeyboardMappingEntry entry in _keyModifierMapping) - { - int value = keyboardState.IsPressed(entry.TargetKey) ? 1 : 0; - - hidKeyboard.Modifier |= value << entry.Target; - } - - return hidKeyboard; + hidKeyboard.Keys[entry.Target / 0x40] |= (value << (entry.Target % 0x40)); } - return null; - } + foreach (HLEKeyboardMappingEntry entry in _keyModifierMapping) + { + int value = keyboardState.IsPressed(entry.TargetKey) ? 1 : 0; + hidKeyboard.Modifier |= value << entry.Target; + } + + return hidKeyboard; + + } protected virtual void Dispose(bool disposing) { diff --git a/src/Ryujinx.Input/HLE/NpadManager.cs b/src/Ryujinx.Input/HLE/NpadManager.cs index 5ae73bda1..1dc87358d 100644 --- a/src/Ryujinx.Input/HLE/NpadManager.cs +++ b/src/Ryujinx.Input/HLE/NpadManager.cs @@ -174,6 +174,11 @@ namespace Ryujinx.Input.HLE { lock (_lock) { + foreach (InputConfig inputConfig in _inputConfig) + { + _controllers[(int)inputConfig.PlayerIndex]?.GamepadDriver?.Clear(); + } + _blockInputUpdates = false; } } @@ -231,11 +236,6 @@ namespace Ryujinx.Input.HLE var altMotionState = isJoyconPair ? controller.GetHLEMotionState(true) : default; motionState = (controller.GetHLEMotionState(), altMotionState); - - if (_enableKeyboard) - { - hleKeyboardInput = controller.GetHLEKeyboardInput(); - } } else { @@ -257,6 +257,11 @@ namespace Ryujinx.Input.HLE } } + if (!_blockInputUpdates && _enableKeyboard) + { + hleKeyboardInput = NpadController.GetHLEKeyboardInput(_keyboardDriver); + } + _device.Hid.Npads.Update(hleInputStates); _device.Hid.Npads.UpdateSixAxis(hleMotionStates); diff --git a/src/Ryujinx.Input/IGamepadDriver.cs b/src/Ryujinx.Input/IGamepadDriver.cs index 67b01c26c..625c3e694 100644 --- a/src/Ryujinx.Input/IGamepadDriver.cs +++ b/src/Ryujinx.Input/IGamepadDriver.cs @@ -33,5 +33,11 @@ namespace Ryujinx.Input /// The unique id of the gamepad /// An instance of associated to the gamepad id given or null if not found IGamepad GetGamepad(string id); + + /// + /// Clear the internal state of the driver. + /// + /// Does nothing by default. + void Clear() { } } } diff --git a/src/Ryujinx.Memory/IWritableBlock.cs b/src/Ryujinx.Memory/IWritableBlock.cs index 0858e0c96..78ae2479d 100644 --- a/src/Ryujinx.Memory/IWritableBlock.cs +++ b/src/Ryujinx.Memory/IWritableBlock.cs @@ -1,9 +1,25 @@ using System; +using System.Buffers; namespace Ryujinx.Memory { public interface IWritableBlock { + /// + /// Writes data to CPU mapped memory, with write tracking. + /// + /// Virtual address to write the data into + /// Data to be written + /// Throw for unhandled invalid or unmapped memory accesses + void Write(ulong va, ReadOnlySequence data) + { + foreach (ReadOnlyMemory segment in data) + { + Write(va, segment.Span); + va += (ulong)segment.Length; + } + } + void Write(ulong va, ReadOnlySpan data); void WriteUntracked(ulong va, ReadOnlySpan data) => Write(va, data); diff --git a/src/Ryujinx.Memory/NativeMemoryManager.cs b/src/Ryujinx.Memory/NativeMemoryManager.cs index 9ca632938..cb8d5c243 100644 --- a/src/Ryujinx.Memory/NativeMemoryManager.cs +++ b/src/Ryujinx.Memory/NativeMemoryManager.cs @@ -8,6 +8,11 @@ namespace Ryujinx.Memory private readonly T* _pointer; private readonly int _length; + public NativeMemoryManager(nuint pointer, int length) + : this((T*)pointer, length) + { + } + public NativeMemoryManager(T* pointer, int length) { _pointer = pointer; diff --git a/src/Ryujinx.ShaderTools/Program.cs b/src/Ryujinx.ShaderTools/Program.cs index 4252f1b25..a84d7b466 100644 --- a/src/Ryujinx.ShaderTools/Program.cs +++ b/src/Ryujinx.ShaderTools/Program.cs @@ -25,32 +25,32 @@ namespace Ryujinx.ShaderTools _imagesCount = 0; } - public int CreateConstantBufferBinding(int index) + public SetBindingPair CreateConstantBufferBinding(int index) { - return index + 1; + return new SetBindingPair(0, index + 1); } - public int CreateImageBinding(int count, bool isBuffer) + public SetBindingPair CreateImageBinding(int count, bool isBuffer) { int binding = _imagesCount; _imagesCount += count; - return binding; + return new SetBindingPair(3, binding); } - public int CreateStorageBufferBinding(int index) + public SetBindingPair CreateStorageBufferBinding(int index) { - return index; + return new SetBindingPair(1, index); } - public int CreateTextureBinding(int count, bool isBuffer) + public SetBindingPair CreateTextureBinding(int count, bool isBuffer) { int binding = _texturesCount; _texturesCount += count; - return binding; + return new SetBindingPair(2, binding); } public ReadOnlySpan GetCode(ulong address, int minimumSize) @@ -58,10 +58,20 @@ namespace Ryujinx.ShaderTools return MemoryMarshal.Cast(new ReadOnlySpan(_data)[(int)address..]); } + public int QuerySamplerArrayLengthFromPool() + { + return DefaultArrayLength; + } + public int QueryTextureArrayLengthFromBuffer(int slot) { return DefaultArrayLength; } + + public int QueryTextureArrayLengthFromPool() + { + return DefaultArrayLength; + } } private class Options diff --git a/src/Ryujinx.Tests/Audio/Renderer/Server/BehaviourContextTests.cs b/src/Ryujinx.Tests/Audio/Renderer/Server/BehaviourContextTests.cs index 557581881..3e48a5b4e 100644 --- a/src/Ryujinx.Tests/Audio/Renderer/Server/BehaviourContextTests.cs +++ b/src/Ryujinx.Tests/Audio/Renderer/Server/BehaviourContextTests.cs @@ -52,7 +52,9 @@ namespace Ryujinx.Tests.Audio.Renderer.Server Assert.IsFalse(behaviourContext.IsMixInParameterDirtyOnlyUpdateSupported()); Assert.IsFalse(behaviourContext.IsWaveBufferVersion2Supported()); Assert.IsFalse(behaviourContext.IsEffectInfoVersion2Supported()); - Assert.IsFalse(behaviourContext.IsBiquadFilterGroupedOptimizationSupported()); + Assert.IsFalse(behaviourContext.UseMultiTapBiquadFilterProcessing()); + Assert.IsFalse(behaviourContext.IsNewEffectChannelMappingSupported()); + Assert.IsFalse(behaviourContext.IsBiquadFilterParameterForSplitterEnabled()); Assert.AreEqual(0.70f, behaviourContext.GetAudioRendererProcessingTimeLimit()); Assert.AreEqual(1, behaviourContext.GetCommandProcessingTimeEstimatorVersion()); @@ -78,7 +80,9 @@ namespace Ryujinx.Tests.Audio.Renderer.Server Assert.IsFalse(behaviourContext.IsMixInParameterDirtyOnlyUpdateSupported()); Assert.IsFalse(behaviourContext.IsWaveBufferVersion2Supported()); Assert.IsFalse(behaviourContext.IsEffectInfoVersion2Supported()); - Assert.IsFalse(behaviourContext.IsBiquadFilterGroupedOptimizationSupported()); + Assert.IsFalse(behaviourContext.UseMultiTapBiquadFilterProcessing()); + Assert.IsFalse(behaviourContext.IsNewEffectChannelMappingSupported()); + Assert.IsFalse(behaviourContext.IsBiquadFilterParameterForSplitterEnabled()); Assert.AreEqual(0.70f, behaviourContext.GetAudioRendererProcessingTimeLimit()); Assert.AreEqual(1, behaviourContext.GetCommandProcessingTimeEstimatorVersion()); @@ -104,7 +108,9 @@ namespace Ryujinx.Tests.Audio.Renderer.Server Assert.IsFalse(behaviourContext.IsMixInParameterDirtyOnlyUpdateSupported()); Assert.IsFalse(behaviourContext.IsWaveBufferVersion2Supported()); Assert.IsFalse(behaviourContext.IsEffectInfoVersion2Supported()); - Assert.IsFalse(behaviourContext.IsBiquadFilterGroupedOptimizationSupported()); + Assert.IsFalse(behaviourContext.UseMultiTapBiquadFilterProcessing()); + Assert.IsFalse(behaviourContext.IsNewEffectChannelMappingSupported()); + Assert.IsFalse(behaviourContext.IsBiquadFilterParameterForSplitterEnabled()); Assert.AreEqual(0.70f, behaviourContext.GetAudioRendererProcessingTimeLimit()); Assert.AreEqual(1, behaviourContext.GetCommandProcessingTimeEstimatorVersion()); @@ -130,7 +136,9 @@ namespace Ryujinx.Tests.Audio.Renderer.Server Assert.IsFalse(behaviourContext.IsMixInParameterDirtyOnlyUpdateSupported()); Assert.IsFalse(behaviourContext.IsWaveBufferVersion2Supported()); Assert.IsFalse(behaviourContext.IsEffectInfoVersion2Supported()); - Assert.IsFalse(behaviourContext.IsBiquadFilterGroupedOptimizationSupported()); + Assert.IsFalse(behaviourContext.UseMultiTapBiquadFilterProcessing()); + Assert.IsFalse(behaviourContext.IsNewEffectChannelMappingSupported()); + Assert.IsFalse(behaviourContext.IsBiquadFilterParameterForSplitterEnabled()); Assert.AreEqual(0.75f, behaviourContext.GetAudioRendererProcessingTimeLimit()); Assert.AreEqual(1, behaviourContext.GetCommandProcessingTimeEstimatorVersion()); @@ -156,7 +164,9 @@ namespace Ryujinx.Tests.Audio.Renderer.Server Assert.IsFalse(behaviourContext.IsMixInParameterDirtyOnlyUpdateSupported()); Assert.IsFalse(behaviourContext.IsWaveBufferVersion2Supported()); Assert.IsFalse(behaviourContext.IsEffectInfoVersion2Supported()); - Assert.IsFalse(behaviourContext.IsBiquadFilterGroupedOptimizationSupported()); + Assert.IsFalse(behaviourContext.UseMultiTapBiquadFilterProcessing()); + Assert.IsFalse(behaviourContext.IsNewEffectChannelMappingSupported()); + Assert.IsFalse(behaviourContext.IsBiquadFilterParameterForSplitterEnabled()); Assert.AreEqual(0.80f, behaviourContext.GetAudioRendererProcessingTimeLimit()); Assert.AreEqual(2, behaviourContext.GetCommandProcessingTimeEstimatorVersion()); @@ -182,7 +192,9 @@ namespace Ryujinx.Tests.Audio.Renderer.Server Assert.IsFalse(behaviourContext.IsMixInParameterDirtyOnlyUpdateSupported()); Assert.IsFalse(behaviourContext.IsWaveBufferVersion2Supported()); Assert.IsFalse(behaviourContext.IsEffectInfoVersion2Supported()); - Assert.IsFalse(behaviourContext.IsBiquadFilterGroupedOptimizationSupported()); + Assert.IsFalse(behaviourContext.UseMultiTapBiquadFilterProcessing()); + Assert.IsFalse(behaviourContext.IsNewEffectChannelMappingSupported()); + Assert.IsFalse(behaviourContext.IsBiquadFilterParameterForSplitterEnabled()); Assert.AreEqual(0.80f, behaviourContext.GetAudioRendererProcessingTimeLimit()); Assert.AreEqual(2, behaviourContext.GetCommandProcessingTimeEstimatorVersion()); @@ -208,7 +220,9 @@ namespace Ryujinx.Tests.Audio.Renderer.Server Assert.IsTrue(behaviourContext.IsMixInParameterDirtyOnlyUpdateSupported()); Assert.IsFalse(behaviourContext.IsWaveBufferVersion2Supported()); Assert.IsFalse(behaviourContext.IsEffectInfoVersion2Supported()); - Assert.IsFalse(behaviourContext.IsBiquadFilterGroupedOptimizationSupported()); + Assert.IsFalse(behaviourContext.UseMultiTapBiquadFilterProcessing()); + Assert.IsFalse(behaviourContext.IsNewEffectChannelMappingSupported()); + Assert.IsFalse(behaviourContext.IsBiquadFilterParameterForSplitterEnabled()); Assert.AreEqual(0.80f, behaviourContext.GetAudioRendererProcessingTimeLimit()); Assert.AreEqual(2, behaviourContext.GetCommandProcessingTimeEstimatorVersion()); @@ -234,7 +248,9 @@ namespace Ryujinx.Tests.Audio.Renderer.Server Assert.IsTrue(behaviourContext.IsMixInParameterDirtyOnlyUpdateSupported()); Assert.IsTrue(behaviourContext.IsWaveBufferVersion2Supported()); Assert.IsFalse(behaviourContext.IsEffectInfoVersion2Supported()); - Assert.IsFalse(behaviourContext.IsBiquadFilterGroupedOptimizationSupported()); + Assert.IsFalse(behaviourContext.UseMultiTapBiquadFilterProcessing()); + Assert.IsFalse(behaviourContext.IsNewEffectChannelMappingSupported()); + Assert.IsFalse(behaviourContext.IsBiquadFilterParameterForSplitterEnabled()); Assert.AreEqual(0.80f, behaviourContext.GetAudioRendererProcessingTimeLimit()); Assert.AreEqual(3, behaviourContext.GetCommandProcessingTimeEstimatorVersion()); @@ -260,7 +276,9 @@ namespace Ryujinx.Tests.Audio.Renderer.Server Assert.IsTrue(behaviourContext.IsMixInParameterDirtyOnlyUpdateSupported()); Assert.IsTrue(behaviourContext.IsWaveBufferVersion2Supported()); Assert.IsTrue(behaviourContext.IsEffectInfoVersion2Supported()); - Assert.IsFalse(behaviourContext.IsBiquadFilterGroupedOptimizationSupported()); + Assert.IsFalse(behaviourContext.UseMultiTapBiquadFilterProcessing()); + Assert.IsFalse(behaviourContext.IsNewEffectChannelMappingSupported()); + Assert.IsFalse(behaviourContext.IsBiquadFilterParameterForSplitterEnabled()); Assert.AreEqual(0.80f, behaviourContext.GetAudioRendererProcessingTimeLimit()); Assert.AreEqual(3, behaviourContext.GetCommandProcessingTimeEstimatorVersion()); @@ -286,11 +304,69 @@ namespace Ryujinx.Tests.Audio.Renderer.Server Assert.IsTrue(behaviourContext.IsMixInParameterDirtyOnlyUpdateSupported()); Assert.IsTrue(behaviourContext.IsWaveBufferVersion2Supported()); Assert.IsTrue(behaviourContext.IsEffectInfoVersion2Supported()); - Assert.IsTrue(behaviourContext.IsBiquadFilterGroupedOptimizationSupported()); + Assert.IsTrue(behaviourContext.UseMultiTapBiquadFilterProcessing()); + Assert.IsFalse(behaviourContext.IsNewEffectChannelMappingSupported()); + Assert.IsFalse(behaviourContext.IsBiquadFilterParameterForSplitterEnabled()); Assert.AreEqual(0.80f, behaviourContext.GetAudioRendererProcessingTimeLimit()); Assert.AreEqual(4, behaviourContext.GetCommandProcessingTimeEstimatorVersion()); Assert.AreEqual(2, behaviourContext.GetPerformanceMetricsDataFormat()); } + + [Test] + public void TestRevision11() + { + BehaviourContext behaviourContext = new(); + + behaviourContext.SetUserRevision(BehaviourContext.BaseRevisionMagic + BehaviourContext.Revision11); + + Assert.IsTrue(behaviourContext.IsAdpcmLoopContextBugFixed()); + Assert.IsTrue(behaviourContext.IsSplitterSupported()); + Assert.IsTrue(behaviourContext.IsLongSizePreDelaySupported()); + Assert.IsTrue(behaviourContext.IsAudioUsbDeviceOutputSupported()); + Assert.IsTrue(behaviourContext.IsFlushVoiceWaveBuffersSupported()); + Assert.IsTrue(behaviourContext.IsSplitterBugFixed()); + Assert.IsTrue(behaviourContext.IsElapsedFrameCountSupported()); + Assert.IsTrue(behaviourContext.IsDecodingBehaviourFlagSupported()); + Assert.IsTrue(behaviourContext.IsBiquadFilterEffectStateClearBugFixed()); + Assert.IsTrue(behaviourContext.IsMixInParameterDirtyOnlyUpdateSupported()); + Assert.IsTrue(behaviourContext.IsWaveBufferVersion2Supported()); + Assert.IsTrue(behaviourContext.IsEffectInfoVersion2Supported()); + Assert.IsTrue(behaviourContext.UseMultiTapBiquadFilterProcessing()); + Assert.IsTrue(behaviourContext.IsNewEffectChannelMappingSupported()); + Assert.IsFalse(behaviourContext.IsBiquadFilterParameterForSplitterEnabled()); + + Assert.AreEqual(0.80f, behaviourContext.GetAudioRendererProcessingTimeLimit()); + Assert.AreEqual(5, behaviourContext.GetCommandProcessingTimeEstimatorVersion()); + Assert.AreEqual(2, behaviourContext.GetPerformanceMetricsDataFormat()); + } + + [Test] + public void TestRevision12() + { + BehaviourContext behaviourContext = new(); + + behaviourContext.SetUserRevision(BehaviourContext.BaseRevisionMagic + BehaviourContext.Revision12); + + Assert.IsTrue(behaviourContext.IsAdpcmLoopContextBugFixed()); + Assert.IsTrue(behaviourContext.IsSplitterSupported()); + Assert.IsTrue(behaviourContext.IsLongSizePreDelaySupported()); + Assert.IsTrue(behaviourContext.IsAudioUsbDeviceOutputSupported()); + Assert.IsTrue(behaviourContext.IsFlushVoiceWaveBuffersSupported()); + Assert.IsTrue(behaviourContext.IsSplitterBugFixed()); + Assert.IsTrue(behaviourContext.IsElapsedFrameCountSupported()); + Assert.IsTrue(behaviourContext.IsDecodingBehaviourFlagSupported()); + Assert.IsTrue(behaviourContext.IsBiquadFilterEffectStateClearBugFixed()); + Assert.IsTrue(behaviourContext.IsMixInParameterDirtyOnlyUpdateSupported()); + Assert.IsTrue(behaviourContext.IsWaveBufferVersion2Supported()); + Assert.IsTrue(behaviourContext.IsEffectInfoVersion2Supported()); + Assert.IsTrue(behaviourContext.UseMultiTapBiquadFilterProcessing()); + Assert.IsTrue(behaviourContext.IsNewEffectChannelMappingSupported()); + Assert.IsTrue(behaviourContext.IsBiquadFilterParameterForSplitterEnabled()); + + Assert.AreEqual(0.80f, behaviourContext.GetAudioRendererProcessingTimeLimit()); + Assert.AreEqual(5, behaviourContext.GetCommandProcessingTimeEstimatorVersion()); + Assert.AreEqual(2, behaviourContext.GetPerformanceMetricsDataFormat()); + } } } diff --git a/src/Ryujinx.Tests/Audio/Renderer/Server/SplitterDestinationTests.cs b/src/Ryujinx.Tests/Audio/Renderer/Server/SplitterDestinationTests.cs index ad974aab1..80b801336 100644 --- a/src/Ryujinx.Tests/Audio/Renderer/Server/SplitterDestinationTests.cs +++ b/src/Ryujinx.Tests/Audio/Renderer/Server/SplitterDestinationTests.cs @@ -9,7 +9,8 @@ namespace Ryujinx.Tests.Audio.Renderer.Server [Test] public void EnsureTypeSize() { - Assert.AreEqual(0xE0, Unsafe.SizeOf()); + Assert.AreEqual(0xE0, Unsafe.SizeOf()); + Assert.AreEqual(0x110, Unsafe.SizeOf()); } } } diff --git a/src/Ryujinx.Tests/Memory/PartialUnmaps.cs b/src/Ryujinx.Tests/Memory/PartialUnmaps.cs index 04f7f40e6..ace68e5c2 100644 --- a/src/Ryujinx.Tests/Memory/PartialUnmaps.cs +++ b/src/Ryujinx.Tests/Memory/PartialUnmaps.cs @@ -388,14 +388,14 @@ namespace Ryujinx.Tests.Memory { rwLock.AcquireReaderLock(); - int originalValue = Thread.VolatileRead(ref value); + int originalValue = Volatile.Read(ref value); count++; // Spin a bit. for (int i = 0; i < 100; i++) { - if (Thread.VolatileRead(ref readersAllowed) == 0) + if (Volatile.Read(ref readersAllowed) == 0) { error = true; running = false; @@ -403,7 +403,7 @@ namespace Ryujinx.Tests.Memory } // Should not change while the lock is held. - if (Thread.VolatileRead(ref value) != originalValue) + if (Volatile.Read(ref value) != originalValue) { error = true; running = false; diff --git a/src/Ryujinx.UI.Common/App/ApplicationData.cs b/src/Ryujinx.UI.Common/App/ApplicationData.cs index 8cc7238e9..13c05655b 100644 --- a/src/Ryujinx.UI.Common/App/ApplicationData.cs +++ b/src/Ryujinx.UI.Common/App/ApplicationData.cs @@ -45,7 +45,7 @@ namespace Ryujinx.UI.App.Common if (!System.IO.Path.Exists(titleFilePath)) { - Logger.Error?.Print(LogClass.Application, $"File does not exists. {titleFilePath}"); + Logger.Error?.Print(LogClass.Application, $"File \"{titleFilePath}\" does not exist."); return string.Empty; } diff --git a/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs b/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs index 65cf7a9e6..176011dde 100644 --- a/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs +++ b/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs @@ -65,7 +65,7 @@ namespace Ryujinx.UI.App.Common Stream resourceStream = Assembly.GetCallingAssembly().GetManifestResourceStream(resourceName); byte[] resourceByteArray = new byte[resourceStream.Length]; - resourceStream.Read(resourceByteArray); + resourceStream.ReadExactly(resourceByteArray); return resourceByteArray; } @@ -106,7 +106,7 @@ namespace Ryujinx.UI.App.Common if (!Directory.Exists(appDir)) { - Logger.Warning?.Print(LogClass.Application, $"The \"game_dirs\" section in \"{ReleaseInformation.ConfigName}\" contains an invalid directory: \"{appDir}\""); + Logger.Warning?.Print(LogClass.Application, $"The specified game directory \"{appDir}\" does not exist."); continue; } diff --git a/src/Ryujinx.UI.Common/Configuration/ConfigurationFileFormat.cs b/src/Ryujinx.UI.Common/Configuration/ConfigurationFileFormat.cs index 3390e5171..10e492d48 100644 --- a/src/Ryujinx.UI.Common/Configuration/ConfigurationFileFormat.cs +++ b/src/Ryujinx.UI.Common/Configuration/ConfigurationFileFormat.cs @@ -15,7 +15,7 @@ namespace Ryujinx.UI.Common.Configuration /// /// The current version of the file format /// - public const int CurrentVersion = 50; + public const int CurrentVersion = 51; /// /// Version of the configuration file format @@ -162,6 +162,11 @@ namespace Ryujinx.UI.Common.Configuration /// public bool ShowConfirmExit { get; set; } + /// + /// Enables or disables save window size, position and state on close. + /// + public bool RememberWindowState { get; set; } + /// /// Enables hardware-accelerated rendering for Avalonia /// diff --git a/src/Ryujinx.UI.Common/Configuration/ConfigurationState.cs b/src/Ryujinx.UI.Common/Configuration/ConfigurationState.cs index c4f5fa5de..20f5eda7d 100644 --- a/src/Ryujinx.UI.Common/Configuration/ConfigurationState.cs +++ b/src/Ryujinx.UI.Common/Configuration/ConfigurationState.cs @@ -11,6 +11,7 @@ using Ryujinx.UI.Common.Configuration.UI; using Ryujinx.UI.Common.Helper; using System; using System.Collections.Generic; +using System.Globalization; using System.Text.Json.Nodes; namespace Ryujinx.UI.Common.Configuration @@ -633,6 +634,11 @@ namespace Ryujinx.UI.Common.Configuration /// public ReactiveObject ShowConfirmExit { get; private set; } + /// + /// Enables or disables save window size, position and state on close. + /// + public ReactiveObject RememberWindowState { get; private set; } + /// /// Enables hardware-accelerated rendering for Avalonia /// @@ -654,6 +660,7 @@ namespace Ryujinx.UI.Common.Configuration EnableDiscordIntegration = new ReactiveObject(); CheckUpdatesOnStart = new ReactiveObject(); ShowConfirmExit = new ReactiveObject(); + RememberWindowState = new ReactiveObject(); EnableHardwareAcceleration = new ReactiveObject(); HideCursor = new ReactiveObject(); } @@ -692,6 +699,7 @@ namespace Ryujinx.UI.Common.Configuration EnableDiscordIntegration = EnableDiscordIntegration, CheckUpdatesOnStart = CheckUpdatesOnStart, ShowConfirmExit = ShowConfirmExit, + RememberWindowState = RememberWindowState, EnableHardwareAcceleration = EnableHardwareAcceleration, HideCursor = HideCursor, EnableVsync = Graphics.EnableVsync, @@ -801,6 +809,7 @@ namespace Ryujinx.UI.Common.Configuration EnableDiscordIntegration.Value = true; CheckUpdatesOnStart.Value = true; ShowConfirmExit.Value = true; + RememberWindowState.Value = true; EnableHardwareAcceleration.Value = true; HideCursor.Value = HideCursorMode.OnIdle; Graphics.EnableVsync.Value = true; @@ -1474,6 +1483,15 @@ namespace Ryujinx.UI.Common.Configuration { Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 51."); + configurationFileFormat.RememberWindowState = true; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 52) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 52."); + configurationFileFormat.TurboMultiplier = 200; configurationFileFormat.Hotkeys = new KeyboardHotkeys @@ -1525,6 +1543,7 @@ namespace Ryujinx.UI.Common.Configuration EnableDiscordIntegration.Value = configurationFileFormat.EnableDiscordIntegration; CheckUpdatesOnStart.Value = configurationFileFormat.CheckUpdatesOnStart; ShowConfirmExit.Value = configurationFileFormat.ShowConfirmExit; + RememberWindowState.Value = configurationFileFormat.RememberWindowState; EnableHardwareAcceleration.Value = configurationFileFormat.EnableHardwareAcceleration; HideCursor.Value = configurationFileFormat.HideCursor; Graphics.EnableVsync.Value = configurationFileFormat.EnableVsync; @@ -1612,7 +1631,9 @@ namespace Ryujinx.UI.Common.Configuration private static void LogValueChange(ReactiveEventArgs eventArgs, string valueName) { - Ryujinx.Common.Logging.Logger.Info?.Print(LogClass.Configuration, $"{valueName} set to: {eventArgs.NewValue}"); + string message = string.Create(CultureInfo.InvariantCulture, $"{valueName} set to: {eventArgs.NewValue}"); + + Ryujinx.Common.Logging.Logger.Info?.Print(LogClass.Configuration, message); } public static void Initialize() diff --git a/src/Ryujinx.UI.Common/DiscordIntegrationModule.cs b/src/Ryujinx.UI.Common/DiscordIntegrationModule.cs index bbece1e1d..fb07195d0 100644 --- a/src/Ryujinx.UI.Common/DiscordIntegrationModule.cs +++ b/src/Ryujinx.UI.Common/DiscordIntegrationModule.cs @@ -1,6 +1,7 @@ using DiscordRPC; using Ryujinx.Common; using Ryujinx.UI.Common.Configuration; +using System.Text; namespace Ryujinx.UI.Common { @@ -9,6 +10,9 @@ namespace Ryujinx.UI.Common private const string Description = "A simple, experimental Nintendo Switch emulator."; private const string ApplicationId = "1216775165866807456"; + private const int ApplicationByteLimit = 128; + private const string Ellipsis = "…"; + private static DiscordRpcClient _discordClient; private static RichPresence _discordPresenceMain; @@ -60,18 +64,18 @@ namespace Ryujinx.UI.Common } } - public static void SwitchToPlayingState(string titleId, string titleName) + public static void SwitchToPlayingState(string titleId, string applicationName) { _discordClient?.SetPresence(new RichPresence { Assets = new Assets { LargeImageKey = "game", - LargeImageText = titleName, + LargeImageText = TruncateToByteLength(applicationName, ApplicationByteLimit), SmallImageKey = "ryujinx", SmallImageText = Description, }, - Details = $"Playing {titleName}", + Details = TruncateToByteLength($"Playing {applicationName}", ApplicationByteLimit), State = (titleId == "0000000000000000") ? "Homebrew" : titleId.ToUpper(), Timestamps = Timestamps.Now, Buttons = @@ -90,6 +94,28 @@ namespace Ryujinx.UI.Common _discordClient?.SetPresence(_discordPresenceMain); } + private static string TruncateToByteLength(string input, int byteLimit) + { + if (Encoding.UTF8.GetByteCount(input) <= byteLimit) + { + return input; + } + + // Find the length to trim the string to guarantee we have space for the trailing ellipsis. + int trimLimit = byteLimit - Encoding.UTF8.GetByteCount(Ellipsis); + + // Basic trim to best case scenario of 1 byte characters. + input = input[..trimLimit]; + + while (Encoding.UTF8.GetByteCount(input) > trimLimit) + { + // Remove one character from the end of the string at a time. + input = input[..^1]; + } + + return input.TrimEnd() + Ellipsis; + } + public static void Exit() { _discordClient?.Dispose(); diff --git a/src/Ryujinx/App.axaml.cs b/src/Ryujinx/App.axaml.cs index 387a6dc14..24d8a70a1 100644 --- a/src/Ryujinx/App.axaml.cs +++ b/src/Ryujinx/App.axaml.cs @@ -1,8 +1,10 @@ using Avalonia; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Markup.Xaml; +using Avalonia.Platform; using Avalonia.Styling; using Avalonia.Threading; +using Ryujinx.Ava.Common; using Ryujinx.Ava.Common.Locale; using Ryujinx.Ava.UI.Helpers; using Ryujinx.Ava.UI.Windows; @@ -84,7 +86,7 @@ namespace Ryujinx.Ava ApplyConfiguredTheme(); } - private void ApplyConfiguredTheme() + public void ApplyConfiguredTheme() { try { @@ -92,13 +94,18 @@ namespace Ryujinx.Ava if (string.IsNullOrWhiteSpace(baseStyle)) { - ConfigurationState.Instance.UI.BaseStyle.Value = "Dark"; + ConfigurationState.Instance.UI.BaseStyle.Value = "Auto"; baseStyle = ConfigurationState.Instance.UI.BaseStyle; } + ThemeVariant systemTheme = DetectSystemTheme(); + + ThemeManager.OnThemeChanged(); + RequestedThemeVariant = baseStyle switch { + "Auto" => systemTheme, "Light" => ThemeVariant.Light, "Dark" => ThemeVariant.Dark, _ => ThemeVariant.Default, @@ -111,5 +118,28 @@ namespace Ryujinx.Ava ShowRestartDialog(); } } + + /// + /// Converts a PlatformThemeVariant value to the corresponding ThemeVariant value. + /// + public static ThemeVariant ConvertThemeVariant(PlatformThemeVariant platformThemeVariant) => + platformThemeVariant switch + { + PlatformThemeVariant.Dark => ThemeVariant.Dark, + PlatformThemeVariant.Light => ThemeVariant.Light, + _ => ThemeVariant.Default, + }; + + public static ThemeVariant DetectSystemTheme() + { + if (Application.Current is App app) + { + var colorValues = app.PlatformSettings.GetColorValues(); + + return ConvertThemeVariant(colorValues.ThemeVariant); + } + + return ThemeVariant.Default; + } } } diff --git a/src/Ryujinx/AppHost.cs b/src/Ryujinx/AppHost.cs index cf1b5f27e..c74c0acc9 100644 --- a/src/Ryujinx/AppHost.cs +++ b/src/Ryujinx/AppHost.cs @@ -95,6 +95,17 @@ namespace Ryujinx.Ava private long _lastCursorMoveTime; private bool _isCursorInRenderer = true; + private bool _ignoreCursorState = false; + + private enum CursorStates + { + CursorIsHidden, + CursorIsVisible, + ForceChangeCursor + }; + + private CursorStates _cursorState = !ConfigurationState.Instance.Hid.EnableMouse.Value ? + CursorStates.CursorIsVisible : CursorStates.CursorIsHidden; private bool _isStopped; private bool _isActive; @@ -202,23 +213,65 @@ namespace Ryujinx.Ava private void TopLevel_PointerEnteredOrMoved(object sender, PointerEventArgs e) { + if (!_viewModel.IsActive) + { + _isCursorInRenderer = false; + _ignoreCursorState = false; + return; + } + if (sender is MainWindow window) { - _lastCursorMoveTime = Stopwatch.GetTimestamp(); + if (ConfigurationState.Instance.HideCursor.Value == HideCursorMode.OnIdle) + { + _lastCursorMoveTime = Stopwatch.GetTimestamp(); + } var point = e.GetCurrentPoint(window).Position; var bounds = RendererHost.EmbeddedWindow.Bounds; + var windowYOffset = bounds.Y + window.MenuBarHeight; + var windowYLimit = (int)window.Bounds.Height - window.StatusBarHeight - 1; + + if (!_viewModel.ShowMenuAndStatusBar) + { + windowYOffset -= window.MenuBarHeight; + windowYLimit += window.StatusBarHeight + 1; + } _isCursorInRenderer = point.X >= bounds.X && - point.X <= bounds.Width + bounds.X && - point.Y >= bounds.Y && - point.Y <= bounds.Height + bounds.Y; + Math.Ceiling(point.X) <= (int)window.Bounds.Width && + point.Y >= windowYOffset && + point.Y <= windowYLimit && + !_viewModel.IsSubMenuOpen; + + _ignoreCursorState = false; } } private void TopLevel_PointerExited(object sender, PointerEventArgs e) { _isCursorInRenderer = false; + + if (sender is MainWindow window) + { + var point = e.GetCurrentPoint(window).Position; + var bounds = RendererHost.EmbeddedWindow.Bounds; + var windowYOffset = bounds.Y + window.MenuBarHeight; + var windowYLimit = (int)window.Bounds.Height - window.StatusBarHeight - 1; + + if (!_viewModel.ShowMenuAndStatusBar) + { + windowYOffset -= window.MenuBarHeight; + windowYLimit += window.StatusBarHeight + 1; + } + + _ignoreCursorState = (point.X == bounds.X || + Math.Ceiling(point.X) == (int)window.Bounds.Width) && + point.Y >= windowYOffset && + point.Y <= windowYLimit; + } + + _cursorState = CursorStates.ForceChangeCursor; } private void UpdateScalingFilterLevel(object sender, ReactiveEventArgs e) @@ -246,9 +299,14 @@ namespace Ryujinx.Ava if (OperatingSystem.IsWindows()) { - SetCursor(_defaultCursorWin); + if (_cursorState != CursorStates.CursorIsHidden && !_ignoreCursorState) + { + SetCursor(_defaultCursorWin); + } } }); + + _cursorState = CursorStates.CursorIsVisible; } private void HideCursor() @@ -262,6 +320,8 @@ namespace Ryujinx.Ava SetCursor(_invisibleCursorWin); } }); + + _cursorState = CursorStates.CursorIsHidden; } private void SetRendererWindowSize(Size size) @@ -524,6 +584,8 @@ namespace Ryujinx.Ava { _lastCursorMoveTime = Stopwatch.GetTimestamp(); } + + _cursorState = CursorStates.ForceChangeCursor; } public async Task LoadGuestApplication() @@ -1040,38 +1102,32 @@ namespace Ryujinx.Ava if (_viewModel.IsActive) { - if (_isCursorInRenderer) + bool isCursorVisible = true; + + if (_isCursorInRenderer && !_viewModel.ShowLoadProgress) { - if (ConfigurationState.Instance.Hid.EnableMouse) + if (ConfigurationState.Instance.Hid.EnableMouse.Value) { - HideCursor(); + isCursorVisible = ConfigurationState.Instance.HideCursor.Value == HideCursorMode.Never; } else { - switch (ConfigurationState.Instance.HideCursor.Value) - { - case HideCursorMode.Never: - ShowCursor(); - break; - case HideCursorMode.OnIdle: - if (Stopwatch.GetTimestamp() - _lastCursorMoveTime >= CursorHideIdleTime * Stopwatch.Frequency) - { - HideCursor(); - } - else - { - ShowCursor(); - } - break; - case HideCursorMode.Always: - HideCursor(); - break; - } + isCursorVisible = ConfigurationState.Instance.HideCursor.Value == HideCursorMode.Never || + (ConfigurationState.Instance.HideCursor.Value == HideCursorMode.OnIdle && + Stopwatch.GetTimestamp() - _lastCursorMoveTime < CursorHideIdleTime * Stopwatch.Frequency); } } - else + + if (_cursorState != (isCursorVisible ? CursorStates.CursorIsVisible : CursorStates.CursorIsHidden)) { - ShowCursor(); + if (isCursorVisible) + { + ShowCursor(); + } + else + { + HideCursor(); + } } Dispatcher.UIThread.Post(() => @@ -1168,7 +1224,7 @@ namespace Ryujinx.Ava // Touchscreen. bool hasTouch = false; - if (_viewModel.IsActive && !ConfigurationState.Instance.Hid.EnableMouse) + if (_viewModel.IsActive && !ConfigurationState.Instance.Hid.EnableMouse.Value) { hasTouch = TouchScreenManager.Update(true, (_inputManager.MouseDriver as AvaloniaMouseDriver).IsButtonPressed(MouseButton.Button1), ConfigurationState.Instance.Graphics.AspectRatio.Value.ToFloat()); } diff --git a/src/Ryujinx/Assets/Locales/ar_SA.json b/src/Ryujinx/Assets/Locales/ar_SA.json index 51df3a052..73e55633b 100644 --- a/src/Ryujinx/Assets/Locales/ar_SA.json +++ b/src/Ryujinx/Assets/Locales/ar_SA.json @@ -1,38 +1,42 @@ { - "Language": "العربية", - "MenuBarFileOpenApplet": "فتح التطبيق المُصغَّر", - "MenuBarFileOpenAppletOpenMiiAppletToolTip": "افتح تطبيق محرر الـMii المُصغَّر في الوضع المستقل", + "Language": "اَلْعَرَبِيَّةُ", + "MenuBarFileOpenApplet": "فتح التطبيق المصغر", + "MenuBarFileOpenAppletOpenMiiAppletToolTip": "‫افتح تطبيق تحرير Mii في الوضع المستقل", "SettingsTabInputDirectMouseAccess": "الوصول المباشر للفأرة", "SettingsTabSystemMemoryManagerMode": "وضع إدارة الذاكرة:", "SettingsTabSystemMemoryManagerModeSoftware": "البرنامج", "SettingsTabSystemMemoryManagerModeHost": "المُضيف (سريع)", - "SettingsTabSystemMemoryManagerModeHostUnchecked": "المضيف غير محدد (سريع، غير آمن)", - "SettingsTabSystemUseHypervisor": "استخدم الهايبرڤايزور", + "SettingsTabSystemMemoryManagerModeHostUnchecked": "المضيف (غير مفحوص) (أسرع، غير آمن)", + "SettingsTabSystemUseHypervisor": "استخدم مراقب الأجهزة الافتراضية", "MenuBarFile": "_ملف", "MenuBarFileOpenFromFile": "_تحميل تطبيق من ملف", - "MenuBarFileOpenUnpacked": "تحميل لعبه غير محزومه", - "MenuBarFileOpenEmuFolder": "فتح مجلد Ryujinx", + "MenuBarFileOpenUnpacked": "تحميل لُعْبَة غير محزومة", + "MenuBarFileOpenEmuFolder": "‫فتح مجلد Ryujinx", "MenuBarFileOpenLogsFolder": "فتح مجلد السجلات", "MenuBarFileExit": "_خروج", "MenuBarOptions": "_خيارات", - "MenuBarOptionsToggleFullscreen": "وضع ملء الشاشة", + "MenuBarOptionsToggleFullscreen": "التبديل إلى وضع ملء الشاشة", "MenuBarOptionsStartGamesInFullscreen": "ابدأ الألعاب في وضع ملء الشاشة", "MenuBarOptionsStopEmulation": "إيقاف المحاكاة", - "MenuBarOptionsSettings": "الإعدادات", - "MenuBarOptionsManageUserProfiles": "إدارة الملفات الشخصية للمستخدم", - "MenuBarActions": "الإجراءات", + "MenuBarOptionsSettings": "_الإعدادات", + "MenuBarOptionsManageUserProfiles": "_إدارة الملفات الشخصية للمستخدم", + "MenuBarActions": "_الإجراءات", "MenuBarOptionsSimulateWakeUpMessage": "محاكاة رسالة الاستيقاظ", - "MenuBarActionsScanAmiibo": "فحص Amiibo", - "MenuBarTools": "الأدوات", - "MenuBarToolsInstallFirmware": "تثبيت البرامج الثابتة", - "MenuBarFileToolsInstallFirmwareFromFile": "تثبيت البرنامج الثابت من XCI أو ZIP", + "MenuBarActionsScanAmiibo": "‫فحص Amiibo", + "MenuBarTools": "_الأدوات", + "MenuBarToolsInstallFirmware": "تثبيت البرنامج الثابت", + "MenuBarFileToolsInstallFirmwareFromFile": "تثبيت برنامج ثابت من XCI أو ZIP", "MenuBarFileToolsInstallFirmwareFromDirectory": "تثبيت برنامج ثابت من مجلد", "MenuBarToolsManageFileTypes": "إدارة أنواع الملفات", "MenuBarToolsInstallFileTypes": "تثبيت أنواع الملفات", "MenuBarToolsUninstallFileTypes": "إزالة أنواع الملفات", + "MenuBarView": "_عرض", + "MenuBarViewWindow": "حجم النافذة", + "MenuBarViewWindow720": "720p", + "MenuBarViewWindow1080": "1080p", "MenuBarHelp": "_مساعدة", "MenuBarHelpCheckForUpdates": "تحقق من التحديثات", - "MenuBarHelpAbout": "عن البرنامج", + "MenuBarHelpAbout": "حول", "MenuSearch": "بحث...", "GameListHeaderFavorite": "مفضلة", "GameListHeaderIcon": "الأيقونة", @@ -40,62 +44,63 @@ "GameListHeaderDeveloper": "المطور", "GameListHeaderVersion": "الإصدار", "GameListHeaderTimePlayed": "وقت اللعب", - "GameListHeaderLastPlayed": "اخر تشغيل", - "GameListHeaderFileExtension": "امتداد الملف", + "GameListHeaderLastPlayed": "آخر مرة لُعبت", + "GameListHeaderFileExtension": "صيغة الملف", "GameListHeaderFileSize": "حجم الملف", "GameListHeaderPath": "المسار", "GameListContextMenuOpenUserSaveDirectory": "فتح مجلد حفظ المستخدم", "GameListContextMenuOpenUserSaveDirectoryToolTip": "يفتح المجلد الذي يحتوي على حفظ المستخدم للتطبيق", "GameListContextMenuOpenDeviceSaveDirectory": "فتح مجلد حفظ الجهاز", "GameListContextMenuOpenDeviceSaveDirectoryToolTip": "يفتح المجلد الذي يحتوي على حفظ الجهاز للتطبيق", - "GameListContextMenuOpenBcatSaveDirectory": "فتح مجلد حفظ الـBCAT", - "GameListContextMenuOpenBcatSaveDirectoryToolTip": "يفتح المجلد الذي يحتوي على حفظ الـBCAT للتطبيق", - "GameListContextMenuManageTitleUpdates": "إدارة تحديثات العنوان", - "GameListContextMenuManageTitleUpdatesToolTip": "يفتح نافذة إدارة تحديث العنوان", + "GameListContextMenuOpenBcatSaveDirectory": "‫فتح مجلد حفظ الـBCAT", + "GameListContextMenuOpenBcatSaveDirectoryToolTip": "‫يفتح المجلد الذي يحتوي على حفظ الـBCAT للتطبيق", + "GameListContextMenuManageTitleUpdates": "إدارة تحديثات اللُعبة", + "GameListContextMenuManageTitleUpdatesToolTip": "يفتح نافذة إدارة تحديث اللُعبة", "GameListContextMenuManageDlc": "إدارة المحتوي الإضافي", "GameListContextMenuManageDlcToolTip": "يفتح نافذة إدارة المحتوي الإضافي", "GameListContextMenuCacheManagement": "إدارة ذاكرة التخزين المؤقت", - "GameListContextMenuCacheManagementPurgePptc": "إعادة بناء PPTC في قائمة الانتظار", - "GameListContextMenuCacheManagementPurgePptcToolTip": "تنشيط PPTC لإعادة البناء في وقت التمهيد عند بدء تشغيل اللعبة التالية", - "GameListContextMenuCacheManagementPurgeShaderCache": "إزالة ذاكرة التشغيل المؤقتة للمظللات ", - "GameListContextMenuCacheManagementPurgeShaderCacheToolTip": "حذف الذاكرة المؤقتة للمظللات الخاصة بالتطبيق", - "GameListContextMenuCacheManagementOpenPptcDirectory": "فتح مجلد PPTC", - "GameListContextMenuCacheManagementOpenPptcDirectoryToolTip": "يفتح المجلد الذي يحتوي على الـPPTC للتطبيق", - "GameListContextMenuCacheManagementOpenShaderCacheDirectory": "فتح مجلد الذاكرة المؤقتة للمظللات ", - "GameListContextMenuCacheManagementOpenShaderCacheDirectoryToolTip": "يفتح المجلد الذي يحتوي على ذاكرة التشغيل المؤقتة للمظللات الخاصة بالتطبيق", - "GameListContextMenuExtractData": "إستخراج البيانات", + "GameListContextMenuCacheManagementPurgePptc": "قائمة انتظار إعادة بناء الـ‫PPTC", + "GameListContextMenuCacheManagementPurgePptcToolTip": "تنشيط ‫PPTC لإعادة البناء في وقت الإقلاع عند بدء تشغيل اللعبة التالي", + "GameListContextMenuCacheManagementPurgeShaderCache": "تنظيف ذاكرة مرشحات الفيديو المؤقتة", + "GameListContextMenuCacheManagementPurgeShaderCacheToolTip": "يحذف ذاكرة مرشحات الفيديو المؤقتة الخاصة بالتطبيق", + "GameListContextMenuCacheManagementOpenPptcDirectory": "‫فتح مجلد PPTC", + "GameListContextMenuCacheManagementOpenPptcDirectoryToolTip": "‫‫يفتح المجلد الذي يحتوي على ذاكرة التخزين المؤقت للترجمة المستمرة (PPTC) للتطبيق", + "GameListContextMenuCacheManagementOpenShaderCacheDirectory": "فتح مجلد الذاكرة المؤقتة لمرشحات الفيديو ", + "GameListContextMenuCacheManagementOpenShaderCacheDirectoryToolTip": "يفتح المجلد الذي يحتوي على ذاكرة المظللات المؤقتة للتطبيق", + "GameListContextMenuExtractData": "استخراج البيانات", "GameListContextMenuExtractDataExeFS": "ExeFS", - "GameListContextMenuExtractDataExeFSToolTip": "إستخراج قسم ExeFS من التكوين الحالي للتطبيقات (بما في ذلك التحديثات)", + "GameListContextMenuExtractDataExeFSToolTip": "‫‫ استخراج قسم نظام الملفات القابل للتنفيذ (ExeFS) من الإعدادات الحالية للتطبيقات (يتضمن التحديثات)", "GameListContextMenuExtractDataRomFS": "RomFS", - "GameListContextMenuExtractDataRomFSToolTip": "استخراج قسم RomFS من التكوين الحالي للتطبيقات (بما في ذلك التحديثات)", + "GameListContextMenuExtractDataRomFSToolTip": "استخراج قسم RomFS من الإعدادات الحالية للتطبيقات (يتضمن التحديثات)", "GameListContextMenuExtractDataLogo": "شعار", - "GameListContextMenuExtractDataLogoToolTip": "استخراج قسم الشعار من التكوين الحالي للتطبيقات (بما في ذلك التحديثات)", + "GameListContextMenuExtractDataLogoToolTip": "استخراج قسم الشعار من الإعدادات الحالية للتطبيقات (يتضمن التحديثات)", "GameListContextMenuCreateShortcut": "إنشاء اختصار للتطبيق", - "GameListContextMenuCreateShortcutToolTip": "قم بإنشاء اختصار لسطح المكتب لتشغيل التطبيق المحدد", - "GameListContextMenuCreateShortcutToolTipMacOS": "قم بإنشاء اختصار في مجلد تطبيقات نظام التشغيل MacOS الذي يقوم بتشغيل التطبيق المحدد", - "GameListContextMenuOpenModsDirectory": "فتح مجلد التعديلات", - "GameListContextMenuOpenModsDirectoryToolTip": "يفتح المجلد الذي يحتوي على تعديلات التطبيق", - "GameListContextMenuOpenSdModsDirectory": "فتح مجلد تعديلات Atmosphere", - "GameListContextMenuOpenSdModsDirectoryToolTip": "يفتح دليل Atmosphere لبطاقة SD البديلة الذي يحتوي على تعديلات التطبيق. مفيد للتعديلات التي تم تعبئتها للأجهزة الحقيقية.", - "StatusBarGamesLoaded": "{0}/{1} الألعاب التي تم تحميلها", + "GameListContextMenuCreateShortcutToolTip": "أنشئ اختصار سطح مكتب لتشغيل التطبيق المحدد", + "GameListContextMenuCreateShortcutToolTipMacOS": "أنشئ اختصار يُشغل التطبيق المحدد في مجلد تطبيقات ‫macOS", + "GameListContextMenuOpenModsDirectory": "‫فتح مجلد التعديلات (Mods)", + "GameListContextMenuOpenModsDirectoryToolTip": "يفتح المجلد الذي يحتوي على تعديلات‫(mods) التطبيق", + "GameListContextMenuOpenSdModsDirectory": "فتح مجلد تعديلات‫(mods) أتموسفير", + "GameListContextMenuOpenSdModsDirectoryToolTip": "يفتح مجلد أتموسفير لبطاقة SD البديلة الذي يحتوي على تعديلات التطبيق. مفيد للتعديلات التي تم تعبئتها للأجهزة الحقيقية.", + "StatusBarGamesLoaded": "{0}/{1} لعبة تم تحميلها", "StatusBarSystemVersion": "إصدار النظام: {0}", "LinuxVmMaxMapCountDialogTitle": "الحد الأدنى لتعيينات الذاكرة المكتشفة", "LinuxVmMaxMapCountDialogTextPrimary": "هل ترغب في زيادة قيمة vm.max_map_count إلى {0}", - "LinuxVmMaxMapCountDialogTextSecondary": "قد تحاول بعض الألعاب إنشاء المزيد من تعيينات الذاكرة أكثر مما هو مسموح به حاليا. سيتحطم Ryujinx بمجرد تجاوز هذا الحد.", + "LinuxVmMaxMapCountDialogTextSecondary": "قد تحاول بعض الألعاب إنشاء المزيد من تعيينات الذاكرة أكثر مما هو مسموح به حاليا. سيغلق ريوجينكس بمجرد تجاوز هذا الحد.", "LinuxVmMaxMapCountDialogButtonUntilRestart": "نعم، حتى إعادة التشغيل التالية", - "LinuxVmMaxMapCountDialogButtonPersistent": "نعم، بشكل دائم", + "LinuxVmMaxMapCountDialogButtonPersistent": "نعم، دائمًا", "LinuxVmMaxMapCountWarningTextPrimary": "الحد الأقصى لمقدار تعيينات الذاكرة أقل من الموصى به.", - "LinuxVmMaxMapCountWarningTextSecondary": "القيمة الحالية لـ vm.max_map_count ({0}) أقل من {1}. قد تحاول بعض الألعاب إنشاء المزيد من تعيينات الذاكرة أكثر مما هو مسموح به حاليا. سيتحطم Ryujinx بمجرد تجاوز هذا الحد.\n\nقد ترغب في زيادة الحد اليدوي أو تثبيت pkexec، مما يسمح لـ Ryujinx بالمساعدة في ذلك.", + "LinuxVmMaxMapCountWarningTextSecondary": "القيمة الحالية لـ vm.max_map_count ({0}) أقل من {1}. قد تحاول بعض الألعاب إنشاء المزيد من تعيينات الذاكرة أكثر مما هو مسموح به حاليا. سيغلق ريوجينكس بمجرد تجاوز هذا الحد.\n\nقد ترغب إما في زيادة الحد يدويا أو تثبيت pkexec، مما يسمح لـ ريوجينكس بالمساعدة في ذلك.", "Settings": "إعدادات", "SettingsTabGeneral": "واجهة المستخدم", - "SettingsTabGeneralGeneral": "العامة", + "SettingsTabGeneralGeneral": "عام", "SettingsTabGeneralEnableDiscordRichPresence": "تمكين وجود ديسكورد الغني", "SettingsTabGeneralCheckUpdatesOnLaunch": "التحقق من وجود تحديثات عند التشغيل", "SettingsTabGeneralShowConfirmExitDialog": "إظهار مربع حوار \"تأكيد الخروج\"", + "SettingsTabGeneralRememberWindowState": "تذكر حجم/موضع النافذة", "SettingsTabGeneralHideCursor": "إخفاء المؤشر:", - "SettingsTabGeneralHideCursorNever": "مطلقاً", + "SettingsTabGeneralHideCursorNever": "مطلقا", "SettingsTabGeneralHideCursorOnIdle": "عند الخمول", - "SettingsTabGeneralHideCursorAlways": "دائماً", + "SettingsTabGeneralHideCursorAlways": "دائما", "SettingsTabGeneralGameDirectories": "مجلدات الألعاب", "SettingsTabGeneralAdd": "إضافة", "SettingsTabGeneralRemove": "إزالة", @@ -127,24 +132,24 @@ "SettingsTabSystemSystemLanguageLatinAmericanSpanish": "إسبانية أمريكا اللاتينية", "SettingsTabSystemSystemLanguageSimplifiedChinese": "الصينية المبسطة", "SettingsTabSystemSystemLanguageTraditionalChinese": "الصينية التقليدية", - "SettingsTabSystemSystemTimeZone": "نظام التوقيت للنظام:", + "SettingsTabSystemSystemTimeZone": "النطاق الزمني للنظام:", "SettingsTabSystemSystemTime": "توقيت النظام:", "SettingsTabSystemEnableVsync": "VSync", - "SettingsTabSystemEnablePptc": "PPTC (Profiled Persistent Translation Cache)", - "SettingsTabSystemEnableFsIntegrityChecks": "FS Integrity Checks", + "SettingsTabSystemEnablePptc": "PPTC (ذاكرة التخزين المؤقت للترجمة المستمرة)", + "SettingsTabSystemEnableFsIntegrityChecks": "التحقق من سلامة نظام الملفات", "SettingsTabSystemAudioBackend": "خلفية الصوت:", "SettingsTabSystemAudioBackendDummy": "زائف", "SettingsTabSystemAudioBackendOpenAL": "OpenAL", "SettingsTabSystemAudioBackendSoundIO": "SoundIO", "SettingsTabSystemAudioBackendSDL2": "SDL2", - "SettingsTabSystemHacks": "الاختراقات", + "SettingsTabSystemHacks": "هاكات", "SettingsTabSystemHacksNote": "قد يتسبب في عدم الاستقرار", "SettingsTabSystemExpandDramSize": "استخدام تخطيط الذاكرة البديل (المطورين)", "SettingsTabSystemIgnoreMissingServices": "تجاهل الخدمات المفقودة", "SettingsTabGraphics": "الرسومات", - "SettingsTabGraphicsAPI": "الرسومات API", + "SettingsTabGraphicsAPI": "API الرسومات ", "SettingsTabGraphicsEnableShaderCache": "تفعيل ذاكرة المظللات المؤقتة", - "SettingsTabGraphicsAnisotropicFiltering": "تصفية متباين الخواص:", + "SettingsTabGraphicsAnisotropicFiltering": "تصفية:", "SettingsTabGraphicsAnisotropicFilteringAuto": "تلقائي", "SettingsTabGraphicsAnisotropicFiltering2x": "2x", "SettingsTabGraphicsAnisotropicFiltering4x": "4×", @@ -152,7 +157,7 @@ "SettingsTabGraphicsAnisotropicFiltering16x": "16x", "SettingsTabGraphicsResolutionScale": "مقياس الدقة", "SettingsTabGraphicsResolutionScaleCustom": "مخصص (لا ينصح به)", - "SettingsTabGraphicsResolutionScaleNative": "الأصل (720p/1080p)", + "SettingsTabGraphicsResolutionScaleNative": "الأصل ‫(720p/1080p)", "SettingsTabGraphicsResolutionScale2x": "2x (1440p/2160p)", "SettingsTabGraphicsResolutionScale3x": "3x (2160p/3240p)", "SettingsTabGraphicsResolutionScale4x": "4x (2880p/4320p) (لا ينصح به)", @@ -162,11 +167,11 @@ "SettingsTabGraphicsAspectRatio16x10": "16:10", "SettingsTabGraphicsAspectRatio21x9": "21:9", "SettingsTabGraphicsAspectRatio32x9": "32:9", - "SettingsTabGraphicsAspectRatioStretch": "تمدد لتلائم حجم النافذة", + "SettingsTabGraphicsAspectRatioStretch": "تمديد لتناسب النافذة", "SettingsTabGraphicsDeveloperOptions": "خيارات المطور", - "SettingsTabGraphicsShaderDumpPath": "مسار تفريغ الرسومات:", - "SettingsTabLogging": "التسجيل", - "SettingsTabLoggingLogging": "التسجيل", + "SettingsTabGraphicsShaderDumpPath": "مسار تفريغ المظللات:", + "SettingsTabLogging": "تسجيل", + "SettingsTabLoggingLogging": "تسجيل", "SettingsTabLoggingEnableLoggingToFile": "تفعيل التسجيل إلى ملف", "SettingsTabLoggingEnableStubLogs": "تفعيل سجلات الـStub", "SettingsTabLoggingEnableInfoLogs": "تفعيل سجلات المعلومات", @@ -174,8 +179,8 @@ "SettingsTabLoggingEnableErrorLogs": "تفعيل سجلات الأخطاء", "SettingsTabLoggingEnableTraceLogs": "تفعيل سجلات التتبع", "SettingsTabLoggingEnableGuestLogs": "تفعيل سجلات الضيوف", - "SettingsTabLoggingEnableFsAccessLogs": "تمكين سجلات الوصول لـ Fs", - "SettingsTabLoggingFsGlobalAccessLogMode": "وضع سجل وصول عالمي Fs :", + "SettingsTabLoggingEnableFsAccessLogs": "تمكين سجلات الوصول إلى نظام الملفات", + "SettingsTabLoggingFsGlobalAccessLogMode": "وضع سجل الوصول العالمي لنظام الملفات:", "SettingsTabLoggingDeveloperOptions": "خيارات المطور", "SettingsTabLoggingDeveloperOptionsNote": "تحذير: سوف يقلل من الأداء", "SettingsTabLoggingGraphicsBackendLogLevel": "مستوى سجل خلفية الرسومات:", @@ -185,15 +190,15 @@ "SettingsTabLoggingGraphicsBackendLogLevelAll": "الكل", "SettingsTabLoggingEnableDebugLogs": "تمكين سجلات التصحيح", "SettingsTabInput": "الإدخال", - "SettingsTabInputEnableDockedMode": "مركب بالمنصة", - "SettingsTabInputDirectKeyboardAccess": "الوصول المباشر إلى لوحة المفاتيح", + "SettingsTabInputEnableDockedMode": "تركيب بالمنصة", + "SettingsTabInputDirectKeyboardAccess": "الوصول المباشر للوحة المفاتيح", "SettingsButtonSave": "حفظ", "SettingsButtonClose": "إغلاق", "SettingsButtonOk": "موافق", "SettingsButtonCancel": "إلغاء", "SettingsButtonApply": "تطبيق", "ControllerSettingsPlayer": "اللاعب", - "ControllerSettingsPlayer1": "اللاعب ١", + "ControllerSettingsPlayer1": "اللاعب 1", "ControllerSettingsPlayer2": "اللاعب 2", "ControllerSettingsPlayer3": "اللاعب 3", "ControllerSettingsPlayer4": "اللاعب 4", @@ -205,12 +210,12 @@ "ControllerSettingsInputDevice": "جهاز الإدخال", "ControllerSettingsRefresh": "تحديث", "ControllerSettingsDeviceDisabled": "معطل", - "ControllerSettingsControllerType": "نوع ذراع التحكم", + "ControllerSettingsControllerType": "نوع وحدة التحكم", "ControllerSettingsControllerTypeHandheld": "محمول", - "ControllerSettingsControllerTypeProController": "Pro Controller", - "ControllerSettingsControllerTypeJoyConPair": "اقتران جوي كون", - "ControllerSettingsControllerTypeJoyConLeft": "يسار جوي كون", - "ControllerSettingsControllerTypeJoyConRight": "يمين جوي كون", + "ControllerSettingsControllerTypeProController": "وحدة تحكم برو", + "ControllerSettingsControllerTypeJoyConPair": "زوج جوي كون", + "ControllerSettingsControllerTypeJoyConLeft": "جوي كون اليسار ", + "ControllerSettingsControllerTypeJoyConRight": " جوي كون اليمين", "ControllerSettingsProfile": "الملف الشخصي", "ControllerSettingsProfileDefault": "افتراضي", "ControllerSettingsLoad": "تحميل", @@ -223,13 +228,13 @@ "ControllerSettingsButtonY": "Y", "ControllerSettingsButtonPlus": "+", "ControllerSettingsButtonMinus": "-", - "ControllerSettingsDPad": "لوحة الاتجاه", + "ControllerSettingsDPad": "أسهم الاتجاهات", "ControllerSettingsDPadUp": "اعلى", "ControllerSettingsDPadDown": "أسفل", "ControllerSettingsDPadLeft": "يسار", "ControllerSettingsDPadRight": "يمين", "ControllerSettingsStickButton": "زر", - "ControllerSettingsStickUp": "اعلى", + "ControllerSettingsStickUp": "فوق", "ControllerSettingsStickDown": "أسفل", "ControllerSettingsStickLeft": "يسار", "ControllerSettingsStickRight": "يمين", @@ -239,11 +244,11 @@ "ControllerSettingsStickDeadzone": "المنطقة الميتة:", "ControllerSettingsLStick": "العصا اليسرى", "ControllerSettingsRStick": "العصا اليمنى", - "ControllerSettingsTriggersLeft": "المحفزات اليسرى", - "ControllerSettingsTriggersRight": "المحفزات اليمني", - "ControllerSettingsTriggersButtonsLeft": "أزرار التحفيز اليسرى", - "ControllerSettingsTriggersButtonsRight": "أزرار التحفيز اليمنى", - "ControllerSettingsTriggers": "المحفزات", + "ControllerSettingsTriggersLeft": "الأزندة اليسرى", + "ControllerSettingsTriggersRight": "الأزندة اليمني", + "ControllerSettingsTriggersButtonsLeft": "أزرار الزناد اليسرى", + "ControllerSettingsTriggersButtonsRight": "أزرار الزناد اليمنى", + "ControllerSettingsTriggers": "أزندة", "ControllerSettingsTriggerL": "L", "ControllerSettingsTriggerR": "R", "ControllerSettingsTriggerZL": "ZL", @@ -258,27 +263,128 @@ "ControllerSettingsTriggerThreshold": "قوة التحفيز:", "ControllerSettingsMotion": "الحركة", "ControllerSettingsMotionUseCemuhookCompatibleMotion": "استخدام الحركة المتوافقة مع CemuHook", - "ControllerSettingsMotionControllerSlot": "خانة ذراع التحكم:", + "ControllerSettingsMotionControllerSlot": "خانة وحدة التحكم:", "ControllerSettingsMotionMirrorInput": "إعادة الإدخال", - "ControllerSettingsMotionRightJoyConSlot": "خانة اليمين JoyCon :", + "ControllerSettingsMotionRightJoyConSlot": "خانة جويكون اليمين :", "ControllerSettingsMotionServerHost": "مضيف الخادم:", - "ControllerSettingsMotionGyroSensitivity": "حساسية الغيرو:", - "ControllerSettingsMotionGyroDeadzone": "منطقة الغيرو الميتة:", + "ControllerSettingsMotionGyroSensitivity": "حساسية مستشعر الحركة:", + "ControllerSettingsMotionGyroDeadzone": "منطقة مستشعر الحركة الميتة:", "ControllerSettingsSave": "حفظ", "ControllerSettingsClose": "إغلاق", + "KeyUnknown": "مجهول", + "KeyShiftLeft": "زر ‫Shift الأيسر", + "KeyShiftRight": "زر ‫Shift الأيمن", + "KeyControlLeft": "زر ‫Ctrl الأيسر", + "KeyMacControlLeft": "زر ⌃ الأيسر", + "KeyControlRight": "زر ‫Ctrl الأيمن", + "KeyMacControlRight": "زر ⌃ الأيمن", + "KeyAltLeft": "زر ‫Alt الأيسر", + "KeyMacAltLeft": "زر ⌥ الأيسر", + "KeyAltRight": "زر ‫Alt الأيمن", + "KeyMacAltRight": "زر ⌥ الأيمن", + "KeyWinLeft": "زر ⊞ الأيسر", + "KeyMacWinLeft": "زر ⌘ الأيسر", + "KeyWinRight": "زر ⊞ الأيمن", + "KeyMacWinRight": "زر ⌘ الأيمن", + "KeyMenu": "زر القائمة", + "KeyUp": "فوق", + "KeyDown": "اسفل", + "KeyLeft": "يسار", + "KeyRight": "يمين", + "KeyEnter": "مفتاح الإدخال", + "KeyEscape": "زر ‫Escape", + "KeySpace": "مسافة", + "KeyTab": "زر ‫Tab", + "KeyBackSpace": "زر المسح للخلف", + "KeyInsert": "زر Insert", + "KeyDelete": "زر الحذف", + "KeyPageUp": "زر ‫Page Up", + "KeyPageDown": "زر ‫Page Down", + "KeyHome": "زر ‫Home", + "KeyEnd": "زر ‫End", + "KeyCapsLock": "زر الحروف الكبيرة", + "KeyScrollLock": "زر ‫Scroll Lock", + "KeyPrintScreen": "زر ‫Print Screen", + "KeyPause": "زر Pause", + "KeyNumLock": "زر Num Lock", + "KeyClear": "زر Clear", + "KeyKeypad0": "لوحة الأرقام 0", + "KeyKeypad1": "لوحة الأرقام 1", + "KeyKeypad2": "لوحة الأرقام 2", + "KeyKeypad3": "لوحة الأرقام 3", + "KeyKeypad4": "لوحة الأرقام 4", + "KeyKeypad5": "لوحة الأرقام 5", + "KeyKeypad6": "لوحة الأرقام 6", + "KeyKeypad7": "لوحة الأرقام 7", + "KeyKeypad8": "لوحة الأرقام 8", + "KeyKeypad9": "لوحة الأرقام 9", + "KeyKeypadDivide": "لوحة الأرقام علامة القسمة", + "KeyKeypadMultiply": "لوحة الأرقام علامة الضرب", + "KeyKeypadSubtract": "لوحة الأرقام علامة الطرح\n", + "KeyKeypadAdd": "لوحة الأرقام علامة الزائد", + "KeyKeypadDecimal": "لوحة الأرقام الفاصلة العشرية", + "KeyKeypadEnter": "لوحة الأرقام زر الإدخال", + "KeyNumber0": "٠", + "KeyNumber1": "١", + "KeyNumber2": "٢", + "KeyNumber3": "٣", + "KeyNumber4": "٤", + "KeyNumber5": "٥", + "KeyNumber6": "٦", + "KeyNumber7": "٧", + "KeyNumber8": "٨", + "KeyNumber9": "٩", + "KeyTilde": "~", + "KeyGrave": "`", + "KeyMinus": "-", + "KeyPlus": "+", + "KeyBracketLeft": "[", + "KeyBracketRight": "]", + "KeySemicolon": ";", + "KeyQuote": "\"", + "KeyComma": ",", + "KeyPeriod": ".", + "KeySlash": "/", + "KeyBackSlash": "\\", + "KeyUnbound": "غير مرتبط", + "GamepadLeftStick": "زر عصا التحكم اليسرى", + "GamepadRightStick": "زر عصا التحكم اليمنى", + "GamepadLeftShoulder": "زر الكتف الأيسر‫ L", + "GamepadRightShoulder": "زر الكتف الأيمن‫ R", + "GamepadLeftTrigger": "زر الزناد الأيسر‫ (ZL)", + "GamepadRightTrigger": "زر الزناد الأيمن‫ (ZR)", + "GamepadDpadUp": "فوق", + "GamepadDpadDown": "اسفل", + "GamepadDpadLeft": "يسار", + "GamepadDpadRight": "يمين", + "GamepadMinus": "-", + "GamepadPlus": "+", + "GamepadGuide": "دليل", + "GamepadMisc1": "متنوع", + "GamepadPaddle1": "Paddle 1", + "GamepadPaddle2": "Paddle 2", + "GamepadPaddle3": "Paddle 3", + "GamepadPaddle4": "Paddle 4", + "GamepadTouchpad": "لوحة اللمس", + "GamepadSingleLeftTrigger0": "زر الزناد الأيسر 0", + "GamepadSingleRightTrigger0": "زر الزناد الأيمن 0", + "GamepadSingleLeftTrigger1": "زر الزناد الأيسر 1", + "GamepadSingleRightTrigger1": "زر الزناد الأيمن 1", + "StickLeft": "عصا التحكم اليسرى", + "StickRight": "عصا التحكم اليمنى", "UserProfilesSelectedUserProfile": "الملف الشخصي المحدد للمستخدم:", "UserProfilesSaveProfileName": "حفظ اسم الملف الشخصي", "UserProfilesChangeProfileImage": "تغيير صورة الملف الشخصي", "UserProfilesAvailableUserProfiles": "الملفات الشخصية للمستخدم المتاحة:", - "UserProfilesAddNewProfile": "أنشئ ملف شخصي", + "UserProfilesAddNewProfile": "إنشاء ملف الشخصي", "UserProfilesDelete": "حذف", "UserProfilesClose": "إغلاق", "ProfileNameSelectionWatermark": "اختر اسم مستعار", "ProfileImageSelectionTitle": "تحديد صورة الملف الشخصي", "ProfileImageSelectionHeader": "اختر صورة الملف الشخصي", - "ProfileImageSelectionNote": "يمكنك استيراد صورة ملف تعريف مخصصة، أو تحديد صورة رمزية من البرامج الثابتة للنظام", + "ProfileImageSelectionNote": "يمكنك استيراد صورة ملف شخصي مخصصة، أو تحديد صورة رمزية من البرامج الثابتة للنظام", "ProfileImageSelectionImportImage": "استيراد ملف الصورة", - "ProfileImageSelectionSelectAvatar": "حدد الصورة الرمزية للبرنامج الثابت", + "ProfileImageSelectionSelectAvatar": "حدد الصورة الرمزية من البرنامج الثابتة", "InputDialogTitle": "حوار الإدخال", "InputDialogOk": "موافق", "InputDialogCancel": "إلغاء", @@ -289,13 +395,13 @@ "AvatarSetBackgroundColor": "تعيين لون الخلفية", "AvatarClose": "إغلاق", "ControllerSettingsLoadProfileToolTip": "تحميل الملف الشخصي", - "ControllerSettingsAddProfileToolTip": "إضافة ملف تعريف", - "ControllerSettingsRemoveProfileToolTip": "إزالة ملف التعريف", - "ControllerSettingsSaveProfileToolTip": "حفظ ملف التعريف", + "ControllerSettingsAddProfileToolTip": "إضافة ملف شخصي", + "ControllerSettingsRemoveProfileToolTip": "إزالة الملف الشخصي", + "ControllerSettingsSaveProfileToolTip": "حفظ الملف الشخصي", "MenuBarFileToolsTakeScreenshot": "أخذ لقطة للشاشة", "MenuBarFileToolsHideUi": "إخفاء واجهة المستخدم", "GameListContextMenuRunApplication": "تشغيل التطبيق", - "GameListContextMenuToggleFavorite": "تبديل المفضلة", + "GameListContextMenuToggleFavorite": "تعيين كمفضل", "GameListContextMenuToggleFavoriteToolTip": "تبديل الحالة المفضلة للعبة", "SettingsTabGeneralTheme": "السمة:", "SettingsTabGeneralThemeDark": "داكن", @@ -304,44 +410,44 @@ "ControllerSettingsRumble": "الاهتزاز", "ControllerSettingsRumbleStrongMultiplier": "مضاعف اهتزاز قوي", "ControllerSettingsRumbleWeakMultiplier": "مضاعف اهتزاز ضعيف", - "DialogMessageSaveNotAvailableMessage": "لا يوجد حفظ لـ {0} [{1:x16}]", - "DialogMessageSaveNotAvailableCreateSaveMessage": "هل ترغب في إنشاء حفظ لهذه اللعبة؟", - "DialogConfirmationTitle": "Ryujinx - تأكيد", - "DialogUpdaterTitle": "تحديث Ryujinx", - "DialogErrorTitle": "Ryujinx - خطأ", - "DialogWarningTitle": "Ryujinx - تحذير", - "DialogExitTitle": "Ryujinx - الخروج", - "DialogErrorMessage": "واجه Ryujinx خطأ", - "DialogExitMessage": "هل أنت متأكد من أنك تريد إغلاق Ryujinx؟", + "DialogMessageSaveNotAvailableMessage": "لا توجد بيانات الحفظ لـ {0} [{1:x16}]", + "DialogMessageSaveNotAvailableCreateSaveMessage": "هل ترغب في إنشاء بيانات الحفظ لهذه اللعبة؟", + "DialogConfirmationTitle": "ريوجينكس - تأكيد", + "DialogUpdaterTitle": "ريوجينكس - المحدث", + "DialogErrorTitle": "ريوجينكس - خطأ", + "DialogWarningTitle": "ريوجينكس - تحذير", + "DialogExitTitle": "ريوجينكس - الخروج", + "DialogErrorMessage": "واجه ريوجينكس خطأ", + "DialogExitMessage": "هل أنت متأكد من أنك تريد إغلاق ريوجينكس؟", "DialogExitSubMessage": "سيتم فقدان كافة البيانات غير المحفوظة!", - "DialogMessageCreateSaveErrorMessage": "حدث خطأ أثناء إنشاء المحفوظة المحددة: {0}", - "DialogMessageFindSaveErrorMessage": "حدث خطأ أثناء البحث عن البيانات المحفوظة المحددة: {0}", + "DialogMessageCreateSaveErrorMessage": "حدث خطأ أثناء إنشاء بيانات الحفظ المحددة: {0}", + "DialogMessageFindSaveErrorMessage": "حدث خطأ أثناء البحث عن بيانات الحفظ المحددة: {0}", "FolderDialogExtractTitle": "اختر المجلد الذي تريد الاستخراج إليه", "DialogNcaExtractionMessage": "استخراج قسم {0} من {1}...", - "DialogNcaExtractionTitle": "Ryujinx - مستخرج قسم NCA", - "DialogNcaExtractionMainNcaNotFoundErrorMessage": "فشل الاستخراج. لم يكن NCA الرئيسي موجودًا في الملف المحدد.", - "DialogNcaExtractionCheckLogErrorMessage": "فشل الاستخراج. اقرأ ملف السجل لمزيد من المعلومات.", + "DialogNcaExtractionTitle": "ريوجينكس - مستخرج قسم NCA", + "DialogNcaExtractionMainNcaNotFoundErrorMessage": "فشل الاستخراج. لم يكن NCA الرئيسي موجودا في الملف المحدد.", + "DialogNcaExtractionCheckLogErrorMessage": "فشل الاستخراج. اقرأ ملف التسجيل لمزيد من المعلومات.", "DialogNcaExtractionSuccessMessage": "تم الاستخراج بنجاح.", - "DialogUpdaterConvertFailedMessage": "فشل تحويل إصدار Ryujinx الحالي.", + "DialogUpdaterConvertFailedMessage": "فشل تحويل إصدار ريوجينكس الحالي.", "DialogUpdaterCancelUpdateMessage": "إلغاء التحديث", - "DialogUpdaterAlreadyOnLatestVersionMessage": "أنت تستخدم بالفعل أحدث إصدار من Ryujinx!", - "DialogUpdaterFailedToGetVersionMessage": "حدث خطأ أثناء محاولة الحصول على معلومات الإصدار من إصدار GitHub. يمكن أن يحدث هذا إذا تم تجميع إصدار جديد بواسطة GitHub Actions. جرب مجددا بعد دقائق.", - "DialogUpdaterConvertFailedGithubMessage": "فشل تحويل إصدار Ryujinx المستلم من إصدار Github.", - "DialogUpdaterDownloadingMessage": "تحميل التحديث...", - "DialogUpdaterExtractionMessage": "استخراج التحديث...", + "DialogUpdaterAlreadyOnLatestVersionMessage": "أنت تستخدم بالفعل أحدث إصدار من ريوجينكس!", + "DialogUpdaterFailedToGetVersionMessage": "حدث خطأ أثناء محاولة الحصول على معلومات الإصدار من إصدار غيت هاب. يمكن أن يحدث هذا إذا تم تجميع إصدار جديد بواسطة إجراءات غيت هاب. جرب مجددا بعد دقائق.", + "DialogUpdaterConvertFailedGithubMessage": "فشل تحويل إصدار ريوجينكس المستلم من إصدار غيت هاب.", + "DialogUpdaterDownloadingMessage": "جاري تنزيل التحديث...", + "DialogUpdaterExtractionMessage": "جاري استخراج التحديث...", "DialogUpdaterRenamingMessage": "إعادة تسمية التحديث...", "DialogUpdaterAddingFilesMessage": "إضافة تحديث جديد...", "DialogUpdaterCompleteMessage": "اكتمل التحديث", - "DialogUpdaterRestartMessage": "هل تريد إعادة تشغيل Ryujinx الآن؟", + "DialogUpdaterRestartMessage": "هل تريد إعادة تشغيل ريوجينكس الآن؟", "DialogUpdaterNoInternetMessage": "أنت غير متصل بالإنترنت.", "DialogUpdaterNoInternetSubMessage": "يرجى التحقق من أن لديك اتصال إنترنت فعال!", - "DialogUpdaterDirtyBuildMessage": "لا يمكنك تحديث البناء القذر من Ryujinx!", - "DialogUpdaterDirtyBuildSubMessage": "الرجاء تحميل Ryujinx على https://ryujinx.org/ إذا كنت تبحث عن إصدار مدعوم.", + "DialogUpdaterDirtyBuildMessage": "لا يمكنك تحديث نسخة القذرة من ريوجينكس!", + "DialogUpdaterDirtyBuildSubMessage": "الرجاء تحميل ريوجينكس من https://ryujinx.org إذا كنت تبحث عن إصدار مدعوم.", "DialogRestartRequiredMessage": "يتطلب إعادة التشغيل", "DialogThemeRestartMessage": "تم حفظ السمة. إعادة التشغيل مطلوبة لتطبيق السمة.", "DialogThemeRestartSubMessage": "هل تريد إعادة التشغيل", "DialogFirmwareInstallEmbeddedMessage": "هل ترغب في تثبيت البرنامج الثابت المدمج في هذه اللعبة؟ (البرنامج الثابت {0})", - "DialogFirmwareInstallEmbeddedSuccessMessage": "لم يتم العثور على أي برنامج ثابت مثبت ولكن Ryujinx كان قادراً على تثبيت البرنامج الثابت {0} من اللعبة المقدمة.\nسيبدأ المحاكي الآن.", + "DialogFirmwareInstallEmbeddedSuccessMessage": "لم يتم العثور على أي برنامج ثابت مثبت ولكن ريوجينكس كان قادرا على تثبيت البرنامج الثابت {0} من اللعبة المقدمة.\nسيبدأ المحاكي الآن.", "DialogFirmwareNoFirmwareInstalledMessage": "لا يوجد برنامج ثابت مثبت", "DialogFirmwareInstalledMessage": "تم تثبيت البرنامج الثابت {0}", "DialogInstallFileTypesSuccessMessage": "تم تثبيت أنواع الملفات بنجاح!", @@ -349,38 +455,38 @@ "DialogUninstallFileTypesSuccessMessage": "تم إلغاء تثبيت أنواع الملفات بنجاح!", "DialogUninstallFileTypesErrorMessage": "فشل إلغاء تثبيت أنواع الملفات.", "DialogOpenSettingsWindowLabel": "فتح نافذة الإعدادات", - "DialogControllerAppletTitle": "برنامج التحكم", + "DialogControllerAppletTitle": "تطبيق وحدة التحكم المصغر", "DialogMessageDialogErrorExceptionMessage": "خطأ في عرض مربع حوار الرسالة: {0}", "DialogSoftwareKeyboardErrorExceptionMessage": "خطأ في عرض لوحة مفاتيح البرامج: {0}", - "DialogErrorAppletErrorExceptionMessage": "Error displaying ErrorApplet Dialog: {0}", + "DialogErrorAppletErrorExceptionMessage": "خطأ في عرض مربع حوار خطأ التطبيق المصغر: {0}", "DialogUserErrorDialogMessage": "{0}: {1}", "DialogUserErrorDialogInfoMessage": "لمزيد من المعلومات حول كيفية إصلاح هذا الخطأ، اتبع دليل الإعداد الخاص بنا.", "DialogUserErrorDialogTitle": "خطأ ريوجينكس ({0})", "DialogAmiiboApiTitle": "أميبو API", - "DialogAmiiboApiFailFetchMessage": "حدث خطأ أثناء جلب المعلومات من واجهة برمجة التطبيقات.", - "DialogAmiiboApiConnectErrorMessage": "غير قادر على الاتصال بخادم API Amiibo. قد تكون الخدمة متوقفة أو قد تحتاج إلى التحقق من اتصال الإنترنت الخاص بك على الإنترنت.", + "DialogAmiiboApiFailFetchMessage": "حدث خطأ أثناء جلب المعلومات من API.", + "DialogAmiiboApiConnectErrorMessage": "غير قادر على الاتصال بخادم API أميبو. قد تكون الخدمة معطلة أو قد تحتاج إلى التحقق من اتصالك بالإنترنت.", "DialogProfileInvalidProfileErrorMessage": "الملف الشخصي {0} غير متوافق مع نظام تكوين الإدخال الحالي.", "DialogProfileDefaultProfileOverwriteErrorMessage": "لا يمكن الكتابة فوق الملف الشخصي الافتراضي", - "DialogProfileDeleteProfileTitle": "حذف ملف التعريف", + "DialogProfileDeleteProfileTitle": "حذف الملف الشخصي", "DialogProfileDeleteProfileMessage": "هذا الإجراء لا رجعة فيه، هل أنت متأكد من أنك تريد المتابعة؟", "DialogWarning": "تحذير", - "DialogPPTCDeletionMessage": "أنت على وشك الإنتظار لإعادة بناء PTC على التمهيد التالي :\n\n{0}\n\nهل أنت متأكد من أنك تريد المتابعة؟", - "DialogPPTCDeletionErrorMessage": "خطأ في إزالة ذاكرة التخزين المؤقت PPTC في {0}: {1}", - "DialogShaderDeletionMessage": "أنت على وشك حذف ذاكرة التخزين المؤقت لـ Shader من أجل:\n\n{0}\n\nهل انت متأكد انك تريد المتابعة؟", - "DialogShaderDeletionErrorMessage": "Error purging Shader cache at {0}: {1}", - "DialogRyujinxErrorMessage": "واجه Ryujinx خطأ", + "DialogPPTCDeletionMessage": "أنت على وشك الإنتظار لإعادة بناء ذاكرة التخزين المؤقت للترجمة المستمرة (PPTC) عند الإقلاع التالي لـ:\n\n{0}\n\nأمتأكد من رغبتك في المتابعة؟", + "DialogPPTCDeletionErrorMessage": "خطأ خلال تنظيف ذاكرة التخزين المؤقت للترجمة المستمرة (PPTC) في {0}: {1}", + "DialogShaderDeletionMessage": "أنت على وشك حذف ذاكرة المظللات المؤقتة ل:\n\n{0}\n\nهل انت متأكد انك تريد المتابعة؟", + "DialogShaderDeletionErrorMessage": "حدث خطأ أثناء تنظيف ذاكرة المظللات المؤقتة في {0}: {1}", + "DialogRyujinxErrorMessage": "واجه ريوجينكس خطأ", "DialogInvalidTitleIdErrorMessage": "خطأ في واجهة المستخدم: اللعبة المحددة لم يكن لديها معرف عنوان صالح", - "DialogFirmwareInstallerFirmwareNotFoundErrorMessage": "لم يتم العثور على فريموير للنظام صالح في {0}.", + "DialogFirmwareInstallerFirmwareNotFoundErrorMessage": "لم يتم العثور على برنامج ثابت للنظام صالح في {0}.", "DialogFirmwareInstallerFirmwareInstallTitle": "تثبيت البرنامج الثابت {0}", "DialogFirmwareInstallerFirmwareInstallMessage": "سيتم تثبيت إصدار النظام {0}.", "DialogFirmwareInstallerFirmwareInstallSubMessage": "\n\nهذا سيحل محل إصدار النظام الحالي {0}.", "DialogFirmwareInstallerFirmwareInstallConfirmMessage": "\nهل تريد المتابعة؟", "DialogFirmwareInstallerFirmwareInstallWaitMessage": "تثبيت البرنامج الثابت...", "DialogFirmwareInstallerFirmwareInstallSuccessMessage": "تم تثبيت إصدار النظام {0} بنجاح.", - "DialogUserProfileDeletionWarningMessage": "لن تكون هناك ملفات تعريف أخرى لفتحها إذا تم حذف الملف الشخصي المحدد", + "DialogUserProfileDeletionWarningMessage": "لن تكون هناك ملفات الشخصية أخرى لفتحها إذا تم حذف الملف الشخصي المحدد", "DialogUserProfileDeletionConfirmMessage": "هل تريد حذف الملف الشخصي المحدد", - "DialogUserProfileUnsavedChangesTitle": "تحذير - التغييرات غير المحفوظة", - "DialogUserProfileUnsavedChangesMessage": "لقد قمت بإجراء تغييرات على ملف تعريف المستخدم هذا ولم يتم حفظها.", + "DialogUserProfileUnsavedChangesTitle": "تحذير - التغييرات غير محفوظة", + "DialogUserProfileUnsavedChangesMessage": "لقد قمت بإجراء تغييرات على الملف الشخصي لهذا المستخدم هذا ولم يتم حفظها.", "DialogUserProfileUnsavedChangesSubMessage": "هل تريد تجاهل التغييرات؟", "DialogControllerSettingsModifiedConfirmMessage": "تم تحديث إعدادات وحدة التحكم الحالية.", "DialogControllerSettingsModifiedConfirmSubMessage": "هل تريد الحفظ ؟", @@ -388,29 +494,29 @@ "DialogModAlreadyExistsMessage": "التعديل موجود بالفعل", "DialogModInvalidMessage": "المجلد المحدد لا يحتوي على تعديل!", "DialogModDeleteNoParentMessage": "فشل الحذف: لم يمكن العثور على المجلد الرئيسي للتعديل\"{0}\"!", - "DialogDlcNoDlcErrorMessage": "الملف المحدد لا يحتوي على DLC للعنوان المحدد!", + "DialogDlcNoDlcErrorMessage": "الملف المحدد لا يحتوي على محتوى إضافي للعنوان المحدد!", "DialogPerformanceCheckLoggingEnabledMessage": "لقد تم تمكين تسجيل التتبع، والذي تم تصميمه ليتم استخدامه من قبل المطورين فقط.", "DialogPerformanceCheckLoggingEnabledConfirmMessage": "للحصول على الأداء الأمثل، يوصى بتعطيل تسجيل التتبع. هل ترغب في تعطيل تسجيل التتبع الآن؟", - "DialogPerformanceCheckShaderDumpEnabledMessage": "لقد قمت بتمكين تفريغ التظليل، والذي تم تصميمه ليستخدمه المطورون فقط.", - "DialogPerformanceCheckShaderDumpEnabledConfirmMessage": "For optimal performance, it's recommended to disable shader dumping. Would you like to disable shader dumping now?", + "DialogPerformanceCheckShaderDumpEnabledMessage": "لقد قمت بتمكين تفريغ المظللات، والذي تم تصميمه ليستخدمه المطورون فقط.", + "DialogPerformanceCheckShaderDumpEnabledConfirmMessage": "للحصول على الأداء الأمثل، يوصى بتعطيل تفريغ المظللات. هل ترغب في تعطيل تفريغ المظللات الآن؟", "DialogLoadAppGameAlreadyLoadedMessage": "تم تحميل لعبة بالفعل", "DialogLoadAppGameAlreadyLoadedSubMessage": "الرجاء إيقاف المحاكاة أو إغلاق المحاكي قبل بدء لعبة أخرى.", - "DialogUpdateAddUpdateErrorMessage": "الملف المحدد لا يحتوي على تحديث للملف المحدد!", - "DialogSettingsBackendThreadingWarningTitle": "Warning - Backend Threading", - "DialogSettingsBackendThreadingWarningMessage": "Ryujinx must be restarted after changing this option for it to apply fully. Depending on your platform, you may need to manually disable your driver's own multithreading when using Ryujinx's.", + "DialogUpdateAddUpdateErrorMessage": "الملف المحدد لا يحتوي على تحديث للعنوان المحدد!", + "DialogSettingsBackendThreadingWarningTitle": "تحذير - خلفية متعددة المسارات", + "DialogSettingsBackendThreadingWarningMessage": "يجب إعادة تشغيل ريوجينكس بعد تغيير هذا الخيار حتى يتم تطبيقه بالكامل. اعتمادا على النظام الأساسي الخاص بك، قد تحتاج إلى تعطيل تعدد المسارات الخاص ببرنامج الرسومات التشغيل الخاص بك يدويًا عند استخدام الخاص بريوجينكس.", "DialogModManagerDeletionWarningMessage": "أنت على وشك حذف التعديل: {0}\n\nهل انت متأكد انك تريد المتابعة؟", "DialogModManagerDeletionAllWarningMessage": "أنت على وشك حذف كافة التعديلات لهذا العنوان.\n\nهل انت متأكد انك تريد المتابعة؟", "SettingsTabGraphicsFeaturesOptions": "المميزات", - "SettingsTabGraphicsBackendMultithreading": "Graphics Backend Multithreading:", + "SettingsTabGraphicsBackendMultithreading": "تعدد المسارات لخلفية الرسومات:", "CommonAuto": "تلقائي", - "CommonOff": "إيقاف", + "CommonOff": "معطل", "CommonOn": "تشغيل", "InputDialogYes": "نعم", "InputDialogNo": "لا", "DialogProfileInvalidProfileNameErrorMessage": "يحتوي اسم الملف على أحرف غير صالحة. يرجى المحاولة مرة أخرى.", "MenuBarOptionsPauseEmulation": "إيقاف مؤقت", "MenuBarOptionsResumeEmulation": "استئناف", - "AboutUrlTooltipMessage": "انقر لفتح موقع Ryujinx في متصفحك الافتراضي.", + "AboutUrlTooltipMessage": "انقر لفتح موقع ريوجينكس في متصفحك الافتراضي.", "AboutDisclaimerMessage": "ريوجينكس لا ينتمي إلى نينتندو™،\nأو أي من شركائها بأي شكل من الأشكال.", "AboutAmiiboDisclaimerMessage": "AmiiboAPI (www.amiiboapi.com) يتم \nاستخدامه في محاكاة أمبيو لدينا.", "AboutPatreonUrlTooltipMessage": "انقر لفتح صفحة ريوجينكس في باتريون في متصفحك الافتراضي.", @@ -419,18 +525,18 @@ "AboutTwitterUrlTooltipMessage": "انقر لفتح صفحة ريوجينكس في تويتر في متصفحك الافتراضي.", "AboutRyujinxAboutTitle": "حول:", "AboutRyujinxAboutContent": "ريوجينكس هو محاكي لجهاز نينتندو سويتش™.\nمن فضلك ادعمنا على باتريون.\nاحصل على آخر الأخبار على تويتر أو ديسكورد.\nيمكن للمطورين المهتمين بالمساهمة معرفة المزيد على غيت هاب أو ديسكورد.", - "AboutRyujinxMaintainersTitle": "تم إصلاحها بواسطة:", + "AboutRyujinxMaintainersTitle": "تتم صيانته بواسطة:", "AboutRyujinxMaintainersContentTooltipMessage": "انقر لفتح صفحة المساهمين في متصفحك الافتراضي.", "AboutRyujinxSupprtersTitle": "مدعوم على باتريون بواسطة:", "AmiiboSeriesLabel": "مجموعة أميبو", "AmiiboCharacterLabel": "شخصية", "AmiiboScanButtonLabel": "فحصه", "AmiiboOptionsShowAllLabel": "إظهار كل أميبو", - "AmiiboOptionsUsRandomTagLabel": "Hack: Use Random tag Uuid", + "AmiiboOptionsUsRandomTagLabel": "هاك: استخدم علامة Uuid عشوائية ", "DlcManagerTableHeadingEnabledLabel": "مفعل", "DlcManagerTableHeadingTitleIdLabel": "معرف العنوان", "DlcManagerTableHeadingContainerPathLabel": "مسار الحاوية", - "DlcManagerTableHeadingFullPathLabel": "المسار كاملاً", + "DlcManagerTableHeadingFullPathLabel": "المسار كاملا", "DlcManagerRemoveAllButton": "حذف الكل", "DlcManagerEnableAllButton": "تشغيل الكل", "DlcManagerDisableAllButton": "تعطيل الكل", @@ -440,100 +546,100 @@ "CommonSort": "فرز", "CommonShowNames": "عرض الأسماء", "CommonFavorite": "المفضلة", - "OrderAscending": "ترتيب تصاعدي", - "OrderDescending": "ترتيب تنازلي", + "OrderAscending": "تصاعدي", + "OrderDescending": "تنازلي", "SettingsTabGraphicsFeatures": "الميزات والتحسينات", "ErrorWindowTitle": "نافذة الخطأ", - "ToggleDiscordTooltip": "اختر ما إذا كنت تريد عرض ريوجينكس في نشاط ديسكورد \"يتم تشغيله حاليًا\" أم لا", - "AddGameDirBoxTooltip": "أدخل دليل اللعبة لإضافته إلى القائمة", - "AddGameDirTooltip": "إضافة دليل اللعبة إلى القائمة", - "RemoveGameDirTooltip": "إزالة دليل اللعبة المحدد", + "ToggleDiscordTooltip": "اختر ما إذا كنت تريد عرض ريوجينكس في نشاط ديسكورد \"يتم تشغيله حاليا\" أم لا", + "AddGameDirBoxTooltip": "أدخل مجلد اللعبة لإضافته إلى القائمة", + "AddGameDirTooltip": "إضافة مجلد اللعبة إلى القائمة", + "RemoveGameDirTooltip": "إزالة مجلد اللعبة المحدد", "CustomThemeCheckTooltip": "استخدم سمة أفالونيا المخصصة لواجهة المستخدم الرسومية لتغيير مظهر قوائم المحاكي", "CustomThemePathTooltip": "مسار سمة واجهة المستخدم المخصصة", "CustomThemeBrowseTooltip": "تصفح للحصول على سمة واجهة المستخدم المخصصة", - "DockModeToggleTooltip": "يجعل وضع الإرساء النظام الذي تمت محاكاته بمثابة جهاز نينتندو سويتش الذي تم إرساءه. يؤدي هذا إلى تحسين الدقة الرسومية في معظم الألعاب. على العكس من ذلك، سيؤدي تعطيل هذا إلى جعل النظام الذي تمت محاكاته يعمل كجهاز نينتندو سويتش محمول، مما يقلل من جودة الرسومات.\n\nقم بتكوين عناصر تحكم اللاعب 1 إذا كنت تخطط لاستخدام وضع الإرساء؛ قم بتكوين عناصر التحكم المحمولة إذا كنت تخطط لاستخدام الوضع المحمول.\n\nاتركه مشغل إذا لم تكن متأكدًا.", - "DirectKeyboardTooltip": "دعم الوصول المباشر إلى لوحة المفاتيح (HID). يوفر وصول الألعاب إلى لوحة المفاتيح الخاصة بك كجهاز لإدخال النص.\n\nيعمل فقط مع الألعاب التي تدعم استخدام لوحة المفاتيح في الأصل على أجهزة سويتش.\n\nاتركه معطلا إذا كنت غير متأكد.", - "DirectMouseTooltip": "دعم الوصول المباشر إلى لوحة المفاتيح (HID). يوفر وصول الألعاب إلى لوحة المفاتيح الخاصة بك كجهاز لإدخال النص.\n\nيعمل فقط مع الألعاب التي تدعم استخدام لوحة المفاتيح في الأصل على أجهزة سويتش.\n\nاتركه معطلا إذا كنت غير متأكد.", + "DockModeToggleTooltip": "يجعل وضع تركيب بالمنصة النظام الذي تمت محاكاته بمثابة جهاز نينتندو سويتش الذي تم تركيبه بالمنصة. يؤدي هذا إلى تحسين الدقة الرسومية في معظم الألعاب. على العكس من ذلك، سيؤدي تعطيل هذا إلى جعل النظام الذي تمت محاكاته يعمل كجهاز نينتندو سويتش محمول، مما يقلل من جودة الرسومات.\n\nقم بتكوين عناصر تحكم اللاعب 1 إذا كنت تخطط لاستخدام وضع تركيب بالمنصة؛ قم بتكوين عناصر التحكم المحمولة إذا كنت تخطط لاستخدام الوضع المحمول.\n\nاتركه مشغل إذا لم تكن متأكدا.", + "DirectKeyboardTooltip": "دعم الوصول المباشر للوحة المفاتيح (HID). يوفر وصول الألعاب إلى لوحة المفاتيح الخاصة بك كجهاز لإدخال النص.\n\nيعمل فقط مع الألعاب التي تدعم استخدام لوحة المفاتيح في الأصل على أجهزة سويتش.\n\nاتركه معطلا إذا كنت غير متأكد.", + "DirectMouseTooltip": "دعم الوصول المباشر للوحة المفاتيح (HID). يوفر وصول الألعاب إلى لوحة المفاتيح الخاصة بك كجهاز لإدخال النص.\n\nيعمل فقط مع الألعاب التي تدعم استخدام لوحة المفاتيح في الأصل على أجهزة سويتش.\n\nاتركه معطلا إذا كنت غير متأكد.", "RegionTooltip": "تغيير منطقة النظام", "LanguageTooltip": "تغيير لغة النظام", - "TimezoneTooltip": "تغيير المنطقة الزمنية للنظام", + "TimezoneTooltip": "تغيير النطاق الزمني للنظام", "TimeTooltip": "تغيير وقت النظام", - "VSyncToggleTooltip": "Emulated console's Vertical Sync. Essentially a frame-limiter for the majority of games; disabling it may cause games to run at higher speed or make loading screens take longer or get stuck.\n\nCan be toggled in-game with a hotkey of your preference (F1 by default). We recommend doing this if you plan on disabling it.\n\nLeave ON if unsure.", - "PptcToggleTooltip": "Saves translated JIT functions so that they do not need to be translated every time the game loads.\n\nReduces stuttering and significantly speeds up boot times after the first boot of a game.\n\nLeave ON if unsure.", + "VSyncToggleTooltip": "محاكاة المزامنة العمودية للجهاز. في الأساس محدد الإطار لغالبية الألعاب؛ قد يؤدي تعطيله إلى تشغيل الألعاب بسرعة أعلى أو جعل شاشات التحميل تستغرق وقتا أطول أو تتعطل.\n\nيمكن تبديله داخل اللعبة باستخدام مفتاح التشغيل السريع الذي تفضله (F1 افتراضيا). نوصي بالقيام بذلك إذا كنت تخطط لتعطيله.\n\nاتركه ممكنا إذا لم تكن متأكدا.", + "PptcToggleTooltip": "يحفظ وظائف JIT المترجمة بحيث لا تحتاج إلى ترجمتها في كل مرة يتم فيها تحميل اللعبة.\n\nيقلل من التقطيع ويسرع بشكل ملحوظ أوقات التشغيل بعد التشغيل الأول للعبة.\n\nاتركه ممكنا إذا لم تكن متأكدا.", "FsIntegrityToggleTooltip": "يتحقق من وجود ملفات تالفة عند تشغيل لعبة ما، وإذا تم اكتشاف ملفات تالفة، فسيتم عرض خطأ تجزئة في السجل.\n\nليس له أي تأثير على الأداء ويهدف إلى المساعدة في استكشاف الأخطاء وإصلاحها.\n\nاتركه مفعلا إذا كنت غير متأكد.", - "AudioBackendTooltip": "Changes the backend used to render audio.\n\nSDL2 is the preferred one, while OpenAL and SoundIO are used as fallbacks. Dummy will have no sound.\n\nSet to SDL2 if unsure.", - "MemoryManagerTooltip": "Change how guest memory is mapped and accessed. Greatly affects emulated CPU performance.\n\nSet to HOST UNCHECKED if unsure.", + "AudioBackendTooltip": "يغير الواجهة الخلفية المستخدمة لتقديم الصوت.\n\nSDL2 هو الخيار المفضل، بينما يتم استخدام OpenAL وSoundIO كبديلين. زائف لن يكون لها صوت.\n\nاضبط على SDL2 إذا لم تكن متأكدا.", + "MemoryManagerTooltip": "تغيير كيفية تعيين ذاكرة الضيف والوصول إليها. يؤثر بشكل كبير على أداء وحدة المعالجة المركزية التي تمت محاكاتها.\n\nاضبط على المضيف غير محدد إذا لم تكن متأكدا.", "MemoryManagerSoftwareTooltip": "استخدام جدول الصفحات البرمجي لترجمة العناوين. أعلى دقة ولكن أبطأ أداء.", - "MemoryManagerHostTooltip": "Directly map memory in the host address space. Much faster JIT compilation and execution.", - "MemoryManagerUnsafeTooltip": "Directly map memory, but do not mask the address within the guest address space before access. Faster, but at the cost of safety. The guest application can access memory from anywhere in Ryujinx, so only run programs you trust with this mode.", - "UseHypervisorTooltip": "Use Hypervisor instead of JIT. Greatly improves performance when available, but can be unstable in its current state.", - "DRamTooltip": "Utilizes an alternative MemoryMode layout to mimic a Switch development model.\n\nThis is only useful for higher-resolution texture packs or 4k resolution mods. Does NOT improve performance.\n\nLeave OFF if unsure.", + "MemoryManagerHostTooltip": "تعيين الذاكرة مباشرة في مساحة عنوان المضيف. تجميع وتنفيذ JIT أسرع بكثير.", + "MemoryManagerUnsafeTooltip": "تعيين الذاكرة مباشرة، ولكن لا تخفي العنوان داخل مساحة عنوان الضيف قبل الوصول. أسرع، ولكن على حساب السلامة. يمكن لتطبيق الضيف الوصول إلى الذاكرة من أي مكان في ريوجينكس، لذا قم بتشغيل البرامج التي تثق بها فقط مع هذا الوضع.", + "UseHypervisorTooltip": "استخدم هايبرڤايزور بدلا من JIT. يعمل على تحسين الأداء بشكل كبير عند توفره، ولكنه قد يكون غير مستقر في حالته الحالية.", + "DRamTooltip": "يستخدم تخطيط وضع الذاكرة البديل لتقليد نموذج سويتش المطورين.\n\nيعد هذا مفيدا فقط لحزم النسيج عالية الدقة أو تعديلات دقة 4K. لا يحسن الأداء.\n\nاتركه معطلا إذا لم تكن متأكدا.", "IgnoreMissingServicesTooltip": "يتجاهل خدمات نظام هوريزون غير المنفذة. قد يساعد هذا في تجاوز الأعطال عند تشغيل ألعاب معينة.\n\nاتركه معطلا إذا كنت غير متأكد.", - "GraphicsBackendThreadingTooltip": "Executes graphics backend commands on a second thread.\n\nSpeeds up shader compilation, reduces stuttering, and improves performance on GPU drivers without multithreading support of their own. Slightly better performance on drivers with multithreading.\n\nSet to AUTO if unsure.", - "GalThreadingTooltip": "Executes graphics backend commands on a second thread.\n\nSpeeds up shader compilation, reduces stuttering, and improves performance on GPU drivers without multithreading support of their own. Slightly better performance on drivers with multithreading.\n\nSet to AUTO if unsure.", - "ShaderCacheToggleTooltip": "Saves a disk shader cache which reduces stuttering in subsequent runs.\n\nLeave ON if unsure.", - "ResolutionScaleTooltip": "يضاعف دقة عرض اللعبة.\n\nقد لا تعمل بعض الألعاب مع هذا وتبدو منقطة حتى عند زيادة الدقة؛ بالنسبة لهذه الألعاب، قد تحتاج إلى العثور على تعديلات تزيل الصقل أو تزيد من دقة العرض الداخلي. لاستخدام الأخير، من المحتمل أن ترغب في تحديد أصلي.\n\nيمكن تغيير هذا الخيار أثناء تشغيل اللعبة بالنقر فوق \"تطبيق\" أدناه؛ يمكنك ببساطة تحريك نافذة الإعدادات جانبًا والتجربة حتى تجد المظهر المفضل للعبة.\n\nضع في اعتبارك أن 4x مبالغة في أي إعداد تقريبًا.", + "GraphicsBackendThreadingTooltip": "ينفذ أوامر الواجهة الخلفية للرسومات على مسار ثاني.\n\nيعمل على تسريع عملية تجميع المظللات وتقليل التقطيع وتحسين الأداء على برامج تشغيل وحدة الرسوميات دون دعم المسارات المتعددة الخاصة بهم. أداء أفضل قليلا على برامج التشغيل ذات المسارات المتعددة.\n\nاضبط على تلقائي إذا لم تكن متأكدا.", + "GalThreadingTooltip": "ينفذ أوامر الواجهة الخلفية للرسومات على مسار ثاني.\n\nيعمل على تسريع عملية تجميع المظللات وتقليل التقطيع وتحسين الأداء على برامج تشغيل وحدة الرسوميات دون دعم المسارات المتعددة الخاصة بهم. أداء أفضل قليلا على برامج التشغيل ذات المسارات المتعددة.\n\nاضبط على تلقائي إذا لم تكن متأكدا.", + "ShaderCacheToggleTooltip": "يحفظ ذاكرة المظللات المؤقتة على القرص مما يقلل من التقطيع في عمليات التشغيل اللاحقة.\n\nاتركه مفعلا إذا لم تكن متأكدا.", + "ResolutionScaleTooltip": "يضاعف دقة عرض اللعبة.\n\nقد لا تعمل بعض الألعاب مع هذا وتبدو منقطة حتى عند زيادة الدقة؛ بالنسبة لهذه الألعاب، قد تحتاج إلى العثور على تعديلات تزيل تنعيم الحواف أو تزيد من دقة العرض الداخلي. لاستخدام الأخير، من المحتمل أن ترغب في تحديد أصلي.\n\nيمكن تغيير هذا الخيار أثناء تشغيل اللعبة بالنقر فوق \"تطبيق\" أدناه؛ يمكنك ببساطة تحريك نافذة الإعدادات جانبًا والتجربة حتى تجد المظهر المفضل للعبة.\n\nضع في اعتبارك أن 4x مبالغة في أي إعداد تقريبًا.", "ResolutionScaleEntryTooltip": "مقياس دقة النقطة العائمة، مثل 1.5. من المرجح أن تتسبب المقاييس غير المتكاملة في حدوث مشكلات أو تعطل.", - "AnisotropyTooltip": "مستوى تصفية متباين الخواص. اضبط على تلقائي لاستخدام القيمة التي تطلبها اللعبة.", - "AspectRatioTooltip": "يتم تطبيق نسبة العرض إلى الارتفاع على نافذة العارض.\n\nقم بتغيير هذا فقط إذا كنت تستخدم تعديل نسبة العرض إلى الارتفاع للعبتك، وإلا سيتم تمديد الرسومات.\n\nاتركه على 16:9 إذا لم تكن متأكدًا.", - "ShaderDumpPathTooltip": "Graphics Shaders Dump Path", + "AnisotropyTooltip": "مستوى تصفية. اضبط على تلقائي لاستخدام القيمة التي تطلبها اللعبة.", + "AspectRatioTooltip": "يتم تطبيق نسبة العرض إلى الارتفاع على نافذة العارض.\n\nقم بتغيير هذا فقط إذا كنت تستخدم تعديل نسبة العرض إلى الارتفاع للعبتك، وإلا سيتم تمديد الرسومات.\n\nاتركه16:9 إذا لم تكن متأكدا.", + "ShaderDumpPathTooltip": "مسار تفريغ المظللات", "FileLogTooltip": "حفظ تسجيل وحدة التحكم إلى ملف سجل على القرص. لا يؤثر على الأداء.", - "StubLogTooltip": "Prints stub log messages in the console. Does not affect performance.", - "InfoLogTooltip": "Prints info log messages in the console. Does not affect performance.", + "StubLogTooltip": "طباعة رسائل سجل stub في وحدة التحكم. لا يؤثر على الأداء.", + "InfoLogTooltip": "طباعة رسائل سجل المعلومات في وحدة التحكم. لا يؤثر على الأداء.", "WarnLogTooltip": "طباعة رسائل سجل التحذير في وحدة التحكم. لا يؤثر على الأداء.", "ErrorLogTooltip": "طباعة رسائل سجل الأخطاء في وحدة التحكم. لا يؤثر على الأداء.", "TraceLogTooltip": "طباعة رسائل سجل التتبع في وحدة التحكم. لا يؤثر على الأداء.", "GuestLogTooltip": "طباعة رسائل سجل الضيف في وحدة التحكم. لا يؤثر على الأداء.", "FileAccessLogTooltip": "طباعة رسائل سجل الوصول إلى الملفات في وحدة التحكم.", - "FSAccessLogModeTooltip": "Enables FS access log output to the console. Possible modes are 0-3", + "FSAccessLogModeTooltip": "تمكين إخراج سجل الوصول إلى نظام الملفات إلى وحدة التحكم. الأوضاع الممكنة هي 0-3", "DeveloperOptionTooltip": "استخدمه بعناية", "OpenGlLogLevel": "يتطلب تمكين مستويات السجل المناسبة", "DebugLogTooltip": "طباعة رسائل سجل التصحيح في وحدة التحكم.\n\nاستخدم هذا فقط إذا طلب منك أحد الموظفين تحديدًا ذلك، لأنه سيجعل من الصعب قراءة السجلات وسيؤدي إلى تدهور أداء المحاكي.", - "LoadApplicationFileTooltip": "افتح مستكشف الملفات لاختيار ملف متوافق مع Switch لتحميله", - "LoadApplicationFolderTooltip": "افتح مستكشف الملفات لاختيار تطبيق متوافق مع Switch للتحميل", - "OpenRyujinxFolderTooltip": "فتح مجلد نظام ملفات Ryujinx", + "LoadApplicationFileTooltip": "افتح مستكشف الملفات لاختيار ملف متوافق مع سويتش لتحميله", + "LoadApplicationFolderTooltip": "افتح مستكشف الملفات لاختيار تطبيق متوافق مع سويتش للتحميل", + "OpenRyujinxFolderTooltip": "فتح مجلد نظام ملفات ريوجينكس", "OpenRyujinxLogsTooltip": "يفتح المجلد الذي تتم كتابة السجلات إليه", - "ExitTooltip": "الخروج من Ryujinx", + "ExitTooltip": "الخروج من ريوجينكس", "OpenSettingsTooltip": "فتح نافذة الإعدادات", - "OpenProfileManagerTooltip": "فتح نافذة إدارة ملفات تعريف المستخدمين", + "OpenProfileManagerTooltip": "فتح نافذة إدارة الملفات الشخصية للمستخدمين", "StopEmulationTooltip": "إيقاف محاكاة اللعبة الحالية والعودة إلى اختيار اللعبة", - "CheckUpdatesTooltip": "التحقق من وجود تحديثات لـ Ryujinx", + "CheckUpdatesTooltip": "التحقق من وجود تحديثات لريوجينكس", "OpenAboutTooltip": "فتح حول النافذة", "GridSize": "حجم الشبكة", "GridSizeTooltip": "تغيير حجم عناصر الشبكة", "SettingsTabSystemSystemLanguageBrazilianPortuguese": "البرتغالية البرازيلية", "AboutRyujinxContributorsButtonHeader": "رؤية جميع المساهمين", - "SettingsTabSystemAudioVolume": "الحجم:", + "SettingsTabSystemAudioVolume": "مستوى الصوت:", "AudioVolumeTooltip": "تغيير مستوى الصوت", "SettingsTabSystemEnableInternetAccess": "الوصول إلى إنترنت كضيف/وضع LAN", - "EnableInternetAccessTooltip": "للسماح للتطبيق المحاكي بالاتصال بالإنترنت.\n\nالألعاب ذات وضع الشبكة المحلية يمكن الاتصال ببعضها البعض عندما يتم تمكين هذا النظام وتكون الأنظمة متصلة بنفس نقطة الوصول. هذا يشمل وحدات التحكم الحقيقية أيضًا.\n\nلا يسمح بالاتصال بخوادم Nintendo. قد يسبب الانهيار في بعض الألعاب التي تحاول الاتصال بالإنترنت.\n\nاتركه متوقفا إن لم يكن مؤكدا.", + "EnableInternetAccessTooltip": "للسماح للتطبيق الذي تمت محاكاته بالاتصال بالإنترنت.\n\nيمكن للألعاب التي تحتوي على وضع LAN الاتصال ببعضها البعض عند تمكين ذلك وتوصيل الأنظمة بنفس نقطة الوصول. وهذا يشمل الأجهزة الحقيقية أيضا.\n\nلا يسمح بالاتصال بخوادم نينتندو. قد يتسبب في حدوث عطل في بعض الألعاب التي تحاول الاتصال بالإنترنت.\n\nاتركه معطلا إذا لم تكن متأكدا.", "GameListContextMenuManageCheatToolTip": "إدارة الغش", "GameListContextMenuManageCheat": "إدارة الغش", "GameListContextMenuManageModToolTip": "إدارة التعديلات", "GameListContextMenuManageMod": "إدارة التعديلات", "ControllerSettingsStickRange": "نطاق:", - "DialogStopEmulationTitle": "Ryujinx - إيقاف المحاكاة", + "DialogStopEmulationTitle": "ريوجينكس - إيقاف المحاكاة", "DialogStopEmulationMessage": "هل أنت متأكد أنك تريد إيقاف المحاكاة؟", - "SettingsTabCpu": "CPU", + "SettingsTabCpu": "المعالج", "SettingsTabAudio": "الصوت", "SettingsTabNetwork": "الشبكة", "SettingsTabNetworkConnection": "اتصال الشبكة", "SettingsTabCpuCache": "ذاكرة المعالج المؤقت", "SettingsTabCpuMemory": "وضع المعالج", - "DialogUpdaterFlatpakNotSupportedMessage": "الرجاء تحديث Ryujinx عبر FlatHub.", - "UpdaterDisabledWarningTitle": "التحديث معطل!", + "DialogUpdaterFlatpakNotSupportedMessage": "الرجاء تحديث ريوجينكس عبر فلات هاب.", + "UpdaterDisabledWarningTitle": "المحدث معطل!", "ControllerSettingsRotate90": "تدوير 90 درجة في اتجاه عقارب الساعة", "IconSize": "حجم الأيقونة", "IconSizeTooltip": "تغيير حجم أيقونات اللعبة", "MenuBarOptionsShowConsole": "عرض وحدة التحكم", - "ShaderCachePurgeError": "Error purging shader cache at {0}: {1}", + "ShaderCachePurgeError": "حدث خطأ أثناء تنظيف ذاكرة المظللات المؤقتة في {0}: {1}", "UserErrorNoKeys": "المفاتيح غير موجودة", "UserErrorNoFirmware": "لم يتم العثور على البرنامج الثابت", "UserErrorFirmwareParsingFailed": "خطأ في تحليل البرنامج الثابت", "UserErrorApplicationNotFound": "التطبيق غير موجود", "UserErrorUnknown": "خطأ غير معروف", "UserErrorUndefined": "خطأ غير محدد", - "UserErrorNoKeysDescription": "لم يتمكن Ryujinx من العثور على ملف 'prod.keys' الخاص بك", + "UserErrorNoKeysDescription": "لم يتمكن ريوجينكس من العثور على ملف 'prod.keys' الخاص بك", "UserErrorNoFirmwareDescription": "لم يتمكن ريوجينكس من العثور على أية برامج ثابتة مثبتة", "UserErrorFirmwareParsingFailedDescription": "لم يتمكن ريوجينكس من تحليل البرامج الثابتة المتوفرة. يحدث هذا عادة بسبب المفاتيح القديمة.", "UserErrorApplicationNotFoundDescription": "تعذر على ريوجينكس العثور على تطبيق صالح في المسار المحدد.", @@ -542,72 +648,73 @@ "OpenSetupGuideMessage": "فتح دليل الإعداد", "NoUpdate": "لا يوجد تحديث", "TitleUpdateVersionLabel": "الإصدار: {0}", - "RyujinxInfo": "Ryujinx - معلومات", - "RyujinxConfirm": "Ryujinx - تأكيد", + "RyujinxInfo": "ريوجينكس - معلومات", + "RyujinxConfirm": "ريوجينكس - تأكيد", "FileDialogAllTypes": "كل الأنواع", - "Never": "مطلقاً", - "SwkbdMinCharacters": "يجب أن يبلغ طوله {0} حرفًا على الأقل", - "SwkbdMinRangeCharacters": "يجب أن يتكون من {0}-{1} حرفًا", + "Never": "مطلقا", + "SwkbdMinCharacters": "يجب أن يبلغ طوله {0} حرفا على الأقل", + "SwkbdMinRangeCharacters": "يجب أن يتكون من {0}-{1} حرفا", "SoftwareKeyboard": "لوحة المفاتيح البرمجية", "SoftwareKeyboardModeNumeric": "يجب أن يكون 0-9 أو '.' فقط", "SoftwareKeyboardModeAlphabet": "يجب أن تكون الأحرف غير CJK فقط", "SoftwareKeyboardModeASCII": "يجب أن يكون نص ASCII فقط", - "ControllerAppletControllers": "ذراع التحكم المدعومة:", + "ControllerAppletControllers": "وحدات التحكم المدعومة:", "ControllerAppletPlayers": "اللاعبين:", "ControllerAppletDescription": "الإعدادات الحالية غير صالحة. افتح الإعدادات وأعد تكوين المدخلات الخاصة بك.", - "ControllerAppletDocked": "Docked mode set. Handheld control should be disabled.", + "ControllerAppletDocked": "تم ضبط وضع تركيب بالمنصة. يجب تعطيل التحكم المحمول.", "UpdaterRenaming": "إعادة تسمية الملفات القديمة...", - "UpdaterRenameFailed": "التحديث غير قادر على إعادة تسمية الملف: {0}", + "UpdaterRenameFailed": "المحدث غير قادر على إعادة تسمية الملف: {0}", "UpdaterAddingFiles": "إضافة ملفات جديدة...", "UpdaterExtracting": "استخراج التحديث...", "UpdaterDownloading": "تحميل التحديث...", "Game": "لعبة", - "Docked": "مركب بالمنصة", + "Docked": "تركيب بالمنصة", "Handheld": "محمول", "ConnectionError": "خطأ في الاتصال", "AboutPageDeveloperListMore": "{0} والمزيد...", "ApiError": "خطأ في API.", - "LoadingHeading": "جارٍ تحميل {0}", - "CompilingPPTC": "تجميع الـ PTC", - "CompilingShaders": "تجميع الظلال", + "LoadingHeading": "جاري تحميل {0}", + "CompilingPPTC": "تجميع الـ‫(PPTC)", + "CompilingShaders": "تجميع المظللات", "AllKeyboards": "كل لوحات المفاتيح", "OpenFileDialogTitle": "حدد ملف مدعوم لفتحه", - "OpenFolderDialogTitle": "حدد مجلدًا يحتوي على لعبة غير مضغوطة", + "OpenFolderDialogTitle": "حدد مجلدا يحتوي على لعبة غير مضغوطة", "AllSupportedFormats": "كل التنسيقات المدعومة", - "RyujinxUpdater": "تحديث Ryujinx", + "RyujinxUpdater": "محدث ريوجينكس", "SettingsTabHotkeys": "مفاتيح الاختصار في لوحة المفاتيح", "SettingsTabHotkeysHotkeys": "مفاتيح الاختصار في لوحة المفاتيح", - "SettingsTabHotkeysToggleVsyncHotkey": "تبديل VSync:", + "SettingsTabHotkeysToggleVsyncHotkey": "تبديل المزامنة العمودية:", "SettingsTabHotkeysScreenshotHotkey": "لقطة الشاشة:", "SettingsTabHotkeysShowUiHotkey": "عرض واجهة المستخدم:", "SettingsTabHotkeysPauseHotkey": "إيقاف مؤقت:", - "SettingsTabHotkeysToggleMuteHotkey": "كتم الصوت:", + "SettingsTabHotkeysToggleMuteHotkey": "كتم:", "ControllerMotionTitle": "إعدادات التحكم بالحركة", "ControllerRumbleTitle": "إعدادات الهزاز", "SettingsSelectThemeFileDialogTitle": "حدد ملف السمة", - "SettingsXamlThemeFile": "Xaml Theme File", + "SettingsXamlThemeFile": "ملف سمة Xaml", "AvatarWindowTitle": "إدارة الحسابات - الصورة الرمزية", "Amiibo": "أميبو", "Unknown": "غير معروف", "Usage": "الاستخدام", "Writable": "قابل للكتابة", - "SelectDlcDialogTitle": "حدد ملفات DLC", + "SelectDlcDialogTitle": "حدد ملفات المحتوي الإضافي", "SelectUpdateDialogTitle": "حدد ملفات التحديث", "SelectModDialogTitle": "حدد مجلد التعديل", - "UserProfileWindowTitle": "مدير ملفات تعريف المستخدمين", + "UserProfileWindowTitle": "مدير الملفات الشخصية للمستخدمين", "CheatWindowTitle": "مدير الغش", "DlcWindowTitle": "إدارة المحتوى القابل للتنزيل لـ {0} ({1})", + "ModWindowTitle": "إدارة التعديلات لـ {0} ({1})", "UpdateWindowTitle": "مدير تحديث العنوان", "CheatWindowHeading": "الغش متوفر لـ {0} [{1}]", "BuildId": "معرف البناء:", "DlcWindowHeading": "المحتويات القابلة للتنزيل {0}", "ModWindowHeading": "{0} تعديل", - "UserProfilesEditProfile": "تعديل المحددة", + "UserProfilesEditProfile": "تعديل المحدد", "Cancel": "إلغاء", "Save": "حفظ", "Discard": "تجاهل", "Paused": "متوقف مؤقتا", - "UserProfilesSetProfileImage": "تعيين صورة ملف التعريف", + "UserProfilesSetProfileImage": "تعيين صورة الملف الشخصي", "UserProfileEmptyNameError": "الاسم مطلوب", "UserProfileNoImageError": "يجب تعيين صورة الملف الشخصي", "GameUpdateWindowHeading": "إدارة التحديثات لـ {0} ({1})", @@ -616,22 +723,22 @@ "UserProfilesName": "الاسم:", "UserProfilesUserId": "معرف المستخدم:", "SettingsTabGraphicsBackend": "خلفية الرسومات", - "SettingsTabGraphicsBackendTooltip": "Select the graphics backend that will be used in the emulator.\n\nVulkan is overall better for all modern graphics cards, as long as their drivers are up to date. Vulkan also features faster shader compilation (less stuttering) on all GPU vendors.\n\nOpenGL may achieve better results on old Nvidia GPUs, on old AMD GPUs on Linux, or on GPUs with lower VRAM, though shader compilation stutters will be greater.\n\nSet to Vulkan if unsure. Set to OpenGL if your GPU does not support Vulkan even with the latest graphics drivers.", + "SettingsTabGraphicsBackendTooltip": "حدد الواجهة الخلفية للرسومات التي سيتم استخدامها في المحاكي.\n\nيعد برنامج فولكان أفضل بشكل عام لجميع بطاقات الرسومات الحديثة، طالما أن برامج التشغيل الخاصة بها محدثة. يتميز فولكان أيضا بتجميع مظللات أسرع (أقل تقطيعا) على جميع بائعي وحدات معالجة الرسومات.\n\nقد يحقق أوبن جي أل نتائج أفضل على وحدات معالجة الرسومات إنفيديا القديمة، أو على وحدات معالجة الرسومات إي إم دي القديمة على لينكس، أو على وحدات معالجة الرسومات ذات ذاكرة الوصول العشوائي للفيديوالأقل، على الرغم من أن تعثرات تجميع المظللات ستكون أكبر.\n\nاضبط على فولكان إذا لم تكن متأكدا. اضبط على أوبن جي أل إذا كانت وحدة معالجة الرسومات الخاصة بك لا تدعم فولكان حتى مع أحدث برامج تشغيل الرسومات.", "SettingsEnableTextureRecompression": "تمكين إعادة ضغط التكستر", - "SettingsEnableTextureRecompressionTooltip": "Compresses ASTC textures in order to reduce VRAM usage.\n\nGames using this texture format include Astral Chain, Bayonetta 3, Fire Emblem Engage, Metroid Prime Remastered, Super Mario Bros. Wonder and The Legend of Zelda: Tears of the Kingdom.\n\nGraphics cards with 4GiB VRAM or less will likely crash at some point while running these games.\n\nEnable only if you're running out of VRAM on the aforementioned games. Leave OFF if unsure.", - "SettingsTabGraphicsPreferredGpu": "GPU المفضل", - "SettingsTabGraphicsPreferredGpuTooltip": "Select the graphics card that will be used with the Vulkan graphics backend.\n\nDoes not affect the GPU that OpenGL will use.\n\nSet to the GPU flagged as \"dGPU\" if unsure. If there isn't one, leave untouched.", - "SettingsAppRequiredRestartMessage": "مطلوب إعادة تشغيل Ryujinx", - "SettingsGpuBackendRestartMessage": "Graphics Backend or GPU settings have been modified. This will require a restart to be applied", + "SettingsEnableTextureRecompressionTooltip": "يضغط تكستر ASTC من أجل تقليل استخدام ذاكرة الوصول العشوائي للفيديو.\n\nتتضمن الألعاب التي تستخدم تنسيق النسيج هذا Astral Chain وBayonetta 3 وFire Emblem Engage وMetroid Prime Remastered وSuper Mario Bros. Wonder وThe Legend of Zelda: Tears of the Kingdom.\n\nمن المحتمل أن تتعطل بطاقات الرسومات التي تحتوي على 4 جيجا بايت من ذاكرة الوصول العشوائي للفيديو أو أقل في مرحلة ما أثناء تشغيل هذه الألعاب.\n\nقم بالتمكين فقط في حالة نفاد ذاكرة الوصول العشوائي للفيديو في الألعاب المذكورة أعلاه. اتركه معطلا إذا لم تكن متأكدا.", + "SettingsTabGraphicsPreferredGpu": "وحدة معالجة الرسوميات المفضلة", + "SettingsTabGraphicsPreferredGpuTooltip": "حدد بطاقة الرسومات التي سيتم استخدامها مع الواجهة الخلفية لرسومات فولكان.\n\nلا يؤثر على وحدة معالجة الرسومات التي سيستخدمها أوبن جي أل.\n\nاضبط على وحدة معالجة الرسومات التي تم وضع علامة عليها كـ \"dGPU\" إذا لم تكن متأكدًا. إذا لم يكن هناك واحد، اتركه.", + "SettingsAppRequiredRestartMessage": "مطلوب إعادة تشغيل ريوجينكس", + "SettingsGpuBackendRestartMessage": "تم تعديل إعدادات الواجهة الخلفية للرسومات أو وحدة معالجة الرسومات. سيتطلب هذا إعادة التشغيل ليتم تطبيقه", "SettingsGpuBackendRestartSubMessage": "\n\nهل تريد إعادة التشغيل الآن؟", - "RyujinxUpdaterMessage": "هل تريد تحديث Ryujinx إلى أحدث إصدار؟", + "RyujinxUpdaterMessage": "هل تريد تحديث ريوجينكس إلى أحدث إصدار؟", "SettingsTabHotkeysVolumeUpHotkey": "زيادة مستوى الصوت:", "SettingsTabHotkeysVolumeDownHotkey": "خفض مستوى الصوت:", - "SettingsEnableMacroHLE": "Enable Macro HLE", - "SettingsEnableMacroHLETooltip": "High-level emulation of GPU Macro code.\n\nImproves performance, but may cause graphical glitches in some games.\n\nLeave ON if unsure.", - "SettingsEnableColorSpacePassthrough": "Color Space Passthrough", - "SettingsEnableColorSpacePassthroughTooltip": "Directs the Vulkan backend to pass through color information without specifying a color space. For users with wide gamut displays, this may result in more vibrant colors, at the cost of color correctness.", - "VolumeShort": "الحجم", + "SettingsEnableMacroHLE": "تمكين Maro HLE", + "SettingsEnableMacroHLETooltip": "محاكاة عالية المستوى لكود مايكرو وحدة معالجة الرسوميات.\n\nيعمل على تحسين الأداء، ولكنه قد يسبب خللا رسوميا في بعض الألعاب.\n\nاتركه مفعلا إذا لم تكن متأكدا.", + "SettingsEnableColorSpacePassthrough": "عبور مساحة اللون", + "SettingsEnableColorSpacePassthroughTooltip": "يوجه واجهة فولكان الخلفية لتمرير معلومات الألوان دون تحديد مساحة اللون. بالنسبة للمستخدمين الذين لديهم شاشات ذات نطاق واسع، قد يؤدي ذلك إلى الحصول على ألوان أكثر حيوية، على حساب صحة الألوان.", + "VolumeShort": "مستوى", "UserProfilesManageSaves": "إدارة الحفظ", "DeleteUserSave": "هل تريد حذف حفظ المستخدم لهذه اللعبة؟", "IrreversibleActionNote": "هذا الإجراء لا يمكن التراجع عنه.", @@ -642,12 +749,12 @@ "Search": "بحث", "UserProfilesRecoverLostAccounts": "استعادة الحسابات المفقودة", "Recover": "استعادة", - "UserProfilesRecoverHeading": "تم العثور على الحفظ للحسابات التالية", - "UserProfilesRecoverEmptyList": "لا توجد ملفات تعريف لاستردادها", - "GraphicsAATooltip": "Applies anti-aliasing to the game render.\n\nFXAA will blur most of the image, while SMAA will attempt to find jagged edges and smooth them out.\n\nNot recommended to use in conjunction with the FSR scaling filter.\n\nThis option can be changed while a game is running by clicking \"Apply\" below; you can simply move the settings window aside and experiment until you find your preferred look for a game.\n\nLeave on NONE if unsure.", + "UserProfilesRecoverHeading": "تم العثور على حفظ للحسابات التالية", + "UserProfilesRecoverEmptyList": "لا توجد ملفات شخصية لاستردادها", + "GraphicsAATooltip": "يتم تطبيق تنعيم الحواف على عرض اللعبة.\n\nسوف يقوم FXAA بتعتيم معظم الصورة، بينما سيحاول SMAA العثور على حواف خشنة وتنعيمها.\n\nلا ينصح باستخدامه مع فلتر FSR لتكبير.\n\nيمكن تغيير هذا الخيار أثناء تشغيل اللعبة بالنقر فوق \"تطبيق\" أدناه؛ يمكنك ببساطة تحريك نافذة الإعدادات جانبا والتجربة حتى تجد المظهر المفضل للعبة.\n\nاتركه على لا شيء إذا لم تكن متأكدا.", "GraphicsAALabel": "تنعيم الحواف:", "GraphicsScalingFilterLabel": "فلتر التكبير:", - "GraphicsScalingFilterTooltip": "Choose the scaling filter that will be applied when using resolution scale.\n\nBilinear works well for 3D games and is a safe default option.\n\nNearest is recommended for pixel art games.\n\nFSR 1.0 is merely a sharpening filter, not recommended for use with FXAA or SMAA.\n\nThis option can be changed while a game is running by clicking \"Apply\" below; you can simply move the settings window aside and experiment until you find your preferred look for a game.\n\nLeave on BILINEAR if unsure.", + "GraphicsScalingFilterTooltip": "اختر فلتر التكبير الذي سيتم تطبيقه عند استخدام مقياس الدقة.\n\nيعمل Bilinear بشكل جيد مع الألعاب ثلاثية الأبعاد وهو خيار افتراضي آمن.\n\nيوصى باستخدام Nearest لألعاب البكسل الفنية.\n\nFSR 1.0 هو مجرد مرشح توضيحي، ولا ينصح باستخدامه مع FXAA أو SMAA.\n\nيمكن تغيير هذا الخيار أثناء تشغيل اللعبة بالنقر فوق \"تطبيق\" أدناه؛ يمكنك ببساطة تحريك نافذة الإعدادات جانبا والتجربة حتى تجد المظهر المفضل للعبة.\n\nاتركه على Bilinear إذا لم تكن متأكدا.", "GraphicsScalingFilterBilinear": "Bilinear", "GraphicsScalingFilterNearest": "Nearest", "GraphicsScalingFilterFsr": "FSR", @@ -660,14 +767,14 @@ "UserEditorTitle": "تعديل المستخدم", "UserEditorTitleCreate": "إنشاء مستخدم", "SettingsTabNetworkInterface": "واجهة الشبكة:", - "NetworkInterfaceTooltip": "The network interface used for LAN/LDN features.\n\nIn conjunction with a VPN or XLink Kai and a game with LAN support, can be used to spoof a same-network connection over the Internet.\n\nLeave on DEFAULT if unsure.", + "NetworkInterfaceTooltip": "واجهة الشبكة مستخدمة لميزات LAN/LDN.\n\nبالاشتراك مع VPN أو XLink Kai ولعبة تدعم LAN، يمكن استخدامها لتزييف اتصال الشبكة نفسها عبر الإنترنت.\n\nاتركه على الافتراضي إذا لم تكن متأكدا.", "NetworkInterfaceDefault": "افتراضي", - "PackagingShaders": "Packaging Shaders", - "AboutChangelogButton": "عرض سجل التغييرات على GitHub", + "PackagingShaders": "تعبئة المظللات", + "AboutChangelogButton": "عرض سجل التغييرات على غيت هاب", "AboutChangelogButtonTooltipMessage": "انقر لفتح سجل التغيير لهذا الإصدار في متصفحك الافتراضي.", "SettingsTabNetworkMultiplayer": "لعب جماعي", - "MultiplayerMode": "النمط:", - "MultiplayerModeTooltip": "Change LDN multiplayer mode.\n\nLdnMitm will modify local wireless/local play functionality in games to function as if it were LAN, allowing for local, same-network connections with other Ryujinx instances and hacked Nintendo Switch consoles that have the ldn_mitm module installed.\n\nMultiplayer requires all players to be on the same game version (i.e. Super Smash Bros. Ultimate v13.0.1 can't connect to v13.0.0).\n\nLeave DISABLED if unsure.", + "MultiplayerMode": "الوضع:", + "MultiplayerModeTooltip": "تغيير وضع LDN متعدد اللاعبين.\n\nسوف يقوم LdnMitm بتعديل وظيفة اللعب المحلية/اللاسلكية المحلية في الألعاب لتعمل كما لو كانت شبكة LAN، مما يسمح باتصالات الشبكة المحلية نفسها مع محاكيات ريوجينكس الأخرى وأجهزة نينتندو سويتش المخترقة التي تم تثبيت وحدة ldn_mitm عليها.\n\nيتطلب وضع اللاعبين المتعددين أن يكون جميع اللاعبين على نفس إصدار اللعبة (على سبيل المثال، يتعذر على الإصدار 13.0.1 من سوبر سماش برذرز ألتميت الاتصال بالإصدار 13.0.0).\n\nاتركه معطلا إذا لم تكن متأكدا.", "MultiplayerModeDisabled": "معطل", "MultiplayerModeLdnMitm": "ldn_mitm" } diff --git a/src/Ryujinx/Assets/Locales/de_DE.json b/src/Ryujinx/Assets/Locales/de_DE.json index 71af4f3fe..401293198 100644 --- a/src/Ryujinx/Assets/Locales/de_DE.json +++ b/src/Ryujinx/Assets/Locales/de_DE.json @@ -30,6 +30,10 @@ "MenuBarToolsManageFileTypes": "Dateitypen verwalten", "MenuBarToolsInstallFileTypes": "Dateitypen installieren", "MenuBarToolsUninstallFileTypes": "Dateitypen deinstallieren", + "MenuBarView": "_Ansicht", + "MenuBarViewWindow": "Fenstergröße", + "MenuBarViewWindow720": "720p", + "MenuBarViewWindow1080": "1080p", "MenuBarHelp": "_Hilfe", "MenuBarHelpCheckForUpdates": "Nach Updates suchen", "MenuBarHelpAbout": "Über Ryujinx", @@ -92,6 +96,7 @@ "SettingsTabGeneralEnableDiscordRichPresence": "Aktiviere die Statusanzeige für Discord", "SettingsTabGeneralCheckUpdatesOnLaunch": "Beim Start nach Updates suchen", "SettingsTabGeneralShowConfirmExitDialog": "Zeige den \"Beenden bestätigen\"-Dialog", + "SettingsTabGeneralRememberWindowState": "Fenstergröße/-position merken", "SettingsTabGeneralHideCursor": "Mauszeiger ausblenden", "SettingsTabGeneralHideCursorNever": "Niemals", "SettingsTabGeneralHideCursorOnIdle": "Mauszeiger bei Inaktivität ausblenden", @@ -266,6 +271,107 @@ "ControllerSettingsMotionGyroDeadzone": "Gyro-Deadzone:", "ControllerSettingsSave": "Speichern", "ControllerSettingsClose": "Schließen", + "KeyUnknown": "Unbekannt", + "KeyShiftLeft": "Shift Left", + "KeyShiftRight": "Shift Right", + "KeyControlLeft": "Ctrl Left", + "KeyMacControlLeft": "⌃ Left", + "KeyControlRight": "Ctrl Right", + "KeyMacControlRight": "⌃ Right", + "KeyAltLeft": "Alt Left", + "KeyMacAltLeft": "⌥ Left", + "KeyAltRight": "Alt Right", + "KeyMacAltRight": "⌥ Right", + "KeyWinLeft": "⊞ Left", + "KeyMacWinLeft": "⌘ Left", + "KeyWinRight": "⊞ Right", + "KeyMacWinRight": "⌘ Right", + "KeyMenu": "Menu", + "KeyUp": "Up", + "KeyDown": "Down", + "KeyLeft": "Left", + "KeyRight": "Right", + "KeyEnter": "Enter", + "KeyEscape": "Escape", + "KeySpace": "Space", + "KeyTab": "Tab", + "KeyBackSpace": "Backspace", + "KeyInsert": "Insert", + "KeyDelete": "Delete", + "KeyPageUp": "Page Up", + "KeyPageDown": "Page Down", + "KeyHome": "Home", + "KeyEnd": "End", + "KeyCapsLock": "Caps Lock", + "KeyScrollLock": "Scroll Lock", + "KeyPrintScreen": "Print Screen", + "KeyPause": "Pause", + "KeyNumLock": "Num Lock", + "KeyClear": "Clear", + "KeyKeypad0": "Keypad 0", + "KeyKeypad1": "Keypad 1", + "KeyKeypad2": "Keypad 2", + "KeyKeypad3": "Keypad 3", + "KeyKeypad4": "Keypad 4", + "KeyKeypad5": "Keypad 5", + "KeyKeypad6": "Keypad 6", + "KeyKeypad7": "Keypad 7", + "KeyKeypad8": "Keypad 8", + "KeyKeypad9": "Keypad 9", + "KeyKeypadDivide": "Keypad Divide", + "KeyKeypadMultiply": "Keypad Multiply", + "KeyKeypadSubtract": "Keypad Subtract", + "KeyKeypadAdd": "Keypad Add", + "KeyKeypadDecimal": "Keypad Decimal", + "KeyKeypadEnter": "Keypad Enter", + "KeyNumber0": "0", + "KeyNumber1": "1", + "KeyNumber2": "2", + "KeyNumber3": "3", + "KeyNumber4": "4", + "KeyNumber5": "5", + "KeyNumber6": "6", + "KeyNumber7": "7", + "KeyNumber8": "8", + "KeyNumber9": "9", + "KeyTilde": "~", + "KeyGrave": "`", + "KeyMinus": "-", + "KeyPlus": "+", + "KeyBracketLeft": "[", + "KeyBracketRight": "]", + "KeySemicolon": ";", + "KeyQuote": "\"", + "KeyComma": ",", + "KeyPeriod": ".", + "KeySlash": "/", + "KeyBackSlash": "\\", + "KeyUnbound": "Unbound", + "GamepadLeftStick": "L Stick Button", + "GamepadRightStick": "R Stick Button", + "GamepadLeftShoulder": "Left Shoulder", + "GamepadRightShoulder": "Right Shoulder", + "GamepadLeftTrigger": "Left Trigger", + "GamepadRightTrigger": "Right Trigger", + "GamepadDpadUp": "Up", + "GamepadDpadDown": "Down", + "GamepadDpadLeft": "Left", + "GamepadDpadRight": "Right", + "GamepadMinus": "-", + "GamepadPlus": "+", + "GamepadGuide": "Guide", + "GamepadMisc1": "Misc", + "GamepadPaddle1": "Paddle 1", + "GamepadPaddle2": "Paddle 2", + "GamepadPaddle3": "Paddle 3", + "GamepadPaddle4": "Paddle 4", + "GamepadTouchpad": "Touchpad", + "GamepadSingleLeftTrigger0": "Left Trigger 0", + "GamepadSingleRightTrigger0": "Right Trigger 0", + "GamepadSingleLeftTrigger1": "Left Trigger 1", + "GamepadSingleRightTrigger1": "Right Trigger 1", + "StickLeft": "Left Stick", + "StickRight": "Right Stick", "UserProfilesSelectedUserProfile": "Ausgewähltes Profil:", "UserProfilesSaveProfileName": "Profilname speichern", "UserProfilesChangeProfileImage": "Profilbild ändern", @@ -597,6 +703,7 @@ "UserProfileWindowTitle": "Benutzerprofile verwalten", "CheatWindowTitle": "Spiel-Cheats verwalten", "DlcWindowTitle": "Spiel-DLC verwalten", + "ModWindowTitle": "Manage Mods for {0} ({1})", "UpdateWindowTitle": "Spiel-Updates verwalten", "CheatWindowHeading": "Cheats verfügbar für {0} [{1}]", "BuildId": "BuildId:", diff --git a/src/Ryujinx/Assets/Locales/el_GR.json b/src/Ryujinx/Assets/Locales/el_GR.json index ba5c7078c..ccdf6e0e4 100644 --- a/src/Ryujinx/Assets/Locales/el_GR.json +++ b/src/Ryujinx/Assets/Locales/el_GR.json @@ -30,6 +30,10 @@ "MenuBarToolsManageFileTypes": "Διαχείριση τύπων αρχείων", "MenuBarToolsInstallFileTypes": "Εγκαταστήσετε τύπους αρχείων.", "MenuBarToolsUninstallFileTypes": "Απεγκαταστήσετε τύπους αρχείων", + "MenuBarView": "_View", + "MenuBarViewWindow": "Window Size", + "MenuBarViewWindow720": "720p", + "MenuBarViewWindow1080": "1080p", "MenuBarHelp": "_Βοήθεια", "MenuBarHelpCheckForUpdates": "Έλεγχος για Ενημερώσεις", "MenuBarHelpAbout": "Σχετικά με", @@ -92,6 +96,7 @@ "SettingsTabGeneralEnableDiscordRichPresence": "Ενεργοποίηση Εμπλουτισμένης Παρουσίας Discord", "SettingsTabGeneralCheckUpdatesOnLaunch": "Έλεγχος για Ενημερώσεις στην Εκκίνηση", "SettingsTabGeneralShowConfirmExitDialog": "Εμφάνιση διαλόγου \"Επιβεβαίωση Εξόδου\".", + "SettingsTabGeneralRememberWindowState": "Remember Window Size/Position", "SettingsTabGeneralHideCursor": "Απόκρυψη Κέρσορα:", "SettingsTabGeneralHideCursorNever": "Ποτέ", "SettingsTabGeneralHideCursorOnIdle": "Απόκρυψη Δρομέα στην Αδράνεια", @@ -266,6 +271,107 @@ "ControllerSettingsMotionGyroDeadzone": "Νεκρή Ζώνη Γυροσκοπίου:", "ControllerSettingsSave": "Αποθήκευση", "ControllerSettingsClose": "Κλείσιμο", + "KeyUnknown": "Unknown", + "KeyShiftLeft": "Shift Left", + "KeyShiftRight": "Shift Right", + "KeyControlLeft": "Ctrl Left", + "KeyMacControlLeft": "⌃ Left", + "KeyControlRight": "Ctrl Right", + "KeyMacControlRight": "⌃ Right", + "KeyAltLeft": "Alt Left", + "KeyMacAltLeft": "⌥ Left", + "KeyAltRight": "Alt Right", + "KeyMacAltRight": "⌥ Right", + "KeyWinLeft": "⊞ Left", + "KeyMacWinLeft": "⌘ Left", + "KeyWinRight": "⊞ Right", + "KeyMacWinRight": "⌘ Right", + "KeyMenu": "Menu", + "KeyUp": "Up", + "KeyDown": "Down", + "KeyLeft": "Left", + "KeyRight": "Right", + "KeyEnter": "Enter", + "KeyEscape": "Escape", + "KeySpace": "Space", + "KeyTab": "Tab", + "KeyBackSpace": "Backspace", + "KeyInsert": "Insert", + "KeyDelete": "Delete", + "KeyPageUp": "Page Up", + "KeyPageDown": "Page Down", + "KeyHome": "Home", + "KeyEnd": "End", + "KeyCapsLock": "Caps Lock", + "KeyScrollLock": "Scroll Lock", + "KeyPrintScreen": "Print Screen", + "KeyPause": "Pause", + "KeyNumLock": "Num Lock", + "KeyClear": "Clear", + "KeyKeypad0": "Keypad 0", + "KeyKeypad1": "Keypad 1", + "KeyKeypad2": "Keypad 2", + "KeyKeypad3": "Keypad 3", + "KeyKeypad4": "Keypad 4", + "KeyKeypad5": "Keypad 5", + "KeyKeypad6": "Keypad 6", + "KeyKeypad7": "Keypad 7", + "KeyKeypad8": "Keypad 8", + "KeyKeypad9": "Keypad 9", + "KeyKeypadDivide": "Keypad Divide", + "KeyKeypadMultiply": "Keypad Multiply", + "KeyKeypadSubtract": "Keypad Subtract", + "KeyKeypadAdd": "Keypad Add", + "KeyKeypadDecimal": "Keypad Decimal", + "KeyKeypadEnter": "Keypad Enter", + "KeyNumber0": "0", + "KeyNumber1": "1", + "KeyNumber2": "2", + "KeyNumber3": "3", + "KeyNumber4": "4", + "KeyNumber5": "5", + "KeyNumber6": "6", + "KeyNumber7": "7", + "KeyNumber8": "8", + "KeyNumber9": "9", + "KeyTilde": "~", + "KeyGrave": "`", + "KeyMinus": "-", + "KeyPlus": "+", + "KeyBracketLeft": "[", + "KeyBracketRight": "]", + "KeySemicolon": ";", + "KeyQuote": "\"", + "KeyComma": ",", + "KeyPeriod": ".", + "KeySlash": "/", + "KeyBackSlash": "\\", + "KeyUnbound": "Unbound", + "GamepadLeftStick": "L Stick Button", + "GamepadRightStick": "R Stick Button", + "GamepadLeftShoulder": "Left Shoulder", + "GamepadRightShoulder": "Right Shoulder", + "GamepadLeftTrigger": "Left Trigger", + "GamepadRightTrigger": "Right Trigger", + "GamepadDpadUp": "Up", + "GamepadDpadDown": "Down", + "GamepadDpadLeft": "Left", + "GamepadDpadRight": "Right", + "GamepadMinus": "-", + "GamepadPlus": "+", + "GamepadGuide": "Guide", + "GamepadMisc1": "Misc", + "GamepadPaddle1": "Paddle 1", + "GamepadPaddle2": "Paddle 2", + "GamepadPaddle3": "Paddle 3", + "GamepadPaddle4": "Paddle 4", + "GamepadTouchpad": "Touchpad", + "GamepadSingleLeftTrigger0": "Left Trigger 0", + "GamepadSingleRightTrigger0": "Right Trigger 0", + "GamepadSingleLeftTrigger1": "Left Trigger 1", + "GamepadSingleRightTrigger1": "Right Trigger 1", + "StickLeft": "Left Stick", + "StickRight": "Right Stick", "UserProfilesSelectedUserProfile": "Επιλεγμένο Προφίλ Χρήστη:", "UserProfilesSaveProfileName": "Αποθήκευση Ονόματος Προφίλ", "UserProfilesChangeProfileImage": "Αλλαγή Εικόνας Προφίλ", @@ -597,6 +703,7 @@ "UserProfileWindowTitle": "Διαχειριστής Προφίλ Χρήστη", "CheatWindowTitle": "Διαχειριστής των Cheats", "DlcWindowTitle": "Downloadable Content Manager", + "ModWindowTitle": "Manage Mods for {0} ({1})", "UpdateWindowTitle": "Διαχειριστής Ενημερώσεων Τίτλου", "CheatWindowHeading": "Διαθέσιμα Cheats για {0} [{1}]", "BuildId": "BuildId:", diff --git a/src/Ryujinx/Assets/Locales/en_US.json b/src/Ryujinx/Assets/Locales/en_US.json index 16f12f519..e6afe0259 100644 --- a/src/Ryujinx/Assets/Locales/en_US.json +++ b/src/Ryujinx/Assets/Locales/en_US.json @@ -30,6 +30,10 @@ "MenuBarToolsManageFileTypes": "Manage file types", "MenuBarToolsInstallFileTypes": "Install file types", "MenuBarToolsUninstallFileTypes": "Uninstall file types", + "MenuBarView": "_View", + "MenuBarViewWindow": "Window Size", + "MenuBarViewWindow720": "720p", + "MenuBarViewWindow1080": "1080p", "MenuBarHelp": "_Help", "MenuBarHelpCheckForUpdates": "Check for Updates", "MenuBarHelpAbout": "About", @@ -92,6 +96,7 @@ "SettingsTabGeneralEnableDiscordRichPresence": "Enable Discord Rich Presence", "SettingsTabGeneralCheckUpdatesOnLaunch": "Check for Updates on Launch", "SettingsTabGeneralShowConfirmExitDialog": "Show \"Confirm Exit\" Dialog", + "SettingsTabGeneralRememberWindowState": "Remember Window Size/Position", "SettingsTabGeneralHideCursor": "Hide Cursor:", "SettingsTabGeneralHideCursorNever": "Never", "SettingsTabGeneralHideCursorOnIdle": "On Idle", @@ -269,6 +274,107 @@ "ControllerSettingsMotionGyroDeadzone": "Gyro Deadzone:", "ControllerSettingsSave": "Save", "ControllerSettingsClose": "Close", + "KeyUnknown": "Unknown", + "KeyShiftLeft": "Shift Left", + "KeyShiftRight": "Shift Right", + "KeyControlLeft": "Ctrl Left", + "KeyMacControlLeft": "⌃ Left", + "KeyControlRight": "Ctrl Right", + "KeyMacControlRight": "⌃ Right", + "KeyAltLeft": "Alt Left", + "KeyMacAltLeft": "⌥ Left", + "KeyAltRight": "Alt Right", + "KeyMacAltRight": "⌥ Right", + "KeyWinLeft": "⊞ Left", + "KeyMacWinLeft": "⌘ Left", + "KeyWinRight": "⊞ Right", + "KeyMacWinRight": "⌘ Right", + "KeyMenu": "Menu", + "KeyUp": "Up", + "KeyDown": "Down", + "KeyLeft": "Left", + "KeyRight": "Right", + "KeyEnter": "Enter", + "KeyEscape": "Escape", + "KeySpace": "Space", + "KeyTab": "Tab", + "KeyBackSpace": "Backspace", + "KeyInsert": "Insert", + "KeyDelete": "Delete", + "KeyPageUp": "Page Up", + "KeyPageDown": "Page Down", + "KeyHome": "Home", + "KeyEnd": "End", + "KeyCapsLock": "Caps Lock", + "KeyScrollLock": "Scroll Lock", + "KeyPrintScreen": "Print Screen", + "KeyPause": "Pause", + "KeyNumLock": "Num Lock", + "KeyClear": "Clear", + "KeyKeypad0": "Keypad 0", + "KeyKeypad1": "Keypad 1", + "KeyKeypad2": "Keypad 2", + "KeyKeypad3": "Keypad 3", + "KeyKeypad4": "Keypad 4", + "KeyKeypad5": "Keypad 5", + "KeyKeypad6": "Keypad 6", + "KeyKeypad7": "Keypad 7", + "KeyKeypad8": "Keypad 8", + "KeyKeypad9": "Keypad 9", + "KeyKeypadDivide": "Keypad Divide", + "KeyKeypadMultiply": "Keypad Multiply", + "KeyKeypadSubtract": "Keypad Subtract", + "KeyKeypadAdd": "Keypad Add", + "KeyKeypadDecimal": "Keypad Decimal", + "KeyKeypadEnter": "Keypad Enter", + "KeyNumber0": "0", + "KeyNumber1": "1", + "KeyNumber2": "2", + "KeyNumber3": "3", + "KeyNumber4": "4", + "KeyNumber5": "5", + "KeyNumber6": "6", + "KeyNumber7": "7", + "KeyNumber8": "8", + "KeyNumber9": "9", + "KeyTilde": "~", + "KeyGrave": "`", + "KeyMinus": "-", + "KeyPlus": "+", + "KeyBracketLeft": "[", + "KeyBracketRight": "]", + "KeySemicolon": ";", + "KeyQuote": "\"", + "KeyComma": ",", + "KeyPeriod": ".", + "KeySlash": "/", + "KeyBackSlash": "\\", + "KeyUnbound": "Unbound", + "GamepadLeftStick": "L Stick Button", + "GamepadRightStick": "R Stick Button", + "GamepadLeftShoulder": "Left Shoulder", + "GamepadRightShoulder": "Right Shoulder", + "GamepadLeftTrigger": "Left Trigger", + "GamepadRightTrigger": "Right Trigger", + "GamepadDpadUp": "Up", + "GamepadDpadDown": "Down", + "GamepadDpadLeft": "Left", + "GamepadDpadRight": "Right", + "GamepadMinus": "-", + "GamepadPlus": "+", + "GamepadGuide": "Guide", + "GamepadMisc1": "Misc", + "GamepadPaddle1": "Paddle 1", + "GamepadPaddle2": "Paddle 2", + "GamepadPaddle3": "Paddle 3", + "GamepadPaddle4": "Paddle 4", + "GamepadTouchpad": "Touchpad", + "GamepadSingleLeftTrigger0": "Left Trigger 0", + "GamepadSingleRightTrigger0": "Right Trigger 0", + "GamepadSingleLeftTrigger1": "Left Trigger 1", + "GamepadSingleRightTrigger1": "Right Trigger 1", + "StickLeft": "Left Stick", + "StickRight": "Right Stick", "UserProfilesSelectedUserProfile": "Selected User Profile:", "UserProfilesSaveProfileName": "Save Profile Name", "UserProfilesChangeProfileImage": "Change Profile Image", @@ -301,6 +407,7 @@ "GameListContextMenuToggleFavorite": "Toggle Favorite", "GameListContextMenuToggleFavoriteToolTip": "Toggle Favorite status of Game", "SettingsTabGeneralTheme": "Theme:", + "SettingsTabGeneralThemeAuto": "Auto", "SettingsTabGeneralThemeDark": "Dark", "SettingsTabGeneralThemeLight": "Light", "ControllerSettingsConfigureGeneral": "Configure", diff --git a/src/Ryujinx/Assets/Locales/es_ES.json b/src/Ryujinx/Assets/Locales/es_ES.json index 38499c046..e58fa5dcf 100644 --- a/src/Ryujinx/Assets/Locales/es_ES.json +++ b/src/Ryujinx/Assets/Locales/es_ES.json @@ -30,6 +30,10 @@ "MenuBarToolsManageFileTypes": "Administrar tipos de archivo", "MenuBarToolsInstallFileTypes": "Instalar tipos de archivo", "MenuBarToolsUninstallFileTypes": "Desinstalar tipos de archivo", + "MenuBarView": "_View", + "MenuBarViewWindow": "Window Size", + "MenuBarViewWindow720": "720p", + "MenuBarViewWindow1080": "1080p", "MenuBarHelp": "_Ayuda", "MenuBarHelpCheckForUpdates": "Buscar actualizaciones", "MenuBarHelpAbout": "Acerca de", @@ -92,6 +96,7 @@ "SettingsTabGeneralEnableDiscordRichPresence": "Habilitar estado en Discord", "SettingsTabGeneralCheckUpdatesOnLaunch": "Buscar actualizaciones al iniciar", "SettingsTabGeneralShowConfirmExitDialog": "Mostrar diálogo de confirmación al cerrar", + "SettingsTabGeneralRememberWindowState": "Remember Window Size/Position", "SettingsTabGeneralHideCursor": "Esconder el cursor:", "SettingsTabGeneralHideCursorNever": "Nunca", "SettingsTabGeneralHideCursorOnIdle": "Ocultar cursor cuando esté inactivo", @@ -155,7 +160,7 @@ "SettingsTabGraphicsResolutionScaleNative": "Nativa (720p/1080p)", "SettingsTabGraphicsResolutionScale2x": "x2 (1440p/2160p)", "SettingsTabGraphicsResolutionScale3x": "x3 (2160p/3240p)", - "SettingsTabGraphicsResolutionScale4x": "4x (2880p/4320p) (Not recommended)", + "SettingsTabGraphicsResolutionScale4x": "4x (2880p/4320p) (no recomendado)", "SettingsTabGraphicsAspectRatio": "Relación de aspecto:", "SettingsTabGraphicsAspectRatio4x3": "4:3", "SettingsTabGraphicsAspectRatio16x9": "16:9", @@ -266,6 +271,107 @@ "ControllerSettingsMotionGyroDeadzone": "Zona muerta de Gyro:", "ControllerSettingsSave": "Guardar", "ControllerSettingsClose": "Cerrar", + "KeyUnknown": "Desconocido", + "KeyShiftLeft": "Shift Left", + "KeyShiftRight": "Shift Right", + "KeyControlLeft": "Ctrl Left", + "KeyMacControlLeft": "⌃ Left", + "KeyControlRight": "Ctrl Right", + "KeyMacControlRight": "⌃ Right", + "KeyAltLeft": "Alt Left", + "KeyMacAltLeft": "⌥ Left", + "KeyAltRight": "Alt Right", + "KeyMacAltRight": "⌥ Right", + "KeyWinLeft": "⊞ Left", + "KeyMacWinLeft": "⌘ Left", + "KeyWinRight": "⊞ Right", + "KeyMacWinRight": "⌘ Right", + "KeyMenu": "Menu", + "KeyUp": "Up", + "KeyDown": "Down", + "KeyLeft": "Left", + "KeyRight": "Right", + "KeyEnter": "Enter", + "KeyEscape": "Escape", + "KeySpace": "Space", + "KeyTab": "Tab", + "KeyBackSpace": "Backspace", + "KeyInsert": "Insert", + "KeyDelete": "Delete", + "KeyPageUp": "Page Up", + "KeyPageDown": "Page Down", + "KeyHome": "Home", + "KeyEnd": "End", + "KeyCapsLock": "Caps Lock", + "KeyScrollLock": "Scroll Lock", + "KeyPrintScreen": "Print Screen", + "KeyPause": "Pause", + "KeyNumLock": "Num Lock", + "KeyClear": "Clear", + "KeyKeypad0": "Keypad 0", + "KeyKeypad1": "Keypad 1", + "KeyKeypad2": "Keypad 2", + "KeyKeypad3": "Keypad 3", + "KeyKeypad4": "Keypad 4", + "KeyKeypad5": "Keypad 5", + "KeyKeypad6": "Keypad 6", + "KeyKeypad7": "Keypad 7", + "KeyKeypad8": "Keypad 8", + "KeyKeypad9": "Keypad 9", + "KeyKeypadDivide": "Keypad Divide", + "KeyKeypadMultiply": "Keypad Multiply", + "KeyKeypadSubtract": "Keypad Subtract", + "KeyKeypadAdd": "Keypad Add", + "KeyKeypadDecimal": "Keypad Decimal", + "KeyKeypadEnter": "Keypad Enter", + "KeyNumber0": "0", + "KeyNumber1": "1", + "KeyNumber2": "2", + "KeyNumber3": "3", + "KeyNumber4": "4", + "KeyNumber5": "5", + "KeyNumber6": "6", + "KeyNumber7": "7", + "KeyNumber8": "8", + "KeyNumber9": "9", + "KeyTilde": "~", + "KeyGrave": "`", + "KeyMinus": "-", + "KeyPlus": "+", + "KeyBracketLeft": "[", + "KeyBracketRight": "]", + "KeySemicolon": ";", + "KeyQuote": "\"", + "KeyComma": ",", + "KeyPeriod": ".", + "KeySlash": "/", + "KeyBackSlash": "\\", + "KeyUnbound": "Unbound", + "GamepadLeftStick": "L Stick Button", + "GamepadRightStick": "R Stick Button", + "GamepadLeftShoulder": "Left Shoulder", + "GamepadRightShoulder": "Right Shoulder", + "GamepadLeftTrigger": "Left Trigger", + "GamepadRightTrigger": "Right Trigger", + "GamepadDpadUp": "Up", + "GamepadDpadDown": "Down", + "GamepadDpadLeft": "Left", + "GamepadDpadRight": "Right", + "GamepadMinus": "-", + "GamepadPlus": "+", + "GamepadGuide": "Guide", + "GamepadMisc1": "Misc", + "GamepadPaddle1": "Paddle 1", + "GamepadPaddle2": "Paddle 2", + "GamepadPaddle3": "Paddle 3", + "GamepadPaddle4": "Paddle 4", + "GamepadTouchpad": "Touchpad", + "GamepadSingleLeftTrigger0": "Left Trigger 0", + "GamepadSingleRightTrigger0": "Right Trigger 0", + "GamepadSingleLeftTrigger1": "Left Trigger 1", + "GamepadSingleRightTrigger1": "Right Trigger 1", + "StickLeft": "Left Stick", + "StickRight": "Right Stick", "UserProfilesSelectedUserProfile": "Perfil de usuario seleccionado:", "UserProfilesSaveProfileName": "Guardar nombre de perfil", "UserProfilesChangeProfileImage": "Cambiar imagen de perfil", @@ -597,6 +703,7 @@ "UserProfileWindowTitle": "Administrar perfiles de usuario", "CheatWindowTitle": "Administrar cheats", "DlcWindowTitle": "Administrar contenido descargable", + "ModWindowTitle": "Manage Mods for {0} ({1})", "UpdateWindowTitle": "Administrar actualizaciones", "CheatWindowHeading": "Cheats disponibles para {0} [{1}]", "BuildId": "Id de compilación:", @@ -647,12 +754,12 @@ "GraphicsAATooltip": "Applies anti-aliasing to the game render.\n\nFXAA will blur most of the image, while SMAA will attempt to find jagged edges and smooth them out.\n\nNot recommended to use in conjunction with the FSR scaling filter.\n\nThis option can be changed while a game is running by clicking \"Apply\" below; you can simply move the settings window aside and experiment until you find your preferred look for a game.\n\nLeave on NONE if unsure.", "GraphicsAALabel": "Suavizado de bordes:", "GraphicsScalingFilterLabel": "Filtro de escalado:", - "GraphicsScalingFilterTooltip": "Choose the scaling filter that will be applied when using resolution scale.\n\nBilinear works well for 3D games and is a safe default option.\n\nNearest is recommended for pixel art games.\n\nFSR 1.0 is merely a sharpening filter, not recommended for use with FXAA or SMAA.\n\nThis option can be changed while a game is running by clicking \"Apply\" below; you can simply move the settings window aside and experiment until you find your preferred look for a game.\n\nLeave on BILINEAR if unsure.", - "GraphicsScalingFilterBilinear": "Bilinear", - "GraphicsScalingFilterNearest": "Nearest", + "GraphicsScalingFilterTooltip": "Elija el filtro de escala que se aplicará al utilizar la escala de resolución.\n\nBilinear funciona bien para juegos 3D y es una opción predeterminada segura.\n\nSe recomienda el bilinear para juegos de pixel art.\n\nFSR 1.0 es simplemente un filtro de afilado, no se recomienda su uso con FXAA o SMAA.\n\nEsta opción se puede cambiar mientras se ejecuta un juego haciendo clic en \"Aplicar\" a continuación; simplemente puedes mover la ventana de configuración a un lado y experimentar hasta que encuentres tu estilo preferido para un juego.\n\nDéjelo en BILINEAR si no está seguro.", + "GraphicsScalingFilterBilinear": "Bilinear\n", + "GraphicsScalingFilterNearest": "Cercano", "GraphicsScalingFilterFsr": "FSR", "GraphicsScalingFilterLevelLabel": "Nivel", - "GraphicsScalingFilterLevelTooltip": "Set FSR 1.0 sharpening level. Higher is sharper.", + "GraphicsScalingFilterLevelTooltip": "Ajuste el nivel de nitidez FSR 1.0. Mayor es más nítido.", "SmaaLow": "SMAA Bajo", "SmaaMedium": "SMAA Medio", "SmaaHigh": "SMAA Alto", @@ -660,14 +767,14 @@ "UserEditorTitle": "Editar usuario", "UserEditorTitleCreate": "Crear Usuario", "SettingsTabNetworkInterface": "Interfaz de Red", - "NetworkInterfaceTooltip": "The network interface used for LAN/LDN features.\n\nIn conjunction with a VPN or XLink Kai and a game with LAN support, can be used to spoof a same-network connection over the Internet.\n\nLeave on DEFAULT if unsure.", + "NetworkInterfaceTooltip": "Interfaz de red usada para características LAN/LDN.\n\njunto con una VPN o XLink Kai y un juego con soporte LAN, puede usarse para suplantar una conexión de la misma red a través de Internet.\n\nDeje en DEFAULT si no está seguro.", "NetworkInterfaceDefault": "Predeterminado", "PackagingShaders": "Empaquetando sombreadores", "AboutChangelogButton": "Ver registro de cambios en GitHub", "AboutChangelogButtonTooltipMessage": "Haga clic para abrir el registro de cambios para esta versión en su navegador predeterminado.", "SettingsTabNetworkMultiplayer": "Multijugador", "MultiplayerMode": "Modo:", - "MultiplayerModeTooltip": "Change LDN multiplayer mode.\n\nLdnMitm will modify local wireless/local play functionality in games to function as if it were LAN, allowing for local, same-network connections with other Ryujinx instances and hacked Nintendo Switch consoles that have the ldn_mitm module installed.\n\nMultiplayer requires all players to be on the same game version (i.e. Super Smash Bros. Ultimate v13.0.1 can't connect to v13.0.0).\n\nLeave DISABLED if unsure.", - "MultiplayerModeDisabled": "Disabled", + "MultiplayerModeTooltip": "Cambiar modo LDN multijugador.\n\nLdnMitm modificará la funcionalidad local de juego inalámbrico para funcionar como si fuera LAN, permitiendo locales conexiones de la misma red con otras instancias de Ryujinx y consolas hackeadas de Nintendo Switch que tienen instalado el módulo ldn_mitm.\n\nMultijugador requiere que todos los jugadores estén en la misma versión del juego (por ejemplo, Super Smash Bros. Ultimate v13.0.1 no se puede conectar a v13.0.0).\n\nDejar DESACTIVADO si no está seguro.", + "MultiplayerModeDisabled": "Deshabilitar", "MultiplayerModeLdnMitm": "ldn_mitm" } diff --git a/src/Ryujinx/Assets/Locales/fr_FR.json b/src/Ryujinx/Assets/Locales/fr_FR.json index bb2864cf2..99a060650 100644 --- a/src/Ryujinx/Assets/Locales/fr_FR.json +++ b/src/Ryujinx/Assets/Locales/fr_FR.json @@ -30,6 +30,10 @@ "MenuBarToolsManageFileTypes": "Gérer les types de fichiers", "MenuBarToolsInstallFileTypes": "Installer les types de fichiers", "MenuBarToolsUninstallFileTypes": "Désinstaller les types de fichiers", + "MenuBarView": "_View", + "MenuBarViewWindow": "Window Size", + "MenuBarViewWindow720": "720p", + "MenuBarViewWindow1080": "1080p", "MenuBarHelp": "_Aide", "MenuBarHelpCheckForUpdates": "Vérifier les mises à jour", "MenuBarHelpAbout": "À propos", @@ -92,6 +96,7 @@ "SettingsTabGeneralEnableDiscordRichPresence": "Activer Discord Rich Presence", "SettingsTabGeneralCheckUpdatesOnLaunch": "Vérifier les mises à jour au démarrage", "SettingsTabGeneralShowConfirmExitDialog": "Afficher le message de \"Confirmation de sortie\"", + "SettingsTabGeneralRememberWindowState": "Remember Window Size/Position", "SettingsTabGeneralHideCursor": "Masquer le Curseur :", "SettingsTabGeneralHideCursorNever": "Jamais", "SettingsTabGeneralHideCursorOnIdle": "Masquer le curseur si inactif", @@ -266,6 +271,107 @@ "ControllerSettingsMotionGyroDeadzone": "Zone morte du gyroscope:", "ControllerSettingsSave": "Enregistrer", "ControllerSettingsClose": "Fermer", + "KeyUnknown": "Unknown", + "KeyShiftLeft": "Shift Left", + "KeyShiftRight": "Shift Right", + "KeyControlLeft": "Ctrl Left", + "KeyMacControlLeft": "⌃ Left", + "KeyControlRight": "Ctrl Right", + "KeyMacControlRight": "⌃ Right", + "KeyAltLeft": "Alt Left", + "KeyMacAltLeft": "⌥ Left", + "KeyAltRight": "Alt Right", + "KeyMacAltRight": "⌥ Right", + "KeyWinLeft": "⊞ Left", + "KeyMacWinLeft": "⌘ Left", + "KeyWinRight": "⊞ Right", + "KeyMacWinRight": "⌘ Right", + "KeyMenu": "Menu", + "KeyUp": "Up", + "KeyDown": "Down", + "KeyLeft": "Left", + "KeyRight": "Right", + "KeyEnter": "Enter", + "KeyEscape": "Escape", + "KeySpace": "Space", + "KeyTab": "Tab", + "KeyBackSpace": "Backspace", + "KeyInsert": "Insert", + "KeyDelete": "Delete", + "KeyPageUp": "Page Up", + "KeyPageDown": "Page Down", + "KeyHome": "Home", + "KeyEnd": "End", + "KeyCapsLock": "Caps Lock", + "KeyScrollLock": "Scroll Lock", + "KeyPrintScreen": "Print Screen", + "KeyPause": "Pause", + "KeyNumLock": "Num Lock", + "KeyClear": "Clear", + "KeyKeypad0": "Keypad 0", + "KeyKeypad1": "Keypad 1", + "KeyKeypad2": "Keypad 2", + "KeyKeypad3": "Keypad 3", + "KeyKeypad4": "Keypad 4", + "KeyKeypad5": "Keypad 5", + "KeyKeypad6": "Keypad 6", + "KeyKeypad7": "Keypad 7", + "KeyKeypad8": "Keypad 8", + "KeyKeypad9": "Keypad 9", + "KeyKeypadDivide": "Keypad Divide", + "KeyKeypadMultiply": "Keypad Multiply", + "KeyKeypadSubtract": "Keypad Subtract", + "KeyKeypadAdd": "Keypad Add", + "KeyKeypadDecimal": "Keypad Decimal", + "KeyKeypadEnter": "Keypad Enter", + "KeyNumber0": "0", + "KeyNumber1": "1", + "KeyNumber2": "2", + "KeyNumber3": "3", + "KeyNumber4": "4", + "KeyNumber5": "5", + "KeyNumber6": "6", + "KeyNumber7": "7", + "KeyNumber8": "8", + "KeyNumber9": "9", + "KeyTilde": "~", + "KeyGrave": "`", + "KeyMinus": "-", + "KeyPlus": "+", + "KeyBracketLeft": "[", + "KeyBracketRight": "]", + "KeySemicolon": ";", + "KeyQuote": "\"", + "KeyComma": ",", + "KeyPeriod": ".", + "KeySlash": "/", + "KeyBackSlash": "\\", + "KeyUnbound": "Unbound", + "GamepadLeftStick": "L Stick Button", + "GamepadRightStick": "R Stick Button", + "GamepadLeftShoulder": "Left Shoulder", + "GamepadRightShoulder": "Right Shoulder", + "GamepadLeftTrigger": "Left Trigger", + "GamepadRightTrigger": "Right Trigger", + "GamepadDpadUp": "Up", + "GamepadDpadDown": "Down", + "GamepadDpadLeft": "Left", + "GamepadDpadRight": "Right", + "GamepadMinus": "-", + "GamepadPlus": "+", + "GamepadGuide": "Guide", + "GamepadMisc1": "Misc", + "GamepadPaddle1": "Paddle 1", + "GamepadPaddle2": "Paddle 2", + "GamepadPaddle3": "Paddle 3", + "GamepadPaddle4": "Paddle 4", + "GamepadTouchpad": "Touchpad", + "GamepadSingleLeftTrigger0": "Left Trigger 0", + "GamepadSingleRightTrigger0": "Right Trigger 0", + "GamepadSingleLeftTrigger1": "Left Trigger 1", + "GamepadSingleRightTrigger1": "Right Trigger 1", + "StickLeft": "Left Stick", + "StickRight": "Right Stick", "UserProfilesSelectedUserProfile": "Profil utilisateur sélectionné :", "UserProfilesSaveProfileName": "Enregistrer le nom du profil", "UserProfilesChangeProfileImage": "Changer l'image du profil", @@ -597,6 +703,7 @@ "UserProfileWindowTitle": "Gestionnaire de profils utilisateur", "CheatWindowTitle": "Gestionnaire de triches", "DlcWindowTitle": "Gérer le contenu téléchargeable pour {0} ({1})", + "ModWindowTitle": "Gérer les mods pour {0} ({1})", "UpdateWindowTitle": "Gestionnaire de mises à jour", "CheatWindowHeading": "Cheats disponibles pour {0} [{1}]", "BuildId": "BuildId:", @@ -648,8 +755,8 @@ "GraphicsAALabel": "Anticrénelage :", "GraphicsScalingFilterLabel": "Filtre de mise à l'échelle :", "GraphicsScalingFilterTooltip": "Choisissez le filtre de mise à l'échelle qui sera appliqué lors de l'utilisation de la mise à l'échelle de la résolution.\n\nLe filtre bilinéaire fonctionne bien pour les jeux en 3D et constitue une option par défaut sûre.\n\nLe filtre le plus proche est recommandé pour les jeux de pixel art.\n\nFSR 1.0 est simplement un filtre de netteté, non recommandé pour une utilisation avec FXAA ou SMAA.\n\nCette option peut être modifiée pendant qu'un jeu est en cours d'exécution en cliquant sur \"Appliquer\" ci-dessous ; vous pouvez simplement déplacer la fenêtre des paramètres de côté et expérimenter jusqu'à ce que vous trouviez l'aspect souhaité pour un jeu.\n\nLaissez sur BILINEAR si vous n'êtes pas sûr.", - "GraphicsScalingFilterBilinear": "Bilinear", - "GraphicsScalingFilterNearest": "Nearest", + "GraphicsScalingFilterBilinear": "Bilinéaire", + "GraphicsScalingFilterNearest": "Le plus proche", "GraphicsScalingFilterFsr": "FSR", "GraphicsScalingFilterLevelLabel": "Niveau ", "GraphicsScalingFilterLevelTooltip": "Définissez le niveau de netteté FSR 1.0. Plus élevé signifie plus net.", @@ -668,6 +775,6 @@ "SettingsTabNetworkMultiplayer": "Multijoueur", "MultiplayerMode": "Mode :", "MultiplayerModeTooltip": "Changer le mode multijoueur LDN.\n\nLdnMitm modifiera la fonctionnalité de jeu sans fil local/jeu local dans les jeux pour fonctionner comme s'il s'agissait d'un LAN, permettant des connexions locales sur le même réseau avec d'autres instances de Ryujinx et des consoles Nintendo Switch piratées ayant le module ldn_mitm installé.\n\nLe multijoueur nécessite que tous les joueurs soient sur la même version du jeu (par exemple, Super Smash Bros. Ultimate v13.0.1 ne peut pas se connecter à v13.0.0).\n\nLaissez DÉSACTIVÉ si vous n'êtes pas sûr.", - "MultiplayerModeDisabled": "Disabled", + "MultiplayerModeDisabled": "Désactivé", "MultiplayerModeLdnMitm": "ldn_mitm" } diff --git a/src/Ryujinx/Assets/Locales/he_IL.json b/src/Ryujinx/Assets/Locales/he_IL.json index 16d68b775..848f78080 100644 --- a/src/Ryujinx/Assets/Locales/he_IL.json +++ b/src/Ryujinx/Assets/Locales/he_IL.json @@ -30,6 +30,10 @@ "MenuBarToolsManageFileTypes": "ניהול סוגי קבצים", "MenuBarToolsInstallFileTypes": "סוגי קבצי התקנה", "MenuBarToolsUninstallFileTypes": "סוגי קבצי הסרה", + "MenuBarView": "_View", + "MenuBarViewWindow": "Window Size", + "MenuBarViewWindow720": "720p", + "MenuBarViewWindow1080": "1080p", "MenuBarHelp": "_עזרה", "MenuBarHelpCheckForUpdates": "חפש עדכונים", "MenuBarHelpAbout": "אודות", @@ -92,6 +96,7 @@ "SettingsTabGeneralEnableDiscordRichPresence": "הפעלת תצוגה עשירה בדיסקורד", "SettingsTabGeneralCheckUpdatesOnLaunch": "בדוק אם קיימים עדכונים בהפעלה", "SettingsTabGeneralShowConfirmExitDialog": "הראה דיאלוג \"אשר יציאה\"", + "SettingsTabGeneralRememberWindowState": "Remember Window Size/Position", "SettingsTabGeneralHideCursor": "הסתר את הסמן", "SettingsTabGeneralHideCursorNever": "אף פעם", "SettingsTabGeneralHideCursorOnIdle": "במצב סרק", @@ -266,6 +271,107 @@ "ControllerSettingsMotionGyroDeadzone": "שטח מת של הג'ירוסקופ:", "ControllerSettingsSave": "שמירה", "ControllerSettingsClose": "סגירה", + "KeyUnknown": "Unknown", + "KeyShiftLeft": "Shift Left", + "KeyShiftRight": "Shift Right", + "KeyControlLeft": "Ctrl Left", + "KeyMacControlLeft": "⌃ Left", + "KeyControlRight": "Ctrl Right", + "KeyMacControlRight": "⌃ Right", + "KeyAltLeft": "Alt Left", + "KeyMacAltLeft": "⌥ Left", + "KeyAltRight": "Alt Right", + "KeyMacAltRight": "⌥ Right", + "KeyWinLeft": "⊞ Left", + "KeyMacWinLeft": "⌘ Left", + "KeyWinRight": "⊞ Right", + "KeyMacWinRight": "⌘ Right", + "KeyMenu": "Menu", + "KeyUp": "Up", + "KeyDown": "Down", + "KeyLeft": "Left", + "KeyRight": "Right", + "KeyEnter": "Enter", + "KeyEscape": "Escape", + "KeySpace": "Space", + "KeyTab": "Tab", + "KeyBackSpace": "Backspace", + "KeyInsert": "Insert", + "KeyDelete": "Delete", + "KeyPageUp": "Page Up", + "KeyPageDown": "Page Down", + "KeyHome": "Home", + "KeyEnd": "End", + "KeyCapsLock": "Caps Lock", + "KeyScrollLock": "Scroll Lock", + "KeyPrintScreen": "Print Screen", + "KeyPause": "Pause", + "KeyNumLock": "Num Lock", + "KeyClear": "Clear", + "KeyKeypad0": "Keypad 0", + "KeyKeypad1": "Keypad 1", + "KeyKeypad2": "Keypad 2", + "KeyKeypad3": "Keypad 3", + "KeyKeypad4": "Keypad 4", + "KeyKeypad5": "Keypad 5", + "KeyKeypad6": "Keypad 6", + "KeyKeypad7": "Keypad 7", + "KeyKeypad8": "Keypad 8", + "KeyKeypad9": "Keypad 9", + "KeyKeypadDivide": "Keypad Divide", + "KeyKeypadMultiply": "Keypad Multiply", + "KeyKeypadSubtract": "Keypad Subtract", + "KeyKeypadAdd": "Keypad Add", + "KeyKeypadDecimal": "Keypad Decimal", + "KeyKeypadEnter": "Keypad Enter", + "KeyNumber0": "0", + "KeyNumber1": "1", + "KeyNumber2": "2", + "KeyNumber3": "3", + "KeyNumber4": "4", + "KeyNumber5": "5", + "KeyNumber6": "6", + "KeyNumber7": "7", + "KeyNumber8": "8", + "KeyNumber9": "9", + "KeyTilde": "~", + "KeyGrave": "`", + "KeyMinus": "-", + "KeyPlus": "+", + "KeyBracketLeft": "[", + "KeyBracketRight": "]", + "KeySemicolon": ";", + "KeyQuote": "\"", + "KeyComma": ",", + "KeyPeriod": ".", + "KeySlash": "/", + "KeyBackSlash": "\\", + "KeyUnbound": "Unbound", + "GamepadLeftStick": "L Stick Button", + "GamepadRightStick": "R Stick Button", + "GamepadLeftShoulder": "Left Shoulder", + "GamepadRightShoulder": "Right Shoulder", + "GamepadLeftTrigger": "Left Trigger", + "GamepadRightTrigger": "Right Trigger", + "GamepadDpadUp": "Up", + "GamepadDpadDown": "Down", + "GamepadDpadLeft": "Left", + "GamepadDpadRight": "Right", + "GamepadMinus": "-", + "GamepadPlus": "+", + "GamepadGuide": "Guide", + "GamepadMisc1": "Misc", + "GamepadPaddle1": "Paddle 1", + "GamepadPaddle2": "Paddle 2", + "GamepadPaddle3": "Paddle 3", + "GamepadPaddle4": "Paddle 4", + "GamepadTouchpad": "Touchpad", + "GamepadSingleLeftTrigger0": "Left Trigger 0", + "GamepadSingleRightTrigger0": "Right Trigger 0", + "GamepadSingleLeftTrigger1": "Left Trigger 1", + "GamepadSingleRightTrigger1": "Right Trigger 1", + "StickLeft": "Left Stick", + "StickRight": "Right Stick", "UserProfilesSelectedUserProfile": "פרופיל המשתמש הנבחר:", "UserProfilesSaveProfileName": "שמור שם פרופיל", "UserProfilesChangeProfileImage": "שנה תמונת פרופיל", @@ -597,6 +703,7 @@ "UserProfileWindowTitle": "ניהול פרופילי משתמש", "CheatWindowTitle": "נהל צ'יטים למשחק", "DlcWindowTitle": "נהל הרחבות משחק עבור {0} ({1})", + "ModWindowTitle": "Manage Mods for {0} ({1})", "UpdateWindowTitle": "נהל עדכוני משחקים", "CheatWindowHeading": "צ'יטים זמינים עבור {0} [{1}]", "BuildId": "מזהה בניה:", diff --git a/src/Ryujinx/Assets/Locales/it_IT.json b/src/Ryujinx/Assets/Locales/it_IT.json index 0e832ffb6..280ebd880 100644 --- a/src/Ryujinx/Assets/Locales/it_IT.json +++ b/src/Ryujinx/Assets/Locales/it_IT.json @@ -2,7 +2,7 @@ "Language": "Italiano", "MenuBarFileOpenApplet": "Apri applet", "MenuBarFileOpenAppletOpenMiiAppletToolTip": "Apri l'applet Mii Editor in modalità Standalone", - "SettingsTabInputDirectMouseAccess": "Accesso diretto mouse", + "SettingsTabInputDirectMouseAccess": "Accesso diretto al mouse", "SettingsTabSystemMemoryManagerMode": "Modalità di gestione della memoria:", "SettingsTabSystemMemoryManagerModeSoftware": "Software", "SettingsTabSystemMemoryManagerModeHost": "Host (veloce)", @@ -30,6 +30,10 @@ "MenuBarToolsManageFileTypes": "Gestisci i tipi di file", "MenuBarToolsInstallFileTypes": "Installa i tipi di file", "MenuBarToolsUninstallFileTypes": "Disinstalla i tipi di file", + "MenuBarView": "_View", + "MenuBarViewWindow": "Window Size", + "MenuBarViewWindow720": "720p", + "MenuBarViewWindow1080": "1080p", "MenuBarHelp": "_Aiuto", "MenuBarHelpCheckForUpdates": "Controlla aggiornamenti", "MenuBarHelpAbout": "Informazioni", @@ -92,6 +96,7 @@ "SettingsTabGeneralEnableDiscordRichPresence": "Attiva Discord Rich Presence", "SettingsTabGeneralCheckUpdatesOnLaunch": "Controlla aggiornamenti all'avvio", "SettingsTabGeneralShowConfirmExitDialog": "Mostra dialogo \"Conferma Uscita\"", + "SettingsTabGeneralRememberWindowState": "Remember Window Size/Position", "SettingsTabGeneralHideCursor": "Nascondi il cursore:", "SettingsTabGeneralHideCursorNever": "Mai", "SettingsTabGeneralHideCursorOnIdle": "Quando è inattivo", @@ -266,6 +271,107 @@ "ControllerSettingsMotionGyroDeadzone": "Zona morta del giroscopio:", "ControllerSettingsSave": "Salva", "ControllerSettingsClose": "Chiudi", + "KeyUnknown": "Sconosciuto", + "KeyShiftLeft": "Maiusc sinistro", + "KeyShiftRight": "Maiusc destro", + "KeyControlLeft": "Ctrl sinistro", + "KeyMacControlLeft": "⌃ sinistro", + "KeyControlRight": "Ctrl destro", + "KeyMacControlRight": "⌃ destro", + "KeyAltLeft": "Alt sinistro", + "KeyMacAltLeft": "⌥ sinistro", + "KeyAltRight": "Alt destro", + "KeyMacAltRight": "⌥ destro", + "KeyWinLeft": "⊞ sinistro", + "KeyMacWinLeft": "⌘ sinistro", + "KeyWinRight": "⊞ destro", + "KeyMacWinRight": "⌘ destro", + "KeyMenu": "Menù", + "KeyUp": "Su", + "KeyDown": "Giù", + "KeyLeft": "Sinistra", + "KeyRight": "Destra", + "KeyEnter": "Invio", + "KeyEscape": "Esc", + "KeySpace": "Spazio", + "KeyTab": "Tab", + "KeyBackSpace": "Backspace", + "KeyInsert": "Ins", + "KeyDelete": "Canc", + "KeyPageUp": "Pag. Su", + "KeyPageDown": "Pag. Giù", + "KeyHome": "Inizio", + "KeyEnd": "Fine", + "KeyCapsLock": "Bloc Maiusc", + "KeyScrollLock": "Bloc Scorr", + "KeyPrintScreen": "Stamp", + "KeyPause": "Pausa", + "KeyNumLock": "Bloc Num", + "KeyClear": "Clear", + "KeyKeypad0": "Tast. num. 0", + "KeyKeypad1": "Tast. num. 1", + "KeyKeypad2": "Tast. num. 2", + "KeyKeypad3": "Tast. num. 3", + "KeyKeypad4": "Tast. num. 4", + "KeyKeypad5": "Tast. num. 5", + "KeyKeypad6": "Tast. num. 6", + "KeyKeypad7": "Tast. num. 7", + "KeyKeypad8": "Tast. num. 8", + "KeyKeypad9": "Tast. num. 9", + "KeyKeypadDivide": "Tast. num. /", + "KeyKeypadMultiply": "Tast. num. *", + "KeyKeypadSubtract": "Tast. num. -", + "KeyKeypadAdd": "Tast. num. +", + "KeyKeypadDecimal": "Tast. num. sep. decimale", + "KeyKeypadEnter": "Tast. num. Invio", + "KeyNumber0": "0", + "KeyNumber1": "1", + "KeyNumber2": "2", + "KeyNumber3": "3", + "KeyNumber4": "4", + "KeyNumber5": "5", + "KeyNumber6": "6", + "KeyNumber7": "7", + "KeyNumber8": "8", + "KeyNumber9": "9", + "KeyTilde": "ò", + "KeyGrave": "`", + "KeyMinus": "-", + "KeyPlus": "+", + "KeyBracketLeft": "'", + "KeyBracketRight": "ì", + "KeySemicolon": "è", + "KeyQuote": "à", + "KeyComma": ",", + "KeyPeriod": ".", + "KeySlash": "ù", + "KeyBackSlash": "<", + "KeyUnbound": "Non assegnato", + "GamepadLeftStick": "Pulsante levetta sinistra", + "GamepadRightStick": "Pulsante levetta destra", + "GamepadLeftShoulder": "Pulsante dorsale sinistro", + "GamepadRightShoulder": "Pulsante dorsale destro", + "GamepadLeftTrigger": "Grilletto sinistro", + "GamepadRightTrigger": "Grilletto destro", + "GamepadDpadUp": "Su", + "GamepadDpadDown": "Giù", + "GamepadDpadLeft": "Sinistra", + "GamepadDpadRight": "Destra", + "GamepadMinus": "-", + "GamepadPlus": "+", + "GamepadGuide": "Guide", + "GamepadMisc1": "Misc", + "GamepadPaddle1": "Paddle 1", + "GamepadPaddle2": "Paddle 2", + "GamepadPaddle3": "Paddle 3", + "GamepadPaddle4": "Paddle 4", + "GamepadTouchpad": "Touchpad", + "GamepadSingleLeftTrigger0": "Grilletto sinistro 0", + "GamepadSingleRightTrigger0": "Grilletto destro 0", + "GamepadSingleLeftTrigger1": "Grilletto sinistro 1", + "GamepadSingleRightTrigger1": "Grilletto destro 1", + "StickLeft": "Levetta sinistra", + "StickRight": "Levetta destra", "UserProfilesSelectedUserProfile": "Profilo utente selezionato:", "UserProfilesSaveProfileName": "Salva nome del profilo", "UserProfilesChangeProfileImage": "Cambia immagine profilo", @@ -597,6 +703,7 @@ "UserProfileWindowTitle": "Gestione profili utente", "CheatWindowTitle": "Gestione trucchi", "DlcWindowTitle": "Gestisci DLC per {0} ({1})", + "ModWindowTitle": "Gestisci mod per {0} ({1})", "UpdateWindowTitle": "Gestione aggiornamenti", "CheatWindowHeading": "Trucchi disponibili per {0} [{1}]", "BuildId": "ID Build", diff --git a/src/Ryujinx/Assets/Locales/ja_JP.json b/src/Ryujinx/Assets/Locales/ja_JP.json index 8790135e0..61e963258 100644 --- a/src/Ryujinx/Assets/Locales/ja_JP.json +++ b/src/Ryujinx/Assets/Locales/ja_JP.json @@ -30,6 +30,10 @@ "MenuBarToolsManageFileTypes": "ファイル形式を管理", "MenuBarToolsInstallFileTypes": "ファイル形式をインストール", "MenuBarToolsUninstallFileTypes": "ファイル形式をアンインストール", + "MenuBarView": "_View", + "MenuBarViewWindow": "Window Size", + "MenuBarViewWindow720": "720p", + "MenuBarViewWindow1080": "1080p", "MenuBarHelp": "ヘルプ(_H)", "MenuBarHelpCheckForUpdates": "アップデートを確認", "MenuBarHelpAbout": "Ryujinx について", @@ -92,6 +96,7 @@ "SettingsTabGeneralEnableDiscordRichPresence": "Discord リッチプレゼンスを有効にする", "SettingsTabGeneralCheckUpdatesOnLaunch": "起動時にアップデートを確認する", "SettingsTabGeneralShowConfirmExitDialog": "\"終了を確認\" ダイアログを表示する", + "SettingsTabGeneralRememberWindowState": "Remember Window Size/Position", "SettingsTabGeneralHideCursor": "マウスカーソルを非表示", "SettingsTabGeneralHideCursorNever": "決して", "SettingsTabGeneralHideCursorOnIdle": "アイドル時", @@ -266,6 +271,107 @@ "ControllerSettingsMotionGyroDeadzone": "ジャイロ遊び:", "ControllerSettingsSave": "セーブ", "ControllerSettingsClose": "閉じる", + "KeyUnknown": "Unknown", + "KeyShiftLeft": "Shift Left", + "KeyShiftRight": "Shift Right", + "KeyControlLeft": "Ctrl Left", + "KeyMacControlLeft": "⌃ Left", + "KeyControlRight": "Ctrl Right", + "KeyMacControlRight": "⌃ Right", + "KeyAltLeft": "Alt Left", + "KeyMacAltLeft": "⌥ Left", + "KeyAltRight": "Alt Right", + "KeyMacAltRight": "⌥ Right", + "KeyWinLeft": "⊞ Left", + "KeyMacWinLeft": "⌘ Left", + "KeyWinRight": "⊞ Right", + "KeyMacWinRight": "⌘ Right", + "KeyMenu": "Menu", + "KeyUp": "Up", + "KeyDown": "Down", + "KeyLeft": "Left", + "KeyRight": "Right", + "KeyEnter": "Enter", + "KeyEscape": "Escape", + "KeySpace": "Space", + "KeyTab": "Tab", + "KeyBackSpace": "Backspace", + "KeyInsert": "Insert", + "KeyDelete": "Delete", + "KeyPageUp": "Page Up", + "KeyPageDown": "Page Down", + "KeyHome": "Home", + "KeyEnd": "End", + "KeyCapsLock": "Caps Lock", + "KeyScrollLock": "Scroll Lock", + "KeyPrintScreen": "Print Screen", + "KeyPause": "Pause", + "KeyNumLock": "Num Lock", + "KeyClear": "Clear", + "KeyKeypad0": "Keypad 0", + "KeyKeypad1": "Keypad 1", + "KeyKeypad2": "Keypad 2", + "KeyKeypad3": "Keypad 3", + "KeyKeypad4": "Keypad 4", + "KeyKeypad5": "Keypad 5", + "KeyKeypad6": "Keypad 6", + "KeyKeypad7": "Keypad 7", + "KeyKeypad8": "Keypad 8", + "KeyKeypad9": "Keypad 9", + "KeyKeypadDivide": "Keypad Divide", + "KeyKeypadMultiply": "Keypad Multiply", + "KeyKeypadSubtract": "Keypad Subtract", + "KeyKeypadAdd": "Keypad Add", + "KeyKeypadDecimal": "Keypad Decimal", + "KeyKeypadEnter": "Keypad Enter", + "KeyNumber0": "0", + "KeyNumber1": "1", + "KeyNumber2": "2", + "KeyNumber3": "3", + "KeyNumber4": "4", + "KeyNumber5": "5", + "KeyNumber6": "6", + "KeyNumber7": "7", + "KeyNumber8": "8", + "KeyNumber9": "9", + "KeyTilde": "~", + "KeyGrave": "`", + "KeyMinus": "-", + "KeyPlus": "+", + "KeyBracketLeft": "[", + "KeyBracketRight": "]", + "KeySemicolon": ";", + "KeyQuote": "\"", + "KeyComma": ",", + "KeyPeriod": ".", + "KeySlash": "/", + "KeyBackSlash": "\\", + "KeyUnbound": "Unbound", + "GamepadLeftStick": "L Stick Button", + "GamepadRightStick": "R Stick Button", + "GamepadLeftShoulder": "Left Shoulder", + "GamepadRightShoulder": "Right Shoulder", + "GamepadLeftTrigger": "Left Trigger", + "GamepadRightTrigger": "Right Trigger", + "GamepadDpadUp": "Up", + "GamepadDpadDown": "Down", + "GamepadDpadLeft": "Left", + "GamepadDpadRight": "Right", + "GamepadMinus": "-", + "GamepadPlus": "+", + "GamepadGuide": "Guide", + "GamepadMisc1": "Misc", + "GamepadPaddle1": "Paddle 1", + "GamepadPaddle2": "Paddle 2", + "GamepadPaddle3": "Paddle 3", + "GamepadPaddle4": "Paddle 4", + "GamepadTouchpad": "Touchpad", + "GamepadSingleLeftTrigger0": "Left Trigger 0", + "GamepadSingleRightTrigger0": "Right Trigger 0", + "GamepadSingleLeftTrigger1": "Left Trigger 1", + "GamepadSingleRightTrigger1": "Right Trigger 1", + "StickLeft": "Left Stick", + "StickRight": "Right Stick", "UserProfilesSelectedUserProfile": "選択されたユーザプロファイル:", "UserProfilesSaveProfileName": "プロファイル名をセーブ", "UserProfilesChangeProfileImage": "プロファイル画像を変更", @@ -597,6 +703,7 @@ "UserProfileWindowTitle": "ユーザプロファイルを管理", "CheatWindowTitle": "チート管理", "DlcWindowTitle": "DLC 管理", + "ModWindowTitle": "Manage Mods for {0} ({1})", "UpdateWindowTitle": "アップデート管理", "CheatWindowHeading": "利用可能なチート {0} [{1}]", "BuildId": "ビルドID:", diff --git a/src/Ryujinx/Assets/Locales/ko_KR.json b/src/Ryujinx/Assets/Locales/ko_KR.json index 0cd054cab..a92d084e0 100644 --- a/src/Ryujinx/Assets/Locales/ko_KR.json +++ b/src/Ryujinx/Assets/Locales/ko_KR.json @@ -30,6 +30,10 @@ "MenuBarToolsManageFileTypes": "파일 형식 관리", "MenuBarToolsInstallFileTypes": "파일 형식 설치", "MenuBarToolsUninstallFileTypes": "파일 형식 설치 제거", + "MenuBarView": "_보기", + "MenuBarViewWindow": "창 크기", + "MenuBarViewWindow720": "720p", + "MenuBarViewWindow1080": "1080p", "MenuBarHelp": "도움말(_H)", "MenuBarHelpCheckForUpdates": "업데이트 확인", "MenuBarHelpAbout": "정보", @@ -92,6 +96,7 @@ "SettingsTabGeneralEnableDiscordRichPresence": "디스코드 활동 상태 활성화", "SettingsTabGeneralCheckUpdatesOnLaunch": "시작 시, 업데이트 확인", "SettingsTabGeneralShowConfirmExitDialog": "\"종료 확인\" 대화 상자 표시", + "SettingsTabGeneralRememberWindowState": "창 크기/위치 기억", "SettingsTabGeneralHideCursor": "마우스 커서 숨기기", "SettingsTabGeneralHideCursorNever": "절대 안 함", "SettingsTabGeneralHideCursorOnIdle": "유휴 상태", @@ -266,6 +271,107 @@ "ControllerSettingsMotionGyroDeadzone": "자이로 사각지대 :", "ControllerSettingsSave": "저장", "ControllerSettingsClose": "닫기", + "KeyUnknown": "알 수 없음", + "KeyShiftLeft": "왼쪽 Shift", + "KeyShiftRight": "오른쪽 Shift", + "KeyControlLeft": "왼쪽 Ctrl", + "KeyMacControlLeft": "왼쪽 ^", + "KeyControlRight": "오른쪽 Ctrl", + "KeyMacControlRight": "오른쪽 ^", + "KeyAltLeft": "왼쪽 Alt", + "KeyMacAltLeft": "왼쪽 ⌥", + "KeyAltRight": "오른쪽 Alt", + "KeyMacAltRight": "오른쪽 ⌥", + "KeyWinLeft": "왼쪽 ⊞", + "KeyMacWinLeft": "왼쪽 ⌘", + "KeyWinRight": "오른쪽 ⊞", + "KeyMacWinRight": "오른쪽 ⌘", + "KeyMenu": "메뉴", + "KeyUp": "↑", + "KeyDown": "↓", + "KeyLeft": "←", + "KeyRight": "→", + "KeyEnter": "엔터", + "KeyEscape": "이스케이프", + "KeySpace": "스페이스", + "KeyTab": "탭", + "KeyBackSpace": "백스페이스", + "KeyInsert": "Ins", + "KeyDelete": "Del", + "KeyPageUp": "Page Up", + "KeyPageDown": "Page Down", + "KeyHome": "Home", + "KeyEnd": "End", + "KeyCapsLock": "Caps Lock", + "KeyScrollLock": "Scroll Lock", + "KeyPrintScreen": "프린트 스크린", + "KeyPause": "Pause", + "KeyNumLock": "Num Lock", + "KeyClear": "지우기", + "KeyKeypad0": "키패드 0", + "KeyKeypad1": "키패드 1", + "KeyKeypad2": "키패드 2", + "KeyKeypad3": "키패드 3", + "KeyKeypad4": "키패드 4", + "KeyKeypad5": "키패드 5", + "KeyKeypad6": "키패드 6", + "KeyKeypad7": "키패드 7", + "KeyKeypad8": "키패드 8", + "KeyKeypad9": "키패드 9", + "KeyKeypadDivide": "키패드 분할", + "KeyKeypadMultiply": "키패드 멀티플", + "KeyKeypadSubtract": "키패드 빼기", + "KeyKeypadAdd": "키패드 추가", + "KeyKeypadDecimal": "숫자 키패드", + "KeyKeypadEnter": "키패드 엔터", + "KeyNumber0": "0", + "KeyNumber1": "1", + "KeyNumber2": "2", + "KeyNumber3": "3", + "KeyNumber4": "4", + "KeyNumber5": "5", + "KeyNumber6": "6", + "KeyNumber7": "7", + "KeyNumber8": "8", + "KeyNumber9": "9", + "KeyTilde": "~", + "KeyGrave": "`", + "KeyMinus": "-", + "KeyPlus": "+", + "KeyBracketLeft": "[", + "KeyBracketRight": "]", + "KeySemicolon": ";", + "KeyQuote": "\"", + "KeyComma": ",", + "KeyPeriod": ".", + "KeySlash": "/", + "KeyBackSlash": "\\", + "KeyUnbound": "바인딩 해제", + "GamepadLeftStick": "L 스틱 버튼", + "GamepadRightStick": "R 스틱 버튼", + "GamepadLeftShoulder": "좌측 숄더", + "GamepadRightShoulder": "우측 숄더", + "GamepadLeftTrigger": "좌측 트리거", + "GamepadRightTrigger": "우측 트리거", + "GamepadDpadUp": "↑", + "GamepadDpadDown": "↓", + "GamepadDpadLeft": "←", + "GamepadDpadRight": "→", + "GamepadMinus": "-", + "GamepadPlus": "+", + "GamepadGuide": "안내", + "GamepadMisc1": "기타", + "GamepadPaddle1": "패들 1", + "GamepadPaddle2": "패들 2", + "GamepadPaddle3": "패들 3", + "GamepadPaddle4": "패들 4", + "GamepadTouchpad": "터치패드", + "GamepadSingleLeftTrigger0": "왼쪽 트리거 0", + "GamepadSingleRightTrigger0": "오른쪽 트리거 0", + "GamepadSingleLeftTrigger1": "왼쪽 트리거 1", + "GamepadSingleRightTrigger1": "오른쪽 트리거 1", + "StickLeft": "좌측 스틱", + "StickRight": "우측 스틱", "UserProfilesSelectedUserProfile": "선택한 사용자 프로필 :", "UserProfilesSaveProfileName": "프로필 이름 저장", "UserProfilesChangeProfileImage": "프로필 이미지 변경", @@ -597,6 +703,7 @@ "UserProfileWindowTitle": "사용자 프로파일 관리자", "CheatWindowTitle": "치트 관리자", "DlcWindowTitle": "{0} ({1})의 다운로드 가능한 콘텐츠 관리", + "ModWindowTitle": "{0} ({1})의 Mod 관리", "UpdateWindowTitle": "타이틀 업데이트 관리자", "CheatWindowHeading": "{0} [{1}]에 사용할 수 있는 치트", "BuildId": "빌드ID :", diff --git a/src/Ryujinx/Assets/Locales/pl_PL.json b/src/Ryujinx/Assets/Locales/pl_PL.json index 94096dd7b..9d1bd7b44 100644 --- a/src/Ryujinx/Assets/Locales/pl_PL.json +++ b/src/Ryujinx/Assets/Locales/pl_PL.json @@ -30,6 +30,10 @@ "MenuBarToolsManageFileTypes": "Zarządzaj rodzajami plików", "MenuBarToolsInstallFileTypes": "Typy plików instalacyjnych", "MenuBarToolsUninstallFileTypes": "Typy plików dezinstalacyjnych", + "MenuBarView": "_View", + "MenuBarViewWindow": "Window Size", + "MenuBarViewWindow720": "720p", + "MenuBarViewWindow1080": "1080p", "MenuBarHelp": "_Pomoc", "MenuBarHelpCheckForUpdates": "Sprawdź aktualizacje", "MenuBarHelpAbout": "O programie", @@ -92,6 +96,7 @@ "SettingsTabGeneralEnableDiscordRichPresence": "Włącz Bogatą Obecność Discord", "SettingsTabGeneralCheckUpdatesOnLaunch": "Sprawdzaj aktualizacje przy uruchomieniu", "SettingsTabGeneralShowConfirmExitDialog": "Pokazuj okno dialogowe \"Potwierdź wyjście\"", + "SettingsTabGeneralRememberWindowState": "Remember Window Size/Position", "SettingsTabGeneralHideCursor": "Ukryj kursor:", "SettingsTabGeneralHideCursorNever": "Nigdy", "SettingsTabGeneralHideCursorOnIdle": "Gdy bezczynny", @@ -266,6 +271,107 @@ "ControllerSettingsMotionGyroDeadzone": "Deadzone Żyroskopu:", "ControllerSettingsSave": "Zapisz", "ControllerSettingsClose": "Zamknij", + "KeyUnknown": "Unknown", + "KeyShiftLeft": "Shift Left", + "KeyShiftRight": "Shift Right", + "KeyControlLeft": "Ctrl Left", + "KeyMacControlLeft": "⌃ Left", + "KeyControlRight": "Ctrl Right", + "KeyMacControlRight": "⌃ Right", + "KeyAltLeft": "Alt Left", + "KeyMacAltLeft": "⌥ Left", + "KeyAltRight": "Alt Right", + "KeyMacAltRight": "⌥ Right", + "KeyWinLeft": "⊞ Left", + "KeyMacWinLeft": "⌘ Left", + "KeyWinRight": "⊞ Right", + "KeyMacWinRight": "⌘ Right", + "KeyMenu": "Menu", + "KeyUp": "Up", + "KeyDown": "Down", + "KeyLeft": "Left", + "KeyRight": "Right", + "KeyEnter": "Enter", + "KeyEscape": "Escape", + "KeySpace": "Space", + "KeyTab": "Tab", + "KeyBackSpace": "Backspace", + "KeyInsert": "Insert", + "KeyDelete": "Delete", + "KeyPageUp": "Page Up", + "KeyPageDown": "Page Down", + "KeyHome": "Home", + "KeyEnd": "End", + "KeyCapsLock": "Caps Lock", + "KeyScrollLock": "Scroll Lock", + "KeyPrintScreen": "Print Screen", + "KeyPause": "Pause", + "KeyNumLock": "Num Lock", + "KeyClear": "Clear", + "KeyKeypad0": "Keypad 0", + "KeyKeypad1": "Keypad 1", + "KeyKeypad2": "Keypad 2", + "KeyKeypad3": "Keypad 3", + "KeyKeypad4": "Keypad 4", + "KeyKeypad5": "Keypad 5", + "KeyKeypad6": "Keypad 6", + "KeyKeypad7": "Keypad 7", + "KeyKeypad8": "Keypad 8", + "KeyKeypad9": "Keypad 9", + "KeyKeypadDivide": "Keypad Divide", + "KeyKeypadMultiply": "Keypad Multiply", + "KeyKeypadSubtract": "Keypad Subtract", + "KeyKeypadAdd": "Keypad Add", + "KeyKeypadDecimal": "Keypad Decimal", + "KeyKeypadEnter": "Keypad Enter", + "KeyNumber0": "0", + "KeyNumber1": "1", + "KeyNumber2": "2", + "KeyNumber3": "3", + "KeyNumber4": "4", + "KeyNumber5": "5", + "KeyNumber6": "6", + "KeyNumber7": "7", + "KeyNumber8": "8", + "KeyNumber9": "9", + "KeyTilde": "~", + "KeyGrave": "`", + "KeyMinus": "-", + "KeyPlus": "+", + "KeyBracketLeft": "[", + "KeyBracketRight": "]", + "KeySemicolon": ";", + "KeyQuote": "\"", + "KeyComma": ",", + "KeyPeriod": ".", + "KeySlash": "/", + "KeyBackSlash": "\\", + "KeyUnbound": "Unbound", + "GamepadLeftStick": "L Stick Button", + "GamepadRightStick": "R Stick Button", + "GamepadLeftShoulder": "Left Shoulder", + "GamepadRightShoulder": "Right Shoulder", + "GamepadLeftTrigger": "Left Trigger", + "GamepadRightTrigger": "Right Trigger", + "GamepadDpadUp": "Up", + "GamepadDpadDown": "Down", + "GamepadDpadLeft": "Left", + "GamepadDpadRight": "Right", + "GamepadMinus": "-", + "GamepadPlus": "+", + "GamepadGuide": "Guide", + "GamepadMisc1": "Misc", + "GamepadPaddle1": "Paddle 1", + "GamepadPaddle2": "Paddle 2", + "GamepadPaddle3": "Paddle 3", + "GamepadPaddle4": "Paddle 4", + "GamepadTouchpad": "Touchpad", + "GamepadSingleLeftTrigger0": "Left Trigger 0", + "GamepadSingleRightTrigger0": "Right Trigger 0", + "GamepadSingleLeftTrigger1": "Left Trigger 1", + "GamepadSingleRightTrigger1": "Right Trigger 1", + "StickLeft": "Left Stick", + "StickRight": "Right Stick", "UserProfilesSelectedUserProfile": "Wybrany profil użytkownika:", "UserProfilesSaveProfileName": "Zapisz nazwę profilu", "UserProfilesChangeProfileImage": "Zmień obrazek profilu", @@ -341,7 +447,7 @@ "DialogThemeRestartMessage": "Motyw został zapisany. Aby zastosować motyw, konieczne jest ponowne uruchomienie.", "DialogThemeRestartSubMessage": "Czy chcesz uruchomić ponownie?", "DialogFirmwareInstallEmbeddedMessage": "Czy chcesz zainstalować firmware wbudowany w tę grę? (Firmware {0})", - "DialogFirmwareInstallEmbeddedSuccessMessage": "No installed firmware was found but Ryujinx was able to install firmware {0} from the provided game.\nThe emulator will now start.", + "DialogFirmwareInstallEmbeddedSuccessMessage": "Nie znaleziono zainstalowanego oprogramowania, ale Ryujinx był w stanie zainstalować oprogramowanie {0} z dostarczonej gry.\n\nEmulator uruchomi się teraz.", "DialogFirmwareNoFirmwareInstalledMessage": "Brak Zainstalowanego Firmware'u", "DialogFirmwareInstalledMessage": "Firmware {0} został zainstalowany", "DialogInstallFileTypesSuccessMessage": "Pomyślnie zainstalowano typy plików!", @@ -597,6 +703,7 @@ "UserProfileWindowTitle": "Menedżer Profili Użytkowników", "CheatWindowTitle": "Menedżer Kodów", "DlcWindowTitle": "Menedżer Zawartości do Pobrania", + "ModWindowTitle": "Zarządzaj modami dla {0} ({1})", "UpdateWindowTitle": "Menedżer Aktualizacji Tytułu", "CheatWindowHeading": "Kody Dostępne dla {0} [{1}]", "BuildId": "Identyfikator wersji:", @@ -648,11 +755,11 @@ "GraphicsAALabel": "Antyaliasing:", "GraphicsScalingFilterLabel": "Filtr skalowania:", "GraphicsScalingFilterTooltip": "Choose the scaling filter that will be applied when using resolution scale.\n\nBilinear works well for 3D games and is a safe default option.\n\nNearest is recommended for pixel art games.\n\nFSR 1.0 is merely a sharpening filter, not recommended for use with FXAA or SMAA.\n\nThis option can be changed while a game is running by clicking \"Apply\" below; you can simply move the settings window aside and experiment until you find your preferred look for a game.\n\nLeave on BILINEAR if unsure.", - "GraphicsScalingFilterBilinear": "Bilinear", - "GraphicsScalingFilterNearest": "Nearest", + "GraphicsScalingFilterBilinear": "Dwuliniowe", + "GraphicsScalingFilterNearest": "Najbliższe", "GraphicsScalingFilterFsr": "FSR", "GraphicsScalingFilterLevelLabel": "Poziom", - "GraphicsScalingFilterLevelTooltip": "Set FSR 1.0 sharpening level. Higher is sharper.", + "GraphicsScalingFilterLevelTooltip": "Ustaw poziom ostrzeżenia FSR 1.0. Wyższy jest ostrzejszy.", "SmaaLow": "SMAA Niskie", "SmaaMedium": "SMAA Średnie", "SmaaHigh": "SMAA Wysokie", @@ -660,7 +767,7 @@ "UserEditorTitle": "Edytuj użytkownika", "UserEditorTitleCreate": "Utwórz użytkownika", "SettingsTabNetworkInterface": "Interfejs sieci:", - "NetworkInterfaceTooltip": "The network interface used for LAN/LDN features.\n\nIn conjunction with a VPN or XLink Kai and a game with LAN support, can be used to spoof a same-network connection over the Internet.\n\nLeave on DEFAULT if unsure.", + "NetworkInterfaceTooltip": "Interfejs sieciowy używany dla funkcji LAN/LDN.\n\nw połączeniu z VPN lub XLink Kai i grą z obsługą sieci LAN, może być użyty do spoofowania połączenia z tą samą siecią przez Internet.\n\nZostaw DOMYŚLNE, jeśli nie ma pewności.", "NetworkInterfaceDefault": "Domyślny", "PackagingShaders": "Pakuje Shadery ", "AboutChangelogButton": "Zobacz listę zmian na GitHubie", @@ -668,6 +775,6 @@ "SettingsTabNetworkMultiplayer": "Gra Wieloosobowa", "MultiplayerMode": "Tryb:", "MultiplayerModeTooltip": "Change LDN multiplayer mode.\n\nLdnMitm will modify local wireless/local play functionality in games to function as if it were LAN, allowing for local, same-network connections with other Ryujinx instances and hacked Nintendo Switch consoles that have the ldn_mitm module installed.\n\nMultiplayer requires all players to be on the same game version (i.e. Super Smash Bros. Ultimate v13.0.1 can't connect to v13.0.0).\n\nLeave DISABLED if unsure.", - "MultiplayerModeDisabled": "Disabled", + "MultiplayerModeDisabled": "Wyłączone", "MultiplayerModeLdnMitm": "ldn_mitm" } diff --git a/src/Ryujinx/Assets/Locales/pt_BR.json b/src/Ryujinx/Assets/Locales/pt_BR.json index fb48bd593..a8c244b65 100644 --- a/src/Ryujinx/Assets/Locales/pt_BR.json +++ b/src/Ryujinx/Assets/Locales/pt_BR.json @@ -30,6 +30,10 @@ "MenuBarToolsManageFileTypes": "Gerenciar tipos de arquivo", "MenuBarToolsInstallFileTypes": "Instalar tipos de arquivo", "MenuBarToolsUninstallFileTypes": "Desinstalar tipos de arquivos", + "MenuBarView": "_View", + "MenuBarViewWindow": "Window Size", + "MenuBarViewWindow720": "720p", + "MenuBarViewWindow1080": "1080p", "MenuBarHelp": "_Ajuda", "MenuBarHelpCheckForUpdates": "_Verificar se há atualizações", "MenuBarHelpAbout": "_Sobre", @@ -92,6 +96,7 @@ "SettingsTabGeneralEnableDiscordRichPresence": "Habilitar Rich Presence do Discord", "SettingsTabGeneralCheckUpdatesOnLaunch": "Verificar se há atualizações ao iniciar", "SettingsTabGeneralShowConfirmExitDialog": "Exibir diálogo de confirmação ao sair", + "SettingsTabGeneralRememberWindowState": "Lembrar tamanho/posição da Janela", "SettingsTabGeneralHideCursor": "Esconder o cursor do mouse:", "SettingsTabGeneralHideCursorNever": "Nunca", "SettingsTabGeneralHideCursorOnIdle": "Esconder o cursor quando ocioso", @@ -266,6 +271,107 @@ "ControllerSettingsMotionGyroDeadzone": "Zona morta do giroscópio:", "ControllerSettingsSave": "Salvar", "ControllerSettingsClose": "Fechar", + "KeyUnknown": "Unknown", + "KeyShiftLeft": "Shift Left", + "KeyShiftRight": "Shift Right", + "KeyControlLeft": "Ctrl Left", + "KeyMacControlLeft": "⌃ Left", + "KeyControlRight": "Ctrl Right", + "KeyMacControlRight": "⌃ Right", + "KeyAltLeft": "Alt Left", + "KeyMacAltLeft": "⌥ Left", + "KeyAltRight": "Alt Right", + "KeyMacAltRight": "⌥ Right", + "KeyWinLeft": "⊞ Left", + "KeyMacWinLeft": "⌘ Left", + "KeyWinRight": "⊞ Right", + "KeyMacWinRight": "⌘ Right", + "KeyMenu": "Menu", + "KeyUp": "Up", + "KeyDown": "Down", + "KeyLeft": "Left", + "KeyRight": "Right", + "KeyEnter": "Enter", + "KeyEscape": "Escape", + "KeySpace": "Space", + "KeyTab": "Tab", + "KeyBackSpace": "Backspace", + "KeyInsert": "Insert", + "KeyDelete": "Delete", + "KeyPageUp": "Page Up", + "KeyPageDown": "Page Down", + "KeyHome": "Home", + "KeyEnd": "End", + "KeyCapsLock": "Caps Lock", + "KeyScrollLock": "Scroll Lock", + "KeyPrintScreen": "Print Screen", + "KeyPause": "Pause", + "KeyNumLock": "Num Lock", + "KeyClear": "Clear", + "KeyKeypad0": "Keypad 0", + "KeyKeypad1": "Keypad 1", + "KeyKeypad2": "Keypad 2", + "KeyKeypad3": "Keypad 3", + "KeyKeypad4": "Keypad 4", + "KeyKeypad5": "Keypad 5", + "KeyKeypad6": "Keypad 6", + "KeyKeypad7": "Keypad 7", + "KeyKeypad8": "Keypad 8", + "KeyKeypad9": "Keypad 9", + "KeyKeypadDivide": "Keypad Divide", + "KeyKeypadMultiply": "Keypad Multiply", + "KeyKeypadSubtract": "Keypad Subtract", + "KeyKeypadAdd": "Keypad Add", + "KeyKeypadDecimal": "Keypad Decimal", + "KeyKeypadEnter": "Keypad Enter", + "KeyNumber0": "0", + "KeyNumber1": "1", + "KeyNumber2": "2", + "KeyNumber3": "3", + "KeyNumber4": "4", + "KeyNumber5": "5", + "KeyNumber6": "6", + "KeyNumber7": "7", + "KeyNumber8": "8", + "KeyNumber9": "9", + "KeyTilde": "~", + "KeyGrave": "`", + "KeyMinus": "-", + "KeyPlus": "+", + "KeyBracketLeft": "[", + "KeyBracketRight": "]", + "KeySemicolon": ";", + "KeyQuote": "\"", + "KeyComma": ",", + "KeyPeriod": ".", + "KeySlash": "/", + "KeyBackSlash": "\\", + "KeyUnbound": "Unbound", + "GamepadLeftStick": "L Stick Button", + "GamepadRightStick": "R Stick Button", + "GamepadLeftShoulder": "Left Shoulder", + "GamepadRightShoulder": "Right Shoulder", + "GamepadLeftTrigger": "Left Trigger", + "GamepadRightTrigger": "Right Trigger", + "GamepadDpadUp": "Up", + "GamepadDpadDown": "Down", + "GamepadDpadLeft": "Left", + "GamepadDpadRight": "Right", + "GamepadMinus": "-", + "GamepadPlus": "+", + "GamepadGuide": "Guide", + "GamepadMisc1": "Misc", + "GamepadPaddle1": "Paddle 1", + "GamepadPaddle2": "Paddle 2", + "GamepadPaddle3": "Paddle 3", + "GamepadPaddle4": "Paddle 4", + "GamepadTouchpad": "Touchpad", + "GamepadSingleLeftTrigger0": "Left Trigger 0", + "GamepadSingleRightTrigger0": "Right Trigger 0", + "GamepadSingleLeftTrigger1": "Left Trigger 1", + "GamepadSingleRightTrigger1": "Right Trigger 1", + "StickLeft": "Left Stick", + "StickRight": "Right Stick", "UserProfilesSelectedUserProfile": "Perfil de usuário selecionado:", "UserProfilesSaveProfileName": "Salvar nome de perfil", "UserProfilesChangeProfileImage": "Mudar imagem de perfil", @@ -597,6 +703,7 @@ "UserProfileWindowTitle": "Gerenciador de perfis de usuário", "CheatWindowTitle": "Gerenciador de Cheats", "DlcWindowTitle": "Gerenciador de DLC", + "ModWindowTitle": "Manage Mods for {0} ({1})", "UpdateWindowTitle": "Gerenciador de atualizações", "CheatWindowHeading": "Cheats disponíveis para {0} [{1}]", "BuildId": "ID da Build", diff --git a/src/Ryujinx/Assets/Locales/ru_RU.json b/src/Ryujinx/Assets/Locales/ru_RU.json index 284b8e2b4..75fd4fe12 100644 --- a/src/Ryujinx/Assets/Locales/ru_RU.json +++ b/src/Ryujinx/Assets/Locales/ru_RU.json @@ -1,9 +1,9 @@ { "Language": "Русский (RU)", "MenuBarFileOpenApplet": "Открыть апплет", - "MenuBarFileOpenAppletOpenMiiAppletToolTip": "Открыть апплет Mii Editor в автономном режиме", + "MenuBarFileOpenAppletOpenMiiAppletToolTip": "Открывает апплет Mii Editor в автономном режиме", "SettingsTabInputDirectMouseAccess": "Прямой ввод мыши", - "SettingsTabSystemMemoryManagerMode": "Режим диспетчера памяти:", + "SettingsTabSystemMemoryManagerMode": "Режим менеджера памяти:", "SettingsTabSystemMemoryManagerModeSoftware": "Программное обеспечение", "SettingsTabSystemMemoryManagerModeHost": "Хост (быстро)", "SettingsTabSystemMemoryManagerModeHostUnchecked": "Хост не установлен (самый быстрый, небезопасный)", @@ -30,6 +30,10 @@ "MenuBarToolsManageFileTypes": "Управление типами файлов", "MenuBarToolsInstallFileTypes": "Установить типы файлов", "MenuBarToolsUninstallFileTypes": "Удалить типы файлов", + "MenuBarView": "_Вид", + "MenuBarViewWindow": "Размер окна", + "MenuBarViewWindow720": "720p", + "MenuBarViewWindow1080": "1080p", "MenuBarHelp": "_Помощь", "MenuBarHelpCheckForUpdates": "Проверить наличие обновлений", "MenuBarHelpAbout": "О программе", @@ -40,7 +44,7 @@ "GameListHeaderDeveloper": "Разработчик", "GameListHeaderVersion": "Версия", "GameListHeaderTimePlayed": "Время в игре", - "GameListHeaderLastPlayed": "Последняя игра", + "GameListHeaderLastPlayed": "Последний запуск", "GameListHeaderFileExtension": "Расширение файла", "GameListHeaderFileSize": "Размер файла", "GameListHeaderPath": "Путь", @@ -80,7 +84,7 @@ "StatusBarGamesLoaded": "{0}/{1} игр загружено", "StatusBarSystemVersion": "Версия прошивки: {0}", "LinuxVmMaxMapCountDialogTitle": "Обнаружен низкий лимит разметки памяти", - "LinuxVmMaxMapCountDialogTextPrimary": "Вы хотите увеличить значение vm.max_map_count до {0}", + "LinuxVmMaxMapCountDialogTextPrimary": "Хотите увеличить значение vm.max_map_count до {0}", "LinuxVmMaxMapCountDialogTextSecondary": "Некоторые игры могут создавать большую разметку памяти, чем разрешено на данный момент по умолчанию. Ryujinx вылетит при превышении этого лимита.", "LinuxVmMaxMapCountDialogButtonUntilRestart": "Да, до следующего перезапуска", "LinuxVmMaxMapCountDialogButtonPersistent": "Да, постоянно", @@ -89,12 +93,13 @@ "Settings": "Параметры", "SettingsTabGeneral": "Интерфейс", "SettingsTabGeneralGeneral": "Общее", - "SettingsTabGeneralEnableDiscordRichPresence": "Включить статус активности в Discord", + "SettingsTabGeneralEnableDiscordRichPresence": "Статус активности в Discord", "SettingsTabGeneralCheckUpdatesOnLaunch": "Проверять наличие обновлений при запуске", "SettingsTabGeneralShowConfirmExitDialog": "Подтверждать выход из приложения", + "SettingsTabGeneralRememberWindowState": "Запомнить размер/положение окна", "SettingsTabGeneralHideCursor": "Скрывать курсор", "SettingsTabGeneralHideCursorNever": "Никогда", - "SettingsTabGeneralHideCursorOnIdle": "В режиме ожидания", + "SettingsTabGeneralHideCursorOnIdle": "В простое", "SettingsTabGeneralHideCursorAlways": "Всегда", "SettingsTabGeneralGameDirectories": "Папки с играми", "SettingsTabGeneralAdd": "Добавить", @@ -125,13 +130,13 @@ "SettingsTabSystemSystemLanguageBritishEnglish": "Английский (Британия)", "SettingsTabSystemSystemLanguageCanadianFrench": "Французский (Канада)", "SettingsTabSystemSystemLanguageLatinAmericanSpanish": "Испанский (Латинская Америка)", - "SettingsTabSystemSystemLanguageSimplifiedChinese": "Китайский упрощённый", - "SettingsTabSystemSystemLanguageTraditionalChinese": "Китайский традиционный", + "SettingsTabSystemSystemLanguageSimplifiedChinese": "Китайский (упрощённый)", + "SettingsTabSystemSystemLanguageTraditionalChinese": "Китайский (традиционный)", "SettingsTabSystemSystemTimeZone": "Часовой пояс прошивки:", - "SettingsTabSystemSystemTime": "Время в прошивке:", - "SettingsTabSystemEnableVsync": "Включить вертикальную синхронизацию", - "SettingsTabSystemEnablePptc": "Включить PPTC (Profiled Persistent Translation Cache)", - "SettingsTabSystemEnableFsIntegrityChecks": "Включить проверку целостности файловой системы", + "SettingsTabSystemSystemTime": "Системное время в прошивке:", + "SettingsTabSystemEnableVsync": "Вертикальная синхронизация", + "SettingsTabSystemEnablePptc": "Использовать PPTC (Profiled Persistent Translation Cache)", + "SettingsTabSystemEnableFsIntegrityChecks": "Проверка целостности файловой системы", "SettingsTabSystemAudioBackend": "Аудио бэкенд:", "SettingsTabSystemAudioBackendDummy": "Без звука", "SettingsTabSystemAudioBackendOpenAL": "OpenAL", @@ -143,7 +148,7 @@ "SettingsTabSystemIgnoreMissingServices": "Игнорировать отсутствующие службы", "SettingsTabGraphics": "Графика", "SettingsTabGraphicsAPI": "Графические API", - "SettingsTabGraphicsEnableShaderCache": "Включить кэш шейдеров", + "SettingsTabGraphicsEnableShaderCache": "Кэшировать шейдеры", "SettingsTabGraphicsAnisotropicFiltering": "Анизотропная фильтрация:", "SettingsTabGraphicsAnisotropicFilteringAuto": "Автоматически", "SettingsTabGraphicsAnisotropicFiltering2x": "2x", @@ -164,7 +169,7 @@ "SettingsTabGraphicsAspectRatio32x9": "32:9", "SettingsTabGraphicsAspectRatioStretch": "Растянуть до размеров окна", "SettingsTabGraphicsDeveloperOptions": "Параметры разработчика", - "SettingsTabGraphicsShaderDumpPath": "Путь дампа графического шейдера:", + "SettingsTabGraphicsShaderDumpPath": "Путь дампа графических шейдеров", "SettingsTabLogging": "Журналирование", "SettingsTabLoggingLogging": "Журналирование", "SettingsTabLoggingEnableLoggingToFile": "Включить запись в файл", @@ -266,6 +271,107 @@ "ControllerSettingsMotionGyroDeadzone": "Мертвая зона гироскопа:", "ControllerSettingsSave": "Сохранить", "ControllerSettingsClose": "Закрыть", + "KeyUnknown": "Неизвестно", + "KeyShiftLeft": "Левый Shift", + "KeyShiftRight": "Правый Shift", + "KeyControlLeft": "Левый Ctrl", + "KeyMacControlLeft": "Левый ⌃", + "KeyControlRight": "Правый Ctrl", + "KeyMacControlRight": "Правый ⌃", + "KeyAltLeft": "Левый Alt", + "KeyMacAltLeft": "Левый ⌥", + "KeyAltRight": "Правый Alt", + "KeyMacAltRight": "Правый ⌥", + "KeyWinLeft": "Левый ⊞", + "KeyMacWinLeft": "Левый ⌘", + "KeyWinRight": "Правый ⊞", + "KeyMacWinRight": "Правый ⌘", + "KeyMenu": "Меню", + "KeyUp": "Вверх", + "KeyDown": "Вниз", + "KeyLeft": "Влево", + "KeyRight": "Вправо", + "KeyEnter": "Enter", + "KeyEscape": "Escape", + "KeySpace": "Пробел", + "KeyTab": "Tab", + "KeyBackSpace": "Backspace", + "KeyInsert": "Insert", + "KeyDelete": "Delete", + "KeyPageUp": "Page Up", + "KeyPageDown": "Page Down", + "KeyHome": "Home", + "KeyEnd": "End", + "KeyCapsLock": "Caps Lock", + "KeyScrollLock": "Scroll Lock", + "KeyPrintScreen": "Print Screen", + "KeyPause": "Pause", + "KeyNumLock": "Num Lock", + "KeyClear": "Очистить", + "KeyKeypad0": "Блок цифр 0", + "KeyKeypad1": "Блок цифр 1", + "KeyKeypad2": "Блок цифр 2", + "KeyKeypad3": "Блок цифр 3", + "KeyKeypad4": "Блок цифр 4", + "KeyKeypad5": "Блок цифр 5", + "KeyKeypad6": "Блок цифр 6", + "KeyKeypad7": "Блок цифр 7", + "KeyKeypad8": "Блок цифр 8", + "KeyKeypad9": "Блок цифр 9", + "KeyKeypadDivide": "/ (блок цифр)", + "KeyKeypadMultiply": "* (блок цифр)", + "KeyKeypadSubtract": "- (блок цифр)", + "KeyKeypadAdd": "+ (блок цифр)", + "KeyKeypadDecimal": ". (блок цифр)", + "KeyKeypadEnter": "Enter (блок цифр)", + "KeyNumber0": "0", + "KeyNumber1": "1", + "KeyNumber2": "2", + "KeyNumber3": "3", + "KeyNumber4": "4", + "KeyNumber5": "5", + "KeyNumber6": "6", + "KeyNumber7": "7", + "KeyNumber8": "8", + "KeyNumber9": "9", + "KeyTilde": "~", + "KeyGrave": "`", + "KeyMinus": "-", + "KeyPlus": "+", + "KeyBracketLeft": "[", + "KeyBracketRight": "]", + "KeySemicolon": ";", + "KeyQuote": "\"", + "KeyComma": ",", + "KeyPeriod": ".", + "KeySlash": "/", + "KeyBackSlash": "\\", + "KeyUnbound": "Не привязано", + "GamepadLeftStick": "Кнопка лев. стика", + "GamepadRightStick": "Кнопка пр. стика", + "GamepadLeftShoulder": "Левый бампер", + "GamepadRightShoulder": "Правый бампер", + "GamepadLeftTrigger": "Левый триггер", + "GamepadRightTrigger": "Правый триггер", + "GamepadDpadUp": "Вверх", + "GamepadDpadDown": "Вниз", + "GamepadDpadLeft": "Влево", + "GamepadDpadRight": "Вправо", + "GamepadMinus": "-", + "GamepadPlus": "+", + "GamepadGuide": "Кнопка Xbox", + "GamepadMisc1": "Прочее", + "GamepadPaddle1": "Доп.кнопка 1", + "GamepadPaddle2": "Доп.кнопка 2", + "GamepadPaddle3": "Доп.кнопка 3", + "GamepadPaddle4": "Доп.кнопка 4", + "GamepadTouchpad": "Тачпад", + "GamepadSingleLeftTrigger0": "Левый триггер 0", + "GamepadSingleRightTrigger0": "Правый триггер 0", + "GamepadSingleLeftTrigger1": "Левый триггер 1", + "GamepadSingleRightTrigger1": "Правый триггер 1", + "StickLeft": "Левый стик", + "StickRight": "Правый стик", "UserProfilesSelectedUserProfile": "Выбранный пользовательский профиль:", "UserProfilesSaveProfileName": "Сохранить пользовательский профиль", "UserProfilesChangeProfileImage": "Изменить аватар", @@ -273,17 +379,17 @@ "UserProfilesAddNewProfile": "Добавить новый профиль", "UserProfilesDelete": "Удалить", "UserProfilesClose": "Закрыть", - "ProfileNameSelectionWatermark": "Выберите никнейм", + "ProfileNameSelectionWatermark": "Укажите никнейм", "ProfileImageSelectionTitle": "Выбор изображения профиля", - "ProfileImageSelectionHeader": "Выберите аватар", + "ProfileImageSelectionHeader": "Выбор аватара", "ProfileImageSelectionNote": "Вы можете импортировать собственное изображение или выбрать аватар из системной прошивки.", "ProfileImageSelectionImportImage": "Импорт изображения", "ProfileImageSelectionSelectAvatar": "Встроенные аватары", "InputDialogTitle": "Диалоговое окно ввода", "InputDialogOk": "ОК", "InputDialogCancel": "Отмена", - "InputDialogAddNewProfileTitle": "Выберите имя профиля", - "InputDialogAddNewProfileHeader": "Пожалуйста, введите имя профиля", + "InputDialogAddNewProfileTitle": "Выберите никнейм", + "InputDialogAddNewProfileHeader": "Пожалуйста, введите никнейм", "InputDialogAddNewProfileSubtext": "(Максимальная длина: {0})", "AvatarChoose": "Выбрать аватар", "AvatarSetBackgroundColor": "Установить цвет фона", @@ -312,7 +418,7 @@ "DialogWarningTitle": "Ryujinx - Предупреждение", "DialogExitTitle": "Ryujinx - Выход", "DialogErrorMessage": "Ryujinx обнаружил ошибку", - "DialogExitMessage": "Вы уверены, что хотите закрыть Ryujinx?", + "DialogExitMessage": "Вы уверены, что хотите выйти из Ryujinx?", "DialogExitSubMessage": "Все несохраненные данные будут потеряны", "DialogMessageCreateSaveErrorMessage": "Произошла ошибка при создании указанных данных сохранения: {0}", "DialogMessageFindSaveErrorMessage": "Произошла ошибка при поиске указанных данных сохранения: {0}", @@ -324,7 +430,7 @@ "DialogNcaExtractionSuccessMessage": "Извлечение завершено успешно.", "DialogUpdaterConvertFailedMessage": "Не удалось преобразовать текущую версию Ryujinx.", "DialogUpdaterCancelUpdateMessage": "Отмена обновления...", - "DialogUpdaterAlreadyOnLatestVersionMessage": "Вы уже используете самую последнюю версию Ryujinx", + "DialogUpdaterAlreadyOnLatestVersionMessage": "Вы используете самую последнюю версию Ryujinx", "DialogUpdaterFailedToGetVersionMessage": "Произошла ошибка при попытке получить информацию о выпуске от GitHub Release. Это может быть вызвано тем, что в данный момент в GitHub Actions компилируется новый релиз. Повторите попытку позже.", "DialogUpdaterConvertFailedGithubMessage": "Не удалось преобразовать полученную версию Ryujinx из Github Release.", "DialogUpdaterDownloadingMessage": "Загрузка обновления...", @@ -332,14 +438,14 @@ "DialogUpdaterRenamingMessage": "Переименование обновления...", "DialogUpdaterAddingFilesMessage": "Добавление нового обновления...", "DialogUpdaterCompleteMessage": "Обновление завершено", - "DialogUpdaterRestartMessage": "Вы хотите перезапустить Ryujinx сейчас?", + "DialogUpdaterRestartMessage": "Перезапустить Ryujinx?", "DialogUpdaterNoInternetMessage": "Вы не подключены к интернету", "DialogUpdaterNoInternetSubMessage": "Убедитесь, что у вас работает подключение к интернету", "DialogUpdaterDirtyBuildMessage": "Вы не можете обновлять Dirty Build", "DialogUpdaterDirtyBuildSubMessage": "Загрузите Ryujinx по адресу https://ryujinx.org/ если вам нужна поддерживаемая версия.", "DialogRestartRequiredMessage": "Требуется перезагрузка", "DialogThemeRestartMessage": "Тема сохранена. Для применения темы требуется перезапуск.", - "DialogThemeRestartSubMessage": "Вы хотите перезапустить?", + "DialogThemeRestartSubMessage": "Хотите перезапустить", "DialogFirmwareInstallEmbeddedMessage": "Хотите установить прошивку, встроенную в эту игру? (Прошивка {0})", "DialogFirmwareInstallEmbeddedSuccessMessage": "Установленная прошивка не была найдена, но Ryujinx удалось установить прошивку {0} из предоставленной игры.\nТеперь эмулятор запустится.", "DialogFirmwareNoFirmwareInstalledMessage": "Прошивка не установлена", @@ -348,7 +454,7 @@ "DialogInstallFileTypesErrorMessage": "Не удалось установить типы файлов.", "DialogUninstallFileTypesSuccessMessage": "Типы файлов успешно удалены", "DialogUninstallFileTypesErrorMessage": "Не удалось удалить типы файлов.", - "DialogOpenSettingsWindowLabel": "Открыть окно параметров", + "DialogOpenSettingsWindowLabel": "Открывает окно параметров", "DialogControllerAppletTitle": "Апплет контроллера", "DialogMessageDialogErrorExceptionMessage": "Ошибка отображения сообщения: {0}", "DialogSoftwareKeyboardErrorExceptionMessage": "Ошибка отображения программной клавиатуры: {0}", @@ -364,7 +470,7 @@ "DialogProfileDeleteProfileTitle": "Удаление профиля", "DialogProfileDeleteProfileMessage": "Это действие необратимо. Вы уверены, что хотите продолжить?", "DialogWarning": "Внимание", - "DialogPPTCDeletionMessage": "Вы собираетесь удалить кэш PPTC для:\n\n{0}\n\nВы уверены, что хотите продолжить?", + "DialogPPTCDeletionMessage": "Вы собираетесь перестроить кэш PPTC при следующем запуске для:\n\n{0}\n\nВы уверены, что хотите продолжить?", "DialogPPTCDeletionErrorMessage": "Ошибка очистки кэша PPTC в {0}: {1}", "DialogShaderDeletionMessage": "Вы собираетесь удалить кэш шейдеров для:\n\n{0}\n\nВы уверены, что хотите продолжить?", "DialogShaderDeletionErrorMessage": "Ошибка очистки кэша шейдеров в {0}: {1}", @@ -378,10 +484,10 @@ "DialogFirmwareInstallerFirmwareInstallWaitMessage": "Установка прошивки...", "DialogFirmwareInstallerFirmwareInstallSuccessMessage": "Прошивка версии {0} успешно установлена.", "DialogUserProfileDeletionWarningMessage": "Если выбранный профиль будет удален, другие профили не будут открываться.", - "DialogUserProfileDeletionConfirmMessage": "Вы хотите удалить выбранный профиль", + "DialogUserProfileDeletionConfirmMessage": "Удалить выбранный профиль?", "DialogUserProfileUnsavedChangesTitle": "Внимание - Несохраненные изменения", - "DialogUserProfileUnsavedChangesMessage": "Вы внесли изменения в этот профиль пользователя которые не были сохранены.", - "DialogUserProfileUnsavedChangesSubMessage": "Вы хотите отменить изменения?", + "DialogUserProfileUnsavedChangesMessage": "В эту учетную запись внесены изменения, которые не были сохранены.", + "DialogUserProfileUnsavedChangesSubMessage": "Отменить изменения?", "DialogControllerSettingsModifiedConfirmMessage": "Текущие настройки управления обновлены.", "DialogControllerSettingsModifiedConfirmSubMessage": "Сохранить?", "DialogLoadFileErrorMessage": "{0}. Файл с ошибкой: {1}", @@ -444,40 +550,40 @@ "OrderDescending": "По убыванию", "SettingsTabGraphicsFeatures": "Функции", "ErrorWindowTitle": "Окно ошибки", - "ToggleDiscordTooltip": "Включает или отключает отображение в Discord статуса \"Сейчас играет\"", - "AddGameDirBoxTooltip": "Введите папку игры для добавления в список", + "ToggleDiscordTooltip": "Включает или отключает отображение статуса \"Играет в игру\" в Discord", + "AddGameDirBoxTooltip": "Введите путь к папке с играми для добавления ее в список выше", "AddGameDirTooltip": "Добавить папку с играми в список", "RemoveGameDirTooltip": "Удалить выбранную папку игры", "CustomThemeCheckTooltip": "Включить или отключить пользовательские темы", "CustomThemePathTooltip": "Путь к пользовательской теме для интерфейса", "CustomThemeBrowseTooltip": "Просмотр пользовательской темы интерфейса", - "DockModeToggleTooltip": "\"Стационарный\" режим запускает эмулятор, как если бы Nintendo Switch находилась в доке, что улучшает графику и разрешение в большинстве игр. И наоборот, при отключении этого режима эмулятор будет запускать игры в \"Портативном\" режиме, снижая качество графики.\n\nНастройте управление для Игрока 1 если планируете использовать в \"Стационарном\" режиме; настройте портативное управление если планируете использовать эмулятор в \"Портативном\" режиме.\n\nОставьте включенным если не уверены.", + "DockModeToggleTooltip": "\"Стационарный\" режим запускает эмулятор, как если бы Nintendo Switch находилась в доке, что улучшает графику и разрешение в большинстве игр. И наоборот, при отключении этого режима эмулятор будет запускать игры в \"Портативном\" режиме, снижая качество графики.\n\nНастройте управление для Игрока 1 если планируете использовать в \"Стационарном\" режиме; настройте портативное управление если планируете использовать эмулятор в \"Портативном\" режиме.\n\nРекомендуется оставить включенным.", "DirectKeyboardTooltip": "Поддержка прямого ввода с клавиатуры (HID). Предоставляет игре прямой доступ к клавиатуре в качестве устройства ввода текста.\nРаботает только с играми, которые изначально поддерживают использование клавиатуры с Switch.\nРекомендуется оставить выключенным.", "DirectMouseTooltip": "Поддержка прямого ввода мыши (HID). Предоставляет игре прямой доступ к мыши в качестве указывающего устройства.\nРаботает только с играми, которые изначально поддерживают использование мыши совместно с железом Switch.\nРекомендуется оставить выключенным.", - "RegionTooltip": "Изменение региона прошивки", - "LanguageTooltip": "Изменение языка прошивки", - "TimezoneTooltip": "Изменение часового пояса прошивки", - "TimeTooltip": "Изменение системного времени", + "RegionTooltip": "Сменяет регион прошивки", + "LanguageTooltip": "Меняет язык прошивки", + "TimezoneTooltip": "Меняет часовой пояс прошивки", + "TimeTooltip": "Меняет системное время прошивки", "VSyncToggleTooltip": "Эмуляция вертикальной синхронизации консоли, которая ограничивает количество кадров в секунду в большинстве игр; отключение может привести к тому, что игры будут запущены с более высокой частотой кадров, но загрузка игры может занять больше времени, либо игра не запустится вообще.\n\nМожно включать и выключать эту настройку непосредственно в игре с помощью горячих клавиш (F1 по умолчанию). Если планируете отключить вертикальную синхронизацию, рекомендуем настроить горячие клавиши.\n\nРекомендуется оставить включенным.", - "PptcToggleTooltip": "Сохранение преобразованных JIT-функций для того, чтобы не преобразовывать их по новой каждый раз при запуске игры.\n\nУменьшает статтеры и значительно ускоряет последующую загрузку игр.\n\nРекомендуется оставить включенным.", - "FsIntegrityToggleTooltip": "Проверяет поврежденные файлы при загрузке игры и если поврежденные файлы обнаружены, отображает ошибку о поврежденном хэше в журнале.\n\nНе влияет на производительность и необходим для помощи в устранении неполадок.\n\nРекомендуется оставить включенным.", - "AudioBackendTooltip": "Изменяет используемый аудио-бэкенд для рендера звука.\n\nSDL2 является предпочтительным, в то время как OpenAL и SoundIO используются в качестве резервных. \n\nРекомендуется использование SDL2.", - "MemoryManagerTooltip": "Изменение разметки и доступа к гостевой памяти. Значительно влияет на производительность процессора.\n\nРекомендуется оставить \"Хост не установлен\"", - "MemoryManagerSoftwareTooltip": "Использует таблицу страниц для преобразования адресов. Самая высокая точность, но самая низкая производительность.", - "MemoryManagerHostTooltip": "Прямая разметка памяти в адресном пространстве хоста. Значительно более быстрая JIT-компиляция и запуск.", - "MemoryManagerUnsafeTooltip": "Производит прямую разметку памяти, но не маскирует адрес в гостевом адресном пространстве перед получением доступа. Быстро, но небезопасно. Гостевое приложение может получить доступ к памяти из Ryujinx, поэтому в этом режиме рекомендуется запускать только те программы, которым вы доверяете.", + "PptcToggleTooltip": "Сохраняет скомпилированные JIT-функции для того, чтобы не преобразовывать их по новой каждый раз при запуске игры.\n\nУменьшает статтеры и значительно ускоряет последующую загрузку игр.\n\nРекомендуется оставить включенным.", + "FsIntegrityToggleTooltip": "Проверяет файлы при загрузке игры и если обнаружены поврежденные файлы, выводит сообщение о поврежденном хэше в журнале.\n\nНе влияет на производительность и необходим для помощи в устранении неполадок.\n\nРекомендуется оставить включенным.", + "AudioBackendTooltip": "Изменяет используемый аудио бэкенд для рендера звука.\n\nSDL2 является предпочтительным вариантом, в то время как OpenAL и SoundIO используются в качестве резервных.\n\nРекомендуется использование SDL2.", + "MemoryManagerTooltip": "Меняет разметку и доступ к гостевой памяти. Значительно влияет на производительность процессора.\n\nРекомендуется оставить \"Хост не установлен\"", + "MemoryManagerSoftwareTooltip": "Использует таблицу страниц для преобразования адресов. \nСамая высокая точность, но самая низкая производительность.", + "MemoryManagerHostTooltip": "Прямая разметка памяти в адресном пространстве хоста. \nЗначительно более быстрые запуск и компиляция JIT.", + "MemoryManagerUnsafeTooltip": "Производит прямую разметку памяти, но не маскирует адрес в гостевом адресном пространстве перед получением доступа. \nБыстро, но небезопасно. Гостевое приложение может получить доступ к памяти из Ryujinx, поэтому в этом режиме рекомендуется запускать только те программы, которым вы доверяете.", "UseHypervisorTooltip": "Использует Hypervisor вместо JIT. Значительно увеличивает производительность, но может работать нестабильно.", "DRamTooltip": "Использует альтернативный макет MemoryMode для имитации использования Nintendo Switch в режиме разработчика.\n\nПолезно только для пакетов текстур с высоким разрешением или модов добавляющих разрешение 4К. Не улучшает производительность.\n\nРекомендуется оставить выключенным.", "IgnoreMissingServicesTooltip": "Игнорирует нереализованные сервисы Horizon в новых прошивках. Эта настройка поможет избежать вылеты при запуске определенных игр.\n\nРекомендуется оставить выключенным.", - "GraphicsBackendThreadingTooltip": "Выполняет команды графического бэкенда на втором потоке.\n\nУскоряет компиляцию шейдеров, уменьшает статтеры и повышает производительность на драйверах GPU без поддержки многопоточности. Производительность на драйверах с многопоточностью немного выше.\n\nРекомендуется оставить Автоматически.", - "GalThreadingTooltip": "Выполняет команды графического бэкенда на втором потоке.\n\nУскоряет компиляцию шейдеров, уменьшает статтеры и повышает производительность на драйверах GPU без поддержки многопоточности. Производительность на драйверах с многопоточностью немного выше.\n\nРекомендуется оставить в Авто.", + "GraphicsBackendThreadingTooltip": "Выполняет команды графического бэкенда на втором потоке.\n\nУскоряет компиляцию шейдеров, уменьшает статтеры и повышает производительность на драйверах видеоадаптера без поддержки многопоточности. Производительность на драйверах с многопоточностью немного выше.\n\nРекомендуется оставить Автоматически.", + "GalThreadingTooltip": "Выполняет команды графического бэкенда на втором потоке.\n\nУскоряет компиляцию шейдеров, уменьшает статтеры и повышает производительность на драйверах видеоадаптера без поддержки многопоточности. Производительность на драйверах с многопоточностью немного выше.\n\nРекомендуется оставить Автоматически.", "ShaderCacheToggleTooltip": "Сохраняет кэш шейдеров на диске, для уменьшения статтеров при последующих запусках.\n\nРекомендуется оставить включенным.", - "ResolutionScaleTooltip": "Увеличивает разрешение рендера игры.\n\nНекоторые игры могут не работать с этой настройкой и выглядеть смазано даже когда разрешение увеличено; Для таких игр вам может потребоваться установить моды, которые убирают сглаживание или увеличивают разрешение рендеринга. Для использования последнего, вам нужно будет выбрать опцию \"Нативное\".\n\nЭта опция может быть изменена во время игры по нажатию кнопки \"Применить\" ниже; Вы можете просто переместить окно настроек в сторону и поэкспериментировать, пока не подберете подходящие настройки для конкретной игры.\n\nИмейте в виду, что \"4x\" является излишеством.", + "ResolutionScaleTooltip": "Увеличивает разрешение рендера игры.\n\nНекоторые игры могут не работать с этой настройкой и выглядеть смазано даже когда разрешение увеличено. Для таких игр может потребоваться установка модов, которые убирают сглаживание или увеличивают разрешение рендеринга. \nДля использования последнего, вам нужно будет выбрать опцию \"Нативное\".\n\nЭта опция может быть изменена во время игры по нажатию кнопки \"Применить\" ниже. Вы можете просто переместить окно настроек в сторону и поэкспериментировать, пока не подберете подходящие настройки для конкретной игры.\n\nИмейте в виду, что \"4x\" является излишеством.", "ResolutionScaleEntryTooltip": "Масштабирование разрешения с плавающей запятой, например 1,5. Неинтегральное масштабирование с большой вероятностью вызовет сбои в работе.", - "AnisotropyTooltip": "Уровень анизотропной фильтрации. Установите значение «Авто», чтобы использовать в игре значение по умолчанию игре.", + "AnisotropyTooltip": "Уровень анизотропной фильтрации. \n\nУстановите значение Автоматически, чтобы использовать значение по умолчанию игры.", "AspectRatioTooltip": "Соотношение сторон окна рендерера.\n\nИзмените эту настройку только если вы используете мод для соотношения сторон, иначе изображение будет растянуто.\n\nРекомендуется настройка 16:9.", "ShaderDumpPathTooltip": "Путь с дампами графических шейдеров", - "FileLogTooltip": "Включает или отключает ведение журнала в файл на диске. Не влияет на производительность.", + "FileLogTooltip": "Включает ведение журнала в файл на диске. Не влияет на производительность.", "StubLogTooltip": "Включает ведение журнала-заглушки. Не влияет на производительность.", "InfoLogTooltip": "Включает вывод сообщений информационного журнала в консоль. Не влияет на производительность.", "WarnLogTooltip": "Включает вывод сообщений журнала предупреждений в консоль. Не влияет на производительность.", @@ -489,42 +595,42 @@ "DeveloperOptionTooltip": "Используйте с осторожностью", "OpenGlLogLevel": "Требует включения соответствующих уровней ведения журнала", "DebugLogTooltip": "Выводит журнал сообщений отладки в консоли.\n\nИспользуйте только в случае просьбы разработчика, так как включение этой функции затруднит чтение журналов и ухудшит работу эмулятора.", - "LoadApplicationFileTooltip": "Открыть файловый менеджер для выбора файла, совместимого с Nintendo Switch.", - "LoadApplicationFolderTooltip": "Открыть файловый менеджер для выбора распакованного приложения, совместимого с Nintendo Switch.", + "LoadApplicationFileTooltip": "Открывает файловый менеджер для выбора файла, совместимого с Nintendo Switch.", + "LoadApplicationFolderTooltip": "Открывает файловый менеджер для выбора распакованного приложения, совместимого с Nintendo Switch.", "OpenRyujinxFolderTooltip": "Открывает папку с файлами Ryujinx. ", "OpenRyujinxLogsTooltip": "Открывает папку в которую записываются логи", "ExitTooltip": "Выйти из Ryujinx", - "OpenSettingsTooltip": "Открыть окно параметров", + "OpenSettingsTooltip": "Открывает окно параметров", "OpenProfileManagerTooltip": "Открыть менеджер учетных записей", "StopEmulationTooltip": "Остановка эмуляции текущей игры и возврат к списку игр", - "CheckUpdatesTooltip": "Проверка наличия обновления Ryujinx", - "OpenAboutTooltip": "Открыть окно «О программе»", + "CheckUpdatesTooltip": "Проверяет наличие обновлений для Ryujinx", + "OpenAboutTooltip": "Открывает окно «О программе»", "GridSize": "Размер сетки", - "GridSizeTooltip": "Изменение размера элементов сетки", + "GridSizeTooltip": "Меняет размер сетки элементов", "SettingsTabSystemSystemLanguageBrazilianPortuguese": "Португальский язык (Бразилия)", "AboutRyujinxContributorsButtonHeader": "Посмотреть всех участников", "SettingsTabSystemAudioVolume": "Громкость: ", "AudioVolumeTooltip": "Изменяет громкость звука", - "SettingsTabSystemEnableInternetAccess": "Включить гостевой доступ в Интернет/сетевой режим", + "SettingsTabSystemEnableInternetAccess": "Гостевой доступ в интернет/сетевой режим", "EnableInternetAccessTooltip": "Позволяет эмулированному приложению подключаться к Интернету.\n\nПри включении этой функции игры с возможностью сетевой игры могут подключаться друг к другу, если все эмуляторы (или реальные консоли) подключены к одной и той же точке доступа.\n\nНЕ разрешает подключение к серверам Nintendo. Может вызвать сбой в некоторых играх, которые пытаются подключиться к Интернету.\n\nРекомендутеся оставить выключенным.", - "GameListContextMenuManageCheatToolTip": "Управление читами", + "GameListContextMenuManageCheatToolTip": "Открывает окно управления читами", "GameListContextMenuManageCheat": "Управление читами", - "GameListContextMenuManageModToolTip": "Управление модами", + "GameListContextMenuManageModToolTip": "Открывает окно управления модами", "GameListContextMenuManageMod": "Управление модами", "ControllerSettingsStickRange": "Диапазон:", - "DialogStopEmulationTitle": "Ryujinx - Остановить эмуляцию", + "DialogStopEmulationTitle": "Ryujinx - Остановка эмуляции", "DialogStopEmulationMessage": "Вы уверены, что хотите остановить эмуляцию?", - "SettingsTabCpu": "ЦП", + "SettingsTabCpu": "Процессор", "SettingsTabAudio": "Аудио", "SettingsTabNetwork": "Сеть", "SettingsTabNetworkConnection": "Подключение к сети", - "SettingsTabCpuCache": "Кэш ЦП", - "SettingsTabCpuMemory": "Память ЦП", + "SettingsTabCpuCache": "Кэш процессора", + "SettingsTabCpuMemory": "Режим процессора", "DialogUpdaterFlatpakNotSupportedMessage": "Пожалуйста, обновите Ryujinx через FlatHub.", "UpdaterDisabledWarningTitle": "Средство обновления отключено", "ControllerSettingsRotate90": "Повернуть на 90° по часовой стрелке", "IconSize": "Размер обложек", - "IconSizeTooltip": "Изменить размер обложек", + "IconSizeTooltip": "Меняет размер обложек", "MenuBarOptionsShowConsole": "Показать консоль", "ShaderCachePurgeError": "Ошибка очистки кэша шейдеров в {0}: {1}", "UserErrorNoKeys": "Ключи не найдены", @@ -578,7 +684,7 @@ "SettingsTabHotkeys": "Горячие клавиши", "SettingsTabHotkeysHotkeys": "Горячие клавиши", "SettingsTabHotkeysToggleVsyncHotkey": "Вертикальная синхронизация:", - "SettingsTabHotkeysScreenshotHotkey": "Скриншот:", + "SettingsTabHotkeysScreenshotHotkey": "Сделать скриншот:", "SettingsTabHotkeysShowUiHotkey": "Показать интерфейс:", "SettingsTabHotkeysPauseHotkey": "Пауза эмуляции:", "SettingsTabHotkeysToggleMuteHotkey": "Выключить звук:", @@ -597,6 +703,7 @@ "UserProfileWindowTitle": "Менеджер учетных записей", "CheatWindowTitle": "Менеджер читов", "DlcWindowTitle": "Управление DLC для {0} ({1})", + "ModWindowTitle": "Управление модами для {0} ({1})", "UpdateWindowTitle": "Менеджер обновлений игр", "CheatWindowHeading": "Доступные читы для {0} [{1}]", "BuildId": "ID версии:", @@ -607,33 +714,33 @@ "Save": "Сохранить", "Discard": "Отменить", "Paused": "Приостановлено", - "UserProfilesSetProfileImage": "Установить аватар профиля", - "UserProfileEmptyNameError": "Имя обязательно", + "UserProfilesSetProfileImage": "Установить аватар", + "UserProfileEmptyNameError": "Необходимо ввести никнейм", "UserProfileNoImageError": "Необходимо установить аватар", "GameUpdateWindowHeading": "Доступные обновления для {0} ({1})", "SettingsTabHotkeysResScaleUpHotkey": "Увеличить разрешение:", "SettingsTabHotkeysResScaleDownHotkey": "Уменьшить разрешение:", - "UserProfilesName": "Имя:", + "UserProfilesName": "Никнейм:", "UserProfilesUserId": "ID пользователя:", "SettingsTabGraphicsBackend": "Графический бэкенд", - "SettingsTabGraphicsBackendTooltip": "Выберите бэкенд, который будет использован в эмуляторе.\n\nVulkan является лучшим выбором для всех современных графических карт с актуальными драйверами. В Vulkan также включена более быстрая компиляция шейдеров (меньше статтеров) для всех GPU.\n\nOpenGL может достичь лучших результатов на старых GPU Nvidia, на старых GPU AMD на Linux или на GPU с небольшим количеством VRAM, хотя статтеров при компиляции шейдеров будет больше.\n\nРекомендуется использовать Vulkan. Используйте OpenGL, если ваш GPU не поддерживает Vulkan даже с актуальными драйверами.", - "SettingsEnableTextureRecompression": "Включить пережатие текстур", - "SettingsEnableTextureRecompressionTooltip": "Сжатие ASTC текстур для уменьшения использования VRAM. \n\nИгры, использующие этот формат текстур: Astral Chain, Bayonetta 3, Fire Emblem Engage, Metroid Prime Remastered, Super Mario Bros. Wonder и The Legend of Zelda: Tears of the Kingdom. \n\nНа GPU с 4GiB VRAM или менее возможны вылеты при запуске этих игр. \n\nВключите, только если у вас заканчивается VRAM в вышеупомянутых играх. Рекомендуется оставить выключенным.", - "SettingsTabGraphicsPreferredGpu": "Предпочтительный GPU", - "SettingsTabGraphicsPreferredGpuTooltip": "Выберите GPU, который будет использоваться с графическим бэкендом Vulkan.\n\nЭта настройка не влияет на GPU, который будет использовать OpenGL.\n\nЕсли вы не уверены что нужно выбрать, используйте графический процессор, помеченный как \"dGPU\". Если его нет, оставьте выбор по умолчанию.", + "SettingsTabGraphicsBackendTooltip": "Выберает бэкенд, который будет использован в эмуляторе.\n\nVulkan является лучшим выбором для всех современных графических карт с актуальными драйверами. В Vulkan также включена более быстрая компиляция шейдеров (меньше статтеров) для всех видеоадаптеров.\n\nПри использовании OpenGL можно достичь лучших результатов на старых видеоадаптерах Nvidia и AMD в Linux или на видеоадаптерах с небольшим количеством видеопамяти, хотя статтеров при компиляции шейдеров будет больше.\n\nРекомендуется использовать Vulkan. Используйте OpenGL, если ваш видеоадаптер не поддерживает Vulkan даже с актуальными драйверами.", + "SettingsEnableTextureRecompression": "Пережимать текстуры", + "SettingsEnableTextureRecompressionTooltip": "Сжатие ASTC текстур для уменьшения использования VRAM. \n\nИгры, использующие этот формат текстур: Astral Chain, Bayonetta 3, Fire Emblem Engage, Metroid Prime Remastered, Super Mario Bros. Wonder и The Legend of Zelda: Tears of the Kingdom. \nНа видеоадаптерах с 4GiB видеопамяти или менее возможны вылеты при запуске этих игр. \n\nВключите, только если у вас заканчивается видеопамять в вышеупомянутых играх. \n\nРекомендуется оставить выключенным.", + "SettingsTabGraphicsPreferredGpu": "Предпочтительный видеоадаптер", + "SettingsTabGraphicsPreferredGpuTooltip": "Выберает видеоадаптер, который будет использоваться графическим бэкендом Vulkan.\n\nЭта настройка не влияет на видеоадаптер, который будет использоваться с OpenGL.\n\nЕсли вы не уверены что нужно выбрать, используйте графический процессор, помеченный как \"dGPU\". Если его нет, оставьте выбор по умолчанию.", "SettingsAppRequiredRestartMessage": "Требуется перезапуск Ryujinx", "SettingsGpuBackendRestartMessage": "Графический бэкенд или настройки графического процессора были изменены. Требуется перезапуск для вступления в силу изменений.", "SettingsGpuBackendRestartSubMessage": "Перезапустить сейчас?", - "RyujinxUpdaterMessage": "Вы хотите обновить Ryujinx до последней версии?", + "RyujinxUpdaterMessage": "Обновить Ryujinx до последней версии?", "SettingsTabHotkeysVolumeUpHotkey": "Увеличить громкость:", "SettingsTabHotkeysVolumeDownHotkey": "Уменьшить громкость:", - "SettingsEnableMacroHLE": "Включить Macro HLE", - "SettingsEnableMacroHLETooltip": "Высокоуровневая эмуляции макроса GPU.\n\nПовышает производительность, но может вызывать графические сбои в некоторых играх.\n\nРекомендуется оставить включенным.", - "SettingsEnableColorSpacePassthrough": "Пропуск цветового пространства", + "SettingsEnableMacroHLE": "Использовать макрос высокоуровневой эмуляции видеоадаптера", + "SettingsEnableMacroHLETooltip": "Высокоуровневая эмуляции макрокода видеоадаптера.\n\nПовышает производительность, но может вызывать графические артефакты в некоторых играх.\n\nРекомендуется оставить включенным.", + "SettingsEnableColorSpacePassthrough": "Пропускать цветовое пространство", "SettingsEnableColorSpacePassthroughTooltip": "Направляет бэкенд Vulkan на передачу информации о цвете без указания цветового пространства. Для пользователей с экранами с расширенной гаммой данная настройка приводит к получению более ярких цветов за счет снижения корректности цветопередачи.", "VolumeShort": "Громкость", "UserProfilesManageSaves": "Управление сохранениями", - "DeleteUserSave": "Вы хотите удалить сохранения для этой игры?", + "DeleteUserSave": "Удалить сохранения для этой игры?", "IrreversibleActionNote": "Данное действие является необратимым.", "SaveManagerHeading": "Редактирование сохранений для {0} ({1})", "SaveManagerTitle": "Менеджер сохранений", @@ -644,10 +751,10 @@ "Recover": "Восстановление", "UserProfilesRecoverHeading": "Были найдены сохранения для следующих аккаунтов", "UserProfilesRecoverEmptyList": "Нет учетных записей для восстановления", - "GraphicsAATooltip": "Применимое сглаживание для рендера.\n\nFXAA размывает большую часть изображения, SMAA попытается найти \"зазубренные\" края и сгладить их.\n\nНе рекомендуется использовать вместе с масштабирующим фильтром FSR.\n\nЭта опция может быть изменена во время игры по нажатию \"Применить\" ниже; Вы можете просто переместить окно настроек в сторону и поэкспериментировать, пока не найдёте подходящую настройку игры.\n\nРекомендуется использовать \"Нет\".", + "GraphicsAATooltip": "Применимое сглаживание для рендера.\n\nFXAA размывает большую часть изображения, SMAA попытается найти \"зазубренные\" края и сгладить их.\n\nНе рекомендуется использовать вместе с масштабирующим фильтром FSR.\n\nЭта опция может быть изменена во время игры по нажатию \"Применить\" ниже; \nВы можете просто переместить окно настроек в сторону и поэкспериментировать, пока не найдёте подходящую настройку игры.\n\nРекомендуется использовать \"Нет\".", "GraphicsAALabel": "Сглаживание:", "GraphicsScalingFilterLabel": "Интерполяция:", - "GraphicsScalingFilterTooltip": "Выберите фильтрацию, которая будет применяться при масштабировании.\n\n\"Билинейная\" хорошо работает для 3D-игр и является настройкой по умолчанию.\n\n\"Ступенчатая\" рекомендуется для пиксельных игр.\n\n\"FSR\" это фильтр резкости, который не рекомендуется использовать с FXAA или SMAA.\n\nЭта опция может быть изменена во время игры по нажатию кнопки \"Применить\" ниже; Вы можете просто переместить окно настроек в сторону и поэкспериментировать, пока не подберете подходящие настройки для конкретной игры.\n\nРекомендуется использовать \"Билинейная\".", + "GraphicsScalingFilterTooltip": "Фильтрация текстур, которая будет применяться при масштабировании.\n\nБилинейная хорошо работает для 3D-игр и является настройкой по умолчанию.\n\nСтупенчатая рекомендуется для пиксельных игр.\n\nFSR это фильтр резкости, который не рекомендуется использовать с FXAA или SMAA.\n\nЭта опция может быть изменена во время игры по нажатию кнопки \"Применить\" ниже; \nВы можете просто переместить окно настроек в сторону и поэкспериментировать, пока не подберете подходящие настройки для конкретной игры.\n\nРекомендуется использовать \"Билинейная\".", "GraphicsScalingFilterBilinear": "Билинейная", "GraphicsScalingFilterNearest": "Ступенчатая", "GraphicsScalingFilterFsr": "FSR", @@ -667,7 +774,7 @@ "AboutChangelogButtonTooltipMessage": "Нажмите, чтобы открыть список изменений для этой версии", "SettingsTabNetworkMultiplayer": "Мультиплеер", "MultiplayerMode": "Режим:", - "MultiplayerModeTooltip": "Изменение многопользовательского режима LDN.\n\nLdnMitm модифицирует функциональность локальной беспроводной/игры на одном устройстве в играх, позволяя играть с другими пользователями Ryujinx или взломанными консолями Nintendo Switch с установленным модулем ldn_mitm, находящимися в одной локальной сети друг с другом.\n\nМногопользовательская игра требует наличия у всех игроков одной и той же версии игры (т.е. Super Smash Bros. Ultimate v13.0.1 не может подключиться к v13.0.0).\n\nРекомендуется оставить отключенным.", + "MultiplayerModeTooltip": "Меняет многопользовательский режим LDN.\n\nLdnMitm модифицирует функциональность локальной беспроводной/игры на одном устройстве в играх, позволяя играть с другими пользователями Ryujinx или взломанными консолями Nintendo Switch с установленным модулем ldn_mitm, находящимися в одной локальной сети друг с другом.\n\nМногопользовательская игра требует наличия у всех игроков одной и той же версии игры (т.е. Super Smash Bros. Ultimate v13.0.1 не может подключиться к v13.0.0).\n\nРекомендуется оставить отключенным.", "MultiplayerModeDisabled": "Отключено", "MultiplayerModeLdnMitm": "ldn_mitm" } diff --git a/src/Ryujinx/Assets/Locales/th_TH.json b/src/Ryujinx/Assets/Locales/th_TH.json index 23745ad94..629442269 100644 --- a/src/Ryujinx/Assets/Locales/th_TH.json +++ b/src/Ryujinx/Assets/Locales/th_TH.json @@ -11,27 +11,31 @@ "MenuBarFile": "ไฟล์", "MenuBarFileOpenFromFile": "โหลดแอปพลิเคชั่นจากไฟล์", "MenuBarFileOpenUnpacked": "โหลดเกมที่คลายแพ็กแล้ว", - "MenuBarFileOpenEmuFolder": "เปิดโฟลเดอร์ รียูจินซ์", + "MenuBarFileOpenEmuFolder": "เปิดโฟลเดอร์ Ryujinx", "MenuBarFileOpenLogsFolder": "เปิดโฟลเดอร์ Logs", - "MenuBarFileExit": "ออก", + "MenuBarFileExit": "_ออก", "MenuBarOptions": "_ตัวเลือก", "MenuBarOptionsToggleFullscreen": "สลับการแสดงผลแบบเต็มหน้าจอ", "MenuBarOptionsStartGamesInFullscreen": "เริ่มเกมในโหมดเต็มหน้าจอ", "MenuBarOptionsStopEmulation": "หยุดการจำลอง", - "MenuBarOptionsSettings": "การตั้งค่า", - "MenuBarOptionsManageUserProfiles": "จัดการโปรไฟล์ผู้ใช้งาน", + "MenuBarOptionsSettings": "_ตั้งค่า", + "MenuBarOptionsManageUserProfiles": "_จัดการโปรไฟล์ผู้ใช้งาน", "MenuBarActions": "การดำเนินการ", "MenuBarOptionsSimulateWakeUpMessage": "จำลองข้อความปลุก", - "MenuBarActionsScanAmiibo": "สแกนหา อะมิโบ", + "MenuBarActionsScanAmiibo": "สแกนหา Amiibo", "MenuBarTools": "_เครื่องมือ", "MenuBarToolsInstallFirmware": "ติดตั้งเฟิร์มแวร์", "MenuBarFileToolsInstallFirmwareFromFile": "ติดตั้งเฟิร์มแวร์จาก ไฟล์ XCI หรือ ไฟล์ ZIP", "MenuBarFileToolsInstallFirmwareFromDirectory": "ติดตั้งเฟิร์มแวร์จากไดเร็กทอรี", "MenuBarToolsManageFileTypes": "จัดการประเภทไฟล์", - "MenuBarToolsInstallFileTypes": "ติดตั้งประเภทไฟล์", - "MenuBarToolsUninstallFileTypes": "ถอนการติดตั้งประเภทไฟล์", + "MenuBarToolsInstallFileTypes": "ติดตั้งตามประเภทของไฟล์", + "MenuBarToolsUninstallFileTypes": "ถอนการติดตั้งตามประเภทของไฟล์", + "MenuBarView": "_View", + "MenuBarViewWindow": "Window Size", + "MenuBarViewWindow720": "720p", + "MenuBarViewWindow1080": "1080p", "MenuBarHelp": "_ช่วยเหลือ", - "MenuBarHelpCheckForUpdates": "ตรวจหาการอัพเดต", + "MenuBarHelpCheckForUpdates": "ตรวจสอบอัปเดต", "MenuBarHelpAbout": "เกี่ยวกับ", "MenuSearch": "กำลังค้นหา...", "GameListHeaderFavorite": "ชื่นชอบ", @@ -39,31 +43,31 @@ "GameListHeaderApplication": "ชื่อ", "GameListHeaderDeveloper": "ผู้พัฒนา", "GameListHeaderVersion": "เวอร์ชั่น", - "GameListHeaderTimePlayed": "เวลาที่เล่นไปแล้ว", + "GameListHeaderTimePlayed": "เล่นไปแล้ว", "GameListHeaderLastPlayed": "เล่นล่าสุด", "GameListHeaderFileExtension": "นามสกุลไฟล์", "GameListHeaderFileSize": "ขนาดไฟล์", - "GameListHeaderPath": "ที่เก็บไฟล์", + "GameListHeaderPath": "ที่อยู่ไฟล์", "GameListContextMenuOpenUserSaveDirectory": "เปิดไดเร็กทอรี่บันทึกของผู้ใช้", "GameListContextMenuOpenUserSaveDirectoryToolTip": "เปิดไดเร็กทอรี่ซึ่งมีการบันทึกผู้ใช้ของแอปพลิเคชัน", - "GameListContextMenuOpenDeviceSaveDirectory": "เปิดไดเร็กทอรีบันทึกของอุปกรณ์", + "GameListContextMenuOpenDeviceSaveDirectory": "เปิดไดเร็กทอรี่บันทึกของอุปกรณ์", "GameListContextMenuOpenDeviceSaveDirectoryToolTip": "เปิดไดเรกทอรี่ซึ่งมีบันทึกอุปกรณ์ของแอปพลิเคชัน", - "GameListContextMenuOpenBcatSaveDirectory": "เปิดไดเรกทอรีบันทึก BCAT", + "GameListContextMenuOpenBcatSaveDirectory": "เปิดไดเรกทอรี่บันทึก BCAT", "GameListContextMenuOpenBcatSaveDirectoryToolTip": "เปิดไดเรกทอรี่ซึ่งมีการบันทึก BCAT ของแอปพลิเคชัน", - "GameListContextMenuManageTitleUpdates": "จัดการ การอัปเดตหัวข้อ", + "GameListContextMenuManageTitleUpdates": "จัดการอัปเดตตามหัวข้อ", "GameListContextMenuManageTitleUpdatesToolTip": "เปิดหน้าต่างการจัดการการอัพเดตหัวข้อ", "GameListContextMenuManageDlc": "จัดการ DLC", - "GameListContextMenuManageDlcToolTip": "เปิดหน้าต่างการจัดการ DLC", - "GameListContextMenuCacheManagement": "การบริหารจัดการแคช", + "GameListContextMenuManageDlcToolTip": "เปิดหน้าต่างจัดการ DLC", + "GameListContextMenuCacheManagement": "จัดการ แคช", "GameListContextMenuCacheManagementPurgePptc": "เพิ่มเข้าคิวงาน PPTC ที่สร้างใหม่", "GameListContextMenuCacheManagementPurgePptcToolTip": "ทริกเกอร์ PPTC ให้สร้างใหม่ในเวลาบูตเมื่อเปิดตัวเกมครั้งถัดไป", - "GameListContextMenuCacheManagementPurgeShaderCache": "ล้าง เชเดอร์แคช", - "GameListContextMenuCacheManagementPurgeShaderCacheToolTip": "ลบ เชเดอร์แคช ของแอปพลิเคชัน", - "GameListContextMenuCacheManagementOpenPptcDirectory": "เปิดไดเรกทอรี่่ PPTC", - "GameListContextMenuCacheManagementOpenPptcDirectoryToolTip": "เปิดไดเร็กทอรีที่มี PPTC แคช ของแอปพลิเคชัน", - "GameListContextMenuCacheManagementOpenShaderCacheDirectory": "เปิดไดเรกทอรี่ เชเดอร์แคช", - "GameListContextMenuCacheManagementOpenShaderCacheDirectoryToolTip": "เปิดไดเรกทอรี่ที่มี แคชเชเดอร์ ของแอปพลิเคชัน", - "GameListContextMenuExtractData": "แยกข้อมูล", + "GameListContextMenuCacheManagementPurgeShaderCache": "ล้างแคช พื้นผิวและแสงเงา", + "GameListContextMenuCacheManagementPurgeShaderCacheToolTip": "ลบแคช พื้นผิวและแสงเงา ของแอปพลิเคชัน", + "GameListContextMenuCacheManagementOpenPptcDirectory": "เปิดไดเรกทอรี่ PPTC", + "GameListContextMenuCacheManagementOpenPptcDirectoryToolTip": "เปิดไดเร็กทอรี่ PPTC แคช ของแอปพลิเคชัน", + "GameListContextMenuCacheManagementOpenShaderCacheDirectory": "เปิดไดเรกทอรี่ แคช พื้นผิวและแสงเงา", + "GameListContextMenuCacheManagementOpenShaderCacheDirectoryToolTip": "เปิดไดเรกทอรี่ แคช พื้นผิวและแสงเงา ของแอปพลิเคชัน", + "GameListContextMenuExtractData": "แยกส่วนข้อมูล", "GameListContextMenuExtractDataExeFS": "ExeFS", "GameListContextMenuExtractDataExeFSToolTip": "แยกส่วน ExeFS ออกจากการกำหนดค่าปัจจุบันของแอปพลิเคชัน (รวมถึงการอัปเดต)", "GameListContextMenuExtractDataRomFS": "RomFS", @@ -73,28 +77,29 @@ "GameListContextMenuCreateShortcut": "สร้างทางลัดของแอปพลิเคชัน", "GameListContextMenuCreateShortcutToolTip": "สร้างทางลัดบนเดสก์ท็อปที่เรียกใช้แอปพลิเคชันที่เลือก", "GameListContextMenuCreateShortcutToolTipMacOS": "สร้างทางลัดในโฟลเดอร์ Applications ของ macOS ที่เรียกใช้ Application ที่เลือก", - "GameListContextMenuOpenModsDirectory": "เปิดไดเรกทอรี่ Mods", - "GameListContextMenuOpenModsDirectoryToolTip": "เปิดไดเร็กทอรีซึ่งมี Mods ของแอปพลิเคชัน", - "GameListContextMenuOpenSdModsDirectory": "เปิดไดเร็กทอรี Mods Atmosphere", - "GameListContextMenuOpenSdModsDirectoryToolTip": "เปิดไดเร็กทอรี Atmosphere ของการ์ด SD สำรองซึ่งมี Mods ของแอปพลิเคชัน มีประโยชน์สำหรับ mods ที่บรรจุมากับฮาร์ดแวร์จริง", + "GameListContextMenuOpenModsDirectory": "เปิดไดเร็กทอรี่ Mods", + "GameListContextMenuOpenModsDirectoryToolTip": "เปิดไดเร็กทอรี่ Mods ของแอปพลิเคชัน", + "GameListContextMenuOpenSdModsDirectory": "เปิดไดเร็กทอรี่ Mods Atmosphere", + "GameListContextMenuOpenSdModsDirectoryToolTip": "เปิดไดเร็กทอรี่ Atmosphere ของการ์ด SD สำรองซึ่งมี Mods ของแอปพลิเคชัน มีประโยชน์สำหรับ Mods ที่บรรจุมากับฮาร์ดแวร์จริง", "StatusBarGamesLoaded": "เกมส์โหลดแล้ว {0}/{1}", "StatusBarSystemVersion": "เวอร์ชั่นของระบบ: {0}", - "LinuxVmMaxMapCountDialogTitle": "ตรวจพบขีดจำกัดต่ำสุดสำหรับการแมปหน่วยความจำ", + "LinuxVmMaxMapCountDialogTitle": "ตรวจพบขีดจำกัดต่ำสุด สำหรับการแมปหน่วยความจำ", "LinuxVmMaxMapCountDialogTextPrimary": "คุณต้องการที่จะเพิ่มค่า vm.max_map_count ไปยัง {0}", "LinuxVmMaxMapCountDialogTextSecondary": "บางเกมอาจพยายามสร้างการแมปหน่วยความจำมากกว่าที่ได้รับอนุญาตในปัจจุบัน รียูจินซ์ จะปิดตัวลงเมื่อเกินขีดจำกัดนี้", "LinuxVmMaxMapCountDialogButtonUntilRestart": "ใช่, จนกว่าจะรีสตาร์ทครั้งถัดไป", "LinuxVmMaxMapCountDialogButtonPersistent": "ใช่, อย่างถาวร", "LinuxVmMaxMapCountWarningTextPrimary": "จำนวนสูงสุดของการแม็ปหน่วยความจำ ต่ำกว่าที่แนะนำ", "LinuxVmMaxMapCountWarningTextSecondary": "ค่าปัจจุบันของ vm.max_map_count ({0}) มีค่าต่ำกว่า {1} บางเกมอาจพยายามสร้างการแมปหน่วยความจำมากกว่าที่ได้รับอนุญาตในปัจจุบัน รียูจินซ์ จะปิดตัวลงเมื่อเกินขีดจำกัดนี้\n\nคุณอาจต้องการเพิ่มขีดจำกัดด้วยตนเองหรือติดตั้ง pkexec ซึ่งอนุญาตให้ ริวจินซ์ เพื่อช่วยเหลือคุณได้", - "Settings": "การตั้งค่า", + "Settings": "ตั้งค่า", "SettingsTabGeneral": "หน้าจอผู้ใช้", "SettingsTabGeneralGeneral": "ทั่วไป", "SettingsTabGeneralEnableDiscordRichPresence": "เปิดใช้งาน Discord Rich Presence", "SettingsTabGeneralCheckUpdatesOnLaunch": "ตรวจหาการอัปเดตเมื่อเปิดโปรแกรม", "SettingsTabGeneralShowConfirmExitDialog": "แสดง \"ยืนยันการออก\" กล่องข้อความโต้ตอบ", + "SettingsTabGeneralRememberWindowState": "Remember Window Size/Position", "SettingsTabGeneralHideCursor": "ซ่อน เคอร์เซอร์:", "SettingsTabGeneralHideCursorNever": "ไม่มี", - "SettingsTabGeneralHideCursorOnIdle": "ซ่อนอัตโนมัติ", + "SettingsTabGeneralHideCursorOnIdle": "เมื่อไม่ได้ใช้", "SettingsTabGeneralHideCursorAlways": "ตลอดเวลา", "SettingsTabGeneralGameDirectories": "ไดเรกทอรี่ของเกม", "SettingsTabGeneralAdd": "เพิ่ม", @@ -127,13 +132,13 @@ "SettingsTabSystemSystemLanguageLatinAmericanSpanish": "สเปน (ลาตินอเมริกา)", "SettingsTabSystemSystemLanguageSimplifiedChinese": "จีน (ตัวย่อ)", "SettingsTabSystemSystemLanguageTraditionalChinese": "จีน (ดั้งเดิม)", - "SettingsTabSystemSystemTimeZone": "โซนเวลาของระบบ:", + "SettingsTabSystemSystemTimeZone": "เขตเวลาของระบบ:", "SettingsTabSystemSystemTime": "เวลาของระบบ:", "SettingsTabSystemEnableVsync": "VSync", - "SettingsTabSystemEnablePptc": "PPTC (แคชการแปลแบบถาวรที่มีโปรไฟล์)", - "SettingsTabSystemEnableFsIntegrityChecks": "การตรวจสอบความถูกต้องของ FS", - "SettingsTabSystemAudioBackend": "แบ็กเอนด์เสียง:", - "SettingsTabSystemAudioBackendDummy": "ดัมมี่", + "SettingsTabSystemEnablePptc": "PPTC (แคชโปรไฟล์การแปลแบบถาวร)", + "SettingsTabSystemEnableFsIntegrityChecks": "ตรวจสอบความถูกต้องของ FS", + "SettingsTabSystemAudioBackend": "ระบบเสียงเบื้องหลัง:", + "SettingsTabSystemAudioBackendDummy": "Dummy", "SettingsTabSystemAudioBackendOpenAL": "OpenAL", "SettingsTabSystemAudioBackendSoundIO": "SoundIO", "SettingsTabSystemAudioBackendSDL2": "SDL2", @@ -142,9 +147,9 @@ "SettingsTabSystemExpandDramSize": "ใช้รูปแบบหน่วยความจำสำรอง (โหมดนักพัฒนา)", "SettingsTabSystemIgnoreMissingServices": "ไม่สนใจบริการที่ขาดหายไป", "SettingsTabGraphics": "กราฟิก", - "SettingsTabGraphicsAPI": "เอพีไอของกราฟิก", - "SettingsTabGraphicsEnableShaderCache": "เปิดใช้งาน เชเดอร์ แคช", - "SettingsTabGraphicsAnisotropicFiltering": "ตัวกรองแบบ แอนไอโซทรอปิก:", + "SettingsTabGraphicsAPI": "กราฟฟิก API", + "SettingsTabGraphicsEnableShaderCache": "เปิดใช้งาน แคชพื้นผิวและแสงเงา", + "SettingsTabGraphicsAnisotropicFiltering": "ตัวกรองแบบ Anisotropic:", "SettingsTabGraphicsAnisotropicFilteringAuto": "อัตโนมัติ", "SettingsTabGraphicsAnisotropicFiltering2x": "2x", "SettingsTabGraphicsAnisotropicFiltering4x": "4x", @@ -164,26 +169,26 @@ "SettingsTabGraphicsAspectRatio32x9": "32:9", "SettingsTabGraphicsAspectRatioStretch": "ยืดภาพเพื่อให้พอดีกับหน้าต่าง", "SettingsTabGraphicsDeveloperOptions": "ตัวเลือกนักพัฒนา", - "SettingsTabGraphicsShaderDumpPath": "ที่เก็บไฟล์ดัมพ์ของ เชเดอร์กราฟิก:", - "SettingsTabLogging": "การบันทึก", - "SettingsTabLoggingLogging": "การบันทึก", - "SettingsTabLoggingEnableLoggingToFile": "เปิดใช้งาน การบันทึกไปยังไฟล์", - "SettingsTabLoggingEnableStubLogs": "เปิดใช้งาน บันทึกของต้นขั้ว", - "SettingsTabLoggingEnableInfoLogs": "เปิดใช้งาน บันทึกของข้อมูล", - "SettingsTabLoggingEnableWarningLogs": "เปิดใช้งาน บันทึกคำเตือน", - "SettingsTabLoggingEnableErrorLogs": "เปิดใช้งาน บันทึกข้อผิดพลาด", - "SettingsTabLoggingEnableTraceLogs": "เปิดใช้งาน บันทึกการติดตาม", + "SettingsTabGraphicsShaderDumpPath": "ที่เก็บ ดัมพ์ไฟล์ พื้นผิวและแสงเงา:", + "SettingsTabLogging": "ประวัติ", + "SettingsTabLoggingLogging": "ประวัติ", + "SettingsTabLoggingEnableLoggingToFile": "เปิดใช้งาน ประวัติ ไปยังไฟล์", + "SettingsTabLoggingEnableStubLogs": "เปิดใช้งาน ประวัติ", + "SettingsTabLoggingEnableInfoLogs": "เปิดใช้งาน ประวัติการใช้งาน", + "SettingsTabLoggingEnableWarningLogs": "เปิดใช้งาน ประวัติคำเตือน", + "SettingsTabLoggingEnableErrorLogs": "เปิดใช้งาน ประวัติข้อผิดพลาด", + "SettingsTabLoggingEnableTraceLogs": "เปิดใช้งาน ประวัติการติดตาม", "SettingsTabLoggingEnableGuestLogs": "เปิดใช้งาน บันทึกของผู้เยี่ยมชม", - "SettingsTabLoggingEnableFsAccessLogs": "เปิดใช้งาน บันทึกการเข้าถึง Fs", - "SettingsTabLoggingFsGlobalAccessLogMode": "โหมดบันทึกการเข้าถึงส่วนกลาง:", + "SettingsTabLoggingEnableFsAccessLogs": "เปิดใช้งาน ประวัติการเข้าถึง Fs", + "SettingsTabLoggingFsGlobalAccessLogMode": "โหมด ประวัติการเข้าถึงส่วนกลาง:", "SettingsTabLoggingDeveloperOptions": "ตัวเลือกนักพัฒนา", "SettingsTabLoggingDeveloperOptionsNote": "คำเตือน: จะทำให้ประสิทธิภาพลดลง", - "SettingsTabLoggingGraphicsBackendLogLevel": "ระดับการบันทึก แบ็กเอนด์กราฟิก:", + "SettingsTabLoggingGraphicsBackendLogLevel": "ระดับการบันทึกประวัติ กราฟิกเบื้องหลัง:", "SettingsTabLoggingGraphicsBackendLogLevelNone": "ไม่มี", "SettingsTabLoggingGraphicsBackendLogLevelError": "ผิดพลาด", - "SettingsTabLoggingGraphicsBackendLogLevelPerformance": "ชะลอตัว", + "SettingsTabLoggingGraphicsBackendLogLevelPerformance": "ช้าลง", "SettingsTabLoggingGraphicsBackendLogLevelAll": "ทั้งหมด", - "SettingsTabLoggingEnableDebugLogs": "เปิดใช้งานบันทึกการแก้ไขข้อบกพร่อง", + "SettingsTabLoggingEnableDebugLogs": "เปิดใช้งาน ประวัติแก้ไขข้อบกพร่อง", "SettingsTabInput": "ป้อนข้อมูล", "SettingsTabInputEnableDockedMode": "ด็อกโหมด", "SettingsTabInputDirectKeyboardAccess": "เข้าถึงคีย์บอร์ดโดยตรง", @@ -261,11 +266,112 @@ "ControllerSettingsMotionControllerSlot": "ช่องเสียบ คอนโทรลเลอร์:", "ControllerSettingsMotionMirrorInput": "นำเข้าการสะท้อน การควบคุม", "ControllerSettingsMotionRightJoyConSlot": "ช่องเสียบ จอยคอน ด้านขวา:", - "ControllerSettingsMotionServerHost": "เซิร์ฟเวอร์โฮสต์:", + "ControllerSettingsMotionServerHost": "เจ้าของเซิร์ฟเวอร์:", "ControllerSettingsMotionGyroSensitivity": "ความไวของไจโร:", - "ControllerSettingsMotionGyroDeadzone": "โซนที่ไม่ทำงานของไจโร:", + "ControllerSettingsMotionGyroDeadzone": "ส่วนไม่ทำงานของไจโร:", "ControllerSettingsSave": "บันทึก", "ControllerSettingsClose": "ปิด", + "KeyUnknown": "Unknown", + "KeyShiftLeft": "Shift Left", + "KeyShiftRight": "Shift Right", + "KeyControlLeft": "Ctrl Left", + "KeyMacControlLeft": "⌃ Left", + "KeyControlRight": "Ctrl Right", + "KeyMacControlRight": "⌃ Right", + "KeyAltLeft": "Alt Left", + "KeyMacAltLeft": "⌥ Left", + "KeyAltRight": "Alt Right", + "KeyMacAltRight": "⌥ Right", + "KeyWinLeft": "⊞ Left", + "KeyMacWinLeft": "⌘ Left", + "KeyWinRight": "⊞ Right", + "KeyMacWinRight": "⌘ Right", + "KeyMenu": "Menu", + "KeyUp": "Up", + "KeyDown": "Down", + "KeyLeft": "Left", + "KeyRight": "Right", + "KeyEnter": "Enter", + "KeyEscape": "Escape", + "KeySpace": "Space", + "KeyTab": "Tab", + "KeyBackSpace": "Backspace", + "KeyInsert": "Insert", + "KeyDelete": "Delete", + "KeyPageUp": "Page Up", + "KeyPageDown": "Page Down", + "KeyHome": "Home", + "KeyEnd": "End", + "KeyCapsLock": "Caps Lock", + "KeyScrollLock": "Scroll Lock", + "KeyPrintScreen": "Print Screen", + "KeyPause": "Pause", + "KeyNumLock": "Num Lock", + "KeyClear": "Clear", + "KeyKeypad0": "Keypad 0", + "KeyKeypad1": "Keypad 1", + "KeyKeypad2": "Keypad 2", + "KeyKeypad3": "Keypad 3", + "KeyKeypad4": "Keypad 4", + "KeyKeypad5": "Keypad 5", + "KeyKeypad6": "Keypad 6", + "KeyKeypad7": "Keypad 7", + "KeyKeypad8": "Keypad 8", + "KeyKeypad9": "Keypad 9", + "KeyKeypadDivide": "Keypad Divide", + "KeyKeypadMultiply": "Keypad Multiply", + "KeyKeypadSubtract": "Keypad Subtract", + "KeyKeypadAdd": "Keypad Add", + "KeyKeypadDecimal": "Keypad Decimal", + "KeyKeypadEnter": "Keypad Enter", + "KeyNumber0": "0", + "KeyNumber1": "1", + "KeyNumber2": "2", + "KeyNumber3": "3", + "KeyNumber4": "4", + "KeyNumber5": "5", + "KeyNumber6": "6", + "KeyNumber7": "7", + "KeyNumber8": "8", + "KeyNumber9": "9", + "KeyTilde": "~", + "KeyGrave": "`", + "KeyMinus": "-", + "KeyPlus": "+", + "KeyBracketLeft": "[", + "KeyBracketRight": "]", + "KeySemicolon": ";", + "KeyQuote": "\"", + "KeyComma": ",", + "KeyPeriod": ".", + "KeySlash": "/", + "KeyBackSlash": "\\", + "KeyUnbound": "Unbound", + "GamepadLeftStick": "L Stick Button", + "GamepadRightStick": "R Stick Button", + "GamepadLeftShoulder": "Left Shoulder", + "GamepadRightShoulder": "Right Shoulder", + "GamepadLeftTrigger": "Left Trigger", + "GamepadRightTrigger": "Right Trigger", + "GamepadDpadUp": "Up", + "GamepadDpadDown": "Down", + "GamepadDpadLeft": "Left", + "GamepadDpadRight": "Right", + "GamepadMinus": "-", + "GamepadPlus": "+", + "GamepadGuide": "Guide", + "GamepadMisc1": "Misc", + "GamepadPaddle1": "Paddle 1", + "GamepadPaddle2": "Paddle 2", + "GamepadPaddle3": "Paddle 3", + "GamepadPaddle4": "Paddle 4", + "GamepadTouchpad": "Touchpad", + "GamepadSingleLeftTrigger0": "Left Trigger 0", + "GamepadSingleRightTrigger0": "Right Trigger 0", + "GamepadSingleLeftTrigger1": "Left Trigger 1", + "GamepadSingleRightTrigger1": "Right Trigger 1", + "StickLeft": "Left Stick", + "StickRight": "Right Stick", "UserProfilesSelectedUserProfile": "โปรไฟล์ผู้ใช้งานที่เลือก:", "UserProfilesSaveProfileName": "บันทึกชื่อโปรไฟล์", "UserProfilesChangeProfileImage": "เปลี่ยนรูปโปรไฟล์", @@ -273,25 +379,25 @@ "UserProfilesAddNewProfile": "สร้างโปรไฟล์ใหม่", "UserProfilesDelete": "ลบ", "UserProfilesClose": "ปิด", - "ProfileNameSelectionWatermark": "เลือกชื่อเล่น", - "ProfileImageSelectionTitle": "เลือกรูปโปรไฟล์ของคุณ", - "ProfileImageSelectionHeader": "เลือกรูปโปรไฟล์", - "ProfileImageSelectionNote": "คุณสามารถนำเข้ารูปโปรไฟล์ที่กำหนดเอง หรือเลือกอวาต้าจากเฟิร์มแวร์ระบบได้", - "ProfileImageSelectionImportImage": "นำเข้าไฟล์รูปภาพ", - "ProfileImageSelectionSelectAvatar": "เลือกรูปอวาต้าเฟิร์มแวร์", + "ProfileNameSelectionWatermark": "เลือก ชื่อเล่น", + "ProfileImageSelectionTitle": "เลือก รูปโปรไฟล์ ของคุณ", + "ProfileImageSelectionHeader": "เลือก รูปโปรไฟล์", + "ProfileImageSelectionNote": "คุณสามารถนำเข้ารูปโปรไฟล์ที่กำหนดเอง หรือ เลือกอวาต้าจากเฟิร์มแวร์ระบบได้", + "ProfileImageSelectionImportImage": "นำเข้า ไฟล์รูปภาพ", + "ProfileImageSelectionSelectAvatar": "เลือก รูปอวาต้า เฟิร์มแวร์", "InputDialogTitle": "กล่องโต้ตอบการป้อนข้อมูล", "InputDialogOk": "ตกลง", "InputDialogCancel": "ยกเลิก", - "InputDialogAddNewProfileTitle": "เลือกชื่อโปรไฟล์", + "InputDialogAddNewProfileTitle": "เลือก ชื่อโปรไฟล์", "InputDialogAddNewProfileHeader": "กรุณาใส่ชื่อโปรไฟล์", "InputDialogAddNewProfileSubtext": "(ความยาวสูงสุด: {0})", - "AvatarChoose": "เลือกรูปอวาต้าของคุณ", + "AvatarChoose": "เลือก รูปอวาต้า ของคุณ", "AvatarSetBackgroundColor": "ตั้งค่าสีพื้นหลัง", "AvatarClose": "ปิด", - "ControllerSettingsLoadProfileToolTip": "โหลดโปรไฟล์", - "ControllerSettingsAddProfileToolTip": "เพิ่มโปรไฟล์", - "ControllerSettingsRemoveProfileToolTip": "ลบโปรไฟล์", - "ControllerSettingsSaveProfileToolTip": "บันทึกโปรไฟล์", + "ControllerSettingsLoadProfileToolTip": "โหลด โปรไฟล์", + "ControllerSettingsAddProfileToolTip": "เพิ่ม โปรไฟล์", + "ControllerSettingsRemoveProfileToolTip": "ลบ โปรไฟล์", + "ControllerSettingsSaveProfileToolTip": "บันทึก โปรไฟล์", "MenuBarFileToolsTakeScreenshot": "ถ่ายภาพหน้าจอ", "MenuBarFileToolsHideUi": "ซ่อน UI", "GameListContextMenuRunApplication": "เรียกใช้แอปพลิเคชัน", @@ -343,11 +449,11 @@ "DialogFirmwareInstallEmbeddedMessage": "คุณต้องการติดตั้งเฟิร์มแวร์ที่ฝังอยู่ในเกมนี้หรือไม่? (เฟิร์มแวร์ {0})", "DialogFirmwareInstallEmbeddedSuccessMessage": "ไม่พบเฟิร์มแวร์ที่ติดตั้งไว้ แต่ รียูจินซ์ สามารถติดตั้งเฟิร์มแวร์ได้ {0} จากเกมที่ให้มา\nตอนนี้โปรแกรมจำลองจะเริ่มทำงาน", "DialogFirmwareNoFirmwareInstalledMessage": "ไม่มีการติดตั้งเฟิร์มแวร์", - "DialogFirmwareInstalledMessage": "เฟิร์มแวร์ทำการติดตั้งแล้ว {0}", - "DialogInstallFileTypesSuccessMessage": "ติดตั้งประเภทไฟล์สำเร็จแล้ว!", - "DialogInstallFileTypesErrorMessage": "ติดตั้งประเภทไฟล์ไม่สำเร็จ", - "DialogUninstallFileTypesSuccessMessage": "ถอนการติดตั้งประเภทไฟล์สำเร็จแล้ว!", - "DialogUninstallFileTypesErrorMessage": "ไม่สามารถถอนการติดตั้งประเภทไฟล์ได้", + "DialogFirmwareInstalledMessage": "เฟิร์มแวร์ติดตั้งแล้ว {0}", + "DialogInstallFileTypesSuccessMessage": "ติดตั้งตามประเภทของไฟล์สำเร็จแล้ว!", + "DialogInstallFileTypesErrorMessage": "ติดตั้งตามประเภทของไฟล์ไม่สำเร็จ", + "DialogUninstallFileTypesSuccessMessage": "ถอนการติดตั้งตามประเภทของไฟล์สำเร็จแล้ว!", + "DialogUninstallFileTypesErrorMessage": "ไม่สามารถถอนการติดตั้งตามประเภทของไฟล์ได้", "DialogOpenSettingsWindowLabel": "เปิดหน้าต่างการตั้งค่า", "DialogControllerAppletTitle": "แอพเพล็ตคอนโทรลเลอร์", "DialogMessageDialogErrorExceptionMessage": "เกิดข้อผิดพลาดในการแสดงกล่องโต้ตอบข้อความ: {0}", @@ -369,7 +475,7 @@ "DialogShaderDeletionMessage": "คุณกำลังจะลบ เชเดอร์แคช:\n\n{0}\n\nคุณแน่ใจหรือไม่ว่าต้องการดำเนินการต่อหรือไม่?", "DialogShaderDeletionErrorMessage": "เกิดข้อผิดพลาดในการล้าง เชเดอร์แคช {0}: {1}", "DialogRyujinxErrorMessage": "รียูจินซ์ พบข้อผิดพลาด", - "DialogInvalidTitleIdErrorMessage": "ข้อผิดพลาดของ ยูไอ: เกมที่เลือกไม่มีชื่อ ID ที่ถูกต้อง", + "DialogInvalidTitleIdErrorMessage": "ข้อผิดพลาดของ UI: เกมที่เลือกไม่มีชื่อ ID ที่ถูกต้อง", "DialogFirmwareInstallerFirmwareNotFoundErrorMessage": "ไม่พบเฟิร์มแวร์ของระบบที่ถูกต้อง {0}.", "DialogFirmwareInstallerFirmwareInstallTitle": "ติดตั้งเฟิร์มแวร์ {0}", "DialogFirmwareInstallerFirmwareInstallMessage": "นี่คื่อเวอร์ชั่นของระบบ {0} ที่ได้รับการติดตั้งเมื่อเร็วๆ นี้", @@ -401,7 +507,7 @@ "DialogModManagerDeletionWarningMessage": "คุณกำลังจะลบ ม็อด: {0}\n\nคุณแน่ใจหรือไม่ว่าต้องการดำเนินการต่อ?", "DialogModManagerDeletionAllWarningMessage": "คุณกำลังจะลบม็อดทั้งหมดสำหรับชื่อนี้\n\nคุณแน่ใจหรือไม่ว่าต้องการดำเนินการต่อ?", "SettingsTabGraphicsFeaturesOptions": "คุณสมบัติ", - "SettingsTabGraphicsBackendMultithreading": "มัลติเธรด แบ็กเอนด์กราฟิก:", + "SettingsTabGraphicsBackendMultithreading": "มัลติเธรด กราฟิกเบื้องหลัง:", "CommonAuto": "อัตโนมัติ", "CommonOff": "ปิดการใช้งาน", "CommonOn": "เปิดใช้งาน", @@ -421,7 +527,7 @@ "AboutRyujinxAboutContent": "รียูจินซ์ เป็นอีมูเลเตอร์สำหรับ Nintendo Switch™\nโปรดสนับสนุนเราบน เพทรีออน\nรับข่าวสารล่าสุดทั้งหมดบน ทวิตเตอร์ หรือ ดิสคอร์ด ของเรา\nนักพัฒนาที่สนใจจะมีส่วนร่วมสามารถดูข้อมูลเพิ่มเติมได้ที่ กิตฮับ หรือ ดิสคอร์ด ของเรา", "AboutRyujinxMaintainersTitle": "ได้รับการดูแลรักษาโดย:", "AboutRyujinxMaintainersContentTooltipMessage": "คลิกเพื่อเปิดหน้าผู้ร่วมให้ข้อมูลในเบราว์เซอร์เริ่มต้นของคุณ", - "AboutRyujinxSupprtersTitle": "สนับสนุนบน เพทรีออน โดย:", + "AboutRyujinxSupprtersTitle": "ลายนามผู้สนับสนุนบน เพทรีออน:", "AmiiboSeriesLabel": "อะมิโบซีรีส์", "AmiiboCharacterLabel": "ตัวละคร", "AmiiboScanButtonLabel": "สแกนเลย", @@ -451,7 +557,7 @@ "CustomThemeCheckTooltip": "ใช้ธีม Avalonia แบบกำหนดเองสำหรับ GUI เพื่อเปลี่ยนรูปลักษณ์ของเมนูโปรแกรมจำลอง", "CustomThemePathTooltip": "ไปยังที่เก็บไฟล์ธีม GUI แบบกำหนดเอง", "CustomThemeBrowseTooltip": "เรียกดูธีม GUI ที่กำหนดเอง", - "DockModeToggleTooltip": "ด็อกโหมดทำให้ระบบจำลองการทำงานเสมือน Nintendo ที่กำลังเชื่อมต่ออยู่ด็อก สิ่งนี้จะปรับปรุงความเสถียรภาพของกราฟิกในเกมส่วนใหญ่ ในทางกลับกัน การปิดใช้จะทำให้ระบบจำลองทำงานเหมือนกับ Nintendo Switch แบบพกพา ส่งผลให้คุณภาพกราฟิกลดลง\n\nกำหนดค่าส่วนควบคุมของผู้เล่น 1 หากวางแผนที่จะใช้ด็อกโหมด กำหนดค่าการควบคุมแบบ แฮนด์เฮลด์ หากวางแผนที่จะใช้โหมดแฮนด์เฮลด์\n\nเปิดทิ้งไว้หากคุณไม่แน่ใจ", + "DockModeToggleTooltip": "ด็อกโหมด ทำให้ระบบจำลองการทำงานเสมือน Nintendo ที่กำลังเชื่อมต่ออยู่ด็อก สิ่งนี้จะปรับปรุงความเสถียรภาพของกราฟิกในเกมส่วนใหญ่ ในทางกลับกัน การปิดใช้จะทำให้ระบบจำลองทำงานเหมือนกับ Nintendo Switch แบบพกพา ส่งผลให้คุณภาพกราฟิกลดลง\n\nกำหนดค่าส่วนควบคุมของผู้เล่น 1 หากวางแผนที่จะใช้ด็อกโหมด กำหนดค่าการควบคุมแบบ แฮนด์เฮลด์ หากวางแผนที่จะใช้โหมดแฮนด์เฮลด์\n\nเปิดทิ้งไว้หากคุณไม่แน่ใจ", "DirectKeyboardTooltip": "รองรับการเข้าถึงแป้นพิมพ์โดยตรง (HID) ให้เกมเข้าถึงคีย์บอร์ดของคุณเป็นอุปกรณ์ป้อนข้อความ\n\nใช้งานได้กับเกมที่รองรับการใช้งานคีย์บอร์ดบนฮาร์ดแวร์ของ Switch เท่านั้น\n\nหากคุณไม่แน่ใจปล่อยให้ปิดอย่างนั้น", "DirectMouseTooltip": "รองรับการเข้าถึงเมาส์โดยตรง (HID) ให้เกมเข้าถึงเมาส์ของคุณเป็นอุปกรณ์ชี้ตำแหน่ง\n\nใช้งานได้เฉพาะกับเกมที่รองรับการควบคุมเมาส์บนฮาร์ดแวร์ของ Switch เท่านั้น ซึ่งมีอยู่ไม่มากนัก\n\nเมื่อเปิดใช้งาน ฟังก์ชั่นหน้าจอสัมผัสอาจไม่ทำงาน\n\nหากคุณไม่แน่ใจปล่อยให้ปิดอย่างนั้น", "RegionTooltip": "เปลี่ยนภูมิภาคของระบบ", @@ -476,27 +582,27 @@ "ResolutionScaleEntryTooltip": "สเกลความละเอียดจุดทศนิยม เช่น 1.5 ไม่ใช่จำนวนเต็มของสเกล มีแนวโน้มที่จะก่อให้เกิดปัญหาหรือความผิดพลาดได้", "AnisotropyTooltip": "ระดับของการกรองแบบ Anisotropic ตั้งค่าเป็นอัตโนมัติเพื่อใช้ค่าที่เกมร้องขอ", "AspectRatioTooltip": "อัตราส่วนภาพที่ใช้กับหน้าต่างตัวแสดงภาพ\n\nเปลี่ยนสิ่งนี้หากคุณใช้ตัวดัดแปลงอัตราส่วนกว้างยาวสำหรับเกมของคุณ ไม่เช่นนั้นกราฟิกจะถูกยืดออก\n\nทิ้งไว้ที่ 16:9 หากไม่แน่ใจ", - "ShaderDumpPathTooltip": "ที่เก็บ ดัมพ์ไฟล์ของ เชเดอร์กราฟิก", - "FileLogTooltip": "บันทึกการบันทึกคอนโซลลงในไฟล์บันทึกบนดิสก์ จะไม่ส่งผลกระทบต่อประสิทธิภาพการทำงาน", - "StubLogTooltip": "พิมพ์ข้อความบันทึกต้นขั้วในคอนโซล จะไม่ส่งผลกระทบต่อประสิทธิภาพการทำงาน", + "ShaderDumpPathTooltip": "ที่เก็บ ดัมพ์ไฟล์ พื้นผิวและแสงเงา", + "FileLogTooltip": "บันทึก ประวัติคอนโซลลงในไฟล์บันทึกบนดิสก์ จะไม่ส่งผลกระทบต่อประสิทธิภาพการทำงาน", + "StubLogTooltip": "พิมพ์ข้อความประวัติในคอนโซล จะไม่ส่งผลกระทบต่อประสิทธิภาพการทำงาน", "InfoLogTooltip": "พิมพ์ข้อความบันทึกข้อมูลในคอนโซล จะไม่ส่งผลกระทบต่อประสิทธิภาพการทำงาน", - "WarnLogTooltip": "พิมพ์ข้อความบันทึกแจ้งตือนในคอนโซล จะไม่ส่งผลกระทบต่อประสิทธิภาพการทำงาน", + "WarnLogTooltip": "พิมพ์ข้อความประวัติแจ้งตือนในคอนโซล จะไม่ส่งผลกระทบต่อประสิทธิภาพการทำงาน", "ErrorLogTooltip": "พิมพ์ข้อความบันทึกข้อผิดพลาดในคอนโซล จะไม่ส่งผลกระทบต่อประสิทธิภาพการทำงาน", - "TraceLogTooltip": "พิมพ์ข้อความบันทึกการติดตามในคอนโซล ไม่ส่งผลกระทบต่อประสิทธิภาพการทำงาน", - "GuestLogTooltip": "พิมพ์ข้อความบันทึกของผู้เยี่ยมชมในคอนโซล ไม่ส่งผลกระทบต่อประสิทธิภาพการทำงาน", + "TraceLogTooltip": "พิมพ์ข้อความประวัติการติดตามในคอนโซล ไม่ส่งผลกระทบต่อประสิทธิภาพการทำงาน", + "GuestLogTooltip": "พิมพ์ข้อความประวัติของผู้เยี่ยมชมในคอนโซล ไม่ส่งผลกระทบต่อประสิทธิภาพการทำงาน", "FileAccessLogTooltip": "พิมพ์ข้อความบันทึกการเข้าถึงไฟล์ในคอนโซล", - "FSAccessLogModeTooltip": "เปิดใช้งานเอาต์พุตบันทึกการเข้าถึง FS ไปยังคอนโซล โหมดที่เป็นไปได้คือ 0-3", + "FSAccessLogModeTooltip": "เปิดใช้งาน เอาต์พุตประวัติการเข้าถึง FS ไปยังคอนโซล โหมดที่เป็นไปได้คือ 0-3", "DeveloperOptionTooltip": "โปรดใช้ด้วยความระมัดระวัง", "OpenGlLogLevel": "จำเป็นต้องเปิดใช้งานระดับบันทึกที่เหมาะสม", - "DebugLogTooltip": "พิมพ์ข้อความบันทึกการแก้ไขข้อบกพร่องในคอนโซล\n\nใช้สิ่งนี้เฉพาะเมื่อได้รับคำแนะนำจากเจ้าหน้าที่โดยเฉพาะเท่านั้น เนื่องจากจะทำให้บันทึกอ่านยากและทำให้ประสิทธิภาพของโปรแกรมจำลองแย่ลง", + "DebugLogTooltip": "พิมพ์ข้อความประวัติการแก้ไขข้อบกพร่องในคอนโซล\n\nใช้สิ่งนี้เฉพาะเมื่อได้รับคำแนะนำจากเจ้าหน้าที่โดยเฉพาะเท่านั้น เนื่องจากจะทำให้บันทึกอ่านยากและทำให้ประสิทธิภาพของโปรแกรมจำลองแย่ลง", "LoadApplicationFileTooltip": "เปิด File Explorer เพื่อเลือกไฟล์ที่เข้ากันได้กับ Switch ที่จะโหลด", "LoadApplicationFolderTooltip": "เปิดตัวสำรวจไฟล์เพื่อเลือกไฟล์ที่เข้ากันได้กับ Switch ที่จะโหลด", - "OpenRyujinxFolderTooltip": "เปิดโฟลเดอร์ระบบไฟล์ รียูจินซ์", - "OpenRyujinxLogsTooltip": "เปิดโฟลเดอร์ที่มีการเขียนบันทึก", + "OpenRyujinxFolderTooltip": "เปิดโฟลเดอร์ระบบไฟล์ Ryujinx", + "OpenRyujinxLogsTooltip": "เปิดโฟลเดอร์ ที่เก็บไฟล์ประวัติ", "ExitTooltip": "ออกจากโปรแกรม รียูจินซ์", "OpenSettingsTooltip": "เปิดหน้าต่างการตั้งค่า", "OpenProfileManagerTooltip": "เปิดหน้าต่างตัวจัดการโปรไฟล์ผู้ใช้", - "StopEmulationTooltip": "หยุดการจำลองเกมปัจจุบันและกลับไปยังการเลือกเกม", + "StopEmulationTooltip": "หยุดการจำลองของเกมที่เปิดอยู่ในปัจจุบันและกลับไปยังการเลือกเกม", "CheckUpdatesTooltip": "ตรวจสอบการอัปเดตของ รียูจินซ์", "OpenAboutTooltip": "เปิดหน้าต่าง เกี่ยวกับ", "GridSize": "ขนาดตาราง", @@ -519,7 +625,7 @@ "SettingsTabNetwork": "เครือข่าย", "SettingsTabNetworkConnection": "การเชื่อมต่อเครือข่าย", "SettingsTabCpuCache": "ซีพียู แคช", - "SettingsTabCpuMemory": "ซีพียูเมมโมรี่ แคช", + "SettingsTabCpuMemory": "โหมดซีพียู", "DialogUpdaterFlatpakNotSupportedMessage": "โปรดอัปเดต รียูจินซ์ ผ่านช่องทาง FlatHub", "UpdaterDisabledWarningTitle": "ปิดใช้งานการอัปเดตแล้ว!", "ControllerSettingsRotate90": "หมุน 90 องศา ตามเข็มนาฬิกา", @@ -527,20 +633,20 @@ "IconSizeTooltip": "เปลี่ยนขนาดของไอคอนเกม", "MenuBarOptionsShowConsole": "แสดง คอนโซล", "ShaderCachePurgeError": "เกิดข้อผิดพลาดในการล้างแคชเชเดอร์ {0}: {1}", - "UserErrorNoKeys": "ไม่พบคีย์", - "UserErrorNoFirmware": "ไม่พบเฟิร์มแวร์", - "UserErrorFirmwareParsingFailed": "เกิดข้อผิดพลาดในการแยกวิเคราะห์เฟิร์มแวร์", - "UserErrorApplicationNotFound": "ไม่พบแอปพลิเคชัน", + "UserErrorNoKeys": "ไม่พบ คีย์", + "UserErrorNoFirmware": "ไม่พบ เฟิร์มแวร์", + "UserErrorFirmwareParsingFailed": "เกิดข้อผิดพลาดในการวิเคราะห์เฟิร์มแวร์", + "UserErrorApplicationNotFound": "ไม่พบ แอปพลิเคชัน", "UserErrorUnknown": "ข้อผิดพลาดที่ไม่รู้จัก", "UserErrorUndefined": "ข้อผิดพลาดที่ไม่ได้ระบุ", - "UserErrorNoKeysDescription": "รียูจินซ์ ไม่พบไฟล์ 'prod.keys' ของคุณ", - "UserErrorNoFirmwareDescription": "รียูจินซ์ ไม่พบเฟิร์มแวร์ที่ติดตั้งไว้", - "UserErrorFirmwareParsingFailedDescription": "รียูจินซ์ ไม่สามารถแยกวิเคราะห์เฟิร์มแวร์ที่ให้มาได้ ซึ่งมักมีสาเหตุมาจากคีย์ที่ล้าสมัย", + "UserErrorNoKeysDescription": "รียูจินซ์ ไม่พบไฟล์ 'prod.keys' ในเครื่องของคุณ", + "UserErrorNoFirmwareDescription": "รียูจินซ์ ไม่พบ เฟิร์มแวร์ที่ติดตั้งไว้ในเครื่องของคุณ", + "UserErrorFirmwareParsingFailedDescription": "รียูจินซ์ ไม่สามารถวิเคราะห์เฟิร์มแวร์ที่ให้มาได้ ซึ่งมักมีสาเหตุมาจากคีย์ที่ล้าสมัย", "UserErrorApplicationNotFoundDescription": "รียูจินซ์ ไม่พบแอปพลิเคชันที่ถูกต้องในที่เก็บไฟล์ที่กำหนด", - "UserErrorUnknownDescription": "เกิดข้อผิดพลาดที่ไม่รู้จักเกิดขึ้น!", + "UserErrorUnknownDescription": "เกิดข้อผิดพลาดที่ไม่รู้จัก!", "UserErrorUndefinedDescription": "เกิดข้อผิดพลาดที่ไม่สามารถระบุได้! สิ่งนี้ไม่ควรเกิดขึ้น โปรดติดต่อผู้พัฒนา!", "OpenSetupGuideMessage": "เปิดคู่มือการตั้งค่า", - "NoUpdate": "ไม่มีการอัพเดต", + "NoUpdate": "ไม่มีการอัปเดต", "TitleUpdateVersionLabel": "เวอร์ชั่น {0}", "RyujinxInfo": "รียูจินซ์ – ข้อมูล", "RyujinxConfirm": "รียูจินซ์ - ยืนยัน", @@ -565,16 +671,16 @@ "Docked": "ด็อก", "Handheld": "แฮนด์เฮลด์", "ConnectionError": "การเชื่อมต่อล้มเหลว", - "AboutPageDeveloperListMore": "{0} และอื่น ๆ...", + "AboutPageDeveloperListMore": "{0} และอื่นๆ ...", "ApiError": "ข้อผิดพลาดของ API", "LoadingHeading": "กำลังโหลด {0}", "CompilingPPTC": "กำลังคอมไพล์ PTC", - "CompilingShaders": "กำลังคอมไพล์ เชเดอร์", + "CompilingShaders": "กำลังคอมไพล์ พื้นผิวและแสงเงา", "AllKeyboards": "คีย์บอร์ดทั้งหมด", "OpenFileDialogTitle": "เลือกไฟล์ที่สนับสนุนเพื่อเปิด", "OpenFolderDialogTitle": "เลือกโฟลเดอร์ที่มีเกมที่แตกไฟล์แล้ว", "AllSupportedFormats": "รูปแบบที่รองรับทั้งหมด", - "RyujinxUpdater": "อัพเดต รียูจินซ์", + "RyujinxUpdater": "อัปเดต รียูจินซ์", "SettingsTabHotkeys": "ปุ่มลัดของคีย์บอร์ด", "SettingsTabHotkeysHotkeys": "ปุ่มลัดของคีย์บอร์ด", "SettingsTabHotkeysToggleVsyncHotkey": "สลับเป็น VSync:", @@ -582,7 +688,7 @@ "SettingsTabHotkeysShowUiHotkey": "แสดง UI:", "SettingsTabHotkeysPauseHotkey": "หยุดชั่วคราว:", "SettingsTabHotkeysToggleMuteHotkey": "ปิดเสียง:", - "ControllerMotionTitle": "การตั้งค่าการควบคุมการเคลื่อนไหว", + "ControllerMotionTitle": "ตั้งค่าควบคุมการเคลื่อนไหว", "ControllerRumbleTitle": "ตั้งค่าการสั่นไหว", "SettingsSelectThemeFileDialogTitle": "เลือกไฟล์ธีม", "SettingsXamlThemeFile": "ไฟล์ธีมรูปแบบ XAML", @@ -593,11 +699,12 @@ "Writable": "สามารถเขียนได้", "SelectDlcDialogTitle": "เลือกไฟล์ DLC", "SelectUpdateDialogTitle": "เลือกไฟล์อัพเดต", - "SelectModDialogTitle": "เลือกไดเรกทอรี ม็อด", + "SelectModDialogTitle": "เลือกไดเรกทอรี Mods", "UserProfileWindowTitle": "จัดการโปรไฟล์ผู้ใช้", "CheatWindowTitle": "จัดการสูตรโกง", "DlcWindowTitle": "จัดการเนื้อหาที่ดาวน์โหลดได้สำหรับ {0} ({1})", - "UpdateWindowTitle": "จัดการการอัพเดตชื่อเรื่อง", + "ModWindowTitle": "Manage Mods for {0} ({1})", + "UpdateWindowTitle": "จัดการอัปเดตหัวข้อ", "CheatWindowHeading": "สูตรโกงมีให้สำหรับ {0} [{1}]", "BuildId": "รหัสบิวด์:", "DlcWindowHeading": "{0} เนื้อหาที่สามารถดาวน์โหลดได้", @@ -608,21 +715,21 @@ "Discard": "ละทิ้ง", "Paused": "หยุดชั่วคราว", "UserProfilesSetProfileImage": "ตั้งค่ารูปโปรไฟล์", - "UserProfileEmptyNameError": "จำเป็นต้องมีการระบุชื่อ", + "UserProfileEmptyNameError": "จำเป็นต้องระบุชื่อ", "UserProfileNoImageError": "จำเป็นต้องตั้งค่ารูปโปรไฟล์", "GameUpdateWindowHeading": "จัดการอัพเดตสำหรับ {0} ({1})", "SettingsTabHotkeysResScaleUpHotkey": "เพิ่มความละเอียด:", "SettingsTabHotkeysResScaleDownHotkey": "ลดความละเอียด:", "UserProfilesName": "ชื่อ:", "UserProfilesUserId": "รหัสผู้ใช้:", - "SettingsTabGraphicsBackend": "แบ็กเอนด์กราฟิก", - "SettingsTabGraphicsBackendTooltip": "เลือกแบ็กเอนด์กราฟิกที่จะใช้ในโปรแกรมจำลอง\n\nโดยรวมแล้ว Vulkan นั้นดีกว่าสำหรับกราฟิกการ์ดรุ่นใหม่ทั้งหมด ตราบใดที่ไดรเวอร์ยังอัพเดทอยู่เสมอ Vulkan ยังมีคุณสมบัติการคอมไพล์เชเดอร์ที่เร็วขึ้น (ลดอาการกระตุก) ของผู้จำหน่าย GPU ทุกรายอยู่แล้ว\n\nOpenGL อาจได้รับผลลัพธ์ที่ดีกว่าบน Nvidia GPU รุ่นเก่า, AMD GPU รุ่นเก่าบน Linux หรือบน GPU ที่มี VRAM ต่ำกว่า แม้ว่าการคอมไพล์เชเดอร์จะสะดุดมากขึ้นก็ตาม\n\nตั้งค่าเป็น Vulkan หากไม่แน่ใจ ตั้งค่าเป็น OpenGL หาก GPU ของคุณไม่รองรับ Vulkan แม้จะมีไดรเวอร์กราฟิกล่าสุดก็ตาม", - "SettingsEnableTextureRecompression": "เปิดใช้งานการบีบอัดพื้นผิวอีกครั้ง", + "SettingsTabGraphicsBackend": "กราฟิกเบื้องหลัง", + "SettingsTabGraphicsBackendTooltip": "เลือกกราฟิกเบื้องหลังที่จะใช้ในโปรแกรมจำลอง\n\nโดยรวมแล้ว Vulkan นั้นดีกว่าสำหรับกราฟิกการ์ดรุ่นใหม่ทั้งหมด ตราบใดที่ไดรเวอร์ยังอัพเดทอยู่เสมอ Vulkan ยังมีคุณสมบัติการคอมไพล์เชเดอร์ที่เร็วขึ้น (ลดอาการกระตุก) ของผู้จำหน่าย GPU ทุกราย\n\nOpenGL อาจได้รับผลลัพธ์ที่ดีกว่าบน Nvidia GPU รุ่นเก่า, AMD GPU รุ่นเก่าบน Linux หรือบน GPU ที่มี VRAM ต่ำกว่า แม้ว่าการคอมไพล์เชเดอร์ จะทำให้อาการกระตุกมากขึ้นก็ตาม\n\nตั้งค่าเป็น Vulkan หากไม่แน่ใจ ตั้งค่าเป็น OpenGL หาก GPU ของคุณไม่รองรับ Vulkan แม้จะมีไดรเวอร์กราฟิกล่าสุดก็ตาม", + "SettingsEnableTextureRecompression": "เปิดใช้งาน การบีบอัดพื้นผิวอีกครั้ง", "SettingsEnableTextureRecompressionTooltip": "บีบอัดพื้นผิว ASTC เพื่อลดการใช้งาน VRAM\n\nเกมที่ใช้รูปแบบพื้นผิวนี้ ได้แก่ Astral Chain, Bayonetta 3, Fire Emblem Engage, Metroid Prime Remastered, Super Mario Bros. Wonder และ The Legend of Zelda: Tears of the Kingdom\n\nกราฟิกการ์ดที่มี 4 กิกะไบต์ VRAM หรือน้อยกว่ามีแนวโน้มที่จะให้แคชในบางจุดขณะเล่นเกมเหล่านี้\n\nเปิดใช้งานเฉพาะในกรณีที่ VRAM ของคุณใกล้หมดในเกมที่กล่าวมาข้างต้น ปล่อยให้ปิดหากไม่แน่ใจ", "SettingsTabGraphicsPreferredGpu": "GPU ที่ต้องการ", "SettingsTabGraphicsPreferredGpuTooltip": "เลือกกราฟิกการ์ดที่จะใช้กับแบ็กเอนด์กราฟิก Vulkan\n\nไม่ส่งผลต่อ GPU ที่ OpenGL จะใช้\n\nตั้งค่าเป็น GPU ที่ถูกตั้งค่าสถานะเป็น \"dGPU\" หากคุณไม่แน่ใจ หากไม่มีก็ปล่อยทิ้งไว้โดยไม่มีใครแตะต้องมัน", "SettingsAppRequiredRestartMessage": "จำเป็นต้องรีสตาร์ท รียูจินซ์", - "SettingsGpuBackendRestartMessage": "การตั้งค่ากราฟิกแบ็กเอนด์หรือ GPU ได้รับการแก้ไขแล้ว สิ่งนี้จะต้องมีการรีสตาร์ทจึงจะสามารถใช้งานได้", + "SettingsGpuBackendRestartMessage": "การตั้งค่ากราฟิกเบื้องหลังหรือ GPU ได้รับการแก้ไขแล้ว สิ่งนี้จะต้องมีการรีสตาร์ทจึงจะสามารถใช้งานได้", "SettingsGpuBackendRestartSubMessage": "คุณต้องการรีสตาร์ทตอนนี้หรือไม่?", "RyujinxUpdaterMessage": "คุณต้องการอัพเดต รียูจินซ์ เป็นเวอร์ชั่นล่าสุดหรือไม่?", "SettingsTabHotkeysVolumeUpHotkey": "เพิ่มระดับเสียง:", @@ -662,9 +769,9 @@ "SettingsTabNetworkInterface": "เชื่อมต่อเครือข่าย:", "NetworkInterfaceTooltip": "อินเทอร์เฟซเครือข่ายที่ใช้สำหรับคุณสมบัติ LAN/LDN\n\nเมื่อใช้ร่วมกับ VPN หรือ XLink Kai และเกมที่รองรับ LAN สามารถใช้เพื่อปลอมการเชื่อมต่อเครือข่ายเดียวกันผ่านทางอินเทอร์เน็ต\n\nปล่อยให้เป็น ค่าเริ่มต้น หากคุณไม่แน่ใจ", "NetworkInterfaceDefault": "ค่าเริ่มต้น", - "PackagingShaders": "แพ็คเชเดอร์ไฟล์", - "AboutChangelogButton": "ดูบันทึกการเปลี่ยนแปลงบน GitHub", - "AboutChangelogButtonTooltipMessage": "คลิกเพื่อเปิดบันทึกการเปลี่ยนแปลงสำหรับเวอร์ชั่นนี้ บนเบราว์เซอร์เริ่มต้นของคุณ", + "PackagingShaders": "รวม Shaders เข้าด้วยกัน", + "AboutChangelogButton": "ดูประวัติการเปลี่ยนแปลงบน GitHub", + "AboutChangelogButtonTooltipMessage": "คลิกเพื่อเปิดประวัติการเปลี่ยนแปลงสำหรับเวอร์ชั่นนี้ บนเบราว์เซอร์เริ่มต้นของคุณ", "SettingsTabNetworkMultiplayer": "ผู้เล่นหลายคน", "MultiplayerMode": "โหมด:", "MultiplayerModeTooltip": "เปลี่ยนโหมดผู้เล่นหลายคนของ LDN\n\nLdnMitm จะปรับเปลี่ยนฟังก์ชันการเล่นแบบไร้สาย/ภายใน จะให้เกมทำงานเหมือนกับว่าเป็น LAN ช่วยให้สามารถเชื่อมต่อภายในเครือข่ายเดียวกันกับอินสแตนซ์ Ryujinx อื่น ๆ และคอนโซล Nintendo Switch ที่ถูกแฮ็กซึ่งมีโมดูล ldn_mitm ติดตั้งอยู่\n\nผู้เล่นหลายคนต้องการให้ผู้เล่นทุกคนอยู่ในเกมเวอร์ชันเดียวกัน (เช่น Super Smash Bros. Ultimate v13.0.1 ไม่สามารถเชื่อมต่อกับ v13.0.0)\n\nปล่อยให้ปิดการใช้งานหากไม่แน่ใจ", diff --git a/src/Ryujinx/Assets/Locales/tr_TR.json b/src/Ryujinx/Assets/Locales/tr_TR.json index bfb5cb53a..f74baaa18 100644 --- a/src/Ryujinx/Assets/Locales/tr_TR.json +++ b/src/Ryujinx/Assets/Locales/tr_TR.json @@ -30,6 +30,10 @@ "MenuBarToolsManageFileTypes": "Dosya uzantılarını yönet", "MenuBarToolsInstallFileTypes": "Dosya uzantılarını yükle", "MenuBarToolsUninstallFileTypes": "Dosya uzantılarını kaldır", + "MenuBarView": "_Görüntüle", + "MenuBarViewWindow": "Pencere Boyutu", + "MenuBarViewWindow720": "720p", + "MenuBarViewWindow1080": "1080p", "MenuBarHelp": "_Yardım", "MenuBarHelpCheckForUpdates": "Güncellemeleri Denetle", "MenuBarHelpAbout": "Hakkında", @@ -73,7 +77,7 @@ "GameListContextMenuCreateShortcut": "Uygulama Kısayolu Oluştur", "GameListContextMenuCreateShortcutToolTip": "Seçilmiş uygulamayı çalıştıracak bir masaüstü kısayolu oluştur", "GameListContextMenuCreateShortcutToolTipMacOS": "Create a shortcut in macOS's Applications folder that launches the selected Application", - "GameListContextMenuOpenModsDirectory": "Open Mods Directory", + "GameListContextMenuOpenModsDirectory": "Mod Dizinini Aç", "GameListContextMenuOpenModsDirectoryToolTip": "Opens the directory which contains Application's Mods", "GameListContextMenuOpenSdModsDirectory": "Open Atmosphere Mods Directory", "GameListContextMenuOpenSdModsDirectoryToolTip": "Opens the alternative SD card Atmosphere directory which contains Application's Mods. Useful for mods that are packaged for real hardware.", @@ -92,6 +96,7 @@ "SettingsTabGeneralEnableDiscordRichPresence": "Discord Zengin İçerik'i Etkinleştir", "SettingsTabGeneralCheckUpdatesOnLaunch": "Her Açılışta Güncellemeleri Denetle", "SettingsTabGeneralShowConfirmExitDialog": "\"Çıkışı Onayla\" Diyaloğunu Göster", + "SettingsTabGeneralRememberWindowState": "Remember Window Size/Position", "SettingsTabGeneralHideCursor": "İşaretçiyi Gizle:", "SettingsTabGeneralHideCursorNever": "Hiçbir Zaman", "SettingsTabGeneralHideCursorOnIdle": "Hareketsiz Durumda", @@ -155,7 +160,7 @@ "SettingsTabGraphicsResolutionScaleNative": "Yerel (720p/1080p)", "SettingsTabGraphicsResolutionScale2x": "2x (1440p/2160p)", "SettingsTabGraphicsResolutionScale3x": "3x (2160p/3240p)", - "SettingsTabGraphicsResolutionScale4x": "4x (2880p/4320p) (Not recommended)", + "SettingsTabGraphicsResolutionScale4x": "4x (2880p/4320p) (Tavsiye Edilmez)", "SettingsTabGraphicsAspectRatio": "En-Boy Oranı:", "SettingsTabGraphicsAspectRatio4x3": "4:3", "SettingsTabGraphicsAspectRatio16x9": "16:9", @@ -266,6 +271,107 @@ "ControllerSettingsMotionGyroDeadzone": "Gyro Ölü Bölgesi:", "ControllerSettingsSave": "Kaydet", "ControllerSettingsClose": "Kapat", + "KeyUnknown": "Unknown", + "KeyShiftLeft": "Sol Shift", + "KeyShiftRight": "Sağ Shift", + "KeyControlLeft": "Sol Ctrl", + "KeyMacControlLeft": "⌃ Sol", + "KeyControlRight": "Sağ Control", + "KeyMacControlRight": "⌃ Sağ", + "KeyAltLeft": "Sol Alt", + "KeyMacAltLeft": "⌥ Sol", + "KeyAltRight": "Sağ Alt", + "KeyMacAltRight": "⌥ Sağ", + "KeyWinLeft": "⊞ Sol", + "KeyMacWinLeft": "⌘ Sol", + "KeyWinRight": "⊞ Sağ", + "KeyMacWinRight": "⌘ Sağ", + "KeyMenu": "Menü", + "KeyUp": "Yukarı", + "KeyDown": "Aşağı", + "KeyLeft": "Sol", + "KeyRight": "Sağ", + "KeyEnter": "Enter", + "KeyEscape": "Esc", + "KeySpace": "Space", + "KeyTab": "Tab", + "KeyBackSpace": "Geri tuşu", + "KeyInsert": "Insert", + "KeyDelete": "Delete", + "KeyPageUp": "Page Up", + "KeyPageDown": "Page Down", + "KeyHome": "Home", + "KeyEnd": "End", + "KeyCapsLock": "Caps Lock", + "KeyScrollLock": "Scroll Lock", + "KeyPrintScreen": "Print Screen", + "KeyPause": "Pause", + "KeyNumLock": "Num Lock", + "KeyClear": "Clear", + "KeyKeypad0": "Keypad 0", + "KeyKeypad1": "Keypad 1", + "KeyKeypad2": "Keypad 2", + "KeyKeypad3": "Keypad 3", + "KeyKeypad4": "Keypad 4", + "KeyKeypad5": "Keypad 5", + "KeyKeypad6": "Keypad 6", + "KeyKeypad7": "Keypad 7", + "KeyKeypad8": "Keypad 8", + "KeyKeypad9": "Keypad 9", + "KeyKeypadDivide": "Keypad Divide", + "KeyKeypadMultiply": "Keypad Multiply", + "KeyKeypadSubtract": "Keypad Subtract", + "KeyKeypadAdd": "Keypad Add", + "KeyKeypadDecimal": "Keypad Decimal", + "KeyKeypadEnter": "Keypad Enter", + "KeyNumber0": "0", + "KeyNumber1": "1", + "KeyNumber2": "2", + "KeyNumber3": "3", + "KeyNumber4": "4", + "KeyNumber5": "5", + "KeyNumber6": "6", + "KeyNumber7": "7", + "KeyNumber8": "8", + "KeyNumber9": "9", + "KeyTilde": "~", + "KeyGrave": "`", + "KeyMinus": "-", + "KeyPlus": "+", + "KeyBracketLeft": "[", + "KeyBracketRight": "]", + "KeySemicolon": ";", + "KeyQuote": "\"", + "KeyComma": ",", + "KeyPeriod": ".", + "KeySlash": "/", + "KeyBackSlash": "\\", + "KeyUnbound": "Unbound", + "GamepadLeftStick": "L Stick Button", + "GamepadRightStick": "R Stick Button", + "GamepadLeftShoulder": "Left Shoulder", + "GamepadRightShoulder": "Right Shoulder", + "GamepadLeftTrigger": "Left Trigger", + "GamepadRightTrigger": "Right Trigger", + "GamepadDpadUp": "Up", + "GamepadDpadDown": "Down", + "GamepadDpadLeft": "Left", + "GamepadDpadRight": "Sağ", + "GamepadMinus": "-", + "GamepadPlus": "4", + "GamepadGuide": "Rehber", + "GamepadMisc1": "Diğer", + "GamepadPaddle1": "Pedal 1", + "GamepadPaddle2": "Pedal 2", + "GamepadPaddle3": "Pedal 3", + "GamepadPaddle4": "Pedal 4", + "GamepadTouchpad": "Touchpad", + "GamepadSingleLeftTrigger0": "Sol Tetik 0", + "GamepadSingleRightTrigger0": "Sağ Tetik 0", + "GamepadSingleLeftTrigger1": "Sol Tetik 1", + "GamepadSingleRightTrigger1": "Sağ Tetik 1", + "StickLeft": "Sol Çubuk", + "StickRight": "Sağ çubuk", "UserProfilesSelectedUserProfile": "Seçili Kullanıcı Profili:", "UserProfilesSaveProfileName": "Profil İsmini Kaydet", "UserProfilesChangeProfileImage": "Profil Resmini Değiştir", @@ -297,9 +403,9 @@ "GameListContextMenuRunApplication": "Uygulamayı Çalıştır", "GameListContextMenuToggleFavorite": "Favori Ayarla", "GameListContextMenuToggleFavoriteToolTip": "Oyunu Favorilere Ekle/Çıkar", - "SettingsTabGeneralTheme": "Theme:", - "SettingsTabGeneralThemeDark": "Dark", - "SettingsTabGeneralThemeLight": "Light", + "SettingsTabGeneralTheme": "Tema:", + "SettingsTabGeneralThemeDark": "Karanlık", + "SettingsTabGeneralThemeLight": "Aydınlık", "ControllerSettingsConfigureGeneral": "Ayarla", "ControllerSettingsRumble": "Titreşim", "ControllerSettingsRumbleStrongMultiplier": "Güçlü Titreşim Çoklayıcı", @@ -384,10 +490,10 @@ "DialogUserProfileUnsavedChangesSubMessage": "Yaptığınız değişiklikleri iptal etmek istediğinize emin misiniz?", "DialogControllerSettingsModifiedConfirmMessage": "Geçerli kumanda seçenekleri güncellendi.", "DialogControllerSettingsModifiedConfirmSubMessage": "Kaydetmek istiyor musunuz?", - "DialogLoadFileErrorMessage": "{0}. Errored File: {1}", - "DialogModAlreadyExistsMessage": "Mod already exists", + "DialogLoadFileErrorMessage": "{0}. Hatalı Dosya: {1}", + "DialogModAlreadyExistsMessage": "Mod zaten var", "DialogModInvalidMessage": "The specified directory does not contain a mod!", - "DialogModDeleteNoParentMessage": "Failed to Delete: Could not find the parent directory for mod \"{0}\"!", + "DialogModDeleteNoParentMessage": "Silme Başarısız: \"{0}\" Modu için üst dizin bulunamadı! ", "DialogDlcNoDlcErrorMessage": "Belirtilen dosya seçilen oyun için DLC içermiyor!", "DialogPerformanceCheckLoggingEnabledMessage": "Sadece geliştiriler için dizayn edilen Trace Loglama seçeneği etkin.", "DialogPerformanceCheckLoggingEnabledConfirmMessage": "En iyi performans için trace loglama'nın devre dışı bırakılması tavsiye edilir. Trace loglama seçeneğini şimdi devre dışı bırakmak ister misiniz?", @@ -434,7 +540,7 @@ "DlcManagerRemoveAllButton": "Tümünü kaldır", "DlcManagerEnableAllButton": "Tümünü Aktif Et", "DlcManagerDisableAllButton": "Tümünü Devre Dışı Bırak", - "ModManagerDeleteAllButton": "Delete All", + "ModManagerDeleteAllButton": "Hepsini Sil", "MenuBarOptionsChangeLanguage": "Dili Değiştir", "MenuBarShowFileTypes": "Dosya Uzantılarını Göster", "CommonSort": "Sırala", @@ -509,8 +615,8 @@ "EnableInternetAccessTooltip": "Emüle edilen uygulamanın internete bağlanmasını sağlar.\n\nLAN modu bulunan oyunlar bu seçenek ile birbirine bağlanabilir ve sistemler aynı access point'e bağlanır. Bu gerçek konsolları da kapsar.\n\nNintendo sunucularına bağlanmayı sağlaMAZ. Internete bağlanmaya çalışan baz oyunların çökmesine sebep olabilr.\n\nEmin değilseniz devre dışı bırakın.", "GameListContextMenuManageCheatToolTip": "Hileleri yönetmeyi sağlar", "GameListContextMenuManageCheat": "Hileleri Yönet", - "GameListContextMenuManageModToolTip": "Manage Mods", - "GameListContextMenuManageMod": "Manage Mods", + "GameListContextMenuManageModToolTip": "Modları Yönet", + "GameListContextMenuManageMod": "Modları Yönet", "ControllerSettingsStickRange": "Menzil:", "DialogStopEmulationTitle": "Ryujinx - Emülasyonu Durdur", "DialogStopEmulationMessage": "Emülasyonu durdurmak istediğinizden emin misiniz?", @@ -593,15 +699,16 @@ "Writable": "Yazılabilir", "SelectDlcDialogTitle": "DLC dosyalarını seç", "SelectUpdateDialogTitle": "Güncelleme dosyalarını seç", - "SelectModDialogTitle": "Select mod directory", + "SelectModDialogTitle": "Mod Dizinini Seç", "UserProfileWindowTitle": "Kullanıcı Profillerini Yönet", "CheatWindowTitle": "Oyun Hilelerini Yönet", "DlcWindowTitle": "Oyun DLC'lerini Yönet", + "ModWindowTitle": "Manage Mods for {0} ({1})", "UpdateWindowTitle": "Oyun Güncellemelerini Yönet", "CheatWindowHeading": "{0} için Hile mevcut [{1}]", "BuildId": "BuildId:", "DlcWindowHeading": "{0} için DLC mevcut [{1}]", - "ModWindowHeading": "{0} Mod(s)", + "ModWindowHeading": "{0} Mod(lar)", "UserProfilesEditProfile": "Seçiliyi Düzenle", "Cancel": "İptal", "Save": "Kaydet", @@ -668,6 +775,6 @@ "SettingsTabNetworkMultiplayer": "Çok Oyunculu", "MultiplayerMode": "Mod:", "MultiplayerModeTooltip": "Change LDN multiplayer mode.\n\nLdnMitm will modify local wireless/local play functionality in games to function as if it were LAN, allowing for local, same-network connections with other Ryujinx instances and hacked Nintendo Switch consoles that have the ldn_mitm module installed.\n\nMultiplayer requires all players to be on the same game version (i.e. Super Smash Bros. Ultimate v13.0.1 can't connect to v13.0.0).\n\nLeave DISABLED if unsure.", - "MultiplayerModeDisabled": "Disabled", + "MultiplayerModeDisabled": "Devre Dışı", "MultiplayerModeLdnMitm": "ldn_mitm" } diff --git a/src/Ryujinx/Assets/Locales/uk_UA.json b/src/Ryujinx/Assets/Locales/uk_UA.json index dcf85eae9..976edfb1b 100644 --- a/src/Ryujinx/Assets/Locales/uk_UA.json +++ b/src/Ryujinx/Assets/Locales/uk_UA.json @@ -30,6 +30,10 @@ "MenuBarToolsManageFileTypes": "Керувати типами файлів", "MenuBarToolsInstallFileTypes": "Установити типи файлів", "MenuBarToolsUninstallFileTypes": "Видалити типи файлів", + "MenuBarView": "_View", + "MenuBarViewWindow": "Window Size", + "MenuBarViewWindow720": "720p", + "MenuBarViewWindow1080": "1080p", "MenuBarHelp": "_Допомога", "MenuBarHelpCheckForUpdates": "Перевірити оновлення", "MenuBarHelpAbout": "Про застосунок", @@ -92,6 +96,7 @@ "SettingsTabGeneralEnableDiscordRichPresence": "Увімкнути розширену присутність Discord", "SettingsTabGeneralCheckUpdatesOnLaunch": "Перевіряти наявність оновлень під час запуску", "SettingsTabGeneralShowConfirmExitDialog": "Показати діалогове вікно «Підтвердити вихід».", + "SettingsTabGeneralRememberWindowState": "Remember Window Size/Position", "SettingsTabGeneralHideCursor": "Сховати вказівник:", "SettingsTabGeneralHideCursorNever": "Ніколи", "SettingsTabGeneralHideCursorOnIdle": "Сховати у режимі очікування", @@ -266,6 +271,107 @@ "ControllerSettingsMotionGyroDeadzone": "Мертва зона гіроскопа:", "ControllerSettingsSave": "Зберегти", "ControllerSettingsClose": "Закрити", + "KeyUnknown": "Unknown", + "KeyShiftLeft": "Shift Left", + "KeyShiftRight": "Shift Right", + "KeyControlLeft": "Ctrl Left", + "KeyMacControlLeft": "⌃ Left", + "KeyControlRight": "Ctrl Right", + "KeyMacControlRight": "⌃ Right", + "KeyAltLeft": "Alt Left", + "KeyMacAltLeft": "⌥ Left", + "KeyAltRight": "Alt Right", + "KeyMacAltRight": "⌥ Right", + "KeyWinLeft": "⊞ Left", + "KeyMacWinLeft": "⌘ Left", + "KeyWinRight": "⊞ Right", + "KeyMacWinRight": "⌘ Right", + "KeyMenu": "Menu", + "KeyUp": "Up", + "KeyDown": "Down", + "KeyLeft": "Left", + "KeyRight": "Right", + "KeyEnter": "Enter", + "KeyEscape": "Escape", + "KeySpace": "Space", + "KeyTab": "Tab", + "KeyBackSpace": "Backspace", + "KeyInsert": "Insert", + "KeyDelete": "Delete", + "KeyPageUp": "Page Up", + "KeyPageDown": "Page Down", + "KeyHome": "Home", + "KeyEnd": "End", + "KeyCapsLock": "Caps Lock", + "KeyScrollLock": "Scroll Lock", + "KeyPrintScreen": "Print Screen", + "KeyPause": "Pause", + "KeyNumLock": "Num Lock", + "KeyClear": "Clear", + "KeyKeypad0": "Keypad 0", + "KeyKeypad1": "Keypad 1", + "KeyKeypad2": "Keypad 2", + "KeyKeypad3": "Keypad 3", + "KeyKeypad4": "Keypad 4", + "KeyKeypad5": "Keypad 5", + "KeyKeypad6": "Keypad 6", + "KeyKeypad7": "Keypad 7", + "KeyKeypad8": "Keypad 8", + "KeyKeypad9": "Keypad 9", + "KeyKeypadDivide": "Keypad Divide", + "KeyKeypadMultiply": "Keypad Multiply", + "KeyKeypadSubtract": "Keypad Subtract", + "KeyKeypadAdd": "Keypad Add", + "KeyKeypadDecimal": "Keypad Decimal", + "KeyKeypadEnter": "Keypad Enter", + "KeyNumber0": "0", + "KeyNumber1": "1", + "KeyNumber2": "2", + "KeyNumber3": "3", + "KeyNumber4": "4", + "KeyNumber5": "5", + "KeyNumber6": "6", + "KeyNumber7": "7", + "KeyNumber8": "8", + "KeyNumber9": "9", + "KeyTilde": "~", + "KeyGrave": "`", + "KeyMinus": "-", + "KeyPlus": "+", + "KeyBracketLeft": "[", + "KeyBracketRight": "]", + "KeySemicolon": ";", + "KeyQuote": "\"", + "KeyComma": ",", + "KeyPeriod": ".", + "KeySlash": "/", + "KeyBackSlash": "\\", + "KeyUnbound": "Unbound", + "GamepadLeftStick": "L Stick Button", + "GamepadRightStick": "R Stick Button", + "GamepadLeftShoulder": "Left Shoulder", + "GamepadRightShoulder": "Right Shoulder", + "GamepadLeftTrigger": "Left Trigger", + "GamepadRightTrigger": "Right Trigger", + "GamepadDpadUp": "Up", + "GamepadDpadDown": "Down", + "GamepadDpadLeft": "Left", + "GamepadDpadRight": "Right", + "GamepadMinus": "-", + "GamepadPlus": "+", + "GamepadGuide": "Guide", + "GamepadMisc1": "Misc", + "GamepadPaddle1": "Paddle 1", + "GamepadPaddle2": "Paddle 2", + "GamepadPaddle3": "Paddle 3", + "GamepadPaddle4": "Paddle 4", + "GamepadTouchpad": "Touchpad", + "GamepadSingleLeftTrigger0": "Left Trigger 0", + "GamepadSingleRightTrigger0": "Right Trigger 0", + "GamepadSingleLeftTrigger1": "Left Trigger 1", + "GamepadSingleRightTrigger1": "Right Trigger 1", + "StickLeft": "Left Stick", + "StickRight": "Right Stick", "UserProfilesSelectedUserProfile": "Вибраний профіль користувача:", "UserProfilesSaveProfileName": "Зберегти ім'я профілю", "UserProfilesChangeProfileImage": "Змінити зображення профілю", @@ -597,6 +703,7 @@ "UserProfileWindowTitle": "Менеджер профілів користувачів", "CheatWindowTitle": "Менеджер читів", "DlcWindowTitle": "Менеджер вмісту для завантаження", + "ModWindowTitle": "Керувати модами для {0} ({1})", "UpdateWindowTitle": "Менеджер оновлення назв", "CheatWindowHeading": "Коди доступні для {0} [{1}]", "BuildId": "ID збірки:", diff --git a/src/Ryujinx/Assets/Locales/zh_CN.json b/src/Ryujinx/Assets/Locales/zh_CN.json index cc1d583e1..66f59ecd0 100644 --- a/src/Ryujinx/Assets/Locales/zh_CN.json +++ b/src/Ryujinx/Assets/Locales/zh_CN.json @@ -8,29 +8,33 @@ "SettingsTabSystemMemoryManagerModeHost": "本机映射 (较快)", "SettingsTabSystemMemoryManagerModeHostUnchecked": "跳过检查的本机映射 (最快,但不安全)", "SettingsTabSystemUseHypervisor": "使用 Hypervisor 虚拟化", - "MenuBarFile": "文件", - "MenuBarFileOpenFromFile": "加载游戏文件", - "MenuBarFileOpenUnpacked": "加载解包后的游戏", + "MenuBarFile": "文件(_F)", + "MenuBarFileOpenFromFile": "加载游戏文件(_L)", + "MenuBarFileOpenUnpacked": "加载解包后的游戏(_U)", "MenuBarFileOpenEmuFolder": "打开 Ryujinx 系统目录", "MenuBarFileOpenLogsFolder": "打开日志目录", - "MenuBarFileExit": "退出", - "MenuBarOptions": "选项", + "MenuBarFileExit": "退出(_E)", + "MenuBarOptions": "选项(_O)", "MenuBarOptionsToggleFullscreen": "切换全屏", "MenuBarOptionsStartGamesInFullscreen": "全屏模式启动游戏", "MenuBarOptionsStopEmulation": "停止模拟", - "MenuBarOptionsSettings": "设置", - "MenuBarOptionsManageUserProfiles": "管理用户账户", - "MenuBarActions": "操作", + "MenuBarOptionsSettings": "设置(_S)", + "MenuBarOptionsManageUserProfiles": "管理用户账户(_M)", + "MenuBarActions": "操作(_A)", "MenuBarOptionsSimulateWakeUpMessage": "模拟唤醒消息", "MenuBarActionsScanAmiibo": "扫描 Amiibo", - "MenuBarTools": "工具", + "MenuBarTools": "工具(_T)", "MenuBarToolsInstallFirmware": "安装系统固件", "MenuBarFileToolsInstallFirmwareFromFile": "从 XCI 或 ZIP 文件中安装系统固件", "MenuBarFileToolsInstallFirmwareFromDirectory": "从文件夹中安装系统固件", "MenuBarToolsManageFileTypes": "管理文件扩展名", "MenuBarToolsInstallFileTypes": "关联文件扩展名", "MenuBarToolsUninstallFileTypes": "取消关联扩展名", - "MenuBarHelp": "帮助", + "MenuBarView": "视图(_V)", + "MenuBarViewWindow": "窗口大小", + "MenuBarViewWindow720": "720p", + "MenuBarViewWindow1080": "1080p", + "MenuBarHelp": "帮助(_H)", "MenuBarHelpCheckForUpdates": "检查更新", "MenuBarHelpAbout": "关于", "MenuSearch": "搜索…", @@ -92,6 +96,7 @@ "SettingsTabGeneralEnableDiscordRichPresence": "启用 Discord 在线状态展示", "SettingsTabGeneralCheckUpdatesOnLaunch": "启动时检查更新", "SettingsTabGeneralShowConfirmExitDialog": "退出游戏时需要确认", + "SettingsTabGeneralRememberWindowState": "记住窗口大小和位置", "SettingsTabGeneralHideCursor": "隐藏鼠标指针:", "SettingsTabGeneralHideCursorNever": "从不隐藏", "SettingsTabGeneralHideCursorOnIdle": "自动隐藏", @@ -266,6 +271,107 @@ "ControllerSettingsMotionGyroDeadzone": "陀螺仪死区:", "ControllerSettingsSave": "保存", "ControllerSettingsClose": "关闭", + "KeyUnknown": "未知", + "KeyShiftLeft": "左侧Shift", + "KeyShiftRight": "右侧Shift", + "KeyControlLeft": "左侧Ctrl", + "KeyMacControlLeft": "左侧⌃", + "KeyControlRight": "右侧Ctrl", + "KeyMacControlRight": "右侧⌃", + "KeyAltLeft": "左侧Alt", + "KeyMacAltLeft": "左侧⌥", + "KeyAltRight": "右侧Alt", + "KeyMacAltRight": "右侧⌥", + "KeyWinLeft": "左侧⊞", + "KeyMacWinLeft": "左侧⌘", + "KeyWinRight": "右侧⊞", + "KeyMacWinRight": "右侧⌘", + "KeyMenu": "菜单键", + "KeyUp": "上", + "KeyDown": "下", + "KeyLeft": "左", + "KeyRight": "右", + "KeyEnter": "回车键", + "KeyEscape": "Esc", + "KeySpace": "空格键", + "KeyTab": "Tab", + "KeyBackSpace": "退格键", + "KeyInsert": "Insert", + "KeyDelete": "Delete", + "KeyPageUp": "Page Up", + "KeyPageDown": "Page Down", + "KeyHome": "Home", + "KeyEnd": "End", + "KeyCapsLock": "Caps Lock", + "KeyScrollLock": "Scroll Lock", + "KeyPrintScreen": "Print Screen", + "KeyPause": "Pause", + "KeyNumLock": "Num Lock", + "KeyClear": "清除键", + "KeyKeypad0": "小键盘0", + "KeyKeypad1": "小键盘1", + "KeyKeypad2": "小键盘2", + "KeyKeypad3": "小键盘3", + "KeyKeypad4": "小键盘4", + "KeyKeypad5": "小键盘5", + "KeyKeypad6": "小键盘6", + "KeyKeypad7": "小键盘7", + "KeyKeypad8": "小键盘8", + "KeyKeypad9": "小键盘9", + "KeyKeypadDivide": "小键盘/", + "KeyKeypadMultiply": "小键盘*", + "KeyKeypadSubtract": "小键盘-", + "KeyKeypadAdd": "小键盘+", + "KeyKeypadDecimal": "小键盘.", + "KeyKeypadEnter": "小键盘回车键", + "KeyNumber0": "0", + "KeyNumber1": "1", + "KeyNumber2": "2", + "KeyNumber3": "3", + "KeyNumber4": "4", + "KeyNumber5": "5", + "KeyNumber6": "6", + "KeyNumber7": "7", + "KeyNumber8": "8", + "KeyNumber9": "9", + "KeyTilde": "~", + "KeyGrave": "`", + "KeyMinus": "-", + "KeyPlus": "+", + "KeyBracketLeft": "[", + "KeyBracketRight": "]", + "KeySemicolon": ";", + "KeyQuote": "\"", + "KeyComma": ",", + "KeyPeriod": ".", + "KeySlash": "/", + "KeyBackSlash": "\\", + "KeyUnbound": "未分配", + "GamepadLeftStick": "左摇杆按键", + "GamepadRightStick": "右摇杆按键", + "GamepadLeftShoulder": "左肩键L", + "GamepadRightShoulder": "右肩键R", + "GamepadLeftTrigger": "左扳机键ZL", + "GamepadRightTrigger": "右扳机键ZR", + "GamepadDpadUp": "上键", + "GamepadDpadDown": "下键", + "GamepadDpadLeft": "左键", + "GamepadDpadRight": "右键", + "GamepadMinus": "-键", + "GamepadPlus": "+键", + "GamepadGuide": "主页键", + "GamepadMisc1": "截图键", + "GamepadPaddle1": "其他按键1", + "GamepadPaddle2": "其他按键2", + "GamepadPaddle3": "其他按键3", + "GamepadPaddle4": "其他按键4", + "GamepadTouchpad": "触摸板", + "GamepadSingleLeftTrigger0": "左扳机0", + "GamepadSingleRightTrigger0": "右扳机0", + "GamepadSingleLeftTrigger1": "左扳机1", + "GamepadSingleRightTrigger1": "右扳机1", + "StickLeft": "左摇杆", + "StickRight": "右摇杆", "UserProfilesSelectedUserProfile": "选定的用户账户:", "UserProfilesSaveProfileName": "保存名称", "UserProfilesChangeProfileImage": "更换头像", @@ -597,6 +703,7 @@ "UserProfileWindowTitle": "管理用户账户", "CheatWindowTitle": "金手指管理器", "DlcWindowTitle": "管理 {0} ({1}) 的 DLC", + "ModWindowTitle": "管理 {0} ({1}) 的 MOD", "UpdateWindowTitle": "游戏更新管理器", "CheatWindowHeading": "适用于 {0} [{1}] 的金手指", "BuildId": "游戏版本 ID:", diff --git a/src/Ryujinx/Assets/Locales/zh_TW.json b/src/Ryujinx/Assets/Locales/zh_TW.json index 301a55856..fc838d251 100644 --- a/src/Ryujinx/Assets/Locales/zh_TW.json +++ b/src/Ryujinx/Assets/Locales/zh_TW.json @@ -10,7 +10,7 @@ "SettingsTabSystemUseHypervisor": "使用 Hypervisor", "MenuBarFile": "檔案(_F)", "MenuBarFileOpenFromFile": "從檔案載入應用程式(_L)", - "MenuBarFileOpenUnpacked": "載入解開封裝的遊戲(_U)", + "MenuBarFileOpenUnpacked": "載入未封裝的遊戲(_U)", "MenuBarFileOpenEmuFolder": "開啟 Ryujinx 資料夾", "MenuBarFileOpenLogsFolder": "開啟日誌資料夾", "MenuBarFileExit": "結束(_E)", @@ -30,6 +30,10 @@ "MenuBarToolsManageFileTypes": "管理檔案類型", "MenuBarToolsInstallFileTypes": "安裝檔案類型", "MenuBarToolsUninstallFileTypes": "移除檔案類型", + "MenuBarView": "檢視(_V)", + "MenuBarViewWindow": "視窗大小", + "MenuBarViewWindow720": "720p", + "MenuBarViewWindow1080": "1080p", "MenuBarHelp": "說明(_H)", "MenuBarHelpCheckForUpdates": "檢查更新", "MenuBarHelpAbout": "關於", @@ -92,6 +96,7 @@ "SettingsTabGeneralEnableDiscordRichPresence": "啟用 Discord 動態狀態展示", "SettingsTabGeneralCheckUpdatesOnLaunch": "啟動時檢查更新", "SettingsTabGeneralShowConfirmExitDialog": "顯示「確認結束」對話方塊", + "SettingsTabGeneralRememberWindowState": "記住視窗大小/位置", "SettingsTabGeneralHideCursor": "隱藏滑鼠游標:", "SettingsTabGeneralHideCursorNever": "從不", "SettingsTabGeneralHideCursorOnIdle": "閒置時", @@ -144,13 +149,13 @@ "SettingsTabGraphics": "圖形", "SettingsTabGraphicsAPI": "圖形 API", "SettingsTabGraphicsEnableShaderCache": "啟用著色器快取", - "SettingsTabGraphicsAnisotropicFiltering": "各向異性過濾:", + "SettingsTabGraphicsAnisotropicFiltering": "各向異性過濾:", "SettingsTabGraphicsAnisotropicFilteringAuto": "自動", "SettingsTabGraphicsAnisotropicFiltering2x": "2 倍", "SettingsTabGraphicsAnisotropicFiltering4x": "4 倍", "SettingsTabGraphicsAnisotropicFiltering8x": "8 倍", "SettingsTabGraphicsAnisotropicFiltering16x": "16 倍", - "SettingsTabGraphicsResolutionScale": "解析度比例:", + "SettingsTabGraphicsResolutionScale": "解析度比例:", "SettingsTabGraphicsResolutionScaleCustom": "自訂 (不建議使用)", "SettingsTabGraphicsResolutionScaleNative": "原生 (720p/1080p)", "SettingsTabGraphicsResolutionScale2x": "2 倍 (1440p/2160p)", @@ -164,7 +169,7 @@ "SettingsTabGraphicsAspectRatio32x9": "32:9", "SettingsTabGraphicsAspectRatioStretch": "拉伸以適應視窗", "SettingsTabGraphicsDeveloperOptions": "開發者選項", - "SettingsTabGraphicsShaderDumpPath": "圖形著色器傾印路徑:", + "SettingsTabGraphicsShaderDumpPath": "圖形著色器傾印路徑:", "SettingsTabLogging": "日誌", "SettingsTabLoggingLogging": "日誌", "SettingsTabLoggingEnableLoggingToFile": "啟用日誌到檔案", @@ -252,8 +257,8 @@ "ControllerSettingsLeftSR": "SR", "ControllerSettingsRightSL": "SL", "ControllerSettingsRightSR": "SR", - "ControllerSettingsExtraButtonsLeft": "左按鍵", - "ControllerSettingsExtraButtonsRight": "右按鍵", + "ControllerSettingsExtraButtonsLeft": "左背鍵", + "ControllerSettingsExtraButtonsRight": "右背鍵", "ControllerSettingsMisc": "其他", "ControllerSettingsTriggerThreshold": "扳機閾值:", "ControllerSettingsMotion": "體感", @@ -266,6 +271,107 @@ "ControllerSettingsMotionGyroDeadzone": "陀螺儀無感帶:", "ControllerSettingsSave": "儲存", "ControllerSettingsClose": "關閉", + "KeyUnknown": "未知", + "KeyShiftLeft": "左 Shift", + "KeyShiftRight": "右 Shift", + "KeyControlLeft": "左 Ctrl", + "KeyMacControlLeft": "左 ⌃", + "KeyControlRight": "右 Ctrl", + "KeyMacControlRight": "右 ⌃", + "KeyAltLeft": "左 Alt", + "KeyMacAltLeft": "左 ⌥", + "KeyAltRight": "右 Alt", + "KeyMacAltRight": "右 ⌥", + "KeyWinLeft": "左 ⊞", + "KeyMacWinLeft": "左 ⌘", + "KeyWinRight": "右 ⊞", + "KeyMacWinRight": "右 ⌘", + "KeyMenu": "功能表", + "KeyUp": "上", + "KeyDown": "下", + "KeyLeft": "左", + "KeyRight": "右", + "KeyEnter": "Enter 鍵", + "KeyEscape": "Esc 鍵", + "KeySpace": "空白鍵", + "KeyTab": "Tab 鍵", + "KeyBackSpace": "Backspace 鍵", + "KeyInsert": "Insert 鍵", + "KeyDelete": "Delete 鍵", + "KeyPageUp": "向上捲頁鍵", + "KeyPageDown": "向下捲頁鍵", + "KeyHome": "Home 鍵", + "KeyEnd": "End 鍵", + "KeyCapsLock": "Caps Lock 鍵", + "KeyScrollLock": "Scroll Lock 鍵", + "KeyPrintScreen": "Print Screen 鍵", + "KeyPause": "Pause 鍵", + "KeyNumLock": "Num Lock 鍵", + "KeyClear": "清除", + "KeyKeypad0": "數字鍵 0", + "KeyKeypad1": "數字鍵 1", + "KeyKeypad2": "數字鍵 2", + "KeyKeypad3": "數字鍵 3", + "KeyKeypad4": "數字鍵 4", + "KeyKeypad5": "數字鍵 5", + "KeyKeypad6": "數字鍵 6", + "KeyKeypad7": "數字鍵 7", + "KeyKeypad8": "數字鍵 8", + "KeyKeypad9": "數字鍵 9", + "KeyKeypadDivide": "數字鍵除號", + "KeyKeypadMultiply": "數字鍵乘號", + "KeyKeypadSubtract": "數字鍵減號", + "KeyKeypadAdd": "數字鍵加號", + "KeyKeypadDecimal": "數字鍵小數點", + "KeyKeypadEnter": "數字鍵 Enter", + "KeyNumber0": "0", + "KeyNumber1": "1", + "KeyNumber2": "2", + "KeyNumber3": "3", + "KeyNumber4": "4", + "KeyNumber5": "5", + "KeyNumber6": "6", + "KeyNumber7": "7", + "KeyNumber8": "8", + "KeyNumber9": "9", + "KeyTilde": "~", + "KeyGrave": "`", + "KeyMinus": "-", + "KeyPlus": "+", + "KeyBracketLeft": "[", + "KeyBracketRight": "]", + "KeySemicolon": ";", + "KeyQuote": "\"", + "KeyComma": ",", + "KeyPeriod": ".", + "KeySlash": "/", + "KeyBackSlash": "\\", + "KeyUnbound": "未分配", + "GamepadLeftStick": "左搖桿按鍵", + "GamepadRightStick": "右搖桿按鍵", + "GamepadLeftShoulder": "左肩鍵", + "GamepadRightShoulder": "右肩鍵", + "GamepadLeftTrigger": "左扳機", + "GamepadRightTrigger": "右扳機", + "GamepadDpadUp": "上", + "GamepadDpadDown": "下", + "GamepadDpadLeft": "左", + "GamepadDpadRight": "右", + "GamepadMinus": "-", + "GamepadPlus": "+", + "GamepadGuide": "快顯功能表鍵", + "GamepadMisc1": "其他按鍵", + "GamepadPaddle1": "其他按鍵 1", + "GamepadPaddle2": "其他按鍵 2", + "GamepadPaddle3": "其他按鍵 3", + "GamepadPaddle4": "其他按鍵 4", + "GamepadTouchpad": "觸控板", + "GamepadSingleLeftTrigger0": "左扳機 0", + "GamepadSingleRightTrigger0": "右扳機 0", + "GamepadSingleLeftTrigger1": "左扳機 1", + "GamepadSingleRightTrigger1": "右扳機 1", + "StickLeft": "左搖桿", + "StickRight": "右搖桿", "UserProfilesSelectedUserProfile": "選取的使用者設定檔:", "UserProfilesSaveProfileName": "儲存設定檔名稱", "UserProfilesChangeProfileImage": "變更設定檔圖像", @@ -490,7 +596,7 @@ "OpenGlLogLevel": "需要啟用適當的日誌等級", "DebugLogTooltip": "在控制台中輸出偵錯日誌訊息。\n\n只有在人員特別指示的情況下才能使用,因為這會導致日誌難以閱讀,並降低模擬器效能。", "LoadApplicationFileTooltip": "開啟檔案總管,選擇與 Switch 相容的檔案來載入", - "LoadApplicationFolderTooltip": "開啟檔案總管,選擇與 Switch 相容且解開封裝的應用程式來載入", + "LoadApplicationFolderTooltip": "開啟檔案總管,選擇與 Switch 相容且未封裝的應用程式來載入", "OpenRyujinxFolderTooltip": "開啟 Ryujinx 檔案系統資料夾", "OpenRyujinxLogsTooltip": "開啟日誌被寫入的資料夾", "ExitTooltip": "結束 Ryujinx", @@ -550,7 +656,7 @@ "SwkbdMinRangeCharacters": "長度必須為 {0} 到 {1} 個字元", "SoftwareKeyboard": "軟體鍵盤", "SoftwareKeyboardModeNumeric": "必須是 0 到 9 或「.」", - "SoftwareKeyboardModeAlphabet": "必須是非「中日韓字元」 (non CJK)", + "SoftwareKeyboardModeAlphabet": "必須是「非中日韓字元」 (non CJK)", "SoftwareKeyboardModeASCII": "必須是 ASCII 文字", "ControllerAppletControllers": "支援的控制器:", "ControllerAppletPlayers": "玩家:", @@ -572,7 +678,7 @@ "CompilingShaders": "正在編譯著色器", "AllKeyboards": "所有鍵盤", "OpenFileDialogTitle": "選取支援的檔案格式", - "OpenFolderDialogTitle": "選取解開封裝遊戲的資料夾", + "OpenFolderDialogTitle": "選取未封裝遊戲的資料夾", "AllSupportedFormats": "所有支援的格式", "RyujinxUpdater": "Ryujinx 更新程式", "SettingsTabHotkeys": "鍵盤快速鍵", @@ -597,6 +703,7 @@ "UserProfileWindowTitle": "使用者設定檔管理員", "CheatWindowTitle": "密技管理員", "DlcWindowTitle": "管理 {0} 的可下載內容 ({1})", + "ModWindowTitle": "管理 {0} 的模組 ({1})", "UpdateWindowTitle": "遊戲更新管理員", "CheatWindowHeading": "可用於 {0} [{1}] 的密技", "BuildId": "組建識別碼:", @@ -610,7 +717,7 @@ "UserProfilesSetProfileImage": "設定設定檔圖像", "UserProfileEmptyNameError": "名稱為必填", "UserProfileNoImageError": "必須設定設定檔圖像", - "GameUpdateWindowHeading": "可用於 {0} ({1}) 的更新", + "GameUpdateWindowHeading": "管理 {0} 的更新 ({1})", "SettingsTabHotkeysResScaleUpHotkey": "提高解析度:", "SettingsTabHotkeysResScaleDownHotkey": "降低解析度:", "UserProfilesName": "名稱:", @@ -635,7 +742,7 @@ "UserProfilesManageSaves": "管理存檔", "DeleteUserSave": "您想刪除此遊戲的使用者存檔嗎?", "IrreversibleActionNote": "此動作將無法復原。", - "SaveManagerHeading": "管理 {0} 的存檔", + "SaveManagerHeading": "管理 {0} 的存檔 ({1})", "SaveManagerTitle": "存檔管理員", "Name": "名稱", "Size": "大小", @@ -647,9 +754,9 @@ "GraphicsAATooltip": "對遊戲繪製進行反鋸齒處理。\n\nFXAA 會模糊大部分圖像,而 SMAA 則會嘗試找出鋸齒邊緣並將其平滑化。\n\n不建議與 FSR 縮放濾鏡一起使用。\n\n此選項可在遊戲執行時透過點選下方的「套用」進行變更;您只需將設定視窗移到一旁,然後進行試驗,直到找到您喜歡的遊戲效果。\n\n如果不確定,請選擇無狀態。", "GraphicsAALabel": "反鋸齒:", "GraphicsScalingFilterLabel": "縮放過濾器:", - "GraphicsScalingFilterTooltip": "選擇使用解析度縮放時套用的縮放過濾器。\n\nBilinear (雙線性) 濾鏡適用於 3D 遊戲,是一個安全的預設選項。\n\n建議像素美術遊戲使用 Nearest (最近性) 濾鏡。\n\nFSR 1.0 只是一個銳化濾鏡,不建議與 FXAA 或 SMAA 一起使用。\n\n此選項可在遊戲執行時透過點選下方的「套用」進行變更;您只需將設定視窗移到一旁,然後進行試驗,直到找到您喜歡的遊戲效果。\n\n如果不確定,請保持 Bilinear (雙線) 狀態。", - "GraphicsScalingFilterBilinear": "Bilinear", - "GraphicsScalingFilterNearest": "Nearest", + "GraphicsScalingFilterTooltip": "選擇使用解析度縮放時套用的縮放過濾器。\n\n雙線性 (Bilinear) 濾鏡適用於 3D 遊戲,是一個安全的預設選項。\n\n建議像素美術遊戲使用近鄰性 (Nearest) 濾鏡。\n\nFSR 1.0 只是一個銳化濾鏡,不建議與 FXAA 或 SMAA 一起使用。\n\n此選項可在遊戲執行時透過點選下方的「套用」進行變更;您只需將設定視窗移到一旁,然後進行試驗,直到找到您喜歡的遊戲效果。\n\n如果不確定,請保持雙線性 (Bilinear) 狀態。", + "GraphicsScalingFilterBilinear": "雙線性 (Bilinear)", + "GraphicsScalingFilterNearest": "近鄰性 (Nearest)", "GraphicsScalingFilterFsr": "FSR", "GraphicsScalingFilterLevelLabel": "日誌等級", "GraphicsScalingFilterLevelTooltip": "設定 FSR 1.0 銳化等級。越高越清晰。", @@ -662,12 +769,12 @@ "SettingsTabNetworkInterface": "網路介面:", "NetworkInterfaceTooltip": "用於 LAN/LDN 功能的網路介面。\n\n與 VPN 或 XLink Kai 以及支援區域網路的遊戲配合使用,可用於在網路上偽造同網際網路連線。\n\n如果不確定,請保持預設狀態。", "NetworkInterfaceDefault": "預設", - "PackagingShaders": "著色器封裝", + "PackagingShaders": "封裝著色器", "AboutChangelogButton": "在 GitHub 上檢視更新日誌", "AboutChangelogButtonTooltipMessage": "在預設瀏覽器中開啟此版本的更新日誌。", "SettingsTabNetworkMultiplayer": "多人遊戲", "MultiplayerMode": "模式:", "MultiplayerModeTooltip": "變更 LDN 多人遊戲模式。\n\nLdnMitm 將修改遊戲中的本機無線/本機遊戲功能,使其如同區域網路一樣執行,允許與其他安裝了 ldn_mitm 模組的 Ryujinx 實例和已破解的 Nintendo Switch 遊戲機進行本機同網路連線。\n\n多人遊戲要求所有玩家使用相同的遊戲版本 (例如,Super Smash Bros. Ultimate v13.0.1 無法連接 v13.0.0)。\n\n如果不確定,請保持 Disabled (停用) 狀態。", - "MultiplayerModeDisabled": "Disabled", + "MultiplayerModeDisabled": "已停用", "MultiplayerModeLdnMitm": "ldn_mitm" } diff --git a/src/Ryujinx/Assets/Styles/Styles.xaml b/src/Ryujinx/Assets/Styles/Styles.xaml index f7f64be22..b3a6f59c8 100644 --- a/src/Ryujinx/Assets/Styles/Styles.xaml +++ b/src/Ryujinx/Assets/Styles/Styles.xaml @@ -15,8 +15,7 @@ - + @@ -393,4 +392,4 @@ 600 756 - \ No newline at end of file + diff --git a/src/Ryujinx/Common/ThemeManager.cs b/src/Ryujinx/Common/ThemeManager.cs new file mode 100644 index 000000000..8c52c2a66 --- /dev/null +++ b/src/Ryujinx/Common/ThemeManager.cs @@ -0,0 +1,14 @@ +using System; + +namespace Ryujinx.Ava.Common +{ + public static class ThemeManager + { + public static event EventHandler ThemeChanged; + + public static void OnThemeChanged() + { + ThemeChanged?.Invoke(null, EventArgs.Empty); + } + } +} diff --git a/src/Ryujinx/Input/AvaloniaKeyboard.cs b/src/Ryujinx/Input/AvaloniaKeyboard.cs index fbaaaabab..ff88de79e 100644 --- a/src/Ryujinx/Input/AvaloniaKeyboard.cs +++ b/src/Ryujinx/Input/AvaloniaKeyboard.cs @@ -195,7 +195,7 @@ namespace Ryujinx.Ava.Input public void Clear() { - _driver?.ResetKeys(); + _driver?.Clear(); } public void Dispose() { } diff --git a/src/Ryujinx/Input/AvaloniaKeyboardDriver.cs b/src/Ryujinx/Input/AvaloniaKeyboardDriver.cs index e9e71b99b..9f87e821a 100644 --- a/src/Ryujinx/Input/AvaloniaKeyboardDriver.cs +++ b/src/Ryujinx/Input/AvaloniaKeyboardDriver.cs @@ -94,7 +94,7 @@ namespace Ryujinx.Ava.Input return _pressedKeys.Contains(nativeKey); } - public void ResetKeys() + public void Clear() { _pressedKeys.Clear(); } diff --git a/src/Ryujinx/Program.cs b/src/Ryujinx/Program.cs index 89e895e81..4f68ca24f 100644 --- a/src/Ryujinx/Program.cs +++ b/src/Ryujinx/Program.cs @@ -40,7 +40,7 @@ namespace Ryujinx.Ava if (OperatingSystem.IsWindows() && !OperatingSystem.IsWindowsVersionAtLeast(10, 0, 17134)) { - _ = MessageBoxA(IntPtr.Zero, "You are running an outdated version of Windows.\n\nStarting on June 1st 2022, Ryujinx will only support Windows 10 1803 and newer.\n", $"Ryujinx {Version}", MbIconwarning); + _ = MessageBoxA(IntPtr.Zero, "You are running an outdated version of Windows.\n\nRyujinx supports Windows 10 version 1803 and newer.\n", $"Ryujinx {Version}", MbIconwarning); } PreviewerDetached = true; @@ -148,21 +148,24 @@ namespace Ryujinx.Ava { // No configuration, we load the default values and save it to disk ConfigurationPath = appDataConfigurationPath; + Logger.Notice.Print(LogClass.Application, $"No configuration file found. Saving default configuration to: {ConfigurationPath}"); ConfigurationState.Instance.LoadDefault(); ConfigurationState.Instance.ToFileFormat().SaveConfig(ConfigurationPath); } else { + Logger.Notice.Print(LogClass.Application, $"Loading configuration from: {ConfigurationPath}"); + if (ConfigurationFileFormat.TryLoad(ConfigurationPath, out ConfigurationFileFormat configurationFileFormat)) { ConfigurationState.Instance.Load(configurationFileFormat, ConfigurationPath); } else { - ConfigurationState.Instance.LoadDefault(); + Logger.Warning?.PrintMsg(LogClass.Application, $"Failed to load config! Loading the default config instead.\nFailed config location: {ConfigurationPath}"); - Logger.Warning?.PrintMsg(LogClass.Application, $"Failed to load config! Loading the default config instead.\nFailed config location {ConfigurationPath}"); + ConfigurationState.Instance.LoadDefault(); } } diff --git a/src/Ryujinx/Ryujinx.csproj b/src/Ryujinx/Ryujinx.csproj index 38453f2c6..a43f50063 100644 --- a/src/Ryujinx/Ryujinx.csproj +++ b/src/Ryujinx/Ryujinx.csproj @@ -70,7 +70,8 @@ - + @@ -88,7 +89,7 @@ - + Always diff --git a/src/Ryujinx/UI/Applet/AvaHostUIHandler.cs b/src/Ryujinx/UI/Applet/AvaHostUIHandler.cs index 4bcc35a7a..4bcf8eb94 100644 --- a/src/Ryujinx/UI/Applet/AvaHostUIHandler.cs +++ b/src/Ryujinx/UI/Applet/AvaHostUIHandler.cs @@ -122,6 +122,7 @@ namespace Ryujinx.Ava.UI.Applet { try { + _parent.ViewModel.AppHost.NpadManager.BlockInputUpdates(); var response = await SwkbdAppletDialog.ShowInputDialog(LocaleManager.Instance[LocaleKeys.SoftwareKeyboard], args); if (response.Result == UserResult.Ok) @@ -143,6 +144,7 @@ namespace Ryujinx.Ava.UI.Applet }); dialogCloseEvent.WaitOne(); + _parent.ViewModel.AppHost.NpadManager.UnblockInputUpdates(); userText = error ? null : inputText; diff --git a/src/Ryujinx/UI/Applet/ControllerAppletDialog.axaml.cs b/src/Ryujinx/UI/Applet/ControllerAppletDialog.axaml.cs index 5a98b1645..6b999b1f4 100644 --- a/src/Ryujinx/UI/Applet/ControllerAppletDialog.axaml.cs +++ b/src/Ryujinx/UI/Applet/ControllerAppletDialog.axaml.cs @@ -9,7 +9,6 @@ using Ryujinx.Ava.UI.Windows; using Ryujinx.Common; using Ryujinx.HLE.HOS.Applets; using Ryujinx.HLE.HOS.Services.Hid; -using System; using System.Linq; using System.Threading.Tasks; @@ -104,9 +103,7 @@ namespace Ryujinx.Ava.UI.Applet if (!string.IsNullOrWhiteSpace(path)) { - SvgSource source = new(default(Uri)); - - source.Load(EmbeddedResources.GetStream(path)); + SvgSource source = SvgSource.LoadFromStream(EmbeddedResources.GetStream(path)); image.Source = source; } diff --git a/src/Ryujinx/UI/Helpers/ButtonKeyAssigner.cs b/src/Ryujinx/UI/Helpers/ButtonKeyAssigner.cs index 7e8ba7342..4f2b8daae 100644 --- a/src/Ryujinx/UI/Helpers/ButtonKeyAssigner.cs +++ b/src/Ryujinx/UI/Helpers/ButtonKeyAssigner.cs @@ -1,11 +1,8 @@ -using Avalonia.Controls; using Avalonia.Controls.Primitives; -using Avalonia.LogicalTree; using Avalonia.Threading; using Ryujinx.Input; using Ryujinx.Input.Assigner; using System; -using System.Linq; using System.Threading.Tasks; namespace Ryujinx.Ava.UI.Helpers @@ -15,12 +12,12 @@ namespace Ryujinx.Ava.UI.Helpers internal class ButtonAssignedEventArgs : EventArgs { public ToggleButton Button { get; } - public bool IsAssigned { get; } + public Button? ButtonValue { get; } - public ButtonAssignedEventArgs(ToggleButton button, bool isAssigned) + public ButtonAssignedEventArgs(ToggleButton button, Button? buttonValue) { Button = button; - IsAssigned = isAssigned; + ButtonValue = buttonValue; } } @@ -69,7 +66,7 @@ namespace Ryujinx.Ava.UI.Helpers assigner.ReadInput(); - if (assigner.HasAnyButtonPressed() || assigner.ShouldCancel() || (keyboard != null && keyboard.IsPressed(Key.Escape))) + if (assigner.IsAnyButtonPressed() || assigner.ShouldCancel() || (keyboard != null && keyboard.IsPressed(Key.Escape))) { break; } @@ -78,15 +75,11 @@ namespace Ryujinx.Ava.UI.Helpers await Dispatcher.UIThread.InvokeAsync(() => { - string pressedButton = assigner.GetPressedButton(); + Button? pressedButton = assigner.GetPressedButton(); if (_shouldUnbind) { - SetButtonText(ToggledButton, "Unbound"); - } - else if (pressedButton != "") - { - SetButtonText(ToggledButton, pressedButton); + pressedButton = null; } _shouldUnbind = false; @@ -94,17 +87,8 @@ namespace Ryujinx.Ava.UI.Helpers ToggledButton.IsChecked = false; - ButtonAssigned?.Invoke(this, new ButtonAssignedEventArgs(ToggledButton, pressedButton != null)); + ButtonAssigned?.Invoke(this, new ButtonAssignedEventArgs(ToggledButton, pressedButton)); - static void SetButtonText(ToggleButton button, string text) - { - ILogical textBlock = button.GetLogicalDescendants().First(x => x is TextBlock); - - if (textBlock != null && textBlock is TextBlock block) - { - block.Text = text; - } - } }); } diff --git a/src/Ryujinx/UI/Helpers/KeyValueConverter.cs b/src/Ryujinx/UI/Helpers/KeyValueConverter.cs index 028ed6bf4..5b0d6ee1c 100644 --- a/src/Ryujinx/UI/Helpers/KeyValueConverter.cs +++ b/src/Ryujinx/UI/Helpers/KeyValueConverter.cs @@ -1,7 +1,9 @@ using Avalonia.Data.Converters; +using Ryujinx.Ava.Common.Locale; using Ryujinx.Common.Configuration.Hid; using Ryujinx.Common.Configuration.Hid.Controller; using System; +using System.Collections.Generic; using System.Globalization; namespace Ryujinx.Ava.UI.Helpers @@ -10,37 +12,173 @@ namespace Ryujinx.Ava.UI.Helpers { public static KeyValueConverter Instance = new(); + private static readonly Dictionary _keysMap = new() + { + { Key.Unknown, LocaleKeys.KeyUnknown }, + { Key.ShiftLeft, LocaleKeys.KeyShiftLeft }, + { Key.ShiftRight, LocaleKeys.KeyShiftRight }, + { Key.ControlLeft, LocaleKeys.KeyControlLeft }, + { Key.ControlRight, LocaleKeys.KeyControlRight }, + { Key.AltLeft, LocaleKeys.KeyAltLeft }, + { Key.AltRight, LocaleKeys.KeyAltRight }, + { Key.WinLeft, LocaleKeys.KeyWinLeft }, + { Key.WinRight, LocaleKeys.KeyWinRight }, + { Key.Up, LocaleKeys.KeyUp }, + { Key.Down, LocaleKeys.KeyDown }, + { Key.Left, LocaleKeys.KeyLeft }, + { Key.Right, LocaleKeys.KeyRight }, + { Key.Enter, LocaleKeys.KeyEnter }, + { Key.Escape, LocaleKeys.KeyEscape }, + { Key.Space, LocaleKeys.KeySpace }, + { Key.Tab, LocaleKeys.KeyTab }, + { Key.BackSpace, LocaleKeys.KeyBackSpace }, + { Key.Insert, LocaleKeys.KeyInsert }, + { Key.Delete, LocaleKeys.KeyDelete }, + { Key.PageUp, LocaleKeys.KeyPageUp }, + { Key.PageDown, LocaleKeys.KeyPageDown }, + { Key.Home, LocaleKeys.KeyHome }, + { Key.End, LocaleKeys.KeyEnd }, + { Key.CapsLock, LocaleKeys.KeyCapsLock }, + { Key.ScrollLock, LocaleKeys.KeyScrollLock }, + { Key.PrintScreen, LocaleKeys.KeyPrintScreen }, + { Key.Pause, LocaleKeys.KeyPause }, + { Key.NumLock, LocaleKeys.KeyNumLock }, + { Key.Clear, LocaleKeys.KeyClear }, + { Key.Keypad0, LocaleKeys.KeyKeypad0 }, + { Key.Keypad1, LocaleKeys.KeyKeypad1 }, + { Key.Keypad2, LocaleKeys.KeyKeypad2 }, + { Key.Keypad3, LocaleKeys.KeyKeypad3 }, + { Key.Keypad4, LocaleKeys.KeyKeypad4 }, + { Key.Keypad5, LocaleKeys.KeyKeypad5 }, + { Key.Keypad6, LocaleKeys.KeyKeypad6 }, + { Key.Keypad7, LocaleKeys.KeyKeypad7 }, + { Key.Keypad8, LocaleKeys.KeyKeypad8 }, + { Key.Keypad9, LocaleKeys.KeyKeypad9 }, + { Key.KeypadDivide, LocaleKeys.KeyKeypadDivide }, + { Key.KeypadMultiply, LocaleKeys.KeyKeypadMultiply }, + { Key.KeypadSubtract, LocaleKeys.KeyKeypadSubtract }, + { Key.KeypadAdd, LocaleKeys.KeyKeypadAdd }, + { Key.KeypadDecimal, LocaleKeys.KeyKeypadDecimal }, + { Key.KeypadEnter, LocaleKeys.KeyKeypadEnter }, + { Key.Number0, LocaleKeys.KeyNumber0 }, + { Key.Number1, LocaleKeys.KeyNumber1 }, + { Key.Number2, LocaleKeys.KeyNumber2 }, + { Key.Number3, LocaleKeys.KeyNumber3 }, + { Key.Number4, LocaleKeys.KeyNumber4 }, + { Key.Number5, LocaleKeys.KeyNumber5 }, + { Key.Number6, LocaleKeys.KeyNumber6 }, + { Key.Number7, LocaleKeys.KeyNumber7 }, + { Key.Number8, LocaleKeys.KeyNumber8 }, + { Key.Number9, LocaleKeys.KeyNumber9 }, + { Key.Tilde, LocaleKeys.KeyTilde }, + { Key.Grave, LocaleKeys.KeyGrave }, + { Key.Minus, LocaleKeys.KeyMinus }, + { Key.Plus, LocaleKeys.KeyPlus }, + { Key.BracketLeft, LocaleKeys.KeyBracketLeft }, + { Key.BracketRight, LocaleKeys.KeyBracketRight }, + { Key.Semicolon, LocaleKeys.KeySemicolon }, + { Key.Quote, LocaleKeys.KeyQuote }, + { Key.Comma, LocaleKeys.KeyComma }, + { Key.Period, LocaleKeys.KeyPeriod }, + { Key.Slash, LocaleKeys.KeySlash }, + { Key.BackSlash, LocaleKeys.KeyBackSlash }, + { Key.Unbound, LocaleKeys.KeyUnbound }, + }; + + private static readonly Dictionary _gamepadInputIdMap = new() + { + { GamepadInputId.LeftStick, LocaleKeys.GamepadLeftStick }, + { GamepadInputId.RightStick, LocaleKeys.GamepadRightStick }, + { GamepadInputId.LeftShoulder, LocaleKeys.GamepadLeftShoulder }, + { GamepadInputId.RightShoulder, LocaleKeys.GamepadRightShoulder }, + { GamepadInputId.LeftTrigger, LocaleKeys.GamepadLeftTrigger }, + { GamepadInputId.RightTrigger, LocaleKeys.GamepadRightTrigger }, + { GamepadInputId.DpadUp, LocaleKeys.GamepadDpadUp}, + { GamepadInputId.DpadDown, LocaleKeys.GamepadDpadDown}, + { GamepadInputId.DpadLeft, LocaleKeys.GamepadDpadLeft}, + { GamepadInputId.DpadRight, LocaleKeys.GamepadDpadRight}, + { GamepadInputId.Minus, LocaleKeys.GamepadMinus}, + { GamepadInputId.Plus, LocaleKeys.GamepadPlus}, + { GamepadInputId.Guide, LocaleKeys.GamepadGuide}, + { GamepadInputId.Misc1, LocaleKeys.GamepadMisc1}, + { GamepadInputId.Paddle1, LocaleKeys.GamepadPaddle1}, + { GamepadInputId.Paddle2, LocaleKeys.GamepadPaddle2}, + { GamepadInputId.Paddle3, LocaleKeys.GamepadPaddle3}, + { GamepadInputId.Paddle4, LocaleKeys.GamepadPaddle4}, + { GamepadInputId.Touchpad, LocaleKeys.GamepadTouchpad}, + { GamepadInputId.SingleLeftTrigger0, LocaleKeys.GamepadSingleLeftTrigger0}, + { GamepadInputId.SingleRightTrigger0, LocaleKeys.GamepadSingleRightTrigger0}, + { GamepadInputId.SingleLeftTrigger1, LocaleKeys.GamepadSingleLeftTrigger1}, + { GamepadInputId.SingleRightTrigger1, LocaleKeys.GamepadSingleRightTrigger1}, + { GamepadInputId.Unbound, LocaleKeys.KeyUnbound}, + }; + + private static readonly Dictionary _stickInputIdMap = new() + { + { StickInputId.Left, LocaleKeys.StickLeft}, + { StickInputId.Right, LocaleKeys.StickRight}, + { StickInputId.Unbound, LocaleKeys.KeyUnbound}, + }; + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { - if (value == null) + string keyString = ""; + LocaleKeys localeKey; + + switch (value) { - return null; + case Key key: + if (_keysMap.TryGetValue(key, out localeKey)) + { + if (OperatingSystem.IsMacOS()) + { + localeKey = localeKey switch + { + LocaleKeys.KeyControlLeft => LocaleKeys.KeyMacControlLeft, + LocaleKeys.KeyControlRight => LocaleKeys.KeyMacControlRight, + LocaleKeys.KeyAltLeft => LocaleKeys.KeyMacAltLeft, + LocaleKeys.KeyAltRight => LocaleKeys.KeyMacAltRight, + LocaleKeys.KeyWinLeft => LocaleKeys.KeyMacWinLeft, + LocaleKeys.KeyWinRight => LocaleKeys.KeyMacWinRight, + _ => localeKey + }; + } + + keyString = LocaleManager.Instance[localeKey]; + } + else + { + keyString = key.ToString(); + } + break; + case GamepadInputId gamepadInputId: + if (_gamepadInputIdMap.TryGetValue(gamepadInputId, out localeKey)) + { + keyString = LocaleManager.Instance[localeKey]; + } + else + { + keyString = gamepadInputId.ToString(); + } + break; + case StickInputId stickInputId: + if (_stickInputIdMap.TryGetValue(stickInputId, out localeKey)) + { + keyString = LocaleManager.Instance[localeKey]; + } + else + { + keyString = stickInputId.ToString(); + } + break; } - return value.ToString(); + return keyString; } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { - object key = null; - - if (value != null) - { - if (targetType == typeof(Key)) - { - key = Enum.Parse(value.ToString()); - } - else if (targetType == typeof(GamepadInputId)) - { - key = Enum.Parse(value.ToString()); - } - else if (targetType == typeof(StickInputId)) - { - key = Enum.Parse(value.ToString()); - } - } - - return key; + throw new NotSupportedException(); } } } diff --git a/src/Ryujinx/UI/Helpers/Win32NativeInterop.cs b/src/Ryujinx/UI/Helpers/Win32NativeInterop.cs index fce2d518a..01478cb3d 100644 --- a/src/Ryujinx/UI/Helpers/Win32NativeInterop.cs +++ b/src/Ryujinx/UI/Helpers/Win32NativeInterop.cs @@ -111,8 +111,5 @@ namespace Ryujinx.Ava.UI.Helpers [LibraryImport("user32.dll", SetLastError = true)] public static partial IntPtr SetWindowLongPtrW(IntPtr hWnd, int nIndex, IntPtr value); - - [LibraryImport("user32.dll", SetLastError = true)] - public static partial IntPtr SetWindowLongW(IntPtr hWnd, int nIndex, int value); } } diff --git a/src/Ryujinx/UI/Models/Input/GamepadInputConfig.cs b/src/Ryujinx/UI/Models/Input/GamepadInputConfig.cs new file mode 100644 index 000000000..833670bdc --- /dev/null +++ b/src/Ryujinx/UI/Models/Input/GamepadInputConfig.cs @@ -0,0 +1,580 @@ +using Ryujinx.Ava.UI.ViewModels; +using Ryujinx.Common.Configuration.Hid; +using Ryujinx.Common.Configuration.Hid.Controller; +using Ryujinx.Common.Configuration.Hid.Controller.Motion; +using System; + +namespace Ryujinx.Ava.UI.Models.Input +{ + public class GamepadInputConfig : BaseModel + { + public bool EnableCemuHookMotion { get; set; } + public string DsuServerHost { get; set; } + public int DsuServerPort { get; set; } + public int Slot { get; set; } + public int AltSlot { get; set; } + public bool MirrorInput { get; set; } + public int Sensitivity { get; set; } + public double GyroDeadzone { get; set; } + + public float WeakRumble { get; set; } + public float StrongRumble { get; set; } + + public string Id { get; set; } + public ControllerType ControllerType { get; set; } + public PlayerIndex PlayerIndex { get; set; } + + private StickInputId _leftJoystick; + public StickInputId LeftJoystick + { + get => _leftJoystick; + set + { + _leftJoystick = value; + OnPropertyChanged(); + } + } + + private bool _leftInvertStickX; + public bool LeftInvertStickX + { + get => _leftInvertStickX; + set + { + _leftInvertStickX = value; + OnPropertyChanged(); + } + } + + private bool _leftInvertStickY; + public bool LeftInvertStickY + { + get => _leftInvertStickY; + set + { + _leftInvertStickY = value; + OnPropertyChanged(); + } + } + + private bool _leftRotate90; + public bool LeftRotate90 + { + get => _leftRotate90; + set + { + _leftRotate90 = value; + OnPropertyChanged(); + } + } + + private GamepadInputId _leftStickButton; + public GamepadInputId LeftStickButton + { + get => _leftStickButton; + set + { + _leftStickButton = value; + OnPropertyChanged(); + } + } + + private StickInputId _rightJoystick; + public StickInputId RightJoystick + { + get => _rightJoystick; + set + { + _rightJoystick = value; + OnPropertyChanged(); + } + } + + private bool _rightInvertStickX; + public bool RightInvertStickX + { + get => _rightInvertStickX; + set + { + _rightInvertStickX = value; + OnPropertyChanged(); + } + } + + private bool _rightInvertStickY; + public bool RightInvertStickY + { + get => _rightInvertStickY; + set + { + _rightInvertStickY = value; + OnPropertyChanged(); + } + } + + private bool _rightRotate90; + public bool RightRotate90 + { + get => _rightRotate90; + set + { + _rightRotate90 = value; + OnPropertyChanged(); + } + } + + private GamepadInputId _rightStickButton; + public GamepadInputId RightStickButton + { + get => _rightStickButton; + set + { + _rightStickButton = value; + OnPropertyChanged(); + } + } + + private GamepadInputId _dpadUp; + public GamepadInputId DpadUp + { + get => _dpadUp; + set + { + _dpadUp = value; + OnPropertyChanged(); + } + } + + private GamepadInputId _dpadDown; + public GamepadInputId DpadDown + { + get => _dpadDown; + set + { + _dpadDown = value; + OnPropertyChanged(); + } + } + + private GamepadInputId _dpadLeft; + public GamepadInputId DpadLeft + { + get => _dpadLeft; + set + { + _dpadLeft = value; + OnPropertyChanged(); + } + } + + private GamepadInputId _dpadRight; + public GamepadInputId DpadRight + { + get => _dpadRight; + set + { + _dpadRight = value; + OnPropertyChanged(); + } + } + + private GamepadInputId _buttonL; + public GamepadInputId ButtonL + { + get => _buttonL; + set + { + _buttonL = value; + OnPropertyChanged(); + } + } + + private GamepadInputId _buttonMinus; + public GamepadInputId ButtonMinus + { + get => _buttonMinus; + set + { + _buttonMinus = value; + OnPropertyChanged(); + } + } + + private GamepadInputId _leftButtonSl; + public GamepadInputId LeftButtonSl + { + get => _leftButtonSl; + set + { + _leftButtonSl = value; + OnPropertyChanged(); + } + } + + private GamepadInputId _leftButtonSr; + public GamepadInputId LeftButtonSr + { + get => _leftButtonSr; + set + { + _leftButtonSr = value; + OnPropertyChanged(); + } + } + + private GamepadInputId _buttonZl; + public GamepadInputId ButtonZl + { + get => _buttonZl; + set + { + _buttonZl = value; + OnPropertyChanged(); + } + } + + private GamepadInputId _buttonA; + public GamepadInputId ButtonA + { + get => _buttonA; + set + { + _buttonA = value; + OnPropertyChanged(); + } + } + + private GamepadInputId _buttonB; + public GamepadInputId ButtonB + { + get => _buttonB; + set + { + _buttonB = value; + OnPropertyChanged(); + } + } + + private GamepadInputId _buttonX; + public GamepadInputId ButtonX + { + get => _buttonX; + set + { + _buttonX = value; + OnPropertyChanged(); + } + } + + private GamepadInputId _buttonY; + public GamepadInputId ButtonY + { + get => _buttonY; + set + { + _buttonY = value; + OnPropertyChanged(); + } + } + + private GamepadInputId _buttonR; + public GamepadInputId ButtonR + { + get => _buttonR; + set + { + _buttonR = value; + OnPropertyChanged(); + } + } + + private GamepadInputId _buttonPlus; + public GamepadInputId ButtonPlus + { + get => _buttonPlus; + set + { + _buttonPlus = value; + OnPropertyChanged(); + } + } + + private GamepadInputId _rightButtonSl; + public GamepadInputId RightButtonSl + { + get => _rightButtonSl; + set + { + _rightButtonSl = value; + OnPropertyChanged(); + } + } + + private GamepadInputId _rightButtonSr; + public GamepadInputId RightButtonSr + { + get => _rightButtonSr; + set + { + _rightButtonSr = value; + OnPropertyChanged(); + } + } + + private GamepadInputId _buttonZr; + public GamepadInputId ButtonZr + { + get => _buttonZr; + set + { + _buttonZr = value; + OnPropertyChanged(); + } + } + + private float _deadzoneLeft; + public float DeadzoneLeft + { + get => _deadzoneLeft; + set + { + _deadzoneLeft = MathF.Round(value, 3); + OnPropertyChanged(); + } + } + + private float _deadzoneRight; + public float DeadzoneRight + { + get => _deadzoneRight; + set + { + _deadzoneRight = MathF.Round(value, 3); + OnPropertyChanged(); + } + } + + private float _rangeLeft; + public float RangeLeft + { + get => _rangeLeft; + set + { + _rangeLeft = MathF.Round(value, 3); + OnPropertyChanged(); + } + } + + private float _rangeRight; + public float RangeRight + { + get => _rangeRight; + set + { + _rangeRight = MathF.Round(value, 3); + OnPropertyChanged(); + } + } + + private float _triggerThreshold; + public float TriggerThreshold + { + get => _triggerThreshold; + set + { + _triggerThreshold = MathF.Round(value, 3); + OnPropertyChanged(); + } + } + + private bool _enableMotion; + public bool EnableMotion + { + get => _enableMotion; + set + { + _enableMotion = value; + OnPropertyChanged(); + } + } + + private bool _enableRumble; + public bool EnableRumble + { + get => _enableRumble; + set + { + _enableRumble = value; + OnPropertyChanged(); + } + } + + public GamepadInputConfig(InputConfig config) + { + if (config != null) + { + Id = config.Id; + ControllerType = config.ControllerType; + PlayerIndex = config.PlayerIndex; + + if (config is not StandardControllerInputConfig controllerInput) + { + return; + } + + LeftJoystick = controllerInput.LeftJoyconStick.Joystick; + LeftInvertStickX = controllerInput.LeftJoyconStick.InvertStickX; + LeftInvertStickY = controllerInput.LeftJoyconStick.InvertStickY; + LeftRotate90 = controllerInput.LeftJoyconStick.Rotate90CW; + LeftStickButton = controllerInput.LeftJoyconStick.StickButton; + + RightJoystick = controllerInput.RightJoyconStick.Joystick; + RightInvertStickX = controllerInput.RightJoyconStick.InvertStickX; + RightInvertStickY = controllerInput.RightJoyconStick.InvertStickY; + RightRotate90 = controllerInput.RightJoyconStick.Rotate90CW; + RightStickButton = controllerInput.RightJoyconStick.StickButton; + + DpadUp = controllerInput.LeftJoycon.DpadUp; + DpadDown = controllerInput.LeftJoycon.DpadDown; + DpadLeft = controllerInput.LeftJoycon.DpadLeft; + DpadRight = controllerInput.LeftJoycon.DpadRight; + ButtonL = controllerInput.LeftJoycon.ButtonL; + ButtonMinus = controllerInput.LeftJoycon.ButtonMinus; + LeftButtonSl = controllerInput.LeftJoycon.ButtonSl; + LeftButtonSr = controllerInput.LeftJoycon.ButtonSr; + ButtonZl = controllerInput.LeftJoycon.ButtonZl; + + ButtonA = controllerInput.RightJoycon.ButtonA; + ButtonB = controllerInput.RightJoycon.ButtonB; + ButtonX = controllerInput.RightJoycon.ButtonX; + ButtonY = controllerInput.RightJoycon.ButtonY; + ButtonR = controllerInput.RightJoycon.ButtonR; + ButtonPlus = controllerInput.RightJoycon.ButtonPlus; + RightButtonSl = controllerInput.RightJoycon.ButtonSl; + RightButtonSr = controllerInput.RightJoycon.ButtonSr; + ButtonZr = controllerInput.RightJoycon.ButtonZr; + + DeadzoneLeft = controllerInput.DeadzoneLeft; + DeadzoneRight = controllerInput.DeadzoneRight; + RangeLeft = controllerInput.RangeLeft; + RangeRight = controllerInput.RangeRight; + TriggerThreshold = controllerInput.TriggerThreshold; + + if (controllerInput.Motion != null) + { + EnableMotion = controllerInput.Motion.EnableMotion; + GyroDeadzone = controllerInput.Motion.GyroDeadzone; + Sensitivity = controllerInput.Motion.Sensitivity; + + if (controllerInput.Motion is CemuHookMotionConfigController cemuHook) + { + EnableCemuHookMotion = true; + DsuServerHost = cemuHook.DsuServerHost; + DsuServerPort = cemuHook.DsuServerPort; + Slot = cemuHook.Slot; + AltSlot = cemuHook.AltSlot; + MirrorInput = cemuHook.MirrorInput; + } + } + + if (controllerInput.Rumble != null) + { + EnableRumble = controllerInput.Rumble.EnableRumble; + WeakRumble = controllerInput.Rumble.WeakRumble; + StrongRumble = controllerInput.Rumble.StrongRumble; + } + } + } + + public InputConfig GetConfig() + { + var config = new StandardControllerInputConfig + { + Id = Id, + Backend = InputBackendType.GamepadSDL2, + PlayerIndex = PlayerIndex, + ControllerType = ControllerType, + LeftJoycon = new LeftJoyconCommonConfig + { + DpadUp = DpadUp, + DpadDown = DpadDown, + DpadLeft = DpadLeft, + DpadRight = DpadRight, + ButtonL = ButtonL, + ButtonMinus = ButtonMinus, + ButtonSl = LeftButtonSl, + ButtonSr = LeftButtonSr, + ButtonZl = ButtonZl, + }, + RightJoycon = new RightJoyconCommonConfig + { + ButtonA = ButtonA, + ButtonB = ButtonB, + ButtonX = ButtonX, + ButtonY = ButtonY, + ButtonPlus = ButtonPlus, + ButtonSl = RightButtonSl, + ButtonSr = RightButtonSr, + ButtonR = ButtonR, + ButtonZr = ButtonZr, + }, + LeftJoyconStick = new JoyconConfigControllerStick + { + Joystick = LeftJoystick, + InvertStickX = LeftInvertStickX, + InvertStickY = LeftInvertStickY, + Rotate90CW = LeftRotate90, + StickButton = LeftStickButton, + }, + RightJoyconStick = new JoyconConfigControllerStick + { + Joystick = RightJoystick, + InvertStickX = RightInvertStickX, + InvertStickY = RightInvertStickY, + Rotate90CW = RightRotate90, + StickButton = RightStickButton, + }, + Rumble = new RumbleConfigController + { + EnableRumble = EnableRumble, + WeakRumble = WeakRumble, + StrongRumble = StrongRumble, + }, + Version = InputConfig.CurrentVersion, + DeadzoneLeft = DeadzoneLeft, + DeadzoneRight = DeadzoneRight, + RangeLeft = RangeLeft, + RangeRight = RangeRight, + TriggerThreshold = TriggerThreshold, + }; + + if (EnableCemuHookMotion) + { + config.Motion = new CemuHookMotionConfigController + { + EnableMotion = EnableMotion, + MotionBackend = MotionInputBackendType.CemuHook, + GyroDeadzone = GyroDeadzone, + Sensitivity = Sensitivity, + DsuServerHost = DsuServerHost, + DsuServerPort = DsuServerPort, + Slot = Slot, + AltSlot = AltSlot, + MirrorInput = MirrorInput, + }; + } + else + { + config.Motion = new StandardMotionConfigController + { + EnableMotion = EnableMotion, + MotionBackend = MotionInputBackendType.GamepadDriver, + GyroDeadzone = GyroDeadzone, + Sensitivity = Sensitivity, + }; + } + + return config; + } + } +} diff --git a/src/Ryujinx/UI/Models/Input/HotkeyConfig.cs b/src/Ryujinx/UI/Models/Input/HotkeyConfig.cs new file mode 100644 index 000000000..b5f53508b --- /dev/null +++ b/src/Ryujinx/UI/Models/Input/HotkeyConfig.cs @@ -0,0 +1,141 @@ +using Ryujinx.Ava.UI.ViewModels; +using Ryujinx.Common.Configuration.Hid; + +namespace Ryujinx.Ava.UI.Models.Input +{ + public class HotkeyConfig : BaseModel + { + private Key _toggleVsync; + public Key ToggleVsync + { + get => _toggleVsync; + set + { + _toggleVsync = value; + OnPropertyChanged(); + } + } + + private Key _screenshot; + public Key Screenshot + { + get => _screenshot; + set + { + _screenshot = value; + OnPropertyChanged(); + } + } + + private Key _showUI; + public Key ShowUI + { + get => _showUI; + set + { + _showUI = value; + OnPropertyChanged(); + } + } + + private Key _pause; + public Key Pause + { + get => _pause; + set + { + _pause = value; + OnPropertyChanged(); + } + } + + private Key _toggleMute; + public Key ToggleMute + { + get => _toggleMute; + set + { + _toggleMute = value; + OnPropertyChanged(); + } + } + + private Key _resScaleUp; + public Key ResScaleUp + { + get => _resScaleUp; + set + { + _resScaleUp = value; + OnPropertyChanged(); + } + } + + private Key _resScaleDown; + public Key ResScaleDown + { + get => _resScaleDown; + set + { + _resScaleDown = value; + OnPropertyChanged(); + } + } + + private Key _volumeUp; + public Key VolumeUp + { + get => _volumeUp; + set + { + _volumeUp = value; + OnPropertyChanged(); + } + } + + private Key _volumeDown; + public Key VolumeDown + { + get => _volumeDown; + set + { + _volumeDown = value; + OnPropertyChanged(); + } + } + + public HotkeyConfig(KeyboardHotkeys config) + { + if (config != null) + { + ToggleVsync = config.ToggleVsync; + Screenshot = config.Screenshot; + ShowUI = config.ShowUI; + Pause = config.Pause; + ToggleMute = config.ToggleMute; + ResScaleUp = config.ResScaleUp; + ResScaleDown = config.ResScaleDown; + VolumeUp = config.VolumeUp; + VolumeDown = config.VolumeDown; + } + } + + public KeyboardHotkeys GetConfig() + { + var config = new KeyboardHotkeys + { + ToggleVsync = ToggleVsync, + Screenshot = Screenshot, + ShowUI = ShowUI, + Pause = Pause, + ToggleMute = ToggleMute, + ResScaleUp = ResScaleUp, + ResScaleDown = ResScaleDown, + VolumeUp = VolumeUp, + VolumeDown = VolumeDown, + }; + + return config; + } + } +} diff --git a/src/Ryujinx/UI/Models/Input/KeyboardInputConfig.cs b/src/Ryujinx/UI/Models/Input/KeyboardInputConfig.cs new file mode 100644 index 000000000..66f1f62a2 --- /dev/null +++ b/src/Ryujinx/UI/Models/Input/KeyboardInputConfig.cs @@ -0,0 +1,422 @@ +using Ryujinx.Ava.UI.ViewModels; +using Ryujinx.Common.Configuration.Hid; +using Ryujinx.Common.Configuration.Hid.Keyboard; + +namespace Ryujinx.Ava.UI.Models.Input +{ + public class KeyboardInputConfig : BaseModel + { + public string Id { get; set; } + public ControllerType ControllerType { get; set; } + public PlayerIndex PlayerIndex { get; set; } + + private Key _leftStickUp; + public Key LeftStickUp + { + get => _leftStickUp; + set + { + _leftStickUp = value; + OnPropertyChanged(); + } + } + + private Key _leftStickDown; + public Key LeftStickDown + { + get => _leftStickDown; + set + { + _leftStickDown = value; + OnPropertyChanged(); + } + } + + private Key _leftStickLeft; + public Key LeftStickLeft + { + get => _leftStickLeft; + set + { + _leftStickLeft = value; + OnPropertyChanged(); + } + } + + private Key _leftStickRight; + public Key LeftStickRight + { + get => _leftStickRight; + set + { + _leftStickRight = value; + OnPropertyChanged(); + } + } + + private Key _leftStickButton; + public Key LeftStickButton + { + get => _leftStickButton; + set + { + _leftStickButton = value; + OnPropertyChanged(); + } + } + + private Key _rightStickUp; + public Key RightStickUp + { + get => _rightStickUp; + set + { + _rightStickUp = value; + OnPropertyChanged(); + } + } + + private Key _rightStickDown; + public Key RightStickDown + { + get => _rightStickDown; + set + { + _rightStickDown = value; + OnPropertyChanged(); + } + } + + private Key _rightStickLeft; + public Key RightStickLeft + { + get => _rightStickLeft; + set + { + _rightStickLeft = value; + OnPropertyChanged(); + } + } + + private Key _rightStickRight; + public Key RightStickRight + { + get => _rightStickRight; + set + { + _rightStickRight = value; + OnPropertyChanged(); + } + } + + private Key _rightStickButton; + public Key RightStickButton + { + get => _rightStickButton; + set + { + _rightStickButton = value; + OnPropertyChanged(); + } + } + + private Key _dpadUp; + public Key DpadUp + { + get => _dpadUp; + set + { + _dpadUp = value; + OnPropertyChanged(); + } + } + + private Key _dpadDown; + public Key DpadDown + { + get => _dpadDown; + set + { + _dpadDown = value; + OnPropertyChanged(); + } + } + + private Key _dpadLeft; + public Key DpadLeft + { + get => _dpadLeft; + set + { + _dpadLeft = value; + OnPropertyChanged(); + } + } + + private Key _dpadRight; + public Key DpadRight + { + get => _dpadRight; + set + { + _dpadRight = value; + OnPropertyChanged(); + } + } + + private Key _buttonL; + public Key ButtonL + { + get => _buttonL; + set + { + _buttonL = value; + OnPropertyChanged(); + } + } + + private Key _buttonMinus; + public Key ButtonMinus + { + get => _buttonMinus; + set + { + _buttonMinus = value; + OnPropertyChanged(); + } + } + + private Key _leftButtonSl; + public Key LeftButtonSl + { + get => _leftButtonSl; + set + { + _leftButtonSl = value; + OnPropertyChanged(); + } + } + + private Key _leftButtonSr; + public Key LeftButtonSr + { + get => _leftButtonSr; + set + { + _leftButtonSr = value; + OnPropertyChanged(); + } + } + + private Key _buttonZl; + public Key ButtonZl + { + get => _buttonZl; + set + { + _buttonZl = value; + OnPropertyChanged(); + } + } + + private Key _buttonA; + public Key ButtonA + { + get => _buttonA; + set + { + _buttonA = value; + OnPropertyChanged(); + } + } + + private Key _buttonB; + public Key ButtonB + { + get => _buttonB; + set + { + _buttonB = value; + OnPropertyChanged(); + } + } + + private Key _buttonX; + public Key ButtonX + { + get => _buttonX; + set + { + _buttonX = value; + OnPropertyChanged(); + } + } + + private Key _buttonY; + public Key ButtonY + { + get => _buttonY; + set + { + _buttonY = value; + OnPropertyChanged(); + } + } + + private Key _buttonR; + public Key ButtonR + { + get => _buttonR; + set + { + _buttonR = value; + OnPropertyChanged(); + } + } + + private Key _buttonPlus; + public Key ButtonPlus + { + get => _buttonPlus; + set + { + _buttonPlus = value; + OnPropertyChanged(); + } + } + + private Key _rightButtonSl; + public Key RightButtonSl + { + get => _rightButtonSl; + set + { + _rightButtonSl = value; + OnPropertyChanged(); + } + } + + private Key _rightButtonSr; + public Key RightButtonSr + { + get => _rightButtonSr; + set + { + _rightButtonSr = value; + OnPropertyChanged(); + } + } + + private Key _buttonZr; + public Key ButtonZr + { + get => _buttonZr; + set + { + _buttonZr = value; + OnPropertyChanged(); + } + } + + public KeyboardInputConfig(InputConfig config) + { + if (config != null) + { + Id = config.Id; + ControllerType = config.ControllerType; + PlayerIndex = config.PlayerIndex; + + if (config is not StandardKeyboardInputConfig keyboardConfig) + { + return; + } + + LeftStickUp = keyboardConfig.LeftJoyconStick.StickUp; + LeftStickDown = keyboardConfig.LeftJoyconStick.StickDown; + LeftStickLeft = keyboardConfig.LeftJoyconStick.StickLeft; + LeftStickRight = keyboardConfig.LeftJoyconStick.StickRight; + LeftStickButton = keyboardConfig.LeftJoyconStick.StickButton; + + RightStickUp = keyboardConfig.RightJoyconStick.StickUp; + RightStickDown = keyboardConfig.RightJoyconStick.StickDown; + RightStickLeft = keyboardConfig.RightJoyconStick.StickLeft; + RightStickRight = keyboardConfig.RightJoyconStick.StickRight; + RightStickButton = keyboardConfig.RightJoyconStick.StickButton; + + DpadUp = keyboardConfig.LeftJoycon.DpadUp; + DpadDown = keyboardConfig.LeftJoycon.DpadDown; + DpadLeft = keyboardConfig.LeftJoycon.DpadLeft; + DpadRight = keyboardConfig.LeftJoycon.DpadRight; + ButtonL = keyboardConfig.LeftJoycon.ButtonL; + ButtonMinus = keyboardConfig.LeftJoycon.ButtonMinus; + LeftButtonSl = keyboardConfig.LeftJoycon.ButtonSl; + LeftButtonSr = keyboardConfig.LeftJoycon.ButtonSr; + ButtonZl = keyboardConfig.LeftJoycon.ButtonZl; + + ButtonA = keyboardConfig.RightJoycon.ButtonA; + ButtonB = keyboardConfig.RightJoycon.ButtonB; + ButtonX = keyboardConfig.RightJoycon.ButtonX; + ButtonY = keyboardConfig.RightJoycon.ButtonY; + ButtonR = keyboardConfig.RightJoycon.ButtonR; + ButtonPlus = keyboardConfig.RightJoycon.ButtonPlus; + RightButtonSl = keyboardConfig.RightJoycon.ButtonSl; + RightButtonSr = keyboardConfig.RightJoycon.ButtonSr; + ButtonZr = keyboardConfig.RightJoycon.ButtonZr; + } + } + + public InputConfig GetConfig() + { + var config = new StandardKeyboardInputConfig + { + Id = Id, + Backend = InputBackendType.WindowKeyboard, + PlayerIndex = PlayerIndex, + ControllerType = ControllerType, + LeftJoycon = new LeftJoyconCommonConfig + { + DpadUp = DpadUp, + DpadDown = DpadDown, + DpadLeft = DpadLeft, + DpadRight = DpadRight, + ButtonL = ButtonL, + ButtonMinus = ButtonMinus, + ButtonZl = ButtonZl, + ButtonSl = LeftButtonSl, + ButtonSr = LeftButtonSr, + }, + RightJoycon = new RightJoyconCommonConfig + { + ButtonA = ButtonA, + ButtonB = ButtonB, + ButtonX = ButtonX, + ButtonY = ButtonY, + ButtonPlus = ButtonPlus, + ButtonSl = RightButtonSl, + ButtonSr = RightButtonSr, + ButtonR = ButtonR, + ButtonZr = ButtonZr, + }, + LeftJoyconStick = new JoyconConfigKeyboardStick + { + StickUp = LeftStickUp, + StickDown = LeftStickDown, + StickRight = LeftStickRight, + StickLeft = LeftStickLeft, + StickButton = LeftStickButton, + }, + RightJoyconStick = new JoyconConfigKeyboardStick + { + StickUp = RightStickUp, + StickDown = RightStickDown, + StickLeft = RightStickLeft, + StickRight = RightStickRight, + StickButton = RightStickButton, + }, + Version = InputConfig.CurrentVersion, + }; + + return config; + } + } +} diff --git a/src/Ryujinx/UI/Models/InputConfiguration.cs b/src/Ryujinx/UI/Models/InputConfiguration.cs deleted file mode 100644 index f1352c6d8..000000000 --- a/src/Ryujinx/UI/Models/InputConfiguration.cs +++ /dev/null @@ -1,456 +0,0 @@ -using Ryujinx.Ava.UI.ViewModels; -using Ryujinx.Common.Configuration.Hid; -using Ryujinx.Common.Configuration.Hid.Controller; -using Ryujinx.Common.Configuration.Hid.Controller.Motion; -using Ryujinx.Common.Configuration.Hid.Keyboard; -using System; - -namespace Ryujinx.Ava.UI.Models -{ - internal class InputConfiguration : BaseModel - { - private float _deadzoneRight; - private float _triggerThreshold; - private float _deadzoneLeft; - private double _gyroDeadzone; - private int _sensitivity; - private bool _enableMotion; - private float _weakRumble; - private float _strongRumble; - private float _rangeLeft; - private float _rangeRight; - - public InputBackendType Backend { get; set; } - - /// - /// Controller id - /// - public string Id { get; set; } - - /// - /// Controller's Type - /// - public ControllerType ControllerType { get; set; } - - /// - /// Player's Index for the controller - /// - public PlayerIndex PlayerIndex { get; set; } - - public TStick LeftJoystick { get; set; } - public bool LeftInvertStickX { get; set; } - public bool LeftInvertStickY { get; set; } - public bool RightRotate90 { get; set; } - public TKey LeftControllerStickButton { get; set; } - - public TStick RightJoystick { get; set; } - public bool RightInvertStickX { get; set; } - public bool RightInvertStickY { get; set; } - public bool LeftRotate90 { get; set; } - public TKey RightControllerStickButton { get; set; } - - public float DeadzoneLeft - { - get => _deadzoneLeft; - set - { - _deadzoneLeft = MathF.Round(value, 3); - - OnPropertyChanged(); - } - } - - public float RangeLeft - { - get => _rangeLeft; - set - { - _rangeLeft = MathF.Round(value, 3); - - OnPropertyChanged(); - } - } - - public float DeadzoneRight - { - get => _deadzoneRight; - set - { - _deadzoneRight = MathF.Round(value, 3); - - OnPropertyChanged(); - } - } - - public float RangeRight - { - get => _rangeRight; - set - { - _rangeRight = MathF.Round(value, 3); - - OnPropertyChanged(); - } - } - - public float TriggerThreshold - { - get => _triggerThreshold; - set - { - _triggerThreshold = MathF.Round(value, 3); - - OnPropertyChanged(); - } - } - - public MotionInputBackendType MotionBackend { get; set; } - - public TKey ButtonMinus { get; set; } - public TKey ButtonL { get; set; } - public TKey ButtonZl { get; set; } - public TKey LeftButtonSl { get; set; } - public TKey LeftButtonSr { get; set; } - public TKey DpadUp { get; set; } - public TKey DpadDown { get; set; } - public TKey DpadLeft { get; set; } - public TKey DpadRight { get; set; } - - public TKey ButtonPlus { get; set; } - public TKey ButtonR { get; set; } - public TKey ButtonZr { get; set; } - public TKey RightButtonSl { get; set; } - public TKey RightButtonSr { get; set; } - public TKey ButtonX { get; set; } - public TKey ButtonB { get; set; } - public TKey ButtonY { get; set; } - public TKey ButtonA { get; set; } - - public TKey LeftStickUp { get; set; } - public TKey LeftStickDown { get; set; } - public TKey LeftStickLeft { get; set; } - public TKey LeftStickRight { get; set; } - public TKey LeftKeyboardStickButton { get; set; } - - public TKey RightStickUp { get; set; } - public TKey RightStickDown { get; set; } - public TKey RightStickLeft { get; set; } - public TKey RightStickRight { get; set; } - public TKey RightKeyboardStickButton { get; set; } - - public int Sensitivity - { - get => _sensitivity; - set - { - _sensitivity = value; - - OnPropertyChanged(); - } - } - - public double GyroDeadzone - { - get => _gyroDeadzone; - set - { - _gyroDeadzone = Math.Round(value, 3); - - OnPropertyChanged(); - } - } - - public bool EnableMotion - { - get => _enableMotion; set - { - _enableMotion = value; - - OnPropertyChanged(); - } - } - - public bool EnableCemuHookMotion { get; set; } - public int Slot { get; set; } - public int AltSlot { get; set; } - public bool MirrorInput { get; set; } - public string DsuServerHost { get; set; } - public int DsuServerPort { get; set; } - - public bool EnableRumble { get; set; } - public float WeakRumble - { - get => _weakRumble; set - { - _weakRumble = value; - - OnPropertyChanged(); - } - } - public float StrongRumble - { - get => _strongRumble; set - { - _strongRumble = value; - - OnPropertyChanged(); - } - } - - public InputConfiguration(InputConfig config) - { - if (config != null) - { - Backend = config.Backend; - Id = config.Id; - ControllerType = config.ControllerType; - PlayerIndex = config.PlayerIndex; - - if (config is StandardKeyboardInputConfig keyboardConfig) - { - LeftStickUp = (TKey)(object)keyboardConfig.LeftJoyconStick.StickUp; - LeftStickDown = (TKey)(object)keyboardConfig.LeftJoyconStick.StickDown; - LeftStickLeft = (TKey)(object)keyboardConfig.LeftJoyconStick.StickLeft; - LeftStickRight = (TKey)(object)keyboardConfig.LeftJoyconStick.StickRight; - LeftKeyboardStickButton = (TKey)(object)keyboardConfig.LeftJoyconStick.StickButton; - - RightStickUp = (TKey)(object)keyboardConfig.RightJoyconStick.StickUp; - RightStickDown = (TKey)(object)keyboardConfig.RightJoyconStick.StickDown; - RightStickLeft = (TKey)(object)keyboardConfig.RightJoyconStick.StickLeft; - RightStickRight = (TKey)(object)keyboardConfig.RightJoyconStick.StickRight; - RightKeyboardStickButton = (TKey)(object)keyboardConfig.RightJoyconStick.StickButton; - - ButtonA = (TKey)(object)keyboardConfig.RightJoycon.ButtonA; - ButtonB = (TKey)(object)keyboardConfig.RightJoycon.ButtonB; - ButtonX = (TKey)(object)keyboardConfig.RightJoycon.ButtonX; - ButtonY = (TKey)(object)keyboardConfig.RightJoycon.ButtonY; - ButtonR = (TKey)(object)keyboardConfig.RightJoycon.ButtonR; - RightButtonSl = (TKey)(object)keyboardConfig.RightJoycon.ButtonSl; - RightButtonSr = (TKey)(object)keyboardConfig.RightJoycon.ButtonSr; - ButtonZr = (TKey)(object)keyboardConfig.RightJoycon.ButtonZr; - ButtonPlus = (TKey)(object)keyboardConfig.RightJoycon.ButtonPlus; - - DpadUp = (TKey)(object)keyboardConfig.LeftJoycon.DpadUp; - DpadDown = (TKey)(object)keyboardConfig.LeftJoycon.DpadDown; - DpadLeft = (TKey)(object)keyboardConfig.LeftJoycon.DpadLeft; - DpadRight = (TKey)(object)keyboardConfig.LeftJoycon.DpadRight; - ButtonMinus = (TKey)(object)keyboardConfig.LeftJoycon.ButtonMinus; - LeftButtonSl = (TKey)(object)keyboardConfig.LeftJoycon.ButtonSl; - LeftButtonSr = (TKey)(object)keyboardConfig.LeftJoycon.ButtonSr; - ButtonZl = (TKey)(object)keyboardConfig.LeftJoycon.ButtonZl; - ButtonL = (TKey)(object)keyboardConfig.LeftJoycon.ButtonL; - } - else if (config is StandardControllerInputConfig controllerConfig) - { - LeftJoystick = (TStick)(object)controllerConfig.LeftJoyconStick.Joystick; - LeftInvertStickX = controllerConfig.LeftJoyconStick.InvertStickX; - LeftInvertStickY = controllerConfig.LeftJoyconStick.InvertStickY; - LeftRotate90 = controllerConfig.LeftJoyconStick.Rotate90CW; - LeftControllerStickButton = (TKey)(object)controllerConfig.LeftJoyconStick.StickButton; - - RightJoystick = (TStick)(object)controllerConfig.RightJoyconStick.Joystick; - RightInvertStickX = controllerConfig.RightJoyconStick.InvertStickX; - RightInvertStickY = controllerConfig.RightJoyconStick.InvertStickY; - RightRotate90 = controllerConfig.RightJoyconStick.Rotate90CW; - RightControllerStickButton = (TKey)(object)controllerConfig.RightJoyconStick.StickButton; - - ButtonA = (TKey)(object)controllerConfig.RightJoycon.ButtonA; - ButtonB = (TKey)(object)controllerConfig.RightJoycon.ButtonB; - ButtonX = (TKey)(object)controllerConfig.RightJoycon.ButtonX; - ButtonY = (TKey)(object)controllerConfig.RightJoycon.ButtonY; - ButtonR = (TKey)(object)controllerConfig.RightJoycon.ButtonR; - RightButtonSl = (TKey)(object)controllerConfig.RightJoycon.ButtonSl; - RightButtonSr = (TKey)(object)controllerConfig.RightJoycon.ButtonSr; - ButtonZr = (TKey)(object)controllerConfig.RightJoycon.ButtonZr; - ButtonPlus = (TKey)(object)controllerConfig.RightJoycon.ButtonPlus; - - DpadUp = (TKey)(object)controllerConfig.LeftJoycon.DpadUp; - DpadDown = (TKey)(object)controllerConfig.LeftJoycon.DpadDown; - DpadLeft = (TKey)(object)controllerConfig.LeftJoycon.DpadLeft; - DpadRight = (TKey)(object)controllerConfig.LeftJoycon.DpadRight; - ButtonMinus = (TKey)(object)controllerConfig.LeftJoycon.ButtonMinus; - LeftButtonSl = (TKey)(object)controllerConfig.LeftJoycon.ButtonSl; - LeftButtonSr = (TKey)(object)controllerConfig.LeftJoycon.ButtonSr; - ButtonZl = (TKey)(object)controllerConfig.LeftJoycon.ButtonZl; - ButtonL = (TKey)(object)controllerConfig.LeftJoycon.ButtonL; - - DeadzoneLeft = controllerConfig.DeadzoneLeft; - DeadzoneRight = controllerConfig.DeadzoneRight; - RangeLeft = controllerConfig.RangeLeft; - RangeRight = controllerConfig.RangeRight; - TriggerThreshold = controllerConfig.TriggerThreshold; - - if (controllerConfig.Motion != null) - { - EnableMotion = controllerConfig.Motion.EnableMotion; - MotionBackend = controllerConfig.Motion.MotionBackend; - GyroDeadzone = controllerConfig.Motion.GyroDeadzone; - Sensitivity = controllerConfig.Motion.Sensitivity; - - if (controllerConfig.Motion is CemuHookMotionConfigController cemuHook) - { - EnableCemuHookMotion = true; - DsuServerHost = cemuHook.DsuServerHost; - DsuServerPort = cemuHook.DsuServerPort; - Slot = cemuHook.Slot; - AltSlot = cemuHook.AltSlot; - MirrorInput = cemuHook.MirrorInput; - } - - if (controllerConfig.Rumble != null) - { - EnableRumble = controllerConfig.Rumble.EnableRumble; - WeakRumble = controllerConfig.Rumble.WeakRumble; - StrongRumble = controllerConfig.Rumble.StrongRumble; - } - } - } - } - } - - public InputConfiguration() - { - } - - public InputConfig GetConfig() - { - if (Backend == InputBackendType.WindowKeyboard) - { - return new StandardKeyboardInputConfig - { - Id = Id, - Backend = Backend, - PlayerIndex = PlayerIndex, - ControllerType = ControllerType, - LeftJoycon = new LeftJoyconCommonConfig - { - DpadUp = (Key)(object)DpadUp, - DpadDown = (Key)(object)DpadDown, - DpadLeft = (Key)(object)DpadLeft, - DpadRight = (Key)(object)DpadRight, - ButtonL = (Key)(object)ButtonL, - ButtonZl = (Key)(object)ButtonZl, - ButtonSl = (Key)(object)LeftButtonSl, - ButtonSr = (Key)(object)LeftButtonSr, - ButtonMinus = (Key)(object)ButtonMinus, - }, - RightJoycon = new RightJoyconCommonConfig - { - ButtonA = (Key)(object)ButtonA, - ButtonB = (Key)(object)ButtonB, - ButtonX = (Key)(object)ButtonX, - ButtonY = (Key)(object)ButtonY, - ButtonPlus = (Key)(object)ButtonPlus, - ButtonSl = (Key)(object)RightButtonSl, - ButtonSr = (Key)(object)RightButtonSr, - ButtonR = (Key)(object)ButtonR, - ButtonZr = (Key)(object)ButtonZr, - }, - LeftJoyconStick = new JoyconConfigKeyboardStick - { - StickUp = (Key)(object)LeftStickUp, - StickDown = (Key)(object)LeftStickDown, - StickRight = (Key)(object)LeftStickRight, - StickLeft = (Key)(object)LeftStickLeft, - StickButton = (Key)(object)LeftKeyboardStickButton, - }, - RightJoyconStick = new JoyconConfigKeyboardStick - { - StickUp = (Key)(object)RightStickUp, - StickDown = (Key)(object)RightStickDown, - StickLeft = (Key)(object)RightStickLeft, - StickRight = (Key)(object)RightStickRight, - StickButton = (Key)(object)RightKeyboardStickButton, - }, - Version = InputConfig.CurrentVersion, - }; - - } - - if (Backend == InputBackendType.GamepadSDL2) - { - var config = new StandardControllerInputConfig - { - Id = Id, - Backend = Backend, - PlayerIndex = PlayerIndex, - ControllerType = ControllerType, - LeftJoycon = new LeftJoyconCommonConfig - { - DpadUp = (GamepadInputId)(object)DpadUp, - DpadDown = (GamepadInputId)(object)DpadDown, - DpadLeft = (GamepadInputId)(object)DpadLeft, - DpadRight = (GamepadInputId)(object)DpadRight, - ButtonL = (GamepadInputId)(object)ButtonL, - ButtonZl = (GamepadInputId)(object)ButtonZl, - ButtonSl = (GamepadInputId)(object)LeftButtonSl, - ButtonSr = (GamepadInputId)(object)LeftButtonSr, - ButtonMinus = (GamepadInputId)(object)ButtonMinus, - }, - RightJoycon = new RightJoyconCommonConfig - { - ButtonA = (GamepadInputId)(object)ButtonA, - ButtonB = (GamepadInputId)(object)ButtonB, - ButtonX = (GamepadInputId)(object)ButtonX, - ButtonY = (GamepadInputId)(object)ButtonY, - ButtonPlus = (GamepadInputId)(object)ButtonPlus, - ButtonSl = (GamepadInputId)(object)RightButtonSl, - ButtonSr = (GamepadInputId)(object)RightButtonSr, - ButtonR = (GamepadInputId)(object)ButtonR, - ButtonZr = (GamepadInputId)(object)ButtonZr, - }, - LeftJoyconStick = new JoyconConfigControllerStick - { - Joystick = (StickInputId)(object)LeftJoystick, - InvertStickX = LeftInvertStickX, - InvertStickY = LeftInvertStickY, - Rotate90CW = LeftRotate90, - StickButton = (GamepadInputId)(object)LeftControllerStickButton, - }, - RightJoyconStick = new JoyconConfigControllerStick - { - Joystick = (StickInputId)(object)RightJoystick, - InvertStickX = RightInvertStickX, - InvertStickY = RightInvertStickY, - Rotate90CW = RightRotate90, - StickButton = (GamepadInputId)(object)RightControllerStickButton, - }, - Rumble = new RumbleConfigController - { - EnableRumble = EnableRumble, - WeakRumble = WeakRumble, - StrongRumble = StrongRumble, - }, - Version = InputConfig.CurrentVersion, - DeadzoneLeft = DeadzoneLeft, - DeadzoneRight = DeadzoneRight, - RangeLeft = RangeLeft, - RangeRight = RangeRight, - TriggerThreshold = TriggerThreshold, - Motion = EnableCemuHookMotion - ? new CemuHookMotionConfigController - { - DsuServerHost = DsuServerHost, - DsuServerPort = DsuServerPort, - Slot = Slot, - AltSlot = AltSlot, - MirrorInput = MirrorInput, - MotionBackend = MotionInputBackendType.CemuHook, - } - : new StandardMotionConfigController - { - MotionBackend = MotionInputBackendType.GamepadDriver, - }, - }; - - config.Motion.Sensitivity = Sensitivity; - config.Motion.EnableMotion = EnableMotion; - config.Motion.GyroDeadzone = GyroDeadzone; - - return config; - } - - return null; - } - } -} diff --git a/src/Ryujinx/UI/Renderer/EmbeddedWindow.cs b/src/Ryujinx/UI/Renderer/EmbeddedWindow.cs index 8c5e31fff..0930e7795 100644 --- a/src/Ryujinx/UI/Renderer/EmbeddedWindow.cs +++ b/src/Ryujinx/UI/Renderer/EmbeddedWindow.cs @@ -157,7 +157,7 @@ namespace Ryujinx.Ava.UI.Renderer lpfnWndProc = Marshal.GetFunctionPointerForDelegate(_wndProcDelegate), style = ClassStyles.CsOwndc, lpszClassName = Marshal.StringToHGlobalUni(_className), - hCursor = CreateArrowCursor(), + hCursor = CreateArrowCursor() }; RegisterClassEx(ref wndClassEx); diff --git a/src/Ryujinx/UI/ViewModels/AboutWindowViewModel.cs b/src/Ryujinx/UI/ViewModels/AboutWindowViewModel.cs index 6020f40e0..f8fd5b7de 100644 --- a/src/Ryujinx/UI/ViewModels/AboutWindowViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/AboutWindowViewModel.cs @@ -1,6 +1,8 @@ using Avalonia.Media.Imaging; using Avalonia.Platform; +using Avalonia.Styling; using Avalonia.Threading; +using Ryujinx.Ava.Common; using Ryujinx.Ava.Common.Locale; using Ryujinx.Common.Utilities; using Ryujinx.UI.Common.Configuration; @@ -11,7 +13,7 @@ using System.Threading.Tasks; namespace Ryujinx.Ava.UI.ViewModels { - public class AboutWindowViewModel : BaseModel + public class AboutWindowViewModel : BaseModel, IDisposable { private Bitmap _githubLogo; private Bitmap _discordLogo; @@ -86,23 +88,39 @@ namespace Ryujinx.Ava.UI.ViewModels public AboutWindowViewModel() { Version = Program.Version; - - if (ConfigurationState.Instance.UI.BaseStyle.Value == "Light") - { - GithubLogo = new Bitmap(AssetLoader.Open(new Uri("resm:Ryujinx.UI.Common.Resources.Logo_GitHub_Light.png?assembly=Ryujinx.UI.Common"))); - DiscordLogo = new Bitmap(AssetLoader.Open(new Uri("resm:Ryujinx.UI.Common.Resources.Logo_Discord_Light.png?assembly=Ryujinx.UI.Common"))); - PatreonLogo = new Bitmap(AssetLoader.Open(new Uri("resm:Ryujinx.UI.Common.Resources.Logo_Patreon_Light.png?assembly=Ryujinx.UI.Common"))); - TwitterLogo = new Bitmap(AssetLoader.Open(new Uri("resm:Ryujinx.UI.Common.Resources.Logo_Twitter_Light.png?assembly=Ryujinx.UI.Common"))); - } - else - { - GithubLogo = new Bitmap(AssetLoader.Open(new Uri("resm:Ryujinx.UI.Common.Resources.Logo_GitHub_Dark.png?assembly=Ryujinx.UI.Common"))); - DiscordLogo = new Bitmap(AssetLoader.Open(new Uri("resm:Ryujinx.UI.Common.Resources.Logo_Discord_Dark.png?assembly=Ryujinx.UI.Common"))); - PatreonLogo = new Bitmap(AssetLoader.Open(new Uri("resm:Ryujinx.UI.Common.Resources.Logo_Patreon_Dark.png?assembly=Ryujinx.UI.Common"))); - TwitterLogo = new Bitmap(AssetLoader.Open(new Uri("resm:Ryujinx.UI.Common.Resources.Logo_Twitter_Dark.png?assembly=Ryujinx.UI.Common"))); - } - + UpdateLogoTheme(ConfigurationState.Instance.UI.BaseStyle.Value); Dispatcher.UIThread.InvokeAsync(DownloadPatronsJson); + + ThemeManager.ThemeChanged += ThemeManager_ThemeChanged; + } + + private void ThemeManager_ThemeChanged(object sender, EventArgs e) + { + Dispatcher.UIThread.Post(() => UpdateLogoTheme(ConfigurationState.Instance.UI.BaseStyle.Value)); + } + + private void UpdateLogoTheme(string theme) + { + bool isDarkTheme = theme == "Dark" || (theme == "Auto" && App.DetectSystemTheme() == ThemeVariant.Dark); + + string basePath = "resm:Ryujinx.UI.Common.Resources."; + string themeSuffix = isDarkTheme ? "Dark.png" : "Light.png"; + + GithubLogo = LoadBitmap($"{basePath}Logo_GitHub_{themeSuffix}?assembly=Ryujinx.UI.Common"); + DiscordLogo = LoadBitmap($"{basePath}Logo_Discord_{themeSuffix}?assembly=Ryujinx.UI.Common"); + PatreonLogo = LoadBitmap($"{basePath}Logo_Patreon_{themeSuffix}?assembly=Ryujinx.UI.Common"); + TwitterLogo = LoadBitmap($"{basePath}Logo_Twitter_{themeSuffix}?assembly=Ryujinx.UI.Common"); + } + + private Bitmap LoadBitmap(string uri) + { + return new Bitmap(Avalonia.Platform.AssetLoader.Open(new Uri(uri))); + } + + public void Dispose() + { + ThemeManager.ThemeChanged -= ThemeManager_ThemeChanged; + GC.SuppressFinalize(this); } private async Task DownloadPatronsJson() diff --git a/src/Ryujinx/UI/ViewModels/Input/ControllerInputViewModel.cs b/src/Ryujinx/UI/ViewModels/Input/ControllerInputViewModel.cs new file mode 100644 index 000000000..6ee79a371 --- /dev/null +++ b/src/Ryujinx/UI/ViewModels/Input/ControllerInputViewModel.cs @@ -0,0 +1,84 @@ +using Avalonia.Svg.Skia; +using Ryujinx.Ava.UI.Models.Input; +using Ryujinx.Ava.UI.Views.Input; + +namespace Ryujinx.Ava.UI.ViewModels.Input +{ + public class ControllerInputViewModel : BaseModel + { + private GamepadInputConfig _config; + public GamepadInputConfig Config + { + get => _config; + set + { + _config = value; + OnPropertyChanged(); + } + } + + private bool _isLeft; + public bool IsLeft + { + get => _isLeft; + set + { + _isLeft = value; + OnPropertyChanged(); + OnPropertyChanged(nameof(HasSides)); + } + } + + private bool _isRight; + public bool IsRight + { + get => _isRight; + set + { + _isRight = value; + OnPropertyChanged(); + OnPropertyChanged(nameof(HasSides)); + } + } + + public bool HasSides => IsLeft ^ IsRight; + + private SvgImage _image; + public SvgImage Image + { + get => _image; + set + { + _image = value; + OnPropertyChanged(); + } + } + + public readonly InputViewModel ParentModel; + + public ControllerInputViewModel(InputViewModel model, GamepadInputConfig config) + { + ParentModel = model; + model.NotifyChangesEvent += OnParentModelChanged; + OnParentModelChanged(); + Config = config; + } + + public async void ShowMotionConfig() + { + await MotionInputView.Show(this); + } + + public async void ShowRumbleConfig() + { + await RumbleInputView.Show(this); + } + + public void OnParentModelChanged() + { + IsLeft = ParentModel.IsLeft; + IsRight = ParentModel.IsRight; + Image = ParentModel.Image; + } + } +} diff --git a/src/Ryujinx/UI/ViewModels/ControllerInputViewModel.cs b/src/Ryujinx/UI/ViewModels/Input/InputViewModel.cs similarity index 92% rename from src/Ryujinx/UI/ViewModels/ControllerInputViewModel.cs rename to src/Ryujinx/UI/ViewModels/Input/InputViewModel.cs index 71ad2c127..89cc6496d 100644 --- a/src/Ryujinx/UI/ViewModels/ControllerInputViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/Input/InputViewModel.cs @@ -8,7 +8,7 @@ using Ryujinx.Ava.Common.Locale; using Ryujinx.Ava.Input; using Ryujinx.Ava.UI.Helpers; using Ryujinx.Ava.UI.Models; -using Ryujinx.Ava.UI.Views.Input; +using Ryujinx.Ava.UI.Models.Input; using Ryujinx.Ava.UI.Windows; using Ryujinx.Common; using Ryujinx.Common.Configuration; @@ -30,9 +30,9 @@ using ConfigGamepadInputId = Ryujinx.Common.Configuration.Hid.Controller.Gamepad using ConfigStickInputId = Ryujinx.Common.Configuration.Hid.Controller.StickInputId; using Key = Ryujinx.Common.Configuration.Hid.Key; -namespace Ryujinx.Ava.UI.ViewModels +namespace Ryujinx.Ava.UI.ViewModels.Input { - public class ControllerInputViewModel : BaseModel, IDisposable + public class InputViewModel : BaseModel, IDisposable { private const string Disabled = "disabled"; private const string ProControllerResource = "Ryujinx.UI.Common/Resources/Controller_ProCon.svg"; @@ -48,7 +48,7 @@ namespace Ryujinx.Ava.UI.ViewModels private int _controllerNumber; private string _controllerImage; private int _device; - private object _configuration; + private object _configViewModel; private string _profileName; private bool _isLoaded; @@ -71,13 +71,14 @@ namespace Ryujinx.Ava.UI.ViewModels public bool IsLeft { get; set; } public bool IsModified { get; set; } + public event Action NotifyChangesEvent; - public object Configuration + public object ConfigViewModel { - get => _configuration; + get => _configViewModel; set { - _configuration = value; + _configViewModel = value; OnPropertyChanged(); } @@ -180,9 +181,7 @@ namespace Ryujinx.Ava.UI.ViewModels if (!string.IsNullOrWhiteSpace(_controllerImage)) { - SvgSource source = new(default(Uri)); - - source.Load(EmbeddedResources.GetStream(_controllerImage)); + SvgSource source = SvgSource.LoadFromStream(EmbeddedResources.GetStream(_controllerImage)); image.Source = source; } @@ -232,7 +231,7 @@ namespace Ryujinx.Ava.UI.ViewModels public InputConfig Config { get; set; } - public ControllerInputViewModel(UserControl owner) : this() + public InputViewModel(UserControl owner) : this() { if (Program.PreviewerDetached) { @@ -255,7 +254,7 @@ namespace Ryujinx.Ava.UI.ViewModels } } - public ControllerInputViewModel() + public InputViewModel() { PlayerIndexes = new ObservableCollection(); Controllers = new ObservableCollection(); @@ -282,12 +281,12 @@ namespace Ryujinx.Ava.UI.ViewModels if (Config is StandardKeyboardInputConfig keyboardInputConfig) { - Configuration = new InputConfiguration(keyboardInputConfig); + ConfigViewModel = new KeyboardInputViewModel(this, new KeyboardInputConfig(keyboardInputConfig)); } if (Config is StandardControllerInputConfig controllerInputConfig) { - Configuration = new InputConfiguration(controllerInputConfig); + ConfigViewModel = new ControllerInputViewModel(this, new GamepadInputConfig(controllerInputConfig)); } } @@ -323,16 +322,6 @@ namespace Ryujinx.Ava.UI.ViewModels } } - public async void ShowMotionConfig() - { - await MotionInputView.Show(this); - } - - public async void ShowRumbleConfig() - { - await RumbleInputView.Show(this); - } - private void LoadInputDriver() { if (_device < 0) @@ -740,7 +729,7 @@ namespace Ryujinx.Ava.UI.ViewModels return; } - if (Configuration == null) + if (ConfigViewModel == null) { return; } @@ -751,35 +740,37 @@ namespace Ryujinx.Ava.UI.ViewModels return; } - - bool validFileName = ProfileName.IndexOfAny(Path.GetInvalidFileNameChars()) == -1; - - if (validFileName) - { - string path = Path.Combine(GetProfileBasePath(), ProfileName + ".json"); - - InputConfig config = null; - - if (IsKeyboard) - { - config = (Configuration as InputConfiguration).GetConfig(); - } - else if (IsController) - { - config = (Configuration as InputConfiguration).GetConfig(); - } - - config.ControllerType = Controllers[_controller].Type; - - string jsonString = JsonHelper.Serialize(config, _serializerContext.InputConfig); - - await File.WriteAllTextAsync(path, jsonString); - - LoadProfiles(); - } else { - await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogProfileInvalidProfileNameErrorMessage]); + bool validFileName = ProfileName.IndexOfAny(Path.GetInvalidFileNameChars()) == -1; + + if (validFileName) + { + string path = Path.Combine(GetProfileBasePath(), ProfileName + ".json"); + + InputConfig config = null; + + if (IsKeyboard) + { + config = (ConfigViewModel as KeyboardInputViewModel).Config.GetConfig(); + } + else if (IsController) + { + config = (ConfigViewModel as ControllerInputViewModel).Config.GetConfig(); + } + + config.ControllerType = Controllers[_controller].Type; + + string jsonString = JsonHelper.Serialize(config, _serializerContext.InputConfig); + + await File.WriteAllTextAsync(path, jsonString); + + LoadProfiles(); + } + else + { + await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogProfileInvalidProfileNameErrorMessage]); + } } } @@ -830,18 +821,18 @@ namespace Ryujinx.Ava.UI.ViewModels if (device.Type == DeviceType.Keyboard) { - var inputConfig = Configuration as InputConfiguration; + var inputConfig = (ConfigViewModel as KeyboardInputViewModel).Config; inputConfig.Id = device.Id; } else { - var inputConfig = Configuration as InputConfiguration; + var inputConfig = (ConfigViewModel as ControllerInputViewModel).Config; inputConfig.Id = device.Id.Split(" ")[0]; } var config = !IsController - ? (Configuration as InputConfiguration).GetConfig() - : (Configuration as InputConfiguration).GetConfig(); + ? (ConfigViewModel as KeyboardInputViewModel).Config.GetConfig() + : (ConfigViewModel as ControllerInputViewModel).Config.GetConfig(); config.ControllerType = Controllers[_controller].Type; config.PlayerIndex = _playerId; @@ -872,12 +863,13 @@ namespace Ryujinx.Ava.UI.ViewModels public void NotifyChanges() { - OnPropertyChanged(nameof(Configuration)); + OnPropertyChanged(nameof(ConfigViewModel)); OnPropertyChanged(nameof(IsController)); OnPropertyChanged(nameof(ShowSettings)); OnPropertyChanged(nameof(IsKeyboard)); OnPropertyChanged(nameof(IsRight)); OnPropertyChanged(nameof(IsLeft)); + NotifyChangesEvent?.Invoke(); } public void Dispose() diff --git a/src/Ryujinx/UI/ViewModels/Input/KeyboardInputViewModel.cs b/src/Ryujinx/UI/ViewModels/Input/KeyboardInputViewModel.cs new file mode 100644 index 000000000..0b530eb09 --- /dev/null +++ b/src/Ryujinx/UI/ViewModels/Input/KeyboardInputViewModel.cs @@ -0,0 +1,73 @@ +using Avalonia.Svg.Skia; +using Ryujinx.Ava.UI.Models.Input; + +namespace Ryujinx.Ava.UI.ViewModels.Input +{ + public class KeyboardInputViewModel : BaseModel + { + private KeyboardInputConfig _config; + public KeyboardInputConfig Config + { + get => _config; + set + { + _config = value; + OnPropertyChanged(); + } + } + + private bool _isLeft; + public bool IsLeft + { + get => _isLeft; + set + { + _isLeft = value; + OnPropertyChanged(); + OnPropertyChanged(nameof(HasSides)); + } + } + + private bool _isRight; + public bool IsRight + { + get => _isRight; + set + { + _isRight = value; + OnPropertyChanged(); + OnPropertyChanged(nameof(HasSides)); + } + } + + public bool HasSides => IsLeft ^ IsRight; + + private SvgImage _image; + public SvgImage Image + { + get => _image; + set + { + _image = value; + OnPropertyChanged(); + } + } + + public readonly InputViewModel ParentModel; + + public KeyboardInputViewModel(InputViewModel model, KeyboardInputConfig config) + { + ParentModel = model; + model.NotifyChangesEvent += OnParentModelChanged; + OnParentModelChanged(); + Config = config; + } + + public void OnParentModelChanged() + { + IsLeft = ParentModel.IsLeft; + IsRight = ParentModel.IsRight; + Image = ParentModel.Image; + } + } +} diff --git a/src/Ryujinx/UI/ViewModels/MotionInputViewModel.cs b/src/Ryujinx/UI/ViewModels/Input/MotionInputViewModel.cs similarity index 97% rename from src/Ryujinx/UI/ViewModels/MotionInputViewModel.cs rename to src/Ryujinx/UI/ViewModels/Input/MotionInputViewModel.cs index 0b12a51f6..c9ed8f2d4 100644 --- a/src/Ryujinx/UI/ViewModels/MotionInputViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/Input/MotionInputViewModel.cs @@ -1,4 +1,4 @@ -namespace Ryujinx.Ava.UI.ViewModels +namespace Ryujinx.Ava.UI.ViewModels.Input { public class MotionInputViewModel : BaseModel { diff --git a/src/Ryujinx/UI/ViewModels/RumbleInputViewModel.cs b/src/Ryujinx/UI/ViewModels/Input/RumbleInputViewModel.cs similarity index 92% rename from src/Ryujinx/UI/ViewModels/RumbleInputViewModel.cs rename to src/Ryujinx/UI/ViewModels/Input/RumbleInputViewModel.cs index 49de19937..8ad33cf4c 100644 --- a/src/Ryujinx/UI/ViewModels/RumbleInputViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/Input/RumbleInputViewModel.cs @@ -1,4 +1,4 @@ -namespace Ryujinx.Ava.UI.ViewModels +namespace Ryujinx.Ava.UI.ViewModels.Input { public class RumbleInputViewModel : BaseModel { diff --git a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs index 130e708c7..549eebf14 100644 --- a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs @@ -104,6 +104,7 @@ namespace Ryujinx.Ava.UI.ViewModels private double _windowHeight; private bool _isActive; + private bool _isSubMenuOpen; public ApplicationData ListSelectedApplication; public ApplicationData GridSelectedApplication; @@ -317,6 +318,17 @@ namespace Ryujinx.Ava.UI.ViewModels } } + public bool IsSubMenuOpen + { + get => _isSubMenuOpen; + set + { + _isSubMenuOpen = value; + + OnPropertyChanged(); + } + } + public bool ShowAll { get => _showAll; diff --git a/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs b/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs index b3e96b930..50a6fa99c 100644 --- a/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs @@ -7,9 +7,9 @@ using Ryujinx.Audio.Backends.SDL2; using Ryujinx.Audio.Backends.SoundIo; using Ryujinx.Ava.Common.Locale; using Ryujinx.Ava.UI.Helpers; +using Ryujinx.Ava.UI.Models.Input; using Ryujinx.Ava.UI.Windows; using Ryujinx.Common.Configuration; -using Ryujinx.Common.Configuration.Hid; using Ryujinx.Common.Configuration.Multiplayer; using Ryujinx.Common.GraphicsDriver; using Ryujinx.Common.Logging; @@ -47,7 +47,6 @@ namespace Ryujinx.Ava.UI.ViewModels private bool _isVulkanAvailable = true; private bool _directoryChanged; private readonly List _gpuIds = new(); - private KeyboardHotkeys _keyboardHotkeys; private int _graphicsBackendIndex; private int _scalingFilter; private int _scalingFilterLevel; @@ -134,6 +133,7 @@ namespace Ryujinx.Ava.UI.ViewModels public bool EnableDiscordIntegration { get; set; } public bool CheckUpdatesOnStart { get; set; } public bool ShowConfirmExit { get; set; } + public bool RememberWindowState { get; set; } public int HideCursor { get; set; } public bool EnableDockedMode { get; set; } public bool EnableKeyboard { get; set; } @@ -259,16 +259,7 @@ namespace Ryujinx.Ava.UI.ViewModels get => new(_networkInterfaces.Keys); } - public KeyboardHotkeys KeyboardHotkeys - { - get => _keyboardHotkeys; - set - { - _keyboardHotkeys = value; - - OnPropertyChanged(); - } - } + public HotkeyConfig KeyboardHotkey { get; set; } public int NetworkInterfaceIndex { @@ -422,12 +413,19 @@ namespace Ryujinx.Ava.UI.ViewModels EnableDiscordIntegration = config.EnableDiscordIntegration; CheckUpdatesOnStart = config.CheckUpdatesOnStart; ShowConfirmExit = config.ShowConfirmExit; + RememberWindowState = config.RememberWindowState; HideCursor = (int)config.HideCursor.Value; GameDirectories.Clear(); GameDirectories.AddRange(config.UI.GameDirs.Value); - BaseStyleIndex = config.UI.BaseStyle == "Light" ? 0 : 1; + BaseStyleIndex = config.UI.BaseStyle.Value switch + { + "Auto" => 0, + "Light" => 1, + "Dark" => 2, + _ => 0 + }; // Input EnableDockedMode = config.System.EnableDockedMode; @@ -435,17 +433,18 @@ namespace Ryujinx.Ava.UI.ViewModels EnableMouse = config.Hid.EnableMouse; // Keyboard Hotkeys - KeyboardHotkeys = config.Hid.Hotkeys.Value; + KeyboardHotkey = new HotkeyConfig(config.Hid.Hotkeys.Value); // System Region = (int)config.System.Region.Value; Language = (int)config.System.Language.Value; TimeZone = config.System.TimeZone; - DateTime currentDateTime = DateTime.Now; - + DateTime currentHostDateTime = DateTime.Now; + TimeSpan systemDateTimeOffset = TimeSpan.FromSeconds(config.System.SystemTimeOffset); + DateTime currentDateTime = currentHostDateTime.Add(systemDateTimeOffset); CurrentDate = currentDateTime.Date; - CurrentTime = currentDateTime.TimeOfDay.Add(TimeSpan.FromSeconds(config.System.SystemTimeOffset)); + CurrentTime = currentDateTime.TimeOfDay; EnableVsync = config.Graphics.EnableVsync; EnableFsIntegrityChecks = config.System.EnableFsIntegrityChecks; @@ -507,6 +506,7 @@ namespace Ryujinx.Ava.UI.ViewModels config.EnableDiscordIntegration.Value = EnableDiscordIntegration; config.CheckUpdatesOnStart.Value = CheckUpdatesOnStart; config.ShowConfirmExit.Value = ShowConfirmExit; + config.RememberWindowState.Value = RememberWindowState; config.HideCursor.Value = (HideCursorMode)HideCursor; if (_directoryChanged) @@ -515,7 +515,13 @@ namespace Ryujinx.Ava.UI.ViewModels config.UI.GameDirs.Value = gameDirs; } - config.UI.BaseStyle.Value = BaseStyleIndex == 0 ? "Light" : "Dark"; + config.UI.BaseStyle.Value = BaseStyleIndex switch + { + 0 => "Auto", + 1 => "Light", + 2 => "Dark", + _ => "Auto" + }; // Input config.System.EnableDockedMode.Value = EnableDockedMode; @@ -523,7 +529,7 @@ namespace Ryujinx.Ava.UI.ViewModels config.Hid.EnableMouse.Value = EnableMouse; // Keyboard Hotkeys - config.Hid.Hotkeys.Value = KeyboardHotkeys; + config.Hid.Hotkeys.Value = KeyboardHotkey.GetConfig(); // System config.System.Region.Value = (Region)Region; diff --git a/src/Ryujinx/UI/ViewModels/UserFirmwareAvatarSelectorViewModel.cs b/src/Ryujinx/UI/ViewModels/UserFirmwareAvatarSelectorViewModel.cs index 89b591229..12adfe94b 100644 --- a/src/Ryujinx/UI/ViewModels/UserFirmwareAvatarSelectorViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/UserFirmwareAvatarSelectorViewModel.cs @@ -151,7 +151,7 @@ namespace Ryujinx.Ava.UI.ViewModels reader.ReadInt64(); // Padding byte[] input = new byte[stream.Length - stream.Position]; - stream.Read(input, 0, input.Length); + stream.ReadExactly(input, 0, input.Length); uint inputOffset = 0; diff --git a/src/Ryujinx/UI/Views/Input/ControllerInputView.axaml b/src/Ryujinx/UI/Views/Input/ControllerInputView.axaml index 99f2b6b69..08bdf90f4 100644 --- a/src/Ryujinx/UI/Views/Input/ControllerInputView.axaml +++ b/src/Ryujinx/UI/Views/Input/ControllerInputView.axaml @@ -1,13 +1,11 @@ @@ -34,192 +33,10 @@ HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Orientation="Vertical"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + MinHeight="450"> @@ -258,9 +75,9 @@ VerticalAlignment="Center" Text="{locale:Locale ControllerSettingsTriggerZL}" TextAlignment="Center" /> - + @@ -274,9 +91,9 @@ VerticalAlignment="Center" Text="{locale:Locale ControllerSettingsTriggerL}" TextAlignment="Center" /> - + @@ -290,9 +107,9 @@ VerticalAlignment="Center" Text="{locale:Locale ControllerSettingsButtonMinus}" TextAlignment="Center" /> - + @@ -312,100 +129,8 @@ Margin="0,0,0,10" HorizontalAlignment="Center" Text="{locale:Locale ControllerSettingsLStick}" /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + @@ -416,9 +141,9 @@ VerticalAlignment="Center" Text="{locale:Locale ControllerSettingsStickButton}" TextAlignment="Center" /> - + @@ -433,22 +158,22 @@ VerticalAlignment="Center" Text="{locale:Locale ControllerSettingsStickStick}" TextAlignment="Center" /> - + - + - + - + + Value="{Binding Config.DeadzoneLeft, Mode=TwoWay}" /> + Text="{Binding Config.DeadzoneLeft, StringFormat=\{0:0.00\}}" /> + Value="{Binding Config.RangeLeft, Mode=TwoWay}" /> + Text="{Binding Config.RangeLeft, StringFormat=\{0:0.00\}}" /> @@ -526,9 +251,9 @@ VerticalAlignment="Center" Text="{locale:Locale ControllerSettingsDPadUp}" TextAlignment="Center" /> - + @@ -543,9 +268,9 @@ VerticalAlignment="Center" Text="{locale:Locale ControllerSettingsDPadDown}" TextAlignment="Center" /> - + @@ -560,9 +285,9 @@ VerticalAlignment="Center" Text="{locale:Locale ControllerSettingsDPadLeft}" TextAlignment="Center" /> - + @@ -577,9 +302,9 @@ VerticalAlignment="Center" Text="{locale:Locale ControllerSettingsDPadRight}" TextAlignment="Center" /> - + @@ -592,6 +317,13 @@ Grid.Column="1" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"> + + + Value="{Binding Config.TriggerThreshold, Mode=TwoWay}" /> + Text="{Binding Config.TriggerThreshold, StringFormat=\{0:0.00\}}" /> - + - + IsVisible="{Binding IsLeft}" + Orientation="Horizontal"> - - - - + + + + - + IsVisible="{Binding IsLeft}" + Orientation="Horizontal"> - - - - + + + + - + IsVisible="{Binding IsRight}" + Orientation="Horizontal"> - - - - + + + + - + IsVisible="{Binding IsRight}" + Orientation="Horizontal"> - + + + + - - + HorizontalAlignment="Stretch"> @@ -721,7 +449,7 @@ Margin="10" MinWidth="0" Grid.Column="0" - IsChecked="{ReflectionBinding Configuration.EnableMotion, Mode=TwoWay}"> + IsChecked="{Binding Config.EnableMotion, Mode=TwoWay}"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Ryujinx/UI/Views/Input/InputView.axaml.cs b/src/Ryujinx/UI/Views/Input/InputView.axaml.cs new file mode 100644 index 000000000..356381a8a --- /dev/null +++ b/src/Ryujinx/UI/Views/Input/InputView.axaml.cs @@ -0,0 +1,61 @@ +using Avalonia.Controls; +using Ryujinx.Ava.Common.Locale; +using Ryujinx.Ava.UI.Helpers; +using Ryujinx.Ava.UI.Models; +using Ryujinx.Ava.UI.ViewModels.Input; + +namespace Ryujinx.Ava.UI.Views.Input +{ + public partial class InputView : UserControl + { + private bool _dialogOpen; + private InputViewModel ViewModel { get; set; } + + public InputView() + { + DataContext = ViewModel = new InputViewModel(this); + + InitializeComponent(); + } + + public void SaveCurrentProfile() + { + ViewModel.Save(); + } + + private async void PlayerIndexBox_OnSelectionChanged(object sender, SelectionChangedEventArgs e) + { + if (ViewModel.IsModified && !_dialogOpen) + { + _dialogOpen = true; + + var result = await ContentDialogHelper.CreateConfirmationDialog( + LocaleManager.Instance[LocaleKeys.DialogControllerSettingsModifiedConfirmMessage], + LocaleManager.Instance[LocaleKeys.DialogControllerSettingsModifiedConfirmSubMessage], + LocaleManager.Instance[LocaleKeys.InputDialogYes], + LocaleManager.Instance[LocaleKeys.InputDialogNo], + LocaleManager.Instance[LocaleKeys.RyujinxConfirm]); + + if (result == UserResult.Yes) + { + ViewModel.Save(); + } + + _dialogOpen = false; + + ViewModel.IsModified = false; + + if (e.AddedItems.Count > 0) + { + var player = (PlayerModel)e.AddedItems[0]; + ViewModel.PlayerId = player.Id; + } + } + } + + public void Dispose() + { + ViewModel.Dispose(); + } + } +} diff --git a/src/Ryujinx/UI/Views/Input/KeyboardInputView.axaml b/src/Ryujinx/UI/Views/Input/KeyboardInputView.axaml new file mode 100644 index 000000000..e4566f463 --- /dev/null +++ b/src/Ryujinx/UI/Views/Input/KeyboardInputView.axaml @@ -0,0 +1,675 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Ryujinx/UI/Views/Input/KeyboardInputView.axaml.cs b/src/Ryujinx/UI/Views/Input/KeyboardInputView.axaml.cs new file mode 100644 index 000000000..f17c7496c --- /dev/null +++ b/src/Ryujinx/UI/Views/Input/KeyboardInputView.axaml.cs @@ -0,0 +1,208 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.LogicalTree; +using Ryujinx.Ava.UI.Helpers; +using Ryujinx.Ava.UI.ViewModels.Input; +using Ryujinx.Input; +using Ryujinx.Input.Assigner; +using Key = Ryujinx.Common.Configuration.Hid.Key; + +namespace Ryujinx.Ava.UI.Views.Input +{ + public partial class KeyboardInputView : UserControl + { + private ButtonKeyAssigner _currentAssigner; + + public KeyboardInputView() + { + InitializeComponent(); + + foreach (ILogical visual in SettingButtons.GetLogicalDescendants()) + { + if (visual is ToggleButton button and not CheckBox) + { + button.IsCheckedChanged += Button_IsCheckedChanged; + } + } + } + + protected override void OnPointerReleased(PointerReleasedEventArgs e) + { + base.OnPointerReleased(e); + + if (_currentAssigner != null && _currentAssigner.ToggledButton != null && !_currentAssigner.ToggledButton.IsPointerOver) + { + _currentAssigner.Cancel(); + } + } + + private void Button_IsCheckedChanged(object sender, RoutedEventArgs e) + { + if (sender is ToggleButton button) + { + if ((bool)button.IsChecked) + { + if (_currentAssigner != null && button == _currentAssigner.ToggledButton) + { + return; + } + + if (_currentAssigner == null) + { + _currentAssigner = new ButtonKeyAssigner(button); + + Focus(NavigationMethod.Pointer); + + PointerPressed += MouseClick; + + var viewModel = (DataContext as KeyboardInputViewModel); + + IKeyboard keyboard = (IKeyboard)viewModel.ParentModel.AvaloniaKeyboardDriver.GetGamepad("0"); // Open Avalonia keyboard for cancel operations. + IButtonAssigner assigner = CreateButtonAssigner(); + + _currentAssigner.ButtonAssigned += (sender, e) => + { + if (e.ButtonValue.HasValue) + { + var buttonValue = e.ButtonValue.Value; + viewModel.ParentModel.IsModified = true; + + switch (button.Name) + { + case "ButtonZl": + viewModel.Config.ButtonZl = buttonValue.AsHidType(); + break; + case "ButtonL": + viewModel.Config.ButtonL = buttonValue.AsHidType(); + break; + case "ButtonMinus": + viewModel.Config.ButtonMinus = buttonValue.AsHidType(); + break; + case "LeftStickButton": + viewModel.Config.LeftStickButton = buttonValue.AsHidType(); + break; + case "LeftStickUp": + viewModel.Config.LeftStickUp = buttonValue.AsHidType(); + break; + case "LeftStickDown": + viewModel.Config.LeftStickDown = buttonValue.AsHidType(); + break; + case "LeftStickRight": + viewModel.Config.LeftStickRight = buttonValue.AsHidType(); + break; + case "LeftStickLeft": + viewModel.Config.LeftStickLeft = buttonValue.AsHidType(); + break; + case "DpadUp": + viewModel.Config.DpadUp = buttonValue.AsHidType(); + break; + case "DpadDown": + viewModel.Config.DpadDown = buttonValue.AsHidType(); + break; + case "DpadLeft": + viewModel.Config.DpadLeft = buttonValue.AsHidType(); + break; + case "DpadRight": + viewModel.Config.DpadRight = buttonValue.AsHidType(); + break; + case "LeftButtonSr": + viewModel.Config.LeftButtonSr = buttonValue.AsHidType(); + break; + case "LeftButtonSl": + viewModel.Config.LeftButtonSl = buttonValue.AsHidType(); + break; + case "RightButtonSr": + viewModel.Config.RightButtonSr = buttonValue.AsHidType(); + break; + case "RightButtonSl": + viewModel.Config.RightButtonSl = buttonValue.AsHidType(); + break; + case "ButtonZr": + viewModel.Config.ButtonZr = buttonValue.AsHidType(); + break; + case "ButtonR": + viewModel.Config.ButtonR = buttonValue.AsHidType(); + break; + case "ButtonPlus": + viewModel.Config.ButtonPlus = buttonValue.AsHidType(); + break; + case "ButtonA": + viewModel.Config.ButtonA = buttonValue.AsHidType(); + break; + case "ButtonB": + viewModel.Config.ButtonB = buttonValue.AsHidType(); + break; + case "ButtonX": + viewModel.Config.ButtonX = buttonValue.AsHidType(); + break; + case "ButtonY": + viewModel.Config.ButtonY = buttonValue.AsHidType(); + break; + case "RightStickButton": + viewModel.Config.RightStickButton = buttonValue.AsHidType(); + break; + case "RightStickUp": + viewModel.Config.RightStickUp = buttonValue.AsHidType(); + break; + case "RightStickDown": + viewModel.Config.RightStickDown = buttonValue.AsHidType(); + break; + case "RightStickRight": + viewModel.Config.RightStickRight = buttonValue.AsHidType(); + break; + case "RightStickLeft": + viewModel.Config.RightStickLeft = buttonValue.AsHidType(); + break; + } + } + }; + + _currentAssigner.GetInputAndAssign(assigner, keyboard); + } + else + { + if (_currentAssigner != null) + { + _currentAssigner.Cancel(); + _currentAssigner = null; + button.IsChecked = false; + } + } + } + else + { + _currentAssigner?.Cancel(); + _currentAssigner = null; + } + } + } + + private void MouseClick(object sender, PointerPressedEventArgs e) + { + bool shouldUnbind = e.GetCurrentPoint(this).Properties.IsMiddleButtonPressed; + + _currentAssigner?.Cancel(shouldUnbind); + + PointerPressed -= MouseClick; + } + + private IButtonAssigner CreateButtonAssigner() + { + IButtonAssigner assigner; + + assigner = new KeyboardKeyAssigner((IKeyboard)(DataContext as KeyboardInputViewModel).ParentModel.SelectedGamepad); + + return assigner; + } + + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnDetachedFromVisualTree(e); + _currentAssigner?.Cancel(); + _currentAssigner = null; + } + } +} diff --git a/src/Ryujinx/UI/Views/Input/MotionInputView.axaml b/src/Ryujinx/UI/Views/Input/MotionInputView.axaml index a6b587f67..0d018e297 100644 --- a/src/Ryujinx/UI/Views/Input/MotionInputView.axaml +++ b/src/Ryujinx/UI/Views/Input/MotionInputView.axaml @@ -6,7 +6,7 @@ xmlns:controls="clr-namespace:Ryujinx.Ava.UI.Controls" xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale" - xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels" + xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels.Input" mc:Ignorable="d" x:Class="Ryujinx.Ava.UI.Views.Input.MotionInputView" x:DataType="viewModels:MotionInputViewModel" diff --git a/src/Ryujinx/UI/Views/Input/MotionInputView.axaml.cs b/src/Ryujinx/UI/Views/Input/MotionInputView.axaml.cs index 1b340752b..2304364b6 100644 --- a/src/Ryujinx/UI/Views/Input/MotionInputView.axaml.cs +++ b/src/Ryujinx/UI/Views/Input/MotionInputView.axaml.cs @@ -1,9 +1,7 @@ using Avalonia.Controls; using FluentAvalonia.UI.Controls; using Ryujinx.Ava.Common.Locale; -using Ryujinx.Ava.UI.Models; -using Ryujinx.Ava.UI.ViewModels; -using Ryujinx.Common.Configuration.Hid.Controller; +using Ryujinx.Ava.UI.ViewModels.Input; using System.Threading.Tasks; namespace Ryujinx.Ava.UI.Views.Input @@ -19,7 +17,7 @@ namespace Ryujinx.Ava.UI.Views.Input public MotionInputView(ControllerInputViewModel viewModel) { - var config = viewModel.Configuration as InputConfiguration; + var config = viewModel.Config; _viewModel = new MotionInputViewModel { @@ -51,7 +49,7 @@ namespace Ryujinx.Ava.UI.Views.Input }; contentDialog.PrimaryButtonClick += (sender, args) => { - var config = viewModel.Configuration as InputConfiguration; + var config = viewModel.Config; config.Slot = content._viewModel.Slot; config.Sensitivity = content._viewModel.Sensitivity; config.GyroDeadzone = content._viewModel.GyroDeadzone; diff --git a/src/Ryujinx/UI/Views/Input/RumbleInputView.axaml b/src/Ryujinx/UI/Views/Input/RumbleInputView.axaml index 5b7087a47..1beb1f06e 100644 --- a/src/Ryujinx/UI/Views/Input/RumbleInputView.axaml +++ b/src/Ryujinx/UI/Views/Input/RumbleInputView.axaml @@ -5,7 +5,7 @@ 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:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels" + xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels.Input" mc:Ignorable="d" x:Class="Ryujinx.Ava.UI.Views.Input.RumbleInputView" x:DataType="viewModels:RumbleInputViewModel" diff --git a/src/Ryujinx/UI/Views/Input/RumbleInputView.axaml.cs b/src/Ryujinx/UI/Views/Input/RumbleInputView.axaml.cs index 9307f872c..58a4b416b 100644 --- a/src/Ryujinx/UI/Views/Input/RumbleInputView.axaml.cs +++ b/src/Ryujinx/UI/Views/Input/RumbleInputView.axaml.cs @@ -1,9 +1,7 @@ using Avalonia.Controls; using FluentAvalonia.UI.Controls; using Ryujinx.Ava.Common.Locale; -using Ryujinx.Ava.UI.Models; -using Ryujinx.Ava.UI.ViewModels; -using Ryujinx.Common.Configuration.Hid.Controller; +using Ryujinx.Ava.UI.ViewModels.Input; using System.Threading.Tasks; namespace Ryujinx.Ava.UI.Views.Input @@ -19,7 +17,7 @@ namespace Ryujinx.Ava.UI.Views.Input public RumbleInputView(ControllerInputViewModel viewModel) { - var config = viewModel.Configuration as InputConfiguration; + var config = viewModel.Config; _viewModel = new RumbleInputViewModel { @@ -47,7 +45,7 @@ namespace Ryujinx.Ava.UI.Views.Input contentDialog.PrimaryButtonClick += (sender, args) => { - var config = viewModel.Configuration as InputConfiguration; + var config = viewModel.Config; config.StrongRumble = content._viewModel.StrongRumble; config.WeakRumble = content._viewModel.WeakRumble; }; diff --git a/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml index 30358adab..ac3736110 100644 --- a/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml +++ b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml @@ -1,4 +1,4 @@ - + HorizontalAlignment="Left" + IsOpen="{Binding IsSubMenuOpen, Mode=OneWayToSource}"> @@ -185,6 +186,12 @@ + + + + + + + { + ViewModel.WindowState = WindowState.Normal; + + height += (int)Window.StatusBarHeight + (int)Window.MenuBarHeight; + + Window.Arrange(new Rect(Window.Position.X, Window.Position.Y, width, height)); + }); + } + } + public async void CheckForUpdates(object sender, RoutedEventArgs e) { if (Updater.CanUpdate(true)) diff --git a/src/Ryujinx/UI/Views/Settings/SettingsHotkeysView.axaml b/src/Ryujinx/UI/Views/Settings/SettingsHotkeysView.axaml index f624dc1d6..62a69f9e6 100644 --- a/src/Ryujinx/UI/Views/Settings/SettingsHotkeysView.axaml +++ b/src/Ryujinx/UI/Views/Settings/SettingsHotkeysView.axaml @@ -9,6 +9,7 @@ xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers" mc:Ignorable="d" x:DataType="viewModels:SettingsViewModel" + x:CompileBindings="True" Focusable="True"> @@ -16,6 +17,23 @@ + + + + + + - - - - - - + + + + + + - - - - + + + + - - - - + + + + - - - - + + + + - - - - + + + + - - - - + + + + - - - - + + + + - - - - + + + + - - - - + + + + diff --git a/src/Ryujinx/UI/Views/Settings/SettingsHotkeysView.axaml.cs b/src/Ryujinx/UI/Views/Settings/SettingsHotkeysView.axaml.cs index b006d703f..fb0fe2bb1 100644 --- a/src/Ryujinx/UI/Views/Settings/SettingsHotkeysView.axaml.cs +++ b/src/Ryujinx/UI/Views/Settings/SettingsHotkeysView.axaml.cs @@ -2,10 +2,13 @@ using Avalonia.Controls; using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.Interactivity; +using Avalonia.LogicalTree; using Ryujinx.Ava.Input; using Ryujinx.Ava.UI.Helpers; +using Ryujinx.Ava.UI.ViewModels; using Ryujinx.Input; using Ryujinx.Input.Assigner; +using Key = Ryujinx.Common.Configuration.Hid.Key; namespace Ryujinx.Ava.UI.Views.Settings { @@ -17,9 +20,28 @@ namespace Ryujinx.Ava.UI.Views.Settings public SettingsHotkeysView() { InitializeComponent(); + + foreach (ILogical visual in SettingButtons.GetLogicalDescendants()) + { + if (visual is ToggleButton button and not CheckBox) + { + button.IsCheckedChanged += Button_IsCheckedChanged; + } + } + _avaloniaKeyboardDriver = new AvaloniaKeyboardDriver(this); } + protected override void OnPointerReleased(PointerReleasedEventArgs e) + { + base.OnPointerReleased(e); + + if (!_currentAssigner?.ToggledButton?.IsPointerOver ?? false) + { + _currentAssigner.Cancel(); + } + } + private void MouseClick(object sender, PointerPressedEventArgs e) { bool shouldUnbind = e.GetCurrentPoint(this).Properties.IsMiddleButtonPressed; @@ -29,53 +51,94 @@ namespace Ryujinx.Ava.UI.Views.Settings PointerPressed -= MouseClick; } - private void Button_Checked(object sender, RoutedEventArgs e) + private void Button_IsCheckedChanged(object sender, RoutedEventArgs e) { if (sender is ToggleButton button) { - if (_currentAssigner != null && button == _currentAssigner.ToggledButton) + if ((bool)button.IsChecked) { - return; - } + if (_currentAssigner != null && button == _currentAssigner.ToggledButton) + { + return; + } - if (_currentAssigner == null && button.IsChecked != null && (bool)button.IsChecked) - { - _currentAssigner = new ButtonKeyAssigner(button); + if (_currentAssigner == null) + { + _currentAssigner = new ButtonKeyAssigner(button); - this.Focus(NavigationMethod.Pointer); + this.Focus(NavigationMethod.Pointer); - PointerPressed += MouseClick; + PointerPressed += MouseClick; - var keyboard = (IKeyboard)_avaloniaKeyboardDriver.GetGamepad(_avaloniaKeyboardDriver.GamepadsIds[0]); - IButtonAssigner assigner = new KeyboardKeyAssigner(keyboard); + var keyboard = (IKeyboard)_avaloniaKeyboardDriver.GetGamepad("0"); + IButtonAssigner assigner = new KeyboardKeyAssigner(keyboard); - _currentAssigner.GetInputAndAssign(assigner); + _currentAssigner.ButtonAssigned += (sender, e) => + { + if (e.ButtonValue.HasValue) + { + var viewModel = (DataContext) as SettingsViewModel; + var buttonValue = e.ButtonValue.Value; + + switch (button.Name) + { + case "ToggleVsync": + viewModel.KeyboardHotkey.ToggleVsync = buttonValue.AsHidType(); + break; + case "Screenshot": + viewModel.KeyboardHotkey.Screenshot = buttonValue.AsHidType(); + break; + case "ShowUI": + viewModel.KeyboardHotkey.ShowUI = buttonValue.AsHidType(); + break; + case "Pause": + viewModel.KeyboardHotkey.Pause = buttonValue.AsHidType(); + break; + case "ToggleMute": + viewModel.KeyboardHotkey.ToggleMute = buttonValue.AsHidType(); + break; + case "ResScaleUp": + viewModel.KeyboardHotkey.ResScaleUp = buttonValue.AsHidType(); + break; + case "ResScaleDown": + viewModel.KeyboardHotkey.ResScaleDown = buttonValue.AsHidType(); + break; + case "VolumeUp": + viewModel.KeyboardHotkey.VolumeUp = buttonValue.AsHidType(); + break; + case "VolumeDown": + viewModel.KeyboardHotkey.VolumeDown = buttonValue.AsHidType(); + break; + } + } + }; + + _currentAssigner.GetInputAndAssign(assigner, keyboard); + } + else + { + if (_currentAssigner != null) + { + _currentAssigner.Cancel(); + _currentAssigner = null; + button.IsChecked = false; + } + } } else { - if (_currentAssigner != null) - { - ToggleButton oldButton = _currentAssigner.ToggledButton; - - _currentAssigner.Cancel(); - _currentAssigner = null; - - button.IsChecked = false; - } + _currentAssigner?.Cancel(); + _currentAssigner = null; } } } - private void Button_Unchecked(object sender, RoutedEventArgs e) - { - _currentAssigner?.Cancel(); - _currentAssigner = null; - } - public void Dispose() { _currentAssigner?.Cancel(); _currentAssigner = null; + + _avaloniaKeyboardDriver.Dispose(); } } } diff --git a/src/Ryujinx/UI/Views/Settings/SettingsInputView.axaml b/src/Ryujinx/UI/Views/Settings/SettingsInputView.axaml index 81f4b68b7..55c2ed6e3 100644 --- a/src/Ryujinx/UI/Views/Settings/SettingsInputView.axaml +++ b/src/Ryujinx/UI/Views/Settings/SettingsInputView.axaml @@ -27,9 +27,9 @@ - + Name="InputView" /> diff --git a/src/Ryujinx/UI/Views/Settings/SettingsInputView.axaml.cs b/src/Ryujinx/UI/Views/Settings/SettingsInputView.axaml.cs index e75c9f0cc..55b69af06 100644 --- a/src/Ryujinx/UI/Views/Settings/SettingsInputView.axaml.cs +++ b/src/Ryujinx/UI/Views/Settings/SettingsInputView.axaml.cs @@ -11,7 +11,7 @@ namespace Ryujinx.Ava.UI.Views.Settings public void Dispose() { - ControllerSettings.Dispose(); + InputView.Dispose(); } } } diff --git a/src/Ryujinx/UI/Views/Settings/SettingsUIView.axaml b/src/Ryujinx/UI/Views/Settings/SettingsUIView.axaml index 6504637e6..f9b9be44b 100644 --- a/src/Ryujinx/UI/Views/Settings/SettingsUIView.axaml +++ b/src/Ryujinx/UI/Views/Settings/SettingsUIView.axaml @@ -36,6 +36,9 @@ + + + + + + diff --git a/src/Ryujinx/UI/Windows/MainWindow.axaml.cs b/src/Ryujinx/UI/Windows/MainWindow.axaml.cs index 0750221c1..11ce139d4 100644 --- a/src/Ryujinx/UI/Windows/MainWindow.axaml.cs +++ b/src/Ryujinx/UI/Windows/MainWindow.axaml.cs @@ -2,6 +2,7 @@ using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Primitives; using Avalonia.Interactivity; +using Avalonia.Platform; using Avalonia.Threading; using FluentAvalonia.UI.Controls; using Ryujinx.Ava.Common; @@ -56,6 +57,9 @@ namespace Ryujinx.Ava.UI.Windows public static bool ShowKeyErrorOnLoad { get; set; } public ApplicationLibrary ApplicationLibrary { get; set; } + public readonly double StatusBarHeight; + public readonly double MenuBarHeight; + public MainWindow() { ViewModel = new MainWindowViewModel(); @@ -64,8 +68,6 @@ namespace Ryujinx.Ava.UI.Windows DataContext = ViewModel; - SetWindowSizePosition(); - InitializeComponent(); Load(); @@ -74,10 +76,14 @@ namespace Ryujinx.Ava.UI.Windows ViewModel.Title = $"Ryujinx {Program.Version}"; // NOTE: Height of MenuBar and StatusBar is not usable here, since it would still be 0 at this point. - double barHeight = MenuBar.MinHeight + StatusBarView.StatusBar.MinHeight; + StatusBarHeight = StatusBarView.StatusBar.MinHeight; + MenuBarHeight = MenuBar.MinHeight; + double barHeight = MenuBarHeight + StatusBarHeight; Height = ((Height - barHeight) / Program.WindowScaleFactor) + barHeight; Width /= Program.WindowScaleFactor; + SetWindowSizePosition(); + if (Program.PreviewerDetached) { InputManager = new InputManager(new AvaloniaKeyboardDriver(this), new SDL2GamepadDriver()); @@ -87,6 +93,29 @@ namespace Ryujinx.Ava.UI.Windows } } + /// + /// Event handler for detecting OS theme change when using "Follow OS theme" option + /// + private void OnPlatformColorValuesChanged(object sender, PlatformColorValues e) + { + if (Application.Current is App app) + { + app.ApplyConfiguredTheme(); + } + } + + protected override void OnClosed(EventArgs e) + { + base.OnClosed(e); + if (PlatformSettings != null) + { + /// + /// Unsubscribe to the ColorValuesChanged event + /// + PlatformSettings.ColorValuesChanged -= OnPlatformColorValuesChanged; + } + } + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { base.OnApplyTemplate(e); @@ -319,6 +348,17 @@ namespace Ryujinx.Ava.UI.Windows private void SetWindowSizePosition() { + if (!ConfigurationState.Instance.RememberWindowState) + { + ViewModel.WindowHeight = (720 + StatusBarHeight + MenuBarHeight) * Program.WindowScaleFactor; + ViewModel.WindowWidth = 1280 * Program.WindowScaleFactor; + + WindowState = WindowState.Normal; + WindowStartupLocation = WindowStartupLocation.CenterScreen; + + return; + } + PixelPoint savedPoint = new(ConfigurationState.Instance.UI.WindowStartup.WindowPositionX, ConfigurationState.Instance.UI.WindowStartup.WindowPositionY); @@ -353,14 +393,18 @@ namespace Ryujinx.Ava.UI.Windows private void SaveWindowSizePosition() { - ConfigurationState.Instance.UI.WindowStartup.WindowSizeHeight.Value = (int)Height; - ConfigurationState.Instance.UI.WindowStartup.WindowSizeWidth.Value = (int)Width; - - ConfigurationState.Instance.UI.WindowStartup.WindowPositionX.Value = Position.X; - ConfigurationState.Instance.UI.WindowStartup.WindowPositionY.Value = Position.Y; - ConfigurationState.Instance.UI.WindowStartup.WindowMaximized.Value = WindowState == WindowState.Maximized; + // Only save rectangle properties if the window is not in a maximized state. + if (WindowState != WindowState.Maximized) + { + ConfigurationState.Instance.UI.WindowStartup.WindowSizeHeight.Value = (int)Height; + ConfigurationState.Instance.UI.WindowStartup.WindowSizeWidth.Value = (int)Width; + + ConfigurationState.Instance.UI.WindowStartup.WindowPositionX.Value = Position.X; + ConfigurationState.Instance.UI.WindowStartup.WindowPositionY.Value = Position.Y; + } + MainWindowViewModel.SaveConfig(); } @@ -370,6 +414,11 @@ namespace Ryujinx.Ava.UI.Windows Initialize(); + /// + /// Subscribe to the ColorValuesChanged event + /// + PlatformSettings.ColorValuesChanged += OnPlatformColorValuesChanged; + ViewModel.Initialize( ContentManager, StorageProvider, @@ -484,7 +533,10 @@ namespace Ryujinx.Ava.UI.Windows return; } - SaveWindowSizePosition(); + if (ConfigurationState.Instance.RememberWindowState) + { + SaveWindowSizePosition(); + } ApplicationLibrary.CancelLoading(); InputManager.Dispose(); diff --git a/src/Ryujinx/UI/Windows/SettingsWindow.axaml.cs b/src/Ryujinx/UI/Windows/SettingsWindow.axaml.cs index d7bb0b883..314501c52 100644 --- a/src/Ryujinx/UI/Windows/SettingsWindow.axaml.cs +++ b/src/Ryujinx/UI/Windows/SettingsWindow.axaml.cs @@ -37,7 +37,7 @@ namespace Ryujinx.Ava.UI.Windows public void SaveSettings() { - InputPage.ControllerSettings?.SaveCurrentProfile(); + InputPage.InputView?.SaveCurrentProfile(); if (Owner is MainWindow window && ViewModel.DirectoryChanged) {