Compare commits
20 Commits
Author | SHA1 | Date | |
---|---|---|---|
c1ed150949 | |||
c634eb4054 | |||
eb1ce41b00 | |||
2f427deb67 | |||
8f51938e2b | |||
4d84df9487 | |||
9ec8b2c01a | |||
091230af22 | |||
3aea194606 | |||
cdccf89e10 | |||
2ca0b17339 | |||
47639e6eeb | |||
cd78adf07f | |||
a3dc295c5f | |||
2ef4f92b07 | |||
2b6cc4b353 | |||
075575200d | |||
3a3b51893e | |||
44dbab3848 | |||
cada4d04ef |
1
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
1
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@ -1,6 +1,7 @@
|
||||
name: Feature Request
|
||||
description: Suggest a new feature for Ryujinx.
|
||||
title: "[Feature Request]"
|
||||
labels: enhancement
|
||||
body:
|
||||
- type: textarea
|
||||
id: overview
|
||||
|
@ -8,10 +8,10 @@
|
||||
<PackageVersion Include="Avalonia.Desktop" Version="11.0.10" />
|
||||
<PackageVersion Include="Avalonia.Diagnostics" Version="11.0.10" />
|
||||
<PackageVersion Include="Avalonia.Markup.Xaml.Loader" Version="11.0.10" />
|
||||
<PackageVersion Include="Avalonia.Svg" Version="11.0.0.16" />
|
||||
<PackageVersion Include="Avalonia.Svg.Skia" Version="11.0.0.16" />
|
||||
<PackageVersion Include="Avalonia.Svg" Version="11.0.0.18" />
|
||||
<PackageVersion Include="Avalonia.Svg.Skia" Version="11.0.0.18" />
|
||||
<PackageVersion Include="CommandLineParser" Version="2.9.1" />
|
||||
<PackageVersion Include="Concentus" Version="1.1.7" />
|
||||
<PackageVersion Include="Concentus" Version="2.2.0" />
|
||||
<PackageVersion Include="DiscordRichPresence" Version="1.2.1.24" />
|
||||
<PackageVersion Include="DynamicData" Version="8.4.1" />
|
||||
<PackageVersion Include="FluentAvaloniaUI" Version="2.0.5" />
|
||||
@ -49,4 +49,4 @@
|
||||
<PackageVersion Include="System.Management" Version="8.0.0" />
|
||||
<PackageVersion Include="UnicornEngine.Unicorn" Version="2.0.2-rc1-fb78016" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
@ -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).
|
||||
|
||||
|
@ -15,7 +15,6 @@ namespace Ryujinx.Audio.Renderer.Common
|
||||
{
|
||||
public const int Align = 0x10;
|
||||
public const int BiquadStateOffset = 0x0;
|
||||
public const int BiquadStateSize = 0x10;
|
||||
|
||||
/// <summary>
|
||||
/// The state of the biquad filters of this voice.
|
||||
|
@ -16,10 +16,15 @@ namespace Ryujinx.Audio.Renderer.Dsp
|
||||
/// <param name="parameter">The biquad filter parameter</param>
|
||||
/// <param name="state">The biquad filter state</param>
|
||||
/// <param name="outputBuffer">The output buffer to write the result</param>
|
||||
/// <param name="inputBuffer">The input buffer to write the result</param>
|
||||
/// <param name="inputBuffer">The input buffer to read the samples from</param>
|
||||
/// <param name="sampleCount">The count of samples to process</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void ProcessBiquadFilter(ref BiquadFilterParameter parameter, ref BiquadFilterState state, Span<float> outputBuffer, ReadOnlySpan<float> inputBuffer, uint sampleCount)
|
||||
public static void ProcessBiquadFilter(
|
||||
ref BiquadFilterParameter parameter,
|
||||
ref BiquadFilterState state,
|
||||
Span<float> outputBuffer,
|
||||
ReadOnlySpan<float> 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
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Apply a single biquad filter and mix the result into the output buffer.
|
||||
/// </summary>
|
||||
/// <remarks>This is implemented with a direct form 1.</remarks>
|
||||
/// <param name="parameter">The biquad filter parameter</param>
|
||||
/// <param name="state">The biquad filter state</param>
|
||||
/// <param name="outputBuffer">The output buffer to write the result</param>
|
||||
/// <param name="inputBuffer">The input buffer to read the samples from</param>
|
||||
/// <param name="sampleCount">The count of samples to process</param>
|
||||
/// <param name="volume">Mix volume</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void ProcessBiquadFilterAndMix(
|
||||
ref BiquadFilterParameter parameter,
|
||||
ref BiquadFilterState state,
|
||||
Span<float> outputBuffer,
|
||||
ReadOnlySpan<float> 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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Apply a single biquad filter and mix the result into the output buffer with volume ramp.
|
||||
/// </summary>
|
||||
/// <remarks>This is implemented with a direct form 1.</remarks>
|
||||
/// <param name="parameter">The biquad filter parameter</param>
|
||||
/// <param name="state">The biquad filter state</param>
|
||||
/// <param name="outputBuffer">The output buffer to write the result</param>
|
||||
/// <param name="inputBuffer">The input buffer to read the samples from</param>
|
||||
/// <param name="sampleCount">The count of samples to process</param>
|
||||
/// <param name="volume">Initial mix volume</param>
|
||||
/// <param name="ramp">Volume increment step</param>
|
||||
/// <returns>Last filtered sample value</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static float ProcessBiquadFilterAndMixRamp(
|
||||
ref BiquadFilterParameter parameter,
|
||||
ref BiquadFilterState state,
|
||||
Span<float> outputBuffer,
|
||||
ReadOnlySpan<float> 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Apply multiple biquad filter.
|
||||
/// </summary>
|
||||
@ -47,10 +142,15 @@ namespace Ryujinx.Audio.Renderer.Dsp
|
||||
/// <param name="parameters">The biquad filter parameter</param>
|
||||
/// <param name="states">The biquad filter state</param>
|
||||
/// <param name="outputBuffer">The output buffer to write the result</param>
|
||||
/// <param name="inputBuffer">The input buffer to write the result</param>
|
||||
/// <param name="inputBuffer">The input buffer to read the samples from</param>
|
||||
/// <param name="sampleCount">The count of samples to process</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void ProcessBiquadFilter(ReadOnlySpan<BiquadFilterParameter> parameters, Span<BiquadFilterState> states, Span<float> outputBuffer, ReadOnlySpan<float> inputBuffer, uint sampleCount)
|
||||
public static void ProcessBiquadFilter(
|
||||
ReadOnlySpan<BiquadFilterParameter> parameters,
|
||||
Span<BiquadFilterState> states,
|
||||
Span<float> outputBuffer,
|
||||
ReadOnlySpan<float> 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Apply double biquad filter and mix the result into the output buffer.
|
||||
/// </summary>
|
||||
/// <remarks>This is implemented with a direct form 1.</remarks>
|
||||
/// <param name="parameters">The biquad filter parameter</param>
|
||||
/// <param name="states">The biquad filter state</param>
|
||||
/// <param name="outputBuffer">The output buffer to write the result</param>
|
||||
/// <param name="inputBuffer">The input buffer to read the samples from</param>
|
||||
/// <param name="sampleCount">The count of samples to process</param>
|
||||
/// <param name="volume">Mix volume</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void ProcessDoubleBiquadFilterAndMix(
|
||||
ref BiquadFilterParameter parameter0,
|
||||
ref BiquadFilterParameter parameter1,
|
||||
ref BiquadFilterState state0,
|
||||
ref BiquadFilterState state1,
|
||||
Span<float> outputBuffer,
|
||||
ReadOnlySpan<float> 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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Apply double biquad filter and mix the result into the output buffer with volume ramp.
|
||||
/// </summary>
|
||||
/// <remarks>This is implemented with a direct form 1.</remarks>
|
||||
/// <param name="parameters">The biquad filter parameter</param>
|
||||
/// <param name="states">The biquad filter state</param>
|
||||
/// <param name="outputBuffer">The output buffer to write the result</param>
|
||||
/// <param name="inputBuffer">The input buffer to read the samples from</param>
|
||||
/// <param name="sampleCount">The count of samples to process</param>
|
||||
/// <param name="volume">Initial mix volume</param>
|
||||
/// <param name="ramp">Volume increment step</param>
|
||||
/// <returns>Last filtered sample value</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static float ProcessDoubleBiquadFilterAndMixRamp(
|
||||
ref BiquadFilterParameter parameter0,
|
||||
ref BiquadFilterParameter parameter1,
|
||||
ref BiquadFilterState state0,
|
||||
ref BiquadFilterState state1,
|
||||
Span<float> outputBuffer,
|
||||
ReadOnlySpan<float> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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> BiquadFilterState { get; }
|
||||
public Memory<BiquadFilterState> PreviousBiquadFilterState { get; }
|
||||
|
||||
public Memory<VoiceUpdateState> 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<VoiceUpdateState> state,
|
||||
ref BiquadFilterParameter filter,
|
||||
Memory<BiquadFilterState> biquadFilterState,
|
||||
Memory<BiquadFilterState> 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<float> inputBuffer = context.GetBuffer(InputBufferIndex);
|
||||
Span<float> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -30,8 +30,10 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
|
||||
CopyMixBuffer,
|
||||
LimiterVersion1,
|
||||
LimiterVersion2,
|
||||
GroupedBiquadFilter,
|
||||
MultiTapBiquadFilter,
|
||||
CaptureBuffer,
|
||||
Compressor,
|
||||
BiquadFilterAndMix,
|
||||
MultiTapBiquadFilterAndMix,
|
||||
}
|
||||
}
|
||||
|
@ -24,7 +24,14 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
|
||||
|
||||
public Memory<VoiceUpdateState> State { get; }
|
||||
|
||||
public MixRampGroupedCommand(uint mixBufferCount, uint inputBufferIndex, uint outputBufferIndex, Span<float> volume0, Span<float> volume1, Memory<VoiceUpdateState> state, int nodeId)
|
||||
public MixRampGroupedCommand(
|
||||
uint mixBufferCount,
|
||||
uint inputBufferIndex,
|
||||
uint outputBufferIndex,
|
||||
ReadOnlySpan<float> volume0,
|
||||
ReadOnlySpan<float> volume1,
|
||||
Memory<VoiceUpdateState> 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<float> outputBuffer, ReadOnlySpan<float> inputBuffer, float volume0, float volume1, int sampleCount)
|
||||
private static float ProcessMixRampGrouped(
|
||||
Span<float> outputBuffer,
|
||||
ReadOnlySpan<float> inputBuffer,
|
||||
float volume0,
|
||||
float volume1,
|
||||
int sampleCount)
|
||||
{
|
||||
float ramp = (volume1 - volume0) / sampleCount;
|
||||
float volume = volume0;
|
||||
|
@ -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<BiquadFilterState> BiquadFilterState0 { get; }
|
||||
public Memory<BiquadFilterState> BiquadFilterState1 { get; }
|
||||
public Memory<BiquadFilterState> PreviousBiquadFilterState0 { get; }
|
||||
public Memory<BiquadFilterState> PreviousBiquadFilterState1 { get; }
|
||||
|
||||
public Memory<VoiceUpdateState> 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<VoiceUpdateState> state,
|
||||
ref BiquadFilterParameter filter0,
|
||||
ref BiquadFilterParameter filter1,
|
||||
Memory<BiquadFilterState> biquadFilterState0,
|
||||
Memory<BiquadFilterState> biquadFilterState1,
|
||||
Memory<BiquadFilterState> previousBiquadFilterState0,
|
||||
Memory<BiquadFilterState> 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<BiquadFilterState> state, Memory<BiquadFilterState> 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<float> inputBuffer = context.GetBuffer(InputBufferIndex);
|
||||
Span<float> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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<BiquadFilterParameter> filters, Memory<BiquadFilterState> biquadFilterStateMemory, int inputBufferOffset, int outputBufferOffset, ReadOnlySpan<bool> isInitialized, int nodeId)
|
||||
public MultiTapBiquadFilterCommand(int baseIndex, ReadOnlySpan<BiquadFilterParameter> filters, Memory<BiquadFilterState> biquadFilterStateMemory, int inputBufferOffset, int outputBufferOffset, ReadOnlySpan<bool> isInitialized, int nodeId)
|
||||
{
|
||||
_parameters = filters.ToArray();
|
||||
_biquadFilterStates = biquadFilterStateMemory;
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,43 @@
|
||||
using Ryujinx.Common.Memory;
|
||||
using System;
|
||||
|
||||
namespace Ryujinx.Audio.Renderer.Parameter
|
||||
{
|
||||
/// <summary>
|
||||
/// Generic interface for the splitter destination parameters.
|
||||
/// </summary>
|
||||
public interface ISplitterDestinationInParameter
|
||||
{
|
||||
/// <summary>
|
||||
/// Target splitter destination data id.
|
||||
/// </summary>
|
||||
int Id { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The mix to output the result of the splitter.
|
||||
/// </summary>
|
||||
int DestinationId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Biquad filter parameters.
|
||||
/// </summary>
|
||||
Array2<BiquadFilterParameter> BiquadFilters { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Set to true if in use.
|
||||
/// </summary>
|
||||
bool IsUsed { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Mix buffer volumes.
|
||||
/// </summary>
|
||||
/// <remarks>Used when a splitter id is specified in the mix.</remarks>
|
||||
Span<float> MixBufferVolume { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Check if the magic is valid.
|
||||
/// </summary>
|
||||
/// <returns>Returns true if the magic is valid.</returns>
|
||||
bool IsMagicValid();
|
||||
}
|
||||
}
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Input header for a splitter destination update.
|
||||
/// Input header for a splitter destination version 1 update.
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
public struct SplitterDestinationInParameter
|
||||
public struct SplitterDestinationInParameterVersion1 : ISplitterDestinationInParameter
|
||||
{
|
||||
/// <summary>
|
||||
/// Magic of the input header.
|
||||
@ -41,7 +42,7 @@ namespace Ryujinx.Audio.Renderer.Parameter
|
||||
/// </summary>
|
||||
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 { }
|
||||
|
||||
/// <summary>
|
||||
@ -50,6 +51,14 @@ namespace Ryujinx.Audio.Renderer.Parameter
|
||||
/// <remarks>Used when a splitter id is specified in the mix.</remarks>
|
||||
public Span<float> MixBufferVolume => SpanHelpers.AsSpan<MixArray, float>(ref _mixBufferVolume);
|
||||
|
||||
readonly int ISplitterDestinationInParameter.Id => Id;
|
||||
|
||||
readonly int ISplitterDestinationInParameter.DestinationId => DestinationId;
|
||||
|
||||
readonly Array2<BiquadFilterParameter> ISplitterDestinationInParameter.BiquadFilters => default;
|
||||
|
||||
readonly bool ISplitterDestinationInParameter.IsUsed => IsUsed;
|
||||
|
||||
/// <summary>
|
||||
/// The expected constant of any input header.
|
||||
/// </summary>
|
@ -0,0 +1,81 @@
|
||||
using Ryujinx.Common.Memory;
|
||||
using Ryujinx.Common.Utilities;
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Ryujinx.Audio.Renderer.Parameter
|
||||
{
|
||||
/// <summary>
|
||||
/// Input header for a splitter destination version 2 update.
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
public struct SplitterDestinationInParameterVersion2 : ISplitterDestinationInParameter
|
||||
{
|
||||
/// <summary>
|
||||
/// Magic of the input header.
|
||||
/// </summary>
|
||||
public uint Magic;
|
||||
|
||||
/// <summary>
|
||||
/// Target splitter destination data id.
|
||||
/// </summary>
|
||||
public int Id;
|
||||
|
||||
/// <summary>
|
||||
/// Mix buffer volumes storage.
|
||||
/// </summary>
|
||||
private MixArray _mixBufferVolume;
|
||||
|
||||
/// <summary>
|
||||
/// The mix to output the result of the splitter.
|
||||
/// </summary>
|
||||
public int DestinationId;
|
||||
|
||||
/// <summary>
|
||||
/// Biquad filter parameters.
|
||||
/// </summary>
|
||||
public Array2<BiquadFilterParameter> BiquadFilters;
|
||||
|
||||
/// <summary>
|
||||
/// Set to true if in use.
|
||||
/// </summary>
|
||||
[MarshalAs(UnmanagedType.I1)]
|
||||
public bool IsUsed;
|
||||
|
||||
/// <summary>
|
||||
/// Reserved/padding.
|
||||
/// </summary>
|
||||
private unsafe fixed byte _reserved[11];
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Size = sizeof(float) * Constants.MixBufferCountMax, Pack = 1)]
|
||||
private struct MixArray { }
|
||||
|
||||
/// <summary>
|
||||
/// Mix buffer volumes.
|
||||
/// </summary>
|
||||
/// <remarks>Used when a splitter id is specified in the mix.</remarks>
|
||||
public Span<float> MixBufferVolume => SpanHelpers.AsSpan<MixArray, float>(ref _mixBufferVolume);
|
||||
|
||||
readonly int ISplitterDestinationInParameter.Id => Id;
|
||||
|
||||
readonly int ISplitterDestinationInParameter.DestinationId => DestinationId;
|
||||
|
||||
readonly Array2<BiquadFilterParameter> ISplitterDestinationInParameter.BiquadFilters => BiquadFilters;
|
||||
|
||||
readonly bool ISplitterDestinationInParameter.IsUsed => IsUsed;
|
||||
|
||||
/// <summary>
|
||||
/// The expected constant of any input header.
|
||||
/// </summary>
|
||||
private const uint ValidMagic = 0x44444E53;
|
||||
|
||||
/// <summary>
|
||||
/// Check if the magic is valid.
|
||||
/// </summary>
|
||||
/// <returns>Returns true if the magic is valid.</returns>
|
||||
public readonly bool IsMagicValid()
|
||||
{
|
||||
return Magic == ValidMagic;
|
||||
}
|
||||
}
|
||||
}
|
@ -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<BiquadFilterState> splitterBqfStates = Memory<BiquadFilterState>.Empty;
|
||||
|
||||
if (_behaviourContext.IsBiquadFilterParameterForSplitterEnabled() &&
|
||||
parameter.SplitterCount > 0 &&
|
||||
parameter.SplitterDestinationCount > 0)
|
||||
{
|
||||
splitterBqfStates = workBufferAllocator.Allocate<BiquadFilterState>(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<BiquadFilterState>(size, parameter.SplitterDestinationCount * SplitterContext.BqfStatesPerDestination, 0x10);
|
||||
}
|
||||
|
||||
// DSP Voice
|
||||
size = WorkBufferAllocator.GetTargetSize<VoiceUpdateState>(size, parameter.VoiceCount, VoiceUpdateState.Align);
|
||||
|
||||
|
@ -45,7 +45,6 @@ namespace Ryujinx.Audio.Renderer.Server
|
||||
/// <see cref="Parameter.RendererInfoOutStatus"/> 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%.
|
||||
///
|
||||
/// </summary>
|
||||
/// <remarks>This was added in system update 6.0.0</remarks>
|
||||
public const int Revision5 = 5 << 24;
|
||||
@ -101,10 +100,18 @@ namespace Ryujinx.Audio.Renderer.Server
|
||||
/// <remarks>This was added in system update 14.0.0 but some changes were made in 15.0.0</remarks>
|
||||
public const int Revision11 = 11 << 24;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <remarks>This was added in system update 17.0.0</remarks>
|
||||
public const int Revision12 = 12 << 24;
|
||||
|
||||
/// <summary>
|
||||
/// Last revision supported by the implementation.
|
||||
/// </summary>
|
||||
public const int LastRevision = Revision11;
|
||||
public const int LastRevision = Revision12;
|
||||
|
||||
/// <summary>
|
||||
/// Target revision magic supported by the implementation.
|
||||
@ -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.
|
||||
/// </summary>
|
||||
/// <returns>True if the audio renderer should use the optimization.</returns>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if the audio renderer should support biquad filter on splitter.
|
||||
/// </summary>
|
||||
/// <returns>True if the audio renderer support biquad filter on splitter</returns>
|
||||
public bool IsBiquadFilterParameterForSplitterEnabled()
|
||||
{
|
||||
return CheckFeatureSupported(UserRevision, BaseRevisionMagic + Revision12);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the version of the <see cref="ICommandProcessingTimeEstimator"/>.
|
||||
/// </summary>
|
||||
|
@ -204,7 +204,7 @@ namespace Ryujinx.Audio.Renderer.Server
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="GroupedBiquadFilterCommand"/>.
|
||||
/// Create a new <see cref="MultiTapBiquadFilterCommand"/>.
|
||||
/// </summary>
|
||||
/// <param name="baseIndex">The base index of the input and output buffer.</param>
|
||||
/// <param name="filters">The biquad filter parameters.</param>
|
||||
@ -213,9 +213,9 @@ namespace Ryujinx.Audio.Renderer.Server
|
||||
/// <param name="outputBufferOffset">The output buffer offset.</param>
|
||||
/// <param name="isInitialized">Set to true if the biquad filter state is initialized.</param>
|
||||
/// <param name="nodeId">The node id associated to this command.</param>
|
||||
public void GenerateGroupedBiquadFilter(int baseIndex, ReadOnlySpan<BiquadFilterParameter> filters, Memory<BiquadFilterState> biquadFilterStatesMemory, int inputBufferOffset, int outputBufferOffset, ReadOnlySpan<bool> isInitialized, int nodeId)
|
||||
public void GenerateMultiTapBiquadFilter(int baseIndex, ReadOnlySpan<BiquadFilterParameter> filters, Memory<BiquadFilterState> biquadFilterStatesMemory, int inputBufferOffset, int outputBufferOffset, ReadOnlySpan<bool> 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
|
||||
/// <param name="volume">The new volume.</param>
|
||||
/// <param name="state">The <see cref="VoiceUpdateState"/> to generate the command from.</param>
|
||||
/// <param name="nodeId">The node id associated to this command.</param>
|
||||
public void GenerateMixRampGrouped(uint mixBufferCount, uint inputBufferIndex, uint outputBufferIndex, Span<float> previousVolume, Span<float> volume, Memory<VoiceUpdateState> state, int nodeId)
|
||||
public void GenerateMixRampGrouped(uint mixBufferCount, uint inputBufferIndex, uint outputBufferIndex, ReadOnlySpan<float> previousVolume, ReadOnlySpan<float> volume, Memory<VoiceUpdateState> state, int nodeId)
|
||||
{
|
||||
MixRampGroupedCommand command = new(mixBufferCount, inputBufferIndex, outputBufferIndex, previousVolume, volume, state, nodeId);
|
||||
|
||||
@ -260,6 +260,120 @@ namespace Ryujinx.Audio.Renderer.Server
|
||||
AddCommand(command);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generate a new <see cref="BiquadFilterAndMixCommand"/>.
|
||||
/// </summary>
|
||||
/// <param name="previousVolume">The previous volume.</param>
|
||||
/// <param name="volume">The new volume.</param>
|
||||
/// <param name="inputBufferIndex">The input buffer index.</param>
|
||||
/// <param name="outputBufferIndex">The output buffer index.</param>
|
||||
/// <param name="lastSampleIndex">The index in the <see cref="VoiceUpdateState.LastSamples"/> array to store the ramped sample.</param>
|
||||
/// <param name="state">The <see cref="VoiceUpdateState"/> to generate the command from.</param>
|
||||
/// <param name="filter">The biquad filter parameter.</param>
|
||||
/// <param name="biquadFilterState">The biquad state.</param>
|
||||
/// <param name="previousBiquadFilterState">The previous biquad state.</param>
|
||||
/// <param name="needInitialization">Set to true if the biquad filter state needs to be initialized.</param>
|
||||
/// <param name="hasVolumeRamp">Set to true if the mix has volume ramp, and <paramref name="previousVolume"/> should be taken into account.</param>
|
||||
/// <param name="isFirstMixBuffer">Set to true if the buffer is the first mix buffer.</param>
|
||||
/// <param name="nodeId">The node id associated to this command.</param>
|
||||
public void GenerateBiquadFilterAndMix(
|
||||
float previousVolume,
|
||||
float volume,
|
||||
uint inputBufferIndex,
|
||||
uint outputBufferIndex,
|
||||
int lastSampleIndex,
|
||||
Memory<VoiceUpdateState> state,
|
||||
ref BiquadFilterParameter filter,
|
||||
Memory<BiquadFilterState> biquadFilterState,
|
||||
Memory<BiquadFilterState> 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generate a new <see cref="MultiTapBiquadFilterAndMixCommand"/>.
|
||||
/// </summary>
|
||||
/// <param name="previousVolume">The previous volume.</param>
|
||||
/// <param name="volume">The new volume.</param>
|
||||
/// <param name="inputBufferIndex">The input buffer index.</param>
|
||||
/// <param name="outputBufferIndex">The output buffer index.</param>
|
||||
/// <param name="lastSampleIndex">The index in the <see cref="VoiceUpdateState.LastSamples"/> array to store the ramped sample.</param>
|
||||
/// <param name="state">The <see cref="VoiceUpdateState"/> to generate the command from.</param>
|
||||
/// <param name="filter0">First biquad filter parameter.</param>
|
||||
/// <param name="filter1">Second biquad filter parameter.</param>
|
||||
/// <param name="biquadFilterState0">First biquad state.</param>
|
||||
/// <param name="biquadFilterState1">Second biquad state.</param>
|
||||
/// <param name="previousBiquadFilterState0">First previous biquad state.</param>
|
||||
/// <param name="previousBiquadFilterState1">Second previous biquad state.</param>
|
||||
/// <param name="needInitialization0">Set to true if the first biquad filter state needs to be initialized.</param>
|
||||
/// <param name="needInitialization1">Set to true if the second biquad filter state needs to be initialized.</param>
|
||||
/// <param name="hasVolumeRamp">Set to true if the mix has volume ramp, and <paramref name="previousVolume"/> should be taken into account.</param>
|
||||
/// <param name="isFirstMixBuffer">Set to true if the buffer is the first mix buffer.</param>
|
||||
/// <param name="nodeId">The node id associated to this command.</param>
|
||||
public void GenerateMultiTapBiquadFilterAndMix(
|
||||
float previousVolume,
|
||||
float volume,
|
||||
uint inputBufferIndex,
|
||||
uint outputBufferIndex,
|
||||
int lastSampleIndex,
|
||||
Memory<VoiceUpdateState> state,
|
||||
ref BiquadFilterParameter filter0,
|
||||
ref BiquadFilterParameter filter1,
|
||||
Memory<BiquadFilterState> biquadFilterState0,
|
||||
Memory<BiquadFilterState> biquadFilterState1,
|
||||
Memory<BiquadFilterState> previousBiquadFilterState0,
|
||||
Memory<BiquadFilterState> 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generate a new <see cref="DepopForMixBuffersCommand"/>.
|
||||
/// </summary>
|
||||
@ -268,7 +382,7 @@ namespace Ryujinx.Audio.Renderer.Server
|
||||
/// <param name="bufferCount">The buffer count.</param>
|
||||
/// <param name="nodeId">The node id associated to this command.</param>
|
||||
/// <param name="sampleRate">The target sample rate in use.</param>
|
||||
public void GenerateDepopForMixBuffersCommand(Memory<float> depopBuffer, uint bufferOffset, uint bufferCount, int nodeId, uint sampleRate)
|
||||
public void GenerateDepopForMixBuffers(Memory<float> depopBuffer, uint bufferOffset, uint bufferCount, int nodeId, uint sampleRate)
|
||||
{
|
||||
DepopForMixBuffersCommand command = new(depopBuffer, bufferOffset, bufferCount, nodeId, sampleRate);
|
||||
|
||||
|
@ -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<SplitterDestination> 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<VoiceUpdateState> 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<byte> biquadStateRawMemory = SpanMemoryManager<byte>.Cast(state)[..(VoiceUpdateState.BiquadStateSize * Constants.VoiceBiquadFilterCount)];
|
||||
Memory<byte> biquadStateRawMemory = SpanMemoryManager<byte>.Cast(state)[..(Unsafe.SizeOf<BiquadFilterState>() * Constants.VoiceBiquadFilterCount)];
|
||||
Memory<BiquadFilterState> stateMemory = SpanMemoryManager<BiquadFilterState>.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<byte> biquadStateRawMemory = SpanMemoryManager<byte>.Cast(state)[..(VoiceUpdateState.BiquadStateSize * Constants.VoiceBiquadFilterCount)];
|
||||
|
||||
Memory<byte> biquadStateRawMemory = SpanMemoryManager<byte>.Cast(state)[..(Unsafe.SizeOf<BiquadFilterState>() * Constants.VoiceBiquadFilterCount)];
|
||||
Memory<BiquadFilterState> stateMemory = SpanMemoryManager<BiquadFilterState>.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<float> mixVolumes, Span<float> previousMixVolumes, Memory<VoiceUpdateState> state, uint bufferOffset, uint bufferCount, uint bufferIndex, int nodeId)
|
||||
private void GenerateVoiceMixWithSplitter(
|
||||
SplitterDestination destination,
|
||||
Memory<VoiceUpdateState> state,
|
||||
uint bufferOffset,
|
||||
uint bufferCount,
|
||||
uint bufferIndex,
|
||||
int nodeId)
|
||||
{
|
||||
ReadOnlySpan<float> mixVolumes = destination.MixBufferVolume;
|
||||
ReadOnlySpan<float> previousMixVolumes = destination.PreviousMixBufferVolume;
|
||||
|
||||
ref BiquadFilterParameter bqf0 = ref destination.GetBiquadFilterParameter(0);
|
||||
ref BiquadFilterParameter bqf1 = ref destination.GetBiquadFilterParameter(1);
|
||||
|
||||
Memory<BiquadFilterState> 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<float> mixVolumes,
|
||||
ReadOnlySpan<float> previousMixVolumes,
|
||||
Memory<VoiceUpdateState> 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<SplitterDestination> 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<BiquadFilterState> bqfState = _splitterContext.GetBiquadFilterState(destination);
|
||||
|
||||
if (bqf0.Enable && bqf1.Enable)
|
||||
{
|
||||
_commandBuffer.GenerateMultiTapBiquadFilterAndMix(
|
||||
0f,
|
||||
volume,
|
||||
inputBufferIndex,
|
||||
outputBufferIndex,
|
||||
0,
|
||||
Memory<VoiceUpdateState>.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<VoiceUpdateState>.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<VoiceUpdateState>.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<SplitterDestination> 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)
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -225,11 +225,11 @@ namespace Ryujinx.Audio.Renderer.Server.Mix
|
||||
|
||||
for (int i = 0; i < splitter.DestinationCount; i++)
|
||||
{
|
||||
Span<SplitterDestination> 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)
|
||||
{
|
||||
|
@ -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
|
||||
/// </summary>
|
||||
public class SplitterContext
|
||||
{
|
||||
/// <summary>
|
||||
/// Amount of biquad filter states per splitter destination.
|
||||
/// </summary>
|
||||
public const int BqfStatesPerDestination = 4;
|
||||
|
||||
/// <summary>
|
||||
/// Storage for <see cref="SplitterState"/>.
|
||||
/// </summary>
|
||||
private Memory<SplitterState> _splitters;
|
||||
|
||||
/// <summary>
|
||||
/// Storage for <see cref="SplitterDestination"/>.
|
||||
/// Storage for <see cref="SplitterDestinationVersion1"/>.
|
||||
/// </summary>
|
||||
private Memory<SplitterDestination> _splitterDestinations;
|
||||
private Memory<SplitterDestinationVersion1> _splitterDestinationsV1;
|
||||
|
||||
/// <summary>
|
||||
/// Storage for <see cref="SplitterDestinationVersion2"/>.
|
||||
/// </summary>
|
||||
private Memory<SplitterDestinationVersion2> _splitterDestinationsV2;
|
||||
|
||||
/// <summary>
|
||||
/// Splitter biquad filtering states.
|
||||
/// </summary>
|
||||
private Memory<BiquadFilterState> _splitterBqfStates;
|
||||
|
||||
/// <summary>
|
||||
/// Version of the splitter context that is being used, currently can be 1 or 2.
|
||||
/// </summary>
|
||||
public int Version { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// If set to true, trust the user destination count in <see cref="SplitterState.Update(SplitterContext, in SplitterInParameter, ref SequenceReader{byte})"/>.
|
||||
@ -36,12 +57,17 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
|
||||
/// <param name="behaviourContext">The behaviour context.</param>
|
||||
/// <param name="parameter">The audio renderer configuration.</param>
|
||||
/// <param name="workBufferAllocator">The <see cref="WorkBufferAllocator"/>.</param>
|
||||
/// <param name="splitterBqfStates">Memory to store the biquad filtering state for splitters during processing.</param>
|
||||
/// <returns>Return true if the initialization was successful.</returns>
|
||||
public bool Initialize(ref BehaviourContext behaviourContext, ref AudioRendererConfiguration parameter, WorkBufferAllocator workBufferAllocator)
|
||||
public bool Initialize(
|
||||
ref BehaviourContext behaviourContext,
|
||||
ref AudioRendererConfiguration parameter,
|
||||
WorkBufferAllocator workBufferAllocator,
|
||||
Memory<BiquadFilterState> splitterBqfStates)
|
||||
{
|
||||
if (!behaviourContext.IsSplitterSupported() || parameter.SplitterCount <= 0 || parameter.SplitterDestinationCount <= 0)
|
||||
{
|
||||
Setup(Memory<SplitterState>.Empty, Memory<SplitterDestination>.Empty, false);
|
||||
Setup(Memory<SplitterState>.Empty, Memory<SplitterDestinationVersion1>.Empty, Memory<SplitterDestinationVersion2>.Empty, false);
|
||||
|
||||
return true;
|
||||
}
|
||||
@ -60,23 +86,62 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
|
||||
splitter = new SplitterState(splitterId++);
|
||||
}
|
||||
|
||||
Memory<SplitterDestination> splitterDestinations = workBufferAllocator.Allocate<SplitterDestination>(parameter.SplitterDestinationCount,
|
||||
SplitterDestination.Alignment);
|
||||
Memory<SplitterDestinationVersion1> splitterDestinationsV1 = Memory<SplitterDestinationVersion1>.Empty;
|
||||
Memory<SplitterDestinationVersion2> splitterDestinationsV2 = Memory<SplitterDestinationVersion2>.Empty;
|
||||
|
||||
if (splitterDestinations.IsEmpty)
|
||||
if (!behaviourContext.IsBiquadFilterParameterForSplitterEnabled())
|
||||
{
|
||||
return false;
|
||||
Version = 1;
|
||||
|
||||
splitterDestinationsV1 = workBufferAllocator.Allocate<SplitterDestinationVersion1>(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<SplitterDestinationVersion2>(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<BiquadFilterState>.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<SplitterState>(size, parameter.SplitterCount, SplitterState.Alignment);
|
||||
size = WorkBufferAllocator.GetTargetSize<SplitterDestination>(size, parameter.SplitterDestinationCount, SplitterDestination.Alignment);
|
||||
|
||||
if (behaviourContext.IsBiquadFilterParameterForSplitterEnabled())
|
||||
{
|
||||
size = WorkBufferAllocator.GetTargetSize<SplitterDestinationVersion2>(size, parameter.SplitterDestinationCount, SplitterDestinationVersion2.Alignment);
|
||||
}
|
||||
else
|
||||
{
|
||||
size = WorkBufferAllocator.GetTargetSize<SplitterDestinationVersion1>(size, parameter.SplitterDestinationCount, SplitterDestinationVersion1.Alignment);
|
||||
}
|
||||
|
||||
if (behaviourContext.IsSplitterBugFixed())
|
||||
{
|
||||
@ -110,12 +183,18 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
|
||||
/// Setup the <see cref="SplitterContext"/> instance.
|
||||
/// </summary>
|
||||
/// <param name="splitters">The <see cref="SplitterState"/> storage.</param>
|
||||
/// <param name="splitterDestinations">The <see cref="SplitterDestination"/> storage.</param>
|
||||
/// <param name="splitterDestinationsV1">The <see cref="SplitterDestinationVersion1"/> storage.</param>
|
||||
/// <param name="splitterDestinationsV2">The <see cref="SplitterDestinationVersion2"/> storage.</param>
|
||||
/// <param name="isBugFixed">If set to true, trust the user destination count in <see cref="SplitterState.Update(SplitterContext, in SplitterInParameter, ref SequenceReader{byte})"/>.</param>
|
||||
private void Setup(Memory<SplitterState> splitters, Memory<SplitterDestination> splitterDestinations, bool isBugFixed)
|
||||
private void Setup(
|
||||
Memory<SplitterState> splitters,
|
||||
Memory<SplitterDestinationVersion1> splitterDestinationsV1,
|
||||
Memory<SplitterDestinationVersion2> 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -178,7 +259,39 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update one or multiple <see cref="SplitterDestination"/> from user parameters.
|
||||
/// Update one splitter destination data from user parameters.
|
||||
/// </summary>
|
||||
/// <param name="input">The raw data after the splitter header.</param>
|
||||
/// <returns>True if the update was successful, false otherwise</returns>
|
||||
private bool UpdateData<T>(ref SequenceReader<byte> input) where T : unmanaged, ISplitterDestinationInParameter
|
||||
{
|
||||
ref readonly T parameter = ref input.GetRefOrRefToCopy<T>(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<T>());
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update one or multiple splitter destination data from user parameters.
|
||||
/// </summary>
|
||||
/// <param name="inputHeader">The splitter header.</param>
|
||||
/// <param name="input">The raw data after the splitter header.</param>
|
||||
@ -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<SplitterDestinationInParameter>(out _);
|
||||
|
||||
Debug.Assert(parameter.IsMagicValid());
|
||||
|
||||
if (parameter.IsMagicValid())
|
||||
if (Version == 1)
|
||||
{
|
||||
if (parameter.Id >= 0 && parameter.Id < _splitterDestinations.Length)
|
||||
if (!UpdateData<SplitterDestinationInParameterVersion1>(ref input))
|
||||
{
|
||||
ref SplitterDestination destination = ref GetDestination(parameter.Id);
|
||||
|
||||
destination.Update(parameter);
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (Version == 2)
|
||||
{
|
||||
if (!UpdateData<SplitterDestinationInParameterVersion2>(ref input))
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
input.Rewind(Unsafe.SizeOf<SplitterDestinationInParameter>());
|
||||
break;
|
||||
Debug.Fail($"Invalid splitter context version {Version}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -214,7 +327,7 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
|
||||
/// <returns>Return true if the update was successful.</returns>
|
||||
public bool Update(ref SequenceReader<byte> input)
|
||||
{
|
||||
if (_splitterDestinations.IsEmpty || _splitters.IsEmpty)
|
||||
if (!UsingSplitter())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
@ -251,45 +364,52 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a reference to a <see cref="SplitterDestination"/> at the given <paramref name="id"/>.
|
||||
/// Get a reference to the splitter destination data at the given <paramref name="id"/>.
|
||||
/// </summary>
|
||||
/// <param name="id">The index to use.</param>
|
||||
/// <returns>A reference to a <see cref="SplitterDestination"/> at the given <paramref name="id"/>.</returns>
|
||||
public ref SplitterDestination GetDestination(int id)
|
||||
/// <returns>A reference to the splitter destination data at the given <paramref name="id"/>.</returns>
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a <see cref="Memory{SplitterDestination}"/> at the given <paramref name="id"/>.
|
||||
/// </summary>
|
||||
/// <param name="id">The index to use.</param>
|
||||
/// <returns>A <see cref="Memory{SplitterDestination}"/> at the given <paramref name="id"/>.</returns>
|
||||
public Memory<SplitterDestination> GetDestinationMemory(int id)
|
||||
{
|
||||
return SpanIOHelper.GetMemory(_splitterDestinations, id, (uint)_splitterDestinations.Length);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a <see cref="Span{SplitterDestination}"/> in the <see cref="SplitterState"/> at <paramref name="id"/> and pass <paramref name="destinationId"/> to <see cref="SplitterState.GetData(int)"/>.
|
||||
/// Get a <see cref="SplitterDestination"/> in the <see cref="SplitterState"/> at <paramref name="id"/> and pass <paramref name="destinationId"/> to <see cref="SplitterState.GetData(int)"/>.
|
||||
/// </summary>
|
||||
/// <param name="id">The index to use to get the <see cref="SplitterState"/>.</param>
|
||||
/// <param name="destinationId">The index of the <see cref="SplitterDestination"/>.</param>
|
||||
/// <returns>A <see cref="Span{SplitterDestination}"/>.</returns>
|
||||
public Span<SplitterDestination> GetDestination(int id, int destinationId)
|
||||
/// <returns>A <see cref="SplitterDestination"/>.</returns>
|
||||
public SplitterDestination GetDestination(int id, int destinationId)
|
||||
{
|
||||
ref SplitterState splitter = ref GetState(id);
|
||||
|
||||
return splitter.GetData(destinationId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the biquad filter state for a given splitter destination.
|
||||
/// </summary>
|
||||
/// <param name="destination">The splitter destination.</param>
|
||||
/// <returns>Biquad filter state for the specified destination.</returns>
|
||||
public Memory<BiquadFilterState> GetBiquadFilterState(SplitterDestination destination)
|
||||
{
|
||||
return _splitterBqfStates.Slice(destination.Id * BqfStatesPerDestination, BqfStatesPerDestination);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return true if the audio renderer has any splitters.
|
||||
/// </summary>
|
||||
/// <returns>True if the audio renderer has any splitters.</returns>
|
||||
public bool UsingSplitter()
|
||||
{
|
||||
return !_splitters.IsEmpty && !_splitterDestinations.IsEmpty;
|
||||
return !_splitters.IsEmpty && (!_splitterDestinationsV1.IsEmpty || !_splitterDestinationsV2.IsEmpty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Server state for a splitter destination.
|
||||
/// </summary>
|
||||
[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;
|
||||
|
||||
/// <summary>
|
||||
/// The unique id of this <see cref="SplitterDestination"/>.
|
||||
/// Checks if the splitter destination data reference is null.
|
||||
/// </summary>
|
||||
public int Id;
|
||||
public bool IsNull => Unsafe.IsNullRef(ref _v1) && Unsafe.IsNullRef(ref _v2);
|
||||
|
||||
/// <summary>
|
||||
/// The mix to output the result of the splitter.
|
||||
/// The splitter unique id.
|
||||
/// </summary>
|
||||
public int DestinationId;
|
||||
|
||||
/// <summary>
|
||||
/// Mix buffer volumes storage.
|
||||
/// </summary>
|
||||
private MixArray _mix;
|
||||
private MixArray _previousMix;
|
||||
|
||||
/// <summary>
|
||||
/// Pointer to the next linked element.
|
||||
/// </summary>
|
||||
private unsafe SplitterDestination* _next;
|
||||
|
||||
/// <summary>
|
||||
/// Set to true if in use.
|
||||
/// </summary>
|
||||
[MarshalAs(UnmanagedType.I1)]
|
||||
public bool IsUsed;
|
||||
|
||||
/// <summary>
|
||||
/// Set to true if the internal state need to be updated.
|
||||
/// </summary>
|
||||
[MarshalAs(UnmanagedType.I1)]
|
||||
public bool NeedToUpdateInternalState;
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Size = 4 * Constants.MixBufferCountMax, Pack = 1)]
|
||||
private struct MixArray { }
|
||||
|
||||
/// <summary>
|
||||
/// Mix buffer volumes.
|
||||
/// </summary>
|
||||
/// <remarks>Used when a splitter id is specified in the mix.</remarks>
|
||||
public Span<float> MixBufferVolume => SpanHelpers.AsSpan<MixArray, float>(ref _mix);
|
||||
|
||||
/// <summary>
|
||||
/// Previous mix buffer volumes.
|
||||
/// </summary>
|
||||
/// <remarks>Used when a splitter id is specified in the mix.</remarks>
|
||||
public Span<float> PreviousMixBufferVolume => SpanHelpers.AsSpan<MixArray, float>(ref _previousMix);
|
||||
|
||||
/// <summary>
|
||||
/// Get the <see cref="Span{SplitterDestination}"/> of the next element or <see cref="Span{SplitterDestination}.Empty"/> if not present.
|
||||
/// </summary>
|
||||
public readonly Span<SplitterDestination> Next
|
||||
public int Id
|
||||
{
|
||||
get
|
||||
{
|
||||
unsafe
|
||||
if (Unsafe.IsNullRef(ref _v2))
|
||||
{
|
||||
return _next != null ? new Span<SplitterDestination>(_next, 1) : Span<SplitterDestination>.Empty;
|
||||
if (Unsafe.IsNullRef(ref _v1))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
return _v1.Id;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return _v2.Id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="SplitterDestination"/>.
|
||||
/// The mix to output the result of the splitter.
|
||||
/// </summary>
|
||||
/// <param name="id">The unique id of this <see cref="SplitterDestination"/>.</param>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update the <see cref="SplitterDestination"/> from user parameter.
|
||||
/// Mix buffer volumes.
|
||||
/// </summary>
|
||||
/// <remarks>Used when a splitter id is specified in the mix.</remarks>
|
||||
public Span<float> MixBufferVolume
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Unsafe.IsNullRef(ref _v2))
|
||||
{
|
||||
if (Unsafe.IsNullRef(ref _v1))
|
||||
{
|
||||
return Span<float>.Empty;
|
||||
}
|
||||
else
|
||||
{
|
||||
return _v1.MixBufferVolume;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return _v2.MixBufferVolume;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Previous mix buffer volumes.
|
||||
/// </summary>
|
||||
/// <remarks>Used when a splitter id is specified in the mix.</remarks>
|
||||
public Span<float> PreviousMixBufferVolume
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Unsafe.IsNullRef(ref _v2))
|
||||
{
|
||||
if (Unsafe.IsNullRef(ref _v1))
|
||||
{
|
||||
return Span<float>.Empty;
|
||||
}
|
||||
else
|
||||
{
|
||||
return _v1.PreviousMixBufferVolume;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return _v2.PreviousMixBufferVolume;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the <see cref="SplitterDestination"/> of the next element or null if not present.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new splitter destination wrapper for the version 1 splitter destination data.
|
||||
/// </summary>
|
||||
/// <param name="v1">Version 1 splitter destination data</param>
|
||||
public SplitterDestination(ref SplitterDestinationVersion1 v1)
|
||||
{
|
||||
_v1 = ref v1;
|
||||
_v2 = ref Unsafe.NullRef<SplitterDestinationVersion2>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new splitter destination wrapper for the version 2 splitter destination data.
|
||||
/// </summary>
|
||||
/// <param name="v2">Version 2 splitter destination data</param>
|
||||
public SplitterDestination(ref SplitterDestinationVersion2 v2)
|
||||
{
|
||||
|
||||
_v1 = ref Unsafe.NullRef<SplitterDestinationVersion1>();
|
||||
_v2 = ref v2;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new splitter destination wrapper for the splitter destination data.
|
||||
/// </summary>
|
||||
/// <param name="v1">Version 1 splitter destination data</param>
|
||||
/// <param name="v2">Version 2 splitter destination data</param>
|
||||
public unsafe SplitterDestination(SplitterDestinationVersion1* v1, SplitterDestinationVersion2* v2)
|
||||
{
|
||||
_v1 = ref Unsafe.AsRef<SplitterDestinationVersion1>(v1);
|
||||
_v2 = ref Unsafe.AsRef<SplitterDestinationVersion2>(v2);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update the splitter destination data from user parameter.
|
||||
/// </summary>
|
||||
/// <param name="parameter">The user parameter.</param>
|
||||
public void Update(SplitterDestinationInParameter parameter)
|
||||
public void Update<T>(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
|
||||
/// </summary>
|
||||
public void UpdateInternalState()
|
||||
{
|
||||
if (IsUsed && NeedToUpdateInternalState)
|
||||
if (Unsafe.IsNullRef(ref _v2))
|
||||
{
|
||||
MixBufferVolume.CopyTo(PreviousMixBufferVolume);
|
||||
_v1.UpdateInternalState();
|
||||
}
|
||||
else
|
||||
{
|
||||
_v2.UpdateInternalState();
|
||||
}
|
||||
|
||||
NeedToUpdateInternalState = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -131,16 +216,23 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
|
||||
/// </summary>
|
||||
public void MarkAsNeedToUpdateInternalState()
|
||||
{
|
||||
NeedToUpdateInternalState = true;
|
||||
if (Unsafe.IsNullRef(ref _v2))
|
||||
{
|
||||
_v1.MarkAsNeedToUpdateInternalState();
|
||||
}
|
||||
else
|
||||
{
|
||||
_v2.MarkAsNeedToUpdateInternalState();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return true if the <see cref="SplitterDestination"/> is used and has a destination.
|
||||
/// Return true if the splitter destination is used and has a destination.
|
||||
/// </summary>
|
||||
/// <returns>True if the <see cref="SplitterDestination"/> is used and has a destination.</returns>
|
||||
/// <returns>True if the splitter destination is used and has a destination.</returns>
|
||||
public readonly bool IsConfigured()
|
||||
{
|
||||
return IsUsed && DestinationId != Constants.UnusedMixId;
|
||||
return Unsafe.IsNullRef(ref _v2) ? _v1.IsConfigured() : _v2.IsConfigured();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -150,9 +242,17 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
|
||||
/// <returns>The volume for the given destination.</returns>
|
||||
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];
|
||||
/// <summary>
|
||||
/// Get the previous volume for a given destination.
|
||||
/// </summary>
|
||||
/// <param name="destinationIndex">The destination index to use.</param>
|
||||
/// <returns>The volume for the given destination.</returns>
|
||||
public float GetMixVolumePrev(int destinationIndex)
|
||||
{
|
||||
return Unsafe.IsNullRef(ref _v2) ? _v1.GetMixVolumePrev(destinationIndex) : _v2.GetMixVolumePrev(destinationIndex);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -160,22 +260,33 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
|
||||
/// </summary>
|
||||
public void ClearVolumes()
|
||||
{
|
||||
MixBufferVolume.Clear();
|
||||
PreviousMixBufferVolume.Clear();
|
||||
if (Unsafe.IsNullRef(ref _v2))
|
||||
{
|
||||
_v1.ClearVolumes();
|
||||
}
|
||||
else
|
||||
{
|
||||
_v2.ClearVolumes();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Link the next element to the given <see cref="SplitterDestination"/>.
|
||||
/// Link the next element to the given splitter destination.
|
||||
/// </summary>
|
||||
/// <param name="next">The given <see cref="SplitterDestination"/> to link.</param>
|
||||
public void Link(ref SplitterDestination next)
|
||||
/// <param name="next">The given splitter destination to link.</param>
|
||||
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
|
||||
/// </summary>
|
||||
public void Unlink()
|
||||
{
|
||||
unsafe
|
||||
if (Unsafe.IsNullRef(ref _v2))
|
||||
{
|
||||
_next = null;
|
||||
_v1.Unlink();
|
||||
}
|
||||
else
|
||||
{
|
||||
_v2.Unlink();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if any biquad filter is enabled.
|
||||
/// </summary>
|
||||
/// <returns>True if any biquad filter is enabled.</returns>
|
||||
public bool IsBiquadFilterEnabled()
|
||||
{
|
||||
return !Unsafe.IsNullRef(ref _v2) && _v2.IsBiquadFilterEnabled();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if any biquad filter was previously enabled.
|
||||
/// </summary>
|
||||
/// <returns>True if any biquad filter was previously enabled.</returns>
|
||||
public bool IsBiquadFilterEnabledPrev()
|
||||
{
|
||||
return !Unsafe.IsNullRef(ref _v2) && _v2.IsBiquadFilterEnabledPrev();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the biquad filter parameters.
|
||||
/// </summary>
|
||||
/// <param name="index">Biquad filter index (0 or 1).</param>
|
||||
/// <returns>Biquad filter parameters.</returns>
|
||||
public ref BiquadFilterParameter GetBiquadFilterParameter(int index)
|
||||
{
|
||||
Debug.Assert(!Unsafe.IsNullRef(ref _v2));
|
||||
|
||||
return ref _v2.GetBiquadFilterParameter(index);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if any biquad filter was previously enabled.
|
||||
/// </summary>
|
||||
/// <param name="index">Biquad filter index (0 or 1).</param>
|
||||
public void UpdateBiquadFilterEnabledPrev(int index)
|
||||
{
|
||||
if (!Unsafe.IsNullRef(ref _v2))
|
||||
{
|
||||
_v2.UpdateBiquadFilterEnabledPrev(index);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the reference for the version 1 splitter destination data, or null if version 2 is being used or the destination is null.
|
||||
/// </summary>
|
||||
/// <returns>Reference for the version 1 splitter destination data.</returns>
|
||||
public ref SplitterDestinationVersion1 GetV1RefOrNull()
|
||||
{
|
||||
return ref _v1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the reference for the version 2 splitter destination data, or null if version 1 is being used or the destination is null.
|
||||
/// </summary>
|
||||
/// <returns>Reference for the version 2 splitter destination data.</returns>
|
||||
public ref SplitterDestinationVersion2 GetV2RefOrNull()
|
||||
{
|
||||
return ref _v2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Server state for a splitter destination (version 1).
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Sequential, Size = 0xE0, Pack = Alignment)]
|
||||
public struct SplitterDestinationVersion1
|
||||
{
|
||||
public const int Alignment = 0x10;
|
||||
|
||||
/// <summary>
|
||||
/// The unique id of this <see cref="SplitterDestinationVersion1"/>.
|
||||
/// </summary>
|
||||
public int Id;
|
||||
|
||||
/// <summary>
|
||||
/// The mix to output the result of the splitter.
|
||||
/// </summary>
|
||||
public int DestinationId;
|
||||
|
||||
/// <summary>
|
||||
/// Mix buffer volumes storage.
|
||||
/// </summary>
|
||||
private MixArray _mix;
|
||||
private MixArray _previousMix;
|
||||
|
||||
/// <summary>
|
||||
/// Pointer to the next linked element.
|
||||
/// </summary>
|
||||
private unsafe SplitterDestinationVersion1* _next;
|
||||
|
||||
/// <summary>
|
||||
/// Set to true if in use.
|
||||
/// </summary>
|
||||
[MarshalAs(UnmanagedType.I1)]
|
||||
public bool IsUsed;
|
||||
|
||||
/// <summary>
|
||||
/// Set to true if the internal state need to be updated.
|
||||
/// </summary>
|
||||
[MarshalAs(UnmanagedType.I1)]
|
||||
public bool NeedToUpdateInternalState;
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Size = sizeof(float) * Constants.MixBufferCountMax, Pack = 1)]
|
||||
private struct MixArray { }
|
||||
|
||||
/// <summary>
|
||||
/// Mix buffer volumes.
|
||||
/// </summary>
|
||||
/// <remarks>Used when a splitter id is specified in the mix.</remarks>
|
||||
public Span<float> MixBufferVolume => SpanHelpers.AsSpan<MixArray, float>(ref _mix);
|
||||
|
||||
/// <summary>
|
||||
/// Previous mix buffer volumes.
|
||||
/// </summary>
|
||||
/// <remarks>Used when a splitter id is specified in the mix.</remarks>
|
||||
public Span<float> PreviousMixBufferVolume => SpanHelpers.AsSpan<MixArray, float>(ref _previousMix);
|
||||
|
||||
/// <summary>
|
||||
/// Get the reference of the next element or null if not present.
|
||||
/// </summary>
|
||||
public readonly ref SplitterDestinationVersion1 Next
|
||||
{
|
||||
get
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
return ref Unsafe.AsRef<SplitterDestinationVersion1>(_next);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="SplitterDestinationVersion1"/>.
|
||||
/// </summary>
|
||||
/// <param name="id">The unique id of this <see cref="SplitterDestinationVersion1"/>.</param>
|
||||
public SplitterDestinationVersion1(int id) : this()
|
||||
{
|
||||
Id = id;
|
||||
DestinationId = Constants.UnusedMixId;
|
||||
|
||||
ClearVolumes();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update the <see cref="SplitterDestinationVersion1"/> from user parameter.
|
||||
/// </summary>
|
||||
/// <param name="parameter">The user parameter.</param>
|
||||
public void Update<T>(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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update the internal state of the instance.
|
||||
/// </summary>
|
||||
public void UpdateInternalState()
|
||||
{
|
||||
if (IsUsed && NeedToUpdateInternalState)
|
||||
{
|
||||
MixBufferVolume.CopyTo(PreviousMixBufferVolume);
|
||||
}
|
||||
|
||||
NeedToUpdateInternalState = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set the update internal state marker.
|
||||
/// </summary>
|
||||
public void MarkAsNeedToUpdateInternalState()
|
||||
{
|
||||
NeedToUpdateInternalState = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return true if the <see cref="SplitterDestinationVersion1"/> is used and has a destination.
|
||||
/// </summary>
|
||||
/// <returns>True if the <see cref="SplitterDestinationVersion1"/> is used and has a destination.</returns>
|
||||
public readonly bool IsConfigured()
|
||||
{
|
||||
return IsUsed && DestinationId != Constants.UnusedMixId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the volume for a given destination.
|
||||
/// </summary>
|
||||
/// <param name="destinationIndex">The destination index to use.</param>
|
||||
/// <returns>The volume for the given destination.</returns>
|
||||
public float GetMixVolume(int destinationIndex)
|
||||
{
|
||||
Debug.Assert(destinationIndex >= 0 && destinationIndex < Constants.MixBufferCountMax);
|
||||
|
||||
return MixBufferVolume[destinationIndex];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the previous volume for a given destination.
|
||||
/// </summary>
|
||||
/// <param name="destinationIndex">The destination index to use.</param>
|
||||
/// <returns>The volume for the given destination.</returns>
|
||||
public float GetMixVolumePrev(int destinationIndex)
|
||||
{
|
||||
Debug.Assert(destinationIndex >= 0 && destinationIndex < Constants.MixBufferCountMax);
|
||||
|
||||
return PreviousMixBufferVolume[destinationIndex];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clear the volumes.
|
||||
/// </summary>
|
||||
public void ClearVolumes()
|
||||
{
|
||||
MixBufferVolume.Clear();
|
||||
PreviousMixBufferVolume.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Link the next element to the given <see cref="SplitterDestinationVersion1"/>.
|
||||
/// </summary>
|
||||
/// <param name="next">The given <see cref="SplitterDestinationVersion1"/> to link.</param>
|
||||
public void Link(ref SplitterDestinationVersion1 next)
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
fixed (SplitterDestinationVersion1* nextPtr = &next)
|
||||
{
|
||||
_next = nextPtr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove the link to the next element.
|
||||
/// </summary>
|
||||
public void Unlink()
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
_next = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Server state for a splitter destination (version 2).
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Sequential, Size = 0x110, Pack = Alignment)]
|
||||
public struct SplitterDestinationVersion2
|
||||
{
|
||||
public const int Alignment = 0x10;
|
||||
|
||||
/// <summary>
|
||||
/// The unique id of this <see cref="SplitterDestinationVersion2"/>.
|
||||
/// </summary>
|
||||
public int Id;
|
||||
|
||||
/// <summary>
|
||||
/// The mix to output the result of the splitter.
|
||||
/// </summary>
|
||||
public int DestinationId;
|
||||
|
||||
/// <summary>
|
||||
/// Mix buffer volumes storage.
|
||||
/// </summary>
|
||||
private MixArray _mix;
|
||||
private MixArray _previousMix;
|
||||
|
||||
/// <summary>
|
||||
/// Pointer to the next linked element.
|
||||
/// </summary>
|
||||
private unsafe SplitterDestinationVersion2* _next;
|
||||
|
||||
/// <summary>
|
||||
/// Set to true if in use.
|
||||
/// </summary>
|
||||
[MarshalAs(UnmanagedType.I1)]
|
||||
public bool IsUsed;
|
||||
|
||||
/// <summary>
|
||||
/// Set to true if the internal state need to be updated.
|
||||
/// </summary>
|
||||
[MarshalAs(UnmanagedType.I1)]
|
||||
public bool NeedToUpdateInternalState;
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Size = sizeof(float) * Constants.MixBufferCountMax, Pack = 1)]
|
||||
private struct MixArray { }
|
||||
|
||||
/// <summary>
|
||||
/// Mix buffer volumes.
|
||||
/// </summary>
|
||||
/// <remarks>Used when a splitter id is specified in the mix.</remarks>
|
||||
public Span<float> MixBufferVolume => SpanHelpers.AsSpan<MixArray, float>(ref _mix);
|
||||
|
||||
/// <summary>
|
||||
/// Previous mix buffer volumes.
|
||||
/// </summary>
|
||||
/// <remarks>Used when a splitter id is specified in the mix.</remarks>
|
||||
public Span<float> PreviousMixBufferVolume => SpanHelpers.AsSpan<MixArray, float>(ref _previousMix);
|
||||
|
||||
/// <summary>
|
||||
/// Get the reference of the next element or null if not present.
|
||||
/// </summary>
|
||||
public readonly ref SplitterDestinationVersion2 Next
|
||||
{
|
||||
get
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
return ref Unsafe.AsRef<SplitterDestinationVersion2>(_next);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Array2<BiquadFilterParameter> _biquadFilters;
|
||||
|
||||
private Array2<bool> _isPreviousBiquadFilterEnabled;
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="SplitterDestinationVersion2"/>.
|
||||
/// </summary>
|
||||
/// <param name="id">The unique id of this <see cref="SplitterDestinationVersion2"/>.</param>
|
||||
public SplitterDestinationVersion2(int id) : this()
|
||||
{
|
||||
Id = id;
|
||||
DestinationId = Constants.UnusedMixId;
|
||||
|
||||
ClearVolumes();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update the <see cref="SplitterDestinationVersion2"/> from user parameter.
|
||||
/// </summary>
|
||||
/// <param name="parameter">The user parameter.</param>
|
||||
public void Update<T>(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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update the internal state of the instance.
|
||||
/// </summary>
|
||||
public void UpdateInternalState()
|
||||
{
|
||||
if (IsUsed && NeedToUpdateInternalState)
|
||||
{
|
||||
MixBufferVolume.CopyTo(PreviousMixBufferVolume);
|
||||
}
|
||||
|
||||
NeedToUpdateInternalState = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set the update internal state marker.
|
||||
/// </summary>
|
||||
public void MarkAsNeedToUpdateInternalState()
|
||||
{
|
||||
NeedToUpdateInternalState = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return true if the <see cref="SplitterDestinationVersion2"/> is used and has a destination.
|
||||
/// </summary>
|
||||
/// <returns>True if the <see cref="SplitterDestinationVersion2"/> is used and has a destination.</returns>
|
||||
public readonly bool IsConfigured()
|
||||
{
|
||||
return IsUsed && DestinationId != Constants.UnusedMixId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the volume for a given destination.
|
||||
/// </summary>
|
||||
/// <param name="destinationIndex">The destination index to use.</param>
|
||||
/// <returns>The volume for the given destination.</returns>
|
||||
public float GetMixVolume(int destinationIndex)
|
||||
{
|
||||
Debug.Assert(destinationIndex >= 0 && destinationIndex < Constants.MixBufferCountMax);
|
||||
|
||||
return MixBufferVolume[destinationIndex];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the previous volume for a given destination.
|
||||
/// </summary>
|
||||
/// <param name="destinationIndex">The destination index to use.</param>
|
||||
/// <returns>The volume for the given destination.</returns>
|
||||
public float GetMixVolumePrev(int destinationIndex)
|
||||
{
|
||||
Debug.Assert(destinationIndex >= 0 && destinationIndex < Constants.MixBufferCountMax);
|
||||
|
||||
return PreviousMixBufferVolume[destinationIndex];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clear the volumes.
|
||||
/// </summary>
|
||||
public void ClearVolumes()
|
||||
{
|
||||
MixBufferVolume.Clear();
|
||||
PreviousMixBufferVolume.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Link the next element to the given <see cref="SplitterDestinationVersion2"/>.
|
||||
/// </summary>
|
||||
/// <param name="next">The given <see cref="SplitterDestinationVersion2"/> to link.</param>
|
||||
public void Link(ref SplitterDestinationVersion2 next)
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
fixed (SplitterDestinationVersion2* nextPtr = &next)
|
||||
{
|
||||
_next = nextPtr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove the link to the next element.
|
||||
/// </summary>
|
||||
public void Unlink()
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
_next = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if any biquad filter is enabled.
|
||||
/// </summary>
|
||||
/// <returns>True if any biquad filter is enabled.</returns>
|
||||
public bool IsBiquadFilterEnabled()
|
||||
{
|
||||
return _biquadFilters[0].Enable || _biquadFilters[1].Enable;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if any biquad filter was previously enabled.
|
||||
/// </summary>
|
||||
/// <returns>True if any biquad filter was previously enabled.</returns>
|
||||
public bool IsBiquadFilterEnabledPrev()
|
||||
{
|
||||
return _isPreviousBiquadFilterEnabled[0];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the biquad filter parameters.
|
||||
/// </summary>
|
||||
/// <param name="index">Biquad filter index (0 or 1).</param>
|
||||
/// <returns>Biquad filter parameters.</returns>
|
||||
public ref BiquadFilterParameter GetBiquadFilterParameter(int index)
|
||||
{
|
||||
return ref _biquadFilters[index];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if any biquad filter was previously enabled.
|
||||
/// </summary>
|
||||
/// <param name="index">Biquad filter index (0 or 1).</param>
|
||||
public void UpdateBiquadFilterEnabledPrev(int index)
|
||||
{
|
||||
_isPreviousBiquadFilterEnabled[index] = _biquadFilters[index].Enable;
|
||||
}
|
||||
}
|
||||
}
|
@ -15,6 +15,8 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
|
||||
{
|
||||
public const int Alignment = 0x10;
|
||||
|
||||
private delegate void SplitterDestinationAction(SplitterDestination destination, int index);
|
||||
|
||||
/// <summary>
|
||||
/// The unique id of this <see cref="SplitterState"/>.
|
||||
/// </summary>
|
||||
@ -26,7 +28,7 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
|
||||
public uint SampleRate;
|
||||
|
||||
/// <summary>
|
||||
/// Count of splitter destinations (<see cref="SplitterDestination"/>).
|
||||
/// Count of splitter destinations.
|
||||
/// </summary>
|
||||
public int DestinationCount;
|
||||
|
||||
@ -37,20 +39,25 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
|
||||
public bool HasNewConnection;
|
||||
|
||||
/// <summary>
|
||||
/// Linked list of <see cref="SplitterDestination"/>.
|
||||
/// Linked list of <see cref="SplitterDestinationVersion1"/>.
|
||||
/// </summary>
|
||||
private unsafe SplitterDestination* _destinationsData;
|
||||
private unsafe SplitterDestinationVersion1* _destinationDataV1;
|
||||
|
||||
/// <summary>
|
||||
/// Span to the first element of the linked list of <see cref="SplitterDestination"/>.
|
||||
/// Linked list of <see cref="SplitterDestinationVersion2"/>.
|
||||
/// </summary>
|
||||
public readonly Span<SplitterDestination> Destinations
|
||||
private unsafe SplitterDestinationVersion2* _destinationDataV2;
|
||||
|
||||
/// <summary>
|
||||
/// First element of the linked list of splitter destinations data.
|
||||
/// </summary>
|
||||
public readonly SplitterDestination Destination
|
||||
{
|
||||
get
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
return (IntPtr)_destinationsData != IntPtr.Zero ? new Span<SplitterDestination>(_destinationsData, 1) : Span<SplitterDestination>.Empty;
|
||||
return new SplitterDestination(_destinationDataV1, _destinationDataV2);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -64,20 +71,20 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
|
||||
Id = id;
|
||||
}
|
||||
|
||||
public readonly Span<SplitterDestination> GetData(int index)
|
||||
public readonly SplitterDestination GetData(int index)
|
||||
{
|
||||
int i = 0;
|
||||
|
||||
Span<SplitterDestination> 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
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Utility function to apply a given <see cref="SpanAction{T, TArg}"/> to all <see cref="Destinations"/>.
|
||||
/// Utility function to apply an action to all <see cref="Destination"/>.
|
||||
/// </summary>
|
||||
/// <param name="action">The action to execute on each elements.</param>
|
||||
private readonly void ForEachDestination(SpanAction<SplitterDestination, int> action)
|
||||
private readonly void ForEachDestination(SplitterDestinationAction action)
|
||||
{
|
||||
Span<SplitterDestination> temp = Destinations;
|
||||
SplitterDestination temp = Destination;
|
||||
|
||||
int i = 0;
|
||||
|
||||
while (true)
|
||||
{
|
||||
if (temp.IsEmpty)
|
||||
if (temp.IsNull)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
Span<SplitterDestination> 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<SplitterDestination> 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<SplitterDestination> 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
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set the head of the linked list of <see cref="Destinations"/>.
|
||||
/// Set the head of the linked list of <see cref="Destination"/>.
|
||||
/// </summary>
|
||||
/// <param name="newValue">A reference to a <see cref="SplitterDestination"/>.</param>
|
||||
public void SetDestination(ref SplitterDestination newValue)
|
||||
/// <param name="newValue">New destination value.</param>
|
||||
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
|
||||
/// </summary>
|
||||
public readonly void UpdateInternalState()
|
||||
{
|
||||
ForEachDestination((destination, _) => destination[0].UpdateInternalState());
|
||||
ForEachDestination((destination, _) => destination.UpdateInternalState());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clear all links from the <see cref="Destinations"/>.
|
||||
/// Clear all links from the <see cref="Destination"/>.
|
||||
/// </summary>
|
||||
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;
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
@ -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,7 @@ 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;
|
||||
@ -65,6 +67,7 @@ namespace Ryujinx.Graphics.GAL
|
||||
public Capabilities(
|
||||
TargetApi api,
|
||||
string vendorName,
|
||||
SystemMemoryType memoryType,
|
||||
bool hasFrontFacingBug,
|
||||
bool hasVectorIndexingBug,
|
||||
bool needsFragmentOutputSpecialization,
|
||||
@ -93,6 +96,7 @@ namespace Ryujinx.Graphics.GAL
|
||||
bool supportsMismatchingViewFormat,
|
||||
bool supportsCubemapView,
|
||||
bool supportsNonConstantTextureOffset,
|
||||
bool supportsQuads,
|
||||
bool supportsSeparateSampler,
|
||||
bool supportsShaderBallot,
|
||||
bool supportsShaderBarrierDivergence,
|
||||
@ -118,6 +122,7 @@ namespace Ryujinx.Graphics.GAL
|
||||
{
|
||||
Api = api;
|
||||
VendorName = vendorName;
|
||||
MemoryType = memoryType;
|
||||
HasFrontFacingBug = hasFrontFacingBug;
|
||||
HasVectorIndexingBug = hasVectorIndexingBug;
|
||||
NeedsFragmentOutputSpecialization = needsFragmentOutputSpecialization;
|
||||
@ -146,6 +151,7 @@ namespace Ryujinx.Graphics.GAL
|
||||
SupportsMismatchingViewFormat = supportsMismatchingViewFormat;
|
||||
SupportsCubemapView = supportsCubemapView;
|
||||
SupportsNonConstantTextureOffset = supportsNonConstantTextureOffset;
|
||||
SupportsQuads = supportsQuads;
|
||||
SupportsSeparateSampler = supportsSeparateSampler;
|
||||
SupportsShaderBallot = supportsShaderBallot;
|
||||
SupportsShaderBarrierDivergence = supportsShaderBarrierDivergence;
|
||||
|
@ -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<BufferRange> storageBuffers);
|
||||
|
||||
|
@ -44,7 +44,6 @@ namespace Ryujinx.Graphics.GAL.Multithreading
|
||||
}
|
||||
|
||||
Register<ActionCommand>(CommandType.Action);
|
||||
Register<CreateBufferCommand>(CommandType.CreateBuffer);
|
||||
Register<CreateBufferAccessCommand>(CommandType.CreateBufferAccess);
|
||||
Register<CreateBufferSparseCommand>(CommandType.CreateBufferSparse);
|
||||
Register<CreateHostBufferCommand>(CommandType.CreateHostBuffer);
|
||||
|
@ -3,7 +3,6 @@ namespace Ryujinx.Graphics.GAL.Multithreading
|
||||
enum CommandType : byte
|
||||
{
|
||||
Action,
|
||||
CreateBuffer,
|
||||
CreateBufferAccess,
|
||||
CreateBufferSparse,
|
||||
CreateHostBuffer,
|
||||
|
@ -1,31 +0,0 @@
|
||||
namespace Ryujinx.Graphics.GAL.Multithreading.Commands.Renderer
|
||||
{
|
||||
struct CreateBufferCommand : IGALCommand, IGALCommand<CreateBufferCommand>
|
||||
{
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
@ -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<CreateBufferCommand>().Set(handle, size, access, storageHint);
|
||||
QueueCommand();
|
||||
|
||||
return handle;
|
||||
}
|
||||
|
||||
public BufferHandle CreateBuffer(nint pointer, int size)
|
||||
{
|
||||
BufferHandle handle = Buffers.CreateBufferHandle();
|
||||
|
29
src/Ryujinx.Graphics.GAL/SystemMemoryType.cs
Normal file
29
src/Ryujinx.Graphics.GAL/SystemMemoryType.cs
Normal file
@ -0,0 +1,29 @@
|
||||
namespace Ryujinx.Graphics.GAL
|
||||
{
|
||||
public enum SystemMemoryType
|
||||
{
|
||||
/// <summary>
|
||||
/// The backend manages the ownership of memory. This mode never supports host imported memory.
|
||||
/// </summary>
|
||||
BackendManaged,
|
||||
|
||||
/// <summary>
|
||||
/// Device memory has similar performance to host memory, usually because it's shared between CPU/GPU.
|
||||
/// Use host memory whenever possible.
|
||||
/// </summary>
|
||||
UnifiedMemory,
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
DedicatedMemory,
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
DedicatedMemorySlowStorage
|
||||
}
|
||||
}
|
@ -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,
|
||||
|
@ -438,7 +438,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed.ComputeDraw
|
||||
|
||||
ReadOnlySpan<byte> dataBytes = MemoryMarshal.Cast<int, byte>(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<int> 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;
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -393,17 +393,18 @@ namespace Ryujinx.Graphics.Gpu
|
||||
|
||||
if (force || _pendingSync || (syncpoint && SyncpointActions.Count > 0))
|
||||
{
|
||||
Renderer.CreateSync(SyncNumber, strict);
|
||||
|
||||
foreach (var action in SyncActions)
|
||||
{
|
||||
action.SyncPreAction(syncpoint);
|
||||
}
|
||||
|
||||
foreach (var action in SyncpointActions)
|
||||
{
|
||||
action.SyncPreAction(syncpoint);
|
||||
}
|
||||
|
||||
Renderer.CreateSync(SyncNumber, strict);
|
||||
|
||||
SyncNumber++;
|
||||
|
||||
SyncActions.RemoveAll(action => action.SyncAction(syncpoint));
|
||||
|
@ -390,7 +390,7 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
{
|
||||
_views.Remove(texture);
|
||||
|
||||
Group.RemoveView(texture);
|
||||
Group.RemoveView(_views, texture);
|
||||
|
||||
texture._viewStorage = texture;
|
||||
|
||||
|
@ -708,11 +708,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)
|
||||
@ -921,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)
|
||||
|
@ -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<Texture> _textures;
|
||||
private readonly HashSet<Texture> _partiallyMappedTextures;
|
||||
|
||||
private readonly ReaderWriterLockSlim _texturesLock;
|
||||
|
||||
private Texture[] _textureOverlaps;
|
||||
private OverlapInfo[] _overlapInfo;
|
||||
|
||||
@ -57,6 +60,8 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
_textures = new MultiRangeList<Texture>();
|
||||
_partiallyMappedTextures = new HashSet<Texture>();
|
||||
|
||||
_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
|
||||
/// <param name="texture">The texture to be removed</param>
|
||||
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
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
lock (_textures)
|
||||
_texturesLock.EnterReadLock();
|
||||
|
||||
try
|
||||
{
|
||||
foreach (Texture texture in _textures)
|
||||
{
|
||||
texture.Dispose();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_texturesLock.ExitReadLock();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -88,9 +88,9 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
private MultiRange TextureRange => Storage.Range;
|
||||
|
||||
/// <summary>
|
||||
/// The views list from the storage texture.
|
||||
/// The views array from the storage texture.
|
||||
/// </summary>
|
||||
private List<Texture> _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<Texture> 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
|
||||
/// <summary>
|
||||
/// Removes a view from the group, removing it from all overlap lists.
|
||||
/// </summary>
|
||||
/// <param name="views">The views list of the storage texture</param>
|
||||
/// <param name="view">View to remove from the group</param>
|
||||
public void RemoveView(Texture view)
|
||||
public void RemoveView(List<Texture> 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();
|
||||
}
|
||||
|
@ -121,7 +121,7 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
public TextureGroupHandle(TextureGroup group,
|
||||
int offset,
|
||||
ulong size,
|
||||
List<Texture> views,
|
||||
IEnumerable<Texture> 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.
|
||||
/// </summary>
|
||||
/// <param name="group">The parent texture group, used to find a view's base CPU VA offset</param>
|
||||
/// <param name="views">The list of views to search for overlaps</param>
|
||||
public void RecalculateOverlaps(TextureGroup group, List<Texture> views)
|
||||
/// <param name="views">The views to search for overlaps</param>
|
||||
public void RecalculateOverlaps(TextureGroup group, IEnumerable<Texture> views)
|
||||
{
|
||||
// Overlaps can be accessed from the memory tracking signal handler, so access must be atomic.
|
||||
lock (Overlaps)
|
||||
|
@ -10,6 +10,8 @@ using System.Threading;
|
||||
|
||||
namespace Ryujinx.Graphics.Gpu.Memory
|
||||
{
|
||||
delegate void BufferFlushAction(ulong address, ulong size, ulong syncNumber);
|
||||
|
||||
/// <summary>
|
||||
/// Buffer, used to store vertex and index data, uniform and storage buffers, and others.
|
||||
/// </summary>
|
||||
@ -23,7 +25,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
||||
/// <summary>
|
||||
/// Host buffer handle.
|
||||
/// </summary>
|
||||
public BufferHandle Handle { get; }
|
||||
public BufferHandle Handle { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Start address of the buffer in guest memory.
|
||||
@ -60,6 +62,17 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
||||
/// </remarks>
|
||||
private BufferModifiedRangeList _modifiedRanges = null;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
private BufferPreFlush _preFlush = null;
|
||||
|
||||
/// <summary>
|
||||
/// Usage tracking state that determines what type of backing the buffer should use.
|
||||
/// </summary>
|
||||
public BufferBackingState BackingState;
|
||||
|
||||
private readonly MultiRegionHandle _memoryTrackingGranular;
|
||||
private readonly RegionHandle _memoryTracking;
|
||||
|
||||
@ -87,6 +100,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
||||
/// <param name="physicalMemory">Physical memory where the buffer is mapped</param>
|
||||
/// <param name="address">Start address of the buffer</param>
|
||||
/// <param name="size">Size of the buffer in bytes</param>
|
||||
/// <param name="stage">The type of usage that created the buffer</param>
|
||||
/// <param name="sparseCompatible">Indicates if the buffer can be used in a sparse buffer mapping</param>
|
||||
/// <param name="baseBuffers">Buffers which this buffer contains, and will inherit tracking handles from</param>
|
||||
public Buffer(
|
||||
@ -94,6 +108,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
||||
PhysicalMemory physicalMemory,
|
||||
ulong address,
|
||||
ulong size,
|
||||
BufferStage stage,
|
||||
bool sparseCompatible,
|
||||
IEnumerable<Buffer> 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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Recreates the backing buffer based on the desired access type
|
||||
/// reported by the backing state struct.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a sub-range from the buffer, from a start address til a page boundary after the given size.
|
||||
/// </summary>
|
||||
@ -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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="stage">Buffer stage that can change backing type</param>
|
||||
private void TryQueueBackingChange(BufferStage stage)
|
||||
{
|
||||
if (BackingState.ShouldChangeBacking(stage))
|
||||
{
|
||||
if (!_syncActionRegistered)
|
||||
{
|
||||
_context.RegisterSyncAction(this);
|
||||
_syncActionRegistered = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Signal that the given region of the buffer has been modified.
|
||||
/// </summary>
|
||||
/// <param name="address">The start address of the modified region</param>
|
||||
/// <param name="size">The size of the modified region</param>
|
||||
public void SignalModified(ulong address, ulong size)
|
||||
/// <param name="stage">Buffer stage that triggered the modification</param>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Action to be performed immediately before sync is created.
|
||||
/// This will copy any buffer ranges designated for pre-flushing.
|
||||
/// </summary>
|
||||
/// <param name="syncpoint">True if the action is a guest syncpoint</param>
|
||||
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);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// <param name="mSize">Size of the modified region</param>
|
||||
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.
|
||||
/// </summary>
|
||||
/// <param name="handle">Buffer handle to flush data from</param>
|
||||
/// <param name="address">Start address of the range</param>
|
||||
/// <param name="size">Size in bytes of the range</param>
|
||||
public void Flush(ulong address, ulong size)
|
||||
private void FlushImpl(BufferHandle handle, ulong address, ulong size)
|
||||
{
|
||||
int offset = (int)(address - Address);
|
||||
|
||||
using PinnedSpan<byte> data = _context.Renderer.GetBufferData(Handle, offset, (int)size);
|
||||
using PinnedSpan<byte> 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));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Flushes a range of the buffer.
|
||||
/// This writes the range data back into guest memory.
|
||||
/// </summary>
|
||||
/// <param name="address">Start address of the range</param>
|
||||
/// <param name="size">Size in bytes of the range</param>
|
||||
private void FlushImpl(ulong address, ulong size)
|
||||
{
|
||||
FlushImpl(Handle, address, size);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Flushes a range of the buffer from the most optimal source.
|
||||
/// This writes the range data back into guest memory.
|
||||
/// </summary>
|
||||
/// <param name="address">Start address of the range</param>
|
||||
/// <param name="size">Size in bytes of the range</param>
|
||||
/// <param name="syncNumber">Sync number waited for before flushing the data</param>
|
||||
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);
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <returns>An action that flushes data from the specified range, using the buffer handle at the time the method is generated</returns>
|
||||
public Action GetSnapshotDisposeAction()
|
||||
{
|
||||
BufferHandle handle = Handle;
|
||||
BufferPreFlush preFlush = _preFlush;
|
||||
|
||||
return () =>
|
||||
{
|
||||
_context.Renderer.DeleteBuffer(handle);
|
||||
preFlush?.Dispose();
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <returns>An action that flushes data from the specified range, using the buffer handle at the time the method is generated</returns>
|
||||
public BufferFlushAction GetSnapshotFlushAction()
|
||||
{
|
||||
BufferHandle handle = Handle;
|
||||
|
||||
return (ulong address, ulong size, ulong _) =>
|
||||
{
|
||||
FlushImpl(handle, address, size);
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Align a given address and size region to page boundaries.
|
||||
/// </summary>
|
||||
@ -857,6 +1017,8 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
||||
_modifiedRanges?.Clear();
|
||||
|
||||
_context.Renderer.DeleteBuffer(Handle);
|
||||
_preFlush?.Dispose();
|
||||
_preFlush = null;
|
||||
|
||||
UnmappedSequence++;
|
||||
}
|
||||
|
294
src/Ryujinx.Graphics.Gpu/Memory/BufferBackingState.cs
Normal file
294
src/Ryujinx.Graphics.Gpu/Memory/BufferBackingState.cs
Normal file
@ -0,0 +1,294 @@
|
||||
using Ryujinx.Graphics.GAL;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Ryujinx.Graphics.Gpu.Memory
|
||||
{
|
||||
/// <summary>
|
||||
/// Type of backing memory.
|
||||
/// In ascending order of priority when merging multiple buffer backing states.
|
||||
/// </summary>
|
||||
internal enum BufferBackingType
|
||||
{
|
||||
HostMemory,
|
||||
DeviceMemory,
|
||||
DeviceMemoryWithFlush
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Initialize the buffer backing state for a given parent buffer.
|
||||
/// </summary>
|
||||
/// <param name="context">GPU context</param>
|
||||
/// <param name="parent">Parent buffer</param>
|
||||
/// <param name="stage">Initial buffer stage</param>
|
||||
/// <param name="baseBuffers">Buffers to inherit state from</param>
|
||||
public BufferBackingState(GpuContext context, Buffer parent, BufferStage stage, IEnumerable<Buffer> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Combine buffer backing types, selecting the one with highest priority.
|
||||
/// </summary>
|
||||
/// <param name="left">First buffer backing type</param>
|
||||
/// <param name="right">Second buffer backing type</param>
|
||||
/// <returns>Combined buffer backing type</returns>
|
||||
private static BufferBackingType CombineTypes(BufferBackingType left, BufferBackingType right)
|
||||
{
|
||||
return (BufferBackingType)Math.Max((int)left, (int)right);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Combine the state from the given buffer backing state with this one,
|
||||
/// so that the state isn't lost when migrating buffers.
|
||||
/// </summary>
|
||||
/// <param name="oldState">Buffer state to combine into this state</param>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the buffer access for the desired backing type, and record that type as now being active.
|
||||
/// </summary>
|
||||
/// <param name="parent">Parent buffer</param>
|
||||
/// <returns>Buffer access</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Record when data has been uploaded to the buffer.
|
||||
/// </summary>
|
||||
public void RecordSet()
|
||||
{
|
||||
_setCount++;
|
||||
|
||||
ConsiderUseCounts();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Record when data has been flushed from the buffer.
|
||||
/// </summary>
|
||||
public void RecordFlush()
|
||||
{
|
||||
if (_lastFlushWrite != _writeCount)
|
||||
{
|
||||
// If it's on the same page as the last flush, ignore it.
|
||||
_lastFlushWrite = _writeCount;
|
||||
_flushCount++;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determine if the buffer backing should be changed.
|
||||
/// </summary>
|
||||
/// <returns>True if the desired backing type is different from the current type</returns>
|
||||
public readonly bool ShouldChangeBacking()
|
||||
{
|
||||
return _desiredType != _activeType;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determine if the buffer backing should be changed, considering a new use with the given buffer stage.
|
||||
/// </summary>
|
||||
/// <param name="stage">Buffer stage for the use</param>
|
||||
/// <returns>True if the desired backing type is different from the current type</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -107,8 +107,9 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
||||
/// <param name="memoryManager">GPU memory manager where the buffer is mapped</param>
|
||||
/// <param name="gpuVa">Start GPU virtual address of the buffer</param>
|
||||
/// <param name="size">Size in bytes of the buffer</param>
|
||||
/// <param name="stage">The type of usage that created the buffer</param>
|
||||
/// <returns>Contiguous physical range of the buffer, after address translation</returns>
|
||||
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
|
||||
/// <param name="memoryManager">GPU memory manager where the buffer is mapped</param>
|
||||
/// <param name="gpuVa">Start GPU virtual address of the buffer</param>
|
||||
/// <param name="size">Size in bytes of the buffer</param>
|
||||
/// <param name="stage">The type of usage that created the buffer</param>
|
||||
/// <returns>Physical ranges of the buffer, after address translation</returns>
|
||||
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
|
||||
/// <param name="memoryManager">GPU memory manager where the buffer is mapped</param>
|
||||
/// <param name="gpuVa">Start GPU virtual address of the buffer</param>
|
||||
/// <param name="size">Size in bytes of the buffer</param>
|
||||
/// <param name="stage">The type of usage that created the buffer</param>
|
||||
/// <returns>Physical ranges of the buffer, after address translation</returns>
|
||||
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.
|
||||
/// </summary>
|
||||
/// <param name="range">Physical ranges of memory where the buffer data is located</param>
|
||||
public void CreateBuffer(MultiRange range)
|
||||
/// <param name="stage">The type of usage that created the buffer</param>
|
||||
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
|
||||
/// </summary>
|
||||
/// <param name="address">Address of the buffer in memory</param>
|
||||
/// <param name="size">Size of the buffer in bytes</param>
|
||||
public void CreateBuffer(ulong address, ulong size)
|
||||
/// <param name="stage">The type of usage that created the buffer</param>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -248,8 +253,9 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
||||
/// </summary>
|
||||
/// <param name="address">Address of the buffer in memory</param>
|
||||
/// <param name="size">Size of the buffer in bytes</param>
|
||||
/// <param name="stage">The type of usage that created the buffer</param>
|
||||
/// <param name="alignment">Alignment of the start address of the buffer in bytes</param>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -272,7 +278,8 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
||||
/// if it does not exist yet.
|
||||
/// </summary>
|
||||
/// <param name="range">Physical ranges of memory</param>
|
||||
private void CreateMultiRangeBuffer(MultiRange range)
|
||||
/// <param name="stage">The type of usage that created the buffer</param>
|
||||
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
|
||||
/// </summary>
|
||||
/// <param name="address">Address of the buffer in guest memory</param>
|
||||
/// <param name="size">Size in bytes of the buffer</param>
|
||||
private void CreateBufferAligned(ulong address, ulong size)
|
||||
/// <param name="stage">The type of usage that created the buffer</param>
|
||||
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
|
||||
/// </summary>
|
||||
/// <param name="address">Address of the buffer in guest memory</param>
|
||||
/// <param name="size">Size in bytes of the buffer</param>
|
||||
/// <param name="stage">The type of usage that created the buffer</param>
|
||||
/// <param name="alignment">Alignment of the start address of the buffer</param>
|
||||
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
|
||||
/// </summary>
|
||||
/// <param name="address">Address of the buffer in guest memory</param>
|
||||
/// <param name="size">Size in bytes of the buffer</param>
|
||||
/// <param name="stage">The type of usage that created the buffer</param>
|
||||
/// <param name="sparseCompatible">Indicates if the buffer can be used in a sparse buffer mapping</param>
|
||||
/// <param name="overlaps">Buffers overlapping the range</param>
|
||||
/// <param name="overlapsCount">Total of overlaps</param>
|
||||
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
|
||||
/// <param name="size">Size in bytes of the copy</param>
|
||||
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
|
||||
/// <param name="size">Size in bytes of the copy</param>
|
||||
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
|
||||
/// <param name="value">Value to be written into the buffer</param>
|
||||
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.
|
||||
/// </summary>
|
||||
/// <param name="range">Physical regions of memory where the buffer is mapped</param>
|
||||
/// <param name="stage">Buffer stage that triggered the access</param>
|
||||
/// <param name="write">Whether the buffer will be written to by this use</param>
|
||||
/// <returns>The buffer sub-range starting at the given memory address</returns>
|
||||
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.
|
||||
/// </summary>
|
||||
/// <param name="range">Physical regions of memory where the buffer is mapped</param>
|
||||
/// <param name="stage">Buffer stage that triggered the access</param>
|
||||
/// <param name="write">Whether the buffer will be written to by this use</param>
|
||||
/// <returns>The buffer sub-range for the given range</returns>
|
||||
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.
|
||||
/// </summary>
|
||||
/// <param name="range">Physical regions of memory where the buffer is mapped</param>
|
||||
/// <param name="stage">Buffer stage that triggered the access</param>
|
||||
/// <param name="write">Whether the buffer will be written to by this use</param>
|
||||
/// <returns>The buffer where the range is fully contained</returns>
|
||||
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
|
||||
/// </summary>
|
||||
/// <param name="address">Start address of the memory range</param>
|
||||
/// <param name="size">Size in bytes of the memory range</param>
|
||||
/// <param name="stage">Buffer stage that triggered the access</param>
|
||||
/// <param name="write">Whether the buffer will be written to by this use</param>
|
||||
/// <returns>The buffer where the range is fully contained</returns>
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Signal that the given buffer's handle has changed,
|
||||
/// forcing rebind and any overlapping multi-range buffers to be recreated.
|
||||
/// </summary>
|
||||
/// <param name="buffer">The buffer that has changed handle</param>
|
||||
public void BufferBackingChanged(Buffer buffer)
|
||||
{
|
||||
NotifyBuffersModified?.Invoke();
|
||||
|
||||
RecreateMultiRangeBuffers(buffer.Address, buffer.Size);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Prune any invalid entries from a quick access dictionary.
|
||||
/// </summary>
|
||||
|
@ -156,7 +156,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
||||
/// <param name="type">Type of each index buffer element</param>
|
||||
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
|
||||
/// <param name="divisor">Vertex divisor of the buffer, for instanced draws</param>
|
||||
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
|
||||
/// <param name="size">Size in bytes of the transform feedback buffer</param>
|
||||
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<ulong>(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<ulong>(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
|
||||
/// <param name="size">Size in bytes of the storage buffer</param>
|
||||
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
|
||||
/// <param name="size">Size in bytes of the storage buffer</param>
|
||||
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
|
||||
/// <summary>
|
||||
/// Sets the buffer storage of a buffer texture array element. This will be bound when the buffer manager commits bindings.
|
||||
/// </summary>
|
||||
/// <param name="stage">Shader stage accessing the texture</param>
|
||||
/// <param name="array">Texture array where the element will be inserted</param>
|
||||
/// <param name="texture">Buffer texture</param>
|
||||
/// <param name="range">Physical ranges of memory where the buffer texture data is located</param>
|
||||
@ -890,6 +892,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
||||
/// <param name="index">Index of the binding on the array</param>
|
||||
/// <param name="format">Format of the buffer texture</param>
|
||||
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<ITextureArray>(array, texture, range, bindingInfo, index, format));
|
||||
}
|
||||
@ -905,6 +908,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
||||
/// <summary>
|
||||
/// Sets the buffer storage of a buffer image array element. This will be bound when the buffer manager commits bindings.
|
||||
/// </summary>
|
||||
/// <param name="stage">Shader stage accessing the texture</param>
|
||||
/// <param name="array">Image array where the element will be inserted</param>
|
||||
/// <param name="texture">Buffer texture</param>
|
||||
/// <param name="range">Physical ranges of memory where the buffer texture data is located</param>
|
||||
@ -912,6 +916,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
||||
/// <param name="index">Index of the binding on the array</param>
|
||||
/// <param name="format">Format of the buffer texture</param>
|
||||
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<IImageArray>(array, texture, range, bindingInfo, index, format));
|
||||
}
|
||||
|
@ -1,37 +1,21 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
|
||||
namespace Ryujinx.Graphics.Gpu.Memory
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
internal class BufferMigration : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// The offset for the migrated region.
|
||||
/// Ranges from source buffers that were copied as part of this migration.
|
||||
/// Ordered by increasing base address.
|
||||
/// </summary>
|
||||
private readonly ulong _offset;
|
||||
|
||||
/// <summary>
|
||||
/// The size for the migrated region.
|
||||
/// </summary>
|
||||
private readonly ulong _size;
|
||||
|
||||
/// <summary>
|
||||
/// The buffer that was migrated from.
|
||||
/// </summary>
|
||||
private readonly Buffer _buffer;
|
||||
|
||||
/// <summary>
|
||||
/// The source range action, to be called on overlap with an unreached sync number.
|
||||
/// </summary>
|
||||
private readonly Action<ulong, ulong> _sourceRangeAction;
|
||||
|
||||
/// <summary>
|
||||
/// The source range list.
|
||||
/// </summary>
|
||||
private readonly BufferModifiedRangeList _source;
|
||||
public BufferMigrationSpan[] Spans { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The destination range list. This range list must be updated when flushing the source.
|
||||
@ -43,55 +27,193 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
||||
/// </summary>
|
||||
public readonly ulong SyncNumber;
|
||||
|
||||
/// <summary>
|
||||
/// Number of active users there are traversing this migration's spans.
|
||||
/// </summary>
|
||||
private int _refCount;
|
||||
|
||||
/// <summary>
|
||||
/// Create a new buffer migration.
|
||||
/// </summary>
|
||||
/// <param name="spans">Source spans for the migration</param>
|
||||
/// <param name="destination">Destination buffer range list</param>
|
||||
/// <param name="syncNumber">Sync number where this migration will be complete</param>
|
||||
public BufferMigration(BufferMigrationSpan[] spans, BufferModifiedRangeList destination, ulong syncNumber)
|
||||
{
|
||||
Spans = spans;
|
||||
Destination = destination;
|
||||
SyncNumber = syncNumber;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a span to the migration. Allocates a new array with the target size, and replaces it.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The base address for the span is assumed to be higher than all other spans in the migration,
|
||||
/// to keep the span array ordered.
|
||||
/// </remarks>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performs the given range action, or one from a migration that overlaps and has not synced yet.
|
||||
/// </summary>
|
||||
/// <param name="offset">The offset to pass to the action</param>
|
||||
/// <param name="size">The size to pass to the action</param>
|
||||
/// <param name="syncNumber">The sync number that has been reached</param>
|
||||
/// <param name="rangeAction">The action to perform</param>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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)
|
||||
/// </summary>
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
internal readonly struct BufferMigrationSpan : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// The offset for the migrated region.
|
||||
/// </summary>
|
||||
public readonly ulong Address;
|
||||
|
||||
/// <summary>
|
||||
/// The size for the migrated region.
|
||||
/// </summary>
|
||||
public readonly ulong Size;
|
||||
|
||||
/// <summary>
|
||||
/// The action to perform when the migration isn't needed anymore.
|
||||
/// </summary>
|
||||
private readonly Action _disposeAction;
|
||||
|
||||
/// <summary>
|
||||
/// The source range action, to be called on overlap with an unreached sync number.
|
||||
/// </summary>
|
||||
private readonly BufferFlushAction _sourceRangeAction;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
private readonly BufferMigration _source;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a record for a buffer migration.
|
||||
/// </summary>
|
||||
/// <param name="buffer">The source buffer for this migration</param>
|
||||
/// <param name="disposeAction">The action to perform when the migration isn't needed anymore</param>
|
||||
/// <param name="sourceRangeAction">The flush action for the source buffer</param>
|
||||
/// <param name="source">The modified range list for the source buffer</param>
|
||||
/// <param name="dest">The modified range list for the destination buffer</param>
|
||||
/// <param name="syncNumber">The sync number for when the migration is complete</param>
|
||||
public BufferMigration(
|
||||
/// <param name="source">Pending migration for the source buffer</param>
|
||||
public BufferMigrationSpan(
|
||||
Buffer buffer,
|
||||
Action<ulong, ulong> 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a record for a buffer migration, using the default buffer dispose action.
|
||||
/// </summary>
|
||||
/// <param name="buffer">The source buffer for this migration</param>
|
||||
/// <param name="sourceRangeAction">The flush action for the source buffer</param>
|
||||
/// <param name="source">Pending migration for the source buffer</param>
|
||||
public BufferMigrationSpan(
|
||||
Buffer buffer,
|
||||
BufferFlushAction sourceRangeAction,
|
||||
BufferMigration source) : this(buffer, buffer.DecrementReferenceCount, sourceRangeAction, source) { }
|
||||
|
||||
/// <summary>
|
||||
/// Determine if the given range overlaps this migration, and has not been completed yet.
|
||||
/// </summary>
|
||||
/// <param name="offset">Start offset</param>
|
||||
/// <param name="size">Range size</param>
|
||||
/// <param name="syncNumber">The sync number that was waited on</param>
|
||||
/// <returns>True if overlapping and in progress, false otherwise</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determine if the given range matches this migration.
|
||||
/// </summary>
|
||||
/// <param name="offset">Start offset</param>
|
||||
/// <param name="size">Range size</param>
|
||||
/// <returns>True if the range exactly matches, false otherwise</returns>
|
||||
public bool FullyMatches(ulong offset, ulong size)
|
||||
{
|
||||
return _offset == offset && _size == size;
|
||||
return !(end <= Address || offset >= destEnd);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -100,26 +222,30 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
||||
/// <param name="offset">Start offset</param>
|
||||
/// <param name="size">Range size</param>
|
||||
/// <param name="syncNumber">Current sync number</param>
|
||||
/// <param name="parent">The modified range list that originally owned this range</param>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
Destination.RemoveMigration(this);
|
||||
|
||||
_buffer.DecrementReferenceCount();
|
||||
_disposeAction();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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<ulong, ulong> _flushAction;
|
||||
private readonly BufferFlushAction _flushAction;
|
||||
|
||||
private List<BufferMigration> _sources;
|
||||
private BufferMigration _migrationTarget;
|
||||
private BufferMigration _source;
|
||||
private BufferModifiedRangeList _migrationTarget;
|
||||
|
||||
private readonly object _lock = new();
|
||||
|
||||
@ -99,7 +98,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
||||
/// <param name="context">GPU context that the buffer range list belongs to</param>
|
||||
/// <param name="parent">The parent buffer that owns this range list</param>
|
||||
/// <param name="flushAction">The flush action for the parent buffer</param>
|
||||
public BufferModifiedRangeList(GpuContext context, Buffer parent, Action<ulong, ulong> 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
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets modified ranges within the specified region, and then fires the given action for each range individually.
|
||||
/// </summary>
|
||||
/// <param name="address">Start address to query</param>
|
||||
/// <param name="size">Size to query</param>
|
||||
/// <param name="syncNumber">Sync number required for a range to be signalled</param>
|
||||
/// <param name="rangeAction">The action to call for each modified range</param>
|
||||
public void GetRangesAtSync(ulong address, ulong size, ulong syncNumber, Action<ulong, ulong> rangeAction)
|
||||
{
|
||||
int count = 0;
|
||||
|
||||
ref var overlaps = ref ThreadStaticArray<BufferModifiedRange>.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets modified ranges within the specified region, and then fires the given action for each range individually.
|
||||
/// </summary>
|
||||
@ -245,41 +274,16 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
||||
/// <param name="offset">The offset to pass to the action</param>
|
||||
/// <param name="size">The size to pass to the action</param>
|
||||
/// <param name="syncNumber">The sync number that has been reached</param>
|
||||
/// <param name="parent">The modified range list that originally owned this range</param>
|
||||
/// <param name="rangeAction">The action to perform</param>
|
||||
public void RangeActionWithMigration(ulong offset, ulong size, ulong syncNumber, BufferModifiedRangeList parent, Action<ulong, ulong> 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -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
|
||||
/// <summary>
|
||||
/// Inherit ranges from another modified range list.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Assumes that ranges will be inherited in address ascending order.
|
||||
/// </remarks>
|
||||
/// <param name="ranges">The range list to inherit from</param>
|
||||
/// <param name="registerRangeAction">The action to call for each modified range</param>
|
||||
public void InheritRanges(BufferModifiedRangeList ranges, Action<ulong, ulong> 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<BufferMigration>()).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
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a source buffer migration, indicating its copy has completed.
|
||||
/// </summary>
|
||||
@ -453,7 +494,10 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_sources.Remove(migration);
|
||||
if (_source == migration)
|
||||
{
|
||||
_source = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
295
src/Ryujinx.Graphics.Gpu/Memory/BufferPreFlush.cs
Normal file
295
src/Ryujinx.Graphics.Gpu/Memory/BufferPreFlush.cs
Normal file
@ -0,0 +1,295 @@
|
||||
using Ryujinx.Common;
|
||||
using Ryujinx.Graphics.GAL;
|
||||
using System;
|
||||
|
||||
namespace Ryujinx.Graphics.Gpu.Memory
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
internal class BufferPreFlush : IDisposable
|
||||
{
|
||||
private const ulong PageSize = MemoryManager.PageSize;
|
||||
|
||||
/// <summary>
|
||||
/// Threshold for the number of copies without a flush required to disable preflush on a page.
|
||||
/// </summary>
|
||||
private const int DeactivateCopyThreshold = 200;
|
||||
|
||||
/// <summary>
|
||||
/// Value that indicates whether a page has been flushed or copied before.
|
||||
/// </summary>
|
||||
private enum PreFlushState
|
||||
{
|
||||
None,
|
||||
HasFlushed,
|
||||
HasCopied
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
private struct PreFlushPage
|
||||
{
|
||||
public PreFlushState State;
|
||||
public ulong FirstActivatedSync;
|
||||
public ulong LastCopiedSync;
|
||||
public int CopyCount;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// True if there are ranges that should copy to the flush buffer, false otherwise.
|
||||
/// </summary>
|
||||
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<BufferHandle, ulong, ulong> _flushAction;
|
||||
|
||||
private BufferHandle _flushBuffer;
|
||||
|
||||
public BufferPreFlush(GpuContext context, Buffer parent, Action<BufferHandle, ulong, ulong> flushAction)
|
||||
{
|
||||
_context = context;
|
||||
_buffer = parent;
|
||||
_address = parent.Address;
|
||||
_size = parent.Size;
|
||||
_pages = new PreFlushPage[BitUtils.DivRoundUp(_size, PageSize)];
|
||||
_misalignment = _address & (PageSize - 1);
|
||||
|
||||
_flushAction = flushAction;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensure that the flush buffer exists.
|
||||
/// </summary>
|
||||
private void EnsureFlushBuffer()
|
||||
{
|
||||
if (_flushBuffer == BufferHandle.Null)
|
||||
{
|
||||
_flushBuffer = _context.Renderer.CreateBuffer((int)_size, BufferAccess.HostMemory);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a page range from an address and size byte range.
|
||||
/// </summary>
|
||||
/// <param name="address">Range address</param>
|
||||
/// <param name="size">Range size</param>
|
||||
/// <returns>A page index and count</returns>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets an offset and size range in the parent buffer from a page index and count.
|
||||
/// </summary>
|
||||
/// <param name="startPage">Range start page</param>
|
||||
/// <param name="count">Range page count</param>
|
||||
/// <returns>Offset and size range</returns>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copy a range of pages from the parent buffer into the flush buffer.
|
||||
/// </summary>
|
||||
/// <param name="startPage">Range start page</param>
|
||||
/// <param name="count">Range page count</param>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="address">Range address</param>
|
||||
/// <param name="size">Range size</param>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="address">Address range start</param>
|
||||
/// <param name="size">Address range size</param>
|
||||
/// <param name="startPage">Page range start</param>
|
||||
/// <param name="count">Page range count</param>
|
||||
/// <param name="preFlush">True if the data should come from the flush buffer</param>
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="address">Range address</param>
|
||||
/// <param name="size">Range size</param>
|
||||
/// <param name="syncNumber">Sync number that has been waited for</param>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dispose the flush buffer, if present.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
if (_flushBuffer != BufferHandle.Null)
|
||||
{
|
||||
_context.Renderer.DeleteBuffer(_flushBuffer);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
99
src/Ryujinx.Graphics.Gpu/Memory/BufferStage.cs
Normal file
99
src/Ryujinx.Graphics.Gpu/Memory/BufferStage.cs
Normal file
@ -0,0 +1,99 @@
|
||||
using Ryujinx.Graphics.Shader;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Ryujinx.Graphics.Gpu.Memory
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Utility methods to convert shader stages and binding flags into buffer stages.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
@ -141,7 +141,7 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
|
||||
|
||||
if (algorithm == CompressionAlgorithm.Deflate)
|
||||
{
|
||||
_activeStream = new DeflateStream(_stream, CompressionLevel.SmallestSize, true);
|
||||
_activeStream = new DeflateStream(_stream, CompressionLevel.Fastest, true);
|
||||
}
|
||||
}
|
||||
|
||||
@ -206,7 +206,7 @@ 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;
|
||||
|
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of the cached GPU state accessor for shader translation.
|
||||
@ -29,6 +31,7 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
|
||||
/// <param name="newSpecState">Shader specialization state of the recompiled shader</param>
|
||||
/// <param name="counts">Resource counts shared across all shader stages</param>
|
||||
/// <param name="stageIndex">Shader stage index</param>
|
||||
/// <param name="hasGeometryShader">Indicates if a geometry shader is present</param>
|
||||
public DiskCacheGpuAccessor(
|
||||
GpuContext context,
|
||||
ReadOnlyMemory<byte> 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
|
||||
/// <inheritdoc/>
|
||||
public GpuGraphicsState QueryGraphicsState()
|
||||
{
|
||||
return _oldSpecState.GraphicsState.CreateShaderGraphicsState(!_isVulkan, _isVulkan || _oldSpecState.GraphicsState.YNegateEnabled);
|
||||
return _oldSpecState.GraphicsState.CreateShaderGraphicsState(
|
||||
!_isVulkan,
|
||||
_supportsQuads,
|
||||
_hasGeometryShader,
|
||||
_isVulkan || _oldSpecState.GraphicsState.YNegateEnabled);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
@ -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 = 6577;
|
||||
private const uint CodeGenVersion = 5936;
|
||||
|
||||
private const string SharedTocFileName = "shared.toc";
|
||||
private const string SharedDataFileName = "shared.data";
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of the GPU state accessor for graphics shader translation.
|
||||
@ -25,12 +27,20 @@ namespace Ryujinx.Graphics.Gpu.Shader
|
||||
/// <param name="channel">GPU channel</param>
|
||||
/// <param name="state">Current GPU state</param>
|
||||
/// <param name="stageIndex">Graphics shader stage index (0 = Vertex, 4 = Fragment)</param>
|
||||
public GpuAccessor(GpuContext context, GpuChannel channel, GpuAccessorState state, int stageIndex) : base(context, state.ResourceCounts, stageIndex)
|
||||
/// <param name="hasGeometryShader">Indicates if a geometry shader is present</param>
|
||||
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
|
||||
/// <inheritdoc/>
|
||||
public GpuGraphicsState QueryGraphicsState()
|
||||
{
|
||||
return _state.GraphicsState.CreateShaderGraphicsState(!_isVulkan, _isVulkan || _state.GraphicsState.YNegateEnabled);
|
||||
return _state.GraphicsState.CreateShaderGraphicsState(
|
||||
!_isVulkan,
|
||||
_supportsQuads,
|
||||
_hasGeometryShader,
|
||||
_isVulkan || _state.GraphicsState.YNegateEnabled);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
@ -106,8 +106,11 @@ namespace Ryujinx.Graphics.Gpu.Shader
|
||||
/// Creates a new graphics state from this state that can be used for shader generation.
|
||||
/// </summary>
|
||||
/// <param name="hostSupportsAlphaTest">Indicates if the host API supports alpha test operations</param>
|
||||
/// <param name="hostSupportsQuads">Indicates if the host API supports quad primitives</param>
|
||||
/// <param name="hasGeometryShader">Indicates if a geometry shader is used</param>
|
||||
/// <param name="originUpperLeft">If true, indicates that the fragment origin is the upper left corner of the viewport, otherwise it is the lower left corner</param>
|
||||
/// <returns>GPU graphics state that can be used for shader translation</returns>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -339,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)
|
||||
|
@ -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,7 @@ 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),
|
||||
|
@ -102,6 +102,11 @@ namespace Ryujinx.Graphics.Shader
|
||||
/// </summary>
|
||||
public readonly bool OriginUpperLeft;
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that the primitive ID values on the shader should be halved due to quad to triangles conversion.
|
||||
/// </summary>
|
||||
public readonly bool HalvePrimitiveId;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new GPU graphics state.
|
||||
/// </summary>
|
||||
@ -124,6 +129,7 @@ namespace Ryujinx.Graphics.Shader
|
||||
/// <param name="dualSourceBlendEnable">Indicates whether dual source blend is enabled</param>
|
||||
/// <param name="yNegateEnabled">Indicates if negation of the viewport Y axis is enabled</param>
|
||||
/// <param name="originUpperLeft">If true, indicates that the fragment origin is the upper left corner of the viewport, otherwise it is the lower left corner</param>
|
||||
/// <param name="halvePrimitiveId">Indicates that the primitive ID values on the shader should be halved due to quad to triangles conversion</param>
|
||||
public GpuGraphicsState(
|
||||
bool earlyZForce,
|
||||
InputTopology topology,
|
||||
@ -143,7 +149,8 @@ namespace Ryujinx.Graphics.Shader
|
||||
in Array8<AttributeType> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -135,6 +135,7 @@ namespace Ryujinx.Graphics.Shader
|
||||
default,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false);
|
||||
}
|
||||
|
||||
|
@ -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.
|
||||
|
@ -66,9 +66,9 @@ namespace Ryujinx.Graphics.Shader.Translation.Optimizations
|
||||
|
||||
if (nvHandle.AsgOp is not Operation handleOp ||
|
||||
handleOp.Inst != Instruction.Load ||
|
||||
handleOp.StorageKind != StorageKind.Input)
|
||||
(handleOp.StorageKind != StorageKind.Input && handleOp.StorageKind != StorageKind.StorageBuffer))
|
||||
{
|
||||
// Right now, we only allow bindless access when the handle comes from a shader input.
|
||||
// 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.
|
||||
|
@ -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; }
|
||||
|
@ -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<DisposableBuffer> _buffer;
|
||||
private Auto<MemoryAllocation> _allocationAuto;
|
||||
private readonly MemoryAllocation _allocation;
|
||||
private readonly Auto<DisposableBuffer> _buffer;
|
||||
private readonly Auto<MemoryAllocation> _allocationAuto;
|
||||
private readonly bool _allocationImported;
|
||||
private ulong _bufferHandle;
|
||||
private readonly ulong _bufferHandle;
|
||||
|
||||
private CacheByRange<BufferHolder> _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<Action> _swapActions;
|
||||
|
||||
private byte[] _pendingData;
|
||||
private BufferMirrorRangeList _pendingDataRanges;
|
||||
private Dictionary<ulong, StagingBufferReserved> _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<MemoryAllocation>(allocation);
|
||||
_buffer = new Auto<DisposableBuffer>(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<byte>((void*)currentMap, Size).CopyTo(new Span<byte>((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<DisposableBufferView> 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<Action>()).Add(invalidateView);
|
||||
|
||||
return new Auto<DisposableBufferView>(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<byte> 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();
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -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,7 @@ namespace Ryujinx.Graphics.Vulkan
|
||||
supportsMismatchingViewFormat: true,
|
||||
supportsCubemapView: !IsAmdGcn,
|
||||
supportsNonConstantTextureOffset: false,
|
||||
supportsScaledVertexFormats: FormatCapabilities.SupportsScaledVertexFormats(),
|
||||
supportsQuads: false,
|
||||
supportsSeparateSampler: true,
|
||||
supportsShaderBallot: false,
|
||||
supportsShaderBarrierDivergence: Vendor != Vendor.Intel,
|
||||
|
@ -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))
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -2,7 +2,6 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading
|
||||
{
|
||||
enum ThreadType
|
||||
{
|
||||
Dummy,
|
||||
Kernel,
|
||||
Kernel2,
|
||||
User,
|
||||
|
@ -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)
|
||||
|
@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
@ -16,10 +16,4 @@
|
||||
<PackageReference Include="Concentus" />
|
||||
<PackageReference Include="LibHac" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Due to Concentus. -->
|
||||
<PropertyGroup>
|
||||
<NoWarn>NU1605</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
|
@ -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<byte> inData, Span<short> 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<byte> inData, Span<short> 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<byte> inData, Span<short> 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<short> outPcmSpace = MemoryMarshal.Cast<byte, short>(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<short, byte>(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<byte> 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<byte> input,
|
||||
out short[] outPcmData,
|
||||
Span<short> 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<byte> 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();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -174,6 +174,11 @@ namespace Ryujinx.Input.HLE
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
foreach (InputConfig inputConfig in _inputConfig)
|
||||
{
|
||||
_controllers[(int)inputConfig.PlayerIndex]?.GamepadDriver?.Clear();
|
||||
}
|
||||
|
||||
_blockInputUpdates = false;
|
||||
}
|
||||
}
|
||||
|
@ -33,5 +33,11 @@ namespace Ryujinx.Input
|
||||
/// <param name="id">The unique id of the gamepad</param>
|
||||
/// <returns>An instance of <see cref="IGamepad"/> associated to the gamepad id given or null if not found</returns>
|
||||
IGamepad GetGamepad(string id);
|
||||
|
||||
/// <summary>
|
||||
/// Clear the internal state of the driver.
|
||||
/// </summary>
|
||||
/// <remarks>Does nothing by default.</remarks>
|
||||
void Clear() { }
|
||||
}
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9,7 +9,8 @@ namespace Ryujinx.Tests.Audio.Renderer.Server
|
||||
[Test]
|
||||
public void EnsureTypeSize()
|
||||
{
|
||||
Assert.AreEqual(0xE0, Unsafe.SizeOf<SplitterDestination>());
|
||||
Assert.AreEqual(0xE0, Unsafe.SizeOf<SplitterDestinationVersion1>());
|
||||
Assert.AreEqual(0x110, Unsafe.SizeOf<SplitterDestinationVersion2>());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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
|
||||
@ -1594,7 +1595,9 @@ namespace Ryujinx.UI.Common.Configuration
|
||||
|
||||
private static void LogValueChange<T>(ReactiveEventArgs<T> 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()
|
||||
|
@ -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();
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a PlatformThemeVariant value to the corresponding ThemeVariant value.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
|
@ -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:",
|
||||
|
@ -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:",
|
||||
|
@ -404,6 +404,7 @@
|
||||
"GameListContextMenuToggleFavorite": "Toggle Favorite",
|
||||
"GameListContextMenuToggleFavoriteToolTip": "Toggle Favorite status of Game",
|
||||
"SettingsTabGeneralTheme": "Theme:",
|
||||
"SettingsTabGeneralThemeAuto": "Auto",
|
||||
"SettingsTabGeneralThemeDark": "Dark",
|
||||
"SettingsTabGeneralThemeLight": "Light",
|
||||
"ControllerSettingsConfigureGeneral": "Configure",
|
||||
|
@ -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"
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
|
@ -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": "מזהה בניה:",
|
||||
|
@ -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",
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user