+## Documentation
+
+If you are planning to contribute or just want to learn more about this project please read through our [documentation](docs/README.md).
diff --git a/Ryujinx.sln b/Ryujinx.sln
index bb196cabc..76ebd573f 100644
--- a/Ryujinx.sln
+++ b/Ryujinx.sln
@@ -3,7 +3,7 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.1.32228.430
MinimumVisualStudioVersion = 10.0.40219.1
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx", "src\Ryujinx\Ryujinx.csproj", "{074045D4-3ED2-4711-9169-E385F2BFB5A0}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Gtk3", "src\Ryujinx.Gtk3\Ryujinx.Gtk3.csproj", "{074045D4-3ED2-4711-9169-E385F2BFB5A0}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Tests", "src\Ryujinx.Tests\Ryujinx.Tests.csproj", "{EBB55AEA-C7D7-4DEB-BF96-FA1789E225E9}"
EndProject
@@ -69,9 +69,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Headless.SDL2", "sr
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Graphics.Nvdec.FFmpeg", "src\Ryujinx.Graphics.Nvdec.FFmpeg\Ryujinx.Graphics.Nvdec.FFmpeg.csproj", "{BEE1C184-C9A4-410B-8DFC-FB74D5C93AEB}"
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Ava", "src\Ryujinx.Ava\Ryujinx.Ava.csproj", "{7C1B2721-13DA-4B62-B046-C626605ECCE6}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx", "src\Ryujinx\Ryujinx.csproj", "{7C1B2721-13DA-4B62-B046-C626605ECCE6}"
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Ui.Common", "src\Ryujinx.Ui.Common\Ryujinx.Ui.Common.csproj", "{BA161CA0-CD65-4E6E-B644-51C8D1E542DC}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.UI.Common", "src\Ryujinx.UI.Common\Ryujinx.UI.Common.csproj", "{BA161CA0-CD65-4E6E-B644-51C8D1E542DC}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Horizon.Generators", "src\Ryujinx.Horizon.Generators\Ryujinx.Horizon.Generators.csproj", "{6AE2A5E8-4C5A-48B9-997B-E1455C0355C6}"
EndProject
@@ -79,7 +79,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Graphics.Vulkan", "
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Spv.Generator", "src\Spv.Generator\Spv.Generator.csproj", "{2BCB3D7A-38C0-4FE7-8FDA-374C6AD56D0E}"
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Ui.LocaleGenerator", "src\Ryujinx.Ui.LocaleGenerator\Ryujinx.Ui.LocaleGenerator.csproj", "{77D01AD9-2C98-478E-AE1D-8F7100738FB4}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.UI.LocaleGenerator", "src\Ryujinx.UI.LocaleGenerator\Ryujinx.UI.LocaleGenerator.csproj", "{77D01AD9-2C98-478E-AE1D-8F7100738FB4}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Horizon.Common", "src\Ryujinx.Horizon.Common\Ryujinx.Horizon.Common.csproj", "{77F96ECE-4952-42DB-A528-DED25572A573}"
EndProject
@@ -87,6 +87,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Horizon", "src\Ryuj
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Horizon.Kernel.Generators", "src\Ryujinx.Horizon.Kernel.Generators\Ryujinx.Horizon.Kernel.Generators.csproj", "{7F55A45D-4E1D-4A36-ADD3-87F29A285AA2}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.HLE.Generators", "src\Ryujinx.HLE.Generators\Ryujinx.HLE.Generators.csproj", "{B575BCDE-2FD8-4A5D-8756-31CDD7FE81F0}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -249,6 +251,10 @@ Global
{7F55A45D-4E1D-4A36-ADD3-87F29A285AA2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7F55A45D-4E1D-4A36-ADD3-87F29A285AA2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7F55A45D-4E1D-4A36-ADD3-87F29A285AA2}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B575BCDE-2FD8-4A5D-8756-31CDD7FE81F0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B575BCDE-2FD8-4A5D-8756-31CDD7FE81F0}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B575BCDE-2FD8-4A5D-8756-31CDD7FE81F0}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {B575BCDE-2FD8-4A5D-8756-31CDD7FE81F0}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/Ryujinx.sln.DotSettings b/Ryujinx.sln.DotSettings
index 049bdaf69..ed7f3e911 100644
--- a/Ryujinx.sln.DotSettings
+++ b/Ryujinx.sln.DotSettings
@@ -4,6 +4,8 @@
UseExplicitTypeUseExplicitType<Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="I" Suffix="" Style="AaBb" /></Policy>
+ <Policy><Descriptor Staticness="Any" AccessRightKinds="Any" Description="Types and namespaces"><ElementKinds><Kind Name="NAMESPACE" /><Kind Name="CLASS" /><Kind Name="STRUCT" /><Kind Name="ENUM" /><Kind Name="DELEGATE" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="I" Suffix="" Style="AaBb" /></Policy></Policy>
+ TrueTrueTrueTrue
diff --git a/distribution/linux/Ryujinx.desktop b/distribution/linux/Ryujinx.desktop
index a4550d104..44f05bf3f 100644
--- a/distribution/linux/Ryujinx.desktop
+++ b/distribution/linux/Ryujinx.desktop
@@ -4,7 +4,7 @@ Name=Ryujinx
Type=Application
Icon=Ryujinx
Exec=Ryujinx.sh %f
-Comment=Plays Nintendo Switch applications
+Comment=A Nintendo Switch Emulator
GenericName=Nintendo Switch Emulator
Terminal=false
Categories=Game;Emulator;
diff --git a/distribution/linux/Ryujinx.sh b/distribution/linux/Ryujinx.sh
old mode 100644
new mode 100755
index f356cad01..30eb14399
--- a/distribution/linux/Ryujinx.sh
+++ b/distribution/linux/Ryujinx.sh
@@ -1,20 +1,23 @@
#!/bin/sh
SCRIPT_DIR=$(dirname "$(realpath "$0")")
-RYUJINX_BIN="Ryujinx"
-
-if [ -f "$SCRIPT_DIR/Ryujinx.Ava" ]; then
- RYUJINX_BIN="Ryujinx.Ava"
-fi
if [ -f "$SCRIPT_DIR/Ryujinx.Headless.SDL2" ]; then
RYUJINX_BIN="Ryujinx.Headless.SDL2"
fi
+if [ -f "$SCRIPT_DIR/Ryujinx" ]; then
+ RYUJINX_BIN="Ryujinx"
+fi
+
+if [ -z "$RYUJINX_BIN" ]; then
+ exit 1
+fi
+
COMMAND="env DOTNET_EnableAlternateStackCheck=1"
if command -v gamemoderun > /dev/null 2>&1; then
COMMAND="$COMMAND gamemoderun"
fi
-$COMMAND "$SCRIPT_DIR/$RYUJINX_BIN" "$@"
\ No newline at end of file
+exec $COMMAND "$SCRIPT_DIR/$RYUJINX_BIN" "$@"
diff --git a/distribution/macos/create_app_bundle.sh b/distribution/macos/create_app_bundle.sh
index 858c06f59..0fa54eadd 100755
--- a/distribution/macos/create_app_bundle.sh
+++ b/distribution/macos/create_app_bundle.sh
@@ -14,8 +14,8 @@ mkdir "$APP_BUNDLE_DIRECTORY/Contents/Frameworks"
mkdir "$APP_BUNDLE_DIRECTORY/Contents/MacOS"
mkdir "$APP_BUNDLE_DIRECTORY/Contents/Resources"
-# Copy executables first
-cp "$PUBLISH_DIRECTORY/Ryujinx.Ava" "$APP_BUNDLE_DIRECTORY/Contents/MacOS/Ryujinx"
+# Copy executable and nsure executable can be executed
+cp "$PUBLISH_DIRECTORY/Ryujinx" "$APP_BUNDLE_DIRECTORY/Contents/MacOS/Ryujinx"
chmod u+x "$APP_BUNDLE_DIRECTORY/Contents/MacOS/Ryujinx"
# Then all libraries
diff --git a/distribution/macos/create_macos_build_ava.sh b/distribution/macos/create_macos_build_ava.sh
index 80594a40a..23eafc129 100755
--- a/distribution/macos/create_macos_build_ava.sh
+++ b/distribution/macos/create_macos_build_ava.sh
@@ -22,9 +22,9 @@ EXTRA_ARGS=$8
if [ "$VERSION" == "1.1.0" ];
then
- RELEASE_TAR_FILE_NAME=test-ava-ryujinx-$CONFIGURATION-$VERSION+$SOURCE_REVISION_ID-macos_universal.app.tar
+ RELEASE_TAR_FILE_NAME=ryujinx-$CONFIGURATION-$VERSION+$SOURCE_REVISION_ID-macos_universal.app.tar
else
- RELEASE_TAR_FILE_NAME=test-ava-ryujinx-$VERSION-macos_universal.app.tar
+ RELEASE_TAR_FILE_NAME=ryujinx-$VERSION-macos_universal.app.tar
fi
ARM64_APP_BUNDLE="$TEMP_DIRECTORY/output_arm64/Ryujinx.app"
@@ -38,9 +38,9 @@ mkdir -p "$TEMP_DIRECTORY"
DOTNET_COMMON_ARGS=(-p:DebugType=embedded -p:Version="$VERSION" -p:SourceRevisionId="$SOURCE_REVISION_ID" --self-contained true $EXTRA_ARGS)
dotnet restore
-dotnet build -c "$CONFIGURATION" src/Ryujinx.Ava
-dotnet publish -c "$CONFIGURATION" -r osx-arm64 -o "$TEMP_DIRECTORY/publish_arm64" "${DOTNET_COMMON_ARGS[@]}" src/Ryujinx.Ava
-dotnet publish -c "$CONFIGURATION" -r osx-x64 -o "$TEMP_DIRECTORY/publish_x64" "${DOTNET_COMMON_ARGS[@]}" src/Ryujinx.Ava
+dotnet build -c "$CONFIGURATION" src/Ryujinx
+dotnet publish -c "$CONFIGURATION" -r osx-arm64 -o "$TEMP_DIRECTORY/publish_arm64" "${DOTNET_COMMON_ARGS[@]}" src/Ryujinx
+dotnet publish -c "$CONFIGURATION" -r osx-x64 -o "$TEMP_DIRECTORY/publish_x64" "${DOTNET_COMMON_ARGS[@]}" src/Ryujinx
# Get rid of the support library for ARMeilleure for x64 (that's only for arm64)
rm -rf "$TEMP_DIRECTORY/publish_x64/libarmeilleure-jitsupport.dylib"
@@ -108,6 +108,13 @@ tar --exclude "Ryujinx.app/Contents/MacOS/Ryujinx" -cvf "$RELEASE_TAR_FILE_NAME"
python3 "$BASE_DIR/distribution/misc/add_tar_exec.py" "$RELEASE_TAR_FILE_NAME" "Ryujinx.app/Contents/MacOS/Ryujinx" "Ryujinx.app/Contents/MacOS/Ryujinx"
gzip -9 < "$RELEASE_TAR_FILE_NAME" > "$RELEASE_TAR_FILE_NAME.gz"
rm "$RELEASE_TAR_FILE_NAME"
+
+# Create legacy update package for Avalonia to not left behind old testers.
+if [ "$VERSION" != "1.1.0" ];
+then
+ cp $RELEASE_TAR_FILE_NAME.gz test-ava-ryujinx-$VERSION-macos_universal.app.tar.gz
+fi
+
popd
echo "Done"
\ No newline at end of file
diff --git a/distribution/macos/shortcut-launch-script.sh b/distribution/macos/shortcut-launch-script.sh
new file mode 100644
index 000000000..784d780aa
--- /dev/null
+++ b/distribution/macos/shortcut-launch-script.sh
@@ -0,0 +1,8 @@
+#!/bin/sh
+launch_arch="$(uname -m)"
+if [ "$(sysctl -in sysctl.proc_translated)" = "1" ]
+then
+ launch_arch="arm64"
+fi
+
+arch -$launch_arch {0} {1}
diff --git a/docs/README.md b/docs/README.md
index 2213086f6..a22da3c7c 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -33,8 +33,3 @@ Project Docs
=================
To be added. Many project files will contain basic XML docs for key functions and classes in the meantime.
-
-Other Information
-=================
-
-- N/A
diff --git a/src/ARMeilleure/CodeGen/Arm64/CodeGenContext.cs b/src/ARMeilleure/CodeGen/Arm64/CodeGenContext.cs
index 12ebabddd..89b1e9e6b 100644
--- a/src/ARMeilleure/CodeGen/Arm64/CodeGenContext.cs
+++ b/src/ARMeilleure/CodeGen/Arm64/CodeGenContext.cs
@@ -237,7 +237,7 @@ namespace ARMeilleure.CodeGen.Arm64
long originalPosition = _stream.Position;
_stream.Seek(0, SeekOrigin.Begin);
- _stream.Read(code, 0, code.Length);
+ _stream.ReadExactly(code, 0, code.Length);
_stream.Seek(originalPosition, SeekOrigin.Begin);
RelocInfo relocInfo;
diff --git a/src/ARMeilleure/CodeGen/RegisterAllocators/LinearScanAllocator.cs b/src/ARMeilleure/CodeGen/RegisterAllocators/LinearScanAllocator.cs
index f156e0886..16feeb914 100644
--- a/src/ARMeilleure/CodeGen/RegisterAllocators/LinearScanAllocator.cs
+++ b/src/ARMeilleure/CodeGen/RegisterAllocators/LinearScanAllocator.cs
@@ -251,7 +251,20 @@ namespace ARMeilleure.CodeGen.RegisterAllocators
}
}
- int selectedReg = GetHighestValueIndex(freePositions);
+ // If this is a copy destination variable, we prefer the register used for the copy source.
+ // If the register is available, then the copy can be eliminated later as both source
+ // and destination will use the same register.
+ int selectedReg;
+
+ if (current.TryGetCopySourceRegister(out int preferredReg) && freePositions[preferredReg] >= current.GetEnd())
+ {
+ selectedReg = preferredReg;
+ }
+ else
+ {
+ selectedReg = GetHighestValueIndex(freePositions);
+ }
+
int selectedNextUse = freePositions[selectedReg];
// Intervals starts and ends at odd positions, unless they span an entire
@@ -431,7 +444,7 @@ namespace ARMeilleure.CodeGen.RegisterAllocators
}
}
- private static int GetHighestValueIndex(Span span)
+ private static int GetHighestValueIndex(ReadOnlySpan span)
{
int highest = int.MinValue;
@@ -798,12 +811,12 @@ namespace ARMeilleure.CodeGen.RegisterAllocators
// The "visited" state is stored in the MSB of the local's value.
const ulong VisitedMask = 1ul << 63;
- bool IsVisited(Operand local)
+ static bool IsVisited(Operand local)
{
return (local.GetValueUnsafe() & VisitedMask) != 0;
}
- void SetVisited(Operand local)
+ static void SetVisited(Operand local)
{
local.GetValueUnsafe() |= VisitedMask;
}
@@ -826,9 +839,25 @@ namespace ARMeilleure.CodeGen.RegisterAllocators
{
dest.NumberLocal(_intervals.Count);
- _intervals.Add(new LiveInterval(dest));
+ LiveInterval interval = new LiveInterval(dest);
+ _intervals.Add(interval);
SetVisited(dest);
+
+ // If this is a copy (or copy-like operation), set the copy source interval as well.
+ // This is used for register preferencing later on, which allows the copy to be eliminated
+ // in some cases.
+ if (node.Instruction == Instruction.Copy || node.Instruction == Instruction.ZeroExtend32)
+ {
+ Operand source = node.GetSource(0);
+
+ if (source.Kind == OperandKind.LocalVariable &&
+ source.GetLocalNumber() > 0 &&
+ (node.Instruction == Instruction.Copy || source.Type == OperandType.I32))
+ {
+ interval.SetCopySource(_intervals[source.GetLocalNumber()]);
+ }
+ }
}
}
}
diff --git a/src/ARMeilleure/CodeGen/RegisterAllocators/LiveInterval.cs b/src/ARMeilleure/CodeGen/RegisterAllocators/LiveInterval.cs
index 333d3951b..cfe1bc7ca 100644
--- a/src/ARMeilleure/CodeGen/RegisterAllocators/LiveInterval.cs
+++ b/src/ARMeilleure/CodeGen/RegisterAllocators/LiveInterval.cs
@@ -19,6 +19,7 @@ namespace ARMeilleure.CodeGen.RegisterAllocators
public LiveRange CurrRange;
public LiveInterval Parent;
+ public LiveInterval CopySource;
public UseList Uses;
public LiveIntervalList Children;
@@ -37,6 +38,7 @@ namespace ARMeilleure.CodeGen.RegisterAllocators
private ref LiveRange CurrRange => ref _data->CurrRange;
private ref LiveRange PrevRange => ref _data->PrevRange;
private ref LiveInterval Parent => ref _data->Parent;
+ private ref LiveInterval CopySource => ref _data->CopySource;
private ref UseList Uses => ref _data->Uses;
private ref LiveIntervalList Children => ref _data->Children;
@@ -78,6 +80,25 @@ namespace ARMeilleure.CodeGen.RegisterAllocators
Register = register;
}
+ public void SetCopySource(LiveInterval copySource)
+ {
+ CopySource = copySource;
+ }
+
+ public bool TryGetCopySourceRegister(out int copySourceRegIndex)
+ {
+ if (CopySource._data != null)
+ {
+ copySourceRegIndex = CopySource.Register.Index;
+
+ return true;
+ }
+
+ copySourceRegIndex = 0;
+
+ return false;
+ }
+
public void Reset()
{
PrevRange = default;
diff --git a/src/ARMeilleure/CodeGen/X86/Assembler.cs b/src/ARMeilleure/CodeGen/X86/Assembler.cs
index 55bf07248..96f4de049 100644
--- a/src/ARMeilleure/CodeGen/X86/Assembler.cs
+++ b/src/ARMeilleure/CodeGen/X86/Assembler.cs
@@ -1444,7 +1444,7 @@ namespace ARMeilleure.CodeGen.X86
Span buffer = new byte[jump.JumpPosition - _stream.Position];
- _stream.Read(buffer);
+ _stream.ReadExactly(buffer);
_stream.Seek(ReservedBytesForJump, SeekOrigin.Current);
codeStream.Write(buffer);
diff --git a/src/ARMeilleure/Decoders/OpCodeTable.cs b/src/ARMeilleure/Decoders/OpCodeTable.cs
index 9e13bd9b5..20d567fe5 100644
--- a/src/ARMeilleure/Decoders/OpCodeTable.cs
+++ b/src/ARMeilleure/Decoders/OpCodeTable.cs
@@ -517,7 +517,10 @@ namespace ARMeilleure.Decoders
SetA64("0x00111100>>>xxx100111xxxxxxxxxx", InstName.Sqrshrn_V, InstEmit.Sqrshrn_V, OpCodeSimdShImm.Create);
SetA64("0111111100>>>xxx100011xxxxxxxxxx", InstName.Sqrshrun_S, InstEmit.Sqrshrun_S, OpCodeSimdShImm.Create);
SetA64("0x10111100>>>xxx100011xxxxxxxxxx", InstName.Sqrshrun_V, InstEmit.Sqrshrun_V, OpCodeSimdShImm.Create);
+ SetA64("010111110>>>>xxx011101xxxxxxxxxx", InstName.Sqshl_Si, InstEmit.Sqshl_Si, OpCodeSimdShImm.Create);
SetA64("0>001110<<1xxxxx010011xxxxxxxxxx", InstName.Sqshl_V, InstEmit.Sqshl_V, OpCodeSimdReg.Create);
+ SetA64("0000111100>>>xxx011101xxxxxxxxxx", InstName.Sqshl_Vi, InstEmit.Sqshl_Vi, OpCodeSimdShImm.Create);
+ SetA64("010011110>>>>xxx011101xxxxxxxxxx", InstName.Sqshl_Vi, InstEmit.Sqshl_Vi, OpCodeSimdShImm.Create);
SetA64("0101111100>>>xxx100101xxxxxxxxxx", InstName.Sqshrn_S, InstEmit.Sqshrn_S, OpCodeSimdShImm.Create);
SetA64("0x00111100>>>xxx100101xxxxxxxxxx", InstName.Sqshrn_V, InstEmit.Sqshrn_V, OpCodeSimdShImm.Create);
SetA64("0111111100>>>xxx100001xxxxxxxxxx", InstName.Sqshrun_S, InstEmit.Sqshrun_S, OpCodeSimdShImm.Create);
@@ -743,6 +746,7 @@ namespace ARMeilleure.Decoders
SetA32("<<<<01101000xxxxxxxxxxxxxx01xxxx", InstName.Pkh, InstEmit32.Pkh, OpCode32AluRsImm.Create);
SetA32("11110101xx01xxxx1111xxxxxxxxxxxx", InstName.Pld, InstEmit32.Nop, OpCode32.Create);
SetA32("11110111xx01xxxx1111xxxxxxx0xxxx", InstName.Pld, InstEmit32.Nop, OpCode32.Create);
+ SetA32("<<<<01100010xxxxxxxx11110001xxxx", InstName.Qadd16, InstEmit32.Qadd16, OpCode32AluReg.Create);
SetA32("<<<<011011111111xxxx11110011xxxx", InstName.Rbit, InstEmit32.Rbit, OpCode32AluReg.Create);
SetA32("<<<<011010111111xxxx11110011xxxx", InstName.Rev, InstEmit32.Rev, OpCode32AluReg.Create);
SetA32("<<<<011010111111xxxx11111011xxxx", InstName.Rev16, InstEmit32.Rev16, OpCode32AluReg.Create);
@@ -819,6 +823,10 @@ namespace ARMeilleure.Decoders
SetA32("<<<<00000100xxxxxxxxxxxx1001xxxx", InstName.Umaal, InstEmit32.Umaal, OpCode32AluUmull.Create);
SetA32("<<<<0000101xxxxxxxxxxxxx1001xxxx", InstName.Umlal, InstEmit32.Umlal, OpCode32AluUmull.Create);
SetA32("<<<<0000100xxxxxxxxxxxxx1001xxxx", InstName.Umull, InstEmit32.Umull, OpCode32AluUmull.Create);
+ SetA32("<<<<01100110xxxxxxxx11110001xxxx", InstName.Uqadd16, InstEmit32.Uqadd16, OpCode32AluReg.Create);
+ SetA32("<<<<01100110xxxxxxxx11111001xxxx", InstName.Uqadd8, InstEmit32.Uqadd8, OpCode32AluReg.Create);
+ SetA32("<<<<01100110xxxxxxxx11110111xxxx", InstName.Uqsub16, InstEmit32.Uqsub16, OpCode32AluReg.Create);
+ SetA32("<<<<01100110xxxxxxxx11111111xxxx", InstName.Uqsub8, InstEmit32.Uqsub8, OpCode32AluReg.Create);
SetA32("<<<<0110111xxxxxxxxxxxxxxx01xxxx", InstName.Usat, InstEmit32.Usat, OpCode32Sat.Create);
SetA32("<<<<01101110xxxxxxxx11110011xxxx", InstName.Usat16, InstEmit32.Usat16, OpCode32Sat16.Create);
SetA32("<<<<01100101xxxxxxxx11111111xxxx", InstName.Usub8, InstEmit32.Usub8, OpCode32AluReg.Create);
@@ -872,6 +880,7 @@ namespace ARMeilleure.Decoders
SetVfp("<<<<11100x10xxxxxxxx101xx1x0xxxx", InstName.Vnmul, InstEmit32.Vnmul_S, OpCode32SimdRegS.Create, OpCode32SimdRegS.CreateT32);
SetVfp("111111101x1110xxxxxx101x01x0xxxx", InstName.Vrint, InstEmit32.Vrint_RM, OpCode32SimdS.Create, OpCode32SimdS.CreateT32);
SetVfp("<<<<11101x110110xxxx101x11x0xxxx", InstName.Vrint, InstEmit32.Vrint_Z, OpCode32SimdS.Create, OpCode32SimdS.CreateT32);
+ SetVfp("<<<<11101x110110xxxx101x01x0xxxx", InstName.Vrintr, InstEmit32.Vrintr_S, OpCode32SimdS.Create, OpCode32SimdS.CreateT32);
SetVfp("<<<<11101x110111xxxx101x01x0xxxx", InstName.Vrintx, InstEmit32.Vrintx_S, OpCode32SimdS.Create, OpCode32SimdS.CreateT32);
SetVfp("<<<<11101x110001xxxx101x11x0xxxx", InstName.Vsqrt, InstEmit32.Vsqrt_S, OpCode32SimdS.Create, OpCode32SimdS.CreateT32);
SetVfp("111111100xxxxxxxxxxx101xx0x0xxxx", InstName.Vsel, InstEmit32.Vsel, OpCode32SimdSel.Create, OpCode32SimdSel.CreateT32);
@@ -992,6 +1001,7 @@ namespace ARMeilleure.Decoders
SetAsimd("1111001x1x000xxxxxxx<>>xxxxxxx100101x1xxx0", InstName.Vqrshrn, InstEmit32.Vqrshrn, OpCode32SimdShImmNarrow.Create, OpCode32SimdShImmNarrow.CreateT32);
SetAsimd("111100111x>>>xxxxxxx100001x1xxx0", InstName.Vqrshrun, InstEmit32.Vqrshrun, OpCode32SimdShImmNarrow.Create, OpCode32SimdShImmNarrow.CreateT32);
SetAsimd("1111001x1x>>>xxxxxxx100100x1xxx0", InstName.Vqshrn, InstEmit32.Vqshrn, OpCode32SimdShImmNarrow.Create, OpCode32SimdShImmNarrow.CreateT32);
@@ -1023,8 +1035,10 @@ namespace ARMeilleure.Decoders
SetAsimd("111100101x>>>xxxxxxx0101>xx1xxxx", InstName.Vshl, InstEmit32.Vshl, OpCode32SimdShImm.Create, OpCode32SimdShImm.CreateT32);
SetAsimd("1111001x0xxxxxxxxxxx0100xxx0xxxx", InstName.Vshl, InstEmit32.Vshl_I, OpCode32SimdReg.Create, OpCode32SimdReg.CreateT32);
SetAsimd("1111001x1x>>>xxxxxxx101000x1xxxx", InstName.Vshll, InstEmit32.Vshll, OpCode32SimdShImmLong.Create, OpCode32SimdShImmLong.CreateT32); // A1 encoding.
+ SetAsimd("111100111x11<<10xxxx001100x0xxxx", InstName.Vshll, InstEmit32.Vshll2, OpCode32SimdMovn.Create, OpCode32SimdMovn.CreateT32); // A2 encoding.
SetAsimd("1111001x1x>>>xxxxxxx0000>xx1xxxx", InstName.Vshr, InstEmit32.Vshr, OpCode32SimdShImm.Create, OpCode32SimdShImm.CreateT32);
SetAsimd("111100101x>>>xxxxxxx100000x1xxx0", InstName.Vshrn, InstEmit32.Vshrn, OpCode32SimdShImmNarrow.Create, OpCode32SimdShImmNarrow.CreateT32);
+ SetAsimd("111100111x>>>xxxxxxx0101>xx1xxxx", InstName.Vsli, InstEmit32.Vsli_I, OpCode32SimdShImm.Create, OpCode32SimdShImm.CreateT32);
SetAsimd("1111001x1x>>>xxxxxxx0001>xx1xxxx", InstName.Vsra, InstEmit32.Vsra, OpCode32SimdShImm.Create, OpCode32SimdShImm.CreateT32);
SetAsimd("111101001x00xxxxxxxx0000xxx0xxxx", InstName.Vst1, InstEmit32.Vst1, OpCode32SimdMemSingle.Create, OpCode32SimdMemSingle.CreateT32);
SetAsimd("111101001x00xxxxxxxx0100xx0xxxxx", InstName.Vst1, InstEmit32.Vst1, OpCode32SimdMemSingle.Create, OpCode32SimdMemSingle.CreateT32);
@@ -1049,6 +1063,7 @@ namespace ARMeilleure.Decoders
SetAsimd("111100100x10xxxxxxxx1101xxx0xxxx", InstName.Vsub, InstEmit32.Vsub_V, OpCode32SimdReg.Create, OpCode32SimdReg.CreateT32);
SetAsimd("1111001x1x<
+ {
+ EmitSaturateRange(context, d, context.Add(n, m), 16, unsigned: false, setQ: false);
+ }));
+ }
+
public static void Rbit(ArmEmitterContext context)
{
Operand m = GetAluM(context);
@@ -467,6 +485,12 @@ namespace ARMeilleure.Instructions
Operand n = GetAluN(context);
Operand m = GetAluM(context, setCarry: false);
+ if (op.Rn == RegisterAlias.Aarch32Pc && op is OpCodeT32AluImm12)
+ {
+ // For ADR, PC is always 4 bytes aligned, even in Thumb mode.
+ n = context.BitwiseAnd(n, Const(~3u));
+ }
+
Operand res = context.Subtract(n, m);
if (ShouldSetFlags(context))
@@ -546,6 +570,46 @@ namespace ARMeilleure.Instructions
EmitHsub8(context, unsigned: true);
}
+ public static void Uqadd16(ArmEmitterContext context)
+ {
+ OpCode32AluReg op = (OpCode32AluReg)context.CurrOp;
+
+ SetIntA32(context, op.Rd, EmitUnsigned16BitPair(context, GetIntA32(context, op.Rn), GetIntA32(context, op.Rm), (d, n, m) =>
+ {
+ EmitSaturateUqadd(context, d, context.Add(n, m), 16);
+ }));
+ }
+
+ public static void Uqadd8(ArmEmitterContext context)
+ {
+ OpCode32AluReg op = (OpCode32AluReg)context.CurrOp;
+
+ SetIntA32(context, op.Rd, EmitUnsigned8BitPair(context, GetIntA32(context, op.Rn), GetIntA32(context, op.Rm), (d, n, m) =>
+ {
+ EmitSaturateUqadd(context, d, context.Add(n, m), 8);
+ }));
+ }
+
+ public static void Uqsub16(ArmEmitterContext context)
+ {
+ OpCode32AluReg op = (OpCode32AluReg)context.CurrOp;
+
+ SetIntA32(context, op.Rd, EmitUnsigned16BitPair(context, GetIntA32(context, op.Rn), GetIntA32(context, op.Rm), (d, n, m) =>
+ {
+ EmitSaturateUqsub(context, d, context.Subtract(n, m), 16);
+ }));
+ }
+
+ public static void Uqsub8(ArmEmitterContext context)
+ {
+ OpCode32AluReg op = (OpCode32AluReg)context.CurrOp;
+
+ SetIntA32(context, op.Rd, EmitUnsigned8BitPair(context, GetIntA32(context, op.Rn), GetIntA32(context, op.Rm), (d, n, m) =>
+ {
+ EmitSaturateUqsub(context, d, context.Subtract(n, m), 8);
+ }));
+ }
+
public static void Usat(ArmEmitterContext context)
{
OpCode32Sat op = (OpCode32Sat)context.CurrOp;
@@ -922,6 +986,251 @@ namespace ARMeilleure.Instructions
}
}
+ private static void EmitSaturateRange(ArmEmitterContext context, Operand result, Operand value, uint saturateTo, bool unsigned, bool setQ = true)
+ {
+ Debug.Assert(saturateTo <= 32);
+ Debug.Assert(!unsigned || saturateTo < 32);
+
+ if (!unsigned && saturateTo == 32)
+ {
+ // No saturation possible for this case.
+
+ context.Copy(result, value);
+
+ return;
+ }
+ else if (saturateTo == 0)
+ {
+ // Result is always zero if we saturate 0 bits.
+
+ context.Copy(result, Const(0));
+
+ return;
+ }
+
+ Operand satValue;
+
+ if (unsigned)
+ {
+ // Negative values always saturate (to zero).
+ // So we must always ignore the sign bit when masking, so that the truncated value will differ from the original one.
+
+ satValue = context.BitwiseAnd(value, Const((int)(uint.MaxValue >> (32 - (int)saturateTo))));
+ }
+ else
+ {
+ satValue = context.ShiftLeft(value, Const(32 - (int)saturateTo));
+ satValue = context.ShiftRightSI(satValue, Const(32 - (int)saturateTo));
+ }
+
+ // If the result is 0, the values are equal and we don't need saturation.
+ Operand lblNoSat = Label();
+ context.BranchIfFalse(lblNoSat, context.Subtract(value, satValue));
+
+ // Saturate and set Q flag.
+ if (unsigned)
+ {
+ if (saturateTo == 31)
+ {
+ // Only saturation case possible when going from 32 bits signed to 32 or 31 bits unsigned
+ // is when the signed input is negative, as all positive values are representable on a 31 bits range.
+
+ satValue = Const(0);
+ }
+ else
+ {
+ satValue = context.ShiftRightSI(value, Const(31));
+ satValue = context.BitwiseNot(satValue);
+ satValue = context.ShiftRightUI(satValue, Const(32 - (int)saturateTo));
+ }
+ }
+ else
+ {
+ if (saturateTo == 1)
+ {
+ satValue = context.ShiftRightSI(value, Const(31));
+ }
+ else
+ {
+ satValue = Const(uint.MaxValue >> (33 - (int)saturateTo));
+ satValue = context.BitwiseExclusiveOr(satValue, context.ShiftRightSI(value, Const(31)));
+ }
+ }
+
+ if (setQ)
+ {
+ SetFlag(context, PState.QFlag, Const(1));
+ }
+
+ context.Copy(result, satValue);
+
+ Operand lblExit = Label();
+ context.Branch(lblExit);
+
+ context.MarkLabel(lblNoSat);
+
+ context.Copy(result, value);
+
+ context.MarkLabel(lblExit);
+ }
+
+ private static void EmitSaturateUqadd(ArmEmitterContext context, Operand result, Operand value, uint saturateTo)
+ {
+ Debug.Assert(saturateTo <= 32);
+
+ if (saturateTo == 32)
+ {
+ // No saturation possible for this case.
+
+ context.Copy(result, value);
+
+ return;
+ }
+ else if (saturateTo == 0)
+ {
+ // Result is always zero if we saturate 0 bits.
+
+ context.Copy(result, Const(0));
+
+ return;
+ }
+
+ // If the result is 0, the values are equal and we don't need saturation.
+ Operand lblNoSat = Label();
+ context.BranchIfFalse(lblNoSat, context.ShiftRightUI(value, Const((int)saturateTo)));
+
+ // Saturate.
+ context.Copy(result, Const(uint.MaxValue >> (32 - (int)saturateTo)));
+
+ Operand lblExit = Label();
+ context.Branch(lblExit);
+
+ context.MarkLabel(lblNoSat);
+
+ context.Copy(result, value);
+
+ context.MarkLabel(lblExit);
+ }
+
+ private static void EmitSaturateUqsub(ArmEmitterContext context, Operand result, Operand value, uint saturateTo)
+ {
+ Debug.Assert(saturateTo <= 32);
+
+ if (saturateTo == 32)
+ {
+ // No saturation possible for this case.
+
+ context.Copy(result, value);
+
+ return;
+ }
+ else if (saturateTo == 0)
+ {
+ // Result is always zero if we saturate 0 bits.
+
+ context.Copy(result, Const(0));
+
+ return;
+ }
+
+ // If the result is 0, the values are equal and we don't need saturation.
+ Operand lblNoSat = Label();
+ context.BranchIf(lblNoSat, value, Const(0), Comparison.GreaterOrEqual);
+
+ // Saturate.
+ // Assumes that the value can only underflow, since this is only used for unsigned subtraction.
+ context.Copy(result, Const(0));
+
+ Operand lblExit = Label();
+ context.Branch(lblExit);
+
+ context.MarkLabel(lblNoSat);
+
+ context.Copy(result, value);
+
+ context.MarkLabel(lblExit);
+ }
+
+ private static Operand EmitSigned16BitPair(ArmEmitterContext context, Operand rn, Operand rm, Action elementAction)
+ {
+ Operand tempD = context.AllocateLocal(OperandType.I32);
+
+ Operand tempN = context.SignExtend16(OperandType.I32, rn);
+ Operand tempM = context.SignExtend16(OperandType.I32, rm);
+ elementAction(tempD, tempN, tempM);
+ Operand tempD2 = context.ZeroExtend16(OperandType.I32, tempD);
+
+ tempN = context.ShiftRightSI(rn, Const(16));
+ tempM = context.ShiftRightSI(rm, Const(16));
+ elementAction(tempD, tempN, tempM);
+ return context.BitwiseOr(tempD2, context.ShiftLeft(tempD, Const(16)));
+ }
+
+ private static Operand EmitUnsigned16BitPair(ArmEmitterContext context, Operand rn, Operand rm, Action elementAction)
+ {
+ Operand tempD = context.AllocateLocal(OperandType.I32);
+
+ Operand tempN = context.ZeroExtend16(OperandType.I32, rn);
+ Operand tempM = context.ZeroExtend16(OperandType.I32, rm);
+ elementAction(tempD, tempN, tempM);
+ Operand tempD2 = context.ZeroExtend16(OperandType.I32, tempD);
+
+ tempN = context.ShiftRightUI(rn, Const(16));
+ tempM = context.ShiftRightUI(rm, Const(16));
+ elementAction(tempD, tempN, tempM);
+ return context.BitwiseOr(tempD2, context.ShiftLeft(tempD, Const(16)));
+ }
+
+ private static Operand EmitSigned8BitPair(ArmEmitterContext context, Operand rn, Operand rm, Action elementAction)
+ {
+ return Emit8BitPair(context, rn, rm, elementAction, unsigned: false);
+ }
+
+ private static Operand EmitUnsigned8BitPair(ArmEmitterContext context, Operand rn, Operand rm, Action elementAction)
+ {
+ return Emit8BitPair(context, rn, rm, elementAction, unsigned: true);
+ }
+
+ private static Operand Emit8BitPair(ArmEmitterContext context, Operand rn, Operand rm, Action elementAction, bool unsigned)
+ {
+ Operand tempD = context.AllocateLocal(OperandType.I32);
+ Operand result = default;
+
+ for (int b = 0; b < 4; b++)
+ {
+ Operand nByte = b != 0 ? context.ShiftRightUI(rn, Const(b * 8)) : rn;
+ Operand mByte = b != 0 ? context.ShiftRightUI(rm, Const(b * 8)) : rm;
+
+ if (unsigned)
+ {
+ nByte = context.ZeroExtend8(OperandType.I32, nByte);
+ mByte = context.ZeroExtend8(OperandType.I32, mByte);
+ }
+ else
+ {
+ nByte = context.SignExtend8(OperandType.I32, nByte);
+ mByte = context.SignExtend8(OperandType.I32, mByte);
+ }
+
+ elementAction(tempD, nByte, mByte);
+
+ if (b == 0)
+ {
+ result = context.ZeroExtend8(OperandType.I32, tempD);
+ }
+ else if (b < 3)
+ {
+ result = context.BitwiseOr(result, context.ShiftLeft(context.ZeroExtend8(OperandType.I32, tempD), Const(b * 8)));
+ }
+ else
+ {
+ result = context.BitwiseOr(result, context.ShiftLeft(tempD, Const(24)));
+ }
+ }
+
+ return result;
+ }
+
private static void EmitAluStore(ArmEmitterContext context, Operand value)
{
IOpCode32Alu op = (IOpCode32Alu)context.CurrOp;
diff --git a/src/ARMeilleure/Instructions/InstEmitMemoryHelper.cs b/src/ARMeilleure/Instructions/InstEmitMemoryHelper.cs
index 5610b7749..ace6fe1ce 100644
--- a/src/ARMeilleure/Instructions/InstEmitMemoryHelper.cs
+++ b/src/ARMeilleure/Instructions/InstEmitMemoryHelper.cs
@@ -403,19 +403,25 @@ namespace ARMeilleure.Instructions
{
return EmitHostMappedPointer(context, address);
}
- else if (context.Memory.Type == MemoryManagerType.HostTracked)
+ else if (context.Memory.Type.IsHostTracked())
{
+ if (address.Type == OperandType.I32)
+ {
+ address = context.ZeroExtend32(OperandType.I64, address);
+ }
+
+ if (context.Memory.Type == MemoryManagerType.HostTracked)
+ {
+ Operand mask = Const(ulong.MaxValue >> (64 - context.Memory.AddressSpaceBits));
+ address = context.BitwiseAnd(address, mask);
+ }
+
Operand ptBase = !context.HasPtc
? Const(context.Memory.PageTablePointer.ToInt64())
: Const(context.Memory.PageTablePointer.ToInt64(), Ptc.PageTableSymbol);
Operand ptOffset = context.ShiftRightUI(address, Const(PageBits));
- if (ptOffset.Type == OperandType.I32)
- {
- ptOffset = context.ZeroExtend32(OperandType.I64, ptOffset);
- }
-
return context.Add(address, context.Load(OperandType.I64, context.Add(ptBase, context.ShiftLeft(ptOffset, Const(3)))));
}
diff --git a/src/ARMeilleure/Instructions/InstEmitSimdArithmetic.cs b/src/ARMeilleure/Instructions/InstEmitSimdArithmetic.cs
index 543aab023..13d9fac68 100644
--- a/src/ARMeilleure/Instructions/InstEmitSimdArithmetic.cs
+++ b/src/ARMeilleure/Instructions/InstEmitSimdArithmetic.cs
@@ -2426,7 +2426,11 @@ namespace ARMeilleure.Instructions
}
else if (Optimizations.FastFP && Optimizations.UseSse41 && sizeF == 0)
{
- Operand res = EmitSse41Round32Exp8OpF(context, context.AddIntrinsic(Intrinsic.X86Rsqrtss, GetVec(op.Rn)), scalar: true);
+ // RSQRTSS handles subnormals as zero, which differs from Arm, so we can't use it here.
+
+ Operand res = context.AddIntrinsic(Intrinsic.X86Sqrtss, GetVec(op.Rn));
+ res = context.AddIntrinsic(Intrinsic.X86Rcpss, res);
+ res = EmitSse41Round32Exp8OpF(context, res, scalar: true);
context.Copy(GetVec(op.Rd), context.VectorZeroUpper96(res));
}
@@ -2451,7 +2455,11 @@ namespace ARMeilleure.Instructions
}
else if (Optimizations.FastFP && Optimizations.UseSse41 && sizeF == 0)
{
- Operand res = EmitSse41Round32Exp8OpF(context, context.AddIntrinsic(Intrinsic.X86Rsqrtps, GetVec(op.Rn)), scalar: false);
+ // RSQRTPS handles subnormals as zero, which differs from Arm, so we can't use it here.
+
+ Operand res = context.AddIntrinsic(Intrinsic.X86Sqrtps, GetVec(op.Rn));
+ res = context.AddIntrinsic(Intrinsic.X86Rcpps, res);
+ res = EmitSse41Round32Exp8OpF(context, res, scalar: false);
if (op.RegisterSize == RegisterSize.Simd64)
{
diff --git a/src/ARMeilleure/Instructions/InstEmitSimdArithmetic32.cs b/src/ARMeilleure/Instructions/InstEmitSimdArithmetic32.cs
index 27608ebf8..c807fc858 100644
--- a/src/ARMeilleure/Instructions/InstEmitSimdArithmetic32.cs
+++ b/src/ARMeilleure/Instructions/InstEmitSimdArithmetic32.cs
@@ -1115,6 +1115,13 @@ namespace ARMeilleure.Instructions
}
}
+ public static void Vpadal(ArmEmitterContext context)
+ {
+ OpCode32Simd op = (OpCode32Simd)context.CurrOp;
+
+ EmitVectorPairwiseTernaryLongOpI32(context, (op1, op2, op3) => context.Add(context.Add(op1, op2), op3), op.Opc != 1);
+ }
+
public static void Vpaddl(ArmEmitterContext context)
{
OpCode32Simd op = (OpCode32Simd)context.CurrOp;
@@ -1239,6 +1246,33 @@ namespace ARMeilleure.Instructions
EmitVectorUnaryNarrowOp32(context, (op1) => EmitSatQ(context, op1, 8 << op.Size, signedSrc: true, signedDst: false), signed: true);
}
+ public static void Vqrdmulh(ArmEmitterContext context)
+ {
+ OpCode32SimdReg op = (OpCode32SimdReg)context.CurrOp;
+ int eSize = 8 << op.Size;
+
+ EmitVectorBinaryOpI32(context, (op1, op2) =>
+ {
+ if (op.Size == 2)
+ {
+ op1 = context.SignExtend32(OperandType.I64, op1);
+ op2 = context.SignExtend32(OperandType.I64, op2);
+ }
+
+ Operand res = context.Multiply(op1, op2);
+ res = context.Add(res, Const(res.Type, 1L << (eSize - 2)));
+ res = context.ShiftRightSI(res, Const(eSize - 1));
+ res = EmitSatQ(context, res, eSize, signedSrc: true, signedDst: true);
+
+ if (op.Size == 2)
+ {
+ res = context.ConvertI64ToI32(res);
+ }
+
+ return res;
+ }, signed: true);
+ }
+
public static void Vqsub(ArmEmitterContext context)
{
OpCode32SimdReg op = (OpCode32SimdReg)context.CurrOp;
diff --git a/src/ARMeilleure/Instructions/InstEmitSimdCvt32.cs b/src/ARMeilleure/Instructions/InstEmitSimdCvt32.cs
index 630e114c4..8eef6b14d 100644
--- a/src/ARMeilleure/Instructions/InstEmitSimdCvt32.cs
+++ b/src/ARMeilleure/Instructions/InstEmitSimdCvt32.cs
@@ -578,6 +578,22 @@ namespace ARMeilleure.Instructions
}
}
+ // VRINTR (floating-point).
+ public static void Vrintr_S(ArmEmitterContext context)
+ {
+ if (Optimizations.UseAdvSimd)
+ {
+ InstEmitSimdHelper32Arm64.EmitScalarUnaryOpF32(context, Intrinsic.Arm64FrintiS);
+ }
+ else
+ {
+ EmitScalarUnaryOpF32(context, (op1) =>
+ {
+ return EmitRoundByRMode(context, op1);
+ });
+ }
+ }
+
// VRINTZ (floating-point).
public static void Vrint_Z(ArmEmitterContext context)
{
diff --git a/src/ARMeilleure/Instructions/InstEmitSimdHelper32.cs b/src/ARMeilleure/Instructions/InstEmitSimdHelper32.cs
index c1c59b87b..2f021a1a1 100644
--- a/src/ARMeilleure/Instructions/InstEmitSimdHelper32.cs
+++ b/src/ARMeilleure/Instructions/InstEmitSimdHelper32.cs
@@ -673,6 +673,35 @@ namespace ARMeilleure.Instructions
context.Copy(GetVecA32(op.Qd), res);
}
+ public static void EmitVectorPairwiseTernaryLongOpI32(ArmEmitterContext context, Func3I emit, bool signed)
+ {
+ OpCode32Simd op = (OpCode32Simd)context.CurrOp;
+
+ int elems = op.GetBytesCount() >> op.Size;
+ int pairs = elems >> 1;
+
+ Operand res = GetVecA32(op.Qd);
+
+ for (int index = 0; index < pairs; index++)
+ {
+ int pairIndex = index * 2;
+ Operand m1 = EmitVectorExtract32(context, op.Qm, op.Im + pairIndex, op.Size, signed);
+ Operand m2 = EmitVectorExtract32(context, op.Qm, op.Im + pairIndex + 1, op.Size, signed);
+
+ if (op.Size == 2)
+ {
+ m1 = signed ? context.SignExtend32(OperandType.I64, m1) : context.ZeroExtend32(OperandType.I64, m1);
+ m2 = signed ? context.SignExtend32(OperandType.I64, m2) : context.ZeroExtend32(OperandType.I64, m2);
+ }
+
+ Operand d1 = EmitVectorExtract32(context, op.Qd, op.Id + index, op.Size + 1, signed);
+
+ res = EmitVectorInsert(context, res, emit(m1, m2, d1), op.Id + index, op.Size + 1);
+ }
+
+ context.Copy(GetVecA32(op.Qd), res);
+ }
+
// Narrow
public static void EmitVectorUnaryNarrowOp32(ArmEmitterContext context, Func1I emit, bool signed = false)
diff --git a/src/ARMeilleure/Instructions/InstEmitSimdMove32.cs b/src/ARMeilleure/Instructions/InstEmitSimdMove32.cs
index 9fa740997..fb2641f66 100644
--- a/src/ARMeilleure/Instructions/InstEmitSimdMove32.cs
+++ b/src/ARMeilleure/Instructions/InstEmitSimdMove32.cs
@@ -191,6 +191,26 @@ namespace ARMeilleure.Instructions
context.Copy(GetVecA32(op.Qd), res);
}
+ public static void Vswp(ArmEmitterContext context)
+ {
+ OpCode32Simd op = (OpCode32Simd)context.CurrOp;
+
+ if (op.Q)
+ {
+ Operand temp = context.Copy(GetVecA32(op.Qd));
+
+ context.Copy(GetVecA32(op.Qd), GetVecA32(op.Qm));
+ context.Copy(GetVecA32(op.Qm), temp);
+ }
+ else
+ {
+ Operand temp = ExtractScalar(context, OperandType.I64, op.Vd);
+
+ InsertScalar(context, op.Vd, ExtractScalar(context, OperandType.I64, op.Vm));
+ InsertScalar(context, op.Vm, temp);
+ }
+ }
+
public static void Vtbl(ArmEmitterContext context)
{
OpCode32SimdTbl op = (OpCode32SimdTbl)context.CurrOp;
diff --git a/src/ARMeilleure/Instructions/InstEmitSimdShift.cs b/src/ARMeilleure/Instructions/InstEmitSimdShift.cs
index be0670645..94e912579 100644
--- a/src/ARMeilleure/Instructions/InstEmitSimdShift.cs
+++ b/src/ARMeilleure/Instructions/InstEmitSimdShift.cs
@@ -116,7 +116,7 @@ namespace ARMeilleure.Instructions
}
else if (shift >= eSize)
{
- if ((op.RegisterSize == RegisterSize.Simd64))
+ if (op.RegisterSize == RegisterSize.Simd64)
{
Operand res = context.VectorZeroUpper64(GetVec(op.Rd));
@@ -359,6 +359,16 @@ namespace ARMeilleure.Instructions
}
}
+ public static void Sqshl_Si(ArmEmitterContext context)
+ {
+ EmitShlImmOp(context, signedDst: true, ShlRegFlags.Signed | ShlRegFlags.Scalar | ShlRegFlags.Saturating);
+ }
+
+ public static void Sqshl_Vi(ArmEmitterContext context)
+ {
+ EmitShlImmOp(context, signedDst: true, ShlRegFlags.Signed | ShlRegFlags.Saturating);
+ }
+
public static void Sqshrn_S(ArmEmitterContext context)
{
if (Optimizations.UseAdvSimd)
@@ -1593,6 +1603,99 @@ namespace ARMeilleure.Instructions
Saturating = 1 << 3,
}
+ private static void EmitShlImmOp(ArmEmitterContext context, bool signedDst, ShlRegFlags flags = ShlRegFlags.None)
+ {
+ bool scalar = flags.HasFlag(ShlRegFlags.Scalar);
+ bool signed = flags.HasFlag(ShlRegFlags.Signed);
+ bool saturating = flags.HasFlag(ShlRegFlags.Saturating);
+
+ OpCodeSimdShImm op = (OpCodeSimdShImm)context.CurrOp;
+
+ Operand res = context.VectorZero();
+
+ int elems = !scalar ? op.GetBytesCount() >> op.Size : 1;
+
+ for (int index = 0; index < elems; index++)
+ {
+ Operand ne = EmitVectorExtract(context, op.Rn, index, op.Size, signed);
+
+ Operand e = !saturating
+ ? EmitShlImm(context, ne, GetImmShl(op), op.Size)
+ : EmitShlImmSatQ(context, ne, GetImmShl(op), op.Size, signed, signedDst);
+
+ res = EmitVectorInsert(context, res, e, index, op.Size);
+ }
+
+ context.Copy(GetVec(op.Rd), res);
+ }
+
+ private static Operand EmitShlImm(ArmEmitterContext context, Operand op, int shiftLsB, int size)
+ {
+ int eSize = 8 << size;
+
+ Debug.Assert(op.Type == OperandType.I64);
+ Debug.Assert(eSize == 8 || eSize == 16 || eSize == 32 || eSize == 64);
+
+ Operand res = context.AllocateLocal(OperandType.I64);
+
+ if (shiftLsB >= eSize)
+ {
+ Operand shl = context.ShiftLeft(op, Const(shiftLsB));
+ context.Copy(res, shl);
+ }
+ else
+ {
+ Operand zeroL = Const(0L);
+ context.Copy(res, zeroL);
+ }
+
+ return res;
+ }
+
+ private static Operand EmitShlImmSatQ(ArmEmitterContext context, Operand op, int shiftLsB, int size, bool signedSrc, bool signedDst)
+ {
+ int eSize = 8 << size;
+
+ Debug.Assert(op.Type == OperandType.I64);
+ Debug.Assert(eSize == 8 || eSize == 16 || eSize == 32 || eSize == 64);
+
+ Operand lblEnd = Label();
+
+ Operand res = context.Copy(context.AllocateLocal(OperandType.I64), op);
+
+ if (shiftLsB >= eSize)
+ {
+ context.Copy(res, signedSrc
+ ? EmitSignedSignSatQ(context, op, size)
+ : EmitUnsignedSignSatQ(context, op, size));
+ }
+ else
+ {
+ Operand shl = context.ShiftLeft(op, Const(shiftLsB));
+ if (eSize == 64)
+ {
+ Operand sarOrShr = signedSrc
+ ? context.ShiftRightSI(shl, Const(shiftLsB))
+ : context.ShiftRightUI(shl, Const(shiftLsB));
+ context.Copy(res, shl);
+ context.BranchIf(lblEnd, sarOrShr, op, Comparison.Equal);
+ context.Copy(res, signedSrc
+ ? EmitSignedSignSatQ(context, op, size)
+ : EmitUnsignedSignSatQ(context, op, size));
+ }
+ else
+ {
+ context.Copy(res, signedSrc
+ ? EmitSignedSrcSatQ(context, shl, size, signedDst)
+ : EmitUnsignedSrcSatQ(context, shl, size, signedDst));
+ }
+ }
+
+ context.MarkLabel(lblEnd);
+
+ return res;
+ }
+
private static void EmitShlRegOp(ArmEmitterContext context, ShlRegFlags flags = ShlRegFlags.None)
{
bool scalar = flags.HasFlag(ShlRegFlags.Scalar);
diff --git a/src/ARMeilleure/Instructions/InstEmitSimdShift32.cs b/src/ARMeilleure/Instructions/InstEmitSimdShift32.cs
index e40600a47..eb28a0c5a 100644
--- a/src/ARMeilleure/Instructions/InstEmitSimdShift32.cs
+++ b/src/ARMeilleure/Instructions/InstEmitSimdShift32.cs
@@ -106,6 +106,38 @@ namespace ARMeilleure.Instructions
context.Copy(GetVecA32(op.Qd), res);
}
+ public static void Vshll2(ArmEmitterContext context)
+ {
+ OpCode32Simd op = (OpCode32Simd)context.CurrOp;
+
+ Operand res = context.VectorZero();
+
+ int elems = op.GetBytesCount() >> op.Size;
+
+ for (int index = 0; index < elems; index++)
+ {
+ Operand me = EmitVectorExtract32(context, op.Qm, op.Im + index, op.Size, !op.U);
+
+ if (op.Size == 2)
+ {
+ if (op.U)
+ {
+ me = context.ZeroExtend32(OperandType.I64, me);
+ }
+ else
+ {
+ me = context.SignExtend32(OperandType.I64, me);
+ }
+ }
+
+ me = context.ShiftLeft(me, Const(8 << op.Size));
+
+ res = EmitVectorInsert(context, res, me, index, op.Size + 1);
+ }
+
+ context.Copy(GetVecA32(op.Qd), res);
+ }
+
public static void Vshr(ArmEmitterContext context)
{
OpCode32SimdShImm op = (OpCode32SimdShImm)context.CurrOp;
@@ -130,6 +162,36 @@ namespace ARMeilleure.Instructions
EmitVectorUnaryNarrowOp32(context, (op1) => context.ShiftRightUI(op1, Const(shift)));
}
+ public static void Vsli_I(ArmEmitterContext context)
+ {
+ OpCode32SimdShImm op = (OpCode32SimdShImm)context.CurrOp;
+ int shift = op.Shift;
+ int eSize = 8 << op.Size;
+
+ ulong mask = shift != 0 ? ulong.MaxValue >> (64 - shift) : 0UL;
+
+ Operand res = GetVec(op.Qd);
+
+ int elems = op.GetBytesCount() >> op.Size;
+
+ for (int index = 0; index < elems; index++)
+ {
+ Operand me = EmitVectorExtractZx(context, op.Qm, op.Im + index, op.Size);
+
+ Operand neShifted = context.ShiftLeft(me, Const(shift));
+
+ Operand de = EmitVectorExtractZx(context, op.Qd, op.Id + index, op.Size);
+
+ Operand deMasked = context.BitwiseAnd(de, Const(mask));
+
+ Operand e = context.BitwiseOr(neShifted, deMasked);
+
+ res = EmitVectorInsert(context, res, e, op.Id + index, op.Size);
+ }
+
+ context.Copy(GetVec(op.Qd), res);
+ }
+
public static void Vsra(ArmEmitterContext context)
{
OpCode32SimdShImm op = (OpCode32SimdShImm)context.CurrOp;
diff --git a/src/ARMeilleure/Instructions/InstName.cs b/src/ARMeilleure/Instructions/InstName.cs
index 32ae38dad..74c33155b 100644
--- a/src/ARMeilleure/Instructions/InstName.cs
+++ b/src/ARMeilleure/Instructions/InstName.cs
@@ -384,7 +384,9 @@ namespace ARMeilleure.Instructions
Sqrshrn_V,
Sqrshrun_S,
Sqrshrun_V,
+ Sqshl_Si,
Sqshl_V,
+ Sqshl_Vi,
Sqshrn_S,
Sqshrn_V,
Sqshrun_S,
@@ -525,6 +527,7 @@ namespace ARMeilleure.Instructions
Pld,
Pop,
Push,
+ Qadd16,
Rev,
Revsh,
Rsb,
@@ -569,6 +572,10 @@ namespace ARMeilleure.Instructions
Umaal,
Umlal,
Umull,
+ Uqadd16,
+ Uqadd8,
+ Uqsub16,
+ Uqsub8,
Usat,
Usat16,
Usub8,
@@ -635,6 +642,7 @@ namespace ARMeilleure.Instructions
Vorn,
Vorr,
Vpadd,
+ Vpadal,
Vpaddl,
Vpmax,
Vpmin,
@@ -642,6 +650,7 @@ namespace ARMeilleure.Instructions
Vqdmulh,
Vqmovn,
Vqmovun,
+ Vqrdmulh,
Vqrshrn,
Vqrshrun,
Vqshrn,
@@ -654,6 +663,7 @@ namespace ARMeilleure.Instructions
Vrintm,
Vrintn,
Vrintp,
+ Vrintr,
Vrintx,
Vrshr,
Vrshrn,
@@ -662,6 +672,7 @@ namespace ARMeilleure.Instructions
Vshll,
Vshr,
Vshrn,
+ Vsli,
Vst1,
Vst2,
Vst3,
@@ -678,6 +689,7 @@ namespace ARMeilleure.Instructions
Vsub,
Vsubl,
Vsubw,
+ Vswp,
Vtbl,
Vtrn,
Vtst,
diff --git a/src/ARMeilleure/Instructions/NativeInterface.cs b/src/ARMeilleure/Instructions/NativeInterface.cs
index d1b2e353c..0cd3754f7 100644
--- a/src/ARMeilleure/Instructions/NativeInterface.cs
+++ b/src/ARMeilleure/Instructions/NativeInterface.cs
@@ -91,54 +91,54 @@ namespace ARMeilleure.Instructions
#region "Read"
public static byte ReadByte(ulong address)
{
- return GetMemoryManager().ReadTracked(address);
+ return GetMemoryManager().ReadGuest(address);
}
public static ushort ReadUInt16(ulong address)
{
- return GetMemoryManager().ReadTracked(address);
+ return GetMemoryManager().ReadGuest(address);
}
public static uint ReadUInt32(ulong address)
{
- return GetMemoryManager().ReadTracked(address);
+ return GetMemoryManager().ReadGuest(address);
}
public static ulong ReadUInt64(ulong address)
{
- return GetMemoryManager().ReadTracked(address);
+ return GetMemoryManager().ReadGuest(address);
}
public static V128 ReadVector128(ulong address)
{
- return GetMemoryManager().ReadTracked(address);
+ return GetMemoryManager().ReadGuest(address);
}
#endregion
#region "Write"
public static void WriteByte(ulong address, byte value)
{
- GetMemoryManager().Write(address, value);
+ GetMemoryManager().WriteGuest(address, value);
}
public static void WriteUInt16(ulong address, ushort value)
{
- GetMemoryManager().Write(address, value);
+ GetMemoryManager().WriteGuest(address, value);
}
public static void WriteUInt32(ulong address, uint value)
{
- GetMemoryManager().Write(address, value);
+ GetMemoryManager().WriteGuest(address, value);
}
public static void WriteUInt64(ulong address, ulong value)
{
- GetMemoryManager().Write(address, value);
+ GetMemoryManager().WriteGuest(address, value);
}
public static void WriteVector128(ulong address, V128 value)
{
- GetMemoryManager().Write(address, value);
+ GetMemoryManager().WriteGuest(address, value);
}
#endregion
diff --git a/src/ARMeilleure/Memory/IJitMemoryAllocator.cs b/src/ARMeilleure/Memory/IJitMemoryAllocator.cs
index 171bfd2f1..ff64bf13e 100644
--- a/src/ARMeilleure/Memory/IJitMemoryAllocator.cs
+++ b/src/ARMeilleure/Memory/IJitMemoryAllocator.cs
@@ -4,7 +4,5 @@ namespace ARMeilleure.Memory
{
IJitMemoryBlock Allocate(ulong size);
IJitMemoryBlock Reserve(ulong size);
-
- ulong GetPageSize();
}
}
diff --git a/src/ARMeilleure/Memory/IMemoryManager.cs b/src/ARMeilleure/Memory/IMemoryManager.cs
index 952cd2b4f..46d442655 100644
--- a/src/ARMeilleure/Memory/IMemoryManager.cs
+++ b/src/ARMeilleure/Memory/IMemoryManager.cs
@@ -28,6 +28,17 @@ namespace ARMeilleure.Memory
/// The data
T ReadTracked(ulong va) where T : unmanaged;
+ ///
+ /// Reads data from CPU mapped memory, from guest code. (with read tracking)
+ ///
+ /// Type of the data being read
+ /// Virtual address of the data in memory
+ /// The data
+ T ReadGuest(ulong va) where T : unmanaged
+ {
+ return ReadTracked(va);
+ }
+
///
/// Writes data to CPU mapped memory.
///
@@ -36,6 +47,17 @@ namespace ARMeilleure.Memory
/// Data to be written
void Write(ulong va, T value) where T : unmanaged;
+ ///
+ /// Writes data to CPU mapped memory, from guest code.
+ ///
+ /// Type of the data being written
+ /// Virtual address to write the data into
+ /// Data to be written
+ void WriteGuest(ulong va, T value) where T : unmanaged
+ {
+ Write(va, value);
+ }
+
///
/// Gets a read-only span of data from CPU mapped memory.
///
diff --git a/src/ARMeilleure/Memory/MemoryManagerType.cs b/src/ARMeilleure/Memory/MemoryManagerType.cs
index 757322b4b..290503837 100644
--- a/src/ARMeilleure/Memory/MemoryManagerType.cs
+++ b/src/ARMeilleure/Memory/MemoryManagerType.cs
@@ -35,18 +35,29 @@ namespace ARMeilleure.Memory
/// Allows invalid access from JIT code to the rest of the program, but is faster.
///
HostMappedUnsafe,
+
+ ///
+ /// High level implementation using a software flat page table for address translation
+ /// without masking the address and no support for handling invalid or non-contiguous memory access.
+ ///
+ HostTrackedUnsafe,
}
- static class MemoryManagerTypeExtensions
+ public static class MemoryManagerTypeExtensions
{
public static bool IsHostMapped(this MemoryManagerType type)
{
return type == MemoryManagerType.HostMapped || type == MemoryManagerType.HostMappedUnsafe;
}
+ public static bool IsHostTracked(this MemoryManagerType type)
+ {
+ return type == MemoryManagerType.HostTracked || type == MemoryManagerType.HostTrackedUnsafe;
+ }
+
public static bool IsHostMappedOrTracked(this MemoryManagerType type)
{
- return type == MemoryManagerType.HostTracked || type == MemoryManagerType.HostMapped || type == MemoryManagerType.HostMappedUnsafe;
+ return type.IsHostMapped() || type.IsHostTracked();
}
}
}
diff --git a/src/ARMeilleure/Signal/NativeSignalHandler.cs b/src/ARMeilleure/Signal/NativeSignalHandlerGenerator.cs
similarity index 62%
rename from src/ARMeilleure/Signal/NativeSignalHandler.cs
rename to src/ARMeilleure/Signal/NativeSignalHandlerGenerator.cs
index 40860a5d7..49c73ae3d 100644
--- a/src/ARMeilleure/Signal/NativeSignalHandler.cs
+++ b/src/ARMeilleure/Signal/NativeSignalHandlerGenerator.cs
@@ -1,63 +1,14 @@
-using ARMeilleure.IntermediateRepresentation;
-using ARMeilleure.Memory;
+using ARMeilleure.IntermediateRepresentation;
using ARMeilleure.Translation;
-using ARMeilleure.Translation.Cache;
using System;
-using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using static ARMeilleure.IntermediateRepresentation.Operand.Factory;
namespace ARMeilleure.Signal
{
- [StructLayout(LayoutKind.Sequential, Pack = 1)]
- struct SignalHandlerRange
+ public static class NativeSignalHandlerGenerator
{
- public int IsActive;
- public nuint RangeAddress;
- public nuint RangeEndAddress;
- public IntPtr ActionPointer;
- }
-
- [StructLayout(LayoutKind.Sequential, Pack = 1)]
- struct SignalHandlerConfig
- {
- ///
- /// The byte offset of the faulting address in the SigInfo or ExceptionRecord struct.
- ///
- public int StructAddressOffset;
-
- ///
- /// The byte offset of the write flag in the SigInfo or ExceptionRecord struct.
- ///
- public int StructWriteOffset;
-
- ///
- /// The sigaction handler that was registered before this one. (unix only)
- ///
- public nuint UnixOldSigaction;
-
- ///
- /// The type of the previous sigaction. True for the 3 argument variant. (unix only)
- ///
- public int UnixOldSigaction3Arg;
-
- public SignalHandlerRange Range0;
- public SignalHandlerRange Range1;
- public SignalHandlerRange Range2;
- public SignalHandlerRange Range3;
- public SignalHandlerRange Range4;
- public SignalHandlerRange Range5;
- public SignalHandlerRange Range6;
- public SignalHandlerRange Range7;
- }
-
- public static class NativeSignalHandler
- {
- private delegate void UnixExceptionHandler(int sig, IntPtr info, IntPtr ucontext);
- [UnmanagedFunctionPointer(CallingConvention.Winapi)]
- private delegate int VectoredExceptionHandler(IntPtr exceptionInfo);
-
- private const int MaxTrackedRanges = 8;
+ public const int MaxTrackedRanges = 8;
private const int StructAddressOffset = 0;
private const int StructWriteOffset = 4;
@@ -70,124 +21,7 @@ namespace ARMeilleure.Signal
private const uint EXCEPTION_ACCESS_VIOLATION = 0xc0000005;
- private static ulong _pageSize;
- private static ulong _pageMask;
-
- private static readonly IntPtr _handlerConfig;
- private static IntPtr _signalHandlerPtr;
- private static IntPtr _signalHandlerHandle;
-
- private static readonly object _lock = new();
- private static bool _initialized;
-
- static NativeSignalHandler()
- {
- _handlerConfig = Marshal.AllocHGlobal(Unsafe.SizeOf());
- ref SignalHandlerConfig config = ref GetConfigRef();
-
- config = new SignalHandlerConfig();
- }
-
- public static void Initialize(IJitMemoryAllocator allocator)
- {
- JitCache.Initialize(allocator);
- }
-
- public static void InitializeSignalHandler(ulong pageSize, Func customSignalHandlerFactory = null)
- {
- if (_initialized)
- {
- return;
- }
-
- lock (_lock)
- {
- if (_initialized)
- {
- return;
- }
-
- _pageSize = pageSize;
- _pageMask = pageSize - 1;
-
- ref SignalHandlerConfig config = ref GetConfigRef();
-
- if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS() || OperatingSystem.IsIOS())
- {
- _signalHandlerPtr = Marshal.GetFunctionPointerForDelegate(GenerateUnixSignalHandler(_handlerConfig));
-
- if (customSignalHandlerFactory != null)
- {
- _signalHandlerPtr = customSignalHandlerFactory(UnixSignalHandlerRegistration.GetSegfaultExceptionHandler().sa_handler, _signalHandlerPtr);
- }
-
- var old = UnixSignalHandlerRegistration.RegisterExceptionHandler(_signalHandlerPtr);
-
- config.UnixOldSigaction = (nuint)(ulong)old.sa_handler;
- config.UnixOldSigaction3Arg = old.sa_flags & 4;
- }
- else
- {
- config.StructAddressOffset = 40; // ExceptionInformation1
- config.StructWriteOffset = 32; // ExceptionInformation0
-
- _signalHandlerPtr = Marshal.GetFunctionPointerForDelegate(GenerateWindowsSignalHandler(_handlerConfig));
-
- if (customSignalHandlerFactory != null)
- {
- _signalHandlerPtr = customSignalHandlerFactory(IntPtr.Zero, _signalHandlerPtr);
- }
-
- _signalHandlerHandle = WindowsSignalHandlerRegistration.RegisterExceptionHandler(_signalHandlerPtr);
- }
-
- _initialized = true;
- }
- }
-
- private static unsafe ref SignalHandlerConfig GetConfigRef()
- {
- return ref Unsafe.AsRef((void*)_handlerConfig);
- }
-
- public static unsafe bool AddTrackedRegion(nuint address, nuint endAddress, IntPtr action)
- {
- var ranges = &((SignalHandlerConfig*)_handlerConfig)->Range0;
-
- for (int i = 0; i < MaxTrackedRanges; i++)
- {
- if (ranges[i].IsActive == 0)
- {
- ranges[i].RangeAddress = address;
- ranges[i].RangeEndAddress = endAddress;
- ranges[i].ActionPointer = action;
- ranges[i].IsActive = 1;
-
- return true;
- }
- }
-
- return false;
- }
-
- public static unsafe bool RemoveTrackedRegion(nuint address)
- {
- var ranges = &((SignalHandlerConfig*)_handlerConfig)->Range0;
-
- for (int i = 0; i < MaxTrackedRanges; i++)
- {
- if (ranges[i].IsActive == 1 && ranges[i].RangeAddress == address)
- {
- ranges[i].IsActive = 0;
-
- return true;
- }
- }
-
- return false;
- }
-
- private static Operand EmitGenericRegionCheck(EmitterContext context, IntPtr signalStructPtr, Operand faultAddress, Operand isWrite)
+ private static Operand EmitGenericRegionCheck(EmitterContext context, IntPtr signalStructPtr, Operand faultAddress, Operand isWrite, int rangeStructSize)
{
Operand inRegionLocal = context.AllocateLocal(OperandType.I32);
context.Copy(inRegionLocal, Const(0));
@@ -196,7 +30,7 @@ namespace ARMeilleure.Signal
for (int i = 0; i < MaxTrackedRanges; i++)
{
- ulong rangeBaseOffset = (ulong)(RangeOffset + i * Unsafe.SizeOf());
+ ulong rangeBaseOffset = (ulong)(RangeOffset + i * rangeStructSize);
Operand nextLabel = Label();
@@ -210,13 +44,12 @@ namespace ARMeilleure.Signal
// Is the fault address within this tracked region?
Operand inRange = context.BitwiseAnd(
context.ICompare(faultAddress, rangeAddress, Comparison.GreaterOrEqualUI),
- context.ICompare(faultAddress, rangeEndAddress, Comparison.LessUI)
- );
+ context.ICompare(faultAddress, rangeEndAddress, Comparison.LessUI));
// Only call tracking if in range.
context.BranchIfFalse(nextLabel, inRange, BasicBlockFrequency.Cold);
- Operand offset = context.BitwiseAnd(context.Subtract(faultAddress, rangeAddress), Const(~_pageMask));
+ Operand offset = context.Subtract(faultAddress, rangeAddress);
// Call the tracking action, with the pointer's relative offset to the base address.
Operand trackingActionPtr = context.Load(OperandType.I64, Const((ulong)signalStructPtr + rangeBaseOffset + 20));
@@ -227,8 +60,10 @@ namespace ARMeilleure.Signal
// Tracking action should be non-null to call it, otherwise assume false return.
context.BranchIfFalse(skipActionLabel, trackingActionPtr);
- Operand result = context.Call(trackingActionPtr, OperandType.I32, offset, Const(_pageSize), isWrite);
- context.Copy(inRegionLocal, result);
+ Operand result = context.Call(trackingActionPtr, OperandType.I64, offset, Const(1UL), isWrite);
+ context.Copy(inRegionLocal, context.ICompareNotEqual(result, Const(0UL)));
+
+ GenerateFaultAddressPatchCode(context, faultAddress, result);
context.MarkLabel(skipActionLabel);
@@ -269,8 +104,7 @@ namespace ARMeilleure.Signal
Operand esr = context.Load(OperandType.I64, context.Add(ctxPtr, Const(EsrOffset)));
return context.BitwiseAnd(esr, Const(0x40ul));
}
-
- if (RuntimeInformation.ProcessArchitecture == Architecture.X64)
+ else if (RuntimeInformation.ProcessArchitecture == Architecture.X64)
{
const ulong ErrOffset = 4; // __es.__err
Operand err = context.Load(OperandType.I64, context.Add(ctxPtr, Const(ErrOffset)));
@@ -310,8 +144,7 @@ namespace ARMeilleure.Signal
Operand esr = context.Load(OperandType.I64, context.Add(auxPtr, Const(8ul)));
return context.BitwiseAnd(esr, Const(0x40ul));
}
-
- if (RuntimeInformation.ProcessArchitecture == Architecture.X64)
+ else if (RuntimeInformation.ProcessArchitecture == Architecture.X64)
{
const int ErrOffset = 192; // uc_mcontext.gregs[REG_ERR]
Operand err = context.Load(OperandType.I64, context.Add(ucontextPtr, Const(ErrOffset)));
@@ -322,7 +155,7 @@ namespace ARMeilleure.Signal
throw new PlatformNotSupportedException();
}
- private static UnixExceptionHandler GenerateUnixSignalHandler(IntPtr signalStructPtr)
+ public static byte[] GenerateUnixSignalHandler(IntPtr signalStructPtr, int rangeStructSize)
{
EmitterContext context = new();
@@ -335,7 +168,7 @@ namespace ARMeilleure.Signal
Operand isWrite = context.ICompareNotEqual(writeFlag, Const(0L)); // Normalize to 0/1.
- Operand isInRegion = EmitGenericRegionCheck(context, signalStructPtr, faultAddress, isWrite);
+ Operand isInRegion = EmitGenericRegionCheck(context, signalStructPtr, faultAddress, isWrite, rangeStructSize);
Operand endLabel = Label();
@@ -367,10 +200,10 @@ namespace ARMeilleure.Signal
OperandType[] argTypes = new OperandType[] { OperandType.I32, OperandType.I64, OperandType.I64 };
- return Compiler.Compile(cfg, argTypes, OperandType.None, CompilerOptions.HighCq, RuntimeInformation.ProcessArchitecture).Map();
+ return Compiler.Compile(cfg, argTypes, OperandType.None, CompilerOptions.HighCq, RuntimeInformation.ProcessArchitecture).Code;
}
- private static VectoredExceptionHandler GenerateWindowsSignalHandler(IntPtr signalStructPtr)
+ public static byte[] GenerateWindowsSignalHandler(IntPtr signalStructPtr, int rangeStructSize)
{
EmitterContext context = new();
@@ -399,7 +232,7 @@ namespace ARMeilleure.Signal
Operand isWrite = context.ICompareNotEqual(writeFlag, Const(0L)); // Normalize to 0/1.
- Operand isInRegion = EmitGenericRegionCheck(context, signalStructPtr, faultAddress, isWrite);
+ Operand isInRegion = EmitGenericRegionCheck(context, signalStructPtr, faultAddress, isWrite, rangeStructSize);
Operand endLabel = Label();
@@ -421,7 +254,88 @@ namespace ARMeilleure.Signal
OperandType[] argTypes = new OperandType[] { OperandType.I64 };
- return Compiler.Compile(cfg, argTypes, OperandType.I32, CompilerOptions.HighCq, RuntimeInformation.ProcessArchitecture).Map();
+ return Compiler.Compile(cfg, argTypes, OperandType.I32, CompilerOptions.HighCq, RuntimeInformation.ProcessArchitecture).Code;
+ }
+
+ private static void GenerateFaultAddressPatchCode(EmitterContext context, Operand faultAddress, Operand newAddress)
+ {
+ if (RuntimeInformation.ProcessArchitecture == Architecture.Arm64)
+ {
+ if (SupportsFaultAddressPatchingForHostOs())
+ {
+ Operand lblSkip = Label();
+
+ context.BranchIf(lblSkip, faultAddress, newAddress, Comparison.Equal);
+
+ Operand ucontextPtr = context.LoadArgument(OperandType.I64, 2);
+ Operand pcCtxAddress = default;
+ ulong baseRegsOffset = 0;
+
+ if (OperatingSystem.IsLinux())
+ {
+ pcCtxAddress = context.Add(ucontextPtr, Const(440UL));
+ baseRegsOffset = 184UL;
+ }
+ else if (OperatingSystem.IsMacOS() || OperatingSystem.IsIOS())
+ {
+ ucontextPtr = context.Load(OperandType.I64, context.Add(ucontextPtr, Const(48UL)));
+
+ pcCtxAddress = context.Add(ucontextPtr, Const(272UL));
+ baseRegsOffset = 16UL;
+ }
+
+ Operand pc = context.Load(OperandType.I64, pcCtxAddress);
+
+ Operand reg = GetAddressRegisterFromArm64Instruction(context, pc);
+ Operand reg64 = context.ZeroExtend32(OperandType.I64, reg);
+ Operand regCtxAddress = context.Add(ucontextPtr, context.Add(context.ShiftLeft(reg64, Const(3)), Const(baseRegsOffset)));
+ Operand regAddress = context.Load(OperandType.I64, regCtxAddress);
+
+ Operand addressDelta = context.Subtract(regAddress, faultAddress);
+
+ context.Store(regCtxAddress, context.Add(newAddress, addressDelta));
+
+ context.MarkLabel(lblSkip);
+ }
+ }
+ }
+
+ private static Operand GetAddressRegisterFromArm64Instruction(EmitterContext context, Operand pc)
+ {
+ Operand inst = context.Load(OperandType.I32, pc);
+ Operand reg = context.AllocateLocal(OperandType.I32);
+
+ Operand isSysInst = context.ICompareEqual(context.BitwiseAnd(inst, Const(0xFFF80000)), Const(0xD5080000));
+
+ Operand lblSys = Label();
+ Operand lblEnd = Label();
+
+ context.BranchIfTrue(lblSys, isSysInst, BasicBlockFrequency.Cold);
+
+ context.Copy(reg, context.BitwiseAnd(context.ShiftRightUI(inst, Const(5)), Const(0x1F)));
+ context.Branch(lblEnd);
+
+ context.MarkLabel(lblSys);
+ context.Copy(reg, context.BitwiseAnd(inst, Const(0x1F)));
+
+ context.MarkLabel(lblEnd);
+
+ return reg;
+ }
+
+ public static bool SupportsFaultAddressPatchingForHost()
+ {
+ return SupportsFaultAddressPatchingForHostArch() && SupportsFaultAddressPatchingForHostOs();
+ }
+
+ private static bool SupportsFaultAddressPatchingForHostArch()
+ {
+ return RuntimeInformation.ProcessArchitecture == Architecture.Arm64;
+ }
+
+ private static bool SupportsFaultAddressPatchingForHostOs()
+ {
+ return OperatingSystem.IsLinux() || OperatingSystem.IsMacOS() || OperatingSystem.IsIOS();
}
}
}
diff --git a/src/ARMeilleure/Signal/WindowsPartialUnmapHandler.cs b/src/ARMeilleure/Signal/WindowsPartialUnmapHandler.cs
index 27a9ea83c..3bf6a4498 100644
--- a/src/ARMeilleure/Signal/WindowsPartialUnmapHandler.cs
+++ b/src/ARMeilleure/Signal/WindowsPartialUnmapHandler.cs
@@ -2,7 +2,7 @@ using ARMeilleure.IntermediateRepresentation;
using ARMeilleure.Translation;
using Ryujinx.Common.Memory.PartialUnmaps;
using System;
-
+using System.Runtime.InteropServices;
using static ARMeilleure.IntermediateRepresentation.Operand.Factory;
namespace ARMeilleure.Signal
@@ -10,8 +10,28 @@ namespace ARMeilleure.Signal
///
/// Methods to handle signals caused by partial unmaps. See the structs for C# implementations of the methods.
///
- internal static class WindowsPartialUnmapHandler
+ internal static partial class WindowsPartialUnmapHandler
{
+ [LibraryImport("kernel32.dll", SetLastError = true, EntryPoint = "LoadLibraryA")]
+ private static partial IntPtr LoadLibrary([MarshalAs(UnmanagedType.LPStr)] string lpFileName);
+
+ [LibraryImport("kernel32.dll", SetLastError = true)]
+ private static partial IntPtr GetProcAddress(IntPtr hModule, [MarshalAs(UnmanagedType.LPStr)] string procName);
+
+ private static IntPtr _getCurrentThreadIdPtr;
+
+ public static IntPtr GetCurrentThreadIdFunc()
+ {
+ if (_getCurrentThreadIdPtr == IntPtr.Zero)
+ {
+ IntPtr handle = LoadLibrary("kernel32.dll");
+
+ _getCurrentThreadIdPtr = GetProcAddress(handle, "GetCurrentThreadId");
+ }
+
+ return _getCurrentThreadIdPtr;
+ }
+
public static Operand EmitRetryFromAccessViolation(EmitterContext context)
{
IntPtr partialRemapStatePtr = PartialUnmapState.GlobalState;
@@ -20,7 +40,7 @@ namespace ARMeilleure.Signal
// Get the lock first.
EmitNativeReaderLockAcquire(context, IntPtr.Add(partialRemapStatePtr, PartialUnmapState.PartialUnmapLockOffset));
- IntPtr getCurrentThreadId = WindowsSignalHandlerRegistration.GetCurrentThreadIdFunc();
+ IntPtr getCurrentThreadId = GetCurrentThreadIdFunc();
Operand threadId = context.Call(Const((ulong)getCurrentThreadId), OperandType.I32);
Operand threadIndex = EmitThreadLocalMapIntGetOrReserve(context, localCountsPtr, threadId, Const(0));
@@ -137,17 +157,6 @@ namespace ARMeilleure.Signal
return context.Add(structsPtr, context.SignExtend32(OperandType.I64, offset));
}
-#pragma warning disable IDE0051 // Remove unused private member
- private static void EmitThreadLocalMapIntRelease(EmitterContext context, IntPtr threadLocalMapPtr, Operand threadId, Operand index)
- {
- Operand offset = context.Multiply(index, Const(sizeof(int)));
- Operand idsPtr = Const((ulong)IntPtr.Add(threadLocalMapPtr, ThreadLocalMap.ThreadIdsOffset));
- Operand idPtr = context.Add(idsPtr, context.SignExtend32(OperandType.I64, offset));
-
- context.CompareAndSwap(idPtr, threadId, Const(0));
- }
-#pragma warning restore IDE0051
-
private static void EmitAtomicAddI32(EmitterContext context, Operand ptr, Operand additive)
{
Operand loop = Label();
diff --git a/src/ARMeilleure/Signal/WindowsSignalHandlerRegistration.cs b/src/ARMeilleure/Signal/WindowsSignalHandlerRegistration.cs
deleted file mode 100644
index 5444da0ca..000000000
--- a/src/ARMeilleure/Signal/WindowsSignalHandlerRegistration.cs
+++ /dev/null
@@ -1,44 +0,0 @@
-using System;
-using System.Runtime.InteropServices;
-
-namespace ARMeilleure.Signal
-{
- unsafe partial class WindowsSignalHandlerRegistration
- {
- [LibraryImport("kernel32.dll")]
- private static partial IntPtr AddVectoredExceptionHandler(uint first, IntPtr handler);
-
- [LibraryImport("kernel32.dll")]
- private static partial ulong RemoveVectoredExceptionHandler(IntPtr handle);
-
- [LibraryImport("kernel32.dll", SetLastError = true, EntryPoint = "LoadLibraryA")]
- private static partial IntPtr LoadLibrary([MarshalAs(UnmanagedType.LPStr)] string lpFileName);
-
- [LibraryImport("kernel32.dll", SetLastError = true)]
- private static partial IntPtr GetProcAddress(IntPtr hModule, [MarshalAs(UnmanagedType.LPStr)] string procName);
-
- private static IntPtr _getCurrentThreadIdPtr;
-
- public static IntPtr RegisterExceptionHandler(IntPtr action)
- {
- return AddVectoredExceptionHandler(1, action);
- }
-
- public static bool RemoveExceptionHandler(IntPtr handle)
- {
- return RemoveVectoredExceptionHandler(handle) != 0;
- }
-
- public static IntPtr GetCurrentThreadIdFunc()
- {
- if (_getCurrentThreadIdPtr == IntPtr.Zero)
- {
- IntPtr handle = LoadLibrary("kernel32.dll");
-
- _getCurrentThreadIdPtr = GetProcAddress(handle, "GetCurrentThreadId");
- }
-
- return _getCurrentThreadIdPtr;
- }
- }
-}
diff --git a/src/ARMeilleure/Translation/Cache/JitCache.cs b/src/ARMeilleure/Translation/Cache/JitCache.cs
index 862c9e7b5..45f0688bf 100644
--- a/src/ARMeilleure/Translation/Cache/JitCache.cs
+++ b/src/ARMeilleure/Translation/Cache/JitCache.cs
@@ -117,8 +117,15 @@ namespace ARMeilleure.Translation.Cache
if (OperatingSystem.IsIOS())
{
Marshal.Copy(code, 0, funcPtr, code.Length);
- ReprotectAsExecutable(targetRegion, funcOffset, code.Length);
- JitSupportDarwinAot.Invalidate(funcPtr, (ulong)code.Length);
+ if (deferProtect)
+ {
+ _deferredRxProtect.Enqueue((funcOffset, code.Length));
+ }
+ else
+ {
+ ReprotectAsExecutable(targetRegion, funcOffset, code.Length);
+ JitSupportDarwinAot.Invalidate(funcPtr, (ulong)code.Length);
+ }
}
else if (OperatingSystem.IsMacOS()&& RuntimeInformation.ProcessArchitecture == Architecture.Arm64)
{
diff --git a/src/ARMeilleure/Translation/ControlFlowGraph.cs b/src/ARMeilleure/Translation/ControlFlowGraph.cs
index 3ead49c93..45b092ec5 100644
--- a/src/ARMeilleure/Translation/ControlFlowGraph.cs
+++ b/src/ARMeilleure/Translation/ControlFlowGraph.cs
@@ -11,7 +11,7 @@ namespace ARMeilleure.Translation
private int[] _postOrderMap;
public int LocalsCount { get; private set; }
- public BasicBlock Entry { get; }
+ public BasicBlock Entry { get; private set; }
public IntrusiveList Blocks { get; }
public BasicBlock[] PostOrderBlocks => _postOrderBlocks;
public int[] PostOrderMap => _postOrderMap;
@@ -34,6 +34,15 @@ namespace ARMeilleure.Translation
return result;
}
+ public void UpdateEntry(BasicBlock newEntry)
+ {
+ newEntry.AddSuccessor(Entry);
+
+ Entry = newEntry;
+ Blocks.AddFirst(newEntry);
+ Update();
+ }
+
public void Update()
{
RemoveUnreachableBlocks(Blocks);
diff --git a/src/ARMeilleure/Translation/PTC/Ptc.cs b/src/ARMeilleure/Translation/PTC/Ptc.cs
index 5ed27927a..a8383ee11 100644
--- a/src/ARMeilleure/Translation/PTC/Ptc.cs
+++ b/src/ARMeilleure/Translation/PTC/Ptc.cs
@@ -30,7 +30,7 @@ namespace ARMeilleure.Translation.PTC
private const string OuterHeaderMagicString = "PTCohd\0\0";
private const string InnerHeaderMagicString = "PTCihd\0\0";
- private const uint InternalVersion = 5518; //! To be incremented manually for each change to the ARMeilleure project.
+ private const uint InternalVersion = 6950; //! To be incremented manually for each change to the ARMeilleure project.
private const string ActualDir = "0";
private const string BackupDir = "1";
@@ -858,8 +858,14 @@ namespace ARMeilleure.Translation.PTC
Stopwatch sw = Stopwatch.StartNew();
- threads.ForEach((thread) => thread.Start());
- threads.ForEach((thread) => thread.Join());
+ foreach (var thread in threads)
+ {
+ thread.Start();
+ }
+ foreach (var thread in threads)
+ {
+ thread.Join();
+ }
threads.Clear();
diff --git a/src/ARMeilleure/Translation/RegisterUsage.cs b/src/ARMeilleure/Translation/RegisterUsage.cs
index c8c250626..472b0f67b 100644
--- a/src/ARMeilleure/Translation/RegisterUsage.cs
+++ b/src/ARMeilleure/Translation/RegisterUsage.cs
@@ -89,6 +89,17 @@ namespace ARMeilleure.Translation
public static void RunPass(ControlFlowGraph cfg, ExecutionMode mode)
{
+ if (cfg.Entry.Predecessors.Count != 0)
+ {
+ // We expect the entry block to have no predecessors.
+ // This is required because we have a implicit context load at the start of the function,
+ // but if there is a jump to the start of the function, the context load would trash the modified values.
+ // Here we insert a new entry block that will jump to the existing entry block.
+ BasicBlock newEntry = new BasicBlock(cfg.Blocks.Count);
+
+ cfg.UpdateEntry(newEntry);
+ }
+
// Compute local register inputs and outputs used inside blocks.
RegisterMask[] localInputs = new RegisterMask[cfg.Blocks.Count];
RegisterMask[] localOutputs = new RegisterMask[cfg.Blocks.Count];
@@ -201,7 +212,7 @@ namespace ARMeilleure.Translation
// The only block without any predecessor should be the entry block.
// It always needs a context load as it is the first block to run.
- if (block.Predecessors.Count == 0 || hasContextLoad)
+ if (block == cfg.Entry || hasContextLoad)
{
long vecMask = globalInputs[block.Index].VecMask;
long intMask = globalInputs[block.Index].IntMask;
diff --git a/src/ARMeilleure/Translation/Translator.cs b/src/ARMeilleure/Translation/Translator.cs
index 253f25e4a..9b28bed53 100644
--- a/src/ARMeilleure/Translation/Translator.cs
+++ b/src/ARMeilleure/Translation/Translator.cs
@@ -57,9 +57,6 @@ namespace ARMeilleure.Translation
private Thread[] _backgroundTranslationThreads;
private volatile int _threadCount;
- // FIXME: Remove this once the init logic of the emulator will be redone.
- public static readonly ManualResetEvent IsReadyForTranslation = new(false);
-
public Translator(IJitMemoryAllocator allocator, IMemoryManager memory, bool for64Bits)
{
_allocator = allocator;
@@ -79,11 +76,6 @@ namespace ARMeilleure.Translation
Stubs = new TranslatorStubs(FunctionTable);
FunctionTable.Fill = (ulong)Stubs.SlowDispatchStub;
-
- if (memory.Type.IsHostMappedOrTracked())
- {
- NativeSignalHandler.InitializeSignalHandler(allocator.GetPageSize());
- }
}
public IPtcLoadState LoadDiskCache(string titleIdText, string displayVersion, bool enabled)
@@ -105,8 +97,6 @@ namespace ARMeilleure.Translation
{
if (Interlocked.Increment(ref _threadCount) == 1)
{
- IsReadyForTranslation.WaitOne();
-
if (_ptc.State == PtcState.Enabled)
{
Debug.Assert(Functions.Count == 0);
diff --git a/src/ARMeilleure/Translation/TranslatorQueue.cs b/src/ARMeilleure/Translation/TranslatorQueue.cs
index cee2f9080..831522bc1 100644
--- a/src/ARMeilleure/Translation/TranslatorQueue.cs
+++ b/src/ARMeilleure/Translation/TranslatorQueue.cs
@@ -80,7 +80,10 @@ namespace ARMeilleure.Translation
return true;
}
- Monitor.Wait(Sync);
+ if (!_disposed)
+ {
+ Monitor.Wait(Sync);
+ }
}
}
diff --git a/src/Ryujinx.Audio.Backends.OpenAL/OpenALHardwareDeviceDriver.cs b/src/Ryujinx.Audio.Backends.OpenAL/OpenALHardwareDeviceDriver.cs
index 744a4bc56..01286992f 100644
--- a/src/Ryujinx.Audio.Backends.OpenAL/OpenALHardwareDeviceDriver.cs
+++ b/src/Ryujinx.Audio.Backends.OpenAL/OpenALHardwareDeviceDriver.cs
@@ -20,6 +20,25 @@ namespace Ryujinx.Audio.Backends.OpenAL
private bool _stillRunning;
private readonly Thread _updaterThread;
+ private float _volume;
+
+ public float Volume
+ {
+ get
+ {
+ return _volume;
+ }
+ set
+ {
+ _volume = value;
+
+ foreach (OpenALHardwareDeviceSession session in _sessions.Keys)
+ {
+ session.UpdateMasterVolume(value);
+ }
+ }
+ }
+
public OpenALHardwareDeviceDriver()
{
_device = ALC.OpenDevice("");
@@ -34,6 +53,8 @@ namespace Ryujinx.Audio.Backends.OpenAL
Name = "HardwareDeviceDriver.OpenAL",
};
+ _volume = 1f;
+
_updaterThread.Start();
}
@@ -52,7 +73,7 @@ namespace Ryujinx.Audio.Backends.OpenAL
}
}
- public IHardwareDeviceSession OpenDeviceSession(Direction direction, IVirtualMemoryManager memoryManager, SampleFormat sampleFormat, uint sampleRate, uint channelCount, float volume)
+ public IHardwareDeviceSession OpenDeviceSession(Direction direction, IVirtualMemoryManager memoryManager, SampleFormat sampleFormat, uint sampleRate, uint channelCount)
{
if (channelCount == 0)
{
@@ -73,7 +94,7 @@ namespace Ryujinx.Audio.Backends.OpenAL
throw new ArgumentException($"{channelCount}");
}
- OpenALHardwareDeviceSession session = new(this, memoryManager, sampleFormat, sampleRate, channelCount, volume);
+ OpenALHardwareDeviceSession session = new(this, memoryManager, sampleFormat, sampleRate, channelCount);
_sessions.TryAdd(session, 0);
diff --git a/src/Ryujinx.Audio.Backends.OpenAL/OpenALHardwareDeviceSession.cs b/src/Ryujinx.Audio.Backends.OpenAL/OpenALHardwareDeviceSession.cs
index 73e914083..ff35f86e1 100644
--- a/src/Ryujinx.Audio.Backends.OpenAL/OpenALHardwareDeviceSession.cs
+++ b/src/Ryujinx.Audio.Backends.OpenAL/OpenALHardwareDeviceSession.cs
@@ -16,10 +16,11 @@ namespace Ryujinx.Audio.Backends.OpenAL
private bool _isActive;
private readonly Queue _queuedBuffers;
private ulong _playedSampleCount;
+ private float _volume;
private readonly object _lock = new();
- public OpenALHardwareDeviceSession(OpenALHardwareDeviceDriver driver, IVirtualMemoryManager memoryManager, SampleFormat requestedSampleFormat, uint requestedSampleRate, uint requestedChannelCount, float requestedVolume) : base(memoryManager, requestedSampleFormat, requestedSampleRate, requestedChannelCount)
+ public OpenALHardwareDeviceSession(OpenALHardwareDeviceDriver driver, IVirtualMemoryManager memoryManager, SampleFormat requestedSampleFormat, uint requestedSampleRate, uint requestedChannelCount) : base(memoryManager, requestedSampleFormat, requestedSampleRate, requestedChannelCount)
{
_driver = driver;
_queuedBuffers = new Queue();
@@ -27,7 +28,7 @@ namespace Ryujinx.Audio.Backends.OpenAL
_targetFormat = GetALFormat();
_isActive = false;
_playedSampleCount = 0;
- SetVolume(requestedVolume);
+ SetVolume(1f);
}
private ALFormat GetALFormat()
@@ -85,17 +86,22 @@ namespace Ryujinx.Audio.Backends.OpenAL
public override void SetVolume(float volume)
{
- lock (_lock)
- {
- AL.Source(_sourceId, ALSourcef.Gain, volume);
- }
+ _volume = volume;
+
+ UpdateMasterVolume(_driver.Volume);
}
public override float GetVolume()
{
- AL.GetSource(_sourceId, ALSourcef.Gain, out float volume);
+ return _volume;
+ }
- return volume;
+ public void UpdateMasterVolume(float newVolume)
+ {
+ lock (_lock)
+ {
+ AL.Source(_sourceId, ALSourcef.Gain, newVolume * _volume);
+ }
}
public override void Start()
diff --git a/src/Ryujinx.Audio.Backends.SDL2/SDL2HardwareDeviceDriver.cs b/src/Ryujinx.Audio.Backends.SDL2/SDL2HardwareDeviceDriver.cs
index 58137bb38..03a036936 100644
--- a/src/Ryujinx.Audio.Backends.SDL2/SDL2HardwareDeviceDriver.cs
+++ b/src/Ryujinx.Audio.Backends.SDL2/SDL2HardwareDeviceDriver.cs
@@ -20,6 +20,8 @@ namespace Ryujinx.Audio.Backends.SDL2
private readonly bool _supportSurroundConfiguration;
+ public float Volume { get; set; }
+
// TODO: Add this to SDL2-CS
// NOTE: We use a DllImport here because of marshaling issue for spec.
#pragma warning disable SYSLIB1054
@@ -48,6 +50,8 @@ namespace Ryujinx.Audio.Backends.SDL2
{
_supportSurroundConfiguration = spec.channels >= 6;
}
+
+ Volume = 1f;
}
public static bool IsSupported => IsSupportedInternal();
@@ -74,7 +78,7 @@ namespace Ryujinx.Audio.Backends.SDL2
return _pauseEvent;
}
- public IHardwareDeviceSession OpenDeviceSession(Direction direction, IVirtualMemoryManager memoryManager, SampleFormat sampleFormat, uint sampleRate, uint channelCount, float volume)
+ public IHardwareDeviceSession OpenDeviceSession(Direction direction, IVirtualMemoryManager memoryManager, SampleFormat sampleFormat, uint sampleRate, uint channelCount)
{
if (channelCount == 0)
{
@@ -91,7 +95,7 @@ namespace Ryujinx.Audio.Backends.SDL2
throw new NotImplementedException("Input direction is currently not implemented on SDL2 backend!");
}
- SDL2HardwareDeviceSession session = new(this, memoryManager, sampleFormat, sampleRate, channelCount, volume);
+ SDL2HardwareDeviceSession session = new(this, memoryManager, sampleFormat, sampleRate, channelCount);
_sessions.TryAdd(session, 0);
diff --git a/src/Ryujinx.Audio.Backends.SDL2/SDL2HardwareDeviceSession.cs b/src/Ryujinx.Audio.Backends.SDL2/SDL2HardwareDeviceSession.cs
index cf3be473e..ea5ae3661 100644
--- a/src/Ryujinx.Audio.Backends.SDL2/SDL2HardwareDeviceSession.cs
+++ b/src/Ryujinx.Audio.Backends.SDL2/SDL2HardwareDeviceSession.cs
@@ -1,8 +1,10 @@
using Ryujinx.Audio.Backends.Common;
using Ryujinx.Audio.Common;
using Ryujinx.Common.Logging;
+using Ryujinx.Common.Memory;
using Ryujinx.Memory;
using System;
+using System.Buffers;
using System.Collections.Concurrent;
using System.Threading;
@@ -26,7 +28,7 @@ namespace Ryujinx.Audio.Backends.SDL2
private float _volume;
private readonly ushort _nativeSampleFormat;
- public SDL2HardwareDeviceSession(SDL2HardwareDeviceDriver driver, IVirtualMemoryManager memoryManager, SampleFormat requestedSampleFormat, uint requestedSampleRate, uint requestedChannelCount, float requestedVolume) : base(memoryManager, requestedSampleFormat, requestedSampleRate, requestedChannelCount)
+ public SDL2HardwareDeviceSession(SDL2HardwareDeviceDriver driver, IVirtualMemoryManager memoryManager, SampleFormat requestedSampleFormat, uint requestedSampleRate, uint requestedChannelCount) : base(memoryManager, requestedSampleFormat, requestedSampleRate, requestedChannelCount)
{
_driver = driver;
_updateRequiredEvent = _driver.GetUpdateRequiredEvent();
@@ -37,7 +39,7 @@ namespace Ryujinx.Audio.Backends.SDL2
_nativeSampleFormat = SDL2HardwareDeviceDriver.GetSDL2Format(RequestedSampleFormat);
_sampleCount = uint.MaxValue;
_started = false;
- _volume = requestedVolume;
+ _volume = 1f;
}
private void EnsureAudioStreamSetup(AudioBuffer buffer)
@@ -87,7 +89,9 @@ namespace Ryujinx.Audio.Backends.SDL2
return;
}
- byte[] samples = new byte[frameCount * _bytesPerFrame];
+ using SpanOwner samplesOwner = SpanOwner.Rent(frameCount * _bytesPerFrame);
+
+ Span samples = samplesOwner.Span;
_ringBuffer.Read(samples, 0, samples.Length);
@@ -99,7 +103,7 @@ namespace Ryujinx.Audio.Backends.SDL2
streamSpan.Clear();
// Apply volume to written data
- SDL_MixAudioFormat(stream, pStreamSrc, _nativeSampleFormat, (uint)samples.Length, (int)(_volume * SDL_MIX_MAXVOLUME));
+ SDL_MixAudioFormat(stream, pStreamSrc, _nativeSampleFormat, (uint)samples.Length, (int)(_driver.Volume * _volume * SDL_MIX_MAXVOLUME));
}
ulong sampleCount = GetSampleCount(samples.Length);
diff --git a/src/Ryujinx.Audio.Backends.SoundIo/Ryujinx.Audio.Backends.SoundIo.csproj b/src/Ryujinx.Audio.Backends.SoundIo/Ryujinx.Audio.Backends.SoundIo.csproj
index 1d92d9d2e..5c9423463 100644
--- a/src/Ryujinx.Audio.Backends.SoundIo/Ryujinx.Audio.Backends.SoundIo.csproj
+++ b/src/Ryujinx.Audio.Backends.SoundIo/Ryujinx.Audio.Backends.SoundIo.csproj
@@ -11,15 +11,15 @@
-
+ PreserveNewestlibsoundio.dll
-
+ PreserveNewestlibsoundio.dylib
-
+ PreserveNewestlibsoundio.so
diff --git a/src/Ryujinx.Audio.Backends.SoundIo/SoundIoHardwareDeviceDriver.cs b/src/Ryujinx.Audio.Backends.SoundIo/SoundIoHardwareDeviceDriver.cs
index ff0392882..e3e5d2913 100644
--- a/src/Ryujinx.Audio.Backends.SoundIo/SoundIoHardwareDeviceDriver.cs
+++ b/src/Ryujinx.Audio.Backends.SoundIo/SoundIoHardwareDeviceDriver.cs
@@ -19,6 +19,25 @@ namespace Ryujinx.Audio.Backends.SoundIo
private readonly ConcurrentDictionary _sessions;
private int _disposeState;
+ private float _volume = 1f;
+
+ public float Volume
+ {
+ get
+ {
+ return _volume;
+ }
+ set
+ {
+ _volume = value;
+
+ foreach (SoundIoHardwareDeviceSession session in _sessions.Keys)
+ {
+ session.UpdateMasterVolume(value);
+ }
+ }
+ }
+
public SoundIoHardwareDeviceDriver()
{
_audioContext = SoundIoContext.Create();
@@ -122,7 +141,7 @@ namespace Ryujinx.Audio.Backends.SoundIo
return _pauseEvent;
}
- public IHardwareDeviceSession OpenDeviceSession(Direction direction, IVirtualMemoryManager memoryManager, SampleFormat sampleFormat, uint sampleRate, uint channelCount, float volume)
+ public IHardwareDeviceSession OpenDeviceSession(Direction direction, IVirtualMemoryManager memoryManager, SampleFormat sampleFormat, uint sampleRate, uint channelCount)
{
if (channelCount == 0)
{
@@ -134,14 +153,12 @@ namespace Ryujinx.Audio.Backends.SoundIo
sampleRate = Constants.TargetSampleRate;
}
- volume = Math.Clamp(volume, 0, 1);
-
if (direction != Direction.Output)
{
throw new NotImplementedException("Input direction is currently not implemented on SoundIO backend!");
}
- SoundIoHardwareDeviceSession session = new(this, memoryManager, sampleFormat, sampleRate, channelCount, volume);
+ SoundIoHardwareDeviceSession session = new(this, memoryManager, sampleFormat, sampleRate, channelCount);
_sessions.TryAdd(session, 0);
diff --git a/src/Ryujinx.Audio.Backends.SoundIo/SoundIoHardwareDeviceSession.cs b/src/Ryujinx.Audio.Backends.SoundIo/SoundIoHardwareDeviceSession.cs
index b9070dc48..2ef83b34b 100644
--- a/src/Ryujinx.Audio.Backends.SoundIo/SoundIoHardwareDeviceSession.cs
+++ b/src/Ryujinx.Audio.Backends.SoundIo/SoundIoHardwareDeviceSession.cs
@@ -1,8 +1,10 @@
using Ryujinx.Audio.Backends.Common;
using Ryujinx.Audio.Backends.SoundIo.Native;
using Ryujinx.Audio.Common;
+using Ryujinx.Common.Memory;
using Ryujinx.Memory;
using System;
+using System.Buffers;
using System.Collections.Concurrent;
using System.Runtime.CompilerServices;
using System.Threading;
@@ -18,16 +20,18 @@ namespace Ryujinx.Audio.Backends.SoundIo
private readonly DynamicRingBuffer _ringBuffer;
private ulong _playedSampleCount;
private readonly ManualResetEvent _updateRequiredEvent;
+ private float _volume;
private int _disposeState;
- public SoundIoHardwareDeviceSession(SoundIoHardwareDeviceDriver driver, IVirtualMemoryManager memoryManager, SampleFormat requestedSampleFormat, uint requestedSampleRate, uint requestedChannelCount, float requestedVolume) : base(memoryManager, requestedSampleFormat, requestedSampleRate, requestedChannelCount)
+ public SoundIoHardwareDeviceSession(SoundIoHardwareDeviceDriver driver, IVirtualMemoryManager memoryManager, SampleFormat requestedSampleFormat, uint requestedSampleRate, uint requestedChannelCount) : base(memoryManager, requestedSampleFormat, requestedSampleRate, requestedChannelCount)
{
_driver = driver;
_updateRequiredEvent = _driver.GetUpdateRequiredEvent();
_queuedBuffers = new ConcurrentQueue();
_ringBuffer = new DynamicRingBuffer();
+ _volume = 1f;
- SetupOutputStream(requestedVolume);
+ SetupOutputStream(driver.Volume);
}
private void SetupOutputStream(float requestedVolume)
@@ -35,7 +39,7 @@ namespace Ryujinx.Audio.Backends.SoundIo
_outputStream = _driver.OpenStream(RequestedSampleFormat, RequestedSampleRate, RequestedChannelCount);
_outputStream.WriteCallback += Update;
_outputStream.Volume = requestedVolume;
- // TODO: Setup other callbacks (errors, ect).
+ // TODO: Setup other callbacks (errors, etc.)
_outputStream.Open();
}
@@ -47,7 +51,7 @@ namespace Ryujinx.Audio.Backends.SoundIo
public override float GetVolume()
{
- return _outputStream.Volume;
+ return _volume;
}
public override void PrepareToClose() { }
@@ -63,7 +67,14 @@ namespace Ryujinx.Audio.Backends.SoundIo
public override void SetVolume(float volume)
{
- _outputStream.SetVolume(volume);
+ _volume = volume;
+
+ _outputStream.SetVolume(_driver.Volume * volume);
+ }
+
+ public void UpdateMasterVolume(float newVolume)
+ {
+ _outputStream.SetVolume(newVolume * _volume);
}
public override void Start()
@@ -111,7 +122,9 @@ namespace Ryujinx.Audio.Backends.SoundIo
int channelCount = areas.Length;
- byte[] samples = new byte[frameCount * bytesPerFrame];
+ using SpanOwner samplesOwner = SpanOwner.Rent(frameCount * bytesPerFrame);
+
+ Span samples = samplesOwner.Span;
_ringBuffer.Read(samples, 0, samples.Length);
diff --git a/src/Ryujinx.Audio/Backends/Common/DynamicRingBuffer.cs b/src/Ryujinx.Audio/Backends/Common/DynamicRingBuffer.cs
index 05dd2162a..7aefe8865 100644
--- a/src/Ryujinx.Audio/Backends/Common/DynamicRingBuffer.cs
+++ b/src/Ryujinx.Audio/Backends/Common/DynamicRingBuffer.cs
@@ -1,5 +1,7 @@
using Ryujinx.Common;
+using Ryujinx.Common.Memory;
using System;
+using System.Buffers;
namespace Ryujinx.Audio.Backends.Common
{
@@ -12,7 +14,8 @@ namespace Ryujinx.Audio.Backends.Common
private readonly object _lock = new();
- private byte[] _buffer;
+ private MemoryOwner _bufferOwner;
+ private Memory _buffer;
private int _size;
private int _headOffset;
private int _tailOffset;
@@ -21,7 +24,8 @@ namespace Ryujinx.Audio.Backends.Common
public DynamicRingBuffer(int initialCapacity = RingBufferAlignment)
{
- _buffer = new byte[initialCapacity];
+ _bufferOwner = MemoryOwner.RentCleared(initialCapacity);
+ _buffer = _bufferOwner.Memory;
}
public void Clear()
@@ -33,6 +37,11 @@ namespace Ryujinx.Audio.Backends.Common
public void Clear(int size)
{
+ if (size == 0)
+ {
+ return;
+ }
+
lock (_lock)
{
if (size > _size)
@@ -40,11 +49,6 @@ namespace Ryujinx.Audio.Backends.Common
size = _size;
}
- if (size == 0)
- {
- return;
- }
-
_headOffset = (_headOffset + size) % _buffer.Length;
_size -= size;
@@ -58,28 +62,31 @@ namespace Ryujinx.Audio.Backends.Common
private void SetCapacityLocked(int capacity)
{
- byte[] buffer = new byte[capacity];
+ MemoryOwner newBufferOwner = MemoryOwner.RentCleared(capacity);
+ Memory newBuffer = newBufferOwner.Memory;
if (_size > 0)
{
if (_headOffset < _tailOffset)
{
- Buffer.BlockCopy(_buffer, _headOffset, buffer, 0, _size);
+ _buffer.Slice(_headOffset, _size).CopyTo(newBuffer);
}
else
{
- Buffer.BlockCopy(_buffer, _headOffset, buffer, 0, _buffer.Length - _headOffset);
- Buffer.BlockCopy(_buffer, 0, buffer, _buffer.Length - _headOffset, _tailOffset);
+ _buffer[_headOffset..].CopyTo(newBuffer);
+ _buffer[.._tailOffset].CopyTo(newBuffer[(_buffer.Length - _headOffset)..]);
}
}
- _buffer = buffer;
+ _bufferOwner.Dispose();
+
+ _bufferOwner = newBufferOwner;
+ _buffer = newBuffer;
_headOffset = 0;
_tailOffset = _size;
}
-
- public void Write(T[] buffer, int index, int count)
+ public void Write(ReadOnlySpan buffer, int index, int count)
{
if (count == 0)
{
@@ -99,17 +106,17 @@ namespace Ryujinx.Audio.Backends.Common
if (tailLength >= count)
{
- Buffer.BlockCopy(buffer, index, _buffer, _tailOffset, count);
+ buffer.Slice(index, count).CopyTo(_buffer.Span[_tailOffset..]);
}
else
{
- Buffer.BlockCopy(buffer, index, _buffer, _tailOffset, tailLength);
- Buffer.BlockCopy(buffer, index + tailLength, _buffer, 0, count - tailLength);
+ buffer.Slice(index, tailLength).CopyTo(_buffer.Span[_tailOffset..]);
+ buffer.Slice(index + tailLength, count - tailLength).CopyTo(_buffer.Span);
}
}
else
{
- Buffer.BlockCopy(buffer, index, _buffer, _tailOffset, count);
+ buffer.Slice(index, count).CopyTo(_buffer.Span[_tailOffset..]);
}
_size += count;
@@ -117,8 +124,13 @@ namespace Ryujinx.Audio.Backends.Common
}
}
- public int Read(T[] buffer, int index, int count)
+ public int Read(Span buffer, int index, int count)
{
+ if (count == 0)
+ {
+ return 0;
+ }
+
lock (_lock)
{
if (count > _size)
@@ -126,14 +138,9 @@ namespace Ryujinx.Audio.Backends.Common
count = _size;
}
- if (count == 0)
- {
- return 0;
- }
-
if (_headOffset < _tailOffset)
{
- Buffer.BlockCopy(_buffer, _headOffset, buffer, index, count);
+ _buffer.Span.Slice(_headOffset, count).CopyTo(buffer[index..]);
}
else
{
@@ -141,12 +148,12 @@ namespace Ryujinx.Audio.Backends.Common
if (tailLength >= count)
{
- Buffer.BlockCopy(_buffer, _headOffset, buffer, index, count);
+ _buffer.Span.Slice(_headOffset, count).CopyTo(buffer[index..]);
}
else
{
- Buffer.BlockCopy(_buffer, _headOffset, buffer, index, tailLength);
- Buffer.BlockCopy(_buffer, 0, buffer, index + tailLength, count - tailLength);
+ _buffer.Span.Slice(_headOffset, tailLength).CopyTo(buffer[index..]);
+ _buffer.Span[..(count - tailLength)].CopyTo(buffer[(index + tailLength)..]);
}
}
diff --git a/src/Ryujinx.Audio/Backends/CompatLayer/CompatLayerHardwareDeviceDriver.cs b/src/Ryujinx.Audio/Backends/CompatLayer/CompatLayerHardwareDeviceDriver.cs
index 3f3806c3e..a2c2cdcd0 100644
--- a/src/Ryujinx.Audio/Backends/CompatLayer/CompatLayerHardwareDeviceDriver.cs
+++ b/src/Ryujinx.Audio/Backends/CompatLayer/CompatLayerHardwareDeviceDriver.cs
@@ -16,6 +16,12 @@ namespace Ryujinx.Audio.Backends.CompatLayer
public static bool IsSupported => true;
+ public float Volume
+ {
+ get => _realDriver.Volume;
+ set => _realDriver.Volume = value;
+ }
+
public CompatLayerHardwareDeviceDriver(IHardwareDeviceDriver realDevice)
{
_realDriver = realDevice;
@@ -90,7 +96,7 @@ namespace Ryujinx.Audio.Backends.CompatLayer
throw new ArgumentException("No valid sample format configuration found!");
}
- public IHardwareDeviceSession OpenDeviceSession(Direction direction, IVirtualMemoryManager memoryManager, SampleFormat sampleFormat, uint sampleRate, uint channelCount, float volume)
+ public IHardwareDeviceSession OpenDeviceSession(Direction direction, IVirtualMemoryManager memoryManager, SampleFormat sampleFormat, uint sampleRate, uint channelCount)
{
if (channelCount == 0)
{
@@ -102,8 +108,6 @@ namespace Ryujinx.Audio.Backends.CompatLayer
sampleRate = Constants.TargetSampleRate;
}
- volume = Math.Clamp(volume, 0, 1);
-
if (!_realDriver.SupportsDirection(direction))
{
if (direction == Direction.Input)
@@ -119,7 +123,7 @@ namespace Ryujinx.Audio.Backends.CompatLayer
SampleFormat hardwareSampleFormat = SelectHardwareSampleFormat(sampleFormat);
uint hardwareChannelCount = SelectHardwareChannelCount(channelCount);
- IHardwareDeviceSession realSession = _realDriver.OpenDeviceSession(direction, memoryManager, hardwareSampleFormat, sampleRate, hardwareChannelCount, volume);
+ IHardwareDeviceSession realSession = _realDriver.OpenDeviceSession(direction, memoryManager, hardwareSampleFormat, sampleRate, hardwareChannelCount);
if (hardwareChannelCount == channelCount && hardwareSampleFormat == sampleFormat)
{
diff --git a/src/Ryujinx.Audio/Backends/CompatLayer/Downmixing.cs b/src/Ryujinx.Audio/Backends/CompatLayer/Downmixing.cs
index ffd427a5e..7a5ea0deb 100644
--- a/src/Ryujinx.Audio/Backends/CompatLayer/Downmixing.cs
+++ b/src/Ryujinx.Audio/Backends/CompatLayer/Downmixing.cs
@@ -31,7 +31,7 @@ namespace Ryujinx.Audio.Backends.CompatLayer
private const int Minus6dBInQ15 = (int)(0.501f * RawQ15One);
private const int Minus12dBInQ15 = (int)(0.251f * RawQ15One);
- private static readonly int[] _defaultSurroundToStereoCoefficients = new int[4]
+ private static readonly long[] _defaultSurroundToStereoCoefficients = new long[4]
{
RawQ15One,
Minus3dBInQ15,
@@ -39,7 +39,7 @@ namespace Ryujinx.Audio.Backends.CompatLayer
Minus3dBInQ15,
};
- private static readonly int[] _defaultStereoToMonoCoefficients = new int[2]
+ private static readonly long[] _defaultStereoToMonoCoefficients = new long[2]
{
Minus6dBInQ15,
Minus6dBInQ15,
@@ -62,19 +62,23 @@ namespace Ryujinx.Audio.Backends.CompatLayer
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static short DownMixStereoToMono(ReadOnlySpan coefficients, short left, short right)
+ private static short DownMixStereoToMono(ReadOnlySpan coefficients, short left, short right)
{
- return (short)((left * coefficients[0] + right * coefficients[1]) >> Q15Bits);
+ return (short)Math.Clamp((left * coefficients[0] + right * coefficients[1]) >> Q15Bits, short.MinValue, short.MaxValue);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static short DownMixSurroundToStereo(ReadOnlySpan coefficients, short back, short lfe, short center, short front)
+ private static short DownMixSurroundToStereo(ReadOnlySpan coefficients, short back, short lfe, short center, short front)
{
- return (short)((coefficients[3] * back + coefficients[2] * lfe + coefficients[1] * center + coefficients[0] * front + RawQ15HalfOne) >> Q15Bits);
+ return (short)Math.Clamp(
+ (coefficients[3] * back +
+ coefficients[2] * lfe +
+ coefficients[1] * center +
+ coefficients[0] * front + RawQ15HalfOne) >> Q15Bits, short.MinValue, short.MaxValue);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static short[] DownMixSurroundToStereo(ReadOnlySpan coefficients, ReadOnlySpan data)
+ private static short[] DownMixSurroundToStereo(ReadOnlySpan coefficients, ReadOnlySpan data)
{
int samplePerChannelCount = data.Length / SurroundChannelCount;
@@ -94,7 +98,7 @@ namespace Ryujinx.Audio.Backends.CompatLayer
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static short[] DownMixStereoToMono(ReadOnlySpan coefficients, ReadOnlySpan data)
+ private static short[] DownMixStereoToMono(ReadOnlySpan coefficients, ReadOnlySpan data)
{
int samplePerChannelCount = data.Length / StereoChannelCount;
diff --git a/src/Ryujinx.Audio/Backends/DelayLayer/DelayLayerHardwareDeviceDriver.cs b/src/Ryujinx.Audio/Backends/DelayLayer/DelayLayerHardwareDeviceDriver.cs
index cdd5eb8a8..b46b38f5a 100644
--- a/src/Ryujinx.Audio/Backends/DelayLayer/DelayLayerHardwareDeviceDriver.cs
+++ b/src/Ryujinx.Audio/Backends/DelayLayer/DelayLayerHardwareDeviceDriver.cs
@@ -16,15 +16,17 @@ namespace Ryujinx.Audio.Backends.DelayLayer
public ulong SampleDelay48k;
+ public float Volume { get; set; }
+
public DelayLayerHardwareDeviceDriver(IHardwareDeviceDriver realDevice, ulong sampleDelay48k)
{
_realDriver = realDevice;
SampleDelay48k = sampleDelay48k;
}
- public IHardwareDeviceSession OpenDeviceSession(Direction direction, IVirtualMemoryManager memoryManager, SampleFormat sampleFormat, uint sampleRate, uint channelCount, float volume)
+ public IHardwareDeviceSession OpenDeviceSession(Direction direction, IVirtualMemoryManager memoryManager, SampleFormat sampleFormat, uint sampleRate, uint channelCount)
{
- IHardwareDeviceSession session = _realDriver.OpenDeviceSession(direction, memoryManager, sampleFormat, sampleRate, channelCount, volume);
+ IHardwareDeviceSession session = _realDriver.OpenDeviceSession(direction, memoryManager, sampleFormat, sampleRate, channelCount);
if (direction == Direction.Output)
{
diff --git a/src/Ryujinx.Audio/Backends/Dummy/DummyHardwareDeviceDriver.cs b/src/Ryujinx.Audio/Backends/Dummy/DummyHardwareDeviceDriver.cs
index bac21c448..3a3c1d1b1 100644
--- a/src/Ryujinx.Audio/Backends/Dummy/DummyHardwareDeviceDriver.cs
+++ b/src/Ryujinx.Audio/Backends/Dummy/DummyHardwareDeviceDriver.cs
@@ -14,13 +14,17 @@ namespace Ryujinx.Audio.Backends.Dummy
public static bool IsSupported => true;
+ public float Volume { get; set; }
+
public DummyHardwareDeviceDriver()
{
_updateRequiredEvent = new ManualResetEvent(false);
_pauseEvent = new ManualResetEvent(true);
+
+ Volume = 1f;
}
- public IHardwareDeviceSession OpenDeviceSession(Direction direction, IVirtualMemoryManager memoryManager, SampleFormat sampleFormat, uint sampleRate, uint channelCount, float volume)
+ public IHardwareDeviceSession OpenDeviceSession(Direction direction, IVirtualMemoryManager memoryManager, SampleFormat sampleFormat, uint sampleRate, uint channelCount)
{
if (sampleRate == 0)
{
@@ -34,7 +38,7 @@ namespace Ryujinx.Audio.Backends.Dummy
if (direction == Direction.Output)
{
- return new DummyHardwareDeviceSessionOutput(this, memoryManager, sampleFormat, sampleRate, channelCount, volume);
+ return new DummyHardwareDeviceSessionOutput(this, memoryManager, sampleFormat, sampleRate, channelCount);
}
return new DummyHardwareDeviceSessionInput(this, memoryManager);
diff --git a/src/Ryujinx.Audio/Backends/Dummy/DummyHardwareDeviceSessionOutput.cs b/src/Ryujinx.Audio/Backends/Dummy/DummyHardwareDeviceSessionOutput.cs
index 1c248faaa..34cf653c2 100644
--- a/src/Ryujinx.Audio/Backends/Dummy/DummyHardwareDeviceSessionOutput.cs
+++ b/src/Ryujinx.Audio/Backends/Dummy/DummyHardwareDeviceSessionOutput.cs
@@ -13,9 +13,9 @@ namespace Ryujinx.Audio.Backends.Dummy
private ulong _playedSampleCount;
- public DummyHardwareDeviceSessionOutput(IHardwareDeviceDriver manager, IVirtualMemoryManager memoryManager, SampleFormat requestedSampleFormat, uint requestedSampleRate, uint requestedChannelCount, float requestedVolume) : base(memoryManager, requestedSampleFormat, requestedSampleRate, requestedChannelCount)
+ public DummyHardwareDeviceSessionOutput(IHardwareDeviceDriver manager, IVirtualMemoryManager memoryManager, SampleFormat requestedSampleFormat, uint requestedSampleRate, uint requestedChannelCount) : base(memoryManager, requestedSampleFormat, requestedSampleRate, requestedChannelCount)
{
- _volume = requestedVolume;
+ _volume = 1f;
_manager = manager;
}
diff --git a/src/Ryujinx.Audio/Input/AudioInputManager.cs b/src/Ryujinx.Audio/Input/AudioInputManager.cs
index 4d1796c96..d56997e9c 100644
--- a/src/Ryujinx.Audio/Input/AudioInputManager.cs
+++ b/src/Ryujinx.Audio/Input/AudioInputManager.cs
@@ -166,7 +166,6 @@ namespace Ryujinx.Audio.Input
///
/// If true, filter disconnected devices
/// The list of all audio inputs name
-#pragma warning disable CA1822 // Mark member as static
public string[] ListAudioIns(bool filtered)
{
if (filtered)
@@ -176,7 +175,6 @@ namespace Ryujinx.Audio.Input
return new[] { Constants.DefaultDeviceInputName };
}
-#pragma warning restore CA1822
///
/// Open a new .
@@ -188,8 +186,6 @@ namespace Ryujinx.Audio.Input
/// The input device name wanted by the user
/// The sample format to use
/// The user configuration
- /// The applet resource user id of the application
- /// The process handle of the application
/// A reporting an error or a success
public ResultCode OpenAudioIn(out string outputDeviceName,
out AudioOutputConfiguration outputConfiguration,
@@ -197,9 +193,7 @@ namespace Ryujinx.Audio.Input
IVirtualMemoryManager memoryManager,
string inputDeviceName,
SampleFormat sampleFormat,
- ref AudioInputConfiguration parameter,
- ulong appletResourceUserId,
- uint processHandle)
+ ref AudioInputConfiguration parameter)
{
int sessionId = AcquireSessionId();
diff --git a/src/Ryujinx.Audio/Integration/HardwareDeviceImpl.cs b/src/Ryujinx.Audio/Integration/HardwareDeviceImpl.cs
index 576954b96..1369f953a 100644
--- a/src/Ryujinx.Audio/Integration/HardwareDeviceImpl.cs
+++ b/src/Ryujinx.Audio/Integration/HardwareDeviceImpl.cs
@@ -13,9 +13,9 @@ namespace Ryujinx.Audio.Integration
private readonly byte[] _buffer;
- public HardwareDeviceImpl(IHardwareDeviceDriver deviceDriver, uint channelCount, uint sampleRate, float volume)
+ public HardwareDeviceImpl(IHardwareDeviceDriver deviceDriver, uint channelCount, uint sampleRate)
{
- _session = deviceDriver.OpenDeviceSession(IHardwareDeviceDriver.Direction.Output, null, SampleFormat.PcmInt16, sampleRate, channelCount, volume);
+ _session = deviceDriver.OpenDeviceSession(IHardwareDeviceDriver.Direction.Output, null, SampleFormat.PcmInt16, sampleRate, channelCount);
_channelCount = channelCount;
_sampleRate = sampleRate;
_currentBufferTag = 0;
diff --git a/src/Ryujinx.Audio/Integration/IHardwareDeviceDriver.cs b/src/Ryujinx.Audio/Integration/IHardwareDeviceDriver.cs
index 9c812fb9a..95b0e4e5e 100644
--- a/src/Ryujinx.Audio/Integration/IHardwareDeviceDriver.cs
+++ b/src/Ryujinx.Audio/Integration/IHardwareDeviceDriver.cs
@@ -16,7 +16,9 @@ namespace Ryujinx.Audio.Integration
Output,
}
- IHardwareDeviceSession OpenDeviceSession(Direction direction, IVirtualMemoryManager memoryManager, SampleFormat sampleFormat, uint sampleRate, uint channelCount, float volume = 1f);
+ float Volume { get; set; }
+
+ IHardwareDeviceSession OpenDeviceSession(Direction direction, IVirtualMemoryManager memoryManager, SampleFormat sampleFormat, uint sampleRate, uint channelCount);
ManualResetEvent GetUpdateRequiredEvent();
ManualResetEvent GetPauseEvent();
diff --git a/src/Ryujinx.Audio/Output/AudioOutputManager.cs b/src/Ryujinx.Audio/Output/AudioOutputManager.cs
index 5232357bb..308cd1564 100644
--- a/src/Ryujinx.Audio/Output/AudioOutputManager.cs
+++ b/src/Ryujinx.Audio/Output/AudioOutputManager.cs
@@ -165,12 +165,10 @@ namespace Ryujinx.Audio.Output
/// Get the list of all audio outputs name.
///
/// The list of all audio outputs name
-#pragma warning disable CA1822 // Mark member as static
public string[] ListAudioOuts()
{
return new[] { Constants.DefaultDeviceOutputName };
}
-#pragma warning restore CA1822
///
/// Open a new .
@@ -182,9 +180,6 @@ namespace Ryujinx.Audio.Output
/// The input device name wanted by the user
/// The sample format to use
/// The user configuration
- /// The applet resource user id of the application
- /// The process handle of the application
- /// The volume level to request
/// A reporting an error or a success
public ResultCode OpenAudioOut(out string outputDeviceName,
out AudioOutputConfiguration outputConfiguration,
@@ -192,16 +187,13 @@ namespace Ryujinx.Audio.Output
IVirtualMemoryManager memoryManager,
string inputDeviceName,
SampleFormat sampleFormat,
- ref AudioInputConfiguration parameter,
- ulong appletResourceUserId,
- uint processHandle,
- float volume)
+ ref AudioInputConfiguration parameter)
{
int sessionId = AcquireSessionId();
_sessionsBufferEvents[sessionId].Clear();
- IHardwareDeviceSession deviceSession = _deviceDriver.OpenDeviceSession(IHardwareDeviceDriver.Direction.Output, memoryManager, sampleFormat, parameter.SampleRate, parameter.ChannelCount, volume);
+ IHardwareDeviceSession deviceSession = _deviceDriver.OpenDeviceSession(IHardwareDeviceDriver.Direction.Output, memoryManager, sampleFormat, parameter.SampleRate, parameter.ChannelCount);
AudioOutputSystem audioOut = new(this, _lock, deviceSession, _sessionsBufferEvents[sessionId]);
@@ -234,41 +226,6 @@ namespace Ryujinx.Audio.Output
return result;
}
- ///
- /// Sets the volume for all output devices.
- ///
- /// The volume to set.
- public void SetVolume(float volume)
- {
- if (_sessions != null)
- {
- foreach (AudioOutputSystem session in _sessions)
- {
- session?.SetVolume(volume);
- }
- }
- }
-
- ///
- /// Gets the volume for all output devices.
- ///
- /// A float indicating the volume level.
- public float GetVolume()
- {
- if (_sessions != null)
- {
- foreach (AudioOutputSystem session in _sessions)
- {
- if (session != null)
- {
- return session.GetVolume();
- }
- }
- }
-
- return 0.0f;
- }
-
public void Dispose()
{
GC.SuppressFinalize(this);
diff --git a/src/Ryujinx.Audio/Renderer/Common/BehaviourParameter.cs b/src/Ryujinx.Audio/Renderer/Common/BehaviourParameter.cs
index b0963c935..3b8d15dc5 100644
--- a/src/Ryujinx.Audio/Renderer/Common/BehaviourParameter.cs
+++ b/src/Ryujinx.Audio/Renderer/Common/BehaviourParameter.cs
@@ -25,7 +25,7 @@ namespace Ryujinx.Audio.Renderer.Common
public ulong Flags;
///
- /// Represents an error during .
+ /// Represents an error during .
///
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct ErrorInfo
diff --git a/src/Ryujinx.Audio/Renderer/Common/UpdateDataHeader.cs b/src/Ryujinx.Audio/Renderer/Common/UpdateDataHeader.cs
index 7efe3b02b..98b224ebf 100644
--- a/src/Ryujinx.Audio/Renderer/Common/UpdateDataHeader.cs
+++ b/src/Ryujinx.Audio/Renderer/Common/UpdateDataHeader.cs
@@ -4,7 +4,7 @@ using System.Runtime.CompilerServices;
namespace Ryujinx.Audio.Renderer.Common
{
///
- /// Update data header used for input and output of .
+ /// Update data header used for input and output of .
///
public struct UpdateDataHeader
{
diff --git a/src/Ryujinx.Audio/Renderer/Common/VoiceUpdateState.cs b/src/Ryujinx.Audio/Renderer/Common/VoiceUpdateState.cs
index 608381af1..7f881373f 100644
--- a/src/Ryujinx.Audio/Renderer/Common/VoiceUpdateState.cs
+++ b/src/Ryujinx.Audio/Renderer/Common/VoiceUpdateState.cs
@@ -15,7 +15,6 @@ namespace Ryujinx.Audio.Renderer.Common
{
public const int Align = 0x10;
public const int BiquadStateOffset = 0x0;
- public const int BiquadStateSize = 0x10;
///
/// The state of the biquad filters of this voice.
diff --git a/src/Ryujinx.Audio/Renderer/Dsp/AudioProcessor.cs b/src/Ryujinx.Audio/Renderer/Dsp/AudioProcessor.cs
index 9c885b2cf..3e11df056 100644
--- a/src/Ryujinx.Audio/Renderer/Dsp/AudioProcessor.cs
+++ b/src/Ryujinx.Audio/Renderer/Dsp/AudioProcessor.cs
@@ -45,7 +45,6 @@ namespace Ryujinx.Audio.Renderer.Dsp
_event = new ManualResetEvent(false);
}
-#pragma warning disable IDE0051 // Remove unused private member
private static uint GetHardwareChannelCount(IHardwareDeviceDriver deviceDriver)
{
// Get the real device driver (In case the compat layer is on top of it).
@@ -59,9 +58,8 @@ namespace Ryujinx.Audio.Renderer.Dsp
// NOTE: We default to stereo as this will get downmixed to mono by the compat layer if it's not compatible.
return 2;
}
-#pragma warning restore IDE0051
- public void Start(IHardwareDeviceDriver deviceDriver, float volume)
+ public void Start(IHardwareDeviceDriver deviceDriver)
{
OutputDevices = new IHardwareDevice[Constants.AudioRendererSessionCountMax];
@@ -70,7 +68,7 @@ namespace Ryujinx.Audio.Renderer.Dsp
for (int i = 0; i < OutputDevices.Length; i++)
{
// TODO: Don't hardcode sample rate.
- OutputDevices[i] = new HardwareDeviceImpl(deviceDriver, channelCount, Constants.TargetSampleRate, volume);
+ OutputDevices[i] = new HardwareDeviceImpl(deviceDriver, channelCount, Constants.TargetSampleRate);
}
_mailbox = new Mailbox();
@@ -231,33 +229,6 @@ namespace Ryujinx.Audio.Renderer.Dsp
_mailbox.SendResponse(MailboxMessage.Stop);
}
- public float GetVolume()
- {
- if (OutputDevices != null)
- {
- foreach (IHardwareDevice outputDevice in OutputDevices)
- {
- if (outputDevice != null)
- {
- return outputDevice.GetVolume();
- }
- }
- }
-
- return 0f;
- }
-
- public void SetVolume(float volume)
- {
- if (OutputDevices != null)
- {
- foreach (IHardwareDevice outputDevice in OutputDevices)
- {
- outputDevice?.SetVolume(volume);
- }
- }
- }
-
public void Dispose()
{
GC.SuppressFinalize(this);
@@ -269,6 +240,7 @@ namespace Ryujinx.Audio.Renderer.Dsp
if (disposing)
{
_event.Dispose();
+ _mailbox?.Dispose();
}
}
}
diff --git a/src/Ryujinx.Audio/Renderer/Dsp/BiquadFilterHelper.cs b/src/Ryujinx.Audio/Renderer/Dsp/BiquadFilterHelper.cs
index 1a51a1fbd..31f614d67 100644
--- a/src/Ryujinx.Audio/Renderer/Dsp/BiquadFilterHelper.cs
+++ b/src/Ryujinx.Audio/Renderer/Dsp/BiquadFilterHelper.cs
@@ -16,10 +16,15 @@ namespace Ryujinx.Audio.Renderer.Dsp
/// The biquad filter parameter
/// The biquad filter state
/// The output buffer to write the result
- /// The input buffer to write the result
+ /// The input buffer to read the samples from
/// The count of samples to process
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static void ProcessBiquadFilter(ref BiquadFilterParameter parameter, ref BiquadFilterState state, Span outputBuffer, ReadOnlySpan inputBuffer, uint sampleCount)
+ public static void ProcessBiquadFilter(
+ ref BiquadFilterParameter parameter,
+ ref BiquadFilterState state,
+ Span outputBuffer,
+ ReadOnlySpan inputBuffer,
+ uint sampleCount)
{
float a0 = FixedPointHelper.ToFloat(parameter.Numerator[0], FixedPointPrecisionForParameter);
float a1 = FixedPointHelper.ToFloat(parameter.Numerator[1], FixedPointPrecisionForParameter);
@@ -40,6 +45,96 @@ namespace Ryujinx.Audio.Renderer.Dsp
}
}
+ ///
+ /// Apply a single biquad filter and mix the result into the output buffer.
+ ///
+ /// This is implemented with a direct form 1.
+ /// The biquad filter parameter
+ /// The biquad filter state
+ /// The output buffer to write the result
+ /// The input buffer to read the samples from
+ /// The count of samples to process
+ /// Mix volume
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void ProcessBiquadFilterAndMix(
+ ref BiquadFilterParameter parameter,
+ ref BiquadFilterState state,
+ Span outputBuffer,
+ ReadOnlySpan inputBuffer,
+ uint sampleCount,
+ float volume)
+ {
+ float a0 = FixedPointHelper.ToFloat(parameter.Numerator[0], FixedPointPrecisionForParameter);
+ float a1 = FixedPointHelper.ToFloat(parameter.Numerator[1], FixedPointPrecisionForParameter);
+ float a2 = FixedPointHelper.ToFloat(parameter.Numerator[2], FixedPointPrecisionForParameter);
+
+ float b1 = FixedPointHelper.ToFloat(parameter.Denominator[0], FixedPointPrecisionForParameter);
+ float b2 = FixedPointHelper.ToFloat(parameter.Denominator[1], FixedPointPrecisionForParameter);
+
+ for (int i = 0; i < sampleCount; i++)
+ {
+ float input = inputBuffer[i];
+ float output = input * a0 + state.State0 * a1 + state.State1 * a2 + state.State2 * b1 + state.State3 * b2;
+
+ state.State1 = state.State0;
+ state.State0 = input;
+ state.State3 = state.State2;
+ state.State2 = output;
+
+ outputBuffer[i] += FloatingPointHelper.MultiplyRoundUp(output, volume);
+ }
+ }
+
+ ///
+ /// Apply a single biquad filter and mix the result into the output buffer with volume ramp.
+ ///
+ /// This is implemented with a direct form 1.
+ /// The biquad filter parameter
+ /// The biquad filter state
+ /// The output buffer to write the result
+ /// The input buffer to read the samples from
+ /// The count of samples to process
+ /// Initial mix volume
+ /// Volume increment step
+ /// Last filtered sample value
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static float ProcessBiquadFilterAndMixRamp(
+ ref BiquadFilterParameter parameter,
+ ref BiquadFilterState state,
+ Span outputBuffer,
+ ReadOnlySpan inputBuffer,
+ uint sampleCount,
+ float volume,
+ float ramp)
+ {
+ float a0 = FixedPointHelper.ToFloat(parameter.Numerator[0], FixedPointPrecisionForParameter);
+ float a1 = FixedPointHelper.ToFloat(parameter.Numerator[1], FixedPointPrecisionForParameter);
+ float a2 = FixedPointHelper.ToFloat(parameter.Numerator[2], FixedPointPrecisionForParameter);
+
+ float b1 = FixedPointHelper.ToFloat(parameter.Denominator[0], FixedPointPrecisionForParameter);
+ float b2 = FixedPointHelper.ToFloat(parameter.Denominator[1], FixedPointPrecisionForParameter);
+
+ float mixState = 0f;
+
+ for (int i = 0; i < sampleCount; i++)
+ {
+ float input = inputBuffer[i];
+ float output = input * a0 + state.State0 * a1 + state.State1 * a2 + state.State2 * b1 + state.State3 * b2;
+
+ state.State1 = state.State0;
+ state.State0 = input;
+ state.State3 = state.State2;
+ state.State2 = output;
+
+ mixState = FloatingPointHelper.MultiplyRoundUp(output, volume);
+
+ outputBuffer[i] += mixState;
+ volume += ramp;
+ }
+
+ return mixState;
+ }
+
///
/// Apply multiple biquad filter.
///
@@ -47,10 +142,15 @@ namespace Ryujinx.Audio.Renderer.Dsp
/// The biquad filter parameter
/// The biquad filter state
/// The output buffer to write the result
- /// The input buffer to write the result
+ /// The input buffer to read the samples from
/// The count of samples to process
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static void ProcessBiquadFilter(ReadOnlySpan parameters, Span states, Span outputBuffer, ReadOnlySpan inputBuffer, uint sampleCount)
+ public static void ProcessBiquadFilter(
+ ReadOnlySpan parameters,
+ Span states,
+ Span outputBuffer,
+ ReadOnlySpan inputBuffer,
+ uint sampleCount)
{
for (int stageIndex = 0; stageIndex < parameters.Length; stageIndex++)
{
@@ -67,7 +167,7 @@ namespace Ryujinx.Audio.Renderer.Dsp
for (int i = 0; i < sampleCount; i++)
{
- float input = inputBuffer[i];
+ float input = stageIndex != 0 ? outputBuffer[i] : inputBuffer[i];
float output = input * a0 + state.State0 * a1 + state.State1 * a2 + state.State2 * b1 + state.State3 * b2;
state.State1 = state.State0;
@@ -79,5 +179,129 @@ namespace Ryujinx.Audio.Renderer.Dsp
}
}
}
+
+ ///
+ /// Apply double biquad filter and mix the result into the output buffer.
+ ///
+ /// This is implemented with a direct form 1.
+ /// The biquad filter parameter
+ /// The biquad filter state
+ /// The output buffer to write the result
+ /// The input buffer to read the samples from
+ /// The count of samples to process
+ /// Mix volume
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void ProcessDoubleBiquadFilterAndMix(
+ ref BiquadFilterParameter parameter0,
+ ref BiquadFilterParameter parameter1,
+ ref BiquadFilterState state0,
+ ref BiquadFilterState state1,
+ Span outputBuffer,
+ ReadOnlySpan inputBuffer,
+ uint sampleCount,
+ float volume)
+ {
+ float a00 = FixedPointHelper.ToFloat(parameter0.Numerator[0], FixedPointPrecisionForParameter);
+ float a10 = FixedPointHelper.ToFloat(parameter0.Numerator[1], FixedPointPrecisionForParameter);
+ float a20 = FixedPointHelper.ToFloat(parameter0.Numerator[2], FixedPointPrecisionForParameter);
+
+ float b10 = FixedPointHelper.ToFloat(parameter0.Denominator[0], FixedPointPrecisionForParameter);
+ float b20 = FixedPointHelper.ToFloat(parameter0.Denominator[1], FixedPointPrecisionForParameter);
+
+ float a01 = FixedPointHelper.ToFloat(parameter1.Numerator[0], FixedPointPrecisionForParameter);
+ float a11 = FixedPointHelper.ToFloat(parameter1.Numerator[1], FixedPointPrecisionForParameter);
+ float a21 = FixedPointHelper.ToFloat(parameter1.Numerator[2], FixedPointPrecisionForParameter);
+
+ float b11 = FixedPointHelper.ToFloat(parameter1.Denominator[0], FixedPointPrecisionForParameter);
+ float b21 = FixedPointHelper.ToFloat(parameter1.Denominator[1], FixedPointPrecisionForParameter);
+
+ for (int i = 0; i < sampleCount; i++)
+ {
+ float input = inputBuffer[i];
+ float output = input * a00 + state0.State0 * a10 + state0.State1 * a20 + state0.State2 * b10 + state0.State3 * b20;
+
+ state0.State1 = state0.State0;
+ state0.State0 = input;
+ state0.State3 = state0.State2;
+ state0.State2 = output;
+
+ input = output;
+ output = input * a01 + state1.State0 * a11 + state1.State1 * a21 + state1.State2 * b11 + state1.State3 * b21;
+
+ state1.State1 = state1.State0;
+ state1.State0 = input;
+ state1.State3 = state1.State2;
+ state1.State2 = output;
+
+ outputBuffer[i] += FloatingPointHelper.MultiplyRoundUp(output, volume);
+ }
+ }
+
+ ///
+ /// Apply double biquad filter and mix the result into the output buffer with volume ramp.
+ ///
+ /// This is implemented with a direct form 1.
+ /// The biquad filter parameter
+ /// The biquad filter state
+ /// The output buffer to write the result
+ /// The input buffer to read the samples from
+ /// The count of samples to process
+ /// Initial mix volume
+ /// Volume increment step
+ /// Last filtered sample value
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static float ProcessDoubleBiquadFilterAndMixRamp(
+ ref BiquadFilterParameter parameter0,
+ ref BiquadFilterParameter parameter1,
+ ref BiquadFilterState state0,
+ ref BiquadFilterState state1,
+ Span outputBuffer,
+ ReadOnlySpan inputBuffer,
+ uint sampleCount,
+ float volume,
+ float ramp)
+ {
+ float a00 = FixedPointHelper.ToFloat(parameter0.Numerator[0], FixedPointPrecisionForParameter);
+ float a10 = FixedPointHelper.ToFloat(parameter0.Numerator[1], FixedPointPrecisionForParameter);
+ float a20 = FixedPointHelper.ToFloat(parameter0.Numerator[2], FixedPointPrecisionForParameter);
+
+ float b10 = FixedPointHelper.ToFloat(parameter0.Denominator[0], FixedPointPrecisionForParameter);
+ float b20 = FixedPointHelper.ToFloat(parameter0.Denominator[1], FixedPointPrecisionForParameter);
+
+ float a01 = FixedPointHelper.ToFloat(parameter1.Numerator[0], FixedPointPrecisionForParameter);
+ float a11 = FixedPointHelper.ToFloat(parameter1.Numerator[1], FixedPointPrecisionForParameter);
+ float a21 = FixedPointHelper.ToFloat(parameter1.Numerator[2], FixedPointPrecisionForParameter);
+
+ float b11 = FixedPointHelper.ToFloat(parameter1.Denominator[0], FixedPointPrecisionForParameter);
+ float b21 = FixedPointHelper.ToFloat(parameter1.Denominator[1], FixedPointPrecisionForParameter);
+
+ float mixState = 0f;
+
+ for (int i = 0; i < sampleCount; i++)
+ {
+ float input = inputBuffer[i];
+ float output = input * a00 + state0.State0 * a10 + state0.State1 * a20 + state0.State2 * b10 + state0.State3 * b20;
+
+ state0.State1 = state0.State0;
+ state0.State0 = input;
+ state0.State3 = state0.State2;
+ state0.State2 = output;
+
+ input = output;
+ output = input * a01 + state1.State0 * a11 + state1.State1 * a21 + state1.State2 * b11 + state1.State3 * b21;
+
+ state1.State1 = state1.State0;
+ state1.State0 = input;
+ state1.State3 = state1.State2;
+ state1.State2 = output;
+
+ mixState = FloatingPointHelper.MultiplyRoundUp(output, volume);
+
+ outputBuffer[i] += mixState;
+ volume += ramp;
+ }
+
+ return mixState;
+ }
}
}
diff --git a/src/Ryujinx.Audio/Renderer/Dsp/Command/BiquadFilterAndMixCommand.cs b/src/Ryujinx.Audio/Renderer/Dsp/Command/BiquadFilterAndMixCommand.cs
new file mode 100644
index 000000000..106fc0357
--- /dev/null
+++ b/src/Ryujinx.Audio/Renderer/Dsp/Command/BiquadFilterAndMixCommand.cs
@@ -0,0 +1,123 @@
+using Ryujinx.Audio.Renderer.Common;
+using Ryujinx.Audio.Renderer.Dsp.State;
+using Ryujinx.Audio.Renderer.Parameter;
+using System;
+
+namespace Ryujinx.Audio.Renderer.Dsp.Command
+{
+ public class BiquadFilterAndMixCommand : ICommand
+ {
+ public bool Enabled { get; set; }
+
+ public int NodeId { get; }
+
+ public CommandType CommandType => CommandType.BiquadFilterAndMix;
+
+ public uint EstimatedProcessingTime { get; set; }
+
+ public ushort InputBufferIndex { get; }
+ public ushort OutputBufferIndex { get; }
+
+ private BiquadFilterParameter _parameter;
+
+ public Memory BiquadFilterState { get; }
+ public Memory PreviousBiquadFilterState { get; }
+
+ public Memory State { get; }
+
+ public int LastSampleIndex { get; }
+
+ public float Volume0 { get; }
+ public float Volume1 { get; }
+
+ public bool NeedInitialization { get; }
+ public bool HasVolumeRamp { get; }
+ public bool IsFirstMixBuffer { get; }
+
+ public BiquadFilterAndMixCommand(
+ float volume0,
+ float volume1,
+ uint inputBufferIndex,
+ uint outputBufferIndex,
+ int lastSampleIndex,
+ Memory state,
+ ref BiquadFilterParameter filter,
+ Memory biquadFilterState,
+ Memory previousBiquadFilterState,
+ bool needInitialization,
+ bool hasVolumeRamp,
+ bool isFirstMixBuffer,
+ int nodeId)
+ {
+ Enabled = true;
+ NodeId = nodeId;
+
+ InputBufferIndex = (ushort)inputBufferIndex;
+ OutputBufferIndex = (ushort)outputBufferIndex;
+
+ _parameter = filter;
+ BiquadFilterState = biquadFilterState;
+ PreviousBiquadFilterState = previousBiquadFilterState;
+
+ State = state;
+ LastSampleIndex = lastSampleIndex;
+
+ Volume0 = volume0;
+ Volume1 = volume1;
+
+ NeedInitialization = needInitialization;
+ HasVolumeRamp = hasVolumeRamp;
+ IsFirstMixBuffer = isFirstMixBuffer;
+ }
+
+ public void Process(CommandList context)
+ {
+ ReadOnlySpan inputBuffer = context.GetBuffer(InputBufferIndex);
+ Span outputBuffer = context.GetBuffer(OutputBufferIndex);
+
+ if (NeedInitialization)
+ {
+ // If there is no previous state, initialize to zero.
+
+ BiquadFilterState.Span[0] = new BiquadFilterState();
+ }
+ else if (IsFirstMixBuffer)
+ {
+ // This is the first buffer, set previous state to current state.
+
+ PreviousBiquadFilterState.Span[0] = BiquadFilterState.Span[0];
+ }
+ else
+ {
+ // Rewind the current state by copying back the previous state.
+
+ BiquadFilterState.Span[0] = PreviousBiquadFilterState.Span[0];
+ }
+
+ if (HasVolumeRamp)
+ {
+ float volume = Volume0;
+ float ramp = (Volume1 - Volume0) / (int)context.SampleCount;
+
+ State.Span[0].LastSamples[LastSampleIndex] = BiquadFilterHelper.ProcessBiquadFilterAndMixRamp(
+ ref _parameter,
+ ref BiquadFilterState.Span[0],
+ outputBuffer,
+ inputBuffer,
+ context.SampleCount,
+ volume,
+ ramp);
+ }
+ else
+ {
+ BiquadFilterHelper.ProcessBiquadFilterAndMix(
+ ref _parameter,
+ ref BiquadFilterState.Span[0],
+ outputBuffer,
+ inputBuffer,
+ context.SampleCount,
+ Volume1);
+ }
+ }
+ }
+}
diff --git a/src/Ryujinx.Audio/Renderer/Dsp/Command/CommandType.cs b/src/Ryujinx.Audio/Renderer/Dsp/Command/CommandType.cs
index 098a04a04..de5c0ea2c 100644
--- a/src/Ryujinx.Audio/Renderer/Dsp/Command/CommandType.cs
+++ b/src/Ryujinx.Audio/Renderer/Dsp/Command/CommandType.cs
@@ -30,8 +30,10 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
CopyMixBuffer,
LimiterVersion1,
LimiterVersion2,
- GroupedBiquadFilter,
+ MultiTapBiquadFilter,
CaptureBuffer,
Compressor,
+ BiquadFilterAndMix,
+ MultiTapBiquadFilterAndMix,
}
}
diff --git a/src/Ryujinx.Audio/Renderer/Dsp/Command/CompressorCommand.cs b/src/Ryujinx.Audio/Renderer/Dsp/Command/CompressorCommand.cs
index 09f415d20..33f61e6a5 100644
--- a/src/Ryujinx.Audio/Renderer/Dsp/Command/CompressorCommand.cs
+++ b/src/Ryujinx.Audio/Renderer/Dsp/Command/CompressorCommand.cs
@@ -1,9 +1,11 @@
using Ryujinx.Audio.Renderer.Dsp.Effect;
using Ryujinx.Audio.Renderer.Dsp.State;
+using Ryujinx.Audio.Renderer.Parameter;
using Ryujinx.Audio.Renderer.Parameter.Effect;
using Ryujinx.Audio.Renderer.Server.Effect;
using System;
using System.Diagnostics;
+using System.Runtime.InteropServices;
namespace Ryujinx.Audio.Renderer.Dsp.Command
{
@@ -21,18 +23,20 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
public CompressorParameter Parameter => _parameter;
public Memory State { get; }
+ public Memory ResultState { get; }
public ushort[] OutputBufferIndices { get; }
public ushort[] InputBufferIndices { get; }
public bool IsEffectEnabled { get; }
private CompressorParameter _parameter;
- public CompressorCommand(uint bufferOffset, CompressorParameter parameter, Memory state, bool isEnabled, int nodeId)
+ public CompressorCommand(uint bufferOffset, CompressorParameter parameter, Memory state, Memory resultState, bool isEnabled, int nodeId)
{
Enabled = true;
NodeId = nodeId;
_parameter = parameter;
State = state;
+ ResultState = resultState;
IsEffectEnabled = isEnabled;
@@ -71,9 +75,16 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
if (IsEffectEnabled && _parameter.IsChannelCountValid())
{
- Span inputBuffers = stackalloc IntPtr[Parameter.ChannelCount];
- Span outputBuffers = stackalloc IntPtr[Parameter.ChannelCount];
- Span channelInput = stackalloc float[Parameter.ChannelCount];
+ if (!ResultState.IsEmpty && _parameter.StatisticsReset)
+ {
+ ref CompressorStatistics statistics = ref MemoryMarshal.Cast(ResultState.Span[0].SpecificData)[0];
+
+ statistics.Reset(_parameter.ChannelCount);
+ }
+
+ Span inputBuffers = stackalloc IntPtr[_parameter.ChannelCount];
+ Span outputBuffers = stackalloc IntPtr[_parameter.ChannelCount];
+ Span channelInput = stackalloc float[_parameter.ChannelCount];
ExponentialMovingAverage inputMovingAverage = state.InputMovingAverage;
float unknown4 = state.Unknown4;
ExponentialMovingAverage compressionGainAverage = state.CompressionGainAverage;
@@ -92,7 +103,8 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
channelInput[channelIndex] = *((float*)inputBuffers[channelIndex] + sampleIndex);
}
- float newMean = inputMovingAverage.Update(FloatingPointHelper.MeanSquare(channelInput), _parameter.InputGain);
+ float mean = FloatingPointHelper.MeanSquare(channelInput);
+ float newMean = inputMovingAverage.Update(mean, _parameter.InputGain);
float y = FloatingPointHelper.Log10(newMean) * 10.0f;
float z = 1.0f;
@@ -111,7 +123,7 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
if (y >= state.Unknown14)
{
- tmpGain = ((1.0f / Parameter.Ratio) - 1.0f) * (y - Parameter.Threshold);
+ tmpGain = ((1.0f / _parameter.Ratio) - 1.0f) * (y - _parameter.Threshold);
}
else
{
@@ -126,7 +138,7 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
if ((unknown4 - z) <= 0.08f)
{
- compressionEmaAlpha = Parameter.ReleaseCoefficient;
+ compressionEmaAlpha = _parameter.ReleaseCoefficient;
if ((unknown4 - z) >= -0.08f)
{
@@ -140,18 +152,31 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
}
else
{
- compressionEmaAlpha = Parameter.AttackCoefficient;
+ compressionEmaAlpha = _parameter.AttackCoefficient;
}
float compressionGain = compressionGainAverage.Update(z, compressionEmaAlpha);
- for (int channelIndex = 0; channelIndex < Parameter.ChannelCount; channelIndex++)
+ for (int channelIndex = 0; channelIndex < _parameter.ChannelCount; channelIndex++)
{
*((float*)outputBuffers[channelIndex] + sampleIndex) = channelInput[channelIndex] * compressionGain * state.OutputGain;
}
unknown4 = unknown4New;
previousCompressionEmaAlpha = compressionEmaAlpha;
+
+ if (!ResultState.IsEmpty)
+ {
+ ref CompressorStatistics statistics = ref MemoryMarshal.Cast(ResultState.Span[0].SpecificData)[0];
+
+ statistics.MinimumGain = MathF.Min(statistics.MinimumGain, compressionGain * state.OutputGain);
+ statistics.MaximumMean = MathF.Max(statistics.MaximumMean, mean);
+
+ for (int channelIndex = 0; channelIndex < _parameter.ChannelCount; channelIndex++)
+ {
+ statistics.LastSamples[channelIndex] = MathF.Abs(channelInput[channelIndex] * (1f / 32768f));
+ }
+ }
}
state.InputMovingAverage = inputMovingAverage;
@@ -161,7 +186,7 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
}
else
{
- for (int i = 0; i < Parameter.ChannelCount; i++)
+ for (int i = 0; i < _parameter.ChannelCount; i++)
{
if (InputBufferIndices[i] != OutputBufferIndices[i])
{
diff --git a/src/Ryujinx.Audio/Renderer/Dsp/Command/LimiterCommandVersion1.cs b/src/Ryujinx.Audio/Renderer/Dsp/Command/LimiterCommandVersion1.cs
index 3ba0b5884..06e932199 100644
--- a/src/Ryujinx.Audio/Renderer/Dsp/Command/LimiterCommandVersion1.cs
+++ b/src/Ryujinx.Audio/Renderer/Dsp/Command/LimiterCommandVersion1.cs
@@ -38,10 +38,10 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
InputBufferIndices = new ushort[Constants.VoiceChannelCountMax];
OutputBufferIndices = new ushort[Constants.VoiceChannelCountMax];
- for (int i = 0; i < Parameter.ChannelCount; i++)
+ for (int i = 0; i < _parameter.ChannelCount; i++)
{
- InputBufferIndices[i] = (ushort)(bufferOffset + Parameter.Input[i]);
- OutputBufferIndices[i] = (ushort)(bufferOffset + Parameter.Output[i]);
+ InputBufferIndices[i] = (ushort)(bufferOffset + _parameter.Input[i]);
+ OutputBufferIndices[i] = (ushort)(bufferOffset + _parameter.Output[i]);
}
}
@@ -51,11 +51,11 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
if (IsEffectEnabled)
{
- if (Parameter.Status == UsageState.Invalid)
+ if (_parameter.Status == UsageState.Invalid)
{
state = new LimiterState(ref _parameter, WorkBuffer);
}
- else if (Parameter.Status == UsageState.New)
+ else if (_parameter.Status == UsageState.New)
{
LimiterState.UpdateParameter(ref _parameter);
}
@@ -66,56 +66,56 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
private unsafe void ProcessLimiter(CommandList context, ref LimiterState state)
{
- Debug.Assert(Parameter.IsChannelCountValid());
+ Debug.Assert(_parameter.IsChannelCountValid());
- if (IsEffectEnabled && Parameter.IsChannelCountValid())
+ if (IsEffectEnabled && _parameter.IsChannelCountValid())
{
- Span inputBuffers = stackalloc IntPtr[Parameter.ChannelCount];
- Span outputBuffers = stackalloc IntPtr[Parameter.ChannelCount];
+ Span inputBuffers = stackalloc IntPtr[_parameter.ChannelCount];
+ Span outputBuffers = stackalloc IntPtr[_parameter.ChannelCount];
- for (int i = 0; i < Parameter.ChannelCount; i++)
+ for (int i = 0; i < _parameter.ChannelCount; i++)
{
inputBuffers[i] = context.GetBufferPointer(InputBufferIndices[i]);
outputBuffers[i] = context.GetBufferPointer(OutputBufferIndices[i]);
}
- for (int channelIndex = 0; channelIndex < Parameter.ChannelCount; channelIndex++)
+ for (int channelIndex = 0; channelIndex < _parameter.ChannelCount; channelIndex++)
{
for (int sampleIndex = 0; sampleIndex < context.SampleCount; sampleIndex++)
{
float rawInputSample = *((float*)inputBuffers[channelIndex] + sampleIndex);
- float inputSample = (rawInputSample / short.MaxValue) * Parameter.InputGain;
+ float inputSample = (rawInputSample / short.MaxValue) * _parameter.InputGain;
float sampleInputMax = Math.Abs(inputSample);
- float inputCoefficient = Parameter.ReleaseCoefficient;
+ float inputCoefficient = _parameter.ReleaseCoefficient;
if (sampleInputMax > state.DetectorAverage[channelIndex].Read())
{
- inputCoefficient = Parameter.AttackCoefficient;
+ inputCoefficient = _parameter.AttackCoefficient;
}
float detectorValue = state.DetectorAverage[channelIndex].Update(sampleInputMax, inputCoefficient);
float attenuation = 1.0f;
- if (detectorValue > Parameter.Threshold)
+ if (detectorValue > _parameter.Threshold)
{
- attenuation = Parameter.Threshold / detectorValue;
+ attenuation = _parameter.Threshold / detectorValue;
}
- float outputCoefficient = Parameter.ReleaseCoefficient;
+ float outputCoefficient = _parameter.ReleaseCoefficient;
if (state.CompressionGainAverage[channelIndex].Read() > attenuation)
{
- outputCoefficient = Parameter.AttackCoefficient;
+ outputCoefficient = _parameter.AttackCoefficient;
}
float compressionGain = state.CompressionGainAverage[channelIndex].Update(attenuation, outputCoefficient);
- ref float delayedSample = ref state.DelayedSampleBuffer[channelIndex * Parameter.DelayBufferSampleCountMax + state.DelayedSampleBufferPosition[channelIndex]];
+ ref float delayedSample = ref state.DelayedSampleBuffer[channelIndex * _parameter.DelayBufferSampleCountMax + state.DelayedSampleBufferPosition[channelIndex]];
- float outputSample = delayedSample * compressionGain * Parameter.OutputGain;
+ float outputSample = delayedSample * compressionGain * _parameter.OutputGain;
*((float*)outputBuffers[channelIndex] + sampleIndex) = outputSample * short.MaxValue;
@@ -123,16 +123,16 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
state.DelayedSampleBufferPosition[channelIndex]++;
- while (state.DelayedSampleBufferPosition[channelIndex] >= Parameter.DelayBufferSampleCountMin)
+ while (state.DelayedSampleBufferPosition[channelIndex] >= _parameter.DelayBufferSampleCountMin)
{
- state.DelayedSampleBufferPosition[channelIndex] -= Parameter.DelayBufferSampleCountMin;
+ state.DelayedSampleBufferPosition[channelIndex] -= _parameter.DelayBufferSampleCountMin;
}
}
}
}
else
{
- for (int i = 0; i < Parameter.ChannelCount; i++)
+ for (int i = 0; i < _parameter.ChannelCount; i++)
{
if (InputBufferIndices[i] != OutputBufferIndices[i])
{
diff --git a/src/Ryujinx.Audio/Renderer/Dsp/Command/LimiterCommandVersion2.cs b/src/Ryujinx.Audio/Renderer/Dsp/Command/LimiterCommandVersion2.cs
index f6e1654dd..ed0538c06 100644
--- a/src/Ryujinx.Audio/Renderer/Dsp/Command/LimiterCommandVersion2.cs
+++ b/src/Ryujinx.Audio/Renderer/Dsp/Command/LimiterCommandVersion2.cs
@@ -49,10 +49,10 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
InputBufferIndices = new ushort[Constants.VoiceChannelCountMax];
OutputBufferIndices = new ushort[Constants.VoiceChannelCountMax];
- for (int i = 0; i < Parameter.ChannelCount; i++)
+ for (int i = 0; i < _parameter.ChannelCount; i++)
{
- InputBufferIndices[i] = (ushort)(bufferOffset + Parameter.Input[i]);
- OutputBufferIndices[i] = (ushort)(bufferOffset + Parameter.Output[i]);
+ InputBufferIndices[i] = (ushort)(bufferOffset + _parameter.Input[i]);
+ OutputBufferIndices[i] = (ushort)(bufferOffset + _parameter.Output[i]);
}
}
@@ -62,11 +62,11 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
if (IsEffectEnabled)
{
- if (Parameter.Status == UsageState.Invalid)
+ if (_parameter.Status == UsageState.Invalid)
{
state = new LimiterState(ref _parameter, WorkBuffer);
}
- else if (Parameter.Status == UsageState.New)
+ else if (_parameter.Status == UsageState.New)
{
LimiterState.UpdateParameter(ref _parameter);
}
@@ -77,63 +77,63 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
private unsafe void ProcessLimiter(CommandList context, ref LimiterState state)
{
- Debug.Assert(Parameter.IsChannelCountValid());
+ Debug.Assert(_parameter.IsChannelCountValid());
- if (IsEffectEnabled && Parameter.IsChannelCountValid())
+ if (IsEffectEnabled && _parameter.IsChannelCountValid())
{
- if (!ResultState.IsEmpty && Parameter.StatisticsReset)
+ if (!ResultState.IsEmpty && _parameter.StatisticsReset)
{
ref LimiterStatistics statistics = ref MemoryMarshal.Cast(ResultState.Span[0].SpecificData)[0];
statistics.Reset();
}
- Span inputBuffers = stackalloc IntPtr[Parameter.ChannelCount];
- Span outputBuffers = stackalloc IntPtr[Parameter.ChannelCount];
+ Span inputBuffers = stackalloc IntPtr[_parameter.ChannelCount];
+ Span outputBuffers = stackalloc IntPtr[_parameter.ChannelCount];
- for (int i = 0; i < Parameter.ChannelCount; i++)
+ for (int i = 0; i < _parameter.ChannelCount; i++)
{
inputBuffers[i] = context.GetBufferPointer(InputBufferIndices[i]);
outputBuffers[i] = context.GetBufferPointer(OutputBufferIndices[i]);
}
- for (int channelIndex = 0; channelIndex < Parameter.ChannelCount; channelIndex++)
+ for (int channelIndex = 0; channelIndex < _parameter.ChannelCount; channelIndex++)
{
for (int sampleIndex = 0; sampleIndex < context.SampleCount; sampleIndex++)
{
float rawInputSample = *((float*)inputBuffers[channelIndex] + sampleIndex);
- float inputSample = (rawInputSample / short.MaxValue) * Parameter.InputGain;
+ float inputSample = (rawInputSample / short.MaxValue) * _parameter.InputGain;
float sampleInputMax = Math.Abs(inputSample);
- float inputCoefficient = Parameter.ReleaseCoefficient;
+ float inputCoefficient = _parameter.ReleaseCoefficient;
if (sampleInputMax > state.DetectorAverage[channelIndex].Read())
{
- inputCoefficient = Parameter.AttackCoefficient;
+ inputCoefficient = _parameter.AttackCoefficient;
}
float detectorValue = state.DetectorAverage[channelIndex].Update(sampleInputMax, inputCoefficient);
float attenuation = 1.0f;
- if (detectorValue > Parameter.Threshold)
+ if (detectorValue > _parameter.Threshold)
{
- attenuation = Parameter.Threshold / detectorValue;
+ attenuation = _parameter.Threshold / detectorValue;
}
- float outputCoefficient = Parameter.ReleaseCoefficient;
+ float outputCoefficient = _parameter.ReleaseCoefficient;
if (state.CompressionGainAverage[channelIndex].Read() > attenuation)
{
- outputCoefficient = Parameter.AttackCoefficient;
+ outputCoefficient = _parameter.AttackCoefficient;
}
float compressionGain = state.CompressionGainAverage[channelIndex].Update(attenuation, outputCoefficient);
- ref float delayedSample = ref state.DelayedSampleBuffer[channelIndex * Parameter.DelayBufferSampleCountMax + state.DelayedSampleBufferPosition[channelIndex]];
+ ref float delayedSample = ref state.DelayedSampleBuffer[channelIndex * _parameter.DelayBufferSampleCountMax + state.DelayedSampleBufferPosition[channelIndex]];
- float outputSample = delayedSample * compressionGain * Parameter.OutputGain;
+ float outputSample = delayedSample * compressionGain * _parameter.OutputGain;
*((float*)outputBuffers[channelIndex] + sampleIndex) = outputSample * short.MaxValue;
@@ -141,9 +141,9 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
state.DelayedSampleBufferPosition[channelIndex]++;
- while (state.DelayedSampleBufferPosition[channelIndex] >= Parameter.DelayBufferSampleCountMin)
+ while (state.DelayedSampleBufferPosition[channelIndex] >= _parameter.DelayBufferSampleCountMin)
{
- state.DelayedSampleBufferPosition[channelIndex] -= Parameter.DelayBufferSampleCountMin;
+ state.DelayedSampleBufferPosition[channelIndex] -= _parameter.DelayBufferSampleCountMin;
}
if (!ResultState.IsEmpty)
@@ -158,7 +158,7 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
}
else
{
- for (int i = 0; i < Parameter.ChannelCount; i++)
+ for (int i = 0; i < _parameter.ChannelCount; i++)
{
if (InputBufferIndices[i] != OutputBufferIndices[i])
{
diff --git a/src/Ryujinx.Audio/Renderer/Dsp/Command/MixRampGroupedCommand.cs b/src/Ryujinx.Audio/Renderer/Dsp/Command/MixRampGroupedCommand.cs
index 3c7dd63b2..41ac84c1a 100644
--- a/src/Ryujinx.Audio/Renderer/Dsp/Command/MixRampGroupedCommand.cs
+++ b/src/Ryujinx.Audio/Renderer/Dsp/Command/MixRampGroupedCommand.cs
@@ -24,7 +24,14 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
public Memory State { get; }
- public MixRampGroupedCommand(uint mixBufferCount, uint inputBufferIndex, uint outputBufferIndex, Span volume0, Span volume1, Memory state, int nodeId)
+ public MixRampGroupedCommand(
+ uint mixBufferCount,
+ uint inputBufferIndex,
+ uint outputBufferIndex,
+ ReadOnlySpan volume0,
+ ReadOnlySpan volume1,
+ Memory state,
+ int nodeId)
{
Enabled = true;
MixBufferCount = mixBufferCount;
@@ -48,7 +55,12 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static float ProcessMixRampGrouped(Span outputBuffer, ReadOnlySpan inputBuffer, float volume0, float volume1, int sampleCount)
+ private static float ProcessMixRampGrouped(
+ Span outputBuffer,
+ ReadOnlySpan inputBuffer,
+ float volume0,
+ float volume1,
+ int sampleCount)
{
float ramp = (volume1 - volume0) / sampleCount;
float volume = volume0;
diff --git a/src/Ryujinx.Audio/Renderer/Dsp/Command/MultiTapBiquadFilterAndMixCommand.cs b/src/Ryujinx.Audio/Renderer/Dsp/Command/MultiTapBiquadFilterAndMixCommand.cs
new file mode 100644
index 000000000..e359371b4
--- /dev/null
+++ b/src/Ryujinx.Audio/Renderer/Dsp/Command/MultiTapBiquadFilterAndMixCommand.cs
@@ -0,0 +1,145 @@
+using Ryujinx.Audio.Renderer.Common;
+using Ryujinx.Audio.Renderer.Dsp.State;
+using Ryujinx.Audio.Renderer.Parameter;
+using System;
+
+namespace Ryujinx.Audio.Renderer.Dsp.Command
+{
+ public class MultiTapBiquadFilterAndMixCommand : ICommand
+ {
+ public bool Enabled { get; set; }
+
+ public int NodeId { get; }
+
+ public CommandType CommandType => CommandType.MultiTapBiquadFilterAndMix;
+
+ public uint EstimatedProcessingTime { get; set; }
+
+ public ushort InputBufferIndex { get; }
+ public ushort OutputBufferIndex { get; }
+
+ private BiquadFilterParameter _parameter0;
+ private BiquadFilterParameter _parameter1;
+
+ public Memory BiquadFilterState0 { get; }
+ public Memory BiquadFilterState1 { get; }
+ public Memory PreviousBiquadFilterState0 { get; }
+ public Memory PreviousBiquadFilterState1 { get; }
+
+ public Memory State { get; }
+
+ public int LastSampleIndex { get; }
+
+ public float Volume0 { get; }
+ public float Volume1 { get; }
+
+ public bool NeedInitialization0 { get; }
+ public bool NeedInitialization1 { get; }
+ public bool HasVolumeRamp { get; }
+ public bool IsFirstMixBuffer { get; }
+
+ public MultiTapBiquadFilterAndMixCommand(
+ float volume0,
+ float volume1,
+ uint inputBufferIndex,
+ uint outputBufferIndex,
+ int lastSampleIndex,
+ Memory state,
+ ref BiquadFilterParameter filter0,
+ ref BiquadFilterParameter filter1,
+ Memory biquadFilterState0,
+ Memory biquadFilterState1,
+ Memory previousBiquadFilterState0,
+ Memory previousBiquadFilterState1,
+ bool needInitialization0,
+ bool needInitialization1,
+ bool hasVolumeRamp,
+ bool isFirstMixBuffer,
+ int nodeId)
+ {
+ Enabled = true;
+ NodeId = nodeId;
+
+ InputBufferIndex = (ushort)inputBufferIndex;
+ OutputBufferIndex = (ushort)outputBufferIndex;
+
+ _parameter0 = filter0;
+ _parameter1 = filter1;
+ BiquadFilterState0 = biquadFilterState0;
+ BiquadFilterState1 = biquadFilterState1;
+ PreviousBiquadFilterState0 = previousBiquadFilterState0;
+ PreviousBiquadFilterState1 = previousBiquadFilterState1;
+
+ State = state;
+ LastSampleIndex = lastSampleIndex;
+
+ Volume0 = volume0;
+ Volume1 = volume1;
+
+ NeedInitialization0 = needInitialization0;
+ NeedInitialization1 = needInitialization1;
+ HasVolumeRamp = hasVolumeRamp;
+ IsFirstMixBuffer = isFirstMixBuffer;
+ }
+
+ private void UpdateState(Memory state, Memory previousState, bool needInitialization)
+ {
+ if (needInitialization)
+ {
+ // If there is no previous state, initialize to zero.
+
+ state.Span[0] = new BiquadFilterState();
+ }
+ else if (IsFirstMixBuffer)
+ {
+ // This is the first buffer, set previous state to current state.
+
+ previousState.Span[0] = state.Span[0];
+ }
+ else
+ {
+ // Rewind the current state by copying back the previous state.
+
+ state.Span[0] = previousState.Span[0];
+ }
+ }
+
+ public void Process(CommandList context)
+ {
+ ReadOnlySpan inputBuffer = context.GetBuffer(InputBufferIndex);
+ Span outputBuffer = context.GetBuffer(OutputBufferIndex);
+
+ UpdateState(BiquadFilterState0, PreviousBiquadFilterState0, NeedInitialization0);
+ UpdateState(BiquadFilterState1, PreviousBiquadFilterState1, NeedInitialization1);
+
+ if (HasVolumeRamp)
+ {
+ float volume = Volume0;
+ float ramp = (Volume1 - Volume0) / (int)context.SampleCount;
+
+ State.Span[0].LastSamples[LastSampleIndex] = BiquadFilterHelper.ProcessDoubleBiquadFilterAndMixRamp(
+ ref _parameter0,
+ ref _parameter1,
+ ref BiquadFilterState0.Span[0],
+ ref BiquadFilterState1.Span[0],
+ outputBuffer,
+ inputBuffer,
+ context.SampleCount,
+ volume,
+ ramp);
+ }
+ else
+ {
+ BiquadFilterHelper.ProcessDoubleBiquadFilterAndMix(
+ ref _parameter0,
+ ref _parameter1,
+ ref BiquadFilterState0.Span[0],
+ ref BiquadFilterState1.Span[0],
+ outputBuffer,
+ inputBuffer,
+ context.SampleCount,
+ Volume1);
+ }
+ }
+ }
+}
diff --git a/src/Ryujinx.Audio/Renderer/Dsp/Command/GroupedBiquadFilterCommand.cs b/src/Ryujinx.Audio/Renderer/Dsp/Command/MultiTapBiquadFilterCommand.cs
similarity index 84%
rename from src/Ryujinx.Audio/Renderer/Dsp/Command/GroupedBiquadFilterCommand.cs
rename to src/Ryujinx.Audio/Renderer/Dsp/Command/MultiTapBiquadFilterCommand.cs
index 7af851bdc..e159f8ef7 100644
--- a/src/Ryujinx.Audio/Renderer/Dsp/Command/GroupedBiquadFilterCommand.cs
+++ b/src/Ryujinx.Audio/Renderer/Dsp/Command/MultiTapBiquadFilterCommand.cs
@@ -4,13 +4,13 @@ using System;
namespace Ryujinx.Audio.Renderer.Dsp.Command
{
- public class GroupedBiquadFilterCommand : ICommand
+ public class MultiTapBiquadFilterCommand : ICommand
{
public bool Enabled { get; set; }
public int NodeId { get; }
- public CommandType CommandType => CommandType.GroupedBiquadFilter;
+ public CommandType CommandType => CommandType.MultiTapBiquadFilter;
public uint EstimatedProcessingTime { get; set; }
@@ -20,7 +20,7 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
private readonly int _outputBufferIndex;
private readonly bool[] _isInitialized;
- public GroupedBiquadFilterCommand(int baseIndex, ReadOnlySpan filters, Memory biquadFilterStateMemory, int inputBufferOffset, int outputBufferOffset, ReadOnlySpan isInitialized, int nodeId)
+ public MultiTapBiquadFilterCommand(int baseIndex, ReadOnlySpan filters, Memory biquadFilterStateMemory, int inputBufferOffset, int outputBufferOffset, ReadOnlySpan isInitialized, int nodeId)
{
_parameters = filters.ToArray();
_biquadFilterStates = biquadFilterStateMemory;
diff --git a/src/Ryujinx.Audio/Renderer/Dsp/State/BiquadFilterState.cs b/src/Ryujinx.Audio/Renderer/Dsp/State/BiquadFilterState.cs
index f9a32b3f9..58a2d9cce 100644
--- a/src/Ryujinx.Audio/Renderer/Dsp/State/BiquadFilterState.cs
+++ b/src/Ryujinx.Audio/Renderer/Dsp/State/BiquadFilterState.cs
@@ -2,12 +2,16 @@ using System.Runtime.InteropServices;
namespace Ryujinx.Audio.Renderer.Dsp.State
{
- [StructLayout(LayoutKind.Sequential, Pack = 1, Size = 0x10)]
+ [StructLayout(LayoutKind.Sequential, Pack = 1, Size = 0x20)]
public struct BiquadFilterState
{
public float State0;
public float State1;
public float State2;
public float State3;
+ public float State4;
+ public float State5;
+ public float State6;
+ public float State7;
}
}
diff --git a/src/Ryujinx.Audio/Renderer/Parameter/BehaviourErrorInfoOutStatus.cs b/src/Ryujinx.Audio/Renderer/Parameter/BehaviourErrorInfoOutStatus.cs
index 5a0565dc6..72438be0e 100644
--- a/src/Ryujinx.Audio/Renderer/Parameter/BehaviourErrorInfoOutStatus.cs
+++ b/src/Ryujinx.Audio/Renderer/Parameter/BehaviourErrorInfoOutStatus.cs
@@ -8,7 +8,7 @@ namespace Ryujinx.Audio.Renderer.Parameter
///
/// Output information for behaviour.
///
- /// This is used to report errors to the user during processing.
+ /// This is used to report errors to the user during processing.
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct BehaviourErrorInfoOutStatus
{
diff --git a/src/Ryujinx.Audio/Renderer/Parameter/Effect/CompressorParameter.cs b/src/Ryujinx.Audio/Renderer/Parameter/Effect/CompressorParameter.cs
index b403f1370..c00118e49 100644
--- a/src/Ryujinx.Audio/Renderer/Parameter/Effect/CompressorParameter.cs
+++ b/src/Ryujinx.Audio/Renderer/Parameter/Effect/CompressorParameter.cs
@@ -90,9 +90,16 @@ namespace Ryujinx.Audio.Renderer.Parameter.Effect
public bool MakeupGainEnabled;
///
- /// Reserved/padding.
+ /// Indicate if the compressor effect should output statistics.
///
- private Array2 _reserved;
+ [MarshalAs(UnmanagedType.I1)]
+ public bool StatisticsEnabled;
+
+ ///
+ /// Indicate to the DSP that the user did a statistics reset.
+ ///
+ [MarshalAs(UnmanagedType.I1)]
+ public bool StatisticsReset;
///
/// Check if the is valid.
diff --git a/src/Ryujinx.Audio/Renderer/Parameter/Effect/CompressorStatistics.cs b/src/Ryujinx.Audio/Renderer/Parameter/Effect/CompressorStatistics.cs
new file mode 100644
index 000000000..65335e2d9
--- /dev/null
+++ b/src/Ryujinx.Audio/Renderer/Parameter/Effect/CompressorStatistics.cs
@@ -0,0 +1,38 @@
+using Ryujinx.Common.Memory;
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Audio.Renderer.Parameter.Effect
+{
+ ///
+ /// Effect result state for .
+ ///
+ [StructLayout(LayoutKind.Sequential, Pack = 1)]
+ public struct CompressorStatistics
+ {
+ ///
+ /// Maximum input mean value since last reset.
+ ///
+ public float MaximumMean;
+
+ ///
+ /// Minimum output gain since last reset.
+ ///
+ public float MinimumGain;
+
+ ///
+ /// Last processed input sample, per channel.
+ ///
+ public Array6 LastSamples;
+
+ ///
+ /// Reset the statistics.
+ ///
+ /// Number of channels to reset.
+ public void Reset(ushort channelCount)
+ {
+ MaximumMean = 0.0f;
+ MinimumGain = 1.0f;
+ LastSamples.AsSpan()[..channelCount].Clear();
+ }
+ }
+}
diff --git a/src/Ryujinx.Audio/Renderer/Parameter/ISplitterDestinationInParameter.cs b/src/Ryujinx.Audio/Renderer/Parameter/ISplitterDestinationInParameter.cs
new file mode 100644
index 000000000..7ee49f11a
--- /dev/null
+++ b/src/Ryujinx.Audio/Renderer/Parameter/ISplitterDestinationInParameter.cs
@@ -0,0 +1,48 @@
+using Ryujinx.Common.Memory;
+using System;
+
+namespace Ryujinx.Audio.Renderer.Parameter
+{
+ ///
+ /// Generic interface for the splitter destination parameters.
+ ///
+ public interface ISplitterDestinationInParameter
+ {
+ ///
+ /// Target splitter destination data id.
+ ///
+ int Id { get; }
+
+ ///
+ /// The mix to output the result of the splitter.
+ ///
+ int DestinationId { get; }
+
+ ///
+ /// Biquad filter parameters.
+ ///
+ Array2 BiquadFilters { get; }
+
+ ///
+ /// Set to true if in use.
+ ///
+ bool IsUsed { get; }
+
+ ///
+ /// Set to true to force resetting the previous mix volumes.
+ ///
+ bool ResetPrevVolume { get; }
+
+ ///
+ /// Mix buffer volumes.
+ ///
+ /// Used when a splitter id is specified in the mix.
+ Span MixBufferVolume { get; }
+
+ ///
+ /// Check if the magic is valid.
+ ///
+ /// Returns true if the magic is valid.
+ bool IsMagicValid();
+ }
+}
diff --git a/src/Ryujinx.Audio/Renderer/Parameter/SplitterDestinationInParameter.cs b/src/Ryujinx.Audio/Renderer/Parameter/SplitterDestinationInParameterVersion1.cs
similarity index 63%
rename from src/Ryujinx.Audio/Renderer/Parameter/SplitterDestinationInParameter.cs
rename to src/Ryujinx.Audio/Renderer/Parameter/SplitterDestinationInParameterVersion1.cs
index b74b67be0..f346efcb0 100644
--- a/src/Ryujinx.Audio/Renderer/Parameter/SplitterDestinationInParameter.cs
+++ b/src/Ryujinx.Audio/Renderer/Parameter/SplitterDestinationInParameterVersion1.cs
@@ -1,3 +1,4 @@
+using Ryujinx.Common.Memory;
using Ryujinx.Common.Utilities;
using System;
using System.Runtime.InteropServices;
@@ -5,10 +6,10 @@ using System.Runtime.InteropServices;
namespace Ryujinx.Audio.Renderer.Parameter
{
///
- /// Input header for a splitter destination update.
+ /// Input header for a splitter destination version 1 update.
///
[StructLayout(LayoutKind.Sequential, Pack = 1)]
- public struct SplitterDestinationInParameter
+ public struct SplitterDestinationInParameterVersion1 : ISplitterDestinationInParameter
{
///
/// Magic of the input header.
@@ -36,12 +37,18 @@ namespace Ryujinx.Audio.Renderer.Parameter
[MarshalAs(UnmanagedType.I1)]
public bool IsUsed;
+ ///
+ /// Set to true to force resetting the previous mix volumes.
+ ///
+ [MarshalAs(UnmanagedType.I1)]
+ public bool ResetPrevVolume;
+
///
/// Reserved/padding.
///
- private unsafe fixed byte _reserved[3];
+ private unsafe fixed byte _reserved[2];
- [StructLayout(LayoutKind.Sequential, Size = 4 * Constants.MixBufferCountMax, Pack = 1)]
+ [StructLayout(LayoutKind.Sequential, Size = sizeof(float) * Constants.MixBufferCountMax, Pack = 1)]
private struct MixArray { }
///
@@ -50,6 +57,15 @@ namespace Ryujinx.Audio.Renderer.Parameter
/// Used when a splitter id is specified in the mix.
public Span MixBufferVolume => SpanHelpers.AsSpan(ref _mixBufferVolume);
+ readonly int ISplitterDestinationInParameter.Id => Id;
+
+ readonly int ISplitterDestinationInParameter.DestinationId => DestinationId;
+
+ readonly Array2 ISplitterDestinationInParameter.BiquadFilters => default;
+
+ readonly bool ISplitterDestinationInParameter.IsUsed => IsUsed;
+ readonly bool ISplitterDestinationInParameter.ResetPrevVolume => ResetPrevVolume;
+
///
/// The expected constant of any input header.
///
diff --git a/src/Ryujinx.Audio/Renderer/Parameter/SplitterDestinationInParameterVersion2.cs b/src/Ryujinx.Audio/Renderer/Parameter/SplitterDestinationInParameterVersion2.cs
new file mode 100644
index 000000000..1d867919d
--- /dev/null
+++ b/src/Ryujinx.Audio/Renderer/Parameter/SplitterDestinationInParameterVersion2.cs
@@ -0,0 +1,88 @@
+using Ryujinx.Common.Memory;
+using Ryujinx.Common.Utilities;
+using System;
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Audio.Renderer.Parameter
+{
+ ///
+ /// Input header for a splitter destination version 2 update.
+ ///
+ [StructLayout(LayoutKind.Sequential, Pack = 1)]
+ public struct SplitterDestinationInParameterVersion2 : ISplitterDestinationInParameter
+ {
+ ///
+ /// Magic of the input header.
+ ///
+ public uint Magic;
+
+ ///
+ /// Target splitter destination data id.
+ ///
+ public int Id;
+
+ ///
+ /// Mix buffer volumes storage.
+ ///
+ private MixArray _mixBufferVolume;
+
+ ///
+ /// The mix to output the result of the splitter.
+ ///
+ public int DestinationId;
+
+ ///
+ /// Biquad filter parameters.
+ ///
+ public Array2 BiquadFilters;
+
+ ///
+ /// Set to true if in use.
+ ///
+ [MarshalAs(UnmanagedType.I1)]
+ public bool IsUsed;
+
+ ///
+ /// Set to true to force resetting the previous mix volumes.
+ ///
+ [MarshalAs(UnmanagedType.I1)]
+ public bool ResetPrevVolume;
+
+ ///
+ /// Reserved/padding.
+ ///
+ private unsafe fixed byte _reserved[10];
+
+ [StructLayout(LayoutKind.Sequential, Size = sizeof(float) * Constants.MixBufferCountMax, Pack = 1)]
+ private struct MixArray { }
+
+ ///
+ /// Mix buffer volumes.
+ ///
+ /// Used when a splitter id is specified in the mix.
+ public Span MixBufferVolume => SpanHelpers.AsSpan(ref _mixBufferVolume);
+
+ readonly int ISplitterDestinationInParameter.Id => Id;
+
+ readonly int ISplitterDestinationInParameter.DestinationId => DestinationId;
+
+ readonly Array2 ISplitterDestinationInParameter.BiquadFilters => BiquadFilters;
+
+ readonly bool ISplitterDestinationInParameter.IsUsed => IsUsed;
+ readonly bool ISplitterDestinationInParameter.ResetPrevVolume => ResetPrevVolume;
+
+ ///
+ /// The expected constant of any input header.
+ ///
+ private const uint ValidMagic = 0x44444E53;
+
+ ///
+ /// Check if the magic is valid.
+ ///
+ /// Returns true if the magic is valid.
+ public readonly bool IsMagicValid()
+ {
+ return Magic == ValidMagic;
+ }
+ }
+}
diff --git a/src/Ryujinx.Audio/Renderer/Server/AudioRenderSystem.cs b/src/Ryujinx.Audio/Renderer/Server/AudioRenderSystem.cs
index 7bb8ae5ba..246889c48 100644
--- a/src/Ryujinx.Audio/Renderer/Server/AudioRenderSystem.cs
+++ b/src/Ryujinx.Audio/Renderer/Server/AudioRenderSystem.cs
@@ -1,6 +1,7 @@
using Ryujinx.Audio.Integration;
using Ryujinx.Audio.Renderer.Common;
using Ryujinx.Audio.Renderer.Dsp.Command;
+using Ryujinx.Audio.Renderer.Dsp.State;
using Ryujinx.Audio.Renderer.Parameter;
using Ryujinx.Audio.Renderer.Server.Effect;
using Ryujinx.Audio.Renderer.Server.MemoryPool;
@@ -173,6 +174,22 @@ namespace Ryujinx.Audio.Renderer.Server
return ResultCode.WorkBufferTooSmall;
}
+ Memory splitterBqfStates = Memory.Empty;
+
+ if (_behaviourContext.IsBiquadFilterParameterForSplitterEnabled() &&
+ parameter.SplitterCount > 0 &&
+ parameter.SplitterDestinationCount > 0)
+ {
+ splitterBqfStates = workBufferAllocator.Allocate(parameter.SplitterDestinationCount * SplitterContext.BqfStatesPerDestination, 0x10);
+
+ if (splitterBqfStates.IsEmpty)
+ {
+ return ResultCode.WorkBufferTooSmall;
+ }
+
+ splitterBqfStates.Span.Clear();
+ }
+
// Invalidate DSP cache on what was currently allocated with workBuffer.
AudioProcessorMemoryManager.InvalidateDspCache(_dspMemoryPoolState.Translate(workBuffer, workBufferAllocator.Offset), workBufferAllocator.Offset);
@@ -292,7 +309,7 @@ namespace Ryujinx.Audio.Renderer.Server
state = MemoryPoolState.Create(MemoryPoolState.LocationType.Cpu);
}
- if (!_splitterContext.Initialize(ref _behaviourContext, ref parameter, workBufferAllocator))
+ if (!_splitterContext.Initialize(ref _behaviourContext, ref parameter, workBufferAllocator, splitterBqfStates))
{
return ResultCode.WorkBufferTooSmall;
}
@@ -386,7 +403,7 @@ namespace Ryujinx.Audio.Renderer.Server
}
}
- public ResultCode Update(Memory output, Memory performanceOutput, ReadOnlyMemory input)
+ public ResultCode Update(Memory output, Memory performanceOutput, ReadOnlySequence input)
{
lock (_lock)
{
@@ -419,14 +436,16 @@ namespace Ryujinx.Audio.Renderer.Server
return result;
}
- result = stateUpdater.UpdateVoices(_voiceContext, _memoryPools);
+ PoolMapper poolMapper = new PoolMapper(_processHandle, _memoryPools, _behaviourContext.IsMemoryPoolForceMappingEnabled());
+
+ result = stateUpdater.UpdateVoices(_voiceContext, poolMapper);
if (result != ResultCode.Success)
{
return result;
}
- result = stateUpdater.UpdateEffects(_effectContext, _isActive, _memoryPools);
+ result = stateUpdater.UpdateEffects(_effectContext, _isActive, poolMapper);
if (result != ResultCode.Success)
{
@@ -450,7 +469,7 @@ namespace Ryujinx.Audio.Renderer.Server
return result;
}
- result = stateUpdater.UpdateSinks(_sinkContext, _memoryPools);
+ result = stateUpdater.UpdateSinks(_sinkContext, poolMapper);
if (result != ResultCode.Success)
{
@@ -773,6 +792,13 @@ namespace Ryujinx.Audio.Renderer.Server
// Splitter
size = SplitterContext.GetWorkBufferSize(size, ref behaviourContext, ref parameter);
+ if (behaviourContext.IsBiquadFilterParameterForSplitterEnabled() &&
+ parameter.SplitterCount > 0 &&
+ parameter.SplitterDestinationCount > 0)
+ {
+ size = WorkBufferAllocator.GetTargetSize(size, parameter.SplitterDestinationCount * SplitterContext.BqfStatesPerDestination, 0x10);
+ }
+
// DSP Voice
size = WorkBufferAllocator.GetTargetSize(size, parameter.VoiceCount, VoiceUpdateState.Align);
diff --git a/src/Ryujinx.Audio/Renderer/Server/AudioRendererManager.cs b/src/Ryujinx.Audio/Renderer/Server/AudioRendererManager.cs
index 0dbbd26c8..e334a89f6 100644
--- a/src/Ryujinx.Audio/Renderer/Server/AudioRendererManager.cs
+++ b/src/Ryujinx.Audio/Renderer/Server/AudioRendererManager.cs
@@ -177,12 +177,12 @@ namespace Ryujinx.Audio.Renderer.Server
///
/// Start the and worker thread.
///
- private void StartLocked(float volume)
+ private void StartLocked()
{
_isRunning = true;
// TODO: virtual device mapping (IAudioDevice)
- Processor.Start(_deviceDriver, volume);
+ Processor.Start(_deviceDriver);
_workerThread = new Thread(SendCommands)
{
@@ -254,7 +254,7 @@ namespace Ryujinx.Audio.Renderer.Server
/// Register a new .
///
/// The to register.
- private void Register(AudioRenderSystem renderer, float volume)
+ private void Register(AudioRenderSystem renderer)
{
lock (_sessionLock)
{
@@ -265,7 +265,7 @@ namespace Ryujinx.Audio.Renderer.Server
{
if (!_isRunning)
{
- StartLocked(volume);
+ StartLocked();
}
}
}
@@ -312,8 +312,7 @@ namespace Ryujinx.Audio.Renderer.Server
ulong appletResourceUserId,
ulong workBufferAddress,
ulong workBufferSize,
- uint processHandle,
- float volume)
+ uint processHandle)
{
int sessionId = AcquireSessionId();
@@ -338,7 +337,7 @@ namespace Ryujinx.Audio.Renderer.Server
{
renderer = audioRenderer;
- Register(renderer, volume);
+ Register(renderer);
}
else
{
@@ -350,21 +349,6 @@ namespace Ryujinx.Audio.Renderer.Server
return result;
}
- public float GetVolume()
- {
- if (Processor != null)
- {
- return Processor.GetVolume();
- }
-
- return 0f;
- }
-
- public void SetVolume(float volume)
- {
- Processor?.SetVolume(volume);
- }
-
public void Dispose()
{
GC.SuppressFinalize(this);
diff --git a/src/Ryujinx.Audio/Renderer/Server/BehaviourContext.cs b/src/Ryujinx.Audio/Renderer/Server/BehaviourContext.cs
index 3297b5d9f..f725eb9f3 100644
--- a/src/Ryujinx.Audio/Renderer/Server/BehaviourContext.cs
+++ b/src/Ryujinx.Audio/Renderer/Server/BehaviourContext.cs
@@ -1,4 +1,5 @@
using System;
+using System.Buffers;
using System.Diagnostics;
using static Ryujinx.Audio.Renderer.Common.BehaviourParameter;
@@ -44,7 +45,6 @@ namespace Ryujinx.Audio.Renderer.Server
/// was added to supply the count of update done sent to the DSP.
/// A new version of the command estimator was added to address timing changes caused by the voice changes.
/// Additionally, the rendering limit percent was incremented to 80%.
- ///
///
/// This was added in system update 6.0.0
public const int Revision5 = 5 << 24;
@@ -100,10 +100,26 @@ namespace Ryujinx.Audio.Renderer.Server
/// This was added in system update 14.0.0 but some changes were made in 15.0.0
public const int Revision11 = 11 << 24;
+ ///
+ /// REV12:
+ /// Two new commands were added to for biquad filtering and mixing (with optinal volume ramp) on the same command.
+ /// Splitter destinations can now specify up to two biquad filtering parameters, used for filtering the buffer before mixing.
+ ///
+ /// This was added in system update 17.0.0
+ public const int Revision12 = 12 << 24;
+
+ ///
+ /// REV13:
+ /// The compressor effect can now output statistics.
+ /// Splitter destinations now explicitly reset the previous mix volume, instead of doing so on first use.
+ ///
+ /// This was added in system update 18.0.0
+ public const int Revision13 = 13 << 24;
+
///
/// Last revision supported by the implementation.
///
- public const int LastRevision = Revision11;
+ public const int LastRevision = Revision13;
///
/// Target revision magic supported by the implementation.
@@ -211,7 +227,7 @@ namespace Ryujinx.Audio.Renderer.Server
///
/// Check if the audio renderer should fix the GC-ADPCM context not being provided to the DSP.
///
- /// True if if the audio renderer should fix it.
+ /// True if the audio renderer should fix it.
public bool IsAdpcmLoopContextBugFixed()
{
return CheckFeatureSupported(UserRevision, BaseRevisionMagic + Revision2);
@@ -273,7 +289,7 @@ namespace Ryujinx.Audio.Renderer.Server
}
///
- /// Check if the audio renderer should trust the user destination count in .
+ /// Check if the audio renderer should trust the user destination count in .
///
/// True if the audio renderer should trust the user destination count.
public bool IsSplitterBugFixed()
@@ -353,7 +369,7 @@ namespace Ryujinx.Audio.Renderer.Server
/// Check if the audio renderer should use an optimized Biquad Filter (Direct Form 1) in case of two biquad filters are defined on a voice.
///
/// True if the audio renderer should use the optimization.
- public bool IsBiquadFilterGroupedOptimizationSupported()
+ public bool UseMultiTapBiquadFilterProcessing()
{
return CheckFeatureSupported(UserRevision, BaseRevisionMagic + Revision10);
}
@@ -367,6 +383,24 @@ namespace Ryujinx.Audio.Renderer.Server
return CheckFeatureSupported(UserRevision, BaseRevisionMagic + Revision11);
}
+ ///
+ /// Check if the audio renderer should support biquad filter on splitter.
+ ///
+ /// True if the audio renderer support biquad filter on splitter
+ public bool IsBiquadFilterParameterForSplitterEnabled()
+ {
+ return CheckFeatureSupported(UserRevision, BaseRevisionMagic + Revision12);
+ }
+
+ ///
+ /// Check if the audio renderer should support explicit previous mix volume reset on splitter.
+ ///
+ /// True if the audio renderer support explicit previous mix volume reset on splitter
+ public bool IsSplitterPrevVolumeResetSupported()
+ {
+ return CheckFeatureSupported(UserRevision, BaseRevisionMagic + Revision13);
+ }
+
///
/// Get the version of the .
///
diff --git a/src/Ryujinx.Audio/Renderer/Server/CommandBuffer.cs b/src/Ryujinx.Audio/Renderer/Server/CommandBuffer.cs
index f4174a913..4c353b37e 100644
--- a/src/Ryujinx.Audio/Renderer/Server/CommandBuffer.cs
+++ b/src/Ryujinx.Audio/Renderer/Server/CommandBuffer.cs
@@ -204,7 +204,7 @@ namespace Ryujinx.Audio.Renderer.Server
}
///
- /// Create a new .
+ /// Create a new .
///
/// The base index of the input and output buffer.
/// The biquad filter parameters.
@@ -213,9 +213,9 @@ namespace Ryujinx.Audio.Renderer.Server
/// The output buffer offset.
/// Set to true if the biquad filter state is initialized.
/// The node id associated to this command.
- public void GenerateGroupedBiquadFilter(int baseIndex, ReadOnlySpan filters, Memory biquadFilterStatesMemory, int inputBufferOffset, int outputBufferOffset, ReadOnlySpan isInitialized, int nodeId)
+ public void GenerateMultiTapBiquadFilter(int baseIndex, ReadOnlySpan filters, Memory biquadFilterStatesMemory, int inputBufferOffset, int outputBufferOffset, ReadOnlySpan isInitialized, int nodeId)
{
- GroupedBiquadFilterCommand command = new(baseIndex, filters, biquadFilterStatesMemory, inputBufferOffset, outputBufferOffset, isInitialized, nodeId);
+ MultiTapBiquadFilterCommand command = new(baseIndex, filters, biquadFilterStatesMemory, inputBufferOffset, outputBufferOffset, isInitialized, nodeId);
command.EstimatedProcessingTime = _commandProcessingTimeEstimator.Estimate(command);
@@ -232,7 +232,7 @@ namespace Ryujinx.Audio.Renderer.Server
/// The new volume.
/// The to generate the command from.
/// The node id associated to this command.
- public void GenerateMixRampGrouped(uint mixBufferCount, uint inputBufferIndex, uint outputBufferIndex, Span previousVolume, Span volume, Memory state, int nodeId)
+ public void GenerateMixRampGrouped(uint mixBufferCount, uint inputBufferIndex, uint outputBufferIndex, ReadOnlySpan previousVolume, ReadOnlySpan volume, Memory state, int nodeId)
{
MixRampGroupedCommand command = new(mixBufferCount, inputBufferIndex, outputBufferIndex, previousVolume, volume, state, nodeId);
@@ -260,6 +260,120 @@ namespace Ryujinx.Audio.Renderer.Server
AddCommand(command);
}
+ ///
+ /// Generate a new .
+ ///
+ /// The previous volume.
+ /// The new volume.
+ /// The input buffer index.
+ /// The output buffer index.
+ /// The index in the array to store the ramped sample.
+ /// The to generate the command from.
+ /// The biquad filter parameter.
+ /// The biquad state.
+ /// The previous biquad state.
+ /// Set to true if the biquad filter state needs to be initialized.
+ /// Set to true if the mix has volume ramp, and should be taken into account.
+ /// Set to true if the buffer is the first mix buffer.
+ /// The node id associated to this command.
+ public void GenerateBiquadFilterAndMix(
+ float previousVolume,
+ float volume,
+ uint inputBufferIndex,
+ uint outputBufferIndex,
+ int lastSampleIndex,
+ Memory state,
+ ref BiquadFilterParameter filter,
+ Memory biquadFilterState,
+ Memory previousBiquadFilterState,
+ bool needInitialization,
+ bool hasVolumeRamp,
+ bool isFirstMixBuffer,
+ int nodeId)
+ {
+ BiquadFilterAndMixCommand command = new(
+ previousVolume,
+ volume,
+ inputBufferIndex,
+ outputBufferIndex,
+ lastSampleIndex,
+ state,
+ ref filter,
+ biquadFilterState,
+ previousBiquadFilterState,
+ needInitialization,
+ hasVolumeRamp,
+ isFirstMixBuffer,
+ nodeId);
+
+ command.EstimatedProcessingTime = _commandProcessingTimeEstimator.Estimate(command);
+
+ AddCommand(command);
+ }
+
+ ///
+ /// Generate a new .
+ ///
+ /// The previous volume.
+ /// The new volume.
+ /// The input buffer index.
+ /// The output buffer index.
+ /// The index in the array to store the ramped sample.
+ /// The to generate the command from.
+ /// First biquad filter parameter.
+ /// Second biquad filter parameter.
+ /// First biquad state.
+ /// Second biquad state.
+ /// First previous biquad state.
+ /// Second previous biquad state.
+ /// Set to true if the first biquad filter state needs to be initialized.
+ /// Set to true if the second biquad filter state needs to be initialized.
+ /// Set to true if the mix has volume ramp, and should be taken into account.
+ /// Set to true if the buffer is the first mix buffer.
+ /// The node id associated to this command.
+ public void GenerateMultiTapBiquadFilterAndMix(
+ float previousVolume,
+ float volume,
+ uint inputBufferIndex,
+ uint outputBufferIndex,
+ int lastSampleIndex,
+ Memory state,
+ ref BiquadFilterParameter filter0,
+ ref BiquadFilterParameter filter1,
+ Memory biquadFilterState0,
+ Memory biquadFilterState1,
+ Memory previousBiquadFilterState0,
+ Memory previousBiquadFilterState1,
+ bool needInitialization0,
+ bool needInitialization1,
+ bool hasVolumeRamp,
+ bool isFirstMixBuffer,
+ int nodeId)
+ {
+ MultiTapBiquadFilterAndMixCommand command = new(
+ previousVolume,
+ volume,
+ inputBufferIndex,
+ outputBufferIndex,
+ lastSampleIndex,
+ state,
+ ref filter0,
+ ref filter1,
+ biquadFilterState0,
+ biquadFilterState1,
+ previousBiquadFilterState0,
+ previousBiquadFilterState1,
+ needInitialization0,
+ needInitialization1,
+ hasVolumeRamp,
+ isFirstMixBuffer,
+ nodeId);
+
+ command.EstimatedProcessingTime = _commandProcessingTimeEstimator.Estimate(command);
+
+ AddCommand(command);
+ }
+
///
/// Generate a new .
///
@@ -268,7 +382,7 @@ namespace Ryujinx.Audio.Renderer.Server
/// The buffer count.
/// The node id associated to this command.
/// The target sample rate in use.
- public void GenerateDepopForMixBuffersCommand(Memory depopBuffer, uint bufferOffset, uint bufferCount, int nodeId, uint sampleRate)
+ public void GenerateDepopForMixBuffers(Memory depopBuffer, uint bufferOffset, uint bufferCount, int nodeId, uint sampleRate)
{
DepopForMixBuffersCommand command = new(depopBuffer, bufferOffset, bufferCount, nodeId, sampleRate);
@@ -469,11 +583,20 @@ namespace Ryujinx.Audio.Renderer.Server
}
}
- public void GenerateCompressorEffect(uint bufferOffset, CompressorParameter parameter, Memory state, bool isEnabled, int nodeId)
+ ///
+ /// Generate a new .
+ ///
+ /// The target buffer offset.
+ /// The compressor parameter.
+ /// The compressor state.
+ /// The DSP effect result state.
+ /// Set to true if the effect should be active.
+ /// The node id associated to this command.
+ public void GenerateCompressorEffect(uint bufferOffset, CompressorParameter parameter, Memory state, Memory effectResultState, bool isEnabled, int nodeId)
{
if (parameter.IsChannelCountValid())
{
- CompressorCommand command = new(bufferOffset, parameter, state, isEnabled, nodeId);
+ CompressorCommand command = new(bufferOffset, parameter, state, effectResultState, isEnabled, nodeId);
command.EstimatedProcessingTime = _commandProcessingTimeEstimator.Estimate(command);
diff --git a/src/Ryujinx.Audio/Renderer/Server/CommandGenerator.cs b/src/Ryujinx.Audio/Renderer/Server/CommandGenerator.cs
index ae8f699f3..0b789537a 100644
--- a/src/Ryujinx.Audio/Renderer/Server/CommandGenerator.cs
+++ b/src/Ryujinx.Audio/Renderer/Server/CommandGenerator.cs
@@ -12,6 +12,7 @@ using Ryujinx.Audio.Renderer.Server.Voice;
using Ryujinx.Audio.Renderer.Utils;
using System;
using System.Diagnostics;
+using System.Runtime.CompilerServices;
namespace Ryujinx.Audio.Renderer.Server
{
@@ -46,12 +47,13 @@ namespace Ryujinx.Audio.Renderer.Server
{
ref MixState mix = ref _mixContext.GetState(voiceState.MixId);
- _commandBuffer.GenerateDepopPrepare(dspState,
- _rendererContext.DepopBuffer,
- mix.BufferCount,
- mix.BufferOffset,
- voiceState.NodeId,
- voiceState.WasPlaying);
+ _commandBuffer.GenerateDepopPrepare(
+ dspState,
+ _rendererContext.DepopBuffer,
+ mix.BufferCount,
+ mix.BufferOffset,
+ voiceState.NodeId,
+ voiceState.WasPlaying);
}
else if (voiceState.SplitterId != Constants.UnusedSplitterId)
{
@@ -59,15 +61,13 @@ namespace Ryujinx.Audio.Renderer.Server
while (true)
{
- Span destinationSpan = _splitterContext.GetDestination((int)voiceState.SplitterId, destinationId++);
+ SplitterDestination destination = _splitterContext.GetDestination((int)voiceState.SplitterId, destinationId++);
- if (destinationSpan.IsEmpty)
+ if (destination.IsNull)
{
break;
}
- ref SplitterDestination destination = ref destinationSpan[0];
-
if (destination.IsConfigured())
{
int mixId = destination.DestinationId;
@@ -76,12 +76,13 @@ namespace Ryujinx.Audio.Renderer.Server
{
ref MixState mix = ref _mixContext.GetState(mixId);
- _commandBuffer.GenerateDepopPrepare(dspState,
- _rendererContext.DepopBuffer,
- mix.BufferCount,
- mix.BufferOffset,
- voiceState.NodeId,
- voiceState.WasPlaying);
+ _commandBuffer.GenerateDepopPrepare(
+ dspState,
+ _rendererContext.DepopBuffer,
+ mix.BufferCount,
+ mix.BufferOffset,
+ voiceState.NodeId,
+ voiceState.WasPlaying);
destination.MarkAsNeedToUpdateInternalState();
}
@@ -95,35 +96,39 @@ namespace Ryujinx.Audio.Renderer.Server
if (_rendererContext.BehaviourContext.IsWaveBufferVersion2Supported())
{
- _commandBuffer.GenerateDataSourceVersion2(ref voiceState,
- dspState,
- (ushort)_rendererContext.MixBufferCount,
- (ushort)channelIndex,
- voiceState.NodeId);
+ _commandBuffer.GenerateDataSourceVersion2(
+ ref voiceState,
+ dspState,
+ (ushort)_rendererContext.MixBufferCount,
+ (ushort)channelIndex,
+ voiceState.NodeId);
}
else
{
switch (voiceState.SampleFormat)
{
case SampleFormat.PcmInt16:
- _commandBuffer.GeneratePcmInt16DataSourceVersion1(ref voiceState,
- dspState,
- (ushort)_rendererContext.MixBufferCount,
- (ushort)channelIndex,
- voiceState.NodeId);
+ _commandBuffer.GeneratePcmInt16DataSourceVersion1(
+ ref voiceState,
+ dspState,
+ (ushort)_rendererContext.MixBufferCount,
+ (ushort)channelIndex,
+ voiceState.NodeId);
break;
case SampleFormat.PcmFloat:
- _commandBuffer.GeneratePcmFloatDataSourceVersion1(ref voiceState,
- dspState,
- (ushort)_rendererContext.MixBufferCount,
- (ushort)channelIndex,
- voiceState.NodeId);
+ _commandBuffer.GeneratePcmFloatDataSourceVersion1(
+ ref voiceState,
+ dspState,
+ (ushort)_rendererContext.MixBufferCount,
+ (ushort)channelIndex,
+ voiceState.NodeId);
break;
case SampleFormat.Adpcm:
- _commandBuffer.GenerateAdpcmDataSourceVersion1(ref voiceState,
- dspState,
- (ushort)_rendererContext.MixBufferCount,
- voiceState.NodeId);
+ _commandBuffer.GenerateAdpcmDataSourceVersion1(
+ ref voiceState,
+ dspState,
+ (ushort)_rendererContext.MixBufferCount,
+ voiceState.NodeId);
break;
default:
throw new NotImplementedException($"Unsupported data source {voiceState.SampleFormat}");
@@ -134,14 +139,14 @@ namespace Ryujinx.Audio.Renderer.Server
private void GenerateBiquadFilterForVoice(ref VoiceState voiceState, Memory state, int baseIndex, int bufferOffset, int nodeId)
{
- bool supportsOptimizedPath = _rendererContext.BehaviourContext.IsBiquadFilterGroupedOptimizationSupported();
+ bool supportsOptimizedPath = _rendererContext.BehaviourContext.UseMultiTapBiquadFilterProcessing();
if (supportsOptimizedPath && voiceState.BiquadFilters[0].Enable && voiceState.BiquadFilters[1].Enable)
{
- Memory biquadStateRawMemory = SpanMemoryManager.Cast(state)[..(VoiceUpdateState.BiquadStateSize * Constants.VoiceBiquadFilterCount)];
+ Memory biquadStateRawMemory = SpanMemoryManager.Cast(state)[..(Unsafe.SizeOf() * Constants.VoiceBiquadFilterCount)];
Memory stateMemory = SpanMemoryManager.Cast(biquadStateRawMemory);
- _commandBuffer.GenerateGroupedBiquadFilter(baseIndex, voiceState.BiquadFilters.AsSpan(), stateMemory, bufferOffset, bufferOffset, voiceState.BiquadFilterNeedInitialization, nodeId);
+ _commandBuffer.GenerateMultiTapBiquadFilter(baseIndex, voiceState.BiquadFilters.AsSpan(), stateMemory, bufferOffset, bufferOffset, voiceState.BiquadFilterNeedInitialization, nodeId);
}
else
{
@@ -151,33 +156,134 @@ namespace Ryujinx.Audio.Renderer.Server
if (filter.Enable)
{
- Memory biquadStateRawMemory = SpanMemoryManager.Cast(state)[..(VoiceUpdateState.BiquadStateSize * Constants.VoiceBiquadFilterCount)];
-
+ Memory biquadStateRawMemory = SpanMemoryManager.Cast(state)[..(Unsafe.SizeOf() * Constants.VoiceBiquadFilterCount)];
Memory stateMemory = SpanMemoryManager.Cast(biquadStateRawMemory);
- _commandBuffer.GenerateBiquadFilter(baseIndex,
- ref filter,
- stateMemory.Slice(i, 1),
- bufferOffset,
- bufferOffset,
- !voiceState.BiquadFilterNeedInitialization[i],
- nodeId);
+ _commandBuffer.GenerateBiquadFilter(
+ baseIndex,
+ ref filter,
+ stateMemory.Slice(i, 1),
+ bufferOffset,
+ bufferOffset,
+ !voiceState.BiquadFilterNeedInitialization[i],
+ nodeId);
}
}
}
}
- private void GenerateVoiceMix(Span mixVolumes, Span previousMixVolumes, Memory state, uint bufferOffset, uint bufferCount, uint bufferIndex, int nodeId)
+ private void GenerateVoiceMixWithSplitter(
+ SplitterDestination destination,
+ Memory state,
+ uint bufferOffset,
+ uint bufferCount,
+ uint bufferIndex,
+ int nodeId)
+ {
+ ReadOnlySpan mixVolumes = destination.MixBufferVolume;
+ ReadOnlySpan previousMixVolumes = destination.PreviousMixBufferVolume;
+
+ ref BiquadFilterParameter bqf0 = ref destination.GetBiquadFilterParameter(0);
+ ref BiquadFilterParameter bqf1 = ref destination.GetBiquadFilterParameter(1);
+
+ Memory bqfState = _splitterContext.GetBiquadFilterState(destination);
+
+ bool isFirstMixBuffer = true;
+
+ for (int i = 0; i < bufferCount; i++)
+ {
+ float previousMixVolume = previousMixVolumes[i];
+ float mixVolume = mixVolumes[i];
+
+ if (mixVolume != 0.0f || previousMixVolume != 0.0f)
+ {
+ if (bqf0.Enable && bqf1.Enable)
+ {
+ _commandBuffer.GenerateMultiTapBiquadFilterAndMix(
+ previousMixVolume,
+ mixVolume,
+ bufferIndex,
+ bufferOffset + (uint)i,
+ i,
+ state,
+ ref bqf0,
+ ref bqf1,
+ bqfState[..1],
+ bqfState.Slice(1, 1),
+ bqfState.Slice(2, 1),
+ bqfState.Slice(3, 1),
+ !destination.IsBiquadFilterEnabledPrev(),
+ !destination.IsBiquadFilterEnabledPrev(),
+ true,
+ isFirstMixBuffer,
+ nodeId);
+
+ destination.UpdateBiquadFilterEnabledPrev(0);
+ destination.UpdateBiquadFilterEnabledPrev(1);
+ }
+ else if (bqf0.Enable)
+ {
+ _commandBuffer.GenerateBiquadFilterAndMix(
+ previousMixVolume,
+ mixVolume,
+ bufferIndex,
+ bufferOffset + (uint)i,
+ i,
+ state,
+ ref bqf0,
+ bqfState[..1],
+ bqfState.Slice(1, 1),
+ !destination.IsBiquadFilterEnabledPrev(),
+ true,
+ isFirstMixBuffer,
+ nodeId);
+
+ destination.UpdateBiquadFilterEnabledPrev(0);
+ }
+ else if (bqf1.Enable)
+ {
+ _commandBuffer.GenerateBiquadFilterAndMix(
+ previousMixVolume,
+ mixVolume,
+ bufferIndex,
+ bufferOffset + (uint)i,
+ i,
+ state,
+ ref bqf1,
+ bqfState[..1],
+ bqfState.Slice(1, 1),
+ !destination.IsBiquadFilterEnabledPrev(),
+ true,
+ isFirstMixBuffer,
+ nodeId);
+
+ destination.UpdateBiquadFilterEnabledPrev(1);
+ }
+
+ isFirstMixBuffer = false;
+ }
+ }
+ }
+
+ private void GenerateVoiceMix(
+ ReadOnlySpan mixVolumes,
+ ReadOnlySpan previousMixVolumes,
+ Memory state,
+ uint bufferOffset,
+ uint bufferCount,
+ uint bufferIndex,
+ int nodeId)
{
if (bufferCount > Constants.VoiceChannelCountMax)
{
- _commandBuffer.GenerateMixRampGrouped(bufferCount,
- bufferIndex,
- bufferOffset,
- previousMixVolumes,
- mixVolumes,
- state,
- nodeId);
+ _commandBuffer.GenerateMixRampGrouped(
+ bufferCount,
+ bufferIndex,
+ bufferOffset,
+ previousMixVolumes,
+ mixVolumes,
+ state,
+ nodeId);
}
else
{
@@ -188,13 +294,14 @@ namespace Ryujinx.Audio.Renderer.Server
if (mixVolume != 0.0f || previousMixVolume != 0.0f)
{
- _commandBuffer.GenerateMixRamp(previousMixVolume,
- mixVolume,
- bufferIndex,
- bufferOffset + (uint)i,
- i,
- state,
- nodeId);
+ _commandBuffer.GenerateMixRamp(
+ previousMixVolume,
+ mixVolume,
+ bufferIndex,
+ bufferOffset + (uint)i,
+ i,
+ state,
+ nodeId);
}
}
}
@@ -271,10 +378,11 @@ namespace Ryujinx.Audio.Renderer.Server
GeneratePerformance(ref performanceEntry, PerformanceCommand.Type.Start, nodeId);
}
- _commandBuffer.GenerateVolumeRamp(voiceState.PreviousVolume,
- voiceState.Volume,
- _rendererContext.MixBufferCount + (uint)channelIndex,
- nodeId);
+ _commandBuffer.GenerateVolumeRamp(
+ voiceState.PreviousVolume,
+ voiceState.Volume,
+ _rendererContext.MixBufferCount + (uint)channelIndex,
+ nodeId);
if (performanceInitialized)
{
@@ -291,15 +399,13 @@ namespace Ryujinx.Audio.Renderer.Server
while (true)
{
- Span destinationSpan = _splitterContext.GetDestination((int)voiceState.SplitterId, destinationId);
+ SplitterDestination destination = _splitterContext.GetDestination((int)voiceState.SplitterId, destinationId);
- if (destinationSpan.IsEmpty)
+ if (destination.IsNull)
{
break;
}
- ref SplitterDestination destination = ref destinationSpan[0];
-
destinationId += (int)channelsCount;
if (destination.IsConfigured())
@@ -310,13 +416,27 @@ namespace Ryujinx.Audio.Renderer.Server
{
ref MixState mix = ref _mixContext.GetState(mixId);
- GenerateVoiceMix(destination.MixBufferVolume,
- destination.PreviousMixBufferVolume,
- dspStateMemory,
- mix.BufferOffset,
- mix.BufferCount,
- _rendererContext.MixBufferCount + (uint)channelIndex,
- nodeId);
+ if (destination.IsBiquadFilterEnabled())
+ {
+ GenerateVoiceMixWithSplitter(
+ destination,
+ dspStateMemory,
+ mix.BufferOffset,
+ mix.BufferCount,
+ _rendererContext.MixBufferCount + (uint)channelIndex,
+ nodeId);
+ }
+ else
+ {
+ GenerateVoiceMix(
+ destination.MixBufferVolume,
+ destination.PreviousMixBufferVolume,
+ dspStateMemory,
+ mix.BufferOffset,
+ mix.BufferCount,
+ _rendererContext.MixBufferCount + (uint)channelIndex,
+ nodeId);
+ }
destination.MarkAsNeedToUpdateInternalState();
}
@@ -337,13 +457,14 @@ namespace Ryujinx.Audio.Renderer.Server
GeneratePerformance(ref performanceEntry, PerformanceCommand.Type.Start, nodeId);
}
- GenerateVoiceMix(channelResource.Mix.AsSpan(),
- channelResource.PreviousMix.AsSpan(),
- dspStateMemory,
- mix.BufferOffset,
- mix.BufferCount,
- _rendererContext.MixBufferCount + (uint)channelIndex,
- nodeId);
+ GenerateVoiceMix(
+ channelResource.Mix.AsSpan(),
+ channelResource.PreviousMix.AsSpan(),
+ dspStateMemory,
+ mix.BufferOffset,
+ mix.BufferCount,
+ _rendererContext.MixBufferCount + (uint)channelIndex,
+ nodeId);
if (performanceInitialized)
{
@@ -409,10 +530,11 @@ namespace Ryujinx.Audio.Renderer.Server
{
if (effect.Parameter.Volumes[i] != 0.0f)
{
- _commandBuffer.GenerateMix((uint)bufferOffset + effect.Parameter.Input[i],
- (uint)bufferOffset + effect.Parameter.Output[i],
- nodeId,
- effect.Parameter.Volumes[i]);
+ _commandBuffer.GenerateMix(
+ (uint)bufferOffset + effect.Parameter.Input[i],
+ (uint)bufferOffset + effect.Parameter.Output[i],
+ nodeId,
+ effect.Parameter.Volumes[i]);
}
}
}
@@ -447,17 +569,18 @@ namespace Ryujinx.Audio.Renderer.Server
updateCount = newUpdateCount;
}
- _commandBuffer.GenerateAuxEffect(bufferOffset,
- effect.Parameter.Input[i],
- effect.Parameter.Output[i],
- ref effect.State,
- effect.IsEnabled,
- effect.Parameter.BufferStorageSize,
- effect.State.SendBufferInfoBase,
- effect.State.ReturnBufferInfoBase,
- updateCount,
- writeOffset,
- nodeId);
+ _commandBuffer.GenerateAuxEffect(
+ bufferOffset,
+ effect.Parameter.Input[i],
+ effect.Parameter.Output[i],
+ ref effect.State,
+ effect.IsEnabled,
+ effect.Parameter.BufferStorageSize,
+ effect.State.SendBufferInfoBase,
+ effect.State.ReturnBufferInfoBase,
+ updateCount,
+ writeOffset,
+ nodeId);
writeOffset = newUpdateCount;
@@ -500,7 +623,7 @@ namespace Ryujinx.Audio.Renderer.Server
if (effect.IsEnabled)
{
bool needInitialization = effect.Parameter.Status == UsageState.Invalid ||
- (effect.Parameter.Status == UsageState.New && !_rendererContext.BehaviourContext.IsBiquadFilterEffectStateClearBugFixed());
+ (effect.Parameter.Status == UsageState.New && !_rendererContext.BehaviourContext.IsBiquadFilterEffectStateClearBugFixed());
BiquadFilterParameter parameter = new()
{
@@ -512,11 +635,14 @@ namespace Ryujinx.Audio.Renderer.Server
for (int i = 0; i < effect.Parameter.ChannelCount; i++)
{
- _commandBuffer.GenerateBiquadFilter((int)bufferOffset, ref parameter, effect.State.Slice(i, 1),
- effect.Parameter.Input[i],
- effect.Parameter.Output[i],
- needInitialization,
- nodeId);
+ _commandBuffer.GenerateBiquadFilter(
+ (int)bufferOffset,
+ ref parameter,
+ effect.State.Slice(i, 1),
+ effect.Parameter.Input[i],
+ effect.Parameter.Output[i],
+ needInitialization,
+ nodeId);
}
}
else
@@ -591,15 +717,16 @@ namespace Ryujinx.Audio.Renderer.Server
updateCount = newUpdateCount;
}
- _commandBuffer.GenerateCaptureEffect(bufferOffset,
- effect.Parameter.Input[i],
- effect.State.SendBufferInfo,
- effect.IsEnabled,
- effect.Parameter.BufferStorageSize,
- effect.State.SendBufferInfoBase,
- updateCount,
- writeOffset,
- nodeId);
+ _commandBuffer.GenerateCaptureEffect(
+ bufferOffset,
+ effect.Parameter.Input[i],
+ effect.State.SendBufferInfo,
+ effect.IsEnabled,
+ effect.Parameter.BufferStorageSize,
+ effect.State.SendBufferInfoBase,
+ updateCount,
+ writeOffset,
+ nodeId);
writeOffset = newUpdateCount;
@@ -608,15 +735,28 @@ namespace Ryujinx.Audio.Renderer.Server
}
}
- private void GenerateCompressorEffect(uint bufferOffset, CompressorEffect effect, int nodeId)
+ private void GenerateCompressorEffect(uint bufferOffset, CompressorEffect effect, int nodeId, int effectId)
{
Debug.Assert(effect.Type == EffectType.Compressor);
- _commandBuffer.GenerateCompressorEffect(bufferOffset,
- effect.Parameter,
- effect.State,
- effect.IsEnabled,
- nodeId);
+ Memory dspResultState;
+
+ if (effect.Parameter.StatisticsEnabled)
+ {
+ dspResultState = _effectContext.GetDspStateMemory(effectId);
+ }
+ else
+ {
+ dspResultState = Memory.Empty;
+ }
+
+ _commandBuffer.GenerateCompressorEffect(
+ bufferOffset,
+ effect.Parameter,
+ effect.State,
+ dspResultState,
+ effect.IsEnabled,
+ nodeId);
}
private void GenerateEffect(ref MixState mix, int effectId, BaseEffect effect)
@@ -629,8 +769,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;
@@ -664,7 +807,7 @@ namespace Ryujinx.Audio.Renderer.Server
GenerateCaptureEffect(mix.BufferOffset, (CaptureBufferEffect)effect, nodeId);
break;
case EffectType.Compressor:
- GenerateCompressorEffect(mix.BufferOffset, (CompressorEffect)effect, nodeId);
+ GenerateCompressorEffect(mix.BufferOffset, (CompressorEffect)effect, nodeId, effectId);
break;
default:
throw new NotImplementedException($"Unsupported effect type {effect.Type}");
@@ -706,6 +849,85 @@ namespace Ryujinx.Audio.Renderer.Server
}
}
+ private void GenerateMixWithSplitter(
+ uint inputBufferIndex,
+ uint outputBufferIndex,
+ float volume,
+ SplitterDestination destination,
+ ref bool isFirstMixBuffer,
+ int nodeId)
+ {
+ ref BiquadFilterParameter bqf0 = ref destination.GetBiquadFilterParameter(0);
+ ref BiquadFilterParameter bqf1 = ref destination.GetBiquadFilterParameter(1);
+
+ Memory bqfState = _splitterContext.GetBiquadFilterState(destination);
+
+ if (bqf0.Enable && bqf1.Enable)
+ {
+ _commandBuffer.GenerateMultiTapBiquadFilterAndMix(
+ 0f,
+ volume,
+ inputBufferIndex,
+ outputBufferIndex,
+ 0,
+ Memory.Empty,
+ ref bqf0,
+ ref bqf1,
+ bqfState[..1],
+ bqfState.Slice(1, 1),
+ bqfState.Slice(2, 1),
+ bqfState.Slice(3, 1),
+ !destination.IsBiquadFilterEnabledPrev(),
+ !destination.IsBiquadFilterEnabledPrev(),
+ false,
+ isFirstMixBuffer,
+ nodeId);
+
+ destination.UpdateBiquadFilterEnabledPrev(0);
+ destination.UpdateBiquadFilterEnabledPrev(1);
+ }
+ else if (bqf0.Enable)
+ {
+ _commandBuffer.GenerateBiquadFilterAndMix(
+ 0f,
+ volume,
+ inputBufferIndex,
+ outputBufferIndex,
+ 0,
+ Memory.Empty,
+ ref bqf0,
+ bqfState[..1],
+ bqfState.Slice(1, 1),
+ !destination.IsBiquadFilterEnabledPrev(),
+ false,
+ isFirstMixBuffer,
+ nodeId);
+
+ destination.UpdateBiquadFilterEnabledPrev(0);
+ }
+ else if (bqf1.Enable)
+ {
+ _commandBuffer.GenerateBiquadFilterAndMix(
+ 0f,
+ volume,
+ inputBufferIndex,
+ outputBufferIndex,
+ 0,
+ Memory.Empty,
+ ref bqf1,
+ bqfState[..1],
+ bqfState.Slice(1, 1),
+ !destination.IsBiquadFilterEnabledPrev(),
+ false,
+ isFirstMixBuffer,
+ nodeId);
+
+ destination.UpdateBiquadFilterEnabledPrev(1);
+ }
+
+ isFirstMixBuffer = false;
+ }
+
private void GenerateMix(ref MixState mix)
{
if (mix.HasAnyDestination())
@@ -722,15 +944,13 @@ namespace Ryujinx.Audio.Renderer.Server
{
int destinationIndex = destinationId++;
- Span destinationSpan = _splitterContext.GetDestination((int)mix.DestinationSplitterId, destinationIndex);
+ SplitterDestination destination = _splitterContext.GetDestination((int)mix.DestinationSplitterId, destinationIndex);
- if (destinationSpan.IsEmpty)
+ if (destination.IsNull)
{
break;
}
- ref SplitterDestination destination = ref destinationSpan[0];
-
if (destination.IsConfigured())
{
int mixId = destination.DestinationId;
@@ -741,16 +961,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 +1006,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 +1020,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 +1085,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 +1121,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 +1178,45 @@ namespace Ryujinx.Audio.Renderer.Server
if (useCustomDownMixingCommand)
{
- _commandBuffer.GenerateDownMixSurroundToStereo(finalMix.BufferOffset,
- sink.Parameter.Input.AsSpan(),
- sink.Parameter.Input.AsSpan(),
- sink.DownMixCoefficients,
- Constants.InvalidNodeId);
+ _commandBuffer.GenerateDownMixSurroundToStereo(
+ finalMix.BufferOffset,
+ sink.Parameter.Input.AsSpan(),
+ sink.Parameter.Input.AsSpan(),
+ sink.DownMixCoefficients,
+ Constants.InvalidNodeId);
}
// NOTE: We do the downmixing at the DSP level as it's easier that way.
else if (_rendererContext.ChannelCount == 2 && sink.Parameter.InputCount == 6)
{
- _commandBuffer.GenerateDownMixSurroundToStereo(finalMix.BufferOffset,
- sink.Parameter.Input.AsSpan(),
- sink.Parameter.Input.AsSpan(),
- Constants.DefaultSurroundToStereoCoefficients,
- Constants.InvalidNodeId);
+ _commandBuffer.GenerateDownMixSurroundToStereo(
+ finalMix.BufferOffset,
+ sink.Parameter.Input.AsSpan(),
+ sink.Parameter.Input.AsSpan(),
+ Constants.DefaultSurroundToStereoCoefficients,
+ Constants.InvalidNodeId);
}
CommandList commandList = _commandBuffer.CommandList;
if (sink.UpsamplerState != null)
{
- _commandBuffer.GenerateUpsample(finalMix.BufferOffset,
- sink.UpsamplerState,
- sink.Parameter.InputCount,
- sink.Parameter.Input.AsSpan(),
- commandList.BufferCount,
- commandList.SampleCount,
- commandList.SampleRate,
- Constants.InvalidNodeId);
+ _commandBuffer.GenerateUpsample(
+ finalMix.BufferOffset,
+ sink.UpsamplerState,
+ sink.Parameter.InputCount,
+ sink.Parameter.Input.AsSpan(),
+ commandList.BufferCount,
+ commandList.SampleCount,
+ commandList.SampleRate,
+ Constants.InvalidNodeId);
}
- _commandBuffer.GenerateDeviceSink(finalMix.BufferOffset,
- sink,
- _rendererContext.SessionId,
- commandList.Buffers,
- Constants.InvalidNodeId);
+ _commandBuffer.GenerateDeviceSink(
+ finalMix.BufferOffset,
+ sink,
+ _rendererContext.SessionId,
+ commandList.Buffers,
+ Constants.InvalidNodeId);
}
private void GenerateSink(BaseSink sink, ref MixState finalMix)
diff --git a/src/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion1.cs b/src/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion1.cs
index d95e9aa71..cff754b82 100644
--- a/src/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion1.cs
+++ b/src/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion1.cs
@@ -170,7 +170,7 @@ namespace Ryujinx.Audio.Renderer.Server
return 0;
}
- public uint Estimate(GroupedBiquadFilterCommand command)
+ public uint Estimate(MultiTapBiquadFilterCommand command)
{
return 0;
}
@@ -184,5 +184,15 @@ namespace Ryujinx.Audio.Renderer.Server
{
return 0;
}
+
+ public uint Estimate(BiquadFilterAndMixCommand command)
+ {
+ return 0;
+ }
+
+ public uint Estimate(MultiTapBiquadFilterAndMixCommand command)
+ {
+ return 0;
+ }
}
}
diff --git a/src/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion2.cs b/src/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion2.cs
index 929aaf383..ef1326924 100644
--- a/src/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion2.cs
+++ b/src/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion2.cs
@@ -462,7 +462,7 @@ namespace Ryujinx.Audio.Renderer.Server
return 0;
}
- public uint Estimate(GroupedBiquadFilterCommand command)
+ public uint Estimate(MultiTapBiquadFilterCommand command)
{
return 0;
}
@@ -476,5 +476,15 @@ namespace Ryujinx.Audio.Renderer.Server
{
return 0;
}
+
+ public uint Estimate(BiquadFilterAndMixCommand command)
+ {
+ return 0;
+ }
+
+ public uint Estimate(MultiTapBiquadFilterAndMixCommand command)
+ {
+ return 0;
+ }
}
}
diff --git a/src/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion3.cs b/src/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion3.cs
index 8ae4bc059..31a5347b4 100644
--- a/src/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion3.cs
+++ b/src/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion3.cs
@@ -632,7 +632,7 @@ namespace Ryujinx.Audio.Renderer.Server
};
}
- public virtual uint Estimate(GroupedBiquadFilterCommand command)
+ public virtual uint Estimate(MultiTapBiquadFilterCommand command)
{
return 0;
}
@@ -646,5 +646,15 @@ namespace Ryujinx.Audio.Renderer.Server
{
return 0;
}
+
+ public virtual uint Estimate(BiquadFilterAndMixCommand command)
+ {
+ return 0;
+ }
+
+ public virtual uint Estimate(MultiTapBiquadFilterAndMixCommand command)
+ {
+ return 0;
+ }
}
}
diff --git a/src/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion4.cs b/src/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion4.cs
index 25bc67cd9..fb357120d 100644
--- a/src/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion4.cs
+++ b/src/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion4.cs
@@ -10,7 +10,7 @@ namespace Ryujinx.Audio.Renderer.Server
{
public CommandProcessingTimeEstimatorVersion4(uint sampleCount, uint bufferCount) : base(sampleCount, bufferCount) { }
- public override uint Estimate(GroupedBiquadFilterCommand command)
+ public override uint Estimate(MultiTapBiquadFilterCommand command)
{
Debug.Assert(SampleCount == 160 || SampleCount == 240);
diff --git a/src/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion5.cs b/src/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion5.cs
index 7135c1c4f..bc9ba073d 100644
--- a/src/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion5.cs
+++ b/src/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion5.cs
@@ -169,14 +169,28 @@ namespace Ryujinx.Audio.Renderer.Server
{
if (command.Enabled)
{
- return command.Parameter.ChannelCount switch
+ if (command.Parameter.StatisticsEnabled)
{
- 1 => 34431,
- 2 => 44253,
- 4 => 63827,
- 6 => 83361,
- _ => throw new NotImplementedException($"{command.Parameter.ChannelCount}"),
- };
+ return command.Parameter.ChannelCount switch
+ {
+ 1 => 22100,
+ 2 => 33211,
+ 4 => 41587,
+ 6 => 58819,
+ _ => throw new NotImplementedException($"{command.Parameter.ChannelCount}"),
+ };
+ }
+ else
+ {
+ return command.Parameter.ChannelCount switch
+ {
+ 1 => 19052,
+ 2 => 29852,
+ 4 => 37904,
+ 6 => 55020,
+ _ => throw new NotImplementedException($"{command.Parameter.ChannelCount}"),
+ };
+ }
}
return command.Parameter.ChannelCount switch
@@ -191,14 +205,28 @@ namespace Ryujinx.Audio.Renderer.Server
if (command.Enabled)
{
- return command.Parameter.ChannelCount switch
+ if (command.Parameter.StatisticsEnabled)
{
- 1 => 51095,
- 2 => 65693,
- 4 => 95383,
- 6 => 124510,
- _ => throw new NotImplementedException($"{command.Parameter.ChannelCount}"),
- };
+ return command.Parameter.ChannelCount switch
+ {
+ 1 => 32518,
+ 2 => 49102,
+ 4 => 61685,
+ 6 => 87250,
+ _ => throw new NotImplementedException($"{command.Parameter.ChannelCount}"),
+ };
+ }
+ else
+ {
+ return command.Parameter.ChannelCount switch
+ {
+ 1 => 27963,
+ 2 => 44016,
+ 4 => 56183,
+ 6 => 81862,
+ _ => throw new NotImplementedException($"{command.Parameter.ChannelCount}"),
+ };
+ }
}
return command.Parameter.ChannelCount switch
@@ -210,5 +238,53 @@ namespace Ryujinx.Audio.Renderer.Server
_ => throw new NotImplementedException($"{command.Parameter.ChannelCount}"),
};
}
+
+ public override uint Estimate(BiquadFilterAndMixCommand command)
+ {
+ Debug.Assert(SampleCount == 160 || SampleCount == 240);
+
+ if (command.HasVolumeRamp)
+ {
+ if (SampleCount == 160)
+ {
+ return 5204;
+ }
+
+ return 6683;
+ }
+ else
+ {
+ if (SampleCount == 160)
+ {
+ return 3427;
+ }
+
+ return 4752;
+ }
+ }
+
+ public override uint Estimate(MultiTapBiquadFilterAndMixCommand command)
+ {
+ Debug.Assert(SampleCount == 160 || SampleCount == 240);
+
+ if (command.HasVolumeRamp)
+ {
+ if (SampleCount == 160)
+ {
+ return 7939;
+ }
+
+ return 10669;
+ }
+ else
+ {
+ if (SampleCount == 160)
+ {
+ return 6256;
+ }
+
+ return 8683;
+ }
+ }
}
}
diff --git a/src/Ryujinx.Audio/Renderer/Server/Effect/AuxiliaryBufferEffect.cs b/src/Ryujinx.Audio/Renderer/Server/Effect/AuxiliaryBufferEffect.cs
index 57ca266f4..74a9baff2 100644
--- a/src/Ryujinx.Audio/Renderer/Server/Effect/AuxiliaryBufferEffect.cs
+++ b/src/Ryujinx.Audio/Renderer/Server/Effect/AuxiliaryBufferEffect.cs
@@ -33,21 +33,21 @@ namespace Ryujinx.Audio.Renderer.Server.Effect
return WorkBuffers[index].GetReference(true);
}
- public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, ref EffectInParameterVersion1 parameter, PoolMapper mapper)
+ public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, in EffectInParameterVersion1 parameter, PoolMapper mapper)
{
- Update(out updateErrorInfo, ref parameter, mapper);
+ Update(out updateErrorInfo, in parameter, mapper);
}
- public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, ref EffectInParameterVersion2 parameter, PoolMapper mapper)
+ public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, in EffectInParameterVersion2 parameter, PoolMapper mapper)
{
- Update(out updateErrorInfo, ref parameter, mapper);
+ Update(out updateErrorInfo, in parameter, mapper);
}
- public void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, ref T parameter, PoolMapper mapper) where T : unmanaged, IEffectInParameter
+ public void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, in T parameter, PoolMapper mapper) where T : unmanaged, IEffectInParameter
{
- Debug.Assert(IsTypeValid(ref parameter));
+ Debug.Assert(IsTypeValid(in parameter));
- UpdateParameterBase(ref parameter);
+ UpdateParameterBase(in parameter);
Parameter = MemoryMarshal.Cast(parameter.SpecificData)[0];
IsEnabled = parameter.IsEnabled;
diff --git a/src/Ryujinx.Audio/Renderer/Server/Effect/BaseEffect.cs b/src/Ryujinx.Audio/Renderer/Server/Effect/BaseEffect.cs
index a9716db2a..77d9b5c29 100644
--- a/src/Ryujinx.Audio/Renderer/Server/Effect/BaseEffect.cs
+++ b/src/Ryujinx.Audio/Renderer/Server/Effect/BaseEffect.cs
@@ -81,7 +81,7 @@ namespace Ryujinx.Audio.Renderer.Server.Effect
///
/// The user parameter.
/// Returns true if the sent by the user matches the internal .
- public bool IsTypeValid(ref T parameter) where T : unmanaged, IEffectInParameter
+ public bool IsTypeValid(in T parameter) where T : unmanaged, IEffectInParameter
{
return parameter.Type == TargetEffectType;
}
@@ -98,7 +98,7 @@ namespace Ryujinx.Audio.Renderer.Server.Effect
/// Update the internal common parameters from a user parameter.
///
/// The user parameter.
- protected void UpdateParameterBase(ref T parameter) where T : unmanaged, IEffectInParameter
+ protected void UpdateParameterBase(in T parameter) where T : unmanaged, IEffectInParameter
{
MixId = parameter.MixId;
ProcessingOrder = parameter.ProcessingOrder;
@@ -139,7 +139,7 @@ namespace Ryujinx.Audio.Renderer.Server.Effect
///
/// Initialize the given result state.
///
- /// The state to initalize
+ /// The state to initialize
public virtual void InitializeResultState(ref EffectResultState state) { }
///
@@ -155,9 +155,9 @@ namespace Ryujinx.Audio.Renderer.Server.Effect
/// The possible that was generated.
/// The user parameter.
/// The mapper to use.
- public virtual void Update(out ErrorInfo updateErrorInfo, ref EffectInParameterVersion1 parameter, PoolMapper mapper)
+ public virtual void Update(out ErrorInfo updateErrorInfo, in EffectInParameterVersion1 parameter, PoolMapper mapper)
{
- Debug.Assert(IsTypeValid(ref parameter));
+ Debug.Assert(IsTypeValid(in parameter));
updateErrorInfo = new ErrorInfo();
}
@@ -168,9 +168,9 @@ namespace Ryujinx.Audio.Renderer.Server.Effect
/// The possible that was generated.
/// The user parameter.
/// The mapper to use.
- public virtual void Update(out ErrorInfo updateErrorInfo, ref EffectInParameterVersion2 parameter, PoolMapper mapper)
+ public virtual void Update(out ErrorInfo updateErrorInfo, in EffectInParameterVersion2 parameter, PoolMapper mapper)
{
- Debug.Assert(IsTypeValid(ref parameter));
+ Debug.Assert(IsTypeValid(in parameter));
updateErrorInfo = new ErrorInfo();
}
diff --git a/src/Ryujinx.Audio/Renderer/Server/Effect/BiquadFilterEffect.cs b/src/Ryujinx.Audio/Renderer/Server/Effect/BiquadFilterEffect.cs
index b987f7c85..3b3e1021c 100644
--- a/src/Ryujinx.Audio/Renderer/Server/Effect/BiquadFilterEffect.cs
+++ b/src/Ryujinx.Audio/Renderer/Server/Effect/BiquadFilterEffect.cs
@@ -35,21 +35,21 @@ namespace Ryujinx.Audio.Renderer.Server.Effect
public override EffectType TargetEffectType => EffectType.BiquadFilter;
- public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, ref EffectInParameterVersion1 parameter, PoolMapper mapper)
+ public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, in EffectInParameterVersion1 parameter, PoolMapper mapper)
{
- Update(out updateErrorInfo, ref parameter, mapper);
+ Update(out updateErrorInfo, in parameter, mapper);
}
- public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, ref EffectInParameterVersion2 parameter, PoolMapper mapper)
+ public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, in EffectInParameterVersion2 parameter, PoolMapper mapper)
{
- Update(out updateErrorInfo, ref parameter, mapper);
+ Update(out updateErrorInfo, in parameter, mapper);
}
- public void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, ref T parameter, PoolMapper mapper) where T : unmanaged, IEffectInParameter
+ public void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, in T parameter, PoolMapper mapper) where T : unmanaged, IEffectInParameter
{
- Debug.Assert(IsTypeValid(ref parameter));
+ Debug.Assert(IsTypeValid(in parameter));
- UpdateParameterBase(ref parameter);
+ UpdateParameterBase(in parameter);
Parameter = MemoryMarshal.Cast(parameter.SpecificData)[0];
IsEnabled = parameter.IsEnabled;
diff --git a/src/Ryujinx.Audio/Renderer/Server/Effect/BufferMixEffect.cs b/src/Ryujinx.Audio/Renderer/Server/Effect/BufferMixEffect.cs
index d6cb9cfa3..5d82b5ae8 100644
--- a/src/Ryujinx.Audio/Renderer/Server/Effect/BufferMixEffect.cs
+++ b/src/Ryujinx.Audio/Renderer/Server/Effect/BufferMixEffect.cs
@@ -19,21 +19,21 @@ namespace Ryujinx.Audio.Renderer.Server.Effect
public override EffectType TargetEffectType => EffectType.BufferMix;
- public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, ref EffectInParameterVersion1 parameter, PoolMapper mapper)
+ public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, in EffectInParameterVersion1 parameter, PoolMapper mapper)
{
- Update(out updateErrorInfo, ref parameter, mapper);
+ Update(out updateErrorInfo, in parameter, mapper);
}
- public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, ref EffectInParameterVersion2 parameter, PoolMapper mapper)
+ public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, in EffectInParameterVersion2 parameter, PoolMapper mapper)
{
- Update(out updateErrorInfo, ref parameter, mapper);
+ Update(out updateErrorInfo, in parameter, mapper);
}
- public void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, ref T parameter, PoolMapper mapper) where T : unmanaged, IEffectInParameter
+ public void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, in T parameter, PoolMapper mapper) where T : unmanaged, IEffectInParameter
{
- Debug.Assert(IsTypeValid(ref parameter));
+ Debug.Assert(IsTypeValid(in parameter));
- UpdateParameterBase(ref parameter);
+ UpdateParameterBase(in parameter);
Parameter = MemoryMarshal.Cast(parameter.SpecificData)[0];
IsEnabled = parameter.IsEnabled;
diff --git a/src/Ryujinx.Audio/Renderer/Server/Effect/CaptureBufferEffect.cs b/src/Ryujinx.Audio/Renderer/Server/Effect/CaptureBufferEffect.cs
index 5be4b4ed5..6917222f0 100644
--- a/src/Ryujinx.Audio/Renderer/Server/Effect/CaptureBufferEffect.cs
+++ b/src/Ryujinx.Audio/Renderer/Server/Effect/CaptureBufferEffect.cs
@@ -32,21 +32,21 @@ namespace Ryujinx.Audio.Renderer.Server.Effect
return WorkBuffers[index].GetReference(true);
}
- public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, ref EffectInParameterVersion1 parameter, PoolMapper mapper)
+ public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, in EffectInParameterVersion1 parameter, PoolMapper mapper)
{
- Update(out updateErrorInfo, ref parameter, mapper);
+ Update(out updateErrorInfo, in parameter, mapper);
}
- public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, ref EffectInParameterVersion2 parameter, PoolMapper mapper)
+ public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, in EffectInParameterVersion2 parameter, PoolMapper mapper)
{
- Update(out updateErrorInfo, ref parameter, mapper);
+ Update(out updateErrorInfo, in parameter, mapper);
}
- public void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, ref T parameter, PoolMapper mapper) where T : unmanaged, IEffectInParameter
+ public void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, in T parameter, PoolMapper mapper) where T : unmanaged, IEffectInParameter
{
- Debug.Assert(IsTypeValid(ref parameter));
+ Debug.Assert(IsTypeValid(in parameter));
- UpdateParameterBase(ref parameter);
+ UpdateParameterBase(in parameter);
Parameter = MemoryMarshal.Cast(parameter.SpecificData)[0];
IsEnabled = parameter.IsEnabled;
diff --git a/src/Ryujinx.Audio/Renderer/Server/Effect/CompressorEffect.cs b/src/Ryujinx.Audio/Renderer/Server/Effect/CompressorEffect.cs
index 826c32cb0..de0f44e47 100644
--- a/src/Ryujinx.Audio/Renderer/Server/Effect/CompressorEffect.cs
+++ b/src/Ryujinx.Audio/Renderer/Server/Effect/CompressorEffect.cs
@@ -39,17 +39,17 @@ namespace Ryujinx.Audio.Renderer.Server.Effect
return GetSingleBuffer();
}
- public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, ref EffectInParameterVersion1 parameter, PoolMapper mapper)
+ public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, in EffectInParameterVersion1 parameter, PoolMapper mapper)
{
// Nintendo doesn't do anything here but we still require updateErrorInfo to be initialised.
updateErrorInfo = new BehaviourParameter.ErrorInfo();
}
- public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, ref EffectInParameterVersion2 parameter, PoolMapper mapper)
+ public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, in EffectInParameterVersion2 parameter, PoolMapper mapper)
{
- Debug.Assert(IsTypeValid(ref parameter));
+ Debug.Assert(IsTypeValid(in parameter));
- UpdateParameterBase(ref parameter);
+ UpdateParameterBase(in parameter);
Parameter = MemoryMarshal.Cast(parameter.SpecificData)[0];
IsEnabled = parameter.IsEnabled;
@@ -62,6 +62,19 @@ namespace Ryujinx.Audio.Renderer.Server.Effect
UpdateUsageStateForCommandGeneration();
Parameter.Status = UsageState.Enabled;
+ Parameter.StatisticsReset = false;
+ }
+
+ public override void InitializeResultState(ref EffectResultState state)
+ {
+ ref CompressorStatistics statistics = ref MemoryMarshal.Cast(state.SpecificData)[0];
+
+ statistics.Reset(Parameter.ChannelCount);
+ }
+
+ public override void UpdateResultState(ref EffectResultState destState, ref EffectResultState srcState)
+ {
+ destState = srcState;
}
}
}
diff --git a/src/Ryujinx.Audio/Renderer/Server/Effect/DelayEffect.cs b/src/Ryujinx.Audio/Renderer/Server/Effect/DelayEffect.cs
index 43cabb7db..9db1ce465 100644
--- a/src/Ryujinx.Audio/Renderer/Server/Effect/DelayEffect.cs
+++ b/src/Ryujinx.Audio/Renderer/Server/Effect/DelayEffect.cs
@@ -37,19 +37,19 @@ namespace Ryujinx.Audio.Renderer.Server.Effect
return GetSingleBuffer();
}
- public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, ref EffectInParameterVersion1 parameter, PoolMapper mapper)
+ public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, in EffectInParameterVersion1 parameter, PoolMapper mapper)
{
- Update(out updateErrorInfo, ref parameter, mapper);
+ Update(out updateErrorInfo, in parameter, mapper);
}
- public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, ref EffectInParameterVersion2 parameter, PoolMapper mapper)
+ public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, in EffectInParameterVersion2 parameter, PoolMapper mapper)
{
- Update(out updateErrorInfo, ref parameter, mapper);
+ Update(out updateErrorInfo, in parameter, mapper);
}
- public void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, ref T parameter, PoolMapper mapper) where T : unmanaged, IEffectInParameter
+ public void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, in T parameter, PoolMapper mapper) where T : unmanaged, IEffectInParameter
{
- Debug.Assert(IsTypeValid(ref parameter));
+ Debug.Assert(IsTypeValid(in parameter));
ref DelayParameter delayParameter = ref MemoryMarshal.Cast(parameter.SpecificData)[0];
@@ -57,7 +57,7 @@ namespace Ryujinx.Audio.Renderer.Server.Effect
if (delayParameter.IsChannelCountMaxValid())
{
- UpdateParameterBase(ref parameter);
+ UpdateParameterBase(in parameter);
UsageState oldParameterStatus = Parameter.Status;
diff --git a/src/Ryujinx.Audio/Renderer/Server/Effect/LimiterEffect.cs b/src/Ryujinx.Audio/Renderer/Server/Effect/LimiterEffect.cs
index 3e2f7326d..d9b3d5666 100644
--- a/src/Ryujinx.Audio/Renderer/Server/Effect/LimiterEffect.cs
+++ b/src/Ryujinx.Audio/Renderer/Server/Effect/LimiterEffect.cs
@@ -39,25 +39,25 @@ namespace Ryujinx.Audio.Renderer.Server.Effect
return GetSingleBuffer();
}
- public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, ref EffectInParameterVersion1 parameter, PoolMapper mapper)
+ public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, in EffectInParameterVersion1 parameter, PoolMapper mapper)
{
- Update(out updateErrorInfo, ref parameter, mapper);
+ Update(out updateErrorInfo, in parameter, mapper);
}
- public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, ref EffectInParameterVersion2 parameter, PoolMapper mapper)
+ public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, in EffectInParameterVersion2 parameter, PoolMapper mapper)
{
- Update(out updateErrorInfo, ref parameter, mapper);
+ Update(out updateErrorInfo, in parameter, mapper);
}
- public void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, ref T parameter, PoolMapper mapper) where T : unmanaged, IEffectInParameter
+ public void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, in T parameter, PoolMapper mapper) where T : unmanaged, IEffectInParameter
{
- Debug.Assert(IsTypeValid(ref parameter));
+ Debug.Assert(IsTypeValid(in parameter));
ref LimiterParameter limiterParameter = ref MemoryMarshal.Cast(parameter.SpecificData)[0];
updateErrorInfo = new BehaviourParameter.ErrorInfo();
- UpdateParameterBase(ref parameter);
+ UpdateParameterBase(in parameter);
Parameter = limiterParameter;
diff --git a/src/Ryujinx.Audio/Renderer/Server/Effect/Reverb3dEffect.cs b/src/Ryujinx.Audio/Renderer/Server/Effect/Reverb3dEffect.cs
index f9d7f4943..4b13cfec6 100644
--- a/src/Ryujinx.Audio/Renderer/Server/Effect/Reverb3dEffect.cs
+++ b/src/Ryujinx.Audio/Renderer/Server/Effect/Reverb3dEffect.cs
@@ -36,19 +36,19 @@ namespace Ryujinx.Audio.Renderer.Server.Effect
return GetSingleBuffer();
}
- public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, ref EffectInParameterVersion1 parameter, PoolMapper mapper)
+ public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, in EffectInParameterVersion1 parameter, PoolMapper mapper)
{
- Update(out updateErrorInfo, ref parameter, mapper);
+ Update(out updateErrorInfo, in parameter, mapper);
}
- public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, ref EffectInParameterVersion2 parameter, PoolMapper mapper)
+ public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, in EffectInParameterVersion2 parameter, PoolMapper mapper)
{
- Update(out updateErrorInfo, ref parameter, mapper);
+ Update(out updateErrorInfo, in parameter, mapper);
}
- public void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, ref T parameter, PoolMapper mapper) where T : unmanaged, IEffectInParameter
+ public void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, in T parameter, PoolMapper mapper) where T : unmanaged, IEffectInParameter
{
- Debug.Assert(IsTypeValid(ref parameter));
+ Debug.Assert(IsTypeValid(in parameter));
ref Reverb3dParameter reverbParameter = ref MemoryMarshal.Cast(parameter.SpecificData)[0];
@@ -56,7 +56,7 @@ namespace Ryujinx.Audio.Renderer.Server.Effect
if (reverbParameter.IsChannelCountMaxValid())
{
- UpdateParameterBase(ref parameter);
+ UpdateParameterBase(in parameter);
UsageState oldParameterStatus = Parameter.ParameterStatus;
diff --git a/src/Ryujinx.Audio/Renderer/Server/Effect/ReverbEffect.cs b/src/Ryujinx.Audio/Renderer/Server/Effect/ReverbEffect.cs
index 6fdf8fc23..aa6e67448 100644
--- a/src/Ryujinx.Audio/Renderer/Server/Effect/ReverbEffect.cs
+++ b/src/Ryujinx.Audio/Renderer/Server/Effect/ReverbEffect.cs
@@ -39,19 +39,19 @@ namespace Ryujinx.Audio.Renderer.Server.Effect
return GetSingleBuffer();
}
- public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, ref EffectInParameterVersion1 parameter, PoolMapper mapper)
+ public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, in EffectInParameterVersion1 parameter, PoolMapper mapper)
{
- Update(out updateErrorInfo, ref parameter, mapper);
+ Update(out updateErrorInfo, in parameter, mapper);
}
- public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, ref EffectInParameterVersion2 parameter, PoolMapper mapper)
+ public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, in EffectInParameterVersion2 parameter, PoolMapper mapper)
{
- Update(out updateErrorInfo, ref parameter, mapper);
+ Update(out updateErrorInfo, in parameter, mapper);
}
- public void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, ref T parameter, PoolMapper mapper) where T : unmanaged, IEffectInParameter
+ public void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, in T parameter, PoolMapper mapper) where T : unmanaged, IEffectInParameter
{
- Debug.Assert(IsTypeValid(ref parameter));
+ Debug.Assert(IsTypeValid(in parameter));
ref ReverbParameter reverbParameter = ref MemoryMarshal.Cast(parameter.SpecificData)[0];
@@ -59,7 +59,7 @@ namespace Ryujinx.Audio.Renderer.Server.Effect
if (reverbParameter.IsChannelCountMaxValid())
{
- UpdateParameterBase(ref parameter);
+ UpdateParameterBase(in parameter);
UsageState oldParameterStatus = Parameter.Status;
diff --git a/src/Ryujinx.Audio/Renderer/Server/ICommandProcessingTimeEstimator.cs b/src/Ryujinx.Audio/Renderer/Server/ICommandProcessingTimeEstimator.cs
index 27b22363a..9c4312ad6 100644
--- a/src/Ryujinx.Audio/Renderer/Server/ICommandProcessingTimeEstimator.cs
+++ b/src/Ryujinx.Audio/Renderer/Server/ICommandProcessingTimeEstimator.cs
@@ -33,8 +33,10 @@ namespace Ryujinx.Audio.Renderer.Server
uint Estimate(UpsampleCommand command);
uint Estimate(LimiterCommandVersion1 command);
uint Estimate(LimiterCommandVersion2 command);
- uint Estimate(GroupedBiquadFilterCommand command);
+ uint Estimate(MultiTapBiquadFilterCommand command);
uint Estimate(CaptureBufferCommand command);
uint Estimate(CompressorCommand command);
+ uint Estimate(BiquadFilterAndMixCommand command);
+ uint Estimate(MultiTapBiquadFilterAndMixCommand command);
}
}
diff --git a/src/Ryujinx.Audio/Renderer/Server/MemoryPool/PoolMapper.cs b/src/Ryujinx.Audio/Renderer/Server/MemoryPool/PoolMapper.cs
index 391b80f8d..f67d0c124 100644
--- a/src/Ryujinx.Audio/Renderer/Server/MemoryPool/PoolMapper.cs
+++ b/src/Ryujinx.Audio/Renderer/Server/MemoryPool/PoolMapper.cs
@@ -249,7 +249,7 @@ namespace Ryujinx.Audio.Renderer.Server.MemoryPool
/// Input user parameter.
/// Output user parameter.
/// Returns the of the operations performed.
- public UpdateResult Update(ref MemoryPoolState memoryPool, ref MemoryPoolInParameter inParameter, ref MemoryPoolOutStatus outStatus)
+ public UpdateResult Update(ref MemoryPoolState memoryPool, in MemoryPoolInParameter inParameter, ref MemoryPoolOutStatus outStatus)
{
MemoryPoolUserState inputState = inParameter.State;
diff --git a/src/Ryujinx.Audio/Renderer/Server/Mix/MixState.cs b/src/Ryujinx.Audio/Renderer/Server/Mix/MixState.cs
index 88ae44831..5ba58ea5b 100644
--- a/src/Ryujinx.Audio/Renderer/Server/Mix/MixState.cs
+++ b/src/Ryujinx.Audio/Renderer/Server/Mix/MixState.cs
@@ -195,7 +195,7 @@ namespace Ryujinx.Audio.Renderer.Server.Mix
/// The input parameter of the mix.
/// The splitter context.
/// Return true, new connections were done on the adjacency matrix.
- private bool UpdateConnection(EdgeMatrix edgeMatrix, ref MixParameter parameter, ref SplitterContext splitterContext)
+ private bool UpdateConnection(EdgeMatrix edgeMatrix, in MixParameter parameter, ref SplitterContext splitterContext)
{
bool hasNewConnections;
@@ -225,11 +225,11 @@ namespace Ryujinx.Audio.Renderer.Server.Mix
for (int i = 0; i < splitter.DestinationCount; i++)
{
- Span destination = splitter.GetData(i);
+ SplitterDestination destination = splitter.GetData(i);
- if (!destination.IsEmpty)
+ if (!destination.IsNull)
{
- int destinationMixId = destination[0].DestinationId;
+ int destinationMixId = destination.DestinationId;
if (destinationMixId != UnusedMixId)
{
@@ -259,7 +259,7 @@ namespace Ryujinx.Audio.Renderer.Server.Mix
/// The splitter context.
/// The behaviour context.
/// Return true if the mix was changed.
- public bool Update(EdgeMatrix edgeMatrix, ref MixParameter parameter, EffectContext effectContext, SplitterContext splitterContext, BehaviourContext behaviourContext)
+ public bool Update(EdgeMatrix edgeMatrix, in MixParameter parameter, EffectContext effectContext, SplitterContext splitterContext, BehaviourContext behaviourContext)
{
bool isDirty;
@@ -273,7 +273,7 @@ namespace Ryujinx.Audio.Renderer.Server.Mix
if (behaviourContext.IsSplitterSupported())
{
- isDirty = UpdateConnection(edgeMatrix, ref parameter, ref splitterContext);
+ isDirty = UpdateConnection(edgeMatrix, in parameter, ref splitterContext);
}
else
{
diff --git a/src/Ryujinx.Audio/Renderer/Server/Performance/PerformanceManager.cs b/src/Ryujinx.Audio/Renderer/Server/Performance/PerformanceManager.cs
index 0a035916c..da5a0ad45 100644
--- a/src/Ryujinx.Audio/Renderer/Server/Performance/PerformanceManager.cs
+++ b/src/Ryujinx.Audio/Renderer/Server/Performance/PerformanceManager.cs
@@ -18,16 +18,12 @@ namespace Ryujinx.Audio.Renderer.Server.Performance
if (version == 2)
{
- return (ulong)PerformanceManagerGeneric.GetRequiredBufferSizeForPerformanceMetricsPerFrame(ref parameter);
+ return (ulong)PerformanceManagerGeneric.GetRequiredBufferSizeForPerformanceMetricsPerFrame(ref parameter);
}
if (version == 1)
{
- return (ulong)PerformanceManagerGeneric.GetRequiredBufferSizeForPerformanceMetricsPerFrame(ref parameter);
+ return (ulong)PerformanceManagerGeneric.GetRequiredBufferSizeForPerformanceMetricsPerFrame(ref parameter);
}
throw new NotImplementedException($"Unknown Performance metrics data format version {version}");
diff --git a/src/Ryujinx.Audio/Renderer/Server/Performance/PerformanceManagerGeneric.cs b/src/Ryujinx.Audio/Renderer/Server/Performance/PerformanceManagerGeneric.cs
index 5a70a1bcf..2e5d25b9c 100644
--- a/src/Ryujinx.Audio/Renderer/Server/Performance/PerformanceManagerGeneric.cs
+++ b/src/Ryujinx.Audio/Renderer/Server/Performance/PerformanceManagerGeneric.cs
@@ -234,7 +234,7 @@ namespace Ryujinx.Audio.Renderer.Server.Performance
{
performanceEntry = null;
- if (_entryDetailIndex > MaxFrameDetailCount)
+ if (_entryDetailIndex >= MaxFrameDetailCount)
{
return false;
}
@@ -245,7 +245,7 @@ namespace Ryujinx.Audio.Renderer.Server.Performance
EntryCountOffset = (uint)CurrentHeader.GetEntryCountOffset(),
};
- uint baseEntryOffset = (uint)(Unsafe.SizeOf() + GetEntriesSize() + Unsafe.SizeOf() * _entryDetailIndex);
+ uint baseEntryOffset = (uint)(Unsafe.SizeOf() + GetEntriesSize() + Unsafe.SizeOf() * _entryDetailIndex);
ref TEntryDetail entryDetail = ref EntriesDetail[_entryDetailIndex];
diff --git a/src/Ryujinx.Audio/Renderer/Server/Sink/BaseSink.cs b/src/Ryujinx.Audio/Renderer/Server/Sink/BaseSink.cs
index d36c5e260..8c65e09bc 100644
--- a/src/Ryujinx.Audio/Renderer/Server/Sink/BaseSink.cs
+++ b/src/Ryujinx.Audio/Renderer/Server/Sink/BaseSink.cs
@@ -59,7 +59,7 @@ namespace Ryujinx.Audio.Renderer.Server.Sink
///
/// The user parameter.
/// Return true, if the sent by the user match the internal .
- public bool IsTypeValid(ref SinkInParameter parameter)
+ public bool IsTypeValid(in SinkInParameter parameter)
{
return parameter.Type == TargetSinkType;
}
@@ -76,7 +76,7 @@ namespace Ryujinx.Audio.Renderer.Server.Sink
/// Update the internal common parameters from user parameter.
///
/// The user parameter.
- protected void UpdateStandardParameter(ref SinkInParameter parameter)
+ protected void UpdateStandardParameter(in SinkInParameter parameter)
{
if (IsUsed != parameter.IsUsed)
{
@@ -92,9 +92,9 @@ namespace Ryujinx.Audio.Renderer.Server.Sink
/// The user parameter.
/// The user output status.
/// The mapper to use.
- public virtual void Update(out ErrorInfo errorInfo, ref SinkInParameter parameter, ref SinkOutStatus outStatus, PoolMapper mapper)
+ public virtual void Update(out ErrorInfo errorInfo, in SinkInParameter parameter, ref SinkOutStatus outStatus, PoolMapper mapper)
{
- Debug.Assert(IsTypeValid(ref parameter));
+ Debug.Assert(IsTypeValid(in parameter));
errorInfo = new ErrorInfo();
}
diff --git a/src/Ryujinx.Audio/Renderer/Server/Sink/CircularBufferSink.cs b/src/Ryujinx.Audio/Renderer/Server/Sink/CircularBufferSink.cs
index 097757988..f2751cf29 100644
--- a/src/Ryujinx.Audio/Renderer/Server/Sink/CircularBufferSink.cs
+++ b/src/Ryujinx.Audio/Renderer/Server/Sink/CircularBufferSink.cs
@@ -44,18 +44,18 @@ namespace Ryujinx.Audio.Renderer.Server.Sink
public override SinkType TargetSinkType => SinkType.CircularBuffer;
- public override void Update(out BehaviourParameter.ErrorInfo errorInfo, ref SinkInParameter parameter, ref SinkOutStatus outStatus, PoolMapper mapper)
+ public override void Update(out BehaviourParameter.ErrorInfo errorInfo, in SinkInParameter parameter, ref SinkOutStatus outStatus, PoolMapper mapper)
{
errorInfo = new BehaviourParameter.ErrorInfo();
outStatus = new SinkOutStatus();
- Debug.Assert(IsTypeValid(ref parameter));
+ Debug.Assert(IsTypeValid(in parameter));
ref CircularBufferParameter inputDeviceParameter = ref MemoryMarshal.Cast(parameter.SpecificData)[0];
if (parameter.IsUsed != IsUsed || ShouldSkip)
{
- UpdateStandardParameter(ref parameter);
+ UpdateStandardParameter(in parameter);
if (parameter.IsUsed)
{
diff --git a/src/Ryujinx.Audio/Renderer/Server/Sink/DeviceSink.cs b/src/Ryujinx.Audio/Renderer/Server/Sink/DeviceSink.cs
index e03fe11d4..afe2d4b1b 100644
--- a/src/Ryujinx.Audio/Renderer/Server/Sink/DeviceSink.cs
+++ b/src/Ryujinx.Audio/Renderer/Server/Sink/DeviceSink.cs
@@ -49,15 +49,15 @@ namespace Ryujinx.Audio.Renderer.Server.Sink
public override SinkType TargetSinkType => SinkType.Device;
- public override void Update(out BehaviourParameter.ErrorInfo errorInfo, ref SinkInParameter parameter, ref SinkOutStatus outStatus, PoolMapper mapper)
+ public override void Update(out BehaviourParameter.ErrorInfo errorInfo, in SinkInParameter parameter, ref SinkOutStatus outStatus, PoolMapper mapper)
{
- Debug.Assert(IsTypeValid(ref parameter));
+ Debug.Assert(IsTypeValid(in parameter));
ref DeviceParameter inputDeviceParameter = ref MemoryMarshal.Cast(parameter.SpecificData)[0];
if (parameter.IsUsed != IsUsed)
{
- UpdateStandardParameter(ref parameter);
+ UpdateStandardParameter(in parameter);
Parameter = inputDeviceParameter;
}
else
diff --git a/src/Ryujinx.Audio/Renderer/Server/Splitter/SplitterContext.cs b/src/Ryujinx.Audio/Renderer/Server/Splitter/SplitterContext.cs
index e408692ab..6dddb4315 100644
--- a/src/Ryujinx.Audio/Renderer/Server/Splitter/SplitterContext.cs
+++ b/src/Ryujinx.Audio/Renderer/Server/Splitter/SplitterContext.cs
@@ -1,11 +1,13 @@
using Ryujinx.Audio.Renderer.Common;
+using Ryujinx.Audio.Renderer.Dsp.State;
using Ryujinx.Audio.Renderer.Parameter;
using Ryujinx.Audio.Renderer.Utils;
using Ryujinx.Common;
+using Ryujinx.Common.Extensions;
using System;
+using System.Buffers;
using System.Diagnostics;
using System.Runtime.CompilerServices;
-using System.Runtime.InteropServices;
namespace Ryujinx.Audio.Renderer.Server.Splitter
{
@@ -14,33 +16,63 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
///
public class SplitterContext
{
+ ///
+ /// Amount of biquad filter states per splitter destination.
+ ///
+ public const int BqfStatesPerDestination = 4;
+
///
/// Storage for .
///
private Memory _splitters;
///
- /// Storage for .
+ /// Storage for .
///
- private Memory _splitterDestinations;
+ private Memory _splitterDestinationsV1;
///
- /// If set to true, trust the user destination count in .
+ /// Storage for .
+ ///
+ private Memory _splitterDestinationsV2;
+
+ ///
+ /// Splitter biquad filtering states.
+ ///
+ private Memory _splitterBqfStates;
+
+ ///
+ /// Version of the splitter context that is being used, currently can be 1 or 2.
+ ///
+ public int Version { get; private set; }
+
+ ///
+ /// If set to true, trust the user destination count in .
///
public bool IsBugFixed { get; private set; }
+ ///
+ /// If set to true, the previous mix volume is explicitly resetted using the input parameter, instead of implicitly on first use.
+ ///
+ public bool IsSplitterPrevVolumeResetSupported { get; private set; }
+
///
/// Initialize .
///
/// The behaviour context.
/// The audio renderer configuration.
/// The .
+ /// Memory to store the biquad filtering state for splitters during processing.
/// Return true if the initialization was successful.
- public bool Initialize(ref BehaviourContext behaviourContext, ref AudioRendererConfiguration parameter, WorkBufferAllocator workBufferAllocator)
+ public bool Initialize(
+ ref BehaviourContext behaviourContext,
+ ref AudioRendererConfiguration parameter,
+ WorkBufferAllocator workBufferAllocator,
+ Memory splitterBqfStates)
{
if (!behaviourContext.IsSplitterSupported() || parameter.SplitterCount <= 0 || parameter.SplitterDestinationCount <= 0)
{
- Setup(Memory.Empty, Memory.Empty, false);
+ Setup(Memory.Empty, Memory.Empty, Memory.Empty, false);
return true;
}
@@ -59,23 +91,64 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
splitter = new SplitterState(splitterId++);
}
- Memory splitterDestinations = workBufferAllocator.Allocate(parameter.SplitterDestinationCount,
- SplitterDestination.Alignment);
+ Memory splitterDestinationsV1 = Memory.Empty;
+ Memory splitterDestinationsV2 = Memory.Empty;
- if (splitterDestinations.IsEmpty)
+ if (!behaviourContext.IsBiquadFilterParameterForSplitterEnabled())
{
- return false;
+ Version = 1;
+
+ splitterDestinationsV1 = workBufferAllocator.Allocate(parameter.SplitterDestinationCount,
+ SplitterDestinationVersion1.Alignment);
+
+ if (splitterDestinationsV1.IsEmpty)
+ {
+ return false;
+ }
+
+ int splitterDestinationId = 0;
+ foreach (ref SplitterDestinationVersion1 data in splitterDestinationsV1.Span)
+ {
+ data = new SplitterDestinationVersion1(splitterDestinationId++);
+ }
+ }
+ else
+ {
+ Version = 2;
+
+ splitterDestinationsV2 = workBufferAllocator.Allocate(parameter.SplitterDestinationCount,
+ SplitterDestinationVersion2.Alignment);
+
+ if (splitterDestinationsV2.IsEmpty)
+ {
+ return false;
+ }
+
+ int splitterDestinationId = 0;
+ foreach (ref SplitterDestinationVersion2 data in splitterDestinationsV2.Span)
+ {
+ data = new SplitterDestinationVersion2(splitterDestinationId++);
+ }
+
+ if (parameter.SplitterDestinationCount > 0)
+ {
+ // Official code stores it in the SplitterDestinationVersion2 struct,
+ // but we don't to avoid using unsafe code.
+
+ splitterBqfStates.Span.Clear();
+ _splitterBqfStates = splitterBqfStates;
+ }
+ else
+ {
+ _splitterBqfStates = Memory.Empty;
+ }
}
- int splitterDestinationId = 0;
- foreach (ref SplitterDestination data in splitterDestinations.Span)
- {
- data = new SplitterDestination(splitterDestinationId++);
- }
+ IsSplitterPrevVolumeResetSupported = behaviourContext.IsSplitterPrevVolumeResetSupported();
SplitterState.InitializeSplitters(splitters.Span);
- Setup(splitters, splitterDestinations, behaviourContext.IsSplitterBugFixed());
+ Setup(splitters, splitterDestinationsV1, splitterDestinationsV2, behaviourContext.IsSplitterBugFixed());
return true;
}
@@ -92,7 +165,15 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
if (behaviourContext.IsSplitterSupported())
{
size = WorkBufferAllocator.GetTargetSize(size, parameter.SplitterCount, SplitterState.Alignment);
- size = WorkBufferAllocator.GetTargetSize(size, parameter.SplitterDestinationCount, SplitterDestination.Alignment);
+
+ if (behaviourContext.IsBiquadFilterParameterForSplitterEnabled())
+ {
+ size = WorkBufferAllocator.GetTargetSize(size, parameter.SplitterDestinationCount, SplitterDestinationVersion2.Alignment);
+ }
+ else
+ {
+ size = WorkBufferAllocator.GetTargetSize(size, parameter.SplitterDestinationCount, SplitterDestinationVersion1.Alignment);
+ }
if (behaviourContext.IsSplitterBugFixed())
{
@@ -109,12 +190,18 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
/// Setup the instance.
///
/// The storage.
- /// The storage.
- /// If set to true, trust the user destination count in .
- private void Setup(Memory splitters, Memory splitterDestinations, bool isBugFixed)
+ /// The storage.
+ /// The storage.
+ /// If set to true, trust the user destination count in .
+ private void Setup(
+ Memory splitters,
+ Memory splitterDestinationsV1,
+ Memory splitterDestinationsV2,
+ bool isBugFixed)
{
_splitters = splitters;
- _splitterDestinations = splitterDestinations;
+ _splitterDestinationsV1 = splitterDestinationsV1;
+ _splitterDestinationsV2 = splitterDestinationsV2;
IsBugFixed = isBugFixed;
}
@@ -140,7 +227,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;
}
///
@@ -148,11 +237,11 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
///
/// The splitter header.
/// The raw data after the splitter header.
- private void UpdateState(scoped ref SplitterInParameterHeader inputHeader, ref ReadOnlySpan input)
+ private void UpdateState(in SplitterInParameterHeader inputHeader, ref SequenceReader input)
{
for (int i = 0; i < inputHeader.SplitterCount; i++)
{
- SplitterInParameter parameter = MemoryMarshal.Read(input);
+ ref readonly SplitterInParameter parameter = ref input.GetRefOrRefToCopy(out _);
Debug.Assert(parameter.IsMagicValid());
@@ -162,37 +251,78 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
{
ref SplitterState splitter = ref GetState(parameter.Id);
- splitter.Update(this, ref parameter, input[Unsafe.SizeOf()..]);
+ splitter.Update(this, in parameter, ref input);
}
- input = input[(0x1C + parameter.DestinationCount * 4)..];
+ // NOTE: there are 12 bytes of unused/unknown data after the destination IDs array.
+ input.Advance(0xC);
+ }
+ else
+ {
+ input.Rewind(Unsafe.SizeOf());
+ break;
}
}
}
///
- /// Update one or multiple from user parameters.
+ /// Update one splitter destination data from user parameters.
+ ///
+ /// The raw data after the splitter header.
+ /// True if the update was successful, false otherwise
+ private bool UpdateData(ref SequenceReader input) where T : unmanaged, ISplitterDestinationInParameter
+ {
+ ref readonly T parameter = ref input.GetRefOrRefToCopy(out _);
+
+ Debug.Assert(parameter.IsMagicValid());
+
+ if (parameter.IsMagicValid())
+ {
+ int length = _splitterDestinationsV2.IsEmpty ? _splitterDestinationsV1.Length : _splitterDestinationsV2.Length;
+
+ if (parameter.Id >= 0 && parameter.Id < length)
+ {
+ SplitterDestination destination = GetDestination(parameter.Id);
+
+ destination.Update(parameter, IsSplitterPrevVolumeResetSupported);
+ }
+
+ return true;
+ }
+ else
+ {
+ input.Rewind(Unsafe.SizeOf());
+
+ return false;
+ }
+ }
+
+ ///
+ /// Update one or multiple splitter destination data from user parameters.
///
/// The splitter header.
/// The raw data after the splitter header.
- private void UpdateData(scoped ref SplitterInParameterHeader inputHeader, ref ReadOnlySpan input)
+ private void UpdateData(in SplitterInParameterHeader inputHeader, ref SequenceReader input)
{
for (int i = 0; i < inputHeader.SplitterDestinationCount; i++)
{
- SplitterDestinationInParameter parameter = MemoryMarshal.Read(input);
-
- Debug.Assert(parameter.IsMagicValid());
-
- if (parameter.IsMagicValid())
+ if (Version == 1)
{
- if (parameter.Id >= 0 && parameter.Id < _splitterDestinations.Length)
+ if (!UpdateData(ref input))
{
- ref SplitterDestination destination = ref GetDestination(parameter.Id);
-
- destination.Update(parameter);
+ break;
}
-
- input = input[Unsafe.SizeOf()..];
+ }
+ else if (Version == 2)
+ {
+ if (!UpdateData(ref input))
+ {
+ break;
+ }
+ }
+ else
+ {
+ Debug.Fail($"Invalid splitter context version {Version}.");
}
}
}
@@ -201,36 +331,33 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
/// Update splitter from user parameters.
///
/// The input raw user data.
- /// The total consumed size.
/// Return true if the update was successful.
- public bool Update(ReadOnlySpan input, out int consumedSize)
+ public bool Update(ref SequenceReader input)
{
- if (_splitterDestinations.IsEmpty || _splitters.IsEmpty)
+ if (!UsingSplitter())
{
- consumedSize = 0;
-
return true;
}
- int originalSize = input.Length;
-
- SplitterInParameterHeader header = SpanIOHelper.Read(ref input);
+ ref readonly SplitterInParameterHeader header = ref input.GetRefOrRefToCopy(out _);
if (header.IsMagicValid())
{
ClearAllNewConnectionFlag();
- UpdateState(ref header, ref input);
- UpdateData(ref header, ref input);
+ UpdateState(in header, ref input);
+ UpdateData(in header, ref input);
- consumedSize = BitUtils.AlignUp(originalSize - input.Length, 0x10);
+ input.SetConsumed(BitUtils.AlignUp(input.Consumed, 0x10));
return true;
}
+ else
+ {
+ input.Rewind(Unsafe.SizeOf());
- consumedSize = 0;
-
- return false;
+ return false;
+ }
}
///
@@ -244,45 +371,52 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
}
///
- /// Get a reference to a at the given .
+ /// Get a reference to the splitter destination data at the given .
///
/// The index to use.
- /// A reference to a at the given .
- public ref SplitterDestination GetDestination(int id)
+ /// A reference to the splitter destination data at the given .
+ public SplitterDestination GetDestination(int id)
{
- return ref SpanIOHelper.GetFromMemory(_splitterDestinations, id, (uint)_splitterDestinations.Length);
+ if (_splitterDestinationsV2.IsEmpty)
+ {
+ return new SplitterDestination(ref SpanIOHelper.GetFromMemory(_splitterDestinationsV1, id, (uint)_splitterDestinationsV1.Length));
+ }
+ else
+ {
+ return new SplitterDestination(ref SpanIOHelper.GetFromMemory(_splitterDestinationsV2, id, (uint)_splitterDestinationsV2.Length));
+ }
}
///
- /// Get a at the given .
- ///
- /// The index to use.
- /// A at the given .
- public Memory GetDestinationMemory(int id)
- {
- return SpanIOHelper.GetMemory(_splitterDestinations, id, (uint)_splitterDestinations.Length);
- }
-
- ///
- /// Get a in the at and pass to .
+ /// Get a in the at and pass to .
///
/// The index to use to get the .
/// The index of the .
- /// A .
- public Span GetDestination(int id, int destinationId)
+ /// A .
+ public SplitterDestination GetDestination(int id, int destinationId)
{
ref SplitterState splitter = ref GetState(id);
return splitter.GetData(destinationId);
}
+ ///
+ /// Gets the biquad filter state for a given splitter destination.
+ ///
+ /// The splitter destination.
+ /// Biquad filter state for the specified destination.
+ public Memory GetBiquadFilterState(SplitterDestination destination)
+ {
+ return _splitterBqfStates.Slice(destination.Id * BqfStatesPerDestination, BqfStatesPerDestination);
+ }
+
///
/// Return true if the audio renderer has any splitters.
///
/// True if the audio renderer has any splitters.
public bool UsingSplitter()
{
- return !_splitters.IsEmpty && !_splitterDestinations.IsEmpty;
+ return !_splitters.IsEmpty && (!_splitterDestinationsV1.IsEmpty || !_splitterDestinationsV2.IsEmpty);
}
///
diff --git a/src/Ryujinx.Audio/Renderer/Server/Splitter/SplitterDestination.cs b/src/Ryujinx.Audio/Renderer/Server/Splitter/SplitterDestination.cs
index 1faf7921f..1a46d41fd 100644
--- a/src/Ryujinx.Audio/Renderer/Server/Splitter/SplitterDestination.cs
+++ b/src/Ryujinx.Audio/Renderer/Server/Splitter/SplitterDestination.cs
@@ -1,115 +1,199 @@
using Ryujinx.Audio.Renderer.Parameter;
-using Ryujinx.Common.Utilities;
using System;
using System.Diagnostics;
-using System.Runtime.InteropServices;
+using System.Runtime.CompilerServices;
namespace Ryujinx.Audio.Renderer.Server.Splitter
{
///
/// Server state for a splitter destination.
///
- [StructLayout(LayoutKind.Sequential, Size = 0xE0, Pack = Alignment)]
- public struct SplitterDestination
+ public ref struct SplitterDestination
{
- public const int Alignment = 0x10;
+ private ref SplitterDestinationVersion1 _v1;
+ private ref SplitterDestinationVersion2 _v2;
///
- /// The unique id of this .
+ /// Checks if the splitter destination data reference is null.
///
- public int Id;
+ public bool IsNull => Unsafe.IsNullRef(ref _v1) && Unsafe.IsNullRef(ref _v2);
///
- /// The mix to output the result of the splitter.
+ /// The splitter unique id.
///
- public int DestinationId;
-
- ///
- /// Mix buffer volumes storage.
- ///
- private MixArray _mix;
- private MixArray _previousMix;
-
- ///
- /// Pointer to the next linked element.
- ///
- private unsafe SplitterDestination* _next;
-
- ///
- /// Set to true if in use.
- ///
- [MarshalAs(UnmanagedType.I1)]
- public bool IsUsed;
-
- ///
- /// Set to true if the internal state need to be updated.
- ///
- [MarshalAs(UnmanagedType.I1)]
- public bool NeedToUpdateInternalState;
-
- [StructLayout(LayoutKind.Sequential, Size = 4 * Constants.MixBufferCountMax, Pack = 1)]
- private struct MixArray { }
-
- ///
- /// Mix buffer volumes.
- ///
- /// Used when a splitter id is specified in the mix.
- public Span MixBufferVolume => SpanHelpers.AsSpan(ref _mix);
-
- ///
- /// Previous mix buffer volumes.
- ///
- /// Used when a splitter id is specified in the mix.
- public Span PreviousMixBufferVolume => SpanHelpers.AsSpan(ref _previousMix);
-
- ///
- /// Get the of the next element or if not present.
- ///
- public readonly Span Next
+ public int Id
{
get
{
- unsafe
+ if (Unsafe.IsNullRef(ref _v2))
{
- return _next != null ? new Span(_next, 1) : Span.Empty;
+ if (Unsafe.IsNullRef(ref _v1))
+ {
+ return 0;
+ }
+ else
+ {
+ return _v1.Id;
+ }
+ }
+ else
+ {
+ return _v2.Id;
}
}
}
///
- /// Create a new .
+ /// The mix to output the result of the splitter.
///
- /// The unique id of this .
- public SplitterDestination(int id) : this()
+ public int DestinationId
{
- Id = id;
- DestinationId = Constants.UnusedMixId;
-
- ClearVolumes();
+ get
+ {
+ if (Unsafe.IsNullRef(ref _v2))
+ {
+ if (Unsafe.IsNullRef(ref _v1))
+ {
+ return 0;
+ }
+ else
+ {
+ return _v1.DestinationId;
+ }
+ }
+ else
+ {
+ return _v2.DestinationId;
+ }
+ }
}
///
- /// Update the from user parameter.
+ /// Mix buffer volumes.
+ ///
+ /// Used when a splitter id is specified in the mix.
+ public Span MixBufferVolume
+ {
+ get
+ {
+ if (Unsafe.IsNullRef(ref _v2))
+ {
+ if (Unsafe.IsNullRef(ref _v1))
+ {
+ return Span.Empty;
+ }
+ else
+ {
+ return _v1.MixBufferVolume;
+ }
+ }
+ else
+ {
+ return _v2.MixBufferVolume;
+ }
+ }
+ }
+
+ ///
+ /// Previous mix buffer volumes.
+ ///
+ /// Used when a splitter id is specified in the mix.
+ public Span PreviousMixBufferVolume
+ {
+ get
+ {
+ if (Unsafe.IsNullRef(ref _v2))
+ {
+ if (Unsafe.IsNullRef(ref _v1))
+ {
+ return Span.Empty;
+ }
+ else
+ {
+ return _v1.PreviousMixBufferVolume;
+ }
+ }
+ else
+ {
+ return _v2.PreviousMixBufferVolume;
+ }
+ }
+ }
+
+ ///
+ /// Get the of the next element or null if not present.
+ ///
+ public readonly SplitterDestination Next
+ {
+ get
+ {
+ unsafe
+ {
+ if (Unsafe.IsNullRef(ref _v2))
+ {
+ if (Unsafe.IsNullRef(ref _v1))
+ {
+ return new SplitterDestination();
+ }
+ else
+ {
+ return new SplitterDestination(ref _v1.Next);
+ }
+ }
+ else
+ {
+ return new SplitterDestination(ref _v2.Next);
+ }
+ }
+ }
+ }
+
+ ///
+ /// Creates a new splitter destination wrapper for the version 1 splitter destination data.
+ ///
+ /// Version 1 splitter destination data
+ public SplitterDestination(ref SplitterDestinationVersion1 v1)
+ {
+ _v1 = ref v1;
+ _v2 = ref Unsafe.NullRef();
+ }
+
+ ///
+ /// Creates a new splitter destination wrapper for the version 2 splitter destination data.
+ ///
+ /// Version 2 splitter destination data
+ public SplitterDestination(ref SplitterDestinationVersion2 v2)
+ {
+
+ _v1 = ref Unsafe.NullRef();
+ _v2 = ref v2;
+ }
+
+ ///
+ /// Creates a new splitter destination wrapper for the splitter destination data.
+ ///
+ /// Version 1 splitter destination data
+ /// Version 2 splitter destination data
+ public unsafe SplitterDestination(SplitterDestinationVersion1* v1, SplitterDestinationVersion2* v2)
+ {
+ _v1 = ref Unsafe.AsRef(v1);
+ _v2 = ref Unsafe.AsRef(v2);
+ }
+
+ ///
+ /// Update the splitter destination data from user parameter.
///
/// The user parameter.
- public void Update(SplitterDestinationInParameter parameter)
+ /// Indicates that the audio renderer revision in use supports explicitly resetting the volume.
+ public void Update(in T parameter, bool isPrevVolumeResetSupported) 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, isPrevVolumeResetSupported);
+ }
+ else
+ {
+ _v2.Update(parameter, isPrevVolumeResetSupported);
}
}
@@ -118,12 +202,14 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
///
public void UpdateInternalState()
{
- if (IsUsed && NeedToUpdateInternalState)
+ if (Unsafe.IsNullRef(ref _v2))
{
- MixBufferVolume.CopyTo(PreviousMixBufferVolume);
+ _v1.UpdateInternalState();
+ }
+ else
+ {
+ _v2.UpdateInternalState();
}
-
- NeedToUpdateInternalState = false;
}
///
@@ -131,16 +217,23 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
///
public void MarkAsNeedToUpdateInternalState()
{
- NeedToUpdateInternalState = true;
+ if (Unsafe.IsNullRef(ref _v2))
+ {
+ _v1.MarkAsNeedToUpdateInternalState();
+ }
+ else
+ {
+ _v2.MarkAsNeedToUpdateInternalState();
+ }
}
///
- /// Return true if the is used and has a destination.
+ /// Return true if the splitter destination is used and has a destination.
///
- /// True if the is used and has a destination.
+ /// True if the splitter destination is used and has a destination.
public readonly bool IsConfigured()
{
- return IsUsed && DestinationId != Constants.UnusedMixId;
+ return Unsafe.IsNullRef(ref _v2) ? _v1.IsConfigured() : _v2.IsConfigured();
}
///
@@ -150,9 +243,17 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
/// The volume for the given destination.
public float GetMixVolume(int destinationIndex)
{
- Debug.Assert(destinationIndex >= 0 && destinationIndex < Constants.MixBufferCountMax);
+ return Unsafe.IsNullRef(ref _v2) ? _v1.GetMixVolume(destinationIndex) : _v2.GetMixVolume(destinationIndex);
+ }
- return MixBufferVolume[destinationIndex];
+ ///
+ /// Get the previous volume for a given destination.
+ ///
+ /// The destination index to use.
+ /// The volume for the given destination.
+ public float GetMixVolumePrev(int destinationIndex)
+ {
+ return Unsafe.IsNullRef(ref _v2) ? _v1.GetMixVolumePrev(destinationIndex) : _v2.GetMixVolumePrev(destinationIndex);
}
///
@@ -160,22 +261,33 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
///
public void ClearVolumes()
{
- MixBufferVolume.Clear();
- PreviousMixBufferVolume.Clear();
+ if (Unsafe.IsNullRef(ref _v2))
+ {
+ _v1.ClearVolumes();
+ }
+ else
+ {
+ _v2.ClearVolumes();
+ }
}
///
- /// Link the next element to the given .
+ /// Link the next element to the given splitter destination.
///
- /// The given to link.
- public void Link(ref SplitterDestination next)
+ /// The given splitter destination to link.
+ public void Link(SplitterDestination next)
{
- unsafe
+ if (Unsafe.IsNullRef(ref _v2))
{
- fixed (SplitterDestination* nextPtr = &next)
- {
- _next = nextPtr;
- }
+ Debug.Assert(!Unsafe.IsNullRef(ref next._v1));
+
+ _v1.Link(ref next._v1);
+ }
+ else
+ {
+ Debug.Assert(!Unsafe.IsNullRef(ref next._v2));
+
+ _v2.Link(ref next._v2);
}
}
@@ -184,10 +296,74 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
///
public void Unlink()
{
- unsafe
+ if (Unsafe.IsNullRef(ref _v2))
{
- _next = null;
+ _v1.Unlink();
}
+ else
+ {
+ _v2.Unlink();
+ }
+ }
+
+ ///
+ /// Checks if any biquad filter is enabled.
+ ///
+ /// True if any biquad filter is enabled.
+ public bool IsBiquadFilterEnabled()
+ {
+ return !Unsafe.IsNullRef(ref _v2) && _v2.IsBiquadFilterEnabled();
+ }
+
+ ///
+ /// Checks if any biquad filter was previously enabled.
+ ///
+ /// True if any biquad filter was previously enabled.
+ public bool IsBiquadFilterEnabledPrev()
+ {
+ return !Unsafe.IsNullRef(ref _v2) && _v2.IsBiquadFilterEnabledPrev();
+ }
+
+ ///
+ /// Gets the biquad filter parameters.
+ ///
+ /// Biquad filter index (0 or 1).
+ /// Biquad filter parameters.
+ public ref BiquadFilterParameter GetBiquadFilterParameter(int index)
+ {
+ Debug.Assert(!Unsafe.IsNullRef(ref _v2));
+
+ return ref _v2.GetBiquadFilterParameter(index);
+ }
+
+ ///
+ /// Checks if any biquad filter was previously enabled.
+ ///
+ /// Biquad filter index (0 or 1).
+ public void UpdateBiquadFilterEnabledPrev(int index)
+ {
+ if (!Unsafe.IsNullRef(ref _v2))
+ {
+ _v2.UpdateBiquadFilterEnabledPrev(index);
+ }
+ }
+
+ ///
+ /// Get the reference for the version 1 splitter destination data, or null if version 2 is being used or the destination is null.
+ ///
+ /// Reference for the version 1 splitter destination data.
+ public ref SplitterDestinationVersion1 GetV1RefOrNull()
+ {
+ return ref _v1;
+ }
+
+ ///
+ /// Get the reference for the version 2 splitter destination data, or null if version 1 is being used or the destination is null.
+ ///
+ /// Reference for the version 2 splitter destination data.
+ public ref SplitterDestinationVersion2 GetV2RefOrNull()
+ {
+ return ref _v2;
}
}
}
diff --git a/src/Ryujinx.Audio/Renderer/Server/Splitter/SplitterDestinationVersion1.cs b/src/Ryujinx.Audio/Renderer/Server/Splitter/SplitterDestinationVersion1.cs
new file mode 100644
index 000000000..ce8f33685
--- /dev/null
+++ b/src/Ryujinx.Audio/Renderer/Server/Splitter/SplitterDestinationVersion1.cs
@@ -0,0 +1,208 @@
+using Ryujinx.Audio.Renderer.Parameter;
+using Ryujinx.Common.Utilities;
+using System;
+using System.Diagnostics;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Audio.Renderer.Server.Splitter
+{
+ ///
+ /// Server state for a splitter destination (version 1).
+ ///
+ [StructLayout(LayoutKind.Sequential, Size = 0xE0, Pack = Alignment)]
+ public struct SplitterDestinationVersion1
+ {
+ public const int Alignment = 0x10;
+
+ ///
+ /// The unique id of this .
+ ///
+ public int Id;
+
+ ///
+ /// The mix to output the result of the splitter.
+ ///
+ public int DestinationId;
+
+ ///
+ /// Mix buffer volumes storage.
+ ///
+ private MixArray _mix;
+ private MixArray _previousMix;
+
+ ///
+ /// Pointer to the next linked element.
+ ///
+ private unsafe SplitterDestinationVersion1* _next;
+
+ ///
+ /// Set to true if in use.
+ ///
+ [MarshalAs(UnmanagedType.I1)]
+ public bool IsUsed;
+
+ ///
+ /// Set to true if the internal state need to be updated.
+ ///
+ [MarshalAs(UnmanagedType.I1)]
+ public bool NeedToUpdateInternalState;
+
+ [StructLayout(LayoutKind.Sequential, Size = sizeof(float) * Constants.MixBufferCountMax, Pack = 1)]
+ private struct MixArray { }
+
+ ///
+ /// Mix buffer volumes.
+ ///
+ /// Used when a splitter id is specified in the mix.
+ public Span MixBufferVolume => SpanHelpers.AsSpan(ref _mix);
+
+ ///
+ /// Previous mix buffer volumes.
+ ///
+ /// Used when a splitter id is specified in the mix.
+ public Span PreviousMixBufferVolume => SpanHelpers.AsSpan(ref _previousMix);
+
+ ///
+ /// Get the reference of the next element or null if not present.
+ ///
+ public readonly ref SplitterDestinationVersion1 Next
+ {
+ get
+ {
+ unsafe
+ {
+ return ref Unsafe.AsRef(_next);
+ }
+ }
+ }
+
+ ///
+ /// Create a new .
+ ///
+ /// The unique id of this .
+ public SplitterDestinationVersion1(int id) : this()
+ {
+ Id = id;
+ DestinationId = Constants.UnusedMixId;
+
+ ClearVolumes();
+ }
+
+ ///
+ /// Update the from user parameter.
+ ///
+ /// The user parameter.
+ /// Indicates that the audio renderer revision in use supports explicitly resetting the volume.
+ public void Update(in T parameter, bool isPrevVolumeResetSupported) where T : ISplitterDestinationInParameter
+ {
+ Debug.Assert(Id == parameter.Id);
+
+ if (parameter.IsMagicValid() && Id == parameter.Id)
+ {
+ DestinationId = parameter.DestinationId;
+
+ parameter.MixBufferVolume.CopyTo(MixBufferVolume);
+
+ bool resetPrevVolume = isPrevVolumeResetSupported ? parameter.ResetPrevVolume : !IsUsed && parameter.IsUsed;
+ if (resetPrevVolume)
+ {
+ MixBufferVolume.CopyTo(PreviousMixBufferVolume);
+
+ NeedToUpdateInternalState = false;
+ }
+
+ IsUsed = parameter.IsUsed;
+ }
+ }
+
+ ///
+ /// Update the internal state of the instance.
+ ///
+ public void UpdateInternalState()
+ {
+ if (IsUsed && NeedToUpdateInternalState)
+ {
+ MixBufferVolume.CopyTo(PreviousMixBufferVolume);
+ }
+
+ NeedToUpdateInternalState = false;
+ }
+
+ ///
+ /// Set the update internal state marker.
+ ///
+ public void MarkAsNeedToUpdateInternalState()
+ {
+ NeedToUpdateInternalState = true;
+ }
+
+ ///
+ /// Return true if the is used and has a destination.
+ ///
+ /// True if the is used and has a destination.
+ public readonly bool IsConfigured()
+ {
+ return IsUsed && DestinationId != Constants.UnusedMixId;
+ }
+
+ ///
+ /// Get the volume for a given destination.
+ ///
+ /// The destination index to use.
+ /// The volume for the given destination.
+ public float GetMixVolume(int destinationIndex)
+ {
+ Debug.Assert(destinationIndex >= 0 && destinationIndex < Constants.MixBufferCountMax);
+
+ return MixBufferVolume[destinationIndex];
+ }
+
+ ///
+ /// Get the previous volume for a given destination.
+ ///
+ /// The destination index to use.
+ /// The volume for the given destination.
+ public float GetMixVolumePrev(int destinationIndex)
+ {
+ Debug.Assert(destinationIndex >= 0 && destinationIndex < Constants.MixBufferCountMax);
+
+ return PreviousMixBufferVolume[destinationIndex];
+ }
+
+ ///
+ /// Clear the volumes.
+ ///
+ public void ClearVolumes()
+ {
+ MixBufferVolume.Clear();
+ PreviousMixBufferVolume.Clear();
+ }
+
+ ///
+ /// Link the next element to the given .
+ ///
+ /// The given to link.
+ public void Link(ref SplitterDestinationVersion1 next)
+ {
+ unsafe
+ {
+ fixed (SplitterDestinationVersion1* nextPtr = &next)
+ {
+ _next = nextPtr;
+ }
+ }
+ }
+
+ ///
+ /// Remove the link to the next element.
+ ///
+ public void Unlink()
+ {
+ unsafe
+ {
+ _next = null;
+ }
+ }
+ }
+}
diff --git a/src/Ryujinx.Audio/Renderer/Server/Splitter/SplitterDestinationVersion2.cs b/src/Ryujinx.Audio/Renderer/Server/Splitter/SplitterDestinationVersion2.cs
new file mode 100644
index 000000000..5f96ef3aa
--- /dev/null
+++ b/src/Ryujinx.Audio/Renderer/Server/Splitter/SplitterDestinationVersion2.cs
@@ -0,0 +1,252 @@
+using Ryujinx.Audio.Renderer.Parameter;
+using Ryujinx.Common.Memory;
+using Ryujinx.Common.Utilities;
+using System;
+using System.Diagnostics;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Audio.Renderer.Server.Splitter
+{
+ ///
+ /// Server state for a splitter destination (version 2).
+ ///
+ [StructLayout(LayoutKind.Sequential, Size = 0x110, Pack = Alignment)]
+ public struct SplitterDestinationVersion2
+ {
+ public const int Alignment = 0x10;
+
+ ///
+ /// The unique id of this .
+ ///
+ public int Id;
+
+ ///
+ /// The mix to output the result of the splitter.
+ ///
+ public int DestinationId;
+
+ ///
+ /// Mix buffer volumes storage.
+ ///
+ private MixArray _mix;
+ private MixArray _previousMix;
+
+ ///
+ /// Pointer to the next linked element.
+ ///
+ private unsafe SplitterDestinationVersion2* _next;
+
+ ///
+ /// Set to true if in use.
+ ///
+ [MarshalAs(UnmanagedType.I1)]
+ public bool IsUsed;
+
+ ///
+ /// Set to true if the internal state need to be updated.
+ ///
+ [MarshalAs(UnmanagedType.I1)]
+ public bool NeedToUpdateInternalState;
+
+ [StructLayout(LayoutKind.Sequential, Size = sizeof(float) * Constants.MixBufferCountMax, Pack = 1)]
+ private struct MixArray { }
+
+ ///
+ /// Mix buffer volumes.
+ ///
+ /// Used when a splitter id is specified in the mix.
+ public Span MixBufferVolume => SpanHelpers.AsSpan(ref _mix);
+
+ ///
+ /// Previous mix buffer volumes.
+ ///
+ /// Used when a splitter id is specified in the mix.
+ public Span PreviousMixBufferVolume => SpanHelpers.AsSpan(ref _previousMix);
+
+ ///
+ /// Get the reference of the next element or null if not present.
+ ///
+ public readonly ref SplitterDestinationVersion2 Next
+ {
+ get
+ {
+ unsafe
+ {
+ return ref Unsafe.AsRef(_next);
+ }
+ }
+ }
+
+ private Array2 _biquadFilters;
+
+ private Array2 _isPreviousBiquadFilterEnabled;
+
+ ///
+ /// Create a new .
+ ///
+ /// The unique id of this .
+ public SplitterDestinationVersion2(int id) : this()
+ {
+ Id = id;
+ DestinationId = Constants.UnusedMixId;
+
+ ClearVolumes();
+ }
+
+ ///
+ /// Update the from user parameter.
+ ///
+ /// The user parameter.
+ /// Indicates that the audio renderer revision in use supports explicitly resetting the volume.
+ public void Update(in T parameter, bool isPrevVolumeResetSupported) where T : ISplitterDestinationInParameter
+ {
+ Debug.Assert(Id == parameter.Id);
+
+ if (parameter.IsMagicValid() && Id == parameter.Id)
+ {
+ DestinationId = parameter.DestinationId;
+
+ parameter.MixBufferVolume.CopyTo(MixBufferVolume);
+
+ _biquadFilters = parameter.BiquadFilters;
+
+ bool resetPrevVolume = isPrevVolumeResetSupported ? parameter.ResetPrevVolume : !IsUsed && parameter.IsUsed;
+ if (resetPrevVolume)
+ {
+ MixBufferVolume.CopyTo(PreviousMixBufferVolume);
+
+ NeedToUpdateInternalState = false;
+ }
+
+ IsUsed = parameter.IsUsed;
+ }
+ }
+
+ ///
+ /// Update the internal state of the instance.
+ ///
+ public void UpdateInternalState()
+ {
+ if (IsUsed && NeedToUpdateInternalState)
+ {
+ MixBufferVolume.CopyTo(PreviousMixBufferVolume);
+ }
+
+ NeedToUpdateInternalState = false;
+ }
+
+ ///
+ /// Set the update internal state marker.
+ ///
+ public void MarkAsNeedToUpdateInternalState()
+ {
+ NeedToUpdateInternalState = true;
+ }
+
+ ///
+ /// Return true if the is used and has a destination.
+ ///
+ /// True if the is used and has a destination.
+ public readonly bool IsConfigured()
+ {
+ return IsUsed && DestinationId != Constants.UnusedMixId;
+ }
+
+ ///
+ /// Get the volume for a given destination.
+ ///
+ /// The destination index to use.
+ /// The volume for the given destination.
+ public float GetMixVolume(int destinationIndex)
+ {
+ Debug.Assert(destinationIndex >= 0 && destinationIndex < Constants.MixBufferCountMax);
+
+ return MixBufferVolume[destinationIndex];
+ }
+
+ ///
+ /// Get the previous volume for a given destination.
+ ///
+ /// The destination index to use.
+ /// The volume for the given destination.
+ public float GetMixVolumePrev(int destinationIndex)
+ {
+ Debug.Assert(destinationIndex >= 0 && destinationIndex < Constants.MixBufferCountMax);
+
+ return PreviousMixBufferVolume[destinationIndex];
+ }
+
+ ///
+ /// Clear the volumes.
+ ///
+ public void ClearVolumes()
+ {
+ MixBufferVolume.Clear();
+ PreviousMixBufferVolume.Clear();
+ }
+
+ ///
+ /// Link the next element to the given .
+ ///
+ /// The given to link.
+ public void Link(ref SplitterDestinationVersion2 next)
+ {
+ unsafe
+ {
+ fixed (SplitterDestinationVersion2* nextPtr = &next)
+ {
+ _next = nextPtr;
+ }
+ }
+ }
+
+ ///
+ /// Remove the link to the next element.
+ ///
+ public void Unlink()
+ {
+ unsafe
+ {
+ _next = null;
+ }
+ }
+
+ ///
+ /// Checks if any biquad filter is enabled.
+ ///
+ /// True if any biquad filter is enabled.
+ public bool IsBiquadFilterEnabled()
+ {
+ return _biquadFilters[0].Enable || _biquadFilters[1].Enable;
+ }
+
+ ///
+ /// Checks if any biquad filter was previously enabled.
+ ///
+ /// True if any biquad filter was previously enabled.
+ public bool IsBiquadFilterEnabledPrev()
+ {
+ return _isPreviousBiquadFilterEnabled[0];
+ }
+
+ ///
+ /// Gets the biquad filter parameters.
+ ///
+ /// Biquad filter index (0 or 1).
+ /// Biquad filter parameters.
+ public ref BiquadFilterParameter GetBiquadFilterParameter(int index)
+ {
+ return ref _biquadFilters[index];
+ }
+
+ ///
+ /// Checks if any biquad filter was previously enabled.
+ ///
+ /// Biquad filter index (0 or 1).
+ public void UpdateBiquadFilterEnabledPrev(int index)
+ {
+ _isPreviousBiquadFilterEnabled[index] = _biquadFilters[index].Enable;
+ }
+ }
+}
diff --git a/src/Ryujinx.Audio/Renderer/Server/Splitter/SplitterState.cs b/src/Ryujinx.Audio/Renderer/Server/Splitter/SplitterState.cs
index e08ee9ea7..3e7dce559 100644
--- a/src/Ryujinx.Audio/Renderer/Server/Splitter/SplitterState.cs
+++ b/src/Ryujinx.Audio/Renderer/Server/Splitter/SplitterState.cs
@@ -1,4 +1,5 @@
using Ryujinx.Audio.Renderer.Parameter;
+using Ryujinx.Common.Extensions;
using System;
using System.Buffers;
using System.Diagnostics;
@@ -14,6 +15,8 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
{
public const int Alignment = 0x10;
+ private delegate void SplitterDestinationAction(SplitterDestination destination, int index);
+
///
/// The unique id of this .
///
@@ -25,7 +28,7 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
public uint SampleRate;
///
- /// Count of splitter destinations ().
+ /// Count of splitter destinations.
///
public int DestinationCount;
@@ -36,20 +39,25 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
public bool HasNewConnection;
///
- /// Linked list of .
+ /// Linked list of .
///
- private unsafe SplitterDestination* _destinationsData;
+ private unsafe SplitterDestinationVersion1* _destinationDataV1;
///
- /// Span to the first element of the linked list of .
+ /// Linked list of .
///
- public readonly Span Destinations
+ private unsafe SplitterDestinationVersion2* _destinationDataV2;
+
+ ///
+ /// First element of the linked list of splitter destinations data.
+ ///
+ public readonly SplitterDestination Destination
{
get
{
unsafe
{
- return (IntPtr)_destinationsData != IntPtr.Zero ? new Span(_destinationsData, 1) : Span.Empty;
+ return new SplitterDestination(_destinationDataV1, _destinationDataV2);
}
}
}
@@ -63,20 +71,20 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
Id = id;
}
- public readonly Span GetData(int index)
+ public readonly SplitterDestination GetData(int index)
{
int i = 0;
- Span result = Destinations;
+ SplitterDestination result = Destination;
while (i < index)
{
- if (result.IsEmpty)
+ if (result.IsNull)
{
break;
}
- result = result[0].Next;
+ result = result.Next;
i++;
}
@@ -92,25 +100,25 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
}
///
- /// Utility function to apply a given to all .
+ /// Utility function to apply an action to all .
///
/// The action to execute on each elements.
- private readonly void ForEachDestination(SpanAction action)
+ private readonly void ForEachDestination(SplitterDestinationAction action)
{
- Span temp = Destinations;
+ SplitterDestination temp = Destination;
int i = 0;
while (true)
{
- if (temp.IsEmpty)
+ if (temp.IsNull)
{
break;
}
- Span next = temp[0].Next;
+ SplitterDestination next = temp.Next;
- action.Invoke(temp, i++);
+ action(temp, i++);
temp = next;
}
@@ -122,7 +130,7 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
/// The splitter context.
/// The user parameter.
/// The raw input data after the .
- public void Update(SplitterContext context, ref SplitterInParameter parameter, ReadOnlySpan input)
+ public void Update(SplitterContext context, in SplitterInParameter parameter, ref SequenceReader input)
{
ClearLinks();
@@ -139,23 +147,30 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
if (destinationCount > 0)
{
- ReadOnlySpan destinationIds = MemoryMarshal.Cast(input);
+ input.ReadLittleEndian(out int destinationId);
- Memory destination = context.GetDestinationMemory(destinationIds[0]);
+ SplitterDestination destination = context.GetDestination(destinationId);
- SetDestination(ref destination.Span[0]);
+ SetDestination(destination);
DestinationCount = destinationCount;
for (int i = 1; i < destinationCount; i++)
{
- Memory nextDestination = context.GetDestinationMemory(destinationIds[i]);
+ input.ReadLittleEndian(out destinationId);
- destination.Span[0].Link(ref nextDestination.Span[0]);
+ SplitterDestination nextDestination = context.GetDestination(destinationId);
+
+ destination.Link(nextDestination);
destination = nextDestination;
}
}
+ if (destinationCount < parameter.DestinationCount)
+ {
+ input.Advance((parameter.DestinationCount - destinationCount) * sizeof(int));
+ }
+
Debug.Assert(parameter.Id == Id);
if (parameter.Id == Id)
@@ -166,16 +181,21 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
}
///
- /// Set the head of the linked list of .
+ /// Set the head of the linked list of .
///
- /// A reference to a .
- public void SetDestination(ref SplitterDestination newValue)
+ /// New destination value.
+ public void SetDestination(SplitterDestination newValue)
{
unsafe
{
- fixed (SplitterDestination* newValuePtr = &newValue)
+ fixed (SplitterDestinationVersion1* newValuePtr = &newValue.GetV1RefOrNull())
{
- _destinationsData = newValuePtr;
+ _destinationDataV1 = newValuePtr;
+ }
+
+ fixed (SplitterDestinationVersion2* newValuePtr = &newValue.GetV2RefOrNull())
+ {
+ _destinationDataV2 = newValuePtr;
}
}
}
@@ -185,19 +205,20 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
///
public readonly void UpdateInternalState()
{
- ForEachDestination((destination, _) => destination[0].UpdateInternalState());
+ ForEachDestination((destination, _) => destination.UpdateInternalState());
}
///
- /// Clear all links from the .
+ /// Clear all links from the .
///
public void ClearLinks()
{
- ForEachDestination((destination, _) => destination[0].Unlink());
+ ForEachDestination((destination, _) => destination.Unlink());
unsafe
{
- _destinationsData = (SplitterDestination*)IntPtr.Zero;
+ _destinationDataV1 = null;
+ _destinationDataV2 = null;
}
}
@@ -211,7 +232,8 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
{
unsafe
{
- splitter._destinationsData = (SplitterDestination*)IntPtr.Zero;
+ splitter._destinationDataV1 = null;
+ splitter._destinationDataV2 = null;
}
splitter.DestinationCount = 0;
diff --git a/src/Ryujinx.Audio/Renderer/Server/StateUpdater.cs b/src/Ryujinx.Audio/Renderer/Server/StateUpdater.cs
index 22eebc7cc..f8d87f2d1 100644
--- a/src/Ryujinx.Audio/Renderer/Server/StateUpdater.cs
+++ b/src/Ryujinx.Audio/Renderer/Server/StateUpdater.cs
@@ -9,41 +9,40 @@ using Ryujinx.Audio.Renderer.Server.Sink;
using Ryujinx.Audio.Renderer.Server.Splitter;
using Ryujinx.Audio.Renderer.Server.Voice;
using Ryujinx.Audio.Renderer.Utils;
+using Ryujinx.Common.Extensions;
using Ryujinx.Common.Logging;
using System;
using System.Buffers;
using System.Diagnostics;
using System.Runtime.CompilerServices;
-using System.Runtime.InteropServices;
using static Ryujinx.Audio.Renderer.Common.BehaviourParameter;
namespace Ryujinx.Audio.Renderer.Server
{
- public class StateUpdater
+ public ref struct StateUpdater
{
- private readonly ReadOnlyMemory _inputOrigin;
+ private SequenceReader _inputReader;
+
private readonly ReadOnlyMemory _outputOrigin;
- private ReadOnlyMemory _input;
private Memory _output;
private readonly uint _processHandle;
private BehaviourContext _behaviourContext;
- private UpdateDataHeader _inputHeader;
+ private readonly ref readonly UpdateDataHeader _inputHeader;
private readonly Memory _outputHeader;
- private ref UpdateDataHeader OutputHeader => ref _outputHeader.Span[0];
+ private readonly ref UpdateDataHeader OutputHeader => ref _outputHeader.Span[0];
- public StateUpdater(ReadOnlyMemory input, Memory output, uint processHandle, BehaviourContext behaviourContext)
+ public StateUpdater(ReadOnlySequence input, Memory output, uint processHandle, BehaviourContext behaviourContext)
{
- _input = input;
- _inputOrigin = _input;
+ _inputReader = new SequenceReader(input);
_output = output;
_outputOrigin = _output;
_processHandle = processHandle;
_behaviourContext = behaviourContext;
- _inputHeader = SpanIOHelper.Read(ref _input);
+ _inputHeader = ref _inputReader.GetRefOrRefToCopy(out _);
_outputHeader = SpanMemoryManager.Cast(_output[..Unsafe.SizeOf()]);
OutputHeader.Initialize(_behaviourContext.UserRevision);
@@ -52,7 +51,7 @@ namespace Ryujinx.Audio.Renderer.Server
public ResultCode UpdateBehaviourContext()
{
- BehaviourParameter parameter = SpanIOHelper.Read(ref _input);
+ ref readonly BehaviourParameter parameter = ref _inputReader.GetRefOrRefToCopy(out _);
if (!BehaviourContext.CheckValidRevision(parameter.UserRevision) || parameter.UserRevision != _behaviourContext.UserRevision)
{
@@ -81,11 +80,11 @@ namespace Ryujinx.Audio.Renderer.Server
foreach (ref MemoryPoolState memoryPool in memoryPools)
{
- MemoryPoolInParameter parameter = SpanIOHelper.Read(ref _input);
+ ref readonly MemoryPoolInParameter parameter = ref _inputReader.GetRefOrRefToCopy(out _);
ref MemoryPoolOutStatus outStatus = ref SpanIOHelper.GetWriteRef(ref _output)[0];
- PoolMapper.UpdateResult updateResult = mapper.Update(ref memoryPool, ref parameter, ref outStatus);
+ PoolMapper.UpdateResult updateResult = mapper.Update(ref memoryPool, in parameter, ref outStatus);
if (updateResult != PoolMapper.UpdateResult.Success &&
updateResult != PoolMapper.UpdateResult.MapError &&
@@ -115,7 +114,7 @@ namespace Ryujinx.Audio.Renderer.Server
for (int i = 0; i < context.GetCount(); i++)
{
- VoiceChannelResourceInParameter parameter = SpanIOHelper.Read(ref _input);
+ ref readonly VoiceChannelResourceInParameter parameter = ref _inputReader.GetRefOrRefToCopy(out _);
ref VoiceChannelResource resource = ref context.GetChannelResource(i);
@@ -127,7 +126,7 @@ namespace Ryujinx.Audio.Renderer.Server
return ResultCode.Success;
}
- public ResultCode UpdateVoices(VoiceContext context, Memory memoryPools)
+ public ResultCode UpdateVoices(VoiceContext context, PoolMapper mapper)
{
if (context.GetCount() * Unsafe.SizeOf() != _inputHeader.VoicesSize)
{
@@ -136,11 +135,7 @@ namespace Ryujinx.Audio.Renderer.Server
int initialOutputSize = _output.Length;
- ReadOnlySpan parameters = MemoryMarshal.Cast(_input[..(int)_inputHeader.VoicesSize].Span);
-
- _input = _input[(int)_inputHeader.VoicesSize..];
-
- PoolMapper mapper = new(_processHandle, memoryPools, _behaviourContext.IsMemoryPoolForceMappingEnabled());
+ long initialInputConsumed = _inputReader.Consumed;
// First make everything not in use.
for (int i = 0; i < context.GetCount(); i++)
@@ -157,7 +152,7 @@ namespace Ryujinx.Audio.Renderer.Server
// Start processing
for (int i = 0; i < context.GetCount(); i++)
{
- VoiceInParameter parameter = parameters[i];
+ ref readonly VoiceInParameter parameter = ref _inputReader.GetRefOrRefToCopy(out _);
voiceUpdateStates.Fill(Memory.Empty);
@@ -181,14 +176,14 @@ namespace Ryujinx.Audio.Renderer.Server
currentVoiceState.Initialize();
}
- currentVoiceState.UpdateParameters(out ErrorInfo updateParameterError, ref parameter, ref mapper, ref _behaviourContext);
+ currentVoiceState.UpdateParameters(out ErrorInfo updateParameterError, in parameter, mapper, ref _behaviourContext);
if (updateParameterError.ErrorCode != ResultCode.Success)
{
_behaviourContext.AppendError(ref updateParameterError);
}
- currentVoiceState.UpdateWaveBuffers(out ErrorInfo[] waveBufferUpdateErrorInfos, ref parameter, voiceUpdateStates, ref mapper, ref _behaviourContext);
+ currentVoiceState.UpdateWaveBuffers(out ErrorInfo[] waveBufferUpdateErrorInfos, in parameter, voiceUpdateStates, mapper, ref _behaviourContext);
foreach (ref ErrorInfo errorInfo in waveBufferUpdateErrorInfos.AsSpan())
{
@@ -198,7 +193,7 @@ namespace Ryujinx.Audio.Renderer.Server
}
}
- currentVoiceState.WriteOutStatus(ref outStatus, ref parameter, voiceUpdateStates);
+ currentVoiceState.WriteOutStatus(ref outStatus, in parameter, voiceUpdateStates);
}
}
@@ -211,10 +206,12 @@ namespace Ryujinx.Audio.Renderer.Server
Debug.Assert((initialOutputSize - currentOutputSize) == OutputHeader.VoicesSize);
+ _inputReader.SetConsumed(initialInputConsumed + _inputHeader.VoicesSize);
+
return ResultCode.Success;
}
- private static void ResetEffect(ref BaseEffect effect, ref T parameter, PoolMapper mapper) where T : unmanaged, IEffectInParameter
+ private static void ResetEffect(ref BaseEffect effect, in T parameter, PoolMapper mapper) where T : unmanaged, IEffectInParameter
{
effect.ForceUnmapBuffers(mapper);
@@ -234,17 +231,17 @@ namespace Ryujinx.Audio.Renderer.Server
};
}
- public ResultCode UpdateEffects(EffectContext context, bool isAudioRendererActive, Memory memoryPools)
+ public ResultCode UpdateEffects(EffectContext context, bool isAudioRendererActive, PoolMapper mapper)
{
if (_behaviourContext.IsEffectInfoVersion2Supported())
{
- return UpdateEffectsVersion2(context, isAudioRendererActive, memoryPools);
+ return UpdateEffectsVersion2(context, isAudioRendererActive, mapper);
}
- return UpdateEffectsVersion1(context, isAudioRendererActive, memoryPools);
+ return UpdateEffectsVersion1(context, isAudioRendererActive, mapper);
}
- public ResultCode UpdateEffectsVersion2(EffectContext context, bool isAudioRendererActive, Memory memoryPools)
+ public ResultCode UpdateEffectsVersion2(EffectContext context, bool isAudioRendererActive, PoolMapper mapper)
{
if (context.GetCount() * Unsafe.SizeOf() != _inputHeader.EffectsSize)
{
@@ -253,26 +250,22 @@ namespace Ryujinx.Audio.Renderer.Server
int initialOutputSize = _output.Length;
- ReadOnlySpan parameters = MemoryMarshal.Cast(_input[..(int)_inputHeader.EffectsSize].Span);
-
- _input = _input[(int)_inputHeader.EffectsSize..];
-
- PoolMapper mapper = new(_processHandle, memoryPools, _behaviourContext.IsMemoryPoolForceMappingEnabled());
+ long initialInputConsumed = _inputReader.Consumed;
for (int i = 0; i < context.GetCount(); i++)
{
- EffectInParameterVersion2 parameter = parameters[i];
+ ref readonly EffectInParameterVersion2 parameter = ref _inputReader.GetRefOrRefToCopy(out _);
ref EffectOutStatusVersion2 outStatus = ref SpanIOHelper.GetWriteRef(ref _output)[0];
ref BaseEffect effect = ref context.GetEffect(i);
- if (!effect.IsTypeValid(ref parameter))
+ if (!effect.IsTypeValid(in parameter))
{
- ResetEffect(ref effect, ref parameter, mapper);
+ ResetEffect(ref effect, in parameter, mapper);
}
- effect.Update(out ErrorInfo updateErrorInfo, ref parameter, mapper);
+ effect.Update(out ErrorInfo updateErrorInfo, in parameter, mapper);
if (updateErrorInfo.ErrorCode != ResultCode.Success)
{
@@ -297,10 +290,12 @@ namespace Ryujinx.Audio.Renderer.Server
Debug.Assert((initialOutputSize - currentOutputSize) == OutputHeader.EffectsSize);
+ _inputReader.SetConsumed(initialInputConsumed + _inputHeader.EffectsSize);
+
return ResultCode.Success;
}
- public ResultCode UpdateEffectsVersion1(EffectContext context, bool isAudioRendererActive, Memory memoryPools)
+ public ResultCode UpdateEffectsVersion1(EffectContext context, bool isAudioRendererActive, PoolMapper mapper)
{
if (context.GetCount() * Unsafe.SizeOf() != _inputHeader.EffectsSize)
{
@@ -309,26 +304,22 @@ namespace Ryujinx.Audio.Renderer.Server
int initialOutputSize = _output.Length;
- ReadOnlySpan parameters = MemoryMarshal.Cast(_input[..(int)_inputHeader.EffectsSize].Span);
-
- _input = _input[(int)_inputHeader.EffectsSize..];
-
- PoolMapper mapper = new(_processHandle, memoryPools, _behaviourContext.IsMemoryPoolForceMappingEnabled());
+ long initialInputConsumed = _inputReader.Consumed;
for (int i = 0; i < context.GetCount(); i++)
{
- EffectInParameterVersion1 parameter = parameters[i];
+ ref readonly EffectInParameterVersion1 parameter = ref _inputReader.GetRefOrRefToCopy(out _);
ref EffectOutStatusVersion1 outStatus = ref SpanIOHelper.GetWriteRef(ref _output)[0];
ref BaseEffect effect = ref context.GetEffect(i);
- if (!effect.IsTypeValid(ref parameter))
+ if (!effect.IsTypeValid(in parameter))
{
- ResetEffect(ref effect, ref parameter, mapper);
+ ResetEffect(ref effect, in parameter, mapper);
}
- effect.Update(out ErrorInfo updateErrorInfo, ref parameter, mapper);
+ effect.Update(out ErrorInfo updateErrorInfo, in parameter, mapper);
if (updateErrorInfo.ErrorCode != ResultCode.Success)
{
@@ -345,38 +336,40 @@ namespace Ryujinx.Audio.Renderer.Server
Debug.Assert((initialOutputSize - currentOutputSize) == OutputHeader.EffectsSize);
+ _inputReader.SetConsumed(initialInputConsumed + _inputHeader.EffectsSize);
+
return ResultCode.Success;
}
public ResultCode UpdateSplitter(SplitterContext context)
{
- if (context.Update(_input.Span, out int consumedSize))
+ if (context.Update(ref _inputReader))
{
- _input = _input[consumedSize..];
-
return ResultCode.Success;
}
return ResultCode.InvalidUpdateInfo;
}
- private static bool CheckMixParametersValidity(MixContext mixContext, uint mixBufferCount, uint inputMixCount, ReadOnlySpan parameters)
+ private static bool CheckMixParametersValidity(MixContext mixContext, uint mixBufferCount, uint inputMixCount, SequenceReader parameters)
{
uint maxMixStateCount = mixContext.GetCount();
uint totalRequiredMixBufferCount = 0;
for (int i = 0; i < inputMixCount; i++)
{
- if (parameters[i].IsUsed)
+ ref readonly MixParameter parameter = ref parameters.GetRefOrRefToCopy(out _);
+
+ if (parameter.IsUsed)
{
- if (parameters[i].DestinationMixId != Constants.UnusedMixId &&
- parameters[i].DestinationMixId > maxMixStateCount &&
- parameters[i].MixId != Constants.FinalMixId)
+ if (parameter.DestinationMixId != Constants.UnusedMixId &&
+ parameter.DestinationMixId > maxMixStateCount &&
+ parameter.MixId != Constants.FinalMixId)
{
return true;
}
- totalRequiredMixBufferCount += parameters[i].BufferCount;
+ totalRequiredMixBufferCount += parameter.BufferCount;
}
}
@@ -391,7 +384,7 @@ namespace Ryujinx.Audio.Renderer.Server
if (_behaviourContext.IsMixInParameterDirtyOnlyUpdateSupported())
{
- MixInParameterDirtyOnlyUpdate parameter = MemoryMarshal.Cast(_input.Span)[0];
+ ref readonly MixInParameterDirtyOnlyUpdate parameter = ref _inputReader.GetRefOrRefToCopy(out _);
mixCount = parameter.MixCount;
@@ -411,25 +404,20 @@ namespace Ryujinx.Audio.Renderer.Server
return ResultCode.InvalidUpdateInfo;
}
- if (_behaviourContext.IsMixInParameterDirtyOnlyUpdateSupported())
- {
- _input = _input[Unsafe.SizeOf()..];
- }
+ long initialInputConsumed = _inputReader.Consumed;
- ReadOnlySpan parameters = MemoryMarshal.Cast(_input.Span[..(int)inputMixSize]);
+ int parameterCount = (int)inputMixSize / Unsafe.SizeOf();
- _input = _input[(int)inputMixSize..];
-
- if (CheckMixParametersValidity(mixContext, mixBufferCount, mixCount, parameters))
+ if (CheckMixParametersValidity(mixContext, mixBufferCount, mixCount, _inputReader))
{
return ResultCode.InvalidUpdateInfo;
}
bool isMixContextDirty = false;
- for (int i = 0; i < parameters.Length; i++)
+ for (int i = 0; i < parameterCount; i++)
{
- MixParameter parameter = parameters[i];
+ ref readonly MixParameter parameter = ref _inputReader.GetRefOrRefToCopy(out _);
int mixId = i;
@@ -454,7 +442,7 @@ namespace Ryujinx.Audio.Renderer.Server
if (mix.IsUsed)
{
- isMixContextDirty |= mix.Update(mixContext.EdgeMatrix, ref parameter, effectContext, splitterContext, _behaviourContext);
+ isMixContextDirty |= mix.Update(mixContext.EdgeMatrix, in parameter, effectContext, splitterContext, _behaviourContext);
}
}
@@ -473,10 +461,12 @@ namespace Ryujinx.Audio.Renderer.Server
}
}
+ _inputReader.SetConsumed(initialInputConsumed + inputMixSize);
+
return ResultCode.Success;
}
- private static void ResetSink(ref BaseSink sink, ref SinkInParameter parameter)
+ private static void ResetSink(ref BaseSink sink, in SinkInParameter parameter)
{
sink.CleanUp();
@@ -489,10 +479,8 @@ namespace Ryujinx.Audio.Renderer.Server
};
}
- public ResultCode UpdateSinks(SinkContext context, Memory memoryPools)
+ public ResultCode UpdateSinks(SinkContext context, PoolMapper mapper)
{
- PoolMapper mapper = new(_processHandle, memoryPools, _behaviourContext.IsMemoryPoolForceMappingEnabled());
-
if (context.GetCount() * Unsafe.SizeOf() != _inputHeader.SinksSize)
{
return ResultCode.InvalidUpdateInfo;
@@ -500,22 +488,20 @@ namespace Ryujinx.Audio.Renderer.Server
int initialOutputSize = _output.Length;
- ReadOnlySpan parameters = MemoryMarshal.Cast(_input[..(int)_inputHeader.SinksSize].Span);
-
- _input = _input[(int)_inputHeader.SinksSize..];
+ long initialInputConsumed = _inputReader.Consumed;
for (int i = 0; i < context.GetCount(); i++)
{
- SinkInParameter parameter = parameters[i];
+ ref readonly SinkInParameter parameter = ref _inputReader.GetRefOrRefToCopy(out _);
ref SinkOutStatus outStatus = ref SpanIOHelper.GetWriteRef(ref _output)[0];
ref BaseSink sink = ref context.GetSink(i);
- if (!sink.IsTypeValid(ref parameter))
+ if (!sink.IsTypeValid(in parameter))
{
- ResetSink(ref sink, ref parameter);
+ ResetSink(ref sink, in parameter);
}
- sink.Update(out ErrorInfo updateErrorInfo, ref parameter, ref outStatus, mapper);
+ sink.Update(out ErrorInfo updateErrorInfo, in parameter, ref outStatus, mapper);
if (updateErrorInfo.ErrorCode != ResultCode.Success)
{
@@ -530,6 +516,8 @@ namespace Ryujinx.Audio.Renderer.Server
Debug.Assert((initialOutputSize - currentOutputSize) == OutputHeader.SinksSize);
+ _inputReader.SetConsumed(initialInputConsumed + _inputHeader.SinksSize);
+
return ResultCode.Success;
}
@@ -540,7 +528,7 @@ namespace Ryujinx.Audio.Renderer.Server
return ResultCode.InvalidUpdateInfo;
}
- PerformanceInParameter parameter = SpanIOHelper.Read(ref _input);
+ ref readonly PerformanceInParameter parameter = ref _inputReader.GetRefOrRefToCopy(out _);
ref PerformanceOutStatus outStatus = ref SpanIOHelper.GetWriteRef(ref _output)[0];
@@ -585,9 +573,9 @@ namespace Ryujinx.Audio.Renderer.Server
return ResultCode.Success;
}
- public ResultCode CheckConsumedSize()
+ public readonly ResultCode CheckConsumedSize()
{
- int consumedInputSize = _inputOrigin.Length - _input.Length;
+ long consumedInputSize = _inputReader.Consumed;
int consumedOutputSize = _outputOrigin.Length - _output.Length;
if (consumedInputSize != _inputHeader.TotalSize)
diff --git a/src/Ryujinx.Audio/Renderer/Server/Voice/VoiceState.cs b/src/Ryujinx.Audio/Renderer/Server/Voice/VoiceState.cs
index 225f7d31b..040c70e6c 100644
--- a/src/Ryujinx.Audio/Renderer/Server/Voice/VoiceState.cs
+++ b/src/Ryujinx.Audio/Renderer/Server/Voice/VoiceState.cs
@@ -254,7 +254,7 @@ namespace Ryujinx.Audio.Renderer.Server.Voice
///
/// The user parameter.
/// Return true, if the server voice information needs to be updated.
- private readonly bool ShouldUpdateParameters(ref VoiceInParameter parameter)
+ private readonly bool ShouldUpdateParameters(in VoiceInParameter parameter)
{
if (DataSourceStateAddressInfo.CpuAddress == parameter.DataSourceStateAddress)
{
@@ -273,7 +273,7 @@ namespace Ryujinx.Audio.Renderer.Server.Voice
/// The user parameter.
/// The mapper to use.
/// The behaviour context.
- public void UpdateParameters(out ErrorInfo outErrorInfo, ref VoiceInParameter parameter, ref PoolMapper poolMapper, ref BehaviourContext behaviourContext)
+ public void UpdateParameters(out ErrorInfo outErrorInfo, in VoiceInParameter parameter, PoolMapper poolMapper, ref BehaviourContext behaviourContext)
{
InUse = parameter.InUse;
Id = parameter.Id;
@@ -326,7 +326,7 @@ namespace Ryujinx.Audio.Renderer.Server.Voice
VoiceDropFlag = false;
}
- if (ShouldUpdateParameters(ref parameter))
+ if (ShouldUpdateParameters(in parameter))
{
DataSourceStateUnmapped = !poolMapper.TryAttachBuffer(out outErrorInfo, ref DataSourceStateAddressInfo, parameter.DataSourceStateAddress, parameter.DataSourceStateSize);
}
@@ -380,7 +380,7 @@ namespace Ryujinx.Audio.Renderer.Server.Voice
/// The given user output.
/// The user parameter.
/// The voice states associated to the .
- public void WriteOutStatus(ref VoiceOutStatus outStatus, ref VoiceInParameter parameter, ReadOnlySpan> voiceUpdateStates)
+ public void WriteOutStatus(ref VoiceOutStatus outStatus, in VoiceInParameter parameter, ReadOnlySpan> voiceUpdateStates)
{
#if DEBUG
// Sanity check in debug mode of the internal state
@@ -426,7 +426,12 @@ namespace Ryujinx.Audio.Renderer.Server.Voice
/// The voice states associated to the .
/// The mapper to use.
/// The behaviour context.
- public void UpdateWaveBuffers(out ErrorInfo[] errorInfos, ref VoiceInParameter parameter, ReadOnlySpan> voiceUpdateStates, ref PoolMapper mapper, ref BehaviourContext behaviourContext)
+ public void UpdateWaveBuffers(
+ out ErrorInfo[] errorInfos,
+ in VoiceInParameter parameter,
+ ReadOnlySpan> voiceUpdateStates,
+ PoolMapper mapper,
+ ref BehaviourContext behaviourContext)
{
errorInfos = new ErrorInfo[Constants.VoiceWaveBufferCount * 2];
@@ -444,7 +449,7 @@ namespace Ryujinx.Audio.Renderer.Server.Voice
for (int i = 0; i < Constants.VoiceWaveBufferCount; i++)
{
- UpdateWaveBuffer(errorInfos.AsSpan(i * 2, 2), ref WaveBuffers[i], ref parameter.WaveBuffers[i], parameter.SampleFormat, voiceUpdateState.IsWaveBufferValid[i], ref mapper, ref behaviourContext);
+ UpdateWaveBuffer(errorInfos.AsSpan(i * 2, 2), ref WaveBuffers[i], ref parameter.WaveBuffers[i], parameter.SampleFormat, voiceUpdateState.IsWaveBufferValid[i], mapper, ref behaviourContext);
}
}
@@ -458,7 +463,14 @@ namespace Ryujinx.Audio.Renderer.Server.Voice
/// If set to true, the server side wavebuffer is considered valid.
/// The mapper to use.
/// The behaviour context.
- private void UpdateWaveBuffer(Span errorInfos, ref WaveBuffer waveBuffer, ref WaveBufferInternal inputWaveBuffer, SampleFormat sampleFormat, bool isValid, ref PoolMapper mapper, ref BehaviourContext behaviourContext)
+ private void UpdateWaveBuffer(
+ Span errorInfos,
+ ref WaveBuffer waveBuffer,
+ ref WaveBufferInternal inputWaveBuffer,
+ SampleFormat sampleFormat,
+ bool isValid,
+ PoolMapper mapper,
+ ref BehaviourContext behaviourContext)
{
if (!isValid && waveBuffer.IsSendToAudioProcessor && waveBuffer.BufferAddressInfo.CpuAddress != 0)
{
diff --git a/src/Ryujinx.Ava/Assets/Locales/zh_CN.json b/src/Ryujinx.Ava/Assets/Locales/zh_CN.json
deleted file mode 100644
index d09a80ec6..000000000
--- a/src/Ryujinx.Ava/Assets/Locales/zh_CN.json
+++ /dev/null
@@ -1,656 +0,0 @@
-{
- "Language": "简体中文",
- "MenuBarFileOpenApplet": "打开小程序",
- "MenuBarFileOpenAppletOpenMiiAppletToolTip": "打开独立的 Mii 小程序",
- "SettingsTabInputDirectMouseAccess": "直通鼠标操作",
- "SettingsTabSystemMemoryManagerMode": "内存管理模式:",
- "SettingsTabSystemMemoryManagerModeSoftware": "软件",
- "SettingsTabSystemMemoryManagerModeHost": "本机 (快速)",
- "SettingsTabSystemMemoryManagerModeHostUnchecked": "跳过检查的本机 (最快)",
- "SettingsTabSystemUseHypervisor": "使用 Hypervisor 虚拟化",
- "MenuBarFile": "文件",
- "MenuBarFileOpenFromFile": "加载文件",
- "MenuBarFileOpenUnpacked": "加载解包后的游戏",
- "MenuBarFileOpenEmuFolder": "打开 Ryujinx 文件夹",
- "MenuBarFileOpenLogsFolder": "打开日志文件夹",
- "MenuBarFileExit": "退出",
- "MenuBarOptions": "选项",
- "MenuBarOptionsToggleFullscreen": "切换全屏",
- "MenuBarOptionsStartGamesInFullscreen": "全屏模式启动游戏",
- "MenuBarOptionsStopEmulation": "停止模拟",
- "MenuBarOptionsSettings": "设置",
- "MenuBarOptionsManageUserProfiles": "管理用户账户",
- "MenuBarActions": "操作",
- "MenuBarOptionsSimulateWakeUpMessage": "模拟唤醒消息",
- "MenuBarActionsScanAmiibo": "扫描 Amiibo",
- "MenuBarTools": "工具",
- "MenuBarToolsInstallFirmware": "安装固件",
- "MenuBarFileToolsInstallFirmwareFromFile": "从 XCI 或 ZIP 安装固件",
- "MenuBarFileToolsInstallFirmwareFromDirectory": "从文件夹安装固件",
- "MenuBarToolsManageFileTypes": "管理文件扩展名",
- "MenuBarToolsInstallFileTypes": "关联文件扩展名",
- "MenuBarToolsUninstallFileTypes": "取消关联扩展名",
- "MenuBarHelp": "帮助",
- "MenuBarHelpCheckForUpdates": "检查更新",
- "MenuBarHelpAbout": "关于",
- "MenuSearch": "搜索…",
- "GameListHeaderFavorite": "收藏",
- "GameListHeaderIcon": "图标",
- "GameListHeaderApplication": "名称",
- "GameListHeaderDeveloper": "制作商",
- "GameListHeaderVersion": "版本",
- "GameListHeaderTimePlayed": "游玩时长",
- "GameListHeaderLastPlayed": "最近游玩",
- "GameListHeaderFileExtension": "扩展名",
- "GameListHeaderFileSize": "大小",
- "GameListHeaderPath": "路径",
- "GameListContextMenuOpenUserSaveDirectory": "打开用户存档目录",
- "GameListContextMenuOpenUserSaveDirectoryToolTip": "打开储存游戏存档的目录",
- "GameListContextMenuOpenDeviceSaveDirectory": "打开系统目录",
- "GameListContextMenuOpenDeviceSaveDirectoryToolTip": "打开包含游戏系统设置的目录",
- "GameListContextMenuOpenBcatSaveDirectory": "打开 BCAT 目录",
- "GameListContextMenuOpenBcatSaveDirectoryToolTip": "打开包含游戏 BCAT 数据的目录",
- "GameListContextMenuManageTitleUpdates": "管理游戏更新",
- "GameListContextMenuManageTitleUpdatesToolTip": "打开更新管理器",
- "GameListContextMenuManageDlc": "管理 DLC",
- "GameListContextMenuManageDlcToolTip": "打开 DLC 管理窗口",
- "GameListContextMenuOpenModsDirectory": "打开 MOD 目录",
- "GameListContextMenuOpenModsDirectoryToolTip": "打开存放游戏 MOD 的目录",
- "GameListContextMenuCacheManagement": "缓存管理",
- "GameListContextMenuCacheManagementPurgePptc": "清除已编译的 PPTC 文件",
- "GameListContextMenuCacheManagementPurgePptcToolTip": "仅删除 PPTC 转换后的文件,下次打开游戏时将根据 .info 文件重新生成 PPTC 文件。\n如想彻底清除缓存,请进入目录把 .info 文件一并删除",
- "GameListContextMenuCacheManagementPurgeShaderCache": "清除着色器缓存",
- "GameListContextMenuCacheManagementPurgeShaderCacheToolTip": "删除游戏的着色器缓存",
- "GameListContextMenuCacheManagementOpenPptcDirectory": "打开 PPTC 目录",
- "GameListContextMenuCacheManagementOpenPptcDirectoryToolTip": "打开包含游戏 PPTC 缓存的目录",
- "GameListContextMenuCacheManagementOpenShaderCacheDirectory": "打开着色器缓存目录",
- "GameListContextMenuCacheManagementOpenShaderCacheDirectoryToolTip": "打开包含游戏着色器缓存的目录",
- "GameListContextMenuExtractData": "提取数据",
- "GameListContextMenuExtractDataExeFS": "ExeFS",
- "GameListContextMenuExtractDataExeFSToolTip": "从游戏的当前状态中提取 ExeFS 分区 (包括更新)",
- "GameListContextMenuExtractDataRomFS": "RomFS",
- "GameListContextMenuExtractDataRomFSToolTip": "从游戏的当前状态中提取 RomFS 分区 (包括更新)",
- "GameListContextMenuExtractDataLogo": "图标",
- "GameListContextMenuExtractDataLogoToolTip": "从游戏的当前状态中提取图标 (包括更新)",
- "StatusBarGamesLoaded": "{0}/{1} 游戏加载完成",
- "StatusBarSystemVersion": "系统版本:{0}",
- "LinuxVmMaxMapCountDialogTitle": "检测到内存映射的限制过低",
- "LinuxVmMaxMapCountDialogTextPrimary": "你想要将 vm.max_map_count 的值增加到 {0} 吗",
- "LinuxVmMaxMapCountDialogTextSecondary": "有些游戏可能会试图创建超过当前允许的内存映射数量。当超过此限制时,Ryujinx会立即崩溃。",
- "LinuxVmMaxMapCountDialogButtonUntilRestart": "确定,关闭后重置",
- "LinuxVmMaxMapCountDialogButtonPersistent": "确定,永久保存",
- "LinuxVmMaxMapCountWarningTextPrimary": "内存映射的最大数量低于推荐值。",
- "LinuxVmMaxMapCountWarningTextSecondary": "vm.max_map_count ({0}) 的当前值小于 {1}。 有些游戏可能会试图创建超过当前允许的内存映射量。当大于此限制时,Ryujinx 会立即崩溃。\n\n你可以手动增加内存映射限制或者安装 pkexec,它可以辅助Ryujinx解决该问题。",
- "Settings": "设置",
- "SettingsTabGeneral": "用户界面",
- "SettingsTabGeneralGeneral": "常规",
- "SettingsTabGeneralEnableDiscordRichPresence": "启用 Discord 在线状态展示",
- "SettingsTabGeneralCheckUpdatesOnLaunch": "自动检查更新",
- "SettingsTabGeneralShowConfirmExitDialog": "显示 \"确认退出\" 对话框",
- "SettingsTabGeneralHideCursor": "隐藏鼠标指针:",
- "SettingsTabGeneralHideCursorNever": "从不",
- "SettingsTabGeneralHideCursorOnIdle": "自动隐藏",
- "SettingsTabGeneralHideCursorAlways": "始终",
- "SettingsTabGeneralGameDirectories": "游戏目录",
- "SettingsTabGeneralAdd": "添加",
- "SettingsTabGeneralRemove": "删除",
- "SettingsTabSystem": "系统",
- "SettingsTabSystemCore": "核心",
- "SettingsTabSystemSystemRegion": "系统区域:",
- "SettingsTabSystemSystemRegionJapan": "日本",
- "SettingsTabSystemSystemRegionUSA": "美国",
- "SettingsTabSystemSystemRegionEurope": "欧洲",
- "SettingsTabSystemSystemRegionAustralia": "澳大利亚",
- "SettingsTabSystemSystemRegionChina": "中国",
- "SettingsTabSystemSystemRegionKorea": "韩国",
- "SettingsTabSystemSystemRegionTaiwan": "台湾地区",
- "SettingsTabSystemSystemLanguage": "系统语言:",
- "SettingsTabSystemSystemLanguageJapanese": "日语",
- "SettingsTabSystemSystemLanguageAmericanEnglish": "美式英语",
- "SettingsTabSystemSystemLanguageFrench": "法语",
- "SettingsTabSystemSystemLanguageGerman": "德语",
- "SettingsTabSystemSystemLanguageItalian": "意大利语",
- "SettingsTabSystemSystemLanguageSpanish": "西班牙语",
- "SettingsTabSystemSystemLanguageChinese": "简体中文",
- "SettingsTabSystemSystemLanguageKorean": "韩语",
- "SettingsTabSystemSystemLanguageDutch": "荷兰语",
- "SettingsTabSystemSystemLanguagePortuguese": "葡萄牙语",
- "SettingsTabSystemSystemLanguageRussian": "俄语",
- "SettingsTabSystemSystemLanguageTaiwanese": "繁体中文(台湾)",
- "SettingsTabSystemSystemLanguageBritishEnglish": "英式英语",
- "SettingsTabSystemSystemLanguageCanadianFrench": "加拿大法语",
- "SettingsTabSystemSystemLanguageLatinAmericanSpanish": "拉美西班牙语",
- "SettingsTabSystemSystemLanguageSimplifiedChinese": "简体中文(推荐)",
- "SettingsTabSystemSystemLanguageTraditionalChinese": "繁体中文(推荐)",
- "SettingsTabSystemSystemTimeZone": "系统时区:",
- "SettingsTabSystemSystemTime": "系统时钟:",
- "SettingsTabSystemEnableVsync": "启用垂直同步",
- "SettingsTabSystemEnablePptc": "开启 PPTC 缓存",
- "SettingsTabSystemEnableFsIntegrityChecks": "文件系统完整性检查",
- "SettingsTabSystemAudioBackend": "音频后端:",
- "SettingsTabSystemAudioBackendDummy": "无",
- "SettingsTabSystemAudioBackendOpenAL": "OpenAL",
- "SettingsTabSystemAudioBackendSoundIO": "音频输入/输出",
- "SettingsTabSystemAudioBackendSDL2": "SDL2",
- "SettingsTabSystemHacks": "修正",
- "SettingsTabSystemHacksNote": "(会引起模拟器不稳定)",
- "SettingsTabSystemExpandDramSize": "使用开发机的内存布局",
- "SettingsTabSystemIgnoreMissingServices": "忽略缺失的服务",
- "SettingsTabGraphics": "图形",
- "SettingsTabGraphicsAPI": "图形 API",
- "SettingsTabGraphicsEnableShaderCache": "启用着色器缓存",
- "SettingsTabGraphicsAnisotropicFiltering": "各向异性过滤:",
- "SettingsTabGraphicsAnisotropicFilteringAuto": "自动",
- "SettingsTabGraphicsAnisotropicFiltering2x": "2x",
- "SettingsTabGraphicsAnisotropicFiltering4x": "4x",
- "SettingsTabGraphicsAnisotropicFiltering8x": "8x",
- "SettingsTabGraphicsAnisotropicFiltering16x": "16x",
- "SettingsTabGraphicsResolutionScale": "分辨率缩放:",
- "SettingsTabGraphicsResolutionScaleCustom": "自定义(不推荐)",
- "SettingsTabGraphicsResolutionScaleNative": "原生 (720p/1080p)",
- "SettingsTabGraphicsResolutionScale2x": "2x (1440p/2160p)",
- "SettingsTabGraphicsResolutionScale3x": "3x (2160p/3240p)",
- "SettingsTabGraphicsResolutionScale4x": "4x (2880p/4320p)",
- "SettingsTabGraphicsAspectRatio": "宽高比:",
- "SettingsTabGraphicsAspectRatio4x3": "4:3",
- "SettingsTabGraphicsAspectRatio16x9": "16:9",
- "SettingsTabGraphicsAspectRatio16x10": "16:10",
- "SettingsTabGraphicsAspectRatio21x9": "21:9",
- "SettingsTabGraphicsAspectRatio32x9": "32:9",
- "SettingsTabGraphicsAspectRatioStretch": "拉伸以适应窗口",
- "SettingsTabGraphicsDeveloperOptions": "开发者选项",
- "SettingsTabGraphicsShaderDumpPath": "图形着色器转储路径:",
- "SettingsTabLogging": "日志",
- "SettingsTabLoggingLogging": "日志",
- "SettingsTabLoggingEnableLoggingToFile": "保存日志为文件",
- "SettingsTabLoggingEnableStubLogs": "启用 Stub 日志",
- "SettingsTabLoggingEnableInfoLogs": "启用信息日志",
- "SettingsTabLoggingEnableWarningLogs": "启用警告日志",
- "SettingsTabLoggingEnableErrorLogs": "启用错误日志",
- "SettingsTabLoggingEnableTraceLogs": "启用跟踪日志",
- "SettingsTabLoggingEnableGuestLogs": "启用来宾日志",
- "SettingsTabLoggingEnableFsAccessLogs": "启用访问日志",
- "SettingsTabLoggingFsGlobalAccessLogMode": "全局访问日志模式:",
- "SettingsTabLoggingDeveloperOptions": "开发者选项",
- "SettingsTabLoggingDeveloperOptionsNote": "警告:会降低性能",
- "SettingsTabLoggingGraphicsBackendLogLevel": "图形后端日志级别:",
- "SettingsTabLoggingGraphicsBackendLogLevelNone": "无",
- "SettingsTabLoggingGraphicsBackendLogLevelError": "错误",
- "SettingsTabLoggingGraphicsBackendLogLevelPerformance": "减速",
- "SettingsTabLoggingGraphicsBackendLogLevelAll": "全部",
- "SettingsTabLoggingEnableDebugLogs": "启用调试日志",
- "SettingsTabInput": "输入",
- "SettingsTabInputEnableDockedMode": "主机模式",
- "SettingsTabInputDirectKeyboardAccess": "直通键盘控制",
- "SettingsButtonSave": "保存",
- "SettingsButtonClose": "取消",
- "SettingsButtonOk": "确定",
- "SettingsButtonCancel": "取消",
- "SettingsButtonApply": "应用",
- "ControllerSettingsPlayer": "玩家",
- "ControllerSettingsPlayer1": "玩家 1",
- "ControllerSettingsPlayer2": "玩家 2",
- "ControllerSettingsPlayer3": "玩家 3",
- "ControllerSettingsPlayer4": "玩家 4",
- "ControllerSettingsPlayer5": "玩家 5",
- "ControllerSettingsPlayer6": "玩家 6",
- "ControllerSettingsPlayer7": "玩家 7",
- "ControllerSettingsPlayer8": "玩家 8",
- "ControllerSettingsHandheld": "掌机模式",
- "ControllerSettingsInputDevice": "输入设备",
- "ControllerSettingsRefresh": "刷新",
- "ControllerSettingsDeviceDisabled": "关闭",
- "ControllerSettingsControllerType": "手柄类型",
- "ControllerSettingsControllerTypeHandheld": "掌机",
- "ControllerSettingsControllerTypeProController": "Pro 手柄",
- "ControllerSettingsControllerTypeJoyConPair": "JoyCon 组合",
- "ControllerSettingsControllerTypeJoyConLeft": "左 JoyCon",
- "ControllerSettingsControllerTypeJoyConRight": "右 JoyCon",
- "ControllerSettingsProfile": "预设",
- "ControllerSettingsProfileDefault": "默认布局",
- "ControllerSettingsLoad": "加载",
- "ControllerSettingsAdd": "新建",
- "ControllerSettingsRemove": "删除",
- "ControllerSettingsButtons": "按键",
- "ControllerSettingsButtonA": "A",
- "ControllerSettingsButtonB": "B",
- "ControllerSettingsButtonX": "X",
- "ControllerSettingsButtonY": "Y",
- "ControllerSettingsButtonPlus": "+",
- "ControllerSettingsButtonMinus": "-",
- "ControllerSettingsDPad": "方向键",
- "ControllerSettingsDPadUp": "上",
- "ControllerSettingsDPadDown": "下",
- "ControllerSettingsDPadLeft": "左",
- "ControllerSettingsDPadRight": "右",
- "ControllerSettingsStickButton": "按下摇杆",
- "ControllerSettingsStickUp": "上",
- "ControllerSettingsStickDown": "下",
- "ControllerSettingsStickLeft": "左",
- "ControllerSettingsStickRight": "右",
- "ControllerSettingsStickStick": "摇杆",
- "ControllerSettingsStickInvertXAxis": "反转 X 轴方向",
- "ControllerSettingsStickInvertYAxis": "反转 Y 轴方向",
- "ControllerSettingsStickDeadzone": "死区:",
- "ControllerSettingsLStick": "左摇杆",
- "ControllerSettingsRStick": "右摇杆",
- "ControllerSettingsTriggersLeft": "左扳机",
- "ControllerSettingsTriggersRight": "右扳机",
- "ControllerSettingsTriggersButtonsLeft": "左扳机键",
- "ControllerSettingsTriggersButtonsRight": "右扳机键",
- "ControllerSettingsTriggers": "扳机",
- "ControllerSettingsTriggerL": "L",
- "ControllerSettingsTriggerR": "R",
- "ControllerSettingsTriggerZL": "ZL",
- "ControllerSettingsTriggerZR": "ZR",
- "ControllerSettingsLeftSL": "SL",
- "ControllerSettingsLeftSR": "SR",
- "ControllerSettingsRightSL": "SL",
- "ControllerSettingsRightSR": "SR",
- "ControllerSettingsExtraButtonsLeft": "左背键",
- "ControllerSettingsExtraButtonsRight": "右背键",
- "ControllerSettingsMisc": "其他",
- "ControllerSettingsTriggerThreshold": "扳机阈值:",
- "ControllerSettingsMotion": "体感",
- "ControllerSettingsMotionUseCemuhookCompatibleMotion": "使用 CemuHook 体感协议",
- "ControllerSettingsMotionControllerSlot": "手柄:",
- "ControllerSettingsMotionMirrorInput": "镜像操作",
- "ControllerSettingsMotionRightJoyConSlot": "右JoyCon:",
- "ControllerSettingsMotionServerHost": "服务器Host:",
- "ControllerSettingsMotionGyroSensitivity": "陀螺仪敏感度:",
- "ControllerSettingsMotionGyroDeadzone": "陀螺仪死区:",
- "ControllerSettingsSave": "保存",
- "ControllerSettingsClose": "关闭",
- "UserProfilesSelectedUserProfile": "选择的用户账户:",
- "UserProfilesSaveProfileName": "保存名称",
- "UserProfilesChangeProfileImage": "更换头像",
- "UserProfilesAvailableUserProfiles": "现有账户:",
- "UserProfilesAddNewProfile": "新建账户",
- "UserProfilesDelete": "删除",
- "UserProfilesClose": "关闭",
- "ProfileNameSelectionWatermark": "选择昵称",
- "ProfileImageSelectionTitle": "选择头像",
- "ProfileImageSelectionHeader": "选择合适的头像图片",
- "ProfileImageSelectionNote": "您可以导入自定义头像,或从系统中选择头像",
- "ProfileImageSelectionImportImage": "导入图像文件",
- "ProfileImageSelectionSelectAvatar": "选择系统头像",
- "InputDialogTitle": "输入对话框",
- "InputDialogOk": "完成",
- "InputDialogCancel": "取消",
- "InputDialogAddNewProfileTitle": "选择用户名称",
- "InputDialogAddNewProfileHeader": "请输入账户名称",
- "InputDialogAddNewProfileSubtext": "(最大长度:{0})",
- "AvatarChoose": "选择头像",
- "AvatarSetBackgroundColor": "设置背景色",
- "AvatarClose": "关闭",
- "ControllerSettingsLoadProfileToolTip": "加载预设",
- "ControllerSettingsAddProfileToolTip": "新增预设",
- "ControllerSettingsRemoveProfileToolTip": "删除预设",
- "ControllerSettingsSaveProfileToolTip": "保存预设",
- "MenuBarFileToolsTakeScreenshot": "保存截图",
- "MenuBarFileToolsHideUi": "隐藏界面",
- "GameListContextMenuRunApplication": "运行应用",
- "GameListContextMenuToggleFavorite": "收藏",
- "GameListContextMenuToggleFavoriteToolTip": "标记喜爱的游戏",
- "SettingsTabGeneralTheme": "主题",
- "SettingsTabGeneralThemeCustomTheme": "自选主题路径",
- "SettingsTabGeneralThemeBaseStyle": "主题色调",
- "SettingsTabGeneralThemeBaseStyleDark": "暗黑",
- "SettingsTabGeneralThemeBaseStyleLight": "浅色",
- "SettingsTabGeneralThemeEnableCustomTheme": "使用自选主题界面",
- "ButtonBrowse": "浏览",
- "ControllerSettingsConfigureGeneral": "配置",
- "ControllerSettingsRumble": "震动",
- "ControllerSettingsRumbleStrongMultiplier": "强震动幅度",
- "ControllerSettingsRumbleWeakMultiplier": "弱震动幅度",
- "DialogMessageSaveNotAvailableMessage": "没有{0} [{1:x16}]的游戏存档",
- "DialogMessageSaveNotAvailableCreateSaveMessage": "是否创建该游戏的存档文件夹?",
- "DialogConfirmationTitle": "Ryujinx - 设置",
- "DialogUpdaterTitle": "Ryujinx - 更新",
- "DialogErrorTitle": "Ryujinx - 错误",
- "DialogWarningTitle": "Ryujinx - 警告",
- "DialogExitTitle": "Ryujinx - 关闭",
- "DialogErrorMessage": "Ryujinx 发生错误",
- "DialogExitMessage": "是否关闭 Ryujinx?",
- "DialogExitSubMessage": "未保存的进度将会丢失!",
- "DialogMessageCreateSaveErrorMessage": "创建特定的存档时出错:{0}",
- "DialogMessageFindSaveErrorMessage": "查找特定的存档时出错:{0}",
- "FolderDialogExtractTitle": "选择要解压到的文件夹",
- "DialogNcaExtractionMessage": "提取 {1} 的 {0} 分区...",
- "DialogNcaExtractionTitle": "Ryujinx - NCA分区提取",
- "DialogNcaExtractionMainNcaNotFoundErrorMessage": "提取失败。所选文件中不含主NCA文件",
- "DialogNcaExtractionCheckLogErrorMessage": "提取失败。请查看日志文件获取详情。",
- "DialogNcaExtractionSuccessMessage": "提取成功。",
- "DialogUpdaterConvertFailedMessage": "无法转换当前 Ryujinx 版本。",
- "DialogUpdaterCancelUpdateMessage": "更新取消!",
- "DialogUpdaterAlreadyOnLatestVersionMessage": "您使用的 Ryujinx 是最新版本。",
- "DialogUpdaterFailedToGetVersionMessage": "尝试从 Github 获取版本信息时无效。\n可能由于 GitHub Actions 正在编译新版本。请过一会再试。",
- "DialogUpdaterConvertFailedGithubMessage": "无法转换从 Github 接收到的 Ryujinx 版本。",
- "DialogUpdaterDownloadingMessage": "下载新版本中...",
- "DialogUpdaterExtractionMessage": "正在提取更新...",
- "DialogUpdaterRenamingMessage": "正在删除旧文件...",
- "DialogUpdaterAddingFilesMessage": "安装更新中...",
- "DialogUpdaterCompleteMessage": "更新成功!",
- "DialogUpdaterRestartMessage": "立即重启 Ryujinx 完成更新?",
- "DialogUpdaterArchNotSupportedMessage": "您运行的系统架构不受支持!",
- "DialogUpdaterArchNotSupportedSubMessage": "(仅支持 x64 系统)",
- "DialogUpdaterNoInternetMessage": "没有连接到互联网",
- "DialogUpdaterNoInternetSubMessage": "请确保互联网连接正常。",
- "DialogUpdaterDirtyBuildMessage": "不能更新非官方版本的 Ryujinx!",
- "DialogUpdaterDirtyBuildSubMessage": "如果希望使用受支持的版本,请您在 https://ryujinx.org/ 下载。",
- "DialogRestartRequiredMessage": "需要重启模拟器",
- "DialogThemeRestartMessage": "主题设置已保存。需要重新启动才能生效。",
- "DialogThemeRestartSubMessage": "您是否要重启?",
- "DialogFirmwareInstallEmbeddedMessage": "要安装游戏内置的固件吗?(固件 {0})",
- "DialogFirmwareInstallEmbeddedSuccessMessage": "未找到已安装的固件,但 Ryujinx 可以从现有的游戏安装固件{0}.\n模拟器现在可以运行。",
- "DialogFirmwareNoFirmwareInstalledMessage": "未安装固件",
- "DialogFirmwareInstalledMessage": "已安装固件 {0}",
- "DialogInstallFileTypesSuccessMessage": "关联文件类型成功!",
- "DialogInstallFileTypesErrorMessage": "关联文件类型失败。",
- "DialogUninstallFileTypesSuccessMessage": "成功解除文件类型关联!",
- "DialogUninstallFileTypesErrorMessage": "解除文件类型关联失败。",
- "DialogOpenSettingsWindowLabel": "打开设置窗口",
- "DialogControllerAppletTitle": "控制器小窗口",
- "DialogMessageDialogErrorExceptionMessage": "显示消息对话框时出错:{0}",
- "DialogSoftwareKeyboardErrorExceptionMessage": "显示软件键盘时出错:{0}",
- "DialogErrorAppletErrorExceptionMessage": "显示错误对话框时出错:{0}",
- "DialogUserErrorDialogMessage": "{0}: {1}",
- "DialogUserErrorDialogInfoMessage": "\n有关修复此错误的更多信息,可以遵循我们的设置指南。",
- "DialogUserErrorDialogTitle": "Ryujinx 错误 ({0})",
- "DialogAmiiboApiTitle": "Amiibo API",
- "DialogAmiiboApiFailFetchMessage": "从 API 获取信息时出错。",
- "DialogAmiiboApiConnectErrorMessage": "无法连接到 Amiibo API 服务器。服务器可能已关闭,或者您没有连接网络。",
- "DialogProfileInvalidProfileErrorMessage": "预设 {0} 与当前输入配置系统不兼容。",
- "DialogProfileDefaultProfileOverwriteErrorMessage": "默认预设不能被覆盖",
- "DialogProfileDeleteProfileTitle": "删除预设",
- "DialogProfileDeleteProfileMessage": "删除后不可恢复,确认删除吗?",
- "DialogWarning": "警告",
- "DialogPPTCDeletionMessage": "您即将删除:\n\n{0} 的 PPTC 缓存\n\n确定吗?",
- "DialogPPTCDeletionErrorMessage": "清除位于 {0} 的 PPTC 缓存时出错:{1}",
- "DialogShaderDeletionMessage": "您即将删除:\n\n{0} 的着色器缓存\n\n确定吗?",
- "DialogShaderDeletionErrorMessage": "清除位于 {0} 的着色器缓存时出错:{1}",
- "DialogRyujinxErrorMessage": "Ryujinx 遇到错误",
- "DialogInvalidTitleIdErrorMessage": "UI错误:所选游戏没有有效的标题ID",
- "DialogFirmwareInstallerFirmwareNotFoundErrorMessage": "路径 {0} 找不到有效的系统固件。",
- "DialogFirmwareInstallerFirmwareInstallTitle": "固件 {0}",
- "DialogFirmwareInstallerFirmwareInstallMessage": "即将安装系统版本 {0} 。",
- "DialogFirmwareInstallerFirmwareInstallSubMessage": "\n\n会替换当前系统版本 {0} 。",
- "DialogFirmwareInstallerFirmwareInstallConfirmMessage": "\n\n是否确认继续?",
- "DialogFirmwareInstallerFirmwareInstallWaitMessage": "安装固件中...",
- "DialogFirmwareInstallerFirmwareInstallSuccessMessage": "成功安装系统版本 {0} 。",
- "DialogUserProfileDeletionWarningMessage": "删除后将没有可选择的用户账户",
- "DialogUserProfileDeletionConfirmMessage": "是否删除选择的账户",
- "DialogUserProfileUnsavedChangesTitle": "警告 - 未保存的更改",
- "DialogUserProfileUnsavedChangesMessage": "您为该用户做出的部分改动尚未保存。",
- "DialogUserProfileUnsavedChangesSubMessage": "是否舍弃这些改动?",
- "DialogControllerSettingsModifiedConfirmMessage": "目前的输入预设已更新",
- "DialogControllerSettingsModifiedConfirmSubMessage": "要保存吗?",
- "DialogLoadNcaErrorMessage": "{0}. 错误的文件:{1}",
- "DialogDlcNoDlcErrorMessage": "选择的文件不包含所选游戏的 DLC!",
- "DialogPerformanceCheckLoggingEnabledMessage": "您启用了跟踪日志,仅供开发人员使用。",
- "DialogPerformanceCheckLoggingEnabledConfirmMessage": "为了获得最佳性能,建议禁用跟踪日志记录。您是否要立即禁用?",
- "DialogPerformanceCheckShaderDumpEnabledMessage": "您启用了着色器转储,仅供开发人员使用。",
- "DialogPerformanceCheckShaderDumpEnabledConfirmMessage": "为了获得最佳性能,建议禁用着色器转储。您是否要立即禁用?",
- "DialogLoadAppGameAlreadyLoadedMessage": "已有游戏正在运行",
- "DialogLoadAppGameAlreadyLoadedSubMessage": "请停止模拟或关闭程序,再启动另一个游戏。",
- "DialogUpdateAddUpdateErrorMessage": "选择的文件不包含所选游戏的更新!",
- "DialogSettingsBackendThreadingWarningTitle": "警告 - 后端多线程",
- "DialogSettingsBackendThreadingWarningMessage": "改变此选项后必须重启 Ryujinx 才能生效。\n\n取决于您的硬件,可能需要手动禁用驱动面板中的线程优化。",
- "SettingsTabGraphicsFeaturesOptions": "功能",
- "SettingsTabGraphicsBackendMultithreading": "多线程图形后端:",
- "CommonAuto": "自动(推荐)",
- "CommonOff": "关闭",
- "CommonOn": "打开",
- "InputDialogYes": "是",
- "InputDialogNo": "否",
- "DialogProfileInvalidProfileNameErrorMessage": "文件名包含无效字符,请重试。",
- "MenuBarOptionsPauseEmulation": "暂停",
- "MenuBarOptionsResumeEmulation": "继续",
- "AboutUrlTooltipMessage": "在浏览器中打开 Ryujinx 官网。",
- "AboutDisclaimerMessage": "Ryujinx 以任何方式与 Nintendo™ 及其任何商业伙伴都没有关联",
- "AboutAmiiboDisclaimerMessage": "我们的 Amiibo 模拟使用了\nAmiiboAPI (www.amiiboapi.com) ",
- "AboutPatreonUrlTooltipMessage": "在浏览器中打开 Ryujinx 的 Patreon 赞助页。",
- "AboutGithubUrlTooltipMessage": "在浏览器中打开 Ryujinx 的 GitHub 代码库。",
- "AboutDiscordUrlTooltipMessage": "在浏览器中打开 Ryujinx 的 Discord 邀请链接。",
- "AboutTwitterUrlTooltipMessage": "在浏览器中打开 Ryujinx 的 Twitter 主页。",
- "AboutRyujinxAboutTitle": "关于:",
- "AboutRyujinxAboutContent": "Ryujinx 是一款 Nintendo Switch™ 模拟器。\n您可以在 Patreon 上赞助 Ryujinx。\n关注 Twitter 或 Discord 可以获取模拟器最新动态。\n如果您对开发感兴趣,欢迎来 GitHub 或 Discord 加入我们!",
- "AboutRyujinxMaintainersTitle": "由以下作者维护:",
- "AboutRyujinxMaintainersContentTooltipMessage": "在浏览器中打开贡献者页面",
- "AboutRyujinxSupprtersTitle": "感谢 Patreon 的赞助者:",
- "AmiiboSeriesLabel": "Amiibo 系列",
- "AmiiboCharacterLabel": "角色",
- "AmiiboScanButtonLabel": "扫描",
- "AmiiboOptionsShowAllLabel": "显示所有 Amiibo 系列",
- "AmiiboOptionsUsRandomTagLabel": "修复:使用随机标记的 UUID",
- "DlcManagerTableHeadingEnabledLabel": "启用",
- "DlcManagerTableHeadingTitleIdLabel": "游戏ID",
- "DlcManagerTableHeadingContainerPathLabel": "文件夹路径",
- "DlcManagerTableHeadingFullPathLabel": "完整路径",
- "DlcManagerRemoveAllButton": "全部删除",
- "DlcManagerEnableAllButton": "全部启用",
- "DlcManagerDisableAllButton": "全部禁用",
- "MenuBarOptionsChangeLanguage": "更改语言",
- "MenuBarShowFileTypes": "主页显示的文件类型",
- "CommonSort": "排序",
- "CommonShowNames": "显示名称",
- "CommonFavorite": "收藏",
- "OrderAscending": "从小到大",
- "OrderDescending": "从大到小",
- "SettingsTabGraphicsFeatures": "功能与增强",
- "ErrorWindowTitle": "错误窗口",
- "ToggleDiscordTooltip": "控制是否在 Discord 中显示您的游玩状态",
- "AddGameDirBoxTooltip": "输入要添加的游戏目录",
- "AddGameDirTooltip": "添加游戏目录到列表中",
- "RemoveGameDirTooltip": "移除选中的目录",
- "CustomThemeCheckTooltip": "使用自定义UI主题来更改模拟器的外观样式",
- "CustomThemePathTooltip": "自定义主题的目录",
- "CustomThemeBrowseTooltip": "查找自定义主题",
- "DockModeToggleTooltip": "启用 Switch 的主机模式。\n绝大多数游戏画质会提高,略微增加性能消耗。\n在掌机和主机模式切换的过程中,您可能需要重新设置手柄类型。",
- "DirectKeyboardTooltip": "开启 \"直连键盘访问(HID)支持\"\n(部分游戏可以使用您的键盘输入文字)",
- "DirectMouseTooltip": "开启 \"直连鼠标访问(HID)支持\"\n(部分游戏可以使用您的鼠标导航)",
- "RegionTooltip": "更改系统区域",
- "LanguageTooltip": "更改系统语言",
- "TimezoneTooltip": "更改系统时区",
- "TimeTooltip": "更改系统时钟",
- "VSyncToggleTooltip": "关闭后,小部分游戏可以超过60FPS帧率,以获得高帧率体验。\n但是可能出现软锁或读盘时间增加。\n如不确定,就请保持开启状态。",
- "PptcToggleTooltip": "缓存编译完成的游戏CPU指令。减少启动时间和卡顿,提高游戏响应速度。\n如不确定,就请保持开启状态。",
- "FsIntegrityToggleTooltip": "检查游戏文件内容的完整性。\n遇到损坏的文件则记录到日志文件,有助于排查错误。\n对性能没有影响。\n如不确定,就请保持开启状态。",
- "AudioBackendTooltip": "默认推荐SDL2,但每种音频后端对各类游戏兼容性不同,遇到音频问题可以尝试切换后端。",
- "MemoryManagerTooltip": "改变 Switch 内存映射到电脑内存的方式,会影响CPU性能消耗。",
- "MemoryManagerSoftwareTooltip": "使用软件内存页管理,最精确但是速度最慢。",
- "MemoryManagerHostTooltip": "直接映射内存页到电脑内存,使得即时编译效率更高。",
- "MemoryManagerUnsafeTooltip": "直接映射内存页,但不检查内存溢出,使得即时编译效率更高。\nRyujinx 可以访问任何位置的内存,因而相对不安全。\n此模式下只应运行您信任的游戏或软件(即官方游戏)。",
- "UseHypervisorTooltip": "使用 Hypervisor 虚拟机代替即时编译。在可用的情况下能大幅提高性能。但目前可能不稳定。",
- "DRamTooltip": "使用Switch开发机的内存布局。\n不会提高任何性能,某些高清纹理包或 4k 分辨率 MOD 可能需要此选项。\n如果不确定,请始终关闭该选项。",
- "IgnoreMissingServicesTooltip": "开启后,游戏会忽略未实现的系统服务,从而继续运行。\n少部分新发布的游戏由于使用新的未知系统服务,可能需要此选项来避免闪退。\n模拟器更新完善系统服务之后,则无需开启选项。\n如您的游戏已经正常运行,请保持此选项关闭。",
- "GraphicsBackendThreadingTooltip": "在第二个线程上执行图形后端命令。\n\n加速着色器编译,减少卡顿,提高 GPU 的性能。\n\n如果不确定,请设置为自动。",
- "GalThreadingTooltip": "在第二个线程上执行图形后端命令。\n\n加速着色器编译,减少卡顿,提高 GPU 的性能。\n\n如果不确定,请设置为自动。",
- "ShaderCacheToggleTooltip": "开启后,模拟器会保存编译完成的着色器到磁盘,减少游戏渲染新特效和场景时的卡顿。",
- "ResolutionScaleTooltip": "缩放渲染的分辨率",
- "ResolutionScaleEntryTooltip": "尽可能使用例如1.5的浮点倍数。非整数的倍率易引起 BUG。",
- "AnisotropyTooltip": "各向异性过滤等级。提高倾斜视角纹理的清晰度\n('自动'使用游戏默认的等级)",
- "AspectRatioTooltip": "渲染窗口的宽高比。",
- "ShaderDumpPathTooltip": "转储图形着色器的路径",
- "FileLogTooltip": "保存日志文件到硬盘。不会影响性能。",
- "StubLogTooltip": "在控制台中打印 stub 日志消息。不影响性能。",
- "InfoLogTooltip": "在控制台中打印信息日志消息。不影响性能。",
- "WarnLogTooltip": "在控制台中打印警告日志消息。不影响性能。",
- "ErrorLogTooltip": "打印控制台中的错误日志消息。不影响性能。",
- "TraceLogTooltip": "在控制台中打印跟踪日志消息。不影响性能。",
- "GuestLogTooltip": "在控制台中打印访客日志消息。不影响性能。",
- "FileAccessLogTooltip": "在控制台中打印文件访问日志信息。",
- "FSAccessLogModeTooltip": "启用访问日志输出到控制台。可能的模式为 0-3",
- "DeveloperOptionTooltip": "谨慎使用",
- "OpenGlLogLevel": "需要打开适当的日志等级",
- "DebugLogTooltip": "记录Debug消息",
- "LoadApplicationFileTooltip": "选择 Switch 支持的游戏格式并加载",
- "LoadApplicationFolderTooltip": "选择解包后的 Switch 游戏并加载",
- "OpenRyujinxFolderTooltip": "打开 Ryujinx 系统目录",
- "OpenRyujinxLogsTooltip": "打开日志存放的目录",
- "ExitTooltip": "关闭 Ryujinx",
- "OpenSettingsTooltip": "打开设置窗口",
- "OpenProfileManagerTooltip": "打开用户账户管理界面",
- "StopEmulationTooltip": "停止运行当前游戏并回到主界面",
- "CheckUpdatesTooltip": "检查 Ryujinx 新版本",
- "OpenAboutTooltip": "打开“关于”窗口",
- "GridSize": "网格尺寸",
- "GridSizeTooltip": "调整网格模式的大小",
- "SettingsTabSystemSystemLanguageBrazilianPortuguese": "巴西葡萄牙语",
- "AboutRyujinxContributorsButtonHeader": "查看所有参与者",
- "SettingsTabSystemAudioVolume": "音量:",
- "AudioVolumeTooltip": "调节音量",
- "SettingsTabSystemEnableInternetAccess": "允许网络访问/局域网模式",
- "EnableInternetAccessTooltip": "允许模拟的游戏进程访问互联网。\n当多个模拟器/真实的 Switch 连接到同一个局域网时,带有 LAN 模式的游戏可以相互通信。\n即使开启选项也无法访问 Nintendo 服务器。此外可能导致某些尝试联网的游戏崩溃。\n如果您不确定,请关闭该选项。",
- "GameListContextMenuManageCheatToolTip": "管理金手指",
- "GameListContextMenuManageCheat": "管理金手指",
- "ControllerSettingsStickRange": "范围:",
- "DialogStopEmulationTitle": "Ryujinx - 停止模拟",
- "DialogStopEmulationMessage": "是否确定停止模拟?",
- "SettingsTabCpu": "CPU",
- "SettingsTabAudio": "音频",
- "SettingsTabNetwork": "网络",
- "SettingsTabNetworkConnection": "网络连接",
- "SettingsTabCpuCache": "CPU 缓存",
- "SettingsTabCpuMemory": "CPU 内存",
- "DialogUpdaterFlatpakNotSupportedMessage": "请通过 FlatHub 更新 Ryujinx。",
- "UpdaterDisabledWarningTitle": "更新已禁用!",
- "GameListContextMenuOpenSdModsDirectory": "打开 Atmosphere MOD 目录",
- "GameListContextMenuOpenSdModsDirectoryToolTip": "打开适用于 Atmosphere 自制系统的 MOD 目录",
- "ControllerSettingsRotate90": "顺时针旋转 90°",
- "IconSize": "图标尺寸",
- "IconSizeTooltip": "更改游戏图标大小",
- "MenuBarOptionsShowConsole": "显示控制台",
- "ShaderCachePurgeError": "清除着色器缓存时出错:{0}: {1}",
- "UserErrorNoKeys": "找不到密钥",
- "UserErrorNoFirmware": "找不到固件",
- "UserErrorFirmwareParsingFailed": "固件解析错误",
- "UserErrorApplicationNotFound": "找不到应用程序",
- "UserErrorUnknown": "未知错误",
- "UserErrorUndefined": "未定义错误",
- "UserErrorNoKeysDescription": "Ryujinx 找不到 'prod.keys' 文件",
- "UserErrorNoFirmwareDescription": "Ryujinx 找不到任何已安装的固件",
- "UserErrorFirmwareParsingFailedDescription": "Ryujinx 无法解密选择的固件。这通常是由于使用了过旧的密钥。",
- "UserErrorApplicationNotFoundDescription": "Ryujinx 在选中路径找不到有效的应用程序。",
- "UserErrorUnknownDescription": "发生未知错误!",
- "UserErrorUndefinedDescription": "发生了未定义错误!此类错误不应出现,请联系开发人员!",
- "OpenSetupGuideMessage": "打开设置教程",
- "NoUpdate": "无更新",
- "TitleUpdateVersionLabel": "版本 {0}",
- "RyujinxInfo": "Ryujinx - 信息",
- "RyujinxConfirm": "Ryujinx - 确认",
- "FileDialogAllTypes": "全部类型",
- "Never": "从不",
- "SwkbdMinCharacters": "至少应为 {0} 个字长",
- "SwkbdMinRangeCharacters": "必须为 {0}-{1} 个字长",
- "SoftwareKeyboard": "软件键盘",
- "SoftwareKeyboardModeNumbersOnly": "只接受数字",
- "SoftwareKeyboardModeAlphabet": "只接受非中日韩文字",
- "SoftwareKeyboardModeASCII": "只接受 ASCII 符号",
- "DialogControllerAppletMessagePlayerRange": "游戏需要 {0} 个玩家并满足以下要求:\n\n手柄类型:{1}\n\n玩家类型:{2}\n\n{3} 请打开设置窗口,重新配置手柄输入;或者关闭返回。",
- "DialogControllerAppletMessage": "游戏需要刚好 {0} 个玩家并满足以下要求:\n\n手柄类型:{1}\n\n玩家类型:{2}\n\n{3} 请打开设置窗口,重新配置手柄输入;或者关闭返回。",
- "DialogControllerAppletDockModeSet": "目前处于主机模式,无法使用掌机操作方式",
- "UpdaterRenaming": "正在删除旧文件...",
- "UpdaterRenameFailed": "更新过程中无法重命名文件:{0}",
- "UpdaterAddingFiles": "安装更新中...",
- "UpdaterExtracting": "正在提取更新...",
- "UpdaterDownloading": "下载新版本中...",
- "Game": "游戏",
- "Docked": "主机模式",
- "Handheld": "掌机模式",
- "ConnectionError": "连接错误。",
- "AboutPageDeveloperListMore": "{0} 等开发者...",
- "ApiError": "API错误。",
- "LoadingHeading": "正在启动 {0}",
- "CompilingPPTC": "编译PPTC缓存中",
- "CompilingShaders": "编译着色器中",
- "AllKeyboards": "所有键盘",
- "OpenFileDialogTitle": "选择一个支持的文件以打开",
- "OpenFolderDialogTitle": "选择一个包含解包游戏的文件夹",
- "AllSupportedFormats": "所有支持的格式",
- "RyujinxUpdater": "Ryujinx 更新程序",
- "SettingsTabHotkeys": "快捷键",
- "SettingsTabHotkeysHotkeys": "键盘快捷键",
- "SettingsTabHotkeysToggleVsyncHotkey": "切换垂直同步:",
- "SettingsTabHotkeysScreenshotHotkey": "截屏:",
- "SettingsTabHotkeysShowUiHotkey": "隐藏 界面:",
- "SettingsTabHotkeysPauseHotkey": "暂停:",
- "SettingsTabHotkeysToggleMuteHotkey": "静音:",
- "ControllerMotionTitle": "体感操作设置",
- "ControllerRumbleTitle": "震动设置",
- "SettingsSelectThemeFileDialogTitle": "选择主题文件",
- "SettingsXamlThemeFile": "Xaml 主题文件",
- "AvatarWindowTitle": "管理账户 - 头像",
- "Amiibo": "Amiibo",
- "Unknown": "未知",
- "Usage": "扫描可获得",
- "Writable": "可写入",
- "SelectDlcDialogTitle": "选择 DLC 文件",
- "SelectUpdateDialogTitle": "选择更新文件",
- "UserProfileWindowTitle": "管理用户账户",
- "CheatWindowTitle": "金手指管理器",
- "DlcWindowTitle": "管理 {0} ({1}) 的 DLC",
- "UpdateWindowTitle": "游戏更新管理器",
- "CheatWindowHeading": "适用于 {0} [{1}] 的金手指",
- "BuildId": "游戏版本ID:",
- "DlcWindowHeading": "{0} 个适用于 {1} ({2}) 的 DLC",
- "UserProfilesEditProfile": "编辑选中账户",
- "Cancel": "取消",
- "Save": "保存",
- "Discard": "返回",
- "UserProfilesSetProfileImage": "选择头像",
- "UserProfileEmptyNameError": "必须输入名称",
- "UserProfileNoImageError": "请选择您的头像",
- "GameUpdateWindowHeading": "管理 {0} ({1}) 的更新",
- "SettingsTabHotkeysResScaleUpHotkey": "提高分辨率:",
- "SettingsTabHotkeysResScaleDownHotkey": "降低分辨率:",
- "UserProfilesName": "名称:",
- "UserProfilesUserId": "用户ID:",
- "SettingsTabGraphicsBackend": "图形后端",
- "SettingsTabGraphicsBackendTooltip": "显卡使用的图形后端",
- "SettingsEnableTextureRecompression": "启用纹理重压缩",
- "SettingsEnableTextureRecompressionTooltip": "压缩某些纹理以减少显存的使用。\n适合显存小于 4GiB 的 GPU 开启。\n如果您不确定,请保持此项关闭。",
- "SettingsTabGraphicsPreferredGpu": "首选 GPU",
- "SettingsTabGraphicsPreferredGpuTooltip": "选择 Vulkan API 使用的显卡。\n此选项不会影响 OpenGL API。\n如果您不确定,建议选择\"dGPU(独立显卡)\"。如果没有独立显卡,则无需改动此选项。",
- "SettingsAppRequiredRestartMessage": "Ryujinx 需要重启",
- "SettingsGpuBackendRestartMessage": "您修改了图形 API 或显卡设置。需要重新启动才能生效",
- "SettingsGpuBackendRestartSubMessage": "是否重启模拟器?",
- "RyujinxUpdaterMessage": "是否更新 Ryujinx 到最新的版本?",
- "SettingsTabHotkeysVolumeUpHotkey": "音量加:",
- "SettingsTabHotkeysVolumeDownHotkey": "音量减:",
- "SettingsEnableMacroHLE": "启用 HLE 宏",
- "SettingsEnableMacroHLETooltip": "GPU 宏代码的高级模拟。\n提高性能表现,但可能在某些游戏中引起图形错误。\n如果您不确定,请保持此项开启。",
- "SettingsEnableColorSpacePassthrough": "颜色空间穿透",
- "SettingsEnableColorSpacePassthroughTooltip": "指示 Vulkan 后端在不指定颜色空间的情况下传递颜色信息。对于具有宽色域显示器的用户来说,这可能会以颜色正确性为代价,产生更鲜艳的颜色。",
- "VolumeShort": "音量",
- "UserProfilesManageSaves": "管理存档",
- "DeleteUserSave": "确定删除这个游戏的存档吗?",
- "IrreversibleActionNote": "删除后不可恢复。",
- "SaveManagerHeading": "管理 {0} ({1}) 的存档",
- "SaveManagerTitle": "存档管理器",
- "Name": "名称",
- "Size": "大小",
- "Search": "搜索",
- "UserProfilesRecoverLostAccounts": "恢复丢失的账户",
- "Recover": "恢复",
- "UserProfilesRecoverHeading": "找到了这些用户的存档数据",
- "UserProfilesRecoverEmptyList": "没有可以恢复的配置文件",
- "GraphicsAATooltip": "将抗锯齿使用到游戏渲染中",
- "GraphicsAALabel": "抗锯齿:",
- "GraphicsScalingFilterLabel": "缩放过滤:",
- "GraphicsScalingFilterTooltip": "对帧缓冲区进行缩放",
- "GraphicsScalingFilterLevelLabel": "等级",
- "GraphicsScalingFilterLevelTooltip": "设置缩放过滤级别",
- "SmaaLow": "SMAA 低质量",
- "SmaaMedium": "SMAA 中质量",
- "SmaaHigh": "SMAA 高质量",
- "SmaaUltra": "SMAA 极致质量",
- "UserEditorTitle": "编辑用户",
- "UserEditorTitleCreate": "创建用户",
- "SettingsTabNetworkInterface": "网络接口:",
- "NetworkInterfaceTooltip": "用于局域网功能的网络接口",
- "NetworkInterfaceDefault": "默认",
- "PackagingShaders": "整合着色器中",
- "AboutChangelogButton": "在Github上查看更新日志",
- "AboutChangelogButtonTooltipMessage": "点击这里在您的默认浏览器中打开此版本的更新日志。"
-}
\ No newline at end of file
diff --git a/src/Ryujinx.Ava/Assets/Locales/zh_TW.json b/src/Ryujinx.Ava/Assets/Locales/zh_TW.json
deleted file mode 100644
index a2f59f60d..000000000
--- a/src/Ryujinx.Ava/Assets/Locales/zh_TW.json
+++ /dev/null
@@ -1,656 +0,0 @@
-{
- "Language": "英文 (美國)",
- "MenuBarFileOpenApplet": "開啟 Applet 應用程序",
- "MenuBarFileOpenAppletOpenMiiAppletToolTip": "開啟獨立的Mii修改器應用程序",
- "SettingsTabInputDirectMouseAccess": "滑鼠直接操作",
- "SettingsTabSystemMemoryManagerMode": "記憶體管理模式:",
- "SettingsTabSystemMemoryManagerModeSoftware": "軟體",
- "SettingsTabSystemMemoryManagerModeHost": "主機模式 (快速)",
- "SettingsTabSystemMemoryManagerModeHostUnchecked": "主機略過檢查模式 (最快, 但不安全)",
- "SettingsTabSystemUseHypervisor": "使用 Hypervisor",
- "MenuBarFile": "_檔案",
- "MenuBarFileOpenFromFile": "_載入檔案",
- "MenuBarFileOpenUnpacked": "載入_已解開封裝的遊戲",
- "MenuBarFileOpenEmuFolder": "開啟 Ryujinx 資料夾",
- "MenuBarFileOpenLogsFolder": "開啟日誌資料夾",
- "MenuBarFileExit": "_退出",
- "MenuBarOptions": "選項",
- "MenuBarOptionsToggleFullscreen": "切換全螢幕模式",
- "MenuBarOptionsStartGamesInFullscreen": "使用全螢幕模式啟動遊戲",
- "MenuBarOptionsStopEmulation": "停止模擬",
- "MenuBarOptionsSettings": "_設定",
- "MenuBarOptionsManageUserProfiles": "_管理使用者帳戶",
- "MenuBarActions": "_動作",
- "MenuBarOptionsSimulateWakeUpMessage": "模擬喚醒訊息",
- "MenuBarActionsScanAmiibo": "掃描 Amiibo",
- "MenuBarTools": "_工具",
- "MenuBarToolsInstallFirmware": "安裝韌體",
- "MenuBarFileToolsInstallFirmwareFromFile": "從 XCI 或 ZIP 安裝韌體",
- "MenuBarFileToolsInstallFirmwareFromDirectory": "從資料夾安裝韌體",
- "MenuBarToolsManageFileTypes": "管理檔案類型",
- "MenuBarToolsInstallFileTypes": "註冊檔案類型",
- "MenuBarToolsUninstallFileTypes": "取消註冊檔案類型",
- "MenuBarHelp": "幫助",
- "MenuBarHelpCheckForUpdates": "檢查更新",
- "MenuBarHelpAbout": "關於",
- "MenuSearch": "搜尋...",
- "GameListHeaderFavorite": "收藏",
- "GameListHeaderIcon": "圖示",
- "GameListHeaderApplication": "名稱",
- "GameListHeaderDeveloper": "開發人員",
- "GameListHeaderVersion": "版本",
- "GameListHeaderTimePlayed": "遊玩時數",
- "GameListHeaderLastPlayed": "最近遊玩",
- "GameListHeaderFileExtension": "副檔名",
- "GameListHeaderFileSize": "檔案大小",
- "GameListHeaderPath": "路徑",
- "GameListContextMenuOpenUserSaveDirectory": "開啟使用者存檔資料夾",
- "GameListContextMenuOpenUserSaveDirectoryToolTip": "開啟此遊戲的存檔資料夾",
- "GameListContextMenuOpenDeviceSaveDirectory": "開啟系統資料夾",
- "GameListContextMenuOpenDeviceSaveDirectoryToolTip": "開啟此遊戲的系統設定資料夾",
- "GameListContextMenuOpenBcatSaveDirectory": "開啟 BCAT 資料夾",
- "GameListContextMenuOpenBcatSaveDirectoryToolTip": "開啟此遊戲的 BCAT 資料夾\n",
- "GameListContextMenuManageTitleUpdates": "管理遊戲更新",
- "GameListContextMenuManageTitleUpdatesToolTip": "開啟遊戲更新管理視窗",
- "GameListContextMenuManageDlc": "管理 DLC",
- "GameListContextMenuManageDlcToolTip": "開啟 DLC 管理視窗",
- "GameListContextMenuOpenModsDirectory": "開啟模組資料夾",
- "GameListContextMenuOpenModsDirectoryToolTip": "開啟此遊戲的模組資料夾",
- "GameListContextMenuCacheManagement": "快取管理",
- "GameListContextMenuCacheManagementPurgePptc": "清除 PPTC 快取",
- "GameListContextMenuCacheManagementPurgePptcToolTip": "刪除遊戲的 PPTC 快取",
- "GameListContextMenuCacheManagementPurgeShaderCache": "清除著色器快取",
- "GameListContextMenuCacheManagementPurgeShaderCacheToolTip": "刪除遊戲的著色器快取",
- "GameListContextMenuCacheManagementOpenPptcDirectory": "開啟 PPTC 資料夾",
- "GameListContextMenuCacheManagementOpenPptcDirectoryToolTip": "開啟此遊戲的 PPTC 快取資料夾",
- "GameListContextMenuCacheManagementOpenShaderCacheDirectory": "開啟著色器快取資料夾",
- "GameListContextMenuCacheManagementOpenShaderCacheDirectoryToolTip": "開啟此遊戲的著色器快取資料夾",
- "GameListContextMenuExtractData": "提取資料",
- "GameListContextMenuExtractDataExeFS": "ExeFS",
- "GameListContextMenuExtractDataExeFSToolTip": "從遊戲的目前狀態中提取 ExeFS 分區(包含更新)",
- "GameListContextMenuExtractDataRomFS": "RomFS",
- "GameListContextMenuExtractDataRomFSToolTip": "從遊戲的目前狀態中提取 RomFS 分區(包含更新)",
- "GameListContextMenuExtractDataLogo": "圖示",
- "GameListContextMenuExtractDataLogoToolTip": "從遊戲的目前狀態中提取圖示(包含更新)",
- "StatusBarGamesLoaded": "{0}/{1} 遊戲載入完成",
- "StatusBarSystemVersion": "系統版本: {0}",
- "LinuxVmMaxMapCountDialogTitle": "檢測到映射的記憶體上限過低",
- "LinuxVmMaxMapCountDialogTextPrimary": "你願意增加 vm.max_map_count to {0} 的數值嗎?",
- "LinuxVmMaxMapCountDialogTextSecondary": "遊戲佔用的記憶體超出了映射的上限. Ryujinx的處理程序即將面臨崩潰.",
- "LinuxVmMaxMapCountDialogButtonUntilRestart": "碓定 (直至下一次重新啟動)",
- "LinuxVmMaxMapCountDialogButtonPersistent": "碓定 (永遠設定)",
- "LinuxVmMaxMapCountWarningTextPrimary": "映射記憶體的最大值少於目前建議的下限.",
- "LinuxVmMaxMapCountWarningTextSecondary": "目前 vm.max_map_count ({0}) 的數值少於 {1}. 遊戲佔用的記憶體超出了映射的上限. Ryujinx的處理程序即將面臨崩潰.\n\n你可能需要手動增加上限或安裝 pkexec, 這些都能協助Ryujinx完成此操作.",
- "Settings": "設定",
- "SettingsTabGeneral": "使用者介面",
- "SettingsTabGeneralGeneral": "一般",
- "SettingsTabGeneralEnableDiscordRichPresence": "啟用 Discord 動態狀態展示",
- "SettingsTabGeneralCheckUpdatesOnLaunch": "自動檢查更新",
- "SettingsTabGeneralShowConfirmExitDialog": "顯示「確認離開」對話框",
- "SettingsTabGeneralHideCursor": "隱藏滑鼠遊標:",
- "SettingsTabGeneralHideCursorNever": "永不",
- "SettingsTabGeneralHideCursorOnIdle": "自動隱藏滑鼠",
- "SettingsTabGeneralHideCursorAlways": "總是",
- "SettingsTabGeneralGameDirectories": "遊戲資料夾",
- "SettingsTabGeneralAdd": "新增",
- "SettingsTabGeneralRemove": "刪除",
- "SettingsTabSystem": "系統",
- "SettingsTabSystemCore": "核心",
- "SettingsTabSystemSystemRegion": "系統區域:",
- "SettingsTabSystemSystemRegionJapan": "日本",
- "SettingsTabSystemSystemRegionUSA": "美國",
- "SettingsTabSystemSystemRegionEurope": "歐洲",
- "SettingsTabSystemSystemRegionAustralia": "澳洲",
- "SettingsTabSystemSystemRegionChina": "中國",
- "SettingsTabSystemSystemRegionKorea": "韓國",
- "SettingsTabSystemSystemRegionTaiwan": "台灣",
- "SettingsTabSystemSystemLanguage": "系統語言:",
- "SettingsTabSystemSystemLanguageJapanese": "日文",
- "SettingsTabSystemSystemLanguageAmericanEnglish": "英文 (美國)",
- "SettingsTabSystemSystemLanguageFrench": "法文",
- "SettingsTabSystemSystemLanguageGerman": "德文",
- "SettingsTabSystemSystemLanguageItalian": "義大利文",
- "SettingsTabSystemSystemLanguageSpanish": "西班牙文",
- "SettingsTabSystemSystemLanguageChinese": "中文 (中國)",
- "SettingsTabSystemSystemLanguageKorean": "韓文",
- "SettingsTabSystemSystemLanguageDutch": "荷蘭文",
- "SettingsTabSystemSystemLanguagePortuguese": "葡萄牙文",
- "SettingsTabSystemSystemLanguageRussian": "俄文",
- "SettingsTabSystemSystemLanguageTaiwanese": "中文 (台灣)",
- "SettingsTabSystemSystemLanguageBritishEnglish": "英文 (英國)",
- "SettingsTabSystemSystemLanguageCanadianFrench": "加拿大法語",
- "SettingsTabSystemSystemLanguageLatinAmericanSpanish": "拉丁美洲西班牙文",
- "SettingsTabSystemSystemLanguageSimplifiedChinese": "簡體中文",
- "SettingsTabSystemSystemLanguageTraditionalChinese": "繁體中文",
- "SettingsTabSystemSystemTimeZone": "系統時區:",
- "SettingsTabSystemSystemTime": "系統時鐘:",
- "SettingsTabSystemEnableVsync": "垂直同步",
- "SettingsTabSystemEnablePptc": "啟用 PPTC 快取",
- "SettingsTabSystemEnableFsIntegrityChecks": "開啟檔案系統完整性檢查",
- "SettingsTabSystemAudioBackend": "音效處理後台架構:",
- "SettingsTabSystemAudioBackendDummy": "模擬",
- "SettingsTabSystemAudioBackendOpenAL": "OpenAL",
- "SettingsTabSystemAudioBackendSoundIO": "SoundIO",
- "SettingsTabSystemAudioBackendSDL2": "SDL2",
- "SettingsTabSystemHacks": "修正",
- "SettingsTabSystemHacksNote": " (會引起模擬器不穩定)",
- "SettingsTabSystemExpandDramSize": "使用額外的記憶體佈局 (開發人員)",
- "SettingsTabSystemIgnoreMissingServices": "忽略缺少的服務",
- "SettingsTabGraphics": "圖像",
- "SettingsTabGraphicsAPI": "圖像處理應用程式介面",
- "SettingsTabGraphicsEnableShaderCache": "啟用著色器快取",
- "SettingsTabGraphicsAnisotropicFiltering": "各向異性過濾:",
- "SettingsTabGraphicsAnisotropicFilteringAuto": "自動",
- "SettingsTabGraphicsAnisotropicFiltering2x": "2 倍",
- "SettingsTabGraphicsAnisotropicFiltering4x": "4 倍",
- "SettingsTabGraphicsAnisotropicFiltering8x": "8 倍",
- "SettingsTabGraphicsAnisotropicFiltering16x": "16倍",
- "SettingsTabGraphicsResolutionScale": "解析度比例:",
- "SettingsTabGraphicsResolutionScaleCustom": "自訂 (不建議使用)",
- "SettingsTabGraphicsResolutionScaleNative": "原生 (720p/1080p)",
- "SettingsTabGraphicsResolutionScale2x": "2 倍 (1440p/2160p)",
- "SettingsTabGraphicsResolutionScale3x": "3 倍 (2160p/3240p)",
- "SettingsTabGraphicsResolutionScale4x": "4 倍 (2880p/4320p)",
- "SettingsTabGraphicsAspectRatio": "螢幕長寬比例:",
- "SettingsTabGraphicsAspectRatio4x3": "4:3",
- "SettingsTabGraphicsAspectRatio16x9": "16:9",
- "SettingsTabGraphicsAspectRatio16x10": "16:10",
- "SettingsTabGraphicsAspectRatio21x9": "21:9",
- "SettingsTabGraphicsAspectRatio32x9": "32:9",
- "SettingsTabGraphicsAspectRatioStretch": "伸展至螢幕大小",
- "SettingsTabGraphicsDeveloperOptions": "開發者選項",
- "SettingsTabGraphicsShaderDumpPath": "圖形著色器轉存路徑:",
- "SettingsTabLogging": "日誌",
- "SettingsTabLoggingLogging": "日誌",
- "SettingsTabLoggingEnableLoggingToFile": "儲存記錄日誌為檔案",
- "SettingsTabLoggingEnableStubLogs": "啟用 Stub 記錄",
- "SettingsTabLoggingEnableInfoLogs": "啟用資訊記錄",
- "SettingsTabLoggingEnableWarningLogs": "啟用警告記錄",
- "SettingsTabLoggingEnableErrorLogs": "啟用錯誤記錄",
- "SettingsTabLoggingEnableTraceLogs": "啟用追蹤記錄",
- "SettingsTabLoggingEnableGuestLogs": "啟用賓客記錄",
- "SettingsTabLoggingEnableFsAccessLogs": "啟用檔案存取記錄",
- "SettingsTabLoggingFsGlobalAccessLogMode": "記錄全域檔案存取模式:",
- "SettingsTabLoggingDeveloperOptions": "開發者選項",
- "SettingsTabLoggingDeveloperOptionsNote": "警告:此操作會降低效能",
- "SettingsTabLoggingGraphicsBackendLogLevel": "圖像處理後台記錄等級:",
- "SettingsTabLoggingGraphicsBackendLogLevelNone": "無",
- "SettingsTabLoggingGraphicsBackendLogLevelError": "錯誤",
- "SettingsTabLoggingGraphicsBackendLogLevelPerformance": "減速",
- "SettingsTabLoggingGraphicsBackendLogLevelAll": "全部",
- "SettingsTabLoggingEnableDebugLogs": "啟用除錯記錄",
- "SettingsTabInput": "輸入",
- "SettingsTabInputEnableDockedMode": "Docked 模式",
- "SettingsTabInputDirectKeyboardAccess": "鍵盤直接操作",
- "SettingsButtonSave": "儲存",
- "SettingsButtonClose": "關閉",
- "SettingsButtonOk": "確定",
- "SettingsButtonCancel": "取消",
- "SettingsButtonApply": "套用",
- "ControllerSettingsPlayer": "玩家",
- "ControllerSettingsPlayer1": "玩家 1",
- "ControllerSettingsPlayer2": "玩家 2",
- "ControllerSettingsPlayer3": "玩家 3",
- "ControllerSettingsPlayer4": "玩家 4",
- "ControllerSettingsPlayer5": "玩家 5",
- "ControllerSettingsPlayer6": "玩家 6",
- "ControllerSettingsPlayer7": "玩家 7",
- "ControllerSettingsPlayer8": "玩家 8",
- "ControllerSettingsHandheld": "掌機模式",
- "ControllerSettingsInputDevice": "輸入裝置",
- "ControllerSettingsRefresh": "更新",
- "ControllerSettingsDeviceDisabled": "關閉",
- "ControllerSettingsControllerType": "控制器類型",
- "ControllerSettingsControllerTypeHandheld": "掌機",
- "ControllerSettingsControllerTypeProController": "Nintendo Switch Pro控制器",
- "ControllerSettingsControllerTypeJoyConPair": "JoyCon",
- "ControllerSettingsControllerTypeJoyConLeft": "左 JoyCon",
- "ControllerSettingsControllerTypeJoyConRight": "右 JoyCon",
- "ControllerSettingsProfile": "配置檔案",
- "ControllerSettingsProfileDefault": "預設",
- "ControllerSettingsLoad": "載入",
- "ControllerSettingsAdd": "建立",
- "ControllerSettingsRemove": "刪除",
- "ControllerSettingsButtons": "按鍵",
- "ControllerSettingsButtonA": "A",
- "ControllerSettingsButtonB": "B",
- "ControllerSettingsButtonX": "X",
- "ControllerSettingsButtonY": "Y",
- "ControllerSettingsButtonPlus": "+",
- "ControllerSettingsButtonMinus": "-",
- "ControllerSettingsDPad": "方向鍵",
- "ControllerSettingsDPadUp": "上",
- "ControllerSettingsDPadDown": "下",
- "ControllerSettingsDPadLeft": "左",
- "ControllerSettingsDPadRight": "右",
- "ControllerSettingsStickButton": "按鍵",
- "ControllerSettingsStickUp": "上",
- "ControllerSettingsStickDown": "下",
- "ControllerSettingsStickLeft": "左",
- "ControllerSettingsStickRight": "右",
- "ControllerSettingsStickStick": "搖桿",
- "ControllerSettingsStickInvertXAxis": "搖桿左右反向",
- "ControllerSettingsStickInvertYAxis": "搖桿上下反向",
- "ControllerSettingsStickDeadzone": "盲區:",
- "ControllerSettingsLStick": "左搖桿",
- "ControllerSettingsRStick": "右搖桿",
- "ControllerSettingsTriggersLeft": "左 Triggers",
- "ControllerSettingsTriggersRight": "右 Triggers",
- "ControllerSettingsTriggersButtonsLeft": "左 Triggers 鍵",
- "ControllerSettingsTriggersButtonsRight": "右 Triggers 鍵",
- "ControllerSettingsTriggers": "板機",
- "ControllerSettingsTriggerL": "L",
- "ControllerSettingsTriggerR": "R",
- "ControllerSettingsTriggerZL": "ZL",
- "ControllerSettingsTriggerZR": "ZR",
- "ControllerSettingsLeftSL": "SL",
- "ControllerSettingsLeftSR": "SR",
- "ControllerSettingsRightSL": "SL",
- "ControllerSettingsRightSR": "SR",
- "ControllerSettingsExtraButtonsLeft": "左按鍵",
- "ControllerSettingsExtraButtonsRight": "右按鍵",
- "ControllerSettingsMisc": "其他",
- "ControllerSettingsTriggerThreshold": "Triggers 閾值:",
- "ControllerSettingsMotion": "傳感器",
- "ControllerSettingsMotionUseCemuhookCompatibleMotion": "使用 CemuHook 相容性傳感協定",
- "ControllerSettingsMotionControllerSlot": "控制器插槽:",
- "ControllerSettingsMotionMirrorInput": "鏡像輸入",
- "ControllerSettingsMotionRightJoyConSlot": "右 JoyCon:",
- "ControllerSettingsMotionServerHost": "伺服器IP地址:",
- "ControllerSettingsMotionGyroSensitivity": "陀螺儀敏感度:",
- "ControllerSettingsMotionGyroDeadzone": "陀螺儀盲區:",
- "ControllerSettingsSave": "儲存",
- "ControllerSettingsClose": "關閉",
- "UserProfilesSelectedUserProfile": "選擇使用者帳戶:",
- "UserProfilesSaveProfileName": "儲存帳戶名稱",
- "UserProfilesChangeProfileImage": "更換帳戶頭像",
- "UserProfilesAvailableUserProfiles": "現有的使用者帳戶:",
- "UserProfilesAddNewProfile": "建立帳戶",
- "UserProfilesDelete": "刪除",
- "UserProfilesClose": "關閉",
- "ProfileNameSelectionWatermark": "選擇一個暱稱",
- "ProfileImageSelectionTitle": "帳戶頭像選擇",
- "ProfileImageSelectionHeader": "選擇帳戶頭像",
- "ProfileImageSelectionNote": "你可以導入自訂頭像,或從系統中選擇頭像",
- "ProfileImageSelectionImportImage": "導入圖片檔案",
- "ProfileImageSelectionSelectAvatar": "選擇系統頭像",
- "InputDialogTitle": "輸入對話框",
- "InputDialogOk": "完成",
- "InputDialogCancel": "取消",
- "InputDialogAddNewProfileTitle": "選擇帳戶名稱",
- "InputDialogAddNewProfileHeader": "請輸入帳戶名稱",
- "InputDialogAddNewProfileSubtext": "(最大長度:{0})",
- "AvatarChoose": "選擇",
- "AvatarSetBackgroundColor": "設定背景顏色",
- "AvatarClose": "關閉",
- "ControllerSettingsLoadProfileToolTip": "載入配置檔案",
- "ControllerSettingsAddProfileToolTip": "新增配置檔案",
- "ControllerSettingsRemoveProfileToolTip": "刪除配置檔案",
- "ControllerSettingsSaveProfileToolTip": "儲存配置檔案",
- "MenuBarFileToolsTakeScreenshot": "儲存截圖",
- "MenuBarFileToolsHideUi": "隱藏使用者介面",
- "GameListContextMenuRunApplication": "執行程式",
- "GameListContextMenuToggleFavorite": "標記為收藏",
- "GameListContextMenuToggleFavoriteToolTip": "啟用或取消收藏標記",
- "SettingsTabGeneralTheme": "佈景主題",
- "SettingsTabGeneralThemeCustomTheme": "自訂佈景主題路徑",
- "SettingsTabGeneralThemeBaseStyle": "基本佈景主題式樣",
- "SettingsTabGeneralThemeBaseStyleDark": "深色模式",
- "SettingsTabGeneralThemeBaseStyleLight": "淺色模式",
- "SettingsTabGeneralThemeEnableCustomTheme": "使用自訂佈景主題",
- "ButtonBrowse": "瀏覽",
- "ControllerSettingsConfigureGeneral": "配置",
- "ControllerSettingsRumble": "震動",
- "ControllerSettingsRumbleStrongMultiplier": "強震動調節",
- "ControllerSettingsRumbleWeakMultiplier": "弱震動調節",
- "DialogMessageSaveNotAvailableMessage": "沒有{0} [{1:x16}]的遊戲存檔",
- "DialogMessageSaveNotAvailableCreateSaveMessage": "是否建立該遊戲的存檔資料夾?",
- "DialogConfirmationTitle": "Ryujinx - 設定",
- "DialogUpdaterTitle": "Ryujinx - 更新",
- "DialogErrorTitle": "Ryujinx - 錯誤",
- "DialogWarningTitle": "Ryujinx - 警告",
- "DialogExitTitle": "Ryujinx - 關閉",
- "DialogErrorMessage": "Ryujinx 遇到了錯誤",
- "DialogExitMessage": "你確定要關閉 Ryujinx 嗎?",
- "DialogExitSubMessage": "所有未儲存的資料將會遺失!",
- "DialogMessageCreateSaveErrorMessage": "建立特定的存檔時出現錯誤: {0}",
- "DialogMessageFindSaveErrorMessage": "查找特定的存檔時出現錯誤: {0}",
- "FolderDialogExtractTitle": "選擇要解壓到的資料夾",
- "DialogNcaExtractionMessage": "提取{1}的{0}分區...",
- "DialogNcaExtractionTitle": "Ryujinx - NCA分區提取",
- "DialogNcaExtractionMainNcaNotFoundErrorMessage": "提取失敗。所選檔案中不含主NCA檔案",
- "DialogNcaExtractionCheckLogErrorMessage": "提取失敗。請查看日誌檔案取得詳情。",
- "DialogNcaExtractionSuccessMessage": "提取成功。",
- "DialogUpdaterConvertFailedMessage": "無法轉換目前 Ryujinx 版本。",
- "DialogUpdaterCancelUpdateMessage": "更新取消!",
- "DialogUpdaterAlreadyOnLatestVersionMessage": "你使用的 Ryujinx 是最新版本。",
- "DialogUpdaterFailedToGetVersionMessage": "嘗試從 Github 取得版本訊息時失敗。可能是因為 GitHub Actions 正在編譯新版本。請於數分數後重試。",
- "DialogUpdaterConvertFailedGithubMessage": "無法轉換從 Github 接收到的 Ryujinx 版本。",
- "DialogUpdaterDownloadingMessage": "下載最新版本中...",
- "DialogUpdaterExtractionMessage": "正在提取更新...",
- "DialogUpdaterRenamingMessage": "正在刪除舊檔案...",
- "DialogUpdaterAddingFilesMessage": "安裝更新中...",
- "DialogUpdaterCompleteMessage": "更新成功!",
- "DialogUpdaterRestartMessage": "你確定要立即重新啟動 Ryujinx 嗎?",
- "DialogUpdaterArchNotSupportedMessage": "你執行的系統架構不被支援!",
- "DialogUpdaterArchNotSupportedSubMessage": "(僅支援 x64 系統)",
- "DialogUpdaterNoInternetMessage": "你沒有連接到網際網絡!",
- "DialogUpdaterNoInternetSubMessage": "請確保網際網絡連接正常!",
- "DialogUpdaterDirtyBuildMessage": "不能更新非官方版本的 Ryujinx!",
- "DialogUpdaterDirtyBuildSubMessage": "如果你希望使用被受支援的Ryujinx版本,請你在官方網址 https://ryujinx.org/ 下載.",
- "DialogRestartRequiredMessage": "模擬器必須重新啟動",
- "DialogThemeRestartMessage": "佈景主題設定已儲存。需要重新啟動才能生效。",
- "DialogThemeRestartSubMessage": "你確定要現在重新啟動嗎?",
- "DialogFirmwareInstallEmbeddedMessage": "要安裝遊戲內建的韌體嗎?(韌體 {0})",
- "DialogFirmwareInstallEmbeddedSuccessMessage": "未找到已安裝的韌體,但 Ryujinx 可以從現有的遊戲安裝韌體{0}.\\n模擬器現在可以執行。",
- "DialogFirmwareNoFirmwareInstalledMessage": "未安裝韌體",
- "DialogFirmwareInstalledMessage": "已安裝韌體{0}",
- "DialogInstallFileTypesSuccessMessage": "成功註冊檔案類型!",
- "DialogInstallFileTypesErrorMessage": "註冊檔案類型失敗。",
- "DialogUninstallFileTypesSuccessMessage": "成功取消註冊檔案類型!",
- "DialogUninstallFileTypesErrorMessage": "取消註冊檔案類型失敗。",
- "DialogOpenSettingsWindowLabel": "開啟設定視窗",
- "DialogControllerAppletTitle": "控制器小視窗",
- "DialogMessageDialogErrorExceptionMessage": "顯示訊息對話框時出現錯誤: {0}",
- "DialogSoftwareKeyboardErrorExceptionMessage": "顯示軟體鍵盤時出現錯誤: {0}",
- "DialogErrorAppletErrorExceptionMessage": "顯示錯誤對話框時出現錯誤: {0}",
- "DialogUserErrorDialogMessage": "{0}: {1}",
- "DialogUserErrorDialogInfoMessage": "\n有關修復此錯誤的更多訊息,可以遵循我們的設定指南。",
- "DialogUserErrorDialogTitle": "Ryujinx 錯誤 ({0})",
- "DialogAmiiboApiTitle": "Amiibo 應用程式介面",
- "DialogAmiiboApiFailFetchMessage": "從 API 取得訊息時出錯。",
- "DialogAmiiboApiConnectErrorMessage": "無法連接到 Amiibo API 伺服器。伺服器可能已關閉,或你沒有連接到網際網路。",
- "DialogProfileInvalidProfileErrorMessage": "配置檔案 {0} 與目前輸入系統不相容。",
- "DialogProfileDefaultProfileOverwriteErrorMessage": "無法覆蓋預設的配置檔案",
- "DialogProfileDeleteProfileTitle": "刪除帳戶",
- "DialogProfileDeleteProfileMessage": "此操作不可撤銷, 您確定要繼續嗎?",
- "DialogWarning": "警告",
- "DialogPPTCDeletionMessage": "下一次重啟時將會重新建立以下遊戲的 PPTC 快取\n\n{0}\n\n你確定要繼續嗎?",
- "DialogPPTCDeletionErrorMessage": "清除位於{0}的 PPTC 快取時出錯: {1}",
- "DialogShaderDeletionMessage": "即將刪除以下遊戲的著色器快取:\n\n{0}\n\n你確定要繼續嗎?",
- "DialogShaderDeletionErrorMessage": "清除{0}的著色器快取時出現錯誤: {1}",
- "DialogRyujinxErrorMessage": "Ryujinx 遇到錯誤",
- "DialogInvalidTitleIdErrorMessage": "UI 錯誤:所選遊戲沒有有效的標題ID",
- "DialogFirmwareInstallerFirmwareNotFoundErrorMessage": "路徑{0}找不到有效的系統韌體。",
- "DialogFirmwareInstallerFirmwareInstallTitle": "安裝韌體{0}",
- "DialogFirmwareInstallerFirmwareInstallMessage": "將安裝{0}版本的系統。",
- "DialogFirmwareInstallerFirmwareInstallSubMessage": "\n\n這將替換目前系統版本{0}。",
- "DialogFirmwareInstallerFirmwareInstallConfirmMessage": "你確定要繼續嗎?",
- "DialogFirmwareInstallerFirmwareInstallWaitMessage": "安裝韌體中...",
- "DialogFirmwareInstallerFirmwareInstallSuccessMessage": "成功安裝系統版本{0}。",
- "DialogUserProfileDeletionWarningMessage": "刪除後將沒有可選擇的使用者帳戶",
- "DialogUserProfileDeletionConfirmMessage": "你確定要刪除選擇中的帳戶嗎?",
- "DialogUserProfileUnsavedChangesTitle": "警告 - 有未儲存的更改",
- "DialogUserProfileUnsavedChangesMessage": "你對此帳戶所做的更改尚未儲存.",
- "DialogUserProfileUnsavedChangesSubMessage": "你確定要捨棄更改嗎?",
- "DialogControllerSettingsModifiedConfirmMessage": "目前的輸入配置檔案已更新",
- "DialogControllerSettingsModifiedConfirmSubMessage": "你確定要儲存嗎?",
- "DialogLoadNcaErrorMessage": "{0}. 錯誤的檔案: {1}",
- "DialogDlcNoDlcErrorMessage": "選擇的檔案不包含所選遊戲的 DLC!",
- "DialogPerformanceCheckLoggingEnabledMessage": "你啟用了跟蹤記錄,它的設計僅限開發人員使用。",
- "DialogPerformanceCheckLoggingEnabledConfirmMessage": "為了獲得最佳效能,建議停用追蹤記錄。你是否要立即停用?",
- "DialogPerformanceCheckShaderDumpEnabledMessage": "你啟用了著色器轉存,它的設計僅限開發人員使用。",
- "DialogPerformanceCheckShaderDumpEnabledConfirmMessage": "為了獲得最佳效能,建議停用著色器轉存。你是否要立即停用?",
- "DialogLoadAppGameAlreadyLoadedMessage": "目前已載入此遊戲",
- "DialogLoadAppGameAlreadyLoadedSubMessage": "請停止模擬或關閉程式,再啟動另一個遊戲。",
- "DialogUpdateAddUpdateErrorMessage": "選擇的檔案不包含所選遊戲的更新!",
- "DialogSettingsBackendThreadingWarningTitle": "警告 - 後台多工執行中",
- "DialogSettingsBackendThreadingWarningMessage": "更改此選項後必須重啟 Ryujinx 才能生效。根據你的硬體,您開啟該選項時,可能需要手動停用驅動程式本身的GPU多執行緒。",
- "SettingsTabGraphicsFeaturesOptions": "功能",
- "SettingsTabGraphicsBackendMultithreading": "圖像處理後台多線程支援:",
- "CommonAuto": "自動(推薦)",
- "CommonOff": "關閉",
- "CommonOn": "打開",
- "InputDialogYes": "是",
- "InputDialogNo": "否",
- "DialogProfileInvalidProfileNameErrorMessage": "檔案名包含無效字元,請重試。",
- "MenuBarOptionsPauseEmulation": "暫停",
- "MenuBarOptionsResumeEmulation": "繼續",
- "AboutUrlTooltipMessage": "在瀏覽器中打開 Ryujinx 的官方網站。",
- "AboutDisclaimerMessage": "Ryujinx 與 Nintendo™ 並沒有任何關聯, 包括其合作伙伴, 及任何形式上。",
- "AboutAmiiboDisclaimerMessage": "我們的 Amiibo 模擬使用了\nAmiiboAPI (www.amiiboapi.com) ",
- "AboutPatreonUrlTooltipMessage": "在瀏覽器中打開 Ryujinx 的 Patreon 贊助網頁。",
- "AboutGithubUrlTooltipMessage": "在瀏覽器中打開 Ryujinx 的 GitHub 儲存庫。",
- "AboutDiscordUrlTooltipMessage": "在瀏覽器中打開 Ryujinx 的 Discord 伺服器邀請連結。",
- "AboutTwitterUrlTooltipMessage": "在瀏覽器中打開 Ryujinx 的 Twitter 首頁。",
- "AboutRyujinxAboutTitle": "關於:",
- "AboutRyujinxAboutContent": "Ryujinx 是 Nintendo Switch™ 的一款模擬器。\n懇請您在 Patreon 上贊助我們。\n關注 Twitter 或 Discord 可以取得我們的最新動態。\n如果您對開發本軟體感興趣,歡迎來 GitHub 和 Discord 加入我們!",
- "AboutRyujinxMaintainersTitle": "開發及維護名單:",
- "AboutRyujinxMaintainersContentTooltipMessage": "在瀏覽器中打開貢獻者的網頁",
- "AboutRyujinxSupprtersTitle": "Patreon 的贊助人:",
- "AmiiboSeriesLabel": "Amiibo 系列",
- "AmiiboCharacterLabel": "角色",
- "AmiiboScanButtonLabel": "掃描",
- "AmiiboOptionsShowAllLabel": "顯示所有 Amiibo",
- "AmiiboOptionsUsRandomTagLabel": "侵略:使用隨機標記的 Uuid 編碼",
- "DlcManagerTableHeadingEnabledLabel": "已啟用",
- "DlcManagerTableHeadingTitleIdLabel": "遊戲ID",
- "DlcManagerTableHeadingContainerPathLabel": "資料夾路徑",
- "DlcManagerTableHeadingFullPathLabel": "完整路徑",
- "DlcManagerRemoveAllButton": "全部刪除",
- "DlcManagerEnableAllButton": "啟用全部",
- "DlcManagerDisableAllButton": "停用全部",
- "MenuBarOptionsChangeLanguage": "更改語言",
- "MenuBarShowFileTypes": "顯示檔案類型",
- "CommonSort": "排序",
- "CommonShowNames": "顯示名稱",
- "CommonFavorite": "收藏",
- "OrderAscending": "從小到大",
- "OrderDescending": "從大到小",
- "SettingsTabGraphicsFeatures": "功能及優化",
- "ErrorWindowTitle": "錯誤視窗",
- "ToggleDiscordTooltip": "啟用或關閉 Discord 動態狀態展示",
- "AddGameDirBoxTooltip": "輸入要添加的遊戲資料夾",
- "AddGameDirTooltip": "添加遊戲資料夾到列表中",
- "RemoveGameDirTooltip": "移除選擇中的遊戲資料夾",
- "CustomThemeCheckTooltip": "啟用或關閉自訂佈景主題",
- "CustomThemePathTooltip": "自訂佈景主題的資料夾",
- "CustomThemeBrowseTooltip": "查找自訂佈景主題",
- "DockModeToggleTooltip": "是否開啟 Switch 的 Docked 模式",
- "DirectKeyboardTooltip": "支援鍵盤直接存取 (HID協定) . 可供給遊戲使用你的鍵盤作為輸入文字裝置.",
- "DirectMouseTooltip": "支援滑鼠直接存取 (HID協定) . 可供給遊戲使用你的滑鼠作為瞄準裝置.",
- "RegionTooltip": "更改系統區域",
- "LanguageTooltip": "更改系統語言",
- "TimezoneTooltip": "更改系統時區",
- "TimeTooltip": "更改系統時鐘",
- "VSyncToggleTooltip": "模擬遊戲主機垂直同步更新頻率. 重要地反映著遊戲本身的速度; 關閉它可能會令後使用動態更新率的遊戲速度過高, 或會引致載入錯誤等等.\n\n可在遊戲中利用自訂快速鍵開關此功能. 我們也建議使用快速鍵, 如果你計劃關上它.\n\n如果不確定請設定為\"開啟\".",
- "PptcToggleTooltip": "開啟以後減少遊戲啟動時間和卡頓",
- "FsIntegrityToggleTooltip": "是否檢查遊戲檔案內容的完整性",
- "AudioBackendTooltip": "更改音效處理後台架構.\n\n推薦使用SDL2架構, 而OpenAL及SoundIO架構用作後備. Dummy是沒有音效的.\n\n如果不確定請設定為\"SDL2\".",
- "MemoryManagerTooltip": "更改模擬器記憶體至電腦記憶體的映射和存取方式,極其影響CPU效能.\n\n如果不確定, 請設定為\"主機略過檢查模式\".",
- "MemoryManagerSoftwareTooltip": "使用軟體虛擬分頁表換算, 最精確但是速度最慢.",
- "MemoryManagerHostTooltip": "直接地映射模擬記憶體到電腦記憶體. 對 JIT 編譯和執行效率有顯著提升. ",
- "MemoryManagerUnsafeTooltip": "直接地映射模擬記憶體到電腦記憶體, 但是不檢查記憶體溢出. 由於 Ryujinx 的子程式可以存取任何位置的記憶體, 因而相對不安全. 故在此模式下只應執行你信任的遊戲或軟體.",
- "UseHypervisorTooltip": "使用 Hypervisor 代替 JIT。在本功能可用時就可以大幅增大效能,但目前狀態還不穩定。",
- "DRamTooltip": "利用可選擇性的記憶體模式來模擬Switch發展中型號.\n\n此選項只會對高畫質材質包或4K模組有用. 而這並不會提升效能. \n\n如果不確定請關閉本功能.",
- "IgnoreMissingServicesTooltip": "忽略某些未被實施的系統服務. 此功能有助於繞過當啟動遊戲時帶來的故障.\n\n如果不確定請關閉本功能。",
- "GraphicsBackendThreadingTooltip": "執行雙線程後台繪圖指令, 能夠減少著色器編譯斷續, 並提高GPU驅動效能, 即將它不支持多線程處理. 而對於多線程處理也有少量提升.\n\n如果你不確定請設定為\"自動\"",
- "GalThreadingTooltip": "執行雙線程後台繪圖指令.\n\n能夠加速著色器編譯及減少斷續, 並提高GPU驅動效能, 即將它不支持多線程處理. 而對於多線程處理也有少量提升.\n\n如果你不確定請設定為\"自動\"",
- "ShaderCacheToggleTooltip": "儲存著色器快取到硬碟,減少存取斷續。\n\n如果不確定請設定為\"開啟\"。",
- "ResolutionScaleTooltip": "解析度繪圖倍率",
- "ResolutionScaleEntryTooltip": "盡量使用如1.5的浮點倍數。非整數的倍率易引起錯誤",
- "AnisotropyTooltip": "各向異性過濾等級。提高傾斜視角材質的清晰度\n(選擇「自動」將使用遊戲預設指定的等級)",
- "AspectRatioTooltip": "模擬器視窗解析度的長寬比",
- "ShaderDumpPathTooltip": "圖形著色器轉存路徑",
- "FileLogTooltip": "是否儲存日誌檔案到硬碟",
- "StubLogTooltip": "在控制台顯示及記錄 Stub 訊息",
- "InfoLogTooltip": "在控制台顯示及記錄資訊訊息",
- "WarnLogTooltip": "在控制台顯示及記錄警告訊息\n",
- "ErrorLogTooltip": "在控制台顯示及記錄錯誤訊息",
- "TraceLogTooltip": "在控制台顯示及記錄追蹤訊息",
- "GuestLogTooltip": "在控制台顯示及記錄賓客訊息",
- "FileAccessLogTooltip": "在控制台顯示及記錄檔案存取訊息",
- "FSAccessLogModeTooltip": "在控制台顯示及記錄FS 存取訊息. 可選的模式是 0-3",
- "DeveloperOptionTooltip": "使用請謹慎",
- "OpenGlLogLevel": "需要打開適當的日誌等級",
- "DebugLogTooltip": "在控制台顯示及記錄除錯訊息.\n\n僅限於受訓的成員使用, 因為它很難理解而且令模擬的效能非常地差.\n",
- "LoadApplicationFileTooltip": "選擇 Switch 支援的遊戲格式並載入",
- "LoadApplicationFolderTooltip": "選擇解包後的 Switch 遊戲並載入",
- "OpenRyujinxFolderTooltip": "開啟 Ryujinx 系統資料夾",
- "OpenRyujinxLogsTooltip": "開啟存放日誌的資料夾",
- "ExitTooltip": "關閉 Ryujinx",
- "OpenSettingsTooltip": "開啟設定視窗",
- "OpenProfileManagerTooltip": "開啟使用者帳戶管理視窗",
- "StopEmulationTooltip": "停止執行目前遊戲並回到選擇界面",
- "CheckUpdatesTooltip": "檢查 Ryujinx 新版本",
- "OpenAboutTooltip": "開啟關於視窗",
- "GridSize": "網格尺寸",
- "GridSizeTooltip": "調整網格模式的大小",
- "SettingsTabSystemSystemLanguageBrazilianPortuguese": "巴西葡萄牙語",
- "AboutRyujinxContributorsButtonHeader": "查看所有參與者",
- "SettingsTabSystemAudioVolume": "音量:",
- "AudioVolumeTooltip": "調節音量",
- "SettingsTabSystemEnableInternetAccess": "啟用網路連接",
- "EnableInternetAccessTooltip": "開啟網路存取。此選項打開後,效果類似於 Switch 連接到網路的狀態。注意即使此選項關閉,應用程式偶爾也有可能連接到網路",
- "GameListContextMenuManageCheatToolTip": "管理金手指",
- "GameListContextMenuManageCheat": "管理金手指",
- "ControllerSettingsStickRange": "範圍:",
- "DialogStopEmulationTitle": "Ryujinx - 停止模擬",
- "DialogStopEmulationMessage": "你確定要停止模擬嗎?",
- "SettingsTabCpu": "處理器",
- "SettingsTabAudio": "音訊",
- "SettingsTabNetwork": "網路",
- "SettingsTabNetworkConnection": "網路連接",
- "SettingsTabCpuCache": "CPU 快取",
- "SettingsTabCpuMemory": "CPU 模式",
- "DialogUpdaterFlatpakNotSupportedMessage": "請透過 Flathub 更新 Ryujinx。",
- "UpdaterDisabledWarningTitle": "更新已停用!",
- "GameListContextMenuOpenSdModsDirectory": "開啟 Atmosphere 模組資料夾",
- "GameListContextMenuOpenSdModsDirectoryToolTip": "開啟此遊戲額外的SD記憶卡Atmosphere模組資料夾. 有用於包裝在真實主機上的模組.\n",
- "ControllerSettingsRotate90": "順時針旋轉 90°",
- "IconSize": "圖示尺寸",
- "IconSizeTooltip": "更改遊戲圖示大小",
- "MenuBarOptionsShowConsole": "顯示控制台",
- "ShaderCachePurgeError": "清除 {0} 著色器快取時出現錯誤: {1}",
- "UserErrorNoKeys": "找不到金鑰",
- "UserErrorNoFirmware": "找不到韌體",
- "UserErrorFirmwareParsingFailed": "韌體解析錯誤",
- "UserErrorApplicationNotFound": "找不到應用程式",
- "UserErrorUnknown": "未知錯誤",
- "UserErrorUndefined": "未定義錯誤",
- "UserErrorNoKeysDescription": "Ryujinx 找不到 『prod.keys』 檔案",
- "UserErrorNoFirmwareDescription": "Ryujinx 找不到任何已安裝的韌體",
- "UserErrorFirmwareParsingFailedDescription": "Ryujinx 無法解密選擇的韌體。這通常是由於金鑰過舊。",
- "UserErrorApplicationNotFoundDescription": "Ryujinx 在選中路徑找不到有效的應用程式。",
- "UserErrorUnknownDescription": "發生未知錯誤!",
- "UserErrorUndefinedDescription": "發生了未定義錯誤!此類錯誤不應出現,請聯絡開發人員!",
- "OpenSetupGuideMessage": "開啟設定教學",
- "NoUpdate": "沒有新版本",
- "TitleUpdateVersionLabel": "版本 {0} - {1}",
- "RyujinxInfo": "Ryujinx - 訊息",
- "RyujinxConfirm": "Ryujinx - 確認",
- "FileDialogAllTypes": "全部類型",
- "Never": "從不",
- "SwkbdMinCharacters": "至少應為 {0} 個字長",
- "SwkbdMinRangeCharacters": "必須為 {0}-{1} 個字長",
- "SoftwareKeyboard": "軟體鍵盤",
- "SoftwareKeyboardModeNumbersOnly": "只接受數字",
- "SoftwareKeyboardModeAlphabet": "不支援中日韓統一表意文字字元",
- "SoftwareKeyboardModeASCII": "只接受 ASCII 符號",
- "DialogControllerAppletMessagePlayerRange": "本遊戲需要 {0} 個玩家持有:\n\n類型:{1}\n\n玩家:{2}\n\n{3}請打開設定畫面並配置控制器,或者關閉本視窗。",
- "DialogControllerAppletMessage": "本遊戲需要剛好 {0} 個玩家持有:\n\n類型:{1}\n\n玩家:{2}\n\n{3}請打開設定畫面並配置控制器,或者關閉本視窗。",
- "DialogControllerAppletDockModeSet": "現在處於主機模式,無法使用掌機操作方式\n\n",
- "UpdaterRenaming": "正在重新命名舊檔案...",
- "UpdaterRenameFailed": "更新過程中無法重新命名檔案: {0}",
- "UpdaterAddingFiles": "安裝更新中...",
- "UpdaterExtracting": "正在提取更新...",
- "UpdaterDownloading": "下載新版本中...",
- "Game": "遊戲",
- "Docked": "主機模式",
- "Handheld": "掌機模式",
- "ConnectionError": "連接錯誤。",
- "AboutPageDeveloperListMore": "{0} 等開發者...",
- "ApiError": "API 錯誤",
- "LoadingHeading": "正在啟動 {0}",
- "CompilingPPTC": "編譯 PPTC 快取中",
- "CompilingShaders": "編譯著色器中",
- "AllKeyboards": "所有鍵盤",
- "OpenFileDialogTitle": "選擇支援的檔案格式",
- "OpenFolderDialogTitle": "選擇一個包含已解開封裝遊戲的資料夾\n",
- "AllSupportedFormats": "全部支援的格式",
- "RyujinxUpdater": "Ryujinx 更新程式",
- "SettingsTabHotkeys": "快捷鍵",
- "SettingsTabHotkeysHotkeys": "鍵盤快捷鍵",
- "SettingsTabHotkeysToggleVsyncHotkey": "切換垂直同步:",
- "SettingsTabHotkeysScreenshotHotkey": "截圖:",
- "SettingsTabHotkeysShowUiHotkey": "隱藏使用者介面:",
- "SettingsTabHotkeysPauseHotkey": "暫停:",
- "SettingsTabHotkeysToggleMuteHotkey": "靜音:",
- "ControllerMotionTitle": "體感操作設定",
- "ControllerRumbleTitle": "震動設定",
- "SettingsSelectThemeFileDialogTitle": "選擇主題檔案",
- "SettingsXamlThemeFile": "Xaml 主題檔案",
- "AvatarWindowTitle": "管理帳號 - 頭貼",
- "Amiibo": "Amiibo",
- "Unknown": "未知",
- "Usage": "用途",
- "Writable": "可寫入",
- "SelectDlcDialogTitle": "選擇 DLC 檔案",
- "SelectUpdateDialogTitle": "選擇更新檔",
- "UserProfileWindowTitle": "管理使用者帳戶",
- "CheatWindowTitle": "管理遊戲金手指",
- "DlcWindowTitle": "管理遊戲 DLC",
- "UpdateWindowTitle": "管理遊戲更新",
- "CheatWindowHeading": "金手指可用於 {0} [{1}]",
- "BuildId": "版本編號:",
- "DlcWindowHeading": "DLC 可用於 {0} [{1}]",
- "UserProfilesEditProfile": "編輯所選",
- "Cancel": "取消",
- "Save": "儲存",
- "Discard": "放棄更改",
- "UserProfilesSetProfileImage": "設定帳戶頭像",
- "UserProfileEmptyNameError": "使用者名稱為必填",
- "UserProfileNoImageError": "必須設定帳戶頭像",
- "GameUpdateWindowHeading": "更新可用於 {0} [{1}]",
- "SettingsTabHotkeysResScaleUpHotkey": "提高解析度:",
- "SettingsTabHotkeysResScaleDownHotkey": "降低解析度:",
- "UserProfilesName": "使用者名稱:",
- "UserProfilesUserId": "使用者 ID:",
- "SettingsTabGraphicsBackend": "圖像處理後台架構",
- "SettingsTabGraphicsBackendTooltip": "用來圖像處理的後台架構",
- "SettingsEnableTextureRecompression": "開啟材質重新壓縮",
- "SettingsEnableTextureRecompressionTooltip": "壓縮某些材質以減少 VRAM 使用。\n\n推薦用於小於 4GiB VRAM 的 GPU。\n\n如果不確定請關閉本功能。",
- "SettingsTabGraphicsPreferredGpu": "優先選取的 GPU",
- "SettingsTabGraphicsPreferredGpuTooltip": "選擇支持運行Vulkan圖像處理架構的GPU.\n\n此設定不會影響GPU運行OpenGL.\n\n如果不確定, 請設定標籤為\"dGPU\"的GPU. 如果沒有, 請保留原始設定.",
- "SettingsAppRequiredRestartMessage": "必須重啟 Ryujinx",
- "SettingsGpuBackendRestartMessage": "圖像處理後台架構或GPU相關設定已被修改。需要重新啟動才能套用。",
- "SettingsGpuBackendRestartSubMessage": "你確定要現在重新啟動嗎?",
- "RyujinxUpdaterMessage": "你確定要將 Ryujinx 更新到最新版本嗎?",
- "SettingsTabHotkeysVolumeUpHotkey": "增加音量:",
- "SettingsTabHotkeysVolumeDownHotkey": "降低音量:",
- "SettingsEnableMacroHLE": "啟用 Macro HLE",
- "SettingsEnableMacroHLETooltip": "GPU 微代碼的高階模擬。\n\n可以提升效能,但可能會導致某些遊戲出現圖形顯示故障。\n\n如果不確定請設定為\"開啟\"。",
- "SettingsEnableColorSpacePassthrough": "色彩空間直通",
- "SettingsEnableColorSpacePassthroughTooltip": "指揮Vulkan後端直通色彩資訊而不需指定色彩空間,對於廣色域顯示的使用者,這會造成較多抖色,犧牲色彩正確性。",
- "VolumeShort": "音量",
- "UserProfilesManageSaves": "管理遊戲存檔",
- "DeleteUserSave": "你確定要刪除此遊戲的存檔嗎?",
- "IrreversibleActionNote": "本動作將無法挽回。",
- "SaveManagerHeading": "管理 {0} 的遊戲存檔",
- "SaveManagerTitle": "遊戲存檔管理器",
- "Name": "名稱",
- "Size": "大小",
- "Search": "搜尋",
- "UserProfilesRecoverLostAccounts": "恢復遺失的帳戶",
- "Recover": "恢復",
- "UserProfilesRecoverHeading": "在以下帳戶找到了一些遊戲存檔",
- "UserProfilesRecoverEmptyList": "沒有可恢復的使用者帳戶",
- "GraphicsAATooltip": "在遊戲繪圖上套用抗鋸齒",
- "GraphicsAALabel": "抗鋸齒:",
- "GraphicsScalingFilterLabel": "縮放過濾器:",
- "GraphicsScalingFilterTooltip": "啟用畫幀緩衝區縮放",
- "GraphicsScalingFilterLevelLabel": "記錄檔等級",
- "GraphicsScalingFilterLevelTooltip": "設定縮放過濾器的強度",
- "SmaaLow": "低階 SMAA",
- "SmaaMedium": "中階 SMAA",
- "SmaaHigh": "高階 SMAA",
- "SmaaUltra": "超高階 SMAA",
- "UserEditorTitle": "編輯使用者",
- "UserEditorTitleCreate": "建立使用者",
- "SettingsTabNetworkInterface": "網路介面:",
- "NetworkInterfaceTooltip": "用於具有 LAN 功能的網路介面",
- "NetworkInterfaceDefault": "預設",
- "PackagingShaders": "著色器封裝",
- "AboutChangelogButton": "在 GitHub 查看更新日誌",
- "AboutChangelogButtonTooltipMessage": "在瀏覽器中打開此Ryujinx版本的更新日誌。"
-}
\ No newline at end of file
diff --git a/src/Ryujinx.Ava/Modules/Updater/Updater.cs b/src/Ryujinx.Ava/Modules/Updater/Updater.cs
deleted file mode 100644
index af7608d34..000000000
--- a/src/Ryujinx.Ava/Modules/Updater/Updater.cs
+++ /dev/null
@@ -1,777 +0,0 @@
-using Avalonia.Controls;
-using Avalonia.Threading;
-using FluentAvalonia.UI.Controls;
-using ICSharpCode.SharpZipLib.GZip;
-using ICSharpCode.SharpZipLib.Tar;
-using ICSharpCode.SharpZipLib.Zip;
-using Ryujinx.Ava;
-using Ryujinx.Ava.Common.Locale;
-using Ryujinx.Ava.UI.Helpers;
-using Ryujinx.Common;
-using Ryujinx.Common.Logging;
-using Ryujinx.Common.Utilities;
-using Ryujinx.Ui.Common.Helper;
-using Ryujinx.Ui.Common.Models.Github;
-using System;
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.IO;
-using System.Linq;
-using System.Net;
-using System.Net.Http;
-using System.Net.NetworkInformation;
-using System.Runtime.CompilerServices;
-using System.Runtime.InteropServices;
-using System.Runtime.Versioning;
-using System.Text;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace Ryujinx.Modules
-{
- internal static class Updater
- {
- private const string GitHubApiUrl = "https://api.github.com";
- private static readonly GithubReleasesJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions());
-
- private static readonly string _homeDir = AppDomain.CurrentDomain.BaseDirectory;
- private static readonly string _updateDir = Path.Combine(Path.GetTempPath(), "Ryujinx", "update");
- private static readonly string _updatePublishDir = Path.Combine(_updateDir, "publish");
- private const int ConnectionCount = 4;
-
- private static string _buildVer;
- private static string _platformExt;
- private static string _buildUrl;
- private static long _buildSize;
- private static bool _updateSuccessful;
- private static bool _running;
-
- private static readonly string[] _windowsDependencyDirs = Array.Empty();
-
- public static async Task BeginParse(Window mainWindow, bool showVersionUpToDate)
- {
- if (_running)
- {
- return;
- }
-
- _running = true;
-
- // Detect current platform
- if (OperatingSystem.IsMacOS())
- {
- _platformExt = "macos_universal.app.tar.gz";
- }
- else if (OperatingSystem.IsWindows())
- {
- _platformExt = "win_x64.zip";
- }
- else if (OperatingSystem.IsLinux())
- {
- _platformExt = "linux_x64.tar.gz";
- }
-
- Version newVersion;
- Version currentVersion;
-
- try
- {
- currentVersion = Version.Parse(Program.Version);
- }
- catch
- {
- Logger.Error?.Print(LogClass.Application, "Failed to convert the current Ryujinx version!");
-
- await ContentDialogHelper.CreateWarningDialog(
- LocaleManager.Instance[LocaleKeys.DialogUpdaterConvertFailedMessage],
- LocaleManager.Instance[LocaleKeys.DialogUpdaterCancelUpdateMessage]);
-
- _running = false;
-
- return;
- }
-
- // Get latest version number from GitHub API
- try
- {
- using HttpClient jsonClient = ConstructHttpClient();
-
- string buildInfoUrl = $"{GitHubApiUrl}/repos/{ReleaseInformation.ReleaseChannelOwner}/{ReleaseInformation.ReleaseChannelRepo}/releases/latest";
- string fetchedJson = await jsonClient.GetStringAsync(buildInfoUrl);
- var fetched = JsonHelper.Deserialize(fetchedJson, _serializerContext.GithubReleasesJsonResponse);
- _buildVer = fetched.Name;
-
- foreach (var asset in fetched.Assets)
- {
- if (asset.Name.StartsWith("test-ava-ryujinx") && asset.Name.EndsWith(_platformExt))
- {
- _buildUrl = asset.BrowserDownloadUrl;
-
- if (asset.State != "uploaded")
- {
- if (showVersionUpToDate)
- {
- await ContentDialogHelper.CreateUpdaterInfoDialog(
- LocaleManager.Instance[LocaleKeys.DialogUpdaterAlreadyOnLatestVersionMessage],
- "");
- }
-
- _running = false;
-
- return;
- }
-
- break;
- }
- }
-
- // If build not done, assume no new update are available.
- if (_buildUrl is null)
- {
- if (showVersionUpToDate)
- {
- await ContentDialogHelper.CreateUpdaterInfoDialog(
- LocaleManager.Instance[LocaleKeys.DialogUpdaterAlreadyOnLatestVersionMessage],
- "");
- }
-
- _running = false;
-
- return;
- }
- }
- catch (Exception exception)
- {
- Logger.Error?.Print(LogClass.Application, exception.Message);
-
- await ContentDialogHelper.CreateErrorDialog(
- LocaleManager.Instance[LocaleKeys.DialogUpdaterFailedToGetVersionMessage]);
-
- _running = false;
-
- return;
- }
-
- try
- {
- newVersion = Version.Parse(_buildVer);
- }
- catch
- {
- Logger.Error?.Print(LogClass.Application, "Failed to convert the received Ryujinx version from Github!");
-
- await ContentDialogHelper.CreateWarningDialog(
- LocaleManager.Instance[LocaleKeys.DialogUpdaterConvertFailedGithubMessage],
- LocaleManager.Instance[LocaleKeys.DialogUpdaterCancelUpdateMessage]);
-
- _running = false;
-
- return;
- }
-
- if (newVersion <= currentVersion)
- {
- if (showVersionUpToDate)
- {
- await ContentDialogHelper.CreateUpdaterInfoDialog(
- LocaleManager.Instance[LocaleKeys.DialogUpdaterAlreadyOnLatestVersionMessage],
- "");
- }
-
- _running = false;
-
- return;
- }
-
- // Fetch build size information to learn chunk sizes.
- using HttpClient buildSizeClient = ConstructHttpClient();
- try
- {
- buildSizeClient.DefaultRequestHeaders.Add("Range", "bytes=0-0");
-
- HttpResponseMessage message = await buildSizeClient.GetAsync(new Uri(_buildUrl), HttpCompletionOption.ResponseHeadersRead);
-
- _buildSize = message.Content.Headers.ContentRange.Length.Value;
- }
- catch (Exception ex)
- {
- Logger.Warning?.Print(LogClass.Application, ex.Message);
- Logger.Warning?.Print(LogClass.Application, "Couldn't determine build size for update, using single-threaded updater");
-
- _buildSize = -1;
- }
-
- await Dispatcher.UIThread.InvokeAsync(async () =>
- {
- // Show a message asking the user if they want to update
- var shouldUpdate = await ContentDialogHelper.CreateChoiceDialog(
- LocaleManager.Instance[LocaleKeys.RyujinxUpdater],
- LocaleManager.Instance[LocaleKeys.RyujinxUpdaterMessage],
- $"{Program.Version} -> {newVersion}");
-
- if (shouldUpdate)
- {
- await UpdateRyujinx(mainWindow, _buildUrl);
- }
- else
- {
- _running = false;
- }
- });
- }
-
- private static HttpClient ConstructHttpClient()
- {
- HttpClient result = new();
-
- // Required by GitHub to interact with APIs.
- result.DefaultRequestHeaders.Add("User-Agent", "Ryujinx-Updater/1.0.0");
-
- return result;
- }
-
- private static async Task UpdateRyujinx(Window parent, string downloadUrl)
- {
- _updateSuccessful = false;
-
- // Empty update dir, although it shouldn't ever have anything inside it
- if (Directory.Exists(_updateDir))
- {
- Directory.Delete(_updateDir, true);
- }
-
- Directory.CreateDirectory(_updateDir);
-
- string updateFile = Path.Combine(_updateDir, "update.bin");
-
- TaskDialog taskDialog = new()
- {
- Header = LocaleManager.Instance[LocaleKeys.RyujinxUpdater],
- SubHeader = LocaleManager.Instance[LocaleKeys.UpdaterDownloading],
- IconSource = new SymbolIconSource { Symbol = Symbol.Download },
- ShowProgressBar = true,
- XamlRoot = parent,
- };
-
- taskDialog.Opened += (s, e) =>
- {
- if (_buildSize >= 0)
- {
- DoUpdateWithMultipleThreads(taskDialog, downloadUrl, updateFile);
- }
- else
- {
- DoUpdateWithSingleThread(taskDialog, downloadUrl, updateFile);
- }
- };
-
- await taskDialog.ShowAsync(true);
-
- if (_updateSuccessful)
- {
- bool shouldRestart = true;
-
- if (!OperatingSystem.IsMacOS())
- {
- shouldRestart = await ContentDialogHelper.CreateChoiceDialog(LocaleManager.Instance[LocaleKeys.RyujinxUpdater],
- LocaleManager.Instance[LocaleKeys.DialogUpdaterCompleteMessage],
- LocaleManager.Instance[LocaleKeys.DialogUpdaterRestartMessage]);
- }
-
- if (shouldRestart)
- {
- List arguments = CommandLineState.Arguments.ToList();
- string executableDirectory = AppDomain.CurrentDomain.BaseDirectory;
-
- // On macOS we perform the update at relaunch.
- if (OperatingSystem.IsMacOS())
- {
- string baseBundlePath = Path.GetFullPath(Path.Combine(executableDirectory, "..", ".."));
- string newBundlePath = Path.Combine(_updateDir, "Ryujinx.app");
- string updaterScriptPath = Path.Combine(newBundlePath, "Contents", "Resources", "updater.sh");
- string currentPid = Environment.ProcessId.ToString();
-
- arguments.InsertRange(0, new List { updaterScriptPath, baseBundlePath, newBundlePath, currentPid });
- Process.Start("/bin/bash", arguments);
- }
- else
- {
- // Find the process name.
- string ryuName = Path.GetFileName(Environment.ProcessPath);
-
- // Some operating systems can see the renamed executable, so strip off the .ryuold if found.
- if (ryuName.EndsWith(".ryuold"))
- {
- ryuName = ryuName[..^7];
- }
-
- // Fallback if the executable could not be found.
- if (!Path.Exists(Path.Combine(executableDirectory, ryuName)))
- {
- ryuName = OperatingSystem.IsWindows() ? "Ryujinx.Ava.exe" : "Ryujinx.Ava";
- }
-
- ProcessStartInfo processStart = new(ryuName)
- {
- UseShellExecute = true,
- WorkingDirectory = executableDirectory,
- };
-
- foreach (string argument in CommandLineState.Arguments)
- {
- processStart.ArgumentList.Add(argument);
- }
-
- Process.Start(processStart);
- }
-
- Environment.Exit(0);
- }
- }
- }
-
- private static void DoUpdateWithMultipleThreads(TaskDialog taskDialog, string downloadUrl, string updateFile)
- {
- // Multi-Threaded Updater
- long chunkSize = _buildSize / ConnectionCount;
- long remainderChunk = _buildSize % ConnectionCount;
-
- int completedRequests = 0;
- int totalProgressPercentage = 0;
- int[] progressPercentage = new int[ConnectionCount];
-
- List list = new(ConnectionCount);
- List webClients = new(ConnectionCount);
-
- for (int i = 0; i < ConnectionCount; i++)
- {
- list.Add(Array.Empty());
- }
-
- for (int i = 0; i < ConnectionCount; i++)
- {
-#pragma warning disable SYSLIB0014
- // TODO: WebClient is obsolete and need to be replaced with a more complex logic using HttpClient.
- using WebClient client = new();
-#pragma warning restore SYSLIB0014
-
- webClients.Add(client);
-
- if (i == ConnectionCount - 1)
- {
- client.Headers.Add("Range", $"bytes={chunkSize * i}-{(chunkSize * (i + 1) - 1) + remainderChunk}");
- }
- else
- {
- client.Headers.Add("Range", $"bytes={chunkSize * i}-{chunkSize * (i + 1) - 1}");
- }
-
- client.DownloadProgressChanged += (_, args) =>
- {
- int index = (int)args.UserState;
-
- Interlocked.Add(ref totalProgressPercentage, -1 * progressPercentage[index]);
- Interlocked.Exchange(ref progressPercentage[index], args.ProgressPercentage);
- Interlocked.Add(ref totalProgressPercentage, args.ProgressPercentage);
-
- taskDialog.SetProgressBarState(totalProgressPercentage / ConnectionCount, TaskDialogProgressState.Normal);
- };
-
- client.DownloadDataCompleted += (_, args) =>
- {
- int index = (int)args.UserState;
-
- if (args.Cancelled)
- {
- webClients[index].Dispose();
-
- taskDialog.Hide();
-
- return;
- }
-
- list[index] = args.Result;
- Interlocked.Increment(ref completedRequests);
-
- if (Equals(completedRequests, ConnectionCount))
- {
- byte[] mergedFileBytes = new byte[_buildSize];
- for (int connectionIndex = 0, destinationOffset = 0; connectionIndex < ConnectionCount; connectionIndex++)
- {
- Array.Copy(list[connectionIndex], 0, mergedFileBytes, destinationOffset, list[connectionIndex].Length);
- destinationOffset += list[connectionIndex].Length;
- }
-
- File.WriteAllBytes(updateFile, mergedFileBytes);
-
- // On macOS, ensure that we remove the quarantine bit to prevent Gatekeeper from blocking execution.
- if (OperatingSystem.IsMacOS())
- {
- using Process xattrProcess = Process.Start("xattr", new List { "-d", "com.apple.quarantine", updateFile });
-
- xattrProcess.WaitForExit();
- }
-
- try
- {
- InstallUpdate(taskDialog, updateFile);
- }
- catch (Exception e)
- {
- Logger.Warning?.Print(LogClass.Application, e.Message);
- Logger.Warning?.Print(LogClass.Application, "Multi-Threaded update failed, falling back to single-threaded updater.");
-
- DoUpdateWithSingleThread(taskDialog, downloadUrl, updateFile);
- }
- }
- };
-
- try
- {
- client.DownloadDataAsync(new Uri(downloadUrl), i);
- }
- catch (WebException ex)
- {
- Logger.Warning?.Print(LogClass.Application, ex.Message);
- Logger.Warning?.Print(LogClass.Application, "Multi-Threaded update failed, falling back to single-threaded updater.");
-
- foreach (WebClient webClient in webClients)
- {
- webClient.CancelAsync();
- }
-
- DoUpdateWithSingleThread(taskDialog, downloadUrl, updateFile);
-
- return;
- }
- }
- }
-
- private static void DoUpdateWithSingleThreadWorker(TaskDialog taskDialog, string downloadUrl, string updateFile)
- {
- using HttpClient client = new();
- // We do not want to timeout while downloading
- client.Timeout = TimeSpan.FromDays(1);
-
- using HttpResponseMessage response = client.GetAsync(downloadUrl, HttpCompletionOption.ResponseHeadersRead).Result;
- using Stream remoteFileStream = response.Content.ReadAsStreamAsync().Result;
- using Stream updateFileStream = File.Open(updateFile, FileMode.Create);
-
- long totalBytes = response.Content.Headers.ContentLength.Value;
- long byteWritten = 0;
-
- byte[] buffer = new byte[32 * 1024];
-
- while (true)
- {
- int readSize = remoteFileStream.Read(buffer);
-
- if (readSize == 0)
- {
- break;
- }
-
- byteWritten += readSize;
-
- taskDialog.SetProgressBarState(GetPercentage(byteWritten, totalBytes), TaskDialogProgressState.Normal);
-
- updateFileStream.Write(buffer, 0, readSize);
- }
-
- InstallUpdate(taskDialog, updateFile);
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static double GetPercentage(double value, double max)
- {
- return max == 0 ? 0 : value / max * 100;
- }
-
- private static void DoUpdateWithSingleThread(TaskDialog taskDialog, string downloadUrl, string updateFile)
- {
- Thread worker = new(() => DoUpdateWithSingleThreadWorker(taskDialog, downloadUrl, updateFile))
- {
- Name = "Updater.SingleThreadWorker",
- };
-
- worker.Start();
- }
-
- [SupportedOSPlatform("linux")]
- [SupportedOSPlatform("macos")]
- private static void ExtractTarGzipFile(TaskDialog taskDialog, string archivePath, string outputDirectoryPath)
- {
- using Stream inStream = File.OpenRead(archivePath);
- using GZipInputStream gzipStream = new(inStream);
- using TarInputStream tarStream = new(gzipStream, Encoding.ASCII);
-
- TarEntry tarEntry;
-
- while ((tarEntry = tarStream.GetNextEntry()) is not null)
- {
- if (tarEntry.IsDirectory)
- {
- continue;
- }
-
- string outPath = Path.Combine(outputDirectoryPath, tarEntry.Name);
-
- Directory.CreateDirectory(Path.GetDirectoryName(outPath));
-
- using FileStream outStream = File.OpenWrite(outPath);
- tarStream.CopyEntryContents(outStream);
-
- File.SetUnixFileMode(outPath, (UnixFileMode)tarEntry.TarHeader.Mode);
- File.SetLastWriteTime(outPath, DateTime.SpecifyKind(tarEntry.ModTime, DateTimeKind.Utc));
-
- Dispatcher.UIThread.Post(() =>
- {
- if (tarEntry is null)
- {
- return;
- }
-
- taskDialog.SetProgressBarState(GetPercentage(tarEntry.Size, inStream.Length), TaskDialogProgressState.Normal);
- });
- }
- }
-
- private static void ExtractZipFile(TaskDialog taskDialog, string archivePath, string outputDirectoryPath)
- {
- using Stream inStream = File.OpenRead(archivePath);
- using ZipFile zipFile = new(inStream);
-
- double count = 0;
- foreach (ZipEntry zipEntry in zipFile)
- {
- count++;
- if (zipEntry.IsDirectory)
- {
- continue;
- }
-
- string outPath = Path.Combine(outputDirectoryPath, zipEntry.Name);
-
- Directory.CreateDirectory(Path.GetDirectoryName(outPath));
-
- using Stream zipStream = zipFile.GetInputStream(zipEntry);
- using FileStream outStream = File.OpenWrite(outPath);
-
- zipStream.CopyTo(outStream);
-
- File.SetLastWriteTime(outPath, DateTime.SpecifyKind(zipEntry.DateTime, DateTimeKind.Utc));
-
- Dispatcher.UIThread.Post(() =>
- {
- taskDialog.SetProgressBarState(GetPercentage(count, zipFile.Count), TaskDialogProgressState.Normal);
- });
- }
- }
-
- private static void InstallUpdate(TaskDialog taskDialog, string updateFile)
- {
- // Extract Update
- taskDialog.SubHeader = LocaleManager.Instance[LocaleKeys.UpdaterExtracting];
- taskDialog.SetProgressBarState(0, TaskDialogProgressState.Normal);
-
- if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS())
- {
- ExtractTarGzipFile(taskDialog, updateFile, _updateDir);
- }
- else if (OperatingSystem.IsWindows())
- {
- ExtractZipFile(taskDialog, updateFile, _updateDir);
- }
- else
- {
- throw new NotSupportedException();
- }
-
- // Delete downloaded zip
- File.Delete(updateFile);
-
- List allFiles = EnumerateFilesToDelete().ToList();
-
- taskDialog.SubHeader = LocaleManager.Instance[LocaleKeys.UpdaterRenaming];
- taskDialog.SetProgressBarState(0, TaskDialogProgressState.Normal);
-
- // NOTE: On macOS, replacement is delayed to the restart phase.
- if (!OperatingSystem.IsMacOS())
- {
- // Replace old files
- double count = 0;
- foreach (string file in allFiles)
- {
- count++;
- try
- {
- File.Move(file, file + ".ryuold");
-
- Dispatcher.UIThread.InvokeAsync(() =>
- {
- taskDialog.SetProgressBarState(GetPercentage(count, allFiles.Count), TaskDialogProgressState.Normal);
- });
- }
- catch
- {
- Logger.Warning?.Print(LogClass.Application, LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.UpdaterRenameFailed, file));
- }
- }
-
- Dispatcher.UIThread.InvokeAsync(() =>
- {
- taskDialog.SubHeader = LocaleManager.Instance[LocaleKeys.UpdaterAddingFiles];
- taskDialog.SetProgressBarState(0, TaskDialogProgressState.Normal);
- });
-
- MoveAllFilesOver(_updatePublishDir, _homeDir, taskDialog);
-
- Directory.Delete(_updateDir, true);
- }
-
- _updateSuccessful = true;
-
- taskDialog.Hide();
- }
-
- public static bool CanUpdate(bool showWarnings)
- {
-#if !DISABLE_UPDATER
- if (RuntimeInformation.OSArchitecture != Architecture.X64 && !OperatingSystem.IsMacOS())
- {
- if (showWarnings)
- {
- Dispatcher.UIThread.InvokeAsync(() =>
- ContentDialogHelper.CreateWarningDialog(
- LocaleManager.Instance[LocaleKeys.DialogUpdaterArchNotSupportedMessage],
- LocaleManager.Instance[LocaleKeys.DialogUpdaterArchNotSupportedSubMessage])
- );
- }
-
- return false;
- }
-
- if (!NetworkInterface.GetIsNetworkAvailable())
- {
- if (showWarnings)
- {
- Dispatcher.UIThread.InvokeAsync(() =>
- ContentDialogHelper.CreateWarningDialog(
- LocaleManager.Instance[LocaleKeys.DialogUpdaterNoInternetMessage],
- LocaleManager.Instance[LocaleKeys.DialogUpdaterNoInternetSubMessage])
- );
- }
-
- return false;
- }
-
- if (Program.Version.Contains("dirty") || !ReleaseInformation.IsValid())
- {
- if (showWarnings)
- {
- Dispatcher.UIThread.InvokeAsync(() =>
- ContentDialogHelper.CreateWarningDialog(
- LocaleManager.Instance[LocaleKeys.DialogUpdaterDirtyBuildMessage],
- LocaleManager.Instance[LocaleKeys.DialogUpdaterDirtyBuildSubMessage])
- );
- }
-
- return false;
- }
-
- return true;
-#else
- if (showWarnings)
- {
- if (ReleaseInformation.IsFlatHubBuild())
- {
- Dispatcher.UIThread.InvokeAsync(() =>
- ContentDialogHelper.CreateWarningDialog(
- LocaleManager.Instance[LocaleKeys.UpdaterDisabledWarningTitle],
- LocaleManager.Instance[LocaleKeys.DialogUpdaterFlatpakNotSupportedMessage])
- );
- }
- else
- {
- Dispatcher.UIThread.InvokeAsync(() =>
- ContentDialogHelper.CreateWarningDialog(
- LocaleManager.Instance[LocaleKeys.UpdaterDisabledWarningTitle],
- LocaleManager.Instance[LocaleKeys.DialogUpdaterDirtyBuildSubMessage])
- );
- }
- }
-
- return false;
-#endif
- }
-
- // NOTE: This method should always reflect the latest build layout.
- private static IEnumerable EnumerateFilesToDelete()
- {
- var files = Directory.EnumerateFiles(_homeDir); // All files directly in base dir.
-
- // Determine and exclude user files only when the updater is running, not when cleaning old files
- if (_running && !OperatingSystem.IsMacOS())
- {
- // Compare the loose files in base directory against the loose files from the incoming update, and store foreign ones in a user list.
- var oldFiles = Directory.EnumerateFiles(_homeDir, "*", SearchOption.TopDirectoryOnly).Select(Path.GetFileName);
- var newFiles = Directory.EnumerateFiles(_updatePublishDir, "*", SearchOption.TopDirectoryOnly).Select(Path.GetFileName);
- var userFiles = oldFiles.Except(newFiles).Select(filename => Path.Combine(_homeDir, filename));
-
- // Remove user files from the paths in files.
- files = files.Except(userFiles);
- }
-
- if (OperatingSystem.IsWindows())
- {
- foreach (string dir in _windowsDependencyDirs)
- {
- string dirPath = Path.Combine(_homeDir, dir);
- if (Directory.Exists(dirPath))
- {
- files = files.Concat(Directory.EnumerateFiles(dirPath, "*", SearchOption.AllDirectories));
- }
- }
- }
-
- return files.Where(f => !new FileInfo(f).Attributes.HasFlag(FileAttributes.Hidden | FileAttributes.System));
- }
-
- private static void MoveAllFilesOver(string root, string dest, TaskDialog taskDialog)
- {
- int total = Directory.GetFiles(root, "*", SearchOption.AllDirectories).Length;
- foreach (string directory in Directory.GetDirectories(root))
- {
- string dirName = Path.GetFileName(directory);
-
- if (!Directory.Exists(Path.Combine(dest, dirName)))
- {
- Directory.CreateDirectory(Path.Combine(dest, dirName));
- }
-
- MoveAllFilesOver(directory, Path.Combine(dest, dirName), taskDialog);
- }
-
- double count = 0;
- foreach (string file in Directory.GetFiles(root))
- {
- count++;
-
- File.Move(file, Path.Combine(dest, Path.GetFileName(file)), true);
-
- Dispatcher.UIThread.InvokeAsync(() =>
- {
- taskDialog.SetProgressBarState(GetPercentage(count, total), TaskDialogProgressState.Normal);
- });
- }
- }
-
- public static void CleanupUpdate()
- {
- foreach (string file in Directory.GetFiles(_homeDir, "*.ryuold", SearchOption.AllDirectories))
- {
- File.Delete(file);
- }
- }
- }
-}
diff --git a/src/Ryujinx.Ava/Program.cs b/src/Ryujinx.Ava/Program.cs
deleted file mode 100644
index cc062a256..000000000
--- a/src/Ryujinx.Ava/Program.cs
+++ /dev/null
@@ -1,236 +0,0 @@
-using Avalonia;
-using Avalonia.Threading;
-using Ryujinx.Ava.UI.Helpers;
-using Ryujinx.Ava.UI.Windows;
-using Ryujinx.Common;
-using Ryujinx.Common.Configuration;
-using Ryujinx.Common.GraphicsDriver;
-using Ryujinx.Common.Logging;
-using Ryujinx.Common.SystemInterop;
-using Ryujinx.Modules;
-using Ryujinx.SDL2.Common;
-using Ryujinx.Ui.Common;
-using Ryujinx.Ui.Common.Configuration;
-using Ryujinx.Ui.Common.Helper;
-using Ryujinx.Ui.Common.SystemInfo;
-using System;
-using System.IO;
-using System.Runtime.InteropServices;
-using System.Threading.Tasks;
-
-namespace Ryujinx.Ava
-{
- internal partial class Program
- {
- public static double WindowScaleFactor { get; set; }
- public static double DesktopScaleFactor { get; set; } = 1.0;
- public static string Version { get; private set; }
- public static string ConfigurationPath { get; private set; }
- public static bool PreviewerDetached { get; private set; }
-
- [LibraryImport("user32.dll", SetLastError = true)]
- public static partial int MessageBoxA(IntPtr hWnd, [MarshalAs(UnmanagedType.LPStr)] string text, [MarshalAs(UnmanagedType.LPStr)] string caption, uint type);
-
- private const uint MbIconwarning = 0x30;
-
- public static void Main(string[] args)
- {
- Version = ReleaseInformation.GetVersion();
-
- if (OperatingSystem.IsWindows() && !OperatingSystem.IsWindowsVersionAtLeast(10, 0, 17134))
- {
- _ = MessageBoxA(IntPtr.Zero, "You are running an outdated version of Windows.\n\nStarting on June 1st 2022, Ryujinx will only support Windows 10 1803 and newer.\n", $"Ryujinx {Version}", MbIconwarning);
- }
-
- PreviewerDetached = true;
-
- Initialize(args);
-
- LoggerAdapter.Register();
-
- BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
- }
-
- public static AppBuilder BuildAvaloniaApp()
- {
- return AppBuilder.Configure()
- .UsePlatformDetect()
- .With(new X11PlatformOptions
- {
- EnableMultiTouch = true,
- EnableIme = true,
- RenderingMode = new[] { X11RenderingMode.Glx, X11RenderingMode.Software },
- })
- .With(new Win32PlatformOptions
- {
- WinUICompositionBackdropCornerRadius = 8.0f,
- RenderingMode = new[] { Win32RenderingMode.AngleEgl, Win32RenderingMode.Software },
- })
- .UseSkia();
- }
-
- private static void Initialize(string[] args)
- {
- // Parse arguments
- CommandLineState.ParseArguments(args);
-
- // Delete backup files after updating.
- Task.Run(Updater.CleanupUpdate);
-
- Console.Title = $"Ryujinx Console {Version}";
-
- // Hook unhandled exception and process exit events.
- AppDomain.CurrentDomain.UnhandledException += (sender, e) => ProcessUnhandledException(e.ExceptionObject as Exception, e.IsTerminating);
- AppDomain.CurrentDomain.ProcessExit += (sender, e) => Exit();
-
- // Setup base data directory.
- AppDataManager.Initialize(CommandLineState.BaseDirPathArg);
-
- // Initialize the configuration.
- ConfigurationState.Initialize();
-
- // Initialize the logger system.
- LoggerModule.Initialize();
-
- // Initialize Discord integration.
- DiscordIntegrationModule.Initialize();
-
- // Initialize SDL2 driver
- SDL2Driver.MainThreadDispatcher = action => Dispatcher.UIThread.InvokeAsync(action, DispatcherPriority.Input);
-
- ReloadConfig();
-
- WindowScaleFactor = ForceDpiAware.GetWindowScaleFactor();
-
- // Logging system information.
- PrintSystemInfo();
-
- // Enable OGL multithreading on the driver, when available.
- DriverUtilities.ToggleOGLThreading(ConfigurationState.Instance.Graphics.BackendThreading == BackendThreading.Off);
-
- // Check if keys exists.
- if (!File.Exists(Path.Combine(AppDataManager.KeysDirPath, "prod.keys")))
- {
- if (!(AppDataManager.Mode == AppDataManager.LaunchMode.UserProfile && File.Exists(Path.Combine(AppDataManager.KeysDirPathUser, "prod.keys"))))
- {
- MainWindow.ShowKeyErrorOnLoad = true;
- }
- }
-
- if (CommandLineState.LaunchPathArg != null)
- {
- MainWindow.DeferLoadApplication(CommandLineState.LaunchPathArg, CommandLineState.StartFullscreenArg);
- }
- }
-
- public static void ReloadConfig()
- {
- string localConfigurationPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Config.json");
- string appDataConfigurationPath = Path.Combine(AppDataManager.BaseDirPath, "Config.json");
-
- // Now load the configuration as the other subsystems are now registered
- if (File.Exists(localConfigurationPath))
- {
- ConfigurationPath = localConfigurationPath;
- }
- else if (File.Exists(appDataConfigurationPath))
- {
- ConfigurationPath = appDataConfigurationPath;
- }
-
- if (ConfigurationPath == null)
- {
- // No configuration, we load the default values and save it to disk
- ConfigurationPath = appDataConfigurationPath;
-
- ConfigurationState.Instance.LoadDefault();
- ConfigurationState.Instance.ToFileFormat().SaveConfig(ConfigurationPath);
- }
- else
- {
- 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}");
- }
- }
-
- // Check if graphics backend was overridden
- if (CommandLineState.OverrideGraphicsBackend != null)
- {
- if (CommandLineState.OverrideGraphicsBackend.ToLower() == "opengl")
- {
- ConfigurationState.Instance.Graphics.GraphicsBackend.Value = GraphicsBackend.OpenGl;
- }
- else if (CommandLineState.OverrideGraphicsBackend.ToLower() == "vulkan")
- {
- ConfigurationState.Instance.Graphics.GraphicsBackend.Value = GraphicsBackend.Vulkan;
- }
- }
-
- // Check if docked mode was overriden.
- if (CommandLineState.OverrideDockedMode.HasValue)
- {
- ConfigurationState.Instance.System.EnableDockedMode.Value = CommandLineState.OverrideDockedMode.Value;
- }
-
- // Check if HideCursor was overridden.
- if (CommandLineState.OverrideHideCursor is not null)
- {
- ConfigurationState.Instance.HideCursor.Value = CommandLineState.OverrideHideCursor!.ToLower() switch
- {
- "never" => HideCursorMode.Never,
- "onidle" => HideCursorMode.OnIdle,
- "always" => HideCursorMode.Always,
- _ => ConfigurationState.Instance.HideCursor.Value,
- };
- }
- }
-
- private static void PrintSystemInfo()
- {
- Logger.Notice.Print(LogClass.Application, $"Ryujinx Version: {Version}");
- SystemInfo.Gather().Print();
-
- Logger.Notice.Print(LogClass.Application, $"Logs Enabled: {(Logger.GetEnabledLevels().Count == 0 ? "" : string.Join(", ", Logger.GetEnabledLevels()))}");
-
- if (AppDataManager.Mode == AppDataManager.LaunchMode.Custom)
- {
- Logger.Notice.Print(LogClass.Application, $"Launch Mode: Custom Path {AppDataManager.BaseDirPath}");
- }
- else
- {
- Logger.Notice.Print(LogClass.Application, $"Launch Mode: {AppDataManager.Mode}");
- }
- }
-
- private static void ProcessUnhandledException(Exception ex, bool isTerminating)
- {
- string message = $"Unhandled exception caught: {ex}";
-
- Logger.Error?.PrintMsg(LogClass.Application, message);
-
- if (Logger.Error == null)
- {
- Logger.Notice.PrintMsg(LogClass.Application, message);
- }
-
- if (isTerminating)
- {
- Exit();
- }
- }
-
- public static void Exit()
- {
- DiscordIntegrationModule.Exit();
-
- Logger.Shutdown();
- }
- }
-}
diff --git a/src/Ryujinx.Ava/UI/Helpers/KeyValueConverter.cs b/src/Ryujinx.Ava/UI/Helpers/KeyValueConverter.cs
deleted file mode 100644
index 028ed6bf4..000000000
--- a/src/Ryujinx.Ava/UI/Helpers/KeyValueConverter.cs
+++ /dev/null
@@ -1,46 +0,0 @@
-using Avalonia.Data.Converters;
-using Ryujinx.Common.Configuration.Hid;
-using Ryujinx.Common.Configuration.Hid.Controller;
-using System;
-using System.Globalization;
-
-namespace Ryujinx.Ava.UI.Helpers
-{
- internal class KeyValueConverter : IValueConverter
- {
- public static KeyValueConverter Instance = new();
-
- public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
- {
- if (value == null)
- {
- return null;
- }
-
- return value.ToString();
- }
-
- public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
- {
- object key = null;
-
- if (value != null)
- {
- if (targetType == typeof(Key))
- {
- key = Enum.Parse(value.ToString());
- }
- else if (targetType == typeof(GamepadInputId))
- {
- key = Enum.Parse(value.ToString());
- }
- else if (targetType == typeof(StickInputId))
- {
- key = Enum.Parse(value.ToString());
- }
- }
-
- return key;
- }
- }
-}
diff --git a/src/Ryujinx.Ava/UI/Models/TitleUpdateModel.cs b/src/Ryujinx.Ava/UI/Models/TitleUpdateModel.cs
deleted file mode 100644
index c270c9ed4..000000000
--- a/src/Ryujinx.Ava/UI/Models/TitleUpdateModel.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-using LibHac.Ns;
-using Ryujinx.Ava.Common.Locale;
-
-namespace Ryujinx.Ava.UI.Models
-{
- public class TitleUpdateModel
- {
- public ApplicationControlProperty Control { get; }
- public string Path { get; }
-
- public string Label => LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.TitleUpdateVersionLabel, Control.DisplayVersionString.ToString());
-
- public TitleUpdateModel(ApplicationControlProperty control, string path)
- {
- Control = control;
- Path = path;
- }
- }
-}
diff --git a/src/Ryujinx.Ava/UI/ViewModels/GamePadInputViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/GamePadInputViewModel.cs
deleted file mode 100644
index c8d98d24d..000000000
--- a/src/Ryujinx.Ava/UI/ViewModels/GamePadInputViewModel.cs
+++ /dev/null
@@ -1,63 +0,0 @@
-using Ryujinx.Ava.UI.Models;
-using Ryujinx.Ava.UI.Views.Input;
-using Ryujinx.Common.Configuration.Hid;
-using System;
-using ConfigGamepadInputId = Ryujinx.Common.Configuration.Hid.Controller.GamepadInputId;
-using ConfigStickInputId = Ryujinx.Common.Configuration.Hid.Controller.StickInputId;
-
-namespace Ryujinx.Ava.UI.ViewModels
-{
- public class GamePadInputViewModel : InputViewModel
- {
- private InputConfiguration _configuration;
- private Func _showMotionConfigCommand;
- private Func _showRumbleConfigCommand;
-
-
- public InputConfiguration Configuration
- {
- get => _configuration;
- set
- {
- _configuration = value;
-
- OnPropertyChanged();
- }
- }
-
- internal override object Config => _configuration;
-
- public GamePadInputViewModel(InputConfiguration configuration, Func showMotionConfigCommand, Func showRumbleConfigCommand)
- {
- Configuration = configuration;
- _showMotionConfigCommand = showMotionConfigCommand;
- _showRumbleConfigCommand = showRumbleConfigCommand;
- }
-
- public GamePadInputViewModel()
- {
- }
-
- public override void NotifyChanges()
- {
- OnPropertyChanged(nameof(Configuration));
-
- base.NotifyChanges();
- }
-
- public override InputConfig GetConfig()
- {
- return _configuration.GetConfig();
- }
-
- public async void ShowMotionConfig()
- {
- await _showMotionConfigCommand();
- }
-
- public async void ShowRumbleConfig()
- {
- await _showRumbleConfigCommand();
- }
- }
-}
diff --git a/src/Ryujinx.Ava/UI/ViewModels/InputViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/InputViewModel.cs
deleted file mode 100644
index 2021746cf..000000000
--- a/src/Ryujinx.Ava/UI/ViewModels/InputViewModel.cs
+++ /dev/null
@@ -1,64 +0,0 @@
-using Avalonia.Svg.Skia;
-using Ryujinx.Ava.UI.Models;
-using Ryujinx.Common;
-using Ryujinx.Common.Configuration.Hid;
-using ConfigStickInputId = Ryujinx.Common.Configuration.Hid.Controller.StickInputId;
-using Key = Ryujinx.Common.Configuration.Hid.Key;
-
-namespace Ryujinx.Ava.UI.ViewModels
-{
- public abstract class InputViewModel : BaseModel
- {
- private string _controllerImage;
-
- public bool IsRight { get; set; }
- public bool IsLeft { get; set; }
-
- internal abstract object Config { get; }
-
- public void NotifyChange(string property)
- {
- OnPropertyChanged(property);
- }
-
- public string ControllerImage
- {
- get => _controllerImage;
- set
- {
- _controllerImage = value;
-
- OnPropertyChanged();
- OnPropertyChanged(nameof(Image));
- }
- }
-
- public SvgImage Image
- {
- get
- {
- SvgImage image = new();
-
- if (!string.IsNullOrWhiteSpace(_controllerImage))
- {
- SvgSource source = new();
-
- source.Load(EmbeddedResources.GetStream(_controllerImage));
-
- image.Source = source;
- }
-
- return image;
- }
- }
-
- public virtual void NotifyChanges()
- {
- OnPropertyChanged(nameof(IsRight));
- OnPropertyChanged(nameof(IsLeft));
- OnPropertyChanged(nameof(Image));
- }
-
- public abstract InputConfig GetConfig();
- }
-}
diff --git a/src/Ryujinx.Ava/UI/ViewModels/KeyboardInputViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/KeyboardInputViewModel.cs
deleted file mode 100644
index b918e6732..000000000
--- a/src/Ryujinx.Ava/UI/ViewModels/KeyboardInputViewModel.cs
+++ /dev/null
@@ -1,47 +0,0 @@
-using Ryujinx.Ava.UI.Models;
-using Ryujinx.Common.Configuration.Hid;
-using ConfigStickInputId = Ryujinx.Common.Configuration.Hid.Controller.StickInputId;
-using Key = Ryujinx.Common.Configuration.Hid.Key;
-
-namespace Ryujinx.Ava.UI.ViewModels
-{
- public class KeyboardInputViewModel : InputViewModel
- {
- private InputConfiguration _configuration;
-
-
- public InputConfiguration Configuration
- {
- get => _configuration;
- set
- {
- _configuration = value;
-
- OnPropertyChanged();
- }
- }
-
- internal override object Config => _configuration;
-
- public KeyboardInputViewModel(InputConfiguration configuration)
- {
- Configuration = configuration;
- }
-
- public KeyboardInputViewModel()
- {
- }
-
- public override void NotifyChanges()
- {
- OnPropertyChanged(nameof(Configuration));
-
- base.NotifyChanges();
- }
-
- public override InputConfig GetConfig()
- {
- return _configuration.GetConfig();
- }
- }
-}
diff --git a/src/Ryujinx.Ava/UI/Views/Input/GamePadInputView.axaml b/src/Ryujinx.Ava/UI/Views/Input/GamePadInputView.axaml
deleted file mode 100644
index db1e588e6..000000000
--- a/src/Ryujinx.Ava/UI/Views/Input/GamePadInputView.axaml
+++ /dev/null
@@ -1,741 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/Ryujinx.Ava/UI/Views/Input/GamePadInputView.axaml.cs b/src/Ryujinx.Ava/UI/Views/Input/GamePadInputView.axaml.cs
deleted file mode 100644
index cf1037f5d..000000000
--- a/src/Ryujinx.Ava/UI/Views/Input/GamePadInputView.axaml.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-using Avalonia;
-using Avalonia.Controls;
-using Avalonia.Markup.Xaml;
-
-namespace Ryujinx.Ava.UI.Views.Input
-{
- public partial class GamePadInputView : UserControl
- {
- public GamePadInputView()
- {
- InitializeComponent();
- }
- }
-}
diff --git a/src/Ryujinx.Ava/UI/Views/Input/KeyboardInputView.axaml b/src/Ryujinx.Ava/UI/Views/Input/KeyboardInputView.axaml
deleted file mode 100644
index bcc31873f..000000000
--- a/src/Ryujinx.Ava/UI/Views/Input/KeyboardInputView.axaml
+++ /dev/null
@@ -1,672 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/Ryujinx.Ava/UI/Views/Input/KeyboardInputView.axaml.cs b/src/Ryujinx.Ava/UI/Views/Input/KeyboardInputView.axaml.cs
deleted file mode 100644
index 9e1470589..000000000
--- a/src/Ryujinx.Ava/UI/Views/Input/KeyboardInputView.axaml.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-using Avalonia;
-using Avalonia.Controls;
-using Avalonia.Markup.Xaml;
-
-namespace Ryujinx.Ava.UI.Views.Input
-{
- public partial class KeyboardInputView : UserControl
- {
- public KeyboardInputView()
- {
- InitializeComponent();
- }
- }
-}
diff --git a/src/Ryujinx.Ava/UI/Views/Settings/SettingsHotkeysView.axaml b/src/Ryujinx.Ava/UI/Views/Settings/SettingsHotkeysView.axaml
deleted file mode 100644
index a53c1dfe4..000000000
--- a/src/Ryujinx.Ava/UI/Views/Settings/SettingsHotkeysView.axaml
+++ /dev/null
@@ -1,103 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/src/Ryujinx.Ava/UI/Views/Settings/SettingsHotkeysView.axaml.cs b/src/Ryujinx.Ava/UI/Views/Settings/SettingsHotkeysView.axaml.cs
deleted file mode 100644
index b006d703f..000000000
--- a/src/Ryujinx.Ava/UI/Views/Settings/SettingsHotkeysView.axaml.cs
+++ /dev/null
@@ -1,81 +0,0 @@
-using Avalonia.Controls;
-using Avalonia.Controls.Primitives;
-using Avalonia.Input;
-using Avalonia.Interactivity;
-using Ryujinx.Ava.Input;
-using Ryujinx.Ava.UI.Helpers;
-using Ryujinx.Input;
-using Ryujinx.Input.Assigner;
-
-namespace Ryujinx.Ava.UI.Views.Settings
-{
- public partial class SettingsHotkeysView : UserControl
- {
- private ButtonKeyAssigner _currentAssigner;
- private readonly IGamepadDriver _avaloniaKeyboardDriver;
-
- public SettingsHotkeysView()
- {
- InitializeComponent();
- _avaloniaKeyboardDriver = new AvaloniaKeyboardDriver(this);
- }
-
- private void MouseClick(object sender, PointerPressedEventArgs e)
- {
- bool shouldUnbind = e.GetCurrentPoint(this).Properties.IsMiddleButtonPressed;
-
- _currentAssigner?.Cancel(shouldUnbind);
-
- PointerPressed -= MouseClick;
- }
-
- private void Button_Checked(object sender, RoutedEventArgs e)
- {
- if (sender is ToggleButton button)
- {
- if (_currentAssigner != null && button == _currentAssigner.ToggledButton)
- {
- return;
- }
-
- if (_currentAssigner == null && button.IsChecked != null && (bool)button.IsChecked)
- {
- _currentAssigner = new ButtonKeyAssigner(button);
-
- this.Focus(NavigationMethod.Pointer);
-
- PointerPressed += MouseClick;
-
- var keyboard = (IKeyboard)_avaloniaKeyboardDriver.GetGamepad(_avaloniaKeyboardDriver.GamepadsIds[0]);
- IButtonAssigner assigner = new KeyboardKeyAssigner(keyboard);
-
- _currentAssigner.GetInputAndAssign(assigner);
- }
- else
- {
- if (_currentAssigner != null)
- {
- ToggleButton oldButton = _currentAssigner.ToggledButton;
-
- _currentAssigner.Cancel();
- _currentAssigner = null;
-
- button.IsChecked = false;
- }
- }
- }
- }
-
- private void Button_Unchecked(object sender, RoutedEventArgs e)
- {
- _currentAssigner?.Cancel();
- _currentAssigner = null;
- }
-
- public void Dispose()
- {
- _currentAssigner?.Cancel();
- _currentAssigner = null;
- }
- }
-}
diff --git a/src/Ryujinx.Common/Configuration/AppDataManager.cs b/src/Ryujinx.Common/Configuration/AppDataManager.cs
index 2e7765a15..619d5f237 100644
--- a/src/Ryujinx.Common/Configuration/AppDataManager.cs
+++ b/src/Ryujinx.Common/Configuration/AppDataManager.cs
@@ -1,13 +1,15 @@
using Ryujinx.Common.Logging;
+using Ryujinx.Common.Utilities;
using System;
using System.IO;
+using System.Runtime.Versioning;
namespace Ryujinx.Common.Configuration
{
public static class AppDataManager
{
- public const string DefaultBaseDir = "Ryujinx";
- public const string DefaultPortableDir = "portable";
+ private const string DefaultBaseDir = "Ryujinx";
+ private const string DefaultPortableDir = "portable";
// The following 3 are always part of Base Directory
private const string GamesDir = "games";
@@ -29,6 +31,8 @@ namespace Ryujinx.Common.Configuration
public static string KeysDirPath { get; private set; }
public static string KeysDirPathUser { get; }
+ public static string LogsDirPath { get; private set; }
+
public const string DefaultNandDir = "bis";
public const string DefaultSdcardDir = "sdcard";
private const string DefaultModsDir = "mods";
@@ -75,6 +79,17 @@ namespace Ryujinx.Common.Configuration
string portablePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, DefaultPortableDir);
+ // On macOS, check for a portable directory next to the app bundle as well.
+ if (OperatingSystem.IsMacOS() && !Directory.Exists(portablePath))
+ {
+ string bundlePath = Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "..", ".."));
+ // Make sure we're actually running within an app bundle.
+ if (bundlePath.EndsWith(".app"))
+ {
+ portablePath = Path.GetFullPath(Path.Combine(bundlePath, "..", DefaultPortableDir));
+ }
+ }
+
if (Directory.Exists(portablePath))
{
BaseDirPath = portablePath;
@@ -101,65 +116,227 @@ namespace Ryujinx.Common.Configuration
BaseDirPath = Path.GetFullPath(BaseDirPath); // convert relative paths
- // NOTE: Moves the Ryujinx folder in `~/.config` to `~/Library/Application Support` if one is found
- // and a Ryujinx folder does not already exist in Application Support.
- // Also creates a symlink from `~/.config/Ryujinx` to `~/Library/Application Support/Ryujinx` to preserve backwards compatibility.
- // This should be removed in the future.
- if (OperatingSystem.IsMacOS() && Mode == LaunchMode.UserProfile)
+ if (IsPathSymlink(BaseDirPath))
{
- string oldConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), DefaultBaseDir);
- if (Path.Exists(oldConfigPath) && !IsPathSymlink(oldConfigPath) && !Path.Exists(BaseDirPath))
- {
- CopyDirectory(oldConfigPath, BaseDirPath);
- Directory.Delete(oldConfigPath, true);
- Directory.CreateSymbolicLink(oldConfigPath, BaseDirPath);
- }
+ Logger.Warning?.Print(LogClass.Application, $"Application data directory is a symlink. This may be unintended.");
}
SetupBasePaths();
}
+ public static string GetOrCreateLogsDir()
+ {
+ if (Directory.Exists(LogsDirPath))
+ {
+ return LogsDirPath;
+ }
+
+ Logger.Notice.Print(LogClass.Application, "Logging directory not found; attempting to create new logging directory.");
+ LogsDirPath = SetUpLogsDir();
+
+ return LogsDirPath;
+ }
+
+ private static string SetUpLogsDir()
+ {
+ string logDir = "";
+
+ if (Mode == LaunchMode.Portable)
+ {
+ logDir = Path.Combine(BaseDirPath, "Logs");
+ try
+ {
+ Directory.CreateDirectory(logDir);
+ }
+ catch
+ {
+ Logger.Warning?.Print(LogClass.Application, $"Logging directory could not be created '{logDir}'");
+
+ return null;
+ }
+ }
+ else
+ {
+ if (OperatingSystem.IsMacOS())
+ {
+ // NOTE: Should evaluate to "~/Library/Logs/Ryujinx/".
+ logDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Library", "Logs", DefaultBaseDir);
+ try
+ {
+ Directory.CreateDirectory(logDir);
+ }
+ catch
+ {
+ Logger.Warning?.Print(LogClass.Application, $"Logging directory could not be created '{logDir}'");
+ logDir = "";
+ }
+
+ if (string.IsNullOrEmpty(logDir))
+ {
+ // NOTE: Should evaluate to "~/Library/Application Support/Ryujinx/Logs".
+ logDir = Path.Combine(BaseDirPath, "Logs");
+
+ try
+ {
+ Directory.CreateDirectory(logDir);
+ }
+ catch
+ {
+ Logger.Warning?.Print(LogClass.Application, $"Logging directory could not be created '{logDir}'");
+
+ return null;
+ }
+ }
+ }
+ else if (OperatingSystem.IsWindows())
+ {
+ // NOTE: Should evaluate to a "Logs" directory in whatever directory Ryujinx was launched from.
+ logDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Logs");
+ try
+ {
+ Directory.CreateDirectory(logDir);
+ }
+ catch
+ {
+ Logger.Warning?.Print(LogClass.Application, $"Logging directory could not be created '{logDir}'");
+ logDir = "";
+ }
+
+ if (string.IsNullOrEmpty(logDir))
+ {
+ // NOTE: Should evaluate to "C:\Users\user\AppData\Roaming\Ryujinx\Logs".
+ logDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), DefaultBaseDir, "Logs");
+
+ try
+ {
+ Directory.CreateDirectory(logDir);
+ }
+ catch
+ {
+ Logger.Warning?.Print(LogClass.Application, $"Logging directory could not be created '{logDir}'");
+
+ return null;
+ }
+ }
+ }
+ else if (OperatingSystem.IsLinux())
+ {
+ // NOTE: Should evaluate to "~/.config/Ryujinx/Logs".
+ logDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), DefaultBaseDir, "Logs");
+
+ try
+ {
+ Directory.CreateDirectory(logDir);
+ }
+ catch
+ {
+ Logger.Warning?.Print(LogClass.Application, $"Logging directory could not be created '{logDir}'");
+
+ return null;
+ }
+ }
+ }
+
+ return logDir;
+ }
+
private static void SetupBasePaths()
{
Directory.CreateDirectory(BaseDirPath);
+ LogsDirPath = SetUpLogsDir();
Directory.CreateDirectory(GamesDirPath = Path.Combine(BaseDirPath, GamesDir));
Directory.CreateDirectory(ProfilesDirPath = Path.Combine(BaseDirPath, ProfilesDir));
Directory.CreateDirectory(KeysDirPath = Path.Combine(BaseDirPath, KeysDir));
}
// Check if existing old baseDirPath is a symlink, to prevent possible errors.
- // Should be removed, when the existance of the old directory isn't checked anymore.
+ // Should be removed, when the existence of the old directory isn't checked anymore.
private static bool IsPathSymlink(string path)
{
- FileAttributes attributes = File.GetAttributes(path);
- return (attributes & FileAttributes.ReparsePoint) == FileAttributes.ReparsePoint;
+ try
+ {
+ FileAttributes attributes = File.GetAttributes(path);
+ return (attributes & FileAttributes.ReparsePoint) == FileAttributes.ReparsePoint;
+ }
+ catch
+ {
+ return false;
+ }
}
- private static void CopyDirectory(string sourceDir, string destinationDir)
+ [SupportedOSPlatform("macos")]
+ public static void FixMacOSConfigurationFolders()
{
- var dir = new DirectoryInfo(sourceDir);
-
- if (!dir.Exists)
+ string oldConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
+ ".config", DefaultBaseDir);
+ if (Path.Exists(oldConfigPath) && !IsPathSymlink(oldConfigPath) && !Path.Exists(BaseDirPath))
{
- throw new DirectoryNotFoundException($"Source directory not found: {dir.FullName}");
+ FileSystemUtils.MoveDirectory(oldConfigPath, BaseDirPath);
+ Directory.CreateSymbolicLink(oldConfigPath, BaseDirPath);
}
- DirectoryInfo[] subDirs = dir.GetDirectories();
- Directory.CreateDirectory(destinationDir);
-
- foreach (FileInfo file in dir.GetFiles())
+ string correctApplicationDataDirectoryPath =
+ Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), DefaultBaseDir);
+ if (IsPathSymlink(correctApplicationDataDirectoryPath))
{
- if (file.Name == ".DS_Store")
+ //copy the files somewhere temporarily
+ string tempPath = Path.Combine(Path.GetTempPath(), DefaultBaseDir);
+ try
{
- continue;
+ FileSystemUtils.CopyDirectory(correctApplicationDataDirectoryPath, tempPath, true);
+ }
+ catch (Exception exception)
+ {
+ Logger.Error?.Print(LogClass.Application,
+ $"Critical error copying Ryujinx application data into the temp folder. {exception}");
+ try
+ {
+ FileSystemInfo resolvedDirectoryInfo =
+ Directory.ResolveLinkTarget(correctApplicationDataDirectoryPath, true);
+ string resolvedPath = resolvedDirectoryInfo.FullName;
+ Logger.Error?.Print(LogClass.Application, $"Please manually move your Ryujinx data from {resolvedPath} to {correctApplicationDataDirectoryPath}, and remove the symlink.");
+ }
+ catch (Exception symlinkException)
+ {
+ Logger.Error?.Print(LogClass.Application, $"Unable to resolve the symlink for Ryujinx application data: {symlinkException}. Follow the symlink at {correctApplicationDataDirectoryPath} and move your data back to the Application Support folder.");
+ }
+ return;
}
- file.CopyTo(Path.Combine(destinationDir, file.Name));
- }
+ //delete the symlink
+ try
+ {
+ //This will fail if this is an actual directory, so there is no way we can actually delete user data here.
+ File.Delete(correctApplicationDataDirectoryPath);
+ }
+ catch (Exception exception)
+ {
+ Logger.Error?.Print(LogClass.Application,
+ $"Critical error deleting the Ryujinx application data folder symlink at {correctApplicationDataDirectoryPath}. {exception}");
+ try
+ {
+ FileSystemInfo resolvedDirectoryInfo =
+ Directory.ResolveLinkTarget(correctApplicationDataDirectoryPath, true);
+ string resolvedPath = resolvedDirectoryInfo.FullName;
+ Logger.Error?.Print(LogClass.Application, $"Please manually move your Ryujinx data from {resolvedPath} to {correctApplicationDataDirectoryPath}, and remove the symlink.");
+ }
+ catch (Exception symlinkException)
+ {
+ Logger.Error?.Print(LogClass.Application, $"Unable to resolve the symlink for Ryujinx application data: {symlinkException}. Follow the symlink at {correctApplicationDataDirectoryPath} and move your data back to the Application Support folder.");
+ }
+ return;
+ }
- foreach (DirectoryInfo subDir in subDirs)
- {
- CopyDirectory(subDir.FullName, Path.Combine(destinationDir, subDir.Name));
+ //put the files back
+ try
+ {
+ FileSystemUtils.CopyDirectory(tempPath, correctApplicationDataDirectoryPath, true);
+ }
+ catch (Exception exception)
+ {
+ Logger.Error?.Print(LogClass.Application,
+ $"Critical error copying Ryujinx application data into the correct location. {exception}. Please manually move your application data from {tempPath} to {correctApplicationDataDirectoryPath}.");
+ }
}
}
diff --git a/src/Ryujinx.Common/Configuration/Hid/KeyboardHotkeys.cs b/src/Ryujinx.Common/Configuration/Hid/KeyboardHotkeys.cs
index b4f2f9468..0cb49ca8c 100644
--- a/src/Ryujinx.Common/Configuration/Hid/KeyboardHotkeys.cs
+++ b/src/Ryujinx.Common/Configuration/Hid/KeyboardHotkeys.cs
@@ -1,12 +1,10 @@
namespace Ryujinx.Common.Configuration.Hid
{
- // NOTE: Please don't change this to struct.
- // This breaks Avalonia's TwoWay binding, which makes us unable to save new KeyboardHotkeys.
public class KeyboardHotkeys
{
public Key ToggleVsync { get; set; }
public Key Screenshot { get; set; }
- public Key ShowUi { get; set; }
+ public Key ShowUI { get; set; }
public Key Pause { get; set; }
public Key ToggleMute { get; set; }
public Key ResScaleUp { get; set; }
diff --git a/src/Ryujinx.Common/Configuration/Mod.cs b/src/Ryujinx.Common/Configuration/Mod.cs
new file mode 100644
index 000000000..052c7c8d1
--- /dev/null
+++ b/src/Ryujinx.Common/Configuration/Mod.cs
@@ -0,0 +1,9 @@
+namespace Ryujinx.Common.Configuration
+{
+ public class Mod
+ {
+ public string Name { get; set; }
+ public string Path { get; set; }
+ public bool Enabled { get; set; }
+ }
+}
diff --git a/src/Ryujinx.Common/Configuration/ModMetadata.cs b/src/Ryujinx.Common/Configuration/ModMetadata.cs
new file mode 100644
index 000000000..174320d0a
--- /dev/null
+++ b/src/Ryujinx.Common/Configuration/ModMetadata.cs
@@ -0,0 +1,14 @@
+using System.Collections.Generic;
+
+namespace Ryujinx.Common.Configuration
+{
+ public struct ModMetadata
+ {
+ public List Mods { get; set; }
+
+ public ModMetadata()
+ {
+ Mods = new List();
+ }
+ }
+}
diff --git a/src/Ryujinx.Common/Configuration/ModMetadataJsonSerializerContext.cs b/src/Ryujinx.Common/Configuration/ModMetadataJsonSerializerContext.cs
new file mode 100644
index 000000000..8c1e242ad
--- /dev/null
+++ b/src/Ryujinx.Common/Configuration/ModMetadataJsonSerializerContext.cs
@@ -0,0 +1,10 @@
+using System.Text.Json.Serialization;
+
+namespace Ryujinx.Common.Configuration
+{
+ [JsonSourceGenerationOptions(WriteIndented = true)]
+ [JsonSerializable(typeof(ModMetadata))]
+ public partial class ModMetadataJsonSerializerContext : JsonSerializerContext
+ {
+ }
+}
diff --git a/src/Ryujinx.Common/Extensions/SequenceReaderExtensions.cs b/src/Ryujinx.Common/Extensions/SequenceReaderExtensions.cs
new file mode 100644
index 000000000..79b5d743b
--- /dev/null
+++ b/src/Ryujinx.Common/Extensions/SequenceReaderExtensions.cs
@@ -0,0 +1,181 @@
+using System;
+using System.Buffers;
+using System.Diagnostics;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Common.Extensions
+{
+ public static class SequenceReaderExtensions
+ {
+ ///
+ /// Dumps the entire to a file, restoring its previous location afterward.
+ /// Useful for debugging purposes.
+ ///
+ /// The to write to a file
+ /// The path and name of the file to create and dump to
+ public static void DumpToFile(this ref SequenceReader reader, string fileFullName)
+ {
+ var initialConsumed = reader.Consumed;
+
+ reader.Rewind(initialConsumed);
+
+ using (var fileStream = System.IO.File.Create(fileFullName, 4096, System.IO.FileOptions.None))
+ {
+ while (reader.End == false)
+ {
+ var span = reader.CurrentSpan;
+ fileStream.Write(span);
+ reader.Advance(span.Length);
+ }
+ }
+
+ reader.SetConsumed(initialConsumed);
+ }
+
+ ///
+ /// Returns a reference to the desired value. This ref should always be used. The argument passed in should never be used, as this is only used for storage if the value
+ /// must be copied from multiple segments held by the .
+ ///
+ /// Type to get
+ /// The to read from
+ /// A location used as storage if (and only if) the value to be read spans multiple segments
+ /// A reference to the desired value, either directly to memory in the , or to if it has been used for copying the value in to
+ ///
+ /// DO NOT use after calling this method, as it will only
+ /// contain a value if the value couldn't be referenced directly because it spans multiple segments.
+ /// To discourage use, it is recommended to call this method like the following:
+ ///
+ /// ref readonly MyStruct value = ref sequenceReader.GetRefOrRefToCopy{MyStruct}(out _);
+ ///
+ ///
+ /// The does not contain enough data to read a value of type
+ public static ref readonly T GetRefOrRefToCopy(this scoped ref SequenceReader reader, out T copyDestinationIfRequiredDoNotUse) where T : unmanaged
+ {
+ int lengthRequired = Unsafe.SizeOf();
+
+ ReadOnlySpan span = reader.UnreadSpan;
+ if (lengthRequired <= span.Length)
+ {
+ reader.Advance(lengthRequired);
+
+ copyDestinationIfRequiredDoNotUse = default;
+
+ ReadOnlySpan spanOfT = MemoryMarshal.Cast(span);
+
+ return ref spanOfT[0];
+ }
+ else
+ {
+ copyDestinationIfRequiredDoNotUse = default;
+
+ Span valueSpan = MemoryMarshal.CreateSpan(ref copyDestinationIfRequiredDoNotUse, 1);
+
+ Span valueBytesSpan = MemoryMarshal.AsBytes(valueSpan);
+
+ if (!reader.TryCopyTo(valueBytesSpan))
+ {
+ throw new ArgumentOutOfRangeException(nameof(reader), "The sequence is not long enough to read the desired value.");
+ }
+
+ reader.Advance(lengthRequired);
+
+ return ref valueSpan[0];
+ }
+ }
+
+ ///
+ /// Reads an as little endian.
+ ///
+ /// The to read from
+ /// A location to receive the read value
+ /// Thrown if there wasn't enough data for an
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void ReadLittleEndian(this ref SequenceReader reader, out int value)
+ {
+ if (!reader.TryReadLittleEndian(out value))
+ {
+ throw new ArgumentOutOfRangeException(nameof(value), "The sequence is not long enough to read the desired value.");
+ }
+ }
+
+ ///
+ /// Reads the desired unmanaged value by copying it to the specified .
+ ///
+ /// Type to read
+ /// The to read from
+ /// The target that will receive the read value
+ /// The does not contain enough data to read a value of type
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void ReadUnmanaged(this ref SequenceReader reader, out T value) where T : unmanaged
+ {
+ if (!reader.TryReadUnmanaged(out value))
+ {
+ throw new ArgumentOutOfRangeException(nameof(value), "The sequence is not long enough to read the desired value.");
+ }
+ }
+
+ ///
+ /// Sets the reader's position as bytes consumed.
+ ///
+ /// The to set the position
+ /// The number of bytes consumed
+ public static void SetConsumed(ref this SequenceReader reader, long consumed)
+ {
+ reader.Rewind(reader.Consumed);
+ reader.Advance(consumed);
+ }
+
+ ///
+ /// Try to read the given type out of the buffer if possible. Warning: this is dangerous to use with arbitrary
+ /// structs - see remarks for full details.
+ ///
+ /// Type to read
+ ///
+ /// IMPORTANT: The read is a straight copy of bits. If a struct depends on specific state of it's members to
+ /// behave correctly this can lead to exceptions, etc. If reading endian specific integers, use the explicit
+ /// overloads such as
+ ///
+ ///
+ /// True if successful. will be default if failed (due to lack of space).
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static unsafe bool TryReadUnmanaged(ref this SequenceReader reader, out T value) where T : unmanaged
+ {
+ ReadOnlySpan span = reader.UnreadSpan;
+
+ if (span.Length < sizeof(T))
+ {
+ return TryReadUnmanagedMultiSegment(ref reader, out value);
+ }
+
+ value = Unsafe.ReadUnaligned(ref MemoryMarshal.GetReference(span));
+
+ reader.Advance(sizeof(T));
+
+ return true;
+ }
+
+ private static unsafe bool TryReadUnmanagedMultiSegment(ref SequenceReader reader, out T value) where T : unmanaged
+ {
+ Debug.Assert(reader.UnreadSpan.Length < sizeof(T));
+
+ // Not enough data in the current segment, try to peek for the data we need.
+ T buffer = default;
+
+ Span tempSpan = new Span(&buffer, sizeof(T));
+
+ if (!reader.TryCopyTo(tempSpan))
+ {
+ value = default;
+ return false;
+ }
+
+ value = Unsafe.ReadUnaligned(ref MemoryMarshal.GetReference(tempSpan));
+
+ reader.Advance(sizeof(T));
+
+ return true;
+ }
+ }
+}
diff --git a/src/Ryujinx.Common/GraphicsDriver/DriverUtilities.cs b/src/Ryujinx.Common/GraphicsDriver/DriverUtilities.cs
index 7fe2a4f02..a9163f348 100644
--- a/src/Ryujinx.Common/GraphicsDriver/DriverUtilities.cs
+++ b/src/Ryujinx.Common/GraphicsDriver/DriverUtilities.cs
@@ -1,13 +1,33 @@
+using Ryujinx.Common.Utilities;
using System;
namespace Ryujinx.Common.GraphicsDriver
{
public static class DriverUtilities
{
+ private static void AddMesaFlags(string envVar, string newFlags)
+ {
+ string existingFlags = Environment.GetEnvironmentVariable(envVar);
+
+ string flags = existingFlags == null ? newFlags : $"{existingFlags},{newFlags}";
+
+ OsUtils.SetEnvironmentVariableNoCaching(envVar, flags);
+ }
+
+ public static void InitDriverConfig(bool oglThreading)
+ {
+ if (OperatingSystem.IsLinux())
+ {
+ AddMesaFlags("RADV_DEBUG", "nodcc");
+ }
+
+ ToggleOGLThreading(oglThreading);
+ }
+
public static void ToggleOGLThreading(bool enabled)
{
- Environment.SetEnvironmentVariable("mesa_glthread", enabled.ToString().ToLower());
- Environment.SetEnvironmentVariable("__GL_THREADED_OPTIMIZATIONS", enabled ? "1" : "0");
+ OsUtils.SetEnvironmentVariableNoCaching("mesa_glthread", enabled.ToString().ToLower());
+ OsUtils.SetEnvironmentVariableNoCaching("__GL_THREADED_OPTIMIZATIONS", enabled ? "1" : "0");
try
{
diff --git a/src/Ryujinx.Common/Logging/LogClass.cs b/src/Ryujinx.Common/Logging/LogClass.cs
index f277dd06d..1b404a06a 100644
--- a/src/Ryujinx.Common/Logging/LogClass.cs
+++ b/src/Ryujinx.Common/Logging/LogClass.cs
@@ -70,7 +70,7 @@ namespace Ryujinx.Common.Logging
ServiceVi,
SurfaceFlinger,
TamperMachine,
- Ui,
+ UI,
Vic,
}
}
diff --git a/src/Ryujinx.Common/Logging/Logger.cs b/src/Ryujinx.Common/Logging/Logger.cs
index f03a7fd8f..db46739ac 100644
--- a/src/Ryujinx.Common/Logging/Logger.cs
+++ b/src/Ryujinx.Common/Logging/Logger.cs
@@ -3,6 +3,7 @@ using Ryujinx.Common.SystemInterop;
using System;
using System.Collections.Generic;
using System.Diagnostics;
+using System.IO;
using System.Runtime.CompilerServices;
using System.Threading;
@@ -22,6 +23,9 @@ namespace Ryujinx.Common.Logging
public readonly struct Log
{
+ private static readonly string _homeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
+ private static readonly string _homeDirRedacted = Path.Combine(Directory.GetParent(_homeDir).FullName, "[redacted]");
+
internal readonly LogLevel Level;
internal Log(LogLevel level)
@@ -100,7 +104,12 @@ namespace Ryujinx.Common.Logging
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static string FormatMessage(LogClass logClass, string caller, string message) => $"{logClass} {caller}: {message}";
+ private static string FormatMessage(LogClass logClass, string caller, string message)
+ {
+ message = message.Replace(_homeDir, _homeDirRedacted);
+
+ return $"{logClass} {caller}: {message}";
+ }
}
public static Log? Debug { get; private set; }
diff --git a/src/Ryujinx.Common/Logging/Targets/FileLogTarget.cs b/src/Ryujinx.Common/Logging/Targets/FileLogTarget.cs
index 8aa2a26b9..8d4ede96c 100644
--- a/src/Ryujinx.Common/Logging/Targets/FileLogTarget.cs
+++ b/src/Ryujinx.Common/Logging/Targets/FileLogTarget.cs
@@ -13,31 +13,82 @@ namespace Ryujinx.Common.Logging.Targets
string ILogTarget.Name { get => _name; }
- public FileLogTarget(string path, string name)
- : this(path, name, FileShare.Read, FileMode.Append)
- { }
+ public FileLogTarget(string name, FileStream fileStream)
+ {
+ _name = name;
+ _logWriter = new StreamWriter(fileStream);
+ _formatter = new DefaultLogFormatter();
+ }
- public FileLogTarget(string path, string name, FileShare fileShare, FileMode fileMode)
+ public static FileStream PrepareLogFile(string path)
{
// Ensure directory is present
- DirectoryInfo logDir = new(Path.Combine(path, "Logs"));
- logDir.Create();
+ DirectoryInfo logDir;
+ try
+ {
+ logDir = new DirectoryInfo(path);
+ }
+ catch (ArgumentException exception)
+ {
+ Logger.Warning?.Print(LogClass.Application, $"Logging directory path ('{path}') was invalid: {exception}");
+
+ return null;
+ }
+
+ try
+ {
+ logDir.Create();
+ }
+ catch (IOException exception)
+ {
+ Logger.Warning?.Print(LogClass.Application, $"Logging directory could not be created '{logDir}': {exception}");
+
+ return null;
+ }
// Clean up old logs, should only keep 3
FileInfo[] files = logDir.GetFiles("*.log").OrderBy((info => info.CreationTime)).ToArray();
for (int i = 0; i < files.Length - 2; i++)
{
- files[i].Delete();
+ try
+ {
+ files[i].Delete();
+ }
+ catch (UnauthorizedAccessException exception)
+ {
+ Logger.Warning?.Print(LogClass.Application, $"Old log file could not be deleted '{files[i].FullName}': {exception}");
+
+ return null;
+ }
+ catch (IOException exception)
+ {
+ Logger.Warning?.Print(LogClass.Application, $"Old log file could not be deleted '{files[i].FullName}': {exception}");
+
+ return null;
+ }
}
- string version = ReleaseInformation.GetVersion();
+ string version = ReleaseInformation.Version;
// Get path for the current time
path = Path.Combine(logDir.FullName, $"Ryujinx_{version}_{DateTime.Now:yyyy-MM-dd_HH-mm-ss}.log");
- _name = name;
- _logWriter = new StreamWriter(File.Open(path, fileMode, FileAccess.Write, fileShare));
- _formatter = new DefaultLogFormatter();
+ try
+ {
+ return File.Open(path, FileMode.Append, FileAccess.Write, FileShare.Read);
+ }
+ catch (UnauthorizedAccessException exception)
+ {
+ Logger.Warning?.Print(LogClass.Application, $"Log file could not be created '{path}': {exception}");
+
+ return null;
+ }
+ catch (IOException exception)
+ {
+ Logger.Warning?.Print(LogClass.Application, $"Log file could not be created '{path}': {exception}");
+
+ return null;
+ }
}
public void Log(object sender, LogEventArgs args)
diff --git a/src/Ryujinx.Common/Memory/ByteMemoryPool.ByteMemoryPoolBuffer.cs b/src/Ryujinx.Common/Memory/ByteMemoryPool.ByteMemoryPoolBuffer.cs
deleted file mode 100644
index df3f8dc93..000000000
--- a/src/Ryujinx.Common/Memory/ByteMemoryPool.ByteMemoryPoolBuffer.cs
+++ /dev/null
@@ -1,51 +0,0 @@
-using System;
-using System.Buffers;
-using System.Threading;
-
-namespace Ryujinx.Common.Memory
-{
- public sealed partial class ByteMemoryPool
- {
- ///
- /// Represents a that wraps an array rented from
- /// and exposes it as
- /// with a length of the requested size.
- ///
- private sealed class ByteMemoryPoolBuffer : IMemoryOwner
- {
- private byte[] _array;
- private readonly int _length;
-
- public ByteMemoryPoolBuffer(int length)
- {
- _array = ArrayPool.Shared.Rent(length);
- _length = length;
- }
-
- ///
- /// Returns a belonging to this owner.
- ///
- public Memory Memory
- {
- get
- {
- byte[] array = _array;
-
- ObjectDisposedException.ThrowIf(array is null, this);
-
- return new Memory(array, 0, _length);
- }
- }
-
- public void Dispose()
- {
- var array = Interlocked.Exchange(ref _array, null);
-
- if (array != null)
- {
- ArrayPool.Shared.Return(array);
- }
- }
- }
- }
-}
diff --git a/src/Ryujinx.Common/Memory/ByteMemoryPool.cs b/src/Ryujinx.Common/Memory/ByteMemoryPool.cs
deleted file mode 100644
index 071f56b13..000000000
--- a/src/Ryujinx.Common/Memory/ByteMemoryPool.cs
+++ /dev/null
@@ -1,108 +0,0 @@
-using System;
-using System.Buffers;
-
-namespace Ryujinx.Common.Memory
-{
- ///
- /// Provides a pool of re-usable byte array instances.
- ///
- public sealed partial class ByteMemoryPool
- {
- private static readonly ByteMemoryPool _shared = new();
-
- ///
- /// Constructs a instance. Private to force access through
- /// the instance.
- ///
- private ByteMemoryPool()
- {
- // No implementation
- }
-
- ///
- /// Retrieves a shared instance.
- ///
- public static ByteMemoryPool Shared => _shared;
-
- ///
- /// Returns the maximum buffer size supported by this pool.
- ///
- public static int MaxBufferSize => Array.MaxLength;
-
- ///
- /// Rents a byte memory buffer from .
- /// The buffer may contain data from a prior use.
- ///
- /// The buffer's required length in bytes
- /// A wrapping the rented memory
- ///
- public static IMemoryOwner Rent(long length)
- => RentImpl(checked((int)length));
-
- ///
- /// Rents a byte memory buffer from .
- /// The buffer may contain data from a prior use.
- ///
- /// The buffer's required length in bytes
- /// A wrapping the rented memory
- ///
- public static IMemoryOwner Rent(ulong length)
- => RentImpl(checked((int)length));
-
- ///
- /// Rents a byte memory buffer from .
- /// The buffer may contain data from a prior use.
- ///
- /// The buffer's required length in bytes
- /// A wrapping the rented memory
- ///
- public static IMemoryOwner Rent(int length)
- => RentImpl(length);
-
- ///
- /// Rents a byte memory buffer from .
- /// The buffer's contents are cleared (set to all 0s) before returning.
- ///
- /// The buffer's required length in bytes
- /// A wrapping the rented memory
- ///
- public static IMemoryOwner RentCleared(long length)
- => RentCleared(checked((int)length));
-
- ///
- /// Rents a byte memory buffer from .
- /// The buffer's contents are cleared (set to all 0s) before returning.
- ///
- /// The buffer's required length in bytes
- /// A wrapping the rented memory
- ///
- public static IMemoryOwner RentCleared(ulong length)
- => RentCleared(checked((int)length));
-
- ///
- /// Rents a byte memory buffer from .
- /// The buffer's contents are cleared (set to all 0s) before returning.
- ///
- /// The buffer's required length in bytes
- /// A wrapping the rented memory
- ///
- public static IMemoryOwner RentCleared(int length)
- {
- var buffer = RentImpl(length);
-
- buffer.Memory.Span.Clear();
-
- return buffer;
- }
-
- private static ByteMemoryPoolBuffer RentImpl(int length)
- {
- if ((uint)length > Array.MaxLength)
- {
- throw new ArgumentOutOfRangeException(nameof(length), length, null);
- }
-
- return new ByteMemoryPoolBuffer(length);
- }
- }
-}
diff --git a/src/Ryujinx.Common/Memory/MemoryOwner.cs b/src/Ryujinx.Common/Memory/MemoryOwner.cs
new file mode 100644
index 000000000..b7fe1db77
--- /dev/null
+++ b/src/Ryujinx.Common/Memory/MemoryOwner.cs
@@ -0,0 +1,140 @@
+#nullable enable
+using System;
+using System.Buffers;
+using System.Diagnostics.CodeAnalysis;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Threading;
+
+namespace Ryujinx.Common.Memory
+{
+ ///
+ /// An implementation with an embedded length and fast
+ /// accessor, with memory allocated from .
+ ///
+ /// The type of item to store.
+ public sealed class MemoryOwner : IMemoryOwner
+ {
+ private readonly int _length;
+ private T[]? _array;
+
+ ///
+ /// Initializes a new instance of the class with the specified parameters.
+ ///
+ /// The length of the new memory buffer to use
+ private MemoryOwner(int length)
+ {
+ _length = length;
+ _array = ArrayPool.Shared.Rent(length);
+ }
+
+ ///
+ /// Creates a new instance with the specified length.
+ ///
+ /// The length of the new memory buffer to use
+ /// A instance of the requested length
+ /// Thrown when is not valid
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static MemoryOwner Rent(int length) => new(length);
+
+ ///
+ /// Creates a new instance with the specified length and the content cleared.
+ ///
+ /// The length of the new memory buffer to use
+ /// A instance of the requested length and the content cleared
+ /// Thrown when is not valid
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static MemoryOwner RentCleared(int length)
+ {
+ MemoryOwner result = new(length);
+
+ result._array.AsSpan(0, length).Clear();
+
+ return result;
+ }
+
+ ///