diff --git a/COMPILING.md b/COMPILING.md new file mode 100644 index 000000000..20a2eb7ff --- /dev/null +++ b/COMPILING.md @@ -0,0 +1,23 @@ +## Compilation + +Building the project is for users that want to contribute code only. +If you wish to build the emulator yourself, follow these steps: + +### Step 1 + +Install the [.NET 8.0 (or higher) SDK](https://dotnet.microsoft.com/download/dotnet/8.0). +Make sure your SDK version is higher or equal to the required version specified in [global.json](global.json). + +### Step 2 + +Either use `git clone https://github.com/GreemDev/Ryujinx` on the command line to clone the repository or use Code --> Download zip button to get the files. + +### Step 3 + +To build Ryujinx, open a command prompt inside the project directory. +You can quickly access it on Windows by holding shift in File Explorer, then right clicking and selecting `Open command window here`. +Then type the following command: `dotnet build -c Release -o build` +the built files will be found in the newly created build directory. + +Ryujinx system files are stored in the `Ryujinx` folder. +This folder is located in the user folder, which can be accessed by clicking `Open Ryujinx Folder` under the File menu in the GUI. diff --git a/Directory.Packages.props b/Directory.Packages.props index 40275763b..ffb5f2ead 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -3,50 +3,51 @@ true - - - - - - - + + + + + + + + + + - + - - - - - + + + - - - + + + + - + - - - - + + + + + - - - - + + + + - - - - - - - - + + + + + + diff --git a/README.md b/README.md index b934c4ec0..6ecfd90e7 100644 --- a/README.md +++ b/README.md @@ -10,13 +10,13 @@

MeloNX is an iOS Nintendo Switch emulator based on Ryujinx, written primarily in C#. Designed to bring accurate performance and a user-friendly interface to iOS, MeloNX makes Switch games accessible on Apple devices. - Developed from the ground up, MeloNX is open-source and available on Github under the MIT license.
MIT license.

## Compatibility -MeloNX works on iPhone X and later and iPad 7th Gen and later. A lot of games work. +As of October 2024, MeloNX can only play the audio of games, As of November a lot of games run and the memory hsage is messed up. ## Usage -To run MeloNX on your iOS device, at least 4GB of RAM is recommended to ensure stability. For full instructions, refer to our [Setup Guide](https://github.com/MeloNX-Emu/MeloNX/wiki/Setup-Guide). +To run MeloNX on your iOS device, at least 8GB of RAM is recommended to ensure stability. For full instructions, refer to our [Setup Guide](https://github.com/MeloNX-Emu/MeloNX/wiki/Setup-Guide). diff --git a/Ryujinx.sln b/Ryujinx.sln index bb196cabc..d661b903c 100644 --- a/Ryujinx.sln +++ b/Ryujinx.sln @@ -3,8 +3,6 @@ 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}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Tests", "src\Ryujinx.Tests\Ryujinx.Tests.csproj", "{EBB55AEA-C7D7-4DEB-BF96-FA1789E225E9}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Tests.Unicorn", "src\Ryujinx.Tests.Unicorn\Ryujinx.Tests.Unicorn.csproj", "{D8F72938-78EF-4E8C-BAFE-531C9C3C8F15}" @@ -31,12 +29,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Graphics.Nvdec", "s EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Audio", "src\Ryujinx.Audio\Ryujinx.Audio.csproj", "{806ACF6D-90B0-45D0-A1AC-5F220F3B3985}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{36F870C1-3E5F-485F-B426-F0645AF78751}" - ProjectSection(SolutionItems) = preProject - .editorconfig = .editorconfig - Directory.Packages.props = Directory.Packages.props - EndProjectSection -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Memory", "src\Ryujinx.Memory\Ryujinx.Memory.csproj", "{A5E6C691-9E22-4263-8F40-42F002CE66BE}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Tests.Memory", "src\Ryujinx.Tests.Memory\Ryujinx.Tests.Memory.csproj", "{D1CC5322-7325-4F6B-9625-194B30BE1296}" @@ -69,9 +61,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 +71,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,16 +79,23 @@ 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 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{36F870C1-3E5F-485F-B426-F0645AF78751}" + ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig + Directory.Packages.props = Directory.Packages.props + .github/workflows/release.yml = .github/workflows/release.yml + .github/workflows/canary.yml = .github/workflows/canary.yml + .github/workflows/build.yml = .github/workflows/build.yml + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {074045D4-3ED2-4711-9169-E385F2BFB5A0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {074045D4-3ED2-4711-9169-E385F2BFB5A0}.Debug|Any CPU.Build.0 = Debug|Any CPU - {074045D4-3ED2-4711-9169-E385F2BFB5A0}.Release|Any CPU.ActiveCfg = Release|Any CPU - {074045D4-3ED2-4711-9169-E385F2BFB5A0}.Release|Any CPU.Build.0 = Release|Any CPU {EBB55AEA-C7D7-4DEB-BF96-FA1789E225E9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {EBB55AEA-C7D7-4DEB-BF96-FA1789E225E9}.Debug|Any CPU.Build.0 = Debug|Any CPU {EBB55AEA-C7D7-4DEB-BF96-FA1789E225E9}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -249,6 +248,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..018aa1331 100644 --- a/Ryujinx.sln.DotSettings +++ b/Ryujinx.sln.DotSettings @@ -3,7 +3,13 @@ WARNING UseExplicitType UseExplicitType - <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="I" Suffix="" Style="AaBb" /></Policy> + GL + SDL + OS + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="I" Suffix="" Style="AaBb" /></Policy> + <Policy><Descriptor Staticness="Any" AccessRightKinds="Any" Description="Types and namespaces"><ElementKinds><Kind Name="NAMESPACE" /><Kind Name="CLASS" /><Kind Name="STRUCT" /><Kind Name="ENUM" /><Kind Name="DELEGATE" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="I" Suffix="" Style="AaBb" /></Policy></Policy> + True + True True True True @@ -18,4 +24,4 @@ True True True - \ No newline at end of file + diff --git a/assets/amiibo/Amiibo.json b/assets/amiibo/Amiibo.json new file mode 100644 index 000000000..03c2c020e --- /dev/null +++ b/assets/amiibo/Amiibo.json @@ -0,0 +1,48348 @@ +{ + "amiibo": [ + { + "amiiboSeries": "Animal Crossing", + "character": "Sandy", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "04380001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_04380001-03000502.png", + "name": "Sandy", + "release": { + "au": "2016-11-10", + "eu": "2016-11-11", + "jp": "2016-11-03", + "na": "2016-12-02" + }, + "tail": "03000502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Isabelle", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "01810101", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01810101-00b40502.png", + "name": "Isabelle - Winter", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-10-29", + "na": "2016-01-22" + }, + "tail": "00b40502", + "type": "Card" + }, + { + "amiiboSeries": "Super Smash Bros.", + "character": "Sonic", + "gameSeries": "Sonic", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "32000000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_32000000-00300002.png", + "name": "Sonic", + "release": { + "au": "2015-01-29", + "eu": "2015-02-20", + "jp": "2015-01-22", + "na": "2015-02-01" + }, + "tail": "00300002", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Ava", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "029e0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_029e0001-013d0502.png", + "name": "Ava", + "release": { + "au": "2016-03-19", + "eu": "2016-03-18", + "jp": "2016-01-14", + "na": "2016-03-18" + }, + "tail": "013d0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Blanca", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "01b30001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01b30001-00b50502.png", + "name": "Blanca", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-10-29", + "na": "2016-01-22" + }, + "tail": "00b50502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Mac", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "02f80001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_02f80001-01380502.png", + "name": "Mac", + "release": { + "au": "2016-03-19", + "eu": "2016-03-18", + "jp": "2016-01-14", + "na": "2016-03-18" + }, + "tail": "01380502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Lucha", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "023c0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_023c0001-00bd0502.png", + "name": "Lucha", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-10-29", + "na": "2016-01-22" + }, + "tail": "00bd0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Punchy", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "02630001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_02630001-00750502.png", + "name": "Punchy", + "release": { + "au": "2015-10-03", + "eu": "2015-10-02", + "jp": "2015-07-30", + "na": "2015-09-25" + }, + "tail": "00750502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Violet", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "03700001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_03700001-015d0502.png", + "name": "Violet", + "release": { + "au": "2016-03-19", + "eu": "2016-03-18", + "jp": "2016-01-14", + "na": "2016-03-18" + }, + "tail": "015d0502", + "type": "Card" + }, + { + "amiiboSeries": "Super Smash Bros.", + "character": "Mii", + "gameSeries": "Mii", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "07c00000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_07c00000-00210002.png", + "name": "Mii Brawler", + "release": { + "au": "2015-09-26", + "eu": "2015-09-25", + "jp": "2015-09-10", + "na": "2015-11-01" + }, + "tail": "00210002", + "type": "Figure" + }, + { + "amiiboSeries": "Mario Sports Superstars", + "character": "Wario", + "gameSeries": "Mario Sports Superstars", + "gamesSwitch": [], + "head": "09c50201", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_09c50201-02830e02.png", + "name": "Wario - Baseball", + "release": { + "au": "2017-03-11", + "eu": "2017-03-10", + "jp": "2017-03-30", + "na": "2017-03-24" + }, + "tail": "02830e02", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Tom", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "026c0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_026c0001-00c30502.png", + "name": "Tom", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-10-29", + "na": "2016-01-22" + }, + "tail": "00c30502", + "type": "Card" + }, + { + "amiiboSeries": "Legend Of Zelda", + "character": "Zelda", + "gameSeries": "The Legend of Zelda", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive specific crafting materials or a weapon for the character", + "write": false + } + ], + "gameID": [ + "01002B00111A2000" + ], + "gameName": "Hyrule Warriors: Age of Calamity" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a weapon rated 3 stars or higher", + "write": false + } + ], + "gameID": [ + "0100AE00096EA000" + ], + "gameName": "Hyrule Warriors: Definitive Edition" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a Legend of Zelda-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01003DA010E8A000" + ], + "gameName": "Miitopia" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a chest of loot, potentially containing gear inspired by the Legend of Zelda series", + "write": false + } + ], + "gameID": [ + "01000A10041EA000" + ], + "gameName": "The Elder Scrolls V: Skyrim" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock one of five amiibo-exclusive Chamber Dungeon chambers", + "write": false + }, + { + "Usage": "Save your Chamber Dungeon to the amiibo to share with friends", + "write": true + } + ], + "gameID": [ + "01006BB00C6F0000" + ], + "gameName": "The Legend of Zelda: Link's Awakening" + }, + { + "amiiboUsage": [ + { + "Usage": "Quickly travel between the surface and the sky", + "write": false + } + ], + "gameID": [ + "01002DA013484000" + ], + "gameName": "The Legend of Zelda: Skyward Sword HD" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an amiibo-exclusive character-specific paraglider fabric", + "write": false + }, + { + "Usage": "Receive materials and a weapon or rare item", + "write": false + } + ], + "gameID": [ + "0100F2C0115B6000" + ], + "gameName": "The Legend of Zelda: Tears of the Kingdom" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive the Blue Attire", + "write": false + }, + { + "Usage": "Receive random materials", + "write": false + } + ], + "gameID": [ + "01008CF01BAAC000" + ], + "gameName": "The Legend of Zelda: Echoes of Wisdom" + } + ], + "head": "01010300", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01010300-04140902.png", + "name": "Zelda & Loftwing", + "release": { + "au": "2021-07-16", + "eu": "2021-07-16", + "jp": "2021-07-16", + "na": "2021-07-16" + }, + "tail": "04140902", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Mint", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "04e60001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_04e60001-00820502.png", + "name": "Mint", + "release": { + "au": "2015-10-03", + "eu": "2015-10-02", + "jp": "2015-07-30", + "na": "2015-09-25" + }, + "tail": "00820502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Caroline", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "04e30001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_04e30001-01650502.png", + "name": "Caroline", + "release": { + "au": "2016-03-19", + "eu": "2016-03-18", + "jp": "2016-01-14", + "na": "2016-03-18" + }, + "tail": "01650502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Mabel", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "01880001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01880001-01120502.png", + "name": "Mabel", + "release": { + "au": "2016-03-19", + "eu": "2016-03-18", + "jp": "2016-01-14", + "na": "2016-03-18" + }, + "tail": "01120502", + "type": "Card" + }, + { + "amiiboSeries": "Splatoon", + "character": "Inkling", + "gameSeries": "Splatoon", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a Splatoon-themed racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock exclusive gear / Save favorite weapons, gear, and control settings / Take photos with the character or saved gear", + "write": true + } + ], + "gameID": [ + "01003BC0000A0000" + ], + "gameName": "Splatoon 2" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock exclusive gear / Save favorite weapons, gear, and control settings / Take photos with the character or saved gear", + "write": true + } + ], + "gameID": [ + "0100C2500FC20000" + ], + "gameName": "Splatoon 3" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "08000100", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_08000100-04150402.png", + "name": "Inkling - Yellow", + "release": { + "au": "2022-11-11", + "eu": "2022-11-11", + "jp": "2022-11-11", + "na": "2022-11-11" + }, + "tail": "04150402", + "type": "Figure" + }, + { + "amiiboSeries": "Splatoon", + "character": "Shiver", + "gameSeries": "Splatoon", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a Splatoon-themed racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock exclusive gear / Save favorite weapons, gear, and control settings / Take photos with the character or saved gear", + "write": true + } + ], + "gameID": [ + "0100C2500FC20000" + ], + "gameName": "Splatoon 3" + } + ], + "head": "08070000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_08070000-04330402.png", + "name": "Shiver", + "release": { + "au": "2023-11-17", + "eu": "2023-11-17", + "jp": "2023-11-17", + "na": "2023-11-17" + }, + "tail": "04330402", + "type": "Figure" + }, + { + "amiiboSeries": "Splatoon", + "character": "Frye", + "gameSeries": "Splatoon", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a Splatoon-themed racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock exclusive gear / Save favorite weapons, gear, and control settings / Take photos with the character or saved gear", + "write": true + } + ], + "gameID": [ + "0100C2500FC20000" + ], + "gameName": "Splatoon 3" + } + ], + "head": "08080000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_08080000-04340402.png", + "name": "Frye", + "release": { + "au": "2023-11-17", + "eu": "2023-11-17", + "jp": "2023-11-17", + "na": "2023-11-17" + }, + "tail": "04340402", + "type": "Figure" + }, + { + "amiiboSeries": "Splatoon", + "character": "Big Man", + "gameSeries": "Splatoon", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a Splatoon-themed racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock exclusive gear / Save favorite weapons, gear, and control settings / Take photos with the character or saved gear", + "write": true + } + ], + "gameID": [ + "0100C2500FC20000" + ], + "gameName": "Splatoon 3" + } + ], + "head": "08090000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_08090000-04350402.png", + "name": "Big Man", + "release": { + "au": "2023-11-17", + "eu": "2023-11-17", + "jp": "2023-11-17", + "na": "2023-11-17" + }, + "tail": "04350402", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Frett", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "0a1d0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_0a1d0001-03d40502.png", + "name": "Frett", + "release": { + "au": "2021-11-05", + "eu": "2021-11-05", + "jp": "2021-11-05", + "na": "2021-11-05" + }, + "tail": "03d40502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Kidd", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "035d0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_035d0001-00c90502.png", + "name": "Kidd", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-10-29", + "na": "2016-01-22" + }, + "tail": "00c90502", + "type": "Card" + }, + { + "amiiboSeries": "BoxBoy!", + "character": "Qbby", + "gameSeries": "BoxBoy!", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Receive star coins and a boost item", + "write": false + } + ], + "gameID": [ + "01004D300C5AE000" + ], + "gameName": "Kirby and the Forgotten Land" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive two Picture Pieces, a Maxim Tomato, and two Point Stars", + "write": false + } + ], + "gameID": [ + "01007E3006DDA000" + ], + "gameName": "Kirby Star Allies" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive more useful items", + "write": false + } + ], + "gameID": [ + "01006B601380E000" + ], + "gameName": "Kirby's Return to Dream Land Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive 20 Fragments", + "write": false + } + ], + "gameID": [ + "01003FB00C5A8000" + ], + "gameName": "Super Kirby Clash" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a Spirit of the character", + "write": false + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "1f400000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_1f400000-035e1002.png", + "name": "Qbby", + "release": { + "au": null, + "eu": null, + "jp": "2017-02-02", + "na": null + }, + "tail": "035e1002", + "type": "Figure" + }, + { + "amiiboSeries": "Mario Sports Superstars", + "character": "Waluigi", + "gameSeries": "Mario Sports Superstars", + "gamesSwitch": [], + "head": "09c60101", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_09c60101-02870e02.png", + "name": "Waluigi - Soccer", + "release": { + "au": "2017-03-11", + "eu": "2017-03-10", + "jp": "2017-03-30", + "na": "2017-03-24" + }, + "tail": "02870e02", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Purrl", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "02640001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_02640001-01ac0502.png", + "name": "Purrl", + "release": { + "au": "2016-06-18", + "eu": null, + "jp": "2016-03-24", + "na": "2016-06-10" + }, + "tail": "01ac0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Mitzi", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "025e0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_025e0001-01250502.png", + "name": "Mitzi", + "release": { + "au": "2016-03-19", + "eu": "2016-03-18", + "jp": "2016-01-14", + "na": "2016-03-18" + }, + "tail": "01250502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Reneigh", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "0a100001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_0a100001-03c70502.png", + "name": "Reneigh", + "release": { + "au": "2021-11-05", + "eu": "2021-11-05", + "jp": "2021-11-05", + "na": "2021-11-05" + }, + "tail": "03c70502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Rasher", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "047a0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_047a0001-00600502.png", + "name": "Rasher", + "release": { + "au": "2015-10-03", + "eu": "2015-10-02", + "jp": "2015-07-30", + "na": "2015-09-25" + }, + "tail": "00600502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Chrissy", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "04a10001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_04a10001-016f0502.png", + "name": "Chrissy", + "release": { + "au": "2016-03-19", + "eu": "2016-03-18", + "jp": "2016-01-14", + "na": "2016-03-18" + }, + "tail": "016f0502", + "type": "Card" + }, + { + "amiiboSeries": "Mario Sports Superstars", + "character": "Metal Mario", + "gameSeries": "Mario Sports Superstars", + "gamesSwitch": [], + "head": "09d00301", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_09d00301-02bb0e02.png", + "name": "Metal Mario - Tennis", + "release": { + "au": "2017-03-11", + "eu": "2017-03-10", + "jp": "2017-03-30", + "na": "2017-03-24" + }, + "tail": "02bb0e02", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Harriet", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "01910001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01910001-004e0502.png", + "name": "Harriet", + "release": { + "au": "2015-10-03", + "eu": "2015-10-02", + "jp": "2015-07-30", + "na": "2015-09-25" + }, + "tail": "004e0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Daisy", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "02f10001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_02f10001-01450502.png", + "name": "Daisy", + "release": { + "au": "2016-03-19", + "eu": "2016-03-18", + "jp": "2016-01-14", + "na": "2016-03-18" + }, + "tail": "01450502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Bam", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "02d70001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_02d70001-01300502.png", + "name": "Bam", + "release": { + "au": "2016-03-19", + "eu": "2016-03-18", + "jp": "2016-01-14", + "na": "2016-03-18" + }, + "tail": "01300502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Anabelle", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "02030001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_02030001-019a0502.png", + "name": "Anabelle", + "release": { + "au": "2016-06-18", + "eu": null, + "jp": "2016-03-24", + "na": "2016-06-10" + }, + "tail": "019a0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Label", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "01890001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01890001-00ab0502.png", + "name": "Labelle", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-10-29", + "na": "2016-01-22" + }, + "tail": "00ab0502", + "type": "Card" + }, + { + "amiiboSeries": "Yu-Gi-Oh!", + "character": "Tatsuhisa \u201cLuke\u201d Kamij\u014d", + "gameSeries": "Yu-Gi-Oh!", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Receive items/bonuses", + "write": false + } + ], + "gameID": [ + "01003C101454A000" + ], + "gameName": "Yu-Gi-Oh! Rush Duel Saikyo Battle Royale" + } + ], + "head": "38410001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_38410001-04251902.png", + "name": "Tatsuhisa \u201cLuke\u201d Kamij\u014d", + "release": { + "au": null, + "eu": null, + "jp": null, + "na": "2021-08-12" + }, + "tail": "04251902", + "type": "Card" + }, + { + "amiiboSeries": "Splatoon", + "character": "Smallfry", + "gameSeries": "Splatoon", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a Splatoon-themed racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock exclusive gear / Save favorite weapons, gear, and control settings / Take photos with the character or saved gear", + "write": true + } + ], + "gameID": [ + "0100C2500FC20000" + ], + "gameName": "Splatoon 3" + } + ], + "head": "08060100", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_08060100-041c0402.png", + "name": "Smallfry", + "release": { + "au": "2022-11-11", + "eu": "2022-11-11", + "jp": "2022-11-11", + "na": "2022-11-11" + }, + "tail": "041c0402", + "type": "Figure" + }, + { + "amiiboSeries": "Super Smash Bros.", + "character": "Inkling", + "gameSeries": "Splatoon", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock a Splatoon-themed racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01003DA010E8A000" + ], + "gameName": "Miitopia" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock exclusive gear / Save favorite weapons, gear, and control settings / Take photos with the character or saved gear", + "write": true + } + ], + "gameID": [ + "01003BC0000A0000" + ], + "gameName": "Splatoon 2" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock exclusive gear / Save favorite weapons, gear, and control settings / Take photos with the character or saved gear", + "write": true + } + ], + "gameID": [ + "0100C2500FC20000" + ], + "gameName": "Splatoon 3" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "08000100", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_08000100-03820002.png", + "name": "Inkling", + "release": { + "au": "2018-12-07", + "eu": "2018-12-07", + "jp": "2018-12-07", + "na": "2018-12-07" + }, + "tail": "03820002", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Rover", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "018d0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_018d0001-010c0502.png", + "name": "Rover", + "release": { + "au": "2016-03-19", + "eu": "2016-03-18", + "jp": "2016-01-14", + "na": "2016-03-18" + }, + "tail": "010c0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Wendell", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "01a70001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01a70001-01140502.png", + "name": "Wendell", + "release": { + "au": "2016-03-19", + "eu": "2016-03-18", + "jp": "2016-01-14", + "na": "2016-03-18" + }, + "tail": "01140502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Ren\u00e9e", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "04ba0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_04ba0001-005d0502.png", + "name": "Ren\u00e9e", + "release": { + "au": "2015-10-03", + "eu": "2015-10-02", + "jp": "2015-07-30", + "na": "2015-09-25" + }, + "tail": "005d0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Agnes", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "04890001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_04890001-00ef0502.png", + "name": "Agnes", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-10-29", + "na": "2016-01-22" + }, + "tail": "00ef0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Resetti", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "018e0101", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_018e0101-01780502.png", + "name": "Resetti - Without Hat", + "release": { + "au": "2016-06-18", + "eu": null, + "jp": "2016-03-24", + "na": "2016-06-10" + }, + "tail": "01780502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Daisy Mae", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "0a040001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_0a040001-03b50502.png", + "name": "Daisy Mae", + "release": { + "au": "2021-11-05", + "eu": "2021-11-05", + "jp": "2021-11-05", + "na": "2021-11-05" + }, + "tail": "03b50502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Merry", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "026d0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_026d0001-013f0502.png", + "name": "Merry", + "release": { + "au": "2016-03-19", + "eu": "2016-03-18", + "jp": "2016-01-14", + "na": "2016-03-18" + }, + "tail": "013f0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Big Top", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "03250001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_03250001-010a0502.png", + "name": "Big Top", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-10-29", + "na": "2016-01-22" + }, + "tail": "010a0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Leif", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "01b40001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01b40001-01130502.png", + "name": "Leif", + "release": { + "au": "2016-03-19", + "eu": "2016-03-18", + "jp": "2016-01-14", + "na": "2016-03-18" + }, + "tail": "01130502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Rocco", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "03900001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_03900001-01850502.png", + "name": "Rocco", + "release": { + "au": "2016-06-18", + "eu": null, + "jp": "2016-03-24", + "na": "2016-06-10" + }, + "tail": "01850502", + "type": "Card" + }, + { + "amiiboSeries": "Mario Sports Superstars", + "character": "Donkey Kong", + "gameSeries": "Mario Sports Superstars", + "gamesSwitch": [], + "head": "09c70501", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_09c70501-02900e02.png", + "name": "Donkey Kong - Horse Racing", + "release": { + "au": "2017-03-11", + "eu": "2017-03-10", + "jp": "2017-03-30", + "na": "2017-03-24" + }, + "tail": "02900e02", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Gladys", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "04370001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_04370001-01050502.png", + "name": "Gladys", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-10-29", + "na": "2016-01-22" + }, + "tail": "01050502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Twiggy", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "02300001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_02300001-01d20502.png", + "name": "Twiggy", + "release": { + "au": "2016-06-18", + "eu": null, + "jp": "2016-03-24", + "na": "2016-06-10" + }, + "tail": "01d20502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Camofrog", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "033b0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_033b0001-00fa0502.png", + "name": "Camofrog", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-10-29", + "na": "2016-01-22" + }, + "tail": "00fa0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Lottie", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a Spirit of the character", + "write": false + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "01c10000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01c10000-02440502.png", + "name": "Lottie", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-11-21", + "na": "2015-11-22" + }, + "tail": "02440502", + "type": "Figure" + }, + { + "amiiboSeries": "Super Smash Bros.", + "character": "King K. Rool", + "gameSeries": "Donkey Kong", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "00c00000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_00c00000-037b0002.png", + "name": "King K. Rool", + "release": { + "au": "2019-02-15", + "eu": "2019-02-15", + "jp": "2019-02-15", + "na": "2019-02-15" + }, + "tail": "037b0002", + "type": "Figure" + }, + { + "amiiboSeries": "Yu-Gi-Oh!", + "character": "Nail Saionji", + "gameSeries": "Yu-Gi-Oh!", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Receive items/bonuses", + "write": false + } + ], + "gameID": [ + "01003C101454A000" + ], + "gameName": "Yu-Gi-Oh! Rush Duel Saikyo Battle Royale" + } + ], + "head": "38450001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_38450001-04291902.png", + "name": "Nail Saionji", + "release": { + "au": null, + "eu": null, + "jp": null, + "na": "2021-08-12" + }, + "tail": "04291902", + "type": "Card" + }, + { + "amiiboSeries": "Super Smash Bros.", + "character": "Sephiroth", + "gameSeries": "Final Fantasy", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "36010000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_36010000-04210002.png", + "name": "Sephiroth", + "release": { + "au": "2023-01-13", + "eu": "2023-01-13", + "jp": "2023-01-13", + "na": "2023-01-13" + }, + "tail": "04210002", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Deirdre", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "02da0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_02da0001-01330502.png", + "name": "Deirdre", + "release": { + "au": "2016-03-19", + "eu": "2016-03-18", + "jp": "2016-01-14", + "na": "2016-03-18" + }, + "tail": "01330502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Flick", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "0a030001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_0a030001-03b40502.png", + "name": "Flick", + "release": { + "au": "2021-11-05", + "eu": "2021-11-05", + "jp": "2021-11-05", + "na": "2021-11-05" + }, + "tail": "03b40502", + "type": "Card" + }, + { + "amiiboSeries": "Super Smash Bros.", + "character": "Lucina", + "gameSeries": "Fire Emblem", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Receive a Fashion Ticket and a Music Ticket, for unlocking any of the available costumes and music tracks", + "write": false + } + ], + "gameID": [ + "0100A6301214E000" + ], + "gameName": "Fire Emblem Engage" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a weapon", + "write": false + } + ], + "gameID": [ + "0100F15003E64000" + ], + "gameName": "Fire Emblem Warriors" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive better-quality randomized resources, weapons, or equipment", + "write": false + } + ], + "gameID": [ + "010071F0143EA000" + ], + "gameName": "Fire Emblem Warriors: Three Hopes" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special piece of battle music / Receive higher-quality items and materials", + "write": false + } + ], + "gameID": [ + "010055D009F78000" + ], + "gameName": "Fire Emblem: Three Houses" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "21020000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_21020000-00290002.png", + "name": "Lucina", + "release": { + "au": "2015-04-25", + "eu": "2015-04-24", + "jp": "2015-04-29", + "na": "2015-05-29" + }, + "tail": "00290002", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Naomi", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "02b80001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_02b80001-019c0502.png", + "name": "Naomi", + "release": { + "au": "2016-06-18", + "eu": null, + "jp": "2016-03-24", + "na": "2016-06-10" + }, + "tail": "019c0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Raddle", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "03470001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_03470001-03020502.png", + "name": "Raddle", + "release": { + "au": "2016-11-10", + "eu": "2016-11-11", + "jp": "2016-11-03", + "na": "2016-12-02" + }, + "tail": "03020502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Tortimer", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "01b00001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01b00001-00520502.png", + "name": "Tortimer", + "release": { + "au": "2015-10-03", + "eu": "2015-10-02", + "jp": "2015-07-30", + "na": "2015-09-25" + }, + "tail": "00520502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Digby", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a Spirit of the character", + "write": false + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "018c0000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_018c0000-02430502.png", + "name": "Digby", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-11-21", + "na": "2015-11-13" + }, + "tail": "02430502", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Puddles", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "033e0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_033e0001-01a20502.png", + "name": "Puddles", + "release": { + "au": "2016-06-18", + "eu": null, + "jp": "2016-03-24", + "na": "2016-06-10" + }, + "tail": "01a20502", + "type": "Card" + }, + { + "amiiboSeries": "Splatoon", + "character": "Octoling", + "gameSeries": "Splatoon", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a Splatoon-themed racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock exclusive gear / Save favorite weapons, gear, and control settings / Take photos with the character or saved gear", + "write": true + } + ], + "gameID": [ + "01003BC0000A0000" + ], + "gameName": "Splatoon 2" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock exclusive gear / Save favorite weapons, gear, and control settings / Take photos with the character or saved gear", + "write": true + } + ], + "gameID": [ + "0100C2500FC20000" + ], + "gameName": "Splatoon 3" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a Spirit of the character", + "write": false + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "08050200", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_08050200-038f0402.png", + "name": "Octoling Boy", + "release": { + "au": "2018-11-11", + "eu": "2018-11-09", + "jp": "2018-11-09", + "na": "2018-11-09" + }, + "tail": "038f0402", + "type": "Figure" + }, + { + "amiiboSeries": "Legend Of Zelda", + "character": "Guardian", + "gameSeries": "The Legend of Zelda", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive specific crafting materials or a weapon for the character", + "write": false + } + ], + "gameID": [ + "01002B00111A2000" + ], + "gameName": "Hyrule Warriors: Age of Calamity" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a Legend of Zelda-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a Spirit of the character", + "write": false + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a chest of loot, potentially containing gear inspired by the Legend of Zelda series", + "write": false + } + ], + "gameID": [ + "01000A10041EA000" + ], + "gameName": "The Elder Scrolls V: Skyrim" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive materials and a rare or exclusive item", + "write": false + } + ], + "gameID": [ + "01007EF00011E000" + ], + "gameName": "The Legend of Zelda: Breath of the Wild" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock one of five amiibo-exclusive Chamber Dungeon chambers", + "write": false + }, + { + "Usage": "Save your Chamber Dungeon to the amiibo to share with friends", + "write": true + } + ], + "gameID": [ + "01006BB00C6F0000" + ], + "gameName": "The Legend of Zelda: Link's Awakening" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an amiibo-exclusive character-specific paraglider fabric", + "write": false + }, + { + "Usage": "Receive materials and a weapon or rare item", + "write": false + } + ], + "gameID": [ + "0100F2C0115B6000" + ], + "gameName": "The Legend of Zelda: Tears of the Kingdom" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive the Black Cat Clothes", + "write": false + }, + { + "Usage": "Receive random materials", + "write": false + } + ], + "gameID": [ + "01008CF01BAAC000" + ], + "gameName": "The Legend of Zelda: Echoes of Wisdom" + } + ], + "head": "01400000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01400000-03550902.png", + "name": "Guardian", + "release": { + "au": "2017-03-03", + "eu": "2017-03-03", + "jp": "2017-03-03", + "na": "2017-03-03" + }, + "tail": "03550902", + "type": "Figure" + }, + { + "amiiboSeries": "Super Smash Bros.", + "character": "Squirtle", + "gameSeries": "Pokemon", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "19070000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_19070000-03840002.png", + "name": "Squirtle", + "release": { + "au": "2019-09-20", + "eu": "2019-09-20", + "jp": "2019-09-20", + "na": "2019-09-20" + }, + "tail": "03840002", + "type": "Figure" + }, + { + "amiiboSeries": "Super Nintendo World", + "character": "Peach", + "gameSeries": "Super Mario", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock a costume based on the character (short-hair version)", + "write": false + } + ], + "gameID": [ + "01007960049A0000" + ], + "gameName": "Bayonetta 2" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an amiibo-exclusive weapon for the character and their Rabbid counterpart", + "write": false + } + ], + "gameID": [ + "010067300059A000" + ], + "gameName": "Mario + Rabbids: Kingdom Battle" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01003DA010E8A000" + ], + "gameName": "Miitopia" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a particular power-up depending on the character", + "write": false + } + ], + "gameID": [ + "010028600EBDA000" + ], + "gameName": "Super Mario 3D World + Bowser's Fury" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a life-up heart", + "write": false + } + ], + "gameID": [ + "0100000000010000" + ], + "gameName": "Super Mario Odyssey" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock the character's shiny sticker", + "write": false + } + ], + "gameID": [ + "010036B0034E4000" + ], + "gameName": "Super Mario Party" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01006000040C2000" + ], + "gameName": "Yoshi's Crafted World" + } + ], + "head": "00020003", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_00020003-039dff02.png", + "name": "Peach - Power Up Band", + "release": { + "au": null, + "eu": null, + "jp": "2021-02-04", + "na": null + }, + "tail": "039dff02", + "type": "Band" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Jacob", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "02380001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_02380001-02f80502.png", + "name": "Jacob", + "release": { + "au": "2016-11-10", + "eu": "2016-11-11", + "jp": "2016-11-03", + "na": "2016-12-02" + }, + "tail": "02f80502", + "type": "Card" + }, + { + "amiiboSeries": "Super Mario Bros.", + "character": "Boo", + "gameSeries": "Super Mario", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock the Chain Chomp weapon", + "write": false + } + ], + "gameID": [ + "01007960049A0000" + ], + "gameName": "Bayonetta 2" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a particular power-up depending on the character", + "write": false + } + ], + "gameID": [ + "010028600EBDA000" + ], + "gameName": "Super Mario 3D World + Bowser's Fury" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock the character's shiny sticker", + "write": false + } + ], + "gameID": [ + "010036B0034E4000" + ], + "gameName": "Super Mario Party" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a Spirit of the character", + "write": false + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "00170000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_00170000-02680102.png", + "name": "Boo", + "release": { + "au": "2016-10-08", + "eu": "2016-10-07", + "jp": "2016-10-20", + "na": "2016-11-04" + }, + "tail": "02680102", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Pashmina", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "035e0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_035e0001-018e0502.png", + "name": "Pashmina", + "release": { + "au": "2016-06-18", + "eu": null, + "jp": "2016-03-24", + "na": "2016-06-10" + }, + "tail": "018e0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Paolo", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "03280001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_03280001-02eb0502.png", + "name": "Paolo", + "release": { + "au": "2016-11-10", + "eu": "2016-11-11", + "jp": "2016-11-03", + "na": "2016-12-02" + }, + "tail": "02eb0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "June", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "028a0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_028a0001-02e90502.png", + "name": "June", + "release": { + "au": "2016-11-10", + "eu": "2016-11-11", + "jp": "2016-11-03", + "na": "2016-12-02" + }, + "tail": "02e90502", + "type": "Card" + }, + { + "amiiboSeries": "Others", + "character": "Mario Cereal", + "gameSeries": "Kellogs", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "See the location of a Power Moon (as \"delicious amiibo\")", + "write": false + } + ], + "gameID": [ + "0100000000010000" + ], + "gameName": "Super Mario Odyssey" + } + ], + "head": "37400001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_37400001-03741402.png", + "name": "Super Mario Cereal", + "release": { + "au": null, + "eu": null, + "jp": null, + "na": "2017-12-11" + }, + "tail": "03741402", + "type": "Card" + }, + { + "amiiboSeries": "Super Smash Bros.", + "character": "Link", + "gameSeries": "The Legend of Zelda", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock a costume based on the character (short-hair version)", + "write": false + } + ], + "gameID": [ + "01007960049A0000" + ], + "gameName": "Bayonetta 2" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive specific crafting materials or a weapon for the character", + "write": false + } + ], + "gameID": [ + "01002B00111A2000" + ], + "gameName": "Hyrule Warriors: Age of Calamity" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a weapon rated 3 stars or higher", + "write": false + } + ], + "gameID": [ + "0100AE00096EA000" + ], + "gameName": "Hyrule Warriors: Definitive Edition" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a Legend of Zelda-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01003DA010E8A000" + ], + "gameName": "Miitopia" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a chest of loot, potentially containing gear inspired by the Legend of Zelda series", + "write": false + } + ], + "gameID": [ + "01000A10041EA000" + ], + "gameName": "The Elder Scrolls V: Skyrim" + }, + { + "amiiboUsage": [ + { + "Usage": "Bring Epona into the game as a rideable horse", + "write": false + }, + { + "Usage": "Receive materials and a rare or exclusive item", + "write": false + } + ], + "gameID": [ + "01007EF00011E000" + ], + "gameName": "The Legend of Zelda: Breath of the Wild" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock one of five amiibo-exclusive Chamber Dungeon chambers", + "write": false + }, + { + "Usage": "Save your Chamber Dungeon to the amiibo to share with friends", + "write": true + } + ], + "gameID": [ + "01006BB00C6F0000" + ], + "gameName": "The Legend of Zelda: Link's Awakening" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an amiibo-exclusive character-specific paraglider fabric", + "write": false + }, + { + "Usage": "Bring Epona into the game as a rideable horse", + "write": false + }, + { + "Usage": "Receive materials and a weapon or rare item", + "write": false + } + ], + "gameID": [ + "0100F2C0115B6000" + ], + "gameName": "The Legend of Zelda: Tears of the Kingdom" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive the Red Tunic", + "write": false + }, + { + "Usage": "Receive random materials", + "write": false + } + ], + "gameID": [ + "01008CF01BAAC000" + ], + "gameName": "The Legend of Zelda: Echoes of Wisdom" + } + ], + "head": "01000000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01000000-00040002.png", + "name": "Link", + "release": { + "au": "2014-11-29", + "eu": "2014-11-28", + "jp": "2014-12-06", + "na": "2014-11-21" + }, + "tail": "00040002", + "type": "Figure" + }, + { + "amiiboSeries": "Super Smash Bros.", + "character": "Cloud Strife", + "gameSeries": "Final Fantasy", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "36000000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_36000000-02590002.png", + "name": "Cloud", + "release": { + "au": "2017-07-22", + "eu": "2017-07-21", + "jp": "2017-07-21", + "na": "2017-07-21" + }, + "tail": "02590002", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Rudy", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "02710001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_02710001-019b0502.png", + "name": "Rudy", + "release": { + "au": "2016-06-18", + "eu": null, + "jp": "2016-03-24", + "na": "2016-06-10" + }, + "tail": "019b0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Flurry", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "03840001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_03840001-00860502.png", + "name": "Flurry", + "release": { + "au": "2015-10-03", + "eu": "2015-10-02", + "jp": "2015-07-30", + "na": "2015-09-25" + }, + "tail": "00860502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Tammy", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "028e0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_028e0001-019e0502.png", + "name": "Tammy", + "release": { + "au": "2016-06-18", + "eu": null, + "jp": "2016-03-24", + "na": "2016-06-10" + }, + "tail": "019e0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Teddy", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "02140001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_02140001-00e40502.png", + "name": "Teddy", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-10-29", + "na": "2016-01-22" + }, + "tail": "00e40502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Frank", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "04510001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_04510001-015e0502.png", + "name": "Frank", + "release": { + "au": "2016-03-19", + "eu": "2016-03-18", + "jp": "2016-01-14", + "na": "2016-03-18" + }, + "tail": "015e0502", + "type": "Card" + }, + { + "amiiboSeries": "Mario Sports Superstars", + "character": "Peach", + "gameSeries": "Mario Sports Superstars", + "gamesSwitch": [], + "head": "09c20501", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_09c20501-02770e02.png", + "name": "Peach - Horse Racing", + "release": { + "au": "2017-03-11", + "eu": "2017-03-10", + "jp": "2017-03-30", + "na": "2017-03-24" + }, + "tail": "02770e02", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Stu", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "024d0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_024d0001-02f60502.png", + "name": "Stu", + "release": { + "au": "2016-11-10", + "eu": "2016-11-11", + "jp": "2016-11-03", + "na": "2016-12-02" + }, + "tail": "02f60502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Roscoe", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "03a80001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_03a80001-00910502.png", + "name": "Roscoe", + "release": { + "au": "2015-10-03", + "eu": "2015-10-02", + "jp": "2015-07-30", + "na": "2015-09-25" + }, + "tail": "00910502", + "type": "Card" + }, + { + "amiiboSeries": "Super Smash Bros.", + "character": "Ike", + "gameSeries": "Fire Emblem", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Receive a Fashion Ticket and a Music Ticket, for unlocking any of the available costumes and music tracks", + "write": false + } + ], + "gameID": [ + "0100A6301214E000" + ], + "gameName": "Fire Emblem Engage" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a weapon", + "write": false + } + ], + "gameID": [ + "0100F15003E64000" + ], + "gameName": "Fire Emblem Warriors" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive better-quality randomized resources, weapons, or equipment", + "write": false + } + ], + "gameID": [ + "010071F0143EA000" + ], + "gameName": "Fire Emblem Warriors: Three Hopes" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special piece of battle music / Receive higher-quality items and materials", + "write": false + } + ], + "gameID": [ + "010055D009F78000" + ], + "gameName": "Fire Emblem: Three Houses" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "21010000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_21010000-00180002.png", + "name": "Ike", + "release": { + "au": "2015-01-29", + "eu": "2015-01-23", + "jp": "2015-01-22", + "na": "2015-02-01" + }, + "tail": "00180002", + "type": "Figure" + }, + { + "amiiboSeries": "Mario Sports Superstars", + "character": "Luigi", + "gameSeries": "Mario Sports Superstars", + "gamesSwitch": [], + "head": "09c10401", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_09c10401-02710e02.png", + "name": "Luigi - Golf", + "release": { + "au": "2017-03-11", + "eu": "2017-03-10", + "jp": "2017-03-30", + "na": "2017-03-24" + }, + "tail": "02710e02", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Rodney", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "03810001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_03810001-00d50502.png", + "name": "Rodney", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-10-29", + "na": "2016-01-22" + }, + "tail": "00d50502", + "type": "Card" + }, + { + "amiiboSeries": "Mario Sports Superstars", + "character": "Baby Mario", + "gameSeries": "Mario Sports Superstars", + "gamesSwitch": [], + "head": "09cc0101", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_09cc0101-02a50e02.png", + "name": "Baby Mario - Soccer", + "release": { + "au": "2017-03-11", + "eu": "2017-03-10", + "jp": "2017-03-30", + "na": "2017-03-24" + }, + "tail": "02a50e02", + "type": "Card" + }, + { + "amiiboSeries": "Power Pros", + "character": "Daijobu", + "gameSeries": "Power Pros", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Receive in-game items and power-ups / Save items to your card after playing with friends to bring them home", + "write": true + } + ], + "gameID": [ + "0100E9C00BF28000" + ], + "gameName": "Jikkyou Powerful Pro Baseball" + } + ], + "head": "38050001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_38050001-03981702.png", + "name": "Daijobu", + "release": { + "au": null, + "eu": null, + "jp": "2019-06-27", + "na": null + }, + "tail": "03981702", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Bruce", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "02d90001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_02d90001-01c80502.png", + "name": "Bruce", + "release": { + "au": "2016-06-18", + "eu": null, + "jp": "2016-03-24", + "na": "2016-06-10" + }, + "tail": "01c80502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Bree", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "040f0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_040f0001-01500502.png", + "name": "Bree", + "release": { + "au": "2016-03-19", + "eu": "2016-03-18", + "jp": "2016-01-14", + "na": "2016-03-18" + }, + "tail": "01500502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Bangle", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "04fd0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_04fd0001-007b0502.png", + "name": "Bangle", + "release": { + "au": "2015-10-03", + "eu": "2015-10-02", + "jp": "2015-07-30", + "na": "2015-09-25" + }, + "tail": "007b0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Stitches", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "02820001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_02820001-01810502.png", + "name": "Stitches", + "release": { + "au": "2016-06-18", + "eu": null, + "jp": "2016-03-24", + "na": "2016-06-10" + }, + "tail": "01810502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Aurora", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "045f0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_045f0001-01a80502.png", + "name": "Aurora", + "release": { + "au": "2016-06-18", + "eu": null, + "jp": "2016-03-24", + "na": "2016-06-10" + }, + "tail": "01a80502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Iggly", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "046a0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_046a0001-01d00502.png", + "name": "Iggly", + "release": { + "au": "2016-06-18", + "eu": null, + "jp": "2016-03-24", + "na": "2016-06-10" + }, + "tail": "01d00502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Vic", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "02520001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_02520001-00fe0502.png", + "name": "Vic", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-10-29", + "na": "2016-01-22" + }, + "tail": "00fe0502", + "type": "Card" + }, + { + "amiiboSeries": "Monster Hunter Rise", + "character": "Palico", + "gameSeries": "Monster Hunter", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock special layered armor / Enter daily lottery for a variety of useful items", + "write": false + } + ], + "gameID": [ + "0100B04011742000" + ], + "gameName": "Monster Hunter Rise" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock the Hunter Sticker Set / Have Tsukino read your fortune and receive a random item", + "write": false + } + ], + "gameID": [ + "0100E21011446000" + ], + "gameName": "Monster Hunter Stories 2: Wings of Ruin" + } + ], + "head": "35090100", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_35090100-042b1802.png", + "name": "Palico", + "release": { + "au": "2022-06-30", + "eu": "2022-06-30", + "jp": "2022-06-30", + "na": "2022-06-30" + }, + "tail": "042b1802", + "type": "Figure" + }, + { + "amiiboSeries": "Super Nintendo World", + "character": "Mario", + "gameSeries": "Super Mario", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock an amiibo-exclusive weapon for the character and their Rabbid counterpart", + "write": false + } + ], + "gameID": [ + "010067300059A000" + ], + "gameName": "Mario + Rabbids: Kingdom Battle" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01003DA010E8A000" + ], + "gameName": "Miitopia" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a particular power-up depending on the character", + "write": false + } + ], + "gameID": [ + "010028600EBDA000" + ], + "gameName": "Super Mario 3D World + Bowser's Fury" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a character-based costume", + "write": false + }, + { + "Usage": "Gain temporary invincibility", + "write": false + } + ], + "gameID": [ + "0100000000010000" + ], + "gameName": "Super Mario Odyssey" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock the character's shiny sticker", + "write": false + } + ], + "gameID": [ + "010036B0034E4000" + ], + "gameName": "Super Mario Party" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01006000040C2000" + ], + "gameName": "Yoshi's Crafted World" + } + ], + "head": "00000003", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_00000003-0430ff02.png", + "name": "Golden - Power Up Band", + "release": { + "au": null, + "eu": null, + "jp": "2023-03-18", + "na": null + }, + "tail": "0430ff02", + "type": "Band" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Amelia", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "044c0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_044c0001-008e0502.png", + "name": "Amelia", + "release": { + "au": "2015-10-03", + "eu": "2015-10-02", + "jp": "2015-07-30", + "na": "2015-09-25" + }, + "tail": "008e0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Miranda", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "03130001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_03130001-01210502.png", + "name": "Miranda", + "release": { + "au": "2016-03-19", + "eu": "2016-03-18", + "jp": "2016-01-14", + "na": "2016-03-18" + }, + "tail": "01210502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Katrina", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "01a50001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01a50001-01720502.png", + "name": "Katrina", + "release": { + "au": "2016-06-18", + "eu": null, + "jp": "2016-03-24", + "na": "2016-06-10" + }, + "tail": "01720502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Audie", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "0a0c0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_0a0c0001-03c30502.png", + "name": "Audie", + "release": { + "au": "2021-11-05", + "eu": "2021-11-05", + "jp": "2021-11-05", + "na": "2021-11-05" + }, + "tail": "03c30502", + "type": "Card" + }, + { + "amiiboSeries": "Mario Sports Superstars", + "character": "Diddy Kong", + "gameSeries": "Mario Sports Superstars", + "gamesSwitch": [], + "head": "09c80501", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_09c80501-02950e02.png", + "name": "Diddy Kong - Horse Racing", + "release": { + "au": "2017-03-11", + "eu": "2017-03-10", + "jp": "2017-03-30", + "na": "2017-03-24" + }, + "tail": "02950e02", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Bill", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "03070001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_03070001-00640502.png", + "name": "Bill", + "release": { + "au": "2015-10-03", + "eu": "2015-10-02", + "jp": "2015-07-30", + "na": "2015-09-25" + }, + "tail": "00640502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Anchovy", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "022f0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_022f0001-011e0502.png", + "name": "Anchovy", + "release": { + "au": "2016-03-19", + "eu": "2016-03-18", + "jp": "2016-01-14", + "na": "2016-03-18" + }, + "tail": "011e0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Harvey", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "0a050001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_0a050001-03b80502.png", + "name": "Harvey", + "release": { + "au": "2021-11-05", + "eu": "2021-11-05", + "jp": "2021-11-05", + "na": "2021-11-05" + }, + "tail": "03b80502", + "type": "Card" + }, + { + "amiiboSeries": "Legend Of Zelda", + "character": "Link", + "gameSeries": "The Legend of Zelda", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock a costume based on the character (short-hair version)", + "write": false + } + ], + "gameID": [ + "01007960049A0000" + ], + "gameName": "Bayonetta 2" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive specific crafting materials or a weapon for the character", + "write": false + } + ], + "gameID": [ + "01002B00111A2000" + ], + "gameName": "Hyrule Warriors: Age of Calamity" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a weapon rated 3 stars or higher", + "write": false + } + ], + "gameID": [ + "0100AE00096EA000" + ], + "gameName": "Hyrule Warriors: Definitive Edition" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a Legend of Zelda-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01003DA010E8A000" + ], + "gameName": "Miitopia" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a chest of loot, potentially containing gear inspired by the Legend of Zelda series", + "write": false + } + ], + "gameID": [ + "01000A10041EA000" + ], + "gameName": "The Elder Scrolls V: Skyrim" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive materials and a rare or exclusive item", + "write": false + } + ], + "gameID": [ + "01007EF00011E000" + ], + "gameName": "The Legend of Zelda: Breath of the Wild" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock one of five amiibo-exclusive Chamber Dungeon chambers", + "write": false + }, + { + "Usage": "Save your Chamber Dungeon to the amiibo to share with friends", + "write": true + } + ], + "gameID": [ + "01006BB00C6F0000" + ], + "gameName": "The Legend of Zelda: Link's Awakening" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an amiibo-exclusive character-specific paraglider fabric", + "write": false + }, + { + "Usage": "Receive materials and a weapon or rare item", + "write": false + } + ], + "gameID": [ + "0100F2C0115B6000" + ], + "gameName": "The Legend of Zelda: Tears of the Kingdom" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive the Red Tunic", + "write": false + }, + { + "Usage": "Receive random materials", + "write": false + } + ], + "gameID": [ + "01008CF01BAAC000" + ], + "gameName": "The Legend of Zelda: Echoes of Wisdom" + } + ], + "head": "01000000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01000000-03530902.png", + "name": "Link - Archer", + "release": { + "au": "2017-03-03", + "eu": "2017-03-03", + "jp": "2017-03-03", + "na": "2017-03-03" + }, + "tail": "03530902", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Tommy", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "01860101", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01860101-00af0502.png", + "name": "Tommy - Uniform", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-10-29", + "na": "2016-01-22" + }, + "tail": "00af0502", + "type": "Card" + }, + { + "amiiboSeries": "Splatoon", + "character": "Octoling", + "gameSeries": "Splatoon", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a Splatoon-themed racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock exclusive gear / Save favorite weapons, gear, and control settings / Take photos with the character or saved gear", + "write": true + } + ], + "gameID": [ + "01003BC0000A0000" + ], + "gameName": "Splatoon 2" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock exclusive gear / Save favorite weapons, gear, and control settings / Take photos with the character or saved gear", + "write": true + } + ], + "gameID": [ + "0100C2500FC20000" + ], + "gameName": "Splatoon 3" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a Spirit of the character", + "write": false + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "08050100", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_08050100-038e0402.png", + "name": "Octoling Girl", + "release": { + "au": "2018-11-11", + "eu": "2018-11-09", + "jp": "2018-11-09", + "na": "2018-11-09" + }, + "tail": "038e0402", + "type": "Figure" + }, + { + "amiiboSeries": "Legend Of Zelda", + "character": "Link", + "gameSeries": "The Legend of Zelda", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock a costume based on the character (short-hair version)", + "write": false + } + ], + "gameID": [ + "01007960049A0000" + ], + "gameName": "Bayonetta 2" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive specific crafting materials or a weapon for the character", + "write": false + } + ], + "gameID": [ + "01002B00111A2000" + ], + "gameName": "Hyrule Warriors: Age of Calamity" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a weapon rated 3 stars or higher", + "write": false + } + ], + "gameID": [ + "0100AE00096EA000" + ], + "gameName": "Hyrule Warriors: Definitive Edition" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01003DA010E8A000" + ], + "gameName": "Miitopia" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a chest of loot, potentially containing gear inspired by the Legend of Zelda series", + "write": false + } + ], + "gameID": [ + "01000A10041EA000" + ], + "gameName": "The Elder Scrolls V: Skyrim" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive materials and a rare or exclusive item", + "write": false + } + ], + "gameID": [ + "01007EF00011E000" + ], + "gameName": "The Legend of Zelda: Breath of the Wild" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock one of five amiibo-exclusive Chamber Dungeon chambers", + "write": false + }, + { + "Usage": "Save your Chamber Dungeon to the amiibo to share with friends", + "write": true + } + ], + "gameID": [ + "01006BB00C6F0000" + ], + "gameName": "The Legend of Zelda: Link's Awakening" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an amiibo-exclusive character-specific paraglider fabric", + "write": false + }, + { + "Usage": "Receive materials and a weapon or rare item", + "write": false + } + ], + "gameID": [ + "0100F2C0115B6000" + ], + "gameName": "The Legend of Zelda: Tears of the Kingdom" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive the Red Tunic", + "write": false + }, + { + "Usage": "Receive random materials", + "write": false + } + ], + "gameID": [ + "01008CF01BAAC000" + ], + "gameName": "The Legend of Zelda: Echoes of Wisdom" + } + ], + "head": "01000000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01000000-034f0902.png", + "name": "8-Bit Link", + "release": { + "au": "2016-12-03", + "eu": "2016-12-02", + "jp": "2016-12-01", + "na": "2016-12-02" + }, + "tail": "034f0902", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Maggie", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "04820001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_04820001-02fd0502.png", + "name": "Maggie", + "release": { + "au": "2016-11-10", + "eu": "2016-11-11", + "jp": "2016-11-03", + "na": "2016-12-02" + }, + "tail": "02fd0502", + "type": "Card" + }, + { + "amiiboSeries": "Mega Man", + "character": "Mega Man", + "gameSeries": "Megaman", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive E Tanks and other useful in-game items", + "write": false + } + ], + "gameID": [ + "0100B0C0086B0000" + ], + "gameName": "Mega Man 11" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock 11 exclusive challenge stages designed by fans", + "write": false + } + ], + "gameID": [ + "01002D4007AE0000" + ], + "gameName": "Mega Man Legacy Collection" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock new platforming challenges", + "write": false + } + ], + "gameID": [ + "0100842008EC4000" + ], + "gameName": "Mega Man Legacy Collection 2" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a lot of BP / Receive better supplies (compared to other amiibo)", + "write": false + } + ], + "gameID": [ + "0100643002136000" + ], + "gameName": "Resident Evil Revelations" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a lot of BP / Receive better supplies (compared to other amiibo)", + "write": false + } + ], + "gameID": [ + "010095300212A000" + ], + "gameName": "Resident Evil Revelations 2" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "34800000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_34800000-03791502.png", + "name": "Mega Man", + "release": { + "au": null, + "eu": null, + "jp": "2018-10-04", + "na": "2018-10-02" + }, + "tail": "03791502", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Orville", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "0a000001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_0a000001-03ab0502.png", + "name": "Orville", + "release": { + "au": "2021-11-05", + "eu": "2021-11-05", + "jp": "2021-11-05", + "na": "2021-11-05" + }, + "tail": "03ab0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Chai", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + }, + { + "Usage": "Unlock special furniture items and a poster based on the card's Sanrio character", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "032e0101", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_032e0101-031c0502.png", + "name": "Chai", + "release": { + "au": null, + "eu": "2016-11-25", + "jp": "2016-11-03", + "na": null + }, + "tail": "031c0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Dom", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "0a0b0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_0a0b0001-03c20502.png", + "name": "Dom", + "release": { + "au": "2021-11-05", + "eu": "2021-11-05", + "jp": "2021-11-05", + "na": "2021-11-05" + }, + "tail": "03c20502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Kapp'n", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a Spirit of the character", + "write": false + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "01960000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01960000-024e0502.png", + "name": "Kapp'n", + "release": { + "au": "2016-03-19", + "eu": "2016-03-18", + "jp": "2016-03-24", + "na": "2016-03-18" + }, + "tail": "024e0502", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Limberg", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "040d0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_040d0001-00780502.png", + "name": "Limberg", + "release": { + "au": "2015-10-03", + "eu": "2015-10-02", + "jp": "2015-07-30", + "na": "2015-09-25" + }, + "tail": "00780502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Weber", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "03120001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_03120001-03090502.png", + "name": "Weber", + "release": { + "au": "2016-11-10", + "eu": "2016-11-11", + "jp": "2016-11-03", + "na": "2016-12-02" + }, + "tail": "03090502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Bunnie", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "04940001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_04940001-009a0502.png", + "name": "Bunnie", + "release": { + "au": "2015-10-03", + "eu": "2015-10-02", + "jp": "2015-07-30", + "na": "2015-09-25" + }, + "tail": "009a0502", + "type": "Card" + }, + { + "amiiboSeries": "Yoshi's Woolly World", + "character": "Poochy", + "gameSeries": "Yoshi's Woolly World", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a Spirit of the character", + "write": false + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01006000040C2000" + ], + "gameName": "Yoshi's Crafted World" + } + ], + "head": "00800102", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_00800102-035d0302.png", + "name": "Poochy", + "release": { + "au": "2017-02-04", + "eu": "2017-02-03", + "jp": "2017-01-19", + "na": "2017-02-03" + }, + "tail": "035d0302", + "type": "Yarn" + }, + { + "amiiboSeries": "Mario Sports Superstars", + "character": "Waluigi", + "gameSeries": "Mario Sports Superstars", + "gamesSwitch": [], + "head": "09c60301", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_09c60301-02890e02.png", + "name": "Waluigi - Tennis", + "release": { + "au": "2017-03-11", + "eu": "2017-03-10", + "jp": "2017-03-30", + "na": "2017-03-24" + }, + "tail": "02890e02", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Pelly", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "01a00001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01a00001-010f0502.png", + "name": "Pelly", + "release": { + "au": "2016-03-19", + "eu": "2016-03-18", + "jp": "2016-01-14", + "na": "2016-03-18" + }, + "tail": "010f0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Frobert", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "033a0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_033a0001-01cc0502.png", + "name": "Frobert", + "release": { + "au": "2016-06-18", + "eu": null, + "jp": "2016-03-24", + "na": "2016-06-10" + }, + "tail": "01cc0502", + "type": "Card" + }, + { + "amiiboSeries": "Mario Sports Superstars", + "character": "Birdo", + "gameSeries": "Mario Sports Superstars", + "gamesSwitch": [], + "head": "09ce0501", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_09ce0501-02b30e02.png", + "name": "Birdo - Horse Racing", + "release": { + "au": "2017-03-11", + "eu": "2017-03-10", + "jp": "2017-03-30", + "na": "2017-03-24" + }, + "tail": "02b30e02", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Tasha", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "04ea0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_04ea0001-03180502.png", + "name": "Tasha", + "release": { + "au": "2016-11-10", + "eu": "2016-11-11", + "jp": "2016-11-03", + "na": "2016-12-02" + }, + "tail": "03180502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Robin", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "022e0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_022e0001-01d30502.png", + "name": "Robin", + "release": { + "au": "2016-06-18", + "eu": null, + "jp": "2016-03-24", + "na": "2016-06-10" + }, + "tail": "01d30502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Alfonso", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "02c30001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_02c30001-00dc0502.png", + "name": "Alfonso", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-10-29", + "na": "2016-01-22" + }, + "tail": "00dc0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Peck", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "023e0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_023e0001-00d10502.png", + "name": "Peck", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-10-29", + "na": "2016-01-22" + }, + "tail": "00d10502", + "type": "Card" + }, + { + "amiiboSeries": "Mario Sports Superstars", + "character": "Baby Luigi", + "gameSeries": "Mario Sports Superstars", + "gamesSwitch": [], + "head": "09cd0201", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_09cd0201-02ab0e02.png", + "name": "Baby Luigi - Baseball", + "release": { + "au": "2017-03-11", + "eu": "2017-03-10", + "jp": "2017-03-30", + "na": "2017-03-24" + }, + "tail": "02ab0e02", + "type": "Card" + }, + { + "amiiboSeries": "Super Mario Bros.", + "character": "Rosalina", + "gameSeries": "Super Mario", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01003DA010E8A000" + ], + "gameName": "Miitopia" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a particular power-up depending on the character", + "write": false + } + ], + "gameID": [ + "010028600EBDA000" + ], + "gameName": "Super Mario 3D World + Bowser's Fury" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock the character's shiny sticker", + "write": false + } + ], + "gameID": [ + "010036B0034E4000" + ], + "gameName": "Super Mario Party" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "00040000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_00040000-02620102.png", + "name": "Rosalina", + "release": { + "au": "2016-10-08", + "eu": "2016-10-07", + "jp": "2016-10-20", + "na": "2016-11-04" + }, + "tail": "02620102", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Cyrus", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a Spirit of the character", + "write": false + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "018b0000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_018b0000-02460502.png", + "name": "Cyrus", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-11-21", + "na": "2015-11-13" + }, + "tail": "02460502", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Frita", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "04d00001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_04d00001-01960502.png", + "name": "Frita", + "release": { + "au": "2016-06-18", + "eu": null, + "jp": "2016-03-24", + "na": "2016-06-10" + }, + "tail": "01960502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Sprinkle", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "046d0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_046d0001-00f30502.png", + "name": "Sprinkle", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-10-29", + "na": "2016-01-22" + }, + "tail": "00f30502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Bella", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "040e0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_040e0001-00880502.png", + "name": "Bella", + "release": { + "au": "2015-10-03", + "eu": "2015-10-02", + "jp": "2015-07-30", + "na": "2015-09-25" + }, + "tail": "00880502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Drago", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "02cb0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_02cb0001-01360502.png", + "name": "Drago", + "release": { + "au": "2016-03-19", + "eu": "2016-03-18", + "jp": "2016-01-14", + "na": "2016-03-18" + }, + "tail": "01360502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Grams", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "01990001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01990001-01160502.png", + "name": "Grams", + "release": { + "au": "2016-03-19", + "eu": "2016-03-18", + "jp": "2016-01-14", + "na": "2016-03-18" + }, + "tail": "01160502", + "type": "Card" + }, + { + "amiiboSeries": "Super Smash Bros.", + "character": "Corrin", + "gameSeries": "Fire Emblem", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Receive a Fashion Ticket and a Music Ticket, for unlocking any of the available costumes and music tracks", + "write": false + } + ], + "gameID": [ + "0100A6301214E000" + ], + "gameName": "Fire Emblem Engage" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive better-quality randomized resources, weapons, or equipment", + "write": false + } + ], + "gameID": [ + "010071F0143EA000" + ], + "gameName": "Fire Emblem Warriors: Three Hopes" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special piece of battle music / Receive higher-quality items and materials", + "write": false + } + ], + "gameID": [ + "010055D009F78000" + ], + "gameName": "Fire Emblem: Three Houses" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "21050000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_21050000-025a0002.png", + "name": "Corrin", + "release": { + "au": "2017-07-22", + "eu": "2017-07-21", + "jp": "2017-07-21", + "na": "2017-07-21" + }, + "tail": "025a0002", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "OHare", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "04a30001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_04a30001-01c90502.png", + "name": "OHare", + "release": { + "au": "2016-06-18", + "eu": null, + "jp": "2016-03-24", + "na": "2016-06-10" + }, + "tail": "01c90502", + "type": "Card" + }, + { + "amiiboSeries": "Super Smash Bros.", + "character": "Dark Pit", + "gameSeries": "Kid Icarus", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "07410000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_07410000-00200002.png", + "name": "Dark Pit", + "release": { + "au": "2015-07-04", + "eu": "2015-06-26", + "jp": "2015-06-11", + "na": "2015-07-31" + }, + "tail": "00200002", + "type": "Figure" + }, + { + "amiiboSeries": "Super Smash Bros.", + "character": "Rosalina", + "gameSeries": "Super Mario", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01003DA010E8A000" + ], + "gameName": "Miitopia" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a particular power-up depending on the character", + "write": false + } + ], + "gameID": [ + "010028600EBDA000" + ], + "gameName": "Super Mario 3D World + Bowser's Fury" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock the character's shiny sticker", + "write": false + } + ], + "gameID": [ + "010036B0034E4000" + ], + "gameName": "Super Mario Party" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "00040100", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_00040100-00130002.png", + "name": "Rosalina & Luma", + "release": { + "au": "2015-01-29", + "eu": "2015-01-23", + "jp": "2015-01-22", + "na": "2015-02-01" + }, + "tail": "00130002", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Redd", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "01a80001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01a80001-004f0502.png", + "name": "Redd", + "release": { + "au": "2015-10-03", + "eu": "2015-10-02", + "jp": "2015-07-30", + "na": "2015-09-25" + }, + "tail": "004f0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Mabel", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a Spirit of the character", + "write": false + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "01880000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01880000-02410502.png", + "name": "Mabel", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-11-21", + "na": "2015-11-13" + }, + "tail": "02410502", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Hamphrey", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "03850001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_03850001-01060502.png", + "name": "Hamphrey", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-10-29", + "na": "2016-01-22" + }, + "tail": "01060502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Pave", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "01ab0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01ab0001-017c0502.png", + "name": "Pave", + "release": { + "au": "2016-06-18", + "eu": null, + "jp": "2016-03-24", + "na": "2016-06-10" + }, + "tail": "017c0502", + "type": "Card" + }, + { + "amiiboSeries": "Mario Sports Superstars", + "character": "Wario", + "gameSeries": "Mario Sports Superstars", + "gamesSwitch": [], + "head": "09c50101", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_09c50101-02820e02.png", + "name": "Wario - Soccer", + "release": { + "au": "2017-03-11", + "eu": "2017-03-10", + "jp": "2017-03-30", + "na": "2017-03-24" + }, + "tail": "02820e02", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Alli", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "02c40001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_02c40001-00670502.png", + "name": "Alli", + "release": { + "au": "2015-10-03", + "eu": "2015-10-02", + "jp": "2015-07-30", + "na": "2015-09-25" + }, + "tail": "00670502", + "type": "Card" + }, + { + "amiiboSeries": "Mario Sports Superstars", + "character": "Yoshi", + "gameSeries": "Mario Sports Superstars", + "gamesSwitch": [], + "head": "09c40401", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_09c40401-02800e02.png", + "name": "Yoshi - Golf", + "release": { + "au": "2017-03-11", + "eu": "2017-03-10", + "jp": "2017-03-30", + "na": "2017-03-24" + }, + "tail": "02800e02", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Bones", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "02ee0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_02ee0001-01990502.png", + "name": "Bones", + "release": { + "au": "2016-06-18", + "eu": null, + "jp": "2016-03-24", + "na": "2016-06-10" + }, + "tail": "01990502", + "type": "Card" + }, + { + "amiiboSeries": "Metroid", + "character": "E.M.M.I.", + "gameSeries": "Metroid", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a Metroid-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Permanently increase missile capacity by 10", + "write": false + } + ], + "gameID": [ + "010093801237C000" + ], + "gameName": "Metroid Dread" + }, + { + "amiiboUsage": [ + { + "Usage": "Replenish a random amount of missiles once per day", + "write": false + } + ], + "gameID": [ + "010093801237C000" + ], + "gameName": "Metroid Dread" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a Spirit of the character", + "write": false + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "05c40000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_05c40000-04131302.png", + "name": "E.M.M.I.", + "release": { + "au": "2021-10-08", + "eu": "2021-11-05", + "jp": "2021-10-08", + "na": "2021-10-08" + }, + "tail": "04131302", + "type": "Figure" + }, + { + "amiiboSeries": "Legend Of Zelda", + "character": "Link", + "gameSeries": "The Legend of Zelda", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock a costume based on the character (short-hair version)", + "write": false + } + ], + "gameID": [ + "01007960049A0000" + ], + "gameName": "Bayonetta 2" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive specific crafting materials or a weapon for the character", + "write": false + } + ], + "gameID": [ + "01002B00111A2000" + ], + "gameName": "Hyrule Warriors: Age of Calamity" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a weapon rated 3 stars or higher", + "write": false + } + ], + "gameID": [ + "0100AE00096EA000" + ], + "gameName": "Hyrule Warriors: Definitive Edition" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a Legend of Zelda-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01003DA010E8A000" + ], + "gameName": "Miitopia" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a chest of loot, potentially containing gear inspired by the Legend of Zelda series", + "write": false + } + ], + "gameID": [ + "01000A10041EA000" + ], + "gameName": "The Elder Scrolls V: Skyrim" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive materials and a rare or exclusive item", + "write": false + } + ], + "gameID": [ + "01007EF00011E000" + ], + "gameName": "The Legend of Zelda: Breath of the Wild" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock one of five amiibo-exclusive Chamber Dungeon chambers", + "write": false + }, + { + "Usage": "Save your Chamber Dungeon to the amiibo to share with friends", + "write": true + } + ], + "gameID": [ + "01006BB00C6F0000" + ], + "gameName": "The Legend of Zelda: Link's Awakening" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an amiibo-exclusive character-specific paraglider fabric", + "write": false + }, + { + "Usage": "Receive materials and a weapon or rare item", + "write": false + } + ], + "gameID": [ + "0100F2C0115B6000" + ], + "gameName": "The Legend of Zelda: Tears of the Kingdom" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive the Red Tunic", + "write": false + }, + { + "Usage": "Receive random materials", + "write": false + } + ], + "gameID": [ + "01008CF01BAAC000" + ], + "gameName": "The Legend of Zelda: Echoes of Wisdom" + } + ], + "head": "01000000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01000000-03540902.png", + "name": "Link - Rider", + "release": { + "au": "2017-03-03", + "eu": "2017-03-03", + "jp": "2017-03-03", + "na": "2017-03-03" + }, + "tail": "03540902", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Gaston", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "04980001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_04980001-014a0502.png", + "name": "Gaston", + "release": { + "au": "2016-03-19", + "eu": "2016-03-18", + "jp": "2016-01-14", + "na": "2016-03-18" + }, + "tail": "014a0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Quillson", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "03180001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_03180001-006c0502.png", + "name": "Quillson", + "release": { + "au": "2015-10-03", + "eu": "2015-10-02", + "jp": "2015-07-30", + "na": "2015-09-25" + }, + "tail": "006c0502", + "type": "Card" + }, + { + "amiiboSeries": "Mario Sports Superstars", + "character": "Pink Gold Peach", + "gameSeries": "Mario Sports Superstars", + "gamesSwitch": [], + "head": "09d10101", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_09d10101-02be0e02.png", + "name": "Pink Gold Peach - Soccer", + "release": { + "au": "2017-03-11", + "eu": "2017-03-10", + "jp": "2017-03-30", + "na": "2017-03-24" + }, + "tail": "02be0e02", + "type": "Card" + }, + { + "amiiboSeries": "Super Smash Bros.", + "character": "Wii Fit Trainer", + "gameSeries": "Wii Fit", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "07000000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_07000000-00070002.png", + "name": "Wii Fit Trainer", + "release": { + "au": "2014-11-29", + "eu": "2014-11-28", + "jp": "2014-12-06", + "na": "2014-11-21" + }, + "tail": "00070002", + "type": "Figure" + }, + { + "amiiboSeries": "Super Smash Bros.", + "character": "Wario", + "gameSeries": "Super Mario", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a particular power-up depending on the character", + "write": false + } + ], + "gameID": [ + "010028600EBDA000" + ], + "gameName": "Super Mario 3D World + Bowser's Fury" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a character-based costume", + "write": false + } + ], + "gameID": [ + "0100000000010000" + ], + "gameName": "Super Mario Odyssey" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock the character's shiny sticker", + "write": false + } + ], + "gameID": [ + "010036B0034E4000" + ], + "gameName": "Super Mario Party" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "00070000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_00070000-001a0002.png", + "name": "Wario", + "release": { + "au": "2015-04-25", + "eu": "2015-04-24", + "jp": "2015-04-29", + "na": "2015-05-29" + }, + "tail": "001a0002", + "type": "Figure" + }, + { + "amiiboSeries": "Chibi-Robo!", + "character": "Chibi-Robo", + "gameSeries": "Chibi Robo", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Receive a Spirit of the character", + "write": false + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "22c00000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_22c00000-003a0202.png", + "name": "Chibi Robo", + "release": { + "au": "2015-11-07", + "eu": "2015-11-06", + "jp": "2015-10-08", + "na": "2015-10-09" + }, + "tail": "003a0202", + "type": "Figure" + }, + { + "amiiboSeries": "Super Smash Bros.", + "character": "Diddy Kong", + "gameSeries": "Super Mario", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Receive a particular power-up depending on the character", + "write": false + } + ], + "gameID": [ + "010028600EBDA000" + ], + "gameName": "Super Mario 3D World + Bowser's Fury" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a character-based costume", + "write": false + } + ], + "gameID": [ + "0100000000010000" + ], + "gameName": "Super Mario Odyssey" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock the character's shiny sticker", + "write": false + } + ], + "gameID": [ + "010036B0034E4000" + ], + "gameName": "Super Mario Party" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "00090000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_00090000-000d0002.png", + "name": "Diddy Kong", + "release": { + "au": "2014-12-12", + "eu": "2014-12-19", + "jp": "2014-12-06", + "na": "2014-12-14" + }, + "tail": "000d0002", + "type": "Figure" + }, + { + "amiiboSeries": "Monster Hunter", + "character": "Razewing Ratha", + "gameSeries": "Monster Hunter", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock Monster Hunter Stories 2 sticker set", + "write": false + } + ], + "gameID": [ + "0100B04011742000" + ], + "gameName": "Monster Hunter Rise" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a character-based costume for Navirou", + "write": false + } + ], + "gameID": [ + "010069301B1D4000" + ], + "gameName": "Monster Hunter Stories" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-specific special layered armor set / Have Tsukino read your fortune and receive a random item", + "write": false + } + ], + "gameID": [ + "0100E21011446000" + ], + "gameName": "Monster Hunter Stories 2: Wings of Ruin" + } + ], + "head": "35050000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_35050000-040c0f02.png", + "name": "Razewing Ratha", + "release": { + "au": "2021-07-09", + "eu": "2021-07-09", + "jp": "2021-07-09", + "na": "2021-07-09" + }, + "tail": "040c0f02", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Zipper", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "01ac0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01ac0001-017f0502.png", + "name": "Zipper", + "release": { + "au": "2016-06-18", + "eu": null, + "jp": "2016-03-24", + "na": "2016-06-10" + }, + "tail": "017f0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Pierce", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "044d0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_044d0001-01930502.png", + "name": "Pierce", + "release": { + "au": "2016-06-18", + "eu": null, + "jp": "2016-03-24", + "na": "2016-06-10" + }, + "tail": "01930502", + "type": "Card" + }, + { + "amiiboSeries": "Power Pros", + "character": "Ikari", + "gameSeries": "Power Pros", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Receive in-game items and power-ups / Save items to your card after playing with friends to bring them home", + "write": true + } + ], + "gameID": [ + "0100E9C00BF28000" + ], + "gameName": "Jikkyou Powerful Pro Baseball" + } + ], + "head": "38010001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_38010001-03941702.png", + "name": "Ikari", + "release": { + "au": null, + "eu": null, + "jp": "2019-06-27", + "na": null + }, + "tail": "03941702", + "type": "Card" + }, + { + "amiiboSeries": "Legend Of Zelda", + "character": "Link", + "gameSeries": "The Legend of Zelda", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock a costume based on the character (short-hair version)", + "write": false + } + ], + "gameID": [ + "01007960049A0000" + ], + "gameName": "Bayonetta 2" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive specific crafting materials or a weapon for the character", + "write": false + } + ], + "gameID": [ + "01002B00111A2000" + ], + "gameName": "Hyrule Warriors: Age of Calamity" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a weapon rated 3 stars or higher", + "write": false + } + ], + "gameID": [ + "0100AE00096EA000" + ], + "gameName": "Hyrule Warriors: Definitive Edition" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a Legend of Zelda-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01003DA010E8A000" + ], + "gameName": "Miitopia" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a chest of loot, potentially containing gear inspired by the Legend of Zelda series", + "write": false + } + ], + "gameID": [ + "01000A10041EA000" + ], + "gameName": "The Elder Scrolls V: Skyrim" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive materials and a rare or exclusive item", + "write": false + } + ], + "gameID": [ + "01007EF00011E000" + ], + "gameName": "The Legend of Zelda: Breath of the Wild" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock one of five amiibo-exclusive Chamber Dungeon chambers", + "write": false + }, + { + "Usage": "Save your Chamber Dungeon to the amiibo to share with friends", + "write": true + } + ], + "gameID": [ + "01006BB00C6F0000" + ], + "gameName": "The Legend of Zelda: Link's Awakening" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an amiibo-exclusive character-specific paraglider fabric", + "write": false + }, + { + "Usage": "Receive materials and a weapon or rare item", + "write": false + } + ], + "gameID": [ + "0100F2C0115B6000" + ], + "gameName": "The Legend of Zelda: Tears of the Kingdom" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive the Red Tunic", + "write": false + }, + { + "Usage": "Receive random materials", + "write": false + } + ], + "gameID": [ + "01008CF01BAAC000" + ], + "gameName": "The Legend of Zelda: Echoes of Wisdom" + } + ], + "head": "01000100", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01000100-03500902.png", + "name": "Toon Link - The Wind Waker", + "release": { + "au": "2016-12-03", + "eu": "2016-12-02", + "jp": "2016-12-01", + "na": "2016-12-02" + }, + "tail": "03500902", + "type": "Figure" + }, + { + "amiiboSeries": "Super Smash Bros.", + "character": "Hero", + "gameSeries": "Dragon Quest", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "36400000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_36400000-03a20002.png", + "name": "Hero", + "release": { + "au": "2020-09-25", + "eu": "2020-09-25", + "jp": "2020-09-25", + "na": "2020-10-05" + }, + "tail": "03a20002", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Kapp'n", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "01960001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01960001-00480502.png", + "name": "Kapp'n", + "release": { + "au": "2015-10-03", + "eu": "2015-10-02", + "jp": "2015-07-30", + "na": "2015-09-25" + }, + "tail": "00480502", + "type": "Card" + }, + { + "amiiboSeries": "Super Nintendo World", + "character": "Luigi", + "gameSeries": "Super Mario", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock an amiibo-exclusive weapon for the character and their Rabbid counterpart", + "write": false + } + ], + "gameID": [ + "010067300059A000" + ], + "gameName": "Mario + Rabbids: Kingdom Battle" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01003DA010E8A000" + ], + "gameName": "Miitopia" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a particular power-up depending on the character", + "write": false + } + ], + "gameID": [ + "010028600EBDA000" + ], + "gameName": "Super Mario 3D World + Bowser's Fury" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a character-based costume", + "write": false + } + ], + "gameID": [ + "0100000000010000" + ], + "gameName": "Super Mario Odyssey" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock the character's shiny sticker", + "write": false + } + ], + "gameID": [ + "010036B0034E4000" + ], + "gameName": "Super Mario Party" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "00010003", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_00010003-039cff02.png", + "name": "Luigi - Power Up Band", + "release": { + "au": null, + "eu": null, + "jp": "2021-02-04", + "na": null + }, + "tail": "039cff02", + "type": "Band" + }, + { + "amiiboSeries": "Splatoon", + "character": "Inkling", + "gameSeries": "Splatoon", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a Splatoon-themed racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01003DA010E8A000" + ], + "gameName": "Miitopia" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock exclusive gear / Save favorite weapons, gear, and control settings / Take photos with the character or saved gear", + "write": true + } + ], + "gameID": [ + "01003BC0000A0000" + ], + "gameName": "Splatoon 2" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock exclusive gear / Save favorite weapons, gear, and control settings / Take photos with the character or saved gear", + "write": true + } + ], + "gameID": [ + "0100C2500FC20000" + ], + "gameName": "Splatoon 3" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "08000100", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_08000100-003e0402.png", + "name": "Inkling Girl", + "release": { + "au": "2015-05-30", + "eu": "2015-05-29", + "jp": "2015-05-28", + "na": "2015-05-29" + }, + "tail": "003e0402", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Lobo", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "050c0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_050c0001-01c10502.png", + "name": "Lobo", + "release": { + "au": "2016-06-18", + "eu": null, + "jp": "2016-03-24", + "na": "2016-06-10" + }, + "tail": "01c10502", + "type": "Card" + }, + { + "amiiboSeries": "Legend Of Zelda", + "character": "Zelda", + "gameSeries": "The Legend of Zelda", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive specific crafting materials or a weapon for the character", + "write": false + } + ], + "gameID": [ + "01002B00111A2000" + ], + "gameName": "Hyrule Warriors: Age of Calamity" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a weapon rated 3 stars or higher", + "write": false + } + ], + "gameID": [ + "0100AE00096EA000" + ], + "gameName": "Hyrule Warriors: Definitive Edition" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a Legend of Zelda-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01003DA010E8A000" + ], + "gameName": "Miitopia" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a chest of loot, potentially containing gear inspired by the Legend of Zelda series", + "write": false + } + ], + "gameID": [ + "01000A10041EA000" + ], + "gameName": "The Elder Scrolls V: Skyrim" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive materials and a rare or exclusive item", + "write": false + } + ], + "gameID": [ + "01007EF00011E000" + ], + "gameName": "The Legend of Zelda: Breath of the Wild" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock one of five amiibo-exclusive Chamber Dungeon chambers", + "write": false + }, + { + "Usage": "Save your Chamber Dungeon to the amiibo to share with friends", + "write": true + } + ], + "gameID": [ + "01006BB00C6F0000" + ], + "gameName": "The Legend of Zelda: Link's Awakening" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an amiibo-exclusive character-specific paraglider fabric", + "write": false + }, + { + "Usage": "Receive materials and a weapon or rare item", + "write": false + } + ], + "gameID": [ + "0100F2C0115B6000" + ], + "gameName": "The Legend of Zelda: Tears of the Kingdom" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive the Blue Attire", + "write": false + }, + { + "Usage": "Receive random materials", + "write": false + } + ], + "gameID": [ + "01008CF01BAAC000" + ], + "gameName": "The Legend of Zelda: Echoes of Wisdom" + } + ], + "head": "01010000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01010000-03560902.png", + "name": "Zelda", + "release": { + "au": "2017-03-03", + "eu": "2017-03-03", + "jp": "2017-03-03", + "na": "2017-03-03" + }, + "tail": "03560902", + "type": "Figure" + }, + { + "amiiboSeries": "Monster Hunter", + "character": "Rathian", + "gameSeries": "Monster Hunter", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock Monster Hunter Stories 2 sticker set", + "write": false + } + ], + "gameID": [ + "0100B04011742000" + ], + "gameName": "Monster Hunter Rise" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a character-based monstie egg", + "write": false + } + ], + "gameID": [ + "010069301B1D4000" + ], + "gameName": "Monster Hunter Stories" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock the Hakum Rider Outfit layered armor set / Have Tsukino read your fortune and receive a random item", + "write": false + } + ], + "gameID": [ + "0100E21011446000" + ], + "gameName": "Monster Hunter Stories 2: Wings of Ruin" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a lot of BP / Receive better supplies (compared to other amiibo)", + "write": false + } + ], + "gameID": [ + "0100643002136000" + ], + "gameName": "Resident Evil Revelations" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a lot of BP / Receive better supplies (compared to other amiibo)", + "write": false + } + ], + "gameID": [ + "010095300212A000" + ], + "gameName": "Resident Evil Revelations 2" + } + ], + "head": "35020100", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_35020100-02e40f02.png", + "name": "Rathian and Cheval", + "release": { + "au": null, + "eu": null, + "jp": "2016-12-08", + "na": null + }, + "tail": "02e40f02", + "type": "Figure" + }, + { + "amiiboSeries": "Mario Sports Superstars", + "character": "Waluigi", + "gameSeries": "Mario Sports Superstars", + "gamesSwitch": [], + "head": "09c60501", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_09c60501-028b0e02.png", + "name": "Waluigi - Horse Racing", + "release": { + "au": "2017-03-11", + "eu": "2017-03-10", + "jp": "2017-03-30", + "na": "2017-03-24" + }, + "tail": "028b0e02", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Quinn", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "0a180001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_0a180001-03cf0502.png", + "name": "Quinn", + "release": { + "au": "2021-11-05", + "eu": "2021-11-05", + "jp": "2021-11-05", + "na": "2021-11-05" + }, + "tail": "03cf0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Digby", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "018c0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_018c0001-004c0502.png", + "name": "Digby", + "release": { + "au": "2015-10-03", + "eu": "2015-10-02", + "jp": "2015-07-30", + "na": "2015-09-25" + }, + "tail": "004c0502", + "type": "Card" + }, + { + "amiiboSeries": "Super Smash Bros.", + "character": "Link", + "gameSeries": "The Legend of Zelda", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock a costume based on the character (short-hair version)", + "write": false + } + ], + "gameID": [ + "01007960049A0000" + ], + "gameName": "Bayonetta 2" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive specific crafting materials or a weapon for the character", + "write": false + } + ], + "gameID": [ + "01002B00111A2000" + ], + "gameName": "Hyrule Warriors: Age of Calamity" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a weapon rated 3 stars or higher", + "write": false + } + ], + "gameID": [ + "0100AE00096EA000" + ], + "gameName": "Hyrule Warriors: Definitive Edition" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a Legend of Zelda-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01003DA010E8A000" + ], + "gameName": "Miitopia" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a chest of loot, potentially containing gear inspired by the Legend of Zelda series", + "write": false + } + ], + "gameID": [ + "01000A10041EA000" + ], + "gameName": "The Elder Scrolls V: Skyrim" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive materials and a rare or exclusive item", + "write": false + } + ], + "gameID": [ + "01007EF00011E000" + ], + "gameName": "The Legend of Zelda: Breath of the Wild" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock one of five amiibo-exclusive Chamber Dungeon chambers", + "write": false + }, + { + "Usage": "Save your Chamber Dungeon to the amiibo to share with friends", + "write": true + } + ], + "gameID": [ + "01006BB00C6F0000" + ], + "gameName": "The Legend of Zelda: Link's Awakening" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an amiibo-exclusive character-specific paraglider fabric", + "write": false + }, + { + "Usage": "Receive materials and a weapon or rare item", + "write": false + } + ], + "gameID": [ + "0100F2C0115B6000" + ], + "gameName": "The Legend of Zelda: Tears of the Kingdom" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive the Red Tunic", + "write": false + }, + { + "Usage": "Receive random materials", + "write": false + } + ], + "gameID": [ + "01008CF01BAAC000" + ], + "gameName": "The Legend of Zelda: Echoes of Wisdom" + } + ], + "head": "01000000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01000000-037c0002.png", + "name": "Young Link", + "release": { + "au": "2019-04-12", + "eu": "2019-04-12", + "jp": "2019-04-12", + "na": "2019-04-12" + }, + "tail": "037c0002", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Huck", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "03430001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_03430001-02ef0502.png", + "name": "Huck", + "release": { + "au": "2016-11-10", + "eu": "2016-11-11", + "jp": "2016-11-03", + "na": "2016-12-02" + }, + "tail": "02ef0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Avery", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "04500001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_04500001-00cf0502.png", + "name": "Avery", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-10-29", + "na": "2016-01-22" + }, + "tail": "00cf0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Carrie", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "03d30001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_03d30001-02f30502.png", + "name": "Carrie", + "release": { + "au": "2016-11-10", + "eu": "2016-11-11", + "jp": "2016-11-03", + "na": "2016-12-02" + }, + "tail": "02f30502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Kiki", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "02610001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_02610001-00650502.png", + "name": "Kiki", + "release": { + "au": "2015-10-03", + "eu": "2015-10-02", + "jp": "2015-07-30", + "na": "2015-09-25" + }, + "tail": "00650502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Isabelle", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01003DA010E8A000" + ], + "gameName": "Miitopia" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "01810000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01810000-024b0502.png", + "name": "Isabelle - Summer Outfit", + "release": { + "au": "2016-03-19", + "eu": "2016-03-18", + "jp": "2016-03-24", + "na": "2016-06-10" + }, + "tail": "024b0502", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Rooney", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "03da0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_03da0001-01510502.png", + "name": "Rooney", + "release": { + "au": "2016-03-19", + "eu": "2016-03-18", + "jp": "2016-01-14", + "na": "2016-03-18" + }, + "tail": "01510502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Nate", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "02190001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_02190001-007e0502.png", + "name": "Nate", + "release": { + "au": "2015-10-03", + "eu": "2015-10-02", + "jp": "2015-07-30", + "na": "2015-09-25" + }, + "tail": "007e0502", + "type": "Card" + }, + { + "amiiboSeries": "Mario Sports Superstars", + "character": "Metal Mario", + "gameSeries": "Mario Sports Superstars", + "gamesSwitch": [], + "head": "09d00101", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_09d00101-02b90e02.png", + "name": "Metal Mario - Soccer", + "release": { + "au": "2017-03-11", + "eu": "2017-03-10", + "jp": "2017-03-30", + "na": "2017-03-24" + }, + "tail": "02b90e02", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Pippy", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "049a0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_049a0001-014e0502.png", + "name": "Pippy", + "release": { + "au": "2016-03-19", + "eu": "2016-03-18", + "jp": "2016-01-14", + "na": "2016-03-18" + }, + "tail": "014e0502", + "type": "Card" + }, + { + "amiiboSeries": "Super Smash Bros.", + "character": "Yoshi", + "gameSeries": "Super Mario", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock an amiibo-exclusive weapon for the character and their Rabbid counterpart", + "write": false + } + ], + "gameID": [ + "010067300059A000" + ], + "gameName": "Mario + Rabbids: Kingdom Battle" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01003DA010E8A000" + ], + "gameName": "Miitopia" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a particular power-up depending on the character", + "write": false + } + ], + "gameID": [ + "010028600EBDA000" + ], + "gameName": "Super Mario 3D World + Bowser's Fury" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock the character's shiny sticker", + "write": false + } + ], + "gameID": [ + "010036B0034E4000" + ], + "gameName": "Super Mario Party" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01006000040C2000" + ], + "gameName": "Yoshi's Crafted World" + } + ], + "head": "00030000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_00030000-00020002.png", + "name": "Yoshi", + "release": { + "au": "2014-11-29", + "eu": "2014-11-28", + "jp": "2014-12-06", + "na": "2014-11-21" + }, + "tail": "00020002", + "type": "Figure" + }, + { + "amiiboSeries": "Mario Sports Superstars", + "character": "Birdo", + "gameSeries": "Mario Sports Superstars", + "gamesSwitch": [], + "head": "09ce0301", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_09ce0301-02b10e02.png", + "name": "Birdo - Tennis", + "release": { + "au": "2017-03-11", + "eu": "2017-03-10", + "jp": "2017-03-30", + "na": "2017-03-24" + }, + "tail": "02b10e02", + "type": "Card" + }, + { + "amiiboSeries": "Super Smash Bros.", + "character": "Mythra", + "gameSeries": "Xenoblade Chronicles", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-specific weapon skin for characters using the Swordfighter Class", + "write": false + } + ], + "gameID": [ + "010074F013262000" + ], + "gameName": "Xenoblade Chronicles 3" + } + ], + "head": "22420000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_22420000-041f0002.png", + "name": "Mythra", + "release": { + "au": "2023-07-21", + "eu": "2023-07-21", + "jp": "2023-07-21", + "na": "2023-07-21" + }, + "tail": "041f0002", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Rocket", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "03720001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_03720001-010b0502.png", + "name": "Rocket", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-10-29", + "na": "2016-01-22" + }, + "tail": "010b0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Peaches", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "03ac0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_03ac0001-01880502.png", + "name": "Peaches", + "release": { + "au": "2016-06-18", + "eu": null, + "jp": "2016-03-24", + "na": "2016-06-10" + }, + "tail": "01880502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Ken", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "02a60001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_02a60001-01240502.png", + "name": "Ken", + "release": { + "au": "2016-03-19", + "eu": "2016-03-18", + "jp": "2016-01-14", + "na": "2016-03-18" + }, + "tail": "01240502", + "type": "Card" + }, + { + "amiiboSeries": "Splatoon", + "character": "Inkling", + "gameSeries": "Splatoon", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a Splatoon-themed racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01003DA010E8A000" + ], + "gameName": "Miitopia" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock exclusive gear / Save favorite weapons, gear, and control settings / Take photos with the character or saved gear", + "write": true + } + ], + "gameID": [ + "01003BC0000A0000" + ], + "gameName": "Splatoon 2" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock exclusive gear / Save favorite weapons, gear, and control settings / Take photos with the character or saved gear", + "write": true + } + ], + "gameID": [ + "0100C2500FC20000" + ], + "gameName": "Splatoon 3" + } + ], + "head": "08000200", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_08000200-003f0402.png", + "name": "Inkling Boy", + "release": { + "au": "2015-05-30", + "eu": "2015-05-29", + "jp": "2015-05-28", + "na": "2015-05-29" + }, + "tail": "003f0402", + "type": "Figure" + }, + { + "amiiboSeries": "Super Mario Bros.", + "character": "Luigi", + "gameSeries": "Super Mario", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an amiibo-exclusive weapon for the character and their Rabbid counterpart", + "write": false + } + ], + "gameID": [ + "010067300059A000" + ], + "gameName": "Mario + Rabbids: Kingdom Battle" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01003DA010E8A000" + ], + "gameName": "Miitopia" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a particular power-up depending on the character", + "write": false + } + ], + "gameID": [ + "010028600EBDA000" + ], + "gameName": "Super Mario 3D World + Bowser's Fury" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a character-based costume", + "write": false + } + ], + "gameID": [ + "0100000000010000" + ], + "gameName": "Super Mario Odyssey" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock the character's shiny sticker", + "write": false + } + ], + "gameID": [ + "010036B0034E4000" + ], + "gameName": "Super Mario Party" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01006000040C2000" + ], + "gameName": "Yoshi's Crafted World" + } + ], + "head": "00010000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_00010000-00350102.png", + "name": "Luigi", + "release": { + "au": "2015-03-21", + "eu": "2015-03-20", + "jp": "2015-03-12", + "na": "2015-03-20" + }, + "tail": "00350102", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Chip", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "019a0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_019a0001-00b70502.png", + "name": "Chip", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-10-29", + "na": "2016-01-22" + }, + "tail": "00b70502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Claudia", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "04ff0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_04ff0001-01620502.png", + "name": "Claudia", + "release": { + "au": "2016-03-19", + "eu": "2016-03-18", + "jp": "2016-01-14", + "na": "2016-03-18" + }, + "tail": "01620502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Greta", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "041c0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_041c0001-01410502.png", + "name": "Greta", + "release": { + "au": "2016-03-19", + "eu": "2016-03-18", + "jp": "2016-01-14", + "na": "2016-03-18" + }, + "tail": "01410502", + "type": "Card" + }, + { + "amiiboSeries": "Fire Emblem", + "character": "Celica", + "gameSeries": "Fire Emblem", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Receive a Fashion Ticket and a Music Ticket, for unlocking any of the available costumes and music tracks", + "write": false + } + ], + "gameID": [ + "0100A6301214E000" + ], + "gameName": "Fire Emblem Engage" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a weapon", + "write": false + } + ], + "gameID": [ + "0100F15003E64000" + ], + "gameName": "Fire Emblem Warriors" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive better-quality randomized resources, weapons, or equipment", + "write": false + } + ], + "gameID": [ + "010071F0143EA000" + ], + "gameName": "Fire Emblem Warriors: Three Hopes" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special piece of battle music / Receive higher-quality items and materials", + "write": false + } + ], + "gameID": [ + "010055D009F78000" + ], + "gameName": "Fire Emblem: Three Houses" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a Spirit of the character", + "write": false + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "21070000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_21070000-03611202.png", + "name": "Celica", + "release": { + "au": "2017-05-20", + "eu": "2017-05-19", + "jp": "2017-04-20", + "na": "2017-05-19" + }, + "tail": "03611202", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Deena", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "030b0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_030b0001-00790502.png", + "name": "Deena", + "release": { + "au": "2015-10-03", + "eu": "2015-10-02", + "jp": "2015-07-30", + "na": "2015-09-25" + }, + "tail": "00790502", + "type": "Card" + }, + { + "amiiboSeries": "Super Smash Bros.", + "character": "Ivysaur", + "gameSeries": "Pokemon", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "19020000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_19020000-03830002.png", + "name": "Ivysaur", + "release": { + "au": "2019-09-20", + "eu": "2019-09-20", + "jp": "2019-09-20", + "na": "2019-09-20" + }, + "tail": "03830002", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Filbert", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "04df0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_04df0001-00e80502.png", + "name": "Filbert", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-10-29", + "na": "2016-01-22" + }, + "tail": "00e80502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Velma", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "035c0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_035c0001-01290502.png", + "name": "Velma", + "release": { + "au": "2016-03-19", + "eu": "2016-03-18", + "jp": "2016-01-14", + "na": "2016-03-18" + }, + "tail": "01290502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Beardo", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "02210001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_02210001-013c0502.png", + "name": "Beardo", + "release": { + "au": "2016-03-19", + "eu": "2016-03-18", + "jp": "2016-01-14", + "na": "2016-03-18" + }, + "tail": "013c0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Azalea", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "0a1e0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_0a1e0001-03d50502.png", + "name": "Azalea", + "release": { + "au": "2021-11-05", + "eu": "2021-11-05", + "jp": "2021-11-05", + "na": "2021-11-05" + }, + "tail": "03d50502", + "type": "Card" + }, + { + "amiiboSeries": "Super Smash Bros.", + "character": "Samus", + "gameSeries": "Metroid", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock a costume based on the character (short-hair version)", + "write": false + } + ], + "gameID": [ + "01007960049A0000" + ], + "gameName": "Bayonetta 2" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a Metroid-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Restore a random amount of health once per day", + "write": false + } + ], + "gameID": [ + "010093801237C000" + ], + "gameName": "Metroid Dread" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01003DA010E8A000" + ], + "gameName": "Miitopia" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "05c00000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_05c00000-00060002.png", + "name": "Samus", + "release": { + "au": "2014-11-29", + "eu": "2014-11-28", + "jp": "2014-12-06", + "na": "2014-11-21" + }, + "tail": "00060002", + "type": "Figure" + }, + { + "amiiboSeries": "Super Smash Bros.", + "character": "Simon", + "gameSeries": "Castlevania", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "37c00000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_37c00000-038b0002.png", + "name": "Simon", + "release": { + "au": "2019-11-15", + "eu": "2019-11-15", + "jp": "2019-11-08", + "na": "2019-11-15" + }, + "tail": "038b0002", + "type": "Figure" + }, + { + "amiiboSeries": "Monster Hunter Rise", + "character": "Magnamalo", + "gameSeries": "Monster Hunter", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock special layered armor / Enter daily lottery for a variety of useful items", + "write": false + } + ], + "gameID": [ + "0100B04011742000" + ], + "gameName": "Monster Hunter Rise" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock the Hunter Sticker Set / Have Tsukino read your fortune and receive a random item", + "write": false + } + ], + "gameID": [ + "0100E21011446000" + ], + "gameName": "Monster Hunter Stories 2: Wings of Ruin" + } + ], + "head": "35080000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_35080000-040f1802.png", + "name": "Magnamalo", + "release": { + "au": "2021-03-26", + "eu": "2021-03-26", + "jp": "2021-03-26", + "na": "2021-03-26" + }, + "tail": "040f1802", + "type": "Figure" + }, + { + "amiiboSeries": "Mario Sports Superstars", + "character": "Rosalina", + "gameSeries": "Mario Sports Superstars", + "gamesSwitch": [], + "head": "09cf0301", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_09cf0301-02b60e02.png", + "name": "Rosalina - Tennis", + "release": { + "au": "2017-03-11", + "eu": "2017-03-10", + "jp": "2017-03-30", + "na": "2017-03-24" + }, + "tail": "02b60e02", + "type": "Card" + }, + { + "amiiboSeries": "Mario Sports Superstars", + "character": "Boo", + "gameSeries": "Mario Sports Superstars", + "gamesSwitch": [], + "head": "09cb0401", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_09cb0401-02a30e02.png", + "name": "Boo - Golf", + "release": { + "au": "2017-03-11", + "eu": "2017-03-10", + "jp": "2017-03-30", + "na": "2017-03-24" + }, + "tail": "02a30e02", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Flip", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "03ff0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_03ff0001-00f40502.png", + "name": "Flip", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-10-29", + "na": "2016-01-22" + }, + "tail": "00f40502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Hazel", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "04ef0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_04ef0001-013b0502.png", + "name": "Hazel", + "release": { + "au": "2016-03-19", + "eu": "2016-03-18", + "jp": "2016-01-14", + "na": "2016-03-18" + }, + "tail": "013b0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Bubbles", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "03920001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_03920001-01270502.png", + "name": "Bubbles", + "release": { + "au": "2016-03-19", + "eu": "2016-03-18", + "jp": "2016-01-14", + "na": "2016-03-18" + }, + "tail": "01270502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Elmer", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "03a70001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_03a70001-01a10502.png", + "name": "Elmer", + "release": { + "au": "2016-06-18", + "eu": null, + "jp": "2016-03-24", + "na": "2016-06-10" + }, + "tail": "01a10502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Boots", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "02c50001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_02c50001-03080502.png", + "name": "Boots", + "release": { + "au": "2016-11-10", + "eu": "2016-11-11", + "jp": "2016-11-03", + "na": "2016-12-02" + }, + "tail": "03080502", + "type": "Card" + }, + { + "amiiboSeries": "Skylanders", + "character": "Bowser", + "gameSeries": "Super Mario", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock the Chain Chomp weapon", + "write": false + } + ], + "gameID": [ + "01007960049A0000" + ], + "gameName": "Bayonetta 2" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01003DA010E8A000" + ], + "gameName": "Miitopia" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a particular power-up depending on the character", + "write": false + }, + { + "Usage": "Make Fury Bowser appear (in Bowser's Fury mode)", + "write": false + } + ], + "gameID": [ + "010028600EBDA000" + ], + "gameName": "Super Mario 3D World + Bowser's Fury" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock the character's shiny sticker", + "write": false + } + ], + "gameID": [ + "010036B0034E4000" + ], + "gameName": "Super Mario Party" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01006000040C2000" + ], + "gameName": "Yoshi's Crafted World" + } + ], + "head": "0005ff00", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_0005ff00-023a0702.png", + "name": "Hammer Slam Bowser", + "release": { + "au": "2015-09-24", + "eu": "2015-09-25", + "jp": null, + "na": "2015-09-20" + }, + "tail": "023a0702", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Bea", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "02f40001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_02f40001-03050502.png", + "name": "Bea", + "release": { + "au": "2016-11-10", + "eu": "2016-11-11", + "jp": "2016-11-03", + "na": "2016-12-02" + }, + "tail": "03050502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Boone", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "036b0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_036b0001-018b0502.png", + "name": "Boone", + "release": { + "au": "2016-06-18", + "eu": null, + "jp": "2016-03-24", + "na": "2016-06-10" + }, + "tail": "018b0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Wardell", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "0a080001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_0a080001-03bd0502.png", + "name": "Wardell", + "release": { + "au": "2021-11-05", + "eu": "2021-11-05", + "jp": "2021-11-05", + "na": "2021-11-05" + }, + "tail": "03bd0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Petri", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "0a160001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_0a160001-03cd0502.png", + "name": "Petri", + "release": { + "au": "2021-11-05", + "eu": "2021-11-05", + "jp": "2021-11-05", + "na": "2021-11-05" + }, + "tail": "03cd0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Ace", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "0a1b0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_0a1b0001-03d20502.png", + "name": "Ace", + "release": { + "au": "2021-11-05", + "eu": "2021-11-05", + "jp": "2021-11-05", + "na": "2021-11-05" + }, + "tail": "03d20502", + "type": "Card" + }, + { + "amiiboSeries": "Super Smash Bros.", + "character": "Mii", + "gameSeries": "Mii", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "07c00100", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_07c00100-00220002.png", + "name": "Mii Swordfighter", + "release": { + "au": "2015-09-26", + "eu": "2015-09-25", + "jp": "2015-09-10", + "na": "2015-11-01" + }, + "tail": "00220002", + "type": "Figure" + }, + { + "amiiboSeries": "Yu-Gi-Oh!", + "character": "Gakuto S\u014dgetsu", + "gameSeries": "Yu-Gi-Oh!", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Receive items/bonuses", + "write": false + } + ], + "gameID": [ + "01003C101454A000" + ], + "gameName": "Yu-Gi-Oh! Rush Duel Saikyo Battle Royale" + } + ], + "head": "38420001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_38420001-04261902.png", + "name": "Gakuto S\u014dgetsu", + "release": { + "au": null, + "eu": null, + "jp": null, + "na": "2021-08-12" + }, + "tail": "04261902", + "type": "Card" + }, + { + "amiiboSeries": "Super Smash Bros.", + "character": "Terry", + "gameSeries": "Fatal Fury", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "3c800000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_3c800000-03a40002.png", + "name": "Terry", + "release": { + "au": "2021-03-26", + "eu": "2021-03-26", + "jp": "2021-03-26", + "na": "2021-03-26" + }, + "tail": "03a40002", + "type": "Figure" + }, + { + "amiiboSeries": "Mario Sports Superstars", + "character": "Daisy", + "gameSeries": "Mario Sports Superstars", + "gamesSwitch": [], + "head": "09c30501", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_09c30501-027c0e02.png", + "name": "Daisy - Horse Racing", + "release": { + "au": "2017-03-11", + "eu": "2017-03-10", + "jp": "2017-03-30", + "na": "2017-03-24" + }, + "tail": "027c0e02", + "type": "Card" + }, + { + "amiiboSeries": "Super Smash Bros.", + "character": "Snake", + "gameSeries": "Metal Gear Solid", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "37800000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_37800000-038a0002.png", + "name": "Snake", + "release": { + "au": "2019-09-20", + "eu": "2019-09-20", + "jp": "2019-09-20", + "na": "2019-09-20" + }, + "tail": "038a0002", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Franklin", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "01ae0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01ae0001-011b0502.png", + "name": "Franklin", + "release": { + "au": "2016-03-19", + "eu": "2016-03-18", + "jp": "2016-01-14", + "na": "2016-03-18" + }, + "tail": "011b0502", + "type": "Card" + }, + { + "amiiboSeries": "Pokemon", + "character": "Detective Pikachu", + "gameSeries": "Pokemon", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Receive a Spirit of the character", + "write": false + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "1d010000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_1d010000-03750d02.png", + "name": "Detective Pikachu", + "release": { + "au": "2018-03-24", + "eu": "2018-03-23", + "jp": "2018-03-23", + "na": "2018-03-23" + }, + "tail": "03750d02", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Hopper", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "04620001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_04620001-00f60502.png", + "name": "Hopper", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-10-29", + "na": "2016-01-22" + }, + "tail": "00f60502", + "type": "Card" + }, + { + "amiiboSeries": "Monster Hunter Rise", + "character": "Palamute", + "gameSeries": "Monster Hunter", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock special layered armor / Enter daily lottery for a variety of useful items", + "write": false + } + ], + "gameID": [ + "0100B04011742000" + ], + "gameName": "Monster Hunter Rise" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock the Hunter Sticker Set / Have Tsukino read your fortune and receive a random item", + "write": false + } + ], + "gameID": [ + "0100E21011446000" + ], + "gameName": "Monster Hunter Stories 2: Wings of Ruin" + } + ], + "head": "350a0100", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_350a0100-042c1802.png", + "name": "Palamute", + "release": { + "au": "2022-06-30", + "eu": "2022-06-30", + "jp": "2022-06-30", + "na": "2022-06-30" + }, + "tail": "042c1802", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Colton", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "03af0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_03af0001-012c0502.png", + "name": "Colton", + "release": { + "au": "2016-03-19", + "eu": "2016-03-18", + "jp": "2016-01-14", + "na": "2016-03-18" + }, + "tail": "012c0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Phoebe", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "04400001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_04400001-00ca0502.png", + "name": "Phoebe", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-10-29", + "na": "2016-01-22" + }, + "tail": "00ca0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Ursala", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "021c0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_021c0001-02f70502.png", + "name": "Ursala", + "release": { + "au": "2016-11-10", + "eu": "2016-11-11", + "jp": "2016-11-03", + "na": "2016-12-02" + }, + "tail": "02f70502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Gayle", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "02ca0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_02ca0001-01ca0502.png", + "name": "Gayle", + "release": { + "au": "2016-06-18", + "eu": null, + "jp": "2016-03-24", + "na": "2016-06-10" + }, + "tail": "01ca0502", + "type": "Card" + }, + { + "amiiboSeries": "Super Smash Bros.", + "character": "Daisy", + "gameSeries": "Super Mario", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock a costume based on the character (short-hair version)", + "write": false + } + ], + "gameID": [ + "01007960049A0000" + ], + "gameName": "Bayonetta 2" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01003DA010E8A000" + ], + "gameName": "Miitopia" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a particular power-up depending on the character", + "write": false + } + ], + "gameID": [ + "010028600EBDA000" + ], + "gameName": "Super Mario 3D World + Bowser's Fury" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock the character's shiny sticker", + "write": false + } + ], + "gameID": [ + "010036B0034E4000" + ], + "gameName": "Super Mario Party" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "00130000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_00130000-037a0002.png", + "name": "Daisy", + "release": { + "au": "2019-04-12", + "eu": "2019-04-12", + "jp": "2019-04-12", + "na": "2019-04-12" + }, + "tail": "037a0002", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Lucy", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "047c0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_047c0001-01a00502.png", + "name": "Lucy", + "release": { + "au": "2016-06-18", + "eu": null, + "jp": "2016-03-24", + "na": "2016-06-10" + }, + "tail": "01a00502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Sylvana", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "04eb0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_04eb0001-02f00502.png", + "name": "Sylvana", + "release": { + "au": "2016-11-10", + "eu": "2016-11-11", + "jp": "2016-11-03", + "na": "2016-12-02" + }, + "tail": "02f00502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Billy", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "03580001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_03580001-02fa0502.png", + "name": "Billy", + "release": { + "au": "2016-11-10", + "eu": "2016-11-11", + "jp": "2016-11-03", + "na": "2016-12-02" + }, + "tail": "02fa0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Tutu", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "021b0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_021b0001-00800502.png", + "name": "Tutu", + "release": { + "au": "2015-10-03", + "eu": "2015-10-02", + "jp": "2015-07-30", + "na": "2015-09-25" + }, + "tail": "00800502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Niko", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "0a070001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_0a070001-03bc0502.png", + "name": "Niko", + "release": { + "au": "2021-11-05", + "eu": "2021-11-05", + "jp": "2021-11-05", + "na": "2021-11-05" + }, + "tail": "03bc0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Grizzly", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "021d0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_021d0001-01cd0502.png", + "name": "Grizzly", + "release": { + "au": "2016-06-18", + "eu": null, + "jp": "2016-03-24", + "na": "2016-06-10" + }, + "tail": "01cd0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Isabelle", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "01810001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01810001-00440502.png", + "name": "Isabelle", + "release": { + "au": "2015-10-03", + "eu": "2015-10-02", + "jp": "2015-07-30", + "na": "2015-09-25" + }, + "tail": "00440502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Hopkins", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "04a20001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_04a20001-02e80502.png", + "name": "Hopkins", + "release": { + "au": "2016-11-10", + "eu": "2016-11-11", + "jp": "2016-11-03", + "na": "2016-12-02" + }, + "tail": "02e80502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Scoot", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "03110001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_03110001-00d60502.png", + "name": "Scoot", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-10-29", + "na": "2016-01-22" + }, + "tail": "00d60502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Sydney", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "03bf0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_03bf0001-01bc0502.png", + "name": "Sydney", + "release": { + "au": "2016-06-18", + "eu": null, + "jp": "2016-03-24", + "na": "2016-06-10" + }, + "tail": "01bc0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Candi", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "04140001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_04140001-030a0502.png", + "name": "Candi", + "release": { + "au": "2016-11-10", + "eu": "2016-11-11", + "jp": "2016-11-03", + "na": "2016-12-02" + }, + "tail": "030a0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "\u00c9toile", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + }, + { + "Usage": "Unlock special furniture items and a poster based on the card's Sanrio character", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "04d30101", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_04d30101-031b0502.png", + "name": "\u00c9toile", + "release": { + "au": null, + "eu": "2016-11-25", + "jp": "2016-11-03", + "na": null + }, + "tail": "031b0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Axel", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "03290001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_03290001-009d0502.png", + "name": "Axel", + "release": { + "au": "2015-10-03", + "eu": "2015-10-02", + "jp": "2015-07-30", + "na": "2015-09-25" + }, + "tail": "009d0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Rowan", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "04fb0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_04fb0001-01c60502.png", + "name": "Rowan", + "release": { + "au": "2016-06-18", + "eu": null, + "jp": "2016-03-24", + "na": "2016-06-10" + }, + "tail": "01c60502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Rosie", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "025f0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_025f0001-01d70502.png", + "name": "Rosie - Amiibo Festival", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-11-21", + "na": "2015-11-13" + }, + "tail": "01d70502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Kicks", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a Spirit of the character", + "write": false + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "01940000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01940000-024a0502.png", + "name": "Kicks", + "release": { + "au": "2016-01-30", + "eu": "2016-01-29", + "jp": "2015-12-17", + "na": "2016-01-22" + }, + "tail": "024a0502", + "type": "Figure" + }, + { + "amiiboSeries": "Monster Hunter", + "character": "One-Eyed Rathalos", + "gameSeries": "Monster Hunter", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock Monster Hunter Stories 2 sticker set", + "write": false + } + ], + "gameID": [ + "0100B04011742000" + ], + "gameName": "Monster Hunter Rise" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a character-based monstie egg", + "write": false + } + ], + "gameID": [ + "010069301B1D4000" + ], + "gameName": "Monster Hunter Stories" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock the Hakum Rider Outfit layered armor set / Have Tsukino read your fortune and receive a random item", + "write": false + } + ], + "gameID": [ + "0100E21011446000" + ], + "gameName": "Monster Hunter Stories 2: Wings of Ruin" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a lot of BP / Receive better supplies (compared to other amiibo)", + "write": false + } + ], + "gameID": [ + "0100643002136000" + ], + "gameName": "Resident Evil Revelations" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a lot of BP / Receive better supplies (compared to other amiibo)", + "write": false + } + ], + "gameID": [ + "010095300212A000" + ], + "gameName": "Resident Evil Revelations 2" + } + ], + "head": "35000200", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_35000200-02e20f02.png", + "name": "One-Eyed Rathalos and Rider - Female", + "release": { + "au": null, + "eu": null, + "jp": "2016-10-08", + "na": null + }, + "tail": "02e20f02", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Copper", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "019d0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_019d0001-00ac0502.png", + "name": "Copper", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-10-29", + "na": "2016-01-22" + }, + "tail": "00ac0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Admiral", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + } + ], + "head": "02330001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_02330001-03060502.png", + "name": "Admiral", + "release": { + "au": "2016-11-10", + "eu": "2016-11-11", + "jp": "2016-11-03", + "na": "2016-12-02" + }, + "tail": "03060502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Cranston", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "043c0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_043c0001-01cb0502.png", + "name": "Cranston", + "release": { + "au": "2016-06-18", + "eu": null, + "jp": "2016-03-24", + "na": "2016-06-10" + }, + "tail": "01cb0502", + "type": "Card" + }, + { + "amiiboSeries": "Super Smash Bros.", + "character": "Wolf", + "gameSeries": "Star Fox", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "05840000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_05840000-037e0002.png", + "name": "Wolf", + "release": { + "au": "2018-12-07", + "eu": "2018-12-07", + "jp": "2018-12-07", + "na": "2018-12-07" + }, + "tail": "037e0002", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Apple", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "037f0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_037f0001-01aa0502.png", + "name": "Apple", + "release": { + "au": "2016-06-18", + "eu": null, + "jp": "2016-03-24", + "na": "2016-06-10" + }, + "tail": "01aa0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Resetti", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01003DA010E8A000" + ], + "gameName": "Miitopia" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a Spirit of the character", + "write": false + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "018e0000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_018e0000-02490502.png", + "name": "Resetti", + "release": { + "au": "2016-01-30", + "eu": "2016-01-29", + "jp": "2015-12-17", + "na": "2016-01-22" + }, + "tail": "02490502", + "type": "Figure" + }, + { + "amiiboSeries": "Mario Sports Superstars", + "character": "Daisy", + "gameSeries": "Mario Sports Superstars", + "gamesSwitch": [], + "head": "09c30101", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_09c30101-02780e02.png", + "name": "Daisy - Soccer", + "release": { + "au": "2017-03-11", + "eu": "2017-03-10", + "jp": "2017-03-30", + "na": "2017-03-24" + }, + "tail": "02780e02", + "type": "Card" + }, + { + "amiiboSeries": "Super Smash Bros.", + "character": "Pikachu", + "gameSeries": "Pokemon", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "19190000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_19190000-00090002.png", + "name": "Pikachu", + "release": { + "au": "2014-11-29", + "eu": "2014-11-28", + "jp": "2014-12-06", + "na": "2014-11-21" + }, + "tail": "00090002", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Monty", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "03fd0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_03fd0001-01580502.png", + "name": "Monty", + "release": { + "au": "2016-03-19", + "eu": "2016-03-18", + "jp": "2016-01-14", + "na": "2016-03-18" + }, + "tail": "01580502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Snooty", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "02060001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_02060001-03120502.png", + "name": "Snooty", + "release": { + "au": "2016-11-10", + "eu": "2016-11-11", + "jp": "2016-11-03", + "na": "2016-12-02" + }, + "tail": "03120502", + "type": "Card" + }, + { + "amiiboSeries": "Yoshi's Woolly World", + "character": "Yoshi", + "gameSeries": "Super Mario", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an amiibo-exclusive weapon for the character and their Rabbid counterpart", + "write": false + } + ], + "gameID": [ + "010067300059A000" + ], + "gameName": "Mario + Rabbids: Kingdom Battle" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01003DA010E8A000" + ], + "gameName": "Miitopia" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a particular power-up depending on the character", + "write": false + } + ], + "gameID": [ + "010028600EBDA000" + ], + "gameName": "Super Mario 3D World + Bowser's Fury" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock the character's shiny sticker", + "write": false + } + ], + "gameID": [ + "010036B0034E4000" + ], + "gameName": "Super Mario Party" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01006000040C2000" + ], + "gameName": "Yoshi's Crafted World" + } + ], + "head": "00030102", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_00030102-023e0302.png", + "name": "Mega Yarn Yoshi", + "release": { + "au": "2015-11-28", + "eu": "2015-11-27", + "jp": "2015-12-10", + "na": "2015-11-15" + }, + "tail": "023e0302", + "type": "Yarn" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Plucky", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "02a30001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_02a30001-02ff0502.png", + "name": "Plucky", + "release": { + "au": "2016-11-10", + "eu": "2016-11-11", + "jp": "2016-11-03", + "na": "2016-12-02" + }, + "tail": "02ff0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Genji", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "049c0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_049c0001-01400502.png", + "name": "Genji", + "release": { + "au": "2016-03-19", + "eu": "2016-03-18", + "jp": "2016-01-14", + "na": "2016-03-18" + }, + "tail": "01400502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Timmy", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "01850001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01850001-004b0502.png", + "name": "Timmy", + "release": { + "au": "2015-10-03", + "eu": "2015-10-02", + "jp": "2015-07-30", + "na": "2015-09-25" + }, + "tail": "004b0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Ketchup", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "03140001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_03140001-02f40502.png", + "name": "Ketchup", + "release": { + "au": "2016-11-10", + "eu": "2016-11-11", + "jp": "2016-11-03", + "na": "2016-12-02" + }, + "tail": "02f40502", + "type": "Card" + }, + { + "amiiboSeries": "Legend Of Zelda", + "character": "Midna", + "gameSeries": "The Legend of Zelda", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive specific crafting materials or a weapon for the character", + "write": false + } + ], + "gameID": [ + "01002B00111A2000" + ], + "gameName": "Hyrule Warriors: Age of Calamity" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a weapon rated 3 stars or higher", + "write": false + } + ], + "gameID": [ + "0100AE00096EA000" + ], + "gameName": "Hyrule Warriors: Definitive Edition" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a Legend of Zelda-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a Spirit of the character", + "write": false + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a chest of loot, potentially containing gear inspired by the Legend of Zelda series", + "write": false + } + ], + "gameID": [ + "01000A10041EA000" + ], + "gameName": "The Elder Scrolls V: Skyrim" + }, + { + "amiiboUsage": [ + { + "Usage": "Bring Wolf Link into the game as a partner character", + "write": false + } + ], + "gameID": [ + "01007EF00011E000" + ], + "gameName": "The Legend of Zelda: Breath of the Wild" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock one of five amiibo-exclusive Chamber Dungeon chambers", + "write": false + }, + { + "Usage": "Save your Chamber Dungeon to the amiibo to share with friends", + "write": true + } + ], + "gameID": [ + "01006BB00C6F0000" + ], + "gameName": "The Legend of Zelda: Link's Awakening" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an amiibo-exclusive character-specific paraglider fabric", + "write": false + }, + { + "Usage": "Receive materials and a weapon or rare item", + "write": false + } + ], + "gameID": [ + "0100F2C0115B6000" + ], + "gameName": "The Legend of Zelda: Tears of the Kingdom" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive the Red Tunic", + "write": false + }, + { + "Usage": "Receive random materials", + "write": false + } + ], + "gameID": [ + "01008CF01BAAC000" + ], + "gameName": "The Legend of Zelda: Echoes of Wisdom" + } + ], + "head": "01030000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01030000-024f0902.png", + "name": "Midna & Wolf Link", + "release": { + "au": "2016-03-05", + "eu": "2016-03-04", + "jp": "2016-03-10", + "na": "2016-03-04" + }, + "tail": "024f0902", + "type": "Figure" + }, + { + "amiiboSeries": "Super Smash Bros.", + "character": "Luigi", + "gameSeries": "Super Mario", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock an amiibo-exclusive weapon for the character and their Rabbid counterpart", + "write": false + } + ], + "gameID": [ + "010067300059A000" + ], + "gameName": "Mario + Rabbids: Kingdom Battle" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01003DA010E8A000" + ], + "gameName": "Miitopia" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a particular power-up depending on the character", + "write": false + } + ], + "gameID": [ + "010028600EBDA000" + ], + "gameName": "Super Mario 3D World + Bowser's Fury" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a character-based costume", + "write": false + } + ], + "gameID": [ + "0100000000010000" + ], + "gameName": "Super Mario Odyssey" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock the character's shiny sticker", + "write": false + } + ], + "gameID": [ + "010036B0034E4000" + ], + "gameName": "Super Mario Party" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01006000040C2000" + ], + "gameName": "Yoshi's Crafted World" + } + ], + "head": "00010000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_00010000-000c0002.png", + "name": "Luigi", + "release": { + "au": "2014-12-12", + "eu": "2014-12-19", + "jp": "2014-12-06", + "na": "2014-12-14" + }, + "tail": "000c0002", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Label", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "01890101", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01890101-03b10502.png", + "name": "Label", + "release": { + "au": "2021-11-05", + "eu": "2021-11-05", + "jp": "2021-11-05", + "na": "2021-11-05" + }, + "tail": "03b10502", + "type": "Card" + }, + { + "amiiboSeries": "Super Smash Bros.", + "character": "Steve", + "gameSeries": "Minecraft", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "3dc00000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_3dc00000-04220002.png", + "name": "Steve", + "release": { + "au": "2022-09-09", + "eu": "2022-09-09", + "jp": "2022-09-09", + "na": "2022-09-09" + }, + "tail": "04220002", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Timmy & Tommy", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a Spirit of the character", + "write": false + }, + { + "Usage": "Unlock Timmy & Tommy's shop early", + "write": false + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "01840000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01840000-024d0502.png", + "name": "Timmy & Tommy", + "release": { + "au": "2016-03-19", + "eu": "2016-03-18", + "jp": "2016-03-24", + "na": "2016-03-18" + }, + "tail": "024d0502", + "type": "Figure" + }, + { + "amiiboSeries": "Super Smash Bros.", + "character": "Marth", + "gameSeries": "Fire Emblem", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Receive a Fashion Ticket and a Music Ticket, for unlocking any of the available costumes and music tracks", + "write": false + } + ], + "gameID": [ + "0100A6301214E000" + ], + "gameName": "Fire Emblem Engage" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a weapon", + "write": false + } + ], + "gameID": [ + "0100F15003E64000" + ], + "gameName": "Fire Emblem Warriors" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive better-quality randomized resources, weapons, or equipment", + "write": false + } + ], + "gameID": [ + "010071F0143EA000" + ], + "gameName": "Fire Emblem Warriors: Three Hopes" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special piece of battle music / Receive higher-quality items and materials", + "write": false + } + ], + "gameID": [ + "010055D009F78000" + ], + "gameName": "Fire Emblem: Three Houses" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "21000000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_21000000-000b0002.png", + "name": "Marth", + "release": { + "au": "2014-11-29", + "eu": "2014-11-28", + "jp": "2014-12-06", + "na": "2014-11-21" + }, + "tail": "000b0002", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Porter", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "01950001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01950001-00b00502.png", + "name": "Porter", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-10-29", + "na": "2016-01-22" + }, + "tail": "00b00502", + "type": "Card" + }, + { + "amiiboSeries": "Super Mario Bros.", + "character": "Yoshi", + "gameSeries": "Super Mario", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an amiibo-exclusive weapon for the character and their Rabbid counterpart", + "write": false + } + ], + "gameID": [ + "010067300059A000" + ], + "gameName": "Mario + Rabbids: Kingdom Battle" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01003DA010E8A000" + ], + "gameName": "Miitopia" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a particular power-up depending on the character", + "write": false + } + ], + "gameID": [ + "010028600EBDA000" + ], + "gameName": "Super Mario 3D World + Bowser's Fury" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock the character's shiny sticker", + "write": false + } + ], + "gameID": [ + "010036B0034E4000" + ], + "gameName": "Super Mario Party" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01006000040C2000" + ], + "gameName": "Yoshi's Crafted World" + } + ], + "head": "00030000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_00030000-00370102.png", + "name": "Yoshi", + "release": { + "au": "2015-03-21", + "eu": "2015-03-20", + "jp": "2015-03-12", + "na": "2015-03-20" + }, + "tail": "00370102", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Derwin", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "030f0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_030f0001-016d0502.png", + "name": "Derwin", + "release": { + "au": "2016-03-19", + "eu": "2016-03-18", + "jp": "2016-01-14", + "na": "2016-03-18" + }, + "tail": "016d0502", + "type": "Card" + }, + { + "amiiboSeries": "Super Smash Bros.", + "character": "R.O.B.", + "gameSeries": "Classic Nintendo", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "07810000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_07810000-002e0002.png", + "name": "R.O.B. - Famicom", + "release": { + "au": "2016-03-19", + "eu": "2016-03-18", + "jp": "2015-10-29", + "na": "2016-03-18" + }, + "tail": "002e0002", + "type": "Figure" + }, + { + "amiiboSeries": "Super Smash Bros.", + "character": "Cloud Strife", + "gameSeries": "Final Fantasy", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "36000100", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_36000100-03620002.png", + "name": "Cloud - Player 2", + "release": { + "au": "2017-07-22", + "eu": "2017-07-21", + "jp": "2017-07-21", + "na": "2017-07-21" + }, + "tail": "03620002", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Olaf", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "02090001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_02090001-019f0502.png", + "name": "Olaf", + "release": { + "au": "2016-06-18", + "eu": null, + "jp": "2016-03-24", + "na": "2016-06-10" + }, + "tail": "019f0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Rhonda", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "04b30001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_04b30001-00dd0502.png", + "name": "Rhonda", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-10-29", + "na": "2016-01-22" + }, + "tail": "00dd0502", + "type": "Card" + }, + { + "amiiboSeries": "Mario Sports Superstars", + "character": "Waluigi", + "gameSeries": "Mario Sports Superstars", + "gamesSwitch": [], + "head": "09c60401", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_09c60401-028a0e02.png", + "name": "Waluigi - Golf", + "release": { + "au": "2017-03-11", + "eu": "2017-03-10", + "jp": "2017-03-30", + "na": "2017-03-24" + }, + "tail": "028a0e02", + "type": "Card" + }, + { + "amiiboSeries": "Fire Emblem", + "character": "Chrom", + "gameSeries": "Fire Emblem", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Receive a Fashion Ticket and a Music Ticket, for unlocking any of the available costumes and music tracks", + "write": false + } + ], + "gameID": [ + "0100A6301214E000" + ], + "gameName": "Fire Emblem Engage" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an amiibo-exclusive weapon", + "write": false + }, + { + "Usage": "Receive a weapon", + "write": false + } + ], + "gameID": [ + "0100F15003E64000" + ], + "gameName": "Fire Emblem Warriors" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive better-quality randomized resources, weapons, or equipment", + "write": false + } + ], + "gameID": [ + "010071F0143EA000" + ], + "gameName": "Fire Emblem Warriors: Three Hopes" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special piece of battle music / Receive higher-quality items and materials", + "write": false + } + ], + "gameID": [ + "010055D009F78000" + ], + "gameName": "Fire Emblem: Three Houses" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "21080000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_21080000-036f1202.png", + "name": "Chrom", + "release": { + "au": "2017-10-20", + "eu": "2017-10-20", + "jp": "2017-09-28", + "na": "2017-10-20" + }, + "tail": "036f1202", + "type": "Figure" + }, + { + "amiiboSeries": "Mario Sports Superstars", + "character": "Birdo", + "gameSeries": "Mario Sports Superstars", + "gamesSwitch": [], + "head": "09ce0401", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_09ce0401-02b20e02.png", + "name": "Birdo - Golf", + "release": { + "au": "2017-03-11", + "eu": "2017-03-10", + "jp": "2017-03-30", + "na": "2017-03-24" + }, + "tail": "02b20e02", + "type": "Card" + }, + { + "amiiboSeries": "Mario Sports Superstars", + "character": "Wario", + "gameSeries": "Mario Sports Superstars", + "gamesSwitch": [], + "head": "09c50401", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_09c50401-02850e02.png", + "name": "Wario - Golf", + "release": { + "au": "2017-03-11", + "eu": "2017-03-10", + "jp": "2017-03-30", + "na": "2017-03-24" + }, + "tail": "02850e02", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Cesar", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "03690001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_03690001-00d30502.png", + "name": "Cesar", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-10-29", + "na": "2016-01-22" + }, + "tail": "00d30502", + "type": "Card" + }, + { + "amiiboSeries": "Others", + "character": "Solaire of Astora", + "gameSeries": "Dark Souls", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock and perform the \u201cPraise the Sun\u201d gesture", + "write": false + } + ], + "gameID": [ + "01004AB00A260000" + ], + "gameName": "Dark Souls: Remastered" + } + ], + "head": "33800000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_33800000-03781402.png", + "name": "Solaire of Astora", + "release": { + "au": "2018-10-19", + "eu": "2018-10-19", + "jp": "2018-10-18", + "na": "2018-10-19" + }, + "tail": "03781402", + "type": "Figure" + }, + { + "amiiboSeries": "Yu-Gi-Oh!", + "character": "Roa Kirishima", + "gameSeries": "Yu-Gi-Oh!", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Receive items/bonuses", + "write": false + } + ], + "gameID": [ + "01003C101454A000" + ], + "gameName": "Yu-Gi-Oh! Rush Duel Saikyo Battle Royale" + } + ], + "head": "38440001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_38440001-04281902.png", + "name": "Roa Kirishima", + "release": { + "au": null, + "eu": null, + "jp": null, + "na": "2021-08-12" + }, + "tail": "04281902", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Pinky", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "02150001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_02150001-01820502.png", + "name": "Pinky", + "release": { + "au": "2016-06-18", + "eu": null, + "jp": "2016-03-24", + "na": "2016-06-10" + }, + "tail": "01820502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Olive", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "02860001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_02860001-03130502.png", + "name": "Olive", + "release": { + "au": "2016-11-10", + "eu": "2016-11-11", + "jp": "2016-11-03", + "na": "2016-12-02" + }, + "tail": "03130502", + "type": "Card" + }, + { + "amiiboSeries": "Super Smash Bros.", + "character": "Ice Climbers", + "gameSeries": "Classic Nintendo", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "078f0000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_078f0000-03810002.png", + "name": "Ice Climbers", + "release": { + "au": "2019-02-15", + "eu": "2019-02-15", + "jp": "2019-02-15", + "na": "2019-02-15" + }, + "tail": "03810002", + "type": "Figure" + }, + { + "amiiboSeries": "Yu-Gi-Oh!", + "character": "Asana Mutsuba", + "gameSeries": "Yu-Gi-Oh!", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Receive items/bonuses", + "write": false + } + ], + "gameID": [ + "01003C101454A000" + ], + "gameName": "Yu-Gi-Oh! Rush Duel Saikyo Battle Royale" + } + ], + "head": "38460001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_38460001-042a1902.png", + "name": "Asana Mutsuba", + "release": { + "au": null, + "eu": null, + "jp": null, + "na": "2021-08-12" + }, + "tail": "042a1902", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Tom Nook", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "01830101", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01830101-010e0502.png", + "name": "Tom Nook - Jacket", + "release": { + "au": "2016-03-19", + "eu": "2016-03-18", + "jp": "2016-01-14", + "na": "2016-03-18" + }, + "tail": "010e0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Anicotti", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "04160001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_04160001-00fb0502.png", + "name": "Anicotti", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-10-29", + "na": "2016-01-22" + }, + "tail": "00fb0502", + "type": "Card" + }, + { + "amiiboSeries": "Mario Sports Superstars", + "character": "Mario", + "gameSeries": "Mario Sports Superstars", + "gamesSwitch": [], + "head": "09c00101", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_09c00101-02690e02.png", + "name": "Mario - Soccer", + "release": { + "au": "2017-03-11", + "eu": "2017-03-10", + "jp": "2017-03-30", + "na": "2017-03-24" + }, + "tail": "02690e02", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Croque", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "03490001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_03490001-018d0502.png", + "name": "Croque", + "release": { + "au": "2016-06-18", + "eu": null, + "jp": "2016-03-24", + "na": "2016-06-10" + }, + "tail": "018d0502", + "type": "Card" + }, + { + "amiiboSeries": "Legend Of Zelda", + "character": "Daruk", + "gameSeries": "The Legend of Zelda", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive specific crafting materials or a weapon for the character", + "write": false + } + ], + "gameID": [ + "01002B00111A2000" + ], + "gameName": "Hyrule Warriors: Age of Calamity" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a Legend of Zelda-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a Spirit of the character", + "write": false + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a chest of loot, potentially containing gear inspired by the Legend of Zelda series", + "write": false + } + ], + "gameID": [ + "01000A10041EA000" + ], + "gameName": "The Elder Scrolls V: Skyrim" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive materials and a rare or exclusive item", + "write": false + } + ], + "gameID": [ + "01007EF00011E000" + ], + "gameName": "The Legend of Zelda: Breath of the Wild" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock one of five amiibo-exclusive Chamber Dungeon chambers", + "write": false + }, + { + "Usage": "Save your Chamber Dungeon to the amiibo to share with friends", + "write": true + } + ], + "gameID": [ + "01006BB00C6F0000" + ], + "gameName": "The Legend of Zelda: Link's Awakening" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an amiibo-exclusive character-specific paraglider fabric", + "write": false + }, + { + "Usage": "Receive materials and a weapon or rare item", + "write": false + } + ], + "gameID": [ + "0100F2C0115B6000" + ], + "gameName": "The Legend of Zelda: Tears of the Kingdom" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive the Black Cat Clothes", + "write": false + }, + { + "Usage": "Receive random materials", + "write": false + } + ], + "gameID": [ + "01008CF01BAAC000" + ], + "gameName": "The Legend of Zelda: Echoes of Wisdom" + } + ], + "head": "01050000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01050000-03580902.png", + "name": "Daruk", + "release": { + "au": "2017-11-10", + "eu": "2017-11-10", + "jp": "2017-11-10", + "na": "2017-11-10" + }, + "tail": "03580902", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Chops", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "04860001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_04860001-00fc0502.png", + "name": "Chops", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-10-29", + "na": "2016-01-22" + }, + "tail": "00fc0502", + "type": "Card" + }, + { + "amiiboSeries": "Shovel Knight", + "character": "Shovel Knight", + "gameSeries": "Shovel Knight", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock boss fight against Shovel Knight", + "write": false + } + ], + "gameID": [ + "0100192003FA4000" + ], + "gameName": "Azure Striker Gunvolt: Striker Pack" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a fairy companion and player color palette matching the character", + "write": false + } + ], + "gameID": [ + "01008D100DE46000" + ], + "gameName": "Cyber Shadow" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-specific Shovel Knight remix immediately", + "write": false + } + ], + "gameID": [ + "0100830008426000" + ], + "gameName": "Just Shapes & Beats" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock Custom Knight, and save customizations to the amiibo (Shovel of Hope only)", + "write": true + }, + { + "Usage": "Unlock character-specific challenge stages, a character-based fairy companion, and costumes for the character", + "write": false + } + ], + "gameID": [ + "010057D0021E8000" + ], + "gameName": "Shovel Knight" + }, + { + "amiiboUsage": [ + { + "Usage": "Summon a fairy friend", + "write": false + } + ], + "gameID": [ + "0100B62017E68000" + ], + "gameName": "Shovel Knight Dig" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume for the character", + "write": false + } + ], + "gameID": [ + "0100B380022AE000" + ], + "gameName": "Shovel Knight Showdown" + } + ], + "head": "35c00000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_35c00000-03920a02.png", + "name": "Shovel Knight - Gold Edition", + "release": { + "au": null, + "eu": "2019-12-10", + "jp": null, + "na": "2019-12-10" + }, + "tail": "03920a02", + "type": "Figure" + }, + { + "amiiboSeries": "Super Mario Bros.", + "character": "Bowser", + "gameSeries": "Super Mario", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock the Chain Chomp weapon", + "write": false + } + ], + "gameID": [ + "01007960049A0000" + ], + "gameName": "Bayonetta 2" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock Super Mario Odyssey-themed levels early", + "write": false + } + ], + "gameID": [ + "01009BF0072D4000" + ], + "gameName": "Captain Toad: Treasure Tracker" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01003DA010E8A000" + ], + "gameName": "Miitopia" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a particular power-up depending on the character", + "write": false + }, + { + "Usage": "Make Fury Bowser appear (in Bowser's Fury mode)", + "write": false + } + ], + "gameID": [ + "010028600EBDA000" + ], + "gameName": "Super Mario 3D World + Bowser's Fury" + }, + { + "amiiboUsage": [ + { + "Usage": "Reveal regional coin locations", + "write": false + } + ], + "gameID": [ + "0100000000010000" + ], + "gameName": "Super Mario Odyssey" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a character-based costume", + "write": false + } + ], + "gameID": [ + "0100000000010000" + ], + "gameName": "Super Mario Odyssey" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock the character's shiny sticker", + "write": false + } + ], + "gameID": [ + "010036B0034E4000" + ], + "gameName": "Super Mario Party" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a Spirit of the character", + "write": false + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01006000040C2000" + ], + "gameName": "Yoshi's Crafted World" + } + ], + "head": "00050000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_00050000-03730102.png", + "name": "Bowser - Wedding", + "release": { + "au": "2017-10-27", + "eu": "2017-10-27", + "jp": "2017-10-27", + "na": "2017-10-27" + }, + "tail": "03730102", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "K.K. Slider", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "01820101", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01820101-00460502.png", + "name": "DJ KK", + "release": { + "au": "2015-10-03", + "eu": "2015-10-02", + "jp": "2015-07-30", + "na": "2015-09-25" + }, + "tail": "00460502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Saharah", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "01a60001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01a60001-03b70502.png", + "name": "Saharah", + "release": { + "au": "2021-11-05", + "eu": "2021-11-05", + "jp": "2021-11-05", + "na": "2021-11-05" + }, + "tail": "03b70502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Jitters", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "02310001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_02310001-006a0502.png", + "name": "Jitters", + "release": { + "au": "2015-10-03", + "eu": "2015-10-02", + "jp": "2015-07-30", + "na": "2015-09-25" + }, + "tail": "006a0502", + "type": "Card" + }, + { + "amiiboSeries": "Legend Of Zelda", + "character": "Mipha", + "gameSeries": "The Legend of Zelda", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive specific crafting materials or a weapon for the character", + "write": false + } + ], + "gameID": [ + "01002B00111A2000" + ], + "gameName": "Hyrule Warriors: Age of Calamity" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a Legend of Zelda-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a Spirit of the character", + "write": false + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a chest of loot, potentially containing gear inspired by the Legend of Zelda series", + "write": false + } + ], + "gameID": [ + "01000A10041EA000" + ], + "gameName": "The Elder Scrolls V: Skyrim" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive materials and a rare or exclusive item", + "write": false + } + ], + "gameID": [ + "01007EF00011E000" + ], + "gameName": "The Legend of Zelda: Breath of the Wild" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock one of five amiibo-exclusive Chamber Dungeon chambers", + "write": false + }, + { + "Usage": "Save your Chamber Dungeon to the amiibo to share with friends", + "write": true + } + ], + "gameID": [ + "01006BB00C6F0000" + ], + "gameName": "The Legend of Zelda: Link's Awakening" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an amiibo-exclusive character-specific paraglider fabric", + "write": false + }, + { + "Usage": "Receive materials and a weapon or rare item", + "write": false + } + ], + "gameID": [ + "0100F2C0115B6000" + ], + "gameName": "The Legend of Zelda: Tears of the Kingdom" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive the Black Cat Clothes", + "write": false + }, + { + "Usage": "Receive random materials", + "write": false + } + ], + "gameID": [ + "01008CF01BAAC000" + ], + "gameName": "The Legend of Zelda: Echoes of Wisdom" + } + ], + "head": "01070000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01070000-035a0902.png", + "name": "Mipha", + "release": { + "au": "2017-11-10", + "eu": "2017-11-10", + "jp": "2017-11-10", + "na": "2017-11-10" + }, + "tail": "035a0902", + "type": "Figure" + }, + { + "amiiboSeries": "Monster Hunter", + "character": "Tsukino", + "gameSeries": "Monster Hunter", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock Monster Hunter Stories 2 sticker set", + "write": false + } + ], + "gameID": [ + "0100B04011742000" + ], + "gameName": "Monster Hunter Rise" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a character-based costume for Navirou", + "write": false + } + ], + "gameID": [ + "010069301B1D4000" + ], + "gameName": "Monster Hunter Stories" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-specific special layered armor set / Have Tsukino read your fortune and receive a random item", + "write": false + } + ], + "gameID": [ + "0100E21011446000" + ], + "gameName": "Monster Hunter Stories 2: Wings of Ruin" + } + ], + "head": "35070000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_35070000-040e0f02.png", + "name": "Tsukino", + "release": { + "au": "2021-07-09", + "eu": "2021-07-09", + "jp": "2021-07-09", + "na": "2021-07-09" + }, + "tail": "040e0f02", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Coach", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "02510001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_02510001-00c10502.png", + "name": "Coach", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-10-29", + "na": "2016-01-22" + }, + "tail": "00c10502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Erik", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "02df0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_02df0001-01910502.png", + "name": "Erik", + "release": { + "au": "2016-06-18", + "eu": null, + "jp": "2016-03-24", + "na": "2016-06-10" + }, + "tail": "01910502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Tammi", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "03fc0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_03fc0001-01470502.png", + "name": "Tammi", + "release": { + "au": "2016-03-19", + "eu": "2016-03-18", + "jp": "2016-01-14", + "na": "2016-03-18" + }, + "tail": "01470502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Gigi", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "03480001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_03480001-006b0502.png", + "name": "Gigi", + "release": { + "au": "2015-10-03", + "eu": "2015-10-02", + "jp": "2015-07-30", + "na": "2015-09-25" + }, + "tail": "006b0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Biskit", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "02ed0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_02ed0001-015a0502.png", + "name": "Biskit", + "release": { + "au": "2016-03-19", + "eu": "2016-03-18", + "jp": "2016-01-14", + "na": "2016-03-18" + }, + "tail": "015a0502", + "type": "Card" + }, + { + "amiiboSeries": "Super Smash Bros.", + "character": "Ken", + "gameSeries": "Street fighter", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "34c10000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_34c10000-03890002.png", + "name": "Ken", + "release": { + "au": "2019-04-12", + "eu": "2019-04-12", + "jp": "2019-04-12", + "na": "2019-04-12" + }, + "tail": "03890002", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Sable", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "01870001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01870001-00470502.png", + "name": "Sable", + "release": { + "au": "2015-10-03", + "eu": "2015-10-02", + "jp": "2015-07-30", + "na": "2015-09-25" + }, + "tail": "00470502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Brewster", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "01900001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01900001-01710502.png", + "name": "Brewster", + "release": { + "au": "2016-06-18", + "eu": null, + "jp": "2016-03-24", + "na": "2016-06-10" + }, + "tail": "01710502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Lyman", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "03c50001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_03c50001-015c0502.png", + "name": "Lyman", + "release": { + "au": "2016-03-19", + "eu": "2016-03-18", + "jp": "2016-01-14", + "na": "2016-03-18" + }, + "tail": "015c0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Cyrano", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "02000001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_02000001-00a10502.png", + "name": "Cyrano", + "release": { + "au": "2015-10-03", + "eu": "2015-10-02", + "jp": "2015-07-30", + "na": "2015-09-25" + }, + "tail": "00a10502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Broccolo", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "04180001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_04180001-00d80502.png", + "name": "Broccolo", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-10-29", + "na": "2016-01-22" + }, + "tail": "00d80502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Winnie", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "03a90001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_03a90001-00710502.png", + "name": "Winnie", + "release": { + "au": "2015-10-03", + "eu": "2015-10-02", + "jp": "2015-07-30", + "na": "2015-09-25" + }, + "tail": "00710502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Midge", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "02350001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_02350001-00840502.png", + "name": "Midge", + "release": { + "au": "2015-10-03", + "eu": "2015-10-02", + "jp": "2015-07-30", + "na": "2015-09-25" + }, + "tail": "00840502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Cally", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "04e80001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_04e80001-01ce0502.png", + "name": "Cally", + "release": { + "au": "2016-06-18", + "eu": null, + "jp": "2016-03-24", + "na": "2016-06-10" + }, + "tail": "01ce0502", + "type": "Card" + }, + { + "amiiboSeries": "Monster Hunter", + "character": "Qurupeco", + "gameSeries": "Monster Hunter", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock Monster Hunter Stories 2 sticker set", + "write": false + } + ], + "gameID": [ + "0100B04011742000" + ], + "gameName": "Monster Hunter Rise" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a character-based monstie egg", + "write": false + } + ], + "gameID": [ + "010069301B1D4000" + ], + "gameName": "Monster Hunter Stories" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock the Hakum Rider Outfit layered armor set / Have Tsukino read your fortune and receive a random item", + "write": false + } + ], + "gameID": [ + "0100E21011446000" + ], + "gameName": "Monster Hunter Stories 2: Wings of Ruin" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a lot of BP / Receive better supplies (compared to other amiibo)", + "write": false + } + ], + "gameID": [ + "0100643002136000" + ], + "gameName": "Resident Evil Revelations" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a lot of BP / Receive better supplies (compared to other amiibo)", + "write": false + } + ], + "gameID": [ + "010095300212A000" + ], + "gameName": "Resident Evil Revelations 2" + } + ], + "head": "35040100", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_35040100-02e60f02.png", + "name": "Qurupeco and Dan", + "release": { + "au": null, + "eu": null, + "jp": "2016-12-08", + "na": null + }, + "tail": "02e60f02", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Vladimir", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "02830001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_02830001-00c70502.png", + "name": "Vladimir", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-10-29", + "na": "2016-01-22" + }, + "tail": "00c70502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Leopold", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "03ea0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_03ea0001-030b0502.png", + "name": "Leopold", + "release": { + "au": "2016-11-10", + "eu": "2016-11-11", + "jp": "2016-11-03", + "na": "2016-12-02" + }, + "tail": "030b0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Maelle", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "030a0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_030a0001-01c70502.png", + "name": "Maelle", + "release": { + "au": "2016-06-18", + "eu": null, + "jp": "2016-03-24", + "na": "2016-06-10" + }, + "tail": "01c70502", + "type": "Card" + }, + { + "amiiboSeries": "Splatoon", + "character": "Callie", + "gameSeries": "Splatoon", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a Splatoon-themed racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01003DA010E8A000" + ], + "gameName": "Miitopia" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock exclusive gear / Save favorite weapons, gear, and control settings / Take photos with the character or saved gear", + "write": true + } + ], + "gameID": [ + "01003BC0000A0000" + ], + "gameName": "Splatoon 2" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock exclusive gear / Save favorite weapons, gear, and control settings / Take photos with the character or saved gear", + "write": true + } + ], + "gameID": [ + "0100C2500FC20000" + ], + "gameName": "Splatoon 3" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a Spirit of the character", + "write": false + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "08010000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_08010000-025d0402.png", + "name": "Callie", + "release": { + "au": "2016-07-09", + "eu": "2016-07-08", + "jp": "2016-07-07", + "na": "2016-07-08" + }, + "tail": "025d0402", + "type": "Figure" + }, + { + "amiiboSeries": "Splatoon", + "character": "Callie", + "gameSeries": "Splatoon", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a Splatoon-themed racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01003DA010E8A000" + ], + "gameName": "Miitopia" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock exclusive gear / Save favorite weapons, gear, and control settings / Take photos with the character or saved gear", + "write": true + } + ], + "gameID": [ + "01003BC0000A0000" + ], + "gameName": "Splatoon 2" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock exclusive gear / Save favorite weapons, gear, and control settings / Take photos with the character or saved gear", + "write": true + } + ], + "gameID": [ + "0100C2500FC20000" + ], + "gameName": "Splatoon 3" + } + ], + "head": "08010000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_08010000-04360402.png", + "name": "Callie - Alterna", + "release": { + "au": "2024-09-05", + "eu": "2024-09-05", + "jp": "2024-09-05", + "na": "2024-09-05" + }, + "tail": "04360402", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Toby", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + }, + { + "Usage": "Unlock special furniture items and a poster based on the card's Sanrio character", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "04a80101", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_04a80101-031e0502.png", + "name": "Toby", + "release": { + "au": null, + "eu": "2016-11-25", + "jp": "2016-11-03", + "na": null + }, + "tail": "031e0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Coco", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "04960001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_04960001-00d90502.png", + "name": "Coco", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-10-29", + "na": "2016-01-22" + }, + "tail": "00d90502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Sprocket", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "04390001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_04390001-03110502.png", + "name": "Sprocket", + "release": { + "au": "2016-11-10", + "eu": "2016-11-11", + "jp": "2016-11-03", + "na": "2016-12-02" + }, + "tail": "03110502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Jack", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "01ad0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01ad0001-00b80502.png", + "name": "Jack", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-10-29", + "na": "2016-01-22" + }, + "tail": "00b80502", + "type": "Card" + }, + { + "amiiboSeries": "Power Pros", + "character": "Pawapuro", + "gameSeries": "Power Pros", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Receive in-game items and power-ups / Save items to your card after playing with friends to bring them home", + "write": true + } + ], + "gameID": [ + "0100E9C00BF28000" + ], + "gameName": "Jikkyou Powerful Pro Baseball" + } + ], + "head": "38000001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_38000001-03931702.png", + "name": "Pawapuro", + "release": { + "au": null, + "eu": null, + "jp": "2019-06-27", + "na": null + }, + "tail": "03931702", + "type": "Card" + }, + { + "amiiboSeries": "Super Smash Bros.", + "character": "Banjo", + "gameSeries": "Banjo Kazooie", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "3b400000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_3b400000-03a30002.png", + "name": "Banjo & Kazooie", + "release": { + "au": "2021-03-26", + "eu": "2021-03-26", + "jp": "2021-03-26", + "na": "2021-03-26" + }, + "tail": "03a30002", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Sylvia", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "03d70001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_03d70001-01b40502.png", + "name": "Sylvia", + "release": { + "au": "2016-06-18", + "eu": null, + "jp": "2016-03-24", + "na": "2016-06-10" + }, + "tail": "01b40502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Whitney", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "050e0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_050e0001-00d70502.png", + "name": "Whitney", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-10-29", + "na": "2016-01-22" + }, + "tail": "00d70502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Redd", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "01a80101", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01a80101-017e0502.png", + "name": "Redd - Shirt", + "release": { + "au": "2016-06-18", + "eu": null, + "jp": "2016-03-24", + "na": "2016-06-10" + }, + "tail": "017e0502", + "type": "Card" + }, + { + "amiiboSeries": "Super Smash Bros.", + "character": "Min Min", + "gameSeries": "ARMS", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "0a400000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_0a400000-041d0002.png", + "name": "Min Min", + "release": { + "au": "2022-04-29", + "eu": "2022-04-29", + "jp": "2022-04-29", + "na": "2022-04-29" + }, + "tail": "041d0002", + "type": "Figure" + }, + { + "amiiboSeries": "Mario Sports Superstars", + "character": "Peach", + "gameSeries": "Mario Sports Superstars", + "gamesSwitch": [], + "head": "09c20101", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_09c20101-02730e02.png", + "name": "Peach - Soccer", + "release": { + "au": "2017-03-11", + "eu": "2017-03-10", + "jp": "2017-03-30", + "na": "2017-03-24" + }, + "tail": "02730e02", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Sherb", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "0a090001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_0a090001-03c00502.png", + "name": "Sherb", + "release": { + "au": "2021-11-05", + "eu": "2021-11-05", + "jp": "2021-11-05", + "na": "2021-11-05" + }, + "tail": "03c00502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Stinky", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "026a0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_026a0001-01460502.png", + "name": "Stinky", + "release": { + "au": "2016-03-19", + "eu": "2016-03-18", + "jp": "2016-01-14", + "na": "2016-03-18" + }, + "tail": "01460502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Francine", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "04a00001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_04a00001-016e0502.png", + "name": "Francine", + "release": { + "au": "2016-03-19", + "eu": "2016-03-18", + "jp": "2016-01-14", + "na": "2016-03-18" + }, + "tail": "016e0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Roald", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "04600001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_04600001-00a50502.png", + "name": "Roald", + "release": { + "au": "2015-10-03", + "eu": "2015-10-02", + "jp": "2015-07-30", + "na": "2015-09-25" + }, + "tail": "00a50502", + "type": "Card" + }, + { + "amiiboSeries": "Mario Sports Superstars", + "character": "Boo", + "gameSeries": "Mario Sports Superstars", + "gamesSwitch": [], + "head": "09cb0501", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_09cb0501-02a40e02.png", + "name": "Boo - Horse Racing", + "release": { + "au": "2017-03-11", + "eu": "2017-03-10", + "jp": "2017-03-30", + "na": "2017-03-24" + }, + "tail": "02a40e02", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Monique", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "02680001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_02680001-007d0502.png", + "name": "Monique", + "release": { + "au": "2015-10-03", + "eu": "2015-10-02", + "jp": "2015-07-30", + "na": "2015-09-25" + }, + "tail": "007d0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Tybalt", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "04fc0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_04fc0001-02ee0502.png", + "name": "Tybalt", + "release": { + "au": "2016-11-10", + "eu": "2016-11-11", + "jp": "2016-11-03", + "na": "2016-12-02" + }, + "tail": "02ee0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Melba", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "03be0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_03be0001-01980502.png", + "name": "Melba", + "release": { + "au": "2016-06-18", + "eu": null, + "jp": "2016-03-24", + "na": "2016-06-10" + }, + "tail": "01980502", + "type": "Card" + }, + { + "amiiboSeries": "Mario Sports Superstars", + "character": "Luigi", + "gameSeries": "Mario Sports Superstars", + "gamesSwitch": [], + "head": "09c10101", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_09c10101-026e0e02.png", + "name": "Luigi - Soccer", + "release": { + "au": "2017-03-11", + "eu": "2017-03-10", + "jp": "2017-03-30", + "na": "2017-03-24" + }, + "tail": "026e0e02", + "type": "Card" + }, + { + "amiiboSeries": "Super Smash Bros.", + "character": "Chrom", + "gameSeries": "Fire Emblem", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Receive a Fashion Ticket and a Music Ticket, for unlocking any of the available costumes and music tracks", + "write": false + } + ], + "gameID": [ + "0100A6301214E000" + ], + "gameName": "Fire Emblem Engage" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an amiibo-exclusive weapon", + "write": false + }, + { + "Usage": "Receive a weapon", + "write": false + } + ], + "gameID": [ + "0100F15003E64000" + ], + "gameName": "Fire Emblem Warriors" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive better-quality randomized resources, weapons, or equipment", + "write": false + } + ], + "gameID": [ + "010071F0143EA000" + ], + "gameName": "Fire Emblem Warriors: Three Hopes" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special piece of battle music / Receive higher-quality items and materials", + "write": false + } + ], + "gameID": [ + "010055D009F78000" + ], + "gameName": "Fire Emblem: Three Houses" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "21080000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_21080000-03880002.png", + "name": "Chrom", + "release": { + "au": "2019-11-15", + "eu": "2019-11-15", + "jp": "2019-11-08", + "na": "2019-11-15" + }, + "tail": "03880002", + "type": "Figure" + }, + { + "amiiboSeries": "Super Smash Bros.", + "character": "Meta Knight", + "gameSeries": "Kirby", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Receive star coins and a boost item", + "write": false + } + ], + "gameID": [ + "01004D300C5AE000" + ], + "gameName": "Kirby and the Forgotten Land" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive two Picture Pieces, a Maxim Tomato, and two Point Stars", + "write": false + } + ], + "gameID": [ + "01007E3006DDA000" + ], + "gameName": "Kirby Star Allies" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive more useful items", + "write": false + } + ], + "gameID": [ + "01006B601380E000" + ], + "gameName": "Kirby's Return to Dream Land Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a Kirby-themed racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive 10 Fragments", + "write": false + } + ], + "gameID": [ + "01003FB00C5A8000" + ], + "gameName": "Super Kirby Clash" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "1f010000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_1f010000-00270002.png", + "name": "Meta Knight", + "release": { + "au": "2015-01-29", + "eu": "2015-02-20", + "jp": "2015-01-22", + "na": "2015-02-01" + }, + "tail": "00270002", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Nibbles", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "04e10001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_04e10001-01be0502.png", + "name": "Nibbles", + "release": { + "au": "2016-06-18", + "eu": null, + "jp": "2016-03-24", + "na": "2016-06-10" + }, + "tail": "01be0502", + "type": "Card" + }, + { + "amiiboSeries": "Splatoon", + "character": "Marie", + "gameSeries": "Splatoon", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a Splatoon-themed racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01003DA010E8A000" + ], + "gameName": "Miitopia" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock exclusive gear / Save favorite weapons, gear, and control settings / Take photos with the character or saved gear", + "write": true + } + ], + "gameID": [ + "01003BC0000A0000" + ], + "gameName": "Splatoon 2" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock exclusive gear / Save favorite weapons, gear, and control settings / Take photos with the character or saved gear", + "write": true + } + ], + "gameID": [ + "0100C2500FC20000" + ], + "gameName": "Splatoon 3" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a Spirit of the character", + "write": false + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "08020000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_08020000-025e0402.png", + "name": "Marie", + "release": { + "au": "2016-07-09", + "eu": "2016-07-08", + "jp": "2016-07-07", + "na": "2016-07-08" + }, + "tail": "025e0402", + "type": "Figure" + }, + { + "amiiboSeries": "Splatoon", + "character": "Marie", + "gameSeries": "Splatoon", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a Splatoon-themed racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01003DA010E8A000" + ], + "gameName": "Miitopia" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock exclusive gear / Save favorite weapons, gear, and control settings / Take photos with the character or saved gear", + "write": true + } + ], + "gameID": [ + "01003BC0000A0000" + ], + "gameName": "Splatoon 2" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock exclusive gear / Save favorite weapons, gear, and control settings / Take photos with the character or saved gear", + "write": true + } + ], + "gameID": [ + "0100C2500FC20000" + ], + "gameName": "Splatoon 3" + } + ], + "head": "08020000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_08020000-04370402.png", + "name": "Marie - Alterna", + "release": { + "au": "2024-09-05", + "eu": "2024-09-05", + "jp": "2024-09-05", + "na": "2024-09-05" + }, + "tail": "04370402", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Marty", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + }, + { + "Usage": "Unlock special furniture items and a poster based on the card's Sanrio character", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "028f0101", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_028f0101-031a0502.png", + "name": "Marty", + "release": { + "au": null, + "eu": "2016-11-25", + "jp": "2016-11-03", + "na": null + }, + "tail": "031a0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Spork/Crackle", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "047d0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_047d0001-012e0502.png", + "name": "Spork/Crackle", + "release": { + "au": "2016-03-19", + "eu": "2016-03-18", + "jp": "2016-01-14", + "na": "2016-03-18" + }, + "tail": "012e0502", + "type": "Card" + }, + { + "amiiboSeries": "Mario Sports Superstars", + "character": "Boo", + "gameSeries": "Mario Sports Superstars", + "gamesSwitch": [], + "head": "09cb0101", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_09cb0101-02a00e02.png", + "name": "Boo - Soccer", + "release": { + "au": "2017-03-11", + "eu": "2017-03-10", + "jp": "2017-03-30", + "na": "2017-03-24" + }, + "tail": "02a00e02", + "type": "Card" + }, + { + "amiiboSeries": "Shovel Knight", + "character": "King Knight", + "gameSeries": "Shovel Knight", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock a fairy companion and player color palette matching the character", + "write": false + } + ], + "gameID": [ + "01008D100DE46000" + ], + "gameName": "Cyber Shadow" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-specific Shovel Knight remix immediately", + "write": false + } + ], + "gameID": [ + "0100830008426000" + ], + "gameName": "Just Shapes & Beats" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock character-specific challenge stages, a character-based fairy companion, and costumes for the character", + "write": false + } + ], + "gameID": [ + "010057D0021E8000" + ], + "gameName": "Shovel Knight" + }, + { + "amiiboUsage": [ + { + "Usage": "Summon a fairy friend", + "write": false + } + ], + "gameID": [ + "0100B62017E68000" + ], + "gameName": "Shovel Knight Dig" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume for the character", + "write": false + } + ], + "gameID": [ + "0100B380022AE000" + ], + "gameName": "Shovel Knight Showdown" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a Spirit of the character", + "write": false + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "35c30000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_35c30000-036e0a02.png", + "name": "King Knight", + "release": { + "au": null, + "eu": "2019-12-10", + "jp": null, + "na": "2019-12-10" + }, + "tail": "036e0a02", + "type": "Figure" + }, + { + "amiiboSeries": "Splatoon", + "character": "Inkling", + "gameSeries": "Splatoon", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a Splatoon-themed racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01003DA010E8A000" + ], + "gameName": "Miitopia" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock exclusive gear / Save favorite weapons, gear, and control settings / Take photos with the character or saved gear", + "write": true + } + ], + "gameID": [ + "01003BC0000A0000" + ], + "gameName": "Splatoon 2" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock exclusive gear / Save favorite weapons, gear, and control settings / Take photos with the character or saved gear", + "write": true + } + ], + "gameID": [ + "0100C2500FC20000" + ], + "gameName": "Splatoon 3" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "08000300", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_08000300-00400402.png", + "name": "Inkling Squid", + "release": { + "au": "2015-05-30", + "eu": "2015-05-29", + "jp": "2015-05-28", + "na": "2015-05-29" + }, + "tail": "00400402", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Henry", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "034b0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_034b0001-009f0502.png", + "name": "Henry", + "release": { + "au": "2015-10-03", + "eu": "2015-10-02", + "jp": "2015-07-30", + "na": "2015-09-25" + }, + "tail": "009f0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Blathers", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a Spirit of the character", + "write": false + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "01920000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01920000-02470502.png", + "name": "Blathers", + "release": { + "au": "2016-01-30", + "eu": "2016-01-29", + "jp": "2015-12-17", + "na": "2016-01-22" + }, + "tail": "02470502", + "type": "Figure" + }, + { + "amiiboSeries": "Legend Of Zelda", + "character": "Revali", + "gameSeries": "The Legend of Zelda", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive specific crafting materials or a weapon for the character", + "write": false + } + ], + "gameID": [ + "01002B00111A2000" + ], + "gameName": "Hyrule Warriors: Age of Calamity" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a Legend of Zelda-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a Spirit of the character", + "write": false + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a chest of loot, potentially containing gear inspired by the Legend of Zelda series", + "write": false + } + ], + "gameID": [ + "01000A10041EA000" + ], + "gameName": "The Elder Scrolls V: Skyrim" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive materials and a rare or exclusive item", + "write": false + } + ], + "gameID": [ + "01007EF00011E000" + ], + "gameName": "The Legend of Zelda: Breath of the Wild" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock one of five amiibo-exclusive Chamber Dungeon chambers", + "write": false + }, + { + "Usage": "Save your Chamber Dungeon to the amiibo to share with friends", + "write": true + } + ], + "gameID": [ + "01006BB00C6F0000" + ], + "gameName": "The Legend of Zelda: Link's Awakening" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an amiibo-exclusive character-specific paraglider fabric", + "write": false + }, + { + "Usage": "Receive materials and a weapon or rare item", + "write": false + } + ], + "gameID": [ + "0100F2C0115B6000" + ], + "gameName": "The Legend of Zelda: Tears of the Kingdom" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive the Black Cat Clothes", + "write": false + }, + { + "Usage": "Receive random materials", + "write": false + } + ], + "gameID": [ + "01008CF01BAAC000" + ], + "gameName": "The Legend of Zelda: Echoes of Wisdom" + } + ], + "head": "01080000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01080000-035b0902.png", + "name": "Revali", + "release": { + "au": "2017-11-10", + "eu": "2017-11-10", + "jp": "2017-11-10", + "na": "2017-11-10" + }, + "tail": "035b0902", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Timbra", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "04cf0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_04cf0001-00e10502.png", + "name": "Timbra", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-10-29", + "na": "2016-01-22" + }, + "tail": "00e10502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Ed", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "03aa0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_03aa0001-00e60502.png", + "name": "Ed", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-10-29", + "na": "2016-01-22" + }, + "tail": "00e60502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Sable", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "01870001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01870001-03b00502.png", + "name": "Sable", + "release": { + "au": "2021-11-05", + "eu": "2021-11-05", + "jp": "2021-11-05", + "na": "2021-11-05" + }, + "tail": "03b00502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Keaton", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "04530001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_04530001-01040502.png", + "name": "Keaton", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-10-29", + "na": "2016-01-22" + }, + "tail": "01040502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Hugh", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "047b0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_047b0001-00f50502.png", + "name": "Hugh", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-10-29", + "na": "2016-01-22" + }, + "tail": "00f50502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Wart Jr.", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "033d0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_033d0001-013a0502.png", + "name": "Wart Jr.", + "release": { + "au": "2016-03-19", + "eu": "2016-03-18", + "jp": "2016-01-14", + "na": "2016-03-18" + }, + "tail": "013a0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Kicks", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "01940001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01940001-00aa0502.png", + "name": "Kicks", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-10-29", + "na": "2016-01-22" + }, + "tail": "00aa0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Shrunk", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "01b10001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01b10001-00b20502.png", + "name": "Shrunk", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-10-29", + "na": "2016-01-22" + }, + "tail": "00b20502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Eugene", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "03c60001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_03c60001-00930502.png", + "name": "Eugene", + "release": { + "au": "2015-10-03", + "eu": "2015-10-02", + "jp": "2015-07-30", + "na": "2015-09-25" + }, + "tail": "00930502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Pecan", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "04e00001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_04e00001-00f70502.png", + "name": "Pecan", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-10-29", + "na": "2016-01-22" + }, + "tail": "00f70502", + "type": "Card" + }, + { + "amiiboSeries": "Super Smash Bros.", + "character": "Lucario", + "gameSeries": "Pokemon", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "1ac00000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_1ac00000-00110002.png", + "name": "Lucario", + "release": { + "au": "2015-01-29", + "eu": "2015-01-23", + "jp": "2015-01-22", + "na": "2015-02-01" + }, + "tail": "00110002", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Sparro", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "023f0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_023f0001-01660502.png", + "name": "Sparro", + "release": { + "au": "2016-03-19", + "eu": "2016-03-18", + "jp": "2016-01-14", + "na": "2016-03-18" + }, + "tail": "01660502", + "type": "Card" + }, + { + "amiiboSeries": "Legend Of Zelda", + "character": "Zelda", + "gameSeries": "The Legend of Zelda", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive specific crafting materials or a weapon for the character", + "write": false + } + ], + "gameID": [ + "01002B00111A2000" + ], + "gameName": "Hyrule Warriors: Age of Calamity" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a weapon rated 3 stars or higher", + "write": false + } + ], + "gameID": [ + "0100AE00096EA000" + ], + "gameName": "Hyrule Warriors: Definitive Edition" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a Legend of Zelda-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01003DA010E8A000" + ], + "gameName": "Miitopia" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a chest of loot, potentially containing gear inspired by the Legend of Zelda series", + "write": false + } + ], + "gameID": [ + "01000A10041EA000" + ], + "gameName": "The Elder Scrolls V: Skyrim" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive materials and a rare or exclusive item", + "write": false + } + ], + "gameID": [ + "01007EF00011E000" + ], + "gameName": "The Legend of Zelda: Breath of the Wild" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock one of five amiibo-exclusive Chamber Dungeon chambers", + "write": false + }, + { + "Usage": "Save your Chamber Dungeon to the amiibo to share with friends", + "write": true + } + ], + "gameID": [ + "01006BB00C6F0000" + ], + "gameName": "The Legend of Zelda: Link's Awakening" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an amiibo-exclusive character-specific paraglider fabric", + "write": false + }, + { + "Usage": "Receive materials and a weapon or rare item", + "write": false + } + ], + "gameID": [ + "0100F2C0115B6000" + ], + "gameName": "The Legend of Zelda: Tears of the Kingdom" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive the Blue Attire", + "write": false + }, + { + "Usage": "Receive random materials", + "write": false + } + ], + "gameID": [ + "01008CF01BAAC000" + ], + "gameName": "The Legend of Zelda: Echoes of Wisdom" + } + ], + "head": "01010000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01010000-03520902.png", + "name": "Toon Zelda - The Wind Waker", + "release": { + "au": "2016-12-03", + "eu": "2016-12-02", + "jp": "2016-12-01", + "na": "2016-12-02" + }, + "tail": "03520902", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Isabelle", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "01810301", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01810301-01700502.png", + "name": "Isabelle - Dress", + "release": { + "au": "2016-06-18", + "eu": "2016-06-17", + "jp": "2016-03-24", + "na": "2016-06-10" + }, + "tail": "01700502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Kabuki", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "02660001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_02660001-00680502.png", + "name": "Kabuki", + "release": { + "au": "2015-10-03", + "eu": "2015-10-02", + "jp": "2015-07-30", + "na": "2015-09-25" + }, + "tail": "00680502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Eloise", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "03260001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_03260001-01390502.png", + "name": "Eloise", + "release": { + "au": "2016-03-19", + "eu": "2016-03-18", + "jp": "2016-01-14", + "na": "2016-03-18" + }, + "tail": "01390502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Gala", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "04850001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_04850001-014c0502.png", + "name": "Gala", + "release": { + "au": "2016-03-19", + "eu": "2016-03-18", + "jp": "2016-01-14", + "na": "2016-03-18" + }, + "tail": "014c0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Tex", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "046b0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_046b0001-01970502.png", + "name": "Tex", + "release": { + "au": "2016-06-18", + "eu": null, + "jp": "2016-03-24", + "na": "2016-06-10" + }, + "tail": "01970502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Reese", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a Spirit of the character", + "write": false + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "018a0000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_018a0000-02450502.png", + "name": "Reese", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-11-21", + "na": "2015-11-13" + }, + "tail": "02450502", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Bianca", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "05000001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_05000001-00e70502.png", + "name": "Bianca", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-10-29", + "na": "2016-01-22" + }, + "tail": "00e70502", + "type": "Card" + }, + { + "amiiboSeries": "Super Mario Bros.", + "character": "Mario", + "gameSeries": "Super Mario", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an amiibo-exclusive weapon for the character and their Rabbid counterpart", + "write": false + } + ], + "gameID": [ + "010067300059A000" + ], + "gameName": "Mario + Rabbids: Kingdom Battle" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01003DA010E8A000" + ], + "gameName": "Miitopia" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a particular power-up depending on the character", + "write": false + } + ], + "gameID": [ + "010028600EBDA000" + ], + "gameName": "Super Mario 3D World + Bowser's Fury" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a character-based costume", + "write": false + }, + { + "Usage": "Gain temporary invincibility", + "write": false + } + ], + "gameID": [ + "0100000000010000" + ], + "gameName": "Super Mario Odyssey" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock the character's shiny sticker", + "write": false + } + ], + "gameID": [ + "010036B0034E4000" + ], + "gameName": "Super Mario Party" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01006000040C2000" + ], + "gameName": "Yoshi's Crafted World" + } + ], + "head": "00000000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_00000000-00340102.png", + "name": "Mario", + "release": { + "au": "2015-03-21", + "eu": "2015-03-20", + "jp": "2015-03-12", + "na": "2015-03-20" + }, + "tail": "00340102", + "type": "Figure" + }, + { + "amiiboSeries": "Super Smash Bros.", + "character": "Mii", + "gameSeries": "Mii", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "07c00200", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_07c00200-00230002.png", + "name": "Mii Gunner", + "release": { + "au": "2015-09-26", + "eu": "2015-09-25", + "jp": "2015-09-10", + "na": "2015-11-01" + }, + "tail": "00230002", + "type": "Figure" + }, + { + "amiiboSeries": "Super Mario Bros.", + "character": "Peach", + "gameSeries": "Super Mario", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock a costume based on the character (short-hair version)", + "write": false + } + ], + "gameID": [ + "01007960049A0000" + ], + "gameName": "Bayonetta 2" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock Super Mario Odyssey-themed levels early", + "write": false + } + ], + "gameID": [ + "01009BF0072D4000" + ], + "gameName": "Captain Toad: Treasure Tracker" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an amiibo-exclusive weapon for the character and their Rabbid counterpart", + "write": false + } + ], + "gameID": [ + "010067300059A000" + ], + "gameName": "Mario + Rabbids: Kingdom Battle" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01003DA010E8A000" + ], + "gameName": "Miitopia" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a particular power-up depending on the character", + "write": false + } + ], + "gameID": [ + "010028600EBDA000" + ], + "gameName": "Super Mario 3D World + Bowser's Fury" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a character-based costume", + "write": false + } + ], + "gameID": [ + "0100000000010000" + ], + "gameName": "Super Mario Odyssey" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a life-up heart", + "write": false + } + ], + "gameID": [ + "0100000000010000" + ], + "gameName": "Super Mario Odyssey" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock the character's shiny sticker", + "write": false + } + ], + "gameID": [ + "010036B0034E4000" + ], + "gameName": "Super Mario Party" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a Spirit of the character", + "write": false + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01006000040C2000" + ], + "gameName": "Yoshi's Crafted World" + } + ], + "head": "00020000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_00020000-03720102.png", + "name": "Peach - Wedding", + "release": { + "au": "2017-10-27", + "eu": "2017-10-27", + "jp": "2017-10-27", + "na": "2017-10-27" + }, + "tail": "03720102", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Cephalobot", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "0a170001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_0a170001-03ce0502.png", + "name": "Cephalobot", + "release": { + "au": "2021-11-05", + "eu": "2021-11-05", + "jp": "2021-11-05", + "na": "2021-11-05" + }, + "tail": "03ce0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Gwen", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "04640001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_04640001-00c00502.png", + "name": "Gwen", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-10-29", + "na": "2016-01-22" + }, + "tail": "00c00502", + "type": "Card" + }, + { + "amiiboSeries": "Mario Sports Superstars", + "character": "Donkey Kong", + "gameSeries": "Mario Sports Superstars", + "gamesSwitch": [], + "head": "09c70101", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_09c70101-028c0e02.png", + "name": "Donkey Kong - Soccer", + "release": { + "au": "2017-03-11", + "eu": "2017-03-10", + "jp": "2017-03-30", + "na": "2017-03-24" + }, + "tail": "028c0e02", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Kevin", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "04870001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_04870001-01bf0502.png", + "name": "Kevin", + "release": { + "au": "2016-06-18", + "eu": null, + "jp": "2016-03-24", + "na": "2016-06-10" + }, + "tail": "01bf0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Blaire", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "04de0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_04de0001-00ce0502.png", + "name": "Blaire", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-10-29", + "na": "2016-01-22" + }, + "tail": "00ce0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Wolfgang", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "050d0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_050d0001-01420502.png", + "name": "Wolfgang", + "release": { + "au": "2016-03-19", + "eu": "2016-03-18", + "jp": "2016-01-14", + "na": "2016-03-18" + }, + "tail": "01420502", + "type": "Card" + }, + { + "amiiboSeries": "Mario Sports Superstars", + "character": "Yoshi", + "gameSeries": "Mario Sports Superstars", + "gamesSwitch": [], + "head": "09c40301", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_09c40301-027f0e02.png", + "name": "Yoshi - Tennis", + "release": { + "au": "2017-03-11", + "eu": "2017-03-10", + "jp": "2017-03-30", + "na": "2017-03-24" + }, + "tail": "027f0e02", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Resetti", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "018e0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_018e0001-00490502.png", + "name": "Resetti", + "release": { + "au": "2015-10-03", + "eu": "2015-10-02", + "jp": "2015-07-30", + "na": "2015-09-25" + }, + "tail": "00490502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Peggy", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "04830001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_04830001-01b00502.png", + "name": "Peggy", + "release": { + "au": "2016-06-18", + "eu": null, + "jp": "2016-03-24", + "na": "2016-06-10" + }, + "tail": "01b00502", + "type": "Card" + }, + { + "amiiboSeries": "Legend Of Zelda", + "character": "Link", + "gameSeries": "The Legend of Zelda", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock a costume based on the character (short-hair version)", + "write": false + } + ], + "gameID": [ + "01007960049A0000" + ], + "gameName": "Bayonetta 2" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive specific crafting materials or a weapon for the character", + "write": false + } + ], + "gameID": [ + "01002B00111A2000" + ], + "gameName": "Hyrule Warriors: Age of Calamity" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a weapon rated 3 stars or higher", + "write": false + } + ], + "gameID": [ + "0100AE00096EA000" + ], + "gameName": "Hyrule Warriors: Definitive Edition" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a Legend of Zelda-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01003DA010E8A000" + ], + "gameName": "Miitopia" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a chest of loot, potentially containing gear inspired by the Legend of Zelda series", + "write": false + } + ], + "gameID": [ + "01000A10041EA000" + ], + "gameName": "The Elder Scrolls V: Skyrim" + }, + { + "amiiboUsage": [ + { + "Usage": "Bring Epona into the game as a rideable horse", + "write": false + }, + { + "Usage": "Receive materials and a rare or exclusive item", + "write": false + } + ], + "gameID": [ + "01007EF00011E000" + ], + "gameName": "The Legend of Zelda: Breath of the Wild" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock one of five amiibo-exclusive Chamber Dungeon chambers", + "write": false + }, + { + "Usage": "Save your Chamber Dungeon to the amiibo to share with friends", + "write": true + } + ], + "gameID": [ + "01006BB00C6F0000" + ], + "gameName": "The Legend of Zelda: Link's Awakening" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an amiibo-exclusive character-specific paraglider fabric", + "write": false + }, + { + "Usage": "Bring Epona into the game as a rideable horse", + "write": false + }, + { + "Usage": "Receive materials and a weapon or rare item", + "write": false + } + ], + "gameID": [ + "0100F2C0115B6000" + ], + "gameName": "The Legend of Zelda: Tears of the Kingdom" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive the Red Tunic", + "write": false + }, + { + "Usage": "Receive random materials", + "write": false + } + ], + "gameID": [ + "01008CF01BAAC000" + ], + "gameName": "The Legend of Zelda: Echoes of Wisdom" + } + ], + "head": "01000000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01000000-034d0902.png", + "name": "Link - Twilight Princess", + "release": { + "au": "2017-06-24", + "eu": "2017-06-23", + "jp": "2017-06-23", + "na": "2017-06-23" + }, + "tail": "034d0902", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Boyd", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "036e0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_036e0001-02fb0502.png", + "name": "Boyd", + "release": { + "au": "2016-11-10", + "eu": "2016-11-11", + "jp": "2016-11-03", + "na": "2016-12-02" + }, + "tail": "02fb0502", + "type": "Card" + }, + { + "amiiboSeries": "Monster Hunter", + "character": "One-Eyed Rathalos", + "gameSeries": "Monster Hunter", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock Monster Hunter Stories 2 sticker set", + "write": false + } + ], + "gameID": [ + "0100B04011742000" + ], + "gameName": "Monster Hunter Rise" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a character-based monstie egg", + "write": false + } + ], + "gameID": [ + "010069301B1D4000" + ], + "gameName": "Monster Hunter Stories" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock the Hakum Rider Outfit layered armor set / Have Tsukino read your fortune and receive a random item", + "write": false + } + ], + "gameID": [ + "0100E21011446000" + ], + "gameName": "Monster Hunter Stories 2: Wings of Ruin" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a lot of BP / Receive better supplies (compared to other amiibo)", + "write": false + } + ], + "gameID": [ + "0100643002136000" + ], + "gameName": "Resident Evil Revelations" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a lot of BP / Receive better supplies (compared to other amiibo)", + "write": false + } + ], + "gameID": [ + "010095300212A000" + ], + "gameName": "Resident Evil Revelations 2" + } + ], + "head": "35000100", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_35000100-02e10f02.png", + "name": "One-Eyed Rathalos and Rider - Male", + "release": { + "au": null, + "eu": null, + "jp": "2016-10-08", + "na": null + }, + "tail": "02e10f02", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Gabi", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "04990001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_04990001-00df0502.png", + "name": "Gabi", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-10-29", + "na": "2016-01-22" + }, + "tail": "00df0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "T-Bone", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "024f0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_024f0001-00810502.png", + "name": "T-Bone", + "release": { + "au": "2015-10-03", + "eu": "2015-10-02", + "jp": "2015-07-30", + "na": "2015-09-25" + }, + "tail": "00810502", + "type": "Card" + }, + { + "amiiboSeries": "Mario Sports Superstars", + "character": "Wario", + "gameSeries": "Mario Sports Superstars", + "gamesSwitch": [], + "head": "09c50301", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_09c50301-02840e02.png", + "name": "Wario - Tennis", + "release": { + "au": "2017-03-11", + "eu": "2017-03-10", + "jp": "2017-03-30", + "na": "2017-03-24" + }, + "tail": "02840e02", + "type": "Card" + }, + { + "amiiboSeries": "Super Smash Bros.", + "character": "Fox", + "gameSeries": "Star Fox", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock a Star Fox costume", + "write": false + } + ], + "gameID": [ + "01007960049A0000" + ], + "gameName": "Bayonetta 2" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "05800000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_05800000-00050002.png", + "name": "Fox", + "release": { + "au": "2014-11-29", + "eu": "2014-11-28", + "jp": "2014-12-06", + "na": "2014-11-21" + }, + "tail": "00050002", + "type": "Figure" + }, + { + "amiiboSeries": "Splatoon", + "character": "Inkling", + "gameSeries": "Splatoon", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a Splatoon-themed racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01003DA010E8A000" + ], + "gameName": "Miitopia" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock exclusive gear / Save favorite weapons, gear, and control settings / Take photos with the character or saved gear", + "write": true + } + ], + "gameID": [ + "01003BC0000A0000" + ], + "gameName": "Splatoon 2" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock exclusive gear / Save favorite weapons, gear, and control settings / Take photos with the character or saved gear", + "write": true + } + ], + "gameID": [ + "0100C2500FC20000" + ], + "gameName": "Splatoon 3" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "08000300", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_08000300-02610402.png", + "name": "Inkling Squid - Orange", + "release": { + "au": "2016-07-09", + "eu": "2016-07-08", + "jp": "2016-07-07", + "na": "2016-07-08" + }, + "tail": "02610402", + "type": "Figure" + }, + { + "amiiboSeries": "Legend Of Zelda", + "character": "Ganon", + "gameSeries": "The Legend of Zelda", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive specific crafting materials or a weapon for the character", + "write": false + } + ], + "gameID": [ + "01002B00111A2000" + ], + "gameName": "Hyrule Warriors: Age of Calamity" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a weapon rated 3 stars or higher", + "write": false + } + ], + "gameID": [ + "0100AE00096EA000" + ], + "gameName": "Hyrule Warriors: Definitive Edition" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a Legend of Zelda-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01003DA010E8A000" + ], + "gameName": "Miitopia" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a chest of loot, potentially containing gear inspired by the Legend of Zelda series", + "write": false + } + ], + "gameID": [ + "01000A10041EA000" + ], + "gameName": "The Elder Scrolls V: Skyrim" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive materials and a rare or exclusive item", + "write": false + } + ], + "gameID": [ + "01007EF00011E000" + ], + "gameName": "The Legend of Zelda: Breath of the Wild" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock one of five amiibo-exclusive Chamber Dungeon chambers", + "write": false + }, + { + "Usage": "Save your Chamber Dungeon to the amiibo to share with friends", + "write": true + } + ], + "gameID": [ + "01006BB00C6F0000" + ], + "gameName": "The Legend of Zelda: Link's Awakening" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an amiibo-exclusive character-specific paraglider fabric", + "write": false + }, + { + "Usage": "Receive materials and a weapon or rare item", + "write": false + } + ], + "gameID": [ + "0100F2C0115B6000" + ], + "gameName": "The Legend of Zelda: Tears of the Kingdom" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive the Black Cat Clothes", + "write": false + }, + { + "Usage": "Receive random materials", + "write": false + } + ], + "gameID": [ + "01008CF01BAAC000" + ], + "gameName": "The Legend of Zelda: Echoes of Wisdom" + } + ], + "head": "01020100", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01020100-041a0902.png", + "name": "Ganondorf - Tears of the Kingdom", + "release": { + "au": "2023-11-03", + "eu": "2023-11-03", + "jp": "2023-11-03", + "na": "2023-11-03" + }, + "tail": "041a0902", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Boris", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "04810001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_04810001-02f10502.png", + "name": "Boris", + "release": { + "au": "2016-11-10", + "eu": "2016-11-11", + "jp": "2016-11-03", + "na": "2016-12-02" + }, + "tail": "02f10502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Antonio", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "02010001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_02010001-016a0502.png", + "name": "Antonio", + "release": { + "au": "2016-03-19", + "eu": "2016-03-18", + "jp": "2016-01-14", + "na": "2016-03-18" + }, + "tail": "016a0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Murphy", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "02840001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_02840001-02fe0502.png", + "name": "Murphy", + "release": { + "au": "2016-11-10", + "eu": "2016-11-11", + "jp": "2016-11-03", + "na": "2016-12-02" + }, + "tail": "02fe0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Wade", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "04680001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_04680001-02f20502.png", + "name": "Wade", + "release": { + "au": "2016-11-10", + "eu": "2016-11-11", + "jp": "2016-11-03", + "na": "2016-12-02" + }, + "tail": "02f20502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Stitches", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "02820001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_02820001-01d60502.png", + "name": "Stitches - Amiibo Festival", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-11-21", + "na": "2015-11-13" + }, + "tail": "01d60502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Pascal", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "01a40001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01a40001-004d0502.png", + "name": "Pascal", + "release": { + "au": "2015-10-03", + "eu": "2015-10-02", + "jp": "2015-07-30", + "na": "2015-09-25" + }, + "tail": "004d0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Wisp", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "0a060001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_0a060001-03ba0502.png", + "name": "Wisp", + "release": { + "au": "2021-11-05", + "eu": "2021-11-05", + "jp": "2021-11-05", + "na": "2021-11-05" + }, + "tail": "03ba0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Fuchsia", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "02dc0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_02dc0001-00be0502.png", + "name": "Fuchsia", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-10-29", + "na": "2016-01-22" + }, + "tail": "00be0502", + "type": "Card" + }, + { + "amiiboSeries": "Super Smash Bros.", + "character": "Jigglypuff", + "gameSeries": "Pokemon", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "19270000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_19270000-00260002.png", + "name": "Jigglypuff", + "release": { + "au": "2015-05-30", + "eu": "2015-05-29", + "jp": "2015-04-29", + "na": "2015-05-29" + }, + "tail": "00260002", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Tucker", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "032c0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_032c0001-01480502.png", + "name": "Tucker", + "release": { + "au": "2016-03-19", + "eu": "2016-03-18", + "jp": "2016-01-14", + "na": "2016-03-18" + }, + "tail": "01480502", + "type": "Card" + }, + { + "amiiboSeries": "Legend Of Zelda", + "character": "Link", + "gameSeries": "The Legend of Zelda", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock a costume based on the character (short-hair version)", + "write": false + } + ], + "gameID": [ + "01007960049A0000" + ], + "gameName": "Bayonetta 2" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive specific crafting materials or a weapon for the character", + "write": false + } + ], + "gameID": [ + "01002B00111A2000" + ], + "gameName": "Hyrule Warriors: Age of Calamity" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a weapon rated 3 stars or higher", + "write": false + } + ], + "gameID": [ + "0100AE00096EA000" + ], + "gameName": "Hyrule Warriors: Definitive Edition" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a Legend of Zelda-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01003DA010E8A000" + ], + "gameName": "Miitopia" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a chest of loot, potentially containing gear inspired by the Legend of Zelda series", + "write": false + } + ], + "gameID": [ + "01000A10041EA000" + ], + "gameName": "The Elder Scrolls V: Skyrim" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock one of five amiibo-exclusive Chamber Dungeon chambers", + "write": false + }, + { + "Usage": "Save your Chamber Dungeon to the amiibo to share with friends", + "write": true + } + ], + "gameID": [ + "01006BB00C6F0000" + ], + "gameName": "The Legend of Zelda: Link's Awakening" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an amiibo-exclusive character-specific paraglider fabric", + "write": false + }, + { + "Usage": "Receive materials and a weapon or rare item", + "write": false + } + ], + "gameID": [ + "0100F2C0115B6000" + ], + "gameName": "The Legend of Zelda: Tears of the Kingdom" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive the Red Tunic", + "write": false + }, + { + "Usage": "Receive random materials", + "write": false + } + ], + "gameID": [ + "01008CF01BAAC000" + ], + "gameName": "The Legend of Zelda: Echoes of Wisdom" + } + ], + "head": "01000000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01000000-034b0902.png", + "name": "Link - Ocarina of Time", + "release": { + "au": "2016-12-03", + "eu": "2016-12-02", + "jp": "2016-12-01", + "na": "2016-12-02" + }, + "tail": "034b0902", + "type": "Figure" + }, + { + "amiiboSeries": "Mario Sports Superstars", + "character": "Boo", + "gameSeries": "Mario Sports Superstars", + "gamesSwitch": [], + "head": "09cb0201", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_09cb0201-02a10e02.png", + "name": "Boo - Baseball", + "release": { + "au": "2017-03-11", + "eu": "2017-03-10", + "jp": "2017-03-30", + "na": "2017-03-24" + }, + "tail": "02a10e02", + "type": "Card" + }, + { + "amiiboSeries": "Mario Sports Superstars", + "character": "Peach", + "gameSeries": "Mario Sports Superstars", + "gamesSwitch": [], + "head": "09c20401", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_09c20401-02760e02.png", + "name": "Peach - Golf", + "release": { + "au": "2017-03-11", + "eu": "2017-03-10", + "jp": "2017-03-30", + "na": "2017-03-24" + }, + "tail": "02760e02", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Elvis", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "03e70001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_03e70001-012a0502.png", + "name": "Elvis", + "release": { + "au": "2016-03-19", + "eu": "2016-03-18", + "jp": "2016-01-14", + "na": "2016-03-18" + }, + "tail": "012a0502", + "type": "Card" + }, + { + "amiiboSeries": "Super Smash Bros.", + "character": "Pichu", + "gameSeries": "Pokemon", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "19ac0000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_19ac0000-03850002.png", + "name": "Pichu", + "release": { + "au": "2019-07-19", + "eu": "2019-07-19", + "jp": "2019-07-19", + "na": "2019-07-19" + }, + "tail": "03850002", + "type": "Figure" + }, + { + "amiiboSeries": "Splatoon", + "character": "Pearl", + "gameSeries": "Splatoon", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a Splatoon-themed racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock exclusive gear / Save favorite weapons, gear, and control settings / Take photos with the character or saved gear", + "write": true + } + ], + "gameID": [ + "01003BC0000A0000" + ], + "gameName": "Splatoon 2" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock exclusive gear / Save favorite weapons, gear, and control settings / Take photos with the character or saved gear", + "write": true + } + ], + "gameID": [ + "0100C2500FC20000" + ], + "gameName": "Splatoon 3" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a Spirit of the character", + "write": false + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "08030000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_08030000-03760402.png", + "name": "Pearl", + "release": { + "au": "2018-07-13", + "eu": "2018-07-13", + "jp": "2018-07-13", + "na": "2018-07-13" + }, + "tail": "03760402", + "type": "Figure" + }, + { + "amiiboSeries": "Splatoon", + "character": "Pearl", + "gameSeries": "Splatoon", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a Splatoon-themed racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock exclusive gear / Save favorite weapons, gear, and control settings / Take photos with the character or saved gear", + "write": true + } + ], + "gameID": [ + "01003BC0000A0000" + ], + "gameName": "Splatoon 2" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock exclusive gear / Save favorite weapons, gear, and control settings / Take photos with the character or saved gear", + "write": true + } + ], + "gameID": [ + "0100C2500FC20000" + ], + "gameName": "Splatoon 3" + } + ], + "head": "08030000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_08030000-04380402.png", + "name": "Pearl - Side Order", + "release": { + "au": "2024-09-05", + "eu": "2024-09-05", + "jp": "2024-09-05", + "na": "2024-09-05" + }, + "tail": "04380402", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Shino", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "0a140001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_0a140001-03cb0502.png", + "name": "Shino", + "release": { + "au": "2021-11-05", + "eu": "2021-11-05", + "jp": "2021-11-05", + "na": "2021-11-05" + }, + "tail": "03cb0502", + "type": "Card" + }, + { + "amiiboSeries": "Legend Of Zelda", + "character": "Link", + "gameSeries": "The Legend of Zelda", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock a costume based on the character (short-hair version)", + "write": false + } + ], + "gameID": [ + "01007960049A0000" + ], + "gameName": "Bayonetta 2" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a weapon rated 3 stars or higher", + "write": false + } + ], + "gameID": [ + "0100AE00096EA000" + ], + "gameName": "Hyrule Warriors: Definitive Edition" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a Legend of Zelda-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01003DA010E8A000" + ], + "gameName": "Miitopia" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a chest of loot, potentially containing gear inspired by the Legend of Zelda series", + "write": false + } + ], + "gameID": [ + "01000A10041EA000" + ], + "gameName": "The Elder Scrolls V: Skyrim" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive materials and a rare or exclusive item", + "write": false + } + ], + "gameID": [ + "01007EF00011E000" + ], + "gameName": "The Legend of Zelda: Breath of the Wild" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock one of five amiibo-exclusive Chamber Dungeon chambers", + "write": false + }, + { + "Usage": "Save your Chamber Dungeon to the amiibo to share with friends", + "write": true + } + ], + "gameID": [ + "01006BB00C6F0000" + ], + "gameName": "The Legend of Zelda: Link's Awakening" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an amiibo-exclusive character-specific paraglider fabric", + "write": false + }, + { + "Usage": "Receive materials and a weapon or rare item", + "write": false + } + ], + "gameID": [ + "0100F2C0115B6000" + ], + "gameName": "The Legend of Zelda: Tears of the Kingdom" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive the Red Tunic", + "write": false + }, + { + "Usage": "Receive random materials", + "write": false + } + ], + "gameID": [ + "01008CF01BAAC000" + ], + "gameName": "The Legend of Zelda: Echoes of Wisdom" + } + ], + "head": "01000000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01000000-034c0902.png", + "name": "Link - Majora's Mask", + "release": { + "au": "2017-06-24", + "eu": "2017-06-23", + "jp": "2017-06-23", + "na": "2017-06-23" + }, + "tail": "034c0902", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Lottie", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "01c10101", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01c10101-017a0502.png", + "name": "Lottie - Black Skirt And Bow", + "release": { + "au": "2016-06-18", + "eu": null, + "jp": "2016-03-24", + "na": "2016-06-10" + }, + "tail": "017a0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Vivian", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "05130001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_05130001-02e70502.png", + "name": "Vivian", + "release": { + "au": "2016-11-10", + "eu": "2016-11-11", + "jp": "2016-11-03", + "na": "2016-12-02" + }, + "tail": "02e70502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Bettina", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "041b0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_041b0001-00f10502.png", + "name": "Bettina", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-10-29", + "na": "2016-01-22" + }, + "tail": "00f10502", + "type": "Card" + }, + { + "amiiboSeries": "Super Smash Bros.", + "character": "Alex", + "gameSeries": "Minecraft", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "3dc10000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_3dc10000-04230002.png", + "name": "Alex", + "release": { + "au": "2022-09-09", + "eu": "2022-09-09", + "jp": "2022-09-09", + "na": "2022-09-09" + }, + "tail": "04230002", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Lyle", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "01aa0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01aa0001-00530502.png", + "name": "Lyle", + "release": { + "au": "2015-10-03", + "eu": "2015-10-02", + "jp": "2015-07-30", + "na": "2015-09-25" + }, + "tail": "00530502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Walker", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "02f00001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_02f00001-00a70502.png", + "name": "Walker", + "release": { + "au": "2015-10-03", + "eu": "2015-10-02", + "jp": "2015-07-30", + "na": "2015-09-25" + }, + "tail": "00a70502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Ozzie", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "03c10001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_03c10001-00bb0502.png", + "name": "Ozzie", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-10-29", + "na": "2016-01-22" + }, + "tail": "00bb0502", + "type": "Card" + }, + { + "amiiboSeries": "Skylanders", + "character": "Donkey Kong", + "gameSeries": "Super Mario", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01003DA010E8A000" + ], + "gameName": "Miitopia" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a particular power-up depending on the character", + "write": false + } + ], + "gameID": [ + "010028600EBDA000" + ], + "gameName": "Super Mario 3D World + Bowser's Fury" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock the character's shiny sticker", + "write": false + } + ], + "gameID": [ + "010036B0034E4000" + ], + "gameName": "Super Mario Party" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "0008ff00", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_0008ff00-023b0702.png", + "name": "Turbo Charge Donkey Kong", + "release": { + "au": "2015-09-24", + "eu": "2015-09-25", + "jp": null, + "na": "2015-09-20" + }, + "tail": "023b0702", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Sterling", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "04520001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_04520001-00730502.png", + "name": "Sterling", + "release": { + "au": "2015-10-03", + "eu": "2015-10-02", + "jp": "2015-07-30", + "na": "2015-09-25" + }, + "tail": "00730502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Isabelle", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "01810001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01810001-01d40502.png", + "name": "Isabelle - Character Parfait", + "release": { + "au": null, + "eu": null, + "jp": "2015-08-01", + "na": null + }, + "tail": "01d40502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Claude", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "049f0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_049f0001-03010502.png", + "name": "Claude", + "release": { + "au": "2016-11-10", + "eu": "2016-11-11", + "jp": "2016-11-03", + "na": "2016-12-02" + }, + "tail": "03010502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Goldie", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "02ea0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_02ea0001-01d50502.png", + "name": "Goldie - Amiibo Festival", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-11-21", + "na": "2015-11-13" + }, + "tail": "01d50502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Fauna", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "02d60001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_02d60001-00560502.png", + "name": "Fauna", + "release": { + "au": "2015-10-03", + "eu": "2015-10-02", + "jp": "2015-07-30", + "na": "2015-09-25" + }, + "tail": "00560502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Ankha", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "02700001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_02700001-00ff0502.png", + "name": "Ankha", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-10-29", + "na": "2016-01-22" + }, + "tail": "00ff0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Gulliver", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "01a20001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01a20001-03b90502.png", + "name": "Gulliver", + "release": { + "au": "2021-11-05", + "eu": "2021-11-05", + "jp": "2021-11-05", + "na": "2021-11-05" + }, + "tail": "03b90502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Kid Cat", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "02670001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_02670001-01080502.png", + "name": "Kid Cat", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-10-29", + "na": "2016-01-22" + }, + "tail": "01080502", + "type": "Card" + }, + { + "amiiboSeries": "Yu-Gi-Oh!", + "character": "Romin Kirishima", + "gameSeries": "Yu-Gi-Oh!", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Receive items/bonuses", + "write": false + } + ], + "gameID": [ + "01003C101454A000" + ], + "gameName": "Yu-Gi-Oh! Rush Duel Saikyo Battle Royale" + } + ], + "head": "38430001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_38430001-04271902.png", + "name": "Romin Kirishima", + "release": { + "au": null, + "eu": null, + "jp": null, + "na": "2021-08-12" + }, + "tail": "04271902", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Katt", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "02720001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_02720001-01860502.png", + "name": "Katt", + "release": { + "au": "2016-06-18", + "eu": null, + "jp": "2016-03-24", + "na": "2016-06-10" + }, + "tail": "01860502", + "type": "Card" + }, + { + "amiiboSeries": "Super Smash Bros.", + "character": "Piranha Plant", + "gameSeries": "Super Mario", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Receive a particular power-up depending on the character", + "write": false + } + ], + "gameID": [ + "010028600EBDA000" + ], + "gameName": "Super Mario 3D World + Bowser's Fury" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "00240000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_00240000-038d0002.png", + "name": "Piranha Plant", + "release": { + "au": "2019-02-15", + "eu": "2019-02-15", + "jp": "2019-02-15", + "na": "2019-02-15" + }, + "tail": "038d0002", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Rex", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "03e80001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_03e80001-02f50502.png", + "name": "Rex", + "release": { + "au": "2016-11-10", + "eu": "2016-11-11", + "jp": "2016-11-03", + "na": "2016-12-02" + }, + "tail": "02f50502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Beau", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "02dd0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_02dd0001-00ea0502.png", + "name": "Beau", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-10-29", + "na": "2016-01-22" + }, + "tail": "00ea0502", + "type": "Card" + }, + { + "amiiboSeries": "Legend Of Zelda", + "character": "Bokoblin", + "gameSeries": "The Legend of Zelda", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a Legend of Zelda-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a Spirit of the character", + "write": false + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a chest of loot, potentially containing gear inspired by the Legend of Zelda series", + "write": false + } + ], + "gameID": [ + "01000A10041EA000" + ], + "gameName": "The Elder Scrolls V: Skyrim" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive materials and a rare or exclusive item", + "write": false + } + ], + "gameID": [ + "01007EF00011E000" + ], + "gameName": "The Legend of Zelda: Breath of the Wild" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock one of five amiibo-exclusive Chamber Dungeon chambers", + "write": false + }, + { + "Usage": "Save your Chamber Dungeon to the amiibo to share with friends", + "write": true + } + ], + "gameID": [ + "01006BB00C6F0000" + ], + "gameName": "The Legend of Zelda: Link's Awakening" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an amiibo-exclusive character-specific paraglider fabric", + "write": false + }, + { + "Usage": "Receive materials and a weapon or rare item", + "write": false + } + ], + "gameID": [ + "0100F2C0115B6000" + ], + "gameName": "The Legend of Zelda: Tears of the Kingdom" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive the Black Cat Clothes", + "write": false + }, + { + "Usage": "Receive random materials", + "write": false + } + ], + "gameID": [ + "01008CF01BAAC000" + ], + "gameName": "The Legend of Zelda: Echoes of Wisdom" + } + ], + "head": "01410000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01410000-035c0902.png", + "name": "Bokoblin", + "release": { + "au": "2017-03-03", + "eu": "2017-03-03", + "jp": "2017-03-03", + "na": "2017-03-03" + }, + "tail": "035c0902", + "type": "Figure" + }, + { + "amiiboSeries": "Mario Sports Superstars", + "character": "Rosalina", + "gameSeries": "Mario Sports Superstars", + "gamesSwitch": [], + "head": "09cf0201", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_09cf0201-02b50e02.png", + "name": "Rosalina - Baseball", + "release": { + "au": "2017-03-11", + "eu": "2017-03-10", + "jp": "2017-03-30", + "na": "2017-03-24" + }, + "tail": "02b50e02", + "type": "Card" + }, + { + "amiiboSeries": "Super Smash Bros.", + "character": "Mewtwo", + "gameSeries": "Pokemon", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "19960000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_19960000-023d0002.png", + "name": "Mewtwo", + "release": { + "au": "2015-10-24", + "eu": "2015-10-23", + "jp": "2015-10-29", + "na": "2015-11-13" + }, + "tail": "023d0002", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Bertha", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "03930001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_03930001-00a00502.png", + "name": "Bertha", + "release": { + "au": "2015-10-03", + "eu": "2015-10-02", + "jp": "2015-07-30", + "na": "2015-09-25" + }, + "tail": "00a00502", + "type": "Card" + }, + { + "amiiboSeries": "Pokemon", + "character": "Shadow Mewtwo", + "gameSeries": "Pokemon", + "gamesSwitch": [], + "head": "1d000001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_1d000001-025c0d02.png", + "name": "Shadow Mewtwo", + "release": { + "au": "2016-03-19", + "eu": "2016-03-18", + "jp": "2016-03-18", + "na": "2016-03-18" + }, + "tail": "025c0d02", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Puck", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "04650001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_04650001-006e0502.png", + "name": "Puck", + "release": { + "au": "2015-10-03", + "eu": "2015-10-02", + "jp": "2015-07-30", + "na": "2015-09-25" + }, + "tail": "006e0502", + "type": "Card" + }, + { + "amiiboSeries": "Legend Of Zelda", + "character": "Urbosa", + "gameSeries": "The Legend of Zelda", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive specific crafting materials or a weapon for the character", + "write": false + } + ], + "gameID": [ + "01002B00111A2000" + ], + "gameName": "Hyrule Warriors: Age of Calamity" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a Legend of Zelda-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a Spirit of the character", + "write": false + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a chest of loot, potentially containing gear inspired by the Legend of Zelda series", + "write": false + } + ], + "gameID": [ + "01000A10041EA000" + ], + "gameName": "The Elder Scrolls V: Skyrim" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive materials and a rare or exclusive item", + "write": false + } + ], + "gameID": [ + "01007EF00011E000" + ], + "gameName": "The Legend of Zelda: Breath of the Wild" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock one of five amiibo-exclusive Chamber Dungeon chambers", + "write": false + }, + { + "Usage": "Save your Chamber Dungeon to the amiibo to share with friends", + "write": true + } + ], + "gameID": [ + "01006BB00C6F0000" + ], + "gameName": "The Legend of Zelda: Link's Awakening" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an amiibo-exclusive character-specific paraglider fabric", + "write": false + }, + { + "Usage": "Receive materials and a weapon or rare item", + "write": false + } + ], + "gameID": [ + "0100F2C0115B6000" + ], + "gameName": "The Legend of Zelda: Tears of the Kingdom" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive the Black Cat Clothes", + "write": false + }, + { + "Usage": "Receive random materials", + "write": false + } + ], + "gameID": [ + "01008CF01BAAC000" + ], + "gameName": "The Legend of Zelda: Echoes of Wisdom" + } + ], + "head": "01060000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01060000-03590902.png", + "name": "Urbosa", + "release": { + "au": "2017-11-10", + "eu": "2017-11-10", + "jp": "2017-11-10", + "na": "2017-11-10" + }, + "tail": "03590902", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Rosie", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "025f0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_025f0001-01c50502.png", + "name": "Rosie", + "release": { + "au": "2016-06-18", + "eu": null, + "jp": "2016-03-24", + "na": "2016-06-10" + }, + "tail": "01c50502", + "type": "Card" + }, + { + "amiiboSeries": "Super Smash Bros.", + "character": "Byleth", + "gameSeries": "Fire Emblem", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Receive a Fashion Ticket and a Music Ticket, for unlocking any of the available costumes and music tracks", + "write": false + } + ], + "gameID": [ + "0100A6301214E000" + ], + "gameName": "Fire Emblem Engage" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive better-quality randomized resources, weapons, or equipment", + "write": false + } + ], + "gameID": [ + "010071F0143EA000" + ], + "gameName": "Fire Emblem Warriors: Three Hopes" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "210b0000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_210b0000-03a50002.png", + "name": "Byleth", + "release": { + "au": "2021-03-26", + "eu": "2021-03-26", + "jp": "2021-03-26", + "na": "2021-03-26" + }, + "tail": "03a50002", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Spike", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "04b40001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_04b40001-030c0502.png", + "name": "Spike", + "release": { + "au": "2016-11-10", + "eu": "2016-11-11", + "jp": "2016-11-03", + "na": "2016-12-02" + }, + "tail": "030c0502", + "type": "Card" + }, + { + "amiiboSeries": "Legend Of Zelda", + "character": "Link", + "gameSeries": "The Legend of Zelda", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock a costume based on the character (short-hair version)", + "write": false + } + ], + "gameID": [ + "01007960049A0000" + ], + "gameName": "Bayonetta 2" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive specific crafting materials or a weapon for the character", + "write": false + } + ], + "gameID": [ + "01002B00111A2000" + ], + "gameName": "Hyrule Warriors: Age of Calamity" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a weapon rated 3 stars or higher", + "write": false + } + ], + "gameID": [ + "0100AE00096EA000" + ], + "gameName": "Hyrule Warriors: Definitive Edition" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a Legend of Zelda-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01003DA010E8A000" + ], + "gameName": "Miitopia" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a chest of loot, potentially containing gear inspired by the Legend of Zelda series", + "write": false + } + ], + "gameID": [ + "01000A10041EA000" + ], + "gameName": "The Elder Scrolls V: Skyrim" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive materials and a rare or exclusive item", + "write": false + } + ], + "gameID": [ + "01007EF00011E000" + ], + "gameName": "The Legend of Zelda: Breath of the Wild" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock the Shadow Link Plus Effect for Chamber Dungeons", + "write": false + } + ], + "gameID": [ + "01006BB00C6F0000" + ], + "gameName": "The Legend of Zelda: Link's Awakening" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock one of five amiibo-exclusive Chamber Dungeon chambers", + "write": false + }, + { + "Usage": "Save your Chamber Dungeon to the amiibo to share with friends", + "write": true + } + ], + "gameID": [ + "01006BB00C6F0000" + ], + "gameName": "The Legend of Zelda: Link's Awakening" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an amiibo-exclusive character-specific paraglider fabric", + "write": false + }, + { + "Usage": "Receive materials and a weapon or rare item", + "write": false + } + ], + "gameID": [ + "0100F2C0115B6000" + ], + "gameName": "The Legend of Zelda: Tears of the Kingdom" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive the Red Tunic", + "write": false + }, + { + "Usage": "Receive random materials", + "write": false + } + ], + "gameID": [ + "01008CF01BAAC000" + ], + "gameName": "The Legend of Zelda: Echoes of Wisdom" + } + ], + "head": "01000000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01000000-03990902.png", + "name": "Link - Link's Awakening", + "release": { + "au": "2019-09-20", + "eu": "2019-09-20", + "jp": "2019-09-20", + "na": "2019-09-20" + }, + "tail": "03990902", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Moose", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "041a0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_041a0001-00e00502.png", + "name": "Moose", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-10-29", + "na": "2016-01-22" + }, + "tail": "00e00502", + "type": "Card" + }, + { + "amiiboSeries": "8-bit Mario", + "character": "Mario", + "gameSeries": "Super Mario", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an amiibo-exclusive weapon for the character and their Rabbid counterpart", + "write": false + } + ], + "gameID": [ + "010067300059A000" + ], + "gameName": "Mario + Rabbids: Kingdom Battle" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01003DA010E8A000" + ], + "gameName": "Miitopia" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a particular power-up depending on the character", + "write": false + } + ], + "gameID": [ + "010028600EBDA000" + ], + "gameName": "Super Mario 3D World + Bowser's Fury" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a character-based costume", + "write": false + }, + { + "Usage": "Gain temporary invincibility", + "write": false + } + ], + "gameID": [ + "0100000000010000" + ], + "gameName": "Super Mario Odyssey" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock the character's shiny sticker", + "write": false + } + ], + "gameID": [ + "010036B0034E4000" + ], + "gameName": "Super Mario Party" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01006000040C2000" + ], + "gameName": "Yoshi's Crafted World" + } + ], + "head": "00000000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_00000000-02390602.png", + "name": "8-Bit Mario Modern Color", + "release": { + "au": "2015-10-24", + "eu": "2015-10-23", + "jp": "2015-09-10", + "na": "2015-09-11" + }, + "tail": "02390602", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Cobb", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "04800001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_04800001-008d0502.png", + "name": "Cobb", + "release": { + "au": "2015-10-03", + "eu": "2015-10-02", + "jp": "2015-07-30", + "na": "2015-09-25" + }, + "tail": "008d0502", + "type": "Card" + }, + { + "amiiboSeries": "Mario Sports Superstars", + "character": "Diddy Kong", + "gameSeries": "Mario Sports Superstars", + "gamesSwitch": [], + "head": "09c80101", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_09c80101-02910e02.png", + "name": "Diddy Kong - Soccer", + "release": { + "au": "2017-03-11", + "eu": "2017-03-10", + "jp": "2017-03-30", + "na": "2017-03-24" + }, + "tail": "02910e02", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Portia", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "02ef0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_02ef0001-00580502.png", + "name": "Portia", + "release": { + "au": "2015-10-03", + "eu": "2015-10-02", + "jp": "2015-07-30", + "na": "2015-09-25" + }, + "tail": "00580502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Benedict", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "029a0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_029a0001-00ee0502.png", + "name": "Benedict", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-10-29", + "na": "2016-01-22" + }, + "tail": "00ee0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Pudge", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "02800001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_02800001-00830502.png", + "name": "Pudge", + "release": { + "au": "2015-10-03", + "eu": "2015-10-02", + "jp": "2015-07-30", + "na": "2015-09-25" + }, + "tail": "00830502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Jay", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "022d0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_022d0001-00f20502.png", + "name": "Jay", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-10-29", + "na": "2016-01-22" + }, + "tail": "00f20502", + "type": "Card" + }, + { + "amiiboSeries": "Shovel Knight", + "character": "Shovel Knight", + "gameSeries": "Shovel Knight", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock boss fight against Shovel Knight", + "write": false + } + ], + "gameID": [ + "0100192003FA4000" + ], + "gameName": "Azure Striker Gunvolt: Striker Pack" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a fairy companion and player color palette matching the character", + "write": false + } + ], + "gameID": [ + "01008D100DE46000" + ], + "gameName": "Cyber Shadow" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-specific Shovel Knight remix immediately", + "write": false + } + ], + "gameID": [ + "0100830008426000" + ], + "gameName": "Just Shapes & Beats" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock Custom Knight, and save customizations to the amiibo (Shovel of Hope only)", + "write": true + }, + { + "Usage": "Unlock character-specific challenge stages, a character-based fairy companion, and costumes for the character", + "write": false + } + ], + "gameID": [ + "010057D0021E8000" + ], + "gameName": "Shovel Knight" + }, + { + "amiiboUsage": [ + { + "Usage": "Summon a fairy friend", + "write": false + } + ], + "gameID": [ + "0100B62017E68000" + ], + "gameName": "Shovel Knight Dig" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume for the character", + "write": false + } + ], + "gameID": [ + "0100B380022AE000" + ], + "gameName": "Shovel Knight Showdown" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a Spirit of the character", + "write": false + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "35c00000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_35c00000-02500a02.png", + "name": "Shovel Knight", + "release": { + "au": "2015-12-11", + "eu": "2016-01-08", + "jp": "2016-06-30", + "na": "2016-01-08" + }, + "tail": "02500a02", + "type": "Figure" + }, + { + "amiiboSeries": "Diablo", + "character": "Loot Goblin", + "gameSeries": "Diablo", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Summon a portal to Golden Greed's Domain", + "write": false + } + ], + "gameID": [ + "01001B300B9BE000" + ], + "gameName": "Diablo III: Eternal Collection" + } + ], + "head": "38c00000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_38c00000-03911602.png", + "name": "Loot Goblin", + "release": { + "au": null, + "eu": null, + "jp": null, + "na": "2018-12-21" + }, + "tail": "03911602", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Tabby", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "02690001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_02690001-011f0502.png", + "name": "Tabby", + "release": { + "au": "2016-03-19", + "eu": "2016-03-18", + "jp": "2016-01-14", + "na": "2016-03-18" + }, + "tail": "011f0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Kody", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "02810001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_02810001-01200502.png", + "name": "Kody", + "release": { + "au": "2016-03-19", + "eu": "2016-03-18", + "jp": "2016-01-14", + "na": "2016-03-18" + }, + "tail": "01200502", + "type": "Card" + }, + { + "amiiboSeries": "Mario Sports Superstars", + "character": "Bowser Jr.", + "gameSeries": "Mario Sports Superstars", + "gamesSwitch": [], + "head": "09ca0501", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_09ca0501-029f0e02.png", + "name": "Bowser Jr. - Horse Racing", + "release": { + "au": "2017-03-11", + "eu": "2017-03-10", + "jp": "2017-03-30", + "na": "2017-03-24" + }, + "tail": "029f0e02", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Apollo", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "044b0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_044b0001-016c0502.png", + "name": "Apollo", + "release": { + "au": "2016-03-19", + "eu": "2016-03-18", + "jp": "2016-01-14", + "na": "2016-03-18" + }, + "tail": "016c0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Timmy & Tommy", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "01840501", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01840501-03a90502.png", + "name": "Timmy & Tommy", + "release": { + "au": "2021-10-05", + "eu": "2021-10-05", + "jp": "2021-10-05", + "na": "2021-10-05" + }, + "tail": "03a90502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Maple", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "027e0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_027e0001-01690502.png", + "name": "Maple", + "release": { + "au": "2016-03-19", + "eu": "2016-03-18", + "jp": "2016-01-14", + "na": "2016-03-18" + }, + "tail": "01690502", + "type": "Card" + }, + { + "amiiboSeries": "Super Smash Bros.", + "character": "Mario", + "gameSeries": "Super Mario", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock an amiibo-exclusive weapon for the character and their Rabbid counterpart", + "write": false + } + ], + "gameID": [ + "010067300059A000" + ], + "gameName": "Mario + Rabbids: Kingdom Battle" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01003DA010E8A000" + ], + "gameName": "Miitopia" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a particular power-up depending on the character", + "write": false + } + ], + "gameID": [ + "010028600EBDA000" + ], + "gameName": "Super Mario 3D World + Bowser's Fury" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a character-based costume", + "write": false + }, + { + "Usage": "Gain temporary invincibility", + "write": false + } + ], + "gameID": [ + "0100000000010000" + ], + "gameName": "Super Mario Odyssey" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock the character's shiny sticker", + "write": false + } + ], + "gameID": [ + "010036B0034E4000" + ], + "gameName": "Super Mario Party" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01006000040C2000" + ], + "gameName": "Yoshi's Crafted World" + } + ], + "head": "00000100", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_00000100-00190002.png", + "name": "Dr. Mario", + "release": { + "au": "2015-07-23", + "eu": "2015-07-17", + "jp": "2015-07-17", + "na": "2015-09-11" + }, + "tail": "00190002", + "type": "Figure" + }, + { + "amiiboSeries": "Mario Sports Superstars", + "character": "Baby Mario", + "gameSeries": "Mario Sports Superstars", + "gamesSwitch": [], + "head": "09cc0501", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_09cc0501-02a90e02.png", + "name": "Baby Mario - Horse Racing", + "release": { + "au": "2017-03-11", + "eu": "2017-03-10", + "jp": "2017-03-30", + "na": "2017-03-24" + }, + "tail": "02a90e02", + "type": "Card" + }, + { + "amiiboSeries": "Mario Sports Superstars", + "character": "Baby Luigi", + "gameSeries": "Mario Sports Superstars", + "gamesSwitch": [], + "head": "09cd0301", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_09cd0301-02ac0e02.png", + "name": "Baby Luigi - Tennis", + "release": { + "au": "2017-03-11", + "eu": "2017-03-10", + "jp": "2017-03-30", + "na": "2017-03-24" + }, + "tail": "02ac0e02", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Annalisa", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "02080001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_02080001-00960502.png", + "name": "Annalisa", + "release": { + "au": "2015-10-03", + "eu": "2015-10-02", + "jp": "2015-07-30", + "na": "2015-09-25" + }, + "tail": "00960502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Tia", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "032d0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_032d0001-00bc0502.png", + "name": "Tia", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-10-29", + "na": "2016-01-22" + }, + "tail": "00bc0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Yuka", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "03bc0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_03bc0001-008a0502.png", + "name": "Yuka", + "release": { + "au": "2015-10-03", + "eu": "2015-10-02", + "jp": "2015-07-30", + "na": "2015-09-25" + }, + "tail": "008a0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Tom Nook", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01003DA010E8A000" + ], + "gameName": "Miitopia" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a Spirit of the character", + "write": false + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "01830000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01830000-02420502.png", + "name": "Tom Nook", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-11-21", + "na": "2015-11-13" + }, + "tail": "02420502", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Pate", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "03090001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_03090001-00c60502.png", + "name": "Pate", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-10-29", + "na": "2016-01-22" + }, + "tail": "00c60502", + "type": "Card" + }, + { + "amiiboSeries": "Super Smash Bros.", + "character": "Kirby", + "gameSeries": "Kirby", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Receive star coins and a boost item", + "write": false + } + ], + "gameID": [ + "01004D300C5AE000" + ], + "gameName": "Kirby and the Forgotten Land" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive two Picture Pieces, a Maxim Tomato, and two Point Stars", + "write": false + } + ], + "gameID": [ + "01007E3006DDA000" + ], + "gameName": "Kirby Star Allies" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive more useful items", + "write": false + } + ], + "gameID": [ + "01006B601380E000" + ], + "gameName": "Kirby's Return to Dream Land Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a Kirby-themed racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01003DA010E8A000" + ], + "gameName": "Miitopia" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive 10 Fragments", + "write": false + } + ], + "gameID": [ + "01003FB00C5A8000" + ], + "gameName": "Super Kirby Clash" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "1f000000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_1f000000-000a0002.png", + "name": "Kirby", + "release": { + "au": "2014-11-29", + "eu": "2014-11-28", + "jp": "2014-12-06", + "na": "2014-11-21" + }, + "tail": "000a0002", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Carmen", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "04a40001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_04a40001-00d40502.png", + "name": "Carmen", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-10-29", + "na": "2016-01-22" + }, + "tail": "00d40502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Phineas", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "019c0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_019c0001-01730502.png", + "name": "Phineas", + "release": { + "au": "2016-06-18", + "eu": null, + "jp": "2016-03-24", + "na": "2016-06-10" + }, + "tail": "01730502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Lily", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "03380001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_03380001-011d0502.png", + "name": "Lily", + "release": { + "au": "2016-03-19", + "eu": "2016-03-18", + "jp": "2016-01-14", + "na": "2016-03-18" + }, + "tail": "011d0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Bob", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "025d0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_025d0001-00550502.png", + "name": "Bob", + "release": { + "au": "2015-10-03", + "eu": "2015-10-02", + "jp": "2015-07-30", + "na": "2015-09-25" + }, + "tail": "00550502", + "type": "Card" + }, + { + "amiiboSeries": "Monster Hunter", + "character": "Nabiru", + "gameSeries": "Monster Hunter", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock Monster Hunter Stories 2 sticker set", + "write": false + } + ], + "gameID": [ + "0100B04011742000" + ], + "gameName": "Monster Hunter Rise" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a character-based monstie egg", + "write": false + } + ], + "gameID": [ + "010069301B1D4000" + ], + "gameName": "Monster Hunter Stories" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock the Hakum Rider Outfit layered armor set / Have Tsukino read your fortune and receive a random item", + "write": false + } + ], + "gameID": [ + "0100E21011446000" + ], + "gameName": "Monster Hunter Stories 2: Wings of Ruin" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a lot of BP / Receive better supplies (compared to other amiibo)", + "write": false + } + ], + "gameID": [ + "0100643002136000" + ], + "gameName": "Resident Evil Revelations" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a lot of BP / Receive better supplies (compared to other amiibo)", + "write": false + } + ], + "gameID": [ + "010095300212A000" + ], + "gameName": "Resident Evil Revelations 2" + } + ], + "head": "35010000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_35010000-02e30f02.png", + "name": "Nabiru", + "release": { + "au": null, + "eu": null, + "jp": "2016-10-08", + "na": null + }, + "tail": "02e30f02", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Cyrus", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "018b0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_018b0001-01150502.png", + "name": "Cyrus", + "release": { + "au": "2016-03-19", + "eu": "2016-03-18", + "jp": "2016-01-14", + "na": "2016-03-18" + }, + "tail": "01150502", + "type": "Card" + }, + { + "amiiboSeries": "Monster Hunter Rise", + "character": "Palamute", + "gameSeries": "Monster Hunter", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock special layered armor / Enter daily lottery for a variety of useful items", + "write": false + } + ], + "gameID": [ + "0100B04011742000" + ], + "gameName": "Monster Hunter Rise" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock the Hunter Sticker Set / Have Tsukino read your fortune and receive a random item", + "write": false + } + ], + "gameID": [ + "0100E21011446000" + ], + "gameName": "Monster Hunter Stories 2: Wings of Ruin" + } + ], + "head": "350a0000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_350a0000-04111802.png", + "name": "Palamute", + "release": { + "au": "2021-03-26", + "eu": "2021-03-26", + "jp": "2021-03-26", + "na": "2021-03-26" + }, + "tail": "04111802", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Isabelle", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "01810401", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01810401-03aa0502.png", + "name": "Isabelle", + "release": { + "au": "2021-11-05", + "eu": "2021-11-05", + "jp": "2021-11-05", + "na": "2021-11-05" + }, + "tail": "03aa0502", + "type": "Card" + }, + { + "amiiboSeries": "Super Smash Bros.", + "character": "Kazuya", + "gameSeries": "Tekken", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "33c00000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_33c00000-04200002.png", + "name": "Kazuya", + "release": { + "au": "2023-01-13", + "eu": "2023-01-13", + "jp": "2023-01-13", + "na": "2023-01-13" + }, + "tail": "04200002", + "type": "Figure" + }, + { + "amiiboSeries": "Super Smash Bros.", + "character": "Falco", + "gameSeries": "Star Fox", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock a Star Fox costume", + "write": false + } + ], + "gameID": [ + "01007960049A0000" + ], + "gameName": "Bayonetta 2" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "05810000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_05810000-001c0002.png", + "name": "Falco", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-11-05", + "na": "2015-11-20" + }, + "tail": "001c0002", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Savannah", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "03a60001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_03a60001-00c80502.png", + "name": "Savannah", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-10-29", + "na": "2016-01-22" + }, + "tail": "00c80502", + "type": "Card" + }, + { + "amiiboSeries": "Super Smash Bros.", + "character": "Lucas", + "gameSeries": "Earthbound", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "22810000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_22810000-02510002.png", + "name": "Lucas", + "release": { + "au": "2016-01-30", + "eu": "2016-01-29", + "jp": "2015-12-17", + "na": "2016-01-30" + }, + "tail": "02510002", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Phil", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "043d0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_043d0001-007c0502.png", + "name": "Phil", + "release": { + "au": "2015-10-03", + "eu": "2015-10-02", + "jp": "2015-07-30", + "na": "2015-09-25" + }, + "tail": "007c0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Pete", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "019f0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_019f0001-01110502.png", + "name": "Pete", + "release": { + "au": "2016-03-19", + "eu": "2016-03-18", + "jp": "2016-01-14", + "na": "2016-03-18" + }, + "tail": "01110502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Graham", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "03800001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_03800001-01870502.png", + "name": "Graham", + "release": { + "au": "2016-06-18", + "eu": null, + "jp": "2016-03-24", + "na": "2016-06-10" + }, + "tail": "01870502", + "type": "Card" + }, + { + "amiiboSeries": "Monster Hunter", + "character": "Barioth", + "gameSeries": "Monster Hunter", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock Monster Hunter Stories 2 sticker set", + "write": false + } + ], + "gameID": [ + "0100B04011742000" + ], + "gameName": "Monster Hunter Rise" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a character-based monstie egg", + "write": false + } + ], + "gameID": [ + "010069301B1D4000" + ], + "gameName": "Monster Hunter Stories" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock the Hakum Rider Outfit layered armor set / Have Tsukino read your fortune and receive a random item", + "write": false + } + ], + "gameID": [ + "0100E21011446000" + ], + "gameName": "Monster Hunter Stories 2: Wings of Ruin" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a lot of BP / Receive better supplies (compared to other amiibo)", + "write": false + } + ], + "gameID": [ + "0100643002136000" + ], + "gameName": "Resident Evil Revelations" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a lot of BP / Receive better supplies (compared to other amiibo)", + "write": false + } + ], + "gameID": [ + "010095300212A000" + ], + "gameName": "Resident Evil Revelations 2" + } + ], + "head": "35030100", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_35030100-02e50f02.png", + "name": "Barioth and Ayuria", + "release": { + "au": null, + "eu": null, + "jp": "2016-12-08", + "na": null + }, + "tail": "02e50f02", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Freckles", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "030e0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_030e0001-012f0502.png", + "name": "Freckles", + "release": { + "au": "2016-03-19", + "eu": "2016-03-18", + "jp": "2016-01-14", + "na": "2016-03-18" + }, + "tail": "012f0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Roswell", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "0a1f0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_0a1f0001-03d60502.png", + "name": "Roswell", + "release": { + "au": "2021-11-05", + "eu": "2021-11-05", + "jp": "2021-11-05", + "na": "2021-11-05" + }, + "tail": "03d60502", + "type": "Card" + }, + { + "amiiboSeries": "Super Smash Bros.", + "character": "Bowser", + "gameSeries": "Super Mario", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock the Chain Chomp weapon", + "write": false + } + ], + "gameID": [ + "01007960049A0000" + ], + "gameName": "Bayonetta 2" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01003DA010E8A000" + ], + "gameName": "Miitopia" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a particular power-up depending on the character", + "write": false + }, + { + "Usage": "Make Fury Bowser appear (in Bowser's Fury mode)", + "write": false + } + ], + "gameID": [ + "010028600EBDA000" + ], + "gameName": "Super Mario 3D World + Bowser's Fury" + }, + { + "amiiboUsage": [ + { + "Usage": "Reveal regional coin locations", + "write": false + } + ], + "gameID": [ + "0100000000010000" + ], + "gameName": "Super Mario Odyssey" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock the character's shiny sticker", + "write": false + } + ], + "gameID": [ + "010036B0034E4000" + ], + "gameName": "Super Mario Party" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01006000040C2000" + ], + "gameName": "Yoshi's Crafted World" + } + ], + "head": "00050000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_00050000-00140002.png", + "name": "Bowser", + "release": { + "au": "2015-01-29", + "eu": "2015-01-23", + "jp": "2015-01-22", + "na": "2015-02-01" + }, + "tail": "00140002", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Benjamin", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "02fa0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_02fa0001-00970502.png", + "name": "Benjamin", + "release": { + "au": "2015-10-03", + "eu": "2015-10-02", + "jp": "2015-07-30", + "na": "2015-09-25" + }, + "tail": "00970502", + "type": "Card" + }, + { + "amiiboSeries": "Metroid", + "character": "Samus", + "gameSeries": "Metroid", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock a costume based on the character (short-hair version)", + "write": false + } + ], + "gameID": [ + "01007960049A0000" + ], + "gameName": "Bayonetta 2" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a Metroid-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Permanently increase health by 100", + "write": false + } + ], + "gameID": [ + "010093801237C000" + ], + "gameName": "Metroid Dread" + }, + { + "amiiboUsage": [ + { + "Usage": "Restore a random amount of health once per day", + "write": false + } + ], + "gameID": [ + "010093801237C000" + ], + "gameName": "Metroid Dread" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01003DA010E8A000" + ], + "gameName": "Miitopia" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a Spirit of the character", + "write": false + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "05c00000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_05c00000-04121302.png", + "name": "Samus - Metroid Dread", + "release": { + "au": "2021-10-08", + "eu": "2021-11-05", + "jp": "2021-10-08", + "na": "2021-10-08" + }, + "tail": "04121302", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Lopez", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "02db0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_02db0001-005e0502.png", + "name": "Lopez", + "release": { + "au": "2015-10-03", + "eu": "2015-10-02", + "jp": "2015-07-30", + "na": "2015-09-25" + }, + "tail": "005e0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Willow", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "04cc0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_04cc0001-00a40502.png", + "name": "Willow", + "release": { + "au": "2015-10-03", + "eu": "2015-10-02", + "jp": "2015-07-30", + "na": "2015-09-25" + }, + "tail": "00a40502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Jambette", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "03450001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_03450001-005f0502.png", + "name": "Jambette", + "release": { + "au": "2015-10-03", + "eu": "2015-10-02", + "jp": "2015-07-30", + "na": "2015-09-25" + }, + "tail": "005f0502", + "type": "Card" + }, + { + "amiiboSeries": "Super Smash Bros.", + "character": "Charizard", + "gameSeries": "Pokemon", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "19060000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_19060000-00240002.png", + "name": "Charizard", + "release": { + "au": "2015-04-25", + "eu": "2015-04-24", + "jp": "2015-04-29", + "na": "2015-05-29" + }, + "tail": "00240002", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Mira", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "04a70001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_04a70001-01a60502.png", + "name": "Mira", + "release": { + "au": "2016-06-18", + "eu": null, + "jp": "2016-03-24", + "na": "2016-06-10" + }, + "tail": "01a60502", + "type": "Card" + }, + { + "amiiboSeries": "Super Mario Bros.", + "character": "Mario", + "gameSeries": "Super Mario", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an amiibo-exclusive weapon for the character and their Rabbid counterpart", + "write": false + } + ], + "gameID": [ + "010067300059A000" + ], + "gameName": "Mario + Rabbids: Kingdom Battle" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01003DA010E8A000" + ], + "gameName": "Miitopia" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a particular power-up depending on the character", + "write": false + } + ], + "gameID": [ + "010028600EBDA000" + ], + "gameName": "Super Mario 3D World + Bowser's Fury" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock the character's shiny sticker", + "write": false + } + ], + "gameID": [ + "010036B0034E4000" + ], + "gameName": "Super Mario Party" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01006000040C2000" + ], + "gameName": "Yoshi's Crafted World" + } + ], + "head": "00000300", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_00000300-03a60102.png", + "name": "Mario - Cat", + "release": { + "au": "2021-02-12", + "eu": "2021-02-12", + "jp": "2021-02-12", + "na": "2021-02-12" + }, + "tail": "03a60102", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Shrunk", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "01b10101", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01b10101-017b0502.png", + "name": "Shrunk - Loud Jacket", + "release": { + "au": "2016-06-18", + "eu": null, + "jp": "2016-03-24", + "na": "2016-06-10" + }, + "tail": "017b0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Raymond", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "0a0f0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_0a0f0001-03c60502.png", + "name": "Raymond", + "release": { + "au": "2021-11-05", + "eu": "2021-11-05", + "jp": "2021-11-05", + "na": "2021-11-05" + }, + "tail": "03c60502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Ione", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "0a120001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_0a120001-03c90502.png", + "name": "Ione", + "release": { + "au": "2021-11-05", + "eu": "2021-11-05", + "jp": "2021-11-05", + "na": "2021-11-05" + }, + "tail": "03c90502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Molly", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "03170001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_03170001-00a60502.png", + "name": "Molly", + "release": { + "au": "2015-10-03", + "eu": "2015-10-02", + "jp": "2015-07-30", + "na": "2015-09-25" + }, + "tail": "00a60502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Gracie", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "01a90001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01a90001-01760502.png", + "name": "Gracie", + "release": { + "au": "2016-06-18", + "eu": null, + "jp": "2016-03-24", + "na": "2016-06-10" + }, + "tail": "01760502", + "type": "Card" + }, + { + "amiiboSeries": "Fire Emblem", + "character": "Tiki", + "gameSeries": "Fire Emblem", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Receive a Fashion Ticket and a Music Ticket, for unlocking any of the available costumes and music tracks", + "write": false + } + ], + "gameID": [ + "0100A6301214E000" + ], + "gameName": "Fire Emblem Engage" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an amiibo-exclusive weapon", + "write": false + }, + { + "Usage": "Receive a weapon", + "write": false + } + ], + "gameID": [ + "0100F15003E64000" + ], + "gameName": "Fire Emblem Warriors" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive better-quality randomized resources, weapons, or equipment", + "write": false + } + ], + "gameID": [ + "010071F0143EA000" + ], + "gameName": "Fire Emblem Warriors: Three Hopes" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special piece of battle music / Receive higher-quality items and materials", + "write": false + } + ], + "gameID": [ + "010055D009F78000" + ], + "gameName": "Fire Emblem: Three Houses" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a Spirit of the character", + "write": false + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "21090000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_21090000-03701202.png", + "name": "Tiki", + "release": { + "au": "2017-10-20", + "eu": "2017-10-20", + "jp": "2017-09-28", + "na": "2017-10-20" + }, + "tail": "03701202", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Hans", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "03730001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_03730001-01340502.png", + "name": "Hans", + "release": { + "au": "2016-03-19", + "eu": "2016-03-18", + "jp": "2016-01-14", + "na": "2016-03-18" + }, + "tail": "01340502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Nat", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "019b0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_019b0001-00b60502.png", + "name": "Nat", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-10-29", + "na": "2016-01-22" + }, + "tail": "00b60502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Tommy", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "01860301", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01860301-01750502.png", + "name": "Tommy - Suit", + "release": { + "au": "2016-06-18", + "eu": null, + "jp": "2016-03-24", + "na": "2016-06-10" + }, + "tail": "01750502", + "type": "Card" + }, + { + "amiiboSeries": "Super Smash Bros.", + "character": "Mega Man", + "gameSeries": "Megaman", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive E Tanks and other useful in-game items", + "write": false + } + ], + "gameID": [ + "0100B0C0086B0000" + ], + "gameName": "Mega Man 11" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock 11 exclusive challenge stages designed by fans", + "write": false + } + ], + "gameID": [ + "01002D4007AE0000" + ], + "gameName": "Mega Man Legacy Collection" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock new platforming challenges", + "write": false + } + ], + "gameID": [ + "0100842008EC4000" + ], + "gameName": "Mega Man Legacy Collection 2" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a lot of BP / Receive better supplies (compared to other amiibo)", + "write": false + } + ], + "gameID": [ + "0100643002136000" + ], + "gameName": "Resident Evil Revelations" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a lot of BP / Receive better supplies (compared to other amiibo)", + "write": false + } + ], + "gameID": [ + "010095300212A000" + ], + "gameName": "Resident Evil Revelations 2" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "34800000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_34800000-02580002.png", + "name": "Mega Man - Gold Edition", + "release": { + "au": null, + "eu": null, + "jp": null, + "na": "2016-02-23" + }, + "tail": "02580002", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Timmy", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "01850201", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01850201-01170502.png", + "name": "Timmy - Full Apron", + "release": { + "au": "2016-03-19", + "eu": "2016-03-18", + "jp": "2016-01-14", + "na": "2016-03-18" + }, + "tail": "01170502", + "type": "Card" + }, + { + "amiiboSeries": "Legend Of Zelda", + "character": "Zelda", + "gameSeries": "The Legend of Zelda", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive specific crafting materials or a weapon for the character", + "write": false + } + ], + "gameID": [ + "01002B00111A2000" + ], + "gameName": "Hyrule Warriors: Age of Calamity" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a weapon rated 3 stars or higher", + "write": false + } + ], + "gameID": [ + "0100AE00096EA000" + ], + "gameName": "Hyrule Warriors: Definitive Edition" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a Legend of Zelda-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01003DA010E8A000" + ], + "gameName": "Miitopia" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a chest of loot, potentially containing gear inspired by the Legend of Zelda series", + "write": false + } + ], + "gameID": [ + "01000A10041EA000" + ], + "gameName": "The Elder Scrolls V: Skyrim" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive materials and a rare or exclusive item", + "write": false + } + ], + "gameID": [ + "01007EF00011E000" + ], + "gameName": "The Legend of Zelda: Breath of the Wild" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock one of five amiibo-exclusive Chamber Dungeon chambers", + "write": false + }, + { + "Usage": "Save your Chamber Dungeon to the amiibo to share with friends", + "write": true + } + ], + "gameID": [ + "01006BB00C6F0000" + ], + "gameName": "The Legend of Zelda: Link's Awakening" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an amiibo-exclusive character-specific paraglider fabric", + "write": false + }, + { + "Usage": "Receive materials and a weapon or rare item", + "write": false + } + ], + "gameID": [ + "0100F2C0115B6000" + ], + "gameName": "The Legend of Zelda: Tears of the Kingdom" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive the Blue Attire", + "write": false + }, + { + "Usage": "Receive random materials", + "write": false + } + ], + "gameID": [ + "01008CF01BAAC000" + ], + "gameName": "The Legend of Zelda: Echoes of Wisdom" + } + ], + "head": "01010000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01010000-04190902.png", + "name": "Zelda - Tears of the Kingdom", + "release": { + "au": "2023-11-03", + "eu": "2023-11-03", + "jp": "2023-11-03", + "na": "2023-11-03" + }, + "tail": "04190902", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Felicity", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "026e0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_026e0001-00ba0502.png", + "name": "Felicity", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-10-29", + "na": "2016-01-22" + }, + "tail": "00ba0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Gulliver", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "01a20001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01a20001-017d0502.png", + "name": "Gulliver", + "release": { + "au": "2016-06-18", + "eu": null, + "jp": "2016-03-24", + "na": "2016-06-10" + }, + "tail": "017d0502", + "type": "Card" + }, + { + "amiiboSeries": "Mario Sports Superstars", + "character": "Waluigi", + "gameSeries": "Mario Sports Superstars", + "gamesSwitch": [], + "head": "09c60201", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_09c60201-02880e02.png", + "name": "Waluigi - Baseball", + "release": { + "au": "2017-03-11", + "eu": "2017-03-10", + "jp": "2017-03-30", + "na": "2017-03-24" + }, + "tail": "02880e02", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Blathers", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "01920001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01920001-010d0502.png", + "name": "Blathers", + "release": { + "au": "2016-03-19", + "eu": "2016-03-18", + "jp": "2016-01-14", + "na": "2016-03-18" + }, + "tail": "010d0502", + "type": "Card" + }, + { + "amiiboSeries": "Kirby", + "character": "Kirby", + "gameSeries": "Kirby", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Receive star coins and a boost item", + "write": false + } + ], + "gameID": [ + "01004D300C5AE000" + ], + "gameName": "Kirby and the Forgotten Land" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive two Picture Pieces, a Maxim Tomato, and two Point Stars", + "write": false + } + ], + "gameID": [ + "01007E3006DDA000" + ], + "gameName": "Kirby Star Allies" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive more useful items", + "write": false + } + ], + "gameID": [ + "01006B601380E000" + ], + "gameName": "Kirby's Return to Dream Land Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a Kirby-themed racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01003DA010E8A000" + ], + "gameName": "Miitopia" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive 20 Fragments", + "write": false + } + ], + "gameID": [ + "01003FB00C5A8000" + ], + "gameName": "Super Kirby Clash" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "1f000000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_1f000000-02540c02.png", + "name": "Kirby", + "release": { + "au": "2016-06-11", + "eu": "2016-06-10", + "jp": "2016-04-28", + "na": "2016-06-10" + }, + "tail": "02540c02", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Rodeo", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "024b0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_024b0001-01260502.png", + "name": "Rodeo", + "release": { + "au": "2016-03-19", + "eu": "2016-03-18", + "jp": "2016-01-14", + "na": "2016-03-18" + }, + "tail": "01260502", + "type": "Card" + }, + { + "amiiboSeries": "Mario Sports Superstars", + "character": "Baby Mario", + "gameSeries": "Mario Sports Superstars", + "gamesSwitch": [], + "head": "09cc0401", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_09cc0401-02a80e02.png", + "name": "Baby Mario - Golf", + "release": { + "au": "2017-03-11", + "eu": "2017-03-10", + "jp": "2017-03-30", + "na": "2017-03-24" + }, + "tail": "02a80e02", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Kitt", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "03d10001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_03d10001-00c20502.png", + "name": "Kitt", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-10-29", + "na": "2016-01-22" + }, + "tail": "00c20502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Prince", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "03440001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_03440001-00c50502.png", + "name": "Prince", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-10-29", + "na": "2016-01-22" + }, + "tail": "00c50502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "K.K. Slider", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "01820001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01820001-01d80502.png", + "name": "K. K. Slider - Pikopuri", + "release": { + "au": null, + "eu": null, + "jp": "2016-03-15", + "na": null + }, + "tail": "01d80502", + "type": "Card" + }, + { + "amiiboSeries": "Super Smash Bros.", + "character": "Olimar", + "gameSeries": "Pikmin", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock a Pikmin-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "06400100", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_06400100-001e0002.png", + "name": "Olimar", + "release": { + "au": "2015-07-23", + "eu": "2015-07-17", + "jp": "2015-07-17", + "na": "2015-09-11" + }, + "tail": "001e0002", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Clyde", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "03ae0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_03ae0001-00870502.png", + "name": "Clyde", + "release": { + "au": "2015-10-03", + "eu": "2015-10-02", + "jp": "2015-07-30", + "na": "2015-09-25" + }, + "tail": "00870502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Ruby", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "049d0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_049d0001-00ed0502.png", + "name": "Ruby", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-10-29", + "na": "2016-01-22" + }, + "tail": "00ed0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Groucho", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "021a0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_021a0001-00da0502.png", + "name": "Groucho", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-10-29", + "na": "2016-01-22" + }, + "tail": "00da0502", + "type": "Card" + }, + { + "amiiboSeries": "Super Smash Bros.", + "character": "King Dedede", + "gameSeries": "Kirby", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Receive star coins and a boost item", + "write": false + } + ], + "gameID": [ + "01004D300C5AE000" + ], + "gameName": "Kirby and the Forgotten Land" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive two Picture Pieces, a Maxim Tomato, and two Point Stars", + "write": false + } + ], + "gameID": [ + "01007E3006DDA000" + ], + "gameName": "Kirby Star Allies" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive more useful items", + "write": false + } + ], + "gameID": [ + "01006B601380E000" + ], + "gameName": "Kirby's Return to Dream Land Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a Kirby-themed racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive 10 Fragments", + "write": false + } + ], + "gameID": [ + "01003FB00C5A8000" + ], + "gameName": "Super Kirby Clash" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "1f020000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_1f020000-00280002.png", + "name": "King Dedede", + "release": { + "au": "2015-01-29", + "eu": "2015-02-20", + "jp": "2015-01-22", + "na": "2015-02-01" + }, + "tail": "00280002", + "type": "Figure" + }, + { + "amiiboSeries": "Super Smash Bros.", + "character": "Roy", + "gameSeries": "Fire Emblem", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Receive a Fashion Ticket and a Music Ticket, for unlocking any of the available costumes and music tracks", + "write": false + } + ], + "gameID": [ + "0100A6301214E000" + ], + "gameName": "Fire Emblem Engage" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a weapon", + "write": false + } + ], + "gameID": [ + "0100F15003E64000" + ], + "gameName": "Fire Emblem Warriors" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive better-quality randomized resources, weapons, or equipment", + "write": false + } + ], + "gameID": [ + "010071F0143EA000" + ], + "gameName": "Fire Emblem Warriors: Three Hopes" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special piece of battle music / Receive higher-quality items and materials", + "write": false + } + ], + "gameID": [ + "010055D009F78000" + ], + "gameName": "Fire Emblem: Three Houses" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "21040000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_21040000-02520002.png", + "name": "Roy", + "release": { + "au": "2016-03-19", + "eu": "2016-03-18", + "jp": "2016-04-28", + "na": "2016-03-18" + }, + "tail": "02520002", + "type": "Figure" + }, + { + "amiiboSeries": "Monster Hunter Rise", + "character": "Malzeno", + "gameSeries": "Monster Hunter", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock special layered armor / Enter daily lottery for a variety of useful items", + "write": false + } + ], + "gameID": [ + "0100B04011742000" + ], + "gameName": "Monster Hunter Rise" + } + ], + "head": "350b0000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_350b0000-042d1802.png", + "name": "Malzeno", + "release": { + "au": "2022-06-30", + "eu": "2022-06-30", + "jp": "2022-06-30", + "na": "2022-06-30" + }, + "tail": "042d1802", + "type": "Figure" + }, + { + "amiiboSeries": "Super Mario Bros.", + "character": "Mario", + "gameSeries": "Super Mario", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock Super Mario Odyssey-themed levels early", + "write": false + } + ], + "gameID": [ + "01009BF0072D4000" + ], + "gameName": "Captain Toad: Treasure Tracker" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an amiibo-exclusive weapon for the character and their Rabbid counterpart", + "write": false + } + ], + "gameID": [ + "010067300059A000" + ], + "gameName": "Mario + Rabbids: Kingdom Battle" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01003DA010E8A000" + ], + "gameName": "Miitopia" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a particular power-up depending on the character", + "write": false + } + ], + "gameID": [ + "010028600EBDA000" + ], + "gameName": "Super Mario 3D World + Bowser's Fury" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a character-based costume", + "write": false + } + ], + "gameID": [ + "0100000000010000" + ], + "gameName": "Super Mario Odyssey" + }, + { + "amiiboUsage": [ + { + "Usage": "Gain temporary invincibility", + "write": false + } + ], + "gameID": [ + "0100000000010000" + ], + "gameName": "Super Mario Odyssey" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock the character's shiny sticker", + "write": false + } + ], + "gameID": [ + "010036B0034E4000" + ], + "gameName": "Super Mario Party" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a Spirit of the character", + "write": false + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01006000040C2000" + ], + "gameName": "Yoshi's Crafted World" + } + ], + "head": "00000000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_00000000-03710102.png", + "name": "Mario - Wedding", + "release": { + "au": "2017-10-27", + "eu": "2017-10-27", + "jp": "2017-10-27", + "na": "2017-10-27" + }, + "tail": "03710102", + "type": "Figure" + }, + { + "amiiboSeries": "Yoshi's Woolly World", + "character": "Yoshi", + "gameSeries": "Super Mario", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an amiibo-exclusive weapon for the character and their Rabbid counterpart", + "write": false + } + ], + "gameID": [ + "010067300059A000" + ], + "gameName": "Mario + Rabbids: Kingdom Battle" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01003DA010E8A000" + ], + "gameName": "Miitopia" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a particular power-up depending on the character", + "write": false + } + ], + "gameID": [ + "010028600EBDA000" + ], + "gameName": "Super Mario 3D World + Bowser's Fury" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock the character's shiny sticker", + "write": false + } + ], + "gameID": [ + "010036B0034E4000" + ], + "gameName": "Super Mario Party" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01006000040C2000" + ], + "gameName": "Yoshi's Crafted World" + } + ], + "head": "00030102", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_00030102-00430302.png", + "name": "Light Blue Yarn Yoshi", + "release": { + "au": "2015-06-25", + "eu": "2015-06-26", + "jp": "2015-07-16", + "na": "2015-10-16" + }, + "tail": "00430302", + "type": "Yarn" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Baabara", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "04c60001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_04c60001-01670502.png", + "name": "Baabara", + "release": { + "au": "2016-03-19", + "eu": "2016-03-18", + "jp": "2016-01-14", + "na": "2016-03-18" + }, + "tail": "01670502", + "type": "Card" + }, + { + "amiiboSeries": "Super Smash Bros.", + "character": "Mr. G&W", + "gameSeries": "Classic Nintendo", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "07800000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_07800000-002d0002.png", + "name": "Mr. Game & Watch", + "release": { + "au": "2015-09-26", + "eu": "2015-09-25", + "jp": "2015-10-29", + "na": "2015-09-25" + }, + "tail": "002d0002", + "type": "Figure" + }, + { + "amiiboSeries": "Super Smash Bros.", + "character": "Captain Falcon", + "gameSeries": "F-Zero", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "06000000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_06000000-00120002.png", + "name": "Captain Falcon", + "release": { + "au": "2014-12-12", + "eu": "2014-12-19", + "jp": "2014-12-06", + "na": "2014-12-14" + }, + "tail": "00120002", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Static", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "04e50001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_04e50001-01ad0502.png", + "name": "Static", + "release": { + "au": "2016-06-18", + "eu": null, + "jp": "2016-03-24", + "na": "2016-06-10" + }, + "tail": "01ad0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Rod", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "04110001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_04110001-01ab0502.png", + "name": "Rod", + "release": { + "au": "2016-06-18", + "eu": null, + "jp": "2016-03-24", + "na": "2016-06-10" + }, + "tail": "01ab0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Bitty", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "03950001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_03950001-02fc0502.png", + "name": "Bitty", + "release": { + "au": "2016-11-10", + "eu": "2016-11-11", + "jp": "2016-11-03", + "na": "2016-12-02" + }, + "tail": "02fc0502", + "type": "Card" + }, + { + "amiiboSeries": "Super Mario Bros.", + "character": "Peach", + "gameSeries": "Super Mario", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock a costume based on the character (short-hair version)", + "write": false + } + ], + "gameID": [ + "01007960049A0000" + ], + "gameName": "Bayonetta 2" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an amiibo-exclusive weapon for the character and their Rabbid counterpart", + "write": false + } + ], + "gameID": [ + "010067300059A000" + ], + "gameName": "Mario + Rabbids: Kingdom Battle" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01003DA010E8A000" + ], + "gameName": "Miitopia" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a particular power-up depending on the character", + "write": false + } + ], + "gameID": [ + "010028600EBDA000" + ], + "gameName": "Super Mario 3D World + Bowser's Fury" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock the character's shiny sticker", + "write": false + } + ], + "gameID": [ + "010036B0034E4000" + ], + "gameName": "Super Mario Party" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01006000040C2000" + ], + "gameName": "Yoshi's Crafted World" + } + ], + "head": "00020100", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_00020100-03a70102.png", + "name": "Peach - Cat", + "release": { + "au": "2021-02-12", + "eu": "2021-02-12", + "jp": "2021-02-12", + "na": "2021-02-12" + }, + "tail": "03a70102", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Buzz", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "044e0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_044e0001-03150502.png", + "name": "Buzz", + "release": { + "au": "2016-11-10", + "eu": "2016-11-11", + "jp": "2016-11-03", + "na": "2016-12-02" + }, + "tail": "03150502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Blathers", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "01920001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01920001-03ad0502.png", + "name": "Blathers", + "release": { + "au": "2021-11-05", + "eu": "2021-11-05", + "jp": "2021-11-05", + "na": "2021-11-05" + }, + "tail": "03ad0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Bonbon", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "04a50001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_04a50001-00740502.png", + "name": "Bonbon", + "release": { + "au": "2015-10-03", + "eu": "2015-10-02", + "jp": "2015-07-30", + "na": "2015-09-25" + }, + "tail": "00740502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Rolf", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "04fa0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_04fa0001-01680502.png", + "name": "Rolf", + "release": { + "au": "2016-03-19", + "eu": "2016-03-18", + "jp": "2016-01-14", + "na": "2016-03-18" + }, + "tail": "01680502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Queenie", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "04360001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_04360001-01940502.png", + "name": "Queenie", + "release": { + "au": "2016-06-18", + "eu": null, + "jp": "2016-03-24", + "na": "2016-06-10" + }, + "tail": "01940502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Diana", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "02de0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_02de0001-009c0502.png", + "name": "Diana", + "release": { + "au": "2015-10-03", + "eu": "2015-10-02", + "jp": "2015-07-30", + "na": "2015-09-25" + }, + "tail": "009c0502", + "type": "Card" + }, + { + "amiiboSeries": "Power Pros", + "character": "Hayakawa", + "gameSeries": "Power Pros", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Receive in-game items and power-ups / Save items to your card after playing with friends to bring them home", + "write": true + } + ], + "gameID": [ + "0100E9C00BF28000" + ], + "gameName": "Jikkyou Powerful Pro Baseball" + } + ], + "head": "38030001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_38030001-03961702.png", + "name": "Hayakawa", + "release": { + "au": null, + "eu": null, + "jp": "2019-06-27", + "na": null + }, + "tail": "03961702", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Alice", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "03bd0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_03bd0001-00f90502.png", + "name": "Alice", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-10-29", + "na": "2016-01-22" + }, + "tail": "00f90502", + "type": "Card" + }, + { + "amiiboSeries": "Super Mario Bros.", + "character": "Wario", + "gameSeries": "Super Mario", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a particular power-up depending on the character", + "write": false + } + ], + "gameID": [ + "010028600EBDA000" + ], + "gameName": "Super Mario 3D World + Bowser's Fury" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a character-based costume", + "write": false + } + ], + "gameID": [ + "0100000000010000" + ], + "gameName": "Super Mario Odyssey" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock the character's shiny sticker", + "write": false + } + ], + "gameID": [ + "010036B0034E4000" + ], + "gameName": "Super Mario Party" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "00070000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_00070000-02630102.png", + "name": "Wario", + "release": { + "au": "2016-10-08", + "eu": "2016-10-07", + "jp": "2016-10-20", + "na": "2016-11-04" + }, + "tail": "02630102", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Paula", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "021e0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_021e0001-01230502.png", + "name": "Paula", + "release": { + "au": "2016-03-19", + "eu": "2016-03-18", + "jp": "2016-01-14", + "na": "2016-03-18" + }, + "tail": "01230502", + "type": "Card" + }, + { + "amiiboSeries": "Mario Sports Superstars", + "character": "Metal Mario", + "gameSeries": "Mario Sports Superstars", + "gamesSwitch": [], + "head": "09d00401", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_09d00401-02bc0e02.png", + "name": "Metal Mario - Golf", + "release": { + "au": "2017-03-11", + "eu": "2017-03-10", + "jp": "2017-03-30", + "na": "2017-03-24" + }, + "tail": "02bc0e02", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Piper", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "02320001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_02320001-02ea0502.png", + "name": "Piper", + "release": { + "au": "2016-11-10", + "eu": "2016-11-11", + "jp": "2016-11-03", + "na": "2016-12-02" + }, + "tail": "02ea0502", + "type": "Card" + }, + { + "amiiboSeries": "Kirby", + "character": "Meta Knight", + "gameSeries": "Kirby", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Receive star coins and a boost item", + "write": false + } + ], + "gameID": [ + "01004D300C5AE000" + ], + "gameName": "Kirby and the Forgotten Land" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive two Picture Pieces, a Maxim Tomato, and two Point Stars", + "write": false + } + ], + "gameID": [ + "01007E3006DDA000" + ], + "gameName": "Kirby Star Allies" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive more useful items", + "write": false + } + ], + "gameID": [ + "01006B601380E000" + ], + "gameName": "Kirby's Return to Dream Land Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a Kirby-themed racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive 20 Fragments", + "write": false + } + ], + "gameID": [ + "01003FB00C5A8000" + ], + "gameName": "Super Kirby Clash" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "1f010000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_1f010000-02550c02.png", + "name": "Meta Knight", + "release": { + "au": "2016-06-11", + "eu": "2016-06-10", + "jp": "2016-04-28", + "na": "2016-06-10" + }, + "tail": "02550c02", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Ribbot", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "03390001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_03390001-01b10502.png", + "name": "Ribbot", + "release": { + "au": "2016-06-18", + "eu": null, + "jp": "2016-03-24", + "na": "2016-06-10" + }, + "tail": "01b10502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Hornsby", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "04b60001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_04b60001-02ec0502.png", + "name": "Hornsby", + "release": { + "au": "2016-11-10", + "eu": "2016-11-11", + "jp": "2016-11-03", + "na": "2016-12-02" + }, + "tail": "02ec0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Timmy", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "01850401", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01850401-01790502.png", + "name": "Timmy - Suit", + "release": { + "au": "2016-06-18", + "eu": null, + "jp": "2016-03-24", + "na": "2016-06-10" + }, + "tail": "01790502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Wilbur", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "0a010001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_0a010001-03ac0502.png", + "name": "Wilbur", + "release": { + "au": "2021-11-05", + "eu": "2021-11-05", + "jp": "2021-11-05", + "na": "2021-11-05" + }, + "tail": "03ac0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Ellie", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "032a0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_032a0001-03070502.png", + "name": "Ellie", + "release": { + "au": "2016-11-10", + "eu": "2016-11-11", + "jp": "2016-11-03", + "na": "2016-12-02" + }, + "tail": "03070502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Bluebear", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "027d0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_027d0001-00630502.png", + "name": "Bluebear", + "release": { + "au": "2015-10-03", + "eu": "2015-10-02", + "jp": "2015-07-30", + "na": "2015-09-25" + }, + "tail": "00630502", + "type": "Card" + }, + { + "amiiboSeries": "Super Smash Bros.", + "character": "Bowser Jr.", + "gameSeries": "Super Mario", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock the Chain Chomp weapon", + "write": false + } + ], + "gameID": [ + "01007960049A0000" + ], + "gameName": "Bayonetta 2" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a particular power-up depending on the character", + "write": false + }, + { + "Usage": "Unleash a powerful shockwave to knock out nearby enemies and blocks (in Bowser's Fury mode)", + "write": false + } + ], + "gameID": [ + "010028600EBDA000" + ], + "gameName": "Super Mario 3D World + Bowser's Fury" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock the character's shiny sticker", + "write": false + } + ], + "gameID": [ + "010036B0034E4000" + ], + "gameName": "Super Mario Party" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "00060000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_00060000-00150002.png", + "name": "Bowser Jr.", + "release": { + "au": "2015-07-23", + "eu": "2015-07-17", + "jp": "2015-07-17", + "na": "2015-09-11" + }, + "tail": "00150002", + "type": "Figure" + }, + { + "amiiboSeries": "Super Smash Bros.", + "character": "Shulk", + "gameSeries": "Xenoblade Chronicles", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-specific weapon skin for characters using the Swordfighter Class", + "write": false + } + ], + "gameID": [ + "010074F013262000" + ], + "gameName": "Xenoblade Chronicles 3" + } + ], + "head": "22400000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_22400000-002b0002.png", + "name": "Shulk", + "release": { + "au": "2015-01-29", + "eu": "2015-02-20", + "jp": "2015-01-22", + "na": "2015-02-01" + }, + "tail": "002b0002", + "type": "Figure" + }, + { + "amiiboSeries": "Super Mario Bros.", + "character": "Daisy", + "gameSeries": "Super Mario", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock a costume based on the character (short-hair version)", + "write": false + } + ], + "gameID": [ + "01007960049A0000" + ], + "gameName": "Bayonetta 2" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01003DA010E8A000" + ], + "gameName": "Miitopia" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a particular power-up depending on the character", + "write": false + } + ], + "gameID": [ + "010028600EBDA000" + ], + "gameName": "Super Mario 3D World + Bowser's Fury" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock the character's shiny sticker", + "write": false + } + ], + "gameID": [ + "010036B0034E4000" + ], + "gameName": "Super Mario Party" + } + ], + "head": "00130000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_00130000-02660102.png", + "name": "Daisy", + "release": { + "au": "2016-11-05", + "eu": "2016-11-04", + "jp": "2016-10-20", + "na": "2016-11-04" + }, + "tail": "02660102", + "type": "Figure" + }, + { + "amiiboSeries": "Mario Sports Superstars", + "character": "Bowser", + "gameSeries": "Mario Sports Superstars", + "gamesSwitch": [], + "head": "09c90101", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_09c90101-02960e02.png", + "name": "Bowser - Soccer", + "release": { + "au": "2017-03-11", + "eu": "2017-03-10", + "jp": "2017-03-30", + "na": "2017-03-24" + }, + "tail": "02960e02", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Sally", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "04e40001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_04e40001-01b60502.png", + "name": "Sally", + "release": { + "au": "2016-06-18", + "eu": null, + "jp": "2016-03-24", + "na": "2016-06-10" + }, + "tail": "01b60502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Sasha", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "0a110001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_0a110001-03c80502.png", + "name": "Sasha", + "release": { + "au": "2021-11-05", + "eu": "2021-11-05", + "jp": "2021-11-05", + "na": "2021-11-05" + }, + "tail": "03c80502", + "type": "Card" + }, + { + "amiiboSeries": "Super Smash Bros.", + "character": "Robin", + "gameSeries": "Fire Emblem", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Receive a Fashion Ticket and a Music Ticket, for unlocking any of the available costumes and music tracks", + "write": false + } + ], + "gameID": [ + "0100A6301214E000" + ], + "gameName": "Fire Emblem Engage" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a weapon", + "write": false + } + ], + "gameID": [ + "0100F15003E64000" + ], + "gameName": "Fire Emblem Warriors" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive better-quality randomized resources, weapons, or equipment", + "write": false + } + ], + "gameID": [ + "010071F0143EA000" + ], + "gameName": "Fire Emblem Warriors: Three Hopes" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special piece of battle music / Receive higher-quality items and materials", + "write": false + } + ], + "gameID": [ + "010055D009F78000" + ], + "gameName": "Fire Emblem: Three Houses" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "21030000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_21030000-002a0002.png", + "name": "Robin", + "release": { + "au": "2015-04-25", + "eu": "2015-04-24", + "jp": "2015-04-29", + "na": "2015-05-29" + }, + "tail": "002a0002", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Patty", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "02b10001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_02b10001-00690502.png", + "name": "Patty", + "release": { + "au": "2015-10-03", + "eu": "2015-10-02", + "jp": "2015-07-30", + "na": "2015-09-25" + }, + "tail": "00690502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Gloria", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "03160001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_03160001-01c00502.png", + "name": "Gloria", + "release": { + "au": "2016-06-18", + "eu": null, + "jp": "2016-03-24", + "na": "2016-06-10" + }, + "tail": "01c00502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "K.K. Slider", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "01820001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01820001-00a80502.png", + "name": "K.K. Slider", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-10-29", + "na": "2016-01-22" + }, + "tail": "00a80502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Simon", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "03fb0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_03fb0001-01cf0502.png", + "name": "Simon", + "release": { + "au": "2016-06-18", + "eu": null, + "jp": "2016-03-24", + "na": "2016-06-10" + }, + "tail": "01cf0502", + "type": "Card" + }, + { + "amiiboSeries": "Legend Of Zelda", + "character": "Link", + "gameSeries": "The Legend of Zelda", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock a costume based on the character (short-hair version)", + "write": false + } + ], + "gameID": [ + "01007960049A0000" + ], + "gameName": "Bayonetta 2" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive specific crafting materials or a weapon for the character", + "write": false + } + ], + "gameID": [ + "01002B00111A2000" + ], + "gameName": "Hyrule Warriors: Age of Calamity" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a weapon rated 3 stars or higher", + "write": false + } + ], + "gameID": [ + "0100AE00096EA000" + ], + "gameName": "Hyrule Warriors: Definitive Edition" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a Legend of Zelda-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01003DA010E8A000" + ], + "gameName": "Miitopia" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a chest of loot, potentially containing gear inspired by the Legend of Zelda series", + "write": false + } + ], + "gameID": [ + "01000A10041EA000" + ], + "gameName": "The Elder Scrolls V: Skyrim" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive materials and a rare or exclusive item", + "write": false + } + ], + "gameID": [ + "01007EF00011E000" + ], + "gameName": "The Legend of Zelda: Breath of the Wild" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock one of five amiibo-exclusive Chamber Dungeon chambers", + "write": false + }, + { + "Usage": "Save your Chamber Dungeon to the amiibo to share with friends", + "write": true + } + ], + "gameID": [ + "01006BB00C6F0000" + ], + "gameName": "The Legend of Zelda: Link's Awakening" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an amiibo-exclusive character-specific paraglider fabric", + "write": false + }, + { + "Usage": "Receive materials and a weapon or rare item", + "write": false + } + ], + "gameID": [ + "0100F2C0115B6000" + ], + "gameName": "The Legend of Zelda: Tears of the Kingdom" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive the Red Tunic", + "write": false + }, + { + "Usage": "Receive random materials", + "write": false + } + ], + "gameID": [ + "01008CF01BAAC000" + ], + "gameName": "The Legend of Zelda: Echoes of Wisdom" + } + ], + "head": "01000000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01000000-04180902.png", + "name": "Link - Tears of the Kingdom", + "release": { + "au": "2023-05-12", + "eu": "2023-05-12", + "jp": "2023-05-12", + "na": "2023-05-12" + }, + "tail": "04180902", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Papi", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "03b00001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_03b00001-01a90502.png", + "name": "Papi", + "release": { + "au": "2016-06-18", + "eu": null, + "jp": "2016-03-24", + "na": "2016-06-10" + }, + "tail": "01a90502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Dizzy", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "03240001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_03240001-01890502.png", + "name": "Dizzy", + "release": { + "au": "2016-06-18", + "eu": null, + "jp": "2016-03-24", + "na": "2016-06-10" + }, + "tail": "01890502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Chow", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "02170001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_02170001-01b30502.png", + "name": "Chow", + "release": { + "au": "2016-06-18", + "eu": null, + "jp": "2016-03-24", + "na": "2016-06-10" + }, + "tail": "01b30502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Tipper", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "02b20001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_02b20001-00c40502.png", + "name": "Tipper", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-10-29", + "na": "2016-01-22" + }, + "tail": "00c40502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Leonardo", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "04fe0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_04fe0001-00590502.png", + "name": "Leonardo", + "release": { + "au": "2015-10-03", + "eu": "2015-10-02", + "jp": "2015-07-30", + "na": "2015-09-25" + }, + "tail": "00590502", + "type": "Card" + }, + { + "amiiboSeries": "Super Smash Bros.", + "character": "Link", + "gameSeries": "The Legend of Zelda", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock a costume based on the character (short-hair version)", + "write": false + } + ], + "gameID": [ + "01007960049A0000" + ], + "gameName": "Bayonetta 2" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive specific crafting materials or a weapon for the character", + "write": false + } + ], + "gameID": [ + "01002B00111A2000" + ], + "gameName": "Hyrule Warriors: Age of Calamity" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a weapon rated 3 stars or higher", + "write": false + } + ], + "gameID": [ + "0100AE00096EA000" + ], + "gameName": "Hyrule Warriors: Definitive Edition" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a Legend of Zelda-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01003DA010E8A000" + ], + "gameName": "Miitopia" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a chest of loot, potentially containing gear inspired by the Legend of Zelda series", + "write": false + } + ], + "gameID": [ + "01000A10041EA000" + ], + "gameName": "The Elder Scrolls V: Skyrim" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive materials and a rare or exclusive item", + "write": false + } + ], + "gameID": [ + "01007EF00011E000" + ], + "gameName": "The Legend of Zelda: Breath of the Wild" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock one of five amiibo-exclusive Chamber Dungeon chambers", + "write": false + }, + { + "Usage": "Save your Chamber Dungeon to the amiibo to share with friends", + "write": true + } + ], + "gameID": [ + "01006BB00C6F0000" + ], + "gameName": "The Legend of Zelda: Link's Awakening" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an amiibo-exclusive character-specific paraglider fabric", + "write": false + }, + { + "Usage": "Receive materials and a weapon or rare item", + "write": false + } + ], + "gameID": [ + "0100F2C0115B6000" + ], + "gameName": "The Legend of Zelda: Tears of the Kingdom" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive the Red Tunic", + "write": false + }, + { + "Usage": "Receive random materials", + "write": false + } + ], + "gameID": [ + "01008CF01BAAC000" + ], + "gameName": "The Legend of Zelda: Echoes of Wisdom" + } + ], + "head": "01000100", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01000100-00160002.png", + "name": "Toon Link", + "release": { + "au": "2015-01-29", + "eu": "2015-01-23", + "jp": "2015-01-22", + "na": "2015-02-01" + }, + "tail": "00160002", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Pango", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "02020001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_02020001-01030502.png", + "name": "Pango", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-10-29", + "na": "2016-01-22" + }, + "tail": "01030502", + "type": "Card" + }, + { + "amiiboSeries": "Super Smash Bros.", + "character": "R.O.B.", + "gameSeries": "Classic Nintendo", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "07810000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_07810000-00330002.png", + "name": "R.O.B. - NES", + "release": { + "au": "2015-09-26", + "eu": "2015-09-25", + "jp": null, + "na": "2015-09-25" + }, + "tail": "00330002", + "type": "Figure" + }, + { + "amiiboSeries": "Mario Sports Superstars", + "character": "Pink Gold Peach", + "gameSeries": "Mario Sports Superstars", + "gamesSwitch": [], + "head": "09d10201", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_09d10201-02bf0e02.png", + "name": "Pink Gold Peach - Baseball", + "release": { + "au": "2017-03-11", + "eu": "2017-03-10", + "jp": "2017-03-30", + "na": "2017-03-24" + }, + "tail": "02bf0e02", + "type": "Card" + }, + { + "amiiboSeries": "Super Smash Bros.", + "character": "PAC-MAN", + "gameSeries": "Pac-man", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock the Pakku Mask", + "write": false + } + ], + "gameID": [ + "01002FC00412C000" + ], + "gameName": "Little Nightmares: Complete Edition" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "33400000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_33400000-00320002.png", + "name": "Pac-Man", + "release": { + "au": "2015-04-25", + "eu": "2015-04-24", + "jp": "2015-04-29", + "na": "2015-05-29" + }, + "tail": "00320002", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Marlo", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "0a150001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_0a150001-03cc0502.png", + "name": "Marlo", + "release": { + "au": "2021-11-05", + "eu": "2021-11-05", + "jp": "2021-11-05", + "na": "2021-11-05" + }, + "tail": "03cc0502", + "type": "Card" + }, + { + "amiiboSeries": "Mario Sports Superstars", + "character": "Luigi", + "gameSeries": "Mario Sports Superstars", + "gamesSwitch": [], + "head": "09c10301", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_09c10301-02700e02.png", + "name": "Luigi - Tennis", + "release": { + "au": "2017-03-11", + "eu": "2017-03-10", + "jp": "2017-03-30", + "na": "2017-03-24" + }, + "tail": "02700e02", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Lottie", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "01c10001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01c10001-00540502.png", + "name": "Lottie", + "release": { + "au": "2015-10-03", + "eu": "2015-10-02", + "jp": "2015-07-30", + "na": "2015-09-25" + }, + "tail": "00540502", + "type": "Card" + }, + { + "amiiboSeries": "Mario Sports Superstars", + "character": "Peach", + "gameSeries": "Mario Sports Superstars", + "gamesSwitch": [], + "head": "09c20301", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_09c20301-02750e02.png", + "name": "Peach - Tennis", + "release": { + "au": "2017-03-11", + "eu": "2017-03-10", + "jp": "2017-03-30", + "na": "2017-03-24" + }, + "tail": "02750e02", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Poncho", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "027f0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_027f0001-00b90502.png", + "name": "Poncho", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-10-29", + "na": "2016-01-22" + }, + "tail": "00b90502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Mott", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "03ec0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_03ec0001-01830502.png", + "name": "Mott", + "release": { + "au": "2016-06-18", + "eu": null, + "jp": "2016-03-24", + "na": "2016-06-10" + }, + "tail": "01830502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Zucker", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "042b0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_042b0001-01af0502.png", + "name": "Zucker", + "release": { + "au": "2016-06-18", + "eu": null, + "jp": "2016-03-24", + "na": "2016-06-10" + }, + "tail": "01af0502", + "type": "Card" + }, + { + "amiiboSeries": "Super Nintendo World", + "character": "Daisy", + "gameSeries": "Super Mario", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock a costume based on the character (short-hair version)", + "write": false + } + ], + "gameID": [ + "01007960049A0000" + ], + "gameName": "Bayonetta 2" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01003DA010E8A000" + ], + "gameName": "Miitopia" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a particular power-up depending on the character", + "write": false + } + ], + "gameID": [ + "010028600EBDA000" + ], + "gameName": "Super Mario 3D World + Bowser's Fury" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock the character's shiny sticker", + "write": false + } + ], + "gameID": [ + "010036B0034E4000" + ], + "gameName": "Super Mario Party" + } + ], + "head": "00130003", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_00130003-039eff02.png", + "name": "Daisy - Power Up Band", + "release": { + "au": null, + "eu": null, + "jp": "2021-02-04", + "na": null + }, + "tail": "039eff02", + "type": "Band" + }, + { + "amiiboSeries": "Super Smash Bros.", + "character": "Mega Man", + "gameSeries": "Megaman", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive E Tanks and other useful in-game items", + "write": false + } + ], + "gameID": [ + "0100B0C0086B0000" + ], + "gameName": "Mega Man 11" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock 11 exclusive challenge stages designed by fans", + "write": false + } + ], + "gameID": [ + "01002D4007AE0000" + ], + "gameName": "Mega Man Legacy Collection" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock new platforming challenges", + "write": false + } + ], + "gameID": [ + "0100842008EC4000" + ], + "gameName": "Mega Man Legacy Collection 2" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a lot of BP / Receive better supplies (compared to other amiibo)", + "write": false + } + ], + "gameID": [ + "0100643002136000" + ], + "gameName": "Resident Evil Revelations" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a lot of BP / Receive better supplies (compared to other amiibo)", + "write": false + } + ], + "gameID": [ + "010095300212A000" + ], + "gameName": "Resident Evil Revelations 2" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "34800000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_34800000-00310002.png", + "name": "Mega Man", + "release": { + "au": "2015-01-29", + "eu": "2015-02-20", + "jp": "2015-01-22", + "na": "2015-02-01" + }, + "tail": "00310002", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Gruff", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "035a0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_035a0001-00850502.png", + "name": "Gruff", + "release": { + "au": "2015-10-03", + "eu": "2015-10-02", + "jp": "2015-07-30", + "na": "2015-09-25" + }, + "tail": "00850502", + "type": "Card" + }, + { + "amiiboSeries": "Metroid", + "character": "Metroid", + "gameSeries": "Metroid", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a Metroid-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Replenish a random amount of missiles once per day", + "write": false + } + ], + "gameID": [ + "010093801237C000" + ], + "gameName": "Metroid Dread" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a Spirit of the character", + "write": false + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "05c10000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_05c10000-03661302.png", + "name": "Metroid", + "release": { + "au": "2017-09-16", + "eu": "2017-09-15", + "jp": "2017-09-15", + "na": "2017-09-15" + }, + "tail": "03661302", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Pompom", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "030c0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_030c0001-01b80502.png", + "name": "Pompom", + "release": { + "au": "2016-06-18", + "eu": null, + "jp": "2016-03-24", + "na": "2016-06-10" + }, + "tail": "01b80502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Celeste", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "01930001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01930001-03ae0502.png", + "name": "Celeste", + "release": { + "au": "2021-11-05", + "eu": "2021-11-05", + "jp": "2021-11-05", + "na": "2021-11-05" + }, + "tail": "03ae0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Cashmere", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "04c90001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_04c90001-030d0502.png", + "name": "Cashmere", + "release": { + "au": "2016-11-10", + "eu": "2016-11-11", + "jp": "2016-11-03", + "na": "2016-12-02" + }, + "tail": "030d0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Diva", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "034a0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_034a0001-01430502.png", + "name": "Diva", + "release": { + "au": "2016-03-19", + "eu": "2016-03-18", + "jp": "2016-01-14", + "na": "2016-03-18" + }, + "tail": "01430502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Truffles", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "04790001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_04790001-00920502.png", + "name": "Truffles", + "release": { + "au": "2015-10-03", + "eu": "2015-10-02", + "jp": "2015-07-30", + "na": "2015-09-25" + }, + "tail": "00920502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Marina", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "042a0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_042a0001-012d0502.png", + "name": "Marina", + "release": { + "au": "2016-03-19", + "eu": "2016-03-18", + "jp": "2016-01-14", + "na": "2016-03-18" + }, + "tail": "012d0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Nana", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "03fa0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_03fa0001-00d00502.png", + "name": "Nana", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-10-29", + "na": "2016-01-22" + }, + "tail": "00d00502", + "type": "Card" + }, + { + "amiiboSeries": "Mario Sports Superstars", + "character": "Bowser Jr.", + "gameSeries": "Mario Sports Superstars", + "gamesSwitch": [], + "head": "09ca0301", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_09ca0301-029d0e02.png", + "name": "Bowser Jr. - Tennis", + "release": { + "au": "2017-03-11", + "eu": "2017-03-10", + "jp": "2017-03-30", + "na": "2017-03-24" + }, + "tail": "029d0e02", + "type": "Card" + }, + { + "amiiboSeries": "Splatoon", + "character": "Inkling", + "gameSeries": "Splatoon", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a Splatoon-themed racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01003DA010E8A000" + ], + "gameName": "Miitopia" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock exclusive gear / Save favorite weapons, gear, and control settings / Take photos with the character or saved gear", + "write": true + } + ], + "gameID": [ + "01003BC0000A0000" + ], + "gameName": "Splatoon 2" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock exclusive gear / Save favorite weapons, gear, and control settings / Take photos with the character or saved gear", + "write": true + } + ], + "gameID": [ + "0100C2500FC20000" + ], + "gameName": "Splatoon 3" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "08000200", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_08000200-02600402.png", + "name": "Inkling Boy - Purple", + "release": { + "au": "2016-07-09", + "eu": "2016-07-08", + "jp": "2016-07-07", + "na": "2016-07-08" + }, + "tail": "02600402", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Eunice", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "04c70001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_04c70001-00940502.png", + "name": "Eunice", + "release": { + "au": "2015-10-03", + "eu": "2015-10-02", + "jp": "2015-07-30", + "na": "2015-09-25" + }, + "tail": "00940502", + "type": "Card" + }, + { + "amiiboSeries": "Mario Sports Superstars", + "character": "Bowser", + "gameSeries": "Mario Sports Superstars", + "gamesSwitch": [], + "head": "09c90501", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_09c90501-029a0e02.png", + "name": "Bowser - Horse Racing", + "release": { + "au": "2017-03-11", + "eu": "2017-03-10", + "jp": "2017-03-30", + "na": "2017-03-24" + }, + "tail": "029a0e02", + "type": "Card" + }, + { + "amiiboSeries": "Super Smash Bros.", + "character": "Donkey Kong", + "gameSeries": "Super Mario", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01003DA010E8A000" + ], + "gameName": "Miitopia" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a particular power-up depending on the character", + "write": false + } + ], + "gameID": [ + "010028600EBDA000" + ], + "gameName": "Super Mario 3D World + Bowser's Fury" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock the character's shiny sticker", + "write": false + } + ], + "gameID": [ + "010036B0034E4000" + ], + "gameName": "Super Mario Party" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "00080000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_00080000-00030002.png", + "name": "Donkey Kong", + "release": { + "au": "2014-11-29", + "eu": "2014-11-28", + "jp": "2014-12-06", + "na": "2014-11-21" + }, + "tail": "00030002", + "type": "Figure" + }, + { + "amiiboSeries": "Super Nintendo World", + "character": "Toad", + "gameSeries": "Super Mario", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Receive an invincibility mushroom", + "write": false + } + ], + "gameID": [ + "01009BF0072D4000" + ], + "gameName": "Captain Toad: Treasure Tracker" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01003DA010E8A000" + ], + "gameName": "Miitopia" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a particular power-up depending on the character", + "write": false + } + ], + "gameID": [ + "010028600EBDA000" + ], + "gameName": "Super Mario 3D World + Bowser's Fury" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a Spirit of the character", + "write": false + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01006000040C2000" + ], + "gameName": "Yoshi's Crafted World" + } + ], + "head": "000a0003", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_000a0003-03a0ff02.png", + "name": "Toad - Power Up Band", + "release": { + "au": null, + "eu": null, + "jp": "2021-02-04", + "na": null + }, + "tail": "03a0ff02", + "type": "Band" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Rory", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "03ed0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_03ed0001-01a30502.png", + "name": "Rory", + "release": { + "au": "2016-06-18", + "eu": null, + "jp": "2016-03-24", + "na": "2016-06-10" + }, + "tail": "01a30502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Egbert", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "029b0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_029b0001-00cb0502.png", + "name": "Egbert", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-10-29", + "na": "2016-01-22" + }, + "tail": "00cb0502", + "type": "Card" + }, + { + "amiiboSeries": "Super Smash Bros.", + "character": "Mario", + "gameSeries": "Super Mario", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock an amiibo-exclusive weapon for the character and their Rabbid counterpart", + "write": false + } + ], + "gameID": [ + "010067300059A000" + ], + "gameName": "Mario + Rabbids: Kingdom Battle" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01003DA010E8A000" + ], + "gameName": "Miitopia" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a particular power-up depending on the character", + "write": false + } + ], + "gameID": [ + "010028600EBDA000" + ], + "gameName": "Super Mario 3D World + Bowser's Fury" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a character-based costume", + "write": false + }, + { + "Usage": "Gain temporary invincibility", + "write": false + } + ], + "gameID": [ + "0100000000010000" + ], + "gameName": "Super Mario Odyssey" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock the character's shiny sticker", + "write": false + } + ], + "gameID": [ + "010036B0034E4000" + ], + "gameName": "Super Mario Party" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01006000040C2000" + ], + "gameName": "Yoshi's Crafted World" + } + ], + "head": "00000000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_00000000-00000002.png", + "name": "Mario", + "release": { + "au": "2014-11-29", + "eu": "2014-11-28", + "jp": "2014-12-06", + "na": "2014-11-21" + }, + "tail": "00000002", + "type": "Figure" + }, + { + "amiiboSeries": "Mario Sports Superstars", + "character": "Rosalina", + "gameSeries": "Mario Sports Superstars", + "gamesSwitch": [], + "head": "09cf0101", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_09cf0101-02b40e02.png", + "name": "Rosalina - Soccer", + "release": { + "au": "2017-03-11", + "eu": "2017-03-10", + "jp": "2017-03-30", + "na": "2017-03-24" + }, + "tail": "02b40e02", + "type": "Card" + }, + { + "amiiboSeries": "Legend Of Zelda", + "character": "Link", + "gameSeries": "The Legend of Zelda", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock a costume based on the character (short-hair version)", + "write": false + } + ], + "gameID": [ + "01007960049A0000" + ], + "gameName": "Bayonetta 2" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive specific crafting materials or a weapon for the character", + "write": false + } + ], + "gameID": [ + "01002B00111A2000" + ], + "gameName": "Hyrule Warriors: Age of Calamity" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a weapon rated 3 stars or higher", + "write": false + } + ], + "gameID": [ + "0100AE00096EA000" + ], + "gameName": "Hyrule Warriors: Definitive Edition" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a Legend of Zelda-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01003DA010E8A000" + ], + "gameName": "Miitopia" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a chest of loot, potentially containing gear inspired by the Legend of Zelda series", + "write": false + } + ], + "gameID": [ + "01000A10041EA000" + ], + "gameName": "The Elder Scrolls V: Skyrim" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive materials and a rare or exclusive item", + "write": false + } + ], + "gameID": [ + "01007EF00011E000" + ], + "gameName": "The Legend of Zelda: Breath of the Wild" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock one of five amiibo-exclusive Chamber Dungeon chambers", + "write": false + }, + { + "Usage": "Save your Chamber Dungeon to the amiibo to share with friends", + "write": true + } + ], + "gameID": [ + "01006BB00C6F0000" + ], + "gameName": "The Legend of Zelda: Link's Awakening" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an amiibo-exclusive character-specific paraglider fabric", + "write": false + }, + { + "Usage": "Receive materials and a weapon or rare item", + "write": false + } + ], + "gameID": [ + "0100F2C0115B6000" + ], + "gameName": "The Legend of Zelda: Tears of the Kingdom" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive the Red Tunic", + "write": false + }, + { + "Usage": "Receive random materials", + "write": false + } + ], + "gameID": [ + "01008CF01BAAC000" + ], + "gameName": "The Legend of Zelda: Echoes of Wisdom" + } + ], + "head": "01000000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01000000-034e0902.png", + "name": "Link - Skyward Sword", + "release": { + "au": "2017-06-24", + "eu": "2017-06-23", + "jp": "2017-06-23", + "na": "2017-06-23" + }, + "tail": "034e0902", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Cheri", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "02870001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_02870001-005a0502.png", + "name": "Cheri", + "release": { + "au": "2015-10-03", + "eu": "2015-10-02", + "jp": "2015-07-30", + "na": "2015-09-25" + }, + "tail": "005a0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Tom Nook", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "01830201", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01830201-03a80502.png", + "name": "Tom Nook", + "release": { + "au": "2021-11-05", + "eu": "2021-11-05", + "jp": "2021-11-05", + "na": "2021-11-05" + }, + "tail": "03a80502", + "type": "Card" + }, + { + "amiiboSeries": "Mario Sports Superstars", + "character": "Luigi", + "gameSeries": "Mario Sports Superstars", + "gamesSwitch": [], + "head": "09c10501", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_09c10501-02720e02.png", + "name": "Luigi - Horse Racing", + "release": { + "au": "2017-03-11", + "eu": "2017-03-10", + "jp": "2017-03-30", + "na": "2017-03-24" + }, + "tail": "02720e02", + "type": "Card" + }, + { + "amiiboSeries": "Mario Sports Superstars", + "character": "Yoshi", + "gameSeries": "Mario Sports Superstars", + "gamesSwitch": [], + "head": "09c40101", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_09c40101-027d0e02.png", + "name": "Yoshi - Soccer", + "release": { + "au": "2017-03-11", + "eu": "2017-03-10", + "jp": "2017-03-30", + "na": "2017-03-24" + }, + "tail": "027d0e02", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Mathilda", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "03d20001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_03d20001-00e50502.png", + "name": "Mathilda", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-10-29", + "na": "2016-01-22" + }, + "tail": "00e50502", + "type": "Card" + }, + { + "amiiboSeries": "Mario Sports Superstars", + "character": "Mario", + "gameSeries": "Mario Sports Superstars", + "gamesSwitch": [], + "head": "09c00301", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_09c00301-026b0e02.png", + "name": "Mario - Tennis", + "release": { + "au": "2017-03-11", + "eu": "2017-03-10", + "jp": "2017-03-30", + "na": "2017-03-24" + }, + "tail": "026b0e02", + "type": "Card" + }, + { + "amiiboSeries": "Splatoon", + "character": "Inkling", + "gameSeries": "Splatoon", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a Splatoon-themed racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01003DA010E8A000" + ], + "gameName": "Miitopia" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock exclusive gear / Save favorite weapons, gear, and control settings / Take photos with the character or saved gear", + "write": true + } + ], + "gameID": [ + "01003BC0000A0000" + ], + "gameName": "Splatoon 2" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock exclusive gear / Save favorite weapons, gear, and control settings / Take photos with the character or saved gear", + "write": true + } + ], + "gameID": [ + "0100C2500FC20000" + ], + "gameName": "Splatoon 3" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "08000300", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_08000300-036b0402.png", + "name": "Inkling Squid - Neon Purple", + "release": { + "au": "2017-07-21", + "eu": "2017-07-21", + "jp": "2017-07-21", + "na": "2017-07-21" + }, + "tail": "036b0402", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Pietro", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "04d20001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_04d20001-01a70502.png", + "name": "Pietro", + "release": { + "au": "2016-06-18", + "eu": null, + "jp": "2016-03-24", + "na": "2016-06-10" + }, + "tail": "01a70502", + "type": "Card" + }, + { + "amiiboSeries": "Mario Sports Superstars", + "character": "Baby Mario", + "gameSeries": "Mario Sports Superstars", + "gamesSwitch": [], + "head": "09cc0301", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_09cc0301-02a70e02.png", + "name": "Baby Mario - Tennis", + "release": { + "au": "2017-03-11", + "eu": "2017-03-10", + "jp": "2017-03-30", + "na": "2017-03-24" + }, + "tail": "02a70e02", + "type": "Card" + }, + { + "amiiboSeries": "Mario Sports Superstars", + "character": "Diddy Kong", + "gameSeries": "Mario Sports Superstars", + "gamesSwitch": [], + "head": "09c80201", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_09c80201-02920e02.png", + "name": "Diddy Kong - Baseball", + "release": { + "au": "2017-03-11", + "eu": "2017-03-10", + "jp": "2017-03-30", + "na": "2017-03-24" + }, + "tail": "02920e02", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Drake", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "03100001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_03100001-00f80502.png", + "name": "Drake", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-10-29", + "na": "2016-01-22" + }, + "tail": "00f80502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Dobie", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "050f0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_050f0001-03140502.png", + "name": "Dobie", + "release": { + "au": "2016-11-10", + "eu": "2016-11-11", + "jp": "2016-11-03", + "na": "2016-12-02" + }, + "tail": "03140502", + "type": "Card" + }, + { + "amiiboSeries": "Super Mario Bros.", + "character": "Peach", + "gameSeries": "Super Mario", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock a costume based on the character (short-hair version)", + "write": false + } + ], + "gameID": [ + "01007960049A0000" + ], + "gameName": "Bayonetta 2" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an amiibo-exclusive weapon for the character and their Rabbid counterpart", + "write": false + } + ], + "gameID": [ + "010067300059A000" + ], + "gameName": "Mario + Rabbids: Kingdom Battle" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01003DA010E8A000" + ], + "gameName": "Miitopia" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a particular power-up depending on the character", + "write": false + } + ], + "gameID": [ + "010028600EBDA000" + ], + "gameName": "Super Mario 3D World + Bowser's Fury" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a life-up heart", + "write": false + } + ], + "gameID": [ + "0100000000010000" + ], + "gameName": "Super Mario Odyssey" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock the character's shiny sticker", + "write": false + } + ], + "gameID": [ + "010036B0034E4000" + ], + "gameName": "Super Mario Party" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01006000040C2000" + ], + "gameName": "Yoshi's Crafted World" + } + ], + "head": "00020000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_00020000-00360102.png", + "name": "Peach", + "release": { + "au": "2015-03-21", + "eu": "2015-03-20", + "jp": "2015-03-12", + "na": "2015-03-20" + }, + "tail": "00360102", + "type": "Figure" + }, + { + "amiiboSeries": "Mario Sports Superstars", + "character": "Donkey Kong", + "gameSeries": "Mario Sports Superstars", + "gamesSwitch": [], + "head": "09c70201", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_09c70201-028d0e02.png", + "name": "Donkey Kong - Baseball", + "release": { + "au": "2017-03-11", + "eu": "2017-03-10", + "jp": "2017-03-30", + "na": "2017-03-24" + }, + "tail": "028d0e02", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Zoe", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "0a1a0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_0a1a0001-03d10502.png", + "name": "Zoe", + "release": { + "au": "2021-11-05", + "eu": "2021-11-05", + "jp": "2021-11-05", + "na": "2021-11-05" + }, + "tail": "03d10502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Tom Nook", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "01830001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01830001-00450502.png", + "name": "Tom Nook", + "release": { + "au": "2015-10-03", + "eu": "2015-10-02", + "jp": "2015-07-30", + "na": "2015-09-25" + }, + "tail": "00450502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Nan", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "03570001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_03570001-00eb0502.png", + "name": "Nan", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-10-29", + "na": "2016-01-22" + }, + "tail": "00eb0502", + "type": "Card" + }, + { + "amiiboSeries": "Mario Sports Superstars", + "character": "Yoshi", + "gameSeries": "Mario Sports Superstars", + "gamesSwitch": [], + "head": "09c40201", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_09c40201-027e0e02.png", + "name": "Yoshi - Baseball", + "release": { + "au": "2017-03-11", + "eu": "2017-03-10", + "jp": "2017-03-30", + "na": "2017-03-24" + }, + "tail": "027e0e02", + "type": "Card" + }, + { + "amiiboSeries": "Super Smash Bros.", + "character": "Peach", + "gameSeries": "Super Mario", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock a costume based on the character (short-hair version)", + "write": false + } + ], + "gameID": [ + "01007960049A0000" + ], + "gameName": "Bayonetta 2" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an amiibo-exclusive weapon for the character and their Rabbid counterpart", + "write": false + } + ], + "gameID": [ + "010067300059A000" + ], + "gameName": "Mario + Rabbids: Kingdom Battle" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01003DA010E8A000" + ], + "gameName": "Miitopia" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a particular power-up depending on the character", + "write": false + } + ], + "gameID": [ + "010028600EBDA000" + ], + "gameName": "Super Mario 3D World + Bowser's Fury" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a life-up heart", + "write": false + } + ], + "gameID": [ + "0100000000010000" + ], + "gameName": "Super Mario Odyssey" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock the character's shiny sticker", + "write": false + } + ], + "gameID": [ + "010036B0034E4000" + ], + "gameName": "Super Mario Party" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01006000040C2000" + ], + "gameName": "Yoshi's Crafted World" + } + ], + "head": "00020000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_00020000-00010002.png", + "name": "Peach", + "release": { + "au": "2014-11-29", + "eu": "2014-11-28", + "jp": "2014-12-06", + "na": "2014-11-21" + }, + "tail": "00010002", + "type": "Figure" + }, + { + "amiiboSeries": "Super Mario Bros.", + "character": "Waluigi", + "gameSeries": "Super Mario", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a particular power-up depending on the character", + "write": false + } + ], + "gameID": [ + "010028600EBDA000" + ], + "gameName": "Super Mario 3D World + Bowser's Fury" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a character-based costume", + "write": false + } + ], + "gameID": [ + "0100000000010000" + ], + "gameName": "Super Mario Odyssey" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock the character's shiny sticker", + "write": false + } + ], + "gameID": [ + "010036B0034E4000" + ], + "gameName": "Super Mario Party" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a Spirit of the character", + "write": false + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "00140000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_00140000-02670102.png", + "name": "Waluigi", + "release": { + "au": "2016-11-05", + "eu": "2016-11-04", + "jp": "2016-10-20", + "na": "2016-11-04" + }, + "tail": "02670102", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "K.K. Slider", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "01820001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01820001-03b20502.png", + "name": "K.K. Slider", + "release": { + "au": "2021-11-05", + "eu": "2021-11-05", + "jp": "2021-11-05", + "na": "2021-11-05" + }, + "tail": "03b20502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Victoria", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "03a50001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_03a50001-015b0502.png", + "name": "Victoria", + "release": { + "au": "2016-03-19", + "eu": "2016-03-18", + "jp": "2016-01-14", + "na": "2016-03-18" + }, + "tail": "015b0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Kitty", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "026b0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_026b0001-00e90502.png", + "name": "Kitty", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-10-29", + "na": "2016-01-22" + }, + "tail": "00e90502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Rizzo", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "04150001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_04150001-01bb0502.png", + "name": "Rizzo", + "release": { + "au": "2016-06-18", + "eu": null, + "jp": "2016-03-24", + "na": "2016-06-10" + }, + "tail": "01bb0502", + "type": "Card" + }, + { + "amiiboSeries": "Mario Sports Superstars", + "character": "Diddy Kong", + "gameSeries": "Mario Sports Superstars", + "gamesSwitch": [], + "head": "09c80401", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_09c80401-02940e02.png", + "name": "Diddy Kong - Golf", + "release": { + "au": "2017-03-11", + "eu": "2017-03-10", + "jp": "2017-03-30", + "na": "2017-03-24" + }, + "tail": "02940e02", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Agent S", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "04e20001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_04e20001-01090502.png", + "name": "Agent S", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-10-29", + "na": "2016-01-22" + }, + "tail": "01090502", + "type": "Card" + }, + { + "amiiboSeries": "Super Smash Bros.", + "character": "Greninja", + "gameSeries": "Pokemon", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "1b920000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_1b920000-00250002.png", + "name": "Greninja", + "release": { + "au": "2015-05-30", + "eu": "2015-05-29", + "jp": "2015-04-29", + "na": "2015-05-29" + }, + "tail": "00250002", + "type": "Figure" + }, + { + "amiiboSeries": "Mario Sports Superstars", + "character": "Peach", + "gameSeries": "Mario Sports Superstars", + "gamesSwitch": [], + "head": "09c20201", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_09c20201-02740e02.png", + "name": "Peach - Baseball", + "release": { + "au": "2017-03-11", + "eu": "2017-03-10", + "jp": "2017-03-30", + "na": "2017-03-24" + }, + "tail": "02740e02", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Flora", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "043f0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_043f0001-01550502.png", + "name": "Flora", + "release": { + "au": "2016-03-19", + "eu": "2016-03-18", + "jp": "2016-01-14", + "na": "2016-03-18" + }, + "tail": "01550502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Cole", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "04a60001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_04a60001-00a30502.png", + "name": "Cole", + "release": { + "au": "2015-10-03", + "eu": "2015-10-02", + "jp": "2015-07-30", + "na": "2015-09-25" + }, + "tail": "00a30502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Octavian", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "04290001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_04290001-00700502.png", + "name": "Octavian", + "release": { + "au": "2015-10-03", + "eu": "2015-10-02", + "jp": "2015-07-30", + "na": "2015-09-25" + }, + "tail": "00700502", + "type": "Card" + }, + { + "amiiboSeries": "Super Mario Bros.", + "character": "Koopa Troopa", + "gameSeries": "Super Mario", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock the Chain Chomp weapon", + "write": false + } + ], + "gameID": [ + "01007960049A0000" + ], + "gameName": "Bayonetta 2" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a particular power-up depending on the character", + "write": false + } + ], + "gameID": [ + "010028600EBDA000" + ], + "gameName": "Super Mario 3D World + Bowser's Fury" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock the character's shiny sticker", + "write": false + } + ], + "gameID": [ + "010036B0034E4000" + ], + "gameName": "Super Mario Party" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a Spirit of the character", + "write": false + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01006000040C2000" + ], + "gameName": "Yoshi's Crafted World" + } + ], + "head": "00230000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_00230000-03680102.png", + "name": "Koopa Troopa", + "release": { + "au": "2017-10-07", + "eu": "2017-10-06", + "jp": "2017-10-05", + "na": "2017-10-06" + }, + "tail": "03680102", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Friga", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "04630001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_04630001-01310502.png", + "name": "Friga", + "release": { + "au": "2016-03-19", + "eu": "2016-03-18", + "jp": "2016-01-14", + "na": "2016-03-18" + }, + "tail": "01310502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Cousteau", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "03420001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_03420001-01280502.png", + "name": "Cousteau", + "release": { + "au": "2016-03-19", + "eu": "2016-03-18", + "jp": "2016-01-14", + "na": "2016-03-18" + }, + "tail": "01280502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Don Resetti", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "018f0101", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_018f0101-01190502.png", + "name": "Don Resetti - Without Hat", + "release": { + "au": "2016-03-19", + "eu": "2016-03-18", + "jp": "2016-01-14", + "na": "2016-03-18" + }, + "tail": "01190502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Tiansheng", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "0a130001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_0a130001-03ca0502.png", + "name": "Tiansheng", + "release": { + "au": "2021-11-05", + "eu": "2021-11-05", + "jp": "2021-11-05", + "na": "2021-11-05" + }, + "tail": "03ca0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Becky", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "02a20001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_02a20001-01ba0502.png", + "name": "Becky", + "release": { + "au": "2016-06-18", + "eu": null, + "jp": "2016-03-24", + "na": "2016-06-10" + }, + "tail": "01ba0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Klaus", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "02220001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_02220001-01440502.png", + "name": "Klaus", + "release": { + "au": "2016-03-19", + "eu": "2016-03-18", + "jp": "2016-01-14", + "na": "2016-03-18" + }, + "tail": "01440502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Chadder", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "041e0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_041e0001-015f0502.png", + "name": "Chadder", + "release": { + "au": "2016-03-19", + "eu": "2016-03-18", + "jp": "2016-01-14", + "na": "2016-03-18" + }, + "tail": "015f0502", + "type": "Card" + }, + { + "amiiboSeries": "Splatoon", + "character": "Inkling", + "gameSeries": "Splatoon", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a Splatoon-themed racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01003DA010E8A000" + ], + "gameName": "Miitopia" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock exclusive gear / Save favorite weapons, gear, and control settings / Take photos with the character or saved gear", + "write": true + } + ], + "gameID": [ + "01003BC0000A0000" + ], + "gameName": "Splatoon 2" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock exclusive gear / Save favorite weapons, gear, and control settings / Take photos with the character or saved gear", + "write": true + } + ], + "gameID": [ + "0100C2500FC20000" + ], + "gameName": "Splatoon 3" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "08000100", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_08000100-03690402.png", + "name": "Inkling Girl - Neon Pink", + "release": { + "au": "2017-07-21", + "eu": "2017-07-21", + "jp": "2017-07-21", + "na": "2017-07-21" + }, + "tail": "03690402", + "type": "Figure" + }, + { + "amiiboSeries": "Super Mario Bros.", + "character": "Goomba", + "gameSeries": "Super Mario", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock the Chain Chomp weapon", + "write": false + } + ], + "gameID": [ + "01007960049A0000" + ], + "gameName": "Bayonetta 2" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a particular power-up depending on the character", + "write": false + } + ], + "gameID": [ + "010028600EBDA000" + ], + "gameName": "Super Mario 3D World + Bowser's Fury" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock the character's shiny sticker", + "write": false + } + ], + "gameID": [ + "010036B0034E4000" + ], + "gameName": "Super Mario Party" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a Spirit of the character", + "write": false + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "00150000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_00150000-03670102.png", + "name": "Goomba", + "release": { + "au": "2017-10-07", + "eu": "2017-10-06", + "jp": "2017-10-05", + "na": "2017-10-06" + }, + "tail": "03670102", + "type": "Figure" + }, + { + "amiiboSeries": "Super Smash Bros.", + "character": "Bayonetta", + "gameSeries": "Bayonetta", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock the Super Mirror and Super Mirror 64 and all the costumes they contain", + "write": false + } + ], + "gameID": [ + "01007960049A0000" + ], + "gameName": "Bayonetta 2" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "32400100", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_32400100-03640002.png", + "name": "Bayonetta - Player 2", + "release": { + "au": "2017-07-22", + "eu": "2017-07-21", + "jp": "2017-07-21", + "na": "2017-07-21" + }, + "tail": "03640002", + "type": "Figure" + }, + { + "amiiboSeries": "Super Mario Bros.", + "character": "Mario", + "gameSeries": "Super Mario", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an amiibo-exclusive weapon for the character and their Rabbid counterpart", + "write": false + } + ], + "gameID": [ + "010067300059A000" + ], + "gameName": "Mario + Rabbids: Kingdom Battle" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01003DA010E8A000" + ], + "gameName": "Miitopia" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a particular power-up depending on the character", + "write": false + } + ], + "gameID": [ + "010028600EBDA000" + ], + "gameName": "Super Mario 3D World + Bowser's Fury" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a character-based costume", + "write": false + } + ], + "gameID": [ + "0100000000010000" + ], + "gameName": "Super Mario Odyssey" + }, + { + "amiiboUsage": [ + { + "Usage": "Gain temporary invincibility", + "write": false + } + ], + "gameID": [ + "0100000000010000" + ], + "gameName": "Super Mario Odyssey" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock the character's shiny sticker", + "write": false + } + ], + "gameID": [ + "010036B0034E4000" + ], + "gameName": "Super Mario Party" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01006000040C2000" + ], + "gameName": "Yoshi's Crafted World" + } + ], + "head": "00000000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_00000000-003c0102.png", + "name": "Mario - Gold Edition", + "release": { + "au": "2015-06-25", + "eu": null, + "jp": "2015-12-17", + "na": "2015-03-20" + }, + "tail": "003c0102", + "type": "Figure" + }, + { + "amiiboSeries": "Super Smash Bros.", + "character": "Samus", + "gameSeries": "Metroid", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock a costume based on the character (short-hair version)", + "write": false + } + ], + "gameID": [ + "01007960049A0000" + ], + "gameName": "Bayonetta 2" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a Metroid-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Restore a random amount of health once per day", + "write": false + } + ], + "gameID": [ + "010093801237C000" + ], + "gameName": "Metroid Dread" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01003DA010E8A000" + ], + "gameName": "Miitopia" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "05c00100", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_05c00100-001d0002.png", + "name": "Zero Suit Samus", + "release": { + "au": "2015-07-04", + "eu": "2015-06-26", + "jp": "2015-06-11", + "na": "2015-09-11" + }, + "tail": "001d0002", + "type": "Figure" + }, + { + "amiiboSeries": "Super Nintendo World", + "character": "Mario", + "gameSeries": "Super Mario", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock an amiibo-exclusive weapon for the character and their Rabbid counterpart", + "write": false + } + ], + "gameID": [ + "010067300059A000" + ], + "gameName": "Mario + Rabbids: Kingdom Battle" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01003DA010E8A000" + ], + "gameName": "Miitopia" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a particular power-up depending on the character", + "write": false + } + ], + "gameID": [ + "010028600EBDA000" + ], + "gameName": "Super Mario 3D World + Bowser's Fury" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a character-based costume", + "write": false + }, + { + "Usage": "Gain temporary invincibility", + "write": false + } + ], + "gameID": [ + "0100000000010000" + ], + "gameName": "Super Mario Odyssey" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock the character's shiny sticker", + "write": false + } + ], + "gameID": [ + "010036B0034E4000" + ], + "gameName": "Super Mario Party" + } + ], + "head": "00000003", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_00000003-039bff02.png", + "name": "Mario - Power Up Band", + "release": { + "au": null, + "eu": null, + "jp": "2021-02-04", + "na": null + }, + "tail": "039bff02", + "type": "Band" + }, + { + "amiiboSeries": "Super Smash Bros.", + "character": "Pit", + "gameSeries": "Kid Icarus", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "07400000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_07400000-00100002.png", + "name": "Pit", + "release": { + "au": "2014-12-12", + "eu": "2014-12-19", + "jp": "2014-12-06", + "na": "2014-12-14" + }, + "tail": "00100002", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Dotty", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "04950001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_04950001-01920502.png", + "name": "Dotty", + "release": { + "au": "2016-06-18", + "eu": null, + "jp": "2016-03-24", + "na": "2016-06-10" + }, + "tail": "01920502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Katie", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "01b60001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01b60001-00ae0502.png", + "name": "Katie", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-10-29", + "na": "2016-01-22" + }, + "tail": "00ae0502", + "type": "Card" + }, + { + "amiiboSeries": "Super Smash Bros.", + "character": "Zelda", + "gameSeries": "The Legend of Zelda", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Receive specific crafting materials or a weapon for the character", + "write": false + } + ], + "gameID": [ + "01002B00111A2000" + ], + "gameName": "Hyrule Warriors: Age of Calamity" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a weapon rated 3 stars or higher", + "write": false + } + ], + "gameID": [ + "0100AE00096EA000" + ], + "gameName": "Hyrule Warriors: Definitive Edition" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a Legend of Zelda-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01003DA010E8A000" + ], + "gameName": "Miitopia" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a chest of loot, potentially containing gear inspired by the Legend of Zelda series", + "write": false + } + ], + "gameID": [ + "01000A10041EA000" + ], + "gameName": "The Elder Scrolls V: Skyrim" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive materials and a rare or exclusive item", + "write": false + } + ], + "gameID": [ + "01007EF00011E000" + ], + "gameName": "The Legend of Zelda: Breath of the Wild" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock one of five amiibo-exclusive Chamber Dungeon chambers", + "write": false + }, + { + "Usage": "Save your Chamber Dungeon to the amiibo to share with friends", + "write": true + } + ], + "gameID": [ + "01006BB00C6F0000" + ], + "gameName": "The Legend of Zelda: Link's Awakening" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an amiibo-exclusive character-specific paraglider fabric", + "write": false + }, + { + "Usage": "Receive materials and a weapon or rare item", + "write": false + } + ], + "gameID": [ + "0100F2C0115B6000" + ], + "gameName": "The Legend of Zelda: Tears of the Kingdom" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive the Blue Attire", + "write": false + }, + { + "Usage": "Receive random materials", + "write": false + } + ], + "gameID": [ + "01008CF01BAAC000" + ], + "gameName": "The Legend of Zelda: Echoes of Wisdom" + } + ], + "head": "01010100", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01010100-00170002.png", + "name": "Sheik", + "release": { + "au": "2015-01-29", + "eu": "2015-01-23", + "jp": "2015-01-22", + "na": "2015-02-01" + }, + "tail": "00170002", + "type": "Figure" + }, + { + "amiiboSeries": "Mario Sports Superstars", + "character": "Pink Gold Peach", + "gameSeries": "Mario Sports Superstars", + "gamesSwitch": [], + "head": "09d10501", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_09d10501-02c20e02.png", + "name": "Pink Gold Peach - Horse Racing", + "release": { + "au": "2017-03-11", + "eu": "2017-03-10", + "jp": "2017-03-30", + "na": "2017-03-24" + }, + "tail": "02c20e02", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Jingle", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "01af0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01af0001-011c0502.png", + "name": "Jingle", + "release": { + "au": "2016-03-19", + "eu": "2016-03-18", + "jp": "2016-01-14", + "na": "2016-03-18" + }, + "tail": "011c0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Lolly", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "026f0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_026f0001-01900502.png", + "name": "Lolly", + "release": { + "au": "2016-06-18", + "eu": null, + "jp": "2016-03-24", + "na": "2016-06-10" + }, + "tail": "01900502", + "type": "Card" + }, + { + "amiiboSeries": "Splatoon", + "character": "Inkling", + "gameSeries": "Splatoon", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a Splatoon-themed racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01003DA010E8A000" + ], + "gameName": "Miitopia" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock exclusive gear / Save favorite weapons, gear, and control settings / Take photos with the character or saved gear", + "write": true + } + ], + "gameID": [ + "01003BC0000A0000" + ], + "gameName": "Splatoon 2" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock exclusive gear / Save favorite weapons, gear, and control settings / Take photos with the character or saved gear", + "write": true + } + ], + "gameID": [ + "0100C2500FC20000" + ], + "gameName": "Splatoon 3" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "08000100", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_08000100-025f0402.png", + "name": "Inkling Girl - Lime Green", + "release": { + "au": "2016-07-09", + "eu": "2016-07-08", + "jp": "2016-07-07", + "na": "2016-07-08" + }, + "tail": "025f0402", + "type": "Figure" + }, + { + "amiiboSeries": "Mario Sports Superstars", + "character": "Metal Mario", + "gameSeries": "Mario Sports Superstars", + "gamesSwitch": [], + "head": "09d00501", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_09d00501-02bd0e02.png", + "name": "Metal Mario - Horse Racing", + "release": { + "au": "2017-03-11", + "eu": "2017-03-10", + "jp": "2017-03-30", + "na": "2017-03-24" + }, + "tail": "02bd0e02", + "type": "Card" + }, + { + "amiiboSeries": "Splatoon", + "character": "Octoling", + "gameSeries": "Splatoon", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a Splatoon-themed racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock exclusive gear / Save favorite weapons, gear, and control settings / Take photos with the character or saved gear", + "write": true + } + ], + "gameID": [ + "01003BC0000A0000" + ], + "gameName": "Splatoon 2" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock exclusive gear / Save favorite weapons, gear, and control settings / Take photos with the character or saved gear", + "write": true + } + ], + "gameID": [ + "0100C2500FC20000" + ], + "gameName": "Splatoon 3" + } + ], + "head": "08050200", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_08050200-041b0402.png", + "name": "Octoling - Blue", + "release": { + "au": "2022-11-11", + "eu": "2022-11-11", + "jp": "2022-11-11", + "na": "2022-11-11" + }, + "tail": "041b0402", + "type": "Figure" + }, + { + "amiiboSeries": "Fire Emblem", + "character": "Alm", + "gameSeries": "Fire Emblem", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Receive a Fashion Ticket and a Music Ticket, for unlocking any of the available costumes and music tracks", + "write": false + } + ], + "gameID": [ + "0100A6301214E000" + ], + "gameName": "Fire Emblem Engage" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a weapon", + "write": false + } + ], + "gameID": [ + "0100F15003E64000" + ], + "gameName": "Fire Emblem Warriors" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive better-quality randomized resources, weapons, or equipment", + "write": false + } + ], + "gameID": [ + "010071F0143EA000" + ], + "gameName": "Fire Emblem Warriors: Three Hopes" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a Spirit of the character", + "write": false + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "21060000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_21060000-03601202.png", + "name": "Alm", + "release": { + "au": "2017-05-20", + "eu": "2017-05-19", + "jp": "2017-04-20", + "na": "2017-05-19" + }, + "tail": "03601202", + "type": "Figure" + }, + { + "amiiboSeries": "Super Smash Bros.", + "character": "Richter", + "gameSeries": "Castlevania", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "37c10000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_37c10000-038c0002.png", + "name": "Richter", + "release": { + "au": "2020-01-17", + "eu": "2020-01-17", + "jp": "2020-01-17", + "na": "2020-01-17" + }, + "tail": "038c0002", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Jeremiah", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "033f0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_033f0001-008f0502.png", + "name": "Jeremiah", + "release": { + "au": "2015-10-03", + "eu": "2015-10-02", + "jp": "2015-07-30", + "na": "2015-09-25" + }, + "tail": "008f0502", + "type": "Card" + }, + { + "amiiboSeries": "Mario Sports Superstars", + "character": "Bowser", + "gameSeries": "Mario Sports Superstars", + "gamesSwitch": [], + "head": "09c90401", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_09c90401-02990e02.png", + "name": "Bowser - Golf", + "release": { + "au": "2017-03-11", + "eu": "2017-03-10", + "jp": "2017-03-30", + "na": "2017-03-24" + }, + "tail": "02990e02", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Booker", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "019e0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_019e0001-00ad0502.png", + "name": "Booker", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-10-29", + "na": "2016-01-22" + }, + "tail": "00ad0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Lucky", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "02ec0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_02ec0001-01c40502.png", + "name": "Lucky", + "release": { + "au": "2016-06-18", + "eu": null, + "jp": "2016-03-24", + "na": "2016-06-10" + }, + "tail": "01c40502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "K.K. Slider", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01003DA010E8A000" + ], + "gameName": "Miitopia" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a Spirit of the character", + "write": false + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "01820000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01820000-02400502.png", + "name": "K. K. Slider", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-11-21", + "na": "2015-11-13" + }, + "tail": "02400502", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Luna", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "01b50001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01b50001-00510502.png", + "name": "Luna", + "release": { + "au": "2015-10-03", + "eu": "2015-10-02", + "jp": "2015-07-30", + "na": "2015-09-25" + }, + "tail": "00510502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Doc", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "049e0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_049e0001-01b70502.png", + "name": "Doc", + "release": { + "au": "2016-06-18", + "eu": null, + "jp": "2016-03-24", + "na": "2016-06-10" + }, + "tail": "01b70502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Celia", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "04540001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_04540001-01ae0502.png", + "name": "Celia", + "release": { + "au": "2016-06-18", + "eu": null, + "jp": "2016-03-24", + "na": "2016-06-10" + }, + "tail": "01ae0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Hippeux", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "03990001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_03990001-01c20502.png", + "name": "Hippeux", + "release": { + "au": "2016-06-18", + "eu": null, + "jp": "2016-03-24", + "na": "2016-06-10" + }, + "tail": "01c20502", + "type": "Card" + }, + { + "amiiboSeries": "Mario Sports Superstars", + "character": "Baby Luigi", + "gameSeries": "Mario Sports Superstars", + "gamesSwitch": [], + "head": "09cd0101", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_09cd0101-02aa0e02.png", + "name": "Baby Luigi - Soccer", + "release": { + "au": "2017-03-11", + "eu": "2017-03-10", + "jp": "2017-03-30", + "na": "2017-03-24" + }, + "tail": "02aa0e02", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Poppy", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "04ec0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_04ec0001-00770502.png", + "name": "Poppy", + "release": { + "au": "2015-10-03", + "eu": "2015-10-02", + "jp": "2015-07-30", + "na": "2015-09-25" + }, + "tail": "00770502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Tank", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "04b20001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_04b20001-01b90502.png", + "name": "Tank", + "release": { + "au": "2016-06-18", + "eu": null, + "jp": "2016-03-24", + "na": "2016-06-10" + }, + "tail": "01b90502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Lottie", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "01c10201", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01c10201-03bb0502.png", + "name": "Lottie - Island", + "release": { + "au": "2021-11-05", + "eu": "2021-11-05", + "jp": "2021-11-05", + "na": "2021-11-05" + }, + "tail": "03bb0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Marcel", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "02f90001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_02f90001-01020502.png", + "name": "Marcel", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-10-29", + "na": "2016-01-22" + }, + "tail": "01020502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Reese", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "018a0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_018a0001-00a90502.png", + "name": "Reese", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-10-29", + "na": "2016-01-22" + }, + "tail": "00a90502", + "type": "Card" + }, + { + "amiiboSeries": "Pikmin", + "character": "Pikmin", + "gameSeries": "Pikmin", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock a Pikmin-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a Spirit of the character", + "write": false + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "06420000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_06420000-035f1102.png", + "name": "Pikmin", + "release": { + "au": "2017-07-29", + "eu": "2017-07-28", + "jp": "2017-07-13", + "na": "2017-07-28" + }, + "tail": "035f1102", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Buck", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "03a40001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_03a40001-014f0502.png", + "name": "Buck", + "release": { + "au": "2016-03-19", + "eu": "2016-03-18", + "jp": "2016-01-14", + "na": "2016-03-18" + }, + "tail": "014f0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Ricky", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "04e70001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_04e70001-01320502.png", + "name": "Ricky", + "release": { + "au": "2016-03-19", + "eu": "2016-03-18", + "jp": "2016-01-14", + "na": "2016-03-18" + }, + "tail": "01320502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Cube", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "04610001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_04610001-01610502.png", + "name": "Cube", + "release": { + "au": "2016-03-19", + "eu": "2016-03-18", + "jp": "2016-01-14", + "na": "2016-03-18" + }, + "tail": "01610502", + "type": "Card" + }, + { + "amiiboSeries": "Yu-Gi-Oh!", + "character": "Yuga Ohdo", + "gameSeries": "Yu-Gi-Oh!", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Receive items/bonuses", + "write": false + } + ], + "gameID": [ + "01003C101454A000" + ], + "gameName": "Yu-Gi-Oh! Rush Duel Saikyo Battle Royale" + } + ], + "head": "38400001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_38400001-04241902.png", + "name": "Yuga Ohdo", + "release": { + "au": null, + "eu": null, + "jp": null, + "na": "2021-08-12" + }, + "tail": "04241902", + "type": "Card" + }, + { + "amiiboSeries": "Mario Sports Superstars", + "character": "Birdo", + "gameSeries": "Mario Sports Superstars", + "gamesSwitch": [], + "head": "09ce0101", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_09ce0101-02af0e02.png", + "name": "Birdo - Soccer", + "release": { + "au": "2017-03-11", + "eu": "2017-03-10", + "jp": "2017-03-30", + "na": "2017-03-24" + }, + "tail": "02af0e02", + "type": "Card" + }, + { + "amiiboSeries": "Mario Sports Superstars", + "character": "Daisy", + "gameSeries": "Mario Sports Superstars", + "gamesSwitch": [], + "head": "09c30301", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_09c30301-027a0e02.png", + "name": "Daisy - Tennis", + "release": { + "au": "2017-03-11", + "eu": "2017-03-10", + "jp": "2017-03-30", + "na": "2017-03-24" + }, + "tail": "027a0e02", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Astrid", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "03d60001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_03d60001-01570502.png", + "name": "Astrid", + "release": { + "au": "2016-03-19", + "eu": "2016-03-18", + "jp": "2016-01-14", + "na": "2016-03-18" + }, + "tail": "01570502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Isabelle", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01003DA010E8A000" + ], + "gameName": "Miitopia" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "01810100", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01810100-023f0502.png", + "name": "Isabelle - Winter Outfit", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-11-21", + "na": "2015-11-13" + }, + "tail": "023f0502", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Celeste", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a Spirit of the character", + "write": false + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "01930000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01930000-02480502.png", + "name": "Celeste", + "release": { + "au": "2016-01-30", + "eu": "2016-01-29", + "jp": "2015-12-17", + "na": "2016-01-22" + }, + "tail": "02480502", + "type": "Figure" + }, + { + "amiiboSeries": "Yoshi's Woolly World", + "character": "Yoshi", + "gameSeries": "Super Mario", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an amiibo-exclusive weapon for the character and their Rabbid counterpart", + "write": false + } + ], + "gameID": [ + "010067300059A000" + ], + "gameName": "Mario + Rabbids: Kingdom Battle" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01003DA010E8A000" + ], + "gameName": "Miitopia" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a particular power-up depending on the character", + "write": false + } + ], + "gameID": [ + "010028600EBDA000" + ], + "gameName": "Super Mario 3D World + Bowser's Fury" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock the character's shiny sticker", + "write": false + } + ], + "gameID": [ + "010036B0034E4000" + ], + "gameName": "Super Mario Party" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01006000040C2000" + ], + "gameName": "Yoshi's Crafted World" + } + ], + "head": "00030102", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_00030102-00420302.png", + "name": "Pink Yarn Yoshi", + "release": { + "au": "2015-06-25", + "eu": "2015-06-26", + "jp": "2015-07-16", + "na": "2015-10-16" + }, + "tail": "00420302", + "type": "Yarn" + }, + { + "amiiboSeries": "Power Pros", + "character": "Ganda", + "gameSeries": "Power Pros", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Receive in-game items and power-ups / Save items to your card after playing with friends to bring them home", + "write": true + } + ], + "gameID": [ + "0100E9C00BF28000" + ], + "gameName": "Jikkyou Powerful Pro Baseball" + } + ], + "head": "38040001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_38040001-03971702.png", + "name": "Ganda", + "release": { + "au": null, + "eu": null, + "jp": "2019-06-27", + "na": null + }, + "tail": "03971702", + "type": "Card" + }, + { + "amiiboSeries": "Mario Sports Superstars", + "character": "Daisy", + "gameSeries": "Mario Sports Superstars", + "gamesSwitch": [], + "head": "09c30201", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_09c30201-02790e02.png", + "name": "Daisy - Baseball", + "release": { + "au": "2017-03-11", + "eu": "2017-03-10", + "jp": "2017-03-30", + "na": "2017-03-24" + }, + "tail": "02790e02", + "type": "Card" + }, + { + "amiiboSeries": "Mario Sports Superstars", + "character": "Bowser", + "gameSeries": "Mario Sports Superstars", + "gamesSwitch": [], + "head": "09c90301", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_09c90301-02980e02.png", + "name": "Bowser - Tennis", + "release": { + "au": "2017-03-11", + "eu": "2017-03-10", + "jp": "2017-03-30", + "na": "2017-03-24" + }, + "tail": "02980e02", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Boomer", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "04690001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_04690001-01640502.png", + "name": "Boomer", + "release": { + "au": "2016-03-19", + "eu": "2016-03-18", + "jp": "2016-01-14", + "na": "2016-03-18" + }, + "tail": "01640502", + "type": "Card" + }, + { + "amiiboSeries": "Super Mario Bros.", + "character": "Diddy Kong", + "gameSeries": "Super Mario", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a particular power-up depending on the character", + "write": false + } + ], + "gameID": [ + "010028600EBDA000" + ], + "gameName": "Super Mario 3D World + Bowser's Fury" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a character-based costume", + "write": false + } + ], + "gameID": [ + "0100000000010000" + ], + "gameName": "Super Mario Odyssey" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock the character's shiny sticker", + "write": false + } + ], + "gameID": [ + "010036B0034E4000" + ], + "gameName": "Super Mario Party" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "00090000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_00090000-02650102.png", + "name": "Diddy Kong", + "release": { + "au": "2016-11-05", + "eu": "2016-11-04", + "jp": "2016-10-20", + "na": "2016-11-04" + }, + "tail": "02650102", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Canberra", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "03c40001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_03c40001-012b0502.png", + "name": "Canberra", + "release": { + "au": "2016-03-19", + "eu": "2016-03-18", + "jp": "2016-01-14", + "na": "2016-03-18" + }, + "tail": "012b0502", + "type": "Card" + }, + { + "amiiboSeries": "Mario Sports Superstars", + "character": "Pink Gold Peach", + "gameSeries": "Mario Sports Superstars", + "gamesSwitch": [], + "head": "09d10401", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_09d10401-02c10e02.png", + "name": "Pink Gold Peach - Golf", + "release": { + "au": "2017-03-11", + "eu": "2017-03-10", + "jp": "2017-03-30", + "na": "2017-03-24" + }, + "tail": "02c10e02", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Chief", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "050b0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_050b0001-00990502.png", + "name": "Chief", + "release": { + "au": "2015-10-03", + "eu": "2015-10-02", + "jp": "2015-07-30", + "na": "2015-09-25" + }, + "tail": "00990502", + "type": "Card" + }, + { + "amiiboSeries": "Splatoon", + "character": "Inkling", + "gameSeries": "Splatoon", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a Splatoon-themed racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01003DA010E8A000" + ], + "gameName": "Miitopia" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock exclusive gear / Save favorite weapons, gear, and control settings / Take photos with the character or saved gear", + "write": true + } + ], + "gameID": [ + "01003BC0000A0000" + ], + "gameName": "Splatoon 2" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock exclusive gear / Save favorite weapons, gear, and control settings / Take photos with the character or saved gear", + "write": true + } + ], + "gameID": [ + "0100C2500FC20000" + ], + "gameName": "Splatoon 3" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "08000200", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_08000200-036a0402.png", + "name": "Inkling Boy - Neon Green", + "release": { + "au": "2017-07-21", + "eu": "2017-07-21", + "jp": "2017-07-21", + "na": "2017-07-21" + }, + "tail": "036a0402", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Biff", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "03940001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_03940001-00890502.png", + "name": "Biff", + "release": { + "au": "2015-10-03", + "eu": "2015-10-02", + "jp": "2015-07-30", + "na": "2015-09-25" + }, + "tail": "00890502", + "type": "Card" + }, + { + "amiiboSeries": "Super Smash Bros.", + "character": "Little Mac", + "gameSeries": "Punch Out", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "06c00000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_06c00000-000f0002.png", + "name": "Little Mac", + "release": { + "au": "2014-12-12", + "eu": "2014-12-19", + "jp": "2014-12-06", + "na": "2014-12-14" + }, + "tail": "000f0002", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Julian", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "03b10001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_03b10001-00f00502.png", + "name": "Julian", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-10-29", + "na": "2016-01-22" + }, + "tail": "00f00502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Moe", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "02650001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_02650001-01540502.png", + "name": "Moe", + "release": { + "au": "2016-03-19", + "eu": "2016-03-18", + "jp": "2016-01-14", + "na": "2016-03-18" + }, + "tail": "01540502", + "type": "Card" + }, + { + "amiiboSeries": "Mario Sports Superstars", + "character": "Baby Mario", + "gameSeries": "Mario Sports Superstars", + "gamesSwitch": [], + "head": "09cc0201", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_09cc0201-02a60e02.png", + "name": "Baby Mario - Baseball", + "release": { + "au": "2017-03-11", + "eu": "2017-03-10", + "jp": "2017-03-30", + "na": "2017-03-24" + }, + "tail": "02a60e02", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Phyllis", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "01a10001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01a10001-01100502.png", + "name": "Phyllis", + "release": { + "au": "2016-03-19", + "eu": "2016-03-18", + "jp": "2016-01-14", + "na": "2016-03-18" + }, + "tail": "01100502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Megan", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "0a0a0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_0a0a0001-03c10502.png", + "name": "Megan", + "release": { + "au": "2021-11-05", + "eu": "2021-11-05", + "jp": "2021-11-05", + "na": "2021-11-05" + }, + "tail": "03c10502", + "type": "Card" + }, + { + "amiiboSeries": "Yoshi's Woolly World", + "character": "Yoshi", + "gameSeries": "Super Mario", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an amiibo-exclusive weapon for the character and their Rabbid counterpart", + "write": false + } + ], + "gameID": [ + "010067300059A000" + ], + "gameName": "Mario + Rabbids: Kingdom Battle" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01003DA010E8A000" + ], + "gameName": "Miitopia" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a particular power-up depending on the character", + "write": false + } + ], + "gameID": [ + "010028600EBDA000" + ], + "gameName": "Super Mario 3D World + Bowser's Fury" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock the character's shiny sticker", + "write": false + } + ], + "gameID": [ + "010036B0034E4000" + ], + "gameName": "Super Mario Party" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01006000040C2000" + ], + "gameName": "Yoshi's Crafted World" + } + ], + "head": "00030102", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_00030102-00410302.png", + "name": "Green Yarn Yoshi", + "release": { + "au": "2015-06-25", + "eu": "2015-06-26", + "jp": "2015-07-16", + "na": "2015-10-16" + }, + "tail": "00410302", + "type": "Yarn" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Maddie", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "02f30001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_02f30001-02f90502.png", + "name": "Maddie", + "release": { + "au": "2016-11-10", + "eu": "2016-11-11", + "jp": "2016-11-03", + "na": "2016-12-02" + }, + "tail": "02f90502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Wendy", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "04ce0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_04ce0001-00db0502.png", + "name": "Wendy", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-10-29", + "na": "2016-01-22" + }, + "tail": "00db0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Peanut", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "04dd0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_04dd0001-00a20502.png", + "name": "Peanut", + "release": { + "au": "2015-10-03", + "eu": "2015-10-02", + "jp": "2015-07-30", + "na": "2015-09-25" + }, + "tail": "00a20502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Chabwick", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "0a190001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_0a190001-03d00502.png", + "name": "Chabwick", + "release": { + "au": "2021-11-05", + "eu": "2021-11-05", + "jp": "2021-11-05", + "na": "2021-11-05" + }, + "tail": "03d00502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Leila", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "01980001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01980001-00b10502.png", + "name": "Leila", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-10-29", + "na": "2016-01-22" + }, + "tail": "00b10502", + "type": "Card" + }, + { + "amiiboSeries": "Super Smash Bros.", + "character": "Pyra", + "gameSeries": "Xenoblade Chronicles", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-specific weapon skin for characters using the Swordfighter Class", + "write": false + } + ], + "gameID": [ + "010074F013262000" + ], + "gameName": "Xenoblade Chronicles 3" + } + ], + "head": "22410000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_22410000-041e0002.png", + "name": "Pyra", + "release": { + "au": "2023-07-21", + "eu": "2023-07-21", + "jp": "2023-07-21", + "na": "2023-07-21" + }, + "tail": "041e0002", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Rilla", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + }, + { + "Usage": "Unlock special furniture items and a poster based on the card's Sanrio character", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "03740101", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_03740101-03190502.png", + "name": "Rilla", + "release": { + "au": null, + "eu": "2016-11-25", + "jp": "2016-11-03", + "na": null + }, + "tail": "03190502", + "type": "Card" + }, + { + "amiiboSeries": "Super Smash Bros.", + "character": "Villager", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock a special poster of the character", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "01800000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01800000-00080002.png", + "name": "Villager", + "release": { + "au": "2014-11-29", + "eu": "2014-11-28", + "jp": "2014-12-06", + "na": "2014-11-21" + }, + "tail": "00080002", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Mabel", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "01880001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01880001-03af0502.png", + "name": "Mabel", + "release": { + "au": "2021-11-05", + "eu": "2021-11-05", + "jp": "2021-11-05", + "na": "2021-11-05" + }, + "tail": "03af0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Tiffany", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "049b0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_049b0001-00610502.png", + "name": "Tiffany", + "release": { + "au": "2015-10-03", + "eu": "2015-10-02", + "jp": "2015-07-30", + "na": "2015-09-25" + }, + "tail": "00610502", + "type": "Card" + }, + { + "amiiboSeries": "Mario Sports Superstars", + "character": "Bowser Jr.", + "gameSeries": "Mario Sports Superstars", + "gamesSwitch": [], + "head": "09ca0401", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_09ca0401-029e0e02.png", + "name": "Bowser Jr. - Golf", + "release": { + "au": "2017-03-11", + "eu": "2017-03-10", + "jp": "2017-03-30", + "na": "2017-03-24" + }, + "tail": "029e0e02", + "type": "Card" + }, + { + "amiiboSeries": "Kirby", + "character": "Waddle Dee", + "gameSeries": "Kirby", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Receive star coins and a boost item", + "write": false + } + ], + "gameID": [ + "01004D300C5AE000" + ], + "gameName": "Kirby and the Forgotten Land" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive two Picture Pieces, a Maxim Tomato, and two Point Stars", + "write": false + } + ], + "gameID": [ + "01007E3006DDA000" + ], + "gameName": "Kirby Star Allies" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive more useful items", + "write": false + } + ], + "gameID": [ + "01006B601380E000" + ], + "gameName": "Kirby's Return to Dream Land Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a Kirby-themed racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive 20 Fragments", + "write": false + } + ], + "gameID": [ + "01003FB00C5A8000" + ], + "gameName": "Super Kirby Clash" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a Spirit of the character", + "write": false + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "1f030000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_1f030000-02570c02.png", + "name": "Waddle Dee", + "release": { + "au": "2016-06-11", + "eu": "2016-06-10", + "jp": "2016-04-28", + "na": "2016-06-10" + }, + "tail": "02570c02", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Del", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "02c70001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_02c70001-01220502.png", + "name": "Del", + "release": { + "au": "2016-03-19", + "eu": "2016-03-18", + "jp": "2016-01-14", + "na": "2016-03-18" + }, + "tail": "01220502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Cookie", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "02f20001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_02f20001-00cc0502.png", + "name": "Cookie", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-10-29", + "na": "2016-01-22" + }, + "tail": "00cc0502", + "type": "Card" + }, + { + "amiiboSeries": "Mario Sports Superstars", + "character": "Donkey Kong", + "gameSeries": "Mario Sports Superstars", + "gamesSwitch": [], + "head": "09c70301", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_09c70301-028e0e02.png", + "name": "Donkey Kong - Tennis", + "release": { + "au": "2017-03-11", + "eu": "2017-03-10", + "jp": "2017-03-30", + "na": "2017-03-24" + }, + "tail": "028e0e02", + "type": "Card" + }, + { + "amiiboSeries": "Super Smash Bros.", + "character": "Isabelle", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01003DA010E8A000" + ], + "gameName": "Miitopia" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "01810000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01810000-037d0002.png", + "name": "Isabelle", + "release": { + "au": "2019-07-19", + "eu": "2019-07-19", + "jp": "2019-07-19", + "na": "2019-07-26" + }, + "tail": "037d0002", + "type": "Figure" + }, + { + "amiiboSeries": "Super Smash Bros.", + "character": "Ryu", + "gameSeries": "Street fighter", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Receive a lot of BP / Receive better supplies (compared to other amiibo)", + "write": false + } + ], + "gameID": [ + "0100643002136000" + ], + "gameName": "Resident Evil Revelations" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a lot of BP / Receive better supplies (compared to other amiibo)", + "write": false + } + ], + "gameID": [ + "010095300212A000" + ], + "gameName": "Resident Evil Revelations 2" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "34c00000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_34c00000-02530002.png", + "name": "Ryu", + "release": { + "au": "2016-03-19", + "eu": "2016-03-18", + "jp": "2016-04-28", + "na": "2016-03-18" + }, + "tail": "02530002", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Harry", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "03980001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_03980001-00bf0502.png", + "name": "Harry", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-10-29", + "na": "2016-01-22" + }, + "tail": "00bf0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Deli", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "04010001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_04010001-00660502.png", + "name": "Deli", + "release": { + "au": "2015-10-03", + "eu": "2015-10-02", + "jp": "2015-07-30", + "na": "2015-09-25" + }, + "tail": "00660502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Chelsea", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + }, + { + "Usage": "Unlock special furniture items and a poster based on the card's Sanrio character", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "02e00101", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_02e00101-031d0502.png", + "name": "Chelsea", + "release": { + "au": null, + "eu": "2016-11-25", + "jp": "2016-11-03", + "na": null + }, + "tail": "031d0502", + "type": "Card" + }, + { + "amiiboSeries": "Super Smash Bros.", + "character": "Ganon", + "gameSeries": "The Legend of Zelda", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Receive specific crafting materials or a weapon for the character", + "write": false + } + ], + "gameID": [ + "01002B00111A2000" + ], + "gameName": "Hyrule Warriors: Age of Calamity" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a weapon rated 3 stars or higher", + "write": false + } + ], + "gameID": [ + "0100AE00096EA000" + ], + "gameName": "Hyrule Warriors: Definitive Edition" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a Legend of Zelda-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01003DA010E8A000" + ], + "gameName": "Miitopia" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a chest of loot, potentially containing gear inspired by the Legend of Zelda series", + "write": false + } + ], + "gameID": [ + "01000A10041EA000" + ], + "gameName": "The Elder Scrolls V: Skyrim" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive materials and a rare or exclusive item", + "write": false + } + ], + "gameID": [ + "01007EF00011E000" + ], + "gameName": "The Legend of Zelda: Breath of the Wild" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock one of five amiibo-exclusive Chamber Dungeon chambers", + "write": false + }, + { + "Usage": "Save your Chamber Dungeon to the amiibo to share with friends", + "write": true + } + ], + "gameID": [ + "01006BB00C6F0000" + ], + "gameName": "The Legend of Zelda: Link's Awakening" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an amiibo-exclusive character-specific paraglider fabric", + "write": false + }, + { + "Usage": "Receive materials and a weapon or rare item", + "write": false + } + ], + "gameID": [ + "0100F2C0115B6000" + ], + "gameName": "The Legend of Zelda: Tears of the Kingdom" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive the Black Cat Clothes", + "write": false + }, + { + "Usage": "Receive random materials", + "write": false + } + ], + "gameID": [ + "01008CF01BAAC000" + ], + "gameName": "The Legend of Zelda: Echoes of Wisdom" + } + ], + "head": "01020100", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01020100-001b0002.png", + "name": "Ganondorf", + "release": { + "au": "2015-07-04", + "eu": "2015-06-26", + "jp": "2015-06-11", + "na": "2015-09-11" + }, + "tail": "001b0002", + "type": "Figure" + }, + { + "amiiboSeries": "Mario Sports Superstars", + "character": "Daisy", + "gameSeries": "Mario Sports Superstars", + "gamesSwitch": [], + "head": "09c30401", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_09c30401-027b0e02.png", + "name": "Daisy - Golf", + "release": { + "au": "2017-03-11", + "eu": "2017-03-10", + "jp": "2017-03-30", + "na": "2017-03-24" + }, + "tail": "027b0e02", + "type": "Card" + }, + { + "amiiboSeries": "Mario Sports Superstars", + "character": "Pink Gold Peach", + "gameSeries": "Mario Sports Superstars", + "gamesSwitch": [], + "head": "09d10301", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_09d10301-02c00e02.png", + "name": "Pink Gold Peach - Tennis", + "release": { + "au": "2017-03-11", + "eu": "2017-03-10", + "jp": "2017-03-30", + "na": "2017-03-24" + }, + "tail": "02c00e02", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Bud", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "03e60001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_03e60001-00ec0502.png", + "name": "Bud", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-10-29", + "na": "2016-01-22" + }, + "tail": "00ec0502", + "type": "Card" + }, + { + "amiiboSeries": "Super Smash Bros.", + "character": "Joker", + "gameSeries": "Persona", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "3a000000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_3a000000-03a10002.png", + "name": "Joker", + "release": { + "au": "2020-09-25", + "eu": "2020-09-25", + "jp": "2020-09-25", + "na": "2020-10-02" + }, + "tail": "03a10002", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Al", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "03710001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_03710001-005c0502.png", + "name": "Al", + "release": { + "au": "2015-10-03", + "eu": "2015-10-02", + "jp": "2015-07-30", + "na": "2015-09-25" + }, + "tail": "005c0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Elise", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "03fe0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_03fe0001-01a40502.png", + "name": "Elise", + "release": { + "au": "2016-06-18", + "eu": null, + "jp": "2016-03-24", + "na": "2016-06-10" + }, + "tail": "01a40502", + "type": "Card" + }, + { + "amiiboSeries": "Super Smash Bros.", + "character": "Dark Samus", + "gameSeries": "Metroid", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock a Metroid-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Replenish a random amount of missiles once per day", + "write": false + } + ], + "gameID": [ + "010093801237C000" + ], + "gameName": "Metroid Dread" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "05c30000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_05c30000-03800002.png", + "name": "Dark Samus", + "release": { + "au": "2020-01-17", + "eu": "2020-01-17", + "jp": "2020-01-17", + "na": "2020-01-17" + }, + "tail": "03800002", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Marcie", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "03db0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_03db0001-006d0502.png", + "name": "Marcie", + "release": { + "au": "2015-10-03", + "eu": "2015-10-02", + "jp": "2015-07-30", + "na": "2015-09-25" + }, + "tail": "006d0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Curt", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "02160001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_02160001-00570502.png", + "name": "Curt", + "release": { + "au": "2015-10-03", + "eu": "2015-10-02", + "jp": "2015-07-30", + "na": "2015-09-25" + }, + "tail": "00570502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Kyle", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "05150001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_05150001-005b0502.png", + "name": "Kyle", + "release": { + "au": "2015-10-03", + "eu": "2015-10-02", + "jp": "2015-07-30", + "na": "2015-09-25" + }, + "tail": "005b0502", + "type": "Card" + }, + { + "amiiboSeries": "Super Smash Bros.", + "character": "Corrin", + "gameSeries": "Fire Emblem", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Receive a Fashion Ticket and a Music Ticket, for unlocking any of the available costumes and music tracks", + "write": false + } + ], + "gameID": [ + "0100A6301214E000" + ], + "gameName": "Fire Emblem Engage" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a weapon", + "write": false + } + ], + "gameID": [ + "0100F15003E64000" + ], + "gameName": "Fire Emblem Warriors" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive better-quality randomized resources, weapons, or equipment", + "write": false + } + ], + "gameID": [ + "010071F0143EA000" + ], + "gameName": "Fire Emblem Warriors: Three Hopes" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special piece of battle music / Receive higher-quality items and materials", + "write": false + } + ], + "gameID": [ + "010055D009F78000" + ], + "gameName": "Fire Emblem: Three Houses" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "21050100", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_21050100-03630002.png", + "name": "Corrin - Player 2", + "release": { + "au": "2017-07-22", + "eu": "2017-07-21", + "jp": "2017-07-21", + "na": "2017-07-21" + }, + "tail": "03630002", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Angus", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "024a0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_024a0001-01d10502.png", + "name": "Angus", + "release": { + "au": "2016-06-18", + "eu": null, + "jp": "2016-03-24", + "na": "2016-06-10" + }, + "tail": "01d10502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Curly", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "04780001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_04780001-01630502.png", + "name": "Curly", + "release": { + "au": "2016-03-19", + "eu": "2016-03-18", + "jp": "2016-01-14", + "na": "2016-03-18" + }, + "tail": "01630502", + "type": "Card" + }, + { + "amiiboSeries": "Super Nintendo World", + "character": "Yoshi", + "gameSeries": "Super Mario", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock an amiibo-exclusive weapon for the character and their Rabbid counterpart", + "write": false + } + ], + "gameID": [ + "010067300059A000" + ], + "gameName": "Mario + Rabbids: Kingdom Battle" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01003DA010E8A000" + ], + "gameName": "Miitopia" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a particular power-up depending on the character", + "write": false + } + ], + "gameID": [ + "010028600EBDA000" + ], + "gameName": "Super Mario 3D World + Bowser's Fury" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock the character's shiny sticker", + "write": false + } + ], + "gameID": [ + "010036B0034E4000" + ], + "gameName": "Super Mario Party" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01006000040C2000" + ], + "gameName": "Yoshi's Crafted World" + } + ], + "head": "00030003", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_00030003-039fff02.png", + "name": "Yoshi - Power Up Band", + "release": { + "au": null, + "eu": null, + "jp": "2021-02-04", + "na": null + }, + "tail": "039fff02", + "type": "Band" + }, + { + "amiiboSeries": "Mario Sports Superstars", + "character": "Mario", + "gameSeries": "Mario Sports Superstars", + "gamesSwitch": [], + "head": "09c00401", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_09c00401-026c0e02.png", + "name": "Mario - Golf", + "release": { + "au": "2017-03-11", + "eu": "2017-03-10", + "jp": "2017-03-30", + "na": "2017-03-24" + }, + "tail": "026c0e02", + "type": "Card" + }, + { + "amiiboSeries": "Mario Sports Superstars", + "character": "Baby Luigi", + "gameSeries": "Mario Sports Superstars", + "gamesSwitch": [], + "head": "09cd0401", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_09cd0401-02ad0e02.png", + "name": "Baby Luigi - Golf", + "release": { + "au": "2017-03-11", + "eu": "2017-03-10", + "jp": "2017-03-30", + "na": "2017-03-24" + }, + "tail": "02ad0e02", + "type": "Card" + }, + { + "amiiboSeries": "Mario Sports Superstars", + "character": "Wario", + "gameSeries": "Mario Sports Superstars", + "gamesSwitch": [], + "head": "09c50501", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_09c50501-02860e02.png", + "name": "Wario - Horse Racing", + "release": { + "au": "2017-03-11", + "eu": "2017-03-10", + "jp": "2017-03-30", + "na": "2017-03-24" + }, + "tail": "02860e02", + "type": "Card" + }, + { + "amiiboSeries": "Mario Sports Superstars", + "character": "Bowser Jr.", + "gameSeries": "Mario Sports Superstars", + "gamesSwitch": [], + "head": "09ca0201", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_09ca0201-029c0e02.png", + "name": "Bowser Jr. - Baseball", + "release": { + "au": "2017-03-11", + "eu": "2017-03-10", + "jp": "2017-03-30", + "na": "2017-03-24" + }, + "tail": "029c0e02", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Isabelle", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "01810201", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01810201-011a0502.png", + "name": "Isabelle - Kimono", + "release": { + "au": "2016-03-19", + "eu": "2016-03-18", + "jp": "2016-01-14", + "na": "2016-03-18" + }, + "tail": "011a0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Drift", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "033c0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_033c0001-01000502.png", + "name": "Drift", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-10-29", + "na": "2016-01-22" + }, + "tail": "01000502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Margie", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "03270001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_03270001-01c30502.png", + "name": "Margie", + "release": { + "au": "2016-06-18", + "eu": null, + "jp": "2016-03-24", + "na": "2016-06-10" + }, + "tail": "01c30502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Mallary", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "030d0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_030d0001-01840502.png", + "name": "Mallary", + "release": { + "au": "2016-06-18", + "eu": null, + "jp": "2016-03-24", + "na": "2016-06-10" + }, + "tail": "01840502", + "type": "Card" + }, + { + "amiiboSeries": "Mario Sports Superstars", + "character": "Bowser", + "gameSeries": "Mario Sports Superstars", + "gamesSwitch": [], + "head": "09c90201", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_09c90201-02970e02.png", + "name": "Bowser - Baseball", + "release": { + "au": "2017-03-11", + "eu": "2017-03-10", + "jp": "2017-03-30", + "na": "2017-03-24" + }, + "tail": "02970e02", + "type": "Card" + }, + { + "amiiboSeries": "Kirby", + "character": "King Dedede", + "gameSeries": "Kirby", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Receive star coins and a boost item", + "write": false + } + ], + "gameID": [ + "01004D300C5AE000" + ], + "gameName": "Kirby and the Forgotten Land" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive two Picture Pieces, a Maxim Tomato, and two Point Stars", + "write": false + } + ], + "gameID": [ + "01007E3006DDA000" + ], + "gameName": "Kirby Star Allies" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive more useful items", + "write": false + } + ], + "gameID": [ + "01006B601380E000" + ], + "gameName": "Kirby's Return to Dream Land Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a Kirby-themed racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive 20 Fragments", + "write": false + } + ], + "gameID": [ + "01003FB00C5A8000" + ], + "gameName": "Super Kirby Clash" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "1f020000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_1f020000-02560c02.png", + "name": "King Dedede", + "release": { + "au": "2016-06-11", + "eu": "2016-06-10", + "jp": "2016-04-28", + "na": "2016-06-10" + }, + "tail": "02560c02", + "type": "Figure" + }, + { + "amiiboSeries": "Monster Hunter", + "character": "Ena", + "gameSeries": "Monster Hunter", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock Monster Hunter Stories 2 sticker set", + "write": false + } + ], + "gameID": [ + "0100B04011742000" + ], + "gameName": "Monster Hunter Rise" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a character-based costume for Navirou", + "write": false + } + ], + "gameID": [ + "010069301B1D4000" + ], + "gameName": "Monster Hunter Stories" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-specific special layered armor set / Have Tsukino read your fortune and receive a random item", + "write": false + } + ], + "gameID": [ + "0100E21011446000" + ], + "gameName": "Monster Hunter Stories 2: Wings of Ruin" + } + ], + "head": "35060000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_35060000-040d0f02.png", + "name": "Ena", + "release": { + "au": "2021-07-09", + "eu": "2021-07-09", + "jp": "2021-07-09", + "na": "2021-07-09" + }, + "tail": "040d0f02", + "type": "Figure" + }, + { + "amiiboSeries": "Mario Sports Superstars", + "character": "Luigi", + "gameSeries": "Mario Sports Superstars", + "gamesSwitch": [], + "head": "09c10201", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_09c10201-026f0e02.png", + "name": "Luigi - Baseball", + "release": { + "au": "2017-03-11", + "eu": "2017-03-10", + "jp": "2017-03-30", + "na": "2017-03-24" + }, + "tail": "026f0e02", + "type": "Card" + }, + { + "amiiboSeries": "Super Smash Bros.", + "character": "Duck Hunt", + "gameSeries": "Classic Nintendo", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "07820000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_07820000-002f0002.png", + "name": "Duck Hunt", + "release": { + "au": "2015-09-26", + "eu": "2015-09-25", + "jp": "2015-10-29", + "na": "2015-09-25" + }, + "tail": "002f0002", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Cleo", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "03ab0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_03ab0001-03160502.png", + "name": "Cleo", + "release": { + "au": "2016-11-10", + "eu": "2016-11-11", + "jp": "2016-11-03", + "na": "2016-12-02" + }, + "tail": "03160502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Digby", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "018c0101", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_018c0101-01180502.png", + "name": "Digby - Raincoat", + "release": { + "au": "2016-03-19", + "eu": "2016-03-18", + "jp": "2016-01-14", + "na": "2016-03-18" + }, + "tail": "01180502", + "type": "Card" + }, + { + "amiiboSeries": "Mario Sports Superstars", + "character": "Rosalina", + "gameSeries": "Mario Sports Superstars", + "gamesSwitch": [], + "head": "09cf0501", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_09cf0501-02b80e02.png", + "name": "Rosalina - Horse Racing", + "release": { + "au": "2017-03-11", + "eu": "2017-03-10", + "jp": "2017-03-30", + "na": "2017-03-24" + }, + "tail": "02b80e02", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Cyd", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "0a0d0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_0a0d0001-03c40502.png", + "name": "Cyd", + "release": { + "au": "2021-11-05", + "eu": "2021-11-05", + "jp": "2021-11-05", + "na": "2021-11-05" + }, + "tail": "03c40502", + "type": "Card" + }, + { + "amiiboSeries": "Super Mario Bros.", + "character": "Bowser", + "gameSeries": "Super Mario", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock the Chain Chomp weapon", + "write": false + } + ], + "gameID": [ + "01007960049A0000" + ], + "gameName": "Bayonetta 2" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01003DA010E8A000" + ], + "gameName": "Miitopia" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a particular power-up depending on the character", + "write": false + }, + { + "Usage": "Make Fury Bowser appear (in Bowser's Fury mode)", + "write": false + } + ], + "gameID": [ + "010028600EBDA000" + ], + "gameName": "Super Mario 3D World + Bowser's Fury" + }, + { + "amiiboUsage": [ + { + "Usage": "Reveal regional coin locations", + "write": false + } + ], + "gameID": [ + "0100000000010000" + ], + "gameName": "Super Mario Odyssey" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock the character's shiny sticker", + "write": false + } + ], + "gameID": [ + "010036B0034E4000" + ], + "gameName": "Super Mario Party" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01006000040C2000" + ], + "gameName": "Yoshi's Crafted World" + } + ], + "head": "00050000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_00050000-00390102.png", + "name": "Bowser", + "release": { + "au": "2015-03-21", + "eu": "2015-03-20", + "jp": "2015-03-12", + "na": "2015-03-20" + }, + "tail": "00390102", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Faith", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "0a200001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_0a200001-03d70502.png", + "name": "Faith", + "release": { + "au": "2021-11-05", + "eu": "2021-11-05", + "jp": "2021-11-05", + "na": "2021-11-05" + }, + "tail": "03d70502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Annalise", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "03ad0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_03ad0001-01b20502.png", + "name": "Annalise", + "release": { + "au": "2016-06-18", + "eu": null, + "jp": "2016-03-24", + "na": "2016-06-10" + }, + "tail": "01b20502", + "type": "Card" + }, + { + "amiiboSeries": "Super Mario Bros.", + "character": "Mario", + "gameSeries": "Super Mario", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an amiibo-exclusive weapon for the character and their Rabbid counterpart", + "write": false + } + ], + "gameID": [ + "010067300059A000" + ], + "gameName": "Mario + Rabbids: Kingdom Battle" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01003DA010E8A000" + ], + "gameName": "Miitopia" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a particular power-up depending on the character", + "write": false + } + ], + "gameID": [ + "010028600EBDA000" + ], + "gameName": "Super Mario 3D World + Bowser's Fury" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a character-based costume", + "write": false + } + ], + "gameID": [ + "0100000000010000" + ], + "gameName": "Super Mario Odyssey" + }, + { + "amiiboUsage": [ + { + "Usage": "Gain temporary invincibility", + "write": false + } + ], + "gameID": [ + "0100000000010000" + ], + "gameName": "Super Mario Odyssey" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock the character's shiny sticker", + "write": false + } + ], + "gameID": [ + "010036B0034E4000" + ], + "gameName": "Super Mario Party" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01006000040C2000" + ], + "gameName": "Yoshi's Crafted World" + } + ], + "head": "00000000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_00000000-003d0102.png", + "name": "Mario - Silver Edition", + "release": { + "au": "2015-05-30", + "eu": null, + "jp": null, + "na": "2015-05-29" + }, + "tail": "003d0102", + "type": "Figure" + }, + { + "amiiboSeries": "Super Smash Bros.", + "character": "Incineroar", + "gameSeries": "Pokemon", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "1bd70000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_1bd70000-03860002.png", + "name": "Incineroar", + "release": { + "au": "2019-11-15", + "eu": "2019-11-15", + "jp": "2019-11-08", + "na": "2019-11-15" + }, + "tail": "03860002", + "type": "Figure" + }, + { + "amiiboSeries": "Mario Sports Superstars", + "character": "Yoshi", + "gameSeries": "Mario Sports Superstars", + "gamesSwitch": [], + "head": "09c40501", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_09c40501-02810e02.png", + "name": "Yoshi - Horse Racing", + "release": { + "au": "2017-03-11", + "eu": "2017-03-10", + "jp": "2017-03-30", + "na": "2017-03-24" + }, + "tail": "02810e02", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Sly", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "02c90001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_02c90001-00cd0502.png", + "name": "Sly", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-10-29", + "na": "2016-01-22" + }, + "tail": "00cd0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Goose", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "02990001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_02990001-00950502.png", + "name": "Goose", + "release": { + "au": "2015-10-03", + "eu": "2015-10-02", + "jp": "2015-07-30", + "na": "2015-09-25" + }, + "tail": "00950502", + "type": "Card" + }, + { + "amiiboSeries": "Splatoon", + "character": "Marina", + "gameSeries": "Splatoon", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a Splatoon-themed racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock exclusive gear / Save favorite weapons, gear, and control settings / Take photos with the character or saved gear", + "write": true + } + ], + "gameID": [ + "01003BC0000A0000" + ], + "gameName": "Splatoon 2" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock exclusive gear / Save favorite weapons, gear, and control settings / Take photos with the character or saved gear", + "write": true + } + ], + "gameID": [ + "0100C2500FC20000" + ], + "gameName": "Splatoon 3" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a Spirit of the character", + "write": false + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "08040000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_08040000-03770402.png", + "name": "Marina", + "release": { + "au": "2018-07-13", + "eu": "2018-07-13", + "jp": "2018-07-13", + "na": "2018-07-13" + }, + "tail": "03770402", + "type": "Figure" + }, + { + "amiiboSeries": "Splatoon", + "character": "Marina", + "gameSeries": "Splatoon", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a Splatoon-themed racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock exclusive gear / Save favorite weapons, gear, and control settings / Take photos with the character or saved gear", + "write": true + } + ], + "gameID": [ + "01003BC0000A0000" + ], + "gameName": "Splatoon 2" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock exclusive gear / Save favorite weapons, gear, and control settings / Take photos with the character or saved gear", + "write": true + } + ], + "gameID": [ + "0100C2500FC20000" + ], + "gameName": "Splatoon 3" + } + ], + "head": "08040000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_08040000-04390402.png", + "name": "Marina - Side Order", + "release": { + "au": "2024-09-05", + "eu": "2024-09-05", + "jp": "2024-09-05", + "na": "2024-09-05" + }, + "tail": "04390402", + "type": "Figure" + }, + { + "amiiboSeries": "Super Smash Bros.", + "character": "Bayonetta", + "gameSeries": "Bayonetta", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock the Super Mirror 2 and all the costumes it contains", + "write": false + } + ], + "gameID": [ + "01007960049A0000" + ], + "gameName": "Bayonetta 2" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "32400000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_32400000-025b0002.png", + "name": "Bayonetta", + "release": { + "au": "2017-07-22", + "eu": "2017-07-21", + "jp": "2017-07-21", + "na": "2017-07-21" + }, + "tail": "025b0002", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Joey", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "03080001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_03080001-014d0502.png", + "name": "Joey", + "release": { + "au": "2016-03-19", + "eu": "2016-03-18", + "jp": "2016-01-14", + "na": "2016-03-18" + }, + "tail": "014d0502", + "type": "Card" + }, + { + "amiiboSeries": "Super Smash Bros.", + "character": "Pokemon Trainer", + "gameSeries": "Pokemon", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "1d400000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_1d400000-03870002.png", + "name": "Pokemon Trainer", + "release": { + "au": "2019-07-19", + "eu": "2019-07-19", + "jp": "2019-07-19", + "na": "2019-07-26" + }, + "tail": "03870002", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Tangy", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "02620001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_02620001-01370502.png", + "name": "Tangy", + "release": { + "au": "2016-03-19", + "eu": "2016-03-18", + "jp": "2016-01-14", + "na": "2016-03-18" + }, + "tail": "01370502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Pancetti", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "04880001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_04880001-00980502.png", + "name": "Pancetti", + "release": { + "au": "2015-10-03", + "eu": "2015-10-02", + "jp": "2015-07-30", + "na": "2015-09-25" + }, + "tail": "00980502", + "type": "Card" + }, + { + "amiiboSeries": "Super Smash Bros.", + "character": "Ridley", + "gameSeries": "Metroid", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock a Metroid-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Replenish a random amount of missiles once per day", + "write": false + } + ], + "gameID": [ + "010093801237C000" + ], + "gameName": "Metroid Dread" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "05c20000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_05c20000-037f0002.png", + "name": "Ridley", + "release": { + "au": "2018-12-07", + "eu": "2018-12-07", + "jp": "2018-12-07", + "na": "2018-12-07" + }, + "tail": "037f0002", + "type": "Figure" + }, + { + "amiiboSeries": "Mario Sports Superstars", + "character": "Donkey Kong", + "gameSeries": "Mario Sports Superstars", + "gamesSwitch": [], + "head": "09c70401", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_09c70401-028f0e02.png", + "name": "Donkey Kong - Golf", + "release": { + "au": "2017-03-11", + "eu": "2017-03-10", + "jp": "2017-03-30", + "na": "2017-03-24" + }, + "tail": "028f0e02", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Louie", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "036d0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_036d0001-03040502.png", + "name": "Louie", + "release": { + "au": "2016-11-10", + "eu": "2016-11-11", + "jp": "2016-11-03", + "na": "2016-12-02" + }, + "tail": "03040502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Dora", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "040c0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_040c0001-01590502.png", + "name": "Dora", + "release": { + "au": "2016-03-19", + "eu": "2016-03-18", + "jp": "2016-01-14", + "na": "2016-03-18" + }, + "tail": "01590502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Broffina", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "02a50001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_02a50001-018c0502.png", + "name": "Broffina", + "release": { + "au": "2016-06-18", + "eu": null, + "jp": "2016-03-24", + "na": "2016-06-10" + }, + "tail": "018c0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Blanche", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "043e0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_043e0001-01490502.png", + "name": "Blanche", + "release": { + "au": "2016-03-19", + "eu": "2016-03-18", + "jp": "2016-01-14", + "na": "2016-03-18" + }, + "tail": "01490502", + "type": "Card" + }, + { + "amiiboSeries": "Mario Sports Superstars", + "character": "Mario", + "gameSeries": "Mario Sports Superstars", + "gamesSwitch": [], + "head": "09c00501", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_09c00501-026d0e02.png", + "name": "Mario - Horse Racing", + "release": { + "au": "2017-03-11", + "eu": "2017-03-10", + "jp": "2017-03-30", + "na": "2017-03-24" + }, + "tail": "026d0e02", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Lionel", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "03ee0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_03ee0001-008b0502.png", + "name": "Lionel", + "release": { + "au": "2015-10-03", + "eu": "2015-10-02", + "jp": "2015-07-30", + "na": "2015-09-25" + }, + "tail": "008b0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Jacques", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "023d0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_023d0001-01b50502.png", + "name": "Jacques", + "release": { + "au": "2016-06-18", + "eu": null, + "jp": "2016-03-24", + "na": "2016-06-10" + }, + "tail": "01b50502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Vesta", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "04c50001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_04c50001-01010502.png", + "name": "Vesta", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-10-29", + "na": "2016-01-22" + }, + "tail": "01010502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Chevre", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "03560001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_03560001-01350502.png", + "name": "Chevre", + "release": { + "au": "2016-03-19", + "eu": "2016-03-18", + "jp": "2016-01-14", + "na": "2016-03-18" + }, + "tail": "01350502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Shep", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "02fc0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_02fc0001-018f0502.png", + "name": "Shep", + "release": { + "au": "2016-06-18", + "eu": null, + "jp": "2016-03-24", + "na": "2016-06-10" + }, + "tail": "018f0502", + "type": "Card" + }, + { + "amiiboSeries": "Shovel Knight", + "character": "Specter Knight", + "gameSeries": "Shovel Knight", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock a fairy companion and player color palette matching the character", + "write": false + } + ], + "gameID": [ + "01008D100DE46000" + ], + "gameName": "Cyber Shadow" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-specific Shovel Knight remix immediately", + "write": false + } + ], + "gameID": [ + "0100830008426000" + ], + "gameName": "Just Shapes & Beats" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock character-specific challenge stages, a character-based fairy companion, and costumes for the character", + "write": false + } + ], + "gameID": [ + "010057D0021E8000" + ], + "gameName": "Shovel Knight" + }, + { + "amiiboUsage": [ + { + "Usage": "Summon a fairy friend", + "write": false + } + ], + "gameID": [ + "0100B62017E68000" + ], + "gameName": "Shovel Knight Dig" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume for the character", + "write": false + } + ], + "gameID": [ + "0100B380022AE000" + ], + "gameName": "Shovel Knight Showdown" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a Spirit of the character", + "write": false + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "35c20000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_35c20000-036d0a02.png", + "name": "Specter Knight", + "release": { + "au": null, + "eu": "2019-12-10", + "jp": null, + "na": "2019-12-10" + }, + "tail": "036d0a02", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Fang", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "05110001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_05110001-01950502.png", + "name": "Fang", + "release": { + "au": "2016-06-18", + "eu": null, + "jp": "2016-03-24", + "na": "2016-06-10" + }, + "tail": "01950502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Isabelle", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "01810501", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01810501-03bf0502.png", + "name": "Isabelle - Sweater", + "release": { + "au": "2021-11-05", + "eu": "2021-11-05", + "jp": "2021-11-05", + "na": "2021-11-05" + }, + "tail": "03bf0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Snake", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "04970001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_04970001-007a0502.png", + "name": "Snake", + "release": { + "au": "2015-10-03", + "eu": "2015-10-02", + "jp": "2015-07-30", + "na": "2015-09-25" + }, + "tail": "007a0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Rio", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + }, + { + "Usage": "Unlock special furniture items and a poster based on the card's Sanrio character", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "0a1c0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_0a1c0001-03d30502.png", + "name": "Rio", + "release": { + "au": "2021-11-05", + "eu": "2021-11-05", + "jp": "2021-11-05", + "na": "2021-11-05" + }, + "tail": "03d30502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Pekoe", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "028b0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_028b0001-00e30502.png", + "name": "Pekoe", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-10-29", + "na": "2016-01-22" + }, + "tail": "00e30502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Celeste", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "01930001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01930001-01740502.png", + "name": "Celeste", + "release": { + "au": "2016-06-18", + "eu": null, + "jp": "2016-03-24", + "na": "2016-06-10" + }, + "tail": "01740502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Norma", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "02b70001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_02b70001-030f0502.png", + "name": "Norma", + "release": { + "au": "2016-11-10", + "eu": "2016-11-11", + "jp": "2016-11-03", + "na": "2016-12-02" + }, + "tail": "030f0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Butch", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "02eb0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_02eb0001-00de0502.png", + "name": "Butch", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-10-29", + "na": "2016-01-22" + }, + "tail": "00de0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Marshal", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "04ee0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_04ee0001-014b0502.png", + "name": "Marshal", + "release": { + "au": "2016-03-19", + "eu": "2016-03-18", + "jp": "2016-01-14", + "na": "2016-03-18" + }, + "tail": "014b0502", + "type": "Card" + }, + { + "amiiboSeries": "Splatoon", + "character": "Octoling", + "gameSeries": "Splatoon", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a Splatoon-themed racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock exclusive gear / Save favorite weapons, gear, and control settings / Take photos with the character or saved gear", + "write": true + } + ], + "gameID": [ + "01003BC0000A0000" + ], + "gameName": "Splatoon 2" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock exclusive gear / Save favorite weapons, gear, and control settings / Take photos with the character or saved gear", + "write": true + } + ], + "gameID": [ + "0100C2500FC20000" + ], + "gameName": "Splatoon 3" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a Spirit of the character", + "write": false + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "08050300", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_08050300-03900402.png", + "name": "Octoling Octopus", + "release": { + "au": "2018-11-11", + "eu": "2018-11-09", + "jp": "2018-11-09", + "na": "2018-11-09" + }, + "tail": "03900402", + "type": "Figure" + }, + { + "amiiboSeries": "8-bit Mario", + "character": "Mario", + "gameSeries": "Super Mario", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an amiibo-exclusive weapon for the character and their Rabbid counterpart", + "write": false + } + ], + "gameID": [ + "010067300059A000" + ], + "gameName": "Mario + Rabbids: Kingdom Battle" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01003DA010E8A000" + ], + "gameName": "Miitopia" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a particular power-up depending on the character", + "write": false + } + ], + "gameID": [ + "010028600EBDA000" + ], + "gameName": "Super Mario 3D World + Bowser's Fury" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a character-based costume", + "write": false + }, + { + "Usage": "Gain temporary invincibility", + "write": false + } + ], + "gameID": [ + "0100000000010000" + ], + "gameName": "Super Mario Odyssey" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock the character's shiny sticker", + "write": false + } + ], + "gameID": [ + "010036B0034E4000" + ], + "gameName": "Super Mario Party" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01006000040C2000" + ], + "gameName": "Yoshi's Crafted World" + } + ], + "head": "00000000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_00000000-02380602.png", + "name": "8-Bit Mario Classic Color", + "release": { + "au": "2015-09-12", + "eu": "2015-11-09", + "jp": "2015-09-10", + "na": "2015-09-11" + }, + "tail": "02380602", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Olivia", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "02600001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_02600001-00d20502.png", + "name": "Olivia", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-10-29", + "na": "2016-01-22" + }, + "tail": "00d20502", + "type": "Card" + }, + { + "amiiboSeries": "Super Smash Bros.", + "character": "Zelda", + "gameSeries": "The Legend of Zelda", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Receive specific crafting materials or a weapon for the character", + "write": false + } + ], + "gameID": [ + "01002B00111A2000" + ], + "gameName": "Hyrule Warriors: Age of Calamity" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a weapon rated 3 stars or higher", + "write": false + } + ], + "gameID": [ + "0100AE00096EA000" + ], + "gameName": "Hyrule Warriors: Definitive Edition" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a Legend of Zelda-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01003DA010E8A000" + ], + "gameName": "Miitopia" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a chest of loot, potentially containing gear inspired by the Legend of Zelda series", + "write": false + } + ], + "gameID": [ + "01000A10041EA000" + ], + "gameName": "The Elder Scrolls V: Skyrim" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive materials and a rare or exclusive item", + "write": false + } + ], + "gameID": [ + "01007EF00011E000" + ], + "gameName": "The Legend of Zelda: Breath of the Wild" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock one of five amiibo-exclusive Chamber Dungeon chambers", + "write": false + }, + { + "Usage": "Save your Chamber Dungeon to the amiibo to share with friends", + "write": true + } + ], + "gameID": [ + "01006BB00C6F0000" + ], + "gameName": "The Legend of Zelda: Link's Awakening" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an amiibo-exclusive character-specific paraglider fabric", + "write": false + }, + { + "Usage": "Receive materials and a weapon or rare item", + "write": false + } + ], + "gameID": [ + "0100F2C0115B6000" + ], + "gameName": "The Legend of Zelda: Tears of the Kingdom" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive the Blue Attire", + "write": false + }, + { + "Usage": "Receive random materials", + "write": false + } + ], + "gameID": [ + "01008CF01BAAC000" + ], + "gameName": "The Legend of Zelda: Echoes of Wisdom" + } + ], + "head": "01010000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01010000-000e0002.png", + "name": "Zelda", + "release": { + "au": "2014-12-12", + "eu": "2014-12-19", + "jp": "2014-12-06", + "na": "2014-12-14" + }, + "tail": "000e0002", + "type": "Figure" + }, + { + "amiiboSeries": "Metroid", + "character": "Samus", + "gameSeries": "Metroid", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock a costume based on the character (short-hair version)", + "write": false + } + ], + "gameID": [ + "01007960049A0000" + ], + "gameName": "Bayonetta 2" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a Metroid-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Restore a random amount of health once per day", + "write": false + } + ], + "gameID": [ + "010093801237C000" + ], + "gameName": "Metroid Dread" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01003DA010E8A000" + ], + "gameName": "Miitopia" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "05c00000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_05c00000-03651302.png", + "name": "Samus Aran", + "release": { + "au": "2017-09-16", + "eu": "2017-09-15", + "jp": "2017-09-15", + "na": "2017-09-15" + }, + "tail": "03651302", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Zell", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "02d80001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_02d80001-00e20502.png", + "name": "Zell", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-10-29", + "na": "2016-01-22" + }, + "tail": "00e20502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Clay", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "03830001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_03830001-009b0502.png", + "name": "Clay", + "release": { + "au": "2015-10-03", + "eu": "2015-10-02", + "jp": "2015-07-30", + "na": "2015-09-25" + }, + "tail": "009b0502", + "type": "Card" + }, + { + "amiiboSeries": "Mario Sports Superstars", + "character": "Boo", + "gameSeries": "Mario Sports Superstars", + "gamesSwitch": [], + "head": "09cb0301", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_09cb0301-02a20e02.png", + "name": "Boo - Tennis", + "release": { + "au": "2017-03-11", + "eu": "2017-03-10", + "jp": "2017-03-30", + "na": "2017-03-24" + }, + "tail": "02a20e02", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Goldie", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "02ea0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_02ea0001-01800502.png", + "name": "Goldie", + "release": { + "au": "2016-06-18", + "eu": null, + "jp": "2016-03-24", + "na": "2016-06-10" + }, + "tail": "01800502", + "type": "Card" + }, + { + "amiiboSeries": "Power Pros", + "character": "Yabe", + "gameSeries": "Power Pros", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Receive in-game items and power-ups / Save items to your card after playing with friends to bring them home", + "write": true + } + ], + "gameID": [ + "0100E9C00BF28000" + ], + "gameName": "Jikkyou Powerful Pro Baseball" + } + ], + "head": "38020001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_38020001-03951702.png", + "name": "Yabe", + "release": { + "au": null, + "eu": null, + "jp": "2019-06-27", + "na": null + }, + "tail": "03951702", + "type": "Card" + }, + { + "amiiboSeries": "Super Smash Bros.", + "character": "Ness", + "gameSeries": "Earthbound", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01003DA010E8A000" + ], + "gameName": "Miitopia" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "22800000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_22800000-002c0002.png", + "name": "Ness", + "release": { + "au": "2015-04-25", + "eu": "2015-04-24", + "jp": "2015-04-29", + "na": "2015-05-29" + }, + "tail": "002c0002", + "type": "Figure" + }, + { + "amiiboSeries": "Shovel Knight", + "character": "Plague Knight", + "gameSeries": "Shovel Knight", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock a fairy companion and player color palette matching the character", + "write": false + } + ], + "gameID": [ + "01008D100DE46000" + ], + "gameName": "Cyber Shadow" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-specific Shovel Knight remix immediately", + "write": false + } + ], + "gameID": [ + "0100830008426000" + ], + "gameName": "Just Shapes & Beats" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock character-specific challenge stages, a character-based fairy companion, and costumes for the character", + "write": false + } + ], + "gameID": [ + "010057D0021E8000" + ], + "gameName": "Shovel Knight" + }, + { + "amiiboUsage": [ + { + "Usage": "Summon a fairy friend", + "write": false + } + ], + "gameID": [ + "0100B62017E68000" + ], + "gameName": "Shovel Knight Dig" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume for the character", + "write": false + } + ], + "gameID": [ + "0100B380022AE000" + ], + "gameName": "Shovel Knight Showdown" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a Spirit of the character", + "write": false + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "35c10000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_35c10000-036c0a02.png", + "name": "Plague Knight", + "release": { + "au": null, + "eu": "2019-12-10", + "jp": null, + "na": "2019-12-10" + }, + "tail": "036c0a02", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Tad", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "03410001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_03410001-030e0502.png", + "name": "Tad", + "release": { + "au": "2016-11-10", + "eu": "2016-11-11", + "jp": "2016-11-03", + "na": "2016-12-02" + }, + "tail": "030e0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Penelope", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "041d0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_041d0001-018a0502.png", + "name": "Penelope", + "release": { + "au": "2016-06-18", + "eu": null, + "jp": "2016-03-24", + "na": "2016-06-10" + }, + "tail": "018a0502", + "type": "Card" + }, + { + "amiiboSeries": "Mario Sports Superstars", + "character": "Rosalina", + "gameSeries": "Mario Sports Superstars", + "gamesSwitch": [], + "head": "09cf0401", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_09cf0401-02b70e02.png", + "name": "Rosalina - Golf", + "release": { + "au": "2017-03-11", + "eu": "2017-03-10", + "jp": "2017-03-30", + "na": "2017-03-24" + }, + "tail": "02b70e02", + "type": "Card" + }, + { + "amiiboSeries": "Super Smash Bros.", + "character": "Palutena", + "gameSeries": "Kid Icarus", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "07420000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_07420000-001f0002.png", + "name": "Palutena", + "release": { + "au": "2015-07-04", + "eu": "2015-06-26", + "jp": "2015-06-11", + "na": "2015-07-24" + }, + "tail": "001f0002", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Curlos", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "04cd0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_04cd0001-01520502.png", + "name": "Curlos", + "release": { + "au": "2016-03-19", + "eu": "2016-03-18", + "jp": "2016-01-14", + "na": "2016-03-18" + }, + "tail": "01520502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Samson", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "04100001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_04100001-007f0502.png", + "name": "Samson", + "release": { + "au": "2015-10-03", + "eu": "2015-10-02", + "jp": "2015-07-30", + "na": "2015-09-25" + }, + "tail": "007f0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Saharah", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "01a60001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01a60001-00500502.png", + "name": "Saharah", + "release": { + "au": "2015-10-03", + "eu": "2015-10-02", + "jp": "2015-07-30", + "na": "2015-09-25" + }, + "tail": "00500502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Walt", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "03d90001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_03d90001-01a50502.png", + "name": "Walt", + "release": { + "au": "2016-06-18", + "eu": null, + "jp": "2016-03-24", + "na": "2016-06-10" + }, + "tail": "01a50502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Julia", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "043b0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_043b0001-03030502.png", + "name": "Julia", + "release": { + "au": "2016-11-10", + "eu": "2016-11-11", + "jp": "2016-11-03", + "na": "2016-12-02" + }, + "tail": "03030502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Hamlet", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "037e0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_037e0001-01560502.png", + "name": "Hamlet", + "release": { + "au": "2016-03-19", + "eu": "2016-03-18", + "jp": "2016-01-14", + "na": "2016-03-18" + }, + "tail": "01560502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Charlise", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "02200001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_02200001-00fd0502.png", + "name": "Charlise", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-10-29", + "na": "2016-01-22" + }, + "tail": "00fd0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Gonzo", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "03c00001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_03c00001-03100502.png", + "name": "Gonzo", + "release": { + "au": "2016-11-10", + "eu": "2016-11-11", + "jp": "2016-11-03", + "na": "2016-12-02" + }, + "tail": "03100502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Opal", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "03230001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_03230001-00760502.png", + "name": "Opal", + "release": { + "au": "2015-10-03", + "eu": "2015-10-02", + "jp": "2015-07-30", + "na": "2015-09-25" + }, + "tail": "00760502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Flo", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "046c0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_046c0001-008c0502.png", + "name": "Flo", + "release": { + "au": "2015-10-03", + "eu": "2015-10-02", + "jp": "2015-07-30", + "na": "2015-09-25" + }, + "tail": "008c0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Knox", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "02a40001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_02a40001-00720502.png", + "name": "Knox", + "release": { + "au": "2015-10-03", + "eu": "2015-10-02", + "jp": "2015-07-30", + "na": "2015-09-25" + }, + "tail": "00720502", + "type": "Card" + }, + { + "amiiboSeries": "Super Mario Bros.", + "character": "Donkey Kong", + "gameSeries": "Super Mario", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01003DA010E8A000" + ], + "gameName": "Miitopia" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a particular power-up depending on the character", + "write": false + } + ], + "gameID": [ + "010028600EBDA000" + ], + "gameName": "Super Mario 3D World + Bowser's Fury" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock the character's shiny sticker", + "write": false + } + ], + "gameID": [ + "010036B0034E4000" + ], + "gameName": "Super Mario Party" + }, + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "00080000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_00080000-02640102.png", + "name": "Donkey Kong", + "release": { + "au": "2016-10-08", + "eu": "2016-10-07", + "jp": "2016-10-20", + "na": "2016-11-04" + }, + "tail": "02640102", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Cherry", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "02fb0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_02fb0001-00900502.png", + "name": "Cherry", + "release": { + "au": "2015-10-03", + "eu": "2015-10-02", + "jp": "2015-07-30", + "na": "2015-09-25" + }, + "tail": "00900502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Rover", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a Spirit of the character", + "write": false + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "018d0000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_018d0000-024c0502.png", + "name": "Rover", + "release": { + "au": "2016-03-19", + "eu": "2016-03-18", + "jp": "2016-03-24", + "na": "2016-03-18" + }, + "tail": "024c0502", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Soleil", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "03820001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_03820001-016b0502.png", + "name": "Soleil", + "release": { + "au": "2016-03-19", + "eu": "2016-03-18", + "jp": "2016-01-14", + "na": "2016-03-18" + }, + "tail": "016b0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Barold", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "028d0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_028d0001-01bd0502.png", + "name": "Barold", + "release": { + "au": "2016-06-18", + "eu": null, + "jp": "2016-03-24", + "na": "2016-06-10" + }, + "tail": "01bd0502", + "type": "Card" + }, + { + "amiiboSeries": "Mario Sports Superstars", + "character": "Birdo", + "gameSeries": "Mario Sports Superstars", + "gamesSwitch": [], + "head": "09ce0201", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_09ce0201-02b00e02.png", + "name": "Birdo - Baseball", + "release": { + "au": "2017-03-11", + "eu": "2017-03-10", + "jp": "2017-03-30", + "na": "2017-03-24" + }, + "tail": "02b00e02", + "type": "Card" + }, + { + "amiiboSeries": "Super Mario Bros.", + "character": "Toad", + "gameSeries": "Super Mario", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Receive an invincibility mushroom", + "write": false + } + ], + "gameID": [ + "01009BF0072D4000" + ], + "gameName": "Captain Toad: Treasure Tracker" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01003DA010E8A000" + ], + "gameName": "Miitopia" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a particular power-up depending on the character", + "write": false + } + ], + "gameID": [ + "010028600EBDA000" + ], + "gameName": "Super Mario 3D World + Bowser's Fury" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive a Spirit of the character", + "write": false + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a character-based costume", + "write": false + } + ], + "gameID": [ + "01006000040C2000" + ], + "gameName": "Yoshi's Crafted World" + } + ], + "head": "000a0000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_000a0000-00380102.png", + "name": "Toad", + "release": { + "au": "2015-03-21", + "eu": "2015-03-20", + "jp": "2015-03-12", + "na": "2015-03-20" + }, + "tail": "00380102", + "type": "Figure" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Shari", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "04000001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_04000001-006f0502.png", + "name": "Shari", + "release": { + "au": "2015-10-03", + "eu": "2015-10-02", + "jp": "2015-07-30", + "na": "2015-09-25" + }, + "tail": "006f0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Joan", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "01a30001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01a30001-004a0502.png", + "name": "Joan", + "release": { + "au": "2015-10-03", + "eu": "2015-10-02", + "jp": "2015-07-30", + "na": "2015-09-25" + }, + "tail": "004a0502", + "type": "Card" + }, + { + "amiiboSeries": "Mario Sports Superstars", + "character": "Bowser Jr.", + "gameSeries": "Mario Sports Superstars", + "gamesSwitch": [], + "head": "09ca0101", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_09ca0101-029b0e02.png", + "name": "Bowser Jr. - Soccer", + "release": { + "au": "2017-03-11", + "eu": "2017-03-10", + "jp": "2017-03-30", + "na": "2017-03-24" + }, + "tail": "029b0e02", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Sheldon", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "04ed0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_04ed0001-00620502.png", + "name": "Sheldon", + "release": { + "au": "2015-10-03", + "eu": "2015-10-02", + "jp": "2015-07-30", + "na": "2015-09-25" + }, + "tail": "00620502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Chester", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "028c0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_028c0001-013e0502.png", + "name": "Chester", + "release": { + "au": "2016-03-19", + "eu": "2016-03-18", + "jp": "2016-01-14", + "na": "2016-03-18" + }, + "tail": "013e0502", + "type": "Card" + }, + { + "amiiboSeries": "Mario Sports Superstars", + "character": "Baby Luigi", + "gameSeries": "Mario Sports Superstars", + "gamesSwitch": [], + "head": "09cd0501", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_09cd0501-02ae0e02.png", + "name": "Baby Luigi - Horse Racing", + "release": { + "au": "2017-03-11", + "eu": "2017-03-10", + "jp": "2017-03-30", + "na": "2017-03-24" + }, + "tail": "02ae0e02", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Ike", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "021f0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_021f0001-03170502.png", + "name": "Ike", + "release": { + "au": "2016-11-10", + "eu": "2016-11-11", + "jp": "2016-11-03", + "na": "2016-12-02" + }, + "tail": "03170502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Leilani", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "01970001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01970001-01770502.png", + "name": "Leilani", + "release": { + "au": "2016-06-18", + "eu": null, + "jp": "2016-03-24", + "na": "2016-06-10" + }, + "tail": "01770502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Skye", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "05140001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_05140001-01530502.png", + "name": "Skye", + "release": { + "au": "2016-03-19", + "eu": "2016-03-18", + "jp": "2016-01-14", + "na": "2016-03-18" + }, + "tail": "01530502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "C.J.", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "0a020001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_0a020001-03b30502.png", + "name": "C.J.", + "release": { + "au": "2021-11-05", + "eu": "2021-11-05", + "jp": "2021-11-05", + "na": "2021-11-05" + }, + "tail": "03b30502", + "type": "Card" + }, + { + "amiiboSeries": "Mario Sports Superstars", + "character": "Metal Mario", + "gameSeries": "Mario Sports Superstars", + "gamesSwitch": [], + "head": "09d00201", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_09d00201-02ba0e02.png", + "name": "Metal Mario - Baseball", + "release": { + "au": "2017-03-11", + "eu": "2017-03-10", + "jp": "2017-03-30", + "na": "2017-03-24" + }, + "tail": "02ba0e02", + "type": "Card" + }, + { + "amiiboSeries": "Monster Hunter Rise", + "character": "Palico", + "gameSeries": "Monster Hunter", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock special layered armor / Enter daily lottery for a variety of useful items", + "write": false + } + ], + "gameID": [ + "0100B04011742000" + ], + "gameName": "Monster Hunter Rise" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock the Hunter Sticker Set / Have Tsukino read your fortune and receive a random item", + "write": false + } + ], + "gameID": [ + "0100E21011446000" + ], + "gameName": "Monster Hunter Stories 2: Wings of Ruin" + } + ], + "head": "35090000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_35090000-04101802.png", + "name": "Palico", + "release": { + "au": "2021-03-26", + "eu": "2021-03-26", + "jp": "2021-03-26", + "na": "2021-03-26" + }, + "tail": "04101802", + "type": "Figure" + }, + { + "amiiboSeries": "Mario Sports Superstars", + "character": "Mario", + "gameSeries": "Mario Sports Superstars", + "gamesSwitch": [], + "head": "09c00201", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_09c00201-026a0e02.png", + "name": "Mario - Baseball", + "release": { + "au": "2017-03-11", + "eu": "2017-03-10", + "jp": "2017-03-30", + "na": "2017-03-24" + }, + "tail": "026a0e02", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Kicks", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "01940001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01940001-03b60502.png", + "name": "Kicks", + "release": { + "au": "2021-11-05", + "eu": "2021-11-05", + "jp": "2021-11-05", + "na": "2021-11-05" + }, + "tail": "03b60502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Stella", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "04c80001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_04c80001-02ed0502.png", + "name": "Stella", + "release": { + "au": "2016-11-10", + "eu": "2016-11-11", + "jp": "2016-11-03", + "na": "2016-12-02" + }, + "tail": "02ed0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Freya", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "05100001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_05100001-01070502.png", + "name": "Freya", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-10-29", + "na": "2016-01-22" + }, + "tail": "01070502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Judy", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "0a0e0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_0a0e0001-03c50502.png", + "name": "Judy", + "release": { + "au": "2021-11-05", + "eu": "2021-11-05", + "jp": "2021-11-05", + "na": "2021-11-05" + }, + "tail": "03c50502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Peewee", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "036a0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_036a0001-019d0502.png", + "name": "Peewee", + "release": { + "au": "2016-06-18", + "eu": null, + "jp": "2016-03-24", + "na": "2016-06-10" + }, + "tail": "019d0502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Tom Nook", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "01830301", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_01830301-03be0502.png", + "name": "Tom Nook - Coat", + "release": { + "au": "2021-11-05", + "eu": "2021-11-05", + "jp": "2021-11-05", + "na": "2021-11-05" + }, + "tail": "03be0502", + "type": "Card" + }, + { + "amiiboSeries": "Mario Sports Superstars", + "character": "Diddy Kong", + "gameSeries": "Mario Sports Superstars", + "gamesSwitch": [], + "head": "09c80301", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_09c80301-02930e02.png", + "name": "Diddy Kong - Tennis", + "release": { + "au": "2017-03-11", + "eu": "2017-03-10", + "jp": "2017-03-30", + "na": "2017-03-24" + }, + "tail": "02930e02", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Don Resetti", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "018f0001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_018f0001-00b30502.png", + "name": "Don Resetti", + "release": { + "au": "2015-11-21", + "eu": "2015-11-20", + "jp": "2015-10-29", + "na": "2016-01-22" + }, + "tail": "00b30502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Merengue", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "04b90001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_04b90001-01600502.png", + "name": "Merengue", + "release": { + "au": "2016-03-19", + "eu": "2016-03-18", + "jp": "2016-01-14", + "na": "2016-03-18" + }, + "tail": "01600502", + "type": "Card" + }, + { + "amiiboSeries": "Animal Crossing", + "character": "Muffy", + "gameSeries": "Animal Crossing", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Invite the character to your campsite and, eventually, to move to your island / Invite the character for photo shoots at Photopia / Unlock a special poster of the character / Invite the character to the Roost for a cup of coffee / Invite the character to Happy Home Paradise to design their vacation home", + "write": false + } + ], + "gameID": [ + "01006F8002326000" + ], + "gameName": "Animal Crossing: New Horizons" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock a special costume", + "write": false + } + ], + "gameID": [ + "01007EF00399C000" + ], + "gameName": "Conga Master Party!" + }, + { + "amiiboUsage": [ + { + "Usage": "Unlock an Animal Crossing-themed Mii racing suit", + "write": false + } + ], + "gameID": [ + "0100152000022000" + ], + "gameName": "Mario Kart 8 Deluxe" + } + ], + "head": "04d10001", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_04d10001-009e0502.png", + "name": "Muffy", + "release": { + "au": "2015-10-03", + "eu": "2015-10-02", + "jp": "2015-07-30", + "na": "2015-09-25" + }, + "tail": "009e0502", + "type": "Card" + }, + { + "amiiboSeries": "Xenoblade Chronicles 3", + "character": "Noah", + "gameSeries": "Xenoblade Chronicles", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock the \u201cN\u2019s Consul Suit\u201d outfit for Noah as well as Noah, Lanz, and Eunie costumes without jackets", + "write": false + } + ], + "gameID": [ + "010074F013262000" + ], + "gameName": "Xenoblade Chronicles 3" + } + ], + "head": "22430000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_22430000-043d1b02.png", + "name": "Noah", + "release": { + "au": "2024-01-19", + "eu": "2024-01-19", + "jp": "2024-01-19", + "na": "2024-01-19" + }, + "tail": "043d1b02", + "type": "Figure" + }, + { + "amiiboSeries": "Xenoblade Chronicles 3", + "character": "Mio", + "gameSeries": "Xenoblade Chronicles", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Unlock the \u201cM\u2019s Consul Suit\u201d outfit for Mio as well as Mio, Sena, and Taion costumes without jackets", + "write": false + } + ], + "gameID": [ + "010074F013262000" + ], + "gameName": "Xenoblade Chronicles 3" + } + ], + "head": "22440000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_22440000-043e1b02.png", + "name": "Mio", + "release": { + "au": "2024-01-19", + "eu": "2024-01-19", + "jp": "2024-01-19", + "na": "2024-01-19" + }, + "tail": "043e1b02", + "type": "Figure" + }, + { + "amiiboSeries": "Super Smash Bros.", + "character": "Sora", + "gameSeries": "Kingdom Hearts", + "gamesSwitch": [ + { + "amiiboUsage": [ + { + "Usage": "Battle and train up a computer-controlled Figure Player of the character", + "write": true + } + ], + "gameID": [ + "01006A800016E000" + ], + "gameName": "Super Smash Bros. Ultimate" + } + ], + "head": "3f000000", + "image": "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/images/icon_3f000000-042e0002.png", + "name": "Sora", + "release": { + "au": "2024-02-16", + "eu": "2024-02-16", + "jp": "2024-02-16", + "na": "2024-02-16" + }, + "tail": "042e0002", + "type": "Figure" + } + ], + "lastUpdated": "2024-11-17T15:28:47.035619" +} diff --git a/assets/amiibo/images/icon_00000000-00000002.png b/assets/amiibo/images/icon_00000000-00000002.png new file mode 100644 index 000000000..7e238140d Binary files /dev/null and b/assets/amiibo/images/icon_00000000-00000002.png differ diff --git a/assets/amiibo/images/icon_00000000-00340102.png b/assets/amiibo/images/icon_00000000-00340102.png new file mode 100644 index 000000000..0566243de Binary files /dev/null and b/assets/amiibo/images/icon_00000000-00340102.png differ diff --git a/assets/amiibo/images/icon_00000000-003c0102.png b/assets/amiibo/images/icon_00000000-003c0102.png new file mode 100644 index 000000000..0c93ab0dd Binary files /dev/null and b/assets/amiibo/images/icon_00000000-003c0102.png differ diff --git a/assets/amiibo/images/icon_00000000-003d0102.png b/assets/amiibo/images/icon_00000000-003d0102.png new file mode 100644 index 000000000..ceb3232b1 Binary files /dev/null and b/assets/amiibo/images/icon_00000000-003d0102.png differ diff --git a/assets/amiibo/images/icon_00000000-02380602.png b/assets/amiibo/images/icon_00000000-02380602.png new file mode 100644 index 000000000..e17bfa297 Binary files /dev/null and b/assets/amiibo/images/icon_00000000-02380602.png differ diff --git a/assets/amiibo/images/icon_00000000-02390602.png b/assets/amiibo/images/icon_00000000-02390602.png new file mode 100644 index 000000000..71293f2d6 Binary files /dev/null and b/assets/amiibo/images/icon_00000000-02390602.png differ diff --git a/assets/amiibo/images/icon_00000000-03710102.png b/assets/amiibo/images/icon_00000000-03710102.png new file mode 100644 index 000000000..b42ada43b Binary files /dev/null and b/assets/amiibo/images/icon_00000000-03710102.png differ diff --git a/assets/amiibo/images/icon_00000003-039bff02.png b/assets/amiibo/images/icon_00000003-039bff02.png new file mode 100644 index 000000000..c7e12e520 Binary files /dev/null and b/assets/amiibo/images/icon_00000003-039bff02.png differ diff --git a/assets/amiibo/images/icon_00000003-0430ff02.png b/assets/amiibo/images/icon_00000003-0430ff02.png new file mode 100644 index 000000000..188b8b1ea Binary files /dev/null and b/assets/amiibo/images/icon_00000003-0430ff02.png differ diff --git a/assets/amiibo/images/icon_00000100-00190002.png b/assets/amiibo/images/icon_00000100-00190002.png new file mode 100644 index 000000000..0a61be06e Binary files /dev/null and b/assets/amiibo/images/icon_00000100-00190002.png differ diff --git a/assets/amiibo/images/icon_00000300-03a60102.png b/assets/amiibo/images/icon_00000300-03a60102.png new file mode 100644 index 000000000..e76e47ac2 Binary files /dev/null and b/assets/amiibo/images/icon_00000300-03a60102.png differ diff --git a/assets/amiibo/images/icon_00010000-000c0002.png b/assets/amiibo/images/icon_00010000-000c0002.png new file mode 100644 index 000000000..4c34aae16 Binary files /dev/null and b/assets/amiibo/images/icon_00010000-000c0002.png differ diff --git a/assets/amiibo/images/icon_00010000-00350102.png b/assets/amiibo/images/icon_00010000-00350102.png new file mode 100644 index 000000000..d51680212 Binary files /dev/null and b/assets/amiibo/images/icon_00010000-00350102.png differ diff --git a/assets/amiibo/images/icon_00010003-039cff02.png b/assets/amiibo/images/icon_00010003-039cff02.png new file mode 100644 index 000000000..5c91d4319 Binary files /dev/null and b/assets/amiibo/images/icon_00010003-039cff02.png differ diff --git a/assets/amiibo/images/icon_00020000-00010002.png b/assets/amiibo/images/icon_00020000-00010002.png new file mode 100644 index 000000000..b90bbbd21 Binary files /dev/null and b/assets/amiibo/images/icon_00020000-00010002.png differ diff --git a/assets/amiibo/images/icon_00020000-00360102.png b/assets/amiibo/images/icon_00020000-00360102.png new file mode 100644 index 000000000..69e45a3ab Binary files /dev/null and b/assets/amiibo/images/icon_00020000-00360102.png differ diff --git a/assets/amiibo/images/icon_00020000-03720102.png b/assets/amiibo/images/icon_00020000-03720102.png new file mode 100644 index 000000000..0808fc2ab Binary files /dev/null and b/assets/amiibo/images/icon_00020000-03720102.png differ diff --git a/assets/amiibo/images/icon_00020003-039dff02.png b/assets/amiibo/images/icon_00020003-039dff02.png new file mode 100644 index 000000000..2e62af9e4 Binary files /dev/null and b/assets/amiibo/images/icon_00020003-039dff02.png differ diff --git a/assets/amiibo/images/icon_00020100-03a70102.png b/assets/amiibo/images/icon_00020100-03a70102.png new file mode 100644 index 000000000..dac040cac Binary files /dev/null and b/assets/amiibo/images/icon_00020100-03a70102.png differ diff --git a/assets/amiibo/images/icon_00030000-00020002.png b/assets/amiibo/images/icon_00030000-00020002.png new file mode 100644 index 000000000..1b3b1f81a Binary files /dev/null and b/assets/amiibo/images/icon_00030000-00020002.png differ diff --git a/assets/amiibo/images/icon_00030000-00370102.png b/assets/amiibo/images/icon_00030000-00370102.png new file mode 100644 index 000000000..337dc3db2 Binary files /dev/null and b/assets/amiibo/images/icon_00030000-00370102.png differ diff --git a/assets/amiibo/images/icon_00030003-039fff02.png b/assets/amiibo/images/icon_00030003-039fff02.png new file mode 100644 index 000000000..ec3ce7a9c Binary files /dev/null and b/assets/amiibo/images/icon_00030003-039fff02.png differ diff --git a/assets/amiibo/images/icon_00030102-00410302.png b/assets/amiibo/images/icon_00030102-00410302.png new file mode 100644 index 000000000..518e40610 Binary files /dev/null and b/assets/amiibo/images/icon_00030102-00410302.png differ diff --git a/assets/amiibo/images/icon_00030102-00420302.png b/assets/amiibo/images/icon_00030102-00420302.png new file mode 100644 index 000000000..51945ca20 Binary files /dev/null and b/assets/amiibo/images/icon_00030102-00420302.png differ diff --git a/assets/amiibo/images/icon_00030102-00430302.png b/assets/amiibo/images/icon_00030102-00430302.png new file mode 100644 index 000000000..b426fbaf5 Binary files /dev/null and b/assets/amiibo/images/icon_00030102-00430302.png differ diff --git a/assets/amiibo/images/icon_00030102-023e0302.png b/assets/amiibo/images/icon_00030102-023e0302.png new file mode 100644 index 000000000..3f1396ad9 Binary files /dev/null and b/assets/amiibo/images/icon_00030102-023e0302.png differ diff --git a/assets/amiibo/images/icon_00040000-02620102.png b/assets/amiibo/images/icon_00040000-02620102.png new file mode 100644 index 000000000..29a96ae90 Binary files /dev/null and b/assets/amiibo/images/icon_00040000-02620102.png differ diff --git a/assets/amiibo/images/icon_00040100-00130002.png b/assets/amiibo/images/icon_00040100-00130002.png new file mode 100644 index 000000000..3399daf93 Binary files /dev/null and b/assets/amiibo/images/icon_00040100-00130002.png differ diff --git a/assets/amiibo/images/icon_00050000-00140002.png b/assets/amiibo/images/icon_00050000-00140002.png new file mode 100644 index 000000000..76f480d03 Binary files /dev/null and b/assets/amiibo/images/icon_00050000-00140002.png differ diff --git a/assets/amiibo/images/icon_00050000-00390102.png b/assets/amiibo/images/icon_00050000-00390102.png new file mode 100644 index 000000000..23a574d6c Binary files /dev/null and b/assets/amiibo/images/icon_00050000-00390102.png differ diff --git a/assets/amiibo/images/icon_00050000-03730102.png b/assets/amiibo/images/icon_00050000-03730102.png new file mode 100644 index 000000000..59ef48e1e Binary files /dev/null and b/assets/amiibo/images/icon_00050000-03730102.png differ diff --git a/assets/amiibo/images/icon_0005ff00-023a0702.png b/assets/amiibo/images/icon_0005ff00-023a0702.png new file mode 100644 index 000000000..ba99e5a18 Binary files /dev/null and b/assets/amiibo/images/icon_0005ff00-023a0702.png differ diff --git a/assets/amiibo/images/icon_00060000-00150002.png b/assets/amiibo/images/icon_00060000-00150002.png new file mode 100644 index 000000000..307d0981d Binary files /dev/null and b/assets/amiibo/images/icon_00060000-00150002.png differ diff --git a/assets/amiibo/images/icon_00070000-001a0002.png b/assets/amiibo/images/icon_00070000-001a0002.png new file mode 100644 index 000000000..97e3c66fa Binary files /dev/null and b/assets/amiibo/images/icon_00070000-001a0002.png differ diff --git a/assets/amiibo/images/icon_00070000-02630102.png b/assets/amiibo/images/icon_00070000-02630102.png new file mode 100644 index 000000000..aed740985 Binary files /dev/null and b/assets/amiibo/images/icon_00070000-02630102.png differ diff --git a/assets/amiibo/images/icon_00080000-00030002.png b/assets/amiibo/images/icon_00080000-00030002.png new file mode 100644 index 000000000..5b43fc4f4 Binary files /dev/null and b/assets/amiibo/images/icon_00080000-00030002.png differ diff --git a/assets/amiibo/images/icon_00080000-02640102.png b/assets/amiibo/images/icon_00080000-02640102.png new file mode 100644 index 000000000..f4a6d291a Binary files /dev/null and b/assets/amiibo/images/icon_00080000-02640102.png differ diff --git a/assets/amiibo/images/icon_0008ff00-023b0702.png b/assets/amiibo/images/icon_0008ff00-023b0702.png new file mode 100644 index 000000000..f4869fee9 Binary files /dev/null and b/assets/amiibo/images/icon_0008ff00-023b0702.png differ diff --git a/assets/amiibo/images/icon_00090000-000d0002.png b/assets/amiibo/images/icon_00090000-000d0002.png new file mode 100644 index 000000000..43f563daf Binary files /dev/null and b/assets/amiibo/images/icon_00090000-000d0002.png differ diff --git a/assets/amiibo/images/icon_00090000-02650102.png b/assets/amiibo/images/icon_00090000-02650102.png new file mode 100644 index 000000000..6219c458d Binary files /dev/null and b/assets/amiibo/images/icon_00090000-02650102.png differ diff --git a/assets/amiibo/images/icon_000a0000-00380102.png b/assets/amiibo/images/icon_000a0000-00380102.png new file mode 100644 index 000000000..0da5fbdfd Binary files /dev/null and b/assets/amiibo/images/icon_000a0000-00380102.png differ diff --git a/assets/amiibo/images/icon_000a0003-03a0ff02.png b/assets/amiibo/images/icon_000a0003-03a0ff02.png new file mode 100644 index 000000000..c638956e7 Binary files /dev/null and b/assets/amiibo/images/icon_000a0003-03a0ff02.png differ diff --git a/assets/amiibo/images/icon_00130000-02660102.png b/assets/amiibo/images/icon_00130000-02660102.png new file mode 100644 index 000000000..502850e8f Binary files /dev/null and b/assets/amiibo/images/icon_00130000-02660102.png differ diff --git a/assets/amiibo/images/icon_00130000-037a0002.png b/assets/amiibo/images/icon_00130000-037a0002.png new file mode 100644 index 000000000..cc5d09782 Binary files /dev/null and b/assets/amiibo/images/icon_00130000-037a0002.png differ diff --git a/assets/amiibo/images/icon_00130003-039eff02.png b/assets/amiibo/images/icon_00130003-039eff02.png new file mode 100644 index 000000000..215ea5bcc Binary files /dev/null and b/assets/amiibo/images/icon_00130003-039eff02.png differ diff --git a/assets/amiibo/images/icon_00140000-02670102.png b/assets/amiibo/images/icon_00140000-02670102.png new file mode 100644 index 000000000..e04bba004 Binary files /dev/null and b/assets/amiibo/images/icon_00140000-02670102.png differ diff --git a/assets/amiibo/images/icon_00150000-03670102.png b/assets/amiibo/images/icon_00150000-03670102.png new file mode 100644 index 000000000..76524c612 Binary files /dev/null and b/assets/amiibo/images/icon_00150000-03670102.png differ diff --git a/assets/amiibo/images/icon_00170000-02680102.png b/assets/amiibo/images/icon_00170000-02680102.png new file mode 100644 index 000000000..0b93845c4 Binary files /dev/null and b/assets/amiibo/images/icon_00170000-02680102.png differ diff --git a/assets/amiibo/images/icon_00230000-03680102.png b/assets/amiibo/images/icon_00230000-03680102.png new file mode 100644 index 000000000..d3e966cf4 Binary files /dev/null and b/assets/amiibo/images/icon_00230000-03680102.png differ diff --git a/assets/amiibo/images/icon_00240000-038d0002.png b/assets/amiibo/images/icon_00240000-038d0002.png new file mode 100644 index 000000000..1b06313b5 Binary files /dev/null and b/assets/amiibo/images/icon_00240000-038d0002.png differ diff --git a/assets/amiibo/images/icon_00800102-035d0302.png b/assets/amiibo/images/icon_00800102-035d0302.png new file mode 100644 index 000000000..1b0bb1f9f Binary files /dev/null and b/assets/amiibo/images/icon_00800102-035d0302.png differ diff --git a/assets/amiibo/images/icon_00c00000-037b0002.png b/assets/amiibo/images/icon_00c00000-037b0002.png new file mode 100644 index 000000000..2ecb86bb9 Binary files /dev/null and b/assets/amiibo/images/icon_00c00000-037b0002.png differ diff --git a/assets/amiibo/images/icon_01000000-00040002.png b/assets/amiibo/images/icon_01000000-00040002.png new file mode 100644 index 000000000..89db88178 Binary files /dev/null and b/assets/amiibo/images/icon_01000000-00040002.png differ diff --git a/assets/amiibo/images/icon_01000000-034b0902.png b/assets/amiibo/images/icon_01000000-034b0902.png new file mode 100644 index 000000000..d76fc5f51 Binary files /dev/null and b/assets/amiibo/images/icon_01000000-034b0902.png differ diff --git a/assets/amiibo/images/icon_01000000-034c0902.png b/assets/amiibo/images/icon_01000000-034c0902.png new file mode 100644 index 000000000..d0fefccda Binary files /dev/null and b/assets/amiibo/images/icon_01000000-034c0902.png differ diff --git a/assets/amiibo/images/icon_01000000-034d0902.png b/assets/amiibo/images/icon_01000000-034d0902.png new file mode 100644 index 000000000..5e75dcfa4 Binary files /dev/null and b/assets/amiibo/images/icon_01000000-034d0902.png differ diff --git a/assets/amiibo/images/icon_01000000-034e0902.png b/assets/amiibo/images/icon_01000000-034e0902.png new file mode 100644 index 000000000..aef55553f Binary files /dev/null and b/assets/amiibo/images/icon_01000000-034e0902.png differ diff --git a/assets/amiibo/images/icon_01000000-034f0902.png b/assets/amiibo/images/icon_01000000-034f0902.png new file mode 100644 index 000000000..11a59e917 Binary files /dev/null and b/assets/amiibo/images/icon_01000000-034f0902.png differ diff --git a/assets/amiibo/images/icon_01000000-03530902.png b/assets/amiibo/images/icon_01000000-03530902.png new file mode 100644 index 000000000..46d157378 Binary files /dev/null and b/assets/amiibo/images/icon_01000000-03530902.png differ diff --git a/assets/amiibo/images/icon_01000000-03540902.png b/assets/amiibo/images/icon_01000000-03540902.png new file mode 100644 index 000000000..23c61e036 Binary files /dev/null and b/assets/amiibo/images/icon_01000000-03540902.png differ diff --git a/assets/amiibo/images/icon_01000000-037c0002.png b/assets/amiibo/images/icon_01000000-037c0002.png new file mode 100644 index 000000000..95dec8361 Binary files /dev/null and b/assets/amiibo/images/icon_01000000-037c0002.png differ diff --git a/assets/amiibo/images/icon_01000000-03990902.png b/assets/amiibo/images/icon_01000000-03990902.png new file mode 100644 index 000000000..37d7a0db0 Binary files /dev/null and b/assets/amiibo/images/icon_01000000-03990902.png differ diff --git a/assets/amiibo/images/icon_01000000-04180902.png b/assets/amiibo/images/icon_01000000-04180902.png new file mode 100644 index 000000000..939071f54 Binary files /dev/null and b/assets/amiibo/images/icon_01000000-04180902.png differ diff --git a/assets/amiibo/images/icon_01000100-00160002.png b/assets/amiibo/images/icon_01000100-00160002.png new file mode 100644 index 000000000..2d8738b53 Binary files /dev/null and b/assets/amiibo/images/icon_01000100-00160002.png differ diff --git a/assets/amiibo/images/icon_01000100-03500902.png b/assets/amiibo/images/icon_01000100-03500902.png new file mode 100644 index 000000000..bbb04cd21 Binary files /dev/null and b/assets/amiibo/images/icon_01000100-03500902.png differ diff --git a/assets/amiibo/images/icon_01010000-000e0002.png b/assets/amiibo/images/icon_01010000-000e0002.png new file mode 100644 index 000000000..b399f67a1 Binary files /dev/null and b/assets/amiibo/images/icon_01010000-000e0002.png differ diff --git a/assets/amiibo/images/icon_01010000-03520902.png b/assets/amiibo/images/icon_01010000-03520902.png new file mode 100644 index 000000000..c2259a606 Binary files /dev/null and b/assets/amiibo/images/icon_01010000-03520902.png differ diff --git a/assets/amiibo/images/icon_01010000-03560902.png b/assets/amiibo/images/icon_01010000-03560902.png new file mode 100644 index 000000000..8d336a55d Binary files /dev/null and b/assets/amiibo/images/icon_01010000-03560902.png differ diff --git a/assets/amiibo/images/icon_01010000-04190902.png b/assets/amiibo/images/icon_01010000-04190902.png new file mode 100644 index 000000000..155656e7c Binary files /dev/null and b/assets/amiibo/images/icon_01010000-04190902.png differ diff --git a/assets/amiibo/images/icon_01010100-00170002.png b/assets/amiibo/images/icon_01010100-00170002.png new file mode 100644 index 000000000..418b500f7 Binary files /dev/null and b/assets/amiibo/images/icon_01010100-00170002.png differ diff --git a/assets/amiibo/images/icon_01010300-04140902.png b/assets/amiibo/images/icon_01010300-04140902.png new file mode 100644 index 000000000..c1db27252 Binary files /dev/null and b/assets/amiibo/images/icon_01010300-04140902.png differ diff --git a/assets/amiibo/images/icon_01020100-001b0002.png b/assets/amiibo/images/icon_01020100-001b0002.png new file mode 100644 index 000000000..8a5b3a0a2 Binary files /dev/null and b/assets/amiibo/images/icon_01020100-001b0002.png differ diff --git a/assets/amiibo/images/icon_01020100-041a0902.png b/assets/amiibo/images/icon_01020100-041a0902.png new file mode 100644 index 000000000..f1729e327 Binary files /dev/null and b/assets/amiibo/images/icon_01020100-041a0902.png differ diff --git a/assets/amiibo/images/icon_01030000-024f0902.png b/assets/amiibo/images/icon_01030000-024f0902.png new file mode 100644 index 000000000..e044af322 Binary files /dev/null and b/assets/amiibo/images/icon_01030000-024f0902.png differ diff --git a/assets/amiibo/images/icon_01050000-03580902.png b/assets/amiibo/images/icon_01050000-03580902.png new file mode 100644 index 000000000..4680bbbd6 Binary files /dev/null and b/assets/amiibo/images/icon_01050000-03580902.png differ diff --git a/assets/amiibo/images/icon_01060000-03590902.png b/assets/amiibo/images/icon_01060000-03590902.png new file mode 100644 index 000000000..18c1b0f11 Binary files /dev/null and b/assets/amiibo/images/icon_01060000-03590902.png differ diff --git a/assets/amiibo/images/icon_01070000-035a0902.png b/assets/amiibo/images/icon_01070000-035a0902.png new file mode 100644 index 000000000..19971ab00 Binary files /dev/null and b/assets/amiibo/images/icon_01070000-035a0902.png differ diff --git a/assets/amiibo/images/icon_01080000-035b0902.png b/assets/amiibo/images/icon_01080000-035b0902.png new file mode 100644 index 000000000..215829c7b Binary files /dev/null and b/assets/amiibo/images/icon_01080000-035b0902.png differ diff --git a/assets/amiibo/images/icon_01400000-03550902.png b/assets/amiibo/images/icon_01400000-03550902.png new file mode 100644 index 000000000..8d669c7f8 Binary files /dev/null and b/assets/amiibo/images/icon_01400000-03550902.png differ diff --git a/assets/amiibo/images/icon_01410000-035c0902.png b/assets/amiibo/images/icon_01410000-035c0902.png new file mode 100644 index 000000000..d09e84278 Binary files /dev/null and b/assets/amiibo/images/icon_01410000-035c0902.png differ diff --git a/assets/amiibo/images/icon_01800000-00080002.png b/assets/amiibo/images/icon_01800000-00080002.png new file mode 100644 index 000000000..3c229cb35 Binary files /dev/null and b/assets/amiibo/images/icon_01800000-00080002.png differ diff --git a/assets/amiibo/images/icon_01810000-024b0502.png b/assets/amiibo/images/icon_01810000-024b0502.png new file mode 100644 index 000000000..9cdfdf5ac Binary files /dev/null and b/assets/amiibo/images/icon_01810000-024b0502.png differ diff --git a/assets/amiibo/images/icon_01810000-037d0002.png b/assets/amiibo/images/icon_01810000-037d0002.png new file mode 100644 index 000000000..c46642153 Binary files /dev/null and b/assets/amiibo/images/icon_01810000-037d0002.png differ diff --git a/assets/amiibo/images/icon_01810001-00440502.png b/assets/amiibo/images/icon_01810001-00440502.png new file mode 100644 index 000000000..0d4439f02 Binary files /dev/null and b/assets/amiibo/images/icon_01810001-00440502.png differ diff --git a/assets/amiibo/images/icon_01810001-01d40502.png b/assets/amiibo/images/icon_01810001-01d40502.png new file mode 100644 index 000000000..fb6e5c8ad Binary files /dev/null and b/assets/amiibo/images/icon_01810001-01d40502.png differ diff --git a/assets/amiibo/images/icon_01810100-023f0502.png b/assets/amiibo/images/icon_01810100-023f0502.png new file mode 100644 index 000000000..26a5c0c11 Binary files /dev/null and b/assets/amiibo/images/icon_01810100-023f0502.png differ diff --git a/assets/amiibo/images/icon_01810101-00b40502.png b/assets/amiibo/images/icon_01810101-00b40502.png new file mode 100644 index 000000000..d5a901289 Binary files /dev/null and b/assets/amiibo/images/icon_01810101-00b40502.png differ diff --git a/assets/amiibo/images/icon_01810201-011a0502.png b/assets/amiibo/images/icon_01810201-011a0502.png new file mode 100644 index 000000000..22dc77e1f Binary files /dev/null and b/assets/amiibo/images/icon_01810201-011a0502.png differ diff --git a/assets/amiibo/images/icon_01810301-01700502.png b/assets/amiibo/images/icon_01810301-01700502.png new file mode 100644 index 000000000..bdd969b3a Binary files /dev/null and b/assets/amiibo/images/icon_01810301-01700502.png differ diff --git a/assets/amiibo/images/icon_01810401-03aa0502.png b/assets/amiibo/images/icon_01810401-03aa0502.png new file mode 100644 index 000000000..44e646f67 Binary files /dev/null and b/assets/amiibo/images/icon_01810401-03aa0502.png differ diff --git a/assets/amiibo/images/icon_01810501-03bf0502.png b/assets/amiibo/images/icon_01810501-03bf0502.png new file mode 100644 index 000000000..97c89b5fb Binary files /dev/null and b/assets/amiibo/images/icon_01810501-03bf0502.png differ diff --git a/assets/amiibo/images/icon_01820000-02400502.png b/assets/amiibo/images/icon_01820000-02400502.png new file mode 100644 index 000000000..fae58f523 Binary files /dev/null and b/assets/amiibo/images/icon_01820000-02400502.png differ diff --git a/assets/amiibo/images/icon_01820001-00a80502.png b/assets/amiibo/images/icon_01820001-00a80502.png new file mode 100644 index 000000000..aedee2c99 Binary files /dev/null and b/assets/amiibo/images/icon_01820001-00a80502.png differ diff --git a/assets/amiibo/images/icon_01820001-01d80502.png b/assets/amiibo/images/icon_01820001-01d80502.png new file mode 100644 index 000000000..f31ded694 Binary files /dev/null and b/assets/amiibo/images/icon_01820001-01d80502.png differ diff --git a/assets/amiibo/images/icon_01820001-03b20502.png b/assets/amiibo/images/icon_01820001-03b20502.png new file mode 100644 index 000000000..974b4415d Binary files /dev/null and b/assets/amiibo/images/icon_01820001-03b20502.png differ diff --git a/assets/amiibo/images/icon_01820101-00460502.png b/assets/amiibo/images/icon_01820101-00460502.png new file mode 100644 index 000000000..4eee12a4d Binary files /dev/null and b/assets/amiibo/images/icon_01820101-00460502.png differ diff --git a/assets/amiibo/images/icon_01830000-02420502.png b/assets/amiibo/images/icon_01830000-02420502.png new file mode 100644 index 000000000..df6a8df77 Binary files /dev/null and b/assets/amiibo/images/icon_01830000-02420502.png differ diff --git a/assets/amiibo/images/icon_01830001-00450502.png b/assets/amiibo/images/icon_01830001-00450502.png new file mode 100644 index 000000000..e7a8edd06 Binary files /dev/null and b/assets/amiibo/images/icon_01830001-00450502.png differ diff --git a/assets/amiibo/images/icon_01830101-010e0502.png b/assets/amiibo/images/icon_01830101-010e0502.png new file mode 100644 index 000000000..c397bc1c4 Binary files /dev/null and b/assets/amiibo/images/icon_01830101-010e0502.png differ diff --git a/assets/amiibo/images/icon_01830201-03a80502.png b/assets/amiibo/images/icon_01830201-03a80502.png new file mode 100644 index 000000000..7c53003ea Binary files /dev/null and b/assets/amiibo/images/icon_01830201-03a80502.png differ diff --git a/assets/amiibo/images/icon_01830301-03be0502.png b/assets/amiibo/images/icon_01830301-03be0502.png new file mode 100644 index 000000000..a25b00bd9 Binary files /dev/null and b/assets/amiibo/images/icon_01830301-03be0502.png differ diff --git a/assets/amiibo/images/icon_01840000-024d0502.png b/assets/amiibo/images/icon_01840000-024d0502.png new file mode 100644 index 000000000..af0b9c5ae Binary files /dev/null and b/assets/amiibo/images/icon_01840000-024d0502.png differ diff --git a/assets/amiibo/images/icon_01840501-03a90502.png b/assets/amiibo/images/icon_01840501-03a90502.png new file mode 100644 index 000000000..6f3dd2672 Binary files /dev/null and b/assets/amiibo/images/icon_01840501-03a90502.png differ diff --git a/assets/amiibo/images/icon_01850001-004b0502.png b/assets/amiibo/images/icon_01850001-004b0502.png new file mode 100644 index 000000000..70043a090 Binary files /dev/null and b/assets/amiibo/images/icon_01850001-004b0502.png differ diff --git a/assets/amiibo/images/icon_01850201-01170502.png b/assets/amiibo/images/icon_01850201-01170502.png new file mode 100644 index 000000000..d3950e2bd Binary files /dev/null and b/assets/amiibo/images/icon_01850201-01170502.png differ diff --git a/assets/amiibo/images/icon_01850401-01790502.png b/assets/amiibo/images/icon_01850401-01790502.png new file mode 100644 index 000000000..2f49cc6e2 Binary files /dev/null and b/assets/amiibo/images/icon_01850401-01790502.png differ diff --git a/assets/amiibo/images/icon_01860101-00af0502.png b/assets/amiibo/images/icon_01860101-00af0502.png new file mode 100644 index 000000000..a75b5ddc5 Binary files /dev/null and b/assets/amiibo/images/icon_01860101-00af0502.png differ diff --git a/assets/amiibo/images/icon_01860301-01750502.png b/assets/amiibo/images/icon_01860301-01750502.png new file mode 100644 index 000000000..82075f79e Binary files /dev/null and b/assets/amiibo/images/icon_01860301-01750502.png differ diff --git a/assets/amiibo/images/icon_01870001-00470502.png b/assets/amiibo/images/icon_01870001-00470502.png new file mode 100644 index 000000000..e068c1e52 Binary files /dev/null and b/assets/amiibo/images/icon_01870001-00470502.png differ diff --git a/assets/amiibo/images/icon_01870001-03b00502.png b/assets/amiibo/images/icon_01870001-03b00502.png new file mode 100644 index 000000000..736abcaec Binary files /dev/null and b/assets/amiibo/images/icon_01870001-03b00502.png differ diff --git a/assets/amiibo/images/icon_01880000-02410502.png b/assets/amiibo/images/icon_01880000-02410502.png new file mode 100644 index 000000000..cbfef0032 Binary files /dev/null and b/assets/amiibo/images/icon_01880000-02410502.png differ diff --git a/assets/amiibo/images/icon_01880001-01120502.png b/assets/amiibo/images/icon_01880001-01120502.png new file mode 100644 index 000000000..1167638f9 Binary files /dev/null and b/assets/amiibo/images/icon_01880001-01120502.png differ diff --git a/assets/amiibo/images/icon_01880001-03af0502.png b/assets/amiibo/images/icon_01880001-03af0502.png new file mode 100644 index 000000000..6e162e6d3 Binary files /dev/null and b/assets/amiibo/images/icon_01880001-03af0502.png differ diff --git a/assets/amiibo/images/icon_01890001-00ab0502.png b/assets/amiibo/images/icon_01890001-00ab0502.png new file mode 100644 index 000000000..a96ad753e Binary files /dev/null and b/assets/amiibo/images/icon_01890001-00ab0502.png differ diff --git a/assets/amiibo/images/icon_01890101-03b10502.png b/assets/amiibo/images/icon_01890101-03b10502.png new file mode 100644 index 000000000..c973f9e65 Binary files /dev/null and b/assets/amiibo/images/icon_01890101-03b10502.png differ diff --git a/assets/amiibo/images/icon_018a0000-02450502.png b/assets/amiibo/images/icon_018a0000-02450502.png new file mode 100644 index 000000000..29bcf9427 Binary files /dev/null and b/assets/amiibo/images/icon_018a0000-02450502.png differ diff --git a/assets/amiibo/images/icon_018a0001-00a90502.png b/assets/amiibo/images/icon_018a0001-00a90502.png new file mode 100644 index 000000000..a08a53e75 Binary files /dev/null and b/assets/amiibo/images/icon_018a0001-00a90502.png differ diff --git a/assets/amiibo/images/icon_018b0000-02460502.png b/assets/amiibo/images/icon_018b0000-02460502.png new file mode 100644 index 000000000..d3ca3157f Binary files /dev/null and b/assets/amiibo/images/icon_018b0000-02460502.png differ diff --git a/assets/amiibo/images/icon_018b0001-01150502.png b/assets/amiibo/images/icon_018b0001-01150502.png new file mode 100644 index 000000000..0b50ed0d0 Binary files /dev/null and b/assets/amiibo/images/icon_018b0001-01150502.png differ diff --git a/assets/amiibo/images/icon_018c0000-02430502.png b/assets/amiibo/images/icon_018c0000-02430502.png new file mode 100644 index 000000000..5b8af1a5e Binary files /dev/null and b/assets/amiibo/images/icon_018c0000-02430502.png differ diff --git a/assets/amiibo/images/icon_018c0001-004c0502.png b/assets/amiibo/images/icon_018c0001-004c0502.png new file mode 100644 index 000000000..c67f64bbc Binary files /dev/null and b/assets/amiibo/images/icon_018c0001-004c0502.png differ diff --git a/assets/amiibo/images/icon_018c0101-01180502.png b/assets/amiibo/images/icon_018c0101-01180502.png new file mode 100644 index 000000000..93fbe1bce Binary files /dev/null and b/assets/amiibo/images/icon_018c0101-01180502.png differ diff --git a/assets/amiibo/images/icon_018d0000-024c0502.png b/assets/amiibo/images/icon_018d0000-024c0502.png new file mode 100644 index 000000000..38c2dcb37 Binary files /dev/null and b/assets/amiibo/images/icon_018d0000-024c0502.png differ diff --git a/assets/amiibo/images/icon_018d0001-010c0502.png b/assets/amiibo/images/icon_018d0001-010c0502.png new file mode 100644 index 000000000..fb328b1c0 Binary files /dev/null and b/assets/amiibo/images/icon_018d0001-010c0502.png differ diff --git a/assets/amiibo/images/icon_018e0000-02490502.png b/assets/amiibo/images/icon_018e0000-02490502.png new file mode 100644 index 000000000..f27bd19fa Binary files /dev/null and b/assets/amiibo/images/icon_018e0000-02490502.png differ diff --git a/assets/amiibo/images/icon_018e0001-00490502.png b/assets/amiibo/images/icon_018e0001-00490502.png new file mode 100644 index 000000000..d3f5e504b Binary files /dev/null and b/assets/amiibo/images/icon_018e0001-00490502.png differ diff --git a/assets/amiibo/images/icon_018e0101-01780502.png b/assets/amiibo/images/icon_018e0101-01780502.png new file mode 100644 index 000000000..fb579f84d Binary files /dev/null and b/assets/amiibo/images/icon_018e0101-01780502.png differ diff --git a/assets/amiibo/images/icon_018f0001-00b30502.png b/assets/amiibo/images/icon_018f0001-00b30502.png new file mode 100644 index 000000000..5c8bf0d0d Binary files /dev/null and b/assets/amiibo/images/icon_018f0001-00b30502.png differ diff --git a/assets/amiibo/images/icon_018f0101-01190502.png b/assets/amiibo/images/icon_018f0101-01190502.png new file mode 100644 index 000000000..805ae3eb4 Binary files /dev/null and b/assets/amiibo/images/icon_018f0101-01190502.png differ diff --git a/assets/amiibo/images/icon_01900001-01710502.png b/assets/amiibo/images/icon_01900001-01710502.png new file mode 100644 index 000000000..df82724d5 Binary files /dev/null and b/assets/amiibo/images/icon_01900001-01710502.png differ diff --git a/assets/amiibo/images/icon_01910001-004e0502.png b/assets/amiibo/images/icon_01910001-004e0502.png new file mode 100644 index 000000000..62ab4d28a Binary files /dev/null and b/assets/amiibo/images/icon_01910001-004e0502.png differ diff --git a/assets/amiibo/images/icon_01920000-02470502.png b/assets/amiibo/images/icon_01920000-02470502.png new file mode 100644 index 000000000..c605f10dd Binary files /dev/null and b/assets/amiibo/images/icon_01920000-02470502.png differ diff --git a/assets/amiibo/images/icon_01920001-010d0502.png b/assets/amiibo/images/icon_01920001-010d0502.png new file mode 100644 index 000000000..465c7d348 Binary files /dev/null and b/assets/amiibo/images/icon_01920001-010d0502.png differ diff --git a/assets/amiibo/images/icon_01920001-03ad0502.png b/assets/amiibo/images/icon_01920001-03ad0502.png new file mode 100644 index 000000000..371e97846 Binary files /dev/null and b/assets/amiibo/images/icon_01920001-03ad0502.png differ diff --git a/assets/amiibo/images/icon_01930000-02480502.png b/assets/amiibo/images/icon_01930000-02480502.png new file mode 100644 index 000000000..033acf278 Binary files /dev/null and b/assets/amiibo/images/icon_01930000-02480502.png differ diff --git a/assets/amiibo/images/icon_01930001-01740502.png b/assets/amiibo/images/icon_01930001-01740502.png new file mode 100644 index 000000000..3ccafabe9 Binary files /dev/null and b/assets/amiibo/images/icon_01930001-01740502.png differ diff --git a/assets/amiibo/images/icon_01930001-03ae0502.png b/assets/amiibo/images/icon_01930001-03ae0502.png new file mode 100644 index 000000000..7d85c371c Binary files /dev/null and b/assets/amiibo/images/icon_01930001-03ae0502.png differ diff --git a/assets/amiibo/images/icon_01940000-024a0502.png b/assets/amiibo/images/icon_01940000-024a0502.png new file mode 100644 index 000000000..5d0571c61 Binary files /dev/null and b/assets/amiibo/images/icon_01940000-024a0502.png differ diff --git a/assets/amiibo/images/icon_01940001-00aa0502.png b/assets/amiibo/images/icon_01940001-00aa0502.png new file mode 100644 index 000000000..8fbaa48ab Binary files /dev/null and b/assets/amiibo/images/icon_01940001-00aa0502.png differ diff --git a/assets/amiibo/images/icon_01940001-03b60502.png b/assets/amiibo/images/icon_01940001-03b60502.png new file mode 100644 index 000000000..5ce1e16ad Binary files /dev/null and b/assets/amiibo/images/icon_01940001-03b60502.png differ diff --git a/assets/amiibo/images/icon_01950001-00b00502.png b/assets/amiibo/images/icon_01950001-00b00502.png new file mode 100644 index 000000000..31e3f7c4a Binary files /dev/null and b/assets/amiibo/images/icon_01950001-00b00502.png differ diff --git a/assets/amiibo/images/icon_01960000-024e0502.png b/assets/amiibo/images/icon_01960000-024e0502.png new file mode 100644 index 000000000..3eb260263 Binary files /dev/null and b/assets/amiibo/images/icon_01960000-024e0502.png differ diff --git a/assets/amiibo/images/icon_01960001-00480502.png b/assets/amiibo/images/icon_01960001-00480502.png new file mode 100644 index 000000000..2b5f81c6f Binary files /dev/null and b/assets/amiibo/images/icon_01960001-00480502.png differ diff --git a/assets/amiibo/images/icon_01970001-01770502.png b/assets/amiibo/images/icon_01970001-01770502.png new file mode 100644 index 000000000..64d3a0cee Binary files /dev/null and b/assets/amiibo/images/icon_01970001-01770502.png differ diff --git a/assets/amiibo/images/icon_01980001-00b10502.png b/assets/amiibo/images/icon_01980001-00b10502.png new file mode 100644 index 000000000..5b0fed906 Binary files /dev/null and b/assets/amiibo/images/icon_01980001-00b10502.png differ diff --git a/assets/amiibo/images/icon_01990001-01160502.png b/assets/amiibo/images/icon_01990001-01160502.png new file mode 100644 index 000000000..1fba27a52 Binary files /dev/null and b/assets/amiibo/images/icon_01990001-01160502.png differ diff --git a/assets/amiibo/images/icon_019a0001-00b70502.png b/assets/amiibo/images/icon_019a0001-00b70502.png new file mode 100644 index 000000000..03723b3e9 Binary files /dev/null and b/assets/amiibo/images/icon_019a0001-00b70502.png differ diff --git a/assets/amiibo/images/icon_019b0001-00b60502.png b/assets/amiibo/images/icon_019b0001-00b60502.png new file mode 100644 index 000000000..f1a8d4730 Binary files /dev/null and b/assets/amiibo/images/icon_019b0001-00b60502.png differ diff --git a/assets/amiibo/images/icon_019c0001-01730502.png b/assets/amiibo/images/icon_019c0001-01730502.png new file mode 100644 index 000000000..bacbaa11e Binary files /dev/null and b/assets/amiibo/images/icon_019c0001-01730502.png differ diff --git a/assets/amiibo/images/icon_019d0001-00ac0502.png b/assets/amiibo/images/icon_019d0001-00ac0502.png new file mode 100644 index 000000000..fbc0976fd Binary files /dev/null and b/assets/amiibo/images/icon_019d0001-00ac0502.png differ diff --git a/assets/amiibo/images/icon_019e0001-00ad0502.png b/assets/amiibo/images/icon_019e0001-00ad0502.png new file mode 100644 index 000000000..16ed5854f Binary files /dev/null and b/assets/amiibo/images/icon_019e0001-00ad0502.png differ diff --git a/assets/amiibo/images/icon_019f0001-01110502.png b/assets/amiibo/images/icon_019f0001-01110502.png new file mode 100644 index 000000000..42d0ae64e Binary files /dev/null and b/assets/amiibo/images/icon_019f0001-01110502.png differ diff --git a/assets/amiibo/images/icon_01a00001-010f0502.png b/assets/amiibo/images/icon_01a00001-010f0502.png new file mode 100644 index 000000000..76283ef27 Binary files /dev/null and b/assets/amiibo/images/icon_01a00001-010f0502.png differ diff --git a/assets/amiibo/images/icon_01a10001-01100502.png b/assets/amiibo/images/icon_01a10001-01100502.png new file mode 100644 index 000000000..9f09fbced Binary files /dev/null and b/assets/amiibo/images/icon_01a10001-01100502.png differ diff --git a/assets/amiibo/images/icon_01a20001-017d0502.png b/assets/amiibo/images/icon_01a20001-017d0502.png new file mode 100644 index 000000000..821fb22f7 Binary files /dev/null and b/assets/amiibo/images/icon_01a20001-017d0502.png differ diff --git a/assets/amiibo/images/icon_01a20001-03b90502.png b/assets/amiibo/images/icon_01a20001-03b90502.png new file mode 100644 index 000000000..768c08f4f Binary files /dev/null and b/assets/amiibo/images/icon_01a20001-03b90502.png differ diff --git a/assets/amiibo/images/icon_01a30001-004a0502.png b/assets/amiibo/images/icon_01a30001-004a0502.png new file mode 100644 index 000000000..c7c3ac4f3 Binary files /dev/null and b/assets/amiibo/images/icon_01a30001-004a0502.png differ diff --git a/assets/amiibo/images/icon_01a40001-004d0502.png b/assets/amiibo/images/icon_01a40001-004d0502.png new file mode 100644 index 000000000..b7fc5a5ca Binary files /dev/null and b/assets/amiibo/images/icon_01a40001-004d0502.png differ diff --git a/assets/amiibo/images/icon_01a50001-01720502.png b/assets/amiibo/images/icon_01a50001-01720502.png new file mode 100644 index 000000000..c2606e78c Binary files /dev/null and b/assets/amiibo/images/icon_01a50001-01720502.png differ diff --git a/assets/amiibo/images/icon_01a60001-00500502.png b/assets/amiibo/images/icon_01a60001-00500502.png new file mode 100644 index 000000000..e1979133e Binary files /dev/null and b/assets/amiibo/images/icon_01a60001-00500502.png differ diff --git a/assets/amiibo/images/icon_01a60001-03b70502.png b/assets/amiibo/images/icon_01a60001-03b70502.png new file mode 100644 index 000000000..4679e138a Binary files /dev/null and b/assets/amiibo/images/icon_01a60001-03b70502.png differ diff --git a/assets/amiibo/images/icon_01a70001-01140502.png b/assets/amiibo/images/icon_01a70001-01140502.png new file mode 100644 index 000000000..40d9725c6 Binary files /dev/null and b/assets/amiibo/images/icon_01a70001-01140502.png differ diff --git a/assets/amiibo/images/icon_01a80001-004f0502.png b/assets/amiibo/images/icon_01a80001-004f0502.png new file mode 100644 index 000000000..45f21ea13 Binary files /dev/null and b/assets/amiibo/images/icon_01a80001-004f0502.png differ diff --git a/assets/amiibo/images/icon_01a80101-017e0502.png b/assets/amiibo/images/icon_01a80101-017e0502.png new file mode 100644 index 000000000..f5df61dfa Binary files /dev/null and b/assets/amiibo/images/icon_01a80101-017e0502.png differ diff --git a/assets/amiibo/images/icon_01a90001-01760502.png b/assets/amiibo/images/icon_01a90001-01760502.png new file mode 100644 index 000000000..67651098f Binary files /dev/null and b/assets/amiibo/images/icon_01a90001-01760502.png differ diff --git a/assets/amiibo/images/icon_01aa0001-00530502.png b/assets/amiibo/images/icon_01aa0001-00530502.png new file mode 100644 index 000000000..a68c51f4d Binary files /dev/null and b/assets/amiibo/images/icon_01aa0001-00530502.png differ diff --git a/assets/amiibo/images/icon_01ab0001-017c0502.png b/assets/amiibo/images/icon_01ab0001-017c0502.png new file mode 100644 index 000000000..345a0e7f6 Binary files /dev/null and b/assets/amiibo/images/icon_01ab0001-017c0502.png differ diff --git a/assets/amiibo/images/icon_01ac0001-017f0502.png b/assets/amiibo/images/icon_01ac0001-017f0502.png new file mode 100644 index 000000000..9a54b2bfa Binary files /dev/null and b/assets/amiibo/images/icon_01ac0001-017f0502.png differ diff --git a/assets/amiibo/images/icon_01ad0001-00b80502.png b/assets/amiibo/images/icon_01ad0001-00b80502.png new file mode 100644 index 000000000..78ec67d2d Binary files /dev/null and b/assets/amiibo/images/icon_01ad0001-00b80502.png differ diff --git a/assets/amiibo/images/icon_01ae0001-011b0502.png b/assets/amiibo/images/icon_01ae0001-011b0502.png new file mode 100644 index 000000000..b12d3c1e2 Binary files /dev/null and b/assets/amiibo/images/icon_01ae0001-011b0502.png differ diff --git a/assets/amiibo/images/icon_01af0001-011c0502.png b/assets/amiibo/images/icon_01af0001-011c0502.png new file mode 100644 index 000000000..0b4e4ac27 Binary files /dev/null and b/assets/amiibo/images/icon_01af0001-011c0502.png differ diff --git a/assets/amiibo/images/icon_01b00001-00520502.png b/assets/amiibo/images/icon_01b00001-00520502.png new file mode 100644 index 000000000..57f138b37 Binary files /dev/null and b/assets/amiibo/images/icon_01b00001-00520502.png differ diff --git a/assets/amiibo/images/icon_01b10001-00b20502.png b/assets/amiibo/images/icon_01b10001-00b20502.png new file mode 100644 index 000000000..7a1fbef4f Binary files /dev/null and b/assets/amiibo/images/icon_01b10001-00b20502.png differ diff --git a/assets/amiibo/images/icon_01b10101-017b0502.png b/assets/amiibo/images/icon_01b10101-017b0502.png new file mode 100644 index 000000000..eac173ad6 Binary files /dev/null and b/assets/amiibo/images/icon_01b10101-017b0502.png differ diff --git a/assets/amiibo/images/icon_01b30001-00b50502.png b/assets/amiibo/images/icon_01b30001-00b50502.png new file mode 100644 index 000000000..4a9db5473 Binary files /dev/null and b/assets/amiibo/images/icon_01b30001-00b50502.png differ diff --git a/assets/amiibo/images/icon_01b40001-01130502.png b/assets/amiibo/images/icon_01b40001-01130502.png new file mode 100644 index 000000000..426021a34 Binary files /dev/null and b/assets/amiibo/images/icon_01b40001-01130502.png differ diff --git a/assets/amiibo/images/icon_01b50001-00510502.png b/assets/amiibo/images/icon_01b50001-00510502.png new file mode 100644 index 000000000..5f2979eff Binary files /dev/null and b/assets/amiibo/images/icon_01b50001-00510502.png differ diff --git a/assets/amiibo/images/icon_01b60001-00ae0502.png b/assets/amiibo/images/icon_01b60001-00ae0502.png new file mode 100644 index 000000000..ff7b9006d Binary files /dev/null and b/assets/amiibo/images/icon_01b60001-00ae0502.png differ diff --git a/assets/amiibo/images/icon_01c10000-02440502.png b/assets/amiibo/images/icon_01c10000-02440502.png new file mode 100644 index 000000000..9707816af Binary files /dev/null and b/assets/amiibo/images/icon_01c10000-02440502.png differ diff --git a/assets/amiibo/images/icon_01c10001-00540502.png b/assets/amiibo/images/icon_01c10001-00540502.png new file mode 100644 index 000000000..3971b1a63 Binary files /dev/null and b/assets/amiibo/images/icon_01c10001-00540502.png differ diff --git a/assets/amiibo/images/icon_01c10101-017a0502.png b/assets/amiibo/images/icon_01c10101-017a0502.png new file mode 100644 index 000000000..ee4f4a096 Binary files /dev/null and b/assets/amiibo/images/icon_01c10101-017a0502.png differ diff --git a/assets/amiibo/images/icon_01c10201-03bb0502.png b/assets/amiibo/images/icon_01c10201-03bb0502.png new file mode 100644 index 000000000..5421bf70b Binary files /dev/null and b/assets/amiibo/images/icon_01c10201-03bb0502.png differ diff --git a/assets/amiibo/images/icon_02000001-00a10502.png b/assets/amiibo/images/icon_02000001-00a10502.png new file mode 100644 index 000000000..3a85b87db Binary files /dev/null and b/assets/amiibo/images/icon_02000001-00a10502.png differ diff --git a/assets/amiibo/images/icon_02010001-016a0502.png b/assets/amiibo/images/icon_02010001-016a0502.png new file mode 100644 index 000000000..0f0560f48 Binary files /dev/null and b/assets/amiibo/images/icon_02010001-016a0502.png differ diff --git a/assets/amiibo/images/icon_02020001-01030502.png b/assets/amiibo/images/icon_02020001-01030502.png new file mode 100644 index 000000000..d498b6828 Binary files /dev/null and b/assets/amiibo/images/icon_02020001-01030502.png differ diff --git a/assets/amiibo/images/icon_02030001-019a0502.png b/assets/amiibo/images/icon_02030001-019a0502.png new file mode 100644 index 000000000..e47a6c82b Binary files /dev/null and b/assets/amiibo/images/icon_02030001-019a0502.png differ diff --git a/assets/amiibo/images/icon_02060001-03120502.png b/assets/amiibo/images/icon_02060001-03120502.png new file mode 100644 index 000000000..fee76bd28 Binary files /dev/null and b/assets/amiibo/images/icon_02060001-03120502.png differ diff --git a/assets/amiibo/images/icon_02080001-00960502.png b/assets/amiibo/images/icon_02080001-00960502.png new file mode 100644 index 000000000..74b1f0fed Binary files /dev/null and b/assets/amiibo/images/icon_02080001-00960502.png differ diff --git a/assets/amiibo/images/icon_02090001-019f0502.png b/assets/amiibo/images/icon_02090001-019f0502.png new file mode 100644 index 000000000..b964bb673 Binary files /dev/null and b/assets/amiibo/images/icon_02090001-019f0502.png differ diff --git a/assets/amiibo/images/icon_02140001-00e40502.png b/assets/amiibo/images/icon_02140001-00e40502.png new file mode 100644 index 000000000..0bdc41652 Binary files /dev/null and b/assets/amiibo/images/icon_02140001-00e40502.png differ diff --git a/assets/amiibo/images/icon_02150001-01820502.png b/assets/amiibo/images/icon_02150001-01820502.png new file mode 100644 index 000000000..1ed1e3fb3 Binary files /dev/null and b/assets/amiibo/images/icon_02150001-01820502.png differ diff --git a/assets/amiibo/images/icon_02160001-00570502.png b/assets/amiibo/images/icon_02160001-00570502.png new file mode 100644 index 000000000..d1c719550 Binary files /dev/null and b/assets/amiibo/images/icon_02160001-00570502.png differ diff --git a/assets/amiibo/images/icon_02170001-01b30502.png b/assets/amiibo/images/icon_02170001-01b30502.png new file mode 100644 index 000000000..ca2fb9742 Binary files /dev/null and b/assets/amiibo/images/icon_02170001-01b30502.png differ diff --git a/assets/amiibo/images/icon_02190001-007e0502.png b/assets/amiibo/images/icon_02190001-007e0502.png new file mode 100644 index 000000000..7a83357c8 Binary files /dev/null and b/assets/amiibo/images/icon_02190001-007e0502.png differ diff --git a/assets/amiibo/images/icon_021a0001-00da0502.png b/assets/amiibo/images/icon_021a0001-00da0502.png new file mode 100644 index 000000000..84cf671b3 Binary files /dev/null and b/assets/amiibo/images/icon_021a0001-00da0502.png differ diff --git a/assets/amiibo/images/icon_021b0001-00800502.png b/assets/amiibo/images/icon_021b0001-00800502.png new file mode 100644 index 000000000..2062ec0ff Binary files /dev/null and b/assets/amiibo/images/icon_021b0001-00800502.png differ diff --git a/assets/amiibo/images/icon_021c0001-02f70502.png b/assets/amiibo/images/icon_021c0001-02f70502.png new file mode 100644 index 000000000..e90ac27be Binary files /dev/null and b/assets/amiibo/images/icon_021c0001-02f70502.png differ diff --git a/assets/amiibo/images/icon_021d0001-01cd0502.png b/assets/amiibo/images/icon_021d0001-01cd0502.png new file mode 100644 index 000000000..1cc55a53c Binary files /dev/null and b/assets/amiibo/images/icon_021d0001-01cd0502.png differ diff --git a/assets/amiibo/images/icon_021e0001-01230502.png b/assets/amiibo/images/icon_021e0001-01230502.png new file mode 100644 index 000000000..e00d6b0b2 Binary files /dev/null and b/assets/amiibo/images/icon_021e0001-01230502.png differ diff --git a/assets/amiibo/images/icon_021f0001-03170502.png b/assets/amiibo/images/icon_021f0001-03170502.png new file mode 100644 index 000000000..b1ed41933 Binary files /dev/null and b/assets/amiibo/images/icon_021f0001-03170502.png differ diff --git a/assets/amiibo/images/icon_02200001-00fd0502.png b/assets/amiibo/images/icon_02200001-00fd0502.png new file mode 100644 index 000000000..9d39523c1 Binary files /dev/null and b/assets/amiibo/images/icon_02200001-00fd0502.png differ diff --git a/assets/amiibo/images/icon_02210001-013c0502.png b/assets/amiibo/images/icon_02210001-013c0502.png new file mode 100644 index 000000000..ec0d12d1a Binary files /dev/null and b/assets/amiibo/images/icon_02210001-013c0502.png differ diff --git a/assets/amiibo/images/icon_02220001-01440502.png b/assets/amiibo/images/icon_02220001-01440502.png new file mode 100644 index 000000000..1bcd9f160 Binary files /dev/null and b/assets/amiibo/images/icon_02220001-01440502.png differ diff --git a/assets/amiibo/images/icon_022d0001-00f20502.png b/assets/amiibo/images/icon_022d0001-00f20502.png new file mode 100644 index 000000000..ca575920e Binary files /dev/null and b/assets/amiibo/images/icon_022d0001-00f20502.png differ diff --git a/assets/amiibo/images/icon_022e0001-01d30502.png b/assets/amiibo/images/icon_022e0001-01d30502.png new file mode 100644 index 000000000..8b3d0db28 Binary files /dev/null and b/assets/amiibo/images/icon_022e0001-01d30502.png differ diff --git a/assets/amiibo/images/icon_022f0001-011e0502.png b/assets/amiibo/images/icon_022f0001-011e0502.png new file mode 100644 index 000000000..5c9df328a Binary files /dev/null and b/assets/amiibo/images/icon_022f0001-011e0502.png differ diff --git a/assets/amiibo/images/icon_02300001-01d20502.png b/assets/amiibo/images/icon_02300001-01d20502.png new file mode 100644 index 000000000..d35c91e6d Binary files /dev/null and b/assets/amiibo/images/icon_02300001-01d20502.png differ diff --git a/assets/amiibo/images/icon_02310001-006a0502.png b/assets/amiibo/images/icon_02310001-006a0502.png new file mode 100644 index 000000000..67051c521 Binary files /dev/null and b/assets/amiibo/images/icon_02310001-006a0502.png differ diff --git a/assets/amiibo/images/icon_02320001-02ea0502.png b/assets/amiibo/images/icon_02320001-02ea0502.png new file mode 100644 index 000000000..3d8b089b0 Binary files /dev/null and b/assets/amiibo/images/icon_02320001-02ea0502.png differ diff --git a/assets/amiibo/images/icon_02330001-03060502.png b/assets/amiibo/images/icon_02330001-03060502.png new file mode 100644 index 000000000..d918fa161 Binary files /dev/null and b/assets/amiibo/images/icon_02330001-03060502.png differ diff --git a/assets/amiibo/images/icon_02350001-00840502.png b/assets/amiibo/images/icon_02350001-00840502.png new file mode 100644 index 000000000..5b16b02d5 Binary files /dev/null and b/assets/amiibo/images/icon_02350001-00840502.png differ diff --git a/assets/amiibo/images/icon_02380001-02f80502.png b/assets/amiibo/images/icon_02380001-02f80502.png new file mode 100644 index 000000000..7a6f375c5 Binary files /dev/null and b/assets/amiibo/images/icon_02380001-02f80502.png differ diff --git a/assets/amiibo/images/icon_023c0001-00bd0502.png b/assets/amiibo/images/icon_023c0001-00bd0502.png new file mode 100644 index 000000000..4b79159c7 Binary files /dev/null and b/assets/amiibo/images/icon_023c0001-00bd0502.png differ diff --git a/assets/amiibo/images/icon_023d0001-01b50502.png b/assets/amiibo/images/icon_023d0001-01b50502.png new file mode 100644 index 000000000..3f80dff60 Binary files /dev/null and b/assets/amiibo/images/icon_023d0001-01b50502.png differ diff --git a/assets/amiibo/images/icon_023e0001-00d10502.png b/assets/amiibo/images/icon_023e0001-00d10502.png new file mode 100644 index 000000000..8c76abb05 Binary files /dev/null and b/assets/amiibo/images/icon_023e0001-00d10502.png differ diff --git a/assets/amiibo/images/icon_023f0001-01660502.png b/assets/amiibo/images/icon_023f0001-01660502.png new file mode 100644 index 000000000..7036a0c27 Binary files /dev/null and b/assets/amiibo/images/icon_023f0001-01660502.png differ diff --git a/assets/amiibo/images/icon_024a0001-01d10502.png b/assets/amiibo/images/icon_024a0001-01d10502.png new file mode 100644 index 000000000..d7f6430b2 Binary files /dev/null and b/assets/amiibo/images/icon_024a0001-01d10502.png differ diff --git a/assets/amiibo/images/icon_024b0001-01260502.png b/assets/amiibo/images/icon_024b0001-01260502.png new file mode 100644 index 000000000..fed4228a0 Binary files /dev/null and b/assets/amiibo/images/icon_024b0001-01260502.png differ diff --git a/assets/amiibo/images/icon_024d0001-02f60502.png b/assets/amiibo/images/icon_024d0001-02f60502.png new file mode 100644 index 000000000..a2d0832ef Binary files /dev/null and b/assets/amiibo/images/icon_024d0001-02f60502.png differ diff --git a/assets/amiibo/images/icon_024f0001-00810502.png b/assets/amiibo/images/icon_024f0001-00810502.png new file mode 100644 index 000000000..9456ed75c Binary files /dev/null and b/assets/amiibo/images/icon_024f0001-00810502.png differ diff --git a/assets/amiibo/images/icon_02510001-00c10502.png b/assets/amiibo/images/icon_02510001-00c10502.png new file mode 100644 index 000000000..9ce5c04d0 Binary files /dev/null and b/assets/amiibo/images/icon_02510001-00c10502.png differ diff --git a/assets/amiibo/images/icon_02520001-00fe0502.png b/assets/amiibo/images/icon_02520001-00fe0502.png new file mode 100644 index 000000000..915a8942f Binary files /dev/null and b/assets/amiibo/images/icon_02520001-00fe0502.png differ diff --git a/assets/amiibo/images/icon_025d0001-00550502.png b/assets/amiibo/images/icon_025d0001-00550502.png new file mode 100644 index 000000000..96b45ecdd Binary files /dev/null and b/assets/amiibo/images/icon_025d0001-00550502.png differ diff --git a/assets/amiibo/images/icon_025e0001-01250502.png b/assets/amiibo/images/icon_025e0001-01250502.png new file mode 100644 index 000000000..83a06f997 Binary files /dev/null and b/assets/amiibo/images/icon_025e0001-01250502.png differ diff --git a/assets/amiibo/images/icon_025f0001-01c50502.png b/assets/amiibo/images/icon_025f0001-01c50502.png new file mode 100644 index 000000000..372cea8ba Binary files /dev/null and b/assets/amiibo/images/icon_025f0001-01c50502.png differ diff --git a/assets/amiibo/images/icon_025f0001-01d70502.png b/assets/amiibo/images/icon_025f0001-01d70502.png new file mode 100644 index 000000000..d44a31cac Binary files /dev/null and b/assets/amiibo/images/icon_025f0001-01d70502.png differ diff --git a/assets/amiibo/images/icon_02600001-00d20502.png b/assets/amiibo/images/icon_02600001-00d20502.png new file mode 100644 index 000000000..e1941dab6 Binary files /dev/null and b/assets/amiibo/images/icon_02600001-00d20502.png differ diff --git a/assets/amiibo/images/icon_02610001-00650502.png b/assets/amiibo/images/icon_02610001-00650502.png new file mode 100644 index 000000000..fe5bddf16 Binary files /dev/null and b/assets/amiibo/images/icon_02610001-00650502.png differ diff --git a/assets/amiibo/images/icon_02620001-01370502.png b/assets/amiibo/images/icon_02620001-01370502.png new file mode 100644 index 000000000..6af1156a5 Binary files /dev/null and b/assets/amiibo/images/icon_02620001-01370502.png differ diff --git a/assets/amiibo/images/icon_02630001-00750502.png b/assets/amiibo/images/icon_02630001-00750502.png new file mode 100644 index 000000000..b4ae4ecbb Binary files /dev/null and b/assets/amiibo/images/icon_02630001-00750502.png differ diff --git a/assets/amiibo/images/icon_02640001-01ac0502.png b/assets/amiibo/images/icon_02640001-01ac0502.png new file mode 100644 index 000000000..9ddb980a3 Binary files /dev/null and b/assets/amiibo/images/icon_02640001-01ac0502.png differ diff --git a/assets/amiibo/images/icon_02650001-01540502.png b/assets/amiibo/images/icon_02650001-01540502.png new file mode 100644 index 000000000..2912b78f6 Binary files /dev/null and b/assets/amiibo/images/icon_02650001-01540502.png differ diff --git a/assets/amiibo/images/icon_02660001-00680502.png b/assets/amiibo/images/icon_02660001-00680502.png new file mode 100644 index 000000000..8b6937be3 Binary files /dev/null and b/assets/amiibo/images/icon_02660001-00680502.png differ diff --git a/assets/amiibo/images/icon_02670001-01080502.png b/assets/amiibo/images/icon_02670001-01080502.png new file mode 100644 index 000000000..95882b58b Binary files /dev/null and b/assets/amiibo/images/icon_02670001-01080502.png differ diff --git a/assets/amiibo/images/icon_02680001-007d0502.png b/assets/amiibo/images/icon_02680001-007d0502.png new file mode 100644 index 000000000..18882d1be Binary files /dev/null and b/assets/amiibo/images/icon_02680001-007d0502.png differ diff --git a/assets/amiibo/images/icon_02690001-011f0502.png b/assets/amiibo/images/icon_02690001-011f0502.png new file mode 100644 index 000000000..e83b0c984 Binary files /dev/null and b/assets/amiibo/images/icon_02690001-011f0502.png differ diff --git a/assets/amiibo/images/icon_026a0001-01460502.png b/assets/amiibo/images/icon_026a0001-01460502.png new file mode 100644 index 000000000..9f0e870ec Binary files /dev/null and b/assets/amiibo/images/icon_026a0001-01460502.png differ diff --git a/assets/amiibo/images/icon_026b0001-00e90502.png b/assets/amiibo/images/icon_026b0001-00e90502.png new file mode 100644 index 000000000..b2c9407bd Binary files /dev/null and b/assets/amiibo/images/icon_026b0001-00e90502.png differ diff --git a/assets/amiibo/images/icon_026c0001-00c30502.png b/assets/amiibo/images/icon_026c0001-00c30502.png new file mode 100644 index 000000000..d47fde6b8 Binary files /dev/null and b/assets/amiibo/images/icon_026c0001-00c30502.png differ diff --git a/assets/amiibo/images/icon_026d0001-013f0502.png b/assets/amiibo/images/icon_026d0001-013f0502.png new file mode 100644 index 000000000..ce54b0201 Binary files /dev/null and b/assets/amiibo/images/icon_026d0001-013f0502.png differ diff --git a/assets/amiibo/images/icon_026e0001-00ba0502.png b/assets/amiibo/images/icon_026e0001-00ba0502.png new file mode 100644 index 000000000..eb6d3b580 Binary files /dev/null and b/assets/amiibo/images/icon_026e0001-00ba0502.png differ diff --git a/assets/amiibo/images/icon_026f0001-01900502.png b/assets/amiibo/images/icon_026f0001-01900502.png new file mode 100644 index 000000000..a3a008e52 Binary files /dev/null and b/assets/amiibo/images/icon_026f0001-01900502.png differ diff --git a/assets/amiibo/images/icon_02700001-00ff0502.png b/assets/amiibo/images/icon_02700001-00ff0502.png new file mode 100644 index 000000000..d748bb1cb Binary files /dev/null and b/assets/amiibo/images/icon_02700001-00ff0502.png differ diff --git a/assets/amiibo/images/icon_02710001-019b0502.png b/assets/amiibo/images/icon_02710001-019b0502.png new file mode 100644 index 000000000..fd8f7bbe8 Binary files /dev/null and b/assets/amiibo/images/icon_02710001-019b0502.png differ diff --git a/assets/amiibo/images/icon_02720001-01860502.png b/assets/amiibo/images/icon_02720001-01860502.png new file mode 100644 index 000000000..76854ded4 Binary files /dev/null and b/assets/amiibo/images/icon_02720001-01860502.png differ diff --git a/assets/amiibo/images/icon_027d0001-00630502.png b/assets/amiibo/images/icon_027d0001-00630502.png new file mode 100644 index 000000000..d205c6105 Binary files /dev/null and b/assets/amiibo/images/icon_027d0001-00630502.png differ diff --git a/assets/amiibo/images/icon_027e0001-01690502.png b/assets/amiibo/images/icon_027e0001-01690502.png new file mode 100644 index 000000000..ce2146352 Binary files /dev/null and b/assets/amiibo/images/icon_027e0001-01690502.png differ diff --git a/assets/amiibo/images/icon_027f0001-00b90502.png b/assets/amiibo/images/icon_027f0001-00b90502.png new file mode 100644 index 000000000..2dcf62c96 Binary files /dev/null and b/assets/amiibo/images/icon_027f0001-00b90502.png differ diff --git a/assets/amiibo/images/icon_02800001-00830502.png b/assets/amiibo/images/icon_02800001-00830502.png new file mode 100644 index 000000000..59acd3e09 Binary files /dev/null and b/assets/amiibo/images/icon_02800001-00830502.png differ diff --git a/assets/amiibo/images/icon_02810001-01200502.png b/assets/amiibo/images/icon_02810001-01200502.png new file mode 100644 index 000000000..6d1fe3acb Binary files /dev/null and b/assets/amiibo/images/icon_02810001-01200502.png differ diff --git a/assets/amiibo/images/icon_02820001-01810502.png b/assets/amiibo/images/icon_02820001-01810502.png new file mode 100644 index 000000000..88066c59a Binary files /dev/null and b/assets/amiibo/images/icon_02820001-01810502.png differ diff --git a/assets/amiibo/images/icon_02820001-01d60502.png b/assets/amiibo/images/icon_02820001-01d60502.png new file mode 100644 index 000000000..0ea8032cf Binary files /dev/null and b/assets/amiibo/images/icon_02820001-01d60502.png differ diff --git a/assets/amiibo/images/icon_02830001-00c70502.png b/assets/amiibo/images/icon_02830001-00c70502.png new file mode 100644 index 000000000..1d263c7cb Binary files /dev/null and b/assets/amiibo/images/icon_02830001-00c70502.png differ diff --git a/assets/amiibo/images/icon_02840001-02fe0502.png b/assets/amiibo/images/icon_02840001-02fe0502.png new file mode 100644 index 000000000..f5df3dddf Binary files /dev/null and b/assets/amiibo/images/icon_02840001-02fe0502.png differ diff --git a/assets/amiibo/images/icon_02860001-03130502.png b/assets/amiibo/images/icon_02860001-03130502.png new file mode 100644 index 000000000..769a4aceb Binary files /dev/null and b/assets/amiibo/images/icon_02860001-03130502.png differ diff --git a/assets/amiibo/images/icon_02870001-005a0502.png b/assets/amiibo/images/icon_02870001-005a0502.png new file mode 100644 index 000000000..6a81c697c Binary files /dev/null and b/assets/amiibo/images/icon_02870001-005a0502.png differ diff --git a/assets/amiibo/images/icon_028a0001-02e90502.png b/assets/amiibo/images/icon_028a0001-02e90502.png new file mode 100644 index 000000000..d9f28c379 Binary files /dev/null and b/assets/amiibo/images/icon_028a0001-02e90502.png differ diff --git a/assets/amiibo/images/icon_028b0001-00e30502.png b/assets/amiibo/images/icon_028b0001-00e30502.png new file mode 100644 index 000000000..609bc7d27 Binary files /dev/null and b/assets/amiibo/images/icon_028b0001-00e30502.png differ diff --git a/assets/amiibo/images/icon_028c0001-013e0502.png b/assets/amiibo/images/icon_028c0001-013e0502.png new file mode 100644 index 000000000..5ba4e987c Binary files /dev/null and b/assets/amiibo/images/icon_028c0001-013e0502.png differ diff --git a/assets/amiibo/images/icon_028d0001-01bd0502.png b/assets/amiibo/images/icon_028d0001-01bd0502.png new file mode 100644 index 000000000..b5c349f7e Binary files /dev/null and b/assets/amiibo/images/icon_028d0001-01bd0502.png differ diff --git a/assets/amiibo/images/icon_028e0001-019e0502.png b/assets/amiibo/images/icon_028e0001-019e0502.png new file mode 100644 index 000000000..4edb9846b Binary files /dev/null and b/assets/amiibo/images/icon_028e0001-019e0502.png differ diff --git a/assets/amiibo/images/icon_028f0101-031a0502.png b/assets/amiibo/images/icon_028f0101-031a0502.png new file mode 100644 index 000000000..8c6620594 Binary files /dev/null and b/assets/amiibo/images/icon_028f0101-031a0502.png differ diff --git a/assets/amiibo/images/icon_02990001-00950502.png b/assets/amiibo/images/icon_02990001-00950502.png new file mode 100644 index 000000000..43a7fdb0d Binary files /dev/null and b/assets/amiibo/images/icon_02990001-00950502.png differ diff --git a/assets/amiibo/images/icon_029a0001-00ee0502.png b/assets/amiibo/images/icon_029a0001-00ee0502.png new file mode 100644 index 000000000..ce13951b8 Binary files /dev/null and b/assets/amiibo/images/icon_029a0001-00ee0502.png differ diff --git a/assets/amiibo/images/icon_029b0001-00cb0502.png b/assets/amiibo/images/icon_029b0001-00cb0502.png new file mode 100644 index 000000000..a4205538b Binary files /dev/null and b/assets/amiibo/images/icon_029b0001-00cb0502.png differ diff --git a/assets/amiibo/images/icon_029e0001-013d0502.png b/assets/amiibo/images/icon_029e0001-013d0502.png new file mode 100644 index 000000000..1a0b16897 Binary files /dev/null and b/assets/amiibo/images/icon_029e0001-013d0502.png differ diff --git a/assets/amiibo/images/icon_02a20001-01ba0502.png b/assets/amiibo/images/icon_02a20001-01ba0502.png new file mode 100644 index 000000000..c08f5439f Binary files /dev/null and b/assets/amiibo/images/icon_02a20001-01ba0502.png differ diff --git a/assets/amiibo/images/icon_02a30001-02ff0502.png b/assets/amiibo/images/icon_02a30001-02ff0502.png new file mode 100644 index 000000000..f817f4b6f Binary files /dev/null and b/assets/amiibo/images/icon_02a30001-02ff0502.png differ diff --git a/assets/amiibo/images/icon_02a40001-00720502.png b/assets/amiibo/images/icon_02a40001-00720502.png new file mode 100644 index 000000000..bb2a694b7 Binary files /dev/null and b/assets/amiibo/images/icon_02a40001-00720502.png differ diff --git a/assets/amiibo/images/icon_02a50001-018c0502.png b/assets/amiibo/images/icon_02a50001-018c0502.png new file mode 100644 index 000000000..8fa364a7b Binary files /dev/null and b/assets/amiibo/images/icon_02a50001-018c0502.png differ diff --git a/assets/amiibo/images/icon_02a60001-01240502.png b/assets/amiibo/images/icon_02a60001-01240502.png new file mode 100644 index 000000000..be41540e5 Binary files /dev/null and b/assets/amiibo/images/icon_02a60001-01240502.png differ diff --git a/assets/amiibo/images/icon_02b10001-00690502.png b/assets/amiibo/images/icon_02b10001-00690502.png new file mode 100644 index 000000000..738cfe08a Binary files /dev/null and b/assets/amiibo/images/icon_02b10001-00690502.png differ diff --git a/assets/amiibo/images/icon_02b20001-00c40502.png b/assets/amiibo/images/icon_02b20001-00c40502.png new file mode 100644 index 000000000..02484d867 Binary files /dev/null and b/assets/amiibo/images/icon_02b20001-00c40502.png differ diff --git a/assets/amiibo/images/icon_02b70001-030f0502.png b/assets/amiibo/images/icon_02b70001-030f0502.png new file mode 100644 index 000000000..c516fd13f Binary files /dev/null and b/assets/amiibo/images/icon_02b70001-030f0502.png differ diff --git a/assets/amiibo/images/icon_02b80001-019c0502.png b/assets/amiibo/images/icon_02b80001-019c0502.png new file mode 100644 index 000000000..d09751c2a Binary files /dev/null and b/assets/amiibo/images/icon_02b80001-019c0502.png differ diff --git a/assets/amiibo/images/icon_02c30001-00dc0502.png b/assets/amiibo/images/icon_02c30001-00dc0502.png new file mode 100644 index 000000000..dcb38f063 Binary files /dev/null and b/assets/amiibo/images/icon_02c30001-00dc0502.png differ diff --git a/assets/amiibo/images/icon_02c40001-00670502.png b/assets/amiibo/images/icon_02c40001-00670502.png new file mode 100644 index 000000000..0f360252d Binary files /dev/null and b/assets/amiibo/images/icon_02c40001-00670502.png differ diff --git a/assets/amiibo/images/icon_02c50001-03080502.png b/assets/amiibo/images/icon_02c50001-03080502.png new file mode 100644 index 000000000..ed57c3dc8 Binary files /dev/null and b/assets/amiibo/images/icon_02c50001-03080502.png differ diff --git a/assets/amiibo/images/icon_02c70001-01220502.png b/assets/amiibo/images/icon_02c70001-01220502.png new file mode 100644 index 000000000..a9e004a29 Binary files /dev/null and b/assets/amiibo/images/icon_02c70001-01220502.png differ diff --git a/assets/amiibo/images/icon_02c90001-00cd0502.png b/assets/amiibo/images/icon_02c90001-00cd0502.png new file mode 100644 index 000000000..243025e28 Binary files /dev/null and b/assets/amiibo/images/icon_02c90001-00cd0502.png differ diff --git a/assets/amiibo/images/icon_02ca0001-01ca0502.png b/assets/amiibo/images/icon_02ca0001-01ca0502.png new file mode 100644 index 000000000..3e9f542ee Binary files /dev/null and b/assets/amiibo/images/icon_02ca0001-01ca0502.png differ diff --git a/assets/amiibo/images/icon_02cb0001-01360502.png b/assets/amiibo/images/icon_02cb0001-01360502.png new file mode 100644 index 000000000..f2cf11f71 Binary files /dev/null and b/assets/amiibo/images/icon_02cb0001-01360502.png differ diff --git a/assets/amiibo/images/icon_02d60001-00560502.png b/assets/amiibo/images/icon_02d60001-00560502.png new file mode 100644 index 000000000..da83d04bb Binary files /dev/null and b/assets/amiibo/images/icon_02d60001-00560502.png differ diff --git a/assets/amiibo/images/icon_02d70001-01300502.png b/assets/amiibo/images/icon_02d70001-01300502.png new file mode 100644 index 000000000..82573edb8 Binary files /dev/null and b/assets/amiibo/images/icon_02d70001-01300502.png differ diff --git a/assets/amiibo/images/icon_02d80001-00e20502.png b/assets/amiibo/images/icon_02d80001-00e20502.png new file mode 100644 index 000000000..2d0fdfe06 Binary files /dev/null and b/assets/amiibo/images/icon_02d80001-00e20502.png differ diff --git a/assets/amiibo/images/icon_02d90001-01c80502.png b/assets/amiibo/images/icon_02d90001-01c80502.png new file mode 100644 index 000000000..60b0c8bcf Binary files /dev/null and b/assets/amiibo/images/icon_02d90001-01c80502.png differ diff --git a/assets/amiibo/images/icon_02da0001-01330502.png b/assets/amiibo/images/icon_02da0001-01330502.png new file mode 100644 index 000000000..41710be35 Binary files /dev/null and b/assets/amiibo/images/icon_02da0001-01330502.png differ diff --git a/assets/amiibo/images/icon_02db0001-005e0502.png b/assets/amiibo/images/icon_02db0001-005e0502.png new file mode 100644 index 000000000..9c275d06d Binary files /dev/null and b/assets/amiibo/images/icon_02db0001-005e0502.png differ diff --git a/assets/amiibo/images/icon_02dc0001-00be0502.png b/assets/amiibo/images/icon_02dc0001-00be0502.png new file mode 100644 index 000000000..3a6bd74e5 Binary files /dev/null and b/assets/amiibo/images/icon_02dc0001-00be0502.png differ diff --git a/assets/amiibo/images/icon_02dd0001-00ea0502.png b/assets/amiibo/images/icon_02dd0001-00ea0502.png new file mode 100644 index 000000000..14f97b6db Binary files /dev/null and b/assets/amiibo/images/icon_02dd0001-00ea0502.png differ diff --git a/assets/amiibo/images/icon_02de0001-009c0502.png b/assets/amiibo/images/icon_02de0001-009c0502.png new file mode 100644 index 000000000..36be4a260 Binary files /dev/null and b/assets/amiibo/images/icon_02de0001-009c0502.png differ diff --git a/assets/amiibo/images/icon_02df0001-01910502.png b/assets/amiibo/images/icon_02df0001-01910502.png new file mode 100644 index 000000000..e5dec7031 Binary files /dev/null and b/assets/amiibo/images/icon_02df0001-01910502.png differ diff --git a/assets/amiibo/images/icon_02e00101-031d0502.png b/assets/amiibo/images/icon_02e00101-031d0502.png new file mode 100644 index 000000000..5433fe297 Binary files /dev/null and b/assets/amiibo/images/icon_02e00101-031d0502.png differ diff --git a/assets/amiibo/images/icon_02ea0001-01800502.png b/assets/amiibo/images/icon_02ea0001-01800502.png new file mode 100644 index 000000000..5a267856d Binary files /dev/null and b/assets/amiibo/images/icon_02ea0001-01800502.png differ diff --git a/assets/amiibo/images/icon_02ea0001-01d50502.png b/assets/amiibo/images/icon_02ea0001-01d50502.png new file mode 100644 index 000000000..4c0a9820a Binary files /dev/null and b/assets/amiibo/images/icon_02ea0001-01d50502.png differ diff --git a/assets/amiibo/images/icon_02eb0001-00de0502.png b/assets/amiibo/images/icon_02eb0001-00de0502.png new file mode 100644 index 000000000..3ebf34fb1 Binary files /dev/null and b/assets/amiibo/images/icon_02eb0001-00de0502.png differ diff --git a/assets/amiibo/images/icon_02ec0001-01c40502.png b/assets/amiibo/images/icon_02ec0001-01c40502.png new file mode 100644 index 000000000..a999e5720 Binary files /dev/null and b/assets/amiibo/images/icon_02ec0001-01c40502.png differ diff --git a/assets/amiibo/images/icon_02ed0001-015a0502.png b/assets/amiibo/images/icon_02ed0001-015a0502.png new file mode 100644 index 000000000..cb5991f34 Binary files /dev/null and b/assets/amiibo/images/icon_02ed0001-015a0502.png differ diff --git a/assets/amiibo/images/icon_02ee0001-01990502.png b/assets/amiibo/images/icon_02ee0001-01990502.png new file mode 100644 index 000000000..e38d197c8 Binary files /dev/null and b/assets/amiibo/images/icon_02ee0001-01990502.png differ diff --git a/assets/amiibo/images/icon_02ef0001-00580502.png b/assets/amiibo/images/icon_02ef0001-00580502.png new file mode 100644 index 000000000..17eb8cbe0 Binary files /dev/null and b/assets/amiibo/images/icon_02ef0001-00580502.png differ diff --git a/assets/amiibo/images/icon_02f00001-00a70502.png b/assets/amiibo/images/icon_02f00001-00a70502.png new file mode 100644 index 000000000..bcdb3ec80 Binary files /dev/null and b/assets/amiibo/images/icon_02f00001-00a70502.png differ diff --git a/assets/amiibo/images/icon_02f10001-01450502.png b/assets/amiibo/images/icon_02f10001-01450502.png new file mode 100644 index 000000000..9db98f772 Binary files /dev/null and b/assets/amiibo/images/icon_02f10001-01450502.png differ diff --git a/assets/amiibo/images/icon_02f20001-00cc0502.png b/assets/amiibo/images/icon_02f20001-00cc0502.png new file mode 100644 index 000000000..172e7641a Binary files /dev/null and b/assets/amiibo/images/icon_02f20001-00cc0502.png differ diff --git a/assets/amiibo/images/icon_02f30001-02f90502.png b/assets/amiibo/images/icon_02f30001-02f90502.png new file mode 100644 index 000000000..d13d3ec6a Binary files /dev/null and b/assets/amiibo/images/icon_02f30001-02f90502.png differ diff --git a/assets/amiibo/images/icon_02f40001-03050502.png b/assets/amiibo/images/icon_02f40001-03050502.png new file mode 100644 index 000000000..029075d2c Binary files /dev/null and b/assets/amiibo/images/icon_02f40001-03050502.png differ diff --git a/assets/amiibo/images/icon_02f80001-01380502.png b/assets/amiibo/images/icon_02f80001-01380502.png new file mode 100644 index 000000000..971ce7bad Binary files /dev/null and b/assets/amiibo/images/icon_02f80001-01380502.png differ diff --git a/assets/amiibo/images/icon_02f90001-01020502.png b/assets/amiibo/images/icon_02f90001-01020502.png new file mode 100644 index 000000000..d21d32053 Binary files /dev/null and b/assets/amiibo/images/icon_02f90001-01020502.png differ diff --git a/assets/amiibo/images/icon_02fa0001-00970502.png b/assets/amiibo/images/icon_02fa0001-00970502.png new file mode 100644 index 000000000..f0e5d0f1e Binary files /dev/null and b/assets/amiibo/images/icon_02fa0001-00970502.png differ diff --git a/assets/amiibo/images/icon_02fb0001-00900502.png b/assets/amiibo/images/icon_02fb0001-00900502.png new file mode 100644 index 000000000..0834b1e52 Binary files /dev/null and b/assets/amiibo/images/icon_02fb0001-00900502.png differ diff --git a/assets/amiibo/images/icon_02fc0001-018f0502.png b/assets/amiibo/images/icon_02fc0001-018f0502.png new file mode 100644 index 000000000..016a029f3 Binary files /dev/null and b/assets/amiibo/images/icon_02fc0001-018f0502.png differ diff --git a/assets/amiibo/images/icon_03070001-00640502.png b/assets/amiibo/images/icon_03070001-00640502.png new file mode 100644 index 000000000..93c08298b Binary files /dev/null and b/assets/amiibo/images/icon_03070001-00640502.png differ diff --git a/assets/amiibo/images/icon_03080001-014d0502.png b/assets/amiibo/images/icon_03080001-014d0502.png new file mode 100644 index 000000000..1171a607e Binary files /dev/null and b/assets/amiibo/images/icon_03080001-014d0502.png differ diff --git a/assets/amiibo/images/icon_03090001-00c60502.png b/assets/amiibo/images/icon_03090001-00c60502.png new file mode 100644 index 000000000..a47eebcf4 Binary files /dev/null and b/assets/amiibo/images/icon_03090001-00c60502.png differ diff --git a/assets/amiibo/images/icon_030a0001-01c70502.png b/assets/amiibo/images/icon_030a0001-01c70502.png new file mode 100644 index 000000000..547aa7125 Binary files /dev/null and b/assets/amiibo/images/icon_030a0001-01c70502.png differ diff --git a/assets/amiibo/images/icon_030b0001-00790502.png b/assets/amiibo/images/icon_030b0001-00790502.png new file mode 100644 index 000000000..8fec4500d Binary files /dev/null and b/assets/amiibo/images/icon_030b0001-00790502.png differ diff --git a/assets/amiibo/images/icon_030c0001-01b80502.png b/assets/amiibo/images/icon_030c0001-01b80502.png new file mode 100644 index 000000000..6ba27c7c8 Binary files /dev/null and b/assets/amiibo/images/icon_030c0001-01b80502.png differ diff --git a/assets/amiibo/images/icon_030d0001-01840502.png b/assets/amiibo/images/icon_030d0001-01840502.png new file mode 100644 index 000000000..f7c81b472 Binary files /dev/null and b/assets/amiibo/images/icon_030d0001-01840502.png differ diff --git a/assets/amiibo/images/icon_030e0001-012f0502.png b/assets/amiibo/images/icon_030e0001-012f0502.png new file mode 100644 index 000000000..0a03741e5 Binary files /dev/null and b/assets/amiibo/images/icon_030e0001-012f0502.png differ diff --git a/assets/amiibo/images/icon_030f0001-016d0502.png b/assets/amiibo/images/icon_030f0001-016d0502.png new file mode 100644 index 000000000..562aec36f Binary files /dev/null and b/assets/amiibo/images/icon_030f0001-016d0502.png differ diff --git a/assets/amiibo/images/icon_03100001-00f80502.png b/assets/amiibo/images/icon_03100001-00f80502.png new file mode 100644 index 000000000..e6bd987e4 Binary files /dev/null and b/assets/amiibo/images/icon_03100001-00f80502.png differ diff --git a/assets/amiibo/images/icon_03110001-00d60502.png b/assets/amiibo/images/icon_03110001-00d60502.png new file mode 100644 index 000000000..5075a1ff6 Binary files /dev/null and b/assets/amiibo/images/icon_03110001-00d60502.png differ diff --git a/assets/amiibo/images/icon_03120001-03090502.png b/assets/amiibo/images/icon_03120001-03090502.png new file mode 100644 index 000000000..d7075e5e1 Binary files /dev/null and b/assets/amiibo/images/icon_03120001-03090502.png differ diff --git a/assets/amiibo/images/icon_03130001-01210502.png b/assets/amiibo/images/icon_03130001-01210502.png new file mode 100644 index 000000000..272cd46c1 Binary files /dev/null and b/assets/amiibo/images/icon_03130001-01210502.png differ diff --git a/assets/amiibo/images/icon_03140001-02f40502.png b/assets/amiibo/images/icon_03140001-02f40502.png new file mode 100644 index 000000000..6eb1f94f1 Binary files /dev/null and b/assets/amiibo/images/icon_03140001-02f40502.png differ diff --git a/assets/amiibo/images/icon_03160001-01c00502.png b/assets/amiibo/images/icon_03160001-01c00502.png new file mode 100644 index 000000000..f9098de66 Binary files /dev/null and b/assets/amiibo/images/icon_03160001-01c00502.png differ diff --git a/assets/amiibo/images/icon_03170001-00a60502.png b/assets/amiibo/images/icon_03170001-00a60502.png new file mode 100644 index 000000000..bad1e9869 Binary files /dev/null and b/assets/amiibo/images/icon_03170001-00a60502.png differ diff --git a/assets/amiibo/images/icon_03180001-006c0502.png b/assets/amiibo/images/icon_03180001-006c0502.png new file mode 100644 index 000000000..da154406c Binary files /dev/null and b/assets/amiibo/images/icon_03180001-006c0502.png differ diff --git a/assets/amiibo/images/icon_03230001-00760502.png b/assets/amiibo/images/icon_03230001-00760502.png new file mode 100644 index 000000000..48414da5e Binary files /dev/null and b/assets/amiibo/images/icon_03230001-00760502.png differ diff --git a/assets/amiibo/images/icon_03240001-01890502.png b/assets/amiibo/images/icon_03240001-01890502.png new file mode 100644 index 000000000..e1462000f Binary files /dev/null and b/assets/amiibo/images/icon_03240001-01890502.png differ diff --git a/assets/amiibo/images/icon_03250001-010a0502.png b/assets/amiibo/images/icon_03250001-010a0502.png new file mode 100644 index 000000000..6f99a0539 Binary files /dev/null and b/assets/amiibo/images/icon_03250001-010a0502.png differ diff --git a/assets/amiibo/images/icon_03260001-01390502.png b/assets/amiibo/images/icon_03260001-01390502.png new file mode 100644 index 000000000..2ef5b99a8 Binary files /dev/null and b/assets/amiibo/images/icon_03260001-01390502.png differ diff --git a/assets/amiibo/images/icon_03270001-01c30502.png b/assets/amiibo/images/icon_03270001-01c30502.png new file mode 100644 index 000000000..07a18b1ac Binary files /dev/null and b/assets/amiibo/images/icon_03270001-01c30502.png differ diff --git a/assets/amiibo/images/icon_03280001-02eb0502.png b/assets/amiibo/images/icon_03280001-02eb0502.png new file mode 100644 index 000000000..e1041ad62 Binary files /dev/null and b/assets/amiibo/images/icon_03280001-02eb0502.png differ diff --git a/assets/amiibo/images/icon_03290001-009d0502.png b/assets/amiibo/images/icon_03290001-009d0502.png new file mode 100644 index 000000000..9dc2efb62 Binary files /dev/null and b/assets/amiibo/images/icon_03290001-009d0502.png differ diff --git a/assets/amiibo/images/icon_032a0001-03070502.png b/assets/amiibo/images/icon_032a0001-03070502.png new file mode 100644 index 000000000..d66af0365 Binary files /dev/null and b/assets/amiibo/images/icon_032a0001-03070502.png differ diff --git a/assets/amiibo/images/icon_032c0001-01480502.png b/assets/amiibo/images/icon_032c0001-01480502.png new file mode 100644 index 000000000..f104dd4ca Binary files /dev/null and b/assets/amiibo/images/icon_032c0001-01480502.png differ diff --git a/assets/amiibo/images/icon_032d0001-00bc0502.png b/assets/amiibo/images/icon_032d0001-00bc0502.png new file mode 100644 index 000000000..2ecb354fc Binary files /dev/null and b/assets/amiibo/images/icon_032d0001-00bc0502.png differ diff --git a/assets/amiibo/images/icon_032e0101-031c0502.png b/assets/amiibo/images/icon_032e0101-031c0502.png new file mode 100644 index 000000000..322986fa5 Binary files /dev/null and b/assets/amiibo/images/icon_032e0101-031c0502.png differ diff --git a/assets/amiibo/images/icon_03380001-011d0502.png b/assets/amiibo/images/icon_03380001-011d0502.png new file mode 100644 index 000000000..11061b7e5 Binary files /dev/null and b/assets/amiibo/images/icon_03380001-011d0502.png differ diff --git a/assets/amiibo/images/icon_03390001-01b10502.png b/assets/amiibo/images/icon_03390001-01b10502.png new file mode 100644 index 000000000..030259000 Binary files /dev/null and b/assets/amiibo/images/icon_03390001-01b10502.png differ diff --git a/assets/amiibo/images/icon_033a0001-01cc0502.png b/assets/amiibo/images/icon_033a0001-01cc0502.png new file mode 100644 index 000000000..352a20851 Binary files /dev/null and b/assets/amiibo/images/icon_033a0001-01cc0502.png differ diff --git a/assets/amiibo/images/icon_033b0001-00fa0502.png b/assets/amiibo/images/icon_033b0001-00fa0502.png new file mode 100644 index 000000000..ac370dd6f Binary files /dev/null and b/assets/amiibo/images/icon_033b0001-00fa0502.png differ diff --git a/assets/amiibo/images/icon_033c0001-01000502.png b/assets/amiibo/images/icon_033c0001-01000502.png new file mode 100644 index 000000000..cc28dc6b2 Binary files /dev/null and b/assets/amiibo/images/icon_033c0001-01000502.png differ diff --git a/assets/amiibo/images/icon_033d0001-013a0502.png b/assets/amiibo/images/icon_033d0001-013a0502.png new file mode 100644 index 000000000..cf23f8b8c Binary files /dev/null and b/assets/amiibo/images/icon_033d0001-013a0502.png differ diff --git a/assets/amiibo/images/icon_033e0001-01a20502.png b/assets/amiibo/images/icon_033e0001-01a20502.png new file mode 100644 index 000000000..ae87a860a Binary files /dev/null and b/assets/amiibo/images/icon_033e0001-01a20502.png differ diff --git a/assets/amiibo/images/icon_033f0001-008f0502.png b/assets/amiibo/images/icon_033f0001-008f0502.png new file mode 100644 index 000000000..3d9ebcb22 Binary files /dev/null and b/assets/amiibo/images/icon_033f0001-008f0502.png differ diff --git a/assets/amiibo/images/icon_03410001-030e0502.png b/assets/amiibo/images/icon_03410001-030e0502.png new file mode 100644 index 000000000..c1b04a37a Binary files /dev/null and b/assets/amiibo/images/icon_03410001-030e0502.png differ diff --git a/assets/amiibo/images/icon_03420001-01280502.png b/assets/amiibo/images/icon_03420001-01280502.png new file mode 100644 index 000000000..86296d34d Binary files /dev/null and b/assets/amiibo/images/icon_03420001-01280502.png differ diff --git a/assets/amiibo/images/icon_03430001-02ef0502.png b/assets/amiibo/images/icon_03430001-02ef0502.png new file mode 100644 index 000000000..15bc190cc Binary files /dev/null and b/assets/amiibo/images/icon_03430001-02ef0502.png differ diff --git a/assets/amiibo/images/icon_03440001-00c50502.png b/assets/amiibo/images/icon_03440001-00c50502.png new file mode 100644 index 000000000..910935482 Binary files /dev/null and b/assets/amiibo/images/icon_03440001-00c50502.png differ diff --git a/assets/amiibo/images/icon_03450001-005f0502.png b/assets/amiibo/images/icon_03450001-005f0502.png new file mode 100644 index 000000000..47b7d63d3 Binary files /dev/null and b/assets/amiibo/images/icon_03450001-005f0502.png differ diff --git a/assets/amiibo/images/icon_03470001-03020502.png b/assets/amiibo/images/icon_03470001-03020502.png new file mode 100644 index 000000000..177f635e9 Binary files /dev/null and b/assets/amiibo/images/icon_03470001-03020502.png differ diff --git a/assets/amiibo/images/icon_03480001-006b0502.png b/assets/amiibo/images/icon_03480001-006b0502.png new file mode 100644 index 000000000..280be9772 Binary files /dev/null and b/assets/amiibo/images/icon_03480001-006b0502.png differ diff --git a/assets/amiibo/images/icon_03490001-018d0502.png b/assets/amiibo/images/icon_03490001-018d0502.png new file mode 100644 index 000000000..4588dcd51 Binary files /dev/null and b/assets/amiibo/images/icon_03490001-018d0502.png differ diff --git a/assets/amiibo/images/icon_034a0001-01430502.png b/assets/amiibo/images/icon_034a0001-01430502.png new file mode 100644 index 000000000..207e31c7d Binary files /dev/null and b/assets/amiibo/images/icon_034a0001-01430502.png differ diff --git a/assets/amiibo/images/icon_034b0001-009f0502.png b/assets/amiibo/images/icon_034b0001-009f0502.png new file mode 100644 index 000000000..d2372fe58 Binary files /dev/null and b/assets/amiibo/images/icon_034b0001-009f0502.png differ diff --git a/assets/amiibo/images/icon_03560001-01350502.png b/assets/amiibo/images/icon_03560001-01350502.png new file mode 100644 index 000000000..91bab9566 Binary files /dev/null and b/assets/amiibo/images/icon_03560001-01350502.png differ diff --git a/assets/amiibo/images/icon_03570001-00eb0502.png b/assets/amiibo/images/icon_03570001-00eb0502.png new file mode 100644 index 000000000..18231d6a1 Binary files /dev/null and b/assets/amiibo/images/icon_03570001-00eb0502.png differ diff --git a/assets/amiibo/images/icon_03580001-02fa0502.png b/assets/amiibo/images/icon_03580001-02fa0502.png new file mode 100644 index 000000000..51b60e18b Binary files /dev/null and b/assets/amiibo/images/icon_03580001-02fa0502.png differ diff --git a/assets/amiibo/images/icon_035a0001-00850502.png b/assets/amiibo/images/icon_035a0001-00850502.png new file mode 100644 index 000000000..caff771e8 Binary files /dev/null and b/assets/amiibo/images/icon_035a0001-00850502.png differ diff --git a/assets/amiibo/images/icon_035c0001-01290502.png b/assets/amiibo/images/icon_035c0001-01290502.png new file mode 100644 index 000000000..36a08c4fe Binary files /dev/null and b/assets/amiibo/images/icon_035c0001-01290502.png differ diff --git a/assets/amiibo/images/icon_035d0001-00c90502.png b/assets/amiibo/images/icon_035d0001-00c90502.png new file mode 100644 index 000000000..1e3eb5fae Binary files /dev/null and b/assets/amiibo/images/icon_035d0001-00c90502.png differ diff --git a/assets/amiibo/images/icon_035e0001-018e0502.png b/assets/amiibo/images/icon_035e0001-018e0502.png new file mode 100644 index 000000000..458379e12 Binary files /dev/null and b/assets/amiibo/images/icon_035e0001-018e0502.png differ diff --git a/assets/amiibo/images/icon_03690001-00d30502.png b/assets/amiibo/images/icon_03690001-00d30502.png new file mode 100644 index 000000000..dfe73c64b Binary files /dev/null and b/assets/amiibo/images/icon_03690001-00d30502.png differ diff --git a/assets/amiibo/images/icon_036a0001-019d0502.png b/assets/amiibo/images/icon_036a0001-019d0502.png new file mode 100644 index 000000000..28ea32165 Binary files /dev/null and b/assets/amiibo/images/icon_036a0001-019d0502.png differ diff --git a/assets/amiibo/images/icon_036b0001-018b0502.png b/assets/amiibo/images/icon_036b0001-018b0502.png new file mode 100644 index 000000000..8e6d1ae2a Binary files /dev/null and b/assets/amiibo/images/icon_036b0001-018b0502.png differ diff --git a/assets/amiibo/images/icon_036d0001-03040502.png b/assets/amiibo/images/icon_036d0001-03040502.png new file mode 100644 index 000000000..e7f92cd53 Binary files /dev/null and b/assets/amiibo/images/icon_036d0001-03040502.png differ diff --git a/assets/amiibo/images/icon_036e0001-02fb0502.png b/assets/amiibo/images/icon_036e0001-02fb0502.png new file mode 100644 index 000000000..4faaef80d Binary files /dev/null and b/assets/amiibo/images/icon_036e0001-02fb0502.png differ diff --git a/assets/amiibo/images/icon_03700001-015d0502.png b/assets/amiibo/images/icon_03700001-015d0502.png new file mode 100644 index 000000000..6ad05a10b Binary files /dev/null and b/assets/amiibo/images/icon_03700001-015d0502.png differ diff --git a/assets/amiibo/images/icon_03710001-005c0502.png b/assets/amiibo/images/icon_03710001-005c0502.png new file mode 100644 index 000000000..03f3d8c71 Binary files /dev/null and b/assets/amiibo/images/icon_03710001-005c0502.png differ diff --git a/assets/amiibo/images/icon_03720001-010b0502.png b/assets/amiibo/images/icon_03720001-010b0502.png new file mode 100644 index 000000000..e3fb76304 Binary files /dev/null and b/assets/amiibo/images/icon_03720001-010b0502.png differ diff --git a/assets/amiibo/images/icon_03730001-01340502.png b/assets/amiibo/images/icon_03730001-01340502.png new file mode 100644 index 000000000..14909d468 Binary files /dev/null and b/assets/amiibo/images/icon_03730001-01340502.png differ diff --git a/assets/amiibo/images/icon_03740101-03190502.png b/assets/amiibo/images/icon_03740101-03190502.png new file mode 100644 index 000000000..e83cf5925 Binary files /dev/null and b/assets/amiibo/images/icon_03740101-03190502.png differ diff --git a/assets/amiibo/images/icon_037e0001-01560502.png b/assets/amiibo/images/icon_037e0001-01560502.png new file mode 100644 index 000000000..fdbe3f50b Binary files /dev/null and b/assets/amiibo/images/icon_037e0001-01560502.png differ diff --git a/assets/amiibo/images/icon_037f0001-01aa0502.png b/assets/amiibo/images/icon_037f0001-01aa0502.png new file mode 100644 index 000000000..b36e5eb3d Binary files /dev/null and b/assets/amiibo/images/icon_037f0001-01aa0502.png differ diff --git a/assets/amiibo/images/icon_03800001-01870502.png b/assets/amiibo/images/icon_03800001-01870502.png new file mode 100644 index 000000000..0c73368ed Binary files /dev/null and b/assets/amiibo/images/icon_03800001-01870502.png differ diff --git a/assets/amiibo/images/icon_03810001-00d50502.png b/assets/amiibo/images/icon_03810001-00d50502.png new file mode 100644 index 000000000..eeb9db20c Binary files /dev/null and b/assets/amiibo/images/icon_03810001-00d50502.png differ diff --git a/assets/amiibo/images/icon_03820001-016b0502.png b/assets/amiibo/images/icon_03820001-016b0502.png new file mode 100644 index 000000000..b53577381 Binary files /dev/null and b/assets/amiibo/images/icon_03820001-016b0502.png differ diff --git a/assets/amiibo/images/icon_03830001-009b0502.png b/assets/amiibo/images/icon_03830001-009b0502.png new file mode 100644 index 000000000..21f77c24c Binary files /dev/null and b/assets/amiibo/images/icon_03830001-009b0502.png differ diff --git a/assets/amiibo/images/icon_03840001-00860502.png b/assets/amiibo/images/icon_03840001-00860502.png new file mode 100644 index 000000000..fb98b0086 Binary files /dev/null and b/assets/amiibo/images/icon_03840001-00860502.png differ diff --git a/assets/amiibo/images/icon_03850001-01060502.png b/assets/amiibo/images/icon_03850001-01060502.png new file mode 100644 index 000000000..5f306ef46 Binary files /dev/null and b/assets/amiibo/images/icon_03850001-01060502.png differ diff --git a/assets/amiibo/images/icon_03900001-01850502.png b/assets/amiibo/images/icon_03900001-01850502.png new file mode 100644 index 000000000..5118b7d58 Binary files /dev/null and b/assets/amiibo/images/icon_03900001-01850502.png differ diff --git a/assets/amiibo/images/icon_03920001-01270502.png b/assets/amiibo/images/icon_03920001-01270502.png new file mode 100644 index 000000000..a57a9090b Binary files /dev/null and b/assets/amiibo/images/icon_03920001-01270502.png differ diff --git a/assets/amiibo/images/icon_03930001-00a00502.png b/assets/amiibo/images/icon_03930001-00a00502.png new file mode 100644 index 000000000..0d1b2b0d9 Binary files /dev/null and b/assets/amiibo/images/icon_03930001-00a00502.png differ diff --git a/assets/amiibo/images/icon_03940001-00890502.png b/assets/amiibo/images/icon_03940001-00890502.png new file mode 100644 index 000000000..f39909723 Binary files /dev/null and b/assets/amiibo/images/icon_03940001-00890502.png differ diff --git a/assets/amiibo/images/icon_03950001-02fc0502.png b/assets/amiibo/images/icon_03950001-02fc0502.png new file mode 100644 index 000000000..4dbb794f2 Binary files /dev/null and b/assets/amiibo/images/icon_03950001-02fc0502.png differ diff --git a/assets/amiibo/images/icon_03980001-00bf0502.png b/assets/amiibo/images/icon_03980001-00bf0502.png new file mode 100644 index 000000000..ff4abd930 Binary files /dev/null and b/assets/amiibo/images/icon_03980001-00bf0502.png differ diff --git a/assets/amiibo/images/icon_03990001-01c20502.png b/assets/amiibo/images/icon_03990001-01c20502.png new file mode 100644 index 000000000..591d765bf Binary files /dev/null and b/assets/amiibo/images/icon_03990001-01c20502.png differ diff --git a/assets/amiibo/images/icon_03a40001-014f0502.png b/assets/amiibo/images/icon_03a40001-014f0502.png new file mode 100644 index 000000000..8d994aba0 Binary files /dev/null and b/assets/amiibo/images/icon_03a40001-014f0502.png differ diff --git a/assets/amiibo/images/icon_03a50001-015b0502.png b/assets/amiibo/images/icon_03a50001-015b0502.png new file mode 100644 index 000000000..6a7920391 Binary files /dev/null and b/assets/amiibo/images/icon_03a50001-015b0502.png differ diff --git a/assets/amiibo/images/icon_03a60001-00c80502.png b/assets/amiibo/images/icon_03a60001-00c80502.png new file mode 100644 index 000000000..bf5eabed8 Binary files /dev/null and b/assets/amiibo/images/icon_03a60001-00c80502.png differ diff --git a/assets/amiibo/images/icon_03a70001-01a10502.png b/assets/amiibo/images/icon_03a70001-01a10502.png new file mode 100644 index 000000000..08ed537e6 Binary files /dev/null and b/assets/amiibo/images/icon_03a70001-01a10502.png differ diff --git a/assets/amiibo/images/icon_03a80001-00910502.png b/assets/amiibo/images/icon_03a80001-00910502.png new file mode 100644 index 000000000..30f550e72 Binary files /dev/null and b/assets/amiibo/images/icon_03a80001-00910502.png differ diff --git a/assets/amiibo/images/icon_03a90001-00710502.png b/assets/amiibo/images/icon_03a90001-00710502.png new file mode 100644 index 000000000..d944f0375 Binary files /dev/null and b/assets/amiibo/images/icon_03a90001-00710502.png differ diff --git a/assets/amiibo/images/icon_03aa0001-00e60502.png b/assets/amiibo/images/icon_03aa0001-00e60502.png new file mode 100644 index 000000000..50cdff012 Binary files /dev/null and b/assets/amiibo/images/icon_03aa0001-00e60502.png differ diff --git a/assets/amiibo/images/icon_03ab0001-03160502.png b/assets/amiibo/images/icon_03ab0001-03160502.png new file mode 100644 index 000000000..3d63e2ca0 Binary files /dev/null and b/assets/amiibo/images/icon_03ab0001-03160502.png differ diff --git a/assets/amiibo/images/icon_03ac0001-01880502.png b/assets/amiibo/images/icon_03ac0001-01880502.png new file mode 100644 index 000000000..90341b5b9 Binary files /dev/null and b/assets/amiibo/images/icon_03ac0001-01880502.png differ diff --git a/assets/amiibo/images/icon_03ad0001-01b20502.png b/assets/amiibo/images/icon_03ad0001-01b20502.png new file mode 100644 index 000000000..5c144b07f Binary files /dev/null and b/assets/amiibo/images/icon_03ad0001-01b20502.png differ diff --git a/assets/amiibo/images/icon_03ae0001-00870502.png b/assets/amiibo/images/icon_03ae0001-00870502.png new file mode 100644 index 000000000..765d97b33 Binary files /dev/null and b/assets/amiibo/images/icon_03ae0001-00870502.png differ diff --git a/assets/amiibo/images/icon_03af0001-012c0502.png b/assets/amiibo/images/icon_03af0001-012c0502.png new file mode 100644 index 000000000..23de0e475 Binary files /dev/null and b/assets/amiibo/images/icon_03af0001-012c0502.png differ diff --git a/assets/amiibo/images/icon_03b00001-01a90502.png b/assets/amiibo/images/icon_03b00001-01a90502.png new file mode 100644 index 000000000..585d160fa Binary files /dev/null and b/assets/amiibo/images/icon_03b00001-01a90502.png differ diff --git a/assets/amiibo/images/icon_03b10001-00f00502.png b/assets/amiibo/images/icon_03b10001-00f00502.png new file mode 100644 index 000000000..9a9e5753a Binary files /dev/null and b/assets/amiibo/images/icon_03b10001-00f00502.png differ diff --git a/assets/amiibo/images/icon_03bc0001-008a0502.png b/assets/amiibo/images/icon_03bc0001-008a0502.png new file mode 100644 index 000000000..e6961eb13 Binary files /dev/null and b/assets/amiibo/images/icon_03bc0001-008a0502.png differ diff --git a/assets/amiibo/images/icon_03bd0001-00f90502.png b/assets/amiibo/images/icon_03bd0001-00f90502.png new file mode 100644 index 000000000..6a8dd6768 Binary files /dev/null and b/assets/amiibo/images/icon_03bd0001-00f90502.png differ diff --git a/assets/amiibo/images/icon_03be0001-01980502.png b/assets/amiibo/images/icon_03be0001-01980502.png new file mode 100644 index 000000000..8c0007366 Binary files /dev/null and b/assets/amiibo/images/icon_03be0001-01980502.png differ diff --git a/assets/amiibo/images/icon_03bf0001-01bc0502.png b/assets/amiibo/images/icon_03bf0001-01bc0502.png new file mode 100644 index 000000000..270bf1180 Binary files /dev/null and b/assets/amiibo/images/icon_03bf0001-01bc0502.png differ diff --git a/assets/amiibo/images/icon_03c00001-03100502.png b/assets/amiibo/images/icon_03c00001-03100502.png new file mode 100644 index 000000000..67dabbb83 Binary files /dev/null and b/assets/amiibo/images/icon_03c00001-03100502.png differ diff --git a/assets/amiibo/images/icon_03c10001-00bb0502.png b/assets/amiibo/images/icon_03c10001-00bb0502.png new file mode 100644 index 000000000..20f70a119 Binary files /dev/null and b/assets/amiibo/images/icon_03c10001-00bb0502.png differ diff --git a/assets/amiibo/images/icon_03c40001-012b0502.png b/assets/amiibo/images/icon_03c40001-012b0502.png new file mode 100644 index 000000000..9ae17c709 Binary files /dev/null and b/assets/amiibo/images/icon_03c40001-012b0502.png differ diff --git a/assets/amiibo/images/icon_03c50001-015c0502.png b/assets/amiibo/images/icon_03c50001-015c0502.png new file mode 100644 index 000000000..dfe8385cd Binary files /dev/null and b/assets/amiibo/images/icon_03c50001-015c0502.png differ diff --git a/assets/amiibo/images/icon_03c60001-00930502.png b/assets/amiibo/images/icon_03c60001-00930502.png new file mode 100644 index 000000000..10095b607 Binary files /dev/null and b/assets/amiibo/images/icon_03c60001-00930502.png differ diff --git a/assets/amiibo/images/icon_03d10001-00c20502.png b/assets/amiibo/images/icon_03d10001-00c20502.png new file mode 100644 index 000000000..98de5e48f Binary files /dev/null and b/assets/amiibo/images/icon_03d10001-00c20502.png differ diff --git a/assets/amiibo/images/icon_03d20001-00e50502.png b/assets/amiibo/images/icon_03d20001-00e50502.png new file mode 100644 index 000000000..fe4bd1853 Binary files /dev/null and b/assets/amiibo/images/icon_03d20001-00e50502.png differ diff --git a/assets/amiibo/images/icon_03d30001-02f30502.png b/assets/amiibo/images/icon_03d30001-02f30502.png new file mode 100644 index 000000000..66ed073e3 Binary files /dev/null and b/assets/amiibo/images/icon_03d30001-02f30502.png differ diff --git a/assets/amiibo/images/icon_03d60001-01570502.png b/assets/amiibo/images/icon_03d60001-01570502.png new file mode 100644 index 000000000..b0e001da4 Binary files /dev/null and b/assets/amiibo/images/icon_03d60001-01570502.png differ diff --git a/assets/amiibo/images/icon_03d70001-01b40502.png b/assets/amiibo/images/icon_03d70001-01b40502.png new file mode 100644 index 000000000..6bf80eee1 Binary files /dev/null and b/assets/amiibo/images/icon_03d70001-01b40502.png differ diff --git a/assets/amiibo/images/icon_03d90001-01a50502.png b/assets/amiibo/images/icon_03d90001-01a50502.png new file mode 100644 index 000000000..7b6feae20 Binary files /dev/null and b/assets/amiibo/images/icon_03d90001-01a50502.png differ diff --git a/assets/amiibo/images/icon_03da0001-01510502.png b/assets/amiibo/images/icon_03da0001-01510502.png new file mode 100644 index 000000000..2b99f8760 Binary files /dev/null and b/assets/amiibo/images/icon_03da0001-01510502.png differ diff --git a/assets/amiibo/images/icon_03db0001-006d0502.png b/assets/amiibo/images/icon_03db0001-006d0502.png new file mode 100644 index 000000000..2251a5cc0 Binary files /dev/null and b/assets/amiibo/images/icon_03db0001-006d0502.png differ diff --git a/assets/amiibo/images/icon_03e60001-00ec0502.png b/assets/amiibo/images/icon_03e60001-00ec0502.png new file mode 100644 index 000000000..9dfad5926 Binary files /dev/null and b/assets/amiibo/images/icon_03e60001-00ec0502.png differ diff --git a/assets/amiibo/images/icon_03e70001-012a0502.png b/assets/amiibo/images/icon_03e70001-012a0502.png new file mode 100644 index 000000000..b075a3210 Binary files /dev/null and b/assets/amiibo/images/icon_03e70001-012a0502.png differ diff --git a/assets/amiibo/images/icon_03e80001-02f50502.png b/assets/amiibo/images/icon_03e80001-02f50502.png new file mode 100644 index 000000000..3dfeac4db Binary files /dev/null and b/assets/amiibo/images/icon_03e80001-02f50502.png differ diff --git a/assets/amiibo/images/icon_03ea0001-030b0502.png b/assets/amiibo/images/icon_03ea0001-030b0502.png new file mode 100644 index 000000000..2994374cd Binary files /dev/null and b/assets/amiibo/images/icon_03ea0001-030b0502.png differ diff --git a/assets/amiibo/images/icon_03ec0001-01830502.png b/assets/amiibo/images/icon_03ec0001-01830502.png new file mode 100644 index 000000000..f36a4c5d1 Binary files /dev/null and b/assets/amiibo/images/icon_03ec0001-01830502.png differ diff --git a/assets/amiibo/images/icon_03ed0001-01a30502.png b/assets/amiibo/images/icon_03ed0001-01a30502.png new file mode 100644 index 000000000..82e50fe0a Binary files /dev/null and b/assets/amiibo/images/icon_03ed0001-01a30502.png differ diff --git a/assets/amiibo/images/icon_03ee0001-008b0502.png b/assets/amiibo/images/icon_03ee0001-008b0502.png new file mode 100644 index 000000000..b147f180f Binary files /dev/null and b/assets/amiibo/images/icon_03ee0001-008b0502.png differ diff --git a/assets/amiibo/images/icon_03fa0001-00d00502.png b/assets/amiibo/images/icon_03fa0001-00d00502.png new file mode 100644 index 000000000..d36868c07 Binary files /dev/null and b/assets/amiibo/images/icon_03fa0001-00d00502.png differ diff --git a/assets/amiibo/images/icon_03fb0001-01cf0502.png b/assets/amiibo/images/icon_03fb0001-01cf0502.png new file mode 100644 index 000000000..51b844c62 Binary files /dev/null and b/assets/amiibo/images/icon_03fb0001-01cf0502.png differ diff --git a/assets/amiibo/images/icon_03fc0001-01470502.png b/assets/amiibo/images/icon_03fc0001-01470502.png new file mode 100644 index 000000000..a2e0d07cd Binary files /dev/null and b/assets/amiibo/images/icon_03fc0001-01470502.png differ diff --git a/assets/amiibo/images/icon_03fd0001-01580502.png b/assets/amiibo/images/icon_03fd0001-01580502.png new file mode 100644 index 000000000..e7260d1c5 Binary files /dev/null and b/assets/amiibo/images/icon_03fd0001-01580502.png differ diff --git a/assets/amiibo/images/icon_03fe0001-01a40502.png b/assets/amiibo/images/icon_03fe0001-01a40502.png new file mode 100644 index 000000000..2a453accd Binary files /dev/null and b/assets/amiibo/images/icon_03fe0001-01a40502.png differ diff --git a/assets/amiibo/images/icon_03ff0001-00f40502.png b/assets/amiibo/images/icon_03ff0001-00f40502.png new file mode 100644 index 000000000..5e96cc5fb Binary files /dev/null and b/assets/amiibo/images/icon_03ff0001-00f40502.png differ diff --git a/assets/amiibo/images/icon_04000001-006f0502.png b/assets/amiibo/images/icon_04000001-006f0502.png new file mode 100644 index 000000000..5339ed84e Binary files /dev/null and b/assets/amiibo/images/icon_04000001-006f0502.png differ diff --git a/assets/amiibo/images/icon_04010001-00660502.png b/assets/amiibo/images/icon_04010001-00660502.png new file mode 100644 index 000000000..01d2bf99f Binary files /dev/null and b/assets/amiibo/images/icon_04010001-00660502.png differ diff --git a/assets/amiibo/images/icon_040c0001-01590502.png b/assets/amiibo/images/icon_040c0001-01590502.png new file mode 100644 index 000000000..47cc39b9a Binary files /dev/null and b/assets/amiibo/images/icon_040c0001-01590502.png differ diff --git a/assets/amiibo/images/icon_040d0001-00780502.png b/assets/amiibo/images/icon_040d0001-00780502.png new file mode 100644 index 000000000..5bd30b5ad Binary files /dev/null and b/assets/amiibo/images/icon_040d0001-00780502.png differ diff --git a/assets/amiibo/images/icon_040e0001-00880502.png b/assets/amiibo/images/icon_040e0001-00880502.png new file mode 100644 index 000000000..49849c3cb Binary files /dev/null and b/assets/amiibo/images/icon_040e0001-00880502.png differ diff --git a/assets/amiibo/images/icon_040f0001-01500502.png b/assets/amiibo/images/icon_040f0001-01500502.png new file mode 100644 index 000000000..af18b2c22 Binary files /dev/null and b/assets/amiibo/images/icon_040f0001-01500502.png differ diff --git a/assets/amiibo/images/icon_04100001-007f0502.png b/assets/amiibo/images/icon_04100001-007f0502.png new file mode 100644 index 000000000..b7bb883f3 Binary files /dev/null and b/assets/amiibo/images/icon_04100001-007f0502.png differ diff --git a/assets/amiibo/images/icon_04110001-01ab0502.png b/assets/amiibo/images/icon_04110001-01ab0502.png new file mode 100644 index 000000000..9f058ab98 Binary files /dev/null and b/assets/amiibo/images/icon_04110001-01ab0502.png differ diff --git a/assets/amiibo/images/icon_04140001-030a0502.png b/assets/amiibo/images/icon_04140001-030a0502.png new file mode 100644 index 000000000..e3a2d3b67 Binary files /dev/null and b/assets/amiibo/images/icon_04140001-030a0502.png differ diff --git a/assets/amiibo/images/icon_04150001-01bb0502.png b/assets/amiibo/images/icon_04150001-01bb0502.png new file mode 100644 index 000000000..4ea1fcc40 Binary files /dev/null and b/assets/amiibo/images/icon_04150001-01bb0502.png differ diff --git a/assets/amiibo/images/icon_04160001-00fb0502.png b/assets/amiibo/images/icon_04160001-00fb0502.png new file mode 100644 index 000000000..55288c8af Binary files /dev/null and b/assets/amiibo/images/icon_04160001-00fb0502.png differ diff --git a/assets/amiibo/images/icon_04180001-00d80502.png b/assets/amiibo/images/icon_04180001-00d80502.png new file mode 100644 index 000000000..02940a384 Binary files /dev/null and b/assets/amiibo/images/icon_04180001-00d80502.png differ diff --git a/assets/amiibo/images/icon_041a0001-00e00502.png b/assets/amiibo/images/icon_041a0001-00e00502.png new file mode 100644 index 000000000..a100014ba Binary files /dev/null and b/assets/amiibo/images/icon_041a0001-00e00502.png differ diff --git a/assets/amiibo/images/icon_041b0001-00f10502.png b/assets/amiibo/images/icon_041b0001-00f10502.png new file mode 100644 index 000000000..371ed0838 Binary files /dev/null and b/assets/amiibo/images/icon_041b0001-00f10502.png differ diff --git a/assets/amiibo/images/icon_041c0001-01410502.png b/assets/amiibo/images/icon_041c0001-01410502.png new file mode 100644 index 000000000..f0f3241e9 Binary files /dev/null and b/assets/amiibo/images/icon_041c0001-01410502.png differ diff --git a/assets/amiibo/images/icon_041d0001-018a0502.png b/assets/amiibo/images/icon_041d0001-018a0502.png new file mode 100644 index 000000000..1009e18c4 Binary files /dev/null and b/assets/amiibo/images/icon_041d0001-018a0502.png differ diff --git a/assets/amiibo/images/icon_041e0001-015f0502.png b/assets/amiibo/images/icon_041e0001-015f0502.png new file mode 100644 index 000000000..a9d532763 Binary files /dev/null and b/assets/amiibo/images/icon_041e0001-015f0502.png differ diff --git a/assets/amiibo/images/icon_04290001-00700502.png b/assets/amiibo/images/icon_04290001-00700502.png new file mode 100644 index 000000000..b75f15fd4 Binary files /dev/null and b/assets/amiibo/images/icon_04290001-00700502.png differ diff --git a/assets/amiibo/images/icon_042a0001-012d0502.png b/assets/amiibo/images/icon_042a0001-012d0502.png new file mode 100644 index 000000000..d6b0901d9 Binary files /dev/null and b/assets/amiibo/images/icon_042a0001-012d0502.png differ diff --git a/assets/amiibo/images/icon_042b0001-01af0502.png b/assets/amiibo/images/icon_042b0001-01af0502.png new file mode 100644 index 000000000..568784816 Binary files /dev/null and b/assets/amiibo/images/icon_042b0001-01af0502.png differ diff --git a/assets/amiibo/images/icon_04360001-01940502.png b/assets/amiibo/images/icon_04360001-01940502.png new file mode 100644 index 000000000..dfe7ab602 Binary files /dev/null and b/assets/amiibo/images/icon_04360001-01940502.png differ diff --git a/assets/amiibo/images/icon_04370001-01050502.png b/assets/amiibo/images/icon_04370001-01050502.png new file mode 100644 index 000000000..b4105b6a8 Binary files /dev/null and b/assets/amiibo/images/icon_04370001-01050502.png differ diff --git a/assets/amiibo/images/icon_04380001-03000502.png b/assets/amiibo/images/icon_04380001-03000502.png new file mode 100644 index 000000000..1febdfad9 Binary files /dev/null and b/assets/amiibo/images/icon_04380001-03000502.png differ diff --git a/assets/amiibo/images/icon_04390001-03110502.png b/assets/amiibo/images/icon_04390001-03110502.png new file mode 100644 index 000000000..effd0143b Binary files /dev/null and b/assets/amiibo/images/icon_04390001-03110502.png differ diff --git a/assets/amiibo/images/icon_043b0001-03030502.png b/assets/amiibo/images/icon_043b0001-03030502.png new file mode 100644 index 000000000..11d4f5767 Binary files /dev/null and b/assets/amiibo/images/icon_043b0001-03030502.png differ diff --git a/assets/amiibo/images/icon_043c0001-01cb0502.png b/assets/amiibo/images/icon_043c0001-01cb0502.png new file mode 100644 index 000000000..732f4b31f Binary files /dev/null and b/assets/amiibo/images/icon_043c0001-01cb0502.png differ diff --git a/assets/amiibo/images/icon_043d0001-007c0502.png b/assets/amiibo/images/icon_043d0001-007c0502.png new file mode 100644 index 000000000..34e958ff7 Binary files /dev/null and b/assets/amiibo/images/icon_043d0001-007c0502.png differ diff --git a/assets/amiibo/images/icon_043e0001-01490502.png b/assets/amiibo/images/icon_043e0001-01490502.png new file mode 100644 index 000000000..9ba22b901 Binary files /dev/null and b/assets/amiibo/images/icon_043e0001-01490502.png differ diff --git a/assets/amiibo/images/icon_043f0001-01550502.png b/assets/amiibo/images/icon_043f0001-01550502.png new file mode 100644 index 000000000..4a672c317 Binary files /dev/null and b/assets/amiibo/images/icon_043f0001-01550502.png differ diff --git a/assets/amiibo/images/icon_04400001-00ca0502.png b/assets/amiibo/images/icon_04400001-00ca0502.png new file mode 100644 index 000000000..b0a8729c0 Binary files /dev/null and b/assets/amiibo/images/icon_04400001-00ca0502.png differ diff --git a/assets/amiibo/images/icon_044b0001-016c0502.png b/assets/amiibo/images/icon_044b0001-016c0502.png new file mode 100644 index 000000000..a9233d9e9 Binary files /dev/null and b/assets/amiibo/images/icon_044b0001-016c0502.png differ diff --git a/assets/amiibo/images/icon_044c0001-008e0502.png b/assets/amiibo/images/icon_044c0001-008e0502.png new file mode 100644 index 000000000..6c726a616 Binary files /dev/null and b/assets/amiibo/images/icon_044c0001-008e0502.png differ diff --git a/assets/amiibo/images/icon_044d0001-01930502.png b/assets/amiibo/images/icon_044d0001-01930502.png new file mode 100644 index 000000000..a71549608 Binary files /dev/null and b/assets/amiibo/images/icon_044d0001-01930502.png differ diff --git a/assets/amiibo/images/icon_044e0001-03150502.png b/assets/amiibo/images/icon_044e0001-03150502.png new file mode 100644 index 000000000..49141cc04 Binary files /dev/null and b/assets/amiibo/images/icon_044e0001-03150502.png differ diff --git a/assets/amiibo/images/icon_04500001-00cf0502.png b/assets/amiibo/images/icon_04500001-00cf0502.png new file mode 100644 index 000000000..13be22964 Binary files /dev/null and b/assets/amiibo/images/icon_04500001-00cf0502.png differ diff --git a/assets/amiibo/images/icon_04510001-015e0502.png b/assets/amiibo/images/icon_04510001-015e0502.png new file mode 100644 index 000000000..35e6e8e7d Binary files /dev/null and b/assets/amiibo/images/icon_04510001-015e0502.png differ diff --git a/assets/amiibo/images/icon_04520001-00730502.png b/assets/amiibo/images/icon_04520001-00730502.png new file mode 100644 index 000000000..3ed3c81bc Binary files /dev/null and b/assets/amiibo/images/icon_04520001-00730502.png differ diff --git a/assets/amiibo/images/icon_04530001-01040502.png b/assets/amiibo/images/icon_04530001-01040502.png new file mode 100644 index 000000000..eba854c14 Binary files /dev/null and b/assets/amiibo/images/icon_04530001-01040502.png differ diff --git a/assets/amiibo/images/icon_04540001-01ae0502.png b/assets/amiibo/images/icon_04540001-01ae0502.png new file mode 100644 index 000000000..b157f5b61 Binary files /dev/null and b/assets/amiibo/images/icon_04540001-01ae0502.png differ diff --git a/assets/amiibo/images/icon_045f0001-01a80502.png b/assets/amiibo/images/icon_045f0001-01a80502.png new file mode 100644 index 000000000..15f988762 Binary files /dev/null and b/assets/amiibo/images/icon_045f0001-01a80502.png differ diff --git a/assets/amiibo/images/icon_04600001-00a50502.png b/assets/amiibo/images/icon_04600001-00a50502.png new file mode 100644 index 000000000..8fa50c665 Binary files /dev/null and b/assets/amiibo/images/icon_04600001-00a50502.png differ diff --git a/assets/amiibo/images/icon_04610001-01610502.png b/assets/amiibo/images/icon_04610001-01610502.png new file mode 100644 index 000000000..a9a5c84ea Binary files /dev/null and b/assets/amiibo/images/icon_04610001-01610502.png differ diff --git a/assets/amiibo/images/icon_04620001-00f60502.png b/assets/amiibo/images/icon_04620001-00f60502.png new file mode 100644 index 000000000..731e551d4 Binary files /dev/null and b/assets/amiibo/images/icon_04620001-00f60502.png differ diff --git a/assets/amiibo/images/icon_04630001-01310502.png b/assets/amiibo/images/icon_04630001-01310502.png new file mode 100644 index 000000000..f9f65b68f Binary files /dev/null and b/assets/amiibo/images/icon_04630001-01310502.png differ diff --git a/assets/amiibo/images/icon_04640001-00c00502.png b/assets/amiibo/images/icon_04640001-00c00502.png new file mode 100644 index 000000000..4866ccf00 Binary files /dev/null and b/assets/amiibo/images/icon_04640001-00c00502.png differ diff --git a/assets/amiibo/images/icon_04650001-006e0502.png b/assets/amiibo/images/icon_04650001-006e0502.png new file mode 100644 index 000000000..ccf44f8fe Binary files /dev/null and b/assets/amiibo/images/icon_04650001-006e0502.png differ diff --git a/assets/amiibo/images/icon_04680001-02f20502.png b/assets/amiibo/images/icon_04680001-02f20502.png new file mode 100644 index 000000000..2a9400b03 Binary files /dev/null and b/assets/amiibo/images/icon_04680001-02f20502.png differ diff --git a/assets/amiibo/images/icon_04690001-01640502.png b/assets/amiibo/images/icon_04690001-01640502.png new file mode 100644 index 000000000..5d0c55a48 Binary files /dev/null and b/assets/amiibo/images/icon_04690001-01640502.png differ diff --git a/assets/amiibo/images/icon_046a0001-01d00502.png b/assets/amiibo/images/icon_046a0001-01d00502.png new file mode 100644 index 000000000..499ac6b4a Binary files /dev/null and b/assets/amiibo/images/icon_046a0001-01d00502.png differ diff --git a/assets/amiibo/images/icon_046b0001-01970502.png b/assets/amiibo/images/icon_046b0001-01970502.png new file mode 100644 index 000000000..11baad688 Binary files /dev/null and b/assets/amiibo/images/icon_046b0001-01970502.png differ diff --git a/assets/amiibo/images/icon_046c0001-008c0502.png b/assets/amiibo/images/icon_046c0001-008c0502.png new file mode 100644 index 000000000..d0c18d3ca Binary files /dev/null and b/assets/amiibo/images/icon_046c0001-008c0502.png differ diff --git a/assets/amiibo/images/icon_046d0001-00f30502.png b/assets/amiibo/images/icon_046d0001-00f30502.png new file mode 100644 index 000000000..3d52d9285 Binary files /dev/null and b/assets/amiibo/images/icon_046d0001-00f30502.png differ diff --git a/assets/amiibo/images/icon_04780001-01630502.png b/assets/amiibo/images/icon_04780001-01630502.png new file mode 100644 index 000000000..fc72e2691 Binary files /dev/null and b/assets/amiibo/images/icon_04780001-01630502.png differ diff --git a/assets/amiibo/images/icon_04790001-00920502.png b/assets/amiibo/images/icon_04790001-00920502.png new file mode 100644 index 000000000..d7d5bd169 Binary files /dev/null and b/assets/amiibo/images/icon_04790001-00920502.png differ diff --git a/assets/amiibo/images/icon_047a0001-00600502.png b/assets/amiibo/images/icon_047a0001-00600502.png new file mode 100644 index 000000000..b047d7ef3 Binary files /dev/null and b/assets/amiibo/images/icon_047a0001-00600502.png differ diff --git a/assets/amiibo/images/icon_047b0001-00f50502.png b/assets/amiibo/images/icon_047b0001-00f50502.png new file mode 100644 index 000000000..c7bf99573 Binary files /dev/null and b/assets/amiibo/images/icon_047b0001-00f50502.png differ diff --git a/assets/amiibo/images/icon_047c0001-01a00502.png b/assets/amiibo/images/icon_047c0001-01a00502.png new file mode 100644 index 000000000..29b0f6e45 Binary files /dev/null and b/assets/amiibo/images/icon_047c0001-01a00502.png differ diff --git a/assets/amiibo/images/icon_047d0001-012e0502.png b/assets/amiibo/images/icon_047d0001-012e0502.png new file mode 100644 index 000000000..d67d9926e Binary files /dev/null and b/assets/amiibo/images/icon_047d0001-012e0502.png differ diff --git a/assets/amiibo/images/icon_04800001-008d0502.png b/assets/amiibo/images/icon_04800001-008d0502.png new file mode 100644 index 000000000..80a4ea82d Binary files /dev/null and b/assets/amiibo/images/icon_04800001-008d0502.png differ diff --git a/assets/amiibo/images/icon_04810001-02f10502.png b/assets/amiibo/images/icon_04810001-02f10502.png new file mode 100644 index 000000000..51e1b6ed9 Binary files /dev/null and b/assets/amiibo/images/icon_04810001-02f10502.png differ diff --git a/assets/amiibo/images/icon_04820001-02fd0502.png b/assets/amiibo/images/icon_04820001-02fd0502.png new file mode 100644 index 000000000..911c35d00 Binary files /dev/null and b/assets/amiibo/images/icon_04820001-02fd0502.png differ diff --git a/assets/amiibo/images/icon_04830001-01b00502.png b/assets/amiibo/images/icon_04830001-01b00502.png new file mode 100644 index 000000000..cbd81bd4b Binary files /dev/null and b/assets/amiibo/images/icon_04830001-01b00502.png differ diff --git a/assets/amiibo/images/icon_04850001-014c0502.png b/assets/amiibo/images/icon_04850001-014c0502.png new file mode 100644 index 000000000..6bbb3dd03 Binary files /dev/null and b/assets/amiibo/images/icon_04850001-014c0502.png differ diff --git a/assets/amiibo/images/icon_04860001-00fc0502.png b/assets/amiibo/images/icon_04860001-00fc0502.png new file mode 100644 index 000000000..974999f4d Binary files /dev/null and b/assets/amiibo/images/icon_04860001-00fc0502.png differ diff --git a/assets/amiibo/images/icon_04870001-01bf0502.png b/assets/amiibo/images/icon_04870001-01bf0502.png new file mode 100644 index 000000000..afec0720a Binary files /dev/null and b/assets/amiibo/images/icon_04870001-01bf0502.png differ diff --git a/assets/amiibo/images/icon_04880001-00980502.png b/assets/amiibo/images/icon_04880001-00980502.png new file mode 100644 index 000000000..b7a96db41 Binary files /dev/null and b/assets/amiibo/images/icon_04880001-00980502.png differ diff --git a/assets/amiibo/images/icon_04890001-00ef0502.png b/assets/amiibo/images/icon_04890001-00ef0502.png new file mode 100644 index 000000000..5b6376b80 Binary files /dev/null and b/assets/amiibo/images/icon_04890001-00ef0502.png differ diff --git a/assets/amiibo/images/icon_04940001-009a0502.png b/assets/amiibo/images/icon_04940001-009a0502.png new file mode 100644 index 000000000..904e3c38f Binary files /dev/null and b/assets/amiibo/images/icon_04940001-009a0502.png differ diff --git a/assets/amiibo/images/icon_04950001-01920502.png b/assets/amiibo/images/icon_04950001-01920502.png new file mode 100644 index 000000000..b8e0bff4c Binary files /dev/null and b/assets/amiibo/images/icon_04950001-01920502.png differ diff --git a/assets/amiibo/images/icon_04960001-00d90502.png b/assets/amiibo/images/icon_04960001-00d90502.png new file mode 100644 index 000000000..706c7c38a Binary files /dev/null and b/assets/amiibo/images/icon_04960001-00d90502.png differ diff --git a/assets/amiibo/images/icon_04970001-007a0502.png b/assets/amiibo/images/icon_04970001-007a0502.png new file mode 100644 index 000000000..281b38190 Binary files /dev/null and b/assets/amiibo/images/icon_04970001-007a0502.png differ diff --git a/assets/amiibo/images/icon_04980001-014a0502.png b/assets/amiibo/images/icon_04980001-014a0502.png new file mode 100644 index 000000000..d6255e4d9 Binary files /dev/null and b/assets/amiibo/images/icon_04980001-014a0502.png differ diff --git a/assets/amiibo/images/icon_04990001-00df0502.png b/assets/amiibo/images/icon_04990001-00df0502.png new file mode 100644 index 000000000..d4fb987ce Binary files /dev/null and b/assets/amiibo/images/icon_04990001-00df0502.png differ diff --git a/assets/amiibo/images/icon_049a0001-014e0502.png b/assets/amiibo/images/icon_049a0001-014e0502.png new file mode 100644 index 000000000..7fee6f9bf Binary files /dev/null and b/assets/amiibo/images/icon_049a0001-014e0502.png differ diff --git a/assets/amiibo/images/icon_049b0001-00610502.png b/assets/amiibo/images/icon_049b0001-00610502.png new file mode 100644 index 000000000..23b46d064 Binary files /dev/null and b/assets/amiibo/images/icon_049b0001-00610502.png differ diff --git a/assets/amiibo/images/icon_049c0001-01400502.png b/assets/amiibo/images/icon_049c0001-01400502.png new file mode 100644 index 000000000..9074dc403 Binary files /dev/null and b/assets/amiibo/images/icon_049c0001-01400502.png differ diff --git a/assets/amiibo/images/icon_049d0001-00ed0502.png b/assets/amiibo/images/icon_049d0001-00ed0502.png new file mode 100644 index 000000000..cf18eb9aa Binary files /dev/null and b/assets/amiibo/images/icon_049d0001-00ed0502.png differ diff --git a/assets/amiibo/images/icon_049e0001-01b70502.png b/assets/amiibo/images/icon_049e0001-01b70502.png new file mode 100644 index 000000000..5bbb1ae02 Binary files /dev/null and b/assets/amiibo/images/icon_049e0001-01b70502.png differ diff --git a/assets/amiibo/images/icon_049f0001-03010502.png b/assets/amiibo/images/icon_049f0001-03010502.png new file mode 100644 index 000000000..b3b4417c6 Binary files /dev/null and b/assets/amiibo/images/icon_049f0001-03010502.png differ diff --git a/assets/amiibo/images/icon_04a00001-016e0502.png b/assets/amiibo/images/icon_04a00001-016e0502.png new file mode 100644 index 000000000..4cb4f0083 Binary files /dev/null and b/assets/amiibo/images/icon_04a00001-016e0502.png differ diff --git a/assets/amiibo/images/icon_04a10001-016f0502.png b/assets/amiibo/images/icon_04a10001-016f0502.png new file mode 100644 index 000000000..513f2da6c Binary files /dev/null and b/assets/amiibo/images/icon_04a10001-016f0502.png differ diff --git a/assets/amiibo/images/icon_04a20001-02e80502.png b/assets/amiibo/images/icon_04a20001-02e80502.png new file mode 100644 index 000000000..b5947612d Binary files /dev/null and b/assets/amiibo/images/icon_04a20001-02e80502.png differ diff --git a/assets/amiibo/images/icon_04a30001-01c90502.png b/assets/amiibo/images/icon_04a30001-01c90502.png new file mode 100644 index 000000000..cf1195fcf Binary files /dev/null and b/assets/amiibo/images/icon_04a30001-01c90502.png differ diff --git a/assets/amiibo/images/icon_04a40001-00d40502.png b/assets/amiibo/images/icon_04a40001-00d40502.png new file mode 100644 index 000000000..f6e7a9919 Binary files /dev/null and b/assets/amiibo/images/icon_04a40001-00d40502.png differ diff --git a/assets/amiibo/images/icon_04a50001-00740502.png b/assets/amiibo/images/icon_04a50001-00740502.png new file mode 100644 index 000000000..132809dcf Binary files /dev/null and b/assets/amiibo/images/icon_04a50001-00740502.png differ diff --git a/assets/amiibo/images/icon_04a60001-00a30502.png b/assets/amiibo/images/icon_04a60001-00a30502.png new file mode 100644 index 000000000..2c3f56b3b Binary files /dev/null and b/assets/amiibo/images/icon_04a60001-00a30502.png differ diff --git a/assets/amiibo/images/icon_04a70001-01a60502.png b/assets/amiibo/images/icon_04a70001-01a60502.png new file mode 100644 index 000000000..765967df6 Binary files /dev/null and b/assets/amiibo/images/icon_04a70001-01a60502.png differ diff --git a/assets/amiibo/images/icon_04a80101-031e0502.png b/assets/amiibo/images/icon_04a80101-031e0502.png new file mode 100644 index 000000000..7fa0c4759 Binary files /dev/null and b/assets/amiibo/images/icon_04a80101-031e0502.png differ diff --git a/assets/amiibo/images/icon_04b20001-01b90502.png b/assets/amiibo/images/icon_04b20001-01b90502.png new file mode 100644 index 000000000..944f685e1 Binary files /dev/null and b/assets/amiibo/images/icon_04b20001-01b90502.png differ diff --git a/assets/amiibo/images/icon_04b30001-00dd0502.png b/assets/amiibo/images/icon_04b30001-00dd0502.png new file mode 100644 index 000000000..b15f5bbe1 Binary files /dev/null and b/assets/amiibo/images/icon_04b30001-00dd0502.png differ diff --git a/assets/amiibo/images/icon_04b40001-030c0502.png b/assets/amiibo/images/icon_04b40001-030c0502.png new file mode 100644 index 000000000..626afa842 Binary files /dev/null and b/assets/amiibo/images/icon_04b40001-030c0502.png differ diff --git a/assets/amiibo/images/icon_04b60001-02ec0502.png b/assets/amiibo/images/icon_04b60001-02ec0502.png new file mode 100644 index 000000000..f866d240c Binary files /dev/null and b/assets/amiibo/images/icon_04b60001-02ec0502.png differ diff --git a/assets/amiibo/images/icon_04b90001-01600502.png b/assets/amiibo/images/icon_04b90001-01600502.png new file mode 100644 index 000000000..1a7cb5d0a Binary files /dev/null and b/assets/amiibo/images/icon_04b90001-01600502.png differ diff --git a/assets/amiibo/images/icon_04ba0001-005d0502.png b/assets/amiibo/images/icon_04ba0001-005d0502.png new file mode 100644 index 000000000..523b6993f Binary files /dev/null and b/assets/amiibo/images/icon_04ba0001-005d0502.png differ diff --git a/assets/amiibo/images/icon_04c50001-01010502.png b/assets/amiibo/images/icon_04c50001-01010502.png new file mode 100644 index 000000000..91aaa2926 Binary files /dev/null and b/assets/amiibo/images/icon_04c50001-01010502.png differ diff --git a/assets/amiibo/images/icon_04c60001-01670502.png b/assets/amiibo/images/icon_04c60001-01670502.png new file mode 100644 index 000000000..a6c846513 Binary files /dev/null and b/assets/amiibo/images/icon_04c60001-01670502.png differ diff --git a/assets/amiibo/images/icon_04c70001-00940502.png b/assets/amiibo/images/icon_04c70001-00940502.png new file mode 100644 index 000000000..59ab69e99 Binary files /dev/null and b/assets/amiibo/images/icon_04c70001-00940502.png differ diff --git a/assets/amiibo/images/icon_04c80001-02ed0502.png b/assets/amiibo/images/icon_04c80001-02ed0502.png new file mode 100644 index 000000000..c092125e6 Binary files /dev/null and b/assets/amiibo/images/icon_04c80001-02ed0502.png differ diff --git a/assets/amiibo/images/icon_04c90001-030d0502.png b/assets/amiibo/images/icon_04c90001-030d0502.png new file mode 100644 index 000000000..44ae1f18c Binary files /dev/null and b/assets/amiibo/images/icon_04c90001-030d0502.png differ diff --git a/assets/amiibo/images/icon_04cc0001-00a40502.png b/assets/amiibo/images/icon_04cc0001-00a40502.png new file mode 100644 index 000000000..48e0c40a1 Binary files /dev/null and b/assets/amiibo/images/icon_04cc0001-00a40502.png differ diff --git a/assets/amiibo/images/icon_04cd0001-01520502.png b/assets/amiibo/images/icon_04cd0001-01520502.png new file mode 100644 index 000000000..4f68d039b Binary files /dev/null and b/assets/amiibo/images/icon_04cd0001-01520502.png differ diff --git a/assets/amiibo/images/icon_04ce0001-00db0502.png b/assets/amiibo/images/icon_04ce0001-00db0502.png new file mode 100644 index 000000000..229987607 Binary files /dev/null and b/assets/amiibo/images/icon_04ce0001-00db0502.png differ diff --git a/assets/amiibo/images/icon_04cf0001-00e10502.png b/assets/amiibo/images/icon_04cf0001-00e10502.png new file mode 100644 index 000000000..8d7805621 Binary files /dev/null and b/assets/amiibo/images/icon_04cf0001-00e10502.png differ diff --git a/assets/amiibo/images/icon_04d00001-01960502.png b/assets/amiibo/images/icon_04d00001-01960502.png new file mode 100644 index 000000000..9aa3c0d2d Binary files /dev/null and b/assets/amiibo/images/icon_04d00001-01960502.png differ diff --git a/assets/amiibo/images/icon_04d10001-009e0502.png b/assets/amiibo/images/icon_04d10001-009e0502.png new file mode 100644 index 000000000..7db7be011 Binary files /dev/null and b/assets/amiibo/images/icon_04d10001-009e0502.png differ diff --git a/assets/amiibo/images/icon_04d20001-01a70502.png b/assets/amiibo/images/icon_04d20001-01a70502.png new file mode 100644 index 000000000..1b39b02ad Binary files /dev/null and b/assets/amiibo/images/icon_04d20001-01a70502.png differ diff --git a/assets/amiibo/images/icon_04d30101-031b0502.png b/assets/amiibo/images/icon_04d30101-031b0502.png new file mode 100644 index 000000000..bac3e9ef2 Binary files /dev/null and b/assets/amiibo/images/icon_04d30101-031b0502.png differ diff --git a/assets/amiibo/images/icon_04dd0001-00a20502.png b/assets/amiibo/images/icon_04dd0001-00a20502.png new file mode 100644 index 000000000..d0d5f1bf2 Binary files /dev/null and b/assets/amiibo/images/icon_04dd0001-00a20502.png differ diff --git a/assets/amiibo/images/icon_04de0001-00ce0502.png b/assets/amiibo/images/icon_04de0001-00ce0502.png new file mode 100644 index 000000000..ea0bb8759 Binary files /dev/null and b/assets/amiibo/images/icon_04de0001-00ce0502.png differ diff --git a/assets/amiibo/images/icon_04df0001-00e80502.png b/assets/amiibo/images/icon_04df0001-00e80502.png new file mode 100644 index 000000000..8f1335d1a Binary files /dev/null and b/assets/amiibo/images/icon_04df0001-00e80502.png differ diff --git a/assets/amiibo/images/icon_04e00001-00f70502.png b/assets/amiibo/images/icon_04e00001-00f70502.png new file mode 100644 index 000000000..e9c66d594 Binary files /dev/null and b/assets/amiibo/images/icon_04e00001-00f70502.png differ diff --git a/assets/amiibo/images/icon_04e10001-01be0502.png b/assets/amiibo/images/icon_04e10001-01be0502.png new file mode 100644 index 000000000..9bdd6e29a Binary files /dev/null and b/assets/amiibo/images/icon_04e10001-01be0502.png differ diff --git a/assets/amiibo/images/icon_04e20001-01090502.png b/assets/amiibo/images/icon_04e20001-01090502.png new file mode 100644 index 000000000..e75376d4a Binary files /dev/null and b/assets/amiibo/images/icon_04e20001-01090502.png differ diff --git a/assets/amiibo/images/icon_04e30001-01650502.png b/assets/amiibo/images/icon_04e30001-01650502.png new file mode 100644 index 000000000..534c4c7fd Binary files /dev/null and b/assets/amiibo/images/icon_04e30001-01650502.png differ diff --git a/assets/amiibo/images/icon_04e40001-01b60502.png b/assets/amiibo/images/icon_04e40001-01b60502.png new file mode 100644 index 000000000..f0132edf7 Binary files /dev/null and b/assets/amiibo/images/icon_04e40001-01b60502.png differ diff --git a/assets/amiibo/images/icon_04e50001-01ad0502.png b/assets/amiibo/images/icon_04e50001-01ad0502.png new file mode 100644 index 000000000..e4f6011c7 Binary files /dev/null and b/assets/amiibo/images/icon_04e50001-01ad0502.png differ diff --git a/assets/amiibo/images/icon_04e60001-00820502.png b/assets/amiibo/images/icon_04e60001-00820502.png new file mode 100644 index 000000000..7417922d7 Binary files /dev/null and b/assets/amiibo/images/icon_04e60001-00820502.png differ diff --git a/assets/amiibo/images/icon_04e70001-01320502.png b/assets/amiibo/images/icon_04e70001-01320502.png new file mode 100644 index 000000000..1ab787712 Binary files /dev/null and b/assets/amiibo/images/icon_04e70001-01320502.png differ diff --git a/assets/amiibo/images/icon_04e80001-01ce0502.png b/assets/amiibo/images/icon_04e80001-01ce0502.png new file mode 100644 index 000000000..a8e854a18 Binary files /dev/null and b/assets/amiibo/images/icon_04e80001-01ce0502.png differ diff --git a/assets/amiibo/images/icon_04ea0001-03180502.png b/assets/amiibo/images/icon_04ea0001-03180502.png new file mode 100644 index 000000000..d6109cdde Binary files /dev/null and b/assets/amiibo/images/icon_04ea0001-03180502.png differ diff --git a/assets/amiibo/images/icon_04eb0001-02f00502.png b/assets/amiibo/images/icon_04eb0001-02f00502.png new file mode 100644 index 000000000..3a3a2e89a Binary files /dev/null and b/assets/amiibo/images/icon_04eb0001-02f00502.png differ diff --git a/assets/amiibo/images/icon_04ec0001-00770502.png b/assets/amiibo/images/icon_04ec0001-00770502.png new file mode 100644 index 000000000..dc8033191 Binary files /dev/null and b/assets/amiibo/images/icon_04ec0001-00770502.png differ diff --git a/assets/amiibo/images/icon_04ed0001-00620502.png b/assets/amiibo/images/icon_04ed0001-00620502.png new file mode 100644 index 000000000..911e03c9b Binary files /dev/null and b/assets/amiibo/images/icon_04ed0001-00620502.png differ diff --git a/assets/amiibo/images/icon_04ee0001-014b0502.png b/assets/amiibo/images/icon_04ee0001-014b0502.png new file mode 100644 index 000000000..7f1dad03e Binary files /dev/null and b/assets/amiibo/images/icon_04ee0001-014b0502.png differ diff --git a/assets/amiibo/images/icon_04ef0001-013b0502.png b/assets/amiibo/images/icon_04ef0001-013b0502.png new file mode 100644 index 000000000..1fc03fcba Binary files /dev/null and b/assets/amiibo/images/icon_04ef0001-013b0502.png differ diff --git a/assets/amiibo/images/icon_04fa0001-01680502.png b/assets/amiibo/images/icon_04fa0001-01680502.png new file mode 100644 index 000000000..82b44dbec Binary files /dev/null and b/assets/amiibo/images/icon_04fa0001-01680502.png differ diff --git a/assets/amiibo/images/icon_04fb0001-01c60502.png b/assets/amiibo/images/icon_04fb0001-01c60502.png new file mode 100644 index 000000000..29d0dbce5 Binary files /dev/null and b/assets/amiibo/images/icon_04fb0001-01c60502.png differ diff --git a/assets/amiibo/images/icon_04fc0001-02ee0502.png b/assets/amiibo/images/icon_04fc0001-02ee0502.png new file mode 100644 index 000000000..3426938a4 Binary files /dev/null and b/assets/amiibo/images/icon_04fc0001-02ee0502.png differ diff --git a/assets/amiibo/images/icon_04fd0001-007b0502.png b/assets/amiibo/images/icon_04fd0001-007b0502.png new file mode 100644 index 000000000..001c0e76d Binary files /dev/null and b/assets/amiibo/images/icon_04fd0001-007b0502.png differ diff --git a/assets/amiibo/images/icon_04fe0001-00590502.png b/assets/amiibo/images/icon_04fe0001-00590502.png new file mode 100644 index 000000000..fc41a4ac2 Binary files /dev/null and b/assets/amiibo/images/icon_04fe0001-00590502.png differ diff --git a/assets/amiibo/images/icon_04ff0001-01620502.png b/assets/amiibo/images/icon_04ff0001-01620502.png new file mode 100644 index 000000000..42457c5b2 Binary files /dev/null and b/assets/amiibo/images/icon_04ff0001-01620502.png differ diff --git a/assets/amiibo/images/icon_05000001-00e70502.png b/assets/amiibo/images/icon_05000001-00e70502.png new file mode 100644 index 000000000..73163bed2 Binary files /dev/null and b/assets/amiibo/images/icon_05000001-00e70502.png differ diff --git a/assets/amiibo/images/icon_050b0001-00990502.png b/assets/amiibo/images/icon_050b0001-00990502.png new file mode 100644 index 000000000..a3774feec Binary files /dev/null and b/assets/amiibo/images/icon_050b0001-00990502.png differ diff --git a/assets/amiibo/images/icon_050c0001-01c10502.png b/assets/amiibo/images/icon_050c0001-01c10502.png new file mode 100644 index 000000000..460cea4ab Binary files /dev/null and b/assets/amiibo/images/icon_050c0001-01c10502.png differ diff --git a/assets/amiibo/images/icon_050d0001-01420502.png b/assets/amiibo/images/icon_050d0001-01420502.png new file mode 100644 index 000000000..b4e07f997 Binary files /dev/null and b/assets/amiibo/images/icon_050d0001-01420502.png differ diff --git a/assets/amiibo/images/icon_050e0001-00d70502.png b/assets/amiibo/images/icon_050e0001-00d70502.png new file mode 100644 index 000000000..2b69f818b Binary files /dev/null and b/assets/amiibo/images/icon_050e0001-00d70502.png differ diff --git a/assets/amiibo/images/icon_050f0001-03140502.png b/assets/amiibo/images/icon_050f0001-03140502.png new file mode 100644 index 000000000..1a1211383 Binary files /dev/null and b/assets/amiibo/images/icon_050f0001-03140502.png differ diff --git a/assets/amiibo/images/icon_05100001-01070502.png b/assets/amiibo/images/icon_05100001-01070502.png new file mode 100644 index 000000000..1d1f5e4b5 Binary files /dev/null and b/assets/amiibo/images/icon_05100001-01070502.png differ diff --git a/assets/amiibo/images/icon_05110001-01950502.png b/assets/amiibo/images/icon_05110001-01950502.png new file mode 100644 index 000000000..1367350b9 Binary files /dev/null and b/assets/amiibo/images/icon_05110001-01950502.png differ diff --git a/assets/amiibo/images/icon_05130001-02e70502.png b/assets/amiibo/images/icon_05130001-02e70502.png new file mode 100644 index 000000000..ad3925f85 Binary files /dev/null and b/assets/amiibo/images/icon_05130001-02e70502.png differ diff --git a/assets/amiibo/images/icon_05140001-01530502.png b/assets/amiibo/images/icon_05140001-01530502.png new file mode 100644 index 000000000..c594d3330 Binary files /dev/null and b/assets/amiibo/images/icon_05140001-01530502.png differ diff --git a/assets/amiibo/images/icon_05150001-005b0502.png b/assets/amiibo/images/icon_05150001-005b0502.png new file mode 100644 index 000000000..a6246088c Binary files /dev/null and b/assets/amiibo/images/icon_05150001-005b0502.png differ diff --git a/assets/amiibo/images/icon_05800000-00050002.png b/assets/amiibo/images/icon_05800000-00050002.png new file mode 100644 index 000000000..7ef3ef1d3 Binary files /dev/null and b/assets/amiibo/images/icon_05800000-00050002.png differ diff --git a/assets/amiibo/images/icon_05810000-001c0002.png b/assets/amiibo/images/icon_05810000-001c0002.png new file mode 100644 index 000000000..76cf31c0d Binary files /dev/null and b/assets/amiibo/images/icon_05810000-001c0002.png differ diff --git a/assets/amiibo/images/icon_05840000-037e0002.png b/assets/amiibo/images/icon_05840000-037e0002.png new file mode 100644 index 000000000..4f5511d59 Binary files /dev/null and b/assets/amiibo/images/icon_05840000-037e0002.png differ diff --git a/assets/amiibo/images/icon_05c00000-00060002.png b/assets/amiibo/images/icon_05c00000-00060002.png new file mode 100644 index 000000000..f05b04bfb Binary files /dev/null and b/assets/amiibo/images/icon_05c00000-00060002.png differ diff --git a/assets/amiibo/images/icon_05c00000-03651302.png b/assets/amiibo/images/icon_05c00000-03651302.png new file mode 100644 index 000000000..71b80ee00 Binary files /dev/null and b/assets/amiibo/images/icon_05c00000-03651302.png differ diff --git a/assets/amiibo/images/icon_05c00000-04121302.png b/assets/amiibo/images/icon_05c00000-04121302.png new file mode 100644 index 000000000..f59e2e38d Binary files /dev/null and b/assets/amiibo/images/icon_05c00000-04121302.png differ diff --git a/assets/amiibo/images/icon_05c00100-001d0002.png b/assets/amiibo/images/icon_05c00100-001d0002.png new file mode 100644 index 000000000..4a3f91858 Binary files /dev/null and b/assets/amiibo/images/icon_05c00100-001d0002.png differ diff --git a/assets/amiibo/images/icon_05c10000-03661302.png b/assets/amiibo/images/icon_05c10000-03661302.png new file mode 100644 index 000000000..d0fea06de Binary files /dev/null and b/assets/amiibo/images/icon_05c10000-03661302.png differ diff --git a/assets/amiibo/images/icon_05c20000-037f0002.png b/assets/amiibo/images/icon_05c20000-037f0002.png new file mode 100644 index 000000000..395bbd458 Binary files /dev/null and b/assets/amiibo/images/icon_05c20000-037f0002.png differ diff --git a/assets/amiibo/images/icon_05c30000-03800002.png b/assets/amiibo/images/icon_05c30000-03800002.png new file mode 100644 index 000000000..372efafbd Binary files /dev/null and b/assets/amiibo/images/icon_05c30000-03800002.png differ diff --git a/assets/amiibo/images/icon_05c40000-04131302.png b/assets/amiibo/images/icon_05c40000-04131302.png new file mode 100644 index 000000000..61e224c17 Binary files /dev/null and b/assets/amiibo/images/icon_05c40000-04131302.png differ diff --git a/assets/amiibo/images/icon_06000000-00120002.png b/assets/amiibo/images/icon_06000000-00120002.png new file mode 100644 index 000000000..77d355410 Binary files /dev/null and b/assets/amiibo/images/icon_06000000-00120002.png differ diff --git a/assets/amiibo/images/icon_06400100-001e0002.png b/assets/amiibo/images/icon_06400100-001e0002.png new file mode 100644 index 000000000..1f6948f1a Binary files /dev/null and b/assets/amiibo/images/icon_06400100-001e0002.png differ diff --git a/assets/amiibo/images/icon_06420000-035f1102.png b/assets/amiibo/images/icon_06420000-035f1102.png new file mode 100644 index 000000000..6e851d7e9 Binary files /dev/null and b/assets/amiibo/images/icon_06420000-035f1102.png differ diff --git a/assets/amiibo/images/icon_06c00000-000f0002.png b/assets/amiibo/images/icon_06c00000-000f0002.png new file mode 100644 index 000000000..1f07b9ec1 Binary files /dev/null and b/assets/amiibo/images/icon_06c00000-000f0002.png differ diff --git a/assets/amiibo/images/icon_07000000-00070002.png b/assets/amiibo/images/icon_07000000-00070002.png new file mode 100644 index 000000000..3cd5bd046 Binary files /dev/null and b/assets/amiibo/images/icon_07000000-00070002.png differ diff --git a/assets/amiibo/images/icon_07400000-00100002.png b/assets/amiibo/images/icon_07400000-00100002.png new file mode 100644 index 000000000..c5be3753d Binary files /dev/null and b/assets/amiibo/images/icon_07400000-00100002.png differ diff --git a/assets/amiibo/images/icon_07410000-00200002.png b/assets/amiibo/images/icon_07410000-00200002.png new file mode 100644 index 000000000..3ba5b062f Binary files /dev/null and b/assets/amiibo/images/icon_07410000-00200002.png differ diff --git a/assets/amiibo/images/icon_07420000-001f0002.png b/assets/amiibo/images/icon_07420000-001f0002.png new file mode 100644 index 000000000..b60823370 Binary files /dev/null and b/assets/amiibo/images/icon_07420000-001f0002.png differ diff --git a/assets/amiibo/images/icon_07800000-002d0002.png b/assets/amiibo/images/icon_07800000-002d0002.png new file mode 100644 index 000000000..afa8d7d15 Binary files /dev/null and b/assets/amiibo/images/icon_07800000-002d0002.png differ diff --git a/assets/amiibo/images/icon_07810000-002e0002.png b/assets/amiibo/images/icon_07810000-002e0002.png new file mode 100644 index 000000000..5491c00cc Binary files /dev/null and b/assets/amiibo/images/icon_07810000-002e0002.png differ diff --git a/assets/amiibo/images/icon_07810000-00330002.png b/assets/amiibo/images/icon_07810000-00330002.png new file mode 100644 index 000000000..cacdaa0fd Binary files /dev/null and b/assets/amiibo/images/icon_07810000-00330002.png differ diff --git a/assets/amiibo/images/icon_07820000-002f0002.png b/assets/amiibo/images/icon_07820000-002f0002.png new file mode 100644 index 000000000..b6a0fd750 Binary files /dev/null and b/assets/amiibo/images/icon_07820000-002f0002.png differ diff --git a/assets/amiibo/images/icon_078f0000-03810002.png b/assets/amiibo/images/icon_078f0000-03810002.png new file mode 100644 index 000000000..c33e7947a Binary files /dev/null and b/assets/amiibo/images/icon_078f0000-03810002.png differ diff --git a/assets/amiibo/images/icon_07c00000-00210002.png b/assets/amiibo/images/icon_07c00000-00210002.png new file mode 100644 index 000000000..a9c717d52 Binary files /dev/null and b/assets/amiibo/images/icon_07c00000-00210002.png differ diff --git a/assets/amiibo/images/icon_07c00100-00220002.png b/assets/amiibo/images/icon_07c00100-00220002.png new file mode 100644 index 000000000..5b6f292e5 Binary files /dev/null and b/assets/amiibo/images/icon_07c00100-00220002.png differ diff --git a/assets/amiibo/images/icon_07c00200-00230002.png b/assets/amiibo/images/icon_07c00200-00230002.png new file mode 100644 index 000000000..b0a722736 Binary files /dev/null and b/assets/amiibo/images/icon_07c00200-00230002.png differ diff --git a/assets/amiibo/images/icon_08000100-003e0402.png b/assets/amiibo/images/icon_08000100-003e0402.png new file mode 100644 index 000000000..8b36f2ecb Binary files /dev/null and b/assets/amiibo/images/icon_08000100-003e0402.png differ diff --git a/assets/amiibo/images/icon_08000100-025f0402.png b/assets/amiibo/images/icon_08000100-025f0402.png new file mode 100644 index 000000000..124eadf55 Binary files /dev/null and b/assets/amiibo/images/icon_08000100-025f0402.png differ diff --git a/assets/amiibo/images/icon_08000100-03690402.png b/assets/amiibo/images/icon_08000100-03690402.png new file mode 100644 index 000000000..280a4bc23 Binary files /dev/null and b/assets/amiibo/images/icon_08000100-03690402.png differ diff --git a/assets/amiibo/images/icon_08000100-03820002.png b/assets/amiibo/images/icon_08000100-03820002.png new file mode 100644 index 000000000..cf1f34b7d Binary files /dev/null and b/assets/amiibo/images/icon_08000100-03820002.png differ diff --git a/assets/amiibo/images/icon_08000100-04150402.png b/assets/amiibo/images/icon_08000100-04150402.png new file mode 100644 index 000000000..1b30696e5 Binary files /dev/null and b/assets/amiibo/images/icon_08000100-04150402.png differ diff --git a/assets/amiibo/images/icon_08000200-003f0402.png b/assets/amiibo/images/icon_08000200-003f0402.png new file mode 100644 index 000000000..ad67ecfcb Binary files /dev/null and b/assets/amiibo/images/icon_08000200-003f0402.png differ diff --git a/assets/amiibo/images/icon_08000200-02600402.png b/assets/amiibo/images/icon_08000200-02600402.png new file mode 100644 index 000000000..1dbb393db Binary files /dev/null and b/assets/amiibo/images/icon_08000200-02600402.png differ diff --git a/assets/amiibo/images/icon_08000200-036a0402.png b/assets/amiibo/images/icon_08000200-036a0402.png new file mode 100644 index 000000000..de98a445b Binary files /dev/null and b/assets/amiibo/images/icon_08000200-036a0402.png differ diff --git a/assets/amiibo/images/icon_08000300-00400402.png b/assets/amiibo/images/icon_08000300-00400402.png new file mode 100644 index 000000000..42cd5a5b6 Binary files /dev/null and b/assets/amiibo/images/icon_08000300-00400402.png differ diff --git a/assets/amiibo/images/icon_08000300-02610402.png b/assets/amiibo/images/icon_08000300-02610402.png new file mode 100644 index 000000000..e18b73f3f Binary files /dev/null and b/assets/amiibo/images/icon_08000300-02610402.png differ diff --git a/assets/amiibo/images/icon_08000300-036b0402.png b/assets/amiibo/images/icon_08000300-036b0402.png new file mode 100644 index 000000000..6bc022f43 Binary files /dev/null and b/assets/amiibo/images/icon_08000300-036b0402.png differ diff --git a/assets/amiibo/images/icon_08010000-025d0402.png b/assets/amiibo/images/icon_08010000-025d0402.png new file mode 100644 index 000000000..6cbee1a7a Binary files /dev/null and b/assets/amiibo/images/icon_08010000-025d0402.png differ diff --git a/assets/amiibo/images/icon_08010000-04360402.png b/assets/amiibo/images/icon_08010000-04360402.png new file mode 100644 index 000000000..b45684564 Binary files /dev/null and b/assets/amiibo/images/icon_08010000-04360402.png differ diff --git a/assets/amiibo/images/icon_08020000-025e0402.png b/assets/amiibo/images/icon_08020000-025e0402.png new file mode 100644 index 000000000..5fa18cc97 Binary files /dev/null and b/assets/amiibo/images/icon_08020000-025e0402.png differ diff --git a/assets/amiibo/images/icon_08020000-04370402.png b/assets/amiibo/images/icon_08020000-04370402.png new file mode 100644 index 000000000..10cb5987a Binary files /dev/null and b/assets/amiibo/images/icon_08020000-04370402.png differ diff --git a/assets/amiibo/images/icon_08030000-03760402.png b/assets/amiibo/images/icon_08030000-03760402.png new file mode 100644 index 000000000..85a5e9f6a Binary files /dev/null and b/assets/amiibo/images/icon_08030000-03760402.png differ diff --git a/assets/amiibo/images/icon_08030000-04380402.png b/assets/amiibo/images/icon_08030000-04380402.png new file mode 100644 index 000000000..84425939e Binary files /dev/null and b/assets/amiibo/images/icon_08030000-04380402.png differ diff --git a/assets/amiibo/images/icon_08040000-03770402.png b/assets/amiibo/images/icon_08040000-03770402.png new file mode 100644 index 000000000..35880b996 Binary files /dev/null and b/assets/amiibo/images/icon_08040000-03770402.png differ diff --git a/assets/amiibo/images/icon_08040000-04390402.png b/assets/amiibo/images/icon_08040000-04390402.png new file mode 100644 index 000000000..ab3ee111b Binary files /dev/null and b/assets/amiibo/images/icon_08040000-04390402.png differ diff --git a/assets/amiibo/images/icon_08050100-038e0402.png b/assets/amiibo/images/icon_08050100-038e0402.png new file mode 100644 index 000000000..a14e3641a Binary files /dev/null and b/assets/amiibo/images/icon_08050100-038e0402.png differ diff --git a/assets/amiibo/images/icon_08050200-038f0402.png b/assets/amiibo/images/icon_08050200-038f0402.png new file mode 100644 index 000000000..6764e467d Binary files /dev/null and b/assets/amiibo/images/icon_08050200-038f0402.png differ diff --git a/assets/amiibo/images/icon_08050200-041b0402.png b/assets/amiibo/images/icon_08050200-041b0402.png new file mode 100644 index 000000000..03c0cb37a Binary files /dev/null and b/assets/amiibo/images/icon_08050200-041b0402.png differ diff --git a/assets/amiibo/images/icon_08050300-03900402.png b/assets/amiibo/images/icon_08050300-03900402.png new file mode 100644 index 000000000..8359af06b Binary files /dev/null and b/assets/amiibo/images/icon_08050300-03900402.png differ diff --git a/assets/amiibo/images/icon_08060100-041c0402.png b/assets/amiibo/images/icon_08060100-041c0402.png new file mode 100644 index 000000000..695b655c9 Binary files /dev/null and b/assets/amiibo/images/icon_08060100-041c0402.png differ diff --git a/assets/amiibo/images/icon_08070000-04330402.png b/assets/amiibo/images/icon_08070000-04330402.png new file mode 100644 index 000000000..55641008b Binary files /dev/null and b/assets/amiibo/images/icon_08070000-04330402.png differ diff --git a/assets/amiibo/images/icon_08080000-04340402.png b/assets/amiibo/images/icon_08080000-04340402.png new file mode 100644 index 000000000..d3070548d Binary files /dev/null and b/assets/amiibo/images/icon_08080000-04340402.png differ diff --git a/assets/amiibo/images/icon_08090000-04350402.png b/assets/amiibo/images/icon_08090000-04350402.png new file mode 100644 index 000000000..c7d9baee8 Binary files /dev/null and b/assets/amiibo/images/icon_08090000-04350402.png differ diff --git a/assets/amiibo/images/icon_09c00101-02690e02.png b/assets/amiibo/images/icon_09c00101-02690e02.png new file mode 100644 index 000000000..4b06e9379 Binary files /dev/null and b/assets/amiibo/images/icon_09c00101-02690e02.png differ diff --git a/assets/amiibo/images/icon_09c00201-026a0e02.png b/assets/amiibo/images/icon_09c00201-026a0e02.png new file mode 100644 index 000000000..cf281b228 Binary files /dev/null and b/assets/amiibo/images/icon_09c00201-026a0e02.png differ diff --git a/assets/amiibo/images/icon_09c00301-026b0e02.png b/assets/amiibo/images/icon_09c00301-026b0e02.png new file mode 100644 index 000000000..2b34ce4f4 Binary files /dev/null and b/assets/amiibo/images/icon_09c00301-026b0e02.png differ diff --git a/assets/amiibo/images/icon_09c00401-026c0e02.png b/assets/amiibo/images/icon_09c00401-026c0e02.png new file mode 100644 index 000000000..3626bf2f2 Binary files /dev/null and b/assets/amiibo/images/icon_09c00401-026c0e02.png differ diff --git a/assets/amiibo/images/icon_09c00501-026d0e02.png b/assets/amiibo/images/icon_09c00501-026d0e02.png new file mode 100644 index 000000000..eb75ff4a2 Binary files /dev/null and b/assets/amiibo/images/icon_09c00501-026d0e02.png differ diff --git a/assets/amiibo/images/icon_09c10101-026e0e02.png b/assets/amiibo/images/icon_09c10101-026e0e02.png new file mode 100644 index 000000000..5ed8fa848 Binary files /dev/null and b/assets/amiibo/images/icon_09c10101-026e0e02.png differ diff --git a/assets/amiibo/images/icon_09c10201-026f0e02.png b/assets/amiibo/images/icon_09c10201-026f0e02.png new file mode 100644 index 000000000..a41f561b1 Binary files /dev/null and b/assets/amiibo/images/icon_09c10201-026f0e02.png differ diff --git a/assets/amiibo/images/icon_09c10301-02700e02.png b/assets/amiibo/images/icon_09c10301-02700e02.png new file mode 100644 index 000000000..41f0303ab Binary files /dev/null and b/assets/amiibo/images/icon_09c10301-02700e02.png differ diff --git a/assets/amiibo/images/icon_09c10401-02710e02.png b/assets/amiibo/images/icon_09c10401-02710e02.png new file mode 100644 index 000000000..a2e9857b3 Binary files /dev/null and b/assets/amiibo/images/icon_09c10401-02710e02.png differ diff --git a/assets/amiibo/images/icon_09c10501-02720e02.png b/assets/amiibo/images/icon_09c10501-02720e02.png new file mode 100644 index 000000000..1e2435e58 Binary files /dev/null and b/assets/amiibo/images/icon_09c10501-02720e02.png differ diff --git a/assets/amiibo/images/icon_09c20101-02730e02.png b/assets/amiibo/images/icon_09c20101-02730e02.png new file mode 100644 index 000000000..e16422004 Binary files /dev/null and b/assets/amiibo/images/icon_09c20101-02730e02.png differ diff --git a/assets/amiibo/images/icon_09c20201-02740e02.png b/assets/amiibo/images/icon_09c20201-02740e02.png new file mode 100644 index 000000000..67151ed3e Binary files /dev/null and b/assets/amiibo/images/icon_09c20201-02740e02.png differ diff --git a/assets/amiibo/images/icon_09c20301-02750e02.png b/assets/amiibo/images/icon_09c20301-02750e02.png new file mode 100644 index 000000000..850bf778e Binary files /dev/null and b/assets/amiibo/images/icon_09c20301-02750e02.png differ diff --git a/assets/amiibo/images/icon_09c20401-02760e02.png b/assets/amiibo/images/icon_09c20401-02760e02.png new file mode 100644 index 000000000..ee6e9a14c Binary files /dev/null and b/assets/amiibo/images/icon_09c20401-02760e02.png differ diff --git a/assets/amiibo/images/icon_09c20501-02770e02.png b/assets/amiibo/images/icon_09c20501-02770e02.png new file mode 100644 index 000000000..1d78c40f3 Binary files /dev/null and b/assets/amiibo/images/icon_09c20501-02770e02.png differ diff --git a/assets/amiibo/images/icon_09c30101-02780e02.png b/assets/amiibo/images/icon_09c30101-02780e02.png new file mode 100644 index 000000000..f43043198 Binary files /dev/null and b/assets/amiibo/images/icon_09c30101-02780e02.png differ diff --git a/assets/amiibo/images/icon_09c30201-02790e02.png b/assets/amiibo/images/icon_09c30201-02790e02.png new file mode 100644 index 000000000..ef5474478 Binary files /dev/null and b/assets/amiibo/images/icon_09c30201-02790e02.png differ diff --git a/assets/amiibo/images/icon_09c30301-027a0e02.png b/assets/amiibo/images/icon_09c30301-027a0e02.png new file mode 100644 index 000000000..f4499e97c Binary files /dev/null and b/assets/amiibo/images/icon_09c30301-027a0e02.png differ diff --git a/assets/amiibo/images/icon_09c30401-027b0e02.png b/assets/amiibo/images/icon_09c30401-027b0e02.png new file mode 100644 index 000000000..ad4534890 Binary files /dev/null and b/assets/amiibo/images/icon_09c30401-027b0e02.png differ diff --git a/assets/amiibo/images/icon_09c30501-027c0e02.png b/assets/amiibo/images/icon_09c30501-027c0e02.png new file mode 100644 index 000000000..fd7dd66f4 Binary files /dev/null and b/assets/amiibo/images/icon_09c30501-027c0e02.png differ diff --git a/assets/amiibo/images/icon_09c40101-027d0e02.png b/assets/amiibo/images/icon_09c40101-027d0e02.png new file mode 100644 index 000000000..a06217baf Binary files /dev/null and b/assets/amiibo/images/icon_09c40101-027d0e02.png differ diff --git a/assets/amiibo/images/icon_09c40201-027e0e02.png b/assets/amiibo/images/icon_09c40201-027e0e02.png new file mode 100644 index 000000000..fe801a167 Binary files /dev/null and b/assets/amiibo/images/icon_09c40201-027e0e02.png differ diff --git a/assets/amiibo/images/icon_09c40301-027f0e02.png b/assets/amiibo/images/icon_09c40301-027f0e02.png new file mode 100644 index 000000000..fe358924d Binary files /dev/null and b/assets/amiibo/images/icon_09c40301-027f0e02.png differ diff --git a/assets/amiibo/images/icon_09c40401-02800e02.png b/assets/amiibo/images/icon_09c40401-02800e02.png new file mode 100644 index 000000000..1b29d1338 Binary files /dev/null and b/assets/amiibo/images/icon_09c40401-02800e02.png differ diff --git a/assets/amiibo/images/icon_09c40501-02810e02.png b/assets/amiibo/images/icon_09c40501-02810e02.png new file mode 100644 index 000000000..35c48cf56 Binary files /dev/null and b/assets/amiibo/images/icon_09c40501-02810e02.png differ diff --git a/assets/amiibo/images/icon_09c50101-02820e02.png b/assets/amiibo/images/icon_09c50101-02820e02.png new file mode 100644 index 000000000..8f6833c11 Binary files /dev/null and b/assets/amiibo/images/icon_09c50101-02820e02.png differ diff --git a/assets/amiibo/images/icon_09c50201-02830e02.png b/assets/amiibo/images/icon_09c50201-02830e02.png new file mode 100644 index 000000000..4eac5dbdb Binary files /dev/null and b/assets/amiibo/images/icon_09c50201-02830e02.png differ diff --git a/assets/amiibo/images/icon_09c50301-02840e02.png b/assets/amiibo/images/icon_09c50301-02840e02.png new file mode 100644 index 000000000..5bc86c598 Binary files /dev/null and b/assets/amiibo/images/icon_09c50301-02840e02.png differ diff --git a/assets/amiibo/images/icon_09c50401-02850e02.png b/assets/amiibo/images/icon_09c50401-02850e02.png new file mode 100644 index 000000000..daaee032a Binary files /dev/null and b/assets/amiibo/images/icon_09c50401-02850e02.png differ diff --git a/assets/amiibo/images/icon_09c50501-02860e02.png b/assets/amiibo/images/icon_09c50501-02860e02.png new file mode 100644 index 000000000..88c3f6338 Binary files /dev/null and b/assets/amiibo/images/icon_09c50501-02860e02.png differ diff --git a/assets/amiibo/images/icon_09c60101-02870e02.png b/assets/amiibo/images/icon_09c60101-02870e02.png new file mode 100644 index 000000000..d3874a25b Binary files /dev/null and b/assets/amiibo/images/icon_09c60101-02870e02.png differ diff --git a/assets/amiibo/images/icon_09c60201-02880e02.png b/assets/amiibo/images/icon_09c60201-02880e02.png new file mode 100644 index 000000000..9d16e06c5 Binary files /dev/null and b/assets/amiibo/images/icon_09c60201-02880e02.png differ diff --git a/assets/amiibo/images/icon_09c60301-02890e02.png b/assets/amiibo/images/icon_09c60301-02890e02.png new file mode 100644 index 000000000..5e6ae378b Binary files /dev/null and b/assets/amiibo/images/icon_09c60301-02890e02.png differ diff --git a/assets/amiibo/images/icon_09c60401-028a0e02.png b/assets/amiibo/images/icon_09c60401-028a0e02.png new file mode 100644 index 000000000..8bae55726 Binary files /dev/null and b/assets/amiibo/images/icon_09c60401-028a0e02.png differ diff --git a/assets/amiibo/images/icon_09c60501-028b0e02.png b/assets/amiibo/images/icon_09c60501-028b0e02.png new file mode 100644 index 000000000..1f97ae4ed Binary files /dev/null and b/assets/amiibo/images/icon_09c60501-028b0e02.png differ diff --git a/assets/amiibo/images/icon_09c70101-028c0e02.png b/assets/amiibo/images/icon_09c70101-028c0e02.png new file mode 100644 index 000000000..d13ce245d Binary files /dev/null and b/assets/amiibo/images/icon_09c70101-028c0e02.png differ diff --git a/assets/amiibo/images/icon_09c70201-028d0e02.png b/assets/amiibo/images/icon_09c70201-028d0e02.png new file mode 100644 index 000000000..e20cae51d Binary files /dev/null and b/assets/amiibo/images/icon_09c70201-028d0e02.png differ diff --git a/assets/amiibo/images/icon_09c70301-028e0e02.png b/assets/amiibo/images/icon_09c70301-028e0e02.png new file mode 100644 index 000000000..952ecc950 Binary files /dev/null and b/assets/amiibo/images/icon_09c70301-028e0e02.png differ diff --git a/assets/amiibo/images/icon_09c70401-028f0e02.png b/assets/amiibo/images/icon_09c70401-028f0e02.png new file mode 100644 index 000000000..d0dc99b28 Binary files /dev/null and b/assets/amiibo/images/icon_09c70401-028f0e02.png differ diff --git a/assets/amiibo/images/icon_09c70501-02900e02.png b/assets/amiibo/images/icon_09c70501-02900e02.png new file mode 100644 index 000000000..9945ad8fe Binary files /dev/null and b/assets/amiibo/images/icon_09c70501-02900e02.png differ diff --git a/assets/amiibo/images/icon_09c80101-02910e02.png b/assets/amiibo/images/icon_09c80101-02910e02.png new file mode 100644 index 000000000..b3b1677dc Binary files /dev/null and b/assets/amiibo/images/icon_09c80101-02910e02.png differ diff --git a/assets/amiibo/images/icon_09c80201-02920e02.png b/assets/amiibo/images/icon_09c80201-02920e02.png new file mode 100644 index 000000000..2b8d35f4e Binary files /dev/null and b/assets/amiibo/images/icon_09c80201-02920e02.png differ diff --git a/assets/amiibo/images/icon_09c80301-02930e02.png b/assets/amiibo/images/icon_09c80301-02930e02.png new file mode 100644 index 000000000..436ade949 Binary files /dev/null and b/assets/amiibo/images/icon_09c80301-02930e02.png differ diff --git a/assets/amiibo/images/icon_09c80401-02940e02.png b/assets/amiibo/images/icon_09c80401-02940e02.png new file mode 100644 index 000000000..c5e3778e7 Binary files /dev/null and b/assets/amiibo/images/icon_09c80401-02940e02.png differ diff --git a/assets/amiibo/images/icon_09c80501-02950e02.png b/assets/amiibo/images/icon_09c80501-02950e02.png new file mode 100644 index 000000000..fe389a659 Binary files /dev/null and b/assets/amiibo/images/icon_09c80501-02950e02.png differ diff --git a/assets/amiibo/images/icon_09c90101-02960e02.png b/assets/amiibo/images/icon_09c90101-02960e02.png new file mode 100644 index 000000000..d57f5999e Binary files /dev/null and b/assets/amiibo/images/icon_09c90101-02960e02.png differ diff --git a/assets/amiibo/images/icon_09c90201-02970e02.png b/assets/amiibo/images/icon_09c90201-02970e02.png new file mode 100644 index 000000000..7baf1ce76 Binary files /dev/null and b/assets/amiibo/images/icon_09c90201-02970e02.png differ diff --git a/assets/amiibo/images/icon_09c90301-02980e02.png b/assets/amiibo/images/icon_09c90301-02980e02.png new file mode 100644 index 000000000..616b3b598 Binary files /dev/null and b/assets/amiibo/images/icon_09c90301-02980e02.png differ diff --git a/assets/amiibo/images/icon_09c90401-02990e02.png b/assets/amiibo/images/icon_09c90401-02990e02.png new file mode 100644 index 000000000..dcb3a1c8d Binary files /dev/null and b/assets/amiibo/images/icon_09c90401-02990e02.png differ diff --git a/assets/amiibo/images/icon_09c90501-029a0e02.png b/assets/amiibo/images/icon_09c90501-029a0e02.png new file mode 100644 index 000000000..a7568ace1 Binary files /dev/null and b/assets/amiibo/images/icon_09c90501-029a0e02.png differ diff --git a/assets/amiibo/images/icon_09ca0101-029b0e02.png b/assets/amiibo/images/icon_09ca0101-029b0e02.png new file mode 100644 index 000000000..78eb4604d Binary files /dev/null and b/assets/amiibo/images/icon_09ca0101-029b0e02.png differ diff --git a/assets/amiibo/images/icon_09ca0201-029c0e02.png b/assets/amiibo/images/icon_09ca0201-029c0e02.png new file mode 100644 index 000000000..4252e0811 Binary files /dev/null and b/assets/amiibo/images/icon_09ca0201-029c0e02.png differ diff --git a/assets/amiibo/images/icon_09ca0301-029d0e02.png b/assets/amiibo/images/icon_09ca0301-029d0e02.png new file mode 100644 index 000000000..9b2000936 Binary files /dev/null and b/assets/amiibo/images/icon_09ca0301-029d0e02.png differ diff --git a/assets/amiibo/images/icon_09ca0401-029e0e02.png b/assets/amiibo/images/icon_09ca0401-029e0e02.png new file mode 100644 index 000000000..75629fba4 Binary files /dev/null and b/assets/amiibo/images/icon_09ca0401-029e0e02.png differ diff --git a/assets/amiibo/images/icon_09ca0501-029f0e02.png b/assets/amiibo/images/icon_09ca0501-029f0e02.png new file mode 100644 index 000000000..bf7b20627 Binary files /dev/null and b/assets/amiibo/images/icon_09ca0501-029f0e02.png differ diff --git a/assets/amiibo/images/icon_09cb0101-02a00e02.png b/assets/amiibo/images/icon_09cb0101-02a00e02.png new file mode 100644 index 000000000..0be53a6ec Binary files /dev/null and b/assets/amiibo/images/icon_09cb0101-02a00e02.png differ diff --git a/assets/amiibo/images/icon_09cb0201-02a10e02.png b/assets/amiibo/images/icon_09cb0201-02a10e02.png new file mode 100644 index 000000000..cbeab29a0 Binary files /dev/null and b/assets/amiibo/images/icon_09cb0201-02a10e02.png differ diff --git a/assets/amiibo/images/icon_09cb0301-02a20e02.png b/assets/amiibo/images/icon_09cb0301-02a20e02.png new file mode 100644 index 000000000..116c54001 Binary files /dev/null and b/assets/amiibo/images/icon_09cb0301-02a20e02.png differ diff --git a/assets/amiibo/images/icon_09cb0401-02a30e02.png b/assets/amiibo/images/icon_09cb0401-02a30e02.png new file mode 100644 index 000000000..cc6dd95e3 Binary files /dev/null and b/assets/amiibo/images/icon_09cb0401-02a30e02.png differ diff --git a/assets/amiibo/images/icon_09cb0501-02a40e02.png b/assets/amiibo/images/icon_09cb0501-02a40e02.png new file mode 100644 index 000000000..2d83d372a Binary files /dev/null and b/assets/amiibo/images/icon_09cb0501-02a40e02.png differ diff --git a/assets/amiibo/images/icon_09cc0101-02a50e02.png b/assets/amiibo/images/icon_09cc0101-02a50e02.png new file mode 100644 index 000000000..fed772541 Binary files /dev/null and b/assets/amiibo/images/icon_09cc0101-02a50e02.png differ diff --git a/assets/amiibo/images/icon_09cc0201-02a60e02.png b/assets/amiibo/images/icon_09cc0201-02a60e02.png new file mode 100644 index 000000000..b9e0f5d60 Binary files /dev/null and b/assets/amiibo/images/icon_09cc0201-02a60e02.png differ diff --git a/assets/amiibo/images/icon_09cc0301-02a70e02.png b/assets/amiibo/images/icon_09cc0301-02a70e02.png new file mode 100644 index 000000000..6ae820337 Binary files /dev/null and b/assets/amiibo/images/icon_09cc0301-02a70e02.png differ diff --git a/assets/amiibo/images/icon_09cc0401-02a80e02.png b/assets/amiibo/images/icon_09cc0401-02a80e02.png new file mode 100644 index 000000000..5683d4d60 Binary files /dev/null and b/assets/amiibo/images/icon_09cc0401-02a80e02.png differ diff --git a/assets/amiibo/images/icon_09cc0501-02a90e02.png b/assets/amiibo/images/icon_09cc0501-02a90e02.png new file mode 100644 index 000000000..14563a651 Binary files /dev/null and b/assets/amiibo/images/icon_09cc0501-02a90e02.png differ diff --git a/assets/amiibo/images/icon_09cd0101-02aa0e02.png b/assets/amiibo/images/icon_09cd0101-02aa0e02.png new file mode 100644 index 000000000..b078973ea Binary files /dev/null and b/assets/amiibo/images/icon_09cd0101-02aa0e02.png differ diff --git a/assets/amiibo/images/icon_09cd0201-02ab0e02.png b/assets/amiibo/images/icon_09cd0201-02ab0e02.png new file mode 100644 index 000000000..40e8ebe08 Binary files /dev/null and b/assets/amiibo/images/icon_09cd0201-02ab0e02.png differ diff --git a/assets/amiibo/images/icon_09cd0301-02ac0e02.png b/assets/amiibo/images/icon_09cd0301-02ac0e02.png new file mode 100644 index 000000000..728bdd9bc Binary files /dev/null and b/assets/amiibo/images/icon_09cd0301-02ac0e02.png differ diff --git a/assets/amiibo/images/icon_09cd0401-02ad0e02.png b/assets/amiibo/images/icon_09cd0401-02ad0e02.png new file mode 100644 index 000000000..dd4c21a1c Binary files /dev/null and b/assets/amiibo/images/icon_09cd0401-02ad0e02.png differ diff --git a/assets/amiibo/images/icon_09cd0501-02ae0e02.png b/assets/amiibo/images/icon_09cd0501-02ae0e02.png new file mode 100644 index 000000000..589141a07 Binary files /dev/null and b/assets/amiibo/images/icon_09cd0501-02ae0e02.png differ diff --git a/assets/amiibo/images/icon_09ce0101-02af0e02.png b/assets/amiibo/images/icon_09ce0101-02af0e02.png new file mode 100644 index 000000000..f11a9255d Binary files /dev/null and b/assets/amiibo/images/icon_09ce0101-02af0e02.png differ diff --git a/assets/amiibo/images/icon_09ce0201-02b00e02.png b/assets/amiibo/images/icon_09ce0201-02b00e02.png new file mode 100644 index 000000000..a00c613bd Binary files /dev/null and b/assets/amiibo/images/icon_09ce0201-02b00e02.png differ diff --git a/assets/amiibo/images/icon_09ce0301-02b10e02.png b/assets/amiibo/images/icon_09ce0301-02b10e02.png new file mode 100644 index 000000000..cd80a6c9d Binary files /dev/null and b/assets/amiibo/images/icon_09ce0301-02b10e02.png differ diff --git a/assets/amiibo/images/icon_09ce0401-02b20e02.png b/assets/amiibo/images/icon_09ce0401-02b20e02.png new file mode 100644 index 000000000..0a487fa8c Binary files /dev/null and b/assets/amiibo/images/icon_09ce0401-02b20e02.png differ diff --git a/assets/amiibo/images/icon_09ce0501-02b30e02.png b/assets/amiibo/images/icon_09ce0501-02b30e02.png new file mode 100644 index 000000000..f0b64e622 Binary files /dev/null and b/assets/amiibo/images/icon_09ce0501-02b30e02.png differ diff --git a/assets/amiibo/images/icon_09cf0101-02b40e02.png b/assets/amiibo/images/icon_09cf0101-02b40e02.png new file mode 100644 index 000000000..631fc0625 Binary files /dev/null and b/assets/amiibo/images/icon_09cf0101-02b40e02.png differ diff --git a/assets/amiibo/images/icon_09cf0201-02b50e02.png b/assets/amiibo/images/icon_09cf0201-02b50e02.png new file mode 100644 index 000000000..1369f4a0a Binary files /dev/null and b/assets/amiibo/images/icon_09cf0201-02b50e02.png differ diff --git a/assets/amiibo/images/icon_09cf0301-02b60e02.png b/assets/amiibo/images/icon_09cf0301-02b60e02.png new file mode 100644 index 000000000..47583c338 Binary files /dev/null and b/assets/amiibo/images/icon_09cf0301-02b60e02.png differ diff --git a/assets/amiibo/images/icon_09cf0401-02b70e02.png b/assets/amiibo/images/icon_09cf0401-02b70e02.png new file mode 100644 index 000000000..7ccca04fb Binary files /dev/null and b/assets/amiibo/images/icon_09cf0401-02b70e02.png differ diff --git a/assets/amiibo/images/icon_09cf0501-02b80e02.png b/assets/amiibo/images/icon_09cf0501-02b80e02.png new file mode 100644 index 000000000..d89f69c11 Binary files /dev/null and b/assets/amiibo/images/icon_09cf0501-02b80e02.png differ diff --git a/assets/amiibo/images/icon_09d00101-02b90e02.png b/assets/amiibo/images/icon_09d00101-02b90e02.png new file mode 100644 index 000000000..98251a253 Binary files /dev/null and b/assets/amiibo/images/icon_09d00101-02b90e02.png differ diff --git a/assets/amiibo/images/icon_09d00201-02ba0e02.png b/assets/amiibo/images/icon_09d00201-02ba0e02.png new file mode 100644 index 000000000..6fd847af8 Binary files /dev/null and b/assets/amiibo/images/icon_09d00201-02ba0e02.png differ diff --git a/assets/amiibo/images/icon_09d00301-02bb0e02.png b/assets/amiibo/images/icon_09d00301-02bb0e02.png new file mode 100644 index 000000000..d1777e410 Binary files /dev/null and b/assets/amiibo/images/icon_09d00301-02bb0e02.png differ diff --git a/assets/amiibo/images/icon_09d00401-02bc0e02.png b/assets/amiibo/images/icon_09d00401-02bc0e02.png new file mode 100644 index 000000000..d406b5401 Binary files /dev/null and b/assets/amiibo/images/icon_09d00401-02bc0e02.png differ diff --git a/assets/amiibo/images/icon_09d00501-02bd0e02.png b/assets/amiibo/images/icon_09d00501-02bd0e02.png new file mode 100644 index 000000000..3ace8999f Binary files /dev/null and b/assets/amiibo/images/icon_09d00501-02bd0e02.png differ diff --git a/assets/amiibo/images/icon_09d10101-02be0e02.png b/assets/amiibo/images/icon_09d10101-02be0e02.png new file mode 100644 index 000000000..faa60b54e Binary files /dev/null and b/assets/amiibo/images/icon_09d10101-02be0e02.png differ diff --git a/assets/amiibo/images/icon_09d10201-02bf0e02.png b/assets/amiibo/images/icon_09d10201-02bf0e02.png new file mode 100644 index 000000000..14d77a191 Binary files /dev/null and b/assets/amiibo/images/icon_09d10201-02bf0e02.png differ diff --git a/assets/amiibo/images/icon_09d10301-02c00e02.png b/assets/amiibo/images/icon_09d10301-02c00e02.png new file mode 100644 index 000000000..327294a0a Binary files /dev/null and b/assets/amiibo/images/icon_09d10301-02c00e02.png differ diff --git a/assets/amiibo/images/icon_09d10401-02c10e02.png b/assets/amiibo/images/icon_09d10401-02c10e02.png new file mode 100644 index 000000000..71d4cdbbf Binary files /dev/null and b/assets/amiibo/images/icon_09d10401-02c10e02.png differ diff --git a/assets/amiibo/images/icon_09d10501-02c20e02.png b/assets/amiibo/images/icon_09d10501-02c20e02.png new file mode 100644 index 000000000..4ec9c4fcd Binary files /dev/null and b/assets/amiibo/images/icon_09d10501-02c20e02.png differ diff --git a/assets/amiibo/images/icon_0a000001-03ab0502.png b/assets/amiibo/images/icon_0a000001-03ab0502.png new file mode 100644 index 000000000..510277318 Binary files /dev/null and b/assets/amiibo/images/icon_0a000001-03ab0502.png differ diff --git a/assets/amiibo/images/icon_0a010001-03ac0502.png b/assets/amiibo/images/icon_0a010001-03ac0502.png new file mode 100644 index 000000000..c7c554873 Binary files /dev/null and b/assets/amiibo/images/icon_0a010001-03ac0502.png differ diff --git a/assets/amiibo/images/icon_0a020001-03b30502.png b/assets/amiibo/images/icon_0a020001-03b30502.png new file mode 100644 index 000000000..d18384c2c Binary files /dev/null and b/assets/amiibo/images/icon_0a020001-03b30502.png differ diff --git a/assets/amiibo/images/icon_0a030001-03b40502.png b/assets/amiibo/images/icon_0a030001-03b40502.png new file mode 100644 index 000000000..926e0f1aa Binary files /dev/null and b/assets/amiibo/images/icon_0a030001-03b40502.png differ diff --git a/assets/amiibo/images/icon_0a040001-03b50502.png b/assets/amiibo/images/icon_0a040001-03b50502.png new file mode 100644 index 000000000..363f70b2f Binary files /dev/null and b/assets/amiibo/images/icon_0a040001-03b50502.png differ diff --git a/assets/amiibo/images/icon_0a050001-03b80502.png b/assets/amiibo/images/icon_0a050001-03b80502.png new file mode 100644 index 000000000..16d9bb7cd Binary files /dev/null and b/assets/amiibo/images/icon_0a050001-03b80502.png differ diff --git a/assets/amiibo/images/icon_0a060001-03ba0502.png b/assets/amiibo/images/icon_0a060001-03ba0502.png new file mode 100644 index 000000000..07bc3cd25 Binary files /dev/null and b/assets/amiibo/images/icon_0a060001-03ba0502.png differ diff --git a/assets/amiibo/images/icon_0a070001-03bc0502.png b/assets/amiibo/images/icon_0a070001-03bc0502.png new file mode 100644 index 000000000..3aa13de02 Binary files /dev/null and b/assets/amiibo/images/icon_0a070001-03bc0502.png differ diff --git a/assets/amiibo/images/icon_0a080001-03bd0502.png b/assets/amiibo/images/icon_0a080001-03bd0502.png new file mode 100644 index 000000000..caecf627c Binary files /dev/null and b/assets/amiibo/images/icon_0a080001-03bd0502.png differ diff --git a/assets/amiibo/images/icon_0a090001-03c00502.png b/assets/amiibo/images/icon_0a090001-03c00502.png new file mode 100644 index 000000000..d86a8f141 Binary files /dev/null and b/assets/amiibo/images/icon_0a090001-03c00502.png differ diff --git a/assets/amiibo/images/icon_0a0a0001-03c10502.png b/assets/amiibo/images/icon_0a0a0001-03c10502.png new file mode 100644 index 000000000..746aacea7 Binary files /dev/null and b/assets/amiibo/images/icon_0a0a0001-03c10502.png differ diff --git a/assets/amiibo/images/icon_0a0b0001-03c20502.png b/assets/amiibo/images/icon_0a0b0001-03c20502.png new file mode 100644 index 000000000..e29db73ab Binary files /dev/null and b/assets/amiibo/images/icon_0a0b0001-03c20502.png differ diff --git a/assets/amiibo/images/icon_0a0c0001-03c30502.png b/assets/amiibo/images/icon_0a0c0001-03c30502.png new file mode 100644 index 000000000..22f1a3f45 Binary files /dev/null and b/assets/amiibo/images/icon_0a0c0001-03c30502.png differ diff --git a/assets/amiibo/images/icon_0a0d0001-03c40502.png b/assets/amiibo/images/icon_0a0d0001-03c40502.png new file mode 100644 index 000000000..91c74f3c2 Binary files /dev/null and b/assets/amiibo/images/icon_0a0d0001-03c40502.png differ diff --git a/assets/amiibo/images/icon_0a0e0001-03c50502.png b/assets/amiibo/images/icon_0a0e0001-03c50502.png new file mode 100644 index 000000000..40f7bbc90 Binary files /dev/null and b/assets/amiibo/images/icon_0a0e0001-03c50502.png differ diff --git a/assets/amiibo/images/icon_0a0f0001-03c60502.png b/assets/amiibo/images/icon_0a0f0001-03c60502.png new file mode 100644 index 000000000..718cf8cb1 Binary files /dev/null and b/assets/amiibo/images/icon_0a0f0001-03c60502.png differ diff --git a/assets/amiibo/images/icon_0a100001-03c70502.png b/assets/amiibo/images/icon_0a100001-03c70502.png new file mode 100644 index 000000000..a94756028 Binary files /dev/null and b/assets/amiibo/images/icon_0a100001-03c70502.png differ diff --git a/assets/amiibo/images/icon_0a110001-03c80502.png b/assets/amiibo/images/icon_0a110001-03c80502.png new file mode 100644 index 000000000..8d1780827 Binary files /dev/null and b/assets/amiibo/images/icon_0a110001-03c80502.png differ diff --git a/assets/amiibo/images/icon_0a120001-03c90502.png b/assets/amiibo/images/icon_0a120001-03c90502.png new file mode 100644 index 000000000..501457991 Binary files /dev/null and b/assets/amiibo/images/icon_0a120001-03c90502.png differ diff --git a/assets/amiibo/images/icon_0a130001-03ca0502.png b/assets/amiibo/images/icon_0a130001-03ca0502.png new file mode 100644 index 000000000..8f00679e7 Binary files /dev/null and b/assets/amiibo/images/icon_0a130001-03ca0502.png differ diff --git a/assets/amiibo/images/icon_0a140001-03cb0502.png b/assets/amiibo/images/icon_0a140001-03cb0502.png new file mode 100644 index 000000000..2bb0a28c1 Binary files /dev/null and b/assets/amiibo/images/icon_0a140001-03cb0502.png differ diff --git a/assets/amiibo/images/icon_0a150001-03cc0502.png b/assets/amiibo/images/icon_0a150001-03cc0502.png new file mode 100644 index 000000000..5c4613f86 Binary files /dev/null and b/assets/amiibo/images/icon_0a150001-03cc0502.png differ diff --git a/assets/amiibo/images/icon_0a160001-03cd0502.png b/assets/amiibo/images/icon_0a160001-03cd0502.png new file mode 100644 index 000000000..d30a7be4e Binary files /dev/null and b/assets/amiibo/images/icon_0a160001-03cd0502.png differ diff --git a/assets/amiibo/images/icon_0a170001-03ce0502.png b/assets/amiibo/images/icon_0a170001-03ce0502.png new file mode 100644 index 000000000..ce8a63e93 Binary files /dev/null and b/assets/amiibo/images/icon_0a170001-03ce0502.png differ diff --git a/assets/amiibo/images/icon_0a180001-03cf0502.png b/assets/amiibo/images/icon_0a180001-03cf0502.png new file mode 100644 index 000000000..e6d69cef8 Binary files /dev/null and b/assets/amiibo/images/icon_0a180001-03cf0502.png differ diff --git a/assets/amiibo/images/icon_0a190001-03d00502.png b/assets/amiibo/images/icon_0a190001-03d00502.png new file mode 100644 index 000000000..49be62741 Binary files /dev/null and b/assets/amiibo/images/icon_0a190001-03d00502.png differ diff --git a/assets/amiibo/images/icon_0a1a0001-03d10502.png b/assets/amiibo/images/icon_0a1a0001-03d10502.png new file mode 100644 index 000000000..8d66162d9 Binary files /dev/null and b/assets/amiibo/images/icon_0a1a0001-03d10502.png differ diff --git a/assets/amiibo/images/icon_0a1b0001-03d20502.png b/assets/amiibo/images/icon_0a1b0001-03d20502.png new file mode 100644 index 000000000..5e8a03b75 Binary files /dev/null and b/assets/amiibo/images/icon_0a1b0001-03d20502.png differ diff --git a/assets/amiibo/images/icon_0a1c0001-03d30502.png b/assets/amiibo/images/icon_0a1c0001-03d30502.png new file mode 100644 index 000000000..ae4162a06 Binary files /dev/null and b/assets/amiibo/images/icon_0a1c0001-03d30502.png differ diff --git a/assets/amiibo/images/icon_0a1d0001-03d40502.png b/assets/amiibo/images/icon_0a1d0001-03d40502.png new file mode 100644 index 000000000..4f1882c01 Binary files /dev/null and b/assets/amiibo/images/icon_0a1d0001-03d40502.png differ diff --git a/assets/amiibo/images/icon_0a1e0001-03d50502.png b/assets/amiibo/images/icon_0a1e0001-03d50502.png new file mode 100644 index 000000000..28708b09e Binary files /dev/null and b/assets/amiibo/images/icon_0a1e0001-03d50502.png differ diff --git a/assets/amiibo/images/icon_0a1f0001-03d60502.png b/assets/amiibo/images/icon_0a1f0001-03d60502.png new file mode 100644 index 000000000..e7af35a0b Binary files /dev/null and b/assets/amiibo/images/icon_0a1f0001-03d60502.png differ diff --git a/assets/amiibo/images/icon_0a200001-03d70502.png b/assets/amiibo/images/icon_0a200001-03d70502.png new file mode 100644 index 000000000..6d22d1c07 Binary files /dev/null and b/assets/amiibo/images/icon_0a200001-03d70502.png differ diff --git a/assets/amiibo/images/icon_0a400000-041d0002.png b/assets/amiibo/images/icon_0a400000-041d0002.png new file mode 100644 index 000000000..9274e6a6f Binary files /dev/null and b/assets/amiibo/images/icon_0a400000-041d0002.png differ diff --git a/assets/amiibo/images/icon_19020000-03830002.png b/assets/amiibo/images/icon_19020000-03830002.png new file mode 100644 index 000000000..380231ccd Binary files /dev/null and b/assets/amiibo/images/icon_19020000-03830002.png differ diff --git a/assets/amiibo/images/icon_19060000-00240002.png b/assets/amiibo/images/icon_19060000-00240002.png new file mode 100644 index 000000000..cb3ed0a25 Binary files /dev/null and b/assets/amiibo/images/icon_19060000-00240002.png differ diff --git a/assets/amiibo/images/icon_19070000-03840002.png b/assets/amiibo/images/icon_19070000-03840002.png new file mode 100644 index 000000000..973b04912 Binary files /dev/null and b/assets/amiibo/images/icon_19070000-03840002.png differ diff --git a/assets/amiibo/images/icon_19190000-00090002.png b/assets/amiibo/images/icon_19190000-00090002.png new file mode 100644 index 000000000..bb245cc1a Binary files /dev/null and b/assets/amiibo/images/icon_19190000-00090002.png differ diff --git a/assets/amiibo/images/icon_19270000-00260002.png b/assets/amiibo/images/icon_19270000-00260002.png new file mode 100644 index 000000000..95ef6c6b4 Binary files /dev/null and b/assets/amiibo/images/icon_19270000-00260002.png differ diff --git a/assets/amiibo/images/icon_19960000-023d0002.png b/assets/amiibo/images/icon_19960000-023d0002.png new file mode 100644 index 000000000..fbf00009f Binary files /dev/null and b/assets/amiibo/images/icon_19960000-023d0002.png differ diff --git a/assets/amiibo/images/icon_19ac0000-03850002.png b/assets/amiibo/images/icon_19ac0000-03850002.png new file mode 100644 index 000000000..907fa0f29 Binary files /dev/null and b/assets/amiibo/images/icon_19ac0000-03850002.png differ diff --git a/assets/amiibo/images/icon_1ac00000-00110002.png b/assets/amiibo/images/icon_1ac00000-00110002.png new file mode 100644 index 000000000..160d69dd8 Binary files /dev/null and b/assets/amiibo/images/icon_1ac00000-00110002.png differ diff --git a/assets/amiibo/images/icon_1b920000-00250002.png b/assets/amiibo/images/icon_1b920000-00250002.png new file mode 100644 index 000000000..585dfffce Binary files /dev/null and b/assets/amiibo/images/icon_1b920000-00250002.png differ diff --git a/assets/amiibo/images/icon_1bd70000-03860002.png b/assets/amiibo/images/icon_1bd70000-03860002.png new file mode 100644 index 000000000..0a9991ee9 Binary files /dev/null and b/assets/amiibo/images/icon_1bd70000-03860002.png differ diff --git a/assets/amiibo/images/icon_1d000001-025c0d02.png b/assets/amiibo/images/icon_1d000001-025c0d02.png new file mode 100644 index 000000000..8aa9c256a Binary files /dev/null and b/assets/amiibo/images/icon_1d000001-025c0d02.png differ diff --git a/assets/amiibo/images/icon_1d010000-03750d02.png b/assets/amiibo/images/icon_1d010000-03750d02.png new file mode 100644 index 000000000..df0819ccb Binary files /dev/null and b/assets/amiibo/images/icon_1d010000-03750d02.png differ diff --git a/assets/amiibo/images/icon_1d400000-03870002.png b/assets/amiibo/images/icon_1d400000-03870002.png new file mode 100644 index 000000000..709bdb51d Binary files /dev/null and b/assets/amiibo/images/icon_1d400000-03870002.png differ diff --git a/assets/amiibo/images/icon_1f000000-000a0002.png b/assets/amiibo/images/icon_1f000000-000a0002.png new file mode 100644 index 000000000..a5b1eabbe Binary files /dev/null and b/assets/amiibo/images/icon_1f000000-000a0002.png differ diff --git a/assets/amiibo/images/icon_1f000000-02540c02.png b/assets/amiibo/images/icon_1f000000-02540c02.png new file mode 100644 index 000000000..8ddf186b9 Binary files /dev/null and b/assets/amiibo/images/icon_1f000000-02540c02.png differ diff --git a/assets/amiibo/images/icon_1f010000-00270002.png b/assets/amiibo/images/icon_1f010000-00270002.png new file mode 100644 index 000000000..448d7ede5 Binary files /dev/null and b/assets/amiibo/images/icon_1f010000-00270002.png differ diff --git a/assets/amiibo/images/icon_1f010000-02550c02.png b/assets/amiibo/images/icon_1f010000-02550c02.png new file mode 100644 index 000000000..d25052f61 Binary files /dev/null and b/assets/amiibo/images/icon_1f010000-02550c02.png differ diff --git a/assets/amiibo/images/icon_1f020000-00280002.png b/assets/amiibo/images/icon_1f020000-00280002.png new file mode 100644 index 000000000..d0d74dceb Binary files /dev/null and b/assets/amiibo/images/icon_1f020000-00280002.png differ diff --git a/assets/amiibo/images/icon_1f020000-02560c02.png b/assets/amiibo/images/icon_1f020000-02560c02.png new file mode 100644 index 000000000..20b535278 Binary files /dev/null and b/assets/amiibo/images/icon_1f020000-02560c02.png differ diff --git a/assets/amiibo/images/icon_1f030000-02570c02.png b/assets/amiibo/images/icon_1f030000-02570c02.png new file mode 100644 index 000000000..e4632f365 Binary files /dev/null and b/assets/amiibo/images/icon_1f030000-02570c02.png differ diff --git a/assets/amiibo/images/icon_1f400000-035e1002.png b/assets/amiibo/images/icon_1f400000-035e1002.png new file mode 100644 index 000000000..37f22b45a Binary files /dev/null and b/assets/amiibo/images/icon_1f400000-035e1002.png differ diff --git a/assets/amiibo/images/icon_21000000-000b0002.png b/assets/amiibo/images/icon_21000000-000b0002.png new file mode 100644 index 000000000..2760763fe Binary files /dev/null and b/assets/amiibo/images/icon_21000000-000b0002.png differ diff --git a/assets/amiibo/images/icon_21010000-00180002.png b/assets/amiibo/images/icon_21010000-00180002.png new file mode 100644 index 000000000..b17a65b05 Binary files /dev/null and b/assets/amiibo/images/icon_21010000-00180002.png differ diff --git a/assets/amiibo/images/icon_21020000-00290002.png b/assets/amiibo/images/icon_21020000-00290002.png new file mode 100644 index 000000000..7f39e642f Binary files /dev/null and b/assets/amiibo/images/icon_21020000-00290002.png differ diff --git a/assets/amiibo/images/icon_21030000-002a0002.png b/assets/amiibo/images/icon_21030000-002a0002.png new file mode 100644 index 000000000..f90edce7f Binary files /dev/null and b/assets/amiibo/images/icon_21030000-002a0002.png differ diff --git a/assets/amiibo/images/icon_21040000-02520002.png b/assets/amiibo/images/icon_21040000-02520002.png new file mode 100644 index 000000000..60dfd9339 Binary files /dev/null and b/assets/amiibo/images/icon_21040000-02520002.png differ diff --git a/assets/amiibo/images/icon_21050000-025a0002.png b/assets/amiibo/images/icon_21050000-025a0002.png new file mode 100644 index 000000000..8d351018c Binary files /dev/null and b/assets/amiibo/images/icon_21050000-025a0002.png differ diff --git a/assets/amiibo/images/icon_21050100-03630002.png b/assets/amiibo/images/icon_21050100-03630002.png new file mode 100644 index 000000000..9d8eabefb Binary files /dev/null and b/assets/amiibo/images/icon_21050100-03630002.png differ diff --git a/assets/amiibo/images/icon_21060000-03601202.png b/assets/amiibo/images/icon_21060000-03601202.png new file mode 100644 index 000000000..0d0130aef Binary files /dev/null and b/assets/amiibo/images/icon_21060000-03601202.png differ diff --git a/assets/amiibo/images/icon_21070000-03611202.png b/assets/amiibo/images/icon_21070000-03611202.png new file mode 100644 index 000000000..1b7908769 Binary files /dev/null and b/assets/amiibo/images/icon_21070000-03611202.png differ diff --git a/assets/amiibo/images/icon_21080000-036f1202.png b/assets/amiibo/images/icon_21080000-036f1202.png new file mode 100644 index 000000000..383ee8346 Binary files /dev/null and b/assets/amiibo/images/icon_21080000-036f1202.png differ diff --git a/assets/amiibo/images/icon_21080000-03880002.png b/assets/amiibo/images/icon_21080000-03880002.png new file mode 100644 index 000000000..b33e47866 Binary files /dev/null and b/assets/amiibo/images/icon_21080000-03880002.png differ diff --git a/assets/amiibo/images/icon_21090000-03701202.png b/assets/amiibo/images/icon_21090000-03701202.png new file mode 100644 index 000000000..b7650618d Binary files /dev/null and b/assets/amiibo/images/icon_21090000-03701202.png differ diff --git a/assets/amiibo/images/icon_210b0000-03a50002.png b/assets/amiibo/images/icon_210b0000-03a50002.png new file mode 100644 index 000000000..51c6bca8c Binary files /dev/null and b/assets/amiibo/images/icon_210b0000-03a50002.png differ diff --git a/assets/amiibo/images/icon_22400000-002b0002.png b/assets/amiibo/images/icon_22400000-002b0002.png new file mode 100644 index 000000000..c2a350ac3 Binary files /dev/null and b/assets/amiibo/images/icon_22400000-002b0002.png differ diff --git a/assets/amiibo/images/icon_22410000-041e0002.png b/assets/amiibo/images/icon_22410000-041e0002.png new file mode 100644 index 000000000..56ac2d891 Binary files /dev/null and b/assets/amiibo/images/icon_22410000-041e0002.png differ diff --git a/assets/amiibo/images/icon_22420000-041f0002.png b/assets/amiibo/images/icon_22420000-041f0002.png new file mode 100644 index 000000000..98ad4d2c1 Binary files /dev/null and b/assets/amiibo/images/icon_22420000-041f0002.png differ diff --git a/assets/amiibo/images/icon_22430000-043d1b02.png b/assets/amiibo/images/icon_22430000-043d1b02.png new file mode 100644 index 000000000..e26698a04 Binary files /dev/null and b/assets/amiibo/images/icon_22430000-043d1b02.png differ diff --git a/assets/amiibo/images/icon_22440000-043e1b02.png b/assets/amiibo/images/icon_22440000-043e1b02.png new file mode 100644 index 000000000..79b056f02 Binary files /dev/null and b/assets/amiibo/images/icon_22440000-043e1b02.png differ diff --git a/assets/amiibo/images/icon_22800000-002c0002.png b/assets/amiibo/images/icon_22800000-002c0002.png new file mode 100644 index 000000000..dd550c5cb Binary files /dev/null and b/assets/amiibo/images/icon_22800000-002c0002.png differ diff --git a/assets/amiibo/images/icon_22810000-02510002.png b/assets/amiibo/images/icon_22810000-02510002.png new file mode 100644 index 000000000..2956ad517 Binary files /dev/null and b/assets/amiibo/images/icon_22810000-02510002.png differ diff --git a/assets/amiibo/images/icon_22c00000-003a0202.png b/assets/amiibo/images/icon_22c00000-003a0202.png new file mode 100644 index 000000000..c2849b126 Binary files /dev/null and b/assets/amiibo/images/icon_22c00000-003a0202.png differ diff --git a/assets/amiibo/images/icon_32000000-00300002.png b/assets/amiibo/images/icon_32000000-00300002.png new file mode 100644 index 000000000..e1b5f5b6c Binary files /dev/null and b/assets/amiibo/images/icon_32000000-00300002.png differ diff --git a/assets/amiibo/images/icon_32400000-025b0002.png b/assets/amiibo/images/icon_32400000-025b0002.png new file mode 100644 index 000000000..50e456668 Binary files /dev/null and b/assets/amiibo/images/icon_32400000-025b0002.png differ diff --git a/assets/amiibo/images/icon_32400100-03640002.png b/assets/amiibo/images/icon_32400100-03640002.png new file mode 100644 index 000000000..95994cb88 Binary files /dev/null and b/assets/amiibo/images/icon_32400100-03640002.png differ diff --git a/assets/amiibo/images/icon_33400000-00320002.png b/assets/amiibo/images/icon_33400000-00320002.png new file mode 100644 index 000000000..eb0ec7edd Binary files /dev/null and b/assets/amiibo/images/icon_33400000-00320002.png differ diff --git a/assets/amiibo/images/icon_33800000-03781402.png b/assets/amiibo/images/icon_33800000-03781402.png new file mode 100644 index 000000000..fe1caf2c3 Binary files /dev/null and b/assets/amiibo/images/icon_33800000-03781402.png differ diff --git a/assets/amiibo/images/icon_33c00000-04200002.png b/assets/amiibo/images/icon_33c00000-04200002.png new file mode 100644 index 000000000..b234756ef Binary files /dev/null and b/assets/amiibo/images/icon_33c00000-04200002.png differ diff --git a/assets/amiibo/images/icon_34800000-00310002.png b/assets/amiibo/images/icon_34800000-00310002.png new file mode 100644 index 000000000..8a47fc455 Binary files /dev/null and b/assets/amiibo/images/icon_34800000-00310002.png differ diff --git a/assets/amiibo/images/icon_34800000-02580002.png b/assets/amiibo/images/icon_34800000-02580002.png new file mode 100644 index 000000000..9bb39319f Binary files /dev/null and b/assets/amiibo/images/icon_34800000-02580002.png differ diff --git a/assets/amiibo/images/icon_34800000-03791502.png b/assets/amiibo/images/icon_34800000-03791502.png new file mode 100644 index 000000000..67c2d9bd5 Binary files /dev/null and b/assets/amiibo/images/icon_34800000-03791502.png differ diff --git a/assets/amiibo/images/icon_34c00000-02530002.png b/assets/amiibo/images/icon_34c00000-02530002.png new file mode 100644 index 000000000..67b03e459 Binary files /dev/null and b/assets/amiibo/images/icon_34c00000-02530002.png differ diff --git a/assets/amiibo/images/icon_34c10000-03890002.png b/assets/amiibo/images/icon_34c10000-03890002.png new file mode 100644 index 000000000..4f42acca7 Binary files /dev/null and b/assets/amiibo/images/icon_34c10000-03890002.png differ diff --git a/assets/amiibo/images/icon_35000100-02e10f02.png b/assets/amiibo/images/icon_35000100-02e10f02.png new file mode 100644 index 000000000..98ea3e053 Binary files /dev/null and b/assets/amiibo/images/icon_35000100-02e10f02.png differ diff --git a/assets/amiibo/images/icon_35000200-02e20f02.png b/assets/amiibo/images/icon_35000200-02e20f02.png new file mode 100644 index 000000000..46ccb906e Binary files /dev/null and b/assets/amiibo/images/icon_35000200-02e20f02.png differ diff --git a/assets/amiibo/images/icon_35010000-02e30f02.png b/assets/amiibo/images/icon_35010000-02e30f02.png new file mode 100644 index 000000000..747b4c001 Binary files /dev/null and b/assets/amiibo/images/icon_35010000-02e30f02.png differ diff --git a/assets/amiibo/images/icon_35020100-02e40f02.png b/assets/amiibo/images/icon_35020100-02e40f02.png new file mode 100644 index 000000000..bb15618c1 Binary files /dev/null and b/assets/amiibo/images/icon_35020100-02e40f02.png differ diff --git a/assets/amiibo/images/icon_35030100-02e50f02.png b/assets/amiibo/images/icon_35030100-02e50f02.png new file mode 100644 index 000000000..ce4ca715c Binary files /dev/null and b/assets/amiibo/images/icon_35030100-02e50f02.png differ diff --git a/assets/amiibo/images/icon_35040100-02e60f02.png b/assets/amiibo/images/icon_35040100-02e60f02.png new file mode 100644 index 000000000..82ef53cd8 Binary files /dev/null and b/assets/amiibo/images/icon_35040100-02e60f02.png differ diff --git a/assets/amiibo/images/icon_35050000-040c0f02.png b/assets/amiibo/images/icon_35050000-040c0f02.png new file mode 100644 index 000000000..f36c21ae5 Binary files /dev/null and b/assets/amiibo/images/icon_35050000-040c0f02.png differ diff --git a/assets/amiibo/images/icon_35060000-040d0f02.png b/assets/amiibo/images/icon_35060000-040d0f02.png new file mode 100644 index 000000000..c3e362e30 Binary files /dev/null and b/assets/amiibo/images/icon_35060000-040d0f02.png differ diff --git a/assets/amiibo/images/icon_35070000-040e0f02.png b/assets/amiibo/images/icon_35070000-040e0f02.png new file mode 100644 index 000000000..0268c77df Binary files /dev/null and b/assets/amiibo/images/icon_35070000-040e0f02.png differ diff --git a/assets/amiibo/images/icon_35080000-040f1802.png b/assets/amiibo/images/icon_35080000-040f1802.png new file mode 100644 index 000000000..180032a97 Binary files /dev/null and b/assets/amiibo/images/icon_35080000-040f1802.png differ diff --git a/assets/amiibo/images/icon_35090000-04101802.png b/assets/amiibo/images/icon_35090000-04101802.png new file mode 100644 index 000000000..b51e56865 Binary files /dev/null and b/assets/amiibo/images/icon_35090000-04101802.png differ diff --git a/assets/amiibo/images/icon_35090100-042b1802.png b/assets/amiibo/images/icon_35090100-042b1802.png new file mode 100644 index 000000000..5f9be1e18 Binary files /dev/null and b/assets/amiibo/images/icon_35090100-042b1802.png differ diff --git a/assets/amiibo/images/icon_350a0000-04111802.png b/assets/amiibo/images/icon_350a0000-04111802.png new file mode 100644 index 000000000..86a830c38 Binary files /dev/null and b/assets/amiibo/images/icon_350a0000-04111802.png differ diff --git a/assets/amiibo/images/icon_350a0100-042c1802.png b/assets/amiibo/images/icon_350a0100-042c1802.png new file mode 100644 index 000000000..0bf05657b Binary files /dev/null and b/assets/amiibo/images/icon_350a0100-042c1802.png differ diff --git a/assets/amiibo/images/icon_350b0000-042d1802.png b/assets/amiibo/images/icon_350b0000-042d1802.png new file mode 100644 index 000000000..0cbfd3112 Binary files /dev/null and b/assets/amiibo/images/icon_350b0000-042d1802.png differ diff --git a/assets/amiibo/images/icon_35c00000-02500a02.png b/assets/amiibo/images/icon_35c00000-02500a02.png new file mode 100644 index 000000000..3b451e5fd Binary files /dev/null and b/assets/amiibo/images/icon_35c00000-02500a02.png differ diff --git a/assets/amiibo/images/icon_35c00000-03920a02.png b/assets/amiibo/images/icon_35c00000-03920a02.png new file mode 100644 index 000000000..3eff209bf Binary files /dev/null and b/assets/amiibo/images/icon_35c00000-03920a02.png differ diff --git a/assets/amiibo/images/icon_35c10000-036c0a02.png b/assets/amiibo/images/icon_35c10000-036c0a02.png new file mode 100644 index 000000000..6a6cc340b Binary files /dev/null and b/assets/amiibo/images/icon_35c10000-036c0a02.png differ diff --git a/assets/amiibo/images/icon_35c20000-036d0a02.png b/assets/amiibo/images/icon_35c20000-036d0a02.png new file mode 100644 index 000000000..a40f12e06 Binary files /dev/null and b/assets/amiibo/images/icon_35c20000-036d0a02.png differ diff --git a/assets/amiibo/images/icon_35c30000-036e0a02.png b/assets/amiibo/images/icon_35c30000-036e0a02.png new file mode 100644 index 000000000..f9b10e3b4 Binary files /dev/null and b/assets/amiibo/images/icon_35c30000-036e0a02.png differ diff --git a/assets/amiibo/images/icon_36000000-02590002.png b/assets/amiibo/images/icon_36000000-02590002.png new file mode 100644 index 000000000..5f989aa44 Binary files /dev/null and b/assets/amiibo/images/icon_36000000-02590002.png differ diff --git a/assets/amiibo/images/icon_36000100-03620002.png b/assets/amiibo/images/icon_36000100-03620002.png new file mode 100644 index 000000000..9d7fd0bb7 Binary files /dev/null and b/assets/amiibo/images/icon_36000100-03620002.png differ diff --git a/assets/amiibo/images/icon_36010000-04210002.png b/assets/amiibo/images/icon_36010000-04210002.png new file mode 100644 index 000000000..ea0a4faca Binary files /dev/null and b/assets/amiibo/images/icon_36010000-04210002.png differ diff --git a/assets/amiibo/images/icon_36400000-03a20002.png b/assets/amiibo/images/icon_36400000-03a20002.png new file mode 100644 index 000000000..ac5396b0a Binary files /dev/null and b/assets/amiibo/images/icon_36400000-03a20002.png differ diff --git a/assets/amiibo/images/icon_37400001-03741402.png b/assets/amiibo/images/icon_37400001-03741402.png new file mode 100644 index 000000000..379f45227 Binary files /dev/null and b/assets/amiibo/images/icon_37400001-03741402.png differ diff --git a/assets/amiibo/images/icon_37800000-038a0002.png b/assets/amiibo/images/icon_37800000-038a0002.png new file mode 100644 index 000000000..d9ee41829 Binary files /dev/null and b/assets/amiibo/images/icon_37800000-038a0002.png differ diff --git a/assets/amiibo/images/icon_37c00000-038b0002.png b/assets/amiibo/images/icon_37c00000-038b0002.png new file mode 100644 index 000000000..b27281627 Binary files /dev/null and b/assets/amiibo/images/icon_37c00000-038b0002.png differ diff --git a/assets/amiibo/images/icon_37c10000-038c0002.png b/assets/amiibo/images/icon_37c10000-038c0002.png new file mode 100644 index 000000000..80de69f07 Binary files /dev/null and b/assets/amiibo/images/icon_37c10000-038c0002.png differ diff --git a/assets/amiibo/images/icon_38000001-03931702.png b/assets/amiibo/images/icon_38000001-03931702.png new file mode 100644 index 000000000..ef4ac5d96 Binary files /dev/null and b/assets/amiibo/images/icon_38000001-03931702.png differ diff --git a/assets/amiibo/images/icon_38010001-03941702.png b/assets/amiibo/images/icon_38010001-03941702.png new file mode 100644 index 000000000..9eac608a1 Binary files /dev/null and b/assets/amiibo/images/icon_38010001-03941702.png differ diff --git a/assets/amiibo/images/icon_38020001-03951702.png b/assets/amiibo/images/icon_38020001-03951702.png new file mode 100644 index 000000000..c9871b605 Binary files /dev/null and b/assets/amiibo/images/icon_38020001-03951702.png differ diff --git a/assets/amiibo/images/icon_38030001-03961702.png b/assets/amiibo/images/icon_38030001-03961702.png new file mode 100644 index 000000000..5df5e5451 Binary files /dev/null and b/assets/amiibo/images/icon_38030001-03961702.png differ diff --git a/assets/amiibo/images/icon_38040001-03971702.png b/assets/amiibo/images/icon_38040001-03971702.png new file mode 100644 index 000000000..7a4ea23f2 Binary files /dev/null and b/assets/amiibo/images/icon_38040001-03971702.png differ diff --git a/assets/amiibo/images/icon_38050001-03981702.png b/assets/amiibo/images/icon_38050001-03981702.png new file mode 100644 index 000000000..761af21c2 Binary files /dev/null and b/assets/amiibo/images/icon_38050001-03981702.png differ diff --git a/assets/amiibo/images/icon_38400001-04241902.png b/assets/amiibo/images/icon_38400001-04241902.png new file mode 100644 index 000000000..026dba66e Binary files /dev/null and b/assets/amiibo/images/icon_38400001-04241902.png differ diff --git a/assets/amiibo/images/icon_38410001-04251902.png b/assets/amiibo/images/icon_38410001-04251902.png new file mode 100644 index 000000000..ca0e85254 Binary files /dev/null and b/assets/amiibo/images/icon_38410001-04251902.png differ diff --git a/assets/amiibo/images/icon_38420001-04261902.png b/assets/amiibo/images/icon_38420001-04261902.png new file mode 100644 index 000000000..d2124f8f8 Binary files /dev/null and b/assets/amiibo/images/icon_38420001-04261902.png differ diff --git a/assets/amiibo/images/icon_38430001-04271902.png b/assets/amiibo/images/icon_38430001-04271902.png new file mode 100644 index 000000000..d3b43a51c Binary files /dev/null and b/assets/amiibo/images/icon_38430001-04271902.png differ diff --git a/assets/amiibo/images/icon_38440001-04281902.png b/assets/amiibo/images/icon_38440001-04281902.png new file mode 100644 index 000000000..e492f8682 Binary files /dev/null and b/assets/amiibo/images/icon_38440001-04281902.png differ diff --git a/assets/amiibo/images/icon_38450001-04291902.png b/assets/amiibo/images/icon_38450001-04291902.png new file mode 100644 index 000000000..45fcc10f7 Binary files /dev/null and b/assets/amiibo/images/icon_38450001-04291902.png differ diff --git a/assets/amiibo/images/icon_38460001-042a1902.png b/assets/amiibo/images/icon_38460001-042a1902.png new file mode 100644 index 000000000..7d6001adb Binary files /dev/null and b/assets/amiibo/images/icon_38460001-042a1902.png differ diff --git a/assets/amiibo/images/icon_38c00000-03911602.png b/assets/amiibo/images/icon_38c00000-03911602.png new file mode 100644 index 000000000..c0b859397 Binary files /dev/null and b/assets/amiibo/images/icon_38c00000-03911602.png differ diff --git a/assets/amiibo/images/icon_3a000000-03a10002.png b/assets/amiibo/images/icon_3a000000-03a10002.png new file mode 100644 index 000000000..74d62f68d Binary files /dev/null and b/assets/amiibo/images/icon_3a000000-03a10002.png differ diff --git a/assets/amiibo/images/icon_3b400000-03a30002.png b/assets/amiibo/images/icon_3b400000-03a30002.png new file mode 100644 index 000000000..31f46a05e Binary files /dev/null and b/assets/amiibo/images/icon_3b400000-03a30002.png differ diff --git a/assets/amiibo/images/icon_3c800000-03a40002.png b/assets/amiibo/images/icon_3c800000-03a40002.png new file mode 100644 index 000000000..a84d3e7c0 Binary files /dev/null and b/assets/amiibo/images/icon_3c800000-03a40002.png differ diff --git a/assets/amiibo/images/icon_3dc00000-04220002.png b/assets/amiibo/images/icon_3dc00000-04220002.png new file mode 100644 index 000000000..cf567ee88 Binary files /dev/null and b/assets/amiibo/images/icon_3dc00000-04220002.png differ diff --git a/assets/amiibo/images/icon_3dc10000-04230002.png b/assets/amiibo/images/icon_3dc10000-04230002.png new file mode 100644 index 000000000..f218ad8d9 Binary files /dev/null and b/assets/amiibo/images/icon_3dc10000-04230002.png differ diff --git a/assets/amiibo/images/icon_3f000000-042e0002.png b/assets/amiibo/images/icon_3f000000-042e0002.png new file mode 100644 index 000000000..90f97bd2d Binary files /dev/null and b/assets/amiibo/images/icon_3f000000-042e0002.png differ 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/linux/appimage/AppRun b/distribution/linux/appimage/AppRun new file mode 100755 index 000000000..adbb70a0a --- /dev/null +++ b/distribution/linux/appimage/AppRun @@ -0,0 +1,3 @@ +#!/bin/sh +CURRENTDIR="$(readlink -f "$(dirname "$0")")" +exec "$CURRENTDIR"/usr/bin/Ryujinx.sh "$@" diff --git a/distribution/linux/appimage/build-appimage.sh b/distribution/linux/appimage/build-appimage.sh new file mode 100755 index 000000000..5c32d78a8 --- /dev/null +++ b/distribution/linux/appimage/build-appimage.sh @@ -0,0 +1,33 @@ +#!/bin/sh +set -eu + +ROOTDIR="$(readlink -f "$(dirname "$0")")"/../../../ +cd "$ROOTDIR" + +BUILDDIR=${BUILDDIR:-publish} +OUTDIR=${OUTDIR:-publish_appimage} +UFLAG=${UFLAG:-"gh-releases-zsync|GreemDev|ryujinx|latest|*-x64.AppImage.zsync"} + +rm -rf AppDir +mkdir -p AppDir/usr/bin + +cp distribution/linux/Ryujinx.desktop AppDir/Ryujinx.desktop +cp distribution/linux/appimage/AppRun AppDir/AppRun +cp src/Ryujinx.UI.Common/Resources/Logo_Ryujinx.png AppDir/Ryujinx.svg + + +cp -r "$BUILDDIR"/* AppDir/usr/bin/ + +# Ensure necessary bins are set as executable +chmod +x AppDir/AppRun AppDir/usr/bin/Ryujinx* + +mkdir -p "$OUTDIR" + +appimagetool --comp zstd --mksquashfs-opt -Xcompression-level --mksquashfs-opt 21 \ + -u "$UFLAG" \ + AppDir "$OUTDIR"/Ryujinx.AppImage + +# Move zsync file needed for delta updates +if [ "$RELEASE" = "1" ]; then + mv ./*.AppImage.zsync "$OUTDIR" +fi diff --git a/distribution/macos/Ryujinx.icns b/distribution/macos/Ryujinx.icns index f54a9aeb7..1bb88a5d7 100644 Binary files a/distribution/macos/Ryujinx.icns and b/distribution/macos/Ryujinx.icns differ diff --git a/distribution/macos/create_app_bundle.sh b/distribution/macos/create_app_bundle.sh index 858c06f59..e4397da84 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 @@ -46,5 +46,5 @@ then rcodesign sign --entitlements-xml-path "$ENTITLEMENTS_FILE_PATH" "$APP_BUNDLE_DIRECTORY" else echo "Usign codesign for ad-hoc signing" - codesign --entitlements "$ENTITLEMENTS_FILE_PATH" -f --deep -s - "$APP_BUNDLE_DIRECTORY" -fi \ No newline at end of file + codesign --entitlements "$ENTITLEMENTS_FILE_PATH" -f -s - "$APP_BUNDLE_DIRECTORY" +fi diff --git a/distribution/macos/create_macos_build_ava.sh b/distribution/macos/create_macos_build_ava.sh index 80594a40a..b19fa4863 100755 --- a/distribution/macos/create_macos_build_ava.sh +++ b/distribution/macos/create_macos_build_ava.sh @@ -2,8 +2,8 @@ set -e -if [ "$#" -lt 7 ]; then - echo "usage " +if [ "$#" -lt 8 ]; then + echo "usage " exit 1 fi @@ -18,13 +18,14 @@ ENTITLEMENTS_FILE_PATH=$(readlink -f "$4") VERSION=$5 SOURCE_REVISION_ID=$6 CONFIGURATION=$7 -EXTRA_ARGS=$8 +CANARY=$8 -if [ "$VERSION" == "1.1.0" ]; -then - RELEASE_TAR_FILE_NAME=test-ava-ryujinx-$CONFIGURATION-$VERSION+$SOURCE_REVISION_ID-macos_universal.app.tar +if [ "$CANARY" == "1" ]; then + RELEASE_TAR_FILE_NAME=ryujinx-canary-$VERSION-macos_universal.app.tar +elif [ "$VERSION" == "1.1.0" ]; then + 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 +39,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" @@ -61,7 +62,7 @@ mkdir -p "$OUTPUT_DIRECTORY" cp -R "$ARM64_APP_BUNDLE" "$UNIVERSAL_APP_BUNDLE" rm "$UNIVERSAL_APP_BUNDLE/$EXECUTABLE_SUB_PATH" -# Make it libraries universal +# Make its libraries universal python3 "$BASE_DIR/distribution/macos/construct_universal_dylib.py" "$ARM64_APP_BUNDLE" "$X64_APP_BUNDLE" "$UNIVERSAL_APP_BUNDLE" "**/*.dylib" if ! [ -x "$(command -v lipo)" ]; @@ -99,7 +100,7 @@ then rcodesign sign --entitlements-xml-path "$ENTITLEMENTS_FILE_PATH" "$UNIVERSAL_APP_BUNDLE" else echo "Using codesign for ad-hoc signing" - codesign --entitlements "$ENTITLEMENTS_FILE_PATH" -f --deep -s - "$UNIVERSAL_APP_BUNDLE" + codesign --entitlements "$ENTITLEMENTS_FILE_PATH" -f -s - "$UNIVERSAL_APP_BUNDLE" fi echo "Creating archive" @@ -108,6 +109,7 @@ 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" + popd -echo "Done" \ No newline at end of file +echo "Done" diff --git a/distribution/macos/create_macos_build_headless.sh b/distribution/macos/create_macos_build_headless.sh index a439aef45..01951d878 100755 --- a/distribution/macos/create_macos_build_headless.sh +++ b/distribution/macos/create_macos_build_headless.sh @@ -2,8 +2,8 @@ set -e -if [ "$#" -lt 7 ]; then - echo "usage " +if [ "$#" -lt 8 ]; then + echo "usage " exit 1 fi @@ -18,13 +18,14 @@ ENTITLEMENTS_FILE_PATH=$(readlink -f "$4") VERSION=$5 SOURCE_REVISION_ID=$6 CONFIGURATION=$7 -EXTRA_ARGS=$8 +CANARY=$8 -if [ "$VERSION" == "1.1.0" ]; -then - RELEASE_TAR_FILE_NAME=sdl2-ryujinx-headless-$CONFIGURATION-$VERSION+$SOURCE_REVISION_ID-macos_universal.tar +if [ "$CANARY" == "1" ]; then + RELEASE_TAR_FILE_NAME=nogui-ryujinx-canary-$VERSION-macos_universal.tar +elif [ "$VERSION" == "1.1.0" ]; then + RELEASE_TAR_FILE_NAME=nogui-ryujinx-$CONFIGURATION-$VERSION+$SOURCE_REVISION_ID-macos_universal.tar else - RELEASE_TAR_FILE_NAME=sdl2-ryujinx-headless-$VERSION-macos_universal.tar + RELEASE_TAR_FILE_NAME=nogui-ryujinx-$VERSION-macos_universal.tar fi ARM64_OUTPUT="$TEMP_DIRECTORY/publish_arm64" @@ -56,7 +57,7 @@ mkdir -p "$OUTPUT_DIRECTORY" cp -R "$ARM64_OUTPUT/" "$UNIVERSAL_OUTPUT" rm "$UNIVERSAL_OUTPUT/$EXECUTABLE_SUB_PATH" -# Make it libraries universal +# Make its libraries universal python3 "$BASE_DIR/distribution/macos/construct_universal_dylib.py" "$ARM64_OUTPUT" "$X64_OUTPUT" "$UNIVERSAL_OUTPUT" "**/*.dylib" if ! [ -x "$(command -v lipo)" ]; @@ -95,7 +96,7 @@ else echo "Using codesign for ad-hoc signing" for FILE in "$UNIVERSAL_OUTPUT"/*; do if [[ $(file "$FILE") == *"Mach-O"* ]]; then - codesign --entitlements "$ENTITLEMENTS_FILE_PATH" -f --deep -s - "$FILE" + codesign --entitlements "$ENTITLEMENTS_FILE_PATH" -f -s - "$FILE" fi done fi @@ -108,4 +109,4 @@ gzip -9 < "$RELEASE_TAR_FILE_NAME" > "$RELEASE_TAR_FILE_NAME.gz" rm "$RELEASE_TAR_FILE_NAME" popd -echo "Done" \ No newline at end of file +echo "Done" 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/distribution/misc/Logo.svg b/distribution/misc/Logo.svg index d6a76312a..d3327f2ef 100644 --- a/distribution/misc/Logo.svg +++ b/distribution/misc/Logo.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file 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/Arm64/HardwareCapabilities.cs b/src/ARMeilleure/CodeGen/Arm64/HardwareCapabilities.cs index 374f4746b..639e4476b 100644 --- a/src/ARMeilleure/CodeGen/Arm64/HardwareCapabilities.cs +++ b/src/ARMeilleure/CodeGen/Arm64/HardwareCapabilities.cs @@ -20,7 +20,7 @@ namespace ARMeilleure.CodeGen.Arm64 LinuxFeatureInfoHwCap2 = (LinuxFeatureFlagsHwCap2)getauxval(AT_HWCAP2); } - if (OperatingSystem.IsMacOS() || OperatingSystem.IsIOS()) + if (OperatingSystem.IsMacOS()) { for (int i = 0; i < _sysctlNames.Length; i++) { @@ -127,14 +127,13 @@ namespace ARMeilleure.CodeGen.Arm64 #region macOS [LibraryImport("libSystem.dylib", SetLastError = true)] - private static unsafe partial int sysctlbyname([MarshalAs(UnmanagedType.LPStr)] string name, out int oldValue, ref ulong oldSize, IntPtr newValue, ulong newValueSize); + private static unsafe partial int sysctlbyname([MarshalAs(UnmanagedType.LPStr)] string name, out int oldValue, ref ulong oldSize, nint newValue, ulong newValueSize); [SupportedOSPlatform("macos")] - [SupportedOSPlatform("ios")] private static bool CheckSysctlName(string name) { ulong size = sizeof(int); - if (sysctlbyname(name, out int val, ref size, IntPtr.Zero, 0) == 0 && size == sizeof(int)) + if (sysctlbyname(name, out int val, ref size, nint.Zero, 0) == 0 && size == sizeof(int)) { return val != 0; } diff --git a/src/ARMeilleure/CodeGen/CompiledFunction.cs b/src/ARMeilleure/CodeGen/CompiledFunction.cs index 485c85d16..8ea7ff532 100644 --- a/src/ARMeilleure/CodeGen/CompiledFunction.cs +++ b/src/ARMeilleure/CodeGen/CompiledFunction.cs @@ -58,9 +58,9 @@ namespace ARMeilleure.CodeGen /// Type of delegate /// Pointer to the function code in memory /// A delegate of type pointing to the mapped function - public T MapWithPointer(out IntPtr codePointer, bool deferProtect = false) + public T MapWithPointer(out nint codePointer) { - codePointer = JitCache.Map(this, deferProtect); + codePointer = JitCache.Map(this); return Marshal.GetDelegateForFunctionPointer(codePointer); } 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..3a16186d2 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; @@ -366,7 +387,7 @@ namespace ARMeilleure.CodeGen.RegisterAllocators public override int GetHashCode() { - return HashCode.Combine((IntPtr)_data); + return HashCode.Combine((nint)_data); } public override string ToString() diff --git a/src/ARMeilleure/CodeGen/RegisterAllocators/LiveRange.cs b/src/ARMeilleure/CodeGen/RegisterAllocators/LiveRange.cs index 412d597e8..dcd573a9d 100644 --- a/src/ARMeilleure/CodeGen/RegisterAllocators/LiveRange.cs +++ b/src/ARMeilleure/CodeGen/RegisterAllocators/LiveRange.cs @@ -63,7 +63,7 @@ namespace ARMeilleure.CodeGen.RegisterAllocators public override int GetHashCode() { - return HashCode.Combine((IntPtr)_data); + return HashCode.Combine((nint)_data); } public override string ToString() 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/Common/AddressTable.cs b/src/ARMeilleure/Common/AddressTable.cs deleted file mode 100644 index fcab3a202..000000000 --- a/src/ARMeilleure/Common/AddressTable.cs +++ /dev/null @@ -1,252 +0,0 @@ -using ARMeilleure.Diagnostics; -using System; -using System.Collections.Generic; -using System.Runtime.InteropServices; - -namespace ARMeilleure.Common -{ - /// - /// Represents a table of guest address to a value. - /// - /// Type of the value - public unsafe class AddressTable : IDisposable where TEntry : unmanaged - { - /// - /// Represents a level in an . - /// - public readonly struct Level - { - /// - /// Gets the index of the in the guest address. - /// - public int Index { get; } - - /// - /// Gets the length of the in the guest address. - /// - public int Length { get; } - - /// - /// Gets the mask which masks the bits used by the . - /// - public ulong Mask => ((1ul << Length) - 1) << Index; - - /// - /// Initializes a new instance of the structure with the specified - /// and . - /// - /// Index of the - /// Length of the - public Level(int index, int length) - { - (Index, Length) = (index, length); - } - - /// - /// Gets the value of the from the specified guest . - /// - /// Guest address - /// Value of the from the specified guest - public int GetValue(ulong address) - { - return (int)((address & Mask) >> Index); - } - } - - private bool _disposed; - private TEntry** _table; - private readonly List _pages; - - /// - /// Gets the bits used by the of the instance. - /// - public ulong Mask { get; } - - /// - /// Gets the s used by the instance. - /// - public Level[] Levels { get; } - - /// - /// Gets or sets the default fill value of newly created leaf pages. - /// - public TEntry Fill { get; set; } - - /// - /// Gets the base address of the . - /// - /// instance was disposed - public IntPtr Base - { - get - { - ObjectDisposedException.ThrowIf(_disposed, this); - - lock (_pages) - { - return (IntPtr)GetRootPage(); - } - } - } - - /// - /// Constructs a new instance of the class with the specified list of - /// . - /// - /// is null - /// Length of is less than 2 - public AddressTable(Level[] levels) - { - ArgumentNullException.ThrowIfNull(levels); - - if (levels.Length < 2) - { - throw new ArgumentException("Table must be at least 2 levels deep.", nameof(levels)); - } - - _pages = new List(capacity: 16); - - Levels = levels; - Mask = 0; - - foreach (var level in Levels) - { - Mask |= level.Mask; - } - } - - /// - /// Determines if the specified is in the range of the - /// . - /// - /// Guest address - /// if is valid; otherwise - public bool IsValid(ulong address) - { - return (address & ~Mask) == 0; - } - - /// - /// Gets a reference to the value at the specified guest . - /// - /// Guest address - /// Reference to the value at the specified guest - /// instance was disposed - /// is not mapped - public ref TEntry GetValue(ulong address) - { - ObjectDisposedException.ThrowIf(_disposed, this); - - if (!IsValid(address)) - { - throw new ArgumentException($"Address 0x{address:X} is not mapped onto the table.", nameof(address)); - } - - lock (_pages) - { - return ref GetPage(address)[Levels[^1].GetValue(address)]; - } - } - - /// - /// Gets the leaf page for the specified guest . - /// - /// Guest address - /// Leaf page for the specified guest - private TEntry* GetPage(ulong address) - { - TEntry** page = GetRootPage(); - - for (int i = 0; i < Levels.Length - 1; i++) - { - ref Level level = ref Levels[i]; - ref TEntry* nextPage = ref page[level.GetValue(address)]; - - if (nextPage == null) - { - ref Level nextLevel = ref Levels[i + 1]; - - nextPage = i == Levels.Length - 2 ? - (TEntry*)Allocate(1 << nextLevel.Length, Fill, leaf: true) : - (TEntry*)Allocate(1 << nextLevel.Length, IntPtr.Zero, leaf: false); - } - - page = (TEntry**)nextPage; - } - - return (TEntry*)page; - } - - /// - /// Lazily initialize and get the root page of the . - /// - /// Root page of the - private TEntry** GetRootPage() - { - if (_table == null) - { - _table = (TEntry**)Allocate(1 << Levels[0].Length, fill: IntPtr.Zero, leaf: false); - } - - return _table; - } - - /// - /// Allocates a block of memory of the specified type and length. - /// - /// Type of elements - /// Number of elements - /// Fill value - /// if leaf; otherwise - /// Allocated block - private IntPtr Allocate(int length, T fill, bool leaf) where T : unmanaged - { - var size = sizeof(T) * length; - var page = (IntPtr)NativeAllocator.Instance.Allocate((uint)size); - var span = new Span((void*)page, length); - - span.Fill(fill); - - _pages.Add(page); - - TranslatorEventSource.Log.AddressTableAllocated(size, leaf); - - return page; - } - - /// - /// Releases all resources used by the instance. - /// - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - /// - /// Releases all unmanaged and optionally managed resources used by the - /// instance. - /// - /// to dispose managed resources also; otherwise just unmanaged resouces - protected virtual void Dispose(bool disposing) - { - if (!_disposed) - { - foreach (var page in _pages) - { - Marshal.FreeHGlobal(page); - } - - _disposed = true; - } - } - - /// - /// Frees resources used by the instance. - /// - ~AddressTable() - { - Dispose(false); - } - } -} diff --git a/src/ARMeilleure/Common/AddressTableLevel.cs b/src/ARMeilleure/Common/AddressTableLevel.cs new file mode 100644 index 000000000..6107726ee --- /dev/null +++ b/src/ARMeilleure/Common/AddressTableLevel.cs @@ -0,0 +1,44 @@ +namespace ARMeilleure.Common +{ + /// + /// Represents a level in an . + /// + public readonly struct AddressTableLevel + { + /// + /// Gets the index of the in the guest address. + /// + public int Index { get; } + + /// + /// Gets the length of the in the guest address. + /// + public int Length { get; } + + /// + /// Gets the mask which masks the bits used by the . + /// + public ulong Mask => ((1ul << Length) - 1) << Index; + + /// + /// Initializes a new instance of the structure with the specified + /// and . + /// + /// Index of the + /// Length of the + public AddressTableLevel(int index, int length) + { + (Index, Length) = (index, length); + } + + /// + /// Gets the value of the from the specified guest . + /// + /// Guest address + /// Value of the from the specified guest + public int GetValue(ulong address) + { + return (int)((address & Mask) >> Index); + } + } +} diff --git a/src/ARMeilleure/Common/AddressTablePresets.cs b/src/ARMeilleure/Common/AddressTablePresets.cs new file mode 100644 index 000000000..977e84a36 --- /dev/null +++ b/src/ARMeilleure/Common/AddressTablePresets.cs @@ -0,0 +1,75 @@ +namespace ARMeilleure.Common +{ + public static class AddressTablePresets + { + private static readonly AddressTableLevel[] _levels64Bit = + new AddressTableLevel[] + { + new(31, 17), + new(23, 8), + new(15, 8), + new( 7, 8), + new( 2, 5), + }; + + private static readonly AddressTableLevel[] _levels32Bit = + new AddressTableLevel[] + { + new(31, 17), + new(23, 8), + new(15, 8), + new( 7, 8), + new( 1, 6), + }; + + private static readonly AddressTableLevel[] _levels64BitSparseTiny = + new AddressTableLevel[] + { + new( 11, 28), + new( 2, 9), + }; + + private static readonly AddressTableLevel[] _levels32BitSparseTiny = + new AddressTableLevel[] + { + new( 10, 22), + new( 1, 9), + }; + + private static readonly AddressTableLevel[] _levels64BitSparseGiant = + new AddressTableLevel[] + { + new( 38, 1), + new( 2, 36), + }; + + private static readonly AddressTableLevel[] _levels32BitSparseGiant = + new AddressTableLevel[] + { + new( 31, 1), + new( 1, 30), + }; + + //high power will run worse on DDR3 systems and some DDR4 systems due to the higher ram utilization + //low power will never run worse than non-sparse, but for most systems it won't be necessary + //high power is always used, but I've left low power in here for future reference + public static AddressTableLevel[] GetArmPreset(bool for64Bits, bool sparse, bool lowPower = false) + { + if (sparse) + { + if (lowPower) + { + return for64Bits ? _levels64BitSparseTiny : _levels32BitSparseTiny; + } + else + { + return for64Bits ? _levels64BitSparseGiant : _levels32BitSparseGiant; + } + } + else + { + return for64Bits ? _levels64Bit : _levels32Bit; + } + } + } +} diff --git a/src/ARMeilleure/Common/Allocator.cs b/src/ARMeilleure/Common/Allocator.cs index 6905a614f..de6a77ebe 100644 --- a/src/ARMeilleure/Common/Allocator.cs +++ b/src/ARMeilleure/Common/Allocator.cs @@ -2,7 +2,7 @@ using System; namespace ARMeilleure.Common { - unsafe abstract class Allocator : IDisposable + public unsafe abstract class Allocator : IDisposable { public T* Allocate(ulong count = 1) where T : unmanaged { diff --git a/src/ARMeilleure/Common/ArenaAllocator.cs b/src/ARMeilleure/Common/ArenaAllocator.cs index ce8e33913..f9dbcbb20 100644 --- a/src/ARMeilleure/Common/ArenaAllocator.cs +++ b/src/ARMeilleure/Common/ArenaAllocator.cs @@ -20,7 +20,7 @@ namespace ARMeilleure.Common private List _pages; private readonly ulong _pageSize; private readonly uint _pageCount; - private readonly List _extras; + private readonly List _extras; public ArenaAllocator(uint pageSize, uint pageCount) { @@ -31,11 +31,11 @@ namespace ARMeilleure.Common _pageIndex = -1; _page = null; - _pages = new List(); + _pages = []; _pageSize = pageSize; _pageCount = pageCount; - _extras = new List(); + _extras = []; } public Span AllocateSpan(ulong count) where T : unmanaged @@ -64,7 +64,7 @@ namespace ARMeilleure.Common { void* extra = NativeAllocator.Instance.Allocate(size); - _extras.Add((IntPtr)extra); + _extras.Add((nint)extra); return extra; } @@ -84,7 +84,7 @@ namespace ARMeilleure.Common { _page = new PageInfo { - Pointer = (byte*)NativeAllocator.Instance.Allocate(_pageSize), + Pointer = (byte*)NativeAllocator.Instance.Allocate(_pageSize) }; _pages.Add(_page); @@ -114,7 +114,7 @@ namespace ARMeilleure.Common } // Free extra blocks that are not page-sized - foreach (IntPtr ptr in _extras) + foreach (nint ptr in _extras) { NativeAllocator.Instance.Free((void*)ptr); } @@ -173,7 +173,7 @@ namespace ARMeilleure.Common NativeAllocator.Instance.Free(info.Pointer); } - foreach (IntPtr ptr in _extras) + foreach (nint ptr in _extras) { NativeAllocator.Instance.Free((void*)ptr); } diff --git a/src/ARMeilleure/Common/EntryTable.cs b/src/ARMeilleure/Common/EntryTable.cs index 625e3f73f..e49a0989e 100644 --- a/src/ARMeilleure/Common/EntryTable.cs +++ b/src/ARMeilleure/Common/EntryTable.cs @@ -15,7 +15,7 @@ namespace ARMeilleure.Common private int _freeHint; private readonly int _pageCapacity; // Number of entries per page. private readonly int _pageLogCapacity; - private readonly Dictionary _pages; + private readonly Dictionary _pages; private readonly BitMap _allocated; /// @@ -41,7 +41,7 @@ namespace ARMeilleure.Common } _allocated = new BitMap(NativeAllocator.Instance); - _pages = new Dictionary(); + _pages = new Dictionary(); _pageLogCapacity = BitOperations.Log2((uint)(pageSize / sizeof(TEntry))); _pageCapacity = 1 << _pageLogCapacity; } @@ -138,9 +138,9 @@ namespace ARMeilleure.Common { var pageIndex = (int)((uint)(index & ~(_pageCapacity - 1)) >> _pageLogCapacity); - if (!_pages.TryGetValue(pageIndex, out IntPtr page)) + if (!_pages.TryGetValue(pageIndex, out nint page)) { - page = (IntPtr)NativeAllocator.Instance.Allocate((uint)sizeof(TEntry) * (uint)_pageCapacity); + page = (nint)NativeAllocator.Instance.Allocate((uint)sizeof(TEntry) * (uint)_pageCapacity); _pages.Add(pageIndex, page); } diff --git a/src/ARMeilleure/Common/IAddressTable.cs b/src/ARMeilleure/Common/IAddressTable.cs new file mode 100644 index 000000000..65077ec43 --- /dev/null +++ b/src/ARMeilleure/Common/IAddressTable.cs @@ -0,0 +1,51 @@ +using System; + +namespace ARMeilleure.Common +{ + public interface IAddressTable : IDisposable where TEntry : unmanaged + { + /// + /// True if the address table's bottom level is sparsely mapped. + /// This also ensures the second bottom level is filled with a dummy page rather than 0. + /// + bool Sparse { get; } + + /// + /// Gets the bits used by the of the instance. + /// + ulong Mask { get; } + + /// + /// Gets the s used by the instance. + /// + AddressTableLevel[] Levels { get; } + + /// + /// Gets or sets the default fill value of newly created leaf pages. + /// + TEntry Fill { get; set; } + + /// + /// Gets the base address of the . + /// + /// instance was disposed + nint Base { get; } + + /// + /// Determines if the specified is in the range of the + /// . + /// + /// Guest address + /// if is valid; otherwise + bool IsValid(ulong address); + + /// + /// Gets a reference to the value at the specified guest . + /// + /// Guest address + /// Reference to the value at the specified guest + /// instance was disposed + /// is not mapped + ref TEntry GetValue(ulong address); + } +} diff --git a/src/ARMeilleure/Common/NativeAllocator.cs b/src/ARMeilleure/Common/NativeAllocator.cs index 93c48adda..ffcffa4bc 100644 --- a/src/ARMeilleure/Common/NativeAllocator.cs +++ b/src/ARMeilleure/Common/NativeAllocator.cs @@ -3,13 +3,13 @@ using System.Runtime.InteropServices; namespace ARMeilleure.Common { - unsafe sealed class NativeAllocator : Allocator + public unsafe sealed class NativeAllocator : Allocator { public static NativeAllocator Instance { get; } = new(); public override void* Allocate(ulong size) { - void* result = (void*)Marshal.AllocHGlobal((IntPtr)size); + void* result = (void*)Marshal.AllocHGlobal((nint)size); if (result == null) { @@ -21,7 +21,7 @@ namespace ARMeilleure.Common public override void Free(void* block) { - Marshal.FreeHGlobal((IntPtr)block); + Marshal.FreeHGlobal((nint)block); } } } 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/InstEmitFlowHelper.cs b/src/ARMeilleure/Instructions/InstEmitFlowHelper.cs index 2009bafda..a602ea49e 100644 --- a/src/ARMeilleure/Instructions/InstEmitFlowHelper.cs +++ b/src/ARMeilleure/Instructions/InstEmitFlowHelper.cs @@ -193,6 +193,8 @@ namespace ARMeilleure.Instructions Operand hostAddress; + var table = context.FunctionTable; + // If address is mapped onto the function table, we can skip the table walk. Otherwise we fallback // onto the dispatch stub. if (guestAddress.Kind == OperandKind.Constant && context.FunctionTable.IsValid(guestAddress.Value)) @@ -203,6 +205,30 @@ namespace ARMeilleure.Instructions hostAddress = context.Load(OperandType.I64, hostAddressAddr); } + else if (table.Sparse) + { + // Inline table lookup. Only enabled when the sparse function table is enabled with 2 levels. + // Deliberately attempts to avoid branches. + + Operand tableBase = !context.HasPtc ? + Const(table.Base) : + Const(table.Base, Ptc.FunctionTableSymbol); + + hostAddress = tableBase; + + for (int i = 0; i < table.Levels.Length; i++) + { + var level = table.Levels[i]; + int clearBits = 64 - (level.Index + level.Length); + + Operand index = context.ShiftLeft( + context.ShiftRightUI(context.ShiftLeft(guestAddress, Const(clearBits)), Const(clearBits + level.Index)), + Const(3) + ); + + hostAddress = context.Load(OperandType.I64, context.Add(hostAddress, index)); + } + } else { hostAddress = !context.HasPtc ? 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/InstEmitSystem.cs b/src/ARMeilleure/Instructions/InstEmitSystem.cs index 8c430fc23..fbf3b4a70 100644 --- a/src/ARMeilleure/Instructions/InstEmitSystem.cs +++ b/src/ARMeilleure/Instructions/InstEmitSystem.cs @@ -49,6 +49,9 @@ namespace ARMeilleure.Instructions case 0b11_011_1101_0000_011: EmitGetTpidrroEl0(context); return; + case 0b11_011_1101_0000_101: + EmitGetTpidr2El0(context); + return; case 0b11_011_1110_0000_000: info = typeof(NativeInterface).GetMethod(nameof(NativeInterface.GetCntfrqEl0)); break; @@ -84,6 +87,9 @@ namespace ARMeilleure.Instructions case 0b11_011_1101_0000_010: EmitSetTpidrEl0(context); return; + case 0b11_011_1101_0000_101: + EmitGetTpidr2El0(context); + return; default: throw new NotImplementedException($"Unknown MSR 0x{op.RawOpCode:X8} at 0x{op.Address:X16}."); @@ -213,6 +219,17 @@ namespace ARMeilleure.Instructions SetIntOrZR(context, op.Rt, result); } + private static void EmitGetTpidr2El0(ArmEmitterContext context) + { + OpCodeSystem op = (OpCodeSystem)context.CurrOp; + + Operand nativeContext = context.LoadArgument(OperandType.I64, 0); + + Operand result = context.Load(OperandType.I64, context.Add(nativeContext, Const((ulong)NativeContext.GetTpidr2El0Offset()))); + + SetIntOrZR(context, op.Rt, result); + } + private static void EmitSetNzcv(ArmEmitterContext context) { OpCodeSystem op = (OpCodeSystem)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/IntermediateRepresentation/IntrusiveList.cs b/src/ARMeilleure/IntermediateRepresentation/IntrusiveList.cs index 8d300075d..642e5aa90 100644 --- a/src/ARMeilleure/IntermediateRepresentation/IntrusiveList.cs +++ b/src/ARMeilleure/IntermediateRepresentation/IntrusiveList.cs @@ -32,7 +32,7 @@ namespace ARMeilleure.IntermediateRepresentation /// is not pointer sized. public IntrusiveList() { - if (Unsafe.SizeOf() != IntPtr.Size) + if (Unsafe.SizeOf() != nint.Size) { throw new ArgumentException("T must be a reference type or a pointer sized struct."); } diff --git a/src/ARMeilleure/IntermediateRepresentation/MemoryOperand.cs b/src/ARMeilleure/IntermediateRepresentation/MemoryOperand.cs index 9b3df8ca4..45695396f 100644 --- a/src/ARMeilleure/IntermediateRepresentation/MemoryOperand.cs +++ b/src/ARMeilleure/IntermediateRepresentation/MemoryOperand.cs @@ -24,7 +24,7 @@ namespace ARMeilleure.IntermediateRepresentation { Debug.Assert(operand.Kind == OperandKind.Memory); - _data = (Data*)Unsafe.As(ref operand); + _data = (Data*)Unsafe.As(ref operand); } public Operand BaseAddress diff --git a/src/ARMeilleure/IntermediateRepresentation/Operation.cs b/src/ARMeilleure/IntermediateRepresentation/Operation.cs index bc3a71b31..b0dc173af 100644 --- a/src/ARMeilleure/IntermediateRepresentation/Operation.cs +++ b/src/ARMeilleure/IntermediateRepresentation/Operation.cs @@ -228,7 +228,7 @@ namespace ARMeilleure.IntermediateRepresentation public readonly override int GetHashCode() { - return HashCode.Combine((IntPtr)_data); + return HashCode.Combine((nint)_data); } public static bool operator ==(Operation a, Operation b) 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/IJitMemoryBlock.cs b/src/ARMeilleure/Memory/IJitMemoryBlock.cs index c103fe8d1..59710d1ce 100644 --- a/src/ARMeilleure/Memory/IJitMemoryBlock.cs +++ b/src/ARMeilleure/Memory/IJitMemoryBlock.cs @@ -4,7 +4,7 @@ namespace ARMeilleure.Memory { public interface IJitMemoryBlock : IDisposable { - IntPtr Pointer { get; } + nint Pointer { get; } void Commit(ulong offset, ulong size); diff --git a/src/ARMeilleure/Memory/IMemoryManager.cs b/src/ARMeilleure/Memory/IMemoryManager.cs index 952cd2b4f..84d82caf7 100644 --- a/src/ARMeilleure/Memory/IMemoryManager.cs +++ b/src/ARMeilleure/Memory/IMemoryManager.cs @@ -6,7 +6,7 @@ namespace ARMeilleure.Memory { int AddressSpaceBits { get; } - IntPtr PageTablePointer { get; } + nint PageTablePointer { get; } MemoryManagerType Type { get; } @@ -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..bc8ae2635 100644 --- a/src/ARMeilleure/Memory/MemoryManagerType.cs +++ b/src/ARMeilleure/Memory/MemoryManagerType.cs @@ -18,12 +18,6 @@ namespace ARMeilleure.Memory /// SoftwarePageTable, - /// - /// High level implementation using a software flat page table for address translation, - /// no support for handling invalid or non-contiguous memory access. - /// - HostTracked, - /// /// High level implementation with mappings managed by the host OS, effectively using hardware /// page tables. No address translation is performed in software and the memory is just accessed directly. @@ -35,18 +29,35 @@ 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 + /// with no support for handling invalid or non-contiguous memory access. + /// + HostTracked, + + /// + /// 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/Memory/ReservedRegion.cs b/src/ARMeilleure/Memory/ReservedRegion.cs index 3870d4c84..a3ebd610d 100644 --- a/src/ARMeilleure/Memory/ReservedRegion.cs +++ b/src/ARMeilleure/Memory/ReservedRegion.cs @@ -8,7 +8,7 @@ namespace ARMeilleure.Memory public IJitMemoryBlock Block { get; } - public IntPtr Pointer => Block.Pointer; + public nint Pointer => Block.Pointer; private readonly ulong _maxSize; private readonly ulong _sizeGranularity; diff --git a/src/ARMeilleure/Native/JitSupportDarwin.cs b/src/ARMeilleure/Native/JitSupportDarwin.cs index 497362ef5..39df3878f 100644 --- a/src/ARMeilleure/Native/JitSupportDarwin.cs +++ b/src/ARMeilleure/Native/JitSupportDarwin.cs @@ -5,41 +5,9 @@ using System.Runtime.Versioning; namespace ARMeilleure.Native { [SupportedOSPlatform("macos")] - [SupportedOSPlatform("ios")] static partial class JitSupportDarwin { [LibraryImport("libarmeilleure-jitsupport", EntryPoint = "armeilleure_jit_memcpy")] - public static partial void Copy(IntPtr dst, IntPtr src, ulong n); - } - - [SupportedOSPlatform("ios")] - internal static partial class JitSupportDarwinAot - { - [LibraryImport("pthread", EntryPoint = "pthread_jit_write_protect_np")] - private static partial void pthread_jit_write_protect_np(int enabled); - - [LibraryImport("libc", EntryPoint = "sys_icache_invalidate")] - private static partial void sys_icache_invalidate(IntPtr start, IntPtr length); - - public static unsafe void Copy(IntPtr dst, IntPtr src, ulong n) { - // When NativeAOT is in use, we can toggle per-thread write protection without worrying about breaking .NET code. - - //pthread_jit_write_protect_np(0); - - var srcSpan = new Span(src.ToPointer(), (int)n); - var dstSpan = new Span(dst.ToPointer(), (int)n); - srcSpan.CopyTo(dstSpan); - - //pthread_jit_write_protect_np(1); - - // Ensure that the instruction cache for this range is invalidated. - sys_icache_invalidate(dst, (IntPtr)n); - } - - public static unsafe void Invalidate(IntPtr dst, ulong n) - { - // Ensure that the instruction cache for this range is invalidated. - sys_icache_invalidate(dst, (IntPtr)n); - } + public static partial void Copy(nint dst, nint src, ulong n); } } diff --git a/src/ARMeilleure/Optimizations.cs b/src/ARMeilleure/Optimizations.cs index 8fe478e47..18390de31 100644 --- a/src/ARMeilleure/Optimizations.cs +++ b/src/ARMeilleure/Optimizations.cs @@ -5,6 +5,9 @@ namespace ARMeilleure public static class Optimizations { + // low-core count PPTC + public static bool LowPower { get; set; } = false; + public static bool FastFP { get; set; } = true; public static bool AllowLcqInFunctionTable { get; set; } = true; @@ -51,8 +54,8 @@ namespace ARMeilleure internal static bool UseSse41 => UseSse41IfAvailable && X86HardwareCapabilities.SupportsSse41; internal static bool UseSse42 => UseSse42IfAvailable && X86HardwareCapabilities.SupportsSse42; internal static bool UsePopCnt => UsePopCntIfAvailable && X86HardwareCapabilities.SupportsPopcnt; - internal static bool UseAvx => UseAvxIfAvailable && X86HardwareCapabilities.SupportsAvx && !ForceLegacySse; - internal static bool UseAvx512F => UseAvx512FIfAvailable && X86HardwareCapabilities.SupportsAvx512F && !ForceLegacySse; + internal static bool UseAvx => UseAvxIfAvailable && X86HardwareCapabilities.SupportsAvx && !ForceLegacySse; + internal static bool UseAvx512F => UseAvx512FIfAvailable && X86HardwareCapabilities.SupportsAvx512F && !ForceLegacySse; internal static bool UseAvx512Vl => UseAvx512VlIfAvailable && X86HardwareCapabilities.SupportsAvx512Vl && !ForceLegacySse; internal static bool UseAvx512Bw => UseAvx512BwIfAvailable && X86HardwareCapabilities.SupportsAvx512Bw && !ForceLegacySse; internal static bool UseAvx512Dq => UseAvx512DqIfAvailable && X86HardwareCapabilities.SupportsAvx512Dq && !ForceLegacySse; diff --git a/src/ARMeilleure/Signal/NativeSignalHandler.cs b/src/ARMeilleure/Signal/NativeSignalHandlerGenerator.cs similarity index 61% rename from src/ARMeilleure/Signal/NativeSignalHandler.cs rename to src/ARMeilleure/Signal/NativeSignalHandlerGenerator.cs index 40860a5d7..35747d7a4 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 = 16; 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, nint 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); @@ -252,13 +87,13 @@ namespace ARMeilleure.Signal private static Operand GenerateUnixFaultAddress(EmitterContext context, Operand sigInfoPtr) { - ulong structAddressOffset = (OperatingSystem.IsMacOS() || OperatingSystem.IsIOS()) ? 24ul : 16ul; // si_addr + ulong structAddressOffset = OperatingSystem.IsMacOS() ? 24ul : 16ul; // si_addr return context.Load(OperandType.I64, context.Add(sigInfoPtr, Const(structAddressOffset))); } private static Operand GenerateUnixWriteFlag(EmitterContext context, Operand ucontextPtr) { - if (OperatingSystem.IsMacOS() || OperatingSystem.IsIOS()) + if (OperatingSystem.IsMacOS()) { const ulong McontextOffset = 48; // uc_mcontext Operand ctxPtr = context.Load(OperandType.I64, context.Add(ucontextPtr, Const(McontextOffset))); @@ -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(nint 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(nint 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/TestMethods.cs b/src/ARMeilleure/Signal/TestMethods.cs index 0a8b3f5ff..9d11ab183 100644 --- a/src/ARMeilleure/Signal/TestMethods.cs +++ b/src/ARMeilleure/Signal/TestMethods.cs @@ -16,7 +16,7 @@ namespace ARMeilleure.Signal { public delegate bool DebugPartialUnmap(); public delegate int DebugThreadLocalMapGetOrReserve(int threadId, int initialState); - public delegate void DebugNativeWriteLoop(IntPtr nativeWriteLoopPtr, IntPtr writePtr); + public delegate void DebugNativeWriteLoop(nint nativeWriteLoopPtr, nint writePtr); public static DebugPartialUnmap GenerateDebugPartialUnmap() { @@ -35,7 +35,7 @@ namespace ARMeilleure.Signal return Compiler.Compile(cfg, argTypes, OperandType.I32, CompilerOptions.HighCq, RuntimeInformation.ProcessArchitecture).Map(); } - public static DebugThreadLocalMapGetOrReserve GenerateDebugThreadLocalMapGetOrReserve(IntPtr structPtr) + public static DebugThreadLocalMapGetOrReserve GenerateDebugThreadLocalMapGetOrReserve(nint structPtr) { EmitterContext context = new(); diff --git a/src/ARMeilleure/Signal/WindowsPartialUnmapHandler.cs b/src/ARMeilleure/Signal/WindowsPartialUnmapHandler.cs index 27a9ea83c..7aa3e4788 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,17 +10,37 @@ 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 nint LoadLibrary([MarshalAs(UnmanagedType.LPStr)] string lpFileName); + + [LibraryImport("kernel32.dll", SetLastError = true)] + private static partial nint GetProcAddress(nint hModule, [MarshalAs(UnmanagedType.LPStr)] string procName); + + private static nint _getCurrentThreadIdPtr; + + public static nint GetCurrentThreadIdFunc() + { + if (_getCurrentThreadIdPtr == nint.Zero) + { + nint handle = LoadLibrary("kernel32.dll"); + + _getCurrentThreadIdPtr = GetProcAddress(handle, "GetCurrentThreadId"); + } + + return _getCurrentThreadIdPtr; + } + public static Operand EmitRetryFromAccessViolation(EmitterContext context) { - IntPtr partialRemapStatePtr = PartialUnmapState.GlobalState; - IntPtr localCountsPtr = IntPtr.Add(partialRemapStatePtr, PartialUnmapState.LocalCountsOffset); + nint partialRemapStatePtr = PartialUnmapState.GlobalState; + nint localCountsPtr = nint.Add(partialRemapStatePtr, PartialUnmapState.LocalCountsOffset); // Get the lock first. - EmitNativeReaderLockAcquire(context, IntPtr.Add(partialRemapStatePtr, PartialUnmapState.PartialUnmapLockOffset)); + EmitNativeReaderLockAcquire(context, nint.Add(partialRemapStatePtr, PartialUnmapState.PartialUnmapLockOffset)); - IntPtr getCurrentThreadId = WindowsSignalHandlerRegistration.GetCurrentThreadIdFunc(); + nint getCurrentThreadId = GetCurrentThreadIdFunc(); Operand threadId = context.Call(Const((ulong)getCurrentThreadId), OperandType.I32); Operand threadIndex = EmitThreadLocalMapIntGetOrReserve(context, localCountsPtr, threadId, Const(0)); @@ -38,7 +58,7 @@ namespace ARMeilleure.Signal Operand threadLocalPartialUnmapsPtr = EmitThreadLocalMapIntGetValuePtr(context, localCountsPtr, threadIndex); Operand threadLocalPartialUnmaps = context.Load(OperandType.I32, threadLocalPartialUnmapsPtr); - Operand partialUnmapsCount = context.Load(OperandType.I32, Const((ulong)IntPtr.Add(partialRemapStatePtr, PartialUnmapState.PartialUnmapsCountOffset))); + Operand partialUnmapsCount = context.Load(OperandType.I32, Const((ulong)nint.Add(partialRemapStatePtr, PartialUnmapState.PartialUnmapsCountOffset))); context.Copy(retry, context.ICompareNotEqual(threadLocalPartialUnmaps, partialUnmapsCount)); @@ -59,14 +79,14 @@ namespace ARMeilleure.Signal context.MarkLabel(endLabel); // Finally, release the lock and return the retry value. - EmitNativeReaderLockRelease(context, IntPtr.Add(partialRemapStatePtr, PartialUnmapState.PartialUnmapLockOffset)); + EmitNativeReaderLockRelease(context, nint.Add(partialRemapStatePtr, PartialUnmapState.PartialUnmapLockOffset)); return retry; } - public static Operand EmitThreadLocalMapIntGetOrReserve(EmitterContext context, IntPtr threadLocalMapPtr, Operand threadId, Operand initialState) + public static Operand EmitThreadLocalMapIntGetOrReserve(EmitterContext context, nint threadLocalMapPtr, Operand threadId, Operand initialState) { - Operand idsPtr = Const((ulong)IntPtr.Add(threadLocalMapPtr, ThreadLocalMap.ThreadIdsOffset)); + Operand idsPtr = Const((ulong)nint.Add(threadLocalMapPtr, ThreadLocalMap.ThreadIdsOffset)); Operand i = context.AllocateLocal(OperandType.I32); @@ -110,7 +130,7 @@ namespace ARMeilleure.Signal // If it was 0, then we need to initialize the struct entry and return i. context.BranchIfFalse(idNot0Label, context.ICompareEqual(existingId2, Const(0))); - Operand structsPtr = Const((ulong)IntPtr.Add(threadLocalMapPtr, ThreadLocalMap.StructsOffset)); + Operand structsPtr = Const((ulong)nint.Add(threadLocalMapPtr, ThreadLocalMap.StructsOffset)); Operand structPtr = context.Add(structsPtr, context.SignExtend32(OperandType.I64, offset2)); context.Store(structPtr, initialState); @@ -129,25 +149,14 @@ namespace ARMeilleure.Signal return context.Copy(i); } - private static Operand EmitThreadLocalMapIntGetValuePtr(EmitterContext context, IntPtr threadLocalMapPtr, Operand index) + private static Operand EmitThreadLocalMapIntGetValuePtr(EmitterContext context, nint threadLocalMapPtr, Operand index) { Operand offset = context.Multiply(index, Const(sizeof(int))); - Operand structsPtr = Const((ulong)IntPtr.Add(threadLocalMapPtr, ThreadLocalMap.StructsOffset)); + Operand structsPtr = Const((ulong)nint.Add(threadLocalMapPtr, ThreadLocalMap.StructsOffset)); 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(); @@ -161,9 +170,9 @@ namespace ARMeilleure.Signal context.BranchIfFalse(loop, context.ICompareEqual(initial, replaced)); } - private static void EmitNativeReaderLockAcquire(EmitterContext context, IntPtr nativeReaderLockPtr) + private static void EmitNativeReaderLockAcquire(EmitterContext context, nint nativeReaderLockPtr) { - Operand writeLockPtr = Const((ulong)IntPtr.Add(nativeReaderLockPtr, NativeReaderWriterLock.WriteLockOffset)); + Operand writeLockPtr = Const((ulong)nint.Add(nativeReaderLockPtr, NativeReaderWriterLock.WriteLockOffset)); // Spin until we can acquire the write lock. Operand spinLabel = Label(); @@ -173,16 +182,16 @@ namespace ARMeilleure.Signal context.BranchIfTrue(spinLabel, context.CompareAndSwap(writeLockPtr, Const(0), Const(1))); // Increment reader count. - EmitAtomicAddI32(context, Const((ulong)IntPtr.Add(nativeReaderLockPtr, NativeReaderWriterLock.ReaderCountOffset)), Const(1)); + EmitAtomicAddI32(context, Const((ulong)nint.Add(nativeReaderLockPtr, NativeReaderWriterLock.ReaderCountOffset)), Const(1)); // Release write lock. context.CompareAndSwap(writeLockPtr, Const(1), Const(0)); } - private static void EmitNativeReaderLockRelease(EmitterContext context, IntPtr nativeReaderLockPtr) + private static void EmitNativeReaderLockRelease(EmitterContext context, nint nativeReaderLockPtr) { // Decrement reader count. - EmitAtomicAddI32(context, Const((ulong)IntPtr.Add(nativeReaderLockPtr, NativeReaderWriterLock.ReaderCountOffset)), Const(-1)); + EmitAtomicAddI32(context, Const((ulong)nint.Add(nativeReaderLockPtr, NativeReaderWriterLock.ReaderCountOffset)), Const(-1)); } } } 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/State/ExecutionContext.cs b/src/ARMeilleure/State/ExecutionContext.cs index ce10a591c..314b06b13 100644 --- a/src/ARMeilleure/State/ExecutionContext.cs +++ b/src/ARMeilleure/State/ExecutionContext.cs @@ -9,7 +9,7 @@ namespace ARMeilleure.State private readonly NativeContext _nativeContext; - internal IntPtr NativeContextPtr => _nativeContext.BasePtr; + internal nint NativeContextPtr => _nativeContext.BasePtr; private bool _interrupted; diff --git a/src/ARMeilleure/State/NativeContext.cs b/src/ARMeilleure/State/NativeContext.cs index 5403042ea..140b6f7a7 100644 --- a/src/ARMeilleure/State/NativeContext.cs +++ b/src/ARMeilleure/State/NativeContext.cs @@ -21,13 +21,14 @@ namespace ARMeilleure.State public ulong ExclusiveValueLow; public ulong ExclusiveValueHigh; public int Running; + public long Tpidr2El0; } private static NativeCtxStorage _dummyStorage = new(); private readonly IJitMemoryBlock _block; - public IntPtr BasePtr => _block.Pointer; + public nint BasePtr => _block.Pointer; public NativeContext(IJitMemoryAllocator allocator) { @@ -176,6 +177,9 @@ namespace ARMeilleure.State public long GetTpidrroEl0() => GetStorage().TpidrroEl0; public void SetTpidrroEl0(long value) => GetStorage().TpidrroEl0 = value; + public long GetTpidr2El0() => GetStorage().Tpidr2El0; + public void SetTpidr2El0(long value) => GetStorage().Tpidr2El0 = value; + public int GetCounter() => GetStorage().Counter; public void SetCounter(int value) => GetStorage().Counter = value; @@ -232,6 +236,11 @@ namespace ARMeilleure.State return StorageOffset(ref _dummyStorage, ref _dummyStorage.TpidrroEl0); } + public static int GetTpidr2El0Offset() + { + return StorageOffset(ref _dummyStorage, ref _dummyStorage.Tpidr2El0); + } + public static int GetCounterOffset() { return StorageOffset(ref _dummyStorage, ref _dummyStorage.Counter); diff --git a/src/ARMeilleure/Translation/ArmEmitterContext.cs b/src/ARMeilleure/Translation/ArmEmitterContext.cs index e24074739..82f12bb02 100644 --- a/src/ARMeilleure/Translation/ArmEmitterContext.cs +++ b/src/ARMeilleure/Translation/ArmEmitterContext.cs @@ -46,7 +46,7 @@ namespace ARMeilleure.Translation public IMemoryManager Memory { get; } public EntryTable CountTable { get; } - public AddressTable FunctionTable { get; } + public IAddressTable FunctionTable { get; } public TranslatorStubs Stubs { get; } public ulong EntryAddress { get; } @@ -62,7 +62,7 @@ namespace ARMeilleure.Translation public ArmEmitterContext( IMemoryManager memory, EntryTable countTable, - AddressTable funcTable, + IAddressTable funcTable, TranslatorStubs stubs, ulong entryAddress, bool highCq, @@ -92,7 +92,7 @@ namespace ARMeilleure.Translation else { int index = Delegates.GetDelegateIndex(info); - IntPtr funcPtr = Delegates.GetDelegateFuncPtrByIndex(index); + nint funcPtr = Delegates.GetDelegateFuncPtrByIndex(index); OperandType returnType = GetOperandType(info.ReturnType); diff --git a/src/ARMeilleure/Translation/Cache/CacheMemoryAllocator.cs b/src/ARMeilleure/Translation/Cache/CacheMemoryAllocator.cs index a1bd3933a..f36bf7a3d 100644 --- a/src/ARMeilleure/Translation/Cache/CacheMemoryAllocator.cs +++ b/src/ARMeilleure/Translation/Cache/CacheMemoryAllocator.cs @@ -30,26 +30,21 @@ namespace ARMeilleure.Translation.Cache _blocks.Add(new MemoryBlock(0, capacity)); } - public int Allocate(ref int size, int alignment) + public int Allocate(int size) { - int alignM1 = alignment - 1; for (int i = 0; i < _blocks.Count; i++) { MemoryBlock block = _blocks[i]; - int misAlignment = ((block.Offset + alignM1) & (~alignM1)) - block.Offset; - int alignedSize = size + misAlignment; - if (block.Size > alignedSize) + if (block.Size > size) { - size = alignedSize; - _blocks[i] = new MemoryBlock(block.Offset + alignedSize, block.Size - alignedSize); - return block.Offset + misAlignment; + _blocks[i] = new MemoryBlock(block.Offset + size, block.Size - size); + return block.Offset; } - else if (block.Size == alignedSize) + else if (block.Size == size) { - size = alignedSize; _blocks.RemoveAt(i); - return block.Offset + misAlignment; + return block.Offset; } } diff --git a/src/ARMeilleure/Translation/Cache/JitCache.cs b/src/ARMeilleure/Translation/Cache/JitCache.cs index c27fab14f..cf13cd6cb 100644 --- a/src/ARMeilleure/Translation/Cache/JitCache.cs +++ b/src/ARMeilleure/Translation/Cache/JitCache.cs @@ -4,7 +4,6 @@ using ARMeilleure.Memory; using ARMeilleure.Native; using Ryujinx.Memory; using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Runtime.InteropServices; @@ -19,7 +18,6 @@ namespace ARMeilleure.Translation.Cache private const int CodeAlignment = 4; // Bytes. private const int CacheSize = 2047 * 1024 * 1024; - private const int CacheSizeIOS = 512 * 1024 * 1024; private static ReservedRegion _jitRegion; private static JitCacheInvalidation _jitCacheInvalidator; @@ -33,7 +31,7 @@ namespace ARMeilleure.Translation.Cache [SupportedOSPlatform("windows")] [LibraryImport("kernel32.dll", SetLastError = true)] - public static partial IntPtr FlushInstructionCache(IntPtr hProcess, IntPtr lpAddress, UIntPtr dwSize); + public static partial nint FlushInstructionCache(nint hProcess, nint lpAddress, nuint dwSize); public static void Initialize(IJitMemoryAllocator allocator) { @@ -49,9 +47,9 @@ namespace ARMeilleure.Translation.Cache return; } - _jitRegion = new ReservedRegion(allocator, (ulong)(OperatingSystem.IsIOS() ? CacheSizeIOS : CacheSize)); + _jitRegion = new ReservedRegion(allocator, CacheSize); - if (!OperatingSystem.IsWindows() && !OperatingSystem.IsMacOS() && !OperatingSystem.IsIOS()) + if (!OperatingSystem.IsWindows() && !OperatingSystem.IsMacOS()) { _jitCacheInvalidator = new JitCacheInvalidation(allocator); } @@ -67,17 +65,7 @@ namespace ARMeilleure.Translation.Cache } } - static ConcurrentQueue<(int funcOffset, int length)> _deferredRxProtect = new(); - - public static void RunDeferredRxProtects() - { - while (_deferredRxProtect.TryDequeue(out var result)) - { - ReprotectAsExecutable(result.funcOffset, result.length); - } - } - - public static IntPtr Map(CompiledFunction func, bool deferProtect) + public static nint Map(CompiledFunction func) { byte[] code = func.Code; @@ -85,31 +73,17 @@ namespace ARMeilleure.Translation.Cache { Debug.Assert(_initialized); - int funcOffset = Allocate(code.Length, deferProtect); + int funcOffset = Allocate(code.Length); - IntPtr funcPtr = _jitRegion.Pointer + funcOffset; + nint funcPtr = _jitRegion.Pointer + funcOffset; - if (OperatingSystem.IsIOS()) - { - Marshal.Copy(code, 0, funcPtr, code.Length); - if (deferProtect) - { - _deferredRxProtect.Enqueue((funcOffset, code.Length)); - } - else - { - ReprotectAsExecutable(funcOffset, code.Length); - - JitSupportDarwinAot.Invalidate(funcPtr, (ulong)code.Length); - } - } - else if (OperatingSystem.IsMacOS()&& RuntimeInformation.ProcessArchitecture == Architecture.Arm64) + if (OperatingSystem.IsMacOS() && RuntimeInformation.ProcessArchitecture == Architecture.Arm64) { unsafe { fixed (byte* codePtr = code) { - JitSupportDarwin.Copy(funcPtr, (IntPtr)codePtr, (ulong)code.Length); + JitSupportDarwin.Copy(funcPtr, (nint)codePtr, (ulong)code.Length); } } } @@ -121,7 +95,7 @@ namespace ARMeilleure.Translation.Cache if (OperatingSystem.IsWindows() && RuntimeInformation.ProcessArchitecture == Architecture.Arm64) { - FlushInstructionCache(Process.GetCurrentProcess().Handle, funcPtr, (UIntPtr)code.Length); + FlushInstructionCache(Process.GetCurrentProcess().Handle, funcPtr, (nuint)code.Length); } else { @@ -135,13 +109,8 @@ namespace ARMeilleure.Translation.Cache } } - public static void Unmap(IntPtr pointer) + public static void Unmap(nint pointer) { - if (OperatingSystem.IsIOS()) - { - return; - } - lock (_lock) { Debug.Assert(_initialized); @@ -176,22 +145,11 @@ namespace ARMeilleure.Translation.Cache _jitRegion.Block.MapAsRx((ulong)regionStart, (ulong)(regionEnd - regionStart)); } - private static int Allocate(int codeSize, bool deferProtect = false) + private static int Allocate(int codeSize) { - codeSize = AlignCodeSize(codeSize, deferProtect); + codeSize = AlignCodeSize(codeSize); - int alignment = CodeAlignment; - - if (OperatingSystem.IsIOS() && !deferProtect) - { - alignment = 0x4000; - } - - int allocOffset = _cacheAllocator.Allocate(ref codeSize, alignment); - - //DEBUG: Show JIT Memory Allocation - - //Console.WriteLine($"{allocOffset:x8}: {codeSize:x8} {alignment:x8}"); + int allocOffset = _cacheAllocator.Allocate(codeSize); if (allocOffset < 0) { @@ -203,16 +161,9 @@ namespace ARMeilleure.Translation.Cache return allocOffset; } - private static int AlignCodeSize(int codeSize, bool deferProtect = false) + private static int AlignCodeSize(int codeSize) { - int alignment = CodeAlignment; - - if (OperatingSystem.IsIOS() && !deferProtect) - { - alignment = 0x4000; - } - - return checked(codeSize + (alignment - 1)) & ~(alignment - 1); + return checked(codeSize + (CodeAlignment - 1)) & ~(CodeAlignment - 1); } private static void Add(int offset, int size, UnwindInfo unwindInfo) diff --git a/src/ARMeilleure/Translation/Cache/JitCacheInvalidation.cs b/src/ARMeilleure/Translation/Cache/JitCacheInvalidation.cs index 3aa2e19f1..6f9c22b4a 100644 --- a/src/ARMeilleure/Translation/Cache/JitCacheInvalidation.cs +++ b/src/ARMeilleure/Translation/Cache/JitCacheInvalidation.cs @@ -68,7 +68,7 @@ namespace ARMeilleure.Translation.Cache } } - public void Invalidate(IntPtr basePointer, ulong size) + public void Invalidate(nint basePointer, ulong size) { if (_needsInvalidation) { diff --git a/src/ARMeilleure/Translation/Cache/JitUnwindWindows.cs b/src/ARMeilleure/Translation/Cache/JitUnwindWindows.cs index 3c2a60a1a..642794188 100644 --- a/src/ARMeilleure/Translation/Cache/JitUnwindWindows.cs +++ b/src/ARMeilleure/Translation/Cache/JitUnwindWindows.cs @@ -40,7 +40,7 @@ namespace ARMeilleure.Translation.Cache PushMachframe = 10, } - private unsafe delegate RuntimeFunction* GetRuntimeFunctionCallback(ulong controlPc, IntPtr context); + private unsafe delegate RuntimeFunction* GetRuntimeFunctionCallback(ulong controlPc, nint context); [LibraryImport("kernel32.dll")] [return: MarshalAs(UnmanagedType.Bool)] @@ -49,7 +49,7 @@ namespace ARMeilleure.Translation.Cache ulong baseAddress, uint length, GetRuntimeFunctionCallback callback, - IntPtr context, + nint context, [MarshalAs(UnmanagedType.LPWStr)] string outOfProcessCallbackDll); private static GetRuntimeFunctionCallback _getRuntimeFunctionCallback; @@ -60,7 +60,7 @@ namespace ARMeilleure.Translation.Cache private unsafe static UnwindInfo* _unwindInfo; - public static void InstallFunctionTableHandler(IntPtr codeCachePointer, uint codeCacheLength, IntPtr workBufferPtr) + public static void InstallFunctionTableHandler(nint codeCachePointer, uint codeCacheLength, nint workBufferPtr) { ulong codeCachePtr = (ulong)codeCachePointer.ToInt64(); @@ -91,7 +91,7 @@ namespace ARMeilleure.Translation.Cache } } - private static unsafe RuntimeFunction* FunctionTableHandler(ulong controlPc, IntPtr context) + private static unsafe RuntimeFunction* FunctionTableHandler(ulong controlPc, nint context) { int offset = (int)((long)controlPc - context.ToInt64()); @@ -114,7 +114,7 @@ namespace ARMeilleure.Translation.Cache { int stackOffset = entry.StackOffsetOrAllocSize; - // Debug.Assert(stackOffset % 16 == 0); + Debug.Assert(stackOffset % 16 == 0); if (stackOffset <= 0xFFFF0) { @@ -135,7 +135,7 @@ namespace ARMeilleure.Translation.Cache { int allocSize = entry.StackOffsetOrAllocSize; - // Debug.Assert(allocSize % 8 == 0); + Debug.Assert(allocSize % 8 == 0); if (allocSize <= 128) { 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/DelegateInfo.cs b/src/ARMeilleure/Translation/DelegateInfo.cs index 706625437..d3b535de1 100644 --- a/src/ARMeilleure/Translation/DelegateInfo.cs +++ b/src/ARMeilleure/Translation/DelegateInfo.cs @@ -8,9 +8,9 @@ namespace ARMeilleure.Translation private readonly Delegate _dlg; // Ensure that this delegate will not be garbage collected. #pragma warning restore IDE0052 - public IntPtr FuncPtr { get; } + public nint FuncPtr { get; } - public DelegateInfo(Delegate dlg, IntPtr funcPtr) + public DelegateInfo(Delegate dlg, nint funcPtr) { _dlg = dlg; FuncPtr = funcPtr; diff --git a/src/ARMeilleure/Translation/Delegates.cs b/src/ARMeilleure/Translation/Delegates.cs index 66412b8e6..d8c1cfd58 100644 --- a/src/ARMeilleure/Translation/Delegates.cs +++ b/src/ARMeilleure/Translation/Delegates.cs @@ -9,7 +9,7 @@ namespace ARMeilleure.Translation { static class Delegates { - public static bool TryGetDelegateFuncPtrByIndex(int index, out IntPtr funcPtr) + public static bool TryGetDelegateFuncPtrByIndex(int index, out nint funcPtr) { if (index >= 0 && index < _delegates.Count) { @@ -25,7 +25,7 @@ namespace ARMeilleure.Translation } } - public static IntPtr GetDelegateFuncPtrByIndex(int index) + public static nint GetDelegateFuncPtrByIndex(int index) { if (index < 0 || index >= _delegates.Count) { @@ -35,7 +35,7 @@ namespace ARMeilleure.Translation return _delegates.Values[index].FuncPtr; // O(1). } - public static IntPtr GetDelegateFuncPtr(MethodInfo info) + public static nint GetDelegateFuncPtr(MethodInfo info) { ArgumentNullException.ThrowIfNull(info); @@ -65,7 +65,7 @@ namespace ARMeilleure.Translation return index; } - private static void SetDelegateInfo(Delegate dlg, IntPtr funcPtr) + private static void SetDelegateInfo(Delegate dlg, nint funcPtr) { string key = GetKey(dlg.Method); diff --git a/src/ARMeilleure/Translation/DispatcherFunction.cs b/src/ARMeilleure/Translation/DispatcherFunction.cs index 649fa0f50..f8b9dc31e 100644 --- a/src/ARMeilleure/Translation/DispatcherFunction.cs +++ b/src/ARMeilleure/Translation/DispatcherFunction.cs @@ -2,6 +2,6 @@ using System; namespace ARMeilleure.Translation { - delegate void DispatcherFunction(IntPtr nativeContext, ulong startAddress); - delegate ulong WrapperFunction(IntPtr nativeContext, ulong startAddress); + delegate void DispatcherFunction(nint nativeContext, ulong startAddress); + delegate ulong WrapperFunction(nint nativeContext, ulong startAddress); } diff --git a/src/ARMeilleure/Translation/Dominance.cs b/src/ARMeilleure/Translation/Dominance.cs index e2185bd85..b62714fdf 100644 --- a/src/ARMeilleure/Translation/Dominance.cs +++ b/src/ARMeilleure/Translation/Dominance.cs @@ -77,7 +77,7 @@ namespace ARMeilleure.Translation { continue; } - + for (int pBlkIndex = 0; pBlkIndex < block.Predecessors.Count; pBlkIndex++) { BasicBlock current = block.Predecessors[pBlkIndex]; diff --git a/src/ARMeilleure/Translation/EmitterContext.cs b/src/ARMeilleure/Translation/EmitterContext.cs index 88bfe1335..e2d860f82 100644 --- a/src/ARMeilleure/Translation/EmitterContext.cs +++ b/src/ARMeilleure/Translation/EmitterContext.cs @@ -97,7 +97,7 @@ namespace ARMeilleure.Translation public virtual Operand Call(MethodInfo info, params Operand[] callArgs) { - IntPtr funcPtr = Delegates.GetDelegateFuncPtr(info); + nint funcPtr = Delegates.GetDelegateFuncPtr(info); OperandType returnType = GetOperandType(info.ReturnType); diff --git a/src/ARMeilleure/Translation/GuestFunction.cs b/src/ARMeilleure/Translation/GuestFunction.cs index 6414d6bd0..5c7c733f9 100644 --- a/src/ARMeilleure/Translation/GuestFunction.cs +++ b/src/ARMeilleure/Translation/GuestFunction.cs @@ -2,5 +2,5 @@ using System; namespace ARMeilleure.Translation { - delegate ulong GuestFunction(IntPtr nativeContextPtr); + delegate ulong GuestFunction(nint nativeContextPtr); } diff --git a/src/ARMeilleure/Translation/PTC/Ptc.cs b/src/ARMeilleure/Translation/PTC/Ptc.cs index 5ed27927a..c722ce6be 100644 --- a/src/ARMeilleure/Translation/PTC/Ptc.cs +++ b/src/ARMeilleure/Translation/PTC/Ptc.cs @@ -3,7 +3,6 @@ using ARMeilleure.CodeGen.Linking; using ARMeilleure.CodeGen.Unwinding; using ARMeilleure.Common; using ARMeilleure.Memory; -using ARMeilleure.Translation.Cache; using Ryujinx.Common; using Ryujinx.Common.Configuration; using Ryujinx.Common.Logging; @@ -14,6 +13,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.IO.Compression; +using System.Linq; using System.Runtime; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -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 = 6992; //! To be incremented manually for each change to the ARMeilleure project. private const string ActualDir = "0"; private const string BackupDir = "1"; @@ -41,6 +41,7 @@ namespace ARMeilleure.Translation.PTC public static readonly Symbol PageTableSymbol = new(SymbolType.Special, 1); public static readonly Symbol CountTableSymbol = new(SymbolType.Special, 2); public static readonly Symbol DispatchStubSymbol = new(SymbolType.Special, 3); + public static readonly Symbol FunctionTableSymbol = new(SymbolType.Special, 4); private const byte FillingByte = 0x00; private const CompressionLevel SaveCompressionLevel = CompressionLevel.Fastest; @@ -101,7 +102,7 @@ namespace ARMeilleure.Translation.PTC Disable(); } - public void Initialize(string titleIdText, string displayVersion, bool enabled, MemoryManagerType memoryMode) + public void Initialize(string titleIdText, string displayVersion, bool enabled, MemoryManagerType memoryMode, string cacheSelector) { Wait(); @@ -127,6 +128,8 @@ namespace ARMeilleure.Translation.PTC DisplayVersion = !string.IsNullOrEmpty(displayVersion) ? displayVersion : DisplayVersionDefault; _memoryMode = memoryMode; + Logger.Info?.Print(LogClass.Ptc, $"PPTC (v{InternalVersion}) Profile: {DisplayVersion}-{cacheSelector}"); + string workPathActual = Path.Combine(AppDataManager.GamesDirPath, TitleIdText, "cache", "cpu", ActualDir); string workPathBackup = Path.Combine(AppDataManager.GamesDirPath, TitleIdText, "cache", "cpu", BackupDir); @@ -140,8 +143,8 @@ namespace ARMeilleure.Translation.PTC Directory.CreateDirectory(workPathBackup); } - CachePathActual = Path.Combine(workPathActual, DisplayVersion); - CachePathBackup = Path.Combine(workPathBackup, DisplayVersion); + CachePathActual = Path.Combine(workPathActual, DisplayVersion) + "-" + cacheSelector; + CachePathBackup = Path.Combine(workPathBackup, DisplayVersion) + "-" + cacheSelector; PreLoad(); Profiler.PreLoad(); @@ -269,11 +272,11 @@ namespace ARMeilleure.Translation.PTC return false; } - IntPtr intPtr = IntPtr.Zero; + nint intPtr = nint.Zero; try { - intPtr = Marshal.AllocHGlobal(new IntPtr(outerHeader.UncompressedStreamSize)); + intPtr = Marshal.AllocHGlobal(new nint(outerHeader.UncompressedStreamSize)); using UnmanagedMemoryStream stream = new((byte*)intPtr.ToPointer(), outerHeader.UncompressedStreamSize, outerHeader.UncompressedStreamSize, FileAccess.ReadWrite); try @@ -310,7 +313,7 @@ namespace ARMeilleure.Translation.PTC ReadOnlySpan infosBytes = new(stream.PositionPointer, innerHeader.InfosLength); stream.Seek(innerHeader.InfosLength, SeekOrigin.Current); - Hash128 infosHash = XXHash128.ComputeHash(infosBytes); + Hash128 infosHash = Hash128.ComputeHash(infosBytes); if (innerHeader.InfosHash != infosHash) { @@ -322,7 +325,7 @@ namespace ARMeilleure.Translation.PTC ReadOnlySpan codesBytes = (int)innerHeader.CodesLength > 0 ? new(stream.PositionPointer, (int)innerHeader.CodesLength) : ReadOnlySpan.Empty; stream.Seek(innerHeader.CodesLength, SeekOrigin.Current); - Hash128 codesHash = XXHash128.ComputeHash(codesBytes); + Hash128 codesHash = Hash128.ComputeHash(codesBytes); if (innerHeader.CodesHash != codesHash) { @@ -334,7 +337,7 @@ namespace ARMeilleure.Translation.PTC ReadOnlySpan relocsBytes = new(stream.PositionPointer, innerHeader.RelocsLength); stream.Seek(innerHeader.RelocsLength, SeekOrigin.Current); - Hash128 relocsHash = XXHash128.ComputeHash(relocsBytes); + Hash128 relocsHash = Hash128.ComputeHash(relocsBytes); if (innerHeader.RelocsHash != relocsHash) { @@ -346,7 +349,7 @@ namespace ARMeilleure.Translation.PTC ReadOnlySpan unwindInfosBytes = new(stream.PositionPointer, innerHeader.UnwindInfosLength); stream.Seek(innerHeader.UnwindInfosLength, SeekOrigin.Current); - Hash128 unwindInfosHash = XXHash128.ComputeHash(unwindInfosBytes); + Hash128 unwindInfosHash = Hash128.ComputeHash(unwindInfosBytes); if (innerHeader.UnwindInfosHash != unwindInfosHash) { @@ -374,7 +377,7 @@ namespace ARMeilleure.Translation.PTC } finally { - if (intPtr != IntPtr.Zero) + if (intPtr != nint.Zero) { Marshal.FreeHGlobal(intPtr); } @@ -456,11 +459,11 @@ namespace ARMeilleure.Translation.PTC outerHeader.SetHeaderHash(); - IntPtr intPtr = IntPtr.Zero; + nint intPtr = nint.Zero; try { - intPtr = Marshal.AllocHGlobal(new IntPtr(outerHeader.UncompressedStreamSize)); + intPtr = Marshal.AllocHGlobal(new nint(outerHeader.UncompressedStreamSize)); using UnmanagedMemoryStream stream = new((byte*)intPtr.ToPointer(), outerHeader.UncompressedStreamSize, outerHeader.UncompressedStreamSize, FileAccess.ReadWrite); stream.Seek((long)Unsafe.SizeOf(), SeekOrigin.Begin); @@ -479,10 +482,10 @@ namespace ARMeilleure.Translation.PTC Debug.Assert(stream.Position == stream.Length); - innerHeader.InfosHash = XXHash128.ComputeHash(infosBytes); - innerHeader.CodesHash = XXHash128.ComputeHash(codesBytes); - innerHeader.RelocsHash = XXHash128.ComputeHash(relocsBytes); - innerHeader.UnwindInfosHash = XXHash128.ComputeHash(unwindInfosBytes); + innerHeader.InfosHash = Hash128.ComputeHash(infosBytes); + innerHeader.CodesHash = Hash128.ComputeHash(codesBytes); + innerHeader.RelocsHash = Hash128.ComputeHash(relocsBytes); + innerHeader.UnwindInfosHash = Hash128.ComputeHash(unwindInfosBytes); innerHeader.SetHeaderHash(); @@ -514,7 +517,7 @@ namespace ARMeilleure.Translation.PTC } finally { - if (intPtr != IntPtr.Zero) + if (intPtr != nint.Zero) { Marshal.FreeHGlobal(intPtr); } @@ -665,7 +668,7 @@ namespace ARMeilleure.Translation.PTC foreach (RelocEntry relocEntry in relocEntries) { - IntPtr? imm = null; + nint? imm = null; Symbol symbol = relocEntry.Symbol; if (symbol.Type == SymbolType.FunctionTable) @@ -676,7 +679,7 @@ namespace ARMeilleure.Translation.PTC { unsafe { - imm = (IntPtr)Unsafe.AsPointer(ref translator.FunctionTable.GetValue(guestAddress)); + imm = (nint)Unsafe.AsPointer(ref translator.FunctionTable.GetValue(guestAddress)); } } } @@ -684,7 +687,7 @@ namespace ARMeilleure.Translation.PTC { int index = (int)symbol.Value; - if (Delegates.TryGetDelegateFuncPtrByIndex(index, out IntPtr funcPtr)) + if (Delegates.TryGetDelegateFuncPtrByIndex(index, out nint funcPtr)) { imm = funcPtr; } @@ -699,13 +702,17 @@ namespace ARMeilleure.Translation.PTC unsafe { - imm = (IntPtr)Unsafe.AsPointer(ref callCounter.Value); + imm = (nint)Unsafe.AsPointer(ref callCounter.Value); } } else if (symbol == DispatchStubSymbol) { imm = translator.Stubs.DispatchStub; } + else if (symbol == FunctionTableSymbol) + { + imm = translator.FunctionTable.Base; + } if (imm == null) { @@ -745,7 +752,7 @@ namespace ARMeilleure.Translation.PTC bool highCq) { var cFunc = new CompiledFunction(code, unwindInfo, RelocInfo.Empty); - var gFunc = cFunc.MapWithPointer(out IntPtr gFuncPointer, true); + var gFunc = cFunc.MapWithPointer(out nint gFuncPointer); return new TranslatedFunction(gFunc, gFuncPointer, callCounter, guestSize, highCq); } @@ -796,10 +803,15 @@ namespace ARMeilleure.Translation.PTC return; } + + int degreeOfParallelism = Environment.ProcessorCount; + if (Optimizations.LowPower) + degreeOfParallelism /= 3; + // If there are enough cores lying around, we leave one alone for other tasks. - if (degreeOfParallelism > 4) + if (degreeOfParallelism > 4 && !Optimizations.LowPower) { degreeOfParallelism--; } @@ -827,7 +839,7 @@ namespace ARMeilleure.Translation.PTC Debug.Assert(Profiler.IsAddressInStaticCodeRange(address)); - TranslatedFunction func = translator.Translate(address, item.funcProfile.Mode, item.funcProfile.HighCq, deferProtect: true); + TranslatedFunction func = translator.Translate(address, item.funcProfile.Mode, item.funcProfile.HighCq); bool isAddressUnique = translator.Functions.TryAdd(address, func.GuestSize, func); @@ -844,22 +856,26 @@ namespace ARMeilleure.Translation.PTC } } - List threads = new(); - for (int i = 0; i < degreeOfParallelism; i++) - { - Thread thread = new(TranslateFuncs) - { - IsBackground = true, - }; - - threads.Add(thread); - } + List threads = Enumerable.Range(0, degreeOfParallelism) + .Select(idx => + new Thread(TranslateFuncs) + { + IsBackground = true, + Name = "Ptc.TranslateThread." + idx + } + ).ToList(); 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(); @@ -875,6 +891,7 @@ namespace ARMeilleure.Translation.PTC Thread preSaveThread = new(PreSave) { IsBackground = true, + Name = "Ptc.DiskWriter" }; preSaveThread.Start(); } @@ -902,7 +919,7 @@ namespace ARMeilleure.Translation.PTC public static Hash128 ComputeHash(IMemoryManager memory, ulong address, ulong guestSize) { - return XXHash128.ComputeHash(memory.GetSpan(address, checked((int)(guestSize)))); + return Hash128.ComputeHash(memory.GetSpan(address, checked((int)(guestSize)))); } public void WriteCompiledFunction(ulong address, ulong guestSize, Hash128 hash, bool highCq, CompiledFunction compiledFunc) @@ -1005,7 +1022,6 @@ namespace ARMeilleure.Translation.PTC osPlatform |= (OperatingSystem.IsLinux() ? 1u : 0u) << 1; osPlatform |= (OperatingSystem.IsMacOS() ? 1u : 0u) << 2; osPlatform |= (OperatingSystem.IsWindows() ? 1u : 0u) << 3; - osPlatform |= (OperatingSystem.IsIOS() ? 1u : 0u) << 4; #pragma warning restore IDE0055 return osPlatform; @@ -1032,14 +1048,14 @@ namespace ARMeilleure.Translation.PTC { Span spanHeader = MemoryMarshal.CreateSpan(ref this, 1); - HeaderHash = XXHash128.ComputeHash(MemoryMarshal.AsBytes(spanHeader)[..(Unsafe.SizeOf() - Unsafe.SizeOf())]); + HeaderHash = Hash128.ComputeHash(MemoryMarshal.AsBytes(spanHeader)[..(Unsafe.SizeOf() - Unsafe.SizeOf())]); } public bool IsHeaderValid() { Span spanHeader = MemoryMarshal.CreateSpan(ref this, 1); - return XXHash128.ComputeHash(MemoryMarshal.AsBytes(spanHeader)[..(Unsafe.SizeOf() - Unsafe.SizeOf())]) == HeaderHash; + return Hash128.ComputeHash(MemoryMarshal.AsBytes(spanHeader)[..(Unsafe.SizeOf() - Unsafe.SizeOf())]) == HeaderHash; } } @@ -1067,14 +1083,14 @@ namespace ARMeilleure.Translation.PTC { Span spanHeader = MemoryMarshal.CreateSpan(ref this, 1); - HeaderHash = XXHash128.ComputeHash(MemoryMarshal.AsBytes(spanHeader)[..(Unsafe.SizeOf() - Unsafe.SizeOf())]); + HeaderHash = Hash128.ComputeHash(MemoryMarshal.AsBytes(spanHeader)[..(Unsafe.SizeOf() - Unsafe.SizeOf())]); } public bool IsHeaderValid() { Span spanHeader = MemoryMarshal.CreateSpan(ref this, 1); - return XXHash128.ComputeHash(MemoryMarshal.AsBytes(spanHeader)[..(Unsafe.SizeOf() - Unsafe.SizeOf())]) == HeaderHash; + return Hash128.ComputeHash(MemoryMarshal.AsBytes(spanHeader)[..(Unsafe.SizeOf() - Unsafe.SizeOf())]) == HeaderHash; } } diff --git a/src/ARMeilleure/Translation/PTC/PtcProfiler.cs b/src/ARMeilleure/Translation/PTC/PtcProfiler.cs index 0fe78edab..8e95c5e4b 100644 --- a/src/ARMeilleure/Translation/PTC/PtcProfiler.cs +++ b/src/ARMeilleure/Translation/PTC/PtcProfiler.cs @@ -209,7 +209,7 @@ namespace ARMeilleure.Translation.PTC Hash128 expectedHash = DeserializeStructure(stream); - Hash128 actualHash = XXHash128.ComputeHash(GetReadOnlySpan(stream)); + Hash128 actualHash = Hash128.ComputeHash(GetReadOnlySpan(stream)); if (actualHash != expectedHash) { @@ -313,7 +313,7 @@ namespace ARMeilleure.Translation.PTC Debug.Assert(stream.Position == stream.Length); stream.Seek(Unsafe.SizeOf(), SeekOrigin.Begin); - Hash128 hash = XXHash128.ComputeHash(GetReadOnlySpan(stream)); + Hash128 hash = Hash128.ComputeHash(GetReadOnlySpan(stream)); stream.Seek(0L, SeekOrigin.Begin); SerializeStructure(stream, hash); @@ -374,14 +374,14 @@ namespace ARMeilleure.Translation.PTC { Span spanHeader = MemoryMarshal.CreateSpan(ref this, 1); - HeaderHash = XXHash128.ComputeHash(MemoryMarshal.AsBytes(spanHeader)[..(Unsafe.SizeOf() - Unsafe.SizeOf())]); + HeaderHash = Hash128.ComputeHash(MemoryMarshal.AsBytes(spanHeader)[..(Unsafe.SizeOf() - Unsafe.SizeOf())]); } public bool IsHeaderValid() { Span spanHeader = MemoryMarshal.CreateSpan(ref this, 1); - return XXHash128.ComputeHash(MemoryMarshal.AsBytes(spanHeader)[..(Unsafe.SizeOf() - Unsafe.SizeOf())]) == HeaderHash; + return Hash128.ComputeHash(MemoryMarshal.AsBytes(spanHeader)[..(Unsafe.SizeOf() - Unsafe.SizeOf())]) == HeaderHash; } } 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/TranslatedFunction.cs b/src/ARMeilleure/Translation/TranslatedFunction.cs index 1446c254a..3d7ae9ffe 100644 --- a/src/ARMeilleure/Translation/TranslatedFunction.cs +++ b/src/ARMeilleure/Translation/TranslatedFunction.cs @@ -7,12 +7,12 @@ namespace ARMeilleure.Translation { private readonly GuestFunction _func; // Ensure that this delegate will not be garbage collected. - public IntPtr FuncPointer { get; } + public nint FuncPointer { get; } public Counter CallCounter { get; } public ulong GuestSize { get; } public bool HighCq { get; } - public TranslatedFunction(GuestFunction func, IntPtr funcPointer, Counter callCounter, ulong guestSize, bool highCq) + public TranslatedFunction(GuestFunction func, nint funcPointer, Counter callCounter, ulong guestSize, bool highCq) { _func = func; FuncPointer = funcPointer; diff --git a/src/ARMeilleure/Translation/Translator.cs b/src/ARMeilleure/Translation/Translator.cs index 253f25e4a..162368782 100644 --- a/src/ARMeilleure/Translation/Translator.cs +++ b/src/ARMeilleure/Translation/Translator.cs @@ -22,33 +22,13 @@ namespace ARMeilleure.Translation { public class Translator { - private static readonly AddressTable.Level[] _levels64Bit = - new AddressTable.Level[] - { - new(31, 17), - new(23, 8), - new(15, 8), - new( 7, 8), - new( 2, 5), - }; - - private static readonly AddressTable.Level[] _levels32Bit = - new AddressTable.Level[] - { - new(31, 17), - new(23, 8), - new(15, 8), - new( 7, 8), - new( 1, 6), - }; - private readonly IJitMemoryAllocator _allocator; private readonly ConcurrentQueue> _oldFuncs; private readonly Ptc _ptc; internal TranslatorCache Functions { get; } - internal AddressTable FunctionTable { get; } + internal IAddressTable FunctionTable { get; } internal EntryTable CountTable { get; } internal TranslatorStubs Stubs { get; } internal TranslatorQueue Queue { get; } @@ -57,10 +37,7 @@ 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) + public Translator(IJitMemoryAllocator allocator, IMemoryManager memory, IAddressTable functionTable) { _allocator = allocator; Memory = memory; @@ -75,20 +52,15 @@ namespace ARMeilleure.Translation CountTable = new EntryTable(); Functions = new TranslatorCache(); - FunctionTable = new AddressTable(for64Bits ? _levels64Bit : _levels32Bit); + FunctionTable = functionTable; 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) + public IPtcLoadState LoadDiskCache(string titleIdText, string displayVersion, bool enabled, string cacheSelector) { - _ptc.Initialize(titleIdText, displayVersion, enabled, Memory.Type); + _ptc.Initialize(titleIdText, displayVersion, enabled, Memory.Type, cacheSelector); return _ptc; } @@ -105,15 +77,11 @@ namespace ARMeilleure.Translation { if (Interlocked.Increment(ref _threadCount) == 1) { - IsReadyForTranslation.WaitOne(); - if (_ptc.State == PtcState.Enabled) { Debug.Assert(Functions.Count == 0); _ptc.LoadTranslations(this); _ptc.MakeAndSaveTranslations(this); - - JitCache.RunDeferredRxProtects(); } _ptc.Profiler.Start(); @@ -252,7 +220,7 @@ namespace ARMeilleure.Translation } } - internal TranslatedFunction Translate(ulong address, ExecutionMode mode, bool highCq, bool singleStep = false, bool deferProtect = false) + internal TranslatedFunction Translate(ulong address, ExecutionMode mode, bool highCq, bool singleStep = false) { var context = new ArmEmitterContext( Memory, @@ -310,7 +278,7 @@ namespace ARMeilleure.Translation _ptc.WriteCompiledFunction(address, funcSize, hash, highCq, compiledFunc); } - GuestFunction func = compiledFunc.MapWithPointer(out IntPtr funcPointer, deferProtect); + GuestFunction func = compiledFunc.MapWithPointer(out nint funcPointer); Allocators.ResetAll(); 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/ARMeilleure/Translation/TranslatorStubs.cs b/src/ARMeilleure/Translation/TranslatorStubs.cs index d80823a8b..bd9aed8d4 100644 --- a/src/ARMeilleure/Translation/TranslatorStubs.cs +++ b/src/ARMeilleure/Translation/TranslatorStubs.cs @@ -15,12 +15,12 @@ namespace ARMeilleure.Translation /// class TranslatorStubs : IDisposable { - private readonly Lazy _slowDispatchStub; + private readonly Lazy _slowDispatchStub; private bool _disposed; - private readonly AddressTable _functionTable; - private readonly Lazy _dispatchStub; + private readonly IAddressTable _functionTable; + private readonly Lazy _dispatchStub; private readonly Lazy _dispatchLoop; private readonly Lazy _contextWrapper; @@ -28,7 +28,7 @@ namespace ARMeilleure.Translation /// Gets the dispatch stub. /// /// instance was disposed - public IntPtr DispatchStub + public nint DispatchStub { get { @@ -42,7 +42,7 @@ namespace ARMeilleure.Translation /// Gets the slow dispatch stub. /// /// instance was disposed - public IntPtr SlowDispatchStub + public nint SlowDispatchStub { get { @@ -86,7 +86,7 @@ namespace ARMeilleure.Translation /// /// Function table used to store pointers to the functions that the guest code will call /// is null - public TranslatorStubs(AddressTable functionTable) + public TranslatorStubs(IAddressTable functionTable) { ArgumentNullException.ThrowIfNull(functionTable); @@ -140,7 +140,7 @@ namespace ARMeilleure.Translation /// Generates a . /// /// Generated - private IntPtr GenerateDispatchStub() + private nint GenerateDispatchStub() { var context = new EmitterContext(); @@ -198,7 +198,7 @@ namespace ARMeilleure.Translation /// Generates a . /// /// Generated - private IntPtr GenerateSlowDispatchStub() + private nint GenerateSlowDispatchStub() { var context = new EmitterContext(); diff --git a/src/ARMeilleure/Translation/TranslatorTestMethods.cs b/src/ARMeilleure/Translation/TranslatorTestMethods.cs index 8cc7a3cf8..186780daa 100644 --- a/src/ARMeilleure/Translation/TranslatorTestMethods.cs +++ b/src/ARMeilleure/Translation/TranslatorTestMethods.cs @@ -9,7 +9,7 @@ namespace ARMeilleure.Translation { public static class TranslatorTestMethods { - public delegate int FpFlagsPInvokeTest(IntPtr managedMethod); + public delegate int FpFlagsPInvokeTest(nint managedMethod); private static bool SetPlatformFtz(EmitterContext context, bool ftz) { diff --git a/src/MeloNX/MeloNX.xcodeproj/project.pbxproj b/src/MeloNX/MeloNX.xcodeproj/project.pbxproj index 68b5b44e3..1caec0c46 100644 --- a/src/MeloNX/MeloNX.xcodeproj/project.pbxproj +++ b/src/MeloNX/MeloNX.xcodeproj/project.pbxproj @@ -7,7 +7,7 @@ objects = { /* Begin PBXBuildFile section */ - 4E551F202CF128540096A2DF /* GameController.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4E80AA622CD7122800029585 /* GameController.framework */; }; + 25BFA0892CF956FD0085F3E4 /* GameController.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4E80AA622CD7122800029585 /* GameController.framework */; }; 4E80AA212CD705DD00029585 /* SDL in Frameworks */ = {isa = PBXBuildFile; productRef = 4E80AA202CD705DD00029585 /* SDL */; }; /* End PBXBuildFile section */ @@ -59,7 +59,7 @@ /* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ /* Begin PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet section */ - 4E9A82F32CF87822006D7086 /* Exceptions for "MeloNX" folder in "Embed Libraries" phase from "MeloNX" target */ = { + 4E80AA0A2CD6FAA800029585 /* Exceptions for "MeloNX" folder in "Embed Libraries" phase from "MeloNX" target */ = { isa = PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet; attributesByRelativePath = { "Dependencies/Dynamic Libraries/Ryujinx.Headless.SDL2.dylib" = (CodeSignOnCopy, ); @@ -68,6 +68,7 @@ "Dependencies/Dynamic Libraries/libavutil.dylib" = (CodeSignOnCopy, ); Dependencies/XCFrameworks/MoltenVK.xcframework = (CodeSignOnCopy, RemoveHeadersOnCopy, ); Dependencies/XCFrameworks/SDL2.xcframework = (CodeSignOnCopy, RemoveHeadersOnCopy, ); + Dependencies/XCFrameworks/libSPIRV.xcframework = (CodeSignOnCopy, RemoveHeadersOnCopy, ); Dependencies/XCFrameworks/libavcodec.xcframework = (CodeSignOnCopy, RemoveHeadersOnCopy, ); Dependencies/XCFrameworks/libavfilter.xcframework = (CodeSignOnCopy, RemoveHeadersOnCopy, ); Dependencies/XCFrameworks/libavformat.xcframework = (CodeSignOnCopy, RemoveHeadersOnCopy, ); @@ -86,6 +87,7 @@ Dependencies/XCFrameworks/libavfilter.xcframework, Dependencies/XCFrameworks/libavformat.xcframework, Dependencies/XCFrameworks/libavutil.xcframework, + Dependencies/XCFrameworks/libSPIRV.xcframework, Dependencies/XCFrameworks/libswresample.xcframework, Dependencies/XCFrameworks/libswscale.xcframework, Dependencies/XCFrameworks/libteakra.xcframework, @@ -100,7 +102,7 @@ isa = PBXFileSystemSynchronizedRootGroup; exceptions = ( 4E80AA1D2CD7015100029585 /* Exceptions for "MeloNX" folder in "MeloNX" target */, - 4E9A82F32CF87822006D7086 /* Exceptions for "MeloNX" folder in "Embed Libraries" phase from "MeloNX" target */, + 4E80AA0A2CD6FAA800029585 /* Exceptions for "MeloNX" folder in "Embed Libraries" phase from "MeloNX" target */, ); path = MeloNX; sourceTree = ""; @@ -122,8 +124,8 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 4E551F202CF128540096A2DF /* GameController.framework in Frameworks */, 4E80AA212CD705DD00029585 /* SDL in Frameworks */, + 25BFA0892CF956FD0085F3E4 /* GameController.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -483,7 +485,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = ""; - DEVELOPMENT_TEAM = 95J8WZ4TN8; + DEVELOPMENT_TEAM = 4TD3JXVDW7; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = MeloNX/Info.plist; @@ -553,10 +555,9 @@ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", - "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.stossy11.MeloNX; + PRODUCT_BUNDLE_IDENTIFIER = com.benlawrence.MeloNX; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OBJC_BRIDGING_HEADER = "MeloNX/Core/Headers/Ryujinx-Header.h"; @@ -574,7 +575,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = ""; - DEVELOPMENT_TEAM = 95J8WZ4TN8; + DEVELOPMENT_TEAM = 4TD3JXVDW7; ENABLE_PREVIEWS = YES; GCC_OPTIMIZATION_LEVEL = 3; GENERATE_INFOPLIST_FILE = YES; @@ -645,10 +646,9 @@ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", - "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.stossy11.MeloNX; + PRODUCT_BUNDLE_IDENTIFIER = com.benlawrence.MeloNX; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OBJC_BRIDGING_HEADER = "MeloNX/Core/Headers/Ryujinx-Header.h"; diff --git a/src/MeloNX/MeloNX.xcodeproj/project.xcworkspace/xcuserdata/benlawrence.xcuserdatad/UserInterfaceState.xcuserstate b/src/MeloNX/MeloNX.xcodeproj/project.xcworkspace/xcuserdata/benlawrence.xcuserdatad/UserInterfaceState.xcuserstate new file mode 100644 index 000000000..7ca1b73bb Binary files /dev/null and b/src/MeloNX/MeloNX.xcodeproj/project.xcworkspace/xcuserdata/benlawrence.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/src/MeloNX/MeloNX.xcodeproj/project.xcworkspace/xcuserdata/stossy11.xcuserdatad/UserInterfaceState.xcuserstate b/src/MeloNX/MeloNX.xcodeproj/project.xcworkspace/xcuserdata/stossy11.xcuserdatad/UserInterfaceState.xcuserstate index 27a190214..8b26026b6 100644 Binary files a/src/MeloNX/MeloNX.xcodeproj/project.xcworkspace/xcuserdata/stossy11.xcuserdatad/UserInterfaceState.xcuserstate and b/src/MeloNX/MeloNX.xcodeproj/project.xcworkspace/xcuserdata/stossy11.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/src/MeloNX/MeloNX/Core/Swift/Ryujinx.swift b/src/MeloNX/MeloNX/Core/Swift/Ryujinx.swift index 74ad74716..0d6a6716e 100644 --- a/src/MeloNX/MeloNX/Core/Swift/Ryujinx.swift +++ b/src/MeloNX/MeloNX/Core/Swift/Ryujinx.swift @@ -49,7 +49,7 @@ class Ryujinx { var listinputids: Bool var fullscreen: Bool var memoryManagerMode: String - var disableVSync: Bool + var vsync: String var disableShaderCache: Bool var disableDockedMode: Bool var enableTextureRecompression: Bool @@ -60,14 +60,14 @@ class Ryujinx { inputids: [String] = [], debuglogs: Bool = false, tracelogs: Bool = false, - listinputids: Bool = false, - fullscreen: Bool = true, - memoryManagerMode: String = "HostMapped", - disableVSync: Bool = false, - disableShaderCache: Bool = false, - disableDockedMode: Bool = false, nintendoinput: Bool = true, enableInternet: Bool = false, + listinputids: Bool = false, + fullscreen: Bool = true, + memoryManagerMode: String = "HostMappedUnsafe", + vsync: String = "Switch", + disableShaderCache: Bool = false, + disableDockedMode: Bool = false, enableTextureRecompression: Bool = true, additionalArgs: [String] = [], resscale: Float = 1.00 @@ -76,17 +76,17 @@ class Ryujinx { self.inputids = inputids self.debuglogs = debuglogs self.tracelogs = tracelogs + self.nintendoinput = nintendoinput + self.enableInternet = enableInternet self.listinputids = listinputids self.fullscreen = fullscreen - self.disableVSync = disableVSync + self.vsync = vsync self.disableShaderCache = disableShaderCache self.disableDockedMode = disableDockedMode self.enableTextureRecompression = enableTextureRecompression self.additionalArgs = additionalArgs self.memoryManagerMode = memoryManagerMode self.resscale = resscale - self.nintendoinput = nintendoinput - self.enableInternet = enableInternet } } @@ -157,17 +157,15 @@ class Ryujinx { args.append(contentsOf: ["--resolution-scale", String(config.resscale)]) } + // Adding default args directly into additionalArgs if config.nintendoinput { args.append("--correct-ons-controller") } + if config.enableInternet { args.append("--enable-internet-connection") } - // Adding default args directly into additionalArgs - if config.disableVSync { - args.append("--disable-vsync") - } if config.disableShaderCache { args.append("--disable-shader-cache") } diff --git a/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/libSDL2.dylib b/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/libSDL2.dylib new file mode 100644 index 000000000..a07334895 Binary files /dev/null and b/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/libSDL2.dylib differ diff --git a/src/MeloNX/MeloNX/Views/SettingsView/SettingsView.swift b/src/MeloNX/MeloNX/Views/SettingsView/SettingsView.swift index ddd89965f..12d56a773 100644 --- a/src/MeloNX/MeloNX/Views/SettingsView/SettingsView.swift +++ b/src/MeloNX/MeloNX/Views/SettingsView/SettingsView.swift @@ -12,21 +12,29 @@ struct SettingsView: View { @Binding var MoltenVKSettings: [MoltenVKSettings] var memoryManagerModes = [ + ("HostMappedUnsafe", "Host Unchecked (fastest, unstable / unsafe)"), ("HostMapped", "Host (fast)"), - ("HostMappedUnsafe", "Host Unchecked (fast, unstable / unsafe)"), - ("SoftwarePageTable", "Software (slow)"), + ("SoftwarePageTable", "Software") + ] + + var vsyncModes = [ + ("Switch", "Switch"), + ("Unbound", "Unbound"), ] @AppStorage("RyuDemoControls") var ryuDemo: Bool = false - @AppStorage("MTL_HUD_ENABLED") var metalHUDEnabled: Bool = false var body: some View { ScrollView { VStack { - Section(header: Title("Graphics and Performance")) { + Section(header: Text("Graphics and Performance").bold()) { Toggle("Ryujinx Fullscreen", isOn: $config.fullscreen) - Toggle("Disable V-Sync", isOn: $config.disableVSync) + Picker("V-Sync", selection: $config.vsync) { + ForEach(vsyncModes, id: \.0) { key, displayName in + Text(displayName).tag(key) + } + } Toggle("Disable Shader Cache", isOn: $config.disableShaderCache) Toggle("Enable Texture Recompression", isOn: $config.enableTextureRecompression) Toggle("Disable Docked Mode", isOn: $config.disableDockedMode) @@ -41,18 +49,19 @@ struct SettingsView: View { } } - Section(header: Title("Input Settings")) { + Section(header: Text("Input Settings").bold()) { Toggle("List Input IDs", isOn: $config.listinputids) Toggle("Nintendo Controller Layout", isOn: $config.nintendoinput) Toggle("Ryujinx Demo On-Screen Controller", isOn: $ryuDemo) // Toggle("Host Mapped Memory", isOn: $config.hostMappedMemory) + Toggle("Disable Docked Mode", isOn: $config.disableDockedMode) } - Section(header: Title("Logging Settings")) { + Section(header: Text("Logging Settings").bold()) { Toggle("Enable Debug Logs", isOn: $config.debuglogs) Toggle("Enable Trace Logs", isOn: $config.tracelogs) } - Section(header: Title("CPU Mode")) { + Section(header: Text("CPU Mode").bold()) { HStack { Spacer() Picker("Memory Manager Mode", selection: $config.memoryManagerMode) { @@ -66,7 +75,7 @@ struct SettingsView: View { - Section(header: Title("Additional Settings")) { + Section(header: Text("Additional Settings")) { //TextField("Game Path", text: $config.gamepath) Text("PageSize \(String(Int(getpagesize())))") @@ -81,8 +90,8 @@ struct SettingsView: View { )) } } - .padding() } + .padding() .onAppear { if let configs = loadSettings() { self.config = configs @@ -164,20 +173,3 @@ extension NumberFormatter { return formatter } } - - -struct Title: View { - let string: String - - init(_ string: String) { - self.string = string - } - - var body: some View { - VStack { - Text(string) - .font(.title2) - Divider() - } - } -} diff --git a/src/Ryujinx.Audio.Backends.OpenAL/OpenALHardwareDeviceDriver.cs b/src/Ryujinx.Audio.Backends.OpenAL/OpenALHardwareDeviceDriver.cs index 744a4bc56..25f91f8e9 100644 --- a/src/Ryujinx.Audio.Backends.OpenAL/OpenALHardwareDeviceDriver.cs +++ b/src/Ryujinx.Audio.Backends.OpenAL/OpenALHardwareDeviceDriver.cs @@ -20,9 +20,28 @@ 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(""); + _device = ALC.OpenDevice(string.Empty); _context = ALC.CreateContext(_device, new ALContextAttributes()); _updateRequiredEvent = new ManualResetEvent(false); _pauseEvent = new ManualResetEvent(true); @@ -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..3b9129130 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() @@ -65,7 +66,7 @@ namespace Ryujinx.Audio.Backends.OpenAL { OpenALAudioBuffer driverBuffer = new() { - DriverIdentifier = buffer.HostTag, + DriverIdentifier = buffer.DataPointer, BufferId = AL.GenBuffer(), SampleCount = GetSampleCount(buffer), }; @@ -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() @@ -131,7 +137,7 @@ namespace Ryujinx.Audio.Backends.OpenAL return true; } - return driverBuffer.DriverIdentifier != buffer.HostTag; + return driverBuffer.DriverIdentifier != buffer.DataPointer; } } diff --git a/src/Ryujinx.Audio.Backends.SDL2/SDL2HardwareDeviceDriver.cs b/src/Ryujinx.Audio.Backends.SDL2/SDL2HardwareDeviceDriver.cs index 58137bb38..acd1582ec 100644 --- a/src/Ryujinx.Audio.Backends.SDL2/SDL2HardwareDeviceDriver.cs +++ b/src/Ryujinx.Audio.Backends.SDL2/SDL2HardwareDeviceDriver.cs @@ -20,11 +20,13 @@ 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 - [DllImport("SDL2.framework/SDL2")] - private static extern int SDL_GetDefaultAudioInfo(IntPtr name, out SDL_AudioSpec spec, int isCapture); + [DllImport("SDL2")] + private static extern int SDL_GetDefaultAudioInfo(nint name, out SDL_AudioSpec spec, int isCapture); #pragma warning restore SYSLIB1054 public SDL2HardwareDeviceDriver() @@ -35,7 +37,7 @@ namespace Ryujinx.Audio.Backends.SDL2 SDL2Driver.Instance.Initialize(); - int res = SDL_GetDefaultAudioInfo(IntPtr.Zero, out var spec, 0); + int res = SDL_GetDefaultAudioInfo(nint.Zero, out var spec, 0); if (res != 0) { @@ -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); @@ -132,7 +136,7 @@ namespace Ryujinx.Audio.Backends.SDL2 desired.callback = callback; - uint device = SDL_OpenAudioDevice(IntPtr.Zero, 0, ref desired, out SDL_AudioSpec got, 0); + uint device = SDL_OpenAudioDevice(nint.Zero, 0, ref desired, out SDL_AudioSpec got, 0); if (device == 0) { diff --git a/src/Ryujinx.Audio.Backends.SDL2/SDL2HardwareDeviceSession.cs b/src/Ryujinx.Audio.Backends.SDL2/SDL2HardwareDeviceSession.cs index cf3be473e..51cd43c55 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) @@ -70,7 +72,7 @@ namespace Ryujinx.Audio.Backends.SDL2 } } - private unsafe void Update(IntPtr userdata, IntPtr stream, int streamLength) + private unsafe void Update(nint userdata, nint stream, int streamLength) { Span streamSpan = new((void*)stream, streamLength); @@ -87,19 +89,21 @@ 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); fixed (byte* p = samples) { - IntPtr pStreamSrc = (IntPtr)p; + nint pStreamSrc = (nint)p; // Zero the dest buffer 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); @@ -151,7 +155,7 @@ namespace Ryujinx.Audio.Backends.SDL2 if (_outputStream != 0) { - SDL2AudioBuffer driverBuffer = new(buffer.HostTag, GetSampleCount(buffer)); + SDL2AudioBuffer driverBuffer = new(buffer.DataPointer, GetSampleCount(buffer)); _ringBuffer.Write(buffer.Data, 0, buffer.Data.Length); @@ -205,7 +209,7 @@ namespace Ryujinx.Audio.Backends.SDL2 return true; } - return driverBuffer.DriverIdentifier != buffer.HostTag; + return driverBuffer.DriverIdentifier != buffer.DataPointer; } protected virtual void Dispose(bool disposing) diff --git a/src/Ryujinx.Audio.Backends.SoundIo/Native/SoundIo.cs b/src/Ryujinx.Audio.Backends.SoundIo/Native/SoundIo.cs index 7fdb1fc04..9decd79fc 100644 --- a/src/Ryujinx.Audio.Backends.SoundIo/Native/SoundIo.cs +++ b/src/Ryujinx.Audio.Backends.SoundIo/Native/SoundIo.cs @@ -10,41 +10,41 @@ namespace Ryujinx.Audio.Backends.SoundIo.Native private const string LibraryName = "libsoundio"; [UnmanagedFunctionPointer(CallingConvention.Cdecl)] - public delegate void OnDeviceChangeNativeDelegate(IntPtr ctx); + public delegate void OnDeviceChangeNativeDelegate(nint ctx); [UnmanagedFunctionPointer(CallingConvention.Cdecl)] - public delegate void OnBackendDisconnectedDelegate(IntPtr ctx, SoundIoError err); + public delegate void OnBackendDisconnectedDelegate(nint ctx, SoundIoError err); [UnmanagedFunctionPointer(CallingConvention.Cdecl)] - public delegate void OnEventsSignalDelegate(IntPtr ctx); + public delegate void OnEventsSignalDelegate(nint ctx); [UnmanagedFunctionPointer(CallingConvention.Cdecl)] public delegate void EmitRtPrioWarningDelegate(); [UnmanagedFunctionPointer(CallingConvention.Cdecl)] - public delegate void JackCallbackDelegate(IntPtr msg); + public delegate void JackCallbackDelegate(nint msg); [StructLayout(LayoutKind.Sequential)] public struct SoundIoStruct { - public IntPtr UserData; - public IntPtr OnDeviceChange; - public IntPtr OnBackendDisconnected; - public IntPtr OnEventsSignal; + public nint UserData; + public nint OnDeviceChange; + public nint OnBackendDisconnected; + public nint OnEventsSignal; public SoundIoBackend CurrentBackend; - public IntPtr ApplicationName; - public IntPtr EmitRtPrioWarning; - public IntPtr JackInfoCallback; - public IntPtr JackErrorCallback; + public nint ApplicationName; + public nint EmitRtPrioWarning; + public nint JackInfoCallback; + public nint JackErrorCallback; } public struct SoundIoChannelLayout { - public IntPtr Name; + public nint Name; public int ChannelCount; public Array24 Channels; - public static IntPtr GetDefault(int channelCount) + public static nint GetDefault(int channelCount) { return soundio_channel_layout_get_default(channelCount); } @@ -63,17 +63,17 @@ namespace Ryujinx.Audio.Backends.SoundIo.Native public struct SoundIoDevice { - public IntPtr SoundIo; - public IntPtr Id; - public IntPtr Name; + public nint SoundIo; + public nint Id; + public nint Name; public SoundIoDeviceAim Aim; - public IntPtr Layouts; + public nint Layouts; public int LayoutCount; public SoundIoChannelLayout CurrentLayout; - public IntPtr Formats; + public nint Formats; public int FormatCount; public SoundIoFormat CurrentFormat; - public IntPtr SampleRates; + public nint SampleRates; public int SampleRateCount; public int SampleRateCurrent; public double SoftwareLatencyMin; @@ -86,17 +86,17 @@ namespace Ryujinx.Audio.Backends.SoundIo.Native public struct SoundIoOutStream { - public IntPtr Device; + public nint Device; public SoundIoFormat Format; public int SampleRate; public SoundIoChannelLayout Layout; public double SoftwareLatency; public float Volume; - public IntPtr UserData; - public IntPtr WriteCallback; - public IntPtr UnderflowCallback; - public IntPtr ErrorCallback; - public IntPtr Name; + public nint UserData; + public nint WriteCallback; + public nint UnderflowCallback; + public nint ErrorCallback; + public nint Name; public bool NonTerminalHint; public int BytesPerFrame; public int BytesPerSample; @@ -105,74 +105,74 @@ namespace Ryujinx.Audio.Backends.SoundIo.Native public struct SoundIoChannelArea { - public IntPtr Pointer; + public nint Pointer; public int Step; } [LibraryImport(LibraryName)] - internal static partial IntPtr soundio_create(); + internal static partial nint soundio_create(); [LibraryImport(LibraryName)] - internal static partial SoundIoError soundio_connect(IntPtr ctx); + internal static partial SoundIoError soundio_connect(nint ctx); [LibraryImport(LibraryName)] - internal static partial void soundio_disconnect(IntPtr ctx); + internal static partial void soundio_disconnect(nint ctx); [LibraryImport(LibraryName)] - internal static partial void soundio_flush_events(IntPtr ctx); + internal static partial void soundio_flush_events(nint ctx); [LibraryImport(LibraryName)] - internal static partial int soundio_output_device_count(IntPtr ctx); + internal static partial int soundio_output_device_count(nint ctx); [LibraryImport(LibraryName)] - internal static partial int soundio_default_output_device_index(IntPtr ctx); + internal static partial int soundio_default_output_device_index(nint ctx); [LibraryImport(LibraryName)] - internal static partial IntPtr soundio_get_output_device(IntPtr ctx, int index); + internal static partial nint soundio_get_output_device(nint ctx, int index); [LibraryImport(LibraryName)] [return: MarshalAs(UnmanagedType.Bool)] - internal static partial bool soundio_device_supports_format(IntPtr devCtx, SoundIoFormat format); + internal static partial bool soundio_device_supports_format(nint devCtx, SoundIoFormat format); [LibraryImport(LibraryName)] [return: MarshalAs(UnmanagedType.Bool)] - internal static partial bool soundio_device_supports_layout(IntPtr devCtx, IntPtr layout); + internal static partial bool soundio_device_supports_layout(nint devCtx, nint layout); [LibraryImport(LibraryName)] [return: MarshalAs(UnmanagedType.Bool)] - internal static partial bool soundio_device_supports_sample_rate(IntPtr devCtx, int sampleRate); + internal static partial bool soundio_device_supports_sample_rate(nint devCtx, int sampleRate); [LibraryImport(LibraryName)] - internal static partial IntPtr soundio_outstream_create(IntPtr devCtx); + internal static partial nint soundio_outstream_create(nint devCtx); [LibraryImport(LibraryName)] - internal static partial SoundIoError soundio_outstream_open(IntPtr outStreamCtx); + internal static partial SoundIoError soundio_outstream_open(nint outStreamCtx); [LibraryImport(LibraryName)] - internal static partial SoundIoError soundio_outstream_start(IntPtr outStreamCtx); + internal static partial SoundIoError soundio_outstream_start(nint outStreamCtx); [LibraryImport(LibraryName)] - internal static partial SoundIoError soundio_outstream_begin_write(IntPtr outStreamCtx, IntPtr areas, IntPtr frameCount); + internal static partial SoundIoError soundio_outstream_begin_write(nint outStreamCtx, nint areas, nint frameCount); [LibraryImport(LibraryName)] - internal static partial SoundIoError soundio_outstream_end_write(IntPtr outStreamCtx); + internal static partial SoundIoError soundio_outstream_end_write(nint outStreamCtx); [LibraryImport(LibraryName)] - internal static partial SoundIoError soundio_outstream_pause(IntPtr devCtx, [MarshalAs(UnmanagedType.Bool)] bool pause); + internal static partial SoundIoError soundio_outstream_pause(nint devCtx, [MarshalAs(UnmanagedType.Bool)] bool pause); [LibraryImport(LibraryName)] - internal static partial SoundIoError soundio_outstream_set_volume(IntPtr devCtx, double volume); + internal static partial SoundIoError soundio_outstream_set_volume(nint devCtx, double volume); [LibraryImport(LibraryName)] - internal static partial void soundio_outstream_destroy(IntPtr streamCtx); + internal static partial void soundio_outstream_destroy(nint streamCtx); [LibraryImport(LibraryName)] - internal static partial void soundio_destroy(IntPtr ctx); + internal static partial void soundio_destroy(nint ctx); [LibraryImport(LibraryName)] - internal static partial IntPtr soundio_channel_layout_get_default(int channelCount); + internal static partial nint soundio_channel_layout_get_default(int channelCount); [LibraryImport(LibraryName)] - internal static partial IntPtr soundio_strerror(SoundIoError err); + internal static partial nint soundio_strerror(SoundIoError err); } } diff --git a/src/Ryujinx.Audio.Backends.SoundIo/Native/SoundIoContext.cs b/src/Ryujinx.Audio.Backends.SoundIo/Native/SoundIoContext.cs index f2e91fcd7..a881e8ffe 100644 --- a/src/Ryujinx.Audio.Backends.SoundIo/Native/SoundIoContext.cs +++ b/src/Ryujinx.Audio.Backends.SoundIo/Native/SoundIoContext.cs @@ -8,13 +8,13 @@ namespace Ryujinx.Audio.Backends.SoundIo.Native { public class SoundIoContext : IDisposable { - private IntPtr _context; + private nint _context; private Action _onBackendDisconnect; private OnBackendDisconnectedDelegate _onBackendDisconnectNative; - public IntPtr Context => _context; + public nint Context => _context; - internal SoundIoContext(IntPtr context) + internal SoundIoContext(nint context) { _context = context; _onBackendDisconnect = null; @@ -60,9 +60,9 @@ namespace Ryujinx.Audio.Backends.SoundIo.Native public SoundIoDeviceContext GetOutputDevice(int index) { - IntPtr deviceContext = soundio_get_output_device(_context, index); + nint deviceContext = soundio_get_output_device(_context, index); - if (deviceContext == IntPtr.Zero) + if (deviceContext == nint.Zero) { return null; } @@ -72,9 +72,9 @@ namespace Ryujinx.Audio.Backends.SoundIo.Native public static SoundIoContext Create() { - IntPtr context = soundio_create(); + nint context = soundio_create(); - if (context == IntPtr.Zero) + if (context == nint.Zero) { return null; } @@ -84,9 +84,9 @@ namespace Ryujinx.Audio.Backends.SoundIo.Native protected virtual void Dispose(bool disposing) { - IntPtr currentContext = Interlocked.Exchange(ref _context, IntPtr.Zero); + nint currentContext = Interlocked.Exchange(ref _context, nint.Zero); - if (currentContext != IntPtr.Zero) + if (currentContext != nint.Zero) { soundio_destroy(currentContext); } diff --git a/src/Ryujinx.Audio.Backends.SoundIo/Native/SoundIoDeviceContext.cs b/src/Ryujinx.Audio.Backends.SoundIo/Native/SoundIoDeviceContext.cs index 7923e9b17..efea52b35 100644 --- a/src/Ryujinx.Audio.Backends.SoundIo/Native/SoundIoDeviceContext.cs +++ b/src/Ryujinx.Audio.Backends.SoundIo/Native/SoundIoDeviceContext.cs @@ -7,11 +7,11 @@ namespace Ryujinx.Audio.Backends.SoundIo.Native { public class SoundIoDeviceContext { - private readonly IntPtr _context; + private readonly nint _context; - public IntPtr Context => _context; + public nint Context => _context; - internal SoundIoDeviceContext(IntPtr context) + internal SoundIoDeviceContext(nint context) { _context = context; } @@ -36,9 +36,9 @@ namespace Ryujinx.Audio.Backends.SoundIo.Native public SoundIoOutStreamContext CreateOutStream() { - IntPtr context = soundio_outstream_create(_context); + nint context = soundio_outstream_create(_context); - if (context == IntPtr.Zero) + if (context == nint.Zero) { return null; } diff --git a/src/Ryujinx.Audio.Backends.SoundIo/Native/SoundIoOutStreamContext.cs b/src/Ryujinx.Audio.Backends.SoundIo/Native/SoundIoOutStreamContext.cs index 4148ea0dd..b1823a074 100644 --- a/src/Ryujinx.Audio.Backends.SoundIo/Native/SoundIoOutStreamContext.cs +++ b/src/Ryujinx.Audio.Backends.SoundIo/Native/SoundIoOutStreamContext.cs @@ -8,19 +8,19 @@ namespace Ryujinx.Audio.Backends.SoundIo.Native public class SoundIoOutStreamContext : IDisposable { [UnmanagedFunctionPointer(CallingConvention.Cdecl)] - private unsafe delegate void WriteCallbackDelegate(IntPtr ctx, int frameCountMin, int frameCountMax); + private unsafe delegate void WriteCallbackDelegate(nint ctx, int frameCountMin, int frameCountMax); - private IntPtr _context; - private IntPtr _nameStored; + private nint _context; + private nint _nameStored; private Action _writeCallback; private WriteCallbackDelegate _writeCallbackNative; - public IntPtr Context => _context; + public nint Context => _context; - internal SoundIoOutStreamContext(IntPtr context) + internal SoundIoOutStreamContext(nint context) { _context = context; - _nameStored = IntPtr.Zero; + _nameStored = nint.Zero; _writeCallback = null; _writeCallbackNative = null; } @@ -40,7 +40,7 @@ namespace Ryujinx.Audio.Backends.SoundIo.Native { var context = GetOutContext(); - if (_nameStored != IntPtr.Zero && context.Name == _nameStored) + if (_nameStored != nint.Zero && context.Name == _nameStored) { Marshal.FreeHGlobal(_nameStored); } @@ -124,14 +124,14 @@ namespace Ryujinx.Audio.Backends.SoundIo.Native public Span BeginWrite(ref int frameCount) { - IntPtr arenas = default; + nint arenas = default; int nativeFrameCount = frameCount; unsafe { var frameCountPtr = &nativeFrameCount; var arenasPtr = &arenas; - CheckError(soundio_outstream_begin_write(_context, (IntPtr)arenasPtr, (IntPtr)frameCountPtr)); + CheckError(soundio_outstream_begin_write(_context, (nint)arenasPtr, (nint)frameCountPtr)); frameCount = *frameCountPtr; @@ -143,10 +143,10 @@ namespace Ryujinx.Audio.Backends.SoundIo.Native protected virtual void Dispose(bool disposing) { - if (_context != IntPtr.Zero) + if (_context != nint.Zero) { soundio_outstream_destroy(_context); - _context = IntPtr.Zero; + _context = nint.Zero; } } 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 @@
- + PreserveNewest libsoundio.dll - + PreserveNewest libsoundio.dylib - + PreserveNewest libsoundio.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..e9cc6a8e1 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,14 +51,14 @@ namespace Ryujinx.Audio.Backends.SoundIo public override float GetVolume() { - return _outputStream.Volume; + return _volume; } public override void PrepareToClose() { } public override void QueueBuffer(AudioBuffer buffer) { - SoundIoAudioBuffer driverBuffer = new(buffer.HostTag, GetSampleCount(buffer)); + SoundIoAudioBuffer driverBuffer = new(buffer.DataPointer, GetSampleCount(buffer)); _ringBuffer.Write(buffer.Data, 0, buffer.Data.Length); @@ -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() @@ -90,7 +101,7 @@ namespace Ryujinx.Audio.Backends.SoundIo return true; } - return driverBuffer.DriverIdentifier != buffer.HostTag; + return driverBuffer.DriverIdentifier != buffer.DataPointer; } private unsafe void Update(int minFrameCount, int maxFrameCount) @@ -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/Common/HardwareDeviceSessionOutputBase.cs b/src/Ryujinx.Audio/Backends/Common/HardwareDeviceSessionOutputBase.cs index f193d9861..5599c0827 100644 --- a/src/Ryujinx.Audio/Backends/Common/HardwareDeviceSessionOutputBase.cs +++ b/src/Ryujinx.Audio/Backends/Common/HardwareDeviceSessionOutputBase.cs @@ -40,7 +40,7 @@ namespace Ryujinx.Audio.Backends.Common } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public virtual ulong GetSampleCount(int dataSize) + protected ulong GetSampleCount(int dataSize) { return (ulong)BackendHelper.GetSampleCount(RequestedSampleFormat, (int)RequestedChannelCount, dataSize); } 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/CompatLayerHardwareDeviceSession.cs b/src/Ryujinx.Audio/Backends/CompatLayer/CompatLayerHardwareDeviceSession.cs index 0cfbefd1e..a9acabec9 100644 --- a/src/Ryujinx.Audio/Backends/CompatLayer/CompatLayerHardwareDeviceSession.cs +++ b/src/Ryujinx.Audio/Backends/CompatLayer/CompatLayerHardwareDeviceSession.cs @@ -39,11 +39,6 @@ namespace Ryujinx.Audio.Backends.CompatLayer _realSession.PrepareToClose(); } - public override ulong GetSampleCount(int dataSize) - { - return _realSession.GetSampleCount(dataSize); - } - public override void QueueBuffer(AudioBuffer buffer) { SampleFormat realSampleFormat = _realSession.RequestedSampleFormat; @@ -124,7 +119,6 @@ namespace Ryujinx.Audio.Backends.CompatLayer AudioBuffer fakeBuffer = new() { BufferTag = buffer.BufferTag, - HostTag = buffer.HostTag, DataPointer = buffer.DataPointer, DataSize = (ulong)samples.Length, }; 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 deleted file mode 100644 index cdd5eb8a8..000000000 --- a/src/Ryujinx.Audio/Backends/DelayLayer/DelayLayerHardwareDeviceDriver.cs +++ /dev/null @@ -1,86 +0,0 @@ -using Ryujinx.Audio.Backends.Common; -using Ryujinx.Audio.Common; -using Ryujinx.Audio.Integration; -using Ryujinx.Memory; -using System; -using System.Threading; -using static Ryujinx.Audio.Integration.IHardwareDeviceDriver; - -namespace Ryujinx.Audio.Backends.DelayLayer -{ - public class DelayLayerHardwareDeviceDriver : IHardwareDeviceDriver - { - private readonly IHardwareDeviceDriver _realDriver; - - public static bool IsSupported => true; - - public ulong SampleDelay48k; - - 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) - { - IHardwareDeviceSession session = _realDriver.OpenDeviceSession(direction, memoryManager, sampleFormat, sampleRate, channelCount, volume); - - if (direction == Direction.Output) - { - return new DelayLayerHardwareDeviceSession(this, session as HardwareDeviceSessionOutputBase, sampleFormat, channelCount); - } - - return session; - } - - public ManualResetEvent GetUpdateRequiredEvent() - { - return _realDriver.GetUpdateRequiredEvent(); - } - - public ManualResetEvent GetPauseEvent() - { - return _realDriver.GetPauseEvent(); - } - - public void Dispose() - { - GC.SuppressFinalize(this); - Dispose(true); - } - - protected virtual void Dispose(bool disposing) - { - if (disposing) - { - _realDriver.Dispose(); - } - } - - public bool SupportsSampleRate(uint sampleRate) - { - return _realDriver.SupportsSampleRate(sampleRate); - } - - public bool SupportsSampleFormat(SampleFormat sampleFormat) - { - return _realDriver.SupportsSampleFormat(sampleFormat); - } - - public bool SupportsDirection(Direction direction) - { - return _realDriver.SupportsDirection(direction); - } - - public bool SupportsChannelCount(uint channelCount) - { - return _realDriver.SupportsChannelCount(channelCount); - } - - public IHardwareDeviceDriver GetRealDeviceDriver() - { - return _realDriver.GetRealDeviceDriver(); - } - } -} diff --git a/src/Ryujinx.Audio/Backends/DelayLayer/DelayLayerHardwareDeviceSession.cs b/src/Ryujinx.Audio/Backends/DelayLayer/DelayLayerHardwareDeviceSession.cs deleted file mode 100644 index 996a2a369..000000000 --- a/src/Ryujinx.Audio/Backends/DelayLayer/DelayLayerHardwareDeviceSession.cs +++ /dev/null @@ -1,151 +0,0 @@ -using Ryujinx.Audio.Backends.Common; -using Ryujinx.Audio.Common; -using System.Collections.Generic; -using System.Threading; - -namespace Ryujinx.Audio.Backends.DelayLayer -{ - internal class DelayLayerHardwareDeviceSession : HardwareDeviceSessionOutputBase - { - private readonly HardwareDeviceSessionOutputBase _realSession; - private readonly ManualResetEvent _updateRequiredEvent; - - private readonly ulong _delayTarget; - - private object _sampleCountLock = new(); - - private List _buffers = new(); - - public DelayLayerHardwareDeviceSession(DelayLayerHardwareDeviceDriver driver, HardwareDeviceSessionOutputBase realSession, SampleFormat userSampleFormat, uint userChannelCount) : base(realSession.MemoryManager, realSession.RequestedSampleFormat, realSession.RequestedSampleRate, userChannelCount) - { - _realSession = realSession; - _delayTarget = driver.SampleDelay48k; - - _updateRequiredEvent = driver.GetUpdateRequiredEvent(); - } - - public override void Dispose() - { - _realSession.Dispose(); - } - - public override ulong GetPlayedSampleCount() - { - lock (_sampleCountLock) - { - // Update the played samples count. - WasBufferFullyConsumed(null); - - return _playedSamplesCount; - } - } - - public override float GetVolume() - { - return _realSession.GetVolume(); - } - - public override void PrepareToClose() - { - _realSession.PrepareToClose(); - } - - public override void QueueBuffer(AudioBuffer buffer) - { - _realSession.QueueBuffer(buffer); - - ulong samples = GetSampleCount(buffer); - - lock (_sampleCountLock) - { - _buffers.Add(buffer); - } - - _updateRequiredEvent.Set(); - } - - public override ulong GetSampleCount(int dataSize) - { - return _realSession.GetSampleCount(dataSize); - } - - public override void SetVolume(float volume) - { - _realSession.SetVolume(volume); - } - - public override void Start() - { - _realSession.Start(); - } - - public override void Stop() - { - _realSession.Stop(); - } - - private ulong _playedSamplesCount = 0; - private int _frontIndex = -1; - - public override bool WasBufferFullyConsumed(AudioBuffer buffer) - { - ulong delaySamples = 0; - bool isConsumed = true; - // True if it's in the _delayedSamples range. - lock (_sampleCountLock) - { - for (int i = 0; i < _buffers.Count; i++) - { - AudioBuffer elem = _buffers[i]; - isConsumed = isConsumed && _realSession.WasBufferFullyConsumed(elem); - ulong samples = GetSampleCount(elem); - - bool afterFront = i > _frontIndex; - - if (isConsumed) - { - if (_frontIndex > -1) - { - _frontIndex--; - } - - _buffers.RemoveAt(i--); - - if (afterFront) - { - _playedSamplesCount += samples; - } - - if (buffer == elem) - { - return true; - } - } - else - { - if (afterFront && delaySamples < _delayTarget) - { - _playedSamplesCount += samples; - _frontIndex = i; - } - - if (buffer == elem) - { - return i <= _frontIndex; - } - - delaySamples += samples; - } - } - - // Buffer was not queued. - return true; - } - } - - public override bool RegisterBuffer(AudioBuffer buffer, byte[] samples) - { - return _realSession.RegisterBuffer(buffer, samples); - } - } -} 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/Common/AudioBuffer.cs b/src/Ryujinx.Audio/Common/AudioBuffer.cs index 2c04e9e60..87a7d5f32 100644 --- a/src/Ryujinx.Audio/Common/AudioBuffer.cs +++ b/src/Ryujinx.Audio/Common/AudioBuffer.cs @@ -1,5 +1,4 @@ using Ryujinx.Audio.Integration; -using System.Threading; namespace Ryujinx.Audio.Common { @@ -8,19 +7,12 @@ namespace Ryujinx.Audio.Common /// public class AudioBuffer { - private static ulong UniqueIdGlobal = 0; - /// - /// Unique tag of this buffer, from the guest. + /// Unique tag of this buffer. /// /// Unique per session public ulong BufferTag; - /// - /// Globally unique ID of the buffer on the host. - /// - public ulong HostTag = Interlocked.Increment(ref UniqueIdGlobal); - /// /// Pointer to the user samples. /// 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/AdpcmHelper.cs b/src/Ryujinx.Audio/Renderer/Dsp/AdpcmHelper.cs index 5cb4509ff..8b497fe2a 100644 --- a/src/Ryujinx.Audio/Renderer/Dsp/AdpcmHelper.cs +++ b/src/Ryujinx.Audio/Renderer/Dsp/AdpcmHelper.cs @@ -81,7 +81,7 @@ namespace Ryujinx.Audio.Renderer.Dsp [MethodImpl(MethodImplOptions.AggressiveInlining)] private static short GetCoefficientAtIndex(ReadOnlySpan coefficients, int index) { - if ((uint)index > (uint)coefficients.Length) + if ((uint)index >= (uint)coefficients.Length) { Logger.Error?.Print(LogClass.AudioRenderer, $"Out of bound read for coefficient at index {index}"); 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/CommandList.cs b/src/Ryujinx.Audio/Renderer/Dsp/Command/CommandList.cs index 3fe106ddf..ba19330b6 100644 --- a/src/Ryujinx.Audio/Renderer/Dsp/Command/CommandList.cs +++ b/src/Ryujinx.Audio/Renderer/Dsp/Command/CommandList.cs @@ -64,11 +64,11 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public unsafe IntPtr GetBufferPointer(int index) + public unsafe nint GetBufferPointer(int index) { if (index >= 0 && index < _buffersEntryCount) { - return (IntPtr)((float*)_buffersMemoryHandle.Pointer + index * _sampleCount); + return (nint)((float*)_buffersMemoryHandle.Pointer + index * _sampleCount); } throw new ArgumentOutOfRangeException(nameof(index), index, null); 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..c6c0956a6 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 nint[_parameter.ChannelCount]; + Span outputBuffers = stackalloc nint[_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/DelayCommand.cs b/src/Ryujinx.Audio/Renderer/Dsp/Command/DelayCommand.cs index 6fa3777f4..21cf69504 100644 --- a/src/Ryujinx.Audio/Renderer/Dsp/Command/DelayCommand.cs +++ b/src/Ryujinx.Audio/Renderer/Dsp/Command/DelayCommand.cs @@ -77,7 +77,7 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command } [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - private unsafe void ProcessDelayStereo(ref DelayState state, Span outputBuffers, ReadOnlySpan inputBuffers, uint sampleCount) + private unsafe void ProcessDelayStereo(ref DelayState state, Span outputBuffers, ReadOnlySpan inputBuffers, uint sampleCount) { const ushort ChannelCount = 2; @@ -114,7 +114,7 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command } [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - private unsafe void ProcessDelayQuadraphonic(ref DelayState state, Span outputBuffers, ReadOnlySpan inputBuffers, uint sampleCount) + private unsafe void ProcessDelayQuadraphonic(ref DelayState state, Span outputBuffers, ReadOnlySpan inputBuffers, uint sampleCount) { const ushort ChannelCount = 4; @@ -160,7 +160,7 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command } [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - private unsafe void ProcessDelaySurround(ref DelayState state, Span outputBuffers, ReadOnlySpan inputBuffers, uint sampleCount) + private unsafe void ProcessDelaySurround(ref DelayState state, Span outputBuffers, ReadOnlySpan inputBuffers, uint sampleCount) { const ushort ChannelCount = 6; @@ -219,8 +219,8 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command if (IsEffectEnabled && Parameter.IsChannelCountValid()) { - Span inputBuffers = stackalloc IntPtr[Parameter.ChannelCount]; - Span outputBuffers = stackalloc IntPtr[Parameter.ChannelCount]; + Span inputBuffers = stackalloc nint[Parameter.ChannelCount]; + Span outputBuffers = stackalloc nint[Parameter.ChannelCount]; for (int i = 0; i < Parameter.ChannelCount; i++) { diff --git a/src/Ryujinx.Audio/Renderer/Dsp/Command/LimiterCommandVersion1.cs b/src/Ryujinx.Audio/Renderer/Dsp/Command/LimiterCommandVersion1.cs index 3ba0b5884..4e7f67e78 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 nint[_parameter.ChannelCount]; + Span outputBuffers = stackalloc nint[_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..b0032c5b7 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 nint[_parameter.ChannelCount]; + Span outputBuffers = stackalloc nint[_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/Command/Reverb3dCommand.cs b/src/Ryujinx.Audio/Renderer/Dsp/Command/Reverb3dCommand.cs index 8cdd4843b..58023ac9d 100644 --- a/src/Ryujinx.Audio/Renderer/Dsp/Command/Reverb3dCommand.cs +++ b/src/Ryujinx.Audio/Renderer/Dsp/Command/Reverb3dCommand.cs @@ -71,30 +71,30 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void ProcessReverb3dMono(ref Reverb3dState state, ReadOnlySpan outputBuffers, ReadOnlySpan inputBuffers, uint sampleCount) + private void ProcessReverb3dMono(ref Reverb3dState state, ReadOnlySpan outputBuffers, ReadOnlySpan inputBuffers, uint sampleCount) { ProcessReverb3dGeneric(ref state, outputBuffers, inputBuffers, sampleCount, _outputEarlyIndicesTableMono, _targetEarlyDelayLineIndicesTableMono, _targetOutputFeedbackIndicesTableMono); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void ProcessReverb3dStereo(ref Reverb3dState state, ReadOnlySpan outputBuffers, ReadOnlySpan inputBuffers, uint sampleCount) + private void ProcessReverb3dStereo(ref Reverb3dState state, ReadOnlySpan outputBuffers, ReadOnlySpan inputBuffers, uint sampleCount) { ProcessReverb3dGeneric(ref state, outputBuffers, inputBuffers, sampleCount, _outputEarlyIndicesTableStereo, _targetEarlyDelayLineIndicesTableStereo, _targetOutputFeedbackIndicesTableStereo); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void ProcessReverb3dQuadraphonic(ref Reverb3dState state, ReadOnlySpan outputBuffers, ReadOnlySpan inputBuffers, uint sampleCount) + private void ProcessReverb3dQuadraphonic(ref Reverb3dState state, ReadOnlySpan outputBuffers, ReadOnlySpan inputBuffers, uint sampleCount) { ProcessReverb3dGeneric(ref state, outputBuffers, inputBuffers, sampleCount, _outputEarlyIndicesTableQuadraphonic, _targetEarlyDelayLineIndicesTableQuadraphonic, _targetOutputFeedbackIndicesTableQuadraphonic); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void ProcessReverb3dSurround(ref Reverb3dState state, ReadOnlySpan outputBuffers, ReadOnlySpan inputBuffers, uint sampleCount) + private void ProcessReverb3dSurround(ref Reverb3dState state, ReadOnlySpan outputBuffers, ReadOnlySpan inputBuffers, uint sampleCount) { ProcessReverb3dGeneric(ref state, outputBuffers, inputBuffers, sampleCount, _outputEarlyIndicesTableSurround, _targetEarlyDelayLineIndicesTableSurround, _targetOutputFeedbackIndicesTableSurround); } - private unsafe void ProcessReverb3dGeneric(ref Reverb3dState state, ReadOnlySpan outputBuffers, ReadOnlySpan inputBuffers, uint sampleCount, ReadOnlySpan outputEarlyIndicesTable, ReadOnlySpan targetEarlyDelayLineIndicesTable, ReadOnlySpan targetOutputFeedbackIndicesTable) + private unsafe void ProcessReverb3dGeneric(ref Reverb3dState state, ReadOnlySpan outputBuffers, ReadOnlySpan inputBuffers, uint sampleCount, ReadOnlySpan outputEarlyIndicesTable, ReadOnlySpan targetEarlyDelayLineIndicesTable, ReadOnlySpan targetOutputFeedbackIndicesTable) { const int DelayLineSampleIndexOffset = 1; @@ -193,8 +193,8 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command if (IsEffectEnabled && Parameter.IsChannelCountValid()) { - Span inputBuffers = stackalloc IntPtr[Parameter.ChannelCount]; - Span outputBuffers = stackalloc IntPtr[Parameter.ChannelCount]; + Span inputBuffers = stackalloc nint[Parameter.ChannelCount]; + Span outputBuffers = stackalloc nint[Parameter.ChannelCount]; for (int i = 0; i < Parameter.ChannelCount; i++) { diff --git a/src/Ryujinx.Audio/Renderer/Dsp/Command/ReverbCommand.cs b/src/Ryujinx.Audio/Renderer/Dsp/Command/ReverbCommand.cs index 874eb8e8b..204570cec 100644 --- a/src/Ryujinx.Audio/Renderer/Dsp/Command/ReverbCommand.cs +++ b/src/Ryujinx.Audio/Renderer/Dsp/Command/ReverbCommand.cs @@ -77,7 +77,7 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void ProcessReverbMono(ref ReverbState state, ReadOnlySpan outputBuffers, ReadOnlySpan inputBuffers, uint sampleCount) + private void ProcessReverbMono(ref ReverbState state, ReadOnlySpan outputBuffers, ReadOnlySpan inputBuffers, uint sampleCount) { ProcessReverbGeneric( ref state, @@ -91,7 +91,7 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void ProcessReverbStereo(ref ReverbState state, ReadOnlySpan outputBuffers, ReadOnlySpan inputBuffers, uint sampleCount) + private void ProcessReverbStereo(ref ReverbState state, ReadOnlySpan outputBuffers, ReadOnlySpan inputBuffers, uint sampleCount) { ProcessReverbGeneric( ref state, @@ -105,7 +105,7 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void ProcessReverbQuadraphonic(ref ReverbState state, ReadOnlySpan outputBuffers, ReadOnlySpan inputBuffers, uint sampleCount) + private void ProcessReverbQuadraphonic(ref ReverbState state, ReadOnlySpan outputBuffers, ReadOnlySpan inputBuffers, uint sampleCount) { ProcessReverbGeneric( ref state, @@ -119,7 +119,7 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void ProcessReverbSurround(ref ReverbState state, ReadOnlySpan outputBuffers, ReadOnlySpan inputBuffers, uint sampleCount) + private void ProcessReverbSurround(ref ReverbState state, ReadOnlySpan outputBuffers, ReadOnlySpan inputBuffers, uint sampleCount) { ProcessReverbGeneric( ref state, @@ -132,7 +132,7 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command _outputIndicesTableSurround); } - private unsafe void ProcessReverbGeneric(ref ReverbState state, ReadOnlySpan outputBuffers, ReadOnlySpan inputBuffers, uint sampleCount, ReadOnlySpan outputEarlyIndicesTable, ReadOnlySpan targetEarlyDelayLineIndicesTable, ReadOnlySpan targetOutputFeedbackIndicesTable, ReadOnlySpan outputIndicesTable) + private unsafe void ProcessReverbGeneric(ref ReverbState state, ReadOnlySpan outputBuffers, ReadOnlySpan inputBuffers, uint sampleCount, ReadOnlySpan outputEarlyIndicesTable, ReadOnlySpan targetEarlyDelayLineIndicesTable, ReadOnlySpan targetOutputFeedbackIndicesTable, ReadOnlySpan outputIndicesTable) { bool isSurround = Parameter.ChannelCount == 6; @@ -223,8 +223,8 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command if (IsEffectEnabled && Parameter.IsChannelCountValid()) { - Span inputBuffers = stackalloc IntPtr[Parameter.ChannelCount]; - Span outputBuffers = stackalloc IntPtr[Parameter.ChannelCount]; + Span inputBuffers = stackalloc nint[Parameter.ChannelCount]; + Span outputBuffers = stackalloc nint[Parameter.ChannelCount]; for (int i = 0; i < Parameter.ChannelCount; i++) { 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/AddressInfo.cs b/src/Ryujinx.Audio/Renderer/Server/MemoryPool/AddressInfo.cs index a7ec4cf51..3337e44b0 100644 --- a/src/Ryujinx.Audio/Renderer/Server/MemoryPool/AddressInfo.cs +++ b/src/Ryujinx.Audio/Renderer/Server/MemoryPool/AddressInfo.cs @@ -29,7 +29,7 @@ namespace Ryujinx.Audio.Renderer.Server.MemoryPool private readonly unsafe ref MemoryPoolState MemoryPoolState => ref *_memoryPools; - public readonly unsafe bool HasMemoryPoolState => (IntPtr)_memoryPools != IntPtr.Zero; + public readonly unsafe bool HasMemoryPoolState => (nint)_memoryPools != nint.Zero; /// /// Create an new empty . diff --git a/src/Ryujinx.Audio/Renderer/Server/MemoryPool/MemoryPoolState.cs b/src/Ryujinx.Audio/Renderer/Server/MemoryPool/MemoryPoolState.cs index 91bd5dbf5..d0133622a 100644 --- a/src/Ryujinx.Audio/Renderer/Server/MemoryPool/MemoryPoolState.cs +++ b/src/Ryujinx.Audio/Renderer/Server/MemoryPool/MemoryPoolState.cs @@ -55,7 +55,7 @@ namespace Ryujinx.Audio.Renderer.Server.MemoryPool [MarshalAs(UnmanagedType.I1)] public bool IsUsed; - public static unsafe MemoryPoolState* Null => (MemoryPoolState*)IntPtr.Zero.ToPointer(); + public static unsafe MemoryPoolState* Null => (MemoryPoolState*)nint.Zero.ToPointer(); /// /// Create a new with the given . 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..34b3ed4bd 100644 --- a/src/Ryujinx.Audio/Renderer/Server/Mix/MixState.cs +++ b/src/Ryujinx.Audio/Renderer/Server/Mix/MixState.cs @@ -65,7 +65,7 @@ namespace Ryujinx.Audio.Renderer.Server.Mix /// /// The effect processing order storage. /// - private readonly IntPtr _effectProcessingOrderArrayPointer; + private readonly nint _effectProcessingOrderArrayPointer; /// /// The max element count that can be found in the effect processing order storage. @@ -123,7 +123,7 @@ namespace Ryujinx.Audio.Renderer.Server.Mix { get { - if (_effectProcessingOrderArrayPointer == IntPtr.Zero) + if (_effectProcessingOrderArrayPointer == nint.Zero) { return Span.Empty; } @@ -153,7 +153,7 @@ namespace Ryujinx.Audio.Renderer.Server.Mix unsafe { // SAFETY: safe as effectProcessingOrderArray comes from the work buffer memory that is pinned. - _effectProcessingOrderArrayPointer = (IntPtr)Unsafe.AsPointer(ref MemoryMarshal.GetReference(effectProcessingOrderArray.Span)); + _effectProcessingOrderArrayPointer = (nint)Unsafe.AsPointer(ref MemoryMarshal.GetReference(effectProcessingOrderArray.Span)); } EffectProcessingOrderArrayMaxCount = (uint)effectProcessingOrderArray.Length; @@ -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.Common/Collections/IntervalTree.cs b/src/Ryujinx.Common/Collections/IntervalTree.cs index d3a5e7fcf..f804bca91 100644 --- a/src/Ryujinx.Common/Collections/IntervalTree.cs +++ b/src/Ryujinx.Common/Collections/IntervalTree.cs @@ -492,7 +492,7 @@ namespace Ryujinx.Common.Collections Start = start; End = end; Max = end; - Values = new List> { new RangeNode(start, end, value) }; + Values = [new RangeNode(start, end, value)]; Parent = parent; } } diff --git a/src/Ryujinx.Common/Configuration/AppDataManager.cs b/src/Ryujinx.Common/Configuration/AppDataManager.cs index 010b39dbf..ca8e389ba 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"; @@ -45,19 +49,7 @@ namespace Ryujinx.Common.Configuration public static void Initialize(string baseDirPath) { - string appDataPath; - if (OperatingSystem.IsMacOS()) - { - appDataPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Library", "Application Support"); - } - else if (OperatingSystem.IsIOS()) - { - appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments); - } - else - { - appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); - } + string appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); if (appDataPath.Length == 0) { @@ -67,6 +59,17 @@ namespace Ryujinx.Common.Configuration string userProfilePath = Path.Combine(appDataPath, DefaultBaseDir); 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; @@ -93,65 +96,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 = string.Empty; + + 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 = string.Empty; + } + + 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 = string.Empty; + } + + 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..6b8152b9d 100644 --- a/src/Ryujinx.Common/Configuration/Hid/KeyboardHotkeys.cs +++ b/src/Ryujinx.Common/Configuration/Hid/KeyboardHotkeys.cs @@ -1,17 +1,17 @@ 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 ToggleVSyncMode { 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; } public Key ResScaleDown { get; set; } public Key VolumeUp { get; set; } public Key VolumeDown { get; set; } + public Key CustomVSyncIntervalIncrement { get; set; } + public Key CustomVSyncIntervalDecrement { 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/Configuration/Multiplayer/MultiplayerMode.cs b/src/Ryujinx.Common/Configuration/Multiplayer/MultiplayerMode.cs index 69f7d876d..be0e1518c 100644 --- a/src/Ryujinx.Common/Configuration/Multiplayer/MultiplayerMode.cs +++ b/src/Ryujinx.Common/Configuration/Multiplayer/MultiplayerMode.cs @@ -3,6 +3,7 @@ namespace Ryujinx.Common.Configuration.Multiplayer public enum MultiplayerMode { Disabled, + LdnRyu, LdnMitm, } } diff --git a/src/Ryujinx.Common/Configuration/VSyncMode.cs b/src/Ryujinx.Common/Configuration/VSyncMode.cs new file mode 100644 index 000000000..ca93b5e1c --- /dev/null +++ b/src/Ryujinx.Common/Configuration/VSyncMode.cs @@ -0,0 +1,9 @@ +namespace Ryujinx.Common.Configuration +{ + public enum VSyncMode + { + Switch, + Unbounded, + Custom + } +} 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/Extensions/StreamExtensions.cs b/src/Ryujinx.Common/Extensions/StreamExtensions.cs index 431d5534a..4b02781c9 100644 --- a/src/Ryujinx.Common/Extensions/StreamExtensions.cs +++ b/src/Ryujinx.Common/Extensions/StreamExtensions.cs @@ -8,7 +8,7 @@ namespace Ryujinx.Common public static class StreamExtensions { /// - /// Writes a " /> to this stream. + /// Writes a to this stream. /// /// This default implementation converts each buffer value to a stack-allocated /// byte array, then writes it to the Stream using . @@ -66,8 +66,8 @@ namespace Ryujinx.Common } /// - // Writes a four-byte unsigned integer to this stream. The current position - // of the stream is advanced by four. + /// Writes a four-byte unsigned integer to this stream. The current position + /// of the stream is advanced by four. /// /// The stream to be written to /// The value to be written 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/GraphicsDriver/NVThreadedOptimization.cs b/src/Ryujinx.Common/GraphicsDriver/NVThreadedOptimization.cs index f7b11783d..8d969bd6a 100644 --- a/src/Ryujinx.Common/GraphicsDriver/NVThreadedOptimization.cs +++ b/src/Ryujinx.Common/GraphicsDriver/NVThreadedOptimization.cs @@ -2,6 +2,7 @@ using Ryujinx.Common.GraphicsDriver.NVAPI; using System; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +// ReSharper disable InconsistentNaming namespace Ryujinx.Common.GraphicsDriver { @@ -20,33 +21,33 @@ namespace Ryujinx.Common.GraphicsDriver private const uint NvAPI_DRS_DestroySession_ID = 0x0DAD9CFF8; [LibraryImport("nvapi64")] - private static partial IntPtr nvapi_QueryInterface(uint id); + private static partial nint nvapi_QueryInterface(uint id); private delegate int NvAPI_InitializeDelegate(); private static NvAPI_InitializeDelegate NvAPI_Initialize; - private delegate int NvAPI_DRS_CreateSessionDelegate(out IntPtr handle); + private delegate int NvAPI_DRS_CreateSessionDelegate(out nint handle); private static NvAPI_DRS_CreateSessionDelegate NvAPI_DRS_CreateSession; - private delegate int NvAPI_DRS_LoadSettingsDelegate(IntPtr handle); + private delegate int NvAPI_DRS_LoadSettingsDelegate(nint handle); private static NvAPI_DRS_LoadSettingsDelegate NvAPI_DRS_LoadSettings; - private delegate int NvAPI_DRS_FindProfileByNameDelegate(IntPtr handle, NvapiUnicodeString profileName, out IntPtr profileHandle); + private delegate int NvAPI_DRS_FindProfileByNameDelegate(nint handle, NvapiUnicodeString profileName, out nint profileHandle); private static NvAPI_DRS_FindProfileByNameDelegate NvAPI_DRS_FindProfileByName; - private delegate int NvAPI_DRS_CreateProfileDelegate(IntPtr handle, ref NvdrsProfile profileInfo, out IntPtr profileHandle); + private delegate int NvAPI_DRS_CreateProfileDelegate(nint handle, ref NvdrsProfile profileInfo, out nint profileHandle); private static NvAPI_DRS_CreateProfileDelegate NvAPI_DRS_CreateProfile; - private delegate int NvAPI_DRS_CreateApplicationDelegate(IntPtr handle, IntPtr profileHandle, ref NvdrsApplicationV4 app); + private delegate int NvAPI_DRS_CreateApplicationDelegate(nint handle, nint profileHandle, ref NvdrsApplicationV4 app); private static NvAPI_DRS_CreateApplicationDelegate NvAPI_DRS_CreateApplication; - private delegate int NvAPI_DRS_SetSettingDelegate(IntPtr handle, IntPtr profileHandle, ref NvdrsSetting setting); + private delegate int NvAPI_DRS_SetSettingDelegate(nint handle, nint profileHandle, ref NvdrsSetting setting); private static NvAPI_DRS_SetSettingDelegate NvAPI_DRS_SetSetting; - private delegate int NvAPI_DRS_SaveSettingsDelegate(IntPtr handle); + private delegate int NvAPI_DRS_SaveSettingsDelegate(nint handle); private static NvAPI_DRS_SaveSettingsDelegate NvAPI_DRS_SaveSettings; - private delegate int NvAPI_DRS_DestroySessionDelegate(IntPtr handle); + private delegate int NvAPI_DRS_DestroySessionDelegate(nint handle); private static NvAPI_DRS_DestroySessionDelegate NvAPI_DRS_DestroySession; private static bool _initialized; @@ -93,7 +94,7 @@ namespace Ryujinx.Common.GraphicsDriver Check(NvAPI_Initialize()); - Check(NvAPI_DRS_CreateSession(out IntPtr handle)); + Check(NvAPI_DRS_CreateSession(out nint handle)); Check(NvAPI_DRS_LoadSettings(handle)); @@ -120,8 +121,8 @@ namespace Ryujinx.Common.GraphicsDriver }; application.AppName.Set("Ryujinx.exe"); application.UserFriendlyName.Set("Ryujinx"); - application.Launcher.Set(""); - application.FileInFolder.Set(""); + application.Launcher.Set(string.Empty); + application.FileInFolder.Set(string.Empty); Check(NvAPI_DRS_CreateApplication(handle, profileHandle, ref application)); } @@ -147,9 +148,9 @@ namespace Ryujinx.Common.GraphicsDriver private static T NvAPI_Delegate(uint id) where T : class { - IntPtr ptr = nvapi_QueryInterface(id); + nint ptr = nvapi_QueryInterface(id); - if (ptr != IntPtr.Zero) + if (ptr != nint.Zero) { return Marshal.GetDelegateForFunctionPointer(ptr); } diff --git a/src/Ryujinx.Common/Hash128.cs b/src/Ryujinx.Common/Hash128.cs index e0ffd230e..baee3e7d2 100644 --- a/src/Ryujinx.Common/Hash128.cs +++ b/src/Ryujinx.Common/Hash128.cs @@ -1,48 +1,736 @@ using System; +using System.Buffers.Binary; +using System.Diagnostics; +using System.Numerics; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.X86; +// ReSharper disable InconsistentNaming namespace Ryujinx.Common { [StructLayout(LayoutKind.Sequential)] - public struct Hash128 : IEquatable + public struct Hash128(ulong low, ulong high) : IEquatable { - public ulong Low; - public ulong High; + public ulong Low = low; + public ulong High = high; - public Hash128(ulong low, ulong high) + public readonly override string ToString() => $"{High:x16}{Low:x16}"; + + public static bool operator ==(Hash128 x, Hash128 y) => x.Equals(y); + + public static bool operator !=(Hash128 x, Hash128 y) => !x.Equals(y); + + public readonly override bool Equals(object obj) => obj is Hash128 hash128 && Equals(hash128); + + public readonly bool Equals(Hash128 cmpObj) => Low == cmpObj.Low && High == cmpObj.High; + + public readonly override int GetHashCode() => HashCode.Combine(Low, High); + + public static Hash128 ComputeHash(ReadOnlySpan input) => Xxh3128bitsInternal(input, Xxh3KSecret, 0UL); + + #region Hash computation + + private const int StripeLen = 64; + private const int AccNb = StripeLen / sizeof(ulong); + private const int SecretConsumeRate = 8; + private const int SecretLastAccStart = 7; + private const int SecretMergeAccsStart = 11; + private const int SecretSizeMin = 136; + private const int MidSizeStartOffset = 3; + private const int MidSizeLastOffset = 17; + + private const uint Prime32_1 = 0x9E3779B1U; + private const uint Prime32_2 = 0x85EBCA77U; + private const uint Prime32_3 = 0xC2B2AE3DU; + private const uint Prime32_4 = 0x27D4EB2FU; + private const uint Prime32_5 = 0x165667B1U; + + private const ulong Prime64_1 = 0x9E3779B185EBCA87UL; + private const ulong Prime64_2 = 0xC2B2AE3D27D4EB4FUL; + private const ulong Prime64_3 = 0x165667B19E3779F9UL; + private const ulong Prime64_4 = 0x85EBCA77C2B2AE63UL; + private const ulong Prime64_5 = 0x27D4EB2F165667C5UL; + + private static readonly ulong[] _xxh3InitAcc = + [ + Prime32_3, + Prime64_1, + Prime64_2, + Prime64_3, + Prime64_4, + Prime32_2, + Prime64_5, + Prime32_1 + ]; + + private static ReadOnlySpan Xxh3KSecret => + [ + 0xb8, + 0xfe, + 0x6c, + 0x39, + 0x23, + 0xa4, + 0x4b, + 0xbe, + 0x7c, + 0x01, + 0x81, + 0x2c, + 0xf7, + 0x21, + 0xad, + 0x1c, + 0xde, + 0xd4, + 0x6d, + 0xe9, + 0x83, + 0x90, + 0x97, + 0xdb, + 0x72, + 0x40, + 0xa4, + 0xa4, + 0xb7, + 0xb3, + 0x67, + 0x1f, + 0xcb, + 0x79, + 0xe6, + 0x4e, + 0xcc, + 0xc0, + 0xe5, + 0x78, + 0x82, + 0x5a, + 0xd0, + 0x7d, + 0xcc, + 0xff, + 0x72, + 0x21, + 0xb8, + 0x08, + 0x46, + 0x74, + 0xf7, + 0x43, + 0x24, + 0x8e, + 0xe0, + 0x35, + 0x90, + 0xe6, + 0x81, + 0x3a, + 0x26, + 0x4c, + 0x3c, + 0x28, + 0x52, + 0xbb, + 0x91, + 0xc3, + 0x00, + 0xcb, + 0x88, + 0xd0, + 0x65, + 0x8b, + 0x1b, + 0x53, + 0x2e, + 0xa3, + 0x71, + 0x64, + 0x48, + 0x97, + 0xa2, + 0x0d, + 0xf9, + 0x4e, + 0x38, + 0x19, + 0xef, + 0x46, + 0xa9, + 0xde, + 0xac, + 0xd8, + 0xa8, + 0xfa, + 0x76, + 0x3f, + 0xe3, + 0x9c, + 0x34, + 0x3f, + 0xf9, + 0xdc, + 0xbb, + 0xc7, + 0xc7, + 0x0b, + 0x4f, + 0x1d, + 0x8a, + 0x51, + 0xe0, + 0x4b, + 0xcd, + 0xb4, + 0x59, + 0x31, + 0xc8, + 0x9f, + 0x7e, + 0xc9, + 0xd9, + 0x78, + 0x73, + 0x64, + 0xea, + 0xc5, + 0xac, + 0x83, + 0x34, + 0xd3, + 0xeb, + 0xc3, + 0xc5, + 0x81, + 0xa0, + 0xff, + 0xfa, + 0x13, + 0x63, + 0xeb, + 0x17, + 0x0d, + 0xdd, + 0x51, + 0xb7, + 0xf0, + 0xda, + 0x49, + 0xd3, + 0x16, + 0x55, + 0x26, + 0x29, + 0xd4, + 0x68, + 0x9e, + 0x2b, + 0x16, + 0xbe, + 0x58, + 0x7d, + 0x47, + 0xa1, + 0xfc, + 0x8f, + 0xf8, + 0xb8, + 0xd1, + 0x7a, + 0xd0, + 0x31, + 0xce, + 0x45, + 0xcb, + 0x3a, + 0x8f, + 0x95, + 0x16, + 0x04, + 0x28, + 0xaf, + 0xd7, + 0xfb, + 0xca, + 0xbb, + 0x4b, + 0x40, + 0x7e + ]; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static ulong Mult32To64(ulong x, ulong y) => (uint)x * (ulong)(uint)y; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Hash128 Mult64To128(ulong lhs, ulong rhs) { - Low = low; - High = high; + ulong high = Math.BigMul(lhs, rhs, out ulong low); + + return new Hash128 + { + Low = low, + High = high, + }; } - public readonly override string ToString() + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static ulong Mul128Fold64(ulong lhs, ulong rhs) { - return $"{High:x16}{Low:x16}"; + Hash128 product = Mult64To128(lhs, rhs); + + return product.Low ^ product.High; } - public static bool operator ==(Hash128 x, Hash128 y) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static ulong XorShift64(ulong v64, int shift) { - return x.Equals(y); + Debug.Assert(shift is >= 0 and < 64); + + return v64 ^ (v64 >> shift); } - public static bool operator !=(Hash128 x, Hash128 y) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static ulong Xxh3Avalanche(ulong h64) { - return !x.Equals(y); + h64 = XorShift64(h64, 37); + h64 *= 0x165667919E3779F9UL; + h64 = XorShift64(h64, 32); + + return h64; } - public readonly override bool Equals(object obj) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static ulong Xxh64Avalanche(ulong h64) { - return obj is Hash128 hash128 && Equals(hash128); + h64 ^= h64 >> 33; + h64 *= Prime64_2; + h64 ^= h64 >> 29; + h64 *= Prime64_3; + h64 ^= h64 >> 32; + + return h64; } - public readonly bool Equals(Hash128 cmpObj) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private unsafe static void Xxh3Accumulate512(Span acc, ReadOnlySpan input, ReadOnlySpan secret) { - return Low == cmpObj.Low && High == cmpObj.High; + if (Avx2.IsSupported) + { + fixed (ulong* pAcc = acc) + { + fixed (byte* pInput = input, pSecret = secret) + { + Vector256* xAcc = (Vector256*)pAcc; + Vector256* xInput = (Vector256*)pInput; + Vector256* xSecret = (Vector256*)pSecret; + + for (ulong i = 0; i < StripeLen / 32; i++) + { + Vector256 dataVec = xInput[i]; + Vector256 keyVec = xSecret[i]; + Vector256 dataKey = Avx2.Xor(dataVec, keyVec); + Vector256 dataKeyLo = Avx2.Shuffle(dataKey.AsUInt32(), 0b00110001); + Vector256 product = Avx2.Multiply(dataKey.AsUInt32(), dataKeyLo); + Vector256 dataSwap = Avx2.Shuffle(dataVec.AsUInt32(), 0b01001110); + Vector256 sum = Avx2.Add(xAcc[i], dataSwap.AsUInt64()); + xAcc[i] = Avx2.Add(product, sum); + } + } + } + } + else if (Sse2.IsSupported) + { + fixed (ulong* pAcc = acc) + { + fixed (byte* pInput = input, pSecret = secret) + { + Vector128* xAcc = (Vector128*)pAcc; + Vector128* xInput = (Vector128*)pInput; + Vector128* xSecret = (Vector128*)pSecret; + + for (ulong i = 0; i < StripeLen / 16; i++) + { + Vector128 dataVec = xInput[i]; + Vector128 keyVec = xSecret[i]; + Vector128 dataKey = Sse2.Xor(dataVec, keyVec); + Vector128 dataKeyLo = Sse2.Shuffle(dataKey.AsUInt32(), 0b00110001); + Vector128 product = Sse2.Multiply(dataKey.AsUInt32(), dataKeyLo); + Vector128 dataSwap = Sse2.Shuffle(dataVec.AsUInt32(), 0b01001110); + Vector128 sum = Sse2.Add(xAcc[i], dataSwap.AsUInt64()); + xAcc[i] = Sse2.Add(product, sum); + } + } + } + } + else + { + for (int i = 0; i < AccNb; i++) + { + ulong dataVal = BinaryPrimitives.ReadUInt64LittleEndian(input[(i * sizeof(ulong))..]); + ulong dataKey = dataVal ^ BinaryPrimitives.ReadUInt64LittleEndian(secret[(i * sizeof(ulong))..]); + acc[i ^ 1] += dataVal; + acc[i] += Mult32To64((uint)dataKey, dataKey >> 32); + } + } } - public readonly override int GetHashCode() + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private unsafe static void Xxh3ScrambleAcc(Span acc, ReadOnlySpan secret) { - return HashCode.Combine(Low, High); + if (Avx2.IsSupported) + { + fixed (ulong* pAcc = acc) + { + fixed (byte* pSecret = secret) + { + Vector256 prime32 = Vector256.Create(Prime32_1); + Vector256* xAcc = (Vector256*)pAcc; + Vector256* xSecret = (Vector256*)pSecret; + + for (ulong i = 0; i < StripeLen / 32; i++) + { + Vector256 accVec = xAcc[i]; + Vector256 shifted = Avx2.ShiftRightLogical(accVec, 47); + Vector256 dataVec = Avx2.Xor(accVec, shifted); + + Vector256 keyVec = xSecret[i]; + Vector256 dataKey = Avx2.Xor(dataVec.AsUInt32(), keyVec.AsUInt32()); + + Vector256 dataKeyHi = Avx2.Shuffle(dataKey.AsUInt32(), 0b00110001); + Vector256 prodLo = Avx2.Multiply(dataKey, prime32); + Vector256 prodHi = Avx2.Multiply(dataKeyHi, prime32); + + xAcc[i] = Avx2.Add(prodLo, Avx2.ShiftLeftLogical(prodHi, 32)); + } + } + } + } + else if (Sse2.IsSupported) + { + fixed (ulong* pAcc = acc) + { + fixed (byte* pSecret = secret) + { + Vector128 prime32 = Vector128.Create(Prime32_1); + Vector128* xAcc = (Vector128*)pAcc; + Vector128* xSecret = (Vector128*)pSecret; + + for (ulong i = 0; i < StripeLen / 16; i++) + { + Vector128 accVec = xAcc[i]; + Vector128 shifted = Sse2.ShiftRightLogical(accVec, 47); + Vector128 dataVec = Sse2.Xor(accVec, shifted); + + Vector128 keyVec = xSecret[i]; + Vector128 dataKey = Sse2.Xor(dataVec.AsUInt32(), keyVec.AsUInt32()); + + Vector128 dataKeyHi = Sse2.Shuffle(dataKey.AsUInt32(), 0b00110001); + Vector128 prodLo = Sse2.Multiply(dataKey, prime32); + Vector128 prodHi = Sse2.Multiply(dataKeyHi, prime32); + + xAcc[i] = Sse2.Add(prodLo, Sse2.ShiftLeftLogical(prodHi, 32)); + } + } + } + } + else + { + for (int i = 0; i < AccNb; i++) + { + ulong key64 = BinaryPrimitives.ReadUInt64LittleEndian(secret[(i * sizeof(ulong))..]); + ulong acc64 = acc[i]; + acc64 = XorShift64(acc64, 47); + acc64 ^= key64; + acc64 *= Prime32_1; + acc[i] = acc64; + } + } } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Xxh3Accumulate(Span acc, ReadOnlySpan input, ReadOnlySpan secret, int nbStripes) + { + for (int n = 0; n < nbStripes; n++) + { + ReadOnlySpan inData = input[(n * StripeLen)..]; + Xxh3Accumulate512(acc, inData, secret[(n * SecretConsumeRate)..]); + } + } + + private static void Xxh3HashLongInternalLoop(Span acc, ReadOnlySpan input, ReadOnlySpan secret) + { + int nbStripesPerBlock = (secret.Length - StripeLen) / SecretConsumeRate; + int blockLen = StripeLen * nbStripesPerBlock; + int nbBlocks = (input.Length - 1) / blockLen; + + Debug.Assert(secret.Length >= SecretSizeMin); + + for (int n = 0; n < nbBlocks; n++) + { + Xxh3Accumulate(acc, input[(n * blockLen)..], secret, nbStripesPerBlock); + Xxh3ScrambleAcc(acc, secret[^StripeLen..]); + } + + Debug.Assert(input.Length > StripeLen); + + int nbStripes = (input.Length - 1 - (blockLen * nbBlocks)) / StripeLen; + Debug.Assert(nbStripes <= (secret.Length / SecretConsumeRate)); + Xxh3Accumulate(acc, input[(nbBlocks * blockLen)..], secret, nbStripes); + + ReadOnlySpan p = input[^StripeLen..]; + Xxh3Accumulate512(acc, p, secret[(secret.Length - StripeLen - SecretLastAccStart)..]); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static ulong Xxh3Mix2Accs(Span acc, ReadOnlySpan secret) + { + return Mul128Fold64( + acc[0] ^ BinaryPrimitives.ReadUInt64LittleEndian(secret), + acc[1] ^ BinaryPrimitives.ReadUInt64LittleEndian(secret[8..])); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static ulong Xxh3MergeAccs(Span acc, ReadOnlySpan secret, ulong start) + { + ulong result64 = start; + + for (int i = 0; i < 4; i++) + { + result64 += Xxh3Mix2Accs(acc[(2 * i)..], secret[(16 * i)..]); + } + + return Xxh3Avalanche(result64); + } + + [SkipLocalsInit] + private static Hash128 Xxh3HashLong128bInternal(ReadOnlySpan input, ReadOnlySpan secret) + { + Span acc = stackalloc ulong[AccNb]; + _xxh3InitAcc.CopyTo(acc); + + Xxh3HashLongInternalLoop(acc, input, secret); + + Debug.Assert(acc.Length == 8); + Debug.Assert(secret.Length >= acc.Length * sizeof(ulong) + SecretMergeAccsStart); + + return new Hash128 + { + Low = Xxh3MergeAccs(acc, secret[SecretMergeAccsStart..], (ulong)input.Length * Prime64_1), + High = Xxh3MergeAccs( + acc, + secret[(secret.Length - acc.Length * sizeof(ulong) - SecretMergeAccsStart)..], + ~((ulong)input.Length * Prime64_2)), + }; + } + + private static Hash128 Xxh3Len1To3128b(ReadOnlySpan input, ReadOnlySpan secret, ulong seed) + { + Debug.Assert(1 <= input.Length && input.Length <= 3); + + byte c1 = input[0]; + byte c2 = input[input.Length >> 1]; + byte c3 = input[^1]; + + uint combinedL = ((uint)c1 << 16) | ((uint)c2 << 24) | c3 | ((uint)input.Length << 8); + uint combinedH = BitOperations.RotateLeft(BinaryPrimitives.ReverseEndianness(combinedL), 13); + ulong bitFlipL = (BinaryPrimitives.ReadUInt32LittleEndian(secret) ^ BinaryPrimitives.ReadUInt32LittleEndian(secret[4..])) + seed; + ulong bitFlipH = (BinaryPrimitives.ReadUInt32LittleEndian(secret[8..]) ^ BinaryPrimitives.ReadUInt32LittleEndian(secret[12..])) - seed; + ulong keyedLo = combinedL ^ bitFlipL; + ulong keyedHi = combinedH ^ bitFlipH; + + return new Hash128 + { + Low = Xxh64Avalanche(keyedLo), + High = Xxh64Avalanche(keyedHi), + }; + } + + private static Hash128 Xxh3Len4To8128b(ReadOnlySpan input, ReadOnlySpan secret, ulong seed) + { + Debug.Assert(4 <= input.Length && input.Length <= 8); + + seed ^= BinaryPrimitives.ReverseEndianness((uint)seed) << 32; + + uint inputLo = BinaryPrimitives.ReadUInt32LittleEndian(input); + uint inputHi = BinaryPrimitives.ReadUInt32LittleEndian(input[^4..]); + ulong input64 = inputLo + ((ulong)inputHi << 32); + ulong bitFlip = (BinaryPrimitives.ReadUInt64LittleEndian(secret[16..]) ^ BinaryPrimitives.ReadUInt64LittleEndian(secret[24..])) + seed; + ulong keyed = input64 ^ bitFlip; + + Hash128 m128 = Mult64To128(keyed, Prime64_1 + ((ulong)input.Length << 2)); + + m128.High += m128.Low << 1; + m128.Low ^= m128.High >> 3; + + m128.Low = XorShift64(m128.Low, 35); + m128.Low *= 0x9FB21C651E98DF25UL; + m128.Low = XorShift64(m128.Low, 28); + m128.High = Xxh3Avalanche(m128.High); + + return m128; + } + + private static Hash128 Xxh3Len9To16128b(ReadOnlySpan input, ReadOnlySpan secret, ulong seed) + { + Debug.Assert(9 <= input.Length && input.Length <= 16); + + ulong bitFlipL = (BinaryPrimitives.ReadUInt64LittleEndian(secret[32..]) ^ BinaryPrimitives.ReadUInt64LittleEndian(secret[40..])) - seed; + ulong bitFlipH = (BinaryPrimitives.ReadUInt64LittleEndian(secret[48..]) ^ BinaryPrimitives.ReadUInt64LittleEndian(secret[56..])) + seed; + ulong inputLo = BinaryPrimitives.ReadUInt64LittleEndian(input); + ulong inputHi = BinaryPrimitives.ReadUInt64LittleEndian(input[^8..]); + + Hash128 m128 = Mult64To128(inputLo ^ inputHi ^ bitFlipL, Prime64_1); + m128.Low += ((ulong)input.Length - 1) << 54; + inputHi ^= bitFlipH; + m128.High += inputHi + Mult32To64((uint)inputHi, Prime32_2 - 1); + m128.Low ^= BinaryPrimitives.ReverseEndianness(m128.High); + + Hash128 h128 = Mult64To128(m128.Low, Prime64_2); + h128.High += m128.High * Prime64_2; + h128.Low = Xxh3Avalanche(h128.Low); + h128.High = Xxh3Avalanche(h128.High); + + return h128; + } + + private static Hash128 Xxh3Len0To16128b(ReadOnlySpan input, ReadOnlySpan secret, ulong seed) + { + Debug.Assert(input.Length <= 16); + + if (input.Length > 8) + { + return Xxh3Len9To16128b(input, secret, seed); + } + + if (input.Length >= 4) + { + return Xxh3Len4To8128b(input, secret, seed); + } + + if (input.Length != 0) + { + return Xxh3Len1To3128b(input, secret, seed); + } + + Hash128 h128 = new(); + ulong bitFlipL = BinaryPrimitives.ReadUInt64LittleEndian(secret[64..]) ^ BinaryPrimitives.ReadUInt64LittleEndian(secret[72..]); + ulong bitFlipH = BinaryPrimitives.ReadUInt64LittleEndian(secret[80..]) ^ BinaryPrimitives.ReadUInt64LittleEndian(secret[88..]); + h128.Low = Xxh64Avalanche(seed ^ bitFlipL); + h128.High = Xxh64Avalanche(seed ^ bitFlipH); + + return h128; + } + + private static ulong Xxh3Mix16b(ReadOnlySpan input, ReadOnlySpan secret, ulong seed) + { + ulong inputLo = BinaryPrimitives.ReadUInt64LittleEndian(input); + ulong inputHi = BinaryPrimitives.ReadUInt64LittleEndian(input[8..]); + + return Mul128Fold64( + inputLo ^ (BinaryPrimitives.ReadUInt64LittleEndian(secret) + seed), + inputHi ^ (BinaryPrimitives.ReadUInt64LittleEndian(secret[8..]) - seed)); + } + + private static Hash128 Xxh128Mix32b(Hash128 acc, ReadOnlySpan input, ReadOnlySpan input2, ReadOnlySpan secret, ulong seed) + { + acc.Low += Xxh3Mix16b(input, secret, seed); + acc.Low ^= BinaryPrimitives.ReadUInt64LittleEndian(input2) + BinaryPrimitives.ReadUInt64LittleEndian(input2[8..]); + acc.High += Xxh3Mix16b(input2, secret[16..], seed); + acc.High ^= BinaryPrimitives.ReadUInt64LittleEndian(input) + BinaryPrimitives.ReadUInt64LittleEndian(input[8..]); + + return acc; + } + + private static Hash128 Xxh3Len17To128128b(ReadOnlySpan input, ReadOnlySpan secret, ulong seed) + { + Debug.Assert(secret.Length >= SecretSizeMin); + Debug.Assert(16 < input.Length && input.Length <= 128); + + Hash128 acc = new() + { + Low = (ulong)input.Length * Prime64_1, + High = 0, + }; + + if (input.Length > 32) + { + if (input.Length > 64) + { + if (input.Length > 96) + { + acc = Xxh128Mix32b(acc, input[48..], input[^64..], secret[96..], seed); + } + acc = Xxh128Mix32b(acc, input[32..], input[^48..], secret[64..], seed); + } + acc = Xxh128Mix32b(acc, input[16..], input[^32..], secret[32..], seed); + } + acc = Xxh128Mix32b(acc, input, input[^16..], secret, seed); + + Hash128 h128 = new() + { + Low = acc.Low + acc.High, + High = acc.Low * Prime64_1 + acc.High * Prime64_4 + ((ulong)input.Length - seed) * Prime64_2, + }; + h128.Low = Xxh3Avalanche(h128.Low); + h128.High = 0UL - Xxh3Avalanche(h128.High); + + return h128; + } + + private static Hash128 Xxh3Len129To240128b(ReadOnlySpan input, ReadOnlySpan secret, ulong seed) + { + Debug.Assert(secret.Length >= SecretSizeMin); + Debug.Assert(128 < input.Length && input.Length <= 240); + + Hash128 acc = new(); + + int nbRounds = input.Length / 32; + acc.Low = (ulong)input.Length * Prime64_1; + acc.High = 0; + + for (int i = 0; i < 4; i++) + { + acc = Xxh128Mix32b(acc, input[(32 * i)..], input[(32 * i + 16)..], secret[(32 * i)..], seed); + } + + acc.Low = Xxh3Avalanche(acc.Low); + acc.High = Xxh3Avalanche(acc.High); + Debug.Assert(nbRounds >= 4); + + for (int i = 4; i < nbRounds; i++) + { + acc = Xxh128Mix32b(acc, input[(32 * i)..], input[(32 * i + 16)..], secret[(MidSizeStartOffset + 32 * (i - 4))..], seed); + } + + acc = Xxh128Mix32b(acc, input[^16..], input[^32..], secret[(SecretSizeMin - MidSizeLastOffset - 16)..], 0UL - seed); + + Hash128 h128 = new() + { + Low = acc.Low + acc.High, + High = acc.Low * Prime64_1 + acc.High * Prime64_4 + ((ulong)input.Length - seed) * Prime64_2, + }; + h128.Low = Xxh3Avalanche(h128.Low); + h128.High = 0UL - Xxh3Avalanche(h128.High); + + return h128; + } + + private static Hash128 Xxh3128bitsInternal(ReadOnlySpan input, ReadOnlySpan secret, ulong seed) + { + Debug.Assert(secret.Length >= SecretSizeMin); + + return input.Length switch + { + <= 16 => Xxh3Len0To16128b(input, secret, seed), + <= 128 => Xxh3Len17To128128b(input, secret, seed), + <= 240 => Xxh3Len129To240128b(input, secret, seed), + _ => Xxh3HashLong128bInternal(input, secret) + }; + } + + #endregion } } diff --git a/src/Ryujinx.Common/Logging/LogClass.cs b/src/Ryujinx.Common/Logging/LogClass.cs index f277dd06d..a4117580e 100644 --- a/src/Ryujinx.Common/Logging/LogClass.cs +++ b/src/Ryujinx.Common/Logging/LogClass.cs @@ -70,7 +70,8 @@ namespace Ryujinx.Common.Logging ServiceVi, SurfaceFlinger, TamperMachine, - Ui, + UI, Vic, + XCIFileTrimmer } } diff --git a/src/Ryujinx.Common/Logging/Logger.cs b/src/Ryujinx.Common/Logging/Logger.cs index f03a7fd8f..26d343969 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) @@ -34,7 +38,7 @@ namespace Ryujinx.Common.Logging { if (_enabledClasses[(int)logClass]) { - Updated?.Invoke(null, new LogEventArgs(Level, _time.Elapsed, Thread.CurrentThread.Name, FormatMessage(logClass, "", message))); + Updated?.Invoke(null, new LogEventArgs(Level, _time.Elapsed, Thread.CurrentThread.Name, FormatMessage(logClass, string.Empty, message))); } } @@ -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; } @@ -203,9 +212,7 @@ namespace Ryujinx.Common.Logging foreach (var log in logs) { if (log.HasValue) - { levels.Add(log.Value.Level); - } } return levels; @@ -224,7 +231,8 @@ namespace Ryujinx.Common.Logging case LogLevel.AccessLog : AccessLog = enabled ? new Log(LogLevel.AccessLog) : new Log?(); break; case LogLevel.Stub : Stub = enabled ? new Log(LogLevel.Stub) : new Log?(); break; case LogLevel.Trace : Trace = enabled ? new Log(LogLevel.Trace) : new Log?(); break; - default: throw new ArgumentException("Unknown Log Level"); + case LogLevel.Notice : break; + default: throw new ArgumentException("Unknown Log Level", nameof(logLevel)); #pragma warning restore IDE0055 } } diff --git a/src/Ryujinx.Common/Logging/Targets/AsyncLogTargetWrapper.cs b/src/Ryujinx.Common/Logging/Targets/AsyncLogTargetWrapper.cs index 02c6dc97b..a9dbe646a 100644 --- a/src/Ryujinx.Common/Logging/Targets/AsyncLogTargetWrapper.cs +++ b/src/Ryujinx.Common/Logging/Targets/AsyncLogTargetWrapper.cs @@ -30,10 +30,10 @@ namespace Ryujinx.Common.Logging.Targets string ILogTarget.Name { get => _target.Name; } public AsyncLogTargetWrapper(ILogTarget target) - : this(target, -1, AsyncLogTargetOverflowAction.Block) + : this(target, -1) { } - public AsyncLogTargetWrapper(ILogTarget target, int queueLimit, AsyncLogTargetOverflowAction overflowAction) + public AsyncLogTargetWrapper(ILogTarget target, int queueLimit = -1, AsyncLogTargetOverflowAction overflowAction = AsyncLogTargetOverflowAction.Block) { _target = target; _messageQueue = new BlockingCollection(queueLimit); diff --git a/src/Ryujinx.Common/Logging/Targets/ConsoleLogTarget.cs b/src/Ryujinx.Common/Logging/Targets/ConsoleLogTarget.cs index effc8f507..686a63a49 100644 --- a/src/Ryujinx.Common/Logging/Targets/ConsoleLogTarget.cs +++ b/src/Ryujinx.Common/Logging/Targets/ConsoleLogTarget.cs @@ -30,26 +30,13 @@ namespace Ryujinx.Common.Logging.Targets public void Log(object sender, LogEventArgs args) { - if (OperatingSystem.IsIOS()) - { - Console.WriteLine(_formatter.Format(args)); - } - else - { - Console.ForegroundColor = GetLogColor(args.Level); - Console.WriteLine(_formatter.Format(args)); - Console.ResetColor(); - } + Console.WriteLine(_formatter.Format(args)); } public void Dispose() { GC.SuppressFinalize(this); - - if (!OperatingSystem.IsIOS()) - { - Console.ResetColor(); - } + Console.ResetColor(); } } } diff --git a/src/Ryujinx.Common/Logging/Targets/FileLogTarget.cs b/src/Ryujinx.Common/Logging/Targets/FileLogTarget.cs index 8aa2a26b9..94e9359c8 100644 --- a/src/Ryujinx.Common/Logging/Targets/FileLogTarget.cs +++ b/src/Ryujinx.Common/Logging/Targets/FileLogTarget.cs @@ -13,31 +13,83 @@ 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(); - - // 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++) + DirectoryInfo logDir; + try { - files[i].Delete(); + logDir = new DirectoryInfo(path); + } + catch (ArgumentException exception) + { + Logger.Warning?.Print(LogClass.Application, $"Logging directory path ('{path}') was invalid: {exception}"); + + return null; } - string version = ReleaseInformation.GetVersion(); + 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++) + { + 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.Version; + string appName = ReleaseInformation.IsCanaryBuild ? "Ryujinx_Canary" : "Ryujinx"; // Get path for the current time - path = Path.Combine(logDir.FullName, $"Ryujinx_{version}_{DateTime.Now:yyyy-MM-dd_HH-mm-ss}.log"); + path = Path.Combine(logDir.FullName, $"{appName}_{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/Logging/XCIFileTrimmerLog.cs b/src/Ryujinx.Common/Logging/XCIFileTrimmerLog.cs new file mode 100644 index 000000000..fb11432b0 --- /dev/null +++ b/src/Ryujinx.Common/Logging/XCIFileTrimmerLog.cs @@ -0,0 +1,30 @@ +using Ryujinx.Common.Utilities; + +namespace Ryujinx.Common.Logging +{ + public class XCIFileTrimmerLog : XCIFileTrimmer.ILog + { + public virtual void Progress(long current, long total, string text, bool complete) + { + } + + public void Write(XCIFileTrimmer.LogType logType, string text) + { + switch (logType) + { + case XCIFileTrimmer.LogType.Info: + Logger.Notice.Print(LogClass.XCIFileTrimmer, text); + break; + case XCIFileTrimmer.LogType.Warn: + Logger.Warning?.Print(LogClass.XCIFileTrimmer, text); + break; + case XCIFileTrimmer.LogType.Error: + Logger.Error?.Print(LogClass.XCIFileTrimmer, text); + break; + case XCIFileTrimmer.LogType.Progress: + Logger.Info?.Print(LogClass.XCIFileTrimmer, text); + break; + } + } + } +} diff --git a/src/Ryujinx.Common/Memory/ArrayPtr.cs b/src/Ryujinx.Common/Memory/ArrayPtr.cs index 7487a1ff5..a54bdb3f6 100644 --- a/src/Ryujinx.Common/Memory/ArrayPtr.cs +++ b/src/Ryujinx.Common/Memory/ArrayPtr.cs @@ -11,17 +11,17 @@ namespace Ryujinx.Common.Memory /// Array element type public unsafe struct ArrayPtr : IEquatable>, IArray where T : unmanaged { - private IntPtr _ptr; + private nint _ptr; /// /// Null pointer. /// - public static ArrayPtr Null => new() { _ptr = IntPtr.Zero }; + public static ArrayPtr Null => new() { _ptr = nint.Zero }; /// /// True if the pointer is null, false otherwise. /// - public readonly bool IsNull => _ptr == IntPtr.Zero; + public readonly bool IsNull => _ptr == nint.Zero; /// /// Number of elements on the array. @@ -50,7 +50,7 @@ namespace Ryujinx.Common.Memory /// Number of elements on the array public ArrayPtr(ref T value, int length) { - _ptr = (IntPtr)Unsafe.AsPointer(ref value); + _ptr = (nint)Unsafe.AsPointer(ref value); Length = length; } @@ -61,7 +61,7 @@ namespace Ryujinx.Common.Memory /// Number of elements on the array public ArrayPtr(T* ptr, int length) { - _ptr = (IntPtr)ptr; + _ptr = (nint)ptr; Length = length; } @@ -70,7 +70,7 @@ namespace Ryujinx.Common.Memory /// /// Array base pointer /// Number of elements on the array - public ArrayPtr(IntPtr ptr, int length) + public ArrayPtr(nint ptr, int length) { _ptr = ptr; Length = length; 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; + } + + /// + /// Creates a new instance with the content copied from the specified buffer. + /// + /// The buffer to copy + /// A instance with the same length and content as + public static MemoryOwner RentCopy(ReadOnlySpan buffer) + { + MemoryOwner result = new(buffer.Length); + + buffer.CopyTo(result._array); + + return result; + } + + /// + /// Gets the number of items in the current instance. + /// + public int Length + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _length; + } + + /// + public Memory Memory + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + T[]? array = _array; + + if (array is null) + { + ThrowObjectDisposedException(); + } + + return new(array, 0, _length); + } + } + + /// + /// Gets a wrapping the memory belonging to the current instance. + /// + /// + /// Uses a trick made possible by the .NET 6+ runtime array layout. + /// + public Span Span + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + T[]? array = _array; + + if (array is null) + { + ThrowObjectDisposedException(); + } + + ref T firstElementRef = ref MemoryMarshal.GetArrayDataReference(array); + + return MemoryMarshal.CreateSpan(ref firstElementRef, _length); + } + } + + /// + public void Dispose() + { + T[]? array = Interlocked.Exchange(ref _array, null); + + if (array is not null) + { + ArrayPool.Shared.Return(array, RuntimeHelpers.IsReferenceOrContainsReferences()); + } + } + + /// + /// Throws an when is . + /// + [DoesNotReturn] + private static void ThrowObjectDisposedException() + { + throw new ObjectDisposedException(nameof(MemoryOwner), "The buffer has already been disposed."); + } + } +} diff --git a/src/Ryujinx.Common/Memory/PartialUnmaps/PartialUnmapState.cs b/src/Ryujinx.Common/Memory/PartialUnmaps/PartialUnmapState.cs index 93fef5c3b..60fdd7af6 100644 --- a/src/Ryujinx.Common/Memory/PartialUnmaps/PartialUnmapState.cs +++ b/src/Ryujinx.Common/Memory/PartialUnmaps/PartialUnmapState.cs @@ -21,7 +21,7 @@ namespace Ryujinx.Common.Memory.PartialUnmaps public readonly static int PartialUnmapsCountOffset; public readonly static int LocalCountsOffset; - public readonly static IntPtr GlobalState; + public readonly static nint GlobalState; [SupportedOSPlatform("windows")] [LibraryImport("kernel32.dll")] @@ -29,17 +29,17 @@ namespace Ryujinx.Common.Memory.PartialUnmaps [SupportedOSPlatform("windows")] [LibraryImport("kernel32.dll", SetLastError = true)] - private static partial IntPtr OpenThread(int dwDesiredAccess, [MarshalAs(UnmanagedType.Bool)] bool bInheritHandle, uint dwThreadId); + private static partial nint OpenThread(int dwDesiredAccess, [MarshalAs(UnmanagedType.Bool)] bool bInheritHandle, uint dwThreadId); [SupportedOSPlatform("windows")] [LibraryImport("kernel32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] - private static partial bool CloseHandle(IntPtr hObject); + private static partial bool CloseHandle(nint hObject); [SupportedOSPlatform("windows")] [LibraryImport("kernel32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] - private static partial bool GetExitCodeThread(IntPtr hThread, out uint lpExitCode); + private static partial bool GetExitCodeThread(nint hThread, out uint lpExitCode); /// /// Creates a global static PartialUnmapState and populates the field offsets. @@ -137,9 +137,9 @@ namespace Ryujinx.Common.Memory.PartialUnmaps if (id != 0) { - IntPtr handle = OpenThread(ThreadQueryInformation, false, (uint)id); + nint handle = OpenThread(ThreadQueryInformation, false, (uint)id); - if (handle == IntPtr.Zero) + if (handle == nint.Zero) { Interlocked.CompareExchange(ref ids[i], 0, id); } diff --git a/src/Ryujinx.Common/Memory/Ptr.cs b/src/Ryujinx.Common/Memory/Ptr.cs index d01748c16..3a8c1e1b6 100644 --- a/src/Ryujinx.Common/Memory/Ptr.cs +++ b/src/Ryujinx.Common/Memory/Ptr.cs @@ -10,17 +10,17 @@ namespace Ryujinx.Common.Memory /// Type of the unmanaged resource public unsafe struct Ptr : IEquatable> where T : unmanaged { - private IntPtr _ptr; + private nint _ptr; /// /// Null pointer. /// - public static Ptr Null => new() { _ptr = IntPtr.Zero }; + public static Ptr Null => new() { _ptr = nint.Zero }; /// /// True if the pointer is null, false otherwise. /// - public readonly bool IsNull => _ptr == IntPtr.Zero; + public readonly bool IsNull => _ptr == nint.Zero; /// /// Gets a reference to the value. @@ -37,7 +37,7 @@ namespace Ryujinx.Common.Memory /// Reference to the unmanaged resource public Ptr(ref T value) { - _ptr = (IntPtr)Unsafe.AsPointer(ref value); + _ptr = (nint)Unsafe.AsPointer(ref value); } public readonly override bool Equals(object obj) diff --git a/src/Ryujinx.Common/Memory/SpanOrArray.cs b/src/Ryujinx.Common/Memory/SpanOrArray.cs deleted file mode 100644 index 269ac02fd..000000000 --- a/src/Ryujinx.Common/Memory/SpanOrArray.cs +++ /dev/null @@ -1,89 +0,0 @@ -using System; - -namespace Ryujinx.Common.Memory -{ - /// - /// A struct that can represent both a Span and Array. - /// This is useful to keep the Array representation when possible to avoid copies. - /// - /// Element Type - public readonly ref struct SpanOrArray where T : unmanaged - { - public readonly T[] Array; - public readonly ReadOnlySpan Span; - - /// - /// Create a new SpanOrArray from an array. - /// - /// Array to store - public SpanOrArray(T[] array) - { - Array = array; - Span = ReadOnlySpan.Empty; - } - - /// - /// Create a new SpanOrArray from a readonly span. - /// - /// Span to store - public SpanOrArray(ReadOnlySpan span) - { - Array = null; - Span = span; - } - - /// - /// Return the contained array, or convert the span if necessary. - /// - /// An array containing the data - public T[] ToArray() - { - return Array ?? Span.ToArray(); - } - - /// - /// Return a ReadOnlySpan from either the array or ReadOnlySpan. - /// - /// A ReadOnlySpan containing the data - public ReadOnlySpan AsSpan() - { - return Array ?? Span; - } - - /// - /// Cast an array to a SpanOrArray. - /// - /// Source array - public static implicit operator SpanOrArray(T[] array) - { - return new SpanOrArray(array); - } - - /// - /// Cast a ReadOnlySpan to a SpanOrArray. - /// - /// Source ReadOnlySpan - public static implicit operator SpanOrArray(ReadOnlySpan span) - { - return new SpanOrArray(span); - } - - /// - /// Cast a Span to a SpanOrArray. - /// - /// Source Span - public static implicit operator SpanOrArray(Span span) - { - return new SpanOrArray(span); - } - - /// - /// Cast a SpanOrArray to a ReadOnlySpan - /// - /// Source SpanOrArray - public static implicit operator ReadOnlySpan(SpanOrArray spanOrArray) - { - return spanOrArray.AsSpan(); - } - } -} diff --git a/src/Ryujinx.Common/Memory/SpanOwner.cs b/src/Ryujinx.Common/Memory/SpanOwner.cs new file mode 100644 index 000000000..acb20bcad --- /dev/null +++ b/src/Ryujinx.Common/Memory/SpanOwner.cs @@ -0,0 +1,114 @@ +using System; +using System.Buffers; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace Ryujinx.Common.Memory +{ + /// + /// A stack-only type that rents a buffer of a specified length from . + /// It does not implement to avoid being boxed, but should still be disposed. This + /// is easy since C# 8, which allows use of C# `using` constructs on any type that has a public Dispose() method. + /// To keep this type simple, fast, and read-only, it does not check or guard against multiple disposals. + /// For all these reasons, all usage should be with a `using` block or statement. + /// + /// The type of item to store. + public readonly ref struct SpanOwner + { + private readonly int _length; + private readonly T[] _array; + + /// + /// Initializes a new instance of the struct with the specified parameters. + /// + /// The length of the new memory buffer to use + private SpanOwner(int length) + { + _length = length; + _array = ArrayPool.Shared.Rent(length); + } + + /// + /// Gets an empty instance. + /// + public static SpanOwner Empty + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => new(0); + } + + /// + /// Creates a new instance with the specified length. + /// + /// The length of the new memory buffer to use + /// A instance of the requested length + /// Thrown when is not valid + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static SpanOwner Rent(int length) => new(length); + + /// + /// Creates a new instance with the length and the content cleared. + /// + /// The length of the new memory buffer to use + /// A instance of the requested length and the content cleared + /// Thrown when is not valid + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static SpanOwner RentCleared(int length) + { + SpanOwner result = new(length); + + result._array.AsSpan(0, length).Clear(); + + return result; + } + + /// + /// Creates a new instance with the content copied from the specified buffer. + /// + /// The buffer to copy + /// A instance with the same length and content as + public static SpanOwner RentCopy(ReadOnlySpan buffer) + { + SpanOwner result = new(buffer.Length); + + buffer.CopyTo(result._array); + + return result; + } + + /// + /// Gets the number of items in the current instance + /// + public int Length + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _length; + } + + /// + /// Gets a wrapping the memory belonging to the current instance. + /// + /// + /// Uses a trick made possible by the .NET 6+ runtime array layout. + /// + public Span Span + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + ref T firstElementRef = ref MemoryMarshal.GetArrayDataReference(_array); + + return MemoryMarshal.CreateSpan(ref firstElementRef, _length); + } + } + + /// + /// Implements the duck-typed method. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Dispose() + { + ArrayPool.Shared.Return(_array, RuntimeHelpers.IsReferenceOrContainsReferences()); + } + } +} diff --git a/src/Ryujinx.Common/Memory/StructArrayHelpers.cs b/src/Ryujinx.Common/Memory/StructArrayHelpers.cs index 94ce8d2f5..fcb2229a7 100644 --- a/src/Ryujinx.Common/Memory/StructArrayHelpers.cs +++ b/src/Ryujinx.Common/Memory/StructArrayHelpers.cs @@ -744,6 +744,17 @@ namespace Ryujinx.Common.Memory public Span AsSpan() => MemoryMarshal.CreateSpan(ref _e0, Length); } + public struct Array65 : IArray where T : unmanaged + { + T _e0; + Array64 _other; + public readonly int Length => 65; + public ref T this[int index] => ref AsSpan()[index]; + + [Pure] + public Span AsSpan() => MemoryMarshal.CreateSpan(ref _e0, Length); + } + public struct Array73 : IArray where T : unmanaged { T _e0; @@ -792,18 +803,6 @@ namespace Ryujinx.Common.Memory public Span AsSpan() => MemoryMarshal.CreateSpan(ref _e0, Length); } - public struct Array256 : IArray where T : unmanaged - { - T _e0; - Array128 _other; - Array127 _other2; - public readonly int Length => 256; - public ref T this[int index] => ref AsSpan()[index]; - - [Pure] - public Span AsSpan() => MemoryMarshal.CreateSpan(ref _e0, Length); - } - public struct Array140 : IArray where T : unmanaged { T _e0; @@ -817,6 +816,18 @@ namespace Ryujinx.Common.Memory public Span AsSpan() => MemoryMarshal.CreateSpan(ref _e0, Length); } + public struct Array256 : IArray where T : unmanaged + { + T _e0; + Array128 _other; + Array127 _other2; + public readonly int Length => 256; + public ref T this[int index] => ref AsSpan()[index]; + + [Pure] + public Span AsSpan() => MemoryMarshal.CreateSpan(ref _e0, Length); + } + public struct Array384 : IArray where T : unmanaged { T _e0; diff --git a/src/Ryujinx.Common/Memory/StructByteArrayHelpers.cs b/src/Ryujinx.Common/Memory/StructByteArrayHelpers.cs index 3b0666628..79b7d681a 100644 --- a/src/Ryujinx.Common/Memory/StructByteArrayHelpers.cs +++ b/src/Ryujinx.Common/Memory/StructByteArrayHelpers.cs @@ -63,6 +63,18 @@ namespace Ryujinx.Common.Memory public Span AsSpan() => MemoryMarshal.CreateSpan(ref _element, Size); } + [StructLayout(LayoutKind.Sequential, Size = Size, Pack = 1)] + public struct ByteArray3000 : IArray + { + private const int Size = 3000; + + byte _element; + + public readonly int Length => Size; + public ref byte this[int index] => ref AsSpan()[index]; + public Span AsSpan() => MemoryMarshal.CreateSpan(ref _element, Size); + } + [StructLayout(LayoutKind.Sequential, Size = Size, Pack = 1)] public struct ByteArray4096 : IArray { diff --git a/src/Ryujinx.Common/Pools/ObjectPool.cs b/src/Ryujinx.Common/Pools/ObjectPool.cs index 0b6ce3771..c4610a59c 100644 --- a/src/Ryujinx.Common/Pools/ObjectPool.cs +++ b/src/Ryujinx.Common/Pools/ObjectPool.cs @@ -3,19 +3,11 @@ using System.Threading; namespace Ryujinx.Common { - public class ObjectPool + public class ObjectPool(Func factory, int size) where T : class { private T _firstItem; - private readonly T[] _items; - - private readonly Func _factory; - - public ObjectPool(Func factory, int size) - { - _items = new T[size - 1]; - _factory = factory; - } + private readonly T[] _items = new T[size - 1]; public T Allocate() { @@ -43,7 +35,7 @@ namespace Ryujinx.Common } } - return _factory(); + return factory(); } public void Release(T obj) diff --git a/src/Ryujinx.Common/ReactiveObject.cs b/src/Ryujinx.Common/ReactiveObject.cs index 4831edb52..8df1e20fe 100644 --- a/src/Ryujinx.Common/ReactiveObject.cs +++ b/src/Ryujinx.Common/ReactiveObject.cs @@ -1,11 +1,13 @@ +using Ryujinx.Common.Logging; using System; +using System.Globalization; using System.Threading; namespace Ryujinx.Common { public class ReactiveObject { - private readonly ReaderWriterLockSlim _readerWriterLock = new(); + private readonly ReaderWriterLockSlim _rwLock = new(); private bool _isInitialized; private T _value; @@ -15,15 +17,15 @@ namespace Ryujinx.Common { get { - _readerWriterLock.EnterReadLock(); + _rwLock.EnterReadLock(); T value = _value; - _readerWriterLock.ExitReadLock(); + _rwLock.ExitReadLock(); return value; } set { - _readerWriterLock.EnterWriteLock(); + _rwLock.EnterWriteLock(); T oldValue = _value; @@ -32,7 +34,7 @@ namespace Ryujinx.Common _isInitialized = true; _value = value; - _readerWriterLock.ExitWriteLock(); + _rwLock.ExitWriteLock(); if (!oldIsInitialized || oldValue == null || !oldValue.Equals(_value)) { @@ -40,22 +42,28 @@ namespace Ryujinx.Common } } } + + public void LogChangesToValue(string valueName, LogClass logClass = LogClass.Configuration) + => Event += (_, e) => ReactiveObjectHelper.LogValueChange(logClass, e, valueName); - public static implicit operator T(ReactiveObject obj) - { - return obj.Value; - } + public static implicit operator T(ReactiveObject obj) => obj.Value; } - public class ReactiveEventArgs + public static class ReactiveObjectHelper { - public T OldValue { get; } - public T NewValue { get; } - - public ReactiveEventArgs(T oldValue, T newValue) + public static void LogValueChange(LogClass logClass, ReactiveEventArgs eventArgs, string valueName) { - OldValue = oldValue; - NewValue = newValue; + string message = string.Create(CultureInfo.InvariantCulture, $"{valueName} set to: {eventArgs.NewValue}"); + + Logger.Info?.Print(logClass, message); } + + public static void Toggle(this ReactiveObject rBoolean) => rBoolean.Value = !rBoolean.Value; + } + + public class ReactiveEventArgs(T oldValue, T newValue) + { + public T OldValue { get; } = oldValue; + public T NewValue { get; } = newValue; } } diff --git a/src/Ryujinx.Common/ReleaseInformation.cs b/src/Ryujinx.Common/ReleaseInformation.cs index bf68cbbc8..011d9848a 100644 --- a/src/Ryujinx.Common/ReleaseInformation.cs +++ b/src/Ryujinx.Common/ReleaseInformation.cs @@ -1,4 +1,3 @@ -using Ryujinx.Common.Configuration; using System; using System.Reflection; @@ -7,57 +6,43 @@ namespace Ryujinx.Common // DO NOT EDIT, filled by CI public static class ReleaseInformation { - private const string FlatHubChannelOwner = "flathub"; + private const string FlatHubChannel = "flathub"; + private const string CanaryChannel = "canary"; + private const string ReleaseChannel = "release"; - public const string BuildVersion = "%%RYUJINX_BUILD_VERSION%%"; + private const string BuildVersion = "%%RYUJINX_BUILD_VERSION%%"; public const string BuildGitHash = "%%RYUJINX_BUILD_GIT_HASH%%"; - public const string ReleaseChannelName = "%%RYUJINX_TARGET_RELEASE_CHANNEL_NAME%%"; + private const string ReleaseChannelName = "%%RYUJINX_TARGET_RELEASE_CHANNEL_NAME%%"; + private const string ConfigFileName = "%%RYUJINX_CONFIG_FILE_NAME%%"; + public const string ReleaseChannelOwner = "%%RYUJINX_TARGET_RELEASE_CHANNEL_OWNER%%"; + public const string ReleaseChannelSourceRepo = "%%RYUJINX_TARGET_RELEASE_CHANNEL_SOURCE_REPO%%"; public const string ReleaseChannelRepo = "%%RYUJINX_TARGET_RELEASE_CHANNEL_REPO%%"; - public static bool IsValid() - { - return !BuildGitHash.StartsWith("%%") && - !ReleaseChannelName.StartsWith("%%") && - !ReleaseChannelOwner.StartsWith("%%") && - !ReleaseChannelRepo.StartsWith("%%"); - } + public static string ConfigName => !ConfigFileName.StartsWith("%%") ? ConfigFileName : "Config.json"; - public static bool IsFlatHubBuild() - { - return IsValid() && ReleaseChannelOwner.Equals(FlatHubChannelOwner); - } + public static bool IsValid => + !BuildGitHash.StartsWith("%%") && + !ReleaseChannelName.StartsWith("%%") && + !ReleaseChannelOwner.StartsWith("%%") && + !ReleaseChannelSourceRepo.StartsWith("%%") && + !ReleaseChannelRepo.StartsWith("%%") && + !ConfigFileName.StartsWith("%%"); - public static string GetVersion() - { - if (OperatingSystem.IsIOS()) - { - return "ios"; - } + public static bool IsFlatHubBuild => IsValid && ReleaseChannelOwner.Equals(FlatHubChannel); - if (IsValid()) - { - return BuildVersion; - } + public static bool IsCanaryBuild => IsValid && ReleaseChannelName.Equals(CanaryChannel); + + public static bool IsReleaseBuild => IsValid && ReleaseChannelName.Equals(ReleaseChannel); - return Assembly.GetEntryAssembly().GetCustomAttribute().InformationalVersion; - } + public static string Version => IsValid ? BuildVersion : Assembly.GetEntryAssembly()!.GetCustomAttribute()?.InformationalVersion; -#if FORCE_EXTERNAL_BASE_DIR - public static string GetBaseApplicationDirectory() - { - return AppDataManager.BaseDirPath; - } -#else - public static string GetBaseApplicationDirectory() - { - if (IsFlatHubBuild() || OperatingSystem.IsMacOS() || OperatingSystem.IsIOS()) - { - return AppDataManager.BaseDirPath; - } - - return AppDomain.CurrentDomain.BaseDirectory; - } -#endif + public static string GetChangelogUrl(Version currentVersion, Version newVersion) => + IsCanaryBuild + ? $"https://github.com/{ReleaseChannelOwner}/{ReleaseChannelSourceRepo}/compare/Canary-{currentVersion}...Canary-{newVersion}" + : $"https://github.com/{ReleaseChannelOwner}/{ReleaseChannelSourceRepo}/releases/tag/{newVersion}"; + + public static string GetChangelogForVersion(Version version) => + $"https://github.com/{ReleaseChannelOwner}/{ReleaseChannelRepo}/releases/tag/{version}"; } } diff --git a/src/Ryujinx.Common/Ryujinx.Common.csproj b/src/Ryujinx.Common/Ryujinx.Common.csproj index da2f13a21..dee462fdb 100644 --- a/src/Ryujinx.Common/Ryujinx.Common.csproj +++ b/src/Ryujinx.Common/Ryujinx.Common.csproj @@ -10,6 +10,8 @@ + + diff --git a/src/Ryujinx.Common/SystemInterop/ForceDpiAware.cs b/src/Ryujinx.Common/SystemInterop/ForceDpiAware.cs index b8e1df7d2..330638171 100644 --- a/src/Ryujinx.Common/SystemInterop/ForceDpiAware.cs +++ b/src/Ryujinx.Common/SystemInterop/ForceDpiAware.cs @@ -14,19 +14,19 @@ namespace Ryujinx.Common.SystemInterop private const string X11LibraryName = "libX11.so.6"; [LibraryImport(X11LibraryName)] - private static partial IntPtr XOpenDisplay([MarshalAs(UnmanagedType.LPStr)] string display); + private static partial nint XOpenDisplay([MarshalAs(UnmanagedType.LPStr)] string display); [LibraryImport(X11LibraryName)] - private static partial IntPtr XGetDefault(IntPtr display, [MarshalAs(UnmanagedType.LPStr)] string program, [MarshalAs(UnmanagedType.LPStr)] string option); + private static partial nint XGetDefault(nint display, [MarshalAs(UnmanagedType.LPStr)] string program, [MarshalAs(UnmanagedType.LPStr)] string option); [LibraryImport(X11LibraryName)] - private static partial int XDisplayWidth(IntPtr display, int screenNumber); + private static partial int XDisplayWidth(nint display, int screenNumber); [LibraryImport(X11LibraryName)] - private static partial int XDisplayWidthMM(IntPtr display, int screenNumber); + private static partial int XDisplayWidthMM(nint display, int screenNumber); [LibraryImport(X11LibraryName)] - private static partial int XCloseDisplay(IntPtr display); + private static partial int XCloseDisplay(nint display); private const double StandardDpiScale = 96.0; private const double MaxScaleFactor = 1.25; @@ -51,7 +51,7 @@ namespace Ryujinx.Common.SystemInterop { if (OperatingSystem.IsWindows()) { - userDpiScale = GdiPlusHelper.GetDpiX(IntPtr.Zero); + userDpiScale = GdiPlusHelper.GetDpiX(nint.Zero); } else if (OperatingSystem.IsLinux()) { @@ -59,7 +59,7 @@ namespace Ryujinx.Common.SystemInterop if (xdgSessionType == null || xdgSessionType == "x11") { - IntPtr display = XOpenDisplay(null); + nint display = XOpenDisplay(null); string dpiString = Marshal.PtrToStringAnsi(XGetDefault(display, "Xft", "dpi")); if (dpiString == null || !double.TryParse(dpiString, NumberStyles.Any, CultureInfo.InvariantCulture, out userDpiScale)) { diff --git a/src/Ryujinx.Common/SystemInterop/GdiPlusHelper.cs b/src/Ryujinx.Common/SystemInterop/GdiPlusHelper.cs index 7e8e9f2a5..c00598c98 100644 --- a/src/Ryujinx.Common/SystemInterop/GdiPlusHelper.cs +++ b/src/Ryujinx.Common/SystemInterop/GdiPlusHelper.cs @@ -9,7 +9,7 @@ namespace Ryujinx.Common.SystemInterop { private const string LibraryName = "gdiplus.dll"; - private static readonly IntPtr _initToken; + private static readonly nint _initToken; static GdiPlusHelper() { @@ -29,7 +29,7 @@ namespace Ryujinx.Common.SystemInterop public int GdiplusVersion; #pragma warning disable CS0649 // Field is never assigned to - public IntPtr DebugEventCallback; + public nint DebugEventCallback; public int SuppressBackgroundThread; public int SuppressExternalCodecs; public int StartupParameters; @@ -39,7 +39,7 @@ namespace Ryujinx.Common.SystemInterop { // We assume Windows 8 and upper GdiplusVersion = 2, - DebugEventCallback = IntPtr.Zero, + DebugEventCallback = nint.Zero, SuppressBackgroundThread = 0, SuppressExternalCodecs = 0, StartupParameters = 0, @@ -48,25 +48,25 @@ namespace Ryujinx.Common.SystemInterop private struct StartupOutput { - public IntPtr NotificationHook; - public IntPtr NotificationUnhook; + public nint NotificationHook; + public nint NotificationUnhook; } [LibraryImport(LibraryName)] - private static partial int GdiplusStartup(out IntPtr token, in StartupInputEx input, out StartupOutput output); + private static partial int GdiplusStartup(out nint token, in StartupInputEx input, out StartupOutput output); [LibraryImport(LibraryName)] - private static partial int GdipCreateFromHWND(IntPtr hwnd, out IntPtr graphics); + private static partial int GdipCreateFromHWND(nint hwnd, out nint graphics); [LibraryImport(LibraryName)] - private static partial int GdipDeleteGraphics(IntPtr graphics); + private static partial int GdipDeleteGraphics(nint graphics); [LibraryImport(LibraryName)] - private static partial int GdipGetDpiX(IntPtr graphics, out float dpi); + private static partial int GdipGetDpiX(nint graphics, out float dpi); - public static float GetDpiX(IntPtr hwnd) + public static float GetDpiX(nint hwnd) { - CheckStatus(GdipCreateFromHWND(hwnd, out IntPtr graphicsHandle)); + CheckStatus(GdipCreateFromHWND(hwnd, out nint graphicsHandle)); CheckStatus(GdipGetDpiX(graphicsHandle, out float result)); CheckStatus(GdipDeleteGraphics(graphicsHandle)); diff --git a/src/Ryujinx.Common/SystemInterop/StdErrAdapter.cs b/src/Ryujinx.Common/SystemInterop/StdErrAdapter.cs index 1cf7a1928..a04c404d8 100644 --- a/src/Ryujinx.Common/SystemInterop/StdErrAdapter.cs +++ b/src/Ryujinx.Common/SystemInterop/StdErrAdapter.cs @@ -19,7 +19,7 @@ namespace Ryujinx.Common.SystemInterop public StdErrAdapter() { - if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS()) // TODO: iOS? + if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS()) { RegisterPosix(); } @@ -27,7 +27,6 @@ namespace Ryujinx.Common.SystemInterop [SupportedOSPlatform("linux")] [SupportedOSPlatform("macos")] - [SupportedOSPlatform("ios")] private void RegisterPosix() { const int StdErrFileno = 2; @@ -45,7 +44,6 @@ namespace Ryujinx.Common.SystemInterop [SupportedOSPlatform("linux")] [SupportedOSPlatform("macos")] - [SupportedOSPlatform("ios")] private async Task EventWorkerAsync(CancellationToken cancellationToken) { using TextReader reader = new StreamReader(_pipeReader, leaveOpen: true); @@ -94,7 +92,6 @@ namespace Ryujinx.Common.SystemInterop [SupportedOSPlatform("linux")] [SupportedOSPlatform("macos")] - [SupportedOSPlatform("ios")] private static Stream CreateFileDescriptorStream(int fd) { return new FileStream( diff --git a/src/Ryujinx.Common/Utilities/BitUtils.cs b/src/Ryujinx.Common/Utilities/BitUtils.cs index 0bf9c8acd..b9dae2e53 100644 --- a/src/Ryujinx.Common/Utilities/BitUtils.cs +++ b/src/Ryujinx.Common/Utilities/BitUtils.cs @@ -4,23 +4,18 @@ namespace Ryujinx.Common { public static class BitUtils { - public static T AlignUp(T value, T size) - where T : IBinaryInteger - { - return (value + (size - T.One)) & -size; - } + public static T AlignUp(T value, T size) where T : IBinaryInteger + => (value + (size - T.One)) & -size; - public static T AlignDown(T value, T size) - where T : IBinaryInteger - { - return value & -size; - } + public static T AlignDown(T value, T size) where T : IBinaryInteger + => value & -size; - public static T DivRoundUp(T value, T dividend) - where T : IBinaryInteger - { - return (value + (dividend - T.One)) / dividend; - } + public static T DivRoundUp(T value, T dividend) where T : IBinaryInteger + => (value + (dividend - T.One)) / dividend; + + public static int Pow2RoundDown(int value) => BitOperations.IsPow2(value) ? value : Pow2RoundUp(value) >> 1; + + public static long ReverseBits64(long value) => (long)ReverseBits64((ulong)value); public static int Pow2RoundUp(int value) { @@ -35,16 +30,6 @@ namespace Ryujinx.Common return ++value; } - public static int Pow2RoundDown(int value) - { - return BitOperations.IsPow2(value) ? value : Pow2RoundUp(value) >> 1; - } - - public static long ReverseBits64(long value) - { - return (long)ReverseBits64((ulong)value); - } - private static ulong ReverseBits64(ulong value) { value = ((value & 0xaaaaaaaaaaaaaaaa) >> 1) | ((value & 0x5555555555555555) << 1); diff --git a/src/Ryujinx.Common/Utilities/EmbeddedResources.cs b/src/Ryujinx.Common/Utilities/EmbeddedResources.cs index a4facc2e3..7530c012a 100644 --- a/src/Ryujinx.Common/Utilities/EmbeddedResources.cs +++ b/src/Ryujinx.Common/Utilities/EmbeddedResources.cs @@ -1,3 +1,4 @@ +using Ryujinx.Common.Memory; using Ryujinx.Common.Utilities; using System; using System.IO; @@ -41,6 +42,22 @@ namespace Ryujinx.Common return StreamUtils.StreamToBytes(stream); } + public static MemoryOwner ReadFileToRentedMemory(string filename) + { + var (assembly, path) = ResolveManifestPath(filename); + + return ReadFileToRentedMemory(assembly, path); + } + + public static MemoryOwner ReadFileToRentedMemory(Assembly assembly, string filename) + { + using var stream = GetStream(assembly, filename); + + return stream is null + ? null + : StreamUtils.StreamToRentedMemory(stream); + } + public async static Task ReadAsync(Assembly assembly, string filename) { using var stream = GetStream(assembly, filename); diff --git a/src/Ryujinx.Common/Utilities/FileSystemUtils.cs b/src/Ryujinx.Common/Utilities/FileSystemUtils.cs new file mode 100644 index 000000000..a57fa8a78 --- /dev/null +++ b/src/Ryujinx.Common/Utilities/FileSystemUtils.cs @@ -0,0 +1,56 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace Ryujinx.Common.Utilities +{ + public static class FileSystemUtils + { + public static void CopyDirectory(string sourceDir, string destinationDir, bool recursive) + { + // Get information about the source directory + var dir = new DirectoryInfo(sourceDir); + + // Check if the source directory exists + if (!dir.Exists) + { + throw new DirectoryNotFoundException($"Source directory not found: {dir.FullName}"); + } + + // Cache directories before we start copying + DirectoryInfo[] dirs = dir.GetDirectories(); + + // Create the destination directory + Directory.CreateDirectory(destinationDir); + + // Get the files in the source directory and copy to the destination directory + foreach (FileInfo file in dir.GetFiles()) + { + string targetFilePath = Path.Combine(destinationDir, file.Name); + file.CopyTo(targetFilePath); + } + + // If recursive and copying subdirectories, recursively call this method + if (recursive) + { + foreach (DirectoryInfo subDir in dirs) + { + string newDestinationDir = Path.Combine(destinationDir, subDir.Name); + CopyDirectory(subDir.FullName, newDestinationDir, true); + } + } + } + + public static void MoveDirectory(string sourceDir, string destinationDir) + { + CopyDirectory(sourceDir, destinationDir, true); + Directory.Delete(sourceDir, true); + } + + public static string SanitizeFileName(string fileName) + { + var reservedChars = new HashSet(Path.GetInvalidFileNameChars()); + return string.Concat(fileName.Select(c => reservedChars.Contains(c) ? '_' : c)); + } + } +} diff --git a/src/Ryujinx.Common/Utilities/JsonHelper.cs b/src/Ryujinx.Common/Utilities/JsonHelper.cs index 95daec27a..276dd5f8c 100644 --- a/src/Ryujinx.Common/Utilities/JsonHelper.cs +++ b/src/Ryujinx.Common/Utilities/JsonHelper.cs @@ -17,29 +17,19 @@ namespace Ryujinx.Common.Utilities /// It is REQUIRED for you to save returned options statically or as a part of static serializer context /// in order to avoid performance issues. You can safely modify returned options for your case before storing. /// - public static JsonSerializerOptions GetDefaultSerializerOptions(bool indented = true) - { - JsonSerializerOptions options = new() + public static JsonSerializerOptions GetDefaultSerializerOptions(bool indented = true) => + new() { DictionaryKeyPolicy = _snakeCasePolicy, PropertyNamingPolicy = _snakeCasePolicy, WriteIndented = indented, AllowTrailingCommas = true, - ReadCommentHandling = JsonCommentHandling.Skip, + ReadCommentHandling = JsonCommentHandling.Skip }; - return options; - } + public static string Serialize(T value, JsonTypeInfo typeInfo) => JsonSerializer.Serialize(value, typeInfo); - public static string Serialize(T value, JsonTypeInfo typeInfo) - { - return JsonSerializer.Serialize(value, typeInfo); - } - - public static T Deserialize(string value, JsonTypeInfo typeInfo) - { - return JsonSerializer.Deserialize(value, typeInfo); - } + public static T Deserialize(string value, JsonTypeInfo typeInfo) => JsonSerializer.Deserialize(value, typeInfo); public static void SerializeToFile(string filePath, T value, JsonTypeInfo typeInfo) { @@ -53,10 +43,7 @@ namespace Ryujinx.Common.Utilities return JsonSerializer.Deserialize(file, typeInfo); } - public static void SerializeToStream(Stream stream, T value, JsonTypeInfo typeInfo) - { - JsonSerializer.Serialize(stream, value, typeInfo); - } + public static void SerializeToStream(Stream stream, T value, JsonTypeInfo typeInfo) => JsonSerializer.Serialize(stream, value, typeInfo); private class SnakeCaseNamingPolicy : JsonNamingPolicy { @@ -75,15 +62,10 @@ namespace Ryujinx.Common.Utilities if (char.IsUpper(c)) { - if (i == 0 || char.IsUpper(name[i - 1])) - { - builder.Append(char.ToLowerInvariant(c)); - } - else - { + if (!(i == 0 || char.IsUpper(name[i - 1]))) builder.Append('_'); - builder.Append(char.ToLowerInvariant(c)); - } + + builder.Append(char.ToLowerInvariant(c)); } else { diff --git a/src/Ryujinx.Common/Utilities/NetworkHelpers.cs b/src/Ryujinx.Common/Utilities/NetworkHelpers.cs index e42e30e4f..53d1e4f33 100644 --- a/src/Ryujinx.Common/Utilities/NetworkHelpers.cs +++ b/src/Ryujinx.Common/Utilities/NetworkHelpers.cs @@ -1,7 +1,7 @@ -using System; using System.Buffers.Binary; using System.Net; using System.Net.NetworkInformation; +using System.Runtime.InteropServices; namespace Ryujinx.Common.Utilities { @@ -11,13 +11,12 @@ namespace Ryujinx.Common.Utilities { IPInterfaceProperties properties = adapter.GetIPProperties(); - // Skip problematic checks on non-Windows and iOS platforms - if (isPreferred || OperatingSystem.IsWindows() || properties.UnicastAddresses.Count > 0) + if (isPreferred || (properties.GatewayAddresses.Count > 0 && properties.DnsAddresses.Count > 0)) { foreach (UnicastIPAddressInformation info in properties.UnicastAddresses) { // Only accept an IPv4 address - if (info.Address.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork) + if (info.Address.GetAddressBytes().Length == 4) { return (properties, info); } @@ -46,9 +45,8 @@ namespace Ryujinx.Common.Utilities { bool isPreferred = adapter.Id == guid; - // Ignore loopback and ensure the adapter supports IPv4 - if (isPreferred || - (targetProperties == null && adapter.NetworkInterfaceType != NetworkInterfaceType.Loopback && adapter.Supports(NetworkInterfaceComponent.IPv4))) + // Ignore loopback and non IPv4 capable interface. + if (isPreferred || (targetProperties == null && adapter.NetworkInterfaceType != NetworkInterfaceType.Loopback && adapter.Supports(NetworkInterfaceComponent.IPv4))) { (IPInterfaceProperties properties, UnicastIPAddressInformation info) = GetLocalInterface(adapter, isPreferred); @@ -68,6 +66,11 @@ namespace Ryujinx.Common.Utilities return (targetProperties, targetAddressInfo); } + public static bool SupportsDynamicDns() + { + return RuntimeInformation.IsOSPlatform(OSPlatform.Windows); + } + public static uint ConvertIpv4Address(IPAddress ipAddress) { return BinaryPrimitives.ReadUInt32BigEndian(ipAddress.GetAddressBytes()); @@ -80,13 +83,7 @@ namespace Ryujinx.Common.Utilities public static IPAddress ConvertUint(uint ipAddress) { - return new IPAddress(new byte[] - { - (byte)((ipAddress >> 24) & 0xFF), - (byte)((ipAddress >> 16) & 0xFF), - (byte)((ipAddress >> 8) & 0xFF), - (byte)(ipAddress & 0xFF) - }); + return new IPAddress(new byte[] { (byte)((ipAddress >> 24) & 0xFF), (byte)((ipAddress >> 16) & 0xFF), (byte)((ipAddress >> 8) & 0xFF), (byte)(ipAddress & 0xFF) }); } } } diff --git a/src/Ryujinx.Common/Utilities/OsUtils.cs b/src/Ryujinx.Common/Utilities/OsUtils.cs new file mode 100644 index 000000000..a0791b092 --- /dev/null +++ b/src/Ryujinx.Common/Utilities/OsUtils.cs @@ -0,0 +1,24 @@ +using System; +using System.Diagnostics; +using System.Runtime.InteropServices; + +namespace Ryujinx.Common.Utilities +{ + public partial class OsUtils + { + [LibraryImport("libc", SetLastError = true)] + private static partial int setenv([MarshalAs(UnmanagedType.LPStr)] string name, [MarshalAs(UnmanagedType.LPStr)] string value, int overwrite); + + public static void SetEnvironmentVariableNoCaching(string key, string value) + { + // Set the value in the cached environment variables, too. + Environment.SetEnvironmentVariable(key, value); + + if (!OperatingSystem.IsWindows()) + { + int res = setenv(key, value, 1); + Debug.Assert(res != -1); + } + } + } +} diff --git a/src/Ryujinx.Common/Utilities/StreamUtils.cs b/src/Ryujinx.Common/Utilities/StreamUtils.cs index 7a20c98e9..aeb6e0d52 100644 --- a/src/Ryujinx.Common/Utilities/StreamUtils.cs +++ b/src/Ryujinx.Common/Utilities/StreamUtils.cs @@ -1,3 +1,4 @@ +using Microsoft.IO; using Ryujinx.Common.Memory; using System.IO; using System.Threading; @@ -9,12 +10,50 @@ namespace Ryujinx.Common.Utilities { public static byte[] StreamToBytes(Stream input) { - using MemoryStream stream = MemoryStreamManager.Shared.GetStream(); + using RecyclableMemoryStream output = StreamToRecyclableMemoryStream(input); + return output.ToArray(); + } - input.CopyTo(stream); + public static MemoryOwner StreamToRentedMemory(Stream input) + { + if (input is MemoryStream inputMemoryStream) + { + return MemoryStreamToRentedMemory(inputMemoryStream); + } + else if (input.CanSeek) + { + long bytesExpected = input.Length; - return stream.ToArray(); + MemoryOwner ownedMemory = MemoryOwner.Rent(checked((int)bytesExpected)); + + var destSpan = ownedMemory.Span; + + int totalBytesRead = 0; + + while (totalBytesRead < bytesExpected) + { + int bytesRead = input.Read(destSpan[totalBytesRead..]); + + if (bytesRead == 0) + { + ownedMemory.Dispose(); + + throw new IOException($"Tried reading {bytesExpected} but the stream closed after reading {totalBytesRead}."); + } + + totalBytesRead += bytesRead; + } + + return ownedMemory; + } + else + { + // If input is (non-seekable) then copy twice: first into a RecyclableMemoryStream, then to a rented IMemoryOwner. + using RecyclableMemoryStream output = StreamToRecyclableMemoryStream(input); + + return MemoryStreamToRentedMemory(output); + } } public static async Task StreamToBytesAsync(Stream input, CancellationToken cancellationToken = default) @@ -25,5 +64,26 @@ namespace Ryujinx.Common.Utilities return stream.ToArray(); } + + private static MemoryOwner MemoryStreamToRentedMemory(MemoryStream input) + { + input.Position = 0; + + MemoryOwner ownedMemory = MemoryOwner.Rent(checked((int)input.Length)); + + // Discard the return value because we assume reading a MemoryStream always succeeds completely. + _ = input.Read(ownedMemory.Span); + + return ownedMemory; + } + + private static RecyclableMemoryStream StreamToRecyclableMemoryStream(Stream input) + { + RecyclableMemoryStream stream = MemoryStreamManager.Shared.GetStream(); + + input.CopyTo(stream); + + return stream; + } } } diff --git a/src/Ryujinx.Common/Utilities/UInt128Utils.cs b/src/Ryujinx.Common/Utilities/UInt128Utils.cs index 113855355..23afaa3a1 100644 --- a/src/Ryujinx.Common/Utilities/UInt128Utils.cs +++ b/src/Ryujinx.Common/Utilities/UInt128Utils.cs @@ -5,14 +5,16 @@ namespace Ryujinx.Common.Utilities { public static class UInt128Utils { - public static UInt128 FromHex(string hex) - { - return new UInt128(ulong.Parse(hex.AsSpan(0, 16), NumberStyles.HexNumber), ulong.Parse(hex.AsSpan(16), NumberStyles.HexNumber)); - } + public static UInt128 FromHex(string hex) => + new( + ulong.Parse(hex.AsSpan(0, 16), NumberStyles.HexNumber), + ulong.Parse(hex.AsSpan(16), NumberStyles.HexNumber) + ); - public static UInt128 CreateRandom() - { - return new UInt128((ulong)Random.Shared.NextInt64(), (ulong)Random.Shared.NextInt64()); - } + public static Int128 NextInt128(this Random rand) => + new((ulong)rand.NextInt64(), (ulong)rand.NextInt64()); + + public static UInt128 NextUInt128(this Random rand) => + new((ulong)rand.NextInt64(), (ulong)rand.NextInt64()); } } diff --git a/src/Ryujinx.Common/Utilities/XCIFileTrimmer.cs b/src/Ryujinx.Common/Utilities/XCIFileTrimmer.cs new file mode 100644 index 000000000..050e78d1e --- /dev/null +++ b/src/Ryujinx.Common/Utilities/XCIFileTrimmer.cs @@ -0,0 +1,524 @@ +// Uncomment the line below to ensure XCIFileTrimmer does not modify files +//#define XCI_TRIMMER_READ_ONLY_MODE + +using Gommon; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Threading; + +namespace Ryujinx.Common.Utilities +{ + public sealed class XCIFileTrimmer + { + private const long BytesInAMegabyte = 1024 * 1024; + private const int BufferSize = 8 * (int)BytesInAMegabyte; + + private const long CartSizeMBinFormattedGB = 952; + private const int CartKeyAreaSize = 0x1000; + private const byte PaddingByte = 0xFF; + private const int HeaderFilePos = 0x100; + private const int CartSizeFilePos = 0x10D; + private const int DataSizeFilePos = 0x118; + private const string HeaderMagicValue = "HEAD"; + + /// + /// Cartridge Sizes (ByteIdentifier, SizeInGB) + /// + private static readonly Dictionary _cartSizesGB = new() + { + { 0xFA, 1 }, + { 0xF8, 2 }, + { 0xF0, 4 }, + { 0xE0, 8 }, + { 0xE1, 16 }, + { 0xE2, 32 } + }; + + private static long RecordsToByte(long records) + { + return 512 + (records * 512); + } + + public static bool CanTrim(string filename, ILog log = null) + { + if (Path.GetExtension(filename).Equals(".XCI", StringComparison.InvariantCultureIgnoreCase)) + { + var trimmer = new XCIFileTrimmer(filename, log); + return trimmer.CanBeTrimmed; + } + + return false; + } + + public static bool CanUntrim(string filename, ILog log = null) + { + if (Path.GetExtension(filename).Equals(".XCI", StringComparison.InvariantCultureIgnoreCase)) + { + var trimmer = new XCIFileTrimmer(filename, log); + return trimmer.CanBeUntrimmed; + } + + return false; + } + + private ILog _log; + private string _filename; + private FileStream _fileStream; + private BinaryReader _binaryReader; + private long _offsetB, _dataSizeB, _cartSizeB, _fileSizeB; + private bool _fileOK = true; + private bool _freeSpaceChecked = false; + private bool _freeSpaceValid = false; + + public enum OperationOutcome + { + Undetermined, + InvalidXCIFile, + NoTrimNecessary, + NoUntrimPossible, + FreeSpaceCheckFailed, + FileIOWriteError, + ReadOnlyFileCannotFix, + FileSizeChanged, + Successful, + Cancelled + } + + public enum LogType + { + Info, + Warn, + Error, + Progress + } + + public interface ILog + { + public void Write(LogType logType, string text); + public void Progress(long current, long total, string text, bool complete); + } + + public bool FileOK => _fileOK; + public bool Trimmed => _fileOK && FileSizeB < UntrimmedFileSizeB; + public bool ContainsKeyArea => _offsetB != 0; + public bool CanBeTrimmed => _fileOK && FileSizeB > TrimmedFileSizeB; + public bool CanBeUntrimmed => _fileOK && FileSizeB < UntrimmedFileSizeB; + public bool FreeSpaceChecked => _fileOK && _freeSpaceChecked; + public bool FreeSpaceValid => _fileOK && _freeSpaceValid; + public long DataSizeB => _dataSizeB; + public long CartSizeB => _cartSizeB; + public long FileSizeB => _fileSizeB; + public long DiskSpaceSavedB => CartSizeB - FileSizeB; + public long DiskSpaceSavingsB => CartSizeB - DataSizeB; + public long TrimmedFileSizeB => _offsetB + _dataSizeB; + public long UntrimmedFileSizeB => _offsetB + _cartSizeB; + + public ILog Log + { + get => _log; + set => _log = value; + } + + public String Filename + { + get => _filename; + set + { + _filename = value; + Reset(); + } + } + + public long Pos + { + get => _fileStream.Position; + set => _fileStream.Position = value; + } + + public XCIFileTrimmer(string path, ILog log = null) + { + Log = log; + Filename = path; + ReadHeader(); + } + + public void CheckFreeSpace(CancellationToken? cancelToken = null) + { + if (FreeSpaceChecked) + return; + + try + { + if (CanBeTrimmed) + { + _freeSpaceValid = false; + + OpenReaders(); + + try + { + Pos = TrimmedFileSizeB; + bool freeSpaceValid = true; + long readSizeB = FileSizeB - TrimmedFileSizeB; + + Stopwatch timedSw = Lambda.Timed(() => + { + freeSpaceValid = CheckPadding(readSizeB, cancelToken); + }); + + if (timedSw.Elapsed.TotalSeconds > 0) + { + Log?.Write(LogType.Info, $"Checked at {readSizeB / (double)XCIFileTrimmer.BytesInAMegabyte / timedSw.Elapsed.TotalSeconds:N} Mb/sec"); + } + + if (freeSpaceValid) + Log?.Write(LogType.Info, "Free space is valid"); + + _freeSpaceValid = freeSpaceValid; + } + finally + { + CloseReaders(); + } + + } + else + { + Log?.Write(LogType.Warn, "There is no free space to check."); + _freeSpaceValid = false; + } + } + finally + { + _freeSpaceChecked = true; + } + } + + private bool CheckPadding(long readSizeB, CancellationToken? cancelToken = null) + { + long maxReads = readSizeB / XCIFileTrimmer.BufferSize; + long read = 0; + var buffer = new byte[BufferSize]; + + while (true) + { + if (cancelToken.HasValue && cancelToken.Value.IsCancellationRequested) + { + return false; + } + + int bytes = _fileStream.Read(buffer, 0, XCIFileTrimmer.BufferSize); + if (bytes == 0) + break; + + Log?.Progress(read, maxReads, "Verifying file can be trimmed", false); + if (buffer.Take(bytes).AsParallel().Any(b => b != XCIFileTrimmer.PaddingByte)) + { + Log?.Write(LogType.Warn, "Free space is NOT valid"); + return false; + } + + read++; + } + + return true; + } + + private void Reset() + { + _freeSpaceChecked = false; + _freeSpaceValid = false; + ReadHeader(); + } + + public OperationOutcome Trim(CancellationToken? cancelToken = null) + { + if (!FileOK) + { + return OperationOutcome.InvalidXCIFile; + } + + if (!CanBeTrimmed) + { + return OperationOutcome.NoTrimNecessary; + } + + if (!FreeSpaceChecked) + { + CheckFreeSpace(cancelToken); + } + + if (!FreeSpaceValid) + { + if (cancelToken.HasValue && cancelToken.Value.IsCancellationRequested) + { + return OperationOutcome.Cancelled; + } + else + { + return OperationOutcome.FreeSpaceCheckFailed; + } + } + + Log?.Write(LogType.Info, "Trimming..."); + + try + { + var info = new FileInfo(Filename); + if ((info.Attributes & FileAttributes.ReadOnly) == FileAttributes.ReadOnly) + { + try + { + Log?.Write(LogType.Info, "Attempting to remove ReadOnly attribute"); + File.SetAttributes(Filename, info.Attributes & ~FileAttributes.ReadOnly); + } + catch (Exception e) + { + Log?.Write(LogType.Error, e.ToString()); + return OperationOutcome.ReadOnlyFileCannotFix; + } + } + + if (info.Length != FileSizeB) + { + Log?.Write(LogType.Error, "File size has changed, cannot safely trim."); + return OperationOutcome.FileSizeChanged; + } + + var outfileStream = new FileStream(_filename, FileMode.Open, FileAccess.Write, FileShare.Write); + + try + { + +#if !XCI_TRIMMER_READ_ONLY_MODE + outfileStream.SetLength(TrimmedFileSizeB); +#endif + return OperationOutcome.Successful; + } + finally + { + outfileStream.Close(); + Reset(); + } + } + catch (Exception e) + { + Log?.Write(LogType.Error, e.ToString()); + return OperationOutcome.FileIOWriteError; + } + } + + public OperationOutcome Untrim(CancellationToken? cancelToken = null) + { + if (!FileOK) + { + return OperationOutcome.InvalidXCIFile; + } + + if (!CanBeUntrimmed) + { + return OperationOutcome.NoUntrimPossible; + } + + try + { + Log?.Write(LogType.Info, "Untrimming..."); + + var info = new FileInfo(Filename); + if ((info.Attributes & FileAttributes.ReadOnly) == FileAttributes.ReadOnly) + { + try + { + Log?.Write(LogType.Info, "Attempting to remove ReadOnly attribute"); + File.SetAttributes(Filename, info.Attributes & ~FileAttributes.ReadOnly); + } + catch (Exception e) + { + Log?.Write(LogType.Error, e.ToString()); + return OperationOutcome.ReadOnlyFileCannotFix; + } + } + + if (info.Length != FileSizeB) + { + Log?.Write(LogType.Error, "File size has changed, cannot safely untrim."); + return OperationOutcome.FileSizeChanged; + } + + var outfileStream = new FileStream(_filename, FileMode.Append, FileAccess.Write, FileShare.Write); + long bytesToWriteB = UntrimmedFileSizeB - FileSizeB; + + try + { + Stopwatch timedSw = Lambda.Timed(() => + { + WritePadding(outfileStream, bytesToWriteB, cancelToken); + }); + + if (timedSw.Elapsed.TotalSeconds > 0) + { + Log?.Write(LogType.Info, $"Wrote at {bytesToWriteB / (double)XCIFileTrimmer.BytesInAMegabyte / timedSw.Elapsed.TotalSeconds:N} Mb/sec"); + } + + if (cancelToken.HasValue && cancelToken.Value.IsCancellationRequested) + { + return OperationOutcome.Cancelled; + } + else + { + return OperationOutcome.Successful; + } + } + finally + { + outfileStream.Close(); + Reset(); + } + } + catch (Exception e) + { + Log?.Write(LogType.Error, e.ToString()); + return OperationOutcome.FileIOWriteError; + } + } + + private void WritePadding(FileStream outfileStream, long bytesToWriteB, CancellationToken? cancelToken = null) + { + long bytesLeftToWriteB = bytesToWriteB; + long writes = bytesLeftToWriteB / XCIFileTrimmer.BufferSize; + int write = 0; + + try + { + var buffer = new byte[BufferSize]; + Array.Fill(buffer, XCIFileTrimmer.PaddingByte); + + while (bytesLeftToWriteB > 0) + { + if (cancelToken.HasValue && cancelToken.Value.IsCancellationRequested) + { + return; + } + + long bytesToWrite = Math.Min(XCIFileTrimmer.BufferSize, bytesLeftToWriteB); + +#if !XCI_TRIMMER_READ_ONLY_MODE + outfileStream.Write(buffer, 0, (int)bytesToWrite); +#endif + + bytesLeftToWriteB -= bytesToWrite; + Log?.Progress(write, writes, "Writing padding data...", false); + write++; + } + } + finally + { + Log?.Progress(write, writes, "Writing padding data...", true); + } + } + + private void OpenReaders() + { + if (_binaryReader == null) + { + _fileStream = new FileStream(_filename, FileMode.Open, FileAccess.Read, FileShare.Read); + _binaryReader = new BinaryReader(_fileStream); + } + } + + private void CloseReaders() + { + if (_binaryReader != null && _binaryReader.BaseStream != null) + _binaryReader.Close(); + _binaryReader = null; + _fileStream = null; + GC.Collect(); + } + + private void ReadHeader() + { + try + { + OpenReaders(); + + try + { + // Attempt without key area + bool success = CheckAndReadHeader(false); + + if (!success) + { + // Attempt with key area + success = CheckAndReadHeader(true); + } + + _fileOK = success; + } + finally + { + CloseReaders(); + } + } + catch (Exception ex) + { + Log?.Write(LogType.Error, ex.Message); + _fileOK = false; + _dataSizeB = 0; + _cartSizeB = 0; + _fileSizeB = 0; + _offsetB = 0; + } + } + + private bool CheckAndReadHeader(bool assumeKeyArea) + { + // Read file size + _fileSizeB = _fileStream.Length; + if (_fileSizeB < 32 * 1024) + { + Log?.Write(LogType.Error, "The source file doesn't look like an XCI file as the data size is too small"); + return false; + } + + // Setup offset + _offsetB = (long)(assumeKeyArea ? XCIFileTrimmer.CartKeyAreaSize : 0); + + // Check header + Pos = _offsetB + XCIFileTrimmer.HeaderFilePos; + string head = System.Text.Encoding.ASCII.GetString(_binaryReader.ReadBytes(4)); + if (head != XCIFileTrimmer.HeaderMagicValue) + { + if (!assumeKeyArea) + { + Log?.Write(LogType.Warn, $"Incorrect header found, file mat contain a key area..."); + } + else + { + Log?.Write(LogType.Error, "The source file doesn't look like an XCI file as the header is corrupted"); + } + + return false; + } + + // Read Cart Size + Pos = _offsetB + XCIFileTrimmer.CartSizeFilePos; + byte cartSizeId = _binaryReader.ReadByte(); + if (!_cartSizesGB.TryGetValue(cartSizeId, out long cartSizeNGB)) + { + Log?.Write(LogType.Error, $"The source file doesn't look like an XCI file as the Cartridge Size is incorrect (0x{cartSizeId:X2})"); + return false; + } + _cartSizeB = cartSizeNGB * XCIFileTrimmer.CartSizeMBinFormattedGB * XCIFileTrimmer.BytesInAMegabyte; + + // Read data size + Pos = _offsetB + XCIFileTrimmer.DataSizeFilePos; + long records = (long)BitConverter.ToUInt32(_binaryReader.ReadBytes(4), 0); + _dataSizeB = RecordsToByte(records); + + return true; + } + } +} diff --git a/src/Ryujinx.Common/XXHash128.cs b/src/Ryujinx.Common/XXHash128.cs deleted file mode 100644 index 686867c9d..000000000 --- a/src/Ryujinx.Common/XXHash128.cs +++ /dev/null @@ -1,548 +0,0 @@ -using System; -using System.Buffers.Binary; -using System.Diagnostics; -using System.Numerics; -using System.Runtime.CompilerServices; -using System.Runtime.Intrinsics; -using System.Runtime.Intrinsics.X86; - -namespace Ryujinx.Common -{ - public static class XXHash128 - { - private const int StripeLen = 64; - private const int AccNb = StripeLen / sizeof(ulong); - private const int SecretConsumeRate = 8; - private const int SecretLastAccStart = 7; - private const int SecretMergeAccsStart = 11; - private const int SecretSizeMin = 136; - private const int MidSizeStartOffset = 3; - private const int MidSizeLastOffset = 17; - - private const uint Prime32_1 = 0x9E3779B1U; - private const uint Prime32_2 = 0x85EBCA77U; - private const uint Prime32_3 = 0xC2B2AE3DU; - private const uint Prime32_4 = 0x27D4EB2FU; - private const uint Prime32_5 = 0x165667B1U; - - private const ulong Prime64_1 = 0x9E3779B185EBCA87UL; - private const ulong Prime64_2 = 0xC2B2AE3D27D4EB4FUL; - private const ulong Prime64_3 = 0x165667B19E3779F9UL; - private const ulong Prime64_4 = 0x85EBCA77C2B2AE63UL; - private const ulong Prime64_5 = 0x27D4EB2F165667C5UL; - - private static readonly ulong[] _xxh3InitAcc = { - Prime32_3, - Prime64_1, - Prime64_2, - Prime64_3, - Prime64_4, - Prime32_2, - Prime64_5, - Prime32_1, - }; - - private static ReadOnlySpan Xxh3KSecret => new byte[] - { - 0xb8, 0xfe, 0x6c, 0x39, 0x23, 0xa4, 0x4b, 0xbe, 0x7c, 0x01, 0x81, 0x2c, 0xf7, 0x21, 0xad, 0x1c, - 0xde, 0xd4, 0x6d, 0xe9, 0x83, 0x90, 0x97, 0xdb, 0x72, 0x40, 0xa4, 0xa4, 0xb7, 0xb3, 0x67, 0x1f, - 0xcb, 0x79, 0xe6, 0x4e, 0xcc, 0xc0, 0xe5, 0x78, 0x82, 0x5a, 0xd0, 0x7d, 0xcc, 0xff, 0x72, 0x21, - 0xb8, 0x08, 0x46, 0x74, 0xf7, 0x43, 0x24, 0x8e, 0xe0, 0x35, 0x90, 0xe6, 0x81, 0x3a, 0x26, 0x4c, - 0x3c, 0x28, 0x52, 0xbb, 0x91, 0xc3, 0x00, 0xcb, 0x88, 0xd0, 0x65, 0x8b, 0x1b, 0x53, 0x2e, 0xa3, - 0x71, 0x64, 0x48, 0x97, 0xa2, 0x0d, 0xf9, 0x4e, 0x38, 0x19, 0xef, 0x46, 0xa9, 0xde, 0xac, 0xd8, - 0xa8, 0xfa, 0x76, 0x3f, 0xe3, 0x9c, 0x34, 0x3f, 0xf9, 0xdc, 0xbb, 0xc7, 0xc7, 0x0b, 0x4f, 0x1d, - 0x8a, 0x51, 0xe0, 0x4b, 0xcd, 0xb4, 0x59, 0x31, 0xc8, 0x9f, 0x7e, 0xc9, 0xd9, 0x78, 0x73, 0x64, - 0xea, 0xc5, 0xac, 0x83, 0x34, 0xd3, 0xeb, 0xc3, 0xc5, 0x81, 0xa0, 0xff, 0xfa, 0x13, 0x63, 0xeb, - 0x17, 0x0d, 0xdd, 0x51, 0xb7, 0xf0, 0xda, 0x49, 0xd3, 0x16, 0x55, 0x26, 0x29, 0xd4, 0x68, 0x9e, - 0x2b, 0x16, 0xbe, 0x58, 0x7d, 0x47, 0xa1, 0xfc, 0x8f, 0xf8, 0xb8, 0xd1, 0x7a, 0xd0, 0x31, 0xce, - 0x45, 0xcb, 0x3a, 0x8f, 0x95, 0x16, 0x04, 0x28, 0xaf, 0xd7, 0xfb, 0xca, 0xbb, 0x4b, 0x40, 0x7e, - }; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static ulong Mult32To64(ulong x, ulong y) - { - return (uint)x * (ulong)(uint)y; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Hash128 Mult64To128(ulong lhs, ulong rhs) - { - ulong high = Math.BigMul(lhs, rhs, out ulong low); - - return new Hash128 - { - Low = low, - High = high, - }; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static ulong Mul128Fold64(ulong lhs, ulong rhs) - { - Hash128 product = Mult64To128(lhs, rhs); - - return product.Low ^ product.High; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static ulong XorShift64(ulong v64, int shift) - { - Debug.Assert(0 <= shift && shift < 64); - - return v64 ^ (v64 >> shift); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static ulong Xxh3Avalanche(ulong h64) - { - h64 = XorShift64(h64, 37); - h64 *= 0x165667919E3779F9UL; - h64 = XorShift64(h64, 32); - - return h64; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static ulong Xxh64Avalanche(ulong h64) - { - h64 ^= h64 >> 33; - h64 *= Prime64_2; - h64 ^= h64 >> 29; - h64 *= Prime64_3; - h64 ^= h64 >> 32; - - return h64; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private unsafe static void Xxh3Accumulate512(Span acc, ReadOnlySpan input, ReadOnlySpan secret) - { - if (Avx2.IsSupported) - { - fixed (ulong* pAcc = acc) - { - fixed (byte* pInput = input, pSecret = secret) - { - Vector256* xAcc = (Vector256*)pAcc; - Vector256* xInput = (Vector256*)pInput; - Vector256* xSecret = (Vector256*)pSecret; - - for (ulong i = 0; i < StripeLen / 32; i++) - { - Vector256 dataVec = xInput[i]; - Vector256 keyVec = xSecret[i]; - Vector256 dataKey = Avx2.Xor(dataVec, keyVec); - Vector256 dataKeyLo = Avx2.Shuffle(dataKey.AsUInt32(), 0b00110001); - Vector256 product = Avx2.Multiply(dataKey.AsUInt32(), dataKeyLo); - Vector256 dataSwap = Avx2.Shuffle(dataVec.AsUInt32(), 0b01001110); - Vector256 sum = Avx2.Add(xAcc[i], dataSwap.AsUInt64()); - xAcc[i] = Avx2.Add(product, sum); - } - } - } - } - else if (Sse2.IsSupported) - { - fixed (ulong* pAcc = acc) - { - fixed (byte* pInput = input, pSecret = secret) - { - Vector128* xAcc = (Vector128*)pAcc; - Vector128* xInput = (Vector128*)pInput; - Vector128* xSecret = (Vector128*)pSecret; - - for (ulong i = 0; i < StripeLen / 16; i++) - { - Vector128 dataVec = xInput[i]; - Vector128 keyVec = xSecret[i]; - Vector128 dataKey = Sse2.Xor(dataVec, keyVec); - Vector128 dataKeyLo = Sse2.Shuffle(dataKey.AsUInt32(), 0b00110001); - Vector128 product = Sse2.Multiply(dataKey.AsUInt32(), dataKeyLo); - Vector128 dataSwap = Sse2.Shuffle(dataVec.AsUInt32(), 0b01001110); - Vector128 sum = Sse2.Add(xAcc[i], dataSwap.AsUInt64()); - xAcc[i] = Sse2.Add(product, sum); - } - } - } - } - else - { - for (int i = 0; i < AccNb; i++) - { - ulong dataVal = BinaryPrimitives.ReadUInt64LittleEndian(input[(i * sizeof(ulong))..]); - ulong dataKey = dataVal ^ BinaryPrimitives.ReadUInt64LittleEndian(secret[(i * sizeof(ulong))..]); - acc[i ^ 1] += dataVal; - acc[i] += Mult32To64((uint)dataKey, dataKey >> 32); - } - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private unsafe static void Xxh3ScrambleAcc(Span acc, ReadOnlySpan secret) - { - if (Avx2.IsSupported) - { - fixed (ulong* pAcc = acc) - { - fixed (byte* pSecret = secret) - { - Vector256 prime32 = Vector256.Create(Prime32_1); - Vector256* xAcc = (Vector256*)pAcc; - Vector256* xSecret = (Vector256*)pSecret; - - for (ulong i = 0; i < StripeLen / 32; i++) - { - Vector256 accVec = xAcc[i]; - Vector256 shifted = Avx2.ShiftRightLogical(accVec, 47); - Vector256 dataVec = Avx2.Xor(accVec, shifted); - - Vector256 keyVec = xSecret[i]; - Vector256 dataKey = Avx2.Xor(dataVec.AsUInt32(), keyVec.AsUInt32()); - - Vector256 dataKeyHi = Avx2.Shuffle(dataKey.AsUInt32(), 0b00110001); - Vector256 prodLo = Avx2.Multiply(dataKey, prime32); - Vector256 prodHi = Avx2.Multiply(dataKeyHi, prime32); - - xAcc[i] = Avx2.Add(prodLo, Avx2.ShiftLeftLogical(prodHi, 32)); - } - } - } - } - else if (Sse2.IsSupported) - { - fixed (ulong* pAcc = acc) - { - fixed (byte* pSecret = secret) - { - Vector128 prime32 = Vector128.Create(Prime32_1); - Vector128* xAcc = (Vector128*)pAcc; - Vector128* xSecret = (Vector128*)pSecret; - - for (ulong i = 0; i < StripeLen / 16; i++) - { - Vector128 accVec = xAcc[i]; - Vector128 shifted = Sse2.ShiftRightLogical(accVec, 47); - Vector128 dataVec = Sse2.Xor(accVec, shifted); - - Vector128 keyVec = xSecret[i]; - Vector128 dataKey = Sse2.Xor(dataVec.AsUInt32(), keyVec.AsUInt32()); - - Vector128 dataKeyHi = Sse2.Shuffle(dataKey.AsUInt32(), 0b00110001); - Vector128 prodLo = Sse2.Multiply(dataKey, prime32); - Vector128 prodHi = Sse2.Multiply(dataKeyHi, prime32); - - xAcc[i] = Sse2.Add(prodLo, Sse2.ShiftLeftLogical(prodHi, 32)); - } - } - } - } - else - { - for (int i = 0; i < AccNb; i++) - { - ulong key64 = BinaryPrimitives.ReadUInt64LittleEndian(secret[(i * sizeof(ulong))..]); - ulong acc64 = acc[i]; - acc64 = XorShift64(acc64, 47); - acc64 ^= key64; - acc64 *= Prime32_1; - acc[i] = acc64; - } - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void Xxh3Accumulate(Span acc, ReadOnlySpan input, ReadOnlySpan secret, int nbStripes) - { - for (int n = 0; n < nbStripes; n++) - { - ReadOnlySpan inData = input[(n * StripeLen)..]; - Xxh3Accumulate512(acc, inData, secret[(n * SecretConsumeRate)..]); - } - } - - private static void Xxh3HashLongInternalLoop(Span acc, ReadOnlySpan input, ReadOnlySpan secret) - { - int nbStripesPerBlock = (secret.Length - StripeLen) / SecretConsumeRate; - int blockLen = StripeLen * nbStripesPerBlock; - int nbBlocks = (input.Length - 1) / blockLen; - - Debug.Assert(secret.Length >= SecretSizeMin); - - for (int n = 0; n < nbBlocks; n++) - { - Xxh3Accumulate(acc, input[(n * blockLen)..], secret, nbStripesPerBlock); - Xxh3ScrambleAcc(acc, secret[^StripeLen..]); - } - - Debug.Assert(input.Length > StripeLen); - - int nbStripes = (input.Length - 1 - (blockLen * nbBlocks)) / StripeLen; - Debug.Assert(nbStripes <= (secret.Length / SecretConsumeRate)); - Xxh3Accumulate(acc, input[(nbBlocks * blockLen)..], secret, nbStripes); - - ReadOnlySpan p = input[^StripeLen..]; - Xxh3Accumulate512(acc, p, secret[(secret.Length - StripeLen - SecretLastAccStart)..]); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static ulong Xxh3Mix2Accs(Span acc, ReadOnlySpan secret) - { - return Mul128Fold64( - acc[0] ^ BinaryPrimitives.ReadUInt64LittleEndian(secret), - acc[1] ^ BinaryPrimitives.ReadUInt64LittleEndian(secret[8..])); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static ulong Xxh3MergeAccs(Span acc, ReadOnlySpan secret, ulong start) - { - ulong result64 = start; - - for (int i = 0; i < 4; i++) - { - result64 += Xxh3Mix2Accs(acc[(2 * i)..], secret[(16 * i)..]); - } - - return Xxh3Avalanche(result64); - } - - [SkipLocalsInit] - private static Hash128 Xxh3HashLong128bInternal(ReadOnlySpan input, ReadOnlySpan secret) - { - Span acc = stackalloc ulong[AccNb]; - _xxh3InitAcc.CopyTo(acc); - - Xxh3HashLongInternalLoop(acc, input, secret); - - Debug.Assert(acc.Length == 8); - Debug.Assert(secret.Length >= acc.Length * sizeof(ulong) + SecretMergeAccsStart); - - return new Hash128 - { - Low = Xxh3MergeAccs(acc, secret[SecretMergeAccsStart..], (ulong)input.Length * Prime64_1), - High = Xxh3MergeAccs( - acc, - secret[(secret.Length - acc.Length * sizeof(ulong) - SecretMergeAccsStart)..], - ~((ulong)input.Length * Prime64_2)), - }; - } - - private static Hash128 Xxh3Len1To3128b(ReadOnlySpan input, ReadOnlySpan secret, ulong seed) - { - Debug.Assert(1 <= input.Length && input.Length <= 3); - - byte c1 = input[0]; - byte c2 = input[input.Length >> 1]; - byte c3 = input[^1]; - - uint combinedL = ((uint)c1 << 16) | ((uint)c2 << 24) | c3 | ((uint)input.Length << 8); - uint combinedH = BitOperations.RotateLeft(BinaryPrimitives.ReverseEndianness(combinedL), 13); - ulong bitFlipL = (BinaryPrimitives.ReadUInt32LittleEndian(secret) ^ BinaryPrimitives.ReadUInt32LittleEndian(secret[4..])) + seed; - ulong bitFlipH = (BinaryPrimitives.ReadUInt32LittleEndian(secret[8..]) ^ BinaryPrimitives.ReadUInt32LittleEndian(secret[12..])) - seed; - ulong keyedLo = combinedL ^ bitFlipL; - ulong keyedHi = combinedH ^ bitFlipH; - - return new Hash128 - { - Low = Xxh64Avalanche(keyedLo), - High = Xxh64Avalanche(keyedHi), - }; - } - - private static Hash128 Xxh3Len4To8128b(ReadOnlySpan input, ReadOnlySpan secret, ulong seed) - { - Debug.Assert(4 <= input.Length && input.Length <= 8); - - seed ^= BinaryPrimitives.ReverseEndianness((uint)seed) << 32; - - uint inputLo = BinaryPrimitives.ReadUInt32LittleEndian(input); - uint inputHi = BinaryPrimitives.ReadUInt32LittleEndian(input[^4..]); - ulong input64 = inputLo + ((ulong)inputHi << 32); - ulong bitFlip = (BinaryPrimitives.ReadUInt64LittleEndian(secret[16..]) ^ BinaryPrimitives.ReadUInt64LittleEndian(secret[24..])) + seed; - ulong keyed = input64 ^ bitFlip; - - Hash128 m128 = Mult64To128(keyed, Prime64_1 + ((ulong)input.Length << 2)); - - m128.High += m128.Low << 1; - m128.Low ^= m128.High >> 3; - - m128.Low = XorShift64(m128.Low, 35); - m128.Low *= 0x9FB21C651E98DF25UL; - m128.Low = XorShift64(m128.Low, 28); - m128.High = Xxh3Avalanche(m128.High); - - return m128; - } - - private static Hash128 Xxh3Len9To16128b(ReadOnlySpan input, ReadOnlySpan secret, ulong seed) - { - Debug.Assert(9 <= input.Length && input.Length <= 16); - - ulong bitFlipL = (BinaryPrimitives.ReadUInt64LittleEndian(secret[32..]) ^ BinaryPrimitives.ReadUInt64LittleEndian(secret[40..])) - seed; - ulong bitFlipH = (BinaryPrimitives.ReadUInt64LittleEndian(secret[48..]) ^ BinaryPrimitives.ReadUInt64LittleEndian(secret[56..])) + seed; - ulong inputLo = BinaryPrimitives.ReadUInt64LittleEndian(input); - ulong inputHi = BinaryPrimitives.ReadUInt64LittleEndian(input[^8..]); - - Hash128 m128 = Mult64To128(inputLo ^ inputHi ^ bitFlipL, Prime64_1); - m128.Low += ((ulong)input.Length - 1) << 54; - inputHi ^= bitFlipH; - m128.High += inputHi + Mult32To64((uint)inputHi, Prime32_2 - 1); - m128.Low ^= BinaryPrimitives.ReverseEndianness(m128.High); - - Hash128 h128 = Mult64To128(m128.Low, Prime64_2); - h128.High += m128.High * Prime64_2; - h128.Low = Xxh3Avalanche(h128.Low); - h128.High = Xxh3Avalanche(h128.High); - - return h128; - } - - private static Hash128 Xxh3Len0To16128b(ReadOnlySpan input, ReadOnlySpan secret, ulong seed) - { - Debug.Assert(input.Length <= 16); - - if (input.Length > 8) - { - return Xxh3Len9To16128b(input, secret, seed); - } - - if (input.Length >= 4) - { - return Xxh3Len4To8128b(input, secret, seed); - } - - if (input.Length != 0) - { - return Xxh3Len1To3128b(input, secret, seed); - } - - Hash128 h128 = new(); - ulong bitFlipL = BinaryPrimitives.ReadUInt64LittleEndian(secret[64..]) ^ BinaryPrimitives.ReadUInt64LittleEndian(secret[72..]); - ulong bitFlipH = BinaryPrimitives.ReadUInt64LittleEndian(secret[80..]) ^ BinaryPrimitives.ReadUInt64LittleEndian(secret[88..]); - h128.Low = Xxh64Avalanche(seed ^ bitFlipL); - h128.High = Xxh64Avalanche(seed ^ bitFlipH); - - return h128; - } - - private static ulong Xxh3Mix16b(ReadOnlySpan input, ReadOnlySpan secret, ulong seed) - { - ulong inputLo = BinaryPrimitives.ReadUInt64LittleEndian(input); - ulong inputHi = BinaryPrimitives.ReadUInt64LittleEndian(input[8..]); - - return Mul128Fold64( - inputLo ^ (BinaryPrimitives.ReadUInt64LittleEndian(secret) + seed), - inputHi ^ (BinaryPrimitives.ReadUInt64LittleEndian(secret[8..]) - seed)); - } - - private static Hash128 Xxh128Mix32b(Hash128 acc, ReadOnlySpan input, ReadOnlySpan input2, ReadOnlySpan secret, ulong seed) - { - acc.Low += Xxh3Mix16b(input, secret, seed); - acc.Low ^= BinaryPrimitives.ReadUInt64LittleEndian(input2) + BinaryPrimitives.ReadUInt64LittleEndian(input2[8..]); - acc.High += Xxh3Mix16b(input2, secret[16..], seed); - acc.High ^= BinaryPrimitives.ReadUInt64LittleEndian(input) + BinaryPrimitives.ReadUInt64LittleEndian(input[8..]); - - return acc; - } - - private static Hash128 Xxh3Len17To128128b(ReadOnlySpan input, ReadOnlySpan secret, ulong seed) - { - Debug.Assert(secret.Length >= SecretSizeMin); - Debug.Assert(16 < input.Length && input.Length <= 128); - - Hash128 acc = new() - { - Low = (ulong)input.Length * Prime64_1, - High = 0, - }; - - if (input.Length > 32) - { - if (input.Length > 64) - { - if (input.Length > 96) - { - acc = Xxh128Mix32b(acc, input[48..], input[^64..], secret[96..], seed); - } - acc = Xxh128Mix32b(acc, input[32..], input[^48..], secret[64..], seed); - } - acc = Xxh128Mix32b(acc, input[16..], input[^32..], secret[32..], seed); - } - acc = Xxh128Mix32b(acc, input, input[^16..], secret, seed); - - Hash128 h128 = new() - { - Low = acc.Low + acc.High, - High = acc.Low * Prime64_1 + acc.High * Prime64_4 + ((ulong)input.Length - seed) * Prime64_2, - }; - h128.Low = Xxh3Avalanche(h128.Low); - h128.High = 0UL - Xxh3Avalanche(h128.High); - - return h128; - } - - private static Hash128 Xxh3Len129To240128b(ReadOnlySpan input, ReadOnlySpan secret, ulong seed) - { - Debug.Assert(secret.Length >= SecretSizeMin); - Debug.Assert(128 < input.Length && input.Length <= 240); - - Hash128 acc = new(); - - int nbRounds = input.Length / 32; - acc.Low = (ulong)input.Length * Prime64_1; - acc.High = 0; - - for (int i = 0; i < 4; i++) - { - acc = Xxh128Mix32b(acc, input[(32 * i)..], input[(32 * i + 16)..], secret[(32 * i)..], seed); - } - - acc.Low = Xxh3Avalanche(acc.Low); - acc.High = Xxh3Avalanche(acc.High); - Debug.Assert(nbRounds >= 4); - - for (int i = 4; i < nbRounds; i++) - { - acc = Xxh128Mix32b(acc, input[(32 * i)..], input[(32 * i + 16)..], secret[(MidSizeStartOffset + 32 * (i - 4))..], seed); - } - - acc = Xxh128Mix32b(acc, input[^16..], input[^32..], secret[(SecretSizeMin - MidSizeLastOffset - 16)..], 0UL - seed); - - Hash128 h128 = new() - { - Low = acc.Low + acc.High, - High = acc.Low * Prime64_1 + acc.High * Prime64_4 + ((ulong)input.Length - seed) * Prime64_2, - }; - h128.Low = Xxh3Avalanche(h128.Low); - h128.High = 0UL - Xxh3Avalanche(h128.High); - - return h128; - } - - private static Hash128 Xxh3128bitsInternal(ReadOnlySpan input, ReadOnlySpan secret, ulong seed) - { - Debug.Assert(secret.Length >= SecretSizeMin); - - if (input.Length <= 16) - { - return Xxh3Len0To16128b(input, secret, seed); - } - - if (input.Length <= 128) - { - return Xxh3Len17To128128b(input, secret, seed); - } - - if (input.Length <= 240) - { - return Xxh3Len129To240128b(input, secret, seed); - } - - return Xxh3HashLong128bInternal(input, secret); - } - - public static Hash128 ComputeHash(ReadOnlySpan input) - { - return Xxh3128bitsInternal(input, Xxh3KSecret, 0UL); - } - } -} diff --git a/src/Ryujinx.Cpu/AddressSpace.cs b/src/Ryujinx.Cpu/AddressSpace.cs index beea14bee..6664ed134 100644 --- a/src/Ryujinx.Cpu/AddressSpace.cs +++ b/src/Ryujinx.Cpu/AddressSpace.cs @@ -1,5 +1,3 @@ -using Ryujinx.Common; -using Ryujinx.Common.Collections; using Ryujinx.Memory; using System; @@ -7,175 +5,23 @@ namespace Ryujinx.Cpu { public class AddressSpace : IDisposable { - private const int DefaultBlockAlignment = 1 << 20; - - private enum MappingType : byte - { - None, - Private, - Shared, - } - - private class Mapping : IntrusiveRedBlackTreeNode, IComparable - { - public ulong Address { get; private set; } - public ulong Size { get; private set; } - public ulong EndAddress => Address + Size; - public MappingType Type { get; private set; } - - public Mapping(ulong address, ulong size, MappingType type) - { - Address = address; - Size = size; - Type = type; - } - - public Mapping Split(ulong splitAddress) - { - ulong leftSize = splitAddress - Address; - ulong rightSize = EndAddress - splitAddress; - - Mapping left = new(Address, leftSize, Type); - - Address = splitAddress; - Size = rightSize; - - return left; - } - - public void UpdateState(MappingType newType) - { - Type = newType; - } - - public void Extend(ulong sizeDelta) - { - Size += sizeDelta; - } - - public int CompareTo(Mapping other) - { - if (Address < other.Address) - { - return -1; - } - else if (Address <= other.EndAddress - 1UL) - { - return 0; - } - else - { - return 1; - } - } - } - - private class PrivateMapping : IntrusiveRedBlackTreeNode, IComparable - { - public ulong Address { get; private set; } - public ulong Size { get; private set; } - public ulong EndAddress => Address + Size; - public PrivateMemoryAllocation PrivateAllocation { get; private set; } - - public PrivateMapping(ulong address, ulong size, PrivateMemoryAllocation privateAllocation) - { - Address = address; - Size = size; - PrivateAllocation = privateAllocation; - } - - public PrivateMapping Split(ulong splitAddress) - { - ulong leftSize = splitAddress - Address; - ulong rightSize = EndAddress - splitAddress; - - (var leftAllocation, PrivateAllocation) = PrivateAllocation.Split(leftSize); - - PrivateMapping left = new(Address, leftSize, leftAllocation); - - Address = splitAddress; - Size = rightSize; - - return left; - } - - public void Map(MemoryBlock baseBlock, MemoryBlock mirrorBlock, PrivateMemoryAllocation newAllocation) - { - baseBlock.MapView(newAllocation.Memory, newAllocation.Offset, Address, Size); - mirrorBlock.MapView(newAllocation.Memory, newAllocation.Offset, Address, Size); - PrivateAllocation = newAllocation; - } - - public void Unmap(MemoryBlock baseBlock, MemoryBlock mirrorBlock) - { - if (PrivateAllocation.IsValid) - { - baseBlock.UnmapView(PrivateAllocation.Memory, Address, Size); - mirrorBlock.UnmapView(PrivateAllocation.Memory, Address, Size); - PrivateAllocation.Dispose(); - } - - PrivateAllocation = default; - } - - public void Extend(ulong sizeDelta) - { - Size += sizeDelta; - } - - public int CompareTo(PrivateMapping other) - { - if (Address < other.Address) - { - return -1; - } - else if (Address <= other.EndAddress - 1UL) - { - return 0; - } - else - { - return 1; - } - } - } - private readonly MemoryBlock _backingMemory; - private readonly PrivateMemoryAllocator _privateMemoryAllocator; - private readonly IntrusiveRedBlackTree _mappingTree; - private readonly IntrusiveRedBlackTree _privateTree; - - private readonly object _treeLock; - - private readonly bool _supports4KBPages; public MemoryBlock Base { get; } public MemoryBlock Mirror { get; } public ulong AddressSpaceSize { get; } - public AddressSpace(MemoryBlock backingMemory, MemoryBlock baseMemory, MemoryBlock mirrorMemory, ulong addressSpaceSize, bool supports4KBPages) + public AddressSpace(MemoryBlock backingMemory, MemoryBlock baseMemory, MemoryBlock mirrorMemory, ulong addressSpaceSize) { - if (!supports4KBPages) - { - _privateMemoryAllocator = new PrivateMemoryAllocator(DefaultBlockAlignment, MemoryAllocationFlags.Mirrorable | MemoryAllocationFlags.NoMap); - _mappingTree = new IntrusiveRedBlackTree(); - _privateTree = new IntrusiveRedBlackTree(); - _treeLock = new object(); - - _mappingTree.Add(new Mapping(0UL, addressSpaceSize, MappingType.None)); - _privateTree.Add(new PrivateMapping(0UL, addressSpaceSize, default)); - } - _backingMemory = backingMemory; - _supports4KBPages = supports4KBPages; Base = baseMemory; Mirror = mirrorMemory; AddressSpaceSize = addressSpaceSize; } - public static bool TryCreate(MemoryBlock backingMemory, ulong asSize, bool supports4KBPages, out AddressSpace addressSpace) + public static bool TryCreate(MemoryBlock backingMemory, ulong asSize, out AddressSpace addressSpace) { addressSpace = null; @@ -193,7 +39,7 @@ namespace Ryujinx.Cpu { baseMemory = new MemoryBlock(addressSpaceSize, AsFlags); mirrorMemory = new MemoryBlock(addressSpaceSize, AsFlags); - addressSpace = new AddressSpace(backingMemory, baseMemory, mirrorMemory, addressSpaceSize, supports4KBPages); + addressSpace = new AddressSpace(backingMemory, baseMemory, mirrorMemory, addressSpaceSize); break; } @@ -209,289 +55,20 @@ namespace Ryujinx.Cpu public void Map(ulong va, ulong pa, ulong size, MemoryMapFlags flags) { - if (_supports4KBPages) - { - Base.MapView(_backingMemory, pa, va, size); - Mirror.MapView(_backingMemory, pa, va, size); - - return; - } - - lock (_treeLock) - { - ulong alignment = MemoryBlock.GetPageSize(); - bool isAligned = ((va | pa | size) & (alignment - 1)) == 0; - - if (flags.HasFlag(MemoryMapFlags.Private) && !isAligned) - { - Update(va, pa, size, MappingType.Private); - } - else - { - // The update method assumes that shared mappings are already aligned. - - if (!flags.HasFlag(MemoryMapFlags.Private)) - { - if ((va & (alignment - 1)) != (pa & (alignment - 1))) - { - throw new InvalidMemoryRegionException($"Virtual address 0x{va:X} and physical address 0x{pa:X} are misaligned and can't be aligned."); - } - - ulong endAddress = va + size; - va = BitUtils.AlignDown(va, alignment); - pa = BitUtils.AlignDown(pa, alignment); - size = BitUtils.AlignUp(endAddress, alignment) - va; - } - - Update(va, pa, size, MappingType.Shared); - } - } + Base.MapView(_backingMemory, pa, va, size); + Mirror.MapView(_backingMemory, pa, va, size); } public void Unmap(ulong va, ulong size) { - if (_supports4KBPages) - { - Base.UnmapView(_backingMemory, va, size); - Mirror.UnmapView(_backingMemory, va, size); - - return; - } - - lock (_treeLock) - { - Update(va, 0UL, size, MappingType.None); - } - } - - private void Update(ulong va, ulong pa, ulong size, MappingType type) - { - Mapping map = _mappingTree.GetNode(new Mapping(va, 1UL, MappingType.None)); - - Update(map, va, pa, size, type); - } - - private Mapping Update(Mapping map, ulong va, ulong pa, ulong size, MappingType type) - { - ulong endAddress = va + size; - - for (; map != null; map = map.Successor) - { - if (map.Address < va) - { - _mappingTree.Add(map.Split(va)); - } - - if (map.EndAddress > endAddress) - { - Mapping newMap = map.Split(endAddress); - _mappingTree.Add(newMap); - map = newMap; - } - - switch (type) - { - case MappingType.None: - if (map.Type == MappingType.Shared) - { - ulong startOffset = map.Address - va; - ulong mapVa = va + startOffset; - ulong mapSize = Math.Min(size - startOffset, map.Size); - ulong mapEndAddress = mapVa + mapSize; - ulong alignment = MemoryBlock.GetPageSize(); - - mapVa = BitUtils.AlignDown(mapVa, alignment); - mapEndAddress = BitUtils.AlignUp(mapEndAddress, alignment); - - mapSize = mapEndAddress - mapVa; - - Base.UnmapView(_backingMemory, mapVa, mapSize); - Mirror.UnmapView(_backingMemory, mapVa, mapSize); - } - else - { - UnmapPrivate(va, size); - } - break; - case MappingType.Private: - if (map.Type == MappingType.Shared) - { - throw new InvalidMemoryRegionException($"Private mapping request at 0x{va:X} with size 0x{size:X} overlaps shared mapping at 0x{map.Address:X} with size 0x{map.Size:X}."); - } - else - { - MapPrivate(va, size); - } - break; - case MappingType.Shared: - if (map.Type != MappingType.None) - { - throw new InvalidMemoryRegionException($"Shared mapping request at 0x{va:X} with size 0x{size:X} overlaps mapping at 0x{map.Address:X} with size 0x{map.Size:X}."); - } - else - { - ulong startOffset = map.Address - va; - ulong mapPa = pa + startOffset; - ulong mapVa = va + startOffset; - ulong mapSize = Math.Min(size - startOffset, map.Size); - - Base.MapView(_backingMemory, mapPa, mapVa, mapSize); - Mirror.MapView(_backingMemory, mapPa, mapVa, mapSize); - } - break; - } - - map.UpdateState(type); - map = TryCoalesce(map); - - if (map.EndAddress >= endAddress) - { - break; - } - } - - return map; - } - - private Mapping TryCoalesce(Mapping map) - { - Mapping previousMap = map.Predecessor; - Mapping nextMap = map.Successor; - - if (previousMap != null && CanCoalesce(previousMap, map)) - { - previousMap.Extend(map.Size); - _mappingTree.Remove(map); - map = previousMap; - } - - if (nextMap != null && CanCoalesce(map, nextMap)) - { - map.Extend(nextMap.Size); - _mappingTree.Remove(nextMap); - } - - return map; - } - - private static bool CanCoalesce(Mapping left, Mapping right) - { - return left.Type == right.Type; - } - - private void MapPrivate(ulong va, ulong size) - { - ulong endAddress = va + size; - - ulong alignment = MemoryBlock.GetPageSize(); - - // Expand the range outwards based on page size to ensure that at least the requested region is mapped. - ulong vaAligned = BitUtils.AlignDown(va, alignment); - ulong endAddressAligned = BitUtils.AlignUp(endAddress, alignment); - - PrivateMapping map = _privateTree.GetNode(new PrivateMapping(va, 1UL, default)); - - for (; map != null; map = map.Successor) - { - if (!map.PrivateAllocation.IsValid) - { - if (map.Address < vaAligned) - { - _privateTree.Add(map.Split(vaAligned)); - } - - if (map.EndAddress > endAddressAligned) - { - PrivateMapping newMap = map.Split(endAddressAligned); - _privateTree.Add(newMap); - map = newMap; - } - - map.Map(Base, Mirror, _privateMemoryAllocator.Allocate(map.Size, MemoryBlock.GetPageSize())); - } - - if (map.EndAddress >= endAddressAligned) - { - break; - } - } - } - - private void UnmapPrivate(ulong va, ulong size) - { - ulong endAddress = va + size; - - ulong alignment = MemoryBlock.GetPageSize(); - - // Shrink the range inwards based on page size to ensure we won't unmap memory that might be still in use. - ulong vaAligned = BitUtils.AlignUp(va, alignment); - ulong endAddressAligned = BitUtils.AlignDown(endAddress, alignment); - - if (endAddressAligned <= vaAligned) - { - return; - } - - PrivateMapping map = _privateTree.GetNode(new PrivateMapping(va, 1UL, default)); - - for (; map != null; map = map.Successor) - { - if (map.PrivateAllocation.IsValid) - { - if (map.Address < vaAligned) - { - _privateTree.Add(map.Split(vaAligned)); - } - - if (map.EndAddress > endAddressAligned) - { - PrivateMapping newMap = map.Split(endAddressAligned); - _privateTree.Add(newMap); - map = newMap; - } - - map.Unmap(Base, Mirror); - map = TryCoalesce(map); - } - - if (map.EndAddress >= endAddressAligned) - { - break; - } - } - } - - private PrivateMapping TryCoalesce(PrivateMapping map) - { - PrivateMapping previousMap = map.Predecessor; - PrivateMapping nextMap = map.Successor; - - if (previousMap != null && CanCoalesce(previousMap, map)) - { - previousMap.Extend(map.Size); - _privateTree.Remove(map); - map = previousMap; - } - - if (nextMap != null && CanCoalesce(map, nextMap)) - { - map.Extend(nextMap.Size); - _privateTree.Remove(nextMap); - } - - return map; - } - - private static bool CanCoalesce(PrivateMapping left, PrivateMapping right) - { - return !left.PrivateAllocation.IsValid && !right.PrivateAllocation.IsValid; + Base.UnmapView(_backingMemory, va, size); + Mirror.UnmapView(_backingMemory, va, size); } public void Dispose() { GC.SuppressFinalize(this); - _privateMemoryAllocator?.Dispose(); Base.Dispose(); Mirror.Dispose(); } diff --git a/src/Ryujinx.Cpu/AddressTable.cs b/src/Ryujinx.Cpu/AddressTable.cs new file mode 100644 index 000000000..d87b12ab0 --- /dev/null +++ b/src/Ryujinx.Cpu/AddressTable.cs @@ -0,0 +1,482 @@ +using ARMeilleure.Memory; +using Ryujinx.Common; +using Ryujinx.Cpu.Signal; +using Ryujinx.Memory; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Threading; +using static Ryujinx.Cpu.MemoryEhMeilleure; + +namespace ARMeilleure.Common +{ + /// + /// Represents a table of guest address to a value. + /// + /// Type of the value + public unsafe class AddressTable : IAddressTable where TEntry : unmanaged + { + /// + /// Represents a page of the address table. + /// + private readonly struct AddressTablePage + { + /// + /// True if the allocation belongs to a sparse block, false otherwise. + /// + public readonly bool IsSparse; + + /// + /// Base address for the page. + /// + public readonly IntPtr Address; + + public AddressTablePage(bool isSparse, IntPtr address) + { + IsSparse = isSparse; + Address = address; + } + } + + /// + /// A sparsely mapped block of memory with a signal handler to map pages as they're accessed. + /// + private readonly struct TableSparseBlock : IDisposable + { + public readonly SparseMemoryBlock Block; + private readonly TrackingEventDelegate _trackingEvent; + + public TableSparseBlock(ulong size, Action ensureMapped, PageInitDelegate pageInit) + { + var block = new SparseMemoryBlock(size, pageInit, null); + + _trackingEvent = (ulong address, ulong size, bool write) => + { + ulong pointer = (ulong)block.Block.Pointer + address; + ensureMapped((IntPtr)pointer); + return pointer; + }; + + bool added = NativeSignalHandler.AddTrackedRegion( + (nuint)block.Block.Pointer, + (nuint)(block.Block.Pointer + (IntPtr)block.Block.Size), + Marshal.GetFunctionPointerForDelegate(_trackingEvent)); + + if (!added) + { + throw new InvalidOperationException("Number of allowed tracked regions exceeded."); + } + + Block = block; + } + + public void Dispose() + { + NativeSignalHandler.RemoveTrackedRegion((nuint)Block.Block.Pointer); + + Block.Dispose(); + } + } + + private bool _disposed; + private TEntry** _table; + private readonly List _pages; + private TEntry _fill; + + private readonly MemoryBlock _sparseFill; + private readonly SparseMemoryBlock _fillBottomLevel; + private readonly TEntry* _fillBottomLevelPtr; + + private readonly List _sparseReserved; + private readonly ReaderWriterLockSlim _sparseLock; + + private ulong _sparseBlockSize; + private ulong _sparseReservedOffset; + + public bool Sparse { get; } + + /// + public ulong Mask { get; } + + /// + public AddressTableLevel[] Levels { get; } + + /// + public TEntry Fill + { + get + { + return _fill; + } + set + { + UpdateFill(value); + } + } + + /// + public IntPtr Base + { + get + { + ObjectDisposedException.ThrowIf(_disposed, this); + + lock (_pages) + { + return (IntPtr)GetRootPage(); + } + } + } + + /// + /// Constructs a new instance of the class with the specified list of + /// . + /// + /// Levels for the address table + /// True if the bottom page should be sparsely mapped + /// is null + /// Length of is less than 2 + public AddressTable(AddressTableLevel[] levels, bool sparse) + { + ArgumentNullException.ThrowIfNull(levels); + + _pages = new List(capacity: 16); + + Levels = levels; + Mask = 0; + + foreach (var level in Levels) + { + Mask |= level.Mask; + } + + Sparse = sparse; + + if (sparse) + { + // If the address table is sparse, allocate a fill block + + _sparseFill = new MemoryBlock(268435456ul, MemoryAllocationFlags.Mirrorable); //low Power TC uses size: 65536ul + + ulong bottomLevelSize = (1ul << levels.Last().Length) * (ulong)sizeof(TEntry); + + _fillBottomLevel = new SparseMemoryBlock(bottomLevelSize, null, _sparseFill); + _fillBottomLevelPtr = (TEntry*)_fillBottomLevel.Block.Pointer; + + _sparseReserved = new List(); + _sparseLock = new ReaderWriterLockSlim(); + + _sparseBlockSize = bottomLevelSize; + } + } + + /// + /// Create an instance for an ARM function table. + /// Selects the best table structure for A32/A64, taking into account the selected memory manager type. + /// + /// True if the guest is A64, false otherwise + /// Memory manager type + /// An for ARM function lookup + public static AddressTable CreateForArm(bool for64Bits, MemoryManagerType type) + { + // Assume software memory means that we don't want to use any signal handlers. + bool sparse = type != MemoryManagerType.SoftwareMmu && type != MemoryManagerType.SoftwarePageTable; + + return new AddressTable(AddressTablePresets.GetArmPreset(for64Bits, sparse), sparse); + } + + /// + /// Update the fill value for the bottom level of the table. + /// + /// New fill value + private void UpdateFill(TEntry fillValue) + { + if (_sparseFill != null) + { + Span span = _sparseFill.GetSpan(0, (int)_sparseFill.Size); + MemoryMarshal.Cast(span).Fill(fillValue); + } + + _fill = fillValue; + } + + /// + /// Signal that the given code range exists. + /// + /// + /// + public void SignalCodeRange(ulong address, ulong size) + { + AddressTableLevel bottom = Levels.Last(); + ulong bottomLevelEntries = 1ul << bottom.Length; + + ulong entryIndex = address >> bottom.Index; + ulong entries = size >> bottom.Index; + entries += entryIndex - BitUtils.AlignDown(entryIndex, bottomLevelEntries); + + _sparseBlockSize = Math.Max(_sparseBlockSize, BitUtils.AlignUp(entries, bottomLevelEntries) * (ulong)sizeof(TEntry)); + } + + /// + public bool IsValid(ulong address) + { + return (address & ~Mask) == 0; + } + + /// + public ref TEntry GetValue(ulong address) + { + ObjectDisposedException.ThrowIf(_disposed, this); + + if (!IsValid(address)) + { + throw new ArgumentException($"Address 0x{address:X} is not mapped onto the table.", nameof(address)); + } + + lock (_pages) + { + TEntry* page = GetPage(address); + + int index = Levels[^1].GetValue(address); + + EnsureMapped((IntPtr)(page + index)); + + return ref page[index]; + } + } + + /// + /// Gets the leaf page for the specified guest . + /// + /// Guest address + /// Leaf page for the specified guest + private TEntry* GetPage(ulong address) + { + TEntry** page = GetRootPage(); + + for (int i = 0; i < Levels.Length - 1; i++) + { + ref AddressTableLevel level = ref Levels[i]; + ref TEntry* nextPage = ref page[level.GetValue(address)]; + + if (nextPage == null || nextPage == _fillBottomLevelPtr) + { + ref AddressTableLevel nextLevel = ref Levels[i + 1]; + + if (i == Levels.Length - 2) + { + nextPage = (TEntry*)Allocate(1 << nextLevel.Length, Fill, leaf: true); + } + else + { + nextPage = (TEntry*)Allocate(1 << nextLevel.Length, GetFillValue(i), leaf: false); + } + } + + page = (TEntry**)nextPage; + } + + return (TEntry*)page; + } + + /// + /// Ensure the given pointer is mapped in any overlapping sparse reservations. + /// + /// Pointer to be mapped + private void EnsureMapped(IntPtr ptr) + { + if (Sparse) + { + // Check sparse allocations to see if the pointer is in any of them. + // Ensure the page is committed if there's a match. + + _sparseLock.EnterReadLock(); + + try + { + foreach (TableSparseBlock reserved in _sparseReserved) + { + SparseMemoryBlock sparse = reserved.Block; + + if (ptr >= sparse.Block.Pointer && ptr < sparse.Block.Pointer + (IntPtr)sparse.Block.Size) + { + sparse.EnsureMapped((ulong)(ptr - sparse.Block.Pointer)); + + break; + } + } + } + finally + { + _sparseLock.ExitReadLock(); + } + } + } + + /// + /// Get the fill value for a non-leaf level of the table. + /// + /// Level to get the fill value for + /// The fill value + private IntPtr GetFillValue(int level) + { + if (_fillBottomLevel != null && level == Levels.Length - 2) + { + return (IntPtr)_fillBottomLevelPtr; + } + else + { + return IntPtr.Zero; + } + } + + /// + /// Lazily initialize and get the root page of the . + /// + /// Root page of the + private TEntry** GetRootPage() + { + if (_table == null) + { + if (Levels.Length == 1) + _table = (TEntry**)Allocate(1 << Levels[0].Length, Fill, leaf: true); + else + _table = (TEntry**)Allocate(1 << Levels[0].Length, GetFillValue(0), leaf: false); + } + + return _table; + } + + /// + /// Initialize a leaf page with the fill value. + /// + /// Page to initialize + private void InitLeafPage(Span page) + { + MemoryMarshal.Cast(page).Fill(_fill); + } + + /// + /// Reserve a new sparse block, and add it to the list. + /// + /// The new sparse block that was added + private TableSparseBlock ReserveNewSparseBlock() + { + var block = new TableSparseBlock(_sparseBlockSize, EnsureMapped, InitLeafPage); + + _sparseReserved.Add(block); + _sparseReservedOffset = 0; + + return block; + } + + /// + /// Allocates a block of memory of the specified type and length. + /// + /// Type of elements + /// Number of elements + /// Fill value + /// if leaf; otherwise + /// Allocated block + private IntPtr Allocate(int length, T fill, bool leaf) where T : unmanaged + { + var size = sizeof(T) * length; + + AddressTablePage page; + + if (Sparse && leaf) + { + _sparseLock.EnterWriteLock(); + + SparseMemoryBlock block; + + if (_sparseReserved.Count == 0) + { + block = ReserveNewSparseBlock().Block; + } + else + { + block = _sparseReserved.Last().Block; + + if (_sparseReservedOffset == block.Block.Size) + { + block = ReserveNewSparseBlock().Block; + } + } + + page = new AddressTablePage(true, block.Block.Pointer + (IntPtr)_sparseReservedOffset); + + _sparseReservedOffset += (ulong)size; + + _sparseLock.ExitWriteLock(); + } + else + { + var address = (IntPtr)NativeAllocator.Instance.Allocate((uint)size); + page = new AddressTablePage(false, address); + + var span = new Span((void*)page.Address, length); + span.Fill(fill); + } + + _pages.Add(page); + + //TranslatorEventSource.Log.AddressTableAllocated(size, leaf); + + return page.Address; + } + + /// + /// Releases all resources used by the instance. + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Releases all unmanaged and optionally managed resources used by the + /// instance. + /// + /// to dispose managed resources also; otherwise just unmanaged resouces + protected virtual void Dispose(bool disposing) + { + if (!_disposed) + { + foreach (var page in _pages) + { + if (!page.IsSparse) + { + Marshal.FreeHGlobal(page.Address); + } + } + + if (Sparse) + { + foreach (TableSparseBlock block in _sparseReserved) + { + block.Dispose(); + } + + _sparseReserved.Clear(); + + _fillBottomLevel.Dispose(); + _sparseFill.Dispose(); + _sparseLock.Dispose(); + } + + _disposed = true; + } + } + + /// + /// Frees resources used by the instance. + /// + ~AddressTable() + { + Dispose(false); + } + } +} diff --git a/src/Ryujinx.Cpu/AppleHv/HvApi.cs b/src/Ryujinx.Cpu/AppleHv/HvApi.cs index e6e08111f..864f6b063 100644 --- a/src/Ryujinx.Cpu/AppleHv/HvApi.cs +++ b/src/Ryujinx.Cpu/AppleHv/HvApi.cs @@ -273,7 +273,7 @@ namespace Ryujinx.Cpu.AppleHv public static partial HvResult hv_vm_get_max_vcpu_count(out uint max_vcpu_count); [LibraryImport(LibraryName, SetLastError = true)] - public static partial HvResult hv_vm_create(IntPtr config); + public static partial HvResult hv_vm_create(nint config); [LibraryImport(LibraryName, SetLastError = true)] public static partial HvResult hv_vm_destroy(); @@ -288,7 +288,7 @@ namespace Ryujinx.Cpu.AppleHv public static partial HvResult hv_vm_protect(ulong ipa, ulong size, HvMemoryFlags flags); [LibraryImport(LibraryName, SetLastError = true)] - public unsafe static partial HvResult hv_vcpu_create(out ulong vcpu, ref HvVcpuExit* exit, IntPtr config); + public unsafe static partial HvResult hv_vcpu_create(out ulong vcpu, ref HvVcpuExit* exit, nint config); [LibraryImport(LibraryName, SetLastError = true)] public unsafe static partial HvResult hv_vcpu_destroy(ulong vcpu); diff --git a/src/Ryujinx.Cpu/AppleHv/HvCodePatcher.cs b/src/Ryujinx.Cpu/AppleHv/HvCodePatcher.cs deleted file mode 100644 index 876597b78..000000000 --- a/src/Ryujinx.Cpu/AppleHv/HvCodePatcher.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System; -using System.Runtime.InteropServices; -using System.Runtime.Intrinsics; - -namespace Ryujinx.Cpu.AppleHv -{ - static class HvCodePatcher - { - private const uint XMask = 0x3f808000u; - private const uint XValue = 0x8000000u; - - private const uint ZrIndex = 31u; - - public static void RewriteUnorderedExclusiveInstructions(Span code) - { - Span codeUint = MemoryMarshal.Cast(code); - Span> codeVector = MemoryMarshal.Cast>(code); - - Vector128 mask = Vector128.Create(XMask); - Vector128 value = Vector128.Create(XValue); - - for (int index = 0; index < codeVector.Length; index++) - { - Vector128 v = codeVector[index]; - - if (Vector128.EqualsAny(Vector128.BitwiseAnd(v, mask), value)) - { - int baseIndex = index * 4; - - for (int instIndex = baseIndex; instIndex < baseIndex + 4; instIndex++) - { - ref uint inst = ref codeUint[instIndex]; - - if ((inst & XMask) != XValue) - { - continue; - } - - bool isPair = (inst & (1u << 21)) != 0; - bool isLoad = (inst & (1u << 22)) != 0; - - uint rt2 = (inst >> 10) & 0x1fu; - uint rs = (inst >> 16) & 0x1fu; - - if (isLoad && rs != ZrIndex) - { - continue; - } - - if (!isPair && rt2 != ZrIndex) - { - continue; - } - - // Set the ordered flag. - inst |= 1u << 15; - } - } - } - } - } -} diff --git a/src/Ryujinx.Cpu/AppleHv/HvCpuContext.cs b/src/Ryujinx.Cpu/AppleHv/HvCpuContext.cs index 99e4c0479..784949441 100644 --- a/src/Ryujinx.Cpu/AppleHv/HvCpuContext.cs +++ b/src/Ryujinx.Cpu/AppleHv/HvCpuContext.cs @@ -32,7 +32,7 @@ namespace Ryujinx.Cpu.AppleHv { } - public IDiskCacheLoadState LoadDiskCache(string titleIdText, string displayVersion, bool enabled) + public IDiskCacheLoadState LoadDiskCache(string titleIdText, string displayVersion, bool enabled, string cacheSelector) { return new DummyDiskCacheLoadState(); } diff --git a/src/Ryujinx.Cpu/AppleHv/HvExecutionContextVcpu.cs b/src/Ryujinx.Cpu/AppleHv/HvExecutionContextVcpu.cs index bb232940d..9d459d062 100644 --- a/src/Ryujinx.Cpu/AppleHv/HvExecutionContextVcpu.cs +++ b/src/Ryujinx.Cpu/AppleHv/HvExecutionContextVcpu.cs @@ -10,9 +10,9 @@ namespace Ryujinx.Cpu.AppleHv class HvExecutionContextVcpu : IHvExecutionContext { private static readonly MemoryBlock _setSimdFpRegFuncMem; - private delegate HvResult SetSimdFpReg(ulong vcpu, HvSimdFPReg reg, in V128 value, IntPtr funcPtr); + private delegate HvResult SetSimdFpReg(ulong vcpu, HvSimdFPReg reg, in V128 value, nint funcPtr); private static readonly SetSimdFpReg _setSimdFpReg; - private static readonly IntPtr _setSimdFpRegNativePtr; + private static readonly nint _setSimdFpRegNativePtr; static HvExecutionContextVcpu() { @@ -25,7 +25,7 @@ namespace Ryujinx.Cpu.AppleHv _setSimdFpReg = Marshal.GetDelegateForFunctionPointer(_setSimdFpRegFuncMem.Pointer); - if (NativeLibrary.TryLoad(HvApi.LibraryName, out IntPtr hvLibHandle)) + if (NativeLibrary.TryLoad(HvApi.LibraryName, out nint hvLibHandle)) { _setSimdFpRegNativePtr = NativeLibrary.GetExport(hvLibHandle, nameof(HvApi.hv_vcpu_set_simd_fp_reg)); } diff --git a/src/Ryujinx.Cpu/AppleHv/HvMemoryManager.cs b/src/Ryujinx.Cpu/AppleHv/HvMemoryManager.cs index 947c37100..bb56a4344 100644 --- a/src/Ryujinx.Cpu/AppleHv/HvMemoryManager.cs +++ b/src/Ryujinx.Cpu/AppleHv/HvMemoryManager.cs @@ -3,12 +3,11 @@ using Ryujinx.Memory; using Ryujinx.Memory.Range; using Ryujinx.Memory.Tracking; using System; +using System.Buffers; using System.Collections.Generic; using System.Linq; using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; using System.Runtime.Versioning; -using System.Threading; namespace Ryujinx.Cpu.AppleHv { @@ -16,31 +15,10 @@ namespace Ryujinx.Cpu.AppleHv /// Represents a CPU memory manager which maps guest virtual memory directly onto the Hypervisor page table. /// [SupportedOSPlatform("macos")] - public class HvMemoryManager : MemoryManagerBase, IMemoryManager, IVirtualMemoryManagerTracked, IWritableBlock + public sealed class HvMemoryManager : VirtualMemoryManagerRefCountedBase, IMemoryManager, IVirtualMemoryManagerTracked { - public const int PageBits = 12; - public const int PageSize = 1 << PageBits; - public const int PageMask = PageSize - 1; - - public const int PageToPteShift = 5; // 32 pages (2 bits each) in one ulong page table entry. - public const ulong BlockMappedMask = 0x5555555555555555; // First bit of each table entry set. - - private enum HostMappedPtBits : ulong - { - Unmapped = 0, - Mapped, - WriteTracked, - ReadWriteTracked, - - MappedReplicated = 0x5555555555555555, - WriteTrackedReplicated = 0xaaaaaaaaaaaaaaaa, - ReadWriteTrackedReplicated = ulong.MaxValue, - } - private readonly InvalidAccessHandler _invalidAccessHandler; - private readonly ulong _addressSpaceSize; - private readonly HvAddressSpace _addressSpace; internal HvAddressSpace AddressSpace => _addressSpace; @@ -48,13 +26,13 @@ namespace Ryujinx.Cpu.AppleHv private readonly MemoryBlock _backingMemory; private readonly PageTable _pageTable; - private readonly ulong[] _pageBitmap; + private readonly ManagedPageFlags _pages; - public bool Supports4KBPages => true; + public bool UsesPrivateAllocations => false; public int AddressSpaceBits { get; } - public IntPtr PageTablePointer => IntPtr.Zero; + public nint PageTablePointer => nint.Zero; public MemoryManagerType Type => MemoryManagerType.SoftwarePageTable; @@ -62,6 +40,8 @@ namespace Ryujinx.Cpu.AppleHv public event Action UnmapEvent; + protected override ulong AddressSpaceSize { get; } + /// /// Creates a new instance of the Hypervisor memory manager. /// @@ -73,7 +53,7 @@ namespace Ryujinx.Cpu.AppleHv _backingMemory = backingMemory; _pageTable = new PageTable(); _invalidAccessHandler = invalidAccessHandler; - _addressSpaceSize = addressSpaceSize; + AddressSpaceSize = addressSpaceSize; ulong asSize = PageSize; int asBits = PageBits; @@ -88,46 +68,10 @@ namespace Ryujinx.Cpu.AppleHv AddressSpaceBits = asBits; - _pageBitmap = new ulong[1 << (AddressSpaceBits - (PageBits + PageToPteShift))]; + _pages = new ManagedPageFlags(AddressSpaceBits); Tracking = new MemoryTracking(this, PageSize, invalidAccessHandler); } - /// - /// Checks if the virtual address is part of the addressable space. - /// - /// Virtual address - /// True if the virtual address is part of the addressable space - private bool ValidateAddress(ulong va) - { - return va < _addressSpaceSize; - } - - /// - /// Checks if the combination of virtual address and size is part of the addressable space. - /// - /// Virtual address of the range - /// Size of the range in bytes - /// True if the combination of virtual address and size is part of the addressable space - private bool ValidateAddressAndSize(ulong va, ulong size) - { - ulong endVa = va + size; - return endVa >= va && endVa >= size && endVa <= _addressSpaceSize; - } - - /// - /// Ensures the combination of virtual address and size is part of the addressable space. - /// - /// Virtual address of the range - /// Size of the range in bytes - /// Throw when the memory region specified outside the addressable space - private void AssertValidAddressAndSize(ulong va, ulong size) - { - if (!ValidateAddressAndSize(va, size)) - { - throw new InvalidMemoryRegionException($"va=0x{va:X16}, size=0x{size:X16}"); - } - } - /// public void Map(ulong va, ulong pa, ulong size, MemoryMapFlags flags) { @@ -135,7 +79,7 @@ namespace Ryujinx.Cpu.AppleHv PtMap(va, pa, size); _addressSpace.MapUser(va, pa, size, MemoryPermission.ReadWriteExecute); - AddMapping(va, size); + _pages.AddMapping(va, size); Tracking.Map(va, size); } @@ -152,12 +96,6 @@ namespace Ryujinx.Cpu.AppleHv } } - /// - public void MapForeign(ulong va, nuint hostPointer, ulong size) - { - throw new NotSupportedException(); - } - /// public void Unmap(ulong va, ulong size) { @@ -166,7 +104,7 @@ namespace Ryujinx.Cpu.AppleHv UnmapEvent?.Invoke(va, size); Tracking.Unmap(va, size); - RemoveMapping(va, size); + _pages.RemoveMapping(va, size); _addressSpace.UnmapUser(va, size); PtUnmap(va, size); } @@ -182,20 +120,11 @@ namespace Ryujinx.Cpu.AppleHv } } - /// - public T Read(ulong va) where T : unmanaged - { - return MemoryMarshal.Cast(GetSpan(va, Unsafe.SizeOf()))[0]; - } - - /// - public T ReadTracked(ulong va) where T : unmanaged + public override T ReadTracked(ulong va) { try { - SignalMemoryTracking(va, (ulong)Unsafe.SizeOf(), false); - - return Read(va); + return base.ReadTracked(va); } catch (InvalidMemoryRegionException) { @@ -208,107 +137,11 @@ namespace Ryujinx.Cpu.AppleHv } } - /// - public void Read(ulong va, Span data) - { - ReadImpl(va, data); - } - - /// - public void Write(ulong va, T value) where T : unmanaged - { - Write(va, MemoryMarshal.Cast(MemoryMarshal.CreateSpan(ref value, 1))); - } - - /// - public void Write(ulong va, ReadOnlySpan data) - { - if (data.Length == 0) - { - return; - } - - SignalMemoryTracking(va, (ulong)data.Length, true); - - WriteImpl(va, data); - } - - /// - public void WriteUntracked(ulong va, ReadOnlySpan data) - { - if (data.Length == 0) - { - return; - } - - WriteImpl(va, data); - } - - /// - public bool WriteWithRedundancyCheck(ulong va, ReadOnlySpan data) - { - if (data.Length == 0) - { - return false; - } - - SignalMemoryTracking(va, (ulong)data.Length, false); - - if (IsContiguousAndMapped(va, data.Length)) - { - var target = _backingMemory.GetSpan(GetPhysicalAddressInternal(va), data.Length); - - bool changed = !data.SequenceEqual(target); - - if (changed) - { - data.CopyTo(target); - } - - return changed; - } - else - { - WriteImpl(va, data); - - return true; - } - } - - private void WriteImpl(ulong va, ReadOnlySpan data) + public override void Read(ulong va, Span data) { try { - AssertValidAddressAndSize(va, (ulong)data.Length); - - if (IsContiguousAndMapped(va, data.Length)) - { - data.CopyTo(_backingMemory.GetSpan(GetPhysicalAddressInternal(va), data.Length)); - } - else - { - int offset = 0, size; - - if ((va & PageMask) != 0) - { - ulong pa = GetPhysicalAddressChecked(va); - - size = Math.Min(data.Length, PageSize - (int)(va & PageMask)); - - data[..size].CopyTo(_backingMemory.GetSpan(pa, size)); - - offset += size; - } - - for (; offset < data.Length; offset += size) - { - ulong pa = GetPhysicalAddressChecked(va + (ulong)offset); - - size = Math.Min(data.Length - offset, PageSize); - - data.Slice(offset, size).CopyTo(_backingMemory.GetSpan(pa, size)); - } - } + base.Read(va, data); } catch (InvalidMemoryRegionException) { @@ -319,61 +152,53 @@ namespace Ryujinx.Cpu.AppleHv } } - /// - public ReadOnlySpan GetSpan(ulong va, int size, bool tracked = false) + public override void Write(ulong va, ReadOnlySpan data) { - if (size == 0) + try { - return ReadOnlySpan.Empty; + base.Write(va, data); } - - if (tracked) + catch (InvalidMemoryRegionException) { - SignalMemoryTracking(va, (ulong)size, false); - } - - if (IsContiguousAndMapped(va, size)) - { - return _backingMemory.GetSpan(GetPhysicalAddressInternal(va), size); - } - else - { - Span data = new byte[size]; - - ReadImpl(va, data); - - return data; + if (_invalidAccessHandler == null || !_invalidAccessHandler(va)) + { + throw; + } } } - /// - public WritableRegion GetWritableRegion(ulong va, int size, bool tracked = false) + public override void WriteUntracked(ulong va, ReadOnlySpan data) { - if (size == 0) + try { - return new WritableRegion(null, va, Memory.Empty); + base.WriteUntracked(va, data); } - - if (tracked) + catch (InvalidMemoryRegionException) { - SignalMemoryTracking(va, (ulong)size, true); - } - - if (IsContiguousAndMapped(va, size)) - { - return new WritableRegion(null, va, _backingMemory.GetMemory(GetPhysicalAddressInternal(va), size)); - } - else - { - Memory memory = new byte[size]; - - ReadImpl(va, memory.Span); - - return new WritableRegion(this, va, memory); + if (_invalidAccessHandler == null || !_invalidAccessHandler(va)) + { + throw; + } + } + } + + public override ReadOnlySequence GetReadOnlySequence(ulong va, int size, bool tracked = false) + { + try + { + return base.GetReadOnlySequence(va, size, tracked); + } + catch (InvalidMemoryRegionException) + { + if (_invalidAccessHandler == null || !_invalidAccessHandler(va)) + { + throw; + } + + return ReadOnlySequence.Empty; } } - /// public ref T GetRef(ulong va) where T : unmanaged { if (!IsContiguous(va, Unsafe.SizeOf())) @@ -386,26 +211,10 @@ namespace Ryujinx.Cpu.AppleHv return ref _backingMemory.GetRef(GetPhysicalAddressChecked(va)); } - /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool IsMapped(ulong va) + public override bool IsMapped(ulong va) { - return ValidateAddress(va) && IsMappedImpl(va); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private bool IsMappedImpl(ulong va) - { - ulong page = va >> PageBits; - - int bit = (int)((page & 31) << 1); - - int pageIndex = (int)(page >> PageToPteShift); - ref ulong pageRef = ref _pageBitmap[pageIndex]; - - ulong pte = Volatile.Read(ref pageRef); - - return ((pte >> bit) & 3) != 0; + return ValidateAddress(va) && _pages.IsMapped(va); } /// @@ -413,91 +222,7 @@ namespace Ryujinx.Cpu.AppleHv { AssertValidAddressAndSize(va, size); - return IsRangeMappedImpl(va, size); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void GetPageBlockRange(ulong pageStart, ulong pageEnd, out ulong startMask, out ulong endMask, out int pageIndex, out int pageEndIndex) - { - startMask = ulong.MaxValue << ((int)(pageStart & 31) << 1); - endMask = ulong.MaxValue >> (64 - ((int)(pageEnd & 31) << 1)); - - pageIndex = (int)(pageStart >> PageToPteShift); - pageEndIndex = (int)((pageEnd - 1) >> PageToPteShift); - } - - private bool IsRangeMappedImpl(ulong va, ulong size) - { - int pages = GetPagesCount(va, size, out _); - - if (pages == 1) - { - return IsMappedImpl(va); - } - - ulong pageStart = va >> PageBits; - ulong pageEnd = pageStart + (ulong)pages; - - GetPageBlockRange(pageStart, pageEnd, out ulong startMask, out ulong endMask, out int pageIndex, out int pageEndIndex); - - // Check if either bit in each 2 bit page entry is set. - // OR the block with itself shifted down by 1, and check the first bit of each entry. - - ulong mask = BlockMappedMask & startMask; - - while (pageIndex <= pageEndIndex) - { - if (pageIndex == pageEndIndex) - { - mask &= endMask; - } - - ref ulong pageRef = ref _pageBitmap[pageIndex++]; - ulong pte = Volatile.Read(ref pageRef); - - pte |= pte >> 1; - if ((pte & mask) != mask) - { - return false; - } - - mask = BlockMappedMask; - } - - return true; - } - - private static void ThrowMemoryNotContiguous() => throw new MemoryNotContiguousException(); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private bool IsContiguousAndMapped(ulong va, int size) => IsContiguous(va, size) && IsMapped(va); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private bool IsContiguous(ulong va, int size) - { - if (!ValidateAddress(va) || !ValidateAddressAndSize(va, (ulong)size)) - { - return false; - } - - int pages = GetPagesCount(va, (uint)size, out va); - - for (int page = 0; page < pages - 1; page++) - { - if (!ValidateAddress(va + PageSize)) - { - return false; - } - - if (GetPhysicalAddressInternal(va) + PageSize != GetPhysicalAddressInternal(va + PageSize)) - { - return false; - } - - va += PageSize; - } - - return true; + return _pages.IsRangeMapped(va, size); } /// @@ -519,7 +244,7 @@ namespace Ryujinx.Cpu.AppleHv for (int i = 0; i < regions.Length; i++) { var guestRegion = guestRegions[i]; - IntPtr pointer = _backingMemory.GetPointer(guestRegion.Address, guestRegion.Size); + nint pointer = _backingMemory.GetPointer(guestRegion.Address, guestRegion.Size); regions[i] = new HostMemoryRange((nuint)(ulong)pointer, guestRegion.Size); } @@ -576,53 +301,10 @@ namespace Ryujinx.Cpu.AppleHv return regions; } - private void ReadImpl(ulong va, Span data) - { - if (data.Length == 0) - { - return; - } - - try - { - AssertValidAddressAndSize(va, (ulong)data.Length); - - int offset = 0, size; - - if ((va & PageMask) != 0) - { - ulong pa = GetPhysicalAddressChecked(va); - - size = Math.Min(data.Length, PageSize - (int)(va & PageMask)); - - _backingMemory.GetSpan(pa, size).CopyTo(data[..size]); - - offset += size; - } - - for (; offset < data.Length; offset += size) - { - ulong pa = GetPhysicalAddressChecked(va + (ulong)offset); - - size = Math.Min(data.Length - offset, PageSize); - - _backingMemory.GetSpan(pa, size).CopyTo(data.Slice(offset, size)); - } - } - catch (InvalidMemoryRegionException) - { - if (_invalidAccessHandler == null || !_invalidAccessHandler(va)) - { - throw; - } - } - } - - /// /// /// This function also validates that the given range is both valid and mapped, and will throw if it is not. /// - public void SignalMemoryTracking(ulong va, ulong size, bool write, bool precise = false, int? exemptId = null) + public override void SignalMemoryTracking(ulong va, ulong size, bool write, bool precise = false, int? exemptId = null) { AssertValidAddressAndSize(va, size); @@ -632,211 +314,37 @@ namespace Ryujinx.Cpu.AppleHv return; } - // Software table, used for managed memory tracking. - - int pages = GetPagesCount(va, size, out _); - ulong pageStart = va >> PageBits; - - if (pages == 1) - { - ulong tag = (ulong)(write ? HostMappedPtBits.WriteTracked : HostMappedPtBits.ReadWriteTracked); - - int bit = (int)((pageStart & 31) << 1); - - int pageIndex = (int)(pageStart >> PageToPteShift); - ref ulong pageRef = ref _pageBitmap[pageIndex]; - - ulong pte = Volatile.Read(ref pageRef); - ulong state = ((pte >> bit) & 3); - - if (state >= tag) - { - Tracking.VirtualMemoryEvent(va, size, write, precise: false, exemptId); - return; - } - else if (state == 0) - { - ThrowInvalidMemoryRegionException($"Not mapped: va=0x{va:X16}, size=0x{size:X16}"); - } - } - else - { - ulong pageEnd = pageStart + (ulong)pages; - - GetPageBlockRange(pageStart, pageEnd, out ulong startMask, out ulong endMask, out int pageIndex, out int pageEndIndex); - - ulong mask = startMask; - - ulong anyTrackingTag = (ulong)HostMappedPtBits.WriteTrackedReplicated; - - while (pageIndex <= pageEndIndex) - { - if (pageIndex == pageEndIndex) - { - mask &= endMask; - } - - ref ulong pageRef = ref _pageBitmap[pageIndex++]; - - ulong pte = Volatile.Read(ref pageRef); - ulong mappedMask = mask & BlockMappedMask; - - ulong mappedPte = pte | (pte >> 1); - if ((mappedPte & mappedMask) != mappedMask) - { - ThrowInvalidMemoryRegionException($"Not mapped: va=0x{va:X16}, size=0x{size:X16}"); - } - - pte &= mask; - if ((pte & anyTrackingTag) != 0) // Search for any tracking. - { - // Writes trigger any tracking. - // Only trigger tracking from reads if both bits are set on any page. - if (write || (pte & (pte >> 1) & BlockMappedMask) != 0) - { - Tracking.VirtualMemoryEvent(va, size, write, precise: false, exemptId); - break; - } - } - - mask = ulong.MaxValue; - } - } + _pages.SignalMemoryTracking(Tracking, va, size, write, exemptId); } - /// - /// Computes the number of pages in a virtual address range. - /// - /// Virtual address of the range - /// Size of the range - /// The virtual address of the beginning of the first page - /// This function does not differentiate between allocated and unallocated pages. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static int GetPagesCount(ulong va, ulong size, out ulong startVa) - { - // WARNING: Always check if ulong does not overflow during the operations. - startVa = va & ~(ulong)PageMask; - ulong vaSpan = (va - startVa + size + PageMask) & ~(ulong)PageMask; - - return (int)(vaSpan / PageSize); - } - - /// public void Reprotect(ulong va, ulong size, MemoryPermission protection) { - if (protection.HasFlag(MemoryPermission.Execute)) - { - // Some applications use unordered exclusive memory access instructions - // where it is not valid to do so, leading to memory re-ordering that - // makes the code behave incorrectly on some CPUs. - // To work around this, we force all such accesses to be ordered. - - using WritableRegion writableRegion = GetWritableRegion(va, (int)size); - - HvCodePatcher.RewriteUnorderedExclusiveInstructions(writableRegion.Memory.Span); - } - // TODO } /// - public void TrackingReprotect(ulong va, ulong size, MemoryPermission protection) + public void TrackingReprotect(ulong va, ulong size, MemoryPermission protection, bool guest) { - // Protection is inverted on software pages, since the default value is 0. - protection = (~protection) & MemoryPermission.ReadAndWrite; - - int pages = GetPagesCount(va, size, out va); - ulong pageStart = va >> PageBits; - - if (pages == 1) + if (guest) { - ulong protTag = protection switch - { - MemoryPermission.None => (ulong)HostMappedPtBits.Mapped, - MemoryPermission.Write => (ulong)HostMappedPtBits.WriteTracked, - _ => (ulong)HostMappedPtBits.ReadWriteTracked, - }; - - int bit = (int)((pageStart & 31) << 1); - - ulong tagMask = 3UL << bit; - ulong invTagMask = ~tagMask; - - ulong tag = protTag << bit; - - int pageIndex = (int)(pageStart >> PageToPteShift); - ref ulong pageRef = ref _pageBitmap[pageIndex]; - - ulong pte; - - do - { - pte = Volatile.Read(ref pageRef); - } - while ((pte & tagMask) != 0 && Interlocked.CompareExchange(ref pageRef, (pte & invTagMask) | tag, pte) != pte); + _addressSpace.ReprotectUser(va, size, protection); } else { - ulong pageEnd = pageStart + (ulong)pages; - - GetPageBlockRange(pageStart, pageEnd, out ulong startMask, out ulong endMask, out int pageIndex, out int pageEndIndex); - - ulong mask = startMask; - - ulong protTag = protection switch - { - MemoryPermission.None => (ulong)HostMappedPtBits.MappedReplicated, - MemoryPermission.Write => (ulong)HostMappedPtBits.WriteTrackedReplicated, - _ => (ulong)HostMappedPtBits.ReadWriteTrackedReplicated, - }; - - while (pageIndex <= pageEndIndex) - { - if (pageIndex == pageEndIndex) - { - mask &= endMask; - } - - ref ulong pageRef = ref _pageBitmap[pageIndex++]; - - ulong pte; - ulong mappedMask; - - // Change the protection of all 2 bit entries that are mapped. - do - { - pte = Volatile.Read(ref pageRef); - - mappedMask = pte | (pte >> 1); - mappedMask |= (mappedMask & BlockMappedMask) << 1; - mappedMask &= mask; // Only update mapped pages within the given range. - } - while (Interlocked.CompareExchange(ref pageRef, (pte & (~mappedMask)) | (protTag & mappedMask), pte) != pte); - - mask = ulong.MaxValue; - } + _pages.TrackingReprotect(va, size, protection); } - - protection = protection switch - { - MemoryPermission.None => MemoryPermission.ReadAndWrite, - MemoryPermission.Write => MemoryPermission.Read, - _ => MemoryPermission.None, - }; - - _addressSpace.ReprotectUser(va, size, protection); } /// - public RegionHandle BeginTracking(ulong address, ulong size, int id) + public RegionHandle BeginTracking(ulong address, ulong size, int id, RegionFlags flags = RegionFlags.None) { - return Tracking.BeginTracking(address, size, id); + return Tracking.BeginTracking(address, size, id, flags); } /// - public MultiRegionHandle BeginGranularTracking(ulong address, ulong size, IEnumerable handles, ulong granularity, int id) + public MultiRegionHandle BeginGranularTracking(ulong address, ulong size, IEnumerable handles, ulong granularity, int id, RegionFlags flags = RegionFlags.None) { - return Tracking.BeginGranularTracking(address, size, handles, granularity, id); + return Tracking.BeginGranularTracking(address, size, handles, granularity, id, flags); } /// @@ -845,87 +353,7 @@ namespace Ryujinx.Cpu.AppleHv return Tracking.BeginSmartGranularTracking(address, size, granularity, id); } - /// - /// Adds the given address mapping to the page table. - /// - /// Virtual memory address - /// Size to be mapped - private void AddMapping(ulong va, ulong size) - { - int pages = GetPagesCount(va, size, out _); - ulong pageStart = va >> PageBits; - ulong pageEnd = pageStart + (ulong)pages; - - GetPageBlockRange(pageStart, pageEnd, out ulong startMask, out ulong endMask, out int pageIndex, out int pageEndIndex); - - ulong mask = startMask; - - while (pageIndex <= pageEndIndex) - { - if (pageIndex == pageEndIndex) - { - mask &= endMask; - } - - ref ulong pageRef = ref _pageBitmap[pageIndex++]; - - ulong pte; - ulong mappedMask; - - // Map all 2-bit entries that are unmapped. - do - { - pte = Volatile.Read(ref pageRef); - - mappedMask = pte | (pte >> 1); - mappedMask |= (mappedMask & BlockMappedMask) << 1; - mappedMask |= ~mask; // Treat everything outside the range as mapped, thus unchanged. - } - while (Interlocked.CompareExchange(ref pageRef, (pte & mappedMask) | (BlockMappedMask & (~mappedMask)), pte) != pte); - - mask = ulong.MaxValue; - } - } - - /// - /// Removes the given address mapping from the page table. - /// - /// Virtual memory address - /// Size to be unmapped - private void RemoveMapping(ulong va, ulong size) - { - int pages = GetPagesCount(va, size, out _); - ulong pageStart = va >> PageBits; - ulong pageEnd = pageStart + (ulong)pages; - - GetPageBlockRange(pageStart, pageEnd, out ulong startMask, out ulong endMask, out int pageIndex, out int pageEndIndex); - - startMask = ~startMask; - endMask = ~endMask; - - ulong mask = startMask; - - while (pageIndex <= pageEndIndex) - { - if (pageIndex == pageEndIndex) - { - mask |= endMask; - } - - ref ulong pageRef = ref _pageBitmap[pageIndex++]; - ulong pte; - - do - { - pte = Volatile.Read(ref pageRef); - } - while (Interlocked.CompareExchange(ref pageRef, pte & mask, pte) != pte); - - mask = 0; - } - } - - private ulong GetPhysicalAddressChecked(ulong va) + private nuint GetPhysicalAddressChecked(ulong va) { if (!IsMapped(va)) { @@ -935,9 +363,9 @@ namespace Ryujinx.Cpu.AppleHv return GetPhysicalAddressInternal(va); } - private ulong GetPhysicalAddressInternal(ulong va) + private nuint GetPhysicalAddressInternal(ulong va) { - return _pageTable.Read(va) + (va & PageMask); + return (nuint)(_pageTable.Read(va) + (va & PageMask)); } /// @@ -948,6 +376,17 @@ namespace Ryujinx.Cpu.AppleHv _addressSpace.Dispose(); } - private static void ThrowInvalidMemoryRegionException(string message) => throw new InvalidMemoryRegionException(message); + protected override Memory GetPhysicalAddressMemory(nuint pa, int size) + => _backingMemory.GetMemory(pa, size); + + protected override Span GetPhysicalAddressSpan(nuint pa, int size) + => _backingMemory.GetSpan(pa, size); + + protected override nuint TranslateVirtualAddressChecked(ulong va) + => GetPhysicalAddressChecked(va); + + protected override nuint TranslateVirtualAddressUnchecked(ulong va) + => GetPhysicalAddressInternal(va); + } } diff --git a/src/Ryujinx.Cpu/AppleHv/HvVcpuPool.cs b/src/Ryujinx.Cpu/AppleHv/HvVcpuPool.cs index 2edcd7e4e..af124fc7a 100644 --- a/src/Ryujinx.Cpu/AppleHv/HvVcpuPool.cs +++ b/src/Ryujinx.Cpu/AppleHv/HvVcpuPool.cs @@ -72,7 +72,7 @@ namespace Ryujinx.Cpu.AppleHv // Create VCPU. HvVcpuExit* exitInfo = null; - HvApi.hv_vcpu_create(out ulong vcpuHandle, ref exitInfo, IntPtr.Zero).ThrowOnError(); + HvApi.hv_vcpu_create(out ulong vcpuHandle, ref exitInfo, nint.Zero).ThrowOnError(); // Enable FP and SIMD instructions. HvApi.hv_vcpu_set_sys_reg(vcpuHandle, HvSysReg.CPACR_EL1, 0b11 << 20).ThrowOnError(); diff --git a/src/Ryujinx.Cpu/AppleHv/HvVm.cs b/src/Ryujinx.Cpu/AppleHv/HvVm.cs index c4f107532..a12bbea9b 100644 --- a/src/Ryujinx.Cpu/AppleHv/HvVm.cs +++ b/src/Ryujinx.Cpu/AppleHv/HvVm.cs @@ -22,7 +22,7 @@ namespace Ryujinx.Cpu.AppleHv { if (++_addressSpaces == 1) { - HvApi.hv_vm_create(IntPtr.Zero).ThrowOnError(); + HvApi.hv_vm_create(nint.Zero).ThrowOnError(); _ipaAllocator = ipaAllocator = new HvIpaAllocator(); } else diff --git a/src/Ryujinx.Cpu/ICpuContext.cs b/src/Ryujinx.Cpu/ICpuContext.cs index edcebdfc4..1fb3b674d 100644 --- a/src/Ryujinx.Cpu/ICpuContext.cs +++ b/src/Ryujinx.Cpu/ICpuContext.cs @@ -48,7 +48,7 @@ namespace Ryujinx.Cpu /// Version of the application /// True if the cache should be loaded from disk if it exists, false otherwise /// Disk cache load progress reporter and manager - IDiskCacheLoadState LoadDiskCache(string titleIdText, string displayVersion, bool enabled); + IDiskCacheLoadState LoadDiskCache(string titleIdText, string displayVersion, bool enabled, string cacheSelector); /// /// Indicates that code has been loaded into guest memory, and that it might be executed in the future. diff --git a/src/Ryujinx.Cpu/IVirtualMemoryManagerTracked.cs b/src/Ryujinx.Cpu/IVirtualMemoryManagerTracked.cs index 199bff240..e8d11ede5 100644 --- a/src/Ryujinx.Cpu/IVirtualMemoryManagerTracked.cs +++ b/src/Ryujinx.Cpu/IVirtualMemoryManagerTracked.cs @@ -28,8 +28,9 @@ namespace Ryujinx.Cpu /// CPU virtual address of the region /// Size of the region /// Handle ID + /// Region flags /// The memory tracking handle - RegionHandle BeginTracking(ulong address, ulong size, int id); + RegionHandle BeginTracking(ulong address, ulong size, int id, RegionFlags flags = RegionFlags.None); /// /// Obtains a memory tracking handle for the given virtual region, with a specified granularity. This should be disposed when finished with. @@ -39,8 +40,9 @@ namespace Ryujinx.Cpu /// Handles to inherit state from or reuse. When none are present, provide null /// Desired granularity of write tracking /// Handle ID + /// Region flags /// The memory tracking handle - MultiRegionHandle BeginGranularTracking(ulong address, ulong size, IEnumerable handles, ulong granularity, int id); + MultiRegionHandle BeginGranularTracking(ulong address, ulong size, IEnumerable handles, ulong granularity, int id, RegionFlags flags = RegionFlags.None); /// /// Obtains a smart memory tracking handle for the given virtual region, with a specified granularity. This should be disposed when finished with. diff --git a/src/Ryujinx.Cpu/Jit/AddressSpacePageProtections.cs b/src/Ryujinx.Cpu/Jit/AddressSpacePageProtections.cs deleted file mode 100644 index 6d2e1f600..000000000 --- a/src/Ryujinx.Cpu/Jit/AddressSpacePageProtections.cs +++ /dev/null @@ -1,373 +0,0 @@ -using Ryujinx.Common; -using Ryujinx.Common.Collections; -using Ryujinx.Memory; -using System; -using System.Diagnostics; - -namespace Ryujinx.Cpu.Jit -{ - class AddressSpacePageProtections : IDisposable - { - private const ulong GuestPageSize = 0x1000; - - [ThreadStatic] - private static PageProtection _dummyProtection; - - class PageProtection : IntrusiveRedBlackTreeNode, IComparable, IComparable - { - public readonly AddressSpacePartitionAllocation Memory; - public readonly ulong Offset; - public ulong Address; - public ulong Size; - - private MemoryBlock _viewBlock; - - public bool IsMapped => _viewBlock != null; - - public PageProtection(AddressSpacePartitionAllocation memory, ulong offset, ulong address, ulong size) - { - Memory = memory; - Offset = offset; - Address = address; - Size = size; - } - - public void SetViewBlock(MemoryBlock block) - { - _viewBlock = block; - } - - public void Unmap() - { - if (_viewBlock != null) - { - Memory.UnmapView(_viewBlock, Offset, MemoryBlock.GetPageSize()); - _viewBlock = null; - } - } - - public bool OverlapsWith(ulong va, ulong size) - { - return Address < va + size && va < Address + Size; - } - - public int CompareTo(PageProtection other) - { - if (OverlapsWith(other.Address, other.Size)) - { - return 0; - } - else if (Address < other.Address) - { - return -1; - } - else - { - return 1; - } - } - - public int CompareTo(ulong address) - { - if (address < Address) - { - return -1; - } - else if (address <= Address + Size - 1UL) - { - return 0; - } - else - { - return 1; - } - } - } - - private readonly AddressIntrusiveRedBlackTree _protectionTree; - - public AddressSpacePageProtections() - { - _protectionTree = new(); - } - - public void Reprotect( - AddressSpacePartitionAllocator asAllocator, - AddressSpacePartitioned addressSpace, - AddressSpacePartition partition, - ulong va, - ulong endVa, - MemoryPermission protection, - Action updatePtCallback) - { - while (va < endVa) - { - ReprotectPage(asAllocator, addressSpace, partition, va, protection, updatePtCallback); - - va += GuestPageSize; - } - } - - private void ReprotectPage( - AddressSpacePartitionAllocator asAllocator, - AddressSpacePartitioned addressSpace, - AddressSpacePartition partition, - ulong va, - MemoryPermission protection, - Action updatePtCallback) - { - ulong pageSize = MemoryBlock.GetPageSize(); - - PageProtection pageProtection = _protectionTree.GetNode(va); - - if (pageProtection == null) - { - ulong firstPage = BitUtils.AlignDown(va, pageSize); - ulong lastPage = BitUtils.AlignUp(va + GuestPageSize, pageSize) - GuestPageSize; - - AddressSpacePartitionAllocation block; - PageProtection adjPageProtection = null; - ulong blockOffset = 0; - - if (va == firstPage && va > partition.Address) - { - block = asAllocator.AllocatePage(firstPage - pageSize, pageSize * 2); - - MapView(addressSpace, partition, block, 0, pageSize, va - GuestPageSize, out MemoryBlock adjMemory); - - adjPageProtection = new PageProtection(block, 0, va - GuestPageSize, GuestPageSize); - adjPageProtection.SetViewBlock(adjMemory); - blockOffset = pageSize; - } - else if (va == lastPage) - { - block = asAllocator.AllocatePage(firstPage, pageSize * 2); - - MapView(addressSpace, partition, block, pageSize, pageSize, va + GuestPageSize, out MemoryBlock adjMemory); - - adjPageProtection = new PageProtection(block, pageSize, va + GuestPageSize, GuestPageSize); - adjPageProtection.SetViewBlock(adjMemory); - } - else - { - block = asAllocator.AllocatePage(firstPage, pageSize); - } - - if (!MapView(addressSpace, partition, block, blockOffset, pageSize, va, out MemoryBlock viewMemory)) - { - block.Dispose(); - - return; - } - - pageProtection = new PageProtection(block, blockOffset, va, GuestPageSize); - pageProtection.SetViewBlock(viewMemory); - _protectionTree.Add(pageProtection); - - if (adjPageProtection != null) - { - Debug.Assert(_protectionTree.GetNode(adjPageProtection) == null); - _protectionTree.Add(adjPageProtection); - } - } - - Debug.Assert(pageProtection.IsMapped || partition.GetPrivateAllocation(va).Memory == null); - - pageProtection.Memory.Reprotect(pageProtection.Offset, pageSize, protection, false); - - updatePtCallback(va, pageProtection.Memory.GetPointer(pageProtection.Offset + (va & (pageSize - 1)), GuestPageSize), GuestPageSize); - } - - public void UpdateMappings(AddressSpacePartition partition, ulong va, ulong size) - { - ulong pageSize = MemoryBlock.GetPageSize(); - - PageProtection pageProtection = GetLowestOverlap(va, size); - - while (pageProtection != null) - { - if (pageProtection.Address >= va + size) - { - break; - } - - bool mapped = MapView( - partition, - pageProtection.Memory, - pageProtection.Offset, - pageSize, - pageProtection.Address, - out MemoryBlock memory); - - Debug.Assert(mapped); - - pageProtection.SetViewBlock(memory); - pageProtection = pageProtection.Successor; - } - } - - public void Remove(ulong va, ulong size) - { - ulong pageSize = MemoryBlock.GetPageSize(); - - PageProtection pageProtection = GetLowestOverlap(va, size); - - while (pageProtection != null) - { - if (pageProtection.Address >= va + size) - { - break; - } - - ulong firstPage = BitUtils.AlignDown(pageProtection.Address, pageSize); - ulong lastPage = BitUtils.AlignUp(pageProtection.Address + GuestPageSize, pageSize) - GuestPageSize; - - bool canDelete; - - if (pageProtection.Address == firstPage) - { - canDelete = pageProtection.Predecessor == null || - pageProtection.Predecessor.Address + pageProtection.Predecessor.Size != pageProtection.Address || - !pageProtection.Predecessor.IsMapped; - } - else if (pageProtection.Address == lastPage) - { - canDelete = pageProtection.Successor == null || - pageProtection.Address + pageProtection.Size != pageProtection.Successor.Address || - !pageProtection.Successor.IsMapped; - } - else - { - canDelete = true; - } - - PageProtection successor = pageProtection.Successor; - - if (canDelete) - { - if (pageProtection.Address == firstPage && - pageProtection.Predecessor != null && - pageProtection.Predecessor.Address + pageProtection.Predecessor.Size == pageProtection.Address) - { - _protectionTree.Remove(pageProtection.Predecessor); - } - else if (pageProtection.Address == lastPage && - pageProtection.Successor != null && - pageProtection.Address + pageProtection.Size == pageProtection.Successor.Address) - { - successor = successor.Successor; - _protectionTree.Remove(pageProtection.Successor); - } - - _protectionTree.Remove(pageProtection); - pageProtection.Memory.Dispose(); - } - else - { - pageProtection.Unmap(); - } - - pageProtection = successor; - } - } - - private static bool MapView( - AddressSpacePartitioned addressSpace, - AddressSpacePartition partition, - AddressSpacePartitionAllocation dstBlock, - ulong dstOffset, - ulong size, - ulong va, - out MemoryBlock memory) - { - PrivateRange privateRange; - - if (va >= partition.Address && va < partition.EndAddress) - { - privateRange = partition.GetPrivateAllocation(va); - } - else - { - privateRange = addressSpace.GetPrivateAllocation(va); - } - - memory = privateRange.Memory; - - if (privateRange.Memory == null) - { - return false; - } - - dstBlock.MapView(privateRange.Memory, privateRange.Offset & ~(MemoryBlock.GetPageSize() - 1), dstOffset, size); - - return true; - } - - private static bool MapView( - AddressSpacePartition partition, - AddressSpacePartitionAllocation dstBlock, - ulong dstOffset, - ulong size, - ulong va, - out MemoryBlock memory) - { - Debug.Assert(va >= partition.Address && va < partition.EndAddress); - - PrivateRange privateRange = partition.GetPrivateAllocation(va); - - memory = privateRange.Memory; - - if (privateRange.Memory == null) - { - return false; - } - - dstBlock.MapView(privateRange.Memory, privateRange.Offset & ~(size - 1), dstOffset, size); - - return true; - } - - private PageProtection GetLowestOverlap(ulong va, ulong size) - { - PageProtection lookup = _dummyProtection; - - if (lookup == null) - { - lookup = new(default, 0, va, size); - - _dummyProtection = lookup; - } - else - { - lookup.Address = va; - lookup.Size = size; - } - - PageProtection pageProtection = _protectionTree.GetNode(lookup); - - if (pageProtection == null) - { - return null; - } - - while (pageProtection.Predecessor != null && pageProtection.Predecessor.OverlapsWith(va, size)) - { - pageProtection = pageProtection.Predecessor; - } - - return pageProtection; - } - - protected virtual void Dispose(bool disposing) - { - Remove(0, ulong.MaxValue); - Debug.Assert(_protectionTree.Count == 0); - } - - public void Dispose() - { - Dispose(disposing: true); - GC.SuppressFinalize(this); - } - } -} \ No newline at end of file diff --git a/src/Ryujinx.Cpu/Jit/AddressIntrusiveRedBlackTree.cs b/src/Ryujinx.Cpu/Jit/HostTracked/AddressIntrusiveRedBlackTree.cs similarity index 96% rename from src/Ryujinx.Cpu/Jit/AddressIntrusiveRedBlackTree.cs rename to src/Ryujinx.Cpu/Jit/HostTracked/AddressIntrusiveRedBlackTree.cs index 3084d16d1..0e2443303 100644 --- a/src/Ryujinx.Cpu/Jit/AddressIntrusiveRedBlackTree.cs +++ b/src/Ryujinx.Cpu/Jit/HostTracked/AddressIntrusiveRedBlackTree.cs @@ -1,7 +1,7 @@ using Ryujinx.Common.Collections; using System; -namespace Ryujinx.Cpu.Jit +namespace Ryujinx.Cpu.Jit.HostTracked { internal class AddressIntrusiveRedBlackTree : IntrusiveRedBlackTree where T : IntrusiveRedBlackTreeNode, IComparable, IComparable { diff --git a/src/Ryujinx.Cpu/Jit/AddressSpacePartition.cs b/src/Ryujinx.Cpu/Jit/HostTracked/AddressSpacePartition.cs similarity index 78% rename from src/Ryujinx.Cpu/Jit/AddressSpacePartition.cs rename to src/Ryujinx.Cpu/Jit/HostTracked/AddressSpacePartition.cs index 3b983155d..f9743a0a1 100644 --- a/src/Ryujinx.Cpu/Jit/AddressSpacePartition.cs +++ b/src/Ryujinx.Cpu/Jit/HostTracked/AddressSpacePartition.cs @@ -5,7 +5,7 @@ using System; using System.Diagnostics; using System.Threading; -namespace Ryujinx.Cpu.Jit +namespace Ryujinx.Cpu.Jit.HostTracked { readonly struct PrivateRange { @@ -25,7 +25,7 @@ namespace Ryujinx.Cpu.Jit class AddressSpacePartition : IDisposable { - private const ulong GuestPageSize = 0x1000; + public const ulong GuestPageSize = 0x1000; private const int DefaultBlockAlignment = 1 << 20; @@ -137,13 +137,13 @@ namespace Ryujinx.Cpu.Jit return left; } - public void Map(AddressSpacePartitionAllocation baseBlock, ulong baseAddress, PrivateMemoryAllocation newAllocation) + public void Map(AddressSpacePartitionMultiAllocation baseBlock, ulong baseAddress, PrivateMemoryAllocation newAllocation) { baseBlock.MapView(newAllocation.Memory, newAllocation.Offset, Address - baseAddress, Size); PrivateAllocation = newAllocation; } - public void Unmap(AddressSpacePartitionAllocation baseBlock, ulong baseAddress) + public void Unmap(AddressSpacePartitionMultiAllocation baseBlock, ulong baseAddress) { if (PrivateAllocation.IsValid) { @@ -193,11 +193,11 @@ namespace Ryujinx.Cpu.Jit } private readonly MemoryBlock _backingMemory; - private readonly AddressSpacePartitionAllocation _baseMemory; + private readonly AddressSpacePartitionMultiAllocation _baseMemory; private readonly PrivateMemoryAllocator _privateMemoryAllocator; + private readonly AddressIntrusiveRedBlackTree _mappingTree; private readonly AddressIntrusiveRedBlackTree _privateTree; - private readonly AddressSpacePageProtections _pageProtections; private readonly ReaderWriterLockSlim _treeLock; @@ -207,8 +207,9 @@ namespace Ryujinx.Cpu.Jit private ulong? _lastPagePa; private ulong _cachedFirstPagePa; private ulong _cachedLastPagePa; - private bool _hasBridgeAtEnd; - private MemoryPermission _lastPageProtection; + private MemoryBlock _firstPageMemoryForUnmap; + private ulong _firstPageOffsetForLateMap; + private MemoryPermission _firstPageMemoryProtection; public ulong Address { get; } public ulong Size { get; } @@ -219,7 +220,6 @@ namespace Ryujinx.Cpu.Jit _privateMemoryAllocator = new PrivateMemoryAllocator(DefaultBlockAlignment, MemoryAllocationFlags.Mirrorable); _mappingTree = new AddressIntrusiveRedBlackTree(); _privateTree = new AddressIntrusiveRedBlackTree(); - _pageProtections = new AddressSpacePageProtections(); _treeLock = new ReaderWriterLockSlim(); _mappingTree.Add(new Mapping(address, size, MappingType.None)); @@ -228,11 +228,10 @@ namespace Ryujinx.Cpu.Jit _hostPageSize = MemoryBlock.GetPageSize(); _backingMemory = backingMemory; - _baseMemory = baseMemory; + _baseMemory = new(baseMemory); _cachedFirstPagePa = ulong.MaxValue; _cachedLastPagePa = ulong.MaxValue; - _lastPageProtection = MemoryPermission.ReadAndWrite; Address = address; Size = size; @@ -270,8 +269,6 @@ namespace Ryujinx.Cpu.Jit } Update(va, pa, size, MappingType.Private); - - _pageProtections.UpdateMappings(this, va, size); } public void Unmap(ulong va, ulong size) @@ -290,8 +287,6 @@ namespace Ryujinx.Cpu.Jit } Update(va, 0UL, size, MappingType.None); - - _pageProtections.Remove(va, size); } public void ReprotectAligned(ulong va, ulong size, MemoryPermission protection) @@ -301,18 +296,9 @@ namespace Ryujinx.Cpu.Jit _baseMemory.Reprotect(va - Address, size, protection, false); - if (va == EndAddress - _hostPageSize) + if (va == Address) { - // Protections at the last page also applies to the bridge, if we have one. - // (This is because last page access is always done on the bridge, not on our base mapping, - // for the cases where access crosses a page boundary and reaches the non-contiguous next mapping). - - if (_hasBridgeAtEnd) - { - _baseMemory.Reprotect(Size, size, protection, false); - } - - _lastPageProtection = protection; + _firstPageMemoryProtection = protection; } } @@ -320,63 +306,55 @@ namespace Ryujinx.Cpu.Jit ulong va, ulong size, MemoryPermission protection, - AddressSpacePartitionAllocator asAllocator, AddressSpacePartitioned addressSpace, - Action updatePtCallback) + Action updatePtCallback) { - ulong endVa = va + size; + if (_baseMemory.LazyInitMirrorForProtection(addressSpace, Address, Size, protection)) + { + LateMap(); + } - _pageProtections.Reprotect(asAllocator, addressSpace, this, va, endVa, protection, updatePtCallback); + updatePtCallback(va, _baseMemory.GetPointerForProtection(va - Address, size, protection), size); } - public IntPtr GetPointer(ulong va, ulong size) + public nint GetPointer(ulong va, ulong size) { Debug.Assert(va >= Address); Debug.Assert(va + size <= EndAddress); - if (va >= EndAddress - _hostPageSize && _hasBridgeAtEnd) - { - return _baseMemory.GetPointer(Size + va - (EndAddress - _hostPageSize), size); - } - return _baseMemory.GetPointer(va - Address, size); } - public void InsertBridgeAtEnd(AddressSpacePartition partitionAfter, Action updatePtCallback) + public void InsertBridgeAtEnd(AddressSpacePartition partitionAfter, bool useProtectionMirrors) { - ulong firstPagePa = partitionAfter._firstPagePa.HasValue ? partitionAfter._firstPagePa.Value : ulong.MaxValue; - ulong lastPagePa = _lastPagePa.HasValue ? _lastPagePa.Value : ulong.MaxValue; + ulong firstPagePa = partitionAfter?._firstPagePa ?? ulong.MaxValue; + ulong lastPagePa = _lastPagePa ?? ulong.MaxValue; if (firstPagePa != _cachedFirstPagePa || lastPagePa != _cachedLastPagePa) { - if (partitionAfter._firstPagePa.HasValue && _lastPagePa.HasValue) + if (partitionAfter != null && partitionAfter._firstPagePa.HasValue) { (MemoryBlock firstPageMemory, ulong firstPageOffset) = partitionAfter.GetFirstPageMemoryAndOffset(); - (MemoryBlock lastPageMemory, ulong lastPageOffset) = GetLastPageMemoryAndOffset(); - _baseMemory.MapView(lastPageMemory, lastPageOffset, Size, _hostPageSize); - _baseMemory.MapView(firstPageMemory, firstPageOffset, Size + _hostPageSize, _hostPageSize); + _baseMemory.MapView(firstPageMemory, firstPageOffset, Size, _hostPageSize); - _baseMemory.Reprotect(Size, _hostPageSize, _lastPageProtection, false); + if (!useProtectionMirrors) + { + _baseMemory.Reprotect(Size, _hostPageSize, partitionAfter._firstPageMemoryProtection, throwOnFail: false); + } - updatePtCallback(EndAddress - _hostPageSize, _baseMemory.GetPointer(Size, _hostPageSize), _hostPageSize); - - _hasBridgeAtEnd = true; - - _pageProtections.UpdateMappings(partitionAfter, EndAddress, GuestPageSize); + _firstPageMemoryForUnmap = firstPageMemory; + _firstPageOffsetForLateMap = firstPageOffset; } else { - if (_lastPagePa.HasValue) + MemoryBlock firstPageMemoryForUnmap = _firstPageMemoryForUnmap; + + if (firstPageMemoryForUnmap != null) { - (MemoryBlock lastPageMemory, ulong lastPageOffset) = GetLastPageMemoryAndOffset(); - - updatePtCallback(EndAddress - _hostPageSize, lastPageMemory.GetPointer(lastPageOffset, _hostPageSize), _hostPageSize); + _baseMemory.UnmapView(firstPageMemoryForUnmap, Size, _hostPageSize); + _firstPageMemoryForUnmap = null; } - - _hasBridgeAtEnd = false; - - _pageProtections.Remove(EndAddress, GuestPageSize); } _cachedFirstPagePa = firstPagePa; @@ -384,21 +362,12 @@ namespace Ryujinx.Cpu.Jit } } - public void RemoveBridgeFromEnd(Action updatePtCallback) + public void ReprotectBridge(MemoryPermission protection) { - if (_lastPagePa.HasValue) + if (_firstPageMemoryForUnmap != null) { - (MemoryBlock lastPageMemory, ulong lastPageOffset) = GetLastPageMemoryAndOffset(); - - updatePtCallback(EndAddress - _hostPageSize, lastPageMemory.GetPointer(lastPageOffset, _hostPageSize), _hostPageSize); + _baseMemory.Reprotect(Size, _hostPageSize, protection, throwOnFail: false); } - - _cachedFirstPagePa = ulong.MaxValue; - _cachedLastPagePa = ulong.MaxValue; - - _hasBridgeAtEnd = false; - - _pageProtections.Remove(EndAddress, GuestPageSize); } private (MemoryBlock, ulong) GetFirstPageMemoryAndOffset() @@ -422,29 +391,6 @@ namespace Ryujinx.Cpu.Jit return (_backingMemory, _firstPagePa.Value); } - private (MemoryBlock, ulong) GetLastPageMemoryAndOffset() - { - _treeLock.EnterReadLock(); - - try - { - ulong pageAddress = EndAddress - _hostPageSize; - - PrivateMapping map = _privateTree.GetNode(pageAddress); - - if (map != null && map.PrivateAllocation.IsValid) - { - return (map.PrivateAllocation.Memory, map.PrivateAllocation.Offset + (pageAddress - map.Address)); - } - } - finally - { - _treeLock.ExitReadLock(); - } - - return (_backingMemory, _lastPagePa.Value & ~(_hostPageSize - 1)); - } - public PrivateRange GetPrivateAllocation(ulong va) { _treeLock.EnterReadLock(); @@ -503,7 +449,7 @@ namespace Ryujinx.Cpu.Jit switch (type) { case MappingType.None: - ulong alignment = MemoryBlock.GetPageSize(); + ulong alignment = _hostPageSize; bool unmappedBefore = map.Predecessor == null || (map.Predecessor.Type == MappingType.None && map.Predecessor.Address <= BitUtils.AlignDown(va, alignment)); @@ -560,7 +506,7 @@ namespace Ryujinx.Cpu.Jit { ulong endAddress = va + size; - ulong alignment = MemoryBlock.GetPageSize(); + ulong alignment = _hostPageSize; // Expand the range outwards based on page size to ensure that at least the requested region is mapped. ulong vaAligned = BitUtils.AlignDown(va, alignment); @@ -584,7 +530,7 @@ namespace Ryujinx.Cpu.Jit map = newMap; } - map.Map(_baseMemory, Address, _privateMemoryAllocator.Allocate(map.Size, MemoryBlock.GetPageSize())); + map.Map(_baseMemory, Address, _privateMemoryAllocator.Allocate(map.Size, _hostPageSize)); } if (map.EndAddress >= endAddressAligned) @@ -598,7 +544,7 @@ namespace Ryujinx.Cpu.Jit { ulong endAddress = va + size; - ulong alignment = MemoryBlock.GetPageSize(); + ulong alignment = _hostPageSize; // If the adjacent mappings are unmapped, expand the range outwards, // otherwise shrink it inwards. We must ensure we won't unmap pages that might still be in use. @@ -665,6 +611,30 @@ namespace Ryujinx.Cpu.Jit return !left.PrivateAllocation.IsValid && !right.PrivateAllocation.IsValid; } + private void LateMap() + { + // Map all existing private allocations. + // This is necessary to ensure mirrors that are lazily created have the same mappings as the main one. + + PrivateMapping map = _privateTree.GetNode(Address); + + for (; map != null; map = map.Successor) + { + if (map.PrivateAllocation.IsValid) + { + _baseMemory.LateMapView(map.PrivateAllocation.Memory, map.PrivateAllocation.Offset, map.Address - Address, map.Size); + } + } + + MemoryBlock firstPageMemory = _firstPageMemoryForUnmap; + ulong firstPageOffset = _firstPageOffsetForLateMap; + + if (firstPageMemory != null) + { + _baseMemory.LateMapView(firstPageMemory, firstPageOffset, Size, _hostPageSize); + } + } + public PrivateRange GetFirstPrivateAllocation(ulong va, ulong size, out ulong nextVa) { _treeLock.EnterReadLock(); @@ -695,25 +665,28 @@ namespace Ryujinx.Cpu.Jit public bool HasPrivateAllocation(ulong va, ulong size, ulong startVa, ulong startSize, ref PrivateRange range) { + ulong endVa = va + size; + _treeLock.EnterReadLock(); try { - PrivateMapping map = _privateTree.GetNode(va); - - if (map != null && map.PrivateAllocation.IsValid) + for (PrivateMapping map = _privateTree.GetNode(va); map != null && map.Address < endVa; map = map.Successor) { - if (map.Address <= startVa && map.EndAddress >= startVa + startSize) + if (map.PrivateAllocation.IsValid) { - ulong startOffset = startVa - map.Address; + if (map.Address <= startVa && map.EndAddress >= startVa + startSize) + { + ulong startOffset = startVa - map.Address; - range = new( - map.PrivateAllocation.Memory, - map.PrivateAllocation.Offset + startOffset, - Math.Min(map.PrivateAllocation.Size - startOffset, startSize)); + range = new( + map.PrivateAllocation.Memory, + map.PrivateAllocation.Offset + startOffset, + Math.Min(map.PrivateAllocation.Size - startOffset, startSize)); + } + + return true; } - - return true; } } finally @@ -729,7 +702,6 @@ namespace Ryujinx.Cpu.Jit GC.SuppressFinalize(this); _privateMemoryAllocator.Dispose(); - _pageProtections.Dispose(); _baseMemory.Dispose(); } } diff --git a/src/Ryujinx.Cpu/Jit/AddressSpacePartitionAllocator.cs b/src/Ryujinx.Cpu/Jit/HostTracked/AddressSpacePartitionAllocator.cs similarity index 73% rename from src/Ryujinx.Cpu/Jit/AddressSpacePartitionAllocator.cs rename to src/Ryujinx.Cpu/Jit/HostTracked/AddressSpacePartitionAllocator.cs index 244639457..a49e0179d 100644 --- a/src/Ryujinx.Cpu/Jit/AddressSpacePartitionAllocator.cs +++ b/src/Ryujinx.Cpu/Jit/HostTracked/AddressSpacePartitionAllocator.cs @@ -1,16 +1,19 @@ +using Ryujinx.Common; using Ryujinx.Common.Collections; using Ryujinx.Memory; using Ryujinx.Memory.Tracking; using System; -namespace Ryujinx.Cpu.Jit +namespace Ryujinx.Cpu.Jit.HostTracked { readonly struct AddressSpacePartitionAllocation : IDisposable { private readonly AddressSpacePartitionAllocator _owner; private readonly PrivateMemoryAllocatorImpl.Allocation _allocation; - public IntPtr Pointer => (IntPtr)((ulong)_allocation.Block.Memory.Pointer + _allocation.Offset); + public nint Pointer => (nint)((ulong)_allocation.Block.Memory.Pointer + _allocation.Offset); + + public bool IsValid => _owner != null; public AddressSpacePartitionAllocation( AddressSpacePartitionAllocator owner, @@ -20,9 +23,9 @@ namespace Ryujinx.Cpu.Jit _allocation = allocation; } - public void RegisterMapping(ulong va, ulong endVa, int bridgeSize) + public void RegisterMapping(ulong va, ulong endVa) { - _allocation.Block.AddMapping(_allocation.Offset, _allocation.Size, va, endVa, bridgeSize); + _allocation.Block.AddMapping(_allocation.Offset, _allocation.Size, va, endVa); } public void MapView(MemoryBlock srcBlock, ulong srcOffset, ulong dstOffset, ulong size) @@ -40,7 +43,7 @@ namespace Ryujinx.Cpu.Jit _allocation.Block.Memory.Reprotect(_allocation.Offset + offset, size, permission, throwOnFail); } - public IntPtr GetPointer(ulong offset, ulong size) + public nint GetPointer(ulong offset, ulong size) { return _allocation.Block.Memory.GetPointer(_allocation.Offset + offset, size); } @@ -59,6 +62,7 @@ namespace Ryujinx.Cpu.Jit public class Block : PrivateMemoryAllocator.Block { private readonly MemoryTracking _tracking; + private readonly Func _readPtCallback; private readonly MemoryEhMeilleure _memoryEh; private class Mapping : IntrusiveRedBlackTreeNode, IComparable, IComparable @@ -68,15 +72,13 @@ namespace Ryujinx.Cpu.Jit public ulong EndAddress => Address + Size; public ulong Va { get; } public ulong EndVa { get; } - public int BridgeSize { get; } - public Mapping(ulong address, ulong size, ulong va, ulong endVa, int bridgeSize) + public Mapping(ulong address, ulong size, ulong va, ulong endVa) { Address = address; Size = size; Va = va; EndVa = endVa; - BridgeSize = bridgeSize; } public int CompareTo(Mapping other) @@ -115,17 +117,18 @@ namespace Ryujinx.Cpu.Jit private readonly AddressIntrusiveRedBlackTree _mappingTree; private readonly object _lock; - public Block(MemoryTracking tracking, MemoryBlock memory, ulong size, object locker) : base(memory, size) + public Block(MemoryTracking tracking, Func readPtCallback, MemoryBlock memory, ulong size, object locker) : base(memory, size) { _tracking = tracking; + _readPtCallback = readPtCallback; _memoryEh = new(memory, null, tracking, VirtualMemoryEvent); _mappingTree = new(); _lock = locker; } - public void AddMapping(ulong offset, ulong size, ulong va, ulong endVa, int bridgeSize) + public void AddMapping(ulong offset, ulong size, ulong va, ulong endVa) { - _mappingTree.Add(new(offset, size, va, endVa, bridgeSize)); + _mappingTree.Add(new(offset, size, va, endVa)); } public void RemoveMapping(ulong offset, ulong size) @@ -133,7 +136,7 @@ namespace Ryujinx.Cpu.Jit _mappingTree.Remove(_mappingTree.GetNode(offset)); } - private bool VirtualMemoryEvent(ulong address, ulong size, bool write) + private ulong VirtualMemoryEvent(ulong address, ulong size, bool write) { Mapping map; @@ -144,17 +147,21 @@ namespace Ryujinx.Cpu.Jit if (map == null) { - return false; + return 0; } address -= map.Address; - if (address >= (map.EndVa - map.Va)) + ulong addressAligned = BitUtils.AlignDown(address, AddressSpacePartition.GuestPageSize); + ulong endAddressAligned = BitUtils.AlignUp(address + size, AddressSpacePartition.GuestPageSize); + ulong sizeAligned = endAddressAligned - addressAligned; + + if (!_tracking.VirtualMemoryEvent(map.Va + addressAligned, sizeAligned, write)) { - address -= (ulong)(map.BridgeSize / 2); + return 0; } - return _tracking.VirtualMemoryEvent(map.Va + address, size, write); + return _readPtCallback(map.Va + address); } public override void Destroy() @@ -166,33 +173,30 @@ namespace Ryujinx.Cpu.Jit } private readonly MemoryTracking _tracking; + private readonly Func _readPtCallback; private readonly object _lock; - public AddressSpacePartitionAllocator(MemoryTracking tracking, object locker) : base(DefaultBlockAlignment, MemoryAllocationFlags.Reserve | MemoryAllocationFlags.ViewCompatible) + public AddressSpacePartitionAllocator( + MemoryTracking tracking, + Func readPtCallback, + object locker) : base(DefaultBlockAlignment, MemoryAllocationFlags.Reserve | MemoryAllocationFlags.ViewCompatible) { _tracking = tracking; + _readPtCallback = readPtCallback; _lock = locker; } - public AddressSpacePartitionAllocation Allocate(ulong va, ulong size, int bridgeSize) - { - AddressSpacePartitionAllocation allocation = new(this, Allocate(size + (ulong)bridgeSize, MemoryBlock.GetPageSize(), CreateBlock)); - allocation.RegisterMapping(va, va + size, bridgeSize); - - return allocation; - } - - public AddressSpacePartitionAllocation AllocatePage(ulong va, ulong size) + public AddressSpacePartitionAllocation Allocate(ulong va, ulong size) { AddressSpacePartitionAllocation allocation = new(this, Allocate(size, MemoryBlock.GetPageSize(), CreateBlock)); - allocation.RegisterMapping(va, va + size, 0); + allocation.RegisterMapping(va, va + size); return allocation; } private Block CreateBlock(MemoryBlock memory, ulong size) { - return new Block(_tracking, memory, size, _lock); + return new Block(_tracking, _readPtCallback, memory, size, _lock); } } -} \ No newline at end of file +} diff --git a/src/Ryujinx.Cpu/Jit/HostTracked/AddressSpacePartitionMultiAllocation.cs b/src/Ryujinx.Cpu/Jit/HostTracked/AddressSpacePartitionMultiAllocation.cs new file mode 100644 index 000000000..db1f3ea4b --- /dev/null +++ b/src/Ryujinx.Cpu/Jit/HostTracked/AddressSpacePartitionMultiAllocation.cs @@ -0,0 +1,101 @@ +using Ryujinx.Memory; +using System; +using System.Diagnostics; + +namespace Ryujinx.Cpu.Jit.HostTracked +{ + class AddressSpacePartitionMultiAllocation : IDisposable + { + private readonly AddressSpacePartitionAllocation _baseMemory; + private AddressSpacePartitionAllocation _baseMemoryRo; + private AddressSpacePartitionAllocation _baseMemoryNone; + + public AddressSpacePartitionMultiAllocation(AddressSpacePartitionAllocation baseMemory) + { + _baseMemory = baseMemory; + } + + public void MapView(MemoryBlock srcBlock, ulong srcOffset, ulong dstOffset, ulong size) + { + _baseMemory.MapView(srcBlock, srcOffset, dstOffset, size); + + if (_baseMemoryRo.IsValid) + { + _baseMemoryRo.MapView(srcBlock, srcOffset, dstOffset, size); + _baseMemoryRo.Reprotect(dstOffset, size, MemoryPermission.Read, false); + } + } + + public void LateMapView(MemoryBlock srcBlock, ulong srcOffset, ulong dstOffset, ulong size) + { + _baseMemoryRo.MapView(srcBlock, srcOffset, dstOffset, size); + _baseMemoryRo.Reprotect(dstOffset, size, MemoryPermission.Read, false); + } + + public void UnmapView(MemoryBlock srcBlock, ulong offset, ulong size) + { + _baseMemory.UnmapView(srcBlock, offset, size); + + if (_baseMemoryRo.IsValid) + { + _baseMemoryRo.UnmapView(srcBlock, offset, size); + } + } + + public void Reprotect(ulong offset, ulong size, MemoryPermission permission, bool throwOnFail) + { + _baseMemory.Reprotect(offset, size, permission, throwOnFail); + } + + public nint GetPointer(ulong offset, ulong size) + { + return _baseMemory.GetPointer(offset, size); + } + + public bool LazyInitMirrorForProtection(AddressSpacePartitioned addressSpace, ulong blockAddress, ulong blockSize, MemoryPermission permission) + { + if (permission == MemoryPermission.None && !_baseMemoryNone.IsValid) + { + _baseMemoryNone = addressSpace.CreateAsPartitionAllocation(blockAddress, blockSize); + } + else if (permission == MemoryPermission.Read && !_baseMemoryRo.IsValid) + { + _baseMemoryRo = addressSpace.CreateAsPartitionAllocation(blockAddress, blockSize); + + return true; + } + + return false; + } + + public nint GetPointerForProtection(ulong offset, ulong size, MemoryPermission permission) + { + AddressSpacePartitionAllocation allocation = permission switch + { + MemoryPermission.ReadAndWrite => _baseMemory, + MemoryPermission.Read => _baseMemoryRo, + MemoryPermission.None => _baseMemoryNone, + _ => throw new ArgumentException($"Invalid protection \"{permission}\"."), + }; + + Debug.Assert(allocation.IsValid); + + return allocation.GetPointer(offset, size); + } + + public void Dispose() + { + _baseMemory.Dispose(); + + if (_baseMemoryRo.IsValid) + { + _baseMemoryRo.Dispose(); + } + + if (_baseMemoryNone.IsValid) + { + _baseMemoryNone.Dispose(); + } + } + } +} diff --git a/src/Ryujinx.Cpu/Jit/AddressSpacePartitioned.cs b/src/Ryujinx.Cpu/Jit/HostTracked/AddressSpacePartitioned.cs similarity index 76% rename from src/Ryujinx.Cpu/Jit/AddressSpacePartitioned.cs rename to src/Ryujinx.Cpu/Jit/HostTracked/AddressSpacePartitioned.cs index 18e3272c1..e3cb75f64 100644 --- a/src/Ryujinx.Cpu/Jit/AddressSpacePartitioned.cs +++ b/src/Ryujinx.Cpu/Jit/HostTracked/AddressSpacePartitioned.cs @@ -5,26 +5,26 @@ using System; using System.Collections.Generic; using System.Diagnostics; -namespace Ryujinx.Cpu.Jit +namespace Ryujinx.Cpu.Jit.HostTracked { class AddressSpacePartitioned : IDisposable { - public static readonly bool Use4KBProtection = true; - private const int PartitionBits = 25; private const ulong PartitionSize = 1UL << PartitionBits; private readonly MemoryBlock _backingMemory; private readonly List _partitions; private readonly AddressSpacePartitionAllocator _asAllocator; - private readonly Action _updatePtCallback; + private readonly Action _updatePtCallback; + private readonly bool _useProtectionMirrors; - public AddressSpacePartitioned(MemoryTracking tracking, MemoryBlock backingMemory, Action updatePtCallback) + public AddressSpacePartitioned(MemoryTracking tracking, MemoryBlock backingMemory, NativePageTable nativePageTable, bool useProtectionMirrors) { _backingMemory = backingMemory; _partitions = new(); - _asAllocator = new(tracking, _partitions); - _updatePtCallback = updatePtCallback; + _asAllocator = new(tracking, nativePageTable.Read, _partitions); + _updatePtCallback = nativePageTable.Update; + _useProtectionMirrors = useProtectionMirrors; } public void Map(ulong va, ulong pa, ulong size) @@ -49,7 +49,7 @@ namespace Ryujinx.Cpu.Jit va += currentSize; pa += currentSize; - InsertBridgeIfNeeded(partitionIndex); + InsertOrRemoveBridgeIfNeeded(partitionIndex); } } } @@ -80,7 +80,7 @@ namespace Ryujinx.Cpu.Jit va += clampedEndVa - clampedVa; - RemoveBridgeIfNeeded(partitionIndex); + InsertOrRemoveBridgeIfNeeded(partitionIndex); if (partition.IsEmpty()) { @@ -95,34 +95,11 @@ namespace Ryujinx.Cpu.Jit { ulong endVa = va + size; - if (Use4KBProtection) - { - lock (_partitions) - { - while (va < endVa) - { - AddressSpacePartition partition = FindPartition(va); - - if (partition == null) - { - va += PartitionSize - (va & (PartitionSize - 1)); - - continue; - } - - (ulong clampedVa, ulong clampedEndVa) = ClampRange(partition, va, endVa); - - partition.Reprotect(clampedVa, clampedEndVa - clampedVa, protection, _asAllocator, this, _updatePtCallback); - - va += clampedEndVa - clampedVa; - } - } - } - else + lock (_partitions) { while (va < endVa) { - AddressSpacePartition partition = FindPartition(va); + AddressSpacePartition partition = FindPartitionWithIndex(va, out int partitionIndex); if (partition == null) { @@ -133,7 +110,21 @@ namespace Ryujinx.Cpu.Jit (ulong clampedVa, ulong clampedEndVa) = ClampRange(partition, va, endVa); - partition.ReprotectAligned(clampedVa, clampedEndVa - clampedVa, protection); + if (_useProtectionMirrors) + { + partition.Reprotect(clampedVa, clampedEndVa - clampedVa, protection, this, _updatePtCallback); + } + else + { + partition.ReprotectAligned(clampedVa, clampedEndVa - clampedVa, protection); + + if (clampedVa == partition.Address && + partitionIndex > 0 && + _partitions[partitionIndex - 1].EndAddress == partition.Address) + { + _partitions[partitionIndex - 1].ReprotectBridge(protection); + } + } va += clampedEndVa - clampedVa; } @@ -197,37 +188,31 @@ namespace Ryujinx.Cpu.Jit return false; } - private void InsertBridgeIfNeeded(int partitionIndex) + private void InsertOrRemoveBridgeIfNeeded(int partitionIndex) { - if (partitionIndex > 0 && _partitions[partitionIndex - 1].EndAddress == _partitions[partitionIndex].Address) + if (partitionIndex > 0) { - _partitions[partitionIndex - 1].InsertBridgeAtEnd(_partitions[partitionIndex], _updatePtCallback); + if (_partitions[partitionIndex - 1].EndAddress == _partitions[partitionIndex].Address) + { + _partitions[partitionIndex - 1].InsertBridgeAtEnd(_partitions[partitionIndex], _useProtectionMirrors); + } + else + { + _partitions[partitionIndex - 1].InsertBridgeAtEnd(null, _useProtectionMirrors); + } } if (partitionIndex + 1 < _partitions.Count && _partitions[partitionIndex].EndAddress == _partitions[partitionIndex + 1].Address) { - _partitions[partitionIndex].InsertBridgeAtEnd(_partitions[partitionIndex + 1], _updatePtCallback); - } - } - - private void RemoveBridgeIfNeeded(int partitionIndex) - { - if (partitionIndex > 0 && _partitions[partitionIndex - 1].EndAddress == _partitions[partitionIndex].Address) - { - _partitions[partitionIndex - 1].InsertBridgeAtEnd(_partitions[partitionIndex], _updatePtCallback); - } - - if (partitionIndex + 1 < _partitions.Count && _partitions[partitionIndex].EndAddress == _partitions[partitionIndex + 1].Address) - { - _partitions[partitionIndex].InsertBridgeAtEnd(_partitions[partitionIndex + 1], _updatePtCallback); + _partitions[partitionIndex].InsertBridgeAtEnd(_partitions[partitionIndex + 1], _useProtectionMirrors); } else { - _partitions[partitionIndex].RemoveBridgeFromEnd(_updatePtCallback); + _partitions[partitionIndex].InsertBridgeAtEnd(null, _useProtectionMirrors); } } - public IntPtr GetPointer(ulong va, ulong size) + public nint GetPointer(ulong va, ulong size) { AddressSpacePartition partition = FindPartition(va); @@ -263,6 +248,20 @@ namespace Ryujinx.Cpu.Jit return null; } + private AddressSpacePartition FindPartitionWithIndex(ulong va, out int index) + { + lock (_partitions) + { + index = FindPartitionIndexLocked(va); + if (index >= 0) + { + return _partitions[index]; + } + } + + return null; + } + private int FindPartitionIndexLocked(ulong va) { int left = 0; @@ -332,7 +331,7 @@ namespace Ryujinx.Cpu.Jit gapSize = endVa - partition.EndAddress; } - _partitions.Insert(i + 1, new(CreateAsPartitionAllocation(partition.EndAddress, gapSize), _backingMemory, partition.EndAddress, gapSize)); + _partitions.Insert(i + 1, CreateAsPartition(partition.EndAddress, gapSize)); va = partition.EndAddress + gapSize; i++; } @@ -351,7 +350,7 @@ namespace Ryujinx.Cpu.Jit gapSize = endVa - va; } - _partitions.Insert(i, new(CreateAsPartitionAllocation(va, gapSize), _backingMemory, va, gapSize)); + _partitions.Insert(i, CreateAsPartition(va, gapSize)); va = Math.Min(partition.EndAddress, endVa); i++; } @@ -359,9 +358,15 @@ namespace Ryujinx.Cpu.Jit if (va < endVa) { - _partitions.Add(new(CreateAsPartitionAllocation(va, endVa - va), _backingMemory, va, endVa - va)); + _partitions.Add(CreateAsPartition(va, endVa - va)); } + ValidatePartitionList(); + } + + [Conditional("DEBUG")] + private void ValidatePartitionList() + { for (int i = 1; i < _partitions.Count; i++) { Debug.Assert(_partitions[i].Address > _partitions[i - 1].Address); @@ -369,11 +374,14 @@ namespace Ryujinx.Cpu.Jit } } - private AddressSpacePartitionAllocation CreateAsPartitionAllocation(ulong va, ulong size) + private AddressSpacePartition CreateAsPartition(ulong va, ulong size) { - ulong bridgeSize = MemoryBlock.GetPageSize() * 2; + return new(CreateAsPartitionAllocation(va, size), _backingMemory, va, size); + } - return _asAllocator.Allocate(va, size, (int)bridgeSize); + public AddressSpacePartitionAllocation CreateAsPartitionAllocation(ulong va, ulong size) + { + return _asAllocator.Allocate(va, size + MemoryBlock.GetPageSize()); } protected virtual void Dispose(bool disposing) @@ -396,4 +404,4 @@ namespace Ryujinx.Cpu.Jit GC.SuppressFinalize(this); } } -} \ No newline at end of file +} diff --git a/src/Ryujinx.Cpu/Jit/HostTracked/NativePageTable.cs b/src/Ryujinx.Cpu/Jit/HostTracked/NativePageTable.cs new file mode 100644 index 000000000..aa663d7d9 --- /dev/null +++ b/src/Ryujinx.Cpu/Jit/HostTracked/NativePageTable.cs @@ -0,0 +1,223 @@ +using Ryujinx.Cpu.Signal; +using Ryujinx.Memory; +using System; +using System.Diagnostics; +using System.Numerics; +using System.Runtime.InteropServices; + +namespace Ryujinx.Cpu.Jit.HostTracked +{ + sealed class NativePageTable : IDisposable + { + private delegate ulong TrackingEventDelegate(ulong address, ulong size, bool write); + + private const int PageBits = 12; + private const int PageSize = 1 << PageBits; + private const int PageMask = PageSize - 1; + + private const int PteSize = 8; + + private readonly int _bitsPerPtPage; + private readonly int _entriesPerPtPage; + private readonly int _pageCommitmentBits; + + private readonly PageTable _pageTable; + private readonly MemoryBlock _nativePageTable; + private readonly ulong[] _pageCommitmentBitmap; + private readonly ulong _hostPageSize; + + private readonly TrackingEventDelegate _trackingEvent; + + private bool _disposed; + + public nint PageTablePointer => _nativePageTable.Pointer; + + public NativePageTable(ulong asSize) + { + ulong hostPageSize = MemoryBlock.GetPageSize(); + + _entriesPerPtPage = (int)(hostPageSize / sizeof(ulong)); + _bitsPerPtPage = BitOperations.Log2((uint)_entriesPerPtPage); + _pageCommitmentBits = PageBits + _bitsPerPtPage; + + _hostPageSize = hostPageSize; + _pageTable = new PageTable(); + _nativePageTable = new MemoryBlock((asSize / PageSize) * PteSize + _hostPageSize, MemoryAllocationFlags.Reserve); + _pageCommitmentBitmap = new ulong[(asSize >> _pageCommitmentBits) / (sizeof(ulong) * 8)]; + + ulong ptStart = (ulong)_nativePageTable.Pointer; + ulong ptEnd = ptStart + _nativePageTable.Size; + + _trackingEvent = VirtualMemoryEvent; + + bool added = NativeSignalHandler.AddTrackedRegion((nuint)ptStart, (nuint)ptEnd, Marshal.GetFunctionPointerForDelegate(_trackingEvent)); + + if (!added) + { + throw new InvalidOperationException("Number of allowed tracked regions exceeded."); + } + } + + public void Map(ulong va, ulong pa, ulong size, AddressSpacePartitioned addressSpace, MemoryBlock backingMemory, bool privateMap) + { + while (size != 0) + { + _pageTable.Map(va, pa); + + EnsureCommitment(va); + + if (privateMap) + { + _nativePageTable.Write((va / PageSize) * PteSize, GetPte(va, addressSpace.GetPointer(va, PageSize))); + } + else + { + _nativePageTable.Write((va / PageSize) * PteSize, GetPte(va, backingMemory.GetPointer(pa, PageSize))); + } + + va += PageSize; + pa += PageSize; + size -= PageSize; + } + } + + public void Unmap(ulong va, ulong size) + { + nint guardPagePtr = GetGuardPagePointer(); + + while (size != 0) + { + _pageTable.Unmap(va); + _nativePageTable.Write((va / PageSize) * PteSize, GetPte(va, guardPagePtr)); + + va += PageSize; + size -= PageSize; + } + } + + public ulong Read(ulong va) + { + ulong pte = _nativePageTable.Read((va / PageSize) * PteSize); + + pte += va & ~(ulong)PageMask; + + return pte + (va & PageMask); + } + + public void Update(ulong va, nint ptr, ulong size) + { + ulong remainingSize = size; + + while (remainingSize != 0) + { + EnsureCommitment(va); + + _nativePageTable.Write((va / PageSize) * PteSize, GetPte(va, ptr)); + + va += PageSize; + ptr += PageSize; + remainingSize -= PageSize; + } + } + + private void EnsureCommitment(ulong va) + { + ulong bit = va >> _pageCommitmentBits; + + int index = (int)(bit / (sizeof(ulong) * 8)); + int shift = (int)(bit % (sizeof(ulong) * 8)); + + ulong mask = 1UL << shift; + + ulong oldMask = _pageCommitmentBitmap[index]; + + if ((oldMask & mask) == 0) + { + lock (_pageCommitmentBitmap) + { + oldMask = _pageCommitmentBitmap[index]; + + if ((oldMask & mask) != 0) + { + return; + } + + _nativePageTable.Commit(bit * _hostPageSize, _hostPageSize); + + Span pageSpan = MemoryMarshal.Cast(_nativePageTable.GetSpan(bit * _hostPageSize, (int)_hostPageSize)); + + Debug.Assert(pageSpan.Length == _entriesPerPtPage); + + nint guardPagePtr = GetGuardPagePointer(); + + for (int i = 0; i < pageSpan.Length; i++) + { + pageSpan[i] = GetPte((bit << _pageCommitmentBits) | ((ulong)i * PageSize), guardPagePtr); + } + + _pageCommitmentBitmap[index] = oldMask | mask; + } + } + } + + private nint GetGuardPagePointer() + { + return _nativePageTable.GetPointer(_nativePageTable.Size - _hostPageSize, _hostPageSize); + } + + private static ulong GetPte(ulong va, nint ptr) + { + Debug.Assert((va & PageMask) == 0); + + return (ulong)ptr - va; + } + + public ulong GetPhysicalAddress(ulong va) + { + return _pageTable.Read(va) + (va & PageMask); + } + + private ulong VirtualMemoryEvent(ulong address, ulong size, bool write) + { + if (address < _nativePageTable.Size - _hostPageSize) + { + // Some prefetch instructions do not cause faults with invalid addresses. + // Retry if we are hitting a case where the page table is unmapped, the next + // run will execute the actual instruction. + // The address loaded from the page table will be invalid, and it should hit the else case + // if the instruction faults on unmapped or protected memory. + + ulong va = address * (PageSize / sizeof(ulong)); + + EnsureCommitment(va); + + return (ulong)_nativePageTable.Pointer + address; + } + else + { + throw new InvalidMemoryRegionException(); + } + } + + private void Dispose(bool disposing) + { + if (!_disposed) + { + if (disposing) + { + NativeSignalHandler.RemoveTrackedRegion((nuint)_nativePageTable.Pointer); + + _nativePageTable.Dispose(); + } + + _disposed = true; + } + } + + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } +} diff --git a/src/Ryujinx.Cpu/Jit/JitCpuContext.cs b/src/Ryujinx.Cpu/Jit/JitCpuContext.cs index a5944097d..0793f382d 100644 --- a/src/Ryujinx.Cpu/Jit/JitCpuContext.cs +++ b/src/Ryujinx.Cpu/Jit/JitCpuContext.cs @@ -1,5 +1,8 @@ +using ARMeilleure.Common; using ARMeilleure.Memory; using ARMeilleure.Translation; +using Ryujinx.Cpu.Signal; +using Ryujinx.Memory; namespace Ryujinx.Cpu.Jit { @@ -7,11 +10,19 @@ namespace Ryujinx.Cpu.Jit { private readonly ITickSource _tickSource; private readonly Translator _translator; + private readonly AddressTable _functionTable; public JitCpuContext(ITickSource tickSource, IMemoryManager memory, bool for64Bit) { _tickSource = tickSource; - _translator = new Translator(new JitMemoryAllocator(forJit: true), memory, for64Bit); + _functionTable = AddressTable.CreateForArm(for64Bit, memory.Type); + _translator = new Translator(new JitMemoryAllocator(forJit: true), memory, _functionTable); + + if (memory.Type.IsHostMappedOrTracked()) + { + NativeSignalHandler.InitializeSignalHandler(); + } + memory.UnmapEvent += UnmapHandler; } @@ -39,14 +50,15 @@ namespace Ryujinx.Cpu.Jit } /// - public IDiskCacheLoadState LoadDiskCache(string titleIdText, string displayVersion, bool enabled) + public IDiskCacheLoadState LoadDiskCache(string titleIdText, string displayVersion, bool enabled, string cacheSelector) { - return new JitDiskCacheLoadState(_translator.LoadDiskCache(titleIdText, displayVersion, enabled)); + return new JitDiskCacheLoadState(_translator.LoadDiskCache(titleIdText, displayVersion, enabled, cacheSelector)); } /// public void PrepareCodeRange(ulong address, ulong size) { + _functionTable.SignalCodeRange(address, size); _translator.PrepareCodeRange(address, size); } diff --git a/src/Ryujinx.Cpu/Jit/JitMemoryAllocator.cs b/src/Ryujinx.Cpu/Jit/JitMemoryAllocator.cs index 926dd8a0c..06c11b7b9 100644 --- a/src/Ryujinx.Cpu/Jit/JitMemoryAllocator.cs +++ b/src/Ryujinx.Cpu/Jit/JitMemoryAllocator.cs @@ -14,7 +14,5 @@ namespace Ryujinx.Cpu.Jit public IJitMemoryBlock Allocate(ulong size) => new JitMemoryBlock(size, MemoryAllocationFlags.None); public IJitMemoryBlock Reserve(ulong size) => new JitMemoryBlock(size, MemoryAllocationFlags.Reserve | _jitFlag); - - public ulong GetPageSize() => MemoryBlock.GetPageSize(); } } diff --git a/src/Ryujinx.Cpu/Jit/JitMemoryBlock.cs b/src/Ryujinx.Cpu/Jit/JitMemoryBlock.cs index bd07d349c..0311db565 100644 --- a/src/Ryujinx.Cpu/Jit/JitMemoryBlock.cs +++ b/src/Ryujinx.Cpu/Jit/JitMemoryBlock.cs @@ -8,7 +8,7 @@ namespace Ryujinx.Cpu.Jit { private readonly MemoryBlock _impl; - public IntPtr Pointer => _impl.Pointer; + public nint Pointer => _impl.Pointer; public JitMemoryBlock(ulong size, MemoryAllocationFlags flags) { diff --git a/src/Ryujinx.Cpu/Jit/MemoryManager.cs b/src/Ryujinx.Cpu/Jit/MemoryManager.cs index b9a547025..049e508d0 100644 --- a/src/Ryujinx.Cpu/Jit/MemoryManager.cs +++ b/src/Ryujinx.Cpu/Jit/MemoryManager.cs @@ -3,6 +3,7 @@ using Ryujinx.Memory; using Ryujinx.Memory.Range; using Ryujinx.Memory.Tracking; using System; +using System.Buffers; using System.Collections.Generic; using System.Linq; using System.Runtime.CompilerServices; @@ -14,12 +15,8 @@ namespace Ryujinx.Cpu.Jit /// /// Represents a CPU memory manager. /// - public sealed class MemoryManager : MemoryManagerBase, IMemoryManager, IVirtualMemoryManagerTracked, IWritableBlock + public sealed class MemoryManager : VirtualMemoryManagerRefCountedBase, IMemoryManager, IVirtualMemoryManagerTracked { - public const int PageBits = 12; - public const int PageSize = 1 << PageBits; - public const int PageMask = PageSize - 1; - private const int PteSize = 8; private const int PointerTagBit = 62; @@ -28,21 +25,21 @@ namespace Ryujinx.Cpu.Jit private readonly InvalidAccessHandler _invalidAccessHandler; /// - public bool Supports4KBPages => true; + public bool UsesPrivateAllocations => false; /// /// Address space width in bits. /// public int AddressSpaceBits { get; } - private readonly ulong _addressSpaceSize; - private readonly MemoryBlock _pageTable; + private readonly ManagedPageFlags _pages; + /// /// Page table base pointer. /// - public IntPtr PageTablePointer => _pageTable.Pointer; + public nint PageTablePointer => _pageTable.Pointer; public MemoryManagerType Type => MemoryManagerType.SoftwarePageTable; @@ -50,6 +47,8 @@ namespace Ryujinx.Cpu.Jit public event Action UnmapEvent; + protected override ulong AddressSpaceSize { get; } + /// /// Creates a new instance of the memory manager. /// @@ -71,9 +70,11 @@ namespace Ryujinx.Cpu.Jit } AddressSpaceBits = asBits; - _addressSpaceSize = asSize; + AddressSpaceSize = asSize; _pageTable = new MemoryBlock((asSize / PageSize) * PteSize); + _pages = new ManagedPageFlags(AddressSpaceBits); + Tracking = new MemoryTracking(this, PageSize); } @@ -93,15 +94,10 @@ namespace Ryujinx.Cpu.Jit remainingSize -= PageSize; } + _pages.AddMapping(oVa, size); Tracking.Map(oVa, size); } - /// - public void MapForeign(ulong va, nuint hostPointer, ulong size) - { - throw new NotSupportedException(); - } - /// public void Unmap(ulong va, ulong size) { @@ -115,6 +111,7 @@ namespace Ryujinx.Cpu.Jit UnmapEvent?.Invoke(va, size); Tracking.Unmap(va, size); + _pages.RemoveMapping(va, size); ulong remainingSize = size; while (remainingSize != 0) @@ -126,18 +123,29 @@ namespace Ryujinx.Cpu.Jit } } - /// - public T Read(ulong va) where T : unmanaged - { - return MemoryMarshal.Cast(GetSpan(va, Unsafe.SizeOf()))[0]; - } - - /// - public T ReadTracked(ulong va) where T : unmanaged + public override T ReadTracked(ulong va) { try { - SignalMemoryTracking(va, (ulong)Unsafe.SizeOf(), false); + return base.ReadTracked(va); + } + catch (InvalidMemoryRegionException) + { + if (_invalidAccessHandler == null || !_invalidAccessHandler(va)) + { + throw; + } + + return default; + } + } + + /// + public T ReadGuest(ulong va) where T : unmanaged + { + try + { + SignalMemoryTrackingImpl(va, (ulong)Unsafe.SizeOf(), false, true); return Read(va); } @@ -153,112 +161,26 @@ namespace Ryujinx.Cpu.Jit } /// - public void Read(ulong va, Span data) - { - ReadImpl(va, data); - } - - /// - public void Write(ulong va, T value) where T : unmanaged - { - Write(va, MemoryMarshal.Cast(MemoryMarshal.CreateSpan(ref value, 1))); - } - - /// - public void Write(ulong va, ReadOnlySpan data) - { - if (data.Length == 0) - { - return; - } - - SignalMemoryTracking(va, (ulong)data.Length, true); - - WriteImpl(va, data); - } - - /// - public void WriteUntracked(ulong va, ReadOnlySpan data) - { - if (data.Length == 0) - { - return; - } - - WriteImpl(va, data); - } - - /// - public bool WriteWithRedundancyCheck(ulong va, ReadOnlySpan data) - { - if (data.Length == 0) - { - return false; - } - - SignalMemoryTracking(va, (ulong)data.Length, false); - - if (IsContiguousAndMapped(va, data.Length)) - { - var target = _backingMemory.GetSpan(GetPhysicalAddressInternal(va), data.Length); - - bool changed = !data.SequenceEqual(target); - - if (changed) - { - data.CopyTo(target); - } - - return changed; - } - else - { - WriteImpl(va, data); - - return true; - } - } - - /// - /// Writes data to CPU mapped memory. - /// - /// Virtual address to write the data into - /// Data to be written - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void WriteImpl(ulong va, ReadOnlySpan data) + public override void Read(ulong va, Span data) { try { - AssertValidAddressAndSize(va, (ulong)data.Length); - - if (IsContiguousAndMapped(va, data.Length)) + base.Read(va, data); + } + catch (InvalidMemoryRegionException) + { + if (_invalidAccessHandler == null || !_invalidAccessHandler(va)) { - data.CopyTo(_backingMemory.GetSpan(GetPhysicalAddressInternal(va), data.Length)); + throw; } - else - { - int offset = 0, size; + } + } - if ((va & PageMask) != 0) - { - ulong pa = GetPhysicalAddressInternal(va); - - size = Math.Min(data.Length, PageSize - (int)(va & PageMask)); - - data[..size].CopyTo(_backingMemory.GetSpan(pa, size)); - - offset += size; - } - - for (; offset < data.Length; offset += size) - { - ulong pa = GetPhysicalAddressInternal(va + (ulong)offset); - - size = Math.Min(data.Length - offset, PageSize); - - data.Slice(offset, size).CopyTo(_backingMemory.GetSpan(pa, size)); - } - } + public override void Write(ulong va, ReadOnlySpan data) + { + try + { + base.Write(va, data); } catch (InvalidMemoryRegionException) { @@ -270,60 +192,47 @@ namespace Ryujinx.Cpu.Jit } /// - public ReadOnlySpan GetSpan(ulong va, int size, bool tracked = false) + public void WriteGuest(ulong va, T value) where T : unmanaged { - if (size == 0) + Span data = MemoryMarshal.Cast(MemoryMarshal.CreateSpan(ref value, 1)); + + SignalMemoryTrackingImpl(va, (ulong)data.Length, true, true); + + Write(va, data); + } + + public override void WriteUntracked(ulong va, ReadOnlySpan data) + { + try { - return ReadOnlySpan.Empty; + base.WriteUntracked(va, data); } - - if (tracked) + catch (InvalidMemoryRegionException) { - SignalMemoryTracking(va, (ulong)size, false); - } - - if (IsContiguousAndMapped(va, size)) - { - return _backingMemory.GetSpan(GetPhysicalAddressInternal(va), size); - } - else - { - Span data = new byte[size]; - - ReadImpl(va, data); - - return data; + if (_invalidAccessHandler == null || !_invalidAccessHandler(va)) + { + throw; + } } } - /// - public WritableRegion GetWritableRegion(ulong va, int size, bool tracked = false) + public override ReadOnlySequence GetReadOnlySequence(ulong va, int size, bool tracked = false) { - if (size == 0) + try { - return new WritableRegion(null, va, Memory.Empty); + return base.GetReadOnlySequence(va, size, tracked); } - - if (IsContiguousAndMapped(va, size)) + catch (InvalidMemoryRegionException) { - if (tracked) + if (_invalidAccessHandler == null || !_invalidAccessHandler(va)) { - SignalMemoryTracking(va, (ulong)size, true); + throw; } - return new WritableRegion(null, va, _backingMemory.GetMemory(GetPhysicalAddressInternal(va), size)); - } - else - { - Memory memory = new byte[size]; - - GetSpan(va, size).CopyTo(memory.Span); - - return new WritableRegion(this, va, memory, tracked); + return ReadOnlySequence.Empty; } } - /// public ref T GetRef(ulong va) where T : unmanaged { if (!IsContiguous(va, Unsafe.SizeOf())) @@ -336,56 +245,6 @@ namespace Ryujinx.Cpu.Jit return ref _backingMemory.GetRef(GetPhysicalAddressInternal(va)); } - /// - /// Computes the number of pages in a virtual address range. - /// - /// Virtual address of the range - /// Size of the range - /// The virtual address of the beginning of the first page - /// This function does not differentiate between allocated and unallocated pages. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static int GetPagesCount(ulong va, uint size, out ulong startVa) - { - // WARNING: Always check if ulong does not overflow during the operations. - startVa = va & ~(ulong)PageMask; - ulong vaSpan = (va - startVa + size + PageMask) & ~(ulong)PageMask; - - return (int)(vaSpan / PageSize); - } - - private static void ThrowMemoryNotContiguous() => throw new MemoryNotContiguousException(); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private bool IsContiguousAndMapped(ulong va, int size) => IsContiguous(va, size) && IsMapped(va); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private bool IsContiguous(ulong va, int size) - { - if (!ValidateAddress(va) || !ValidateAddressAndSize(va, (ulong)size)) - { - return false; - } - - int pages = GetPagesCount(va, (uint)size, out va); - - for (int page = 0; page < pages - 1; page++) - { - if (!ValidateAddress(va + PageSize)) - { - return false; - } - - if (GetPhysicalAddressInternal(va) + PageSize != GetPhysicalAddressInternal(va + PageSize)) - { - return false; - } - - va += PageSize; - } - - return true; - } - /// public IEnumerable GetHostRegions(ulong va, ulong size) { @@ -405,7 +264,7 @@ namespace Ryujinx.Cpu.Jit for (int i = 0; i < regions.Length; i++) { var guestRegion = guestRegions[i]; - IntPtr pointer = _backingMemory.GetPointer(guestRegion.Address, guestRegion.Size); + nint pointer = _backingMemory.GetPointer(guestRegion.Address, guestRegion.Size); regions[i] = new HostMemoryRange((nuint)(ulong)pointer, guestRegion.Size); } @@ -462,48 +321,6 @@ namespace Ryujinx.Cpu.Jit return regions; } - private void ReadImpl(ulong va, Span data) - { - if (data.Length == 0) - { - return; - } - - try - { - AssertValidAddressAndSize(va, (ulong)data.Length); - - int offset = 0, size; - - if ((va & PageMask) != 0) - { - ulong pa = GetPhysicalAddressInternal(va); - - size = Math.Min(data.Length, PageSize - (int)(va & PageMask)); - - _backingMemory.GetSpan(pa, size).CopyTo(data[..size]); - - offset += size; - } - - for (; offset < data.Length; offset += size) - { - ulong pa = GetPhysicalAddressInternal(va + (ulong)offset); - - size = Math.Min(data.Length - offset, PageSize); - - _backingMemory.GetSpan(pa, size).CopyTo(data.Slice(offset, size)); - } - } - catch (InvalidMemoryRegionException) - { - if (_invalidAccessHandler == null || !_invalidAccessHandler(va)) - { - throw; - } - } - } - /// public bool IsRangeMapped(ulong va, ulong size) { @@ -532,9 +349,8 @@ namespace Ryujinx.Cpu.Jit return true; } - /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool IsMapped(ulong va) + public override bool IsMapped(ulong va) { if (!ValidateAddress(va)) { @@ -544,40 +360,9 @@ namespace Ryujinx.Cpu.Jit return _pageTable.Read((va / PageSize) * PteSize) != 0; } - private bool ValidateAddress(ulong va) + private nuint GetPhysicalAddressInternal(ulong va) { - return va < _addressSpaceSize; - } - - /// - /// Checks if the combination of virtual address and size is part of the addressable space. - /// - /// Virtual address of the range - /// Size of the range in bytes - /// True if the combination of virtual address and size is part of the addressable space - private bool ValidateAddressAndSize(ulong va, ulong size) - { - ulong endVa = va + size; - return endVa >= va && endVa >= size && endVa <= _addressSpaceSize; - } - - /// - /// Ensures the combination of virtual address and size is part of the addressable space. - /// - /// Virtual address of the range - /// Size of the range in bytes - /// Throw when the memory region specified outside the addressable space - private void AssertValidAddressAndSize(ulong va, ulong size) - { - if (!ValidateAddressAndSize(va, size)) - { - throw new InvalidMemoryRegionException($"va=0x{va:X16}, size=0x{size:X16}"); - } - } - - private ulong GetPhysicalAddressInternal(ulong va) - { - return PteToPa(_pageTable.Read((va / PageSize) * PteSize) & ~(0xffffUL << 48)) + (va & PageMask); + return (nuint)(PteToPa(_pageTable.Read((va / PageSize) * PteSize) & ~(0xffffUL << 48)) + (va & PageMask)); } /// @@ -587,50 +372,57 @@ namespace Ryujinx.Cpu.Jit } /// - public void TrackingReprotect(ulong va, ulong size, MemoryPermission protection) + public void TrackingReprotect(ulong va, ulong size, MemoryPermission protection, bool guest) { AssertValidAddressAndSize(va, size); - // Protection is inverted on software pages, since the default value is 0. - protection = (~protection) & MemoryPermission.ReadAndWrite; - - long tag = protection switch + if (guest) { - MemoryPermission.None => 0L, - MemoryPermission.Write => 2L << PointerTagBit, - _ => 3L << PointerTagBit, - }; + // Protection is inverted on software pages, since the default value is 0. + protection = (~protection) & MemoryPermission.ReadAndWrite; - int pages = GetPagesCount(va, (uint)size, out va); - ulong pageStart = va >> PageBits; - long invTagMask = ~(0xffffL << 48); - - for (int page = 0; page < pages; page++) - { - ref long pageRef = ref _pageTable.GetRef(pageStart * PteSize); - - long pte; - - do + long tag = protection switch { - pte = Volatile.Read(ref pageRef); - } - while (pte != 0 && Interlocked.CompareExchange(ref pageRef, (pte & invTagMask) | tag, pte) != pte); + MemoryPermission.None => 0L, + MemoryPermission.Write => 2L << PointerTagBit, + _ => 3L << PointerTagBit, + }; - pageStart++; + int pages = GetPagesCount(va, (uint)size, out va); + ulong pageStart = va >> PageBits; + long invTagMask = ~(0xffffL << 48); + + for (int page = 0; page < pages; page++) + { + ref long pageRef = ref _pageTable.GetRef(pageStart * PteSize); + + long pte; + + do + { + pte = Volatile.Read(ref pageRef); + } + while (pte != 0 && Interlocked.CompareExchange(ref pageRef, (pte & invTagMask) | tag, pte) != pte); + + pageStart++; + } + } + else + { + _pages.TrackingReprotect(va, size, protection); } } /// - public RegionHandle BeginTracking(ulong address, ulong size, int id) + public RegionHandle BeginTracking(ulong address, ulong size, int id, RegionFlags flags = RegionFlags.None) { - return Tracking.BeginTracking(address, size, id); + return Tracking.BeginTracking(address, size, id, flags); } /// - public MultiRegionHandle BeginGranularTracking(ulong address, ulong size, IEnumerable handles, ulong granularity, int id) + public MultiRegionHandle BeginGranularTracking(ulong address, ulong size, IEnumerable handles, ulong granularity, int id, RegionFlags flags = RegionFlags.None) { - return Tracking.BeginGranularTracking(address, size, handles, granularity, id); + return Tracking.BeginGranularTracking(address, size, handles, granularity, id, flags); } /// @@ -639,8 +431,7 @@ namespace Ryujinx.Cpu.Jit return Tracking.BeginSmartGranularTracking(address, size, granularity, id); } - /// - public void SignalMemoryTracking(ulong va, ulong size, bool write, bool precise = false, int? exemptId = null) + private void SignalMemoryTrackingImpl(ulong va, ulong size, bool write, bool guest, bool precise = false, int? exemptId = null) { AssertValidAddressAndSize(va, size); @@ -650,31 +441,45 @@ namespace Ryujinx.Cpu.Jit return; } - // We emulate guard pages for software memory access. This makes for an easy transition to - // tracking using host guard pages in future, but also supporting platforms where this is not possible. + // If the memory tracking is coming from the guest, use the tag bits in the page table entry. + // Otherwise, use the managed page flags. - // Write tag includes read protection, since we don't have any read actions that aren't performed before write too. - long tag = (write ? 3L : 1L) << PointerTagBit; - - int pages = GetPagesCount(va, (uint)size, out _); - ulong pageStart = va >> PageBits; - - for (int page = 0; page < pages; page++) + if (guest) { - ref long pageRef = ref _pageTable.GetRef(pageStart * PteSize); + // We emulate guard pages for software memory access. This makes for an easy transition to + // tracking using host guard pages in future, but also supporting platforms where this is not possible. - long pte; + // Write tag includes read protection, since we don't have any read actions that aren't performed before write too. + long tag = (write ? 3L : 1L) << PointerTagBit; - pte = Volatile.Read(ref pageRef); + int pages = GetPagesCount(va, (uint)size, out _); + ulong pageStart = va >> PageBits; - if ((pte & tag) != 0) + for (int page = 0; page < pages; page++) { - Tracking.VirtualMemoryEvent(va, size, write, precise: false, exemptId); - break; - } + ref long pageRef = ref _pageTable.GetRef(pageStart * PteSize); - pageStart++; + long pte = Volatile.Read(ref pageRef); + + if ((pte & tag) != 0) + { + Tracking.VirtualMemoryEvent(va, size, write, precise: false, exemptId, true); + break; + } + + pageStart++; + } } + else + { + _pages.SignalMemoryTracking(Tracking, va, size, write, exemptId); + } + } + + /// + public override void SignalMemoryTracking(ulong va, ulong size, bool write, bool precise = false, int? exemptId = null) + { + SignalMemoryTrackingImpl(va, size, write, false, precise, exemptId); } private ulong PaToPte(ulong pa) @@ -691,5 +496,17 @@ namespace Ryujinx.Cpu.Jit /// Disposes of resources used by the memory manager. /// protected override void Destroy() => _pageTable.Dispose(); + + protected override Memory GetPhysicalAddressMemory(nuint pa, int size) + => _backingMemory.GetMemory(pa, size); + + protected override Span GetPhysicalAddressSpan(nuint pa, int size) + => _backingMemory.GetSpan(pa, size); + + protected override nuint TranslateVirtualAddressChecked(ulong va) + => GetPhysicalAddressInternal(va); + + protected override nuint TranslateVirtualAddressUnchecked(ulong va) + => GetPhysicalAddressInternal(va); } } diff --git a/src/Ryujinx.Cpu/Jit/MemoryManagerHostMapped.cs b/src/Ryujinx.Cpu/Jit/MemoryManagerHostMapped.cs index 6d32787ac..0fe8b344f 100644 --- a/src/Ryujinx.Cpu/Jit/MemoryManagerHostMapped.cs +++ b/src/Ryujinx.Cpu/Jit/MemoryManagerHostMapped.cs @@ -1,58 +1,37 @@ -using ARMeilleure.Memory; +using ARMeilleure.Memory; using Ryujinx.Memory; using Ryujinx.Memory.Range; using Ryujinx.Memory.Tracking; using System; +using System.Buffers; using System.Collections.Generic; using System.Linq; using System.Runtime.CompilerServices; -using System.Threading; namespace Ryujinx.Cpu.Jit { /// /// Represents a CPU memory manager which maps guest virtual memory directly onto a host virtual region. /// - public sealed class MemoryManagerHostMapped : MemoryManagerBase, IMemoryManager, IVirtualMemoryManagerTracked, IWritableBlock + public sealed class MemoryManagerHostMapped : VirtualMemoryManagerRefCountedBase, IMemoryManager, IVirtualMemoryManagerTracked { - public const int PageBits = 12; - public const int PageSize = 1 << PageBits; - public const int PageMask = PageSize - 1; - - public const int PageToPteShift = 5; // 32 pages (2 bits each) in one ulong page table entry. - public const ulong BlockMappedMask = 0x5555555555555555; // First bit of each table entry set. - - private enum HostMappedPtBits : ulong - { - Unmapped = 0, - Mapped, - WriteTracked, - ReadWriteTracked, - - MappedReplicated = 0x5555555555555555, - WriteTrackedReplicated = 0xaaaaaaaaaaaaaaaa, - ReadWriteTrackedReplicated = ulong.MaxValue, - } - private readonly InvalidAccessHandler _invalidAccessHandler; private readonly bool _unsafeMode; private readonly AddressSpace _addressSpace; - public ulong AddressSpaceSize { get; } - private readonly PageTable _pageTable; private readonly MemoryEhMeilleure _memoryEh; - private readonly ulong[] _pageBitmap; + private readonly ManagedPageFlags _pages; /// - public bool Supports4KBPages => MemoryBlock.GetPageSize() == PageSize; + public bool UsesPrivateAllocations => false; public int AddressSpaceBits { get; } - public IntPtr PageTablePointer => _addressSpace.Base.Pointer; + public nint PageTablePointer => _addressSpace.Base.Pointer; public MemoryManagerType Type => _unsafeMode ? MemoryManagerType.HostMappedUnsafe : MemoryManagerType.HostMapped; @@ -60,6 +39,8 @@ namespace Ryujinx.Cpu.Jit public event Action UnmapEvent; + protected override ulong AddressSpaceSize { get; } + /// /// Creates a new instance of the host mapped memory manager. /// @@ -85,48 +66,12 @@ namespace Ryujinx.Cpu.Jit AddressSpaceBits = asBits; - _pageBitmap = new ulong[1 << (AddressSpaceBits - (PageBits + PageToPteShift))]; + _pages = new ManagedPageFlags(AddressSpaceBits); Tracking = new MemoryTracking(this, (int)MemoryBlock.GetPageSize(), invalidAccessHandler); _memoryEh = new MemoryEhMeilleure(_addressSpace.Base, _addressSpace.Mirror, Tracking); } - /// - /// Checks if the virtual address is part of the addressable space. - /// - /// Virtual address - /// True if the virtual address is part of the addressable space - private bool ValidateAddress(ulong va) - { - return va < AddressSpaceSize; - } - - /// - /// Checks if the combination of virtual address and size is part of the addressable space. - /// - /// Virtual address of the range - /// Size of the range in bytes - /// True if the combination of virtual address and size is part of the addressable space - private bool ValidateAddressAndSize(ulong va, ulong size) - { - ulong endVa = va + size; - return endVa >= va && endVa >= size && endVa <= AddressSpaceSize; - } - - /// - /// Ensures the combination of virtual address and size is part of the addressable space. - /// - /// Virtual address of the range - /// Size of the range in bytes - /// Throw when the memory region specified outside the addressable space - private void AssertValidAddressAndSize(ulong va, ulong size) - { - if (!ValidateAddressAndSize(va, size)) - { - throw new InvalidMemoryRegionException($"va=0x{va:X16}, size=0x{size:X16}"); - } - } - /// /// Ensures the combination of virtual address and size is part of the addressable space and fully mapped. /// @@ -134,7 +79,7 @@ namespace Ryujinx.Cpu.Jit /// Size of the range in bytes private void AssertMapped(ulong va, ulong size) { - if (!ValidateAddressAndSize(va, size) || !IsRangeMappedImpl(va, size)) + if (!ValidateAddressAndSize(va, size) || !_pages.IsRangeMapped(va, size)) { throw new InvalidMemoryRegionException($"Not mapped: va=0x{va:X16}, size=0x{size:X16}"); } @@ -146,18 +91,12 @@ namespace Ryujinx.Cpu.Jit AssertValidAddressAndSize(va, size); _addressSpace.Map(va, pa, size, flags); - AddMapping(va, size); + _pages.AddMapping(va, size); PtMap(va, pa, size); Tracking.Map(va, size); } - /// - public void MapForeign(ulong va, nuint hostPointer, ulong size) - { - throw new NotSupportedException(); - } - /// public void Unmap(ulong va, ulong size) { @@ -166,7 +105,7 @@ namespace Ryujinx.Cpu.Jit UnmapEvent?.Invoke(va, size); Tracking.Unmap(va, size); - RemoveMapping(va, size); + _pages.RemoveMapping(va, size); PtUnmap(va, size); _addressSpace.Unmap(va, size); } @@ -194,8 +133,7 @@ namespace Ryujinx.Cpu.Jit } } - /// - public T Read(ulong va) where T : unmanaged + public override T Read(ulong va) { try { @@ -214,14 +152,11 @@ namespace Ryujinx.Cpu.Jit } } - /// - public T ReadTracked(ulong va) where T : unmanaged + public override T ReadTracked(ulong va) { try { - SignalMemoryTracking(va, (ulong)Unsafe.SizeOf(), false); - - return Read(va); + return base.ReadTracked(va); } catch (InvalidMemoryRegionException) { @@ -234,8 +169,7 @@ namespace Ryujinx.Cpu.Jit } } - /// - public void Read(ulong va, Span data) + public override void Read(ulong va, Span data) { try { @@ -252,9 +186,7 @@ namespace Ryujinx.Cpu.Jit } } - - /// - public void Write(ulong va, T value) where T : unmanaged + public override void Write(ulong va, T value) { try { @@ -271,8 +203,7 @@ namespace Ryujinx.Cpu.Jit } } - /// - public void Write(ulong va, ReadOnlySpan data) + public override void Write(ulong va, ReadOnlySpan data) { try { @@ -289,8 +220,7 @@ namespace Ryujinx.Cpu.Jit } } - /// - public void WriteUntracked(ulong va, ReadOnlySpan data) + public override void WriteUntracked(ulong va, ReadOnlySpan data) { try { @@ -307,8 +237,7 @@ namespace Ryujinx.Cpu.Jit } } - /// - public bool WriteWithRedundancyCheck(ulong va, ReadOnlySpan data) + public override bool WriteWithRedundancyCheck(ulong va, ReadOnlySpan data) { try { @@ -335,8 +264,21 @@ namespace Ryujinx.Cpu.Jit } } - /// - public ReadOnlySpan GetSpan(ulong va, int size, bool tracked = false) + public override ReadOnlySequence GetReadOnlySequence(ulong va, int size, bool tracked = false) + { + if (tracked) + { + SignalMemoryTracking(va, (ulong)size, write: false); + } + else + { + AssertMapped(va, (ulong)size); + } + + return new ReadOnlySequence(_addressSpace.Mirror.GetMemory(va, size)); + } + + public override ReadOnlySpan GetSpan(ulong va, int size, bool tracked = false) { if (tracked) { @@ -350,8 +292,7 @@ namespace Ryujinx.Cpu.Jit return _addressSpace.Mirror.GetSpan(va, size); } - /// - public WritableRegion GetWritableRegion(ulong va, int size, bool tracked = false) + public override WritableRegion GetWritableRegion(ulong va, int size, bool tracked = false) { if (tracked) { @@ -365,7 +306,6 @@ namespace Ryujinx.Cpu.Jit return _addressSpace.Mirror.GetWritableRegion(va, size); } - /// public ref T GetRef(ulong va) where T : unmanaged { SignalMemoryTracking(va, (ulong)Unsafe.SizeOf(), true); @@ -373,26 +313,10 @@ namespace Ryujinx.Cpu.Jit return ref _addressSpace.Mirror.GetRef(va); } - /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool IsMapped(ulong va) + public override bool IsMapped(ulong va) { - return ValidateAddress(va) && IsMappedImpl(va); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private bool IsMappedImpl(ulong va) - { - ulong page = va >> PageBits; - - int bit = (int)((page & 31) << 1); - - int pageIndex = (int)(page >> PageToPteShift); - ref ulong pageRef = ref _pageBitmap[pageIndex]; - - ulong pte = Volatile.Read(ref pageRef); - - return ((pte >> bit) & 3) != 0; + return ValidateAddress(va) && _pages.IsMapped(va); } /// @@ -400,58 +324,7 @@ namespace Ryujinx.Cpu.Jit { AssertValidAddressAndSize(va, size); - return IsRangeMappedImpl(va, size); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void GetPageBlockRange(ulong pageStart, ulong pageEnd, out ulong startMask, out ulong endMask, out int pageIndex, out int pageEndIndex) - { - startMask = ulong.MaxValue << ((int)(pageStart & 31) << 1); - endMask = ulong.MaxValue >> (64 - ((int)(pageEnd & 31) << 1)); - - pageIndex = (int)(pageStart >> PageToPteShift); - pageEndIndex = (int)((pageEnd - 1) >> PageToPteShift); - } - - private bool IsRangeMappedImpl(ulong va, ulong size) - { - int pages = GetPagesCount(va, size, out _); - - if (pages == 1) - { - return IsMappedImpl(va); - } - - ulong pageStart = va >> PageBits; - ulong pageEnd = pageStart + (ulong)pages; - - GetPageBlockRange(pageStart, pageEnd, out ulong startMask, out ulong endMask, out int pageIndex, out int pageEndIndex); - - // Check if either bit in each 2 bit page entry is set. - // OR the block with itself shifted down by 1, and check the first bit of each entry. - - ulong mask = BlockMappedMask & startMask; - - while (pageIndex <= pageEndIndex) - { - if (pageIndex == pageEndIndex) - { - mask &= endMask; - } - - ref ulong pageRef = ref _pageBitmap[pageIndex++]; - ulong pte = Volatile.Read(ref pageRef); - - pte |= pte >> 1; - if ((pte & mask) != mask) - { - return false; - } - - mask = BlockMappedMask; - } - - return true; + return _pages.IsRangeMapped(va, size); } /// @@ -512,11 +385,10 @@ namespace Ryujinx.Cpu.Jit return _pageTable.Read(va) + (va & PageMask); } - /// /// /// This function also validates that the given range is both valid and mapped, and will throw if it is not. /// - public void SignalMemoryTracking(ulong va, ulong size, bool write, bool precise = false, int? exemptId = null) + public override void SignalMemoryTracking(ulong va, ulong size, bool write, bool precise = false, int? exemptId = null) { AssertValidAddressAndSize(va, size); @@ -526,93 +398,7 @@ namespace Ryujinx.Cpu.Jit return; } - // Software table, used for managed memory tracking. - - int pages = GetPagesCount(va, size, out _); - ulong pageStart = va >> PageBits; - - if (pages == 1) - { - ulong tag = (ulong)(write ? HostMappedPtBits.WriteTracked : HostMappedPtBits.ReadWriteTracked); - - int bit = (int)((pageStart & 31) << 1); - - int pageIndex = (int)(pageStart >> PageToPteShift); - ref ulong pageRef = ref _pageBitmap[pageIndex]; - - ulong pte = Volatile.Read(ref pageRef); - ulong state = ((pte >> bit) & 3); - - if (state >= tag) - { - Tracking.VirtualMemoryEvent(va, size, write, precise: false, exemptId); - return; - } - else if (state == 0) - { - ThrowInvalidMemoryRegionException($"Not mapped: va=0x{va:X16}, size=0x{size:X16}"); - } - } - else - { - ulong pageEnd = pageStart + (ulong)pages; - - GetPageBlockRange(pageStart, pageEnd, out ulong startMask, out ulong endMask, out int pageIndex, out int pageEndIndex); - - ulong mask = startMask; - - ulong anyTrackingTag = (ulong)HostMappedPtBits.WriteTrackedReplicated; - - while (pageIndex <= pageEndIndex) - { - if (pageIndex == pageEndIndex) - { - mask &= endMask; - } - - ref ulong pageRef = ref _pageBitmap[pageIndex++]; - - ulong pte = Volatile.Read(ref pageRef); - ulong mappedMask = mask & BlockMappedMask; - - ulong mappedPte = pte | (pte >> 1); - if ((mappedPte & mappedMask) != mappedMask) - { - ThrowInvalidMemoryRegionException($"Not mapped: va=0x{va:X16}, size=0x{size:X16}"); - } - - pte &= mask; - if ((pte & anyTrackingTag) != 0) // Search for any tracking. - { - // Writes trigger any tracking. - // Only trigger tracking from reads if both bits are set on any page. - if (write || (pte & (pte >> 1) & BlockMappedMask) != 0) - { - Tracking.VirtualMemoryEvent(va, size, write, precise: false, exemptId); - break; - } - } - - mask = ulong.MaxValue; - } - } - } - - /// - /// Computes the number of pages in a virtual address range. - /// - /// Virtual address of the range - /// Size of the range - /// The virtual address of the beginning of the first page - /// This function does not differentiate between allocated and unallocated pages. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static int GetPagesCount(ulong va, ulong size, out ulong startVa) - { - // WARNING: Always check if ulong does not overflow during the operations. - startVa = va & ~(ulong)PageMask; - ulong vaSpan = (va - startVa + size + PageMask) & ~(ulong)PageMask; - - return (int)(vaSpan / PageSize); + _pages.SignalMemoryTracking(Tracking, va, size, write, exemptId); } /// @@ -622,103 +408,28 @@ namespace Ryujinx.Cpu.Jit } /// - public void TrackingReprotect(ulong va, ulong size, MemoryPermission protection) + public void TrackingReprotect(ulong va, ulong size, MemoryPermission protection, bool guest) { - // Protection is inverted on software pages, since the default value is 0. - protection = (~protection) & MemoryPermission.ReadAndWrite; - - int pages = GetPagesCount(va, size, out va); - ulong pageStart = va >> PageBits; - - if (pages == 1) + if (guest) { - ulong protTag = protection switch - { - MemoryPermission.None => (ulong)HostMappedPtBits.Mapped, - MemoryPermission.Write => (ulong)HostMappedPtBits.WriteTracked, - _ => (ulong)HostMappedPtBits.ReadWriteTracked, - }; - - int bit = (int)((pageStart & 31) << 1); - - ulong tagMask = 3UL << bit; - ulong invTagMask = ~tagMask; - - ulong tag = protTag << bit; - - int pageIndex = (int)(pageStart >> PageToPteShift); - ref ulong pageRef = ref _pageBitmap[pageIndex]; - - ulong pte; - - do - { - pte = Volatile.Read(ref pageRef); - } - while ((pte & tagMask) != 0 && Interlocked.CompareExchange(ref pageRef, (pte & invTagMask) | tag, pte) != pte); + _addressSpace.Base.Reprotect(va, size, protection, false); } else { - ulong pageEnd = pageStart + (ulong)pages; - - GetPageBlockRange(pageStart, pageEnd, out ulong startMask, out ulong endMask, out int pageIndex, out int pageEndIndex); - - ulong mask = startMask; - - ulong protTag = protection switch - { - MemoryPermission.None => (ulong)HostMappedPtBits.MappedReplicated, - MemoryPermission.Write => (ulong)HostMappedPtBits.WriteTrackedReplicated, - _ => (ulong)HostMappedPtBits.ReadWriteTrackedReplicated, - }; - - while (pageIndex <= pageEndIndex) - { - if (pageIndex == pageEndIndex) - { - mask &= endMask; - } - - ref ulong pageRef = ref _pageBitmap[pageIndex++]; - - ulong pte; - ulong mappedMask; - - // Change the protection of all 2 bit entries that are mapped. - do - { - pte = Volatile.Read(ref pageRef); - - mappedMask = pte | (pte >> 1); - mappedMask |= (mappedMask & BlockMappedMask) << 1; - mappedMask &= mask; // Only update mapped pages within the given range. - } - while (Interlocked.CompareExchange(ref pageRef, (pte & (~mappedMask)) | (protTag & mappedMask), pte) != pte); - - mask = ulong.MaxValue; - } + _pages.TrackingReprotect(va, size, protection); } - - protection = protection switch - { - MemoryPermission.None => MemoryPermission.ReadAndWrite, - MemoryPermission.Write => MemoryPermission.Read, - _ => MemoryPermission.None, - }; - - _addressSpace.Base.Reprotect(va, size, protection, false); } /// - public RegionHandle BeginTracking(ulong address, ulong size, int id) + public RegionHandle BeginTracking(ulong address, ulong size, int id, RegionFlags flags = RegionFlags.None) { - return Tracking.BeginTracking(address, size, id); + return Tracking.BeginTracking(address, size, id, flags); } /// - public MultiRegionHandle BeginGranularTracking(ulong address, ulong size, IEnumerable handles, ulong granularity, int id) + public MultiRegionHandle BeginGranularTracking(ulong address, ulong size, IEnumerable handles, ulong granularity, int id, RegionFlags flags = RegionFlags.None) { - return Tracking.BeginGranularTracking(address, size, handles, granularity, id); + return Tracking.BeginGranularTracking(address, size, handles, granularity, id, flags); } /// @@ -727,86 +438,6 @@ namespace Ryujinx.Cpu.Jit return Tracking.BeginSmartGranularTracking(address, size, granularity, id); } - /// - /// Adds the given address mapping to the page table. - /// - /// Virtual memory address - /// Size to be mapped - private void AddMapping(ulong va, ulong size) - { - int pages = GetPagesCount(va, size, out _); - ulong pageStart = va >> PageBits; - ulong pageEnd = pageStart + (ulong)pages; - - GetPageBlockRange(pageStart, pageEnd, out ulong startMask, out ulong endMask, out int pageIndex, out int pageEndIndex); - - ulong mask = startMask; - - while (pageIndex <= pageEndIndex) - { - if (pageIndex == pageEndIndex) - { - mask &= endMask; - } - - ref ulong pageRef = ref _pageBitmap[pageIndex++]; - - ulong pte; - ulong mappedMask; - - // Map all 2-bit entries that are unmapped. - do - { - pte = Volatile.Read(ref pageRef); - - mappedMask = pte | (pte >> 1); - mappedMask |= (mappedMask & BlockMappedMask) << 1; - mappedMask |= ~mask; // Treat everything outside the range as mapped, thus unchanged. - } - while (Interlocked.CompareExchange(ref pageRef, (pte & mappedMask) | (BlockMappedMask & (~mappedMask)), pte) != pte); - - mask = ulong.MaxValue; - } - } - - /// - /// Removes the given address mapping from the page table. - /// - /// Virtual memory address - /// Size to be unmapped - private void RemoveMapping(ulong va, ulong size) - { - int pages = GetPagesCount(va, size, out _); - ulong pageStart = va >> PageBits; - ulong pageEnd = pageStart + (ulong)pages; - - GetPageBlockRange(pageStart, pageEnd, out ulong startMask, out ulong endMask, out int pageIndex, out int pageEndIndex); - - startMask = ~startMask; - endMask = ~endMask; - - ulong mask = startMask; - - while (pageIndex <= pageEndIndex) - { - if (pageIndex == pageEndIndex) - { - mask |= endMask; - } - - ref ulong pageRef = ref _pageBitmap[pageIndex++]; - ulong pte; - - do - { - pte = Volatile.Read(ref pageRef); - } - while (Interlocked.CompareExchange(ref pageRef, pte & mask, pte) != pte); - - mask = 0; - } - } - /// /// Disposes of resources used by the memory manager. /// @@ -816,6 +447,16 @@ namespace Ryujinx.Cpu.Jit _memoryEh.Dispose(); } - private static void ThrowInvalidMemoryRegionException(string message) => throw new InvalidMemoryRegionException(message); + protected override Memory GetPhysicalAddressMemory(nuint pa, int size) + => _addressSpace.Mirror.GetMemory(pa, size); + + protected override Span GetPhysicalAddressSpan(nuint pa, int size) + => _addressSpace.Mirror.GetSpan(pa, size); + + protected override nuint TranslateVirtualAddressChecked(ulong va) + => (nuint)GetPhysicalAddressChecked(va); + + protected override nuint TranslateVirtualAddressUnchecked(ulong va) + => (nuint)GetPhysicalAddressInternal(va); } } diff --git a/src/Ryujinx.Cpu/Jit/MemoryManagerHostTracked.cs b/src/Ryujinx.Cpu/Jit/MemoryManagerHostTracked.cs index 03befa088..4dab212a7 100644 --- a/src/Ryujinx.Cpu/Jit/MemoryManagerHostTracked.cs +++ b/src/Ryujinx.Cpu/Jit/MemoryManagerHostTracked.cs @@ -1,82 +1,64 @@ using ARMeilleure.Memory; +using Ryujinx.Common.Memory; +using Ryujinx.Cpu.Jit.HostTracked; +using Ryujinx.Cpu.Signal; using Ryujinx.Memory; using Ryujinx.Memory.Range; using Ryujinx.Memory.Tracking; using System; +using System.Buffers; using System.Collections.Generic; using System.Linq; using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using System.Threading; namespace Ryujinx.Cpu.Jit { /// /// Represents a CPU memory manager which maps guest virtual memory directly onto a host virtual region. /// - public sealed class MemoryManagerHostTracked : MemoryManagerBase, IWritableBlock, IMemoryManager, IVirtualMemoryManagerTracked + public sealed class MemoryManagerHostTracked : VirtualMemoryManagerRefCountedBase, IMemoryManager, IVirtualMemoryManagerTracked { - public const int PageBits = 12; - public const int PageSize = 1 << PageBits; - public const int PageMask = PageSize - 1; - - public const int PageToPteShift = 5; // 32 pages (2 bits each) in one ulong page table entry. - public const ulong BlockMappedMask = 0x5555555555555555; // First bit of each table entry set. - - private enum HostMappedPtBits : ulong - { - Unmapped = 0, - Mapped, - WriteTracked, - ReadWriteTracked, - - MappedReplicated = 0x5555555555555555, - WriteTrackedReplicated = 0xaaaaaaaaaaaaaaaa, - ReadWriteTrackedReplicated = ulong.MaxValue - } - private readonly InvalidAccessHandler _invalidAccessHandler; + private readonly bool _unsafeMode; private readonly MemoryBlock _backingMemory; - private readonly PageTable _pageTable; - - private readonly ulong[] _pageBitmap; public int AddressSpaceBits { get; } - public MemoryTracking Tracking { get; private set; } - - private const int PteSize = 8; + public MemoryTracking Tracking { get; } + private readonly NativePageTable _nativePageTable; private readonly AddressSpacePartitioned _addressSpace; - public ulong AddressSpaceSize { get; } + private readonly ManagedPageFlags _pages; - private readonly MemoryBlock _flatPageTable; + protected override ulong AddressSpaceSize { get; } /// - public bool Supports4KBPages => false; + public bool UsesPrivateAllocations => true; - public IntPtr PageTablePointer => _flatPageTable.Pointer; + public nint PageTablePointer => _nativePageTable.PageTablePointer; - public MemoryManagerType Type => MemoryManagerType.HostTracked; + public MemoryManagerType Type => _unsafeMode ? MemoryManagerType.HostTrackedUnsafe : MemoryManagerType.HostTracked; public event Action UnmapEvent; /// - /// Creates a new instance of the host mapped memory manager. + /// Creates a new instance of the host tracked memory manager. /// /// Physical backing memory where virtual memory will be mapped to /// Size of the address space + /// True if unmanaged access should not be masked (unsafe), false otherwise. /// Optional function to handle invalid memory accesses - public MemoryManagerHostTracked(MemoryBlock backingMemory, ulong addressSpaceSize, InvalidAccessHandler invalidAccessHandler) + public MemoryManagerHostTracked(MemoryBlock backingMemory, ulong addressSpaceSize, bool unsafeMode, InvalidAccessHandler invalidAccessHandler) { - Tracking = new MemoryTracking(this, AddressSpacePartitioned.Use4KBProtection ? PageSize : (int)MemoryBlock.GetPageSize(), invalidAccessHandler); + bool useProtectionMirrors = MemoryBlock.GetPageSize() > PageSize; + + Tracking = new MemoryTracking(this, PageSize, invalidAccessHandler, useProtectionMirrors); _backingMemory = backingMemory; - _pageTable = new PageTable(); _invalidAccessHandler = invalidAccessHandler; - _addressSpace = new(Tracking, backingMemory, UpdatePt); + _unsafeMode = unsafeMode; AddressSpaceSize = addressSpaceSize; ulong asSize = PageSize; @@ -90,8 +72,81 @@ namespace Ryujinx.Cpu.Jit AddressSpaceBits = asBits; - _pageBitmap = new ulong[1 << (AddressSpaceBits - (PageBits + PageToPteShift))]; - _flatPageTable = new MemoryBlock((asSize / PageSize) * PteSize); + if (useProtectionMirrors && !NativeSignalHandler.SupportsFaultAddressPatching()) + { + // Currently we require being able to change the fault address to something else + // in order to "emulate" 4KB granularity protection on systems with larger page size. + + throw new PlatformNotSupportedException(); + } + + _pages = new ManagedPageFlags(asBits); + _nativePageTable = new(asSize); + _addressSpace = new(Tracking, backingMemory, _nativePageTable, useProtectionMirrors); + } + + public override ReadOnlySequence GetReadOnlySequence(ulong va, int size, bool tracked = false) + { + if (size == 0) + { + return ReadOnlySequence.Empty; + } + + try + { + if (tracked) + { + SignalMemoryTracking(va, (ulong)size, false); + } + else + { + AssertValidAddressAndSize(va, (ulong)size); + } + + ulong endVa = va + (ulong)size; + int offset = 0; + + BytesReadOnlySequenceSegment first = null, last = null; + + while (va < endVa) + { + (MemoryBlock memory, ulong rangeOffset, ulong copySize) = GetMemoryOffsetAndSize(va, (ulong)(size - offset)); + + Memory physicalMemory = memory.GetMemory(rangeOffset, (int)copySize); + + if (first is null) + { + first = last = new BytesReadOnlySequenceSegment(physicalMemory); + } + else + { + if (last.IsContiguousWith(physicalMemory, out nuint contiguousStart, out int contiguousSize)) + { + Memory contiguousPhysicalMemory = new NativeMemoryManager(contiguousStart, contiguousSize).Memory; + + last.Replace(contiguousPhysicalMemory); + } + else + { + last = last.Append(physicalMemory); + } + } + + va += copySize; + offset += (int)copySize; + } + + return new ReadOnlySequence(first, 0, last, (int)(size - last.RunningIndex)); + } + catch (InvalidMemoryRegionException) + { + if (_invalidAccessHandler == null || !_invalidAccessHandler(va)) + { + throw; + } + + return ReadOnlySequence.Empty; + } } /// @@ -104,52 +159,12 @@ namespace Ryujinx.Cpu.Jit _addressSpace.Map(va, pa, size); } - AddMapping(va, size); - PtMap(va, pa, size, flags.HasFlag(MemoryMapFlags.Private)); + _pages.AddMapping(va, size); + _nativePageTable.Map(va, pa, size, _addressSpace, _backingMemory, flags.HasFlag(MemoryMapFlags.Private)); Tracking.Map(va, size); } - private void PtMap(ulong va, ulong pa, ulong size, bool privateMap) - { - while (size != 0) - { - _pageTable.Map(va, pa); - - if (privateMap) - { - _flatPageTable.Write((va / PageSize) * PteSize, (ulong)_addressSpace.GetPointer(va, PageSize) - va); - } - else - { - _flatPageTable.Write((va / PageSize) * PteSize, (ulong)_backingMemory.GetPointer(pa, PageSize) - va); - } - - va += PageSize; - pa += PageSize; - size -= PageSize; - } - } - - private void UpdatePt(ulong va, IntPtr ptr, ulong size) - { - ulong remainingSize = size; - while (remainingSize != 0) - { - _flatPageTable.Write((va / PageSize) * PteSize, (ulong)ptr - va); - - va += PageSize; - ptr += PageSize; - remainingSize -= PageSize; - } - } - - /// - public void MapForeign(ulong va, nuint hostPointer, ulong size) - { - throw new NotSupportedException(); - } - /// public void Unmap(ulong va, ulong size) { @@ -160,70 +175,15 @@ namespace Ryujinx.Cpu.Jit UnmapEvent?.Invoke(va, size); Tracking.Unmap(va, size); - RemoveMapping(va, size); - PtUnmap(va, size); + _pages.RemoveMapping(va, size); + _nativePageTable.Unmap(va, size); } - private void PtUnmap(ulong va, ulong size) - { - while (size != 0) - { - _pageTable.Unmap(va); - _flatPageTable.Write((va / PageSize) * PteSize, 0UL); - - va += PageSize; - size -= PageSize; - } - } - - /// - /// Checks if the virtual address is part of the addressable space. - /// - /// Virtual address - /// True if the virtual address is part of the addressable space - private bool ValidateAddress(ulong va) - { - return va < AddressSpaceSize; - } - - /// - /// Checks if the combination of virtual address and size is part of the addressable space. - /// - /// Virtual address of the range - /// Size of the range in bytes - /// True if the combination of virtual address and size is part of the addressable space - private bool ValidateAddressAndSize(ulong va, ulong size) - { - ulong endVa = va + size; - return endVa >= va && endVa >= size && endVa <= AddressSpaceSize; - } - - /// - /// Ensures the combination of virtual address and size is part of the addressable space. - /// - /// Virtual address of the range - /// Size of the range in bytes - /// Throw when the memory region specified outside the addressable space - private void AssertValidAddressAndSize(ulong va, ulong size) - { - if (!ValidateAddressAndSize(va, size)) - { - throw new InvalidMemoryRegionException($"va=0x{va:X16}, size=0x{size:X16}"); - } - } - - public T Read(ulong va) where T : unmanaged - { - return MemoryMarshal.Cast(GetSpan(va, Unsafe.SizeOf()))[0]; - } - - public T ReadTracked(ulong va) where T : unmanaged + public override T ReadTracked(ulong va) { try { - SignalMemoryTracking(va, (ulong)Unsafe.SizeOf(), false); - - return Read(va); + return base.ReadTracked(va); } catch (InvalidMemoryRegionException) { @@ -236,39 +196,40 @@ namespace Ryujinx.Cpu.Jit } } - public void Read(ulong va, Span data) - { - ReadImpl(va, data); - } - - public void Write(ulong va, T value) where T : unmanaged - { - Write(va, MemoryMarshal.Cast(MemoryMarshal.CreateSpan(ref value, 1))); - } - - public void Write(ulong va, ReadOnlySpan data) + public override void Read(ulong va, Span data) { if (data.Length == 0) { return; } - SignalMemoryTracking(va, (ulong)data.Length, true); - - WriteImpl(va, data); - } - - public void WriteUntracked(ulong va, ReadOnlySpan data) - { - if (data.Length == 0) + try { - return; - } + AssertValidAddressAndSize(va, (ulong)data.Length); - WriteImpl(va, data); + ulong endVa = va + (ulong)data.Length; + int offset = 0; + + while (va < endVa) + { + (MemoryBlock memory, ulong rangeOffset, ulong copySize) = GetMemoryOffsetAndSize(va, (ulong)(data.Length - offset)); + + memory.GetSpan(rangeOffset, (int)copySize).CopyTo(data.Slice(offset, (int)copySize)); + + va += copySize; + offset += (int)copySize; + } + } + catch (InvalidMemoryRegionException) + { + if (_invalidAccessHandler == null || !_invalidAccessHandler(va)) + { + throw; + } + } } - public bool WriteWithRedundancyCheck(ulong va, ReadOnlySpan data) + public override bool WriteWithRedundancyCheck(ulong va, ReadOnlySpan data) { if (data.Length == 0) { @@ -298,35 +259,7 @@ namespace Ryujinx.Cpu.Jit } } - private void WriteImpl(ulong va, ReadOnlySpan data) - { - try - { - AssertValidAddressAndSize(va, (ulong)data.Length); - - ulong endVa = va + (ulong)data.Length; - int offset = 0; - - while (va < endVa) - { - (MemoryBlock memory, ulong rangeOffset, ulong copySize) = GetMemoryOffsetAndSize(va, (ulong)(data.Length - offset)); - - data.Slice(offset, (int)copySize).CopyTo(memory.GetSpan(rangeOffset, (int)copySize)); - - va += copySize; - offset += (int)copySize; - } - } - catch (InvalidMemoryRegionException) - { - if (_invalidAccessHandler == null || !_invalidAccessHandler(va)) - { - throw; - } - } - } - - public ReadOnlySpan GetSpan(ulong va, int size, bool tracked = false) + public override ReadOnlySpan GetSpan(ulong va, int size, bool tracked = false) { if (size == 0) { @@ -346,13 +279,13 @@ namespace Ryujinx.Cpu.Jit { Span data = new byte[size]; - ReadImpl(va, data); + Read(va, data); return data; } } - public WritableRegion GetWritableRegion(ulong va, int size, bool tracked = false) + public override WritableRegion GetWritableRegion(ulong va, int size, bool tracked = false) { if (size == 0) { @@ -370,11 +303,11 @@ namespace Ryujinx.Cpu.Jit } else { - Memory memory = new byte[size]; + MemoryOwner memoryOwner = MemoryOwner.Rent(size); - ReadImpl(va, memory.Span); + Read(va, memoryOwner.Span); - return new WritableRegion(this, va, memory); + return new WritableRegion(this, va, memoryOwner); } } @@ -391,92 +324,24 @@ namespace Ryujinx.Cpu.Jit } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool IsMapped(ulong va) + public override bool IsMapped(ulong va) { - return ValidateAddress(va) && IsMappedImpl(va); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private bool IsMappedImpl(ulong va) - { - ulong page = va >> PageBits; - - int bit = (int)((page & 31) << 1); - - int pageIndex = (int)(page >> PageToPteShift); - ref ulong pageRef = ref _pageBitmap[pageIndex]; - - ulong pte = Volatile.Read(ref pageRef); - - return ((pte >> bit) & 3) != 0; + return ValidateAddress(va) && _pages.IsMapped(va); } public bool IsRangeMapped(ulong va, ulong size) { AssertValidAddressAndSize(va, size); - return IsRangeMappedImpl(va, size); + return _pages.IsRangeMapped(va, size); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void GetPageBlockRange(ulong pageStart, ulong pageEnd, out ulong startMask, out ulong endMask, out int pageIndex, out int pageEndIndex) - { - startMask = ulong.MaxValue << ((int)(pageStart & 31) << 1); - endMask = ulong.MaxValue >> (64 - ((int)(pageEnd & 31) << 1)); - - pageIndex = (int)(pageStart >> PageToPteShift); - pageEndIndex = (int)((pageEnd - 1) >> PageToPteShift); - } - - private bool IsRangeMappedImpl(ulong va, ulong size) - { - int pages = GetPagesCount(va, size, out _); - - if (pages == 1) - { - return IsMappedImpl(va); - } - - ulong pageStart = va >> PageBits; - ulong pageEnd = pageStart + (ulong)pages; - - GetPageBlockRange(pageStart, pageEnd, out ulong startMask, out ulong endMask, out int pageIndex, out int pageEndIndex); - - // Check if either bit in each 2 bit page entry is set. - // OR the block with itself shifted down by 1, and check the first bit of each entry. - - ulong mask = BlockMappedMask & startMask; - - while (pageIndex <= pageEndIndex) - { - if (pageIndex == pageEndIndex) - { - mask &= endMask; - } - - ref ulong pageRef = ref _pageBitmap[pageIndex++]; - ulong pte = Volatile.Read(ref pageRef); - - pte |= pte >> 1; - if ((pte & mask) != mask) - { - return false; - } - - mask = BlockMappedMask; - } - - return true; - } - - private static void ThrowMemoryNotContiguous() => throw new MemoryNotContiguousException(); - private bool TryGetVirtualContiguous(ulong va, int size, out MemoryBlock memory, out ulong offset) { if (_addressSpace.HasAnyPrivateAllocation(va, (ulong)size, out PrivateRange range)) { // If we have a private allocation overlapping the range, - // this the access is only considered contiguous if it covers the entire range. + // then the access is only considered contiguous if it covers the entire range. if (range.Memory != null) { @@ -559,8 +424,6 @@ namespace Ryujinx.Cpu.Jit private (MemoryBlock, ulong, ulong) GetMemoryOffsetAndSize(ulong va, ulong size) { - ulong endVa = va + size; - PrivateRange privateRange = _addressSpace.GetFirstPrivateAllocation(va, size, out ulong nextVa); if (privateRange.Memory != null) @@ -570,7 +433,7 @@ namespace Ryujinx.Cpu.Jit ulong physSize = GetContiguousSize(va, Math.Min(size, nextVa - va)); - return new(_backingMemory, GetPhysicalAddressChecked(va), physSize); + return (_backingMemory, GetPhysicalAddressChecked(va), physSize); } public IEnumerable GetHostRegions(ulong va, ulong size) @@ -589,7 +452,7 @@ namespace Ryujinx.Cpu.Jit { (MemoryBlock memory, ulong rangeOffset, ulong rangeSize) = GetMemoryOffsetAndSize(va, endVa - va); - regions.Add(new((UIntPtr)memory.GetPointer(rangeOffset, rangeSize), rangeSize)); + regions.Add(new((nuint)memory.GetPointer(rangeOffset, rangeSize), rangeSize)); va += rangeSize; } @@ -606,7 +469,7 @@ namespace Ryujinx.Cpu.Jit { if (size == 0) { - return Enumerable.Empty(); + return []; } return GetPhysicalRegionsImpl(va, size); @@ -651,44 +514,11 @@ namespace Ryujinx.Cpu.Jit return regions; } - private void ReadImpl(ulong va, Span data) - { - if (data.Length == 0) - { - return; - } - - try - { - AssertValidAddressAndSize(va, (ulong)data.Length); - - ulong endVa = va + (ulong)data.Length; - int offset = 0; - - while (va < endVa) - { - (MemoryBlock memory, ulong rangeOffset, ulong copySize) = GetMemoryOffsetAndSize(va, (ulong)(data.Length - offset)); - - memory.GetSpan(rangeOffset, (int)copySize).CopyTo(data.Slice(offset, (int)copySize)); - - va += copySize; - offset += (int)copySize; - } - } - catch (InvalidMemoryRegionException) - { - if (_invalidAccessHandler == null || !_invalidAccessHandler(va)) - { - throw; - } - } - } - /// /// /// This function also validates that the given range is both valid and mapped, and will throw if it is not. /// - public void SignalMemoryTracking(ulong va, ulong size, bool write, bool precise = false, int? exemptId = null) + public override void SignalMemoryTracking(ulong va, ulong size, bool write, bool precise = false, int? exemptId = null) { AssertValidAddressAndSize(va, size); @@ -700,101 +530,17 @@ namespace Ryujinx.Cpu.Jit // Software table, used for managed memory tracking. - int pages = GetPagesCount(va, size, out _); - ulong pageStart = va >> PageBits; - - if (pages == 1) - { - ulong tag = (ulong)(write ? HostMappedPtBits.WriteTracked : HostMappedPtBits.ReadWriteTracked); - - int bit = (int)((pageStart & 31) << 1); - - int pageIndex = (int)(pageStart >> PageToPteShift); - ref ulong pageRef = ref _pageBitmap[pageIndex]; - - ulong pte = Volatile.Read(ref pageRef); - ulong state = ((pte >> bit) & 3); - - if (state >= tag) - { - Tracking.VirtualMemoryEvent(va, size, write, precise: false, exemptId); - return; - } - else if (state == 0) - { - ThrowInvalidMemoryRegionException($"Not mapped: va=0x{va:X16}, size=0x{size:X16}"); - } - } - else - { - ulong pageEnd = pageStart + (ulong)pages; - - GetPageBlockRange(pageStart, pageEnd, out ulong startMask, out ulong endMask, out int pageIndex, out int pageEndIndex); - - ulong mask = startMask; - - ulong anyTrackingTag = (ulong)HostMappedPtBits.WriteTrackedReplicated; - - while (pageIndex <= pageEndIndex) - { - if (pageIndex == pageEndIndex) - { - mask &= endMask; - } - - ref ulong pageRef = ref _pageBitmap[pageIndex++]; - - ulong pte = Volatile.Read(ref pageRef); - ulong mappedMask = mask & BlockMappedMask; - - ulong mappedPte = pte | (pte >> 1); - if ((mappedPte & mappedMask) != mappedMask) - { - ThrowInvalidMemoryRegionException($"Not mapped: va=0x{va:X16}, size=0x{size:X16}"); - } - - pte &= mask; - if ((pte & anyTrackingTag) != 0) // Search for any tracking. - { - // Writes trigger any tracking. - // Only trigger tracking from reads if both bits are set on any page. - if (write || (pte & (pte >> 1) & BlockMappedMask) != 0) - { - Tracking.VirtualMemoryEvent(va, size, write, precise: false, exemptId); - break; - } - } - - mask = ulong.MaxValue; - } - } + _pages.SignalMemoryTracking(Tracking, va, size, write, exemptId); } - /// - /// Computes the number of pages in a virtual address range. - /// - /// Virtual address of the range - /// Size of the range - /// The virtual address of the beginning of the first page - /// This function does not differentiate between allocated and unallocated pages. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private int GetPagesCount(ulong va, ulong size, out ulong startVa) + public RegionHandle BeginTracking(ulong address, ulong size, int id, RegionFlags flags = RegionFlags.None) { - // WARNING: Always check if ulong does not overflow during the operations. - startVa = va & ~(ulong)PageMask; - ulong vaSpan = (va - startVa + size + PageMask) & ~(ulong)PageMask; - - return (int)(vaSpan / PageSize); + return Tracking.BeginTracking(address, size, id, flags); } - public RegionHandle BeginTracking(ulong address, ulong size, int id) + public MultiRegionHandle BeginGranularTracking(ulong address, ulong size, IEnumerable handles, ulong granularity, int id, RegionFlags flags = RegionFlags.None) { - return Tracking.BeginTracking(address, size, id); - } - - public MultiRegionHandle BeginGranularTracking(ulong address, ulong size, IEnumerable handles, ulong granularity, int id) - { - return Tracking.BeginGranularTracking(address, size, handles, granularity, id); + return Tracking.BeginGranularTracking(address, size, handles, granularity, id, flags); } public SmartMultiRegionHandle BeginSmartGranularTracking(ulong address, ulong size, ulong granularity, int id) @@ -802,86 +548,6 @@ namespace Ryujinx.Cpu.Jit return Tracking.BeginSmartGranularTracking(address, size, granularity, id); } - /// - /// Adds the given address mapping to the page table. - /// - /// Virtual memory address - /// Size to be mapped - private void AddMapping(ulong va, ulong size) - { - int pages = GetPagesCount(va, size, out _); - ulong pageStart = va >> PageBits; - ulong pageEnd = pageStart + (ulong)pages; - - GetPageBlockRange(pageStart, pageEnd, out ulong startMask, out ulong endMask, out int pageIndex, out int pageEndIndex); - - ulong mask = startMask; - - while (pageIndex <= pageEndIndex) - { - if (pageIndex == pageEndIndex) - { - mask &= endMask; - } - - ref ulong pageRef = ref _pageBitmap[pageIndex++]; - - ulong pte; - ulong mappedMask; - - // Map all 2-bit entries that are unmapped. - do - { - pte = Volatile.Read(ref pageRef); - - mappedMask = pte | (pte >> 1); - mappedMask |= (mappedMask & BlockMappedMask) << 1; - mappedMask |= ~mask; // Treat everything outside the range as mapped, thus unchanged. - } - while (Interlocked.CompareExchange(ref pageRef, (pte & mappedMask) | (BlockMappedMask & (~mappedMask)), pte) != pte); - - mask = ulong.MaxValue; - } - } - - /// - /// Removes the given address mapping from the page table. - /// - /// Virtual memory address - /// Size to be unmapped - private void RemoveMapping(ulong va, ulong size) - { - int pages = GetPagesCount(va, size, out _); - ulong pageStart = va >> PageBits; - ulong pageEnd = pageStart + (ulong)pages; - - GetPageBlockRange(pageStart, pageEnd, out ulong startMask, out ulong endMask, out int pageIndex, out int pageEndIndex); - - startMask = ~startMask; - endMask = ~endMask; - - ulong mask = startMask; - - while (pageIndex <= pageEndIndex) - { - if (pageIndex == pageEndIndex) - { - mask |= endMask; - } - - ref ulong pageRef = ref _pageBitmap[pageIndex++]; - ulong pte; - - do - { - pte = Volatile.Read(ref pageRef); - } - while (Interlocked.CompareExchange(ref pageRef, pte & mask, pte) != pte); - - mask = 0; - } - } - private ulong GetPhysicalAddressChecked(ulong va) { if (!IsMapped(va)) @@ -894,11 +560,9 @@ namespace Ryujinx.Cpu.Jit private ulong GetPhysicalAddressInternal(ulong va) { - return _pageTable.Read(va) + (va & PageMask); + return _nativePageTable.GetPhysicalAddress(va); } - private static void ThrowInvalidMemoryRegionException(string message) => throw new InvalidMemoryRegionException(message); - /// public void Reprotect(ulong va, ulong size, MemoryPermission protection) { @@ -906,91 +570,16 @@ namespace Ryujinx.Cpu.Jit } /// - public void TrackingReprotect(ulong va, ulong size, MemoryPermission protection) + public void TrackingReprotect(ulong va, ulong size, MemoryPermission protection, bool guest) { - // Protection is inverted on software pages, since the default value is 0. - protection = (~protection) & MemoryPermission.ReadAndWrite; - - int pages = GetPagesCount(va, size, out va); - ulong pageStart = va >> PageBits; - - if (pages == 1) + if (guest) { - ulong protTag = protection switch - { - MemoryPermission.None => (ulong)HostMappedPtBits.Mapped, - MemoryPermission.Write => (ulong)HostMappedPtBits.WriteTracked, - _ => (ulong)HostMappedPtBits.ReadWriteTracked, - }; - - int bit = (int)((pageStart & 31) << 1); - - ulong tagMask = 3UL << bit; - ulong invTagMask = ~tagMask; - - ulong tag = protTag << bit; - - int pageIndex = (int)(pageStart >> PageToPteShift); - ref ulong pageRef = ref _pageBitmap[pageIndex]; - - ulong pte; - - do - { - pte = Volatile.Read(ref pageRef); - } - while ((pte & tagMask) != 0 && Interlocked.CompareExchange(ref pageRef, (pte & invTagMask) | tag, pte) != pte); + _addressSpace.Reprotect(va, size, protection); } else { - ulong pageEnd = pageStart + (ulong)pages; - - GetPageBlockRange(pageStart, pageEnd, out ulong startMask, out ulong endMask, out int pageIndex, out int pageEndIndex); - - ulong mask = startMask; - - ulong protTag = protection switch - { - MemoryPermission.None => (ulong)HostMappedPtBits.MappedReplicated, - MemoryPermission.Write => (ulong)HostMappedPtBits.WriteTrackedReplicated, - _ => (ulong)HostMappedPtBits.ReadWriteTrackedReplicated, - }; - - while (pageIndex <= pageEndIndex) - { - if (pageIndex == pageEndIndex) - { - mask &= endMask; - } - - ref ulong pageRef = ref _pageBitmap[pageIndex++]; - - ulong pte; - ulong mappedMask; - - // Change the protection of all 2 bit entries that are mapped. - do - { - pte = Volatile.Read(ref pageRef); - - mappedMask = pte | (pte >> 1); - mappedMask |= (mappedMask & BlockMappedMask) << 1; - mappedMask &= mask; // Only update mapped pages within the given range. - } - while (Interlocked.CompareExchange(ref pageRef, (pte & (~mappedMask)) | (protTag & mappedMask), pte) != pte); - - mask = ulong.MaxValue; - } + _pages.TrackingReprotect(va, size, protection); } - - protection = protection switch - { - MemoryPermission.None => MemoryPermission.ReadAndWrite, - MemoryPermission.Write => MemoryPermission.Read, - _ => MemoryPermission.None, - }; - - _addressSpace.Reprotect(va, size, protection); } /// @@ -999,6 +588,47 @@ namespace Ryujinx.Cpu.Jit protected override void Destroy() { _addressSpace.Dispose(); + _nativePageTable.Dispose(); } + + protected override Memory GetPhysicalAddressMemory(nuint pa, int size) + => _backingMemory.GetMemory(pa, size); + + protected override Span GetPhysicalAddressSpan(nuint pa, int size) + => _backingMemory.GetSpan(pa, size); + + protected override void WriteImpl(ulong va, ReadOnlySpan data) + { + try + { + AssertValidAddressAndSize(va, (ulong)data.Length); + + ulong endVa = va + (ulong)data.Length; + int offset = 0; + + while (va < endVa) + { + (MemoryBlock memory, ulong rangeOffset, ulong copySize) = GetMemoryOffsetAndSize(va, (ulong)(data.Length - offset)); + + data.Slice(offset, (int)copySize).CopyTo(memory.GetSpan(rangeOffset, (int)copySize)); + + va += copySize; + offset += (int)copySize; + } + } + catch (InvalidMemoryRegionException) + { + if (_invalidAccessHandler == null || !_invalidAccessHandler(va)) + { + throw; + } + } + } + + protected override nuint TranslateVirtualAddressChecked(ulong va) + => (nuint)GetPhysicalAddressChecked(va); + + protected override nuint TranslateVirtualAddressUnchecked(ulong va) + => (nuint)GetPhysicalAddressInternal(va); } } diff --git a/src/Ryujinx.Cpu/LightningJit/AarchCompiler.cs b/src/Ryujinx.Cpu/LightningJit/AarchCompiler.cs index ee4fc439f..89e1499c0 100644 --- a/src/Ryujinx.Cpu/LightningJit/AarchCompiler.cs +++ b/src/Ryujinx.Cpu/LightningJit/AarchCompiler.cs @@ -15,7 +15,7 @@ namespace Ryujinx.Cpu.LightningJit IMemoryManager memoryManager, ulong address, AddressTable funcTable, - IntPtr dispatchStubPtr, + nint dispatchStubPtr, ExecutionMode executionMode, Architecture targetArch) { diff --git a/src/Ryujinx.Cpu/LightningJit/Arm32/A32Compiler.cs b/src/Ryujinx.Cpu/LightningJit/Arm32/A32Compiler.cs index 7f6024d47..0fe42b923 100644 --- a/src/Ryujinx.Cpu/LightningJit/Arm32/A32Compiler.cs +++ b/src/Ryujinx.Cpu/LightningJit/Arm32/A32Compiler.cs @@ -13,7 +13,7 @@ namespace Ryujinx.Cpu.LightningJit.Arm32 IMemoryManager memoryManager, ulong address, AddressTable funcTable, - IntPtr dispatchStubPtr, + nint dispatchStubPtr, bool isThumb, Architecture targetArch) { diff --git a/src/Ryujinx.Cpu/LightningJit/Arm32/Block.cs b/src/Ryujinx.Cpu/LightningJit/Arm32/Block.cs index 4729f6940..c4568995c 100644 --- a/src/Ryujinx.Cpu/LightningJit/Arm32/Block.cs +++ b/src/Ryujinx.Cpu/LightningJit/Arm32/Block.cs @@ -10,6 +10,7 @@ namespace Ryujinx.Cpu.LightningJit.Arm32 public readonly List Instructions; public readonly bool EndsWithBranch; public readonly bool HasHostCall; + public readonly bool HasHostCallSkipContext; public readonly bool IsTruncated; public readonly bool IsLoopEnd; public readonly bool IsThumb; @@ -20,6 +21,7 @@ namespace Ryujinx.Cpu.LightningJit.Arm32 List instructions, bool endsWithBranch, bool hasHostCall, + bool hasHostCallSkipContext, bool isTruncated, bool isLoopEnd, bool isThumb) @@ -31,6 +33,7 @@ namespace Ryujinx.Cpu.LightningJit.Arm32 Instructions = instructions; EndsWithBranch = endsWithBranch; HasHostCall = hasHostCall; + HasHostCallSkipContext = hasHostCallSkipContext; IsTruncated = isTruncated; IsLoopEnd = isLoopEnd; IsThumb = isThumb; @@ -57,6 +60,7 @@ namespace Ryujinx.Cpu.LightningJit.Arm32 Instructions.GetRange(0, splitIndex), false, HasHostCall, + HasHostCallSkipContext, false, false, IsThumb); @@ -67,6 +71,7 @@ namespace Ryujinx.Cpu.LightningJit.Arm32 Instructions.GetRange(splitIndex, splitCount), EndsWithBranch, HasHostCall, + HasHostCallSkipContext, IsTruncated, IsLoopEnd, IsThumb); diff --git a/src/Ryujinx.Cpu/LightningJit/Arm32/Decoder.cs b/src/Ryujinx.Cpu/LightningJit/Arm32/Decoder.cs index afd2c021f..8a2b389ad 100644 --- a/src/Ryujinx.Cpu/LightningJit/Arm32/Decoder.cs +++ b/src/Ryujinx.Cpu/LightningJit/Arm32/Decoder.cs @@ -208,6 +208,7 @@ namespace Ryujinx.Cpu.LightningJit.Arm32 InstMeta meta; InstFlags extraFlags = InstFlags.None; bool hasHostCall = false; + bool hasHostCallSkipContext = false; bool isTruncated = false; do @@ -246,9 +247,17 @@ namespace Ryujinx.Cpu.LightningJit.Arm32 meta = InstTableA32.GetMeta(encoding, cpuPreset.Version, cpuPreset.Features); } - if (meta.Name.IsSystemOrCall() && !hasHostCall) + if (meta.Name.IsSystemOrCall()) { - hasHostCall = meta.Name.IsCall() || InstEmitSystem.NeedsCall(meta.Name); + if (!hasHostCall) + { + hasHostCall = InstEmitSystem.NeedsCall(meta.Name); + } + + if (!hasHostCallSkipContext) + { + hasHostCallSkipContext = meta.Name.IsCall() || InstEmitSystem.NeedsCallSkipContext(meta.Name); + } } insts.Add(new(encoding, meta.Name, meta.EmitFunc, meta.Flags | extraFlags)); @@ -259,8 +268,8 @@ namespace Ryujinx.Cpu.LightningJit.Arm32 if (!isTruncated && IsBackwardsBranch(meta.Name, encoding)) { - hasHostCall = true; isLoopEnd = true; + hasHostCallSkipContext = true; } return new( @@ -269,6 +278,7 @@ namespace Ryujinx.Cpu.LightningJit.Arm32 insts, !isTruncated, hasHostCall, + hasHostCallSkipContext, isTruncated, isLoopEnd, isThumb); @@ -415,7 +425,7 @@ namespace Ryujinx.Cpu.LightningJit.Arm32 private static bool IsRtWrite(InstName name, uint encoding) { - // Some instruction can move GPR to FP/SIMD or FP/SIMD to GPR depending on the encoding. + // Some instructions can move GPR to FP/SIMD or FP/SIMD to GPR depending on the encoding. // Detect those cases so that we can tell if we're actually doing a register write. switch (name) diff --git a/src/Ryujinx.Cpu/LightningJit/Arm32/MultiBlock.cs b/src/Ryujinx.Cpu/LightningJit/Arm32/MultiBlock.cs index a213c222c..ca25057fe 100644 --- a/src/Ryujinx.Cpu/LightningJit/Arm32/MultiBlock.cs +++ b/src/Ryujinx.Cpu/LightningJit/Arm32/MultiBlock.cs @@ -6,6 +6,7 @@ namespace Ryujinx.Cpu.LightningJit.Arm32 { public readonly List Blocks; public readonly bool HasHostCall; + public readonly bool HasHostCallSkipContext; public readonly bool IsTruncated; public MultiBlock(List blocks) @@ -15,12 +16,14 @@ namespace Ryujinx.Cpu.LightningJit.Arm32 Block block = blocks[0]; HasHostCall = block.HasHostCall; + HasHostCallSkipContext = block.HasHostCallSkipContext; for (int index = 1; index < blocks.Count; index++) { block = blocks[index]; HasHostCall |= block.HasHostCall; + HasHostCallSkipContext |= block.HasHostCallSkipContext; } block = blocks[^1]; diff --git a/src/Ryujinx.Cpu/LightningJit/Arm32/RegisterAllocator.cs b/src/Ryujinx.Cpu/LightningJit/Arm32/RegisterAllocator.cs index 6c7057229..4a3f03b8a 100644 --- a/src/Ryujinx.Cpu/LightningJit/Arm32/RegisterAllocator.cs +++ b/src/Ryujinx.Cpu/LightningJit/Arm32/RegisterAllocator.cs @@ -106,6 +106,7 @@ namespace Ryujinx.Cpu.LightningJit.Arm32 if ((regMask & AbiConstants.ReservedRegsMask) == 0) { _gprMask |= regMask; + UsedGprsMask |= regMask; return firstCalleeSaved; } diff --git a/src/Ryujinx.Cpu/LightningJit/Arm32/Target/Arm64/Compiler.cs b/src/Ryujinx.Cpu/LightningJit/Arm32/Target/Arm64/Compiler.cs index 1e8a89157..0d56f28c9 100644 --- a/src/Ryujinx.Cpu/LightningJit/Arm32/Target/Arm64/Compiler.cs +++ b/src/Ryujinx.Cpu/LightningJit/Arm32/Target/Arm64/Compiler.cs @@ -24,10 +24,10 @@ namespace Ryujinx.Cpu.LightningJit.Arm32.Target.Arm64 public readonly MemoryManagerType MemoryManagerType; public readonly TailMerger TailMerger; public readonly AddressTable FuncTable; - public readonly IntPtr DispatchStubPointer; + public readonly nint DispatchStubPointer; private readonly RegisterSaveRestore _registerSaveRestore; - private readonly IntPtr _pageTablePointer; + private readonly nint _pageTablePointer; public Context( CodeWriter writer, @@ -36,8 +36,8 @@ namespace Ryujinx.Cpu.LightningJit.Arm32.Target.Arm64 TailMerger tailMerger, AddressTable funcTable, RegisterSaveRestore registerSaveRestore, - IntPtr dispatchStubPointer, - IntPtr pageTablePointer) + nint dispatchStubPointer, + nint pageTablePointer) { Writer = writer; RegisterAllocator = registerAllocator; @@ -226,7 +226,7 @@ namespace Ryujinx.Cpu.LightningJit.Arm32.Target.Arm64 } } - public static CompiledFunction Compile(CpuPreset cpuPreset, IMemoryManager memoryManager, ulong address, AddressTable funcTable, IntPtr dispatchStubPtr, bool isThumb) + public static CompiledFunction Compile(CpuPreset cpuPreset, IMemoryManager memoryManager, ulong address, AddressTable funcTable, nint dispatchStubPtr, bool isThumb) { MultiBlock multiBlock = Decoder.DecodeMulti(cpuPreset, memoryManager, address, isThumb); @@ -305,12 +305,23 @@ namespace Ryujinx.Cpu.LightningJit.Arm32.Target.Arm64 ForceConditionalEnd(cgContext, ref lastCondition, lastConditionIp); } + int reservedStackSize = 0; + + if (multiBlock.HasHostCall) + { + reservedStackSize = CalculateStackSizeForCallSpill(regAlloc.UsedGprsMask, regAlloc.UsedFpSimdMask, UsablePStateMask); + } + else if (multiBlock.HasHostCallSkipContext) + { + reservedStackSize = 2 * sizeof(ulong); // Context and page table pointers. + } + RegisterSaveRestore rsr = new( regAlloc.UsedGprsMask & AbiConstants.GprCalleeSavedRegsMask, regAlloc.UsedFpSimdMask & AbiConstants.FpSimdCalleeSavedRegsMask, OperandType.FP64, - multiBlock.HasHostCall, - multiBlock.HasHostCall ? CalculateStackSizeForCallSpill(regAlloc.UsedGprsMask, regAlloc.UsedFpSimdMask, UsablePStateMask) : 0); + multiBlock.HasHostCall || multiBlock.HasHostCallSkipContext, + reservedStackSize); TailMerger tailMerger = new(); @@ -596,7 +607,8 @@ namespace Ryujinx.Cpu.LightningJit.Arm32.Target.Arm64 name == InstName.Ldm || name == InstName.Ldmda || name == InstName.Ldmdb || - name == InstName.Ldmib) + name == InstName.Ldmib || + name == InstName.Pop) { // Arm32 does not have a return instruction, instead returns are implemented // either using BX LR (for leaf functions), or POP { ... PC }. @@ -711,7 +723,14 @@ namespace Ryujinx.Cpu.LightningJit.Arm32.Target.Arm64 switch (type) { case BranchType.SyncPoint: - InstEmitSystem.WriteSyncPoint(context.Writer, context.RegisterAllocator, context.TailMerger, context.GetReservedStackOffset()); + InstEmitSystem.WriteSyncPoint( + context.Writer, + ref asm, + context.RegisterAllocator, + context.TailMerger, + context.GetReservedStackOffset(), + context.StoreToContext, + context.LoadFromContext); break; case BranchType.SoftwareInterrupt: context.StoreToContext(); diff --git a/src/Ryujinx.Cpu/LightningJit/Arm32/Target/Arm64/InstEmitFlow.cs b/src/Ryujinx.Cpu/LightningJit/Arm32/Target/Arm64/InstEmitFlow.cs index 81e44ba00..48bdbb573 100644 --- a/src/Ryujinx.Cpu/LightningJit/Arm32/Target/Arm64/InstEmitFlow.cs +++ b/src/Ryujinx.Cpu/LightningJit/Arm32/Target/Arm64/InstEmitFlow.cs @@ -133,13 +133,17 @@ namespace Ryujinx.Cpu.LightningJit.Arm32.Target.Arm64 TailMerger tailMerger, Action writeEpilogue, AddressTable funcTable, - IntPtr funcPtr, + nint funcPtr, int spillBaseOffset, uint nextAddress, Operand guestAddress, bool isTail = false) { int tempRegister; + int tempGuestAddress = -1; + + bool inlineLookup = guestAddress.Kind != OperandKind.Constant && + funcTable is { Sparse: true }; if (guestAddress.Kind == OperandKind.Constant) { @@ -153,9 +157,16 @@ namespace Ryujinx.Cpu.LightningJit.Arm32.Target.Arm64 else { asm.StrRiUn(guestAddress, Register(regAlloc.FixedContextRegister), NativeContextOffsets.DispatchAddressOffset); + + if (inlineLookup && guestAddress.Value == 0) + { + // X0 will be overwritten. Move the address to a temp register. + tempGuestAddress = regAlloc.AllocateTempGprRegister(); + asm.Mov(Register(tempGuestAddress), guestAddress); + } } - tempRegister = regAlloc.FixedContextRegister == 1 ? 2 : 1; + tempRegister = NextFreeRegister(1, tempGuestAddress); if (!isTail) { @@ -176,6 +187,40 @@ namespace Ryujinx.Cpu.LightningJit.Arm32.Target.Arm64 asm.Mov(rn, funcPtrLoc & ~0xfffUL); asm.LdrRiUn(rn, rn, (int)(funcPtrLoc & 0xfffUL)); } + else if (inlineLookup) + { + // Inline table lookup. Only enabled when the sparse function table is enabled with 2 levels. + + Operand indexReg = Register(NextFreeRegister(tempRegister + 1, tempGuestAddress)); + + if (tempGuestAddress != -1) + { + guestAddress = Register(tempGuestAddress); + } + + ulong tableBase = (ulong)funcTable.Base; + + // Index into the table. + asm.Mov(rn, tableBase); + + for (int i = 0; i < funcTable.Levels.Length; i++) + { + var level = funcTable.Levels[i]; + asm.Ubfx(indexReg, guestAddress, level.Index, level.Length); + asm.Lsl(indexReg, indexReg, Const(3)); + + // Index into the page. + asm.Add(rn, rn, indexReg); + + // Load the page address. + asm.LdrRiUn(rn, rn, 0); + } + + if (tempGuestAddress != -1) + { + regAlloc.FreeTempGprRegister(tempGuestAddress); + } + } else { asm.Mov(rn, (ulong)funcPtr); @@ -199,12 +244,12 @@ namespace Ryujinx.Cpu.LightningJit.Arm32.Target.Arm64 } } - private static void WriteSpillSkipContext(ref Assembler asm, RegisterAllocator regAlloc, int spillOffset) + public static void WriteSpillSkipContext(ref Assembler asm, RegisterAllocator regAlloc, int spillOffset) { WriteSpillOrFillSkipContext(ref asm, regAlloc, spillOffset, spill: true); } - private static void WriteFillSkipContext(ref Assembler asm, RegisterAllocator regAlloc, int spillOffset) + public static void WriteFillSkipContext(ref Assembler asm, RegisterAllocator regAlloc, int spillOffset) { WriteSpillOrFillSkipContext(ref asm, regAlloc, spillOffset, spill: false); } @@ -252,5 +297,20 @@ namespace Ryujinx.Cpu.LightningJit.Arm32.Target.Arm64 { return new Operand(register, RegisterType.Integer, type); } + + private static Operand Const(long value, OperandType type = OperandType.I64) + { + return new Operand(type, (ulong)value); + } + + private static int NextFreeRegister(int start, int avoid) + { + if (start == avoid) + { + start++; + } + + return start; + } } } diff --git a/src/Ryujinx.Cpu/LightningJit/Arm32/Target/Arm64/InstEmitMemory.cs b/src/Ryujinx.Cpu/LightningJit/Arm32/Target/Arm64/InstEmitMemory.cs index 54e156d74..d8caee6e7 100644 --- a/src/Ryujinx.Cpu/LightningJit/Arm32/Target/Arm64/InstEmitMemory.cs +++ b/src/Ryujinx.Cpu/LightningJit/Arm32/Target/Arm64/InstEmitMemory.cs @@ -1126,7 +1126,10 @@ namespace Ryujinx.Cpu.LightningJit.Arm32.Target.Arm64 Operand destination64 = new(destination.Kind, OperandType.I64, destination.Value); Operand basePointer = new(regAlloc.FixedPageTableRegister, RegisterType.Integer, OperandType.I64); - if (mmType == MemoryManagerType.HostTracked) + // We don't need to mask the address for the safe mode, since it is already naturally limited to 32-bit + // and can never reach out of the guest address space. + + if (mmType.IsHostTracked()) { int tempRegister = regAlloc.AllocateTempGprRegister(); @@ -1138,7 +1141,7 @@ namespace Ryujinx.Cpu.LightningJit.Arm32.Target.Arm64 regAlloc.FreeTempGprRegister(tempRegister); } - else if (mmType == MemoryManagerType.HostMapped || mmType == MemoryManagerType.HostMappedUnsafe) + else if (mmType.IsHostMapped()) { asm.Add(destination64, basePointer, guestAddress); } diff --git a/src/Ryujinx.Cpu/LightningJit/Arm32/Target/Arm64/InstEmitMove.cs b/src/Ryujinx.Cpu/LightningJit/Arm32/Target/Arm64/InstEmitMove.cs index 88850cb33..d57750fc1 100644 --- a/src/Ryujinx.Cpu/LightningJit/Arm32/Target/Arm64/InstEmitMove.cs +++ b/src/Ryujinx.Cpu/LightningJit/Arm32/Target/Arm64/InstEmitMove.cs @@ -1,6 +1,5 @@ using Ryujinx.Cpu.LightningJit.CodeGen; using Ryujinx.Cpu.LightningJit.CodeGen.Arm64; -using System.Diagnostics; namespace Ryujinx.Cpu.LightningJit.Arm32.Target.Arm64 { diff --git a/src/Ryujinx.Cpu/LightningJit/Arm32/Target/Arm64/InstEmitNeonMemory.cs b/src/Ryujinx.Cpu/LightningJit/Arm32/Target/Arm64/InstEmitNeonMemory.cs index 7f997b604..e77dc0a29 100644 --- a/src/Ryujinx.Cpu/LightningJit/Arm32/Target/Arm64/InstEmitNeonMemory.cs +++ b/src/Ryujinx.Cpu/LightningJit/Arm32/Target/Arm64/InstEmitNeonMemory.cs @@ -717,6 +717,7 @@ namespace Ryujinx.Cpu.LightningJit.Arm32.Target.Arm64 } } + [Conditional("DEBUG")] private static void AssertSequentialRegisters(ReadOnlySpan registers) { for (int index = 1; index < registers.Length; index++) diff --git a/src/Ryujinx.Cpu/LightningJit/Arm32/Target/Arm64/InstEmitSaturate.cs b/src/Ryujinx.Cpu/LightningJit/Arm32/Target/Arm64/InstEmitSaturate.cs index e2354f448..f1b6e395b 100644 --- a/src/Ryujinx.Cpu/LightningJit/Arm32/Target/Arm64/InstEmitSaturate.cs +++ b/src/Ryujinx.Cpu/LightningJit/Arm32/Target/Arm64/InstEmitSaturate.cs @@ -114,7 +114,7 @@ namespace Ryujinx.Cpu.LightningJit.Arm32.Target.Arm64 InstEmitCommon.EmitUnsigned16BitPair(context, rd, rn, rm, (d, n, m) => { context.Arm64Assembler.Add(d, n, m); - EmitSaturateUnsignedRange(context, d, 16); + EmitSaturateUqadd(context, d, 16); }); } @@ -123,7 +123,7 @@ namespace Ryujinx.Cpu.LightningJit.Arm32.Target.Arm64 InstEmitCommon.EmitUnsigned8BitPair(context, rd, rn, rm, (d, n, m) => { context.Arm64Assembler.Add(d, n, m); - EmitSaturateUnsignedRange(context, d, 8); + EmitSaturateUqadd(context, d, 8); }); } @@ -140,7 +140,7 @@ namespace Ryujinx.Cpu.LightningJit.Arm32.Target.Arm64 context.Arm64Assembler.Add(d, n, m); } - EmitSaturateUnsignedRange(context, d, 16); + EmitSaturateUq(context, d, 16, e == 0); }); } @@ -157,25 +157,25 @@ namespace Ryujinx.Cpu.LightningJit.Arm32.Target.Arm64 context.Arm64Assembler.Sub(d, n, m); } - EmitSaturateUnsignedRange(context, d, 16); + EmitSaturateUq(context, d, 16, e != 0); }); } public static void Uqsub16(CodeGenContext context, uint rd, uint rn, uint rm) { - InstEmitCommon.EmitSigned16BitPair(context, rd, rn, rm, (d, n, m) => + InstEmitCommon.EmitUnsigned16BitPair(context, rd, rn, rm, (d, n, m) => { context.Arm64Assembler.Sub(d, n, m); - EmitSaturateUnsignedRange(context, d, 16); + EmitSaturateUqsub(context, d, 16); }); } public static void Uqsub8(CodeGenContext context, uint rd, uint rn, uint rm) { - InstEmitCommon.EmitSigned8BitPair(context, rd, rn, rm, (d, n, m) => + InstEmitCommon.EmitUnsigned8BitPair(context, rd, rn, rm, (d, n, m) => { context.Arm64Assembler.Sub(d, n, m); - EmitSaturateUnsignedRange(context, d, 8); + EmitSaturateUqsub(context, d, 8); }); } @@ -358,7 +358,17 @@ namespace Ryujinx.Cpu.LightningJit.Arm32.Target.Arm64 } } - private static void EmitSaturateUnsignedRange(CodeGenContext context, Operand value, uint saturateTo) + private static void EmitSaturateUqadd(CodeGenContext context, Operand value, uint saturateTo) + { + EmitSaturateUq(context, value, saturateTo, isSub: false); + } + + private static void EmitSaturateUqsub(CodeGenContext context, Operand value, uint saturateTo) + { + EmitSaturateUq(context, value, saturateTo, isSub: true); + } + + private static void EmitSaturateUq(CodeGenContext context, Operand value, uint saturateTo, bool isSub) { Debug.Assert(saturateTo <= 32); @@ -379,7 +389,7 @@ namespace Ryujinx.Cpu.LightningJit.Arm32.Target.Arm64 return; } - context.Arm64Assembler.Lsr(tempRegister.Operand, value, InstEmitCommon.Const(32 - (int)saturateTo)); + context.Arm64Assembler.Lsr(tempRegister.Operand, value, InstEmitCommon.Const((int)saturateTo)); int branchIndex = context.CodeWriter.InstructionPointer; @@ -387,7 +397,7 @@ namespace Ryujinx.Cpu.LightningJit.Arm32.Target.Arm64 context.Arm64Assembler.Cbz(tempRegister.Operand, 0); // Saturate. - context.Arm64Assembler.Mov(value, uint.MaxValue >> (32 - (int)saturateTo)); + context.Arm64Assembler.Mov(value, isSub ? 0u : uint.MaxValue >> (32 - (int)saturateTo)); int delta = context.CodeWriter.InstructionPointer - branchIndex; context.CodeWriter.WriteInstructionAt(branchIndex, context.CodeWriter.ReadInstructionAt(branchIndex) | (uint)((delta & 0x7ffff) << 5)); diff --git a/src/Ryujinx.Cpu/LightningJit/Arm32/Target/Arm64/InstEmitSystem.cs b/src/Ryujinx.Cpu/LightningJit/Arm32/Target/Arm64/InstEmitSystem.cs index 220f35d4b..4d97a2264 100644 --- a/src/Ryujinx.Cpu/LightningJit/Arm32/Target/Arm64/InstEmitSystem.cs +++ b/src/Ryujinx.Cpu/LightningJit/Arm32/Target/Arm64/InstEmitSystem.cs @@ -324,27 +324,27 @@ namespace Ryujinx.Cpu.LightningJit.Arm32.Target.Arm64 Udf(context, encoding, 0); } - private static IntPtr GetBkptHandlerPtr() + private static nint GetBkptHandlerPtr() { return Marshal.GetFunctionPointerForDelegate(NativeInterface.Break); } - private static IntPtr GetSvcHandlerPtr() + private static nint GetSvcHandlerPtr() { return Marshal.GetFunctionPointerForDelegate(NativeInterface.SupervisorCall); } - private static IntPtr GetUdfHandlerPtr() + private static nint GetUdfHandlerPtr() { return Marshal.GetFunctionPointerForDelegate(NativeInterface.Undefined); } - private static IntPtr GetCntpctEl0Ptr() + private static nint GetCntpctEl0Ptr() { return Marshal.GetFunctionPointerForDelegate(NativeInterface.GetCntpctEl0); } - private static IntPtr CheckSynchronizationPtr() + private static nint CheckSynchronizationPtr() { return Marshal.GetFunctionPointerForDelegate(NativeInterface.CheckSynchronization); } @@ -354,11 +354,18 @@ namespace Ryujinx.Cpu.LightningJit.Arm32.Target.Arm64 // All instructions that might do a host call should be included here. // That is required to reserve space on the stack for caller saved registers. + return name == InstName.Mrrc; + } + + public static bool NeedsCallSkipContext(InstName name) + { + // All instructions that might do a host call should be included here. + // That is required to reserve space on the stack for caller saved registers. + switch (name) { case InstName.Mcr: case InstName.Mrc: - case InstName.Mrrc: case InstName.Svc: case InstName.Udf: return true; @@ -372,7 +379,7 @@ namespace Ryujinx.Cpu.LightningJit.Arm32.Target.Arm64 Assembler asm = new(writer); WriteCall(ref asm, regAlloc, GetBkptHandlerPtr(), skipContext: true, spillBaseOffset, null, pc, imm); - WriteSyncPoint(writer, ref asm, regAlloc, tailMerger, skipContext: true, spillBaseOffset); + WriteSyncPoint(writer, ref asm, regAlloc, tailMerger, spillBaseOffset); } public static void WriteSvc(CodeWriter writer, RegisterAllocator regAlloc, TailMerger tailMerger, int spillBaseOffset, uint pc, uint svcId) @@ -380,7 +387,7 @@ namespace Ryujinx.Cpu.LightningJit.Arm32.Target.Arm64 Assembler asm = new(writer); WriteCall(ref asm, regAlloc, GetSvcHandlerPtr(), skipContext: true, spillBaseOffset, null, pc, svcId); - WriteSyncPoint(writer, ref asm, regAlloc, tailMerger, skipContext: true, spillBaseOffset); + WriteSyncPoint(writer, ref asm, regAlloc, tailMerger, spillBaseOffset); } public static void WriteUdf(CodeWriter writer, RegisterAllocator regAlloc, TailMerger tailMerger, int spillBaseOffset, uint pc, uint imm) @@ -388,7 +395,7 @@ namespace Ryujinx.Cpu.LightningJit.Arm32.Target.Arm64 Assembler asm = new(writer); WriteCall(ref asm, regAlloc, GetUdfHandlerPtr(), skipContext: true, spillBaseOffset, null, pc, imm); - WriteSyncPoint(writer, ref asm, regAlloc, tailMerger, skipContext: true, spillBaseOffset); + WriteSyncPoint(writer, ref asm, regAlloc, tailMerger, spillBaseOffset); } public static void WriteReadCntpct(CodeWriter writer, RegisterAllocator regAlloc, int spillBaseOffset, int rt, int rt2) @@ -422,14 +429,14 @@ namespace Ryujinx.Cpu.LightningJit.Arm32.Target.Arm64 WriteFill(ref asm, regAlloc, resultMask, skipContext: false, spillBaseOffset, tempRegister); } - public static void WriteSyncPoint(CodeWriter writer, RegisterAllocator regAlloc, TailMerger tailMerger, int spillBaseOffset) - { - Assembler asm = new(writer); - - WriteSyncPoint(writer, ref asm, regAlloc, tailMerger, skipContext: false, spillBaseOffset); - } - - private static void WriteSyncPoint(CodeWriter writer, ref Assembler asm, RegisterAllocator regAlloc, TailMerger tailMerger, bool skipContext, int spillBaseOffset) + public static void WriteSyncPoint( + CodeWriter writer, + ref Assembler asm, + RegisterAllocator regAlloc, + TailMerger tailMerger, + int spillBaseOffset, + Action storeToContext = null, + Action loadFromContext = null) { int tempRegister = regAlloc.AllocateTempGprRegister(); @@ -440,7 +447,8 @@ namespace Ryujinx.Cpu.LightningJit.Arm32.Target.Arm64 int branchIndex = writer.InstructionPointer; asm.Cbnz(rt, 0); - WriteSpill(ref asm, regAlloc, 1u << tempRegister, skipContext, spillBaseOffset, tempRegister); + storeToContext?.Invoke(); + WriteSpill(ref asm, regAlloc, 1u << tempRegister, skipContext: true, spillBaseOffset, tempRegister); Operand rn = Register(tempRegister == 0 ? 1 : 0); @@ -449,7 +457,8 @@ namespace Ryujinx.Cpu.LightningJit.Arm32.Target.Arm64 tailMerger.AddConditionalZeroReturn(writer, asm, Register(0, OperandType.I32)); - WriteFill(ref asm, regAlloc, 1u << tempRegister, skipContext, spillBaseOffset, tempRegister); + WriteFill(ref asm, regAlloc, 1u << tempRegister, skipContext: true, spillBaseOffset, tempRegister); + loadFromContext?.Invoke(); asm.LdrRiUn(rt, Register(regAlloc.FixedContextRegister), NativeContextOffsets.CounterOffset); @@ -465,7 +474,7 @@ namespace Ryujinx.Cpu.LightningJit.Arm32.Target.Arm64 private static void WriteCall( ref Assembler asm, RegisterAllocator regAlloc, - IntPtr funcPtr, + nint funcPtr, bool skipContext, int spillBaseOffset, int? resultRegister, @@ -489,8 +498,8 @@ namespace Ryujinx.Cpu.LightningJit.Arm32.Target.Arm64 // We only support up to 7 arguments right now. // ABI defines the first 8 integer arguments to be passed on registers X0-X7. - // We need at least one regiser to put the function address on, so that reduces the amount of - // register we can use for that by one. + // We need at least one register to put the function address on, so that reduces the number of + // registers we can use for that by one. Debug.Assert(callArgs.Length < 8); @@ -514,18 +523,31 @@ namespace Ryujinx.Cpu.LightningJit.Arm32.Target.Arm64 private static void WriteSpill(ref Assembler asm, RegisterAllocator regAlloc, uint exceptMask, bool skipContext, int spillOffset, int tempRegister) { - WriteSpillOrFill(ref asm, regAlloc, skipContext, exceptMask, spillOffset, tempRegister, spill: true); + if (skipContext) + { + InstEmitFlow.WriteSpillSkipContext(ref asm, regAlloc, spillOffset); + } + else + { + WriteSpillOrFill(ref asm, regAlloc, exceptMask, spillOffset, tempRegister, spill: true); + } } private static void WriteFill(ref Assembler asm, RegisterAllocator regAlloc, uint exceptMask, bool skipContext, int spillOffset, int tempRegister) { - WriteSpillOrFill(ref asm, regAlloc, skipContext, exceptMask, spillOffset, tempRegister, spill: false); + if (skipContext) + { + InstEmitFlow.WriteFillSkipContext(ref asm, regAlloc, spillOffset); + } + else + { + WriteSpillOrFill(ref asm, regAlloc, exceptMask, spillOffset, tempRegister, spill: false); + } } private static void WriteSpillOrFill( ref Assembler asm, RegisterAllocator regAlloc, - bool skipContext, uint exceptMask, int spillOffset, int tempRegister, @@ -533,11 +555,6 @@ namespace Ryujinx.Cpu.LightningJit.Arm32.Target.Arm64 { uint gprMask = regAlloc.UsedGprsMask & ~(AbiConstants.GprCalleeSavedRegsMask | exceptMask); - if (skipContext) - { - gprMask &= ~Compiler.UsableGprsMask; - } - if (!spill) { // We must reload the status register before reloading the GPRs, @@ -600,11 +617,6 @@ namespace Ryujinx.Cpu.LightningJit.Arm32.Target.Arm64 uint fpSimdMask = regAlloc.UsedFpSimdMask; - if (skipContext) - { - fpSimdMask &= ~Compiler.UsableFpSimdMask; - } - while (fpSimdMask != 0) { int reg = BitOperations.TrailingZeroCount(fpSimdMask); diff --git a/src/Ryujinx.Cpu/LightningJit/Arm64/A64Compiler.cs b/src/Ryujinx.Cpu/LightningJit/Arm64/A64Compiler.cs index b46ae3b0c..44de4cd0d 100644 --- a/src/Ryujinx.Cpu/LightningJit/Arm64/A64Compiler.cs +++ b/src/Ryujinx.Cpu/LightningJit/Arm64/A64Compiler.cs @@ -13,7 +13,7 @@ namespace Ryujinx.Cpu.LightningJit.Arm64 IMemoryManager memoryManager, ulong address, AddressTable funcTable, - IntPtr dispatchStubPtr, + nint dispatchStubPtr, Architecture targetArch) { if (targetArch == Architecture.Arm64) diff --git a/src/Ryujinx.Cpu/LightningJit/Arm64/InstName.cs b/src/Ryujinx.Cpu/LightningJit/Arm64/InstName.cs index 58d78ae6e..3391a2c14 100644 --- a/src/Ryujinx.Cpu/LightningJit/Arm64/InstName.cs +++ b/src/Ryujinx.Cpu/LightningJit/Arm64/InstName.cs @@ -1106,6 +1106,7 @@ namespace Ryujinx.Cpu.LightningJit.Arm64 case InstName.Mrs: case InstName.MsrImm: case InstName.MsrReg: + case InstName.Sysl: return true; } @@ -1130,5 +1131,37 @@ namespace Ryujinx.Cpu.LightningJit.Arm64 return false; } + + public static bool IsPartialRegisterUpdateMemory(this InstName name) + { + switch (name) + { + case InstName.Ld1AdvsimdSnglAsNoPostIndex: + case InstName.Ld1AdvsimdSnglAsPostIndex: + case InstName.Ld2AdvsimdSnglAsNoPostIndex: + case InstName.Ld2AdvsimdSnglAsPostIndex: + case InstName.Ld3AdvsimdSnglAsNoPostIndex: + case InstName.Ld3AdvsimdSnglAsPostIndex: + case InstName.Ld4AdvsimdSnglAsNoPostIndex: + case InstName.Ld4AdvsimdSnglAsPostIndex: + return true; + } + + return false; + } + + public static bool IsPrefetchMemory(this InstName name) + { + switch (name) + { + case InstName.PrfmImm: + case InstName.PrfmLit: + case InstName.PrfmReg: + case InstName.Prfum: + return true; + } + + return false; + } } } diff --git a/src/Ryujinx.Cpu/LightningJit/Arm64/RegisterAllocator.cs b/src/Ryujinx.Cpu/LightningJit/Arm64/RegisterAllocator.cs index 0a2b4f7aa..1c6eab0de 100644 --- a/src/Ryujinx.Cpu/LightningJit/Arm64/RegisterAllocator.cs +++ b/src/Ryujinx.Cpu/LightningJit/Arm64/RegisterAllocator.cs @@ -1,15 +1,12 @@ +using ARMeilleure.Memory; using Ryujinx.Cpu.LightningJit.CodeGen.Arm64; using System; -using System.Diagnostics; using System.Numerics; namespace Ryujinx.Cpu.LightningJit.Arm64 { class RegisterAllocator { - public const int MaxTemps = 2; - public const int MaxTempsInclFixed = MaxTemps + 2; - private uint _gprMask; private readonly uint _fpSimdMask; private readonly uint _pStateMask; @@ -25,7 +22,7 @@ namespace Ryujinx.Cpu.LightningJit.Arm64 public uint AllFpSimdMask => _fpSimdMask; public uint AllPStateMask => _pStateMask; - public RegisterAllocator(uint gprMask, uint fpSimdMask, uint pStateMask, bool hasHostCall) + public RegisterAllocator(MemoryManagerType mmType, uint gprMask, uint fpSimdMask, uint pStateMask, bool hasHostCall) { _gprMask = gprMask; _fpSimdMask = fpSimdMask; @@ -56,7 +53,7 @@ namespace Ryujinx.Cpu.LightningJit.Arm64 BuildRegisterMap(_registerMap); - Span tempRegisters = stackalloc int[MaxTemps]; + Span tempRegisters = stackalloc int[CalculateMaxTemps(mmType)]; for (int index = 0; index < tempRegisters.Length; index++) { @@ -150,5 +147,15 @@ namespace Ryujinx.Cpu.LightningJit.Arm64 { mask &= ~(1u << index); } + + public static int CalculateMaxTemps(MemoryManagerType mmType) + { + return mmType.IsHostMapped() ? 1 : 2; + } + + public static int CalculateMaxTempsInclFixed(MemoryManagerType mmType) + { + return CalculateMaxTemps(mmType) + 2; + } } } diff --git a/src/Ryujinx.Cpu/LightningJit/Arm64/RegisterUtils.cs b/src/Ryujinx.Cpu/LightningJit/Arm64/RegisterUtils.cs index eb3fc229f..191e03e7b 100644 --- a/src/Ryujinx.Cpu/LightningJit/Arm64/RegisterUtils.cs +++ b/src/Ryujinx.Cpu/LightningJit/Arm64/RegisterUtils.cs @@ -247,7 +247,7 @@ namespace Ryujinx.Cpu.LightningJit.Arm64 } } - if (!flags.HasFlag(InstFlags.ReadRt)) + if (!flags.HasFlag(InstFlags.ReadRt) || name.IsPartialRegisterUpdateMemory()) { if (flags.HasFlag(InstFlags.Rt)) { @@ -281,7 +281,7 @@ namespace Ryujinx.Cpu.LightningJit.Arm64 gprMask |= MaskFromIndex(ExtractRd(flags, encoding)); } - if (!flags.HasFlag(InstFlags.ReadRt)) + if (!flags.HasFlag(InstFlags.ReadRt) || name.IsPartialRegisterUpdateMemory()) { if (flags.HasFlag(InstFlags.Rt)) { diff --git a/src/Ryujinx.Cpu/LightningJit/Arm64/SysUtils.cs b/src/Ryujinx.Cpu/LightningJit/Arm64/SysUtils.cs new file mode 100644 index 000000000..69689a391 --- /dev/null +++ b/src/Ryujinx.Cpu/LightningJit/Arm64/SysUtils.cs @@ -0,0 +1,48 @@ +using System.Diagnostics; + +namespace Ryujinx.Cpu.LightningJit.Arm64 +{ + static class SysUtils + { + public static (uint, uint, uint, uint) UnpackOp1CRnCRmOp2(uint encoding) + { + uint op1 = (encoding >> 16) & 7; + uint crn = (encoding >> 12) & 0xf; + uint crm = (encoding >> 8) & 0xf; + uint op2 = (encoding >> 5) & 7; + + return (op1, crn, crm, op2); + } + + public static bool IsCacheInstEl0(uint encoding) + { + (uint op1, uint crn, uint crm, uint op2) = UnpackOp1CRnCRmOp2(encoding); + + return ((op1 << 11) | (crn << 7) | (crm << 3) | op2) switch + { + 0b011_0111_0100_001 => true, // DC ZVA + 0b011_0111_1010_001 => true, // DC CVAC + 0b011_0111_1100_001 => true, // DC CVAP + 0b011_0111_1011_001 => true, // DC CVAU + 0b011_0111_1110_001 => true, // DC CIVAC + 0b011_0111_0101_001 => true, // IC IVAU + _ => false, + }; + } + + public static bool IsCacheInstUciTrapped(uint encoding) + { + (uint op1, uint crn, uint crm, uint op2) = UnpackOp1CRnCRmOp2(encoding); + + return ((op1 << 11) | (crn << 7) | (crm << 3) | op2) switch + { + 0b011_0111_1010_001 => true, // DC CVAC + 0b011_0111_1100_001 => true, // DC CVAP + 0b011_0111_1011_001 => true, // DC CVAU + 0b011_0111_1110_001 => true, // DC CIVAC + 0b011_0111_0101_001 => true, // IC IVAU + _ => false, + }; + } + } +} diff --git a/src/Ryujinx.Cpu/LightningJit/Arm64/Target/Arm64/Compiler.cs b/src/Ryujinx.Cpu/LightningJit/Arm64/Target/Arm64/Compiler.cs index babe2cb41..4a3c507df 100644 --- a/src/Ryujinx.Cpu/LightningJit/Arm64/Target/Arm64/Compiler.cs +++ b/src/Ryujinx.Cpu/LightningJit/Arm64/Target/Arm64/Compiler.cs @@ -20,11 +20,11 @@ namespace Ryujinx.Cpu.LightningJit.Arm64.Target.Arm64 public readonly RegisterAllocator RegisterAllocator; public readonly TailMerger TailMerger; public readonly AddressTable FuncTable; - public readonly IntPtr DispatchStubPointer; + public readonly nint DispatchStubPointer; private readonly MultiBlock _multiBlock; private readonly RegisterSaveRestore _registerSaveRestore; - private readonly IntPtr _pageTablePointer; + private readonly nint _pageTablePointer; public Context( CodeWriter writer, @@ -33,8 +33,8 @@ namespace Ryujinx.Cpu.LightningJit.Arm64.Target.Arm64 RegisterSaveRestore registerSaveRestore, MultiBlock multiBlock, AddressTable funcTable, - IntPtr dispatchStubPointer, - IntPtr pageTablePointer) + nint dispatchStubPointer, + nint pageTablePointer) { Writer = writer; RegisterAllocator = registerAllocator; @@ -304,7 +304,7 @@ namespace Ryujinx.Cpu.LightningJit.Arm64.Target.Arm64 } } - public static CompiledFunction Compile(CpuPreset cpuPreset, IMemoryManager memoryManager, ulong address, AddressTable funcTable, IntPtr dispatchStubPtr) + public static CompiledFunction Compile(CpuPreset cpuPreset, IMemoryManager memoryManager, ulong address, AddressTable funcTable, nint dispatchStubPtr) { MultiBlock multiBlock = Decoder.DecodeMulti(cpuPreset, memoryManager, address); @@ -316,7 +316,7 @@ namespace Ryujinx.Cpu.LightningJit.Arm64.Target.Arm64 uint pStateUseMask = multiBlock.GlobalUseMask.PStateMask; CodeWriter writer = new(); - RegisterAllocator regAlloc = new(gprUseMask, fpSimdUseMask, pStateUseMask, multiBlock.HasHostCall); + RegisterAllocator regAlloc = new(memoryManager.Type, gprUseMask, fpSimdUseMask, pStateUseMask, multiBlock.HasHostCall); RegisterSaveRestore rsr = new( regAlloc.AllGprMask & AbiConstants.GprCalleeSavedRegsMask, regAlloc.AllFpSimdMask & AbiConstants.FpSimdCalleeSavedRegsMask, @@ -350,11 +350,20 @@ namespace Ryujinx.Cpu.LightningJit.Arm64.Target.Arm64 if (instInfo.AddressForm != AddressForm.None) { - InstEmitMemory.RewriteInstruction(memoryManager.Type, writer, regAlloc, instInfo.Name, instInfo.Flags, instInfo.AddressForm, pc, encoding); + InstEmitMemory.RewriteInstruction( + memoryManager.AddressSpaceBits, + memoryManager.Type, + writer, + regAlloc, + instInfo.Name, + instInfo.Flags, + instInfo.AddressForm, + pc, + encoding); } else if (instInfo.Name == InstName.Sys) { - InstEmitMemory.RewriteSysInstruction(memoryManager.Type, writer, regAlloc, encoding); + InstEmitMemory.RewriteSysInstruction(memoryManager.AddressSpaceBits, memoryManager.Type, writer, regAlloc, encoding); } else if (instInfo.Name.IsSystem()) { diff --git a/src/Ryujinx.Cpu/LightningJit/Arm64/Target/Arm64/Decoder.cs b/src/Ryujinx.Cpu/LightningJit/Arm64/Target/Arm64/Decoder.cs index 738b8a32d..d5e1eb19c 100644 --- a/src/Ryujinx.Cpu/LightningJit/Arm64/Target/Arm64/Decoder.cs +++ b/src/Ryujinx.Cpu/LightningJit/Arm64/Target/Arm64/Decoder.cs @@ -8,7 +8,7 @@ namespace Ryujinx.Cpu.LightningJit.Arm64.Target.Arm64 { static class Decoder { - private const int MaxInstructionsPerBlock = 1000; + private const int MaxInstructionsPerFunction = 10000; private const uint NzcvFlags = 0xfu << 28; private const uint CFlag = 0x1u << 29; @@ -22,10 +22,11 @@ namespace Ryujinx.Cpu.LightningJit.Arm64.Target.Arm64 bool hasHostCall = false; bool hasMemoryInstruction = false; + int totalInsts = 0; while (true) { - Block block = Decode(cpuPreset, memoryManager, address, ref useMask, ref hasHostCall, ref hasMemoryInstruction); + Block block = Decode(cpuPreset, memoryManager, address, ref totalInsts, ref useMask, ref hasHostCall, ref hasMemoryInstruction); if (!block.IsTruncated && TryGetBranchTarget(block, out ulong targetAddress)) { @@ -230,6 +231,7 @@ namespace Ryujinx.Cpu.LightningJit.Arm64.Target.Arm64 CpuPreset cpuPreset, IMemoryManager memoryManager, ulong address, + ref int totalInsts, ref RegisterMask useMask, ref bool hasHostCall, ref bool hasMemoryInstruction) @@ -255,7 +257,7 @@ namespace Ryujinx.Cpu.LightningJit.Arm64.Target.Arm64 (name, flags, AddressForm addressForm) = InstTable.GetInstNameAndFlags(encoding, cpuPreset.Version, cpuPreset.Features); - if (name.IsPrivileged()) + if (name.IsPrivileged() || (name == InstName.Sys && IsPrivilegedSys(encoding))) { name = InstName.UdfPermUndef; flags = InstFlags.None; @@ -272,7 +274,8 @@ namespace Ryujinx.Cpu.LightningJit.Arm64.Target.Arm64 uint tempGprUseMask = gprUseMask | instGprReadMask | instGprWriteMask; - if (CalculateAvailableTemps(tempGprUseMask) < CalculateRequiredGprTemps(tempGprUseMask) || insts.Count >= MaxInstructionsPerBlock) + if (CalculateAvailableTemps(tempGprUseMask) < CalculateRequiredGprTemps(memoryManager.Type, tempGprUseMask) || + totalInsts++ >= MaxInstructionsPerFunction) { isTruncated = true; address -= 4UL; @@ -339,6 +342,11 @@ namespace Ryujinx.Cpu.LightningJit.Arm64.Target.Arm64 return new(startAddress, address, insts, !isTruncated && !name.IsException(), isTruncated, isLoopEnd); } + private static bool IsPrivilegedSys(uint encoding) + { + return !SysUtils.IsCacheInstEl0(encoding); + } + private static bool IsMrsNzcv(uint encoding) { return (encoding & ~0x1fu) == 0xd53b4200u; @@ -371,9 +379,9 @@ namespace Ryujinx.Cpu.LightningJit.Arm64.Target.Arm64 return false; } - private static int CalculateRequiredGprTemps(uint gprUseMask) + private static int CalculateRequiredGprTemps(MemoryManagerType mmType, uint gprUseMask) { - return BitOperations.PopCount(gprUseMask & RegisterUtils.ReservedRegsMask) + RegisterAllocator.MaxTempsInclFixed; + return BitOperations.PopCount(gprUseMask & RegisterUtils.ReservedRegsMask) + RegisterAllocator.CalculateMaxTempsInclFixed(mmType); } private static int CalculateAvailableTemps(uint gprUseMask) diff --git a/src/Ryujinx.Cpu/LightningJit/Arm64/Target/Arm64/InstEmitMemory.cs b/src/Ryujinx.Cpu/LightningJit/Arm64/Target/Arm64/InstEmitMemory.cs index c9c1c86f2..790a7de95 100644 --- a/src/Ryujinx.Cpu/LightningJit/Arm64/Target/Arm64/InstEmitMemory.cs +++ b/src/Ryujinx.Cpu/LightningJit/Arm64/Target/Arm64/InstEmitMemory.cs @@ -11,8 +11,16 @@ namespace Ryujinx.Cpu.LightningJit.Arm64.Target.Arm64 private const uint XMask = 0x3f808000u; private const uint XValue = 0x8000000u; - public static void RewriteSysInstruction(MemoryManagerType mmType, CodeWriter writer, RegisterAllocator regAlloc, uint encoding) + public static void RewriteSysInstruction(int asBits, MemoryManagerType mmType, CodeWriter writer, RegisterAllocator regAlloc, uint encoding) { + // TODO: Handle IC instruction, it should invalidate the JIT cache. + + if (InstEmitSystem.IsCacheInstForbidden(encoding)) + { + // Current OS does not allow cache maintenance instructions from user mode, just do nothing. + return; + } + int rtIndex = RegisterUtils.ExtractRt(encoding); if (rtIndex == RegisterUtils.ZrIndex) { @@ -27,7 +35,7 @@ namespace Ryujinx.Cpu.LightningJit.Arm64.Target.Arm64 Assembler asm = new(writer); - WriteAddressTranslation(mmType, regAlloc, ref asm, rt, guestAddress); + WriteAddressTranslation(asBits, mmType, regAlloc, ref asm, rt, guestAddress); encoding = RegisterUtils.ReplaceRt(encoding, tempRegister); @@ -37,6 +45,7 @@ namespace Ryujinx.Cpu.LightningJit.Arm64.Target.Arm64 } public static void RewriteInstruction( + int asBits, MemoryManagerType mmType, CodeWriter writer, RegisterAllocator regAlloc, @@ -46,22 +55,32 @@ namespace Ryujinx.Cpu.LightningJit.Arm64.Target.Arm64 ulong pc, uint encoding) { + if (name.IsPrefetchMemory() && mmType == MemoryManagerType.HostTrackedUnsafe) + { + // Prefetch to invalid addresses do not cause faults, so for memory manager + // types where we need to access the page table before doing the prefetch, + // we should make sure we won't try to access an out of bounds page table region. + // To do this, we force the masked memory manager variant to be used. + + mmType = MemoryManagerType.HostTracked; + } + switch (addressForm) { case AddressForm.OffsetReg: - RewriteOffsetRegMemoryInstruction(mmType, writer, regAlloc, flags, encoding); + RewriteOffsetRegMemoryInstruction(asBits, mmType, writer, regAlloc, flags, encoding); break; case AddressForm.PostIndexed: - RewritePostIndexedMemoryInstruction(mmType, writer, regAlloc, flags, encoding); + RewritePostIndexedMemoryInstruction(asBits, mmType, writer, regAlloc, flags, encoding); break; case AddressForm.PreIndexed: - RewritePreIndexedMemoryInstruction(mmType, writer, regAlloc, flags, encoding); + RewritePreIndexedMemoryInstruction(asBits, mmType, writer, regAlloc, flags, encoding); break; case AddressForm.SignedScaled: - RewriteSignedScaledMemoryInstruction(mmType, writer, regAlloc, flags, encoding); + RewriteSignedScaledMemoryInstruction(asBits, mmType, writer, regAlloc, flags, encoding); break; case AddressForm.UnsignedScaled: - RewriteUnsignedScaledMemoryInstruction(mmType, writer, regAlloc, flags, encoding); + RewriteUnsignedScaledMemoryInstruction(asBits, mmType, writer, regAlloc, flags, encoding); break; case AddressForm.BaseRegister: // Some applications uses unordered memory instructions in places where @@ -74,19 +93,19 @@ namespace Ryujinx.Cpu.LightningJit.Arm64.Target.Arm64 encoding |= 1u << 15; } - RewriteBaseRegisterMemoryInstruction(mmType, writer, regAlloc, encoding); + RewriteBaseRegisterMemoryInstruction(asBits, mmType, writer, regAlloc, encoding); break; case AddressForm.StructNoOffset: - RewriteBaseRegisterMemoryInstruction(mmType, writer, regAlloc, encoding); + RewriteBaseRegisterMemoryInstruction(asBits, mmType, writer, regAlloc, encoding); break; case AddressForm.BasePlusOffset: - RewriteBasePlusOffsetMemoryInstruction(mmType, writer, regAlloc, encoding); + RewriteBasePlusOffsetMemoryInstruction(asBits, mmType, writer, regAlloc, encoding); break; case AddressForm.Literal: - RewriteLiteralMemoryInstruction(mmType, writer, regAlloc, name, pc, encoding); + RewriteLiteralMemoryInstruction(asBits, mmType, writer, regAlloc, name, pc, encoding); break; case AddressForm.StructPostIndexedReg: - RewriteStructPostIndexedRegMemoryInstruction(mmType, writer, regAlloc, encoding); + RewriteStructPostIndexedRegMemoryInstruction(asBits, mmType, writer, regAlloc, encoding); break; default: writer.WriteInstruction(encoding); @@ -94,7 +113,7 @@ namespace Ryujinx.Cpu.LightningJit.Arm64.Target.Arm64 } } - private static void RewriteOffsetRegMemoryInstruction(MemoryManagerType mmType, CodeWriter writer, RegisterAllocator regAlloc, InstFlags flags, uint encoding) + private static void RewriteOffsetRegMemoryInstruction(int asBits, MemoryManagerType mmType, CodeWriter writer, RegisterAllocator regAlloc, InstFlags flags, uint encoding) { // TODO: Some unallocated encoding cases. @@ -118,7 +137,7 @@ namespace Ryujinx.Cpu.LightningJit.Arm64.Target.Arm64 asm.Add(rn, guestAddress, guestOffset, extensionType, shift); - WriteAddressTranslation(mmType, regAlloc, ref asm, rn, rn); + WriteAddressTranslation(asBits, mmType, regAlloc, ref asm, rn, rn); encoding = RegisterUtils.ReplaceRn(encoding, tempRegister); encoding = (encoding & ~(0xfffu << 10)) | (1u << 24); // Register -> Unsigned offset @@ -128,7 +147,7 @@ namespace Ryujinx.Cpu.LightningJit.Arm64.Target.Arm64 regAlloc.FreeTempGprRegister(tempRegister); } - private static void RewritePostIndexedMemoryInstruction(MemoryManagerType mmType, CodeWriter writer, RegisterAllocator regAlloc, InstFlags flags, uint encoding) + private static void RewritePostIndexedMemoryInstruction(int asBits, MemoryManagerType mmType, CodeWriter writer, RegisterAllocator regAlloc, InstFlags flags, uint encoding) { bool isPair = flags.HasFlag(InstFlags.Rt2); int imm = isPair ? ExtractSImm7Scaled(flags, encoding) : ExtractSImm9(encoding); @@ -139,7 +158,7 @@ namespace Ryujinx.Cpu.LightningJit.Arm64.Target.Arm64 Assembler asm = new(writer); - WriteAddressTranslation(mmType, regAlloc, ref asm, rn, guestAddress); + WriteAddressTranslation(asBits, mmType, regAlloc, ref asm, rn, guestAddress); encoding = RegisterUtils.ReplaceRn(encoding, tempRegister); @@ -162,7 +181,7 @@ namespace Ryujinx.Cpu.LightningJit.Arm64.Target.Arm64 regAlloc.FreeTempGprRegister(tempRegister); } - private static void RewritePreIndexedMemoryInstruction(MemoryManagerType mmType, CodeWriter writer, RegisterAllocator regAlloc, InstFlags flags, uint encoding) + private static void RewritePreIndexedMemoryInstruction(int asBits, MemoryManagerType mmType, CodeWriter writer, RegisterAllocator regAlloc, InstFlags flags, uint encoding) { bool isPair = flags.HasFlag(InstFlags.Rt2); int imm = isPair ? ExtractSImm7Scaled(flags, encoding) : ExtractSImm9(encoding); @@ -174,7 +193,7 @@ namespace Ryujinx.Cpu.LightningJit.Arm64.Target.Arm64 Assembler asm = new(writer); WriteAddConstant(ref asm, guestAddress, guestAddress, imm); - WriteAddressTranslation(mmType, regAlloc, ref asm, rn, guestAddress); + WriteAddressTranslation(asBits, mmType, regAlloc, ref asm, rn, guestAddress); encoding = RegisterUtils.ReplaceRn(encoding, tempRegister); @@ -195,27 +214,27 @@ namespace Ryujinx.Cpu.LightningJit.Arm64.Target.Arm64 regAlloc.FreeTempGprRegister(tempRegister); } - private static void RewriteSignedScaledMemoryInstruction(MemoryManagerType mmType, CodeWriter writer, RegisterAllocator regAlloc, InstFlags flags, uint encoding) + private static void RewriteSignedScaledMemoryInstruction(int asBits, MemoryManagerType mmType, CodeWriter writer, RegisterAllocator regAlloc, InstFlags flags, uint encoding) { - RewriteMemoryInstruction(mmType, writer, regAlloc, encoding, ExtractSImm7Scaled(flags, encoding), 0x7fu << 15); + RewriteMemoryInstruction(asBits, mmType, writer, regAlloc, encoding, ExtractSImm7Scaled(flags, encoding), 0x7fu << 15); } - private static void RewriteUnsignedScaledMemoryInstruction(MemoryManagerType mmType, CodeWriter writer, RegisterAllocator regAlloc, InstFlags flags, uint encoding) + private static void RewriteUnsignedScaledMemoryInstruction(int asBits, MemoryManagerType mmType, CodeWriter writer, RegisterAllocator regAlloc, InstFlags flags, uint encoding) { - RewriteMemoryInstruction(mmType, writer, regAlloc, encoding, ExtractUImm12Scaled(flags, encoding), 0xfffu << 10); + RewriteMemoryInstruction(asBits, mmType, writer, regAlloc, encoding, ExtractUImm12Scaled(flags, encoding), 0xfffu << 10); } - private static void RewriteBaseRegisterMemoryInstruction(MemoryManagerType mmType, CodeWriter writer, RegisterAllocator regAlloc, uint encoding) + private static void RewriteBaseRegisterMemoryInstruction(int asBits, MemoryManagerType mmType, CodeWriter writer, RegisterAllocator regAlloc, uint encoding) { - RewriteMemoryInstruction(mmType, writer, regAlloc, encoding, 0, 0u); + RewriteMemoryInstruction(asBits, mmType, writer, regAlloc, encoding, 0, 0u); } - private static void RewriteBasePlusOffsetMemoryInstruction(MemoryManagerType mmType, CodeWriter writer, RegisterAllocator regAlloc, uint encoding) + private static void RewriteBasePlusOffsetMemoryInstruction(int asBits, MemoryManagerType mmType, CodeWriter writer, RegisterAllocator regAlloc, uint encoding) { - RewriteMemoryInstruction(mmType, writer, regAlloc, encoding, ExtractSImm9(encoding), 0x1ffu << 12); + RewriteMemoryInstruction(asBits, mmType, writer, regAlloc, encoding, ExtractSImm9(encoding), 0x1ffu << 12); } - private static void RewriteMemoryInstruction(MemoryManagerType mmType, CodeWriter writer, RegisterAllocator regAlloc, uint encoding, int imm, uint immMask) + private static void RewriteMemoryInstruction(int asBits, MemoryManagerType mmType, CodeWriter writer, RegisterAllocator regAlloc, uint encoding, int imm, uint immMask) { int tempRegister = regAlloc.AllocateTempGprRegister(); Operand rn = new(tempRegister, RegisterType.Integer, OperandType.I64); @@ -229,7 +248,7 @@ namespace Ryujinx.Cpu.LightningJit.Arm64.Target.Arm64 imm = 0; } - WriteAddressTranslation(mmType, regAlloc, ref asm, rn, guestAddress, imm); + WriteAddressTranslation(asBits, mmType, regAlloc, ref asm, rn, guestAddress, imm); encoding = RegisterUtils.ReplaceRn(encoding, tempRegister); @@ -243,7 +262,7 @@ namespace Ryujinx.Cpu.LightningJit.Arm64.Target.Arm64 regAlloc.FreeTempGprRegister(tempRegister); } - private static void RewriteLiteralMemoryInstruction(MemoryManagerType mmType, CodeWriter writer, RegisterAllocator regAlloc, InstName name, ulong pc, uint encoding) + private static void RewriteLiteralMemoryInstruction(int asBits, MemoryManagerType mmType, CodeWriter writer, RegisterAllocator regAlloc, InstName name, ulong pc, uint encoding) { Assembler asm = new(writer); @@ -308,7 +327,7 @@ namespace Ryujinx.Cpu.LightningJit.Arm64.Target.Arm64 int tempRegister = regAlloc.AllocateTempGprRegister(); Operand rn = new(tempRegister, RegisterType.Integer, OperandType.I64); - WriteAddressTranslation(mmType, regAlloc, ref asm, rn, targetAddress); + WriteAddressTranslation(asBits, mmType, regAlloc, ref asm, rn, targetAddress); switch (name) { @@ -332,7 +351,7 @@ namespace Ryujinx.Cpu.LightningJit.Arm64.Target.Arm64 } } - private static void RewriteStructPostIndexedRegMemoryInstruction(MemoryManagerType mmType, CodeWriter writer, RegisterAllocator regAlloc, uint encoding) + private static void RewriteStructPostIndexedRegMemoryInstruction(int asBits, MemoryManagerType mmType, CodeWriter writer, RegisterAllocator regAlloc, uint encoding) { // TODO: Some unallocated encoding cases. @@ -344,7 +363,7 @@ namespace Ryujinx.Cpu.LightningJit.Arm64.Target.Arm64 Assembler asm = new(writer); - WriteAddressTranslation(mmType, regAlloc, ref asm, rn, guestAddress); + WriteAddressTranslation(asBits, mmType, regAlloc, ref asm, rn, guestAddress); encoding = RegisterUtils.ReplaceRn(encoding, tempRegister); encoding &= ~((0x1fu << 16) | (1u << 23)); // Post-index -> No offset @@ -471,6 +490,7 @@ namespace Ryujinx.Cpu.LightningJit.Arm64.Target.Arm64 } private static void WriteAddressTranslation( + int asBits, MemoryManagerType mmType, RegisterAllocator regAlloc, ref Assembler asm, @@ -498,34 +518,58 @@ namespace Ryujinx.Cpu.LightningJit.Arm64.Target.Arm64 guestAddress = destination; } - WriteAddressTranslation(mmType, regAlloc, ref asm, destination, guestAddress); + WriteAddressTranslation(asBits, mmType, regAlloc, ref asm, destination, guestAddress); } - private static void WriteAddressTranslation(MemoryManagerType mmType, RegisterAllocator regAlloc, ref Assembler asm, Operand destination, ulong guestAddress) + private static void WriteAddressTranslation( + int asBits, + MemoryManagerType mmType, + RegisterAllocator regAlloc, + ref Assembler asm, + Operand destination, + ulong guestAddress) { asm.Mov(destination, guestAddress); - WriteAddressTranslation(mmType, regAlloc, ref asm, destination, destination); + WriteAddressTranslation(asBits, mmType, regAlloc, ref asm, destination, destination); } - private static void WriteAddressTranslation(MemoryManagerType mmType, RegisterAllocator regAlloc, ref Assembler asm, Operand destination, Operand guestAddress) + private static void WriteAddressTranslation( + int asBits, + MemoryManagerType mmType, + RegisterAllocator regAlloc, + ref Assembler asm, + Operand destination, + Operand guestAddress) { Operand basePointer = new(regAlloc.FixedPageTableRegister, RegisterType.Integer, OperandType.I64); - if (mmType == MemoryManagerType.HostTracked) + if (mmType.IsHostTracked()) { int tempRegister = regAlloc.AllocateTempGprRegister(); Operand pte = new(tempRegister, RegisterType.Integer, OperandType.I64); asm.Lsr(pte, guestAddress, new Operand(OperandKind.Constant, OperandType.I32, 12)); + + if (mmType == MemoryManagerType.HostTracked) + { + asm.And(pte, pte, new Operand(OperandKind.Constant, OperandType.I64, ulong.MaxValue >> (64 - (asBits - 12)))); + } + asm.LdrRr(pte, basePointer, pte, ArmExtensionType.Uxtx, true); asm.Add(destination, pte, guestAddress); regAlloc.FreeTempGprRegister(tempRegister); } - else if (mmType == MemoryManagerType.HostMapped || mmType == MemoryManagerType.HostMappedUnsafe) + else if (mmType.IsHostMapped()) { + if (mmType == MemoryManagerType.HostMapped) + { + asm.And(destination, guestAddress, new Operand(OperandKind.Constant, OperandType.I64, ulong.MaxValue >> (64 - asBits))); + guestAddress = destination; + } + asm.Add(destination, basePointer, guestAddress); } else diff --git a/src/Ryujinx.Cpu/LightningJit/Arm64/Target/Arm64/InstEmitSystem.cs b/src/Ryujinx.Cpu/LightningJit/Arm64/Target/Arm64/InstEmitSystem.cs index 8005ecd2b..f534e8b6e 100644 --- a/src/Ryujinx.Cpu/LightningJit/Arm64/Target/Arm64/InstEmitSystem.cs +++ b/src/Ryujinx.Cpu/LightningJit/Arm64/Target/Arm64/InstEmitSystem.cs @@ -69,7 +69,7 @@ namespace Ryujinx.Cpu.LightningJit.Arm64.Target.Arm64 asm.LdrRiUn(Register((int)rd), Register(regAlloc.FixedContextRegister), NativeContextOffsets.TpidrEl0Offset); } } - else if ((encoding & ~0x1f) == 0xd53b0020 && (OperatingSystem.IsMacOS() || OperatingSystem.IsIOS())) // mrs x0, ctr_el0 + else if ((encoding & ~0x1f) == 0xd53b0020 && IsCtrEl0AccessForbidden()) // mrs x0, ctr_el0 { uint rd = encoding & 0x1f; @@ -115,7 +115,7 @@ namespace Ryujinx.Cpu.LightningJit.Arm64.Target.Arm64 { return true; } - else if ((encoding & ~0x1f) == 0xd53b0020 && (OperatingSystem.IsMacOS() || OperatingSystem.IsIOS())) // mrs x0, ctr_el0 + else if ((encoding & ~0x1f) == 0xd53b0020 && IsCtrEl0AccessForbidden()) // mrs x0, ctr_el0 { return true; } @@ -127,32 +127,44 @@ namespace Ryujinx.Cpu.LightningJit.Arm64.Target.Arm64 return false; } + private static bool IsCtrEl0AccessForbidden() + { + // Only Linux allows accessing CTR_EL0 from user mode. + return OperatingSystem.IsWindows() || OperatingSystem.IsMacOS() || OperatingSystem.IsIOS(); + } + + public static bool IsCacheInstForbidden(uint encoding) + { + // Windows does not allow the cache maintenance instructions to be used from user mode. + return OperatingSystem.IsWindows() && SysUtils.IsCacheInstUciTrapped(encoding); + } + public static bool NeedsContextStoreLoad(InstName name) { return name == InstName.Svc; } - private static IntPtr GetBrkHandlerPtr() + private static nint GetBrkHandlerPtr() { return Marshal.GetFunctionPointerForDelegate(NativeInterface.Break); } - private static IntPtr GetSvcHandlerPtr() + private static nint GetSvcHandlerPtr() { return Marshal.GetFunctionPointerForDelegate(NativeInterface.SupervisorCall); } - private static IntPtr GetUdfHandlerPtr() + private static nint GetUdfHandlerPtr() { return Marshal.GetFunctionPointerForDelegate(NativeInterface.Undefined); } - private static IntPtr GetCntpctEl0Ptr() + private static nint GetCntpctEl0Ptr() { return Marshal.GetFunctionPointerForDelegate(NativeInterface.GetCntpctEl0); } - private static IntPtr CheckSynchronizationPtr() + private static nint CheckSynchronizationPtr() { return Marshal.GetFunctionPointerForDelegate(NativeInterface.CheckSynchronization); } @@ -203,7 +215,7 @@ namespace Ryujinx.Cpu.LightningJit.Arm64.Target.Arm64 TailMerger tailMerger, Action writeEpilogue, AddressTable funcTable, - IntPtr dispatchStubPtr, + nint dispatchStubPtr, InstName name, ulong pc, uint encoding, @@ -286,13 +298,17 @@ namespace Ryujinx.Cpu.LightningJit.Arm64.Target.Arm64 TailMerger tailMerger, Action writeEpilogue, AddressTable funcTable, - IntPtr funcPtr, + nint funcPtr, int spillBaseOffset, ulong pc, Operand guestAddress, bool isTail = false) { int tempRegister; + int tempGuestAddress = -1; + + bool inlineLookup = guestAddress.Kind != OperandKind.Constant && + funcTable is { Sparse: true }; if (guestAddress.Kind == OperandKind.Constant) { @@ -306,9 +322,16 @@ namespace Ryujinx.Cpu.LightningJit.Arm64.Target.Arm64 else { asm.StrRiUn(guestAddress, Register(regAlloc.FixedContextRegister), NativeContextOffsets.DispatchAddressOffset); + + if (inlineLookup && guestAddress.Value == 0) + { + // X0 will be overwritten. Move the address to a temp register. + tempGuestAddress = regAlloc.AllocateTempGprRegister(); + asm.Mov(Register(tempGuestAddress), guestAddress); + } } - tempRegister = regAlloc.FixedContextRegister == 1 ? 2 : 1; + tempRegister = NextFreeRegister(1, tempGuestAddress); if (!isTail) { @@ -329,6 +352,40 @@ namespace Ryujinx.Cpu.LightningJit.Arm64.Target.Arm64 asm.Mov(rn, funcPtrLoc & ~0xfffUL); asm.LdrRiUn(rn, rn, (int)(funcPtrLoc & 0xfffUL)); } + else if (inlineLookup) + { + // Inline table lookup. Only enabled when the sparse function table is enabled with 2 levels. + + Operand indexReg = Register(NextFreeRegister(tempRegister + 1, tempGuestAddress)); + + if (tempGuestAddress != -1) + { + guestAddress = Register(tempGuestAddress); + } + + ulong tableBase = (ulong)funcTable.Base; + + // Index into the table. + asm.Mov(rn, tableBase); + + for (int i = 0; i < funcTable.Levels.Length; i++) + { + var level = funcTable.Levels[i]; + asm.Ubfx(indexReg, guestAddress, level.Index, level.Length); + asm.Lsl(indexReg, indexReg, Const(3)); + + // Index into the page. + asm.Add(rn, rn, indexReg); + + // Load the page address. + asm.LdrRiUn(rn, rn, 0); + } + + if (tempGuestAddress != -1) + { + regAlloc.FreeTempGprRegister(tempGuestAddress); + } + } else { asm.Mov(rn, (ulong)funcPtr); @@ -357,7 +414,7 @@ namespace Ryujinx.Cpu.LightningJit.Arm64.Target.Arm64 private static void WriteCall( ref Assembler asm, RegisterAllocator regAlloc, - IntPtr funcPtr, + nint funcPtr, int spillBaseOffset, int? resultRegister, params ulong[] callArgs) @@ -380,8 +437,8 @@ namespace Ryujinx.Cpu.LightningJit.Arm64.Target.Arm64 // We only support up to 7 arguments right now. // ABI defines the first 8 integer arguments to be passed on registers X0-X7. - // We need at least one regiser to put the function address on, so that reduces the amount of - // register we can use for that by one. + // We need at least one register to put the function address on, so that reduces the number of + // registers we can use for that by one. Debug.Assert(callArgs.Length < 8); @@ -601,5 +658,20 @@ namespace Ryujinx.Cpu.LightningJit.Arm64.Target.Arm64 { return new Operand(register, RegisterType.Integer, type); } + + private static Operand Const(long value, OperandType type = OperandType.I64) + { + return new Operand(type, (ulong)value); + } + + private static int NextFreeRegister(int start, int avoid) + { + if (start == avoid) + { + start++; + } + + return start; + } } } diff --git a/src/Ryujinx.Cpu/LightningJit/Cache/CacheEntry.cs b/src/Ryujinx.Cpu/LightningJit/Cache/CacheEntry.cs index b3bb6e31b..0249e24b8 100644 --- a/src/Ryujinx.Cpu/LightningJit/Cache/CacheEntry.cs +++ b/src/Ryujinx.Cpu/LightningJit/Cache/CacheEntry.cs @@ -8,7 +8,6 @@ namespace Ryujinx.Cpu.LightningJit.Cache public int Offset { get; } public int Size { get; } - public CacheEntry(int offset, int size) { Offset = offset; diff --git a/src/Ryujinx.Cpu/LightningJit/Cache/JitCache.cs b/src/Ryujinx.Cpu/LightningJit/Cache/JitCache.cs index 6f1191ca5..ac1274bf6 100644 --- a/src/Ryujinx.Cpu/LightningJit/Cache/JitCache.cs +++ b/src/Ryujinx.Cpu/LightningJit/Cache/JitCache.cs @@ -28,7 +28,7 @@ namespace Ryujinx.Cpu.LightningJit.Cache [SupportedOSPlatform("windows")] [LibraryImport("kernel32.dll", SetLastError = true)] - public static partial IntPtr FlushInstructionCache(IntPtr hProcess, IntPtr lpAddress, UIntPtr dwSize); + public static partial nint FlushInstructionCache(nint hProcess, nint lpAddress, nuint dwSize); public static void Initialize(IJitMemoryAllocator allocator) { @@ -57,7 +57,7 @@ namespace Ryujinx.Cpu.LightningJit.Cache } } - public unsafe static IntPtr Map(ReadOnlySpan code) + public unsafe static nint Map(ReadOnlySpan code) { lock (_lock) { @@ -65,7 +65,7 @@ namespace Ryujinx.Cpu.LightningJit.Cache int funcOffset = Allocate(code.Length); - IntPtr funcPtr = _jitRegion.Pointer + funcOffset; + nint funcPtr = _jitRegion.Pointer + funcOffset; if (OperatingSystem.IsMacOS() && RuntimeInformation.ProcessArchitecture == Architecture.Arm64) { @@ -73,7 +73,7 @@ namespace Ryujinx.Cpu.LightningJit.Cache { fixed (byte* codePtr = code) { - JitSupportDarwin.Copy(funcPtr, (IntPtr)codePtr, (ulong)code.Length); + JitSupportDarwin.Copy(funcPtr, (nint)codePtr, (ulong)code.Length); } } } @@ -85,7 +85,7 @@ namespace Ryujinx.Cpu.LightningJit.Cache if (OperatingSystem.IsWindows() && RuntimeInformation.ProcessArchitecture == Architecture.Arm64) { - FlushInstructionCache(Process.GetCurrentProcess().Handle, funcPtr, (UIntPtr)code.Length); + FlushInstructionCache(Process.GetCurrentProcess().Handle, funcPtr, (nuint)code.Length); } else { @@ -99,7 +99,7 @@ namespace Ryujinx.Cpu.LightningJit.Cache } } - public static void Unmap(IntPtr pointer) + public static void Unmap(nint pointer) { lock (_lock) { diff --git a/src/Ryujinx.Cpu/LightningJit/Cache/JitCacheInvalidation.cs b/src/Ryujinx.Cpu/LightningJit/Cache/JitCacheInvalidation.cs index cd5f3ede4..d0a5e4ac8 100644 --- a/src/Ryujinx.Cpu/LightningJit/Cache/JitCacheInvalidation.cs +++ b/src/Ryujinx.Cpu/LightningJit/Cache/JitCacheInvalidation.cs @@ -68,7 +68,7 @@ namespace Ryujinx.Cpu.LightningJit.Cache } } - public void Invalidate(IntPtr basePointer, ulong size) + public void Invalidate(nint basePointer, ulong size) { if (_needsInvalidation) { diff --git a/src/Ryujinx.Cpu/LightningJit/Cache/JitSupportDarwin.cs b/src/Ryujinx.Cpu/LightningJit/Cache/JitSupportDarwin.cs index 06c81045d..ed02a9c28 100644 --- a/src/Ryujinx.Cpu/LightningJit/Cache/JitSupportDarwin.cs +++ b/src/Ryujinx.Cpu/LightningJit/Cache/JitSupportDarwin.cs @@ -8,9 +8,9 @@ namespace Ryujinx.Cpu.LightningJit.Cache static partial class JitSupportDarwin { [LibraryImport("libarmeilleure-jitsupport", EntryPoint = "armeilleure_jit_memcpy")] - public static partial void Copy(IntPtr dst, IntPtr src, ulong n); + public static partial void Copy(nint dst, nint src, ulong n); [LibraryImport("libc", EntryPoint = "sys_icache_invalidate", SetLastError = true)] - public static partial void SysIcacheInvalidate(IntPtr start, IntPtr len); + public static partial void SysIcacheInvalidate(nint start, nint len); } } diff --git a/src/Ryujinx.Cpu/LightningJit/Cache/NoWxCache.cs b/src/Ryujinx.Cpu/LightningJit/Cache/NoWxCache.cs index a71074995..3cf279ae3 100644 --- a/src/Ryujinx.Cpu/LightningJit/Cache/NoWxCache.cs +++ b/src/Ryujinx.Cpu/LightningJit/Cache/NoWxCache.cs @@ -23,7 +23,7 @@ namespace Ryujinx.Cpu.LightningJit.Cache private readonly CacheMemoryAllocator _cacheAllocator; public CacheMemoryAllocator Allocator => _cacheAllocator; - public IntPtr Pointer => _region.Block.Pointer; + public nint Pointer => _region.Block.Pointer; public MemoryCache(IJitMemoryAllocator allocator, ulong size) { @@ -110,10 +110,10 @@ namespace Ryujinx.Cpu.LightningJit.Cache { public readonly int Offset; public readonly int Size; - public readonly IntPtr FuncPtr; + public readonly nint FuncPtr; private int _useCount; - public ThreadLocalCacheEntry(int offset, int size, IntPtr funcPtr) + public ThreadLocalCacheEntry(int offset, int size, nint funcPtr) { Offset = offset; Size = size; @@ -140,9 +140,9 @@ namespace Ryujinx.Cpu.LightningJit.Cache _lock = new(); } - public unsafe IntPtr Map(IntPtr framePointer, ReadOnlySpan code, ulong guestAddress, ulong guestSize) + public unsafe nint Map(nint framePointer, ReadOnlySpan code, ulong guestAddress, ulong guestSize) { - if (TryGetThreadLocalFunction(guestAddress, out IntPtr funcPtr)) + if (TryGetThreadLocalFunction(guestAddress, out nint funcPtr)) { return funcPtr; } @@ -167,7 +167,7 @@ namespace Ryujinx.Cpu.LightningJit.Cache } } - public unsafe IntPtr MapPageAligned(ReadOnlySpan code) + public unsafe nint MapPageAligned(ReadOnlySpan code) { lock (_lock) { @@ -179,7 +179,7 @@ namespace Ryujinx.Cpu.LightningJit.Cache Debug.Assert((funcOffset & ((int)MemoryBlock.GetPageSize() - 1)) == 0); - IntPtr funcPtr = _sharedCache.Pointer + funcOffset; + nint funcPtr = _sharedCache.Pointer + funcOffset; code.CopyTo(new Span((void*)funcPtr, code.Length)); _sharedCache.ReprotectAsRx(funcOffset, sizeAligned); @@ -188,7 +188,7 @@ namespace Ryujinx.Cpu.LightningJit.Cache } } - private bool TryGetThreadLocalFunction(ulong guestAddress, out IntPtr funcPtr) + private bool TryGetThreadLocalFunction(ulong guestAddress, out nint funcPtr) { if ((_threadLocalCache ??= new()).TryGetValue(guestAddress, out var entry)) { @@ -209,12 +209,12 @@ namespace Ryujinx.Cpu.LightningJit.Cache return true; } - funcPtr = IntPtr.Zero; + funcPtr = nint.Zero; return false; } - private void ClearThreadLocalCache(IntPtr framePointer) + private void ClearThreadLocalCache(nint framePointer) { // Try to delete functions that are already on the shared cache // and no longer being executed. @@ -296,14 +296,14 @@ namespace Ryujinx.Cpu.LightningJit.Cache _threadLocalCache = null; } - private unsafe IntPtr AddThreadLocalFunction(ReadOnlySpan code, ulong guestAddress) + private unsafe nint AddThreadLocalFunction(ReadOnlySpan code, ulong guestAddress) { int alignedSize = BitUtils.AlignUp(code.Length, (int)MemoryBlock.GetPageSize()); int funcOffset = _localCache.Allocate(alignedSize); Debug.Assert((funcOffset & (int)(MemoryBlock.GetPageSize() - 1)) == 0); - IntPtr funcPtr = _localCache.Pointer + funcOffset; + nint funcPtr = _localCache.Pointer + funcOffset; code.CopyTo(new Span((void*)funcPtr, code.Length)); (_threadLocalCache ??= new()).Add(guestAddress, new(funcOffset, code.Length, funcPtr)); diff --git a/src/Ryujinx.Cpu/LightningJit/CodeGen/Arm64/StackWalker.cs b/src/Ryujinx.Cpu/LightningJit/CodeGen/Arm64/StackWalker.cs index 3b01e674b..3ce7c4f9c 100644 --- a/src/Ryujinx.Cpu/LightningJit/CodeGen/Arm64/StackWalker.cs +++ b/src/Ryujinx.Cpu/LightningJit/CodeGen/Arm64/StackWalker.cs @@ -6,13 +6,13 @@ namespace Ryujinx.Cpu.LightningJit.CodeGen.Arm64 { class StackWalker : IStackWalker { - public IEnumerable GetCallStack(IntPtr framePointer, IntPtr codeRegionStart, int codeRegionSize, IntPtr codeRegion2Start, int codeRegion2Size) + public IEnumerable GetCallStack(nint framePointer, nint codeRegionStart, int codeRegionSize, nint codeRegion2Start, int codeRegion2Size) { List functionPointers = new(); while (true) { - IntPtr functionPointer = Marshal.ReadIntPtr(framePointer, IntPtr.Size); + nint functionPointer = Marshal.ReadIntPtr(framePointer, nint.Size); if ((functionPointer < codeRegionStart || functionPointer >= codeRegionStart + codeRegionSize) && (functionPointer < codeRegion2Start || functionPointer >= codeRegion2Start + codeRegion2Size)) diff --git a/src/Ryujinx.Cpu/LightningJit/IStackWalker.cs b/src/Ryujinx.Cpu/LightningJit/IStackWalker.cs index 2fddef659..375c09d26 100644 --- a/src/Ryujinx.Cpu/LightningJit/IStackWalker.cs +++ b/src/Ryujinx.Cpu/LightningJit/IStackWalker.cs @@ -5,6 +5,6 @@ namespace Ryujinx.Cpu.LightningJit { interface IStackWalker { - IEnumerable GetCallStack(IntPtr framePointer, IntPtr codeRegionStart, int codeRegionSize, IntPtr codeRegion2Start, int codeRegion2Size); + IEnumerable GetCallStack(nint framePointer, nint codeRegionStart, int codeRegionSize, nint codeRegion2Start, int codeRegion2Size); } } diff --git a/src/Ryujinx.Cpu/LightningJit/LightningJitCpuContext.cs b/src/Ryujinx.Cpu/LightningJit/LightningJitCpuContext.cs index c53b2d96e..0f47ffb15 100644 --- a/src/Ryujinx.Cpu/LightningJit/LightningJitCpuContext.cs +++ b/src/Ryujinx.Cpu/LightningJit/LightningJitCpuContext.cs @@ -1,3 +1,4 @@ +using ARMeilleure.Common; using ARMeilleure.Memory; using Ryujinx.Cpu.Jit; using Ryujinx.Cpu.LightningJit.State; @@ -8,11 +9,16 @@ namespace Ryujinx.Cpu.LightningJit { private readonly ITickSource _tickSource; private readonly Translator _translator; + private readonly AddressTable _functionTable; public LightningJitCpuContext(ITickSource tickSource, IMemoryManager memory, bool for64Bit) { _tickSource = tickSource; - _translator = new Translator(new JitMemoryAllocator(forJit: true), memory, for64Bit); + + _functionTable = AddressTable.CreateForArm(for64Bit, memory.Type); + + _translator = new Translator(memory, _functionTable); + memory.UnmapEvent += UnmapHandler; } @@ -40,7 +46,7 @@ namespace Ryujinx.Cpu.LightningJit } /// - public IDiskCacheLoadState LoadDiskCache(string titleIdText, string displayVersion, bool enabled) + public IDiskCacheLoadState LoadDiskCache(string titleIdText, string displayVersion, bool enabled, string cacheSelector) { return new DummyDiskCacheLoadState(); } @@ -48,6 +54,7 @@ namespace Ryujinx.Cpu.LightningJit /// public void PrepareCodeRange(ulong address, ulong size) { + _functionTable.SignalCodeRange(address, size); } public void Dispose() diff --git a/src/Ryujinx.Cpu/LightningJit/NativeInterface.cs b/src/Ryujinx.Cpu/LightningJit/NativeInterface.cs index da3ad9832..5f243c0ee 100644 --- a/src/Ryujinx.Cpu/LightningJit/NativeInterface.cs +++ b/src/Ryujinx.Cpu/LightningJit/NativeInterface.cs @@ -61,7 +61,7 @@ namespace Ryujinx.Cpu.LightningJit return GetContext().CntpctEl0; } - public static ulong GetFunctionAddress(IntPtr framePointer, ulong address) + public static ulong GetFunctionAddress(nint framePointer, ulong address) { return (ulong)Context.Translator.GetOrTranslatePointer(framePointer, address, GetContext().ExecutionMode); } diff --git a/src/Ryujinx.Cpu/LightningJit/State/ExecutionContext.cs b/src/Ryujinx.Cpu/LightningJit/State/ExecutionContext.cs index facb9142f..a366dcca6 100644 --- a/src/Ryujinx.Cpu/LightningJit/State/ExecutionContext.cs +++ b/src/Ryujinx.Cpu/LightningJit/State/ExecutionContext.cs @@ -10,7 +10,7 @@ namespace Ryujinx.Cpu.LightningJit.State private readonly NativeContext _nativeContext; - internal IntPtr NativeContextPtr => _nativeContext.BasePtr; + internal nint NativeContextPtr => _nativeContext.BasePtr; private bool _interrupted; private readonly ICounter _counter; diff --git a/src/Ryujinx.Cpu/LightningJit/State/NativeContext.cs b/src/Ryujinx.Cpu/LightningJit/State/NativeContext.cs index fdb8793de..9895c78c2 100644 --- a/src/Ryujinx.Cpu/LightningJit/State/NativeContext.cs +++ b/src/Ryujinx.Cpu/LightningJit/State/NativeContext.cs @@ -25,7 +25,7 @@ namespace Ryujinx.Cpu.LightningJit.State private readonly IJitMemoryBlock _block; - public IntPtr BasePtr => _block.Pointer; + public nint BasePtr => _block.Pointer; public NativeContext(IJitMemoryAllocator allocator) { diff --git a/src/Ryujinx.Cpu/LightningJit/TranslatedFunction.cs b/src/Ryujinx.Cpu/LightningJit/TranslatedFunction.cs index a4e2c7b93..df0f52b8c 100644 --- a/src/Ryujinx.Cpu/LightningJit/TranslatedFunction.cs +++ b/src/Ryujinx.Cpu/LightningJit/TranslatedFunction.cs @@ -4,10 +4,10 @@ namespace Ryujinx.Cpu.LightningJit { class TranslatedFunction { - public IntPtr FuncPointer { get; } + public nint FuncPointer { get; } public ulong GuestSize { get; } - public TranslatedFunction(IntPtr funcPointer, ulong guestSize) + public TranslatedFunction(nint funcPointer, ulong guestSize) { FuncPointer = funcPointer; GuestSize = guestSize; diff --git a/src/Ryujinx.Cpu/LightningJit/Translator.cs b/src/Ryujinx.Cpu/LightningJit/Translator.cs index f59f64fb7..4c4011f11 100644 --- a/src/Ryujinx.Cpu/LightningJit/Translator.cs +++ b/src/Ryujinx.Cpu/LightningJit/Translator.cs @@ -1,10 +1,11 @@ using ARMeilleure.Common; using ARMeilleure.Memory; -using ARMeilleure.Signal; using Ryujinx.Cpu.Jit; using Ryujinx.Cpu.LightningJit.Cache; using Ryujinx.Cpu.LightningJit.CodeGen.Arm64; using Ryujinx.Cpu.LightningJit.State; +using Ryujinx.Cpu.Signal; +using Ryujinx.Memory; using System; using System.Collections.Concurrent; using System.Collections.Generic; @@ -16,26 +17,7 @@ namespace Ryujinx.Cpu.LightningJit class Translator : IDisposable { // Should be enabled on platforms that enforce W^X. - private static bool IsNoWxPlatform => OperatingSystem.IsIOS(); - - private static readonly AddressTable.Level[] _levels64Bit = - new AddressTable.Level[] - { - new(31, 17), - new(23, 8), - new(15, 8), - new( 7, 8), - new( 2, 5), - }; - - private static readonly AddressTable.Level[] _levels32Bit = - new AddressTable.Level[] - { - new(23, 9), - new(15, 8), - new( 7, 8), - new( 1, 6), - }; + private static bool IsNoWxPlatform => false; private readonly ConcurrentQueue> _oldFuncs; private readonly NoWxCache _noWxCache; @@ -46,7 +28,7 @@ namespace Ryujinx.Cpu.LightningJit internal TranslatorStubs Stubs { get; } internal IMemoryManager Memory { get; } - public Translator(IJitMemoryAllocator allocator, IMemoryManager memory, bool for64Bits) + public Translator(IMemoryManager memory, AddressTable functionTable) { Memory = memory; @@ -58,22 +40,18 @@ namespace Ryujinx.Cpu.LightningJit } else { - JitCache.Initialize(allocator); + JitCache.Initialize(new JitMemoryAllocator(forJit: true)); } - NativeSignalHandler.Initialize(allocator); - Functions = new TranslatorCache(); - FunctionTable = new AddressTable(for64Bits ? _levels64Bit : _levels32Bit); + FunctionTable = functionTable; Stubs = new TranslatorStubs(FunctionTable, _noWxCache); FunctionTable.Fill = (ulong)Stubs.SlowDispatchStub; - if (memory.Type == MemoryManagerType.HostTracked || - memory.Type == MemoryManagerType.HostMapped || - memory.Type == MemoryManagerType.HostMappedUnsafe) + if (memory.Type.IsHostMappedOrTracked()) { - NativeSignalHandler.InitializeSignalHandler(allocator.GetPageSize()); + NativeSignalHandler.InitializeSignalHandler(); } } @@ -101,7 +79,7 @@ namespace Ryujinx.Cpu.LightningJit _noWxCache?.ClearEntireThreadLocalCache(); } - internal IntPtr GetOrTranslatePointer(IntPtr framePointer, ulong address, ExecutionMode mode) + internal nint GetOrTranslatePointer(nint framePointer, ulong address, ExecutionMode mode) { if (_noWxCache != null) { @@ -141,15 +119,15 @@ namespace Ryujinx.Cpu.LightningJit } } - internal TranslatedFunction Translate(ulong address, ExecutionMode mode) + private TranslatedFunction Translate(ulong address, ExecutionMode mode) { CompiledFunction func = Compile(address, mode); - IntPtr funcPointer = JitCache.Map(func.Code); + nint funcPointer = JitCache.Map(func.Code); return new TranslatedFunction(funcPointer, (ulong)func.GuestCodeLength); } - internal CompiledFunction Compile(ulong address, ExecutionMode mode) + private CompiledFunction Compile(ulong address, ExecutionMode mode) { return AarchCompiler.Compile(CpuPresets.CortexA57, Memory, address, FunctionTable, Stubs.DispatchStub, mode, RuntimeInformation.ProcessArchitecture); } diff --git a/src/Ryujinx.Cpu/LightningJit/TranslatorStubs.cs b/src/Ryujinx.Cpu/LightningJit/TranslatorStubs.cs index 914712bb1..c5231e506 100644 --- a/src/Ryujinx.Cpu/LightningJit/TranslatorStubs.cs +++ b/src/Ryujinx.Cpu/LightningJit/TranslatorStubs.cs @@ -10,31 +10,31 @@ using System.Runtime.InteropServices; namespace Ryujinx.Cpu.LightningJit { - delegate void DispatcherFunction(IntPtr nativeContext, ulong startAddress); + delegate void DispatcherFunction(nint nativeContext, ulong startAddress); /// /// Represents a stub manager. /// class TranslatorStubs : IDisposable { - private delegate ulong GetFunctionAddressDelegate(IntPtr framePointer, ulong address); + private delegate ulong GetFunctionAddressDelegate(nint framePointer, ulong address); - private readonly Lazy _slowDispatchStub; + private readonly Lazy _slowDispatchStub; private bool _disposed; - private readonly AddressTable _functionTable; + private readonly IAddressTable _functionTable; private readonly NoWxCache _noWxCache; private readonly GetFunctionAddressDelegate _getFunctionAddressRef; - private readonly IntPtr _getFunctionAddress; - private readonly Lazy _dispatchStub; + private readonly nint _getFunctionAddress; + private readonly Lazy _dispatchStub; private readonly Lazy _dispatchLoop; /// /// Gets the dispatch stub. /// /// instance was disposed - public IntPtr DispatchStub + public nint DispatchStub { get { @@ -48,7 +48,7 @@ namespace Ryujinx.Cpu.LightningJit /// Gets the slow dispatch stub. /// /// instance was disposed - public IntPtr SlowDispatchStub + public nint SlowDispatchStub { get { @@ -79,7 +79,7 @@ namespace Ryujinx.Cpu.LightningJit /// Function table used to store pointers to the functions that the guest code will call /// Cache used on platforms that enforce W^X, otherwise should be null /// is null - public TranslatorStubs(AddressTable functionTable, NoWxCache noWxCache) + public TranslatorStubs(IAddressTable functionTable, NoWxCache noWxCache) { ArgumentNullException.ThrowIfNull(functionTable); @@ -138,7 +138,7 @@ namespace Ryujinx.Cpu.LightningJit /// Generates a . /// /// Generated - private IntPtr GenerateDispatchStub() + private nint GenerateDispatchStub() { List branchToFallbackOffsets = new(); @@ -226,7 +226,7 @@ namespace Ryujinx.Cpu.LightningJit /// Generates a . /// /// Generated - private IntPtr GenerateSlowDispatchStub() + private nint GenerateSlowDispatchStub() { CodeWriter writer = new(); @@ -350,12 +350,12 @@ namespace Ryujinx.Cpu.LightningJit throw new PlatformNotSupportedException(); } - IntPtr pointer = Map(writer.AsByteSpan()); + nint pointer = Map(writer.AsByteSpan()); return Marshal.GetDelegateForFunctionPointer(pointer); } - private IntPtr Map(ReadOnlySpan code) + private nint Map(ReadOnlySpan code) { if (_noWxCache != null) { diff --git a/src/Ryujinx.Cpu/ManagedPageFlags.cs b/src/Ryujinx.Cpu/ManagedPageFlags.cs new file mode 100644 index 000000000..a839dae67 --- /dev/null +++ b/src/Ryujinx.Cpu/ManagedPageFlags.cs @@ -0,0 +1,389 @@ +using Ryujinx.Memory; +using Ryujinx.Memory.Tracking; +using System; +using System.Runtime.CompilerServices; +using System.Threading; + +namespace Ryujinx.Cpu +{ + /// + /// A page bitmap that keeps track of mapped state and tracking protection + /// for managed memory accesses (not using host page protection). + /// + internal readonly struct ManagedPageFlags + { + public const int PageBits = 12; + public const int PageSize = 1 << PageBits; + public const int PageMask = PageSize - 1; + + private readonly ulong[] _pageBitmap; + + public const int PageToPteShift = 5; // 32 pages (2 bits each) in one ulong page table entry. + public const ulong BlockMappedMask = 0x5555555555555555; // First bit of each table entry set. + + private enum ManagedPtBits : ulong + { + Unmapped = 0, + Mapped, + WriteTracked, + ReadWriteTracked, + + MappedReplicated = 0x5555555555555555, + WriteTrackedReplicated = 0xaaaaaaaaaaaaaaaa, + ReadWriteTrackedReplicated = ulong.MaxValue, + } + + public ManagedPageFlags(int addressSpaceBits) + { + int bits = Math.Max(0, addressSpaceBits - (PageBits + PageToPteShift)); + _pageBitmap = new ulong[1 << bits]; + } + + /// + /// Computes the number of pages in a virtual address range. + /// + /// Virtual address of the range + /// Size of the range + /// The virtual address of the beginning of the first page + /// This function does not differentiate between allocated and unallocated pages. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int GetPagesCount(ulong va, ulong size, out ulong startVa) + { + // WARNING: Always check if ulong does not overflow during the operations. + startVa = va & ~(ulong)PageMask; + ulong vaSpan = (va - startVa + size + PageMask) & ~(ulong)PageMask; + + return (int)(vaSpan / PageSize); + } + + /// + /// Checks if the page at a given CPU virtual address is mapped. + /// + /// Virtual address to check + /// True if the address is mapped, false otherwise + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly bool IsMapped(ulong va) + { + ulong page = va >> PageBits; + + int bit = (int)((page & 31) << 1); + + int pageIndex = (int)(page >> PageToPteShift); + ref ulong pageRef = ref _pageBitmap[pageIndex]; + + ulong pte = Volatile.Read(ref pageRef); + + return ((pte >> bit) & 3) != 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void GetPageBlockRange(ulong pageStart, ulong pageEnd, out ulong startMask, out ulong endMask, out int pageIndex, out int pageEndIndex) + { + startMask = ulong.MaxValue << ((int)(pageStart & 31) << 1); + endMask = ulong.MaxValue >> (64 - ((int)(pageEnd & 31) << 1)); + + pageIndex = (int)(pageStart >> PageToPteShift); + pageEndIndex = (int)((pageEnd - 1) >> PageToPteShift); + } + + /// + /// Checks if a memory range is mapped. + /// + /// Virtual address of the range + /// Size of the range in bytes + /// True if the entire range is mapped, false otherwise + public readonly bool IsRangeMapped(ulong va, ulong size) + { + int pages = GetPagesCount(va, size, out _); + + if (pages == 1) + { + return IsMapped(va); + } + + ulong pageStart = va >> PageBits; + ulong pageEnd = pageStart + (ulong)pages; + + GetPageBlockRange(pageStart, pageEnd, out ulong startMask, out ulong endMask, out int pageIndex, out int pageEndIndex); + + // Check if either bit in each 2 bit page entry is set. + // OR the block with itself shifted down by 1, and check the first bit of each entry. + + ulong mask = BlockMappedMask & startMask; + + while (pageIndex <= pageEndIndex) + { + if (pageIndex == pageEndIndex) + { + mask &= endMask; + } + + ref ulong pageRef = ref _pageBitmap[pageIndex++]; + ulong pte = Volatile.Read(ref pageRef); + + pte |= pte >> 1; + if ((pte & mask) != mask) + { + return false; + } + + mask = BlockMappedMask; + } + + return true; + } + + /// + /// Reprotect a region of virtual memory for tracking. + /// + /// Virtual address base + /// Size of the region to protect + /// Memory protection to set + public readonly void TrackingReprotect(ulong va, ulong size, MemoryPermission protection) + { + // Protection is inverted on software pages, since the default value is 0. + protection = (~protection) & MemoryPermission.ReadAndWrite; + + int pages = GetPagesCount(va, size, out va); + ulong pageStart = va >> PageBits; + + if (pages == 1) + { + ulong protTag = protection switch + { + MemoryPermission.None => (ulong)ManagedPtBits.Mapped, + MemoryPermission.Write => (ulong)ManagedPtBits.WriteTracked, + _ => (ulong)ManagedPtBits.ReadWriteTracked, + }; + + int bit = (int)((pageStart & 31) << 1); + + ulong tagMask = 3UL << bit; + ulong invTagMask = ~tagMask; + + ulong tag = protTag << bit; + + int pageIndex = (int)(pageStart >> PageToPteShift); + ref ulong pageRef = ref _pageBitmap[pageIndex]; + + ulong pte; + + do + { + pte = Volatile.Read(ref pageRef); + } + while ((pte & tagMask) != 0 && Interlocked.CompareExchange(ref pageRef, (pte & invTagMask) | tag, pte) != pte); + } + else + { + ulong pageEnd = pageStart + (ulong)pages; + + GetPageBlockRange(pageStart, pageEnd, out ulong startMask, out ulong endMask, out int pageIndex, out int pageEndIndex); + + ulong mask = startMask; + + ulong protTag = protection switch + { + MemoryPermission.None => (ulong)ManagedPtBits.MappedReplicated, + MemoryPermission.Write => (ulong)ManagedPtBits.WriteTrackedReplicated, + _ => (ulong)ManagedPtBits.ReadWriteTrackedReplicated, + }; + + while (pageIndex <= pageEndIndex) + { + if (pageIndex == pageEndIndex) + { + mask &= endMask; + } + + ref ulong pageRef = ref _pageBitmap[pageIndex++]; + + ulong pte; + ulong mappedMask; + + // Change the protection of all 2 bit entries that are mapped. + do + { + pte = Volatile.Read(ref pageRef); + + mappedMask = pte | (pte >> 1); + mappedMask |= (mappedMask & BlockMappedMask) << 1; + mappedMask &= mask; // Only update mapped pages within the given range. + } + while (Interlocked.CompareExchange(ref pageRef, (pte & (~mappedMask)) | (protTag & mappedMask), pte) != pte); + + mask = ulong.MaxValue; + } + } + } + + /// + /// Alerts the memory tracking that a given region has been read from or written to. + /// This should be called before read/write is performed. + /// + /// Memory tracking structure to call when pages are protected + /// Virtual address of the region + /// Size of the region + /// True if the region was written, false if read + /// Optional ID of the handles that should not be signalled + /// + /// This function also validates that the given range is both valid and mapped, and will throw if it is not. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly void SignalMemoryTracking(MemoryTracking tracking, ulong va, ulong size, bool write, int? exemptId = null) + { + // Software table, used for managed memory tracking. + + int pages = GetPagesCount(va, size, out _); + ulong pageStart = va >> PageBits; + + if (pages == 1) + { + ulong tag = (ulong)(write ? ManagedPtBits.WriteTracked : ManagedPtBits.ReadWriteTracked); + + int bit = (int)((pageStart & 31) << 1); + + int pageIndex = (int)(pageStart >> PageToPteShift); + ref ulong pageRef = ref _pageBitmap[pageIndex]; + + ulong pte = Volatile.Read(ref pageRef); + ulong state = ((pte >> bit) & 3); + + if (state >= tag) + { + tracking.VirtualMemoryEvent(va, size, write, precise: false, exemptId); + return; + } + else if (state == 0) + { + ThrowInvalidMemoryRegionException($"Not mapped: va=0x{va:X16}, size=0x{size:X16}"); + } + } + else + { + ulong pageEnd = pageStart + (ulong)pages; + + GetPageBlockRange(pageStart, pageEnd, out ulong startMask, out ulong endMask, out int pageIndex, out int pageEndIndex); + + ulong mask = startMask; + + ulong anyTrackingTag = (ulong)ManagedPtBits.WriteTrackedReplicated; + + while (pageIndex <= pageEndIndex) + { + if (pageIndex == pageEndIndex) + { + mask &= endMask; + } + + ref ulong pageRef = ref _pageBitmap[pageIndex++]; + + ulong pte = Volatile.Read(ref pageRef); + ulong mappedMask = mask & BlockMappedMask; + + ulong mappedPte = pte | (pte >> 1); + if ((mappedPte & mappedMask) != mappedMask) + { + ThrowInvalidMemoryRegionException($"Not mapped: va=0x{va:X16}, size=0x{size:X16}"); + } + + pte &= mask; + if ((pte & anyTrackingTag) != 0) // Search for any tracking. + { + // Writes trigger any tracking. + // Only trigger tracking from reads if both bits are set on any page. + if (write || (pte & (pte >> 1) & BlockMappedMask) != 0) + { + tracking.VirtualMemoryEvent(va, size, write, precise: false, exemptId); + break; + } + } + + mask = ulong.MaxValue; + } + } + } + + /// + /// Adds the given address mapping to the page table. + /// + /// Virtual memory address + /// Size to be mapped + public readonly void AddMapping(ulong va, ulong size) + { + int pages = GetPagesCount(va, size, out _); + ulong pageStart = va >> PageBits; + ulong pageEnd = pageStart + (ulong)pages; + + GetPageBlockRange(pageStart, pageEnd, out ulong startMask, out ulong endMask, out int pageIndex, out int pageEndIndex); + + ulong mask = startMask; + + while (pageIndex <= pageEndIndex) + { + if (pageIndex == pageEndIndex) + { + mask &= endMask; + } + + ref ulong pageRef = ref _pageBitmap[pageIndex++]; + + ulong pte; + ulong mappedMask; + + // Map all 2-bit entries that are unmapped. + do + { + pte = Volatile.Read(ref pageRef); + + mappedMask = pte | (pte >> 1); + mappedMask |= (mappedMask & BlockMappedMask) << 1; + mappedMask |= ~mask; // Treat everything outside the range as mapped, thus unchanged. + } + while (Interlocked.CompareExchange(ref pageRef, (pte & mappedMask) | (BlockMappedMask & (~mappedMask)), pte) != pte); + + mask = ulong.MaxValue; + } + } + + /// + /// Removes the given address mapping from the page table. + /// + /// Virtual memory address + /// Size to be unmapped + public readonly void RemoveMapping(ulong va, ulong size) + { + int pages = GetPagesCount(va, size, out _); + ulong pageStart = va >> PageBits; + ulong pageEnd = pageStart + (ulong)pages; + + GetPageBlockRange(pageStart, pageEnd, out ulong startMask, out ulong endMask, out int pageIndex, out int pageEndIndex); + + startMask = ~startMask; + endMask = ~endMask; + + ulong mask = startMask; + + while (pageIndex <= pageEndIndex) + { + if (pageIndex == pageEndIndex) + { + mask |= endMask; + } + + ref ulong pageRef = ref _pageBitmap[pageIndex++]; + ulong pte; + + do + { + pte = Volatile.Read(ref pageRef); + } + while (Interlocked.CompareExchange(ref pageRef, pte & mask, pte) != pte); + + mask = 0; + } + } + + private static void ThrowInvalidMemoryRegionException(string message) => throw new InvalidMemoryRegionException(message); + } +} diff --git a/src/Ryujinx.Cpu/MemoryEhMeilleure.cs b/src/Ryujinx.Cpu/MemoryEhMeilleure.cs index d3763c777..e9a3ac4aa 100644 --- a/src/Ryujinx.Cpu/MemoryEhMeilleure.cs +++ b/src/Ryujinx.Cpu/MemoryEhMeilleure.cs @@ -1,4 +1,5 @@ -using ARMeilleure.Signal; +using Ryujinx.Common; +using Ryujinx.Cpu.Signal; using Ryujinx.Memory; using Ryujinx.Memory.Tracking; using System; @@ -8,10 +9,13 @@ namespace Ryujinx.Cpu { public class MemoryEhMeilleure : IDisposable { - public delegate bool TrackingEventDelegate(ulong address, ulong size, bool write); + public delegate ulong TrackingEventDelegate(ulong address, ulong size, bool write); + private readonly MemoryTracking _tracking; private readonly TrackingEventDelegate _trackingEvent; + private readonly ulong _pageSize; + private readonly ulong _baseAddress; private readonly ulong _mirrorAddress; @@ -21,7 +25,10 @@ namespace Ryujinx.Cpu ulong endAddress = _baseAddress + addressSpace.Size; - _trackingEvent = trackingEvent ?? tracking.VirtualMemoryEvent; + _tracking = tracking; + _trackingEvent = trackingEvent ?? VirtualMemoryEvent; + + _pageSize = MemoryBlock.GetPageSize(); bool added = NativeSignalHandler.AddTrackedRegion((nuint)_baseAddress, (nuint)endAddress, Marshal.GetFunctionPointerForDelegate(_trackingEvent)); @@ -39,7 +46,7 @@ namespace Ryujinx.Cpu _mirrorAddress = (ulong)addressSpaceMirror.Pointer; ulong endAddressMirror = _mirrorAddress + addressSpace.Size; - bool addedMirror = NativeSignalHandler.AddTrackedRegion((nuint)_mirrorAddress, (nuint)endAddressMirror, IntPtr.Zero); + bool addedMirror = NativeSignalHandler.AddTrackedRegion((nuint)_mirrorAddress, (nuint)endAddressMirror, nint.Zero); if (!addedMirror) { @@ -48,6 +55,21 @@ namespace Ryujinx.Cpu } } + private ulong VirtualMemoryEvent(ulong address, ulong size, bool write) + { + ulong pageSize = _pageSize; + ulong addressAligned = BitUtils.AlignDown(address, pageSize); + ulong endAddressAligned = BitUtils.AlignUp(address + size, pageSize); + ulong sizeAligned = endAddressAligned - addressAligned; + + if (_tracking.VirtualMemoryEvent(addressAligned, sizeAligned, write)) + { + return _baseAddress + address; + } + + return 0; + } + public void Dispose() { GC.SuppressFinalize(this); diff --git a/src/Ryujinx.Cpu/Signal/NativeSignalHandler.cs b/src/Ryujinx.Cpu/Signal/NativeSignalHandler.cs new file mode 100644 index 000000000..2985f1c21 --- /dev/null +++ b/src/Ryujinx.Cpu/Signal/NativeSignalHandler.cs @@ -0,0 +1,184 @@ +using ARMeilleure.Signal; +using Ryujinx.Common; +using Ryujinx.Memory; +using System; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace Ryujinx.Cpu.Signal +{ + [StructLayout(LayoutKind.Sequential, Pack = 1)] + struct SignalHandlerRange + { + public int IsActive; + public nuint RangeAddress; + public nuint RangeEndAddress; + public nint ActionPointer; + } + + [InlineArray(NativeSignalHandlerGenerator.MaxTrackedRanges)] + struct SignalHandlerRangeArray + { + public SignalHandlerRange Range0; + } + + [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; + + /// + /// Fixed size array of tracked ranges. + /// + public SignalHandlerRangeArray Ranges; + } + + static class NativeSignalHandler + { + private static readonly nint _handlerConfig; + private static nint _signalHandlerPtr; + + private static MemoryBlock _codeBlock; + + 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 InitializeSignalHandler(Func customSignalHandlerFactory = null) + { + if (_initialized) + { + return; + } + + lock (_lock) + { + if (_initialized) + { + return; + } + + int rangeStructSize = Unsafe.SizeOf(); + + ref SignalHandlerConfig config = ref GetConfigRef(); + + if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS()) + { + _signalHandlerPtr = MapCode(NativeSignalHandlerGenerator.GenerateUnixSignalHandler(_handlerConfig, rangeStructSize)); + + 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 = MapCode(NativeSignalHandlerGenerator.GenerateWindowsSignalHandler(_handlerConfig, rangeStructSize)); + + if (customSignalHandlerFactory != null) + { + _signalHandlerPtr = customSignalHandlerFactory(nint.Zero, _signalHandlerPtr); + } + + WindowsSignalHandlerRegistration.RegisterExceptionHandler(_signalHandlerPtr); + } + + _initialized = true; + } + } + + private static nint MapCode(ReadOnlySpan code) + { + Debug.Assert(_codeBlock == null); + + ulong codeSizeAligned = BitUtils.AlignUp((ulong)code.Length, MemoryBlock.GetPageSize()); + + _codeBlock = new MemoryBlock(codeSizeAligned); + _codeBlock.Write(0, code); + _codeBlock.Reprotect(0, codeSizeAligned, MemoryPermission.ReadAndExecute); + + return _codeBlock.Pointer; + } + + private static unsafe ref SignalHandlerConfig GetConfigRef() + { + return ref Unsafe.AsRef((void*)_handlerConfig); + } + + public static bool AddTrackedRegion(nuint address, nuint endAddress, nint action) + { + Span ranges = GetConfigRef().Ranges; + + for (int i = 0; i < NativeSignalHandlerGenerator.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 bool RemoveTrackedRegion(nuint address) + { + Span ranges = GetConfigRef().Ranges; + + for (int i = 0; i < NativeSignalHandlerGenerator.MaxTrackedRanges; i++) + { + if (ranges[i].IsActive == 1 && ranges[i].RangeAddress == address) + { + ranges[i].IsActive = 0; + + return true; + } + } + + return false; + } + + public static bool SupportsFaultAddressPatching() + { + return NativeSignalHandlerGenerator.SupportsFaultAddressPatchingForHost(); + } + } +} diff --git a/src/ARMeilleure/Signal/UnixSignalHandlerRegistration.cs b/src/Ryujinx.Cpu/Signal/UnixSignalHandlerRegistration.cs similarity index 78% rename from src/ARMeilleure/Signal/UnixSignalHandlerRegistration.cs rename to src/Ryujinx.Cpu/Signal/UnixSignalHandlerRegistration.cs index 433aab636..d40e7cdc9 100644 --- a/src/ARMeilleure/Signal/UnixSignalHandlerRegistration.cs +++ b/src/Ryujinx.Cpu/Signal/UnixSignalHandlerRegistration.cs @@ -1,7 +1,7 @@ using System; using System.Runtime.InteropServices; -namespace ARMeilleure.Signal +namespace Ryujinx.Cpu.Signal { static partial class UnixSignalHandlerRegistration { @@ -14,10 +14,10 @@ namespace ARMeilleure.Signal [StructLayout(LayoutKind.Sequential, Pack = 1)] public struct SigAction { - public IntPtr sa_handler; + public nint sa_handler; public SigSet sa_mask; public int sa_flags; - public IntPtr sa_restorer; + public nint sa_restorer; } private const int SIGSEGV = 11; @@ -28,14 +28,14 @@ namespace ARMeilleure.Signal private static partial int sigaction(int signum, ref SigAction sigAction, out SigAction oldAction); [LibraryImport("libc", SetLastError = true)] - private static partial int sigaction(int signum, IntPtr sigAction, out SigAction oldAction); + private static partial int sigaction(int signum, nint sigAction, out SigAction oldAction); [LibraryImport("libc", SetLastError = true)] private static partial int sigemptyset(ref SigSet set); public static SigAction GetSegfaultExceptionHandler() { - int result = sigaction(SIGSEGV, IntPtr.Zero, out SigAction old); + int result = sigaction(SIGSEGV, nint.Zero, out SigAction old); if (result != 0) { @@ -45,7 +45,7 @@ namespace ARMeilleure.Signal return old; } - public static SigAction RegisterExceptionHandler(IntPtr action) + public static SigAction RegisterExceptionHandler(nint action) { SigAction sig = new() { @@ -62,7 +62,7 @@ namespace ARMeilleure.Signal throw new InvalidOperationException($"Could not register SIGSEGV sigaction. Error: {result}"); } - if (OperatingSystem.IsMacOS() || OperatingSystem.IsIOS()) + if (OperatingSystem.IsMacOS()) { result = sigaction(SIGBUS, ref sig, out _); @@ -77,7 +77,7 @@ namespace ARMeilleure.Signal public static bool RestoreExceptionHandler(SigAction oldAction) { - return sigaction(SIGSEGV, ref oldAction, out SigAction _) == 0 && (!OperatingSystem.IsMacOS() || OperatingSystem.IsIOS() || sigaction(SIGBUS, ref oldAction, out SigAction _) == 0); + return sigaction(SIGSEGV, ref oldAction, out SigAction _) == 0 && (!OperatingSystem.IsMacOS() || sigaction(SIGBUS, ref oldAction, out SigAction _) == 0); } } } diff --git a/src/Ryujinx.Cpu/Signal/WindowsSignalHandlerRegistration.cs b/src/Ryujinx.Cpu/Signal/WindowsSignalHandlerRegistration.cs new file mode 100644 index 000000000..7ac15b816 --- /dev/null +++ b/src/Ryujinx.Cpu/Signal/WindowsSignalHandlerRegistration.cs @@ -0,0 +1,24 @@ +using System; +using System.Runtime.InteropServices; + +namespace Ryujinx.Cpu.Signal +{ + static partial class WindowsSignalHandlerRegistration + { + [LibraryImport("kernel32.dll")] + private static partial nint AddVectoredExceptionHandler(uint first, nint handler); + + [LibraryImport("kernel32.dll")] + private static partial ulong RemoveVectoredExceptionHandler(nint handle); + + public static nint RegisterExceptionHandler(nint action) + { + return AddVectoredExceptionHandler(1, action); + } + + public static bool RemoveExceptionHandler(nint handle) + { + return RemoveVectoredExceptionHandler(handle) != 0; + } + } +} diff --git a/src/Ryujinx.Cpu/MemoryManagerBase.cs b/src/Ryujinx.Cpu/VirtualMemoryManagerRefCountedBase.cs similarity index 86% rename from src/Ryujinx.Cpu/MemoryManagerBase.cs rename to src/Ryujinx.Cpu/VirtualMemoryManagerRefCountedBase.cs index 3288e3a49..3c7b33805 100644 --- a/src/Ryujinx.Cpu/MemoryManagerBase.cs +++ b/src/Ryujinx.Cpu/VirtualMemoryManagerRefCountedBase.cs @@ -4,7 +4,7 @@ using System.Threading; namespace Ryujinx.Cpu { - public abstract class MemoryManagerBase : IRefCounted + public abstract class VirtualMemoryManagerRefCountedBase : VirtualMemoryManagerBase, IRefCounted { private int _referenceCount; diff --git a/src/Ryujinx.Graphics.Device/DeviceMemoryManager.cs b/src/Ryujinx.Graphics.Device/DeviceMemoryManager.cs new file mode 100644 index 000000000..cb1a7c3ab --- /dev/null +++ b/src/Ryujinx.Graphics.Device/DeviceMemoryManager.cs @@ -0,0 +1,396 @@ +using Ryujinx.Common.Memory; +using Ryujinx.Memory; +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace Ryujinx.Graphics.Device +{ + /// + /// Device memory manager. + /// + public class DeviceMemoryManager : IWritableBlock + { + private const int PtLvl0Bits = 10; + private const int PtLvl1Bits = 10; + public const int PtPageBits = 12; + + private const ulong PtLvl0Size = 1UL << PtLvl0Bits; + private const ulong PtLvl1Size = 1UL << PtLvl1Bits; + public const ulong PageSize = 1UL << PtPageBits; + + private const ulong PtLvl0Mask = PtLvl0Size - 1; + private const ulong PtLvl1Mask = PtLvl1Size - 1; + public const ulong PageMask = PageSize - 1; + + private const int PtLvl0Bit = PtPageBits + PtLvl1Bits; + private const int PtLvl1Bit = PtPageBits; + private const int AddressSpaceBits = PtPageBits + PtLvl1Bits + PtLvl0Bits; + + public const ulong PteUnmapped = ulong.MaxValue; + + private readonly ulong[][] _pageTable; + + private readonly IVirtualMemoryManager _physical; + + /// + /// Creates a new instance of the GPU memory manager. + /// + /// Physical memory that this memory manager will map into + public DeviceMemoryManager(IVirtualMemoryManager physicalMemory) + { + _physical = physicalMemory; + _pageTable = new ulong[PtLvl0Size][]; + } + + /// + /// Reads data from GPU mapped memory. + /// + /// Type of the data + /// GPU virtual address where the data is located + /// The data at the specified memory location + public T Read(ulong va) where T : unmanaged + { + int size = Unsafe.SizeOf(); + + if (IsContiguous(va, size)) + { + return _physical.Read(Translate(va)); + } + else + { + Span data = new byte[size]; + + ReadImpl(va, data); + + return MemoryMarshal.Cast(data)[0]; + } + } + + /// + /// Gets a read-only span of data from GPU mapped memory. + /// + /// GPU virtual address where the data is located + /// Size of the data + /// The span of the data at the specified memory location + public ReadOnlySpan GetSpan(ulong va, int size) + { + if (IsContiguous(va, size)) + { + return _physical.GetSpan(Translate(va), size); + } + else + { + Span data = new byte[size]; + + ReadImpl(va, data); + + return data; + } + } + + /// + /// Reads data from a possibly non-contiguous region of GPU mapped memory. + /// + /// GPU virtual address of the data + /// Span to write the read data into + private void ReadImpl(ulong va, Span data) + { + if (data.Length == 0) + { + return; + } + + int offset = 0, size; + + if ((va & PageMask) != 0) + { + ulong pa = Translate(va); + + size = Math.Min(data.Length, (int)PageSize - (int)(va & PageMask)); + + if (pa != PteUnmapped && _physical.IsMapped(pa)) + { + _physical.GetSpan(pa, size).CopyTo(data[..size]); + } + + offset += size; + } + + for (; offset < data.Length; offset += size) + { + ulong pa = Translate(va + (ulong)offset); + + size = Math.Min(data.Length - offset, (int)PageSize); + + if (pa != PteUnmapped && _physical.IsMapped(pa)) + { + _physical.GetSpan(pa, size).CopyTo(data.Slice(offset, size)); + } + } + } + + /// + /// Gets a writable region from GPU mapped memory. + /// + /// Start address of the range + /// Size in bytes to be range + /// A writable region with the data at the specified memory location + public WritableRegion GetWritableRegion(ulong va, int size) + { + if (IsContiguous(va, size)) + { + return _physical.GetWritableRegion(Translate(va), size, tracked: true); + } + else + { + MemoryOwner memoryOwner = MemoryOwner.Rent(size); + + ReadImpl(va, memoryOwner.Span); + + return new WritableRegion(this, va, memoryOwner, tracked: true); + } + } + + /// + /// Writes data to GPU mapped memory. + /// + /// Type of the data + /// GPU virtual address to write the value into + /// The value to be written + public void Write(ulong va, T value) where T : unmanaged + { + Write(va, MemoryMarshal.Cast(MemoryMarshal.CreateSpan(ref value, 1))); + } + + /// + /// Writes data to GPU mapped memory. + /// + /// GPU virtual address to write the data into + /// The data to be written + public void Write(ulong va, ReadOnlySpan data) + { + if (IsContiguous(va, data.Length)) + { + _physical.Write(Translate(va), data); + } + else + { + int offset = 0, size; + + if ((va & PageMask) != 0) + { + ulong pa = Translate(va); + + size = Math.Min(data.Length, (int)PageSize - (int)(va & PageMask)); + + if (pa != PteUnmapped && _physical.IsMapped(pa)) + { + _physical.Write(pa, data[..size]); + } + + offset += size; + } + + for (; offset < data.Length; offset += size) + { + ulong pa = Translate(va + (ulong)offset); + + size = Math.Min(data.Length - offset, (int)PageSize); + + if (pa != PteUnmapped && _physical.IsMapped(pa)) + { + _physical.Write(pa, data.Slice(offset, size)); + } + } + } + } + + /// + /// Writes data to GPU mapped memory without write tracking. + /// + /// GPU virtual address to write the data into + /// The data to be written + public void WriteUntracked(ulong va, ReadOnlySpan data) + { + throw new NotSupportedException(); + } + + /// + /// Maps a given range of pages to the specified CPU virtual address. + /// + /// + /// All addresses and sizes must be page aligned. + /// + /// CPU virtual address to map into + /// GPU virtual address to be mapped + /// Kind of the resource located at the mapping + public void Map(ulong pa, ulong va, ulong size) + { + lock (_pageTable) + { + for (ulong offset = 0; offset < size; offset += PageSize) + { + SetPte(va + offset, PackPte(pa + offset)); + } + } + } + + /// + /// Unmaps a given range of pages at the specified GPU virtual memory region. + /// + /// GPU virtual address to unmap + /// Size in bytes of the region being unmapped + public void Unmap(ulong va, ulong size) + { + lock (_pageTable) + { + for (ulong offset = 0; offset < size; offset += PageSize) + { + SetPte(va + offset, PteUnmapped); + } + } + } + + /// + /// Checks if a region of GPU mapped memory is contiguous. + /// + /// GPU virtual address of the region + /// Size of the region + /// True if the region is contiguous, false otherwise + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool IsContiguous(ulong va, int size) + { + if (!ValidateAddress(va) || GetPte(va) == PteUnmapped) + { + return false; + } + + ulong endVa = (va + (ulong)size + PageMask) & ~PageMask; + + va &= ~PageMask; + + int pages = (int)((endVa - va) / PageSize); + + for (int page = 0; page < pages - 1; page++) + { + if (!ValidateAddress(va + PageSize) || GetPte(va + PageSize) == PteUnmapped) + { + return false; + } + + if (Translate(va) + PageSize != Translate(va + PageSize)) + { + return false; + } + + va += PageSize; + } + + return true; + } + + /// + /// Validates a GPU virtual address. + /// + /// Address to validate + /// True if the address is valid, false otherwise + private static bool ValidateAddress(ulong va) + { + return va < (1UL << AddressSpaceBits); + } + + /// + /// Checks if a given page is mapped. + /// + /// GPU virtual address of the page to check + /// True if the page is mapped, false otherwise + public bool IsMapped(ulong va) + { + return Translate(va) != PteUnmapped; + } + + /// + /// Translates a GPU virtual address to a CPU virtual address. + /// + /// GPU virtual address to be translated + /// CPU virtual address, or if unmapped + public ulong Translate(ulong va) + { + if (!ValidateAddress(va)) + { + return PteUnmapped; + } + + ulong pte = GetPte(va); + + if (pte == PteUnmapped) + { + return PteUnmapped; + } + + return UnpackPaFromPte(pte) + (va & PageMask); + } + + /// + /// Gets the Page Table entry for a given GPU virtual address. + /// + /// GPU virtual address + /// Page table entry (CPU virtual address) + private ulong GetPte(ulong va) + { + ulong l0 = (va >> PtLvl0Bit) & PtLvl0Mask; + ulong l1 = (va >> PtLvl1Bit) & PtLvl1Mask; + + if (_pageTable[l0] == null) + { + return PteUnmapped; + } + + return _pageTable[l0][l1]; + } + + /// + /// Sets a Page Table entry at a given GPU virtual address. + /// + /// GPU virtual address + /// Page table entry (CPU virtual address) + private void SetPte(ulong va, ulong pte) + { + ulong l0 = (va >> PtLvl0Bit) & PtLvl0Mask; + ulong l1 = (va >> PtLvl1Bit) & PtLvl1Mask; + + if (_pageTable[l0] == null) + { + _pageTable[l0] = new ulong[PtLvl1Size]; + + for (ulong index = 0; index < PtLvl1Size; index++) + { + _pageTable[l0][index] = PteUnmapped; + } + } + + _pageTable[l0][l1] = pte; + } + + /// + /// Creates a page table entry from a physical address and kind. + /// + /// Physical address + /// Page table entry + private static ulong PackPte(ulong pa) + { + return pa; + } + + /// + /// Unpacks physical address from a page table entry. + /// + /// Page table entry + /// Physical address + private static ulong UnpackPaFromPte(ulong pte) + { + return pte; + } + } +} diff --git a/src/Ryujinx.Graphics.Device/DeviceState.cs b/src/Ryujinx.Graphics.Device/DeviceState.cs index f5b30836b..0dd4f5904 100644 --- a/src/Ryujinx.Graphics.Device/DeviceState.cs +++ b/src/Ryujinx.Graphics.Device/DeviceState.cs @@ -33,22 +33,16 @@ namespace Ryujinx.Graphics.Device } var fields = typeof(TState).GetFields(); - var t = typeof(TState); int offset = 0; for (int fieldIndex = 0; fieldIndex < fields.Length; fieldIndex++) { var field = fields[fieldIndex]; - var cuurentFieldOffset = (int)Marshal.OffsetOf(field.Name); + var currentFieldOffset = (int)Marshal.OffsetOf(field.Name); var nextFieldOffset = fieldIndex + 1 == fields.Length ? Unsafe.SizeOf() : (int)Marshal.OffsetOf(fields[fieldIndex + 1].Name); - int sizeOfField = nextFieldOffset - cuurentFieldOffset; - - if(sizeOfField == 0) - { - - } + int sizeOfField = nextFieldOffset - currentFieldOffset; for (int i = 0; i < ((sizeOfField + 3) & ~3); i += 4) { @@ -87,7 +81,7 @@ namespace Ryujinx.Graphics.Device { uint alignedOffset = index * RegisterSize; - var readCallback = Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(_readCallbacks), (IntPtr)index); + var readCallback = Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(_readCallbacks), (nint)index); if (readCallback != null) { return readCallback(); @@ -112,7 +106,7 @@ namespace Ryujinx.Graphics.Device GetRefIntAlignedUncheck(index) = data; - Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(_writeCallbacks), (IntPtr)index)?.Invoke(data); + Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(_writeCallbacks), (nint)index)?.Invoke(data); } } @@ -129,7 +123,7 @@ namespace Ryujinx.Graphics.Device changed = storage != data; storage = data; - Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(_writeCallbacks), (IntPtr)index)?.Invoke(data); + Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(_writeCallbacks), (nint)index)?.Invoke(data); } else { @@ -159,13 +153,13 @@ namespace Ryujinx.Graphics.Device [MethodImpl(MethodImplOptions.AggressiveInlining)] private ref T GetRefUnchecked(uint offset) where T : unmanaged { - return ref Unsafe.As(ref Unsafe.AddByteOffset(ref State, (IntPtr)offset)); + return ref Unsafe.As(ref Unsafe.AddByteOffset(ref State, (nint)offset)); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private ref int GetRefIntAlignedUncheck(ulong index) { - return ref Unsafe.Add(ref Unsafe.As(ref State), (IntPtr)index); + return ref Unsafe.Add(ref Unsafe.As(ref State), (nint)index); } } } diff --git a/src/Ryujinx.Graphics.Device/ISynchronizationManager.cs b/src/Ryujinx.Graphics.Device/ISynchronizationManager.cs new file mode 100644 index 000000000..2a8d1d9b7 --- /dev/null +++ b/src/Ryujinx.Graphics.Device/ISynchronizationManager.cs @@ -0,0 +1,39 @@ +using Ryujinx.Common.Logging; +using System; +using System.Threading; + +namespace Ryujinx.Graphics.Device +{ + /// + /// Synchronization manager interface. + /// + public interface ISynchronizationManager + { + /// + /// Increment the value of a syncpoint with a given id. + /// + /// The id of the syncpoint + /// Thrown when id >= MaxHardwareSyncpoints + /// The incremented value of the syncpoint + uint IncrementSyncpoint(uint id); + + /// + /// Get the value of a syncpoint with a given id. + /// + /// The id of the syncpoint + /// Thrown when id >= MaxHardwareSyncpoints + /// The value of the syncpoint + uint GetSyncpointValue(uint id); + + /// + /// Wait on a syncpoint with a given id at a target threshold. + /// The callback will be called once the threshold is reached and will automatically be unregistered. + /// + /// The id of the syncpoint + /// The target threshold + /// The timeout + /// Thrown when id >= MaxHardwareSyncpoints + /// True if timed out + bool WaitOnSyncpoint(uint id, uint threshold, TimeSpan timeout); + } +} diff --git a/src/Ryujinx.Graphics.Device/Ryujinx.Graphics.Device.csproj b/src/Ryujinx.Graphics.Device/Ryujinx.Graphics.Device.csproj index ae2821edb..973a9e260 100644 --- a/src/Ryujinx.Graphics.Device/Ryujinx.Graphics.Device.csproj +++ b/src/Ryujinx.Graphics.Device/Ryujinx.Graphics.Device.csproj @@ -4,4 +4,8 @@ net8.0 + + + + diff --git a/src/Ryujinx.Graphics.GAL/BufferAccess.cs b/src/Ryujinx.Graphics.GAL/BufferAccess.cs index faefa5188..1e7736f8f 100644 --- a/src/Ryujinx.Graphics.GAL/BufferAccess.cs +++ b/src/Ryujinx.Graphics.GAL/BufferAccess.cs @@ -6,8 +6,13 @@ namespace Ryujinx.Graphics.GAL public enum BufferAccess { Default = 0, - FlushPersistent = 1 << 0, - Stream = 1 << 1, - SparseCompatible = 1 << 2, + HostMemory = 1, + DeviceMemory = 2, + DeviceMemoryMapped = 3, + + MemoryTypeMask = 0xf, + + Stream = 1 << 4, + SparseCompatible = 1 << 5, } } diff --git a/src/Ryujinx.Graphics.GAL/Capabilities.cs b/src/Ryujinx.Graphics.GAL/Capabilities.cs index dc927eaba..1eec80e51 100644 --- a/src/Ryujinx.Graphics.GAL/Capabilities.cs +++ b/src/Ryujinx.Graphics.GAL/Capabilities.cs @@ -6,6 +6,7 @@ namespace Ryujinx.Graphics.GAL { public readonly TargetApi Api; public readonly string VendorName; + public readonly SystemMemoryType MemoryType; public readonly bool HasFrontFacingBug; public readonly bool HasVectorIndexingBug; @@ -36,6 +37,8 @@ namespace Ryujinx.Graphics.GAL public readonly bool SupportsMismatchingViewFormat; public readonly bool SupportsCubemapView; public readonly bool SupportsNonConstantTextureOffset; + public readonly bool SupportsQuads; + public readonly bool SupportsSeparateSampler; public readonly bool SupportsShaderBallot; public readonly bool SupportsShaderBarrierDivergence; public readonly bool SupportsShaderFloat64; @@ -48,6 +51,13 @@ namespace Ryujinx.Graphics.GAL public readonly bool SupportsIndirectParameters; public readonly bool SupportsDepthClipControl; + public readonly int UniformBufferSetIndex; + public readonly int StorageBufferSetIndex; + public readonly int TextureSetIndex; + public readonly int ImageSetIndex; + public readonly int ExtraSetBaseIndex; + public readonly int MaximumExtraSets; + public readonly uint MaximumUniformBuffersPerStage; public readonly uint MaximumStorageBuffersPerStage; public readonly uint MaximumTexturesPerStage; @@ -61,9 +71,12 @@ namespace Ryujinx.Graphics.GAL public readonly int GatherBiasPrecision; + public readonly ulong MaximumGpuMemory; + public Capabilities( TargetApi api, string vendorName, + SystemMemoryType memoryType, bool hasFrontFacingBug, bool hasVectorIndexingBug, bool needsFragmentOutputSpecialization, @@ -92,6 +105,8 @@ namespace Ryujinx.Graphics.GAL bool supportsMismatchingViewFormat, bool supportsCubemapView, bool supportsNonConstantTextureOffset, + bool supportsQuads, + bool supportsSeparateSampler, bool supportsShaderBallot, bool supportsShaderBarrierDivergence, bool supportsShaderFloat64, @@ -103,6 +118,12 @@ namespace Ryujinx.Graphics.GAL bool supportsViewportSwizzle, bool supportsIndirectParameters, bool supportsDepthClipControl, + int uniformBufferSetIndex, + int storageBufferSetIndex, + int textureSetIndex, + int imageSetIndex, + int extraSetBaseIndex, + int maximumExtraSets, uint maximumUniformBuffersPerStage, uint maximumStorageBuffersPerStage, uint maximumTexturesPerStage, @@ -112,10 +133,12 @@ namespace Ryujinx.Graphics.GAL int shaderSubgroupSize, int storageBufferOffsetAlignment, int textureBufferOffsetAlignment, - int gatherBiasPrecision) + int gatherBiasPrecision, + ulong maximumGpuMemory) { Api = api; VendorName = vendorName; + MemoryType = memoryType; HasFrontFacingBug = hasFrontFacingBug; HasVectorIndexingBug = hasVectorIndexingBug; NeedsFragmentOutputSpecialization = needsFragmentOutputSpecialization; @@ -144,6 +167,8 @@ namespace Ryujinx.Graphics.GAL SupportsMismatchingViewFormat = supportsMismatchingViewFormat; SupportsCubemapView = supportsCubemapView; SupportsNonConstantTextureOffset = supportsNonConstantTextureOffset; + SupportsQuads = supportsQuads; + SupportsSeparateSampler = supportsSeparateSampler; SupportsShaderBallot = supportsShaderBallot; SupportsShaderBarrierDivergence = supportsShaderBarrierDivergence; SupportsShaderFloat64 = supportsShaderFloat64; @@ -155,6 +180,12 @@ namespace Ryujinx.Graphics.GAL SupportsViewportSwizzle = supportsViewportSwizzle; SupportsIndirectParameters = supportsIndirectParameters; SupportsDepthClipControl = supportsDepthClipControl; + UniformBufferSetIndex = uniformBufferSetIndex; + StorageBufferSetIndex = storageBufferSetIndex; + TextureSetIndex = textureSetIndex; + ImageSetIndex = imageSetIndex; + ExtraSetBaseIndex = extraSetBaseIndex; + MaximumExtraSets = maximumExtraSets; MaximumUniformBuffersPerStage = maximumUniformBuffersPerStage; MaximumStorageBuffersPerStage = maximumStorageBuffersPerStage; MaximumTexturesPerStage = maximumTexturesPerStage; @@ -165,6 +196,7 @@ namespace Ryujinx.Graphics.GAL StorageBufferOffsetAlignment = storageBufferOffsetAlignment; TextureBufferOffsetAlignment = textureBufferOffsetAlignment; GatherBiasPrecision = gatherBiasPrecision; + MaximumGpuMemory = maximumGpuMemory; } } } diff --git a/src/Ryujinx.Graphics.GAL/Format.cs b/src/Ryujinx.Graphics.GAL/Format.cs index 99c89dcec..17c42d2d4 100644 --- a/src/Ryujinx.Graphics.GAL/Format.cs +++ b/src/Ryujinx.Graphics.GAL/Format.cs @@ -147,6 +147,8 @@ namespace Ryujinx.Graphics.GAL A1B5G5R5Unorm, B8G8R8A8Unorm, B8G8R8A8Srgb, + B10G10R10A2Unorm, + X8UintD24Unorm, } public static class FormatExtensions @@ -260,6 +262,7 @@ namespace Ryujinx.Graphics.GAL case Format.R10G10B10A2Sint: case Format.R10G10B10A2Uscaled: case Format.R10G10B10A2Sscaled: + case Format.B10G10R10A2Unorm: return 4; case Format.S8Uint: @@ -267,6 +270,7 @@ namespace Ryujinx.Graphics.GAL case Format.D16Unorm: return 2; case Format.S8UintD24Unorm: + case Format.X8UintD24Unorm: case Format.D32Float: case Format.D24UnormS8Uint: return 4; @@ -347,6 +351,7 @@ namespace Ryujinx.Graphics.GAL case Format.D16Unorm: case Format.D24UnormS8Uint: case Format.S8UintD24Unorm: + case Format.X8UintD24Unorm: case Format.D32Float: case Format.D32FloatS8Uint: return true; @@ -451,6 +456,7 @@ namespace Ryujinx.Graphics.GAL case Format.R32G32Uint: case Format.B8G8R8A8Unorm: case Format.B8G8R8A8Srgb: + case Format.B10G10R10A2Unorm: case Format.R10G10B10A2Unorm: case Format.R10G10B10A2Uint: case Format.R8G8B8A8Unorm: @@ -611,6 +617,7 @@ namespace Ryujinx.Graphics.GAL case Format.B5G5R5A1Unorm: case Format.B8G8R8A8Unorm: case Format.B8G8R8A8Srgb: + case Format.B10G10R10A2Unorm: return true; } @@ -629,6 +636,7 @@ namespace Ryujinx.Graphics.GAL case Format.D16Unorm: case Format.D24UnormS8Uint: case Format.S8UintD24Unorm: + case Format.X8UintD24Unorm: case Format.D32Float: case Format.D32FloatS8Uint: case Format.S8Uint: @@ -703,5 +711,36 @@ namespace Ryujinx.Graphics.GAL { return format.IsUint() || format.IsSint(); } + + /// + /// Checks if the texture format is a float or sRGB color format. + /// + /// + /// Does not include normalized, compressed or depth formats. + /// Float and sRGB formats do not participate in logical operations. + /// + /// Texture format + /// True if the format is a float or sRGB color format, false otherwise + public static bool IsFloatOrSrgb(this Format format) + { + switch (format) + { + case Format.R8G8B8A8Srgb: + case Format.B8G8R8A8Srgb: + case Format.R16Float: + case Format.R16G16Float: + case Format.R16G16B16Float: + case Format.R16G16B16A16Float: + case Format.R32Float: + case Format.R32G32Float: + case Format.R32G32B32Float: + case Format.R32G32B32A32Float: + case Format.R11G11B10Float: + case Format.R9G9B9E5Float: + return true; + } + + return false; + } } } diff --git a/src/Ryujinx.Graphics.GAL/HardwareInfo.cs b/src/Ryujinx.Graphics.GAL/HardwareInfo.cs index d8f7d1f71..c2546fa46 100644 --- a/src/Ryujinx.Graphics.GAL/HardwareInfo.cs +++ b/src/Ryujinx.Graphics.GAL/HardwareInfo.cs @@ -4,11 +4,13 @@ namespace Ryujinx.Graphics.GAL { public string GpuVendor { get; } public string GpuModel { get; } + public string GpuDriver { get; } - public HardwareInfo(string gpuVendor, string gpuModel) + public HardwareInfo(string gpuVendor, string gpuModel, string gpuDriver) { GpuVendor = gpuVendor; GpuModel = gpuModel; + GpuDriver = gpuDriver; } } } diff --git a/src/Ryujinx.Graphics.GAL/IImageArray.cs b/src/Ryujinx.Graphics.GAL/IImageArray.cs new file mode 100644 index 000000000..d87314eb8 --- /dev/null +++ b/src/Ryujinx.Graphics.GAL/IImageArray.cs @@ -0,0 +1,9 @@ +using System; + +namespace Ryujinx.Graphics.GAL +{ + public interface IImageArray : IDisposable + { + void SetImages(int index, ITexture[] images); + } +} diff --git a/src/Ryujinx.Graphics.GAL/IPipeline.cs b/src/Ryujinx.Graphics.GAL/IPipeline.cs index f5978cefa..b8409a573 100644 --- a/src/Ryujinx.Graphics.GAL/IPipeline.cs +++ b/src/Ryujinx.Graphics.GAL/IPipeline.cs @@ -58,7 +58,9 @@ namespace Ryujinx.Graphics.GAL void SetIndexBuffer(BufferRange buffer, IndexType type); - void SetImage(int binding, ITexture texture, Format imageFormat); + void SetImage(ShaderStage stage, int binding, ITexture texture); + void SetImageArray(ShaderStage stage, int binding, IImageArray array); + void SetImageArraySeparate(ShaderStage stage, int setIndex, IImageArray array); void SetLineParameters(float width, bool smooth); @@ -89,6 +91,8 @@ namespace Ryujinx.Graphics.GAL void SetStorageBuffers(ReadOnlySpan buffers); void SetTextureAndSampler(ShaderStage stage, int binding, ITexture texture, ISampler sampler); + void SetTextureArray(ShaderStage stage, int binding, ITextureArray array); + void SetTextureArraySeparate(ShaderStage stage, int setIndex, ITextureArray array); void SetTransformFeedbackBuffers(ReadOnlySpan buffers); void SetUniformBuffers(ReadOnlySpan buffers); diff --git a/src/Ryujinx.Graphics.GAL/IRenderer.cs b/src/Ryujinx.Graphics.GAL/IRenderer.cs index 3bf56465e..c2fdcbe4b 100644 --- a/src/Ryujinx.Graphics.GAL/IRenderer.cs +++ b/src/Ryujinx.Graphics.GAL/IRenderer.cs @@ -14,17 +14,22 @@ namespace Ryujinx.Graphics.GAL IWindow Window { get; } + uint ProgramCount { get; } + void BackgroundContextAction(Action action, bool alwaysBackground = false); BufferHandle CreateBuffer(int size, BufferAccess access = BufferAccess.Default); - BufferHandle CreateBuffer(int size, BufferAccess access, BufferHandle storageHint); BufferHandle CreateBuffer(nint pointer, int size); BufferHandle CreateBufferSparse(ReadOnlySpan storageBuffers); + IImageArray CreateImageArray(int size, bool isBuffer); + IProgram CreateProgram(ShaderSource[] shaders, ShaderInfo info); ISampler CreateSampler(SamplerCreateInfo info); ITexture CreateTexture(TextureCreateInfo info); + ITextureArray CreateTextureArray(int size, bool isBuffer); + bool PrepareHostMapping(nint address, ulong size); void CreateSync(ulong id, bool strict); diff --git a/src/Ryujinx.Graphics.GAL/ITexture.cs b/src/Ryujinx.Graphics.GAL/ITexture.cs index ea8d68649..2aa4053ff 100644 --- a/src/Ryujinx.Graphics.GAL/ITexture.cs +++ b/src/Ryujinx.Graphics.GAL/ITexture.cs @@ -1,5 +1,4 @@ using Ryujinx.Common.Memory; -using System.Buffers; namespace Ryujinx.Graphics.GAL { @@ -19,29 +18,33 @@ namespace Ryujinx.Graphics.GAL PinnedSpan GetData(int layer, int level); /// - /// Sets the texture data. The data passed as a will be disposed when the operation completes. + /// Sets the texture data. The data passed as a will be disposed when + /// the operation completes. /// /// Texture data bytes - void SetData(IMemoryOwner data); + void SetData(MemoryOwner data); /// - /// Sets the texture data. The data passed as a will be disposed when the operation completes. + /// Sets the texture data. The data passed as a will be disposed when + /// the operation completes. /// /// Texture data bytes /// Target layer /// Target level - void SetData(IMemoryOwner data, int layer, int level); + void SetData(MemoryOwner data, int layer, int level); /// - /// Sets the texture data. The data passed as a will be disposed when the operation completes. + /// Sets the texture data. The data passed as a will be disposed when + /// the operation completes. /// /// Texture data bytes /// Target layer /// Target level /// Target sub-region of the texture to update - void SetData(IMemoryOwner data, int layer, int level, Rectangle region); + void SetData(MemoryOwner data, int layer, int level, Rectangle region); void SetStorage(BufferRange buffer); + void Release(); } } diff --git a/src/Ryujinx.Graphics.GAL/ITextureArray.cs b/src/Ryujinx.Graphics.GAL/ITextureArray.cs new file mode 100644 index 000000000..9ee79dacb --- /dev/null +++ b/src/Ryujinx.Graphics.GAL/ITextureArray.cs @@ -0,0 +1,10 @@ +using System; + +namespace Ryujinx.Graphics.GAL +{ + public interface ITextureArray : IDisposable + { + void SetSamplers(int index, ISampler[] samplers); + void SetTextures(int index, ITexture[] textures); + } +} diff --git a/src/Ryujinx.Graphics.GAL/IWindow.cs b/src/Ryujinx.Graphics.GAL/IWindow.cs index 83418e709..12686cb28 100644 --- a/src/Ryujinx.Graphics.GAL/IWindow.cs +++ b/src/Ryujinx.Graphics.GAL/IWindow.cs @@ -8,7 +8,7 @@ namespace Ryujinx.Graphics.GAL void SetSize(int width, int height); - void ChangeVSyncMode(bool vsyncEnabled); + void ChangeVSyncMode(VSyncMode vSyncMode); void SetAntiAliasing(AntiAliasing antialiasing); void SetScalingFilter(ScalingFilter type); diff --git a/src/Ryujinx.Graphics.GAL/Multithreading/CommandHelper.cs b/src/Ryujinx.Graphics.GAL/Multithreading/CommandHelper.cs index 5bf3d3283..a1e6db971 100644 --- a/src/Ryujinx.Graphics.GAL/Multithreading/CommandHelper.cs +++ b/src/Ryujinx.Graphics.GAL/Multithreading/CommandHelper.cs @@ -1,10 +1,12 @@ using Ryujinx.Graphics.GAL.Multithreading.Commands; using Ryujinx.Graphics.GAL.Multithreading.Commands.Buffer; using Ryujinx.Graphics.GAL.Multithreading.Commands.CounterEvent; +using Ryujinx.Graphics.GAL.Multithreading.Commands.ImageArray; using Ryujinx.Graphics.GAL.Multithreading.Commands.Program; using Ryujinx.Graphics.GAL.Multithreading.Commands.Renderer; using Ryujinx.Graphics.GAL.Multithreading.Commands.Sampler; using Ryujinx.Graphics.GAL.Multithreading.Commands.Texture; +using Ryujinx.Graphics.GAL.Multithreading.Commands.TextureArray; using Ryujinx.Graphics.GAL.Multithreading.Commands.Window; using System; using System.Linq; @@ -42,14 +44,15 @@ namespace Ryujinx.Graphics.GAL.Multithreading } Register(CommandType.Action); - Register(CommandType.CreateBuffer); Register(CommandType.CreateBufferAccess); Register(CommandType.CreateBufferSparse); Register(CommandType.CreateHostBuffer); + Register(CommandType.CreateImageArray); Register(CommandType.CreateProgram); Register(CommandType.CreateSampler); Register(CommandType.CreateSync); Register(CommandType.CreateTexture); + Register(CommandType.CreateTextureArray); Register(CommandType.GetCapabilities); Register(CommandType.PreFrame); Register(CommandType.ReportCounter); @@ -63,6 +66,9 @@ namespace Ryujinx.Graphics.GAL.Multithreading Register(CommandType.CounterEventDispose); Register(CommandType.CounterEventFlush); + Register(CommandType.ImageArrayDispose); + Register(CommandType.ImageArraySetImages); + Register(CommandType.ProgramDispose); Register(CommandType.ProgramGetBinary); Register(CommandType.ProgramCheckLink); @@ -82,6 +88,10 @@ namespace Ryujinx.Graphics.GAL.Multithreading Register(CommandType.TextureSetDataSliceRegion); Register(CommandType.TextureSetStorage); + Register(CommandType.TextureArrayDispose); + Register(CommandType.TextureArraySetSamplers); + Register(CommandType.TextureArraySetTextures); + Register(CommandType.WindowPresent); Register(CommandType.Barrier); @@ -114,6 +124,8 @@ namespace Ryujinx.Graphics.GAL.Multithreading Register(CommandType.SetTransformFeedbackBuffers); Register(CommandType.SetUniformBuffers); Register(CommandType.SetImage); + Register(CommandType.SetImageArray); + Register(CommandType.SetImageArraySeparate); Register(CommandType.SetIndexBuffer); Register(CommandType.SetLineParameters); Register(CommandType.SetLogicOpState); @@ -130,6 +142,8 @@ namespace Ryujinx.Graphics.GAL.Multithreading Register(CommandType.SetScissor); Register(CommandType.SetStencilTest); Register(CommandType.SetTextureAndSampler); + Register(CommandType.SetTextureArray); + Register(CommandType.SetTextureArraySeparate); Register(CommandType.SetUserClipDistance); Register(CommandType.SetVertexAttribs); Register(CommandType.SetVertexBuffers); diff --git a/src/Ryujinx.Graphics.GAL/Multithreading/CommandType.cs b/src/Ryujinx.Graphics.GAL/Multithreading/CommandType.cs index 6be639253..348c8e462 100644 --- a/src/Ryujinx.Graphics.GAL/Multithreading/CommandType.cs +++ b/src/Ryujinx.Graphics.GAL/Multithreading/CommandType.cs @@ -3,14 +3,15 @@ namespace Ryujinx.Graphics.GAL.Multithreading enum CommandType : byte { Action, - CreateBuffer, CreateBufferAccess, CreateBufferSparse, CreateHostBuffer, + CreateImageArray, CreateProgram, CreateSampler, CreateSync, CreateTexture, + CreateTextureArray, GetCapabilities, Unused, PreFrame, @@ -25,6 +26,9 @@ namespace Ryujinx.Graphics.GAL.Multithreading CounterEventDispose, CounterEventFlush, + ImageArrayDispose, + ImageArraySetImages, + ProgramDispose, ProgramGetBinary, ProgramCheckLink, @@ -44,6 +48,10 @@ namespace Ryujinx.Graphics.GAL.Multithreading TextureSetDataSliceRegion, TextureSetStorage, + TextureArrayDispose, + TextureArraySetSamplers, + TextureArraySetTextures, + WindowPresent, Barrier, @@ -76,6 +84,8 @@ namespace Ryujinx.Graphics.GAL.Multithreading SetTransformFeedbackBuffers, SetUniformBuffers, SetImage, + SetImageArray, + SetImageArraySeparate, SetIndexBuffer, SetLineParameters, SetLogicOpState, @@ -92,6 +102,8 @@ namespace Ryujinx.Graphics.GAL.Multithreading SetScissor, SetStencilTest, SetTextureAndSampler, + SetTextureArray, + SetTextureArraySeparate, SetUserClipDistance, SetVertexAttribs, SetVertexBuffers, diff --git a/src/Ryujinx.Graphics.GAL/Multithreading/Commands/ImageArray/ImageArrayDisposeCommand.cs b/src/Ryujinx.Graphics.GAL/Multithreading/Commands/ImageArray/ImageArrayDisposeCommand.cs new file mode 100644 index 000000000..ac2ac933b --- /dev/null +++ b/src/Ryujinx.Graphics.GAL/Multithreading/Commands/ImageArray/ImageArrayDisposeCommand.cs @@ -0,0 +1,21 @@ +using Ryujinx.Graphics.GAL.Multithreading.Model; +using Ryujinx.Graphics.GAL.Multithreading.Resources; + +namespace Ryujinx.Graphics.GAL.Multithreading.Commands.ImageArray +{ + struct ImageArrayDisposeCommand : IGALCommand, IGALCommand + { + public readonly CommandType CommandType => CommandType.ImageArrayDispose; + private TableRef _imageArray; + + public void Set(TableRef imageArray) + { + _imageArray = imageArray; + } + + public static void Run(ref ImageArrayDisposeCommand command, ThreadedRenderer threaded, IRenderer renderer) + { + command._imageArray.Get(threaded).Base.Dispose(); + } + } +} diff --git a/src/Ryujinx.Graphics.GAL/Multithreading/Commands/ImageArray/ImageArraySetImagesCommand.cs b/src/Ryujinx.Graphics.GAL/Multithreading/Commands/ImageArray/ImageArraySetImagesCommand.cs new file mode 100644 index 000000000..cc28d585c --- /dev/null +++ b/src/Ryujinx.Graphics.GAL/Multithreading/Commands/ImageArray/ImageArraySetImagesCommand.cs @@ -0,0 +1,27 @@ +using Ryujinx.Graphics.GAL.Multithreading.Model; +using Ryujinx.Graphics.GAL.Multithreading.Resources; +using System.Linq; + +namespace Ryujinx.Graphics.GAL.Multithreading.Commands.ImageArray +{ + struct ImageArraySetImagesCommand : IGALCommand, IGALCommand + { + public readonly CommandType CommandType => CommandType.ImageArraySetImages; + private TableRef _imageArray; + private int _index; + private TableRef _images; + + public void Set(TableRef imageArray, int index, TableRef images) + { + _imageArray = imageArray; + _index = index; + _images = images; + } + + public static void Run(ref ImageArraySetImagesCommand command, ThreadedRenderer threaded, IRenderer renderer) + { + ThreadedImageArray imageArray = command._imageArray.Get(threaded); + imageArray.Base.SetImages(command._index, command._images.Get(threaded).Select(texture => ((ThreadedTexture)texture)?.Base).ToArray()); + } + } +} diff --git a/src/Ryujinx.Graphics.GAL/Multithreading/Commands/Renderer/CreateBufferCommand.cs b/src/Ryujinx.Graphics.GAL/Multithreading/Commands/Renderer/CreateBufferCommand.cs deleted file mode 100644 index 60a6e4bf4..000000000 --- a/src/Ryujinx.Graphics.GAL/Multithreading/Commands/Renderer/CreateBufferCommand.cs +++ /dev/null @@ -1,31 +0,0 @@ -namespace Ryujinx.Graphics.GAL.Multithreading.Commands.Renderer -{ - struct CreateBufferCommand : IGALCommand, IGALCommand - { - public readonly CommandType CommandType => CommandType.CreateBuffer; - private BufferHandle _threadedHandle; - private int _size; - private BufferAccess _access; - private BufferHandle _storageHint; - - public void Set(BufferHandle threadedHandle, int size, BufferAccess access, BufferHandle storageHint) - { - _threadedHandle = threadedHandle; - _size = size; - _access = access; - _storageHint = storageHint; - } - - public static void Run(ref CreateBufferCommand command, ThreadedRenderer threaded, IRenderer renderer) - { - BufferHandle hint = BufferHandle.Null; - - if (command._storageHint != BufferHandle.Null) - { - hint = threaded.Buffers.MapBuffer(command._storageHint); - } - - threaded.Buffers.AssignBuffer(command._threadedHandle, renderer.CreateBuffer(command._size, command._access, hint)); - } - } -} diff --git a/src/Ryujinx.Graphics.GAL/Multithreading/Commands/Renderer/CreateImageArrayCommand.cs b/src/Ryujinx.Graphics.GAL/Multithreading/Commands/Renderer/CreateImageArrayCommand.cs new file mode 100644 index 000000000..1c3fb8120 --- /dev/null +++ b/src/Ryujinx.Graphics.GAL/Multithreading/Commands/Renderer/CreateImageArrayCommand.cs @@ -0,0 +1,25 @@ +using Ryujinx.Graphics.GAL.Multithreading.Model; +using Ryujinx.Graphics.GAL.Multithreading.Resources; + +namespace Ryujinx.Graphics.GAL.Multithreading.Commands.Renderer +{ + struct CreateImageArrayCommand : IGALCommand, IGALCommand + { + public readonly CommandType CommandType => CommandType.CreateImageArray; + private TableRef _imageArray; + private int _size; + private bool _isBuffer; + + public void Set(TableRef imageArray, int size, bool isBuffer) + { + _imageArray = imageArray; + _size = size; + _isBuffer = isBuffer; + } + + public static void Run(ref CreateImageArrayCommand command, ThreadedRenderer threaded, IRenderer renderer) + { + command._imageArray.Get(threaded).Base = renderer.CreateImageArray(command._size, command._isBuffer); + } + } +} diff --git a/src/Ryujinx.Graphics.GAL/Multithreading/Commands/Renderer/CreateTextureArrayCommand.cs b/src/Ryujinx.Graphics.GAL/Multithreading/Commands/Renderer/CreateTextureArrayCommand.cs new file mode 100644 index 000000000..9bd891e68 --- /dev/null +++ b/src/Ryujinx.Graphics.GAL/Multithreading/Commands/Renderer/CreateTextureArrayCommand.cs @@ -0,0 +1,25 @@ +using Ryujinx.Graphics.GAL.Multithreading.Model; +using Ryujinx.Graphics.GAL.Multithreading.Resources; + +namespace Ryujinx.Graphics.GAL.Multithreading.Commands.Renderer +{ + struct CreateTextureArrayCommand : IGALCommand, IGALCommand + { + public readonly CommandType CommandType => CommandType.CreateTextureArray; + private TableRef _textureArray; + private int _size; + private bool _isBuffer; + + public void Set(TableRef textureArray, int size, bool isBuffer) + { + _textureArray = textureArray; + _size = size; + _isBuffer = isBuffer; + } + + public static void Run(ref CreateTextureArrayCommand command, ThreadedRenderer threaded, IRenderer renderer) + { + command._textureArray.Get(threaded).Base = renderer.CreateTextureArray(command._size, command._isBuffer); + } + } +} diff --git a/src/Ryujinx.Graphics.GAL/Multithreading/Commands/SetImageArrayCommand.cs b/src/Ryujinx.Graphics.GAL/Multithreading/Commands/SetImageArrayCommand.cs new file mode 100644 index 000000000..b8d3c7ac5 --- /dev/null +++ b/src/Ryujinx.Graphics.GAL/Multithreading/Commands/SetImageArrayCommand.cs @@ -0,0 +1,26 @@ +using Ryujinx.Graphics.GAL.Multithreading.Model; +using Ryujinx.Graphics.GAL.Multithreading.Resources; +using Ryujinx.Graphics.Shader; + +namespace Ryujinx.Graphics.GAL.Multithreading.Commands +{ + struct SetImageArrayCommand : IGALCommand, IGALCommand + { + public readonly CommandType CommandType => CommandType.SetImageArray; + private ShaderStage _stage; + private int _binding; + private TableRef _array; + + public void Set(ShaderStage stage, int binding, TableRef array) + { + _stage = stage; + _binding = binding; + _array = array; + } + + public static void Run(ref SetImageArrayCommand command, ThreadedRenderer threaded, IRenderer renderer) + { + renderer.Pipeline.SetImageArray(command._stage, command._binding, command._array.GetAs(threaded)?.Base); + } + } +} diff --git a/src/Ryujinx.Graphics.GAL/Multithreading/Commands/SetImageArraySeparateCommand.cs b/src/Ryujinx.Graphics.GAL/Multithreading/Commands/SetImageArraySeparateCommand.cs new file mode 100644 index 000000000..abeb58a06 --- /dev/null +++ b/src/Ryujinx.Graphics.GAL/Multithreading/Commands/SetImageArraySeparateCommand.cs @@ -0,0 +1,26 @@ +using Ryujinx.Graphics.GAL.Multithreading.Model; +using Ryujinx.Graphics.GAL.Multithreading.Resources; +using Ryujinx.Graphics.Shader; + +namespace Ryujinx.Graphics.GAL.Multithreading.Commands +{ + struct SetImageArraySeparateCommand : IGALCommand, IGALCommand + { + public readonly CommandType CommandType => CommandType.SetImageArraySeparate; + private ShaderStage _stage; + private int _setIndex; + private TableRef _array; + + public void Set(ShaderStage stage, int setIndex, TableRef array) + { + _stage = stage; + _setIndex = setIndex; + _array = array; + } + + public static void Run(ref SetImageArraySeparateCommand command, ThreadedRenderer threaded, IRenderer renderer) + { + renderer.Pipeline.SetImageArraySeparate(command._stage, command._setIndex, command._array.GetAs(threaded)?.Base); + } + } +} diff --git a/src/Ryujinx.Graphics.GAL/Multithreading/Commands/SetImageCommand.cs b/src/Ryujinx.Graphics.GAL/Multithreading/Commands/SetImageCommand.cs index b4e966ca8..2ba9db527 100644 --- a/src/Ryujinx.Graphics.GAL/Multithreading/Commands/SetImageCommand.cs +++ b/src/Ryujinx.Graphics.GAL/Multithreading/Commands/SetImageCommand.cs @@ -1,25 +1,26 @@ using Ryujinx.Graphics.GAL.Multithreading.Model; using Ryujinx.Graphics.GAL.Multithreading.Resources; +using Ryujinx.Graphics.Shader; namespace Ryujinx.Graphics.GAL.Multithreading.Commands { struct SetImageCommand : IGALCommand, IGALCommand { public readonly CommandType CommandType => CommandType.SetImage; + private ShaderStage _stage; private int _binding; private TableRef _texture; - private Format _imageFormat; - public void Set(int binding, TableRef texture, Format imageFormat) + public void Set(ShaderStage stage, int binding, TableRef texture) { + _stage = stage; _binding = binding; _texture = texture; - _imageFormat = imageFormat; } public static void Run(ref SetImageCommand command, ThreadedRenderer threaded, IRenderer renderer) { - renderer.Pipeline.SetImage(command._binding, command._texture.GetAs(threaded)?.Base, command._imageFormat); + renderer.Pipeline.SetImage(command._stage, command._binding, command._texture.GetAs(threaded)?.Base); } } } diff --git a/src/Ryujinx.Graphics.GAL/Multithreading/Commands/SetTextureArrayCommand.cs b/src/Ryujinx.Graphics.GAL/Multithreading/Commands/SetTextureArrayCommand.cs new file mode 100644 index 000000000..45e28aa65 --- /dev/null +++ b/src/Ryujinx.Graphics.GAL/Multithreading/Commands/SetTextureArrayCommand.cs @@ -0,0 +1,26 @@ +using Ryujinx.Graphics.GAL.Multithreading.Model; +using Ryujinx.Graphics.GAL.Multithreading.Resources; +using Ryujinx.Graphics.Shader; + +namespace Ryujinx.Graphics.GAL.Multithreading.Commands +{ + struct SetTextureArrayCommand : IGALCommand, IGALCommand + { + public readonly CommandType CommandType => CommandType.SetTextureArray; + private ShaderStage _stage; + private int _binding; + private TableRef _array; + + public void Set(ShaderStage stage, int binding, TableRef array) + { + _stage = stage; + _binding = binding; + _array = array; + } + + public static void Run(ref SetTextureArrayCommand command, ThreadedRenderer threaded, IRenderer renderer) + { + renderer.Pipeline.SetTextureArray(command._stage, command._binding, command._array.GetAs(threaded)?.Base); + } + } +} diff --git a/src/Ryujinx.Graphics.GAL/Multithreading/Commands/SetTextureArraySeparateCommand.cs b/src/Ryujinx.Graphics.GAL/Multithreading/Commands/SetTextureArraySeparateCommand.cs new file mode 100644 index 000000000..b179f2e70 --- /dev/null +++ b/src/Ryujinx.Graphics.GAL/Multithreading/Commands/SetTextureArraySeparateCommand.cs @@ -0,0 +1,26 @@ +using Ryujinx.Graphics.GAL.Multithreading.Model; +using Ryujinx.Graphics.GAL.Multithreading.Resources; +using Ryujinx.Graphics.Shader; + +namespace Ryujinx.Graphics.GAL.Multithreading.Commands +{ + struct SetTextureArraySeparateCommand : IGALCommand, IGALCommand + { + public readonly CommandType CommandType => CommandType.SetTextureArraySeparate; + private ShaderStage _stage; + private int _setIndex; + private TableRef _array; + + public void Set(ShaderStage stage, int setIndex, TableRef array) + { + _stage = stage; + _setIndex = setIndex; + _array = array; + } + + public static void Run(ref SetTextureArraySeparateCommand command, ThreadedRenderer threaded, IRenderer renderer) + { + renderer.Pipeline.SetTextureArraySeparate(command._stage, command._setIndex, command._array.GetAs(threaded)?.Base); + } + } +} diff --git a/src/Ryujinx.Graphics.GAL/Multithreading/Commands/Texture/TextureSetDataCommand.cs b/src/Ryujinx.Graphics.GAL/Multithreading/Commands/Texture/TextureSetDataCommand.cs index a7a8846e4..4449566a7 100644 --- a/src/Ryujinx.Graphics.GAL/Multithreading/Commands/Texture/TextureSetDataCommand.cs +++ b/src/Ryujinx.Graphics.GAL/Multithreading/Commands/Texture/TextureSetDataCommand.cs @@ -1,7 +1,6 @@ +using Ryujinx.Common.Memory; using Ryujinx.Graphics.GAL.Multithreading.Model; using Ryujinx.Graphics.GAL.Multithreading.Resources; -using System; -using System.Buffers; namespace Ryujinx.Graphics.GAL.Multithreading.Commands.Texture { @@ -9,9 +8,9 @@ namespace Ryujinx.Graphics.GAL.Multithreading.Commands.Texture { public readonly CommandType CommandType => CommandType.TextureSetData; private TableRef _texture; - private TableRef> _data; + private TableRef> _data; - public void Set(TableRef texture, TableRef> data) + public void Set(TableRef texture, TableRef> data) { _texture = texture; _data = data; diff --git a/src/Ryujinx.Graphics.GAL/Multithreading/Commands/Texture/TextureSetDataSliceCommand.cs b/src/Ryujinx.Graphics.GAL/Multithreading/Commands/Texture/TextureSetDataSliceCommand.cs index bb24ee140..3619149e9 100644 --- a/src/Ryujinx.Graphics.GAL/Multithreading/Commands/Texture/TextureSetDataSliceCommand.cs +++ b/src/Ryujinx.Graphics.GAL/Multithreading/Commands/Texture/TextureSetDataSliceCommand.cs @@ -1,7 +1,6 @@ +using Ryujinx.Common.Memory; using Ryujinx.Graphics.GAL.Multithreading.Model; using Ryujinx.Graphics.GAL.Multithreading.Resources; -using System; -using System.Buffers; namespace Ryujinx.Graphics.GAL.Multithreading.Commands.Texture { @@ -9,11 +8,11 @@ namespace Ryujinx.Graphics.GAL.Multithreading.Commands.Texture { public readonly CommandType CommandType => CommandType.TextureSetDataSlice; private TableRef _texture; - private TableRef> _data; + private TableRef> _data; private int _layer; private int _level; - public void Set(TableRef texture, TableRef> data, int layer, int level) + public void Set(TableRef texture, TableRef> data, int layer, int level) { _texture = texture; _data = data; diff --git a/src/Ryujinx.Graphics.GAL/Multithreading/Commands/Texture/TextureSetDataSliceRegionCommand.cs b/src/Ryujinx.Graphics.GAL/Multithreading/Commands/Texture/TextureSetDataSliceRegionCommand.cs index 93631267b..6c6a53636 100644 --- a/src/Ryujinx.Graphics.GAL/Multithreading/Commands/Texture/TextureSetDataSliceRegionCommand.cs +++ b/src/Ryujinx.Graphics.GAL/Multithreading/Commands/Texture/TextureSetDataSliceRegionCommand.cs @@ -1,7 +1,6 @@ +using Ryujinx.Common.Memory; using Ryujinx.Graphics.GAL.Multithreading.Model; using Ryujinx.Graphics.GAL.Multithreading.Resources; -using System; -using System.Buffers; namespace Ryujinx.Graphics.GAL.Multithreading.Commands.Texture { @@ -9,12 +8,12 @@ namespace Ryujinx.Graphics.GAL.Multithreading.Commands.Texture { public readonly CommandType CommandType => CommandType.TextureSetDataSliceRegion; private TableRef _texture; - private TableRef> _data; + private TableRef> _data; private int _layer; private int _level; private Rectangle _region; - public void Set(TableRef texture, TableRef> data, int layer, int level, Rectangle region) + public void Set(TableRef texture, TableRef> data, int layer, int level, Rectangle region) { _texture = texture; _data = data; diff --git a/src/Ryujinx.Graphics.GAL/Multithreading/Commands/TextureArray/TextureArrayDisposeCommand.cs b/src/Ryujinx.Graphics.GAL/Multithreading/Commands/TextureArray/TextureArrayDisposeCommand.cs new file mode 100644 index 000000000..fec1c48f0 --- /dev/null +++ b/src/Ryujinx.Graphics.GAL/Multithreading/Commands/TextureArray/TextureArrayDisposeCommand.cs @@ -0,0 +1,21 @@ +using Ryujinx.Graphics.GAL.Multithreading.Model; +using Ryujinx.Graphics.GAL.Multithreading.Resources; + +namespace Ryujinx.Graphics.GAL.Multithreading.Commands.TextureArray +{ + struct TextureArrayDisposeCommand : IGALCommand, IGALCommand + { + public readonly CommandType CommandType => CommandType.TextureArrayDispose; + private TableRef _textureArray; + + public void Set(TableRef textureArray) + { + _textureArray = textureArray; + } + + public static void Run(ref TextureArrayDisposeCommand command, ThreadedRenderer threaded, IRenderer renderer) + { + command._textureArray.Get(threaded).Base.Dispose(); + } + } +} diff --git a/src/Ryujinx.Graphics.GAL/Multithreading/Commands/TextureArray/TextureArraySetSamplersCommand.cs b/src/Ryujinx.Graphics.GAL/Multithreading/Commands/TextureArray/TextureArraySetSamplersCommand.cs new file mode 100644 index 000000000..204ee32da --- /dev/null +++ b/src/Ryujinx.Graphics.GAL/Multithreading/Commands/TextureArray/TextureArraySetSamplersCommand.cs @@ -0,0 +1,27 @@ +using Ryujinx.Graphics.GAL.Multithreading.Model; +using Ryujinx.Graphics.GAL.Multithreading.Resources; +using System.Linq; + +namespace Ryujinx.Graphics.GAL.Multithreading.Commands.TextureArray +{ + struct TextureArraySetSamplersCommand : IGALCommand, IGALCommand + { + public readonly CommandType CommandType => CommandType.TextureArraySetSamplers; + private TableRef _textureArray; + private int _index; + private TableRef _samplers; + + public void Set(TableRef textureArray, int index, TableRef samplers) + { + _textureArray = textureArray; + _index = index; + _samplers = samplers; + } + + public static void Run(ref TextureArraySetSamplersCommand command, ThreadedRenderer threaded, IRenderer renderer) + { + ThreadedTextureArray textureArray = command._textureArray.Get(threaded); + textureArray.Base.SetSamplers(command._index, command._samplers.Get(threaded).Select(sampler => ((ThreadedSampler)sampler)?.Base).ToArray()); + } + } +} diff --git a/src/Ryujinx.Graphics.GAL/Multithreading/Commands/TextureArray/TextureArraySetTexturesCommand.cs b/src/Ryujinx.Graphics.GAL/Multithreading/Commands/TextureArray/TextureArraySetTexturesCommand.cs new file mode 100644 index 000000000..cc94d1b6d --- /dev/null +++ b/src/Ryujinx.Graphics.GAL/Multithreading/Commands/TextureArray/TextureArraySetTexturesCommand.cs @@ -0,0 +1,27 @@ +using Ryujinx.Graphics.GAL.Multithreading.Model; +using Ryujinx.Graphics.GAL.Multithreading.Resources; +using System.Linq; + +namespace Ryujinx.Graphics.GAL.Multithreading.Commands.TextureArray +{ + struct TextureArraySetTexturesCommand : IGALCommand, IGALCommand + { + public readonly CommandType CommandType => CommandType.TextureArraySetTextures; + private TableRef _textureArray; + private int _index; + private TableRef _textures; + + public void Set(TableRef textureArray, int index, TableRef textures) + { + _textureArray = textureArray; + _index = index; + _textures = textures; + } + + public static void Run(ref TextureArraySetTexturesCommand command, ThreadedRenderer threaded, IRenderer renderer) + { + ThreadedTextureArray textureArray = command._textureArray.Get(threaded); + textureArray.Base.SetTextures(command._index, command._textures.Get(threaded).Select(texture => ((ThreadedTexture)texture)?.Base).ToArray()); + } + } +} diff --git a/src/Ryujinx.Graphics.GAL/Multithreading/Resources/ThreadedImageArray.cs b/src/Ryujinx.Graphics.GAL/Multithreading/Resources/ThreadedImageArray.cs new file mode 100644 index 000000000..82587c189 --- /dev/null +++ b/src/Ryujinx.Graphics.GAL/Multithreading/Resources/ThreadedImageArray.cs @@ -0,0 +1,36 @@ +using Ryujinx.Graphics.GAL.Multithreading.Commands.ImageArray; +using Ryujinx.Graphics.GAL.Multithreading.Model; + +namespace Ryujinx.Graphics.GAL.Multithreading.Resources +{ + /// + /// Threaded representation of a image array. + /// + class ThreadedImageArray : IImageArray + { + private readonly ThreadedRenderer _renderer; + public IImageArray Base; + + public ThreadedImageArray(ThreadedRenderer renderer) + { + _renderer = renderer; + } + + private TableRef Ref(T reference) + { + return new TableRef(_renderer, reference); + } + + public void Dispose() + { + _renderer.New().Set(Ref(this)); + _renderer.QueueCommand(); + } + + public void SetImages(int index, ITexture[] images) + { + _renderer.New().Set(Ref(this), index, Ref(images)); + _renderer.QueueCommand(); + } + } +} diff --git a/src/Ryujinx.Graphics.GAL/Multithreading/Resources/ThreadedTexture.cs b/src/Ryujinx.Graphics.GAL/Multithreading/Resources/ThreadedTexture.cs index b674930e1..fa71d20b3 100644 --- a/src/Ryujinx.Graphics.GAL/Multithreading/Resources/ThreadedTexture.cs +++ b/src/Ryujinx.Graphics.GAL/Multithreading/Resources/ThreadedTexture.cs @@ -1,7 +1,6 @@ using Ryujinx.Common.Memory; using Ryujinx.Graphics.GAL.Multithreading.Commands.Texture; using Ryujinx.Graphics.GAL.Multithreading.Model; -using System.Buffers; namespace Ryujinx.Graphics.GAL.Multithreading.Resources { @@ -111,19 +110,22 @@ namespace Ryujinx.Graphics.GAL.Multithreading.Resources _renderer.QueueCommand(); } - public void SetData(IMemoryOwner data) + /// + public void SetData(MemoryOwner data) { _renderer.New().Set(Ref(this), Ref(data)); _renderer.QueueCommand(); } - public void SetData(IMemoryOwner data, int layer, int level) + /// + public void SetData(MemoryOwner data, int layer, int level) { _renderer.New().Set(Ref(this), Ref(data), layer, level); _renderer.QueueCommand(); } - public void SetData(IMemoryOwner data, int layer, int level, Rectangle region) + /// + public void SetData(MemoryOwner data, int layer, int level, Rectangle region) { _renderer.New().Set(Ref(this), Ref(data), layer, level, region); _renderer.QueueCommand(); diff --git a/src/Ryujinx.Graphics.GAL/Multithreading/Resources/ThreadedTextureArray.cs b/src/Ryujinx.Graphics.GAL/Multithreading/Resources/ThreadedTextureArray.cs new file mode 100644 index 000000000..4334c7048 --- /dev/null +++ b/src/Ryujinx.Graphics.GAL/Multithreading/Resources/ThreadedTextureArray.cs @@ -0,0 +1,43 @@ +using Ryujinx.Graphics.GAL.Multithreading.Commands.TextureArray; +using Ryujinx.Graphics.GAL.Multithreading.Model; +using System.Linq; + +namespace Ryujinx.Graphics.GAL.Multithreading.Resources +{ + /// + /// Threaded representation of a texture and sampler array. + /// + class ThreadedTextureArray : ITextureArray + { + private readonly ThreadedRenderer _renderer; + public ITextureArray Base; + + public ThreadedTextureArray(ThreadedRenderer renderer) + { + _renderer = renderer; + } + + private TableRef Ref(T reference) + { + return new TableRef(_renderer, reference); + } + + public void Dispose() + { + _renderer.New().Set(Ref(this)); + _renderer.QueueCommand(); + } + + public void SetSamplers(int index, ISampler[] samplers) + { + _renderer.New().Set(Ref(this), index, Ref(samplers.ToArray())); + _renderer.QueueCommand(); + } + + public void SetTextures(int index, ITexture[] textures) + { + _renderer.New().Set(Ref(this), index, Ref(textures.ToArray())); + _renderer.QueueCommand(); + } + } +} diff --git a/src/Ryujinx.Graphics.GAL/Multithreading/ThreadedPipeline.cs b/src/Ryujinx.Graphics.GAL/Multithreading/ThreadedPipeline.cs index f40d9896c..deec36648 100644 --- a/src/Ryujinx.Graphics.GAL/Multithreading/ThreadedPipeline.cs +++ b/src/Ryujinx.Graphics.GAL/Multithreading/ThreadedPipeline.cs @@ -177,9 +177,21 @@ namespace Ryujinx.Graphics.GAL.Multithreading _renderer.QueueCommand(); } - public void SetImage(int binding, ITexture texture, Format imageFormat) + public void SetImage(ShaderStage stage, int binding, ITexture texture) { - _renderer.New().Set(binding, Ref(texture), imageFormat); + _renderer.New().Set(stage, binding, Ref(texture)); + _renderer.QueueCommand(); + } + + public void SetImageArray(ShaderStage stage, int binding, IImageArray array) + { + _renderer.New().Set(stage, binding, Ref(array)); + _renderer.QueueCommand(); + } + + public void SetImageArraySeparate(ShaderStage stage, int setIndex, IImageArray array) + { + _renderer.New().Set(stage, setIndex, Ref(array)); _renderer.QueueCommand(); } @@ -285,6 +297,18 @@ namespace Ryujinx.Graphics.GAL.Multithreading _renderer.QueueCommand(); } + public void SetTextureArray(ShaderStage stage, int binding, ITextureArray array) + { + _renderer.New().Set(stage, binding, Ref(array)); + _renderer.QueueCommand(); + } + + public void SetTextureArraySeparate(ShaderStage stage, int setIndex, ITextureArray array) + { + _renderer.New().Set(stage, setIndex, Ref(array)); + _renderer.QueueCommand(); + } + public void SetTransformFeedbackBuffers(ReadOnlySpan buffers) { _renderer.New().Set(_renderer.CopySpan(buffers)); diff --git a/src/Ryujinx.Graphics.GAL/Multithreading/ThreadedRenderer.cs b/src/Ryujinx.Graphics.GAL/Multithreading/ThreadedRenderer.cs index 830fbf2d9..0bd3dc592 100644 --- a/src/Ryujinx.Graphics.GAL/Multithreading/ThreadedRenderer.cs +++ b/src/Ryujinx.Graphics.GAL/Multithreading/ThreadedRenderer.cs @@ -55,6 +55,8 @@ namespace Ryujinx.Graphics.GAL.Multithreading private int _refProducerPtr; private int _refConsumerPtr; + public uint ProgramCount { get; set; } = 0; + private Action _interruptAction; private readonly object _interruptLock = new(); @@ -272,15 +274,6 @@ namespace Ryujinx.Graphics.GAL.Multithreading return handle; } - public BufferHandle CreateBuffer(int size, BufferAccess access, BufferHandle storageHint) - { - BufferHandle handle = Buffers.CreateBufferHandle(); - New().Set(handle, size, access, storageHint); - QueueCommand(); - - return handle; - } - public BufferHandle CreateBuffer(nint pointer, int size) { BufferHandle handle = Buffers.CreateBufferHandle(); @@ -299,6 +292,15 @@ namespace Ryujinx.Graphics.GAL.Multithreading return handle; } + public IImageArray CreateImageArray(int size, bool isBuffer) + { + var imageArray = new ThreadedImageArray(this); + New().Set(Ref(imageArray), size, isBuffer); + QueueCommand(); + + return imageArray; + } + public IProgram CreateProgram(ShaderSource[] shaders, ShaderInfo info) { var program = new ThreadedProgram(this); @@ -307,6 +309,8 @@ namespace Ryujinx.Graphics.GAL.Multithreading Programs.Add(request); + ProgramCount++; + New().Set(Ref((IProgramRequest)request)); QueueCommand(); @@ -349,6 +353,14 @@ namespace Ryujinx.Graphics.GAL.Multithreading return texture; } } + public ITextureArray CreateTextureArray(int size, bool isBuffer) + { + var textureArray = new ThreadedTextureArray(this); + New().Set(Ref(textureArray), size, isBuffer); + QueueCommand(); + + return textureArray; + } public void DeleteBuffer(BufferHandle buffer) { diff --git a/src/Ryujinx.Graphics.GAL/Multithreading/ThreadedWindow.cs b/src/Ryujinx.Graphics.GAL/Multithreading/ThreadedWindow.cs index acda37ef3..102fdb1bb 100644 --- a/src/Ryujinx.Graphics.GAL/Multithreading/ThreadedWindow.cs +++ b/src/Ryujinx.Graphics.GAL/Multithreading/ThreadedWindow.cs @@ -31,7 +31,7 @@ namespace Ryujinx.Graphics.GAL.Multithreading _impl.Window.SetSize(width, height); } - public void ChangeVSyncMode(bool vsyncEnabled) { } + public void ChangeVSyncMode(VSyncMode vSyncMode) { } public void SetAntiAliasing(AntiAliasing effect) { } diff --git a/src/Ryujinx.Graphics.GAL/ResourceLayout.cs b/src/Ryujinx.Graphics.GAL/ResourceLayout.cs index 84bca5b41..b7464ee12 100644 --- a/src/Ryujinx.Graphics.GAL/ResourceLayout.cs +++ b/src/Ryujinx.Graphics.GAL/ResourceLayout.cs @@ -71,19 +71,23 @@ namespace Ryujinx.Graphics.GAL public readonly struct ResourceUsage : IEquatable { public int Binding { get; } + public int ArrayLength { get; } public ResourceType Type { get; } public ResourceStages Stages { get; } + public bool Write { get; } - public ResourceUsage(int binding, ResourceType type, ResourceStages stages) + public ResourceUsage(int binding, int arrayLength, ResourceType type, ResourceStages stages, bool write) { Binding = binding; + ArrayLength = arrayLength; Type = type; Stages = stages; + Write = write; } public override int GetHashCode() { - return HashCode.Combine(Binding, Type, Stages); + return HashCode.Combine(Binding, ArrayLength, Type, Stages); } public override bool Equals(object obj) @@ -93,7 +97,7 @@ namespace Ryujinx.Graphics.GAL public bool Equals(ResourceUsage other) { - return Binding == other.Binding && Type == other.Type && Stages == other.Stages; + return Binding == other.Binding && ArrayLength == other.ArrayLength && Type == other.Type && Stages == other.Stages; } public static bool operator ==(ResourceUsage left, ResourceUsage right) diff --git a/src/Ryujinx.Graphics.GAL/SystemMemoryType.cs b/src/Ryujinx.Graphics.GAL/SystemMemoryType.cs new file mode 100644 index 000000000..532921298 --- /dev/null +++ b/src/Ryujinx.Graphics.GAL/SystemMemoryType.cs @@ -0,0 +1,29 @@ +namespace Ryujinx.Graphics.GAL +{ + public enum SystemMemoryType + { + /// + /// The backend manages the ownership of memory. This mode never supports host imported memory. + /// + BackendManaged, + + /// + /// Device memory has similar performance to host memory, usually because it's shared between CPU/GPU. + /// Use host memory whenever possible. + /// + UnifiedMemory, + + /// + /// GPU storage to host memory goes though a slow interconnect, but it would still be preferable to use it if the data is flushed back often. + /// Assumes constant buffer access to host memory is rather fast. + /// + DedicatedMemory, + + /// + /// GPU storage to host memory goes though a slow interconnect, that is very slow when doing access from storage. + /// When frequently accessed, copy buffers to host memory using DMA. + /// Assumes constant buffer access to host memory is rather fast. + /// + DedicatedMemorySlowStorage + } +} diff --git a/src/Ryujinx.Graphics.GAL/TextureCreateInfo.cs b/src/Ryujinx.Graphics.GAL/TextureCreateInfo.cs index 44090291d..79c84db01 100644 --- a/src/Ryujinx.Graphics.GAL/TextureCreateInfo.cs +++ b/src/Ryujinx.Graphics.GAL/TextureCreateInfo.cs @@ -1,6 +1,5 @@ using Ryujinx.Common; using System; -using System.Numerics; namespace Ryujinx.Graphics.GAL { @@ -113,25 +112,6 @@ namespace Ryujinx.Graphics.GAL return 1; } - public int GetLevelsClamped() - { - int maxSize = Width; - - if (Target != Target.Texture1D && - Target != Target.Texture1DArray) - { - maxSize = Math.Max(maxSize, Height); - } - - if (Target == Target.Texture3D) - { - maxSize = Math.Max(maxSize, Depth); - } - - int maxLevels = BitOperations.Log2((uint)maxSize) + 1; - return Math.Min(Levels, maxLevels); - } - private static int GetLevelSize(int size, int level) { return Math.Max(1, size >> level); diff --git a/src/Ryujinx.Graphics.GAL/UpscaleType.cs b/src/Ryujinx.Graphics.GAL/UpscaleType.cs index ca24199c4..e2482faef 100644 --- a/src/Ryujinx.Graphics.GAL/UpscaleType.cs +++ b/src/Ryujinx.Graphics.GAL/UpscaleType.cs @@ -5,5 +5,6 @@ namespace Ryujinx.Graphics.GAL Bilinear, Nearest, Fsr, + Area, } } diff --git a/src/Ryujinx.Graphics.GAL/VSyncMode.cs b/src/Ryujinx.Graphics.GAL/VSyncMode.cs new file mode 100644 index 000000000..c5794b8f7 --- /dev/null +++ b/src/Ryujinx.Graphics.GAL/VSyncMode.cs @@ -0,0 +1,9 @@ +namespace Ryujinx.Graphics.GAL +{ + public enum VSyncMode + { + Switch, + Unbounded, + Custom + } +} diff --git a/src/Ryujinx.Graphics.Gpu/Constants.cs b/src/Ryujinx.Graphics.Gpu/Constants.cs index c553d988e..23b9be5ca 100644 --- a/src/Ryujinx.Graphics.Gpu/Constants.cs +++ b/src/Ryujinx.Graphics.Gpu/Constants.cs @@ -89,5 +89,10 @@ namespace Ryujinx.Graphics.Gpu /// Maximum size that an storage buffer is assumed to have when the correct size is unknown. /// public const ulong MaxUnknownStorageSize = 0x100000; + + /// + /// Size of a bindless texture handle as exposed by guest graphics APIs. + /// + public const int TextureHandleSizeInBytes = sizeof(ulong); } } diff --git a/src/Ryujinx.Graphics.Gpu/Engine/Compute/ComputeClass.cs b/src/Ryujinx.Graphics.Gpu/Engine/Compute/ComputeClass.cs index ccdbe4748..cd8144724 100644 --- a/src/Ryujinx.Graphics.Gpu/Engine/Compute/ComputeClass.cs +++ b/src/Ryujinx.Graphics.Gpu/Engine/Compute/ComputeClass.cs @@ -126,6 +126,8 @@ namespace Ryujinx.Graphics.Gpu.Engine.Compute ulong samplerPoolGpuVa = ((ulong)_state.State.SetTexSamplerPoolAOffsetUpper << 32) | _state.State.SetTexSamplerPoolB; ulong texturePoolGpuVa = ((ulong)_state.State.SetTexHeaderPoolAOffsetUpper << 32) | _state.State.SetTexHeaderPoolB; + int samplerPoolMaximumId = _state.State.SetTexSamplerPoolCMaximumIndex; + GpuChannelPoolState poolState = new( texturePoolGpuVa, _state.State.SetTexHeaderPoolCMaximumIndex, @@ -139,7 +141,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Compute sharedMemorySize, _channel.BufferManager.HasUnalignedStorageBuffers); - CachedShaderProgram cs = memoryManager.Physical.ShaderCache.GetComputeShader(_channel, poolState, computeState, shaderGpuVa); + CachedShaderProgram cs = memoryManager.Physical.ShaderCache.GetComputeShader(_channel, samplerPoolMaximumId, poolState, computeState, shaderGpuVa); _context.Renderer.Pipeline.SetProgram(cs.HostProgram); @@ -184,7 +186,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Compute sharedMemorySize, _channel.BufferManager.HasUnalignedStorageBuffers); - cs = memoryManager.Physical.ShaderCache.GetComputeShader(_channel, poolState, computeState, shaderGpuVa); + cs = memoryManager.Physical.ShaderCache.GetComputeShader(_channel, samplerPoolMaximumId, poolState, computeState, shaderGpuVa); _context.Renderer.Pipeline.SetProgram(cs.HostProgram); } diff --git a/src/Ryujinx.Graphics.Gpu/Engine/Dma/DmaClass.cs b/src/Ryujinx.Graphics.Gpu/Engine/Dma/DmaClass.cs index 0fc10ff49..cdeae0040 100644 --- a/src/Ryujinx.Graphics.Gpu/Engine/Dma/DmaClass.cs +++ b/src/Ryujinx.Graphics.Gpu/Engine/Dma/DmaClass.cs @@ -1,10 +1,10 @@ using Ryujinx.Common; +using Ryujinx.Common.Memory; using Ryujinx.Graphics.Device; using Ryujinx.Graphics.Gpu.Engine.Threed; using Ryujinx.Graphics.Gpu.Memory; using Ryujinx.Graphics.Texture; using System; -using System.Buffers; using System.Collections.Generic; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -276,16 +276,68 @@ namespace Ryujinx.Graphics.Gpu.Engine.Dma dstBaseOffset += dstStride * (yCount - 1); } - ReadOnlySpan srcSpan = memoryManager.GetSpan(srcGpuVa + (ulong)srcBaseOffset, srcSize, true); + // If remapping is disabled, we always copy the components directly, in order. + // If it's enabled, but the mapping is just XYZW, we also copy them in order. + bool isIdentityRemap = !remap || + (_state.State.SetRemapComponentsDstX == SetRemapComponentsDst.SrcX && + (dstComponents < 2 || _state.State.SetRemapComponentsDstY == SetRemapComponentsDst.SrcY) && + (dstComponents < 3 || _state.State.SetRemapComponentsDstZ == SetRemapComponentsDst.SrcZ) && + (dstComponents < 4 || _state.State.SetRemapComponentsDstW == SetRemapComponentsDst.SrcW)); bool completeSource = IsTextureCopyComplete(src, srcLinear, srcBpp, srcStride, xCount, yCount); bool completeDest = IsTextureCopyComplete(dst, dstLinear, dstBpp, dstStride, xCount, yCount); + // Check if the source texture exists on the GPU, if it does, do a GPU side copy. + // Otherwise, we would need to flush the source texture which is costly. + // We don't expect the source to be linear in such cases, as linear source usually indicates buffer or CPU written data. + + if (completeSource && completeDest && !srcLinear && isIdentityRemap) + { + var source = memoryManager.Physical.TextureCache.FindTexture( + memoryManager, + srcGpuVa, + srcBpp, + srcStride, + src.Height, + xCount, + yCount, + srcLinear, + src.MemoryLayout.UnpackGobBlocksInY(), + src.MemoryLayout.UnpackGobBlocksInZ()); + + if (source != null && source.Height == yCount) + { + source.SynchronizeMemory(); + + var target = memoryManager.Physical.TextureCache.FindOrCreateTexture( + memoryManager, + source.Info.FormatInfo, + dstGpuVa, + xCount, + yCount, + dstStride, + dstLinear, + dst.MemoryLayout.UnpackGobBlocksInY(), + dst.MemoryLayout.UnpackGobBlocksInZ()); + + if (source.ScaleFactor != target.ScaleFactor) + { + target.PropagateScale(source); + } + + source.HostTexture.CopyTo(target.HostTexture, 0, 0); + target.SignalModified(); + return; + } + } + + ReadOnlySpan srcSpan = memoryManager.GetSpan(srcGpuVa + (ulong)srcBaseOffset, srcSize, true); + // Try to set the texture data directly, // but only if we are doing a complete copy, // and not for block linear to linear copies, since those are typically accessed from the CPU. - if (completeSource && completeDest && !(dstLinear && !srcLinear)) + if (completeSource && completeDest && !(dstLinear && !srcLinear) && isIdentityRemap) { var target = memoryManager.Physical.TextureCache.FindTexture( memoryManager, @@ -301,7 +353,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Dma if (target != null) { - IMemoryOwner data; + MemoryOwner data; if (srcLinear) { data = LayoutConverter.ConvertLinearStridedToLinear( @@ -354,14 +406,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Dma TextureParams srcParams = new(srcRegionX, srcRegionY, srcBaseOffset, srcBpp, srcLinear, srcCalculator); TextureParams dstParams = new(dstRegionX, dstRegionY, dstBaseOffset, dstBpp, dstLinear, dstCalculator); - // If remapping is enabled, we always copy the components directly, in order. - // If it's enabled, but the mapping is just XYZW, we also copy them in order. - bool isIdentityRemap = !remap || - (_state.State.SetRemapComponentsDstX == SetRemapComponentsDst.SrcX && - (dstComponents < 2 || _state.State.SetRemapComponentsDstY == SetRemapComponentsDst.SrcY) && - (dstComponents < 3 || _state.State.SetRemapComponentsDstZ == SetRemapComponentsDst.SrcZ) && - (dstComponents < 4 || _state.State.SetRemapComponentsDstW == SetRemapComponentsDst.SrcW)); - if (isIdentityRemap) { // The order of the components doesn't change, so we can just copy directly diff --git a/src/Ryujinx.Graphics.Gpu/Engine/GPFifo/GPFifoClass.cs b/src/Ryujinx.Graphics.Gpu/Engine/GPFifo/GPFifoClass.cs index 5bd8ec728..cedd824a1 100644 --- a/src/Ryujinx.Graphics.Gpu/Engine/GPFifo/GPFifoClass.cs +++ b/src/Ryujinx.Graphics.Gpu/Engine/GPFifo/GPFifoClass.cs @@ -157,6 +157,9 @@ namespace Ryujinx.Graphics.Gpu.Engine.GPFifo } else if (operation == SyncpointbOperation.Incr) { + // "Unbind" render targets since a syncpoint increment might indicate future CPU access for the textures. + _parent.TextureManager.RefreshModifiedTextures(); + _context.CreateHostSyncIfNeeded(HostSyncFlags.StrictSyncpoint); _context.Synchronization.IncrementSyncpoint(syncpointId); } diff --git a/src/Ryujinx.Graphics.Gpu/Engine/GPFifo/GPFifoProcessor.cs b/src/Ryujinx.Graphics.Gpu/Engine/GPFifo/GPFifoProcessor.cs index b57109c7d..984a9cff8 100644 --- a/src/Ryujinx.Graphics.Gpu/Engine/GPFifo/GPFifoProcessor.cs +++ b/src/Ryujinx.Graphics.Gpu/Engine/GPFifo/GPFifoProcessor.cs @@ -4,6 +4,7 @@ using Ryujinx.Graphics.Gpu.Engine.Dma; using Ryujinx.Graphics.Gpu.Engine.InlineToMemory; using Ryujinx.Graphics.Gpu.Engine.Threed; using Ryujinx.Graphics.Gpu.Engine.Twod; +using Ryujinx.Graphics.Gpu.Image; using Ryujinx.Graphics.Gpu.Memory; using System; using System.Runtime.CompilerServices; @@ -28,6 +29,11 @@ namespace Ryujinx.Graphics.Gpu.Engine.GPFifo /// public MemoryManager MemoryManager => _channel.MemoryManager; + /// + /// Channel texture manager. + /// + public TextureManager TextureManager => _channel.TextureManager; + /// /// 3D Engine. /// diff --git a/src/Ryujinx.Graphics.Gpu/Engine/InlineToMemory/InlineToMemoryClass.cs b/src/Ryujinx.Graphics.Gpu/Engine/InlineToMemory/InlineToMemoryClass.cs index 576ef3266..78099f74a 100644 --- a/src/Ryujinx.Graphics.Gpu/Engine/InlineToMemory/InlineToMemoryClass.cs +++ b/src/Ryujinx.Graphics.Gpu/Engine/InlineToMemory/InlineToMemoryClass.cs @@ -198,11 +198,9 @@ namespace Ryujinx.Graphics.Gpu.Engine.InlineToMemory if (target != null) { - var memory = ByteMemoryPool.Rent(data.Length); - data.CopyTo(memory.Memory.Span); - target.SynchronizeMemory(); - target.SetData(memory, 0, 0, new GAL.Rectangle(_dstX, _dstY, _lineLengthIn / target.Info.FormatInfo.BytesPerPixel, _lineCount)); + var dataCopy = MemoryOwner.RentCopy(data); + target.SetData(dataCopy, 0, 0, new GAL.Rectangle(_dstX, _dstY, _lineLengthIn / target.Info.FormatInfo.BytesPerPixel, _lineCount)); target.SignalModified(); return; diff --git a/src/Ryujinx.Graphics.Gpu/Engine/MME/MacroHLE.cs b/src/Ryujinx.Graphics.Gpu/Engine/MME/MacroHLE.cs index 7f3772f44..475d1ee4e 100644 --- a/src/Ryujinx.Graphics.Gpu/Engine/MME/MacroHLE.cs +++ b/src/Ryujinx.Graphics.Gpu/Engine/MME/MacroHLE.cs @@ -5,6 +5,7 @@ using Ryujinx.Graphics.GAL; using Ryujinx.Graphics.Gpu.Engine.GPFifo; using Ryujinx.Graphics.Gpu.Engine.Threed; using Ryujinx.Graphics.Gpu.Engine.Types; +using Ryujinx.Graphics.Gpu.Memory; using Ryujinx.Memory.Range; using System; using System.Collections.Generic; @@ -495,8 +496,8 @@ namespace Ryujinx.Graphics.Gpu.Engine.MME ulong indirectBufferSize = (ulong)maxDrawCount * (ulong)stride; - MultiRange indirectBufferRange = bufferCache.TranslateAndCreateMultiBuffers(_processor.MemoryManager, indirectBufferGpuVa, indirectBufferSize); - MultiRange parameterBufferRange = bufferCache.TranslateAndCreateMultiBuffers(_processor.MemoryManager, parameterBufferGpuVa, 4); + MultiRange indirectBufferRange = bufferCache.TranslateAndCreateMultiBuffers(_processor.MemoryManager, indirectBufferGpuVa, indirectBufferSize, BufferStage.Indirect); + MultiRange parameterBufferRange = bufferCache.TranslateAndCreateMultiBuffers(_processor.MemoryManager, parameterBufferGpuVa, 4, BufferStage.Indirect); _processor.ThreedClass.DrawIndirect( topology, diff --git a/src/Ryujinx.Graphics.Gpu/Engine/MME/MacroHLETable.cs b/src/Ryujinx.Graphics.Gpu/Engine/MME/MacroHLETable.cs index 43b701293..e3080228e 100644 --- a/src/Ryujinx.Graphics.Gpu/Engine/MME/MacroHLETable.cs +++ b/src/Ryujinx.Graphics.Gpu/Engine/MME/MacroHLETable.cs @@ -96,7 +96,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.MME { ref var entry = ref _table[i]; - var hash = XXHash128.ComputeHash(mc[..entry.Length]); + var hash = Hash128.ComputeHash(mc[..entry.Length]); if (hash == entry.Hash) { if (IsMacroHLESupported(caps, entry.Name)) diff --git a/src/Ryujinx.Graphics.Gpu/Engine/ShaderTexture.cs b/src/Ryujinx.Graphics.Gpu/Engine/ShaderTexture.cs index 492c6ee60..bdb34180e 100644 --- a/src/Ryujinx.Graphics.Gpu/Engine/ShaderTexture.cs +++ b/src/Ryujinx.Graphics.Gpu/Engine/ShaderTexture.cs @@ -1,5 +1,6 @@ using Ryujinx.Common.Logging; using Ryujinx.Graphics.GAL; +using Ryujinx.Graphics.Gpu.Image; using Ryujinx.Graphics.Shader; namespace Ryujinx.Graphics.Gpu.Engine @@ -16,7 +17,7 @@ namespace Ryujinx.Graphics.Gpu.Engine /// Texture target value public static Target GetTarget(SamplerType type) { - type &= ~(SamplerType.Indexed | SamplerType.Shadow); + type &= ~SamplerType.Shadow; switch (type) { @@ -61,51 +62,51 @@ namespace Ryujinx.Graphics.Gpu.Engine /// /// Shader image format /// Texture format - public static Format GetFormat(TextureFormat format) + public static FormatInfo GetFormatInfo(TextureFormat format) { return format switch { #pragma warning disable IDE0055 // Disable formatting - TextureFormat.R8Unorm => Format.R8Unorm, - TextureFormat.R8Snorm => Format.R8Snorm, - TextureFormat.R8Uint => Format.R8Uint, - TextureFormat.R8Sint => Format.R8Sint, - TextureFormat.R16Float => Format.R16Float, - TextureFormat.R16Unorm => Format.R16Unorm, - TextureFormat.R16Snorm => Format.R16Snorm, - TextureFormat.R16Uint => Format.R16Uint, - TextureFormat.R16Sint => Format.R16Sint, - TextureFormat.R32Float => Format.R32Float, - TextureFormat.R32Uint => Format.R32Uint, - TextureFormat.R32Sint => Format.R32Sint, - TextureFormat.R8G8Unorm => Format.R8G8Unorm, - TextureFormat.R8G8Snorm => Format.R8G8Snorm, - TextureFormat.R8G8Uint => Format.R8G8Uint, - TextureFormat.R8G8Sint => Format.R8G8Sint, - TextureFormat.R16G16Float => Format.R16G16Float, - TextureFormat.R16G16Unorm => Format.R16G16Unorm, - TextureFormat.R16G16Snorm => Format.R16G16Snorm, - TextureFormat.R16G16Uint => Format.R16G16Uint, - TextureFormat.R16G16Sint => Format.R16G16Sint, - TextureFormat.R32G32Float => Format.R32G32Float, - TextureFormat.R32G32Uint => Format.R32G32Uint, - TextureFormat.R32G32Sint => Format.R32G32Sint, - TextureFormat.R8G8B8A8Unorm => Format.R8G8B8A8Unorm, - TextureFormat.R8G8B8A8Snorm => Format.R8G8B8A8Snorm, - TextureFormat.R8G8B8A8Uint => Format.R8G8B8A8Uint, - TextureFormat.R8G8B8A8Sint => Format.R8G8B8A8Sint, - TextureFormat.R16G16B16A16Float => Format.R16G16B16A16Float, - TextureFormat.R16G16B16A16Unorm => Format.R16G16B16A16Unorm, - TextureFormat.R16G16B16A16Snorm => Format.R16G16B16A16Snorm, - TextureFormat.R16G16B16A16Uint => Format.R16G16B16A16Uint, - TextureFormat.R16G16B16A16Sint => Format.R16G16B16A16Sint, - TextureFormat.R32G32B32A32Float => Format.R32G32B32A32Float, - TextureFormat.R32G32B32A32Uint => Format.R32G32B32A32Uint, - TextureFormat.R32G32B32A32Sint => Format.R32G32B32A32Sint, - TextureFormat.R10G10B10A2Unorm => Format.R10G10B10A2Unorm, - TextureFormat.R10G10B10A2Uint => Format.R10G10B10A2Uint, - TextureFormat.R11G11B10Float => Format.R11G11B10Float, - _ => 0, + TextureFormat.R8Unorm => new(Format.R8Unorm, 1, 1, 1, 1), + TextureFormat.R8Snorm => new(Format.R8Snorm, 1, 1, 1, 1), + TextureFormat.R8Uint => new(Format.R8Uint, 1, 1, 1, 1), + TextureFormat.R8Sint => new(Format.R8Sint, 1, 1, 1, 1), + TextureFormat.R16Float => new(Format.R16Float, 1, 1, 2, 1), + TextureFormat.R16Unorm => new(Format.R16Unorm, 1, 1, 2, 1), + TextureFormat.R16Snorm => new(Format.R16Snorm, 1, 1, 2, 1), + TextureFormat.R16Uint => new(Format.R16Uint, 1, 1, 2, 1), + TextureFormat.R16Sint => new(Format.R16Sint, 1, 1, 2, 1), + TextureFormat.R32Float => new(Format.R32Float, 1, 1, 4, 1), + TextureFormat.R32Uint => new(Format.R32Uint, 1, 1, 4, 1), + TextureFormat.R32Sint => new(Format.R32Sint, 1, 1, 4, 1), + TextureFormat.R8G8Unorm => new(Format.R8G8Unorm, 1, 1, 2, 2), + TextureFormat.R8G8Snorm => new(Format.R8G8Snorm, 1, 1, 2, 2), + TextureFormat.R8G8Uint => new(Format.R8G8Uint, 1, 1, 2, 2), + TextureFormat.R8G8Sint => new(Format.R8G8Sint, 1, 1, 2, 2), + TextureFormat.R16G16Float => new(Format.R16G16Float, 1, 1, 4, 2), + TextureFormat.R16G16Unorm => new(Format.R16G16Unorm, 1, 1, 4, 2), + TextureFormat.R16G16Snorm => new(Format.R16G16Snorm, 1, 1, 4, 2), + TextureFormat.R16G16Uint => new(Format.R16G16Uint, 1, 1, 4, 2), + TextureFormat.R16G16Sint => new(Format.R16G16Sint, 1, 1, 4, 2), + TextureFormat.R32G32Float => new(Format.R32G32Float, 1, 1, 8, 2), + TextureFormat.R32G32Uint => new(Format.R32G32Uint, 1, 1, 8, 2), + TextureFormat.R32G32Sint => new(Format.R32G32Sint, 1, 1, 8, 2), + TextureFormat.R8G8B8A8Unorm => new(Format.R8G8B8A8Unorm, 1, 1, 4, 4), + TextureFormat.R8G8B8A8Snorm => new(Format.R8G8B8A8Snorm, 1, 1, 4, 4), + TextureFormat.R8G8B8A8Uint => new(Format.R8G8B8A8Uint, 1, 1, 4, 4), + TextureFormat.R8G8B8A8Sint => new(Format.R8G8B8A8Sint, 1, 1, 4, 4), + TextureFormat.R16G16B16A16Float => new(Format.R16G16B16A16Float, 1, 1, 8, 4), + TextureFormat.R16G16B16A16Unorm => new(Format.R16G16B16A16Unorm, 1, 1, 8, 4), + TextureFormat.R16G16B16A16Snorm => new(Format.R16G16B16A16Snorm, 1, 1, 8, 4), + TextureFormat.R16G16B16A16Uint => new(Format.R16G16B16A16Uint, 1, 1, 8, 4), + TextureFormat.R16G16B16A16Sint => new(Format.R16G16B16A16Sint, 1, 1, 8, 4), + TextureFormat.R32G32B32A32Float => new(Format.R32G32B32A32Float, 1, 1, 16, 4), + TextureFormat.R32G32B32A32Uint => new(Format.R32G32B32A32Uint, 1, 1, 16, 4), + TextureFormat.R32G32B32A32Sint => new(Format.R32G32B32A32Sint, 1, 1, 16, 4), + TextureFormat.R10G10B10A2Unorm => new(Format.R10G10B10A2Unorm, 1, 1, 4, 4), + TextureFormat.R10G10B10A2Uint => new(Format.R10G10B10A2Uint, 1, 1, 4, 4), + TextureFormat.R11G11B10Float => new(Format.R11G11B10Float, 1, 1, 4, 3), + _ => FormatInfo.Invalid, #pragma warning restore IDE0055 }; } diff --git a/src/Ryujinx.Graphics.Gpu/Engine/Threed/Blender/AdvancedBlendFunctions.cs b/src/Ryujinx.Graphics.Gpu/Engine/Threed/Blender/AdvancedBlendFunctions.cs index 0aca39075..13e5d2a86 100644 --- a/src/Ryujinx.Graphics.Gpu/Engine/Threed/Blender/AdvancedBlendFunctions.cs +++ b/src/Ryujinx.Graphics.Gpu/Engine/Threed/Blender/AdvancedBlendFunctions.cs @@ -223,7 +223,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed.Blender foreach (var entry in Table) { - Hash128 hash = XXHash128.ComputeHash(MemoryMarshal.Cast(entry.Code)); + Hash128 hash = Hash128.ComputeHash(MemoryMarshal.Cast(entry.Code)); string[] constants = new string[entry.Constants != null ? entry.Constants.Length : 0]; diff --git a/src/Ryujinx.Graphics.Gpu/Engine/Threed/Blender/AdvancedBlendManager.cs b/src/Ryujinx.Graphics.Gpu/Engine/Threed/Blender/AdvancedBlendManager.cs index b336382d4..ce3d2c236 100644 --- a/src/Ryujinx.Graphics.Gpu/Engine/Threed/Blender/AdvancedBlendManager.cs +++ b/src/Ryujinx.Graphics.Gpu/Engine/Threed/Blender/AdvancedBlendManager.cs @@ -62,7 +62,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed.Blender currentCode = currentCode[..codeLength]; } - Hash128 hash = XXHash128.ComputeHash(MemoryMarshal.Cast(currentCode)); + Hash128 hash = Hash128.ComputeHash(MemoryMarshal.Cast(currentCode)); descriptor = default; diff --git a/src/Ryujinx.Graphics.Gpu/Engine/Threed/ComputeDraw/VtgAsComputeContext.cs b/src/Ryujinx.Graphics.Gpu/Engine/Threed/ComputeDraw/VtgAsComputeContext.cs index f9cb40b0d..6de50fb2e 100644 --- a/src/Ryujinx.Graphics.Gpu/Engine/Threed/ComputeDraw/VtgAsComputeContext.cs +++ b/src/Ryujinx.Graphics.Gpu/Engine/Threed/ComputeDraw/VtgAsComputeContext.cs @@ -438,7 +438,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed.ComputeDraw ReadOnlySpan dataBytes = MemoryMarshal.Cast(data); - BufferHandle buffer = _context.Renderer.CreateBuffer(dataBytes.Length); + BufferHandle buffer = _context.Renderer.CreateBuffer(dataBytes.Length, BufferAccess.DeviceMemory); _context.Renderer.SetBufferData(buffer, 0, dataBytes); return new IndexBuffer(buffer, count, dataBytes.Length); @@ -529,7 +529,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed.ComputeDraw { if (_dummyBuffer == BufferHandle.Null) { - _dummyBuffer = _context.Renderer.CreateBuffer(DummyBufferSize); + _dummyBuffer = _context.Renderer.CreateBuffer(DummyBufferSize, BufferAccess.DeviceMemory); _context.Renderer.Pipeline.ClearBuffer(_dummyBuffer, 0, DummyBufferSize, 0); } @@ -550,7 +550,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed.ComputeDraw _context.Renderer.DeleteBuffer(_sequentialIndexBuffer); } - _sequentialIndexBuffer = _context.Renderer.CreateBuffer(count * sizeof(uint)); + _sequentialIndexBuffer = _context.Renderer.CreateBuffer(count * sizeof(uint), BufferAccess.DeviceMemory); _sequentialIndexBufferCount = count; Span data = new int[count]; @@ -583,7 +583,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed.ComputeDraw _context.Renderer.DeleteBuffer(buffer.Handle); } - buffer.Handle = _context.Renderer.CreateBuffer(newSize); + buffer.Handle = _context.Renderer.CreateBuffer(newSize, BufferAccess.DeviceMemory); buffer.Size = newSize; } diff --git a/src/Ryujinx.Graphics.Gpu/Engine/Threed/ComputeDraw/VtgAsComputeState.cs b/src/Ryujinx.Graphics.Gpu/Engine/Threed/ComputeDraw/VtgAsComputeState.cs index 6324e6a15..73682866b 100644 --- a/src/Ryujinx.Graphics.Gpu/Engine/Threed/ComputeDraw/VtgAsComputeState.cs +++ b/src/Ryujinx.Graphics.Gpu/Engine/Threed/ComputeDraw/VtgAsComputeState.cs @@ -3,6 +3,7 @@ using Ryujinx.Common.Logging; using Ryujinx.Graphics.GAL; using Ryujinx.Graphics.Gpu.Engine.Types; using Ryujinx.Graphics.Gpu.Image; +using Ryujinx.Graphics.Gpu.Memory; using Ryujinx.Graphics.Gpu.Shader; using Ryujinx.Graphics.Shader; using Ryujinx.Graphics.Shader.Translation; @@ -370,7 +371,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed.ComputeDraw { var memoryManager = _channel.MemoryManager; - BufferRange range = memoryManager.Physical.BufferCache.GetBufferRange(memoryManager.GetPhysicalRegions(address, size)); + BufferRange range = memoryManager.Physical.BufferCache.GetBufferRange(memoryManager.GetPhysicalRegions(address, size), BufferStage.VertexBuffer); ITexture bufferTexture = _vacContext.EnsureBufferTexture(index + 2, format); bufferTexture.SetStorage(range); @@ -412,7 +413,9 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed.ComputeDraw var memoryManager = _channel.MemoryManager; ulong misalign = address & ((ulong)_context.Capabilities.TextureBufferOffsetAlignment - 1); - BufferRange range = memoryManager.Physical.BufferCache.GetBufferRange(memoryManager.GetPhysicalRegions(address + indexOffset - misalign, size + misalign)); + BufferRange range = memoryManager.Physical.BufferCache.GetBufferRange( + memoryManager.GetPhysicalRegions(address + indexOffset - misalign, size + misalign), + BufferStage.IndexBuffer); misalignedOffset = (int)misalign >> shift; SetIndexBufferTexture(reservations, range, format); diff --git a/src/Ryujinx.Graphics.Gpu/Engine/Threed/DrawManager.cs b/src/Ryujinx.Graphics.Gpu/Engine/Threed/DrawManager.cs index 8c72663f1..56ef64c6e 100644 --- a/src/Ryujinx.Graphics.Gpu/Engine/Threed/DrawManager.cs +++ b/src/Ryujinx.Graphics.Gpu/Engine/Threed/DrawManager.cs @@ -103,6 +103,8 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed /// Method call argument public void DrawEnd(ThreedClass engine, int argument) { + _drawState.DrawUsesEngineState = true; + DrawEnd( engine, _state.State.IndexBufferState.First, @@ -205,10 +207,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed } else { -#pragma warning disable IDE0059 // Remove unnecessary value assignment - var drawState = _state.State.VertexBufferDrawState; -#pragma warning restore IDE0059 - DrawImpl(engine, drawVertexCount, 1, 0, drawFirstVertex, firstInstance, indexed: false); } @@ -379,6 +377,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed bool oldDrawIndexed = _drawState.DrawIndexed; _drawState.DrawIndexed = true; + _drawState.DrawUsesEngineState = false; engine.ForceStateDirty(IndexBufferCountMethodOffset * 4); DrawEnd(engine, firstIndex, indexCount, 0, 0); @@ -424,6 +423,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed bool oldDrawIndexed = _drawState.DrawIndexed; _drawState.DrawIndexed = false; + _drawState.DrawUsesEngineState = false; engine.ForceStateDirty(VertexBufferFirstMethodOffset * 4); DrawEnd(engine, 0, 0, firstVertex, vertexCount); @@ -544,6 +544,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed _state.State.FirstInstance = (uint)firstInstance; _drawState.DrawIndexed = indexed; + _drawState.DrawUsesEngineState = true; _currentSpecState.SetHasConstantBufferDrawParameters(true); engine.UpdateState(); @@ -676,14 +677,15 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed _drawState.DrawIndexed = indexed; _drawState.DrawIndirect = true; + _drawState.DrawUsesEngineState = true; _currentSpecState.SetHasConstantBufferDrawParameters(true); engine.UpdateState(); if (hasCount) { - var indirectBuffer = memory.BufferCache.GetBufferRange(indirectBufferRange); - var parameterBuffer = memory.BufferCache.GetBufferRange(parameterBufferRange); + var indirectBuffer = memory.BufferCache.GetBufferRange(indirectBufferRange, BufferStage.Indirect); + var parameterBuffer = memory.BufferCache.GetBufferRange(parameterBufferRange, BufferStage.Indirect); if (indexed) { @@ -696,7 +698,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed } else { - var indirectBuffer = memory.BufferCache.GetBufferRange(indirectBufferRange); + var indirectBuffer = memory.BufferCache.GetBufferRange(indirectBufferRange, BufferStage.Indirect); if (indexed) { diff --git a/src/Ryujinx.Graphics.Gpu/Engine/Threed/DrawState.cs b/src/Ryujinx.Graphics.Gpu/Engine/Threed/DrawState.cs index 8113c1827..03b5e3f3b 100644 --- a/src/Ryujinx.Graphics.Gpu/Engine/Threed/DrawState.cs +++ b/src/Ryujinx.Graphics.Gpu/Engine/Threed/DrawState.cs @@ -38,6 +38,11 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed /// public bool DrawIndirect; + /// + /// Indicates that the draw is using the draw parameters on the 3D engine state, rather than inline parameters submitted with the draw command. + /// + public bool DrawUsesEngineState; + /// /// Indicates if any of the currently used vertex shaders reads the instance ID. /// @@ -48,11 +53,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed /// public bool IsAnyVbInstanced; - /// - /// Indicates that the draw is writing the base vertex, base instance and draw index to Constant Buffer 0. - /// - public bool HasConstantBufferDrawParameters; - /// /// Primitive topology for the next draw. /// diff --git a/src/Ryujinx.Graphics.Gpu/Engine/Threed/StateUpdateTracker.cs b/src/Ryujinx.Graphics.Gpu/Engine/Threed/StateUpdateTracker.cs index c8c44d8e5..ea9fc9e31 100644 --- a/src/Ryujinx.Graphics.Gpu/Engine/Threed/StateUpdateTracker.cs +++ b/src/Ryujinx.Graphics.Gpu/Engine/Threed/StateUpdateTracker.cs @@ -79,10 +79,10 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed { var field = fields[fieldIndex]; - var cuurentFieldOffset = (int)Marshal.OffsetOf(field.Name); + var currentFieldOffset = (int)Marshal.OffsetOf(field.Name); var nextFieldOffset = fieldIndex + 1 == fields.Length ? Unsafe.SizeOf() : (int)Marshal.OffsetOf(fields[fieldIndex + 1].Name); - int sizeOfField = nextFieldOffset - cuurentFieldOffset; + int sizeOfField = nextFieldOffset - currentFieldOffset; if (fieldToDelegate.TryGetValue(field.Name, out int entryIndex)) { @@ -109,7 +109,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed if (index < BlockSize) { - int groupIndex = Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(_registerToGroupMapping), (IntPtr)index); + int groupIndex = Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(_registerToGroupMapping), (nint)index); if (groupIndex != 0) { groupIndex--; diff --git a/src/Ryujinx.Graphics.Gpu/Engine/Threed/StateUpdater.cs b/src/Ryujinx.Graphics.Gpu/Engine/Threed/StateUpdater.cs index 24a4d58c7..1dc77b52d 100644 --- a/src/Ryujinx.Graphics.Gpu/Engine/Threed/StateUpdater.cs +++ b/src/Ryujinx.Graphics.Gpu/Engine/Threed/StateUpdater.cs @@ -26,6 +26,9 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed public const int PrimitiveRestartStateIndex = 12; public const int RenderTargetStateIndex = 27; + // Vertex buffers larger than this size will be clamped to the mapped size. + private const ulong VertexBufferSizeToMappedSizeThreshold = 256 * 1024 * 1024; // 256 MB + private readonly GpuContext _context; private readonly GpuChannel _channel; private readonly DeviceStateWithShadow _state; @@ -47,7 +50,8 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed private uint _vbEnableMask; private bool _prevDrawIndexed; - private readonly bool _prevDrawIndirect; + private bool _prevDrawIndirect; + private bool _prevDrawUsesEngineState; private IndexType _prevIndexType; private uint _prevFirstVertex; private bool _prevTfEnable; @@ -236,7 +240,9 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed // method when doing indexed draws, so we need to make sure // to update the vertex buffers if we are doing a regular // draw after a indexed one and vice-versa. - if (_drawState.DrawIndexed != _prevDrawIndexed) + // Some draws also do not update the engine state, so it is possible for it + // to not be dirty even if the vertex counts or other state changed. We need to force it to be dirty in this case. + if (_drawState.DrawIndexed != _prevDrawIndexed || _drawState.DrawUsesEngineState != _prevDrawUsesEngineState) { _updateTracker.ForceDirty(VertexBufferStateIndex); @@ -251,6 +257,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed } _prevDrawIndexed = _drawState.DrawIndexed; + _prevDrawUsesEngineState = _drawState.DrawUsesEngineState; } // Some draw parameters are used to restrict the vertex buffer size, @@ -260,6 +267,8 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed if (_drawState.DrawIndirect != _prevDrawIndirect) { _updateTracker.ForceDirty(VertexBufferStateIndex); + + _prevDrawIndirect = _drawState.DrawIndirect; } // In some cases, the index type is also used to guess the @@ -334,11 +343,22 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed bool unalignedChanged = _currentSpecState.SetHasUnalignedStorageBuffer(_channel.BufferManager.HasUnalignedStorageBuffers); - if (!_channel.TextureManager.CommitGraphicsBindings(_shaderSpecState) || unalignedChanged) + bool scaleMismatch; + do { - // Shader must be reloaded. _vtgWritesRtLayer should not change. - UpdateShaderState(); + if (!_channel.TextureManager.CommitGraphicsBindings(_shaderSpecState, out scaleMismatch) || unalignedChanged) + { + // Shader must be reloaded. _vtgWritesRtLayer should not change. + UpdateShaderState(); + } + + if (scaleMismatch) + { + // Binding textures changed scale of the bound render targets, correct the render target scale and rebind. + UpdateRenderTargetState(); + } } + while (scaleMismatch); _channel.BufferManager.CommitGraphicsBindings(_drawState.DrawIndexed); } @@ -1138,6 +1158,14 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed size = Math.Min(size, maxVertexBufferSize); } + else if (size > VertexBufferSizeToMappedSizeThreshold) + { + // Make sure we have a sane vertex buffer size, since in some cases applications + // might set the "end address" of the vertex buffer to the end of the GPU address space, + // which would result in a several GBs large buffer. + + size = _channel.MemoryManager.GetMappedSize(address, size); + } } else { @@ -1401,7 +1429,18 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed addressesSpan[index] = baseAddress + shader.Offset; } - CachedShaderProgram gs = shaderCache.GetGraphicsShader(ref _state.State, ref _pipeline, _channel, ref _currentSpecState.GetPoolState(), ref _currentSpecState.GetGraphicsState(), addresses); + int samplerPoolMaximumId = _state.State.SamplerIndex == SamplerIndex.ViaHeaderIndex + ? _state.State.TexturePoolState.MaximumId + : _state.State.SamplerPoolState.MaximumId; + + CachedShaderProgram gs = shaderCache.GetGraphicsShader( + ref _state.State, + ref _pipeline, + _channel, + samplerPoolMaximumId, + ref _currentSpecState.GetPoolState(), + ref _currentSpecState.GetGraphicsState(), + addresses); // Consume the modified flag for spec state so that it isn't checked again. _currentSpecState.SetShader(gs); diff --git a/src/Ryujinx.Graphics.Gpu/Engine/Threed/ThreedClassState.cs b/src/Ryujinx.Graphics.Gpu/Engine/Threed/ThreedClassState.cs index dd55e7d1d..35051c6e0 100644 --- a/src/Ryujinx.Graphics.Gpu/Engine/Threed/ThreedClassState.cs +++ b/src/Ryujinx.Graphics.Gpu/Engine/Threed/ThreedClassState.cs @@ -415,7 +415,13 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed #pragma warning disable CS0649 // Field is never assigned to public int Width; public int Height; - public int Depth; + public ushort Depth; + public ushort Flags; + + public readonly bool UnpackIsLayered() + { + return (Flags & 1) == 0; + } #pragma warning restore CS0649 } diff --git a/src/Ryujinx.Graphics.Gpu/Engine/Types/ColorFormat.cs b/src/Ryujinx.Graphics.Gpu/Engine/Types/ColorFormat.cs index c798384f0..273438a67 100644 --- a/src/Ryujinx.Graphics.Gpu/Engine/Types/ColorFormat.cs +++ b/src/Ryujinx.Graphics.Gpu/Engine/Types/ColorFormat.cs @@ -37,6 +37,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Types R16G16Sint = 0xdc, R16G16Uint = 0xdd, R16G16Float = 0xde, + B10G10R10A2Unorm = 0xdf, R11G11B10Float = 0xe0, R32Sint = 0xe3, R32Uint = 0xe4, @@ -104,6 +105,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Types ColorFormat.R16G16Sint => new FormatInfo(Format.R16G16Sint, 1, 1, 4, 2), ColorFormat.R16G16Uint => new FormatInfo(Format.R16G16Uint, 1, 1, 4, 2), ColorFormat.R16G16Float => new FormatInfo(Format.R16G16Float, 1, 1, 4, 2), + ColorFormat.B10G10R10A2Unorm => new FormatInfo(Format.B10G10R10A2Unorm, 1, 1, 4, 4), ColorFormat.R11G11B10Float => new FormatInfo(Format.R11G11B10Float, 1, 1, 4, 3), ColorFormat.R32Sint => new FormatInfo(Format.R32Sint, 1, 1, 4, 1), ColorFormat.R32Uint => new FormatInfo(Format.R32Uint, 1, 1, 4, 1), diff --git a/src/Ryujinx.Graphics.Gpu/Engine/Types/ZetaFormat.cs b/src/Ryujinx.Graphics.Gpu/Engine/Types/ZetaFormat.cs index e2a044e72..88fbe88f9 100644 --- a/src/Ryujinx.Graphics.Gpu/Engine/Types/ZetaFormat.cs +++ b/src/Ryujinx.Graphics.Gpu/Engine/Types/ZetaFormat.cs @@ -8,13 +8,13 @@ namespace Ryujinx.Graphics.Gpu.Engine.Types /// enum ZetaFormat { - D32Float = 0xa, - D16Unorm = 0x13, - D24UnormS8Uint = 0x14, - D24Unorm = 0x15, - S8UintD24Unorm = 0x16, + Zf32 = 0xa, + Z16 = 0x13, + Z24S8 = 0x14, + X8Z24 = 0x15, + S8Z24 = 0x16, S8Uint = 0x17, - D32FloatS8Uint = 0x19, + Zf32X24S8 = 0x19, } static class ZetaFormatConverter @@ -29,14 +29,14 @@ namespace Ryujinx.Graphics.Gpu.Engine.Types return format switch { #pragma warning disable IDE0055 // Disable formatting - ZetaFormat.D32Float => new FormatInfo(Format.D32Float, 1, 1, 4, 1), - ZetaFormat.D16Unorm => new FormatInfo(Format.D16Unorm, 1, 1, 2, 1), - ZetaFormat.D24UnormS8Uint => new FormatInfo(Format.D24UnormS8Uint, 1, 1, 4, 2), - ZetaFormat.D24Unorm => new FormatInfo(Format.D24UnormS8Uint, 1, 1, 4, 1), - ZetaFormat.S8UintD24Unorm => new FormatInfo(Format.S8UintD24Unorm, 1, 1, 4, 2), - ZetaFormat.S8Uint => new FormatInfo(Format.S8Uint, 1, 1, 1, 1), - ZetaFormat.D32FloatS8Uint => new FormatInfo(Format.D32FloatS8Uint, 1, 1, 8, 2), - _ => FormatInfo.Default, + ZetaFormat.Zf32 => new FormatInfo(Format.D32Float, 1, 1, 4, 1), + ZetaFormat.Z16 => new FormatInfo(Format.D16Unorm, 1, 1, 2, 1), + ZetaFormat.Z24S8 => new FormatInfo(Format.D24UnormS8Uint, 1, 1, 4, 2), + ZetaFormat.X8Z24 => new FormatInfo(Format.X8UintD24Unorm, 1, 1, 4, 1), + ZetaFormat.S8Z24 => new FormatInfo(Format.S8UintD24Unorm, 1, 1, 4, 2), + ZetaFormat.S8Uint => new FormatInfo(Format.S8Uint, 1, 1, 1, 1), + ZetaFormat.Zf32X24S8 => new FormatInfo(Format.D32FloatS8Uint, 1, 1, 8, 2), + _ => FormatInfo.Default, #pragma warning restore IDE0055 }; } diff --git a/src/Ryujinx.Graphics.Gpu/GpuContext.cs b/src/Ryujinx.Graphics.Gpu/GpuContext.cs index a50852b05..fb529e914 100644 --- a/src/Ryujinx.Graphics.Gpu/GpuContext.cs +++ b/src/Ryujinx.Graphics.Gpu/GpuContext.cs @@ -1,4 +1,5 @@ using Ryujinx.Common; +using Ryujinx.Graphics.Device; using Ryujinx.Graphics.GAL; using Ryujinx.Graphics.Gpu.Engine.GPFifo; using Ryujinx.Graphics.Gpu.Memory; @@ -106,6 +107,8 @@ namespace Ryujinx.Graphics.Gpu private long _modifiedSequence; private readonly ulong _firstTimestamp; + private readonly ManualResetEvent _gpuReadyEvent; + /// /// Creates a new instance of the GPU emulation context. /// @@ -121,6 +124,7 @@ namespace Ryujinx.Graphics.Gpu Window = new Window(this); HostInitalized = new ManualResetEvent(false); + _gpuReadyEvent = new ManualResetEvent(false); SyncActions = new List(); SyncpointActions = new List(); @@ -148,16 +152,33 @@ namespace Ryujinx.Graphics.Gpu /// Creates a new GPU memory manager. /// /// ID of the process that owns the memory manager + /// The amount of physical CPU Memory Avaiable on the device. /// The memory manager /// Thrown when is invalid - public MemoryManager CreateMemoryManager(ulong pid) + public MemoryManager CreateMemoryManager(ulong pid, ulong cpuMemorySize) { if (!PhysicalMemoryRegistry.TryGetValue(pid, out var physicalMemory)) { throw new ArgumentException("The PID is invalid or the process was not registered", nameof(pid)); } - return new MemoryManager(physicalMemory); + return new MemoryManager(physicalMemory, cpuMemorySize); + } + + /// + /// Creates a new device memory manager. + /// + /// ID of the process that owns the memory manager + /// The memory manager + /// Thrown when is invalid + public DeviceMemoryManager CreateDeviceMemoryManager(ulong pid) + { + if (!PhysicalMemoryRegistry.TryGetValue(pid, out var physicalMemory)) + { + throw new ArgumentException("The PID is invalid or the process was not registered", nameof(pid)); + } + + return physicalMemory.CreateDeviceMemoryManager(); } /// @@ -216,7 +237,7 @@ namespace Ryujinx.Graphics.Gpu /// Gets a sequence number for resource modification ordering. This increments on each call. /// /// A sequence number for resource modification ordering - public long GetModifiedSequence() + internal long GetModifiedSequence() { return _modifiedSequence++; } @@ -225,7 +246,7 @@ namespace Ryujinx.Graphics.Gpu /// Gets the value of the GPU timer. /// /// The current GPU timestamp - public ulong GetTimestamp() + internal ulong GetTimestamp() { // Guest timestamp will start at 0, instead of host value. ulong ticks = ConvertNanosecondsToTicks((ulong)PerformanceCounter.ElapsedNanoseconds) - _firstTimestamp; @@ -262,6 +283,16 @@ namespace Ryujinx.Graphics.Gpu { physicalMemory.ShaderCache.Initialize(cancellationToken); } + + _gpuReadyEvent.Set(); + } + + /// + /// Waits until the GPU is ready to receive commands. + /// + public void WaitUntilGpuReady() + { + _gpuReadyEvent.WaitOne(); } /// @@ -363,10 +394,17 @@ namespace Ryujinx.Graphics.Gpu if (force || _pendingSync || (syncpoint && SyncpointActions.Count > 0)) { - Renderer.CreateSync(SyncNumber, strict); + foreach (var action in SyncActions) + { + action.SyncPreAction(syncpoint); + } - SyncActions.ForEach(action => action.SyncPreAction(syncpoint)); - SyncpointActions.ForEach(action => action.SyncPreAction(syncpoint)); + foreach (var action in SyncpointActions) + { + action.SyncPreAction(syncpoint); + } + + Renderer.CreateSync(SyncNumber, strict); SyncNumber++; @@ -399,6 +437,7 @@ namespace Ryujinx.Graphics.Gpu { GPFifo.Dispose(); HostInitalized.Dispose(); + _gpuReadyEvent.Dispose(); // Has to be disposed before processing deferred actions, as it will produce some. foreach (var physicalMemory in PhysicalMemoryRegistry.Values) diff --git a/src/Ryujinx.Graphics.Gpu/GraphicsConfig.cs b/src/Ryujinx.Graphics.Gpu/GraphicsConfig.cs index 9707d4fac..fbb7399ca 100644 --- a/src/Ryujinx.Graphics.Gpu/GraphicsConfig.cs +++ b/src/Ryujinx.Graphics.Gpu/GraphicsConfig.cs @@ -40,7 +40,7 @@ namespace Ryujinx.Graphics.Gpu /// /// Enables or disables the Just-in-Time compiler for GPU Macro code. /// - public static bool EnableMacroJit = false; + public static bool EnableMacroJit = true; /// /// Enables or disables high-level emulation of common GPU Macro code. diff --git a/src/Ryujinx.Graphics.Gpu/Image/AutoDeleteCache.cs b/src/Ryujinx.Graphics.Gpu/Image/AutoDeleteCache.cs index 05782605b..74967b190 100644 --- a/src/Ryujinx.Graphics.Gpu/Image/AutoDeleteCache.cs +++ b/src/Ryujinx.Graphics.Gpu/Image/AutoDeleteCache.cs @@ -1,3 +1,5 @@ +using Ryujinx.Common.Logging; +using System; using System.Collections; using System.Collections.Generic; @@ -46,7 +48,17 @@ namespace Ryujinx.Graphics.Gpu.Image { private const int MinCountForDeletion = 32; private const int MaxCapacity = 2048; - private const ulong MaxTextureSizeCapacity = 512 * 1024 * 1024; // MB; + private const ulong GiB = 1024 * 1024 * 1024; + private ulong MaxTextureSizeCapacity = 4UL * GiB; + private const ulong MinTextureSizeCapacity = 512 * 1024 * 1024; + private const ulong DefaultTextureSizeCapacity = 1 * GiB; + private const ulong TextureSizeCapacity6GiB = 4 * GiB; + private const ulong TextureSizeCapacity8GiB = 6 * GiB; + private const ulong TextureSizeCapacity12GiB = 12 * GiB; + + + private const float MemoryScaleFactor = 0.50f; + private ulong _maxCacheMemoryUsage = DefaultTextureSizeCapacity; private readonly LinkedList _textures; private ulong _totalSize; @@ -56,6 +68,45 @@ namespace Ryujinx.Graphics.Gpu.Image private readonly Dictionary _shortCacheLookup; + /// + /// Initializes the cache, setting the maximum texture capacity for the specified GPU context. + /// + /// + /// If the backend GPU has 0 memory capacity, the cache size defaults to `DefaultTextureSizeCapacity`. + /// + /// Reads the current Device total CPU Memory, to determine the maximum amount of Vram available. Capped to 50% of Current GPU Memory. + /// + /// The GPU context that the cache belongs to + /// The amount of physical CPU Memory Avaiable on the device. + public void Initialize(GpuContext context, ulong cpuMemorySize) + { + var cpuMemorySizeGiB = cpuMemorySize / GiB; + + if (cpuMemorySizeGiB < 6 || context.Capabilities.MaximumGpuMemory == 0) + { + _maxCacheMemoryUsage = DefaultTextureSizeCapacity; + return; + } + else if (cpuMemorySizeGiB == 6) + { + MaxTextureSizeCapacity = TextureSizeCapacity6GiB; + } + else if (cpuMemorySizeGiB == 8) + { + MaxTextureSizeCapacity = TextureSizeCapacity8GiB; + } + else + { + MaxTextureSizeCapacity = TextureSizeCapacity12GiB; + } + + var cacheMemory = (ulong)(context.Capabilities.MaximumGpuMemory * MemoryScaleFactor); + + _maxCacheMemoryUsage = Math.Clamp(cacheMemory, MinTextureSizeCapacity, MaxTextureSizeCapacity); + + Logger.Info?.Print(LogClass.Gpu, $"AutoDelete Cache Allocated VRAM : {_maxCacheMemoryUsage / GiB} GiB"); + } + /// /// Creates a new instance of the automatic deletion cache. /// @@ -85,7 +136,7 @@ namespace Ryujinx.Graphics.Gpu.Image texture.CacheNode = _textures.AddLast(texture); if (_textures.Count > MaxCapacity || - (_totalSize > MaxTextureSizeCapacity && _textures.Count >= MinCountForDeletion)) + (_totalSize > _maxCacheMemoryUsage && _textures.Count >= MinCountForDeletion)) { RemoveLeastUsedTexture(); } @@ -107,11 +158,10 @@ namespace Ryujinx.Graphics.Gpu.Image if (texture.CacheNode != _textures.Last) { _textures.Remove(texture.CacheNode); - - texture.CacheNode = _textures.AddLast(texture); + _textures.AddLast(texture.CacheNode); } - if (_totalSize > MaxTextureSizeCapacity && _textures.Count >= MinCountForDeletion) + if (_totalSize > _maxCacheMemoryUsage && _textures.Count >= MinCountForDeletion) { RemoveLeastUsedTexture(); } diff --git a/src/Ryujinx.Graphics.Gpu/Image/FormatInfo.cs b/src/Ryujinx.Graphics.Gpu/Image/FormatInfo.cs index 8a9f37bb0..b95c684e4 100644 --- a/src/Ryujinx.Graphics.Gpu/Image/FormatInfo.cs +++ b/src/Ryujinx.Graphics.Gpu/Image/FormatInfo.cs @@ -7,6 +7,11 @@ namespace Ryujinx.Graphics.Gpu.Image /// readonly struct FormatInfo { + /// + /// An invalid texture format. + /// + public static FormatInfo Invalid { get; } = new(0, 0, 0, 0, 0); + /// /// A default, generic RGBA8 texture format. /// @@ -23,7 +28,7 @@ namespace Ryujinx.Graphics.Gpu.Image /// /// Must be 1 for non-compressed formats. /// - public int BlockWidth { get; } + public byte BlockWidth { get; } /// /// The block height for compressed formats. @@ -31,17 +36,17 @@ namespace Ryujinx.Graphics.Gpu.Image /// /// Must be 1 for non-compressed formats. /// - public int BlockHeight { get; } + public byte BlockHeight { get; } /// /// The number of bytes occupied by a single pixel in memory of the texture data. /// - public int BytesPerPixel { get; } + public byte BytesPerPixel { get; } /// /// The maximum number of components this format has defined (in RGBA order). /// - public int Components { get; } + public byte Components { get; } /// /// Whenever or not the texture format is a compressed format. Determined from block size. @@ -57,10 +62,10 @@ namespace Ryujinx.Graphics.Gpu.Image /// The number of bytes occupied by a single pixel in memory of the texture data public FormatInfo( Format format, - int blockWidth, - int blockHeight, - int bytesPerPixel, - int components) + byte blockWidth, + byte blockHeight, + byte bytesPerPixel, + byte components) { Format = format; BlockWidth = blockWidth; diff --git a/src/Ryujinx.Graphics.Gpu/Image/FormatTable.cs b/src/Ryujinx.Graphics.Gpu/Image/FormatTable.cs index 0af0725a2..da9e5c3a9 100644 --- a/src/Ryujinx.Graphics.Gpu/Image/FormatTable.cs +++ b/src/Ryujinx.Graphics.Gpu/Image/FormatTable.cs @@ -185,6 +185,7 @@ namespace Ryujinx.Graphics.Gpu.Image G24R8RUintGUnormBUnormAUnorm = G24R8 | RUint | GUnorm | BUnorm | AUnorm, // 0x24a0e Z24S8RUintGUnormBUnormAUnorm = Z24S8 | RUint | GUnorm | BUnorm | AUnorm, // 0x24a29 Z24S8RUintGUnormBUintAUint = Z24S8 | RUint | GUnorm | BUint | AUint, // 0x48a29 + X8Z24RUnormGUintBUintAUint = X8Z24 | RUnorm | GUint | BUint | AUint, // 0x4912a S8Z24RUnormGUintBUintAUint = S8Z24 | RUnorm | GUint | BUint | AUint, // 0x4912b R32B24G8RFloatGUintBUnormAUnorm = R32B24G8 | RFloat | GUint | BUnorm | AUnorm, // 0x25385 Zf32X24S8RFloatGUintBUnormAUnorm = Zf32X24S8 | RFloat | GUint | BUnorm | AUnorm, // 0x253b0 @@ -410,6 +411,7 @@ namespace Ryujinx.Graphics.Gpu.Image { TextureFormat.G24R8RUintGUnormBUnormAUnorm, new FormatInfo(Format.D24UnormS8Uint, 1, 1, 4, 2) }, { TextureFormat.Z24S8RUintGUnormBUnormAUnorm, new FormatInfo(Format.D24UnormS8Uint, 1, 1, 4, 2) }, { TextureFormat.Z24S8RUintGUnormBUintAUint, new FormatInfo(Format.D24UnormS8Uint, 1, 1, 4, 2) }, + { TextureFormat.X8Z24RUnormGUintBUintAUint, new FormatInfo(Format.X8UintD24Unorm, 1, 1, 4, 2) }, { TextureFormat.S8Z24RUnormGUintBUintAUint, new FormatInfo(Format.S8UintD24Unorm, 1, 1, 4, 2) }, { TextureFormat.R32B24G8RFloatGUintBUnormAUnorm, new FormatInfo(Format.D32FloatS8Uint, 1, 1, 8, 2) }, { TextureFormat.Zf32X24S8RFloatGUintBUnormAUnorm, new FormatInfo(Format.D32FloatS8Uint, 1, 1, 8, 2) }, @@ -672,9 +674,9 @@ namespace Ryujinx.Graphics.Gpu.Image { 1 => new FormatInfo(Format.R8Unorm, 1, 1, 1, 1), 2 => new FormatInfo(Format.R16Unorm, 1, 1, 2, 1), - 4 => new FormatInfo(Format.R32Float, 1, 1, 4, 1), - 8 => new FormatInfo(Format.R32G32Float, 1, 1, 8, 2), - 16 => new FormatInfo(Format.R32G32B32A32Float, 1, 1, 16, 4), + 4 => new FormatInfo(Format.R32Uint, 1, 1, 4, 1), + 8 => new FormatInfo(Format.R32G32Uint, 1, 1, 8, 2), + 16 => new FormatInfo(Format.R32G32B32A32Uint, 1, 1, 16, 4), _ => format, }; } diff --git a/src/Ryujinx.Graphics.Gpu/Image/Pool.cs b/src/Ryujinx.Graphics.Gpu/Image/Pool.cs index 0c3a219de..e12fedc74 100644 --- a/src/Ryujinx.Graphics.Gpu/Image/Pool.cs +++ b/src/Ryujinx.Graphics.Gpu/Image/Pool.cs @@ -69,7 +69,7 @@ namespace Ryujinx.Graphics.Gpu.Image Address = address; Size = size; - _memoryTracking = physicalMemory.BeginGranularTracking(address, size, ResourceKind.Pool); + _memoryTracking = physicalMemory.BeginGranularTracking(address, size, ResourceKind.Pool, RegionFlags.None); _memoryTracking.RegisterPreciseAction(address, size, PreciseAction); _modifiedDelegate = RegionModified; } @@ -111,6 +111,21 @@ namespace Ryujinx.Graphics.Gpu.Image /// The GPU resource with the given ID public abstract T1 Get(int id); + /// + /// Gets the cached item with the given ID, or null if there is no cached item for the specified ID. + /// + /// ID of the item. This is effectively a zero-based index + /// The cached item with the given ID + public T1 GetCachedItem(int id) + { + if (!IsValidId(id)) + { + return default; + } + + return Items[id]; + } + /// /// Checks if a given ID is valid and inside the range of the pool. /// @@ -197,6 +212,23 @@ namespace Ryujinx.Graphics.Gpu.Image return false; } + /// + /// Checks if the pool was modified by comparing the current with a cached one. + /// + /// Cached modified sequence number + /// True if the pool was modified, false otherwise + public bool WasModified(ref int sequenceNumber) + { + if (sequenceNumber != ModifiedSequenceNumber) + { + sequenceNumber = ModifiedSequenceNumber; + + return true; + } + + return false; + } + protected abstract void InvalidateRangeImpl(ulong address, ulong size); protected abstract void Delete(T1 item); diff --git a/src/Ryujinx.Graphics.Gpu/Image/PoolCache.cs b/src/Ryujinx.Graphics.Gpu/Image/PoolCache.cs index d9881f897..50872ab63 100644 --- a/src/Ryujinx.Graphics.Gpu/Image/PoolCache.cs +++ b/src/Ryujinx.Graphics.Gpu/Image/PoolCache.cs @@ -62,8 +62,9 @@ namespace Ryujinx.Graphics.Gpu.Image /// GPU channel that the texture pool cache belongs to /// Start address of the texture pool /// Maximum ID of the texture pool + /// Cache of texture array bindings /// The found or newly created texture pool - public T FindOrCreate(GpuChannel channel, ulong address, int maximumId) + public T FindOrCreate(GpuChannel channel, ulong address, int maximumId, TextureBindingsArrayCache bindingsArrayCache) { // Remove old entries from the cache, if possible. while (_pools.Count > MaxCapacity && (_currentTimestamp - _pools.First.Value.CacheTimestamp) >= MinDeltaForRemoval) @@ -73,6 +74,7 @@ namespace Ryujinx.Graphics.Gpu.Image _pools.RemoveFirst(); oldestPool.Dispose(); oldestPool.CacheNode = null; + bindingsArrayCache.RemoveAllWithPool(oldestPool); } T pool; @@ -87,8 +89,7 @@ namespace Ryujinx.Graphics.Gpu.Image if (pool.CacheNode != _pools.Last) { _pools.Remove(pool.CacheNode); - - pool.CacheNode = _pools.AddLast(pool); + _pools.AddLast(pool.CacheNode); } pool.CacheTimestamp = _currentTimestamp; diff --git a/src/Ryujinx.Graphics.Gpu/Image/Sampler.cs b/src/Ryujinx.Graphics.Gpu/Image/Sampler.cs index d6a3d975b..b007c1591 100644 --- a/src/Ryujinx.Graphics.Gpu/Image/Sampler.cs +++ b/src/Ryujinx.Graphics.Gpu/Image/Sampler.cs @@ -13,6 +13,11 @@ namespace Ryujinx.Graphics.Gpu.Image /// public bool IsDisposed { get; private set; } + /// + /// True if the sampler has sRGB conversion enabled, false otherwise. + /// + public bool IsSrgb { get; } + /// /// Host sampler object. /// @@ -30,6 +35,8 @@ namespace Ryujinx.Graphics.Gpu.Image /// The Maxwell sampler descriptor public Sampler(GpuContext context, SamplerDescriptor descriptor) { + IsSrgb = descriptor.UnpackSrgb(); + MinFilter minFilter = descriptor.UnpackMinFilter(); MagFilter magFilter = descriptor.UnpackMagFilter(); diff --git a/src/Ryujinx.Graphics.Gpu/Image/SamplerDescriptor.cs b/src/Ryujinx.Graphics.Gpu/Image/SamplerDescriptor.cs index e04c31dfa..836a3260c 100644 --- a/src/Ryujinx.Graphics.Gpu/Image/SamplerDescriptor.cs +++ b/src/Ryujinx.Graphics.Gpu/Image/SamplerDescriptor.cs @@ -113,6 +113,15 @@ namespace Ryujinx.Graphics.Gpu.Image return (CompareOp)(((Word0 >> 10) & 7) + 1); } + /// + /// Unpacks the sampler sRGB format flag. + /// + /// True if the has sampler is sRGB conversion enabled, false otherwise + public readonly bool UnpackSrgb() + { + return (Word0 & (1 << 13)) != 0; + } + /// /// Unpacks and converts the maximum anisotropy value used for texture anisotropic filtering. /// diff --git a/src/Ryujinx.Graphics.Gpu/Image/Texture.cs b/src/Ryujinx.Graphics.Gpu/Image/Texture.cs index 195d13084..7ee2e5cf0 100644 --- a/src/Ryujinx.Graphics.Gpu/Image/Texture.cs +++ b/src/Ryujinx.Graphics.Gpu/Image/Texture.cs @@ -7,7 +7,6 @@ using Ryujinx.Graphics.Texture.Astc; using Ryujinx.Memory; using Ryujinx.Memory.Range; using System; -using System.Buffers; using System.Collections.Generic; using System.Diagnostics; using System.Linq; @@ -391,7 +390,7 @@ namespace Ryujinx.Graphics.Gpu.Image { _views.Remove(texture); - Group.RemoveView(texture); + Group.RemoveView(_views, texture); texture._viewStorage = texture; @@ -574,7 +573,7 @@ namespace Ryujinx.Graphics.Gpu.Image /// /// Discards all data for this texture. - /// This clears all dirty flags, modified flags, and pending copies from other textures. + /// This clears all dirty flags and pending copies from other textures. /// It should be used if the texture data will be fully overwritten by the next use. /// public void DiscardData() @@ -662,7 +661,7 @@ namespace Ryujinx.Graphics.Gpu.Image } } - IMemoryOwner result = ConvertToHostCompatibleFormat(data); + MemoryOwner result = ConvertToHostCompatibleFormat(data); if (ScaleFactor != 1f && AllowScaledSetData()) { @@ -682,10 +681,10 @@ namespace Ryujinx.Graphics.Gpu.Image } /// - /// Uploads new texture data to the host GPU. The data passed as a will be disposed when the operation completes. + /// Uploads new texture data to the host GPU. /// /// New data - public void SetData(IMemoryOwner data) + public void SetData(MemoryOwner data) { BlacklistScale(); @@ -699,12 +698,12 @@ namespace Ryujinx.Graphics.Gpu.Image } /// - /// Uploads new texture data to the host GPU for a specific layer/level. The data passed as a will be disposed when the operation completes. + /// Uploads new texture data to the host GPU for a specific layer/level. /// /// New data /// Target layer /// Target level - public void SetData(IMemoryOwner data, int layer, int level) + public void SetData(MemoryOwner data, int layer, int level) { BlacklistScale(); @@ -716,13 +715,13 @@ namespace Ryujinx.Graphics.Gpu.Image } /// - /// Uploads new texture data to the host GPU for a specific layer/level and 2D sub-region. The data passed as a will be disposed when the operation completes. + /// Uploads new texture data to the host GPU for a specific layer/level and 2D sub-region. /// /// New data /// Target layer /// Target level /// Target sub-region of the texture to update - public void SetData(IMemoryOwner data, int layer, int level, Rectangle region) + public void SetData(MemoryOwner data, int layer, int level, Rectangle region) { BlacklistScale(); @@ -740,7 +739,7 @@ namespace Ryujinx.Graphics.Gpu.Image /// Mip level to convert /// True to convert a single slice /// Converted data - public IMemoryOwner ConvertToHostCompatibleFormat(ReadOnlySpan data, int level = 0, bool single = false) + public MemoryOwner ConvertToHostCompatibleFormat(ReadOnlySpan data, int level = 0, bool single = false) { int width = Info.Width; int height = Info.Height; @@ -755,7 +754,7 @@ namespace Ryujinx.Graphics.Gpu.Image int sliceDepth = single ? 1 : depth; - IMemoryOwner linear; + MemoryOwner linear; if (Info.IsLinear) { @@ -788,39 +787,40 @@ namespace Ryujinx.Graphics.Gpu.Image data); } - IMemoryOwner result = linear; + MemoryOwner result = linear; // Handle compressed cases not supported by the host: // - ASTC is usually not supported on desktop cards. // - BC4/BC5 is not supported on 3D textures. if (!_context.Capabilities.SupportsAstcCompression && Format.IsAstc()) { - if (!AstcDecoder.TryDecodeToRgba8P( - result.Memory, - Info.FormatInfo.BlockWidth, - Info.FormatInfo.BlockHeight, - width, - height, - sliceDepth, - levels, - layers, - out IMemoryOwner decoded)) + using (result) { - string texInfo = $"{Info.Target} {Info.FormatInfo.Format} {Info.Width}x{Info.Height}x{Info.DepthOrLayers} levels {Info.Levels}"; - - Logger.Debug?.Print(LogClass.Gpu, $"Invalid ASTC texture at 0x{Info.GpuAddress:X} ({texInfo})."); - } - - if (GraphicsConfig.EnableTextureRecompression) - { - using (decoded) + if (!AstcDecoder.TryDecodeToRgba8P( + result.Memory, + Info.FormatInfo.BlockWidth, + Info.FormatInfo.BlockHeight, + width, + height, + sliceDepth, + levels, + layers, + out MemoryOwner decoded)) { - result = BCnEncoder.EncodeBC7(decoded.Memory, width, height, sliceDepth, levels, layers); + string texInfo = $"{Info.Target} {Info.FormatInfo.Format} {Info.Width}x{Info.Height}x{Info.DepthOrLayers} levels {Info.Levels}"; + + Logger.Debug?.Print(LogClass.Gpu, $"Invalid ASTC texture at 0x{Info.GpuAddress:X} ({texInfo})."); } - } - else - { - result = decoded; + + if (GraphicsConfig.EnableTextureRecompression) + { + using (decoded) + { + return BCnEncoder.EncodeBC7(decoded.Memory, width, height, sliceDepth, levels, layers); + } + } + + return decoded; } } else if (!_context.Capabilities.SupportsEtc2Compression && Format.IsEtc2()) @@ -829,16 +829,22 @@ namespace Ryujinx.Graphics.Gpu.Image { case Format.Etc2RgbaSrgb: case Format.Etc2RgbaUnorm: - result = ETC2Decoder.DecodeRgba(result.Memory.Span, width, height, sliceDepth, levels, layers); - break; + using (result) + { + return ETC2Decoder.DecodeRgba(result.Span, width, height, sliceDepth, levels, layers); + } case Format.Etc2RgbPtaSrgb: case Format.Etc2RgbPtaUnorm: - result = ETC2Decoder.DecodePta(result.Memory.Span, width, height, sliceDepth, levels, layers); - break; + using (result) + { + return ETC2Decoder.DecodePta(result.Span, width, height, sliceDepth, levels, layers); + } case Format.Etc2RgbSrgb: case Format.Etc2RgbUnorm: - result = ETC2Decoder.DecodeRgb(result.Memory.Span, width, height, sliceDepth, levels, layers); - break; + using (result) + { + return ETC2Decoder.DecodeRgb(result.Span, width, height, sliceDepth, levels, layers); + } } } else if (!TextureCompatibility.HostSupportsBcFormat(Format, Target, _context.Capabilities)) @@ -847,55 +853,75 @@ namespace Ryujinx.Graphics.Gpu.Image { case Format.Bc1RgbaSrgb: case Format.Bc1RgbaUnorm: - result = BCnDecoder.DecodeBC1(result.Memory.Span, width, height, sliceDepth, levels, layers); - break; + using (result) + { + return BCnDecoder.DecodeBC1(result.Span, width, height, sliceDepth, levels, layers); + } case Format.Bc2Srgb: case Format.Bc2Unorm: - result = BCnDecoder.DecodeBC2(result.Memory.Span, width, height, sliceDepth, levels, layers); - break; + using (result) + { + return BCnDecoder.DecodeBC2(result.Span, width, height, sliceDepth, levels, layers); + } case Format.Bc3Srgb: case Format.Bc3Unorm: - result = BCnDecoder.DecodeBC3(result.Memory.Span, width, height, sliceDepth, levels, layers); - break; + using (result) + { + return BCnDecoder.DecodeBC3(result.Span, width, height, sliceDepth, levels, layers); + } case Format.Bc4Snorm: case Format.Bc4Unorm: - result = BCnDecoder.DecodeBC4(result.Memory.Span, width, height, sliceDepth, levels, layers, Format == Format.Bc4Snorm); - break; + using (result) + { + return BCnDecoder.DecodeBC4(result.Span, width, height, sliceDepth, levels, layers, Format == Format.Bc4Snorm); + } case Format.Bc5Snorm: case Format.Bc5Unorm: - result = BCnDecoder.DecodeBC5(result.Memory.Span, width, height, sliceDepth, levels, layers, Format == Format.Bc5Snorm); - break; + using (result) + { + return BCnDecoder.DecodeBC5(result.Span, width, height, sliceDepth, levels, layers, Format == Format.Bc5Snorm); + } case Format.Bc6HSfloat: case Format.Bc6HUfloat: - result = BCnDecoder.DecodeBC6(result.Memory.Span, width, height, sliceDepth, levels, layers, Format == Format.Bc6HSfloat); - break; + using (result) + { + return BCnDecoder.DecodeBC6(result.Span, width, height, sliceDepth, levels, layers, Format == Format.Bc6HSfloat); + } case Format.Bc7Srgb: case Format.Bc7Unorm: - result = BCnDecoder.DecodeBC7(result.Memory.Span, width, height, sliceDepth, levels, layers); - break; + using (result) + { + return BCnDecoder.DecodeBC7(result.Span, width, height, sliceDepth, levels, layers); + } } } else if (!_context.Capabilities.SupportsR4G4Format && Format == Format.R4G4Unorm) { - var converted = PixelConverter.ConvertR4G4ToR4G4B4A4(result.Memory.Span, width); + using (result) + { + var converted = PixelConverter.ConvertR4G4ToR4G4B4A4(result.Span, width); - if (!_context.Capabilities.SupportsR4G4B4A4Format) - { - using (converted) + if (_context.Capabilities.SupportsR4G4B4A4Format) { - result = PixelConverter.ConvertR4G4B4A4ToR8G8B8A8(converted.Memory.Span, width); + return converted; + } + else + { + using (converted) + { + return PixelConverter.ConvertR4G4B4A4ToR8G8B8A8(converted.Span, width); + } } - } - else - { - result = converted; } } else if (Format == Format.R4G4B4A4Unorm) { if (!_context.Capabilities.SupportsR4G4B4A4Format) { - result = PixelConverter.ConvertR4G4B4A4ToR8G8B8A8(result.Memory.Span, width); + using (result) + { + return PixelConverter.ConvertR4G4B4A4ToR8G8B8A8(result.Span, width); + } } } else if (!_context.Capabilities.Supports5BitComponentFormat && Format.Is16BitPacked()) @@ -904,27 +930,30 @@ namespace Ryujinx.Graphics.Gpu.Image { case Format.B5G6R5Unorm: case Format.R5G6B5Unorm: - result = PixelConverter.ConvertR5G6B5ToR8G8B8A8(result.Memory.Span, width); - break; + using (result) + { + return PixelConverter.ConvertR5G6B5ToR8G8B8A8(result.Span, width); + } case Format.B5G5R5A1Unorm: case Format.R5G5B5X1Unorm: case Format.R5G5B5A1Unorm: - result = PixelConverter.ConvertR5G5B5ToR8G8B8A8(result.Memory.Span, width, Format == Format.R5G5B5X1Unorm); - break; + using (result) + { + return PixelConverter.ConvertR5G5B5ToR8G8B8A8(result.Span, width, Format == Format.R5G5B5X1Unorm); + } case Format.A1B5G5R5Unorm: - result = PixelConverter.ConvertA1B5G5R5ToR8G8B8A8(result.Memory.Span, width); - break; + using (result) + { + return PixelConverter.ConvertA1B5G5R5ToR8G8B8A8(result.Span, width); + } case Format.R4G4B4A4Unorm: - result = PixelConverter.ConvertR4G4B4A4ToR8G8B8A8(result.Memory.Span, width); - break; + using (result) + { + return PixelConverter.ConvertR4G4B4A4ToR8G8B8A8(result.Span, width); + } } } - if (!ReferenceEquals(linear, result)) - { - linear.Dispose(); - } - return result; } diff --git a/src/Ryujinx.Graphics.Gpu/Image/TextureBindingInfo.cs b/src/Ryujinx.Graphics.Gpu/Image/TextureBindingInfo.cs index 606842d6d..e9930405b 100644 --- a/src/Ryujinx.Graphics.Gpu/Image/TextureBindingInfo.cs +++ b/src/Ryujinx.Graphics.Gpu/Image/TextureBindingInfo.cs @@ -17,13 +17,23 @@ namespace Ryujinx.Graphics.Gpu.Image /// /// For images, indicates the format specified on the shader. /// - public Format Format { get; } + public FormatInfo FormatInfo { get; } + + /// + /// Shader texture host set index. + /// + public int Set { get; } /// /// Shader texture host binding point. /// public int Binding { get; } + /// + /// For array of textures, this indicates the length of the array. A value of one indicates it is not an array. + /// + public int ArrayLength { get; } + /// /// Constant buffer slot with the texture handle. /// @@ -39,20 +49,29 @@ namespace Ryujinx.Graphics.Gpu.Image /// public TextureUsageFlags Flags { get; } + /// + /// Indicates that the binding is for a sampler. + /// + public bool IsSamplerOnly { get; } + /// /// Constructs the texture binding information structure. /// /// The shader sampler target type - /// Format of the image as declared on the shader + /// Format of the image as declared on the shader + /// Shader texture host set index /// The shader texture binding point + /// For array of textures, this indicates the length of the array. A value of one indicates it is not an array /// Constant buffer slot where the texture handle is located /// The shader texture handle (read index into the texture constant buffer) /// The texture's usage flags, indicating how it is used in the shader - public TextureBindingInfo(Target target, Format format, int binding, int cbufSlot, int handle, TextureUsageFlags flags) + public TextureBindingInfo(Target target, FormatInfo formatInfo, int set, int binding, int arrayLength, int cbufSlot, int handle, TextureUsageFlags flags) { Target = target; - Format = format; + FormatInfo = formatInfo; + Set = set; Binding = binding; + ArrayLength = arrayLength; CbufSlot = cbufSlot; Handle = handle; Flags = flags; @@ -62,12 +81,24 @@ namespace Ryujinx.Graphics.Gpu.Image /// Constructs the texture binding information structure. /// /// The shader sampler target type + /// Shader texture host set index /// The shader texture binding point + /// For array of textures, this indicates the length of the array. A value of one indicates it is not an array /// Constant buffer slot where the texture handle is located /// The shader texture handle (read index into the texture constant buffer) /// The texture's usage flags, indicating how it is used in the shader - public TextureBindingInfo(Target target, int binding, int cbufSlot, int handle, TextureUsageFlags flags) : this(target, (Format)0, binding, cbufSlot, handle, flags) + /// Indicates that the binding is for a sampler + public TextureBindingInfo( + Target target, + int set, + int binding, + int arrayLength, + int cbufSlot, + int handle, + TextureUsageFlags flags, + bool isSamplerOnly) : this(target, FormatInfo.Invalid, set, binding, arrayLength, cbufSlot, handle, flags) { + IsSamplerOnly = isSamplerOnly; } } } diff --git a/src/Ryujinx.Graphics.Gpu/Image/TextureBindingsArrayCache.cs b/src/Ryujinx.Graphics.Gpu/Image/TextureBindingsArrayCache.cs new file mode 100644 index 000000000..72bac75e5 --- /dev/null +++ b/src/Ryujinx.Graphics.Gpu/Image/TextureBindingsArrayCache.cs @@ -0,0 +1,1153 @@ +using Ryujinx.Graphics.GAL; +using Ryujinx.Graphics.Gpu.Engine.Types; +using Ryujinx.Graphics.Gpu.Memory; +using Ryujinx.Graphics.Shader; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; + +namespace Ryujinx.Graphics.Gpu.Image +{ + /// + /// Texture bindings array cache. + /// + class TextureBindingsArrayCache + { + /// + /// Minimum timestamp delta until texture array can be removed from the cache. + /// + private const int MinDeltaForRemoval = 20000; + + private readonly GpuContext _context; + private readonly GpuChannel _channel; + + /// + /// Array cache entry key. + /// + private readonly struct CacheEntryFromPoolKey : IEquatable + { + /// + /// Whether the entry is for an image. + /// + public readonly bool IsImage; + + /// + /// Whether the entry is for a sampler. + /// + public readonly bool IsSampler; + + /// + /// Texture or image target type. + /// + public readonly Target Target; + + /// + /// Number of entries of the array. + /// + public readonly int ArrayLength; + + private readonly TexturePool _texturePool; + private readonly SamplerPool _samplerPool; + + /// + /// Creates a new array cache entry. + /// + /// Whether the entry is for an image + /// Binding information for the array + /// Texture pool where the array textures are located + /// Sampler pool where the array samplers are located + public CacheEntryFromPoolKey(bool isImage, TextureBindingInfo bindingInfo, TexturePool texturePool, SamplerPool samplerPool) + { + IsImage = isImage; + IsSampler = bindingInfo.IsSamplerOnly; + Target = bindingInfo.Target; + ArrayLength = bindingInfo.ArrayLength; + + _texturePool = texturePool; + _samplerPool = samplerPool; + } + + /// + /// Checks if the pool matches the cached pool. + /// + /// Texture or sampler pool instance + /// True if the pool matches, false otherwise + public bool MatchesPool(IPool pool) + { + return _texturePool == pool || _samplerPool == pool; + } + + /// + /// Checks if the texture and sampler pools matches the cached pools. + /// + /// Texture pool instance + /// Sampler pool instance + /// True if the pools match, false otherwise + private bool MatchesPools(TexturePool texturePool, SamplerPool samplerPool) + { + return _texturePool == texturePool && _samplerPool == samplerPool; + } + + public bool Equals(CacheEntryFromPoolKey other) + { + return IsImage == other.IsImage && + IsSampler == other.IsSampler && + Target == other.Target && + ArrayLength == other.ArrayLength && + MatchesPools(other._texturePool, other._samplerPool); + } + + public override bool Equals(object obj) + { + return obj is CacheEntryFromBufferKey other && Equals(other); + } + + public override int GetHashCode() + { + return HashCode.Combine(_texturePool, _samplerPool, IsSampler); + } + } + + /// + /// Array cache entry key. + /// + private readonly struct CacheEntryFromBufferKey : IEquatable + { + /// + /// Whether the entry is for an image. + /// + public readonly bool IsImage; + + /// + /// Texture or image target type. + /// + public readonly Target Target; + + /// + /// Word offset of the first handle on the constant buffer. + /// + public readonly int HandleIndex; + + /// + /// Number of entries of the array. + /// + public readonly int ArrayLength; + + private readonly TexturePool _texturePool; + private readonly SamplerPool _samplerPool; + + private readonly BufferBounds _textureBufferBounds; + + /// + /// Creates a new array cache entry. + /// + /// Whether the entry is for an image + /// Binding information for the array + /// Texture pool where the array textures are located + /// Sampler pool where the array samplers are located + /// Constant buffer bounds with the texture handles + public CacheEntryFromBufferKey( + bool isImage, + TextureBindingInfo bindingInfo, + TexturePool texturePool, + SamplerPool samplerPool, + ref BufferBounds textureBufferBounds) + { + IsImage = isImage; + Target = bindingInfo.Target; + HandleIndex = bindingInfo.Handle; + ArrayLength = bindingInfo.ArrayLength; + + _texturePool = texturePool; + _samplerPool = samplerPool; + + _textureBufferBounds = textureBufferBounds; + } + + /// + /// Checks if the texture and sampler pools matches the cached pools. + /// + /// Texture pool instance + /// Sampler pool instance + /// True if the pools match, false otherwise + private bool MatchesPools(TexturePool texturePool, SamplerPool samplerPool) + { + return _texturePool == texturePool && _samplerPool == samplerPool; + } + + /// + /// Checks if the cached constant buffer address and size matches. + /// + /// New buffer address and size + /// True if the address and size matches, false otherwise + private bool MatchesBufferBounds(BufferBounds textureBufferBounds) + { + return _textureBufferBounds.Equals(textureBufferBounds); + } + + public bool Equals(CacheEntryFromBufferKey other) + { + return IsImage == other.IsImage && + Target == other.Target && + HandleIndex == other.HandleIndex && + ArrayLength == other.ArrayLength && + MatchesPools(other._texturePool, other._samplerPool) && + MatchesBufferBounds(other._textureBufferBounds); + } + + public override bool Equals(object obj) + { + return obj is CacheEntryFromBufferKey other && Equals(other); + } + + public override int GetHashCode() + { + return _textureBufferBounds.Range.GetHashCode(); + } + } + + /// + /// Array cache entry from pool. + /// + private class CacheEntry + { + /// + /// All cached textures, along with their invalidated sequence number as value. + /// + public readonly Dictionary Textures; + + /// + /// Backend texture array if the entry is for a texture, otherwise null. + /// + public readonly ITextureArray TextureArray; + + /// + /// Backend image array if the entry is for an image, otherwise null. + /// + public readonly IImageArray ImageArray; + + /// + /// Texture pool where the array textures are located. + /// + protected readonly TexturePool TexturePool; + + /// + /// Sampler pool where the array samplers are located. + /// + protected readonly SamplerPool SamplerPool; + + private int _texturePoolSequence; + private int _samplerPoolSequence; + + /// + /// Creates a new array cache entry. + /// + /// Texture pool where the array textures are located + /// Sampler pool where the array samplers are located + private CacheEntry(TexturePool texturePool, SamplerPool samplerPool) + { + Textures = new Dictionary(); + + TexturePool = texturePool; + SamplerPool = samplerPool; + } + + /// + /// Creates a new array cache entry. + /// + /// Backend texture array + /// Texture pool where the array textures are located + /// Sampler pool where the array samplers are located + public CacheEntry(ITextureArray array, TexturePool texturePool, SamplerPool samplerPool) : this(texturePool, samplerPool) + { + TextureArray = array; + } + + /// + /// Creates a new array cache entry. + /// + /// Backend image array + /// Texture pool where the array textures are located + /// Sampler pool where the array samplers are located + public CacheEntry(IImageArray array, TexturePool texturePool, SamplerPool samplerPool) : this(texturePool, samplerPool) + { + ImageArray = array; + } + + /// + /// Synchronizes memory for all textures in the array. + /// + /// Indicates if the texture may be modified by the access + /// Indicates if the texture should be blacklisted for scaling + public void SynchronizeMemory(bool isStore, bool blacklistScale) + { + foreach (Texture texture in Textures.Keys) + { + texture.SynchronizeMemory(); + + if (isStore) + { + texture.SignalModified(); + } + + if (blacklistScale && texture.ScaleMode != TextureScaleMode.Blacklisted) + { + // Scaling textures used on arrays is currently not supported. + + texture.BlacklistScale(); + } + } + } + + /// + /// Clears all cached texture instances. + /// + public virtual void Reset() + { + Textures.Clear(); + } + + /// + /// Checks if any texture has been deleted since the last call to this method. + /// + /// True if one or more textures have been deleted, false otherwise + public bool ValidateTextures() + { + foreach ((Texture texture, int invalidatedSequence) in Textures) + { + if (texture.InvalidatedSequence != invalidatedSequence) + { + return false; + } + } + + return true; + } + + /// + /// Checks if the cached texture or sampler pool has been modified since the last call to this method. + /// + /// True if any used entries of the pool might have been modified, false otherwise + public bool TexturePoolModified() + { + return TexturePool.WasModified(ref _texturePoolSequence); + } + + /// + /// Checks if the cached texture or sampler pool has been modified since the last call to this method. + /// + /// True if any used entries of the pool might have been modified, false otherwise + public bool SamplerPoolModified() + { + return SamplerPool != null && SamplerPool.WasModified(ref _samplerPoolSequence); + } + } + + /// + /// Array cache entry from constant buffer. + /// + private class CacheEntryFromBuffer : CacheEntry + { + /// + /// Key for this entry on the cache. + /// + public readonly CacheEntryFromBufferKey Key; + + /// + /// Linked list node used on the texture bindings array cache. + /// + public LinkedListNode CacheNode; + + /// + /// Timestamp set on the last use of the array by the cache. + /// + public int CacheTimestamp; + + /// + /// All pool texture IDs along with their textures. + /// + public readonly Dictionary TextureIds; + + /// + /// All pool sampler IDs along with their samplers. + /// + public readonly Dictionary SamplerIds; + + private int[] _cachedTextureBuffer; + private int[] _cachedSamplerBuffer; + + private int _lastSequenceNumber; + + /// + /// Creates a new array cache entry. + /// + /// Key for this entry on the cache + /// Backend texture array + /// Texture pool where the array textures are located + /// Sampler pool where the array samplers are located + public CacheEntryFromBuffer(ref CacheEntryFromBufferKey key, ITextureArray array, TexturePool texturePool, SamplerPool samplerPool) : base(array, texturePool, samplerPool) + { + Key = key; + _lastSequenceNumber = -1; + TextureIds = new Dictionary(); + SamplerIds = new Dictionary(); + } + + /// + /// Creates a new array cache entry. + /// + /// Key for this entry on the cache + /// Backend image array + /// Texture pool where the array textures are located + /// Sampler pool where the array samplers are located + public CacheEntryFromBuffer(ref CacheEntryFromBufferKey key, IImageArray array, TexturePool texturePool, SamplerPool samplerPool) : base(array, texturePool, samplerPool) + { + Key = key; + _lastSequenceNumber = -1; + TextureIds = new Dictionary(); + SamplerIds = new Dictionary(); + } + + /// + public override void Reset() + { + base.Reset(); + TextureIds.Clear(); + SamplerIds.Clear(); + } + + /// + /// Updates the cached constant buffer data. + /// + /// Constant buffer data with the texture handles (and sampler handles, if they are combined) + /// Constant buffer data with the sampler handles + /// Whether and comes from different buffers + public void UpdateData(ReadOnlySpan cachedTextureBuffer, ReadOnlySpan cachedSamplerBuffer, bool separateSamplerBuffer) + { + _cachedTextureBuffer = cachedTextureBuffer.ToArray(); + _cachedSamplerBuffer = separateSamplerBuffer ? cachedSamplerBuffer.ToArray() : _cachedTextureBuffer; + } + + /// + /// Checks if the sequence number matches the one used on the last call to this method. + /// + /// Current sequence number + /// True if the sequence numbers match, false otherwise + public bool MatchesSequenceNumber(int currentSequenceNumber) + { + if (_lastSequenceNumber == currentSequenceNumber) + { + return true; + } + + _lastSequenceNumber = currentSequenceNumber; + + return false; + } + + /// + /// Checks if the buffer data matches the cached data. + /// + /// New texture buffer data + /// New sampler buffer data + /// Whether and comes from different buffers + /// Word offset of the sampler constant buffer handle that is used + /// True if the data matches, false otherwise + public bool MatchesBufferData( + ReadOnlySpan cachedTextureBuffer, + ReadOnlySpan cachedSamplerBuffer, + bool separateSamplerBuffer, + int samplerWordOffset) + { + if (_cachedTextureBuffer != null && cachedTextureBuffer.Length > _cachedTextureBuffer.Length) + { + cachedTextureBuffer = cachedTextureBuffer[.._cachedTextureBuffer.Length]; + } + + if (!_cachedTextureBuffer.AsSpan().SequenceEqual(cachedTextureBuffer)) + { + return false; + } + + if (separateSamplerBuffer) + { + if (_cachedSamplerBuffer == null || + _cachedSamplerBuffer.Length <= samplerWordOffset || + cachedSamplerBuffer.Length <= samplerWordOffset) + { + return false; + } + + int oldValue = _cachedSamplerBuffer[samplerWordOffset]; + int newValue = cachedSamplerBuffer[samplerWordOffset]; + + return oldValue == newValue; + } + + return true; + } + + /// + /// Checks if the cached texture or sampler pool has been modified since the last call to this method. + /// + /// True if any used entries of the pools might have been modified, false otherwise + public bool PoolsModified() + { + bool texturePoolModified = TexturePoolModified(); + bool samplerPoolModified = SamplerPoolModified(); + + // If both pools were not modified since the last check, we have nothing else to check. + if (!texturePoolModified && !samplerPoolModified) + { + return false; + } + + // If the pools were modified, let's check if any of the entries we care about changed. + + // Check if any of our cached textures changed on the pool. + foreach ((int textureId, (Texture texture, TextureDescriptor descriptor)) in TextureIds) + { + if (TexturePool.GetCachedItem(textureId) != texture || + (texture == null && TexturePool.IsValidId(textureId) && !TexturePool.GetDescriptorRef(textureId).Equals(descriptor))) + { + return true; + } + } + + // Check if any of our cached samplers changed on the pool. + if (SamplerPool != null) + { + foreach ((int samplerId, (Sampler sampler, SamplerDescriptor descriptor)) in SamplerIds) + { + if (SamplerPool.GetCachedItem(samplerId) != sampler || + (sampler == null && SamplerPool.IsValidId(samplerId) && !SamplerPool.GetDescriptorRef(samplerId).Equals(descriptor))) + { + return true; + } + } + } + + return false; + } + } + + private readonly Dictionary _cacheFromBuffer; + private readonly Dictionary _cacheFromPool; + private readonly LinkedList _lruCache; + + private int _currentTimestamp; + + /// + /// Creates a new instance of the texture bindings array cache. + /// + /// GPU context + /// GPU channel + public TextureBindingsArrayCache(GpuContext context, GpuChannel channel) + { + _context = context; + _channel = channel; + _cacheFromBuffer = new Dictionary(); + _cacheFromPool = new Dictionary(); + _lruCache = new LinkedList(); + } + + /// + /// Updates a texture array bindings and textures. + /// + /// Texture pool + /// Sampler pool + /// Shader stage where the array is used + /// Shader stage index where the array is used + /// Texture constant buffer index + /// Sampler handles source + /// Array binding information + public void UpdateTextureArray( + TexturePool texturePool, + SamplerPool samplerPool, + ShaderStage stage, + int stageIndex, + int textureBufferIndex, + SamplerIndex samplerIndex, + in TextureBindingInfo bindingInfo) + { + Update(texturePool, samplerPool, stage, stageIndex, textureBufferIndex, isImage: false, samplerIndex, bindingInfo); + } + + /// + /// Updates a image array bindings and textures. + /// + /// Texture pool + /// Shader stage where the array is used + /// Shader stage index where the array is used + /// Texture constant buffer index + /// Array binding information + public void UpdateImageArray(TexturePool texturePool, ShaderStage stage, int stageIndex, int textureBufferIndex, in TextureBindingInfo bindingInfo) + { + Update(texturePool, null, stage, stageIndex, textureBufferIndex, isImage: true, SamplerIndex.ViaHeaderIndex, bindingInfo); + } + + /// + /// Updates a texture or image array bindings and textures. + /// + /// Texture pool + /// Sampler pool + /// Shader stage where the array is used + /// Shader stage index where the array is used + /// Texture constant buffer index + /// Whether the array is a image or texture array + /// Sampler handles source + /// Array binding information + private void Update( + TexturePool texturePool, + SamplerPool samplerPool, + ShaderStage stage, + int stageIndex, + int textureBufferIndex, + bool isImage, + SamplerIndex samplerIndex, + in TextureBindingInfo bindingInfo) + { + if (IsDirectHandleType(bindingInfo.Handle)) + { + UpdateFromPool(texturePool, samplerPool, stage, isImage, bindingInfo); + } + else + { + UpdateFromBuffer(texturePool, samplerPool, stage, stageIndex, textureBufferIndex, isImage, samplerIndex, bindingInfo); + } + } + + /// + /// Updates a texture or image array bindings and textures from a texture or sampler pool. + /// + /// Texture pool + /// Sampler pool + /// Shader stage where the array is used + /// Whether the array is a image or texture array + /// Array binding information + private void UpdateFromPool(TexturePool texturePool, SamplerPool samplerPool, ShaderStage stage, bool isImage, in TextureBindingInfo bindingInfo) + { + CacheEntry entry = GetOrAddEntry(texturePool, samplerPool, bindingInfo, isImage, out bool isNewEntry); + + bool isSampler = bindingInfo.IsSamplerOnly; + bool poolModified = isSampler ? entry.SamplerPoolModified() : entry.TexturePoolModified(); + bool isStore = bindingInfo.Flags.HasFlag(TextureUsageFlags.ImageStore); + bool resScaleUnsupported = bindingInfo.Flags.HasFlag(TextureUsageFlags.ResScaleUnsupported); + + if (!poolModified && !isNewEntry && entry.ValidateTextures()) + { + entry.SynchronizeMemory(isStore, resScaleUnsupported); + + if (isImage) + { + SetImageArray(stage, bindingInfo, entry.ImageArray); + } + else + { + SetTextureArray(stage, bindingInfo, entry.TextureArray); + } + + return; + } + + if (!isNewEntry) + { + entry.Reset(); + } + + int length = (isSampler ? samplerPool.MaximumId : texturePool.MaximumId) + 1; + length = Math.Min(length, bindingInfo.ArrayLength); + + ISampler[] samplers = isImage ? null : new ISampler[bindingInfo.ArrayLength]; + ITexture[] textures = new ITexture[bindingInfo.ArrayLength]; + + for (int index = 0; index < length; index++) + { + Texture texture = null; + Sampler sampler = null; + + if (isSampler) + { + sampler = samplerPool?.Get(index); + } + else + { + ref readonly TextureDescriptor descriptor = ref texturePool.GetForBinding(index, bindingInfo.FormatInfo, out texture); + + if (texture != null) + { + entry.Textures[texture] = texture.InvalidatedSequence; + + if (isStore) + { + texture.SignalModified(); + } + + if (resScaleUnsupported && texture.ScaleMode != TextureScaleMode.Blacklisted) + { + // Scaling textures used on arrays is currently not supported. + + texture.BlacklistScale(); + } + } + } + + ITexture hostTexture = texture?.GetTargetTexture(bindingInfo.Target); + ISampler hostSampler = sampler?.GetHostSampler(texture); + + if (hostTexture != null && texture.Target == Target.TextureBuffer) + { + // Ensure that the buffer texture is using the correct buffer as storage. + // Buffers are frequently re-created to accommodate larger data, so we need to re-bind + // to ensure we're not using a old buffer that was already deleted. + if (isImage) + { + _channel.BufferManager.SetBufferTextureStorage(stage, entry.ImageArray, hostTexture, texture.Range, bindingInfo, index); + } + else + { + _channel.BufferManager.SetBufferTextureStorage(stage, entry.TextureArray, hostTexture, texture.Range, bindingInfo, index); + } + } + else if (isImage) + { + textures[index] = hostTexture; + } + else + { + samplers[index] = hostSampler; + textures[index] = hostTexture; + } + } + + if (isImage) + { + entry.ImageArray.SetImages(0, textures); + + SetImageArray(stage, bindingInfo, entry.ImageArray); + } + else + { + entry.TextureArray.SetSamplers(0, samplers); + entry.TextureArray.SetTextures(0, textures); + + SetTextureArray(stage, bindingInfo, entry.TextureArray); + } + } + + /// + /// Updates a texture or image array bindings and textures from constant buffer handles. + /// + /// Texture pool + /// Sampler pool + /// Shader stage where the array is used + /// Shader stage index where the array is used + /// Texture constant buffer index + /// Whether the array is a image or texture array + /// Sampler handles source + /// Array binding information + private void UpdateFromBuffer( + TexturePool texturePool, + SamplerPool samplerPool, + ShaderStage stage, + int stageIndex, + int textureBufferIndex, + bool isImage, + SamplerIndex samplerIndex, + in TextureBindingInfo bindingInfo) + { + (textureBufferIndex, int samplerBufferIndex) = TextureHandle.UnpackSlots(bindingInfo.CbufSlot, textureBufferIndex); + + bool separateSamplerBuffer = textureBufferIndex != samplerBufferIndex; + bool isCompute = stage == ShaderStage.Compute; + + ref BufferBounds textureBufferBounds = ref _channel.BufferManager.GetUniformBufferBounds(isCompute, stageIndex, textureBufferIndex); + ref BufferBounds samplerBufferBounds = ref _channel.BufferManager.GetUniformBufferBounds(isCompute, stageIndex, samplerBufferIndex); + + CacheEntryFromBuffer entry = GetOrAddEntry( + texturePool, + samplerPool, + bindingInfo, + isImage, + ref textureBufferBounds, + out bool isNewEntry); + + bool poolsModified = entry.PoolsModified(); + bool isStore = bindingInfo.Flags.HasFlag(TextureUsageFlags.ImageStore); + bool resScaleUnsupported = bindingInfo.Flags.HasFlag(TextureUsageFlags.ResScaleUnsupported); + + ReadOnlySpan cachedTextureBuffer; + ReadOnlySpan cachedSamplerBuffer; + + if (!poolsModified && !isNewEntry && entry.ValidateTextures()) + { + if (entry.MatchesSequenceNumber(_context.SequenceNumber)) + { + entry.SynchronizeMemory(isStore, resScaleUnsupported); + + if (isImage) + { + SetImageArray(stage, bindingInfo, entry.ImageArray); + } + else + { + SetTextureArray(stage, bindingInfo, entry.TextureArray); + } + + return; + } + + cachedTextureBuffer = MemoryMarshal.Cast(_channel.MemoryManager.Physical.GetSpan(textureBufferBounds.Range)); + + if (separateSamplerBuffer) + { + cachedSamplerBuffer = MemoryMarshal.Cast(_channel.MemoryManager.Physical.GetSpan(samplerBufferBounds.Range)); + } + else + { + cachedSamplerBuffer = cachedTextureBuffer; + } + + (_, int samplerWordOffset, _) = TextureHandle.UnpackOffsets(bindingInfo.Handle); + + if (entry.MatchesBufferData(cachedTextureBuffer, cachedSamplerBuffer, separateSamplerBuffer, samplerWordOffset)) + { + entry.SynchronizeMemory(isStore, resScaleUnsupported); + + if (isImage) + { + SetImageArray(stage, bindingInfo, entry.ImageArray); + } + else + { + SetTextureArray(stage, bindingInfo, entry.TextureArray); + } + + return; + } + } + else + { + cachedTextureBuffer = MemoryMarshal.Cast(_channel.MemoryManager.Physical.GetSpan(textureBufferBounds.Range)); + + if (separateSamplerBuffer) + { + cachedSamplerBuffer = MemoryMarshal.Cast(_channel.MemoryManager.Physical.GetSpan(samplerBufferBounds.Range)); + } + else + { + cachedSamplerBuffer = cachedTextureBuffer; + } + } + + if (!isNewEntry) + { + entry.Reset(); + } + + entry.UpdateData(cachedTextureBuffer, cachedSamplerBuffer, separateSamplerBuffer); + + ISampler[] samplers = isImage ? null : new ISampler[bindingInfo.ArrayLength]; + ITexture[] textures = new ITexture[bindingInfo.ArrayLength]; + + for (int index = 0; index < bindingInfo.ArrayLength; index++) + { + int handleIndex = bindingInfo.Handle + index * (Constants.TextureHandleSizeInBytes / sizeof(int)); + int packedId = TextureHandle.ReadPackedId(handleIndex, cachedTextureBuffer, cachedSamplerBuffer); + int textureId = TextureHandle.UnpackTextureId(packedId); + int samplerId; + + if (samplerIndex == SamplerIndex.ViaHeaderIndex) + { + samplerId = textureId; + } + else + { + samplerId = TextureHandle.UnpackSamplerId(packedId); + } + + ref readonly TextureDescriptor descriptor = ref texturePool.GetForBinding(textureId, bindingInfo.FormatInfo, out Texture texture); + + if (texture != null) + { + entry.Textures[texture] = texture.InvalidatedSequence; + + if (isStore) + { + texture.SignalModified(); + } + + if (resScaleUnsupported && texture.ScaleMode != TextureScaleMode.Blacklisted) + { + // Scaling textures used on arrays is currently not supported. + + texture.BlacklistScale(); + } + } + + entry.TextureIds[textureId] = (texture, descriptor); + + ITexture hostTexture = texture?.GetTargetTexture(bindingInfo.Target); + ISampler hostSampler = null; + + if (!isImage && bindingInfo.Target != Target.TextureBuffer) + { + Sampler sampler = samplerPool?.Get(samplerId); + + entry.SamplerIds[samplerId] = (sampler, samplerPool?.GetDescriptorRef(samplerId) ?? default); + + hostSampler = sampler?.GetHostSampler(texture); + } + + if (hostTexture != null && texture.Target == Target.TextureBuffer) + { + // Ensure that the buffer texture is using the correct buffer as storage. + // Buffers are frequently re-created to accommodate larger data, so we need to re-bind + // to ensure we're not using a old buffer that was already deleted. + if (isImage) + { + _channel.BufferManager.SetBufferTextureStorage(stage, entry.ImageArray, hostTexture, texture.Range, bindingInfo, index); + } + else + { + _channel.BufferManager.SetBufferTextureStorage(stage, entry.TextureArray, hostTexture, texture.Range, bindingInfo, index); + } + } + else if (isImage) + { + textures[index] = hostTexture; + } + else + { + samplers[index] = hostSampler; + textures[index] = hostTexture; + } + } + + if (isImage) + { + entry.ImageArray.SetImages(0, textures); + + SetImageArray(stage, bindingInfo, entry.ImageArray); + } + else + { + entry.TextureArray.SetSamplers(0, samplers); + entry.TextureArray.SetTextures(0, textures); + + SetTextureArray(stage, bindingInfo, entry.TextureArray); + } + } + + /// + /// Updates a texture array binding on the host. + /// + /// Shader stage where the array is used + /// Array binding information + /// Texture array + private void SetTextureArray(ShaderStage stage, in TextureBindingInfo bindingInfo, ITextureArray array) + { + if (bindingInfo.Set >= _context.Capabilities.ExtraSetBaseIndex && _context.Capabilities.MaximumExtraSets != 0) + { + _context.Renderer.Pipeline.SetTextureArraySeparate(stage, bindingInfo.Set, array); + } + else + { + _context.Renderer.Pipeline.SetTextureArray(stage, bindingInfo.Binding, array); + } + } + + /// + /// Updates a image array binding on the host. + /// + /// Shader stage where the array is used + /// Array binding information + /// Image array + private void SetImageArray(ShaderStage stage, in TextureBindingInfo bindingInfo, IImageArray array) + { + if (bindingInfo.Set >= _context.Capabilities.ExtraSetBaseIndex && _context.Capabilities.MaximumExtraSets != 0) + { + _context.Renderer.Pipeline.SetImageArraySeparate(stage, bindingInfo.Set, array); + } + else + { + _context.Renderer.Pipeline.SetImageArray(stage, bindingInfo.Binding, array); + } + } + + /// + /// Gets a cached texture entry from pool, or creates a new one if not found. + /// + /// Texture pool + /// Sampler pool + /// Array binding information + /// Whether the array is a image or texture array + /// Whether a new entry was created, or an existing one was returned + /// Cache entry + private CacheEntry GetOrAddEntry( + TexturePool texturePool, + SamplerPool samplerPool, + in TextureBindingInfo bindingInfo, + bool isImage, + out bool isNew) + { + CacheEntryFromPoolKey key = new CacheEntryFromPoolKey(isImage, bindingInfo, texturePool, samplerPool); + + isNew = !_cacheFromPool.TryGetValue(key, out CacheEntry entry); + + if (isNew) + { + int arrayLength = bindingInfo.ArrayLength; + + if (isImage) + { + IImageArray array = _context.Renderer.CreateImageArray(arrayLength, bindingInfo.Target == Target.TextureBuffer); + + _cacheFromPool.Add(key, entry = new CacheEntry(array, texturePool, samplerPool)); + } + else + { + ITextureArray array = _context.Renderer.CreateTextureArray(arrayLength, bindingInfo.Target == Target.TextureBuffer); + + _cacheFromPool.Add(key, entry = new CacheEntry(array, texturePool, samplerPool)); + } + } + + return entry; + } + + /// + /// Gets a cached texture entry from constant buffer, or creates a new one if not found. + /// + /// Texture pool + /// Sampler pool + /// Array binding information + /// Whether the array is a image or texture array + /// Constant buffer bounds with the texture handles + /// Whether a new entry was created, or an existing one was returned + /// Cache entry + private CacheEntryFromBuffer GetOrAddEntry( + TexturePool texturePool, + SamplerPool samplerPool, + in TextureBindingInfo bindingInfo, + bool isImage, + ref BufferBounds textureBufferBounds, + out bool isNew) + { + CacheEntryFromBufferKey key = new CacheEntryFromBufferKey( + isImage, + bindingInfo, + texturePool, + samplerPool, + ref textureBufferBounds); + + isNew = !_cacheFromBuffer.TryGetValue(key, out CacheEntryFromBuffer entry); + + if (isNew) + { + int arrayLength = bindingInfo.ArrayLength; + + if (isImage) + { + IImageArray array = _context.Renderer.CreateImageArray(arrayLength, bindingInfo.Target == Target.TextureBuffer); + + _cacheFromBuffer.Add(key, entry = new CacheEntryFromBuffer(ref key, array, texturePool, samplerPool)); + } + else + { + ITextureArray array = _context.Renderer.CreateTextureArray(arrayLength, bindingInfo.Target == Target.TextureBuffer); + + _cacheFromBuffer.Add(key, entry = new CacheEntryFromBuffer(ref key, array, texturePool, samplerPool)); + } + } + + if (entry.CacheNode != null) + { + _lruCache.Remove(entry.CacheNode); + _lruCache.AddLast(entry.CacheNode); + } + else + { + entry.CacheNode = _lruCache.AddLast(entry); + } + + entry.CacheTimestamp = ++_currentTimestamp; + + RemoveLeastUsedEntries(); + + return entry; + } + + /// + /// Remove entries from the cache that have not been used for some time. + /// + private void RemoveLeastUsedEntries() + { + LinkedListNode nextNode = _lruCache.First; + + while (nextNode != null && _currentTimestamp - nextNode.Value.CacheTimestamp >= MinDeltaForRemoval) + { + LinkedListNode toRemove = nextNode; + nextNode = nextNode.Next; + _cacheFromBuffer.Remove(toRemove.Value.Key); + _lruCache.Remove(toRemove); + + if (toRemove.Value.Key.IsImage) + { + toRemove.Value.ImageArray.Dispose(); + } + else + { + toRemove.Value.TextureArray.Dispose(); + } + } + } + + /// + /// Removes all cached texture arrays matching the specified texture pool. + /// + /// Texture pool + public void RemoveAllWithPool(IPool pool) + { + List keysToRemove = null; + + foreach ((CacheEntryFromPoolKey key, CacheEntry entry) in _cacheFromPool) + { + if (key.MatchesPool(pool)) + { + (keysToRemove ??= new()).Add(key); + + if (key.IsImage) + { + entry.ImageArray.Dispose(); + } + else + { + entry.TextureArray.Dispose(); + } + } + } + + if (keysToRemove != null) + { + foreach (CacheEntryFromPoolKey key in keysToRemove) + { + _cacheFromPool.Remove(key); + } + } + } + + /// + /// Checks if a handle indicates the binding should have all its textures sourced directly from a pool. + /// + /// Handle to check + /// True if the handle represents direct pool access, false otherwise + private static bool IsDirectHandleType(int handle) + { + (_, _, TextureHandleType type) = TextureHandle.UnpackOffsets(handle); + + return type == TextureHandleType.Direct; + } + } +} diff --git a/src/Ryujinx.Graphics.Gpu/Image/TextureBindingsManager.cs b/src/Ryujinx.Graphics.Gpu/Image/TextureBindingsManager.cs index 963bd947d..f96ddfb1b 100644 --- a/src/Ryujinx.Graphics.Gpu/Image/TextureBindingsManager.cs +++ b/src/Ryujinx.Graphics.Gpu/Image/TextureBindingsManager.cs @@ -34,6 +34,8 @@ namespace Ryujinx.Graphics.Gpu.Image private readonly TexturePoolCache _texturePoolCache; private readonly SamplerPoolCache _samplerPoolCache; + private readonly TextureBindingsArrayCache _bindingsArrayCache; + private TexturePool _cachedTexturePool; private SamplerPool _cachedSamplerPool; @@ -56,6 +58,8 @@ namespace Ryujinx.Graphics.Gpu.Image private TextureState[] _textureState; private TextureState[] _imageState; + private int[] _textureCounts; + private int _texturePoolSequence; private int _samplerPoolSequence; @@ -68,12 +72,14 @@ namespace Ryujinx.Graphics.Gpu.Image /// /// The GPU context that the texture bindings manager belongs to /// The GPU channel that the texture bindings manager belongs to + /// Cache of texture array bindings /// Texture pools cache used to get texture pools from /// Sampler pools cache used to get sampler pools from /// True if the bindings manager is used for the compute engine public TextureBindingsManager( GpuContext context, GpuChannel channel, + TextureBindingsArrayCache bindingsArrayCache, TexturePoolCache texturePoolCache, SamplerPoolCache samplerPoolCache, bool isCompute) @@ -85,6 +91,8 @@ namespace Ryujinx.Graphics.Gpu.Image _isCompute = isCompute; + _bindingsArrayCache = bindingsArrayCache; + int stages = isCompute ? 1 : Constants.ShaderStages; _textureBindings = new TextureBindingInfo[stages][]; @@ -95,9 +103,11 @@ namespace Ryujinx.Graphics.Gpu.Image for (int stage = 0; stage < stages; stage++) { - _textureBindings[stage] = new TextureBindingInfo[InitialTextureStateSize]; - _imageBindings[stage] = new TextureBindingInfo[InitialImageStateSize]; + _textureBindings[stage] = Array.Empty(); + _imageBindings[stage] = Array.Empty(); } + + _textureCounts = Array.Empty(); } /// @@ -109,6 +119,8 @@ namespace Ryujinx.Graphics.Gpu.Image _textureBindings = bindings.TextureBindings; _imageBindings = bindings.ImageBindings; + _textureCounts = bindings.TextureCounts; + SetMaxBindings(bindings.MaxTextureBinding, bindings.MaxImageBinding); } @@ -175,7 +187,9 @@ namespace Ryujinx.Graphics.Gpu.Image { (TexturePool texturePool, SamplerPool samplerPool) = GetPools(); - return (texturePool.Get(textureId), samplerPool.Get(samplerId)); + Sampler sampler = samplerPool?.Get(samplerId); + + return (texturePool.Get(textureId, sampler?.IsSrgb ?? true), sampler); } /// @@ -401,27 +415,6 @@ namespace Ryujinx.Graphics.Gpu.Image } } -#pragma warning disable IDE0051 // Remove unused private member - /// - /// Counts the total number of texture bindings used by all shader stages. - /// - /// The total amount of textures used - private int GetTextureBindingsCount() - { - int count = 0; - - foreach (TextureBindingInfo[] textureInfo in _textureBindings) - { - if (textureInfo != null) - { - count += textureInfo.Length; - } - } - - return count; - } -#pragma warning restore IDE0051 - /// /// Ensures that the texture bindings are visible to the host GPU. /// Note: this actually performs the binding using the host graphics API. @@ -465,6 +458,13 @@ namespace Ryujinx.Graphics.Gpu.Image TextureBindingInfo bindingInfo = _textureBindings[stageIndex][index]; TextureUsageFlags usageFlags = bindingInfo.Flags; + if (bindingInfo.ArrayLength > 1) + { + _bindingsArrayCache.UpdateTextureArray(texturePool, samplerPool, stage, stageIndex, _textureBufferIndex, _samplerIndex, bindingInfo); + + continue; + } + (int textureBufferIndex, int samplerBufferIndex) = TextureHandle.UnpackSlots(bindingInfo.CbufSlot, _textureBufferIndex); UpdateCachedBuffer(stageIndex, ref cachedTextureBufferIndex, ref cachedSamplerBufferIndex, ref cachedTextureBuffer, ref cachedSamplerBuffer, textureBufferIndex, samplerBufferIndex); @@ -510,12 +510,12 @@ namespace Ryujinx.Graphics.Gpu.Image state.TextureHandle = textureId; state.SamplerHandle = samplerId; - ref readonly TextureDescriptor descriptor = ref texturePool.GetForBinding(textureId, out Texture texture); + Sampler sampler = samplerPool?.Get(samplerId); + + ref readonly TextureDescriptor descriptor = ref texturePool.GetForBinding(textureId, sampler?.IsSrgb ?? true, out Texture texture); specStateMatches &= specState.MatchesTexture(stage, index, descriptor); - Sampler sampler = samplerPool?.Get(samplerId); - ITexture hostTexture = texture?.GetTargetTexture(bindingInfo.Target); ISampler hostSampler = sampler?.GetHostSampler(texture); @@ -524,7 +524,7 @@ namespace Ryujinx.Graphics.Gpu.Image // Ensure that the buffer texture is using the correct buffer as storage. // Buffers are frequently re-created to accommodate larger data, so we need to re-bind // to ensure we're not using a old buffer that was already deleted. - _channel.BufferManager.SetBufferTextureStorage(stage, hostTexture, texture.Range, bindingInfo, bindingInfo.Format, false); + _channel.BufferManager.SetBufferTextureStorage(stage, hostTexture, texture.Range, bindingInfo, false); // Cache is not used for buffer texture, it must always rebind. state.CachedTexture = null; @@ -582,7 +582,7 @@ namespace Ryujinx.Graphics.Gpu.Image } // Scales for images appear after the texture ones. - int baseScaleIndex = _textureBindings[stageIndex].Length; + int baseScaleIndex = _textureCounts[stageIndex]; int cachedTextureBufferIndex = -1; int cachedSamplerBufferIndex = -1; @@ -595,6 +595,14 @@ namespace Ryujinx.Graphics.Gpu.Image { TextureBindingInfo bindingInfo = _imageBindings[stageIndex][index]; TextureUsageFlags usageFlags = bindingInfo.Flags; + + if (bindingInfo.ArrayLength > 1) + { + _bindingsArrayCache.UpdateImageArray(pool, stage, stageIndex, _textureBufferIndex, bindingInfo); + + continue; + } + int scaleIndex = baseScaleIndex + index; (int textureBufferIndex, int samplerBufferIndex) = TextureHandle.UnpackSlots(bindingInfo.CbufSlot, _textureBufferIndex); @@ -610,6 +618,7 @@ namespace Ryujinx.Graphics.Gpu.Image if (!poolModified && state.TextureHandle == textureId && + state.ImageFormat == bindingInfo.FormatInfo.Format && state.CachedTexture != null && state.CachedTexture.InvalidatedSequence == state.InvalidatedSequence) { @@ -620,29 +629,25 @@ namespace Ryujinx.Graphics.Gpu.Image if (isStore) { - cachedTexture?.SignalModified(); + cachedTexture.SignalModified(); } - Format format = bindingInfo.Format == 0 ? cachedTexture.Format : bindingInfo.Format; - - if (state.ImageFormat != format || - ((usageFlags & TextureUsageFlags.NeedsScaleValue) != 0 && - UpdateScale(state.CachedTexture, usageFlags, scaleIndex, stage))) + if ((usageFlags & TextureUsageFlags.NeedsScaleValue) != 0 && UpdateScale(state.CachedTexture, usageFlags, scaleIndex, stage)) { ITexture hostTextureRebind = state.CachedTexture.GetTargetTexture(bindingInfo.Target); state.Texture = hostTextureRebind; - state.ImageFormat = format; - _context.Renderer.Pipeline.SetImage(bindingInfo.Binding, hostTextureRebind, format); + _context.Renderer.Pipeline.SetImage(stage, bindingInfo.Binding, hostTextureRebind); } continue; } state.TextureHandle = textureId; + state.ImageFormat = bindingInfo.FormatInfo.Format; - ref readonly TextureDescriptor descriptor = ref pool.GetForBinding(textureId, out Texture texture); + ref readonly TextureDescriptor descriptor = ref pool.GetForBinding(textureId, bindingInfo.FormatInfo, out Texture texture); specStateMatches &= specState.MatchesImage(stage, index, descriptor); @@ -654,14 +659,7 @@ namespace Ryujinx.Graphics.Gpu.Image // Buffers are frequently re-created to accommodate larger data, so we need to re-bind // to ensure we're not using a old buffer that was already deleted. - Format format = bindingInfo.Format; - - if (format == 0 && texture != null) - { - format = texture.Format; - } - - _channel.BufferManager.SetBufferTextureStorage(stage, hostTexture, texture.Range, bindingInfo, format, true); + _channel.BufferManager.SetBufferTextureStorage(stage, hostTexture, texture.Range, bindingInfo, true); // Cache is not used for buffer texture, it must always rebind. state.CachedTexture = null; @@ -683,16 +681,7 @@ namespace Ryujinx.Graphics.Gpu.Image { state.Texture = hostTexture; - Format format = bindingInfo.Format; - - if (format == 0 && texture != null) - { - format = texture.Format; - } - - state.ImageFormat = format; - - _context.Renderer.Pipeline.SetImage(bindingInfo.Binding, hostTexture, format); + _context.Renderer.Pipeline.SetImage(stage, bindingInfo.Binding, hostTexture); } state.CachedTexture = texture; @@ -728,7 +717,7 @@ namespace Ryujinx.Graphics.Gpu.Image ulong poolAddress = _channel.MemoryManager.Translate(poolGpuVa); - TexturePool texturePool = _texturePoolCache.FindOrCreate(_channel, poolAddress, maximumId); + TexturePool texturePool = _texturePoolCache.FindOrCreate(_channel, poolAddress, maximumId, _bindingsArrayCache); TextureDescriptor descriptor; @@ -766,7 +755,7 @@ namespace Ryujinx.Graphics.Gpu.Image ? _channel.BufferManager.GetComputeUniformBufferAddress(textureBufferIndex) : _channel.BufferManager.GetGraphicsUniformBufferAddress(stageIndex, textureBufferIndex); - int handle = textureBufferAddress != 0 + int handle = textureBufferAddress != MemoryManager.PteUnmapped ? _channel.MemoryManager.Physical.Read(textureBufferAddress + (uint)textureWordOffset * 4) : 0; @@ -786,7 +775,7 @@ namespace Ryujinx.Graphics.Gpu.Image ? _channel.BufferManager.GetComputeUniformBufferAddress(samplerBufferIndex) : _channel.BufferManager.GetGraphicsUniformBufferAddress(stageIndex, samplerBufferIndex); - samplerHandle = samplerBufferAddress != 0 + samplerHandle = samplerBufferAddress != MemoryManager.PteUnmapped ? _channel.MemoryManager.Physical.Read(samplerBufferAddress + (uint)samplerWordOffset * 4) : 0; } @@ -824,7 +813,7 @@ namespace Ryujinx.Graphics.Gpu.Image if (poolAddress != MemoryManager.PteUnmapped) { - texturePool = _texturePoolCache.FindOrCreate(_channel, poolAddress, _texturePoolMaximumId); + texturePool = _texturePoolCache.FindOrCreate(_channel, poolAddress, _texturePoolMaximumId, _bindingsArrayCache); _texturePool = texturePool; } } @@ -835,7 +824,7 @@ namespace Ryujinx.Graphics.Gpu.Image if (poolAddress != MemoryManager.PteUnmapped) { - samplerPool = _samplerPoolCache.FindOrCreate(_channel, poolAddress, _samplerPoolMaximumId); + samplerPool = _samplerPoolCache.FindOrCreate(_channel, poolAddress, _samplerPoolMaximumId, _bindingsArrayCache); _samplerPool = samplerPool; } } diff --git a/src/Ryujinx.Graphics.Gpu/Image/TextureCache.cs b/src/Ryujinx.Graphics.Gpu/Image/TextureCache.cs index 6b92c0aaf..2cfd9af5b 100644 --- a/src/Ryujinx.Graphics.Gpu/Image/TextureCache.cs +++ b/src/Ryujinx.Graphics.Gpu/Image/TextureCache.cs @@ -8,6 +8,7 @@ using Ryujinx.Graphics.Texture; using Ryujinx.Memory.Range; using System; using System.Collections.Generic; +using System.Threading; namespace Ryujinx.Graphics.Gpu.Image { @@ -39,6 +40,8 @@ namespace Ryujinx.Graphics.Gpu.Image private readonly MultiRangeList _textures; private readonly HashSet _partiallyMappedTextures; + private readonly ReaderWriterLockSlim _texturesLock; + private Texture[] _textureOverlaps; private OverlapInfo[] _overlapInfo; @@ -57,12 +60,23 @@ namespace Ryujinx.Graphics.Gpu.Image _textures = new MultiRangeList(); _partiallyMappedTextures = new HashSet(); + _texturesLock = new ReaderWriterLockSlim(); + _textureOverlaps = new Texture[OverlapsBufferInitialCapacity]; _overlapInfo = new OverlapInfo[OverlapsBufferInitialCapacity]; _cache = new AutoDeleteCache(); } + /// + /// Initializes the cache, setting the maximum texture capacity for the specified GPU context. + /// + /// The amount of physical CPU Memory Avaiable on the device. + public void Initialize(ulong cpuMemorySize) + { + _cache.Initialize(_context, cpuMemorySize); + } + /// /// Handles marking of textures written to a memory region being (partially) remapped. /// @@ -75,10 +89,16 @@ namespace Ryujinx.Graphics.Gpu.Image MultiRange unmapped = ((MemoryManager)sender).GetPhysicalRegions(e.Address, e.Size); - lock (_textures) + _texturesLock.EnterReadLock(); + + try { overlapCount = _textures.FindOverlaps(unmapped, ref overlaps); } + finally + { + _texturesLock.ExitReadLock(); + } if (overlapCount > 0) { @@ -217,7 +237,18 @@ namespace Ryujinx.Graphics.Gpu.Image public bool UpdateMapping(Texture texture, MultiRange range) { // There cannot be an existing texture compatible with this mapping in the texture cache already. - int overlapCount = _textures.FindOverlaps(range, ref _textureOverlaps); + int overlapCount; + + _texturesLock.EnterReadLock(); + + try + { + overlapCount = _textures.FindOverlaps(range, ref _textureOverlaps); + } + finally + { + _texturesLock.ExitReadLock(); + } for (int i = 0; i < overlapCount; i++) { @@ -231,11 +262,20 @@ namespace Ryujinx.Graphics.Gpu.Image } } - _textures.Remove(texture); + _texturesLock.EnterWriteLock(); - texture.ReplaceRange(range); + try + { + _textures.Remove(texture); - _textures.Add(texture); + texture.ReplaceRange(range); + + _textures.Add(texture); + } + finally + { + _texturesLock.ExitWriteLock(); + } return true; } @@ -316,6 +356,53 @@ namespace Ryujinx.Graphics.Gpu.Image return texture; } + /// + /// Tries to find an existing texture, or create a new one if not found. + /// + /// GPU memory manager where the texture is mapped + /// Format of the texture + /// GPU virtual address of the texture + /// Texture width in bytes + /// Texture height + /// Texture stride if linear, otherwise ignored + /// Indicates if the texture is linear or block linear + /// GOB blocks in Y for block linear textures + /// GOB blocks in Z for 3D block linear textures + /// The texture + public Texture FindOrCreateTexture( + MemoryManager memoryManager, + FormatInfo formatInfo, + ulong gpuAddress, + int xCount, + int yCount, + int stride, + bool isLinear, + int gobBlocksInY, + int gobBlocksInZ) + { + TextureInfo info = new( + gpuAddress, + xCount / formatInfo.BytesPerPixel, + yCount, + 1, + 1, + 1, + 1, + stride, + isLinear, + gobBlocksInY, + gobBlocksInZ, + 1, + Target.Texture2D, + formatInfo); + + Texture texture = FindOrCreateTexture(memoryManager, TextureSearchFlags.ForCopy, info, 0, sizeHint: new Size(xCount, yCount, 1)); + + texture?.SynchronizeMemory(); + + return texture; + } + /// /// Tries to find an existing texture, or create a new one if not found. /// @@ -437,13 +524,11 @@ namespace Ryujinx.Graphics.Gpu.Image int gobBlocksInY = dsState.MemoryLayout.UnpackGobBlocksInY(); int gobBlocksInZ = dsState.MemoryLayout.UnpackGobBlocksInZ(); + layered &= size.UnpackIsLayered(); + Target target; - if (dsState.MemoryLayout.UnpackIsTarget3D()) - { - target = Target.Texture3D; - } - else if ((samplesInX | samplesInY) != 1) + if ((samplesInX | samplesInY) != 1) { target = size.Depth > 1 && layered ? Target.Texture2DMultisampleArray @@ -611,11 +696,17 @@ namespace Ryujinx.Graphics.Gpu.Image int sameAddressOverlapsCount; - lock (_textures) + _texturesLock.EnterReadLock(); + + try { // Try to find a perfect texture match, with the same address and parameters. sameAddressOverlapsCount = _textures.FindOverlaps(address, ref _textureOverlaps); } + finally + { + _texturesLock.ExitReadLock(); + } Texture texture = null; @@ -698,10 +789,16 @@ namespace Ryujinx.Graphics.Gpu.Image if (info.Target != Target.TextureBuffer) { - lock (_textures) + _texturesLock.EnterReadLock(); + + try { overlapsCount = _textures.FindOverlaps(range.Value, ref _textureOverlaps); } + finally + { + _texturesLock.ExitReadLock(); + } } if (_overlapInfo.Length != _textureOverlaps.Length) @@ -790,8 +887,12 @@ namespace Ryujinx.Graphics.Gpu.Image texture = new Texture(_context, _physicalMemory, info, sizeInfo, range.Value, scaleMode); + // If the new texture is larger than the existing one, we need to fill the remaining space with CPU data, + // otherwise we only need the data that is copied from the existing texture, without loading the CPU data. + bool updateNewTexture = texture.Width > overlap.Width || texture.Height > overlap.Height; + texture.InitializeGroup(true, true, new List()); - texture.InitializeData(false, false); + texture.InitializeData(false, updateNewTexture); overlap.SynchronizeMemory(); overlap.CreateCopyDependency(texture, oInfo.FirstLayer, oInfo.FirstLevel, true); @@ -1021,10 +1122,16 @@ namespace Ryujinx.Graphics.Gpu.Image _cache.Add(texture); } - lock (_textures) + _texturesLock.EnterWriteLock(); + + try { _textures.Add(texture); } + finally + { + _texturesLock.ExitWriteLock(); + } if (partiallyMapped) { @@ -1087,7 +1194,19 @@ namespace Ryujinx.Graphics.Gpu.Image return null; } - int addressMatches = _textures.FindOverlaps(address, ref _textureOverlaps); + int addressMatches; + + _texturesLock.EnterReadLock(); + + try + { + addressMatches = _textures.FindOverlaps(address, ref _textureOverlaps); + } + finally + { + _texturesLock.ExitReadLock(); + } + Texture textureMatch = null; for (int i = 0; i < addressMatches; i++) @@ -1228,10 +1347,16 @@ namespace Ryujinx.Graphics.Gpu.Image /// The texture to be removed public void RemoveTextureFromCache(Texture texture) { - lock (_textures) + _texturesLock.EnterWriteLock(); + + try { _textures.Remove(texture); } + finally + { + _texturesLock.ExitWriteLock(); + } lock (_partiallyMappedTextures) { @@ -1320,13 +1445,19 @@ namespace Ryujinx.Graphics.Gpu.Image /// public void Dispose() { - lock (_textures) + _texturesLock.EnterReadLock(); + + try { foreach (Texture texture in _textures) { texture.Dispose(); } } + finally + { + _texturesLock.ExitReadLock(); + } } } } diff --git a/src/Ryujinx.Graphics.Gpu/Image/TextureCompatibility.cs b/src/Ryujinx.Graphics.Gpu/Image/TextureCompatibility.cs index eef38948d..8bed6363b 100644 --- a/src/Ryujinx.Graphics.Gpu/Image/TextureCompatibility.cs +++ b/src/Ryujinx.Graphics.Gpu/Image/TextureCompatibility.cs @@ -242,7 +242,12 @@ namespace Ryujinx.Graphics.Gpu.Image return TextureMatchQuality.FormatAlias; } else if ((lhs.FormatInfo.Format == Format.D24UnormS8Uint || - lhs.FormatInfo.Format == Format.S8UintD24Unorm) && rhs.FormatInfo.Format == Format.B8G8R8A8Unorm) + lhs.FormatInfo.Format == Format.S8UintD24Unorm || + lhs.FormatInfo.Format == Format.X8UintD24Unorm) && rhs.FormatInfo.Format == Format.B8G8R8A8Unorm) + { + return TextureMatchQuality.FormatAlias; + } + else if (lhs.FormatInfo.Format == Format.D32FloatS8Uint && rhs.FormatInfo.Format == Format.R32G32Float) { return TextureMatchQuality.FormatAlias; } @@ -734,7 +739,8 @@ namespace Ryujinx.Graphics.Gpu.Image } return (lhsFormat.Format == Format.R8G8B8A8Unorm && rhsFormat.Format == Format.R32G32B32A32Float) || - (lhsFormat.Format == Format.R8Unorm && rhsFormat.Format == Format.R8G8B8A8Unorm); + (lhsFormat.Format == Format.R8Unorm && rhsFormat.Format == Format.R8G8B8A8Unorm) || + (lhsFormat.Format == Format.R8Unorm && rhsFormat.Format == Format.R32Uint); } /// diff --git a/src/Ryujinx.Graphics.Gpu/Image/TextureGroup.cs b/src/Ryujinx.Graphics.Gpu/Image/TextureGroup.cs index e45c36528..526fc0c24 100644 --- a/src/Ryujinx.Graphics.Gpu/Image/TextureGroup.cs +++ b/src/Ryujinx.Graphics.Gpu/Image/TextureGroup.cs @@ -6,7 +6,6 @@ using Ryujinx.Memory; using Ryujinx.Memory.Range; using Ryujinx.Memory.Tracking; using System; -using System.Buffers; using System.Collections.Generic; using System.Runtime.CompilerServices; @@ -89,9 +88,9 @@ namespace Ryujinx.Graphics.Gpu.Image private MultiRange TextureRange => Storage.Range; /// - /// The views list from the storage texture. + /// The views array from the storage texture. /// - private List _views; + private Texture[] _views; private TextureGroupHandle[] _handles; private bool[] _loadNeeded; @@ -283,7 +282,7 @@ namespace Ryujinx.Graphics.Gpu.Image /// /// Discards all data for a given texture. - /// This clears all dirty flags, modified flags, and pending copies from other textures. + /// This clears all dirty flags and pending copies from other textures. /// /// The texture being discarded public void DiscardData(Texture texture) @@ -446,7 +445,7 @@ namespace Ryujinx.Graphics.Gpu.Image ReadOnlySpan data = dataSpan[(offset - spanBase)..]; - IMemoryOwner result = Storage.ConvertToHostCompatibleFormat(data, info.BaseLevel + level, true); + MemoryOwner result = Storage.ConvertToHostCompatibleFormat(data, info.BaseLevel + level, true); Storage.SetData(result, info.BaseLayer + layer, info.BaseLevel + level); } @@ -646,7 +645,7 @@ namespace Ryujinx.Graphics.Gpu.Image } else { - _flushBuffer = _context.Renderer.CreateBuffer((int)Storage.Size, BufferAccess.FlushPersistent); + _flushBuffer = _context.Renderer.CreateBuffer((int)Storage.Size, BufferAccess.HostMemory); _flushBufferImported = false; } @@ -1075,7 +1074,7 @@ namespace Ryujinx.Graphics.Gpu.Image public void UpdateViews(List views, Texture texture) { // This is saved to calculate overlapping views for each handle. - _views = views; + _views = views.ToArray(); bool layerViews = _hasLayerViews; bool mipViews = _hasMipViews; @@ -1137,9 +1136,13 @@ namespace Ryujinx.Graphics.Gpu.Image /// /// Removes a view from the group, removing it from all overlap lists. /// + /// The views list of the storage texture /// View to remove from the group - public void RemoveView(Texture view) + public void RemoveView(List views, Texture view) { + // This is saved to calculate overlapping views for each handle. + _views = views.ToArray(); + int offset = FindOffset(view); foreach (TextureGroupHandle handle in _handles) @@ -1606,9 +1609,11 @@ namespace Ryujinx.Graphics.Gpu.Image Storage.SignalModifiedDirty(); - if (_views != null) + Texture[] views = _views; + + if (views != null) { - foreach (Texture texture in _views) + foreach (Texture texture in views) { texture.SignalModifiedDirty(); } @@ -1623,20 +1628,6 @@ namespace Ryujinx.Graphics.Gpu.Image /// The size of the flushing memory access public void FlushAction(TextureGroupHandle handle, ulong address, ulong size) { - // If the page size is larger than 4KB, we will have a lot of false positives for flushing. - // Let's avoid flushing textures that are unlikely to be read from CPU to improve performance - // on those platforms. - if (!_physicalMemory.Supports4KBPages && !Storage.Info.IsLinear && !_context.IsGpuThread()) - { - //return; - } - - // If size is zero, we have nothing to flush. - if (size == 0) - { - return; - } - // There is a small gap here where the action is removed but _actionRegistered is still 1. // In this case it will skip registering the action, but here we are already handling it, // so there shouldn't be any issue as it's the same handler for all actions. diff --git a/src/Ryujinx.Graphics.Gpu/Image/TextureGroupHandle.cs b/src/Ryujinx.Graphics.Gpu/Image/TextureGroupHandle.cs index d345ec2db..860922d59 100644 --- a/src/Ryujinx.Graphics.Gpu/Image/TextureGroupHandle.cs +++ b/src/Ryujinx.Graphics.Gpu/Image/TextureGroupHandle.cs @@ -121,7 +121,7 @@ namespace Ryujinx.Graphics.Gpu.Image public TextureGroupHandle(TextureGroup group, int offset, ulong size, - List views, + IEnumerable views, int firstLayer, int firstLevel, int baseSlice, @@ -182,11 +182,10 @@ namespace Ryujinx.Graphics.Gpu.Image /// /// Discards all data for this handle. - /// This clears all dirty flags, modified flags, and pending copies from other handles. + /// This clears all dirty flags and pending copies from other handles. /// public void DiscardData() { - Modified = false; DeferredCopy = null; foreach (RegionHandle handle in Handles) @@ -202,8 +201,8 @@ namespace Ryujinx.Graphics.Gpu.Image /// Calculate a list of which views overlap this handle. /// /// The parent texture group, used to find a view's base CPU VA offset - /// The list of views to search for overlaps - public void RecalculateOverlaps(TextureGroup group, List views) + /// The views to search for overlaps + public void RecalculateOverlaps(TextureGroup group, IEnumerable views) { // Overlaps can be accessed from the memory tracking signal handler, so access must be atomic. lock (Overlaps) diff --git a/src/Ryujinx.Graphics.Gpu/Image/TextureManager.cs b/src/Ryujinx.Graphics.Gpu/Image/TextureManager.cs index 3bf0beefd..db2921468 100644 --- a/src/Ryujinx.Graphics.Gpu/Image/TextureManager.cs +++ b/src/Ryujinx.Graphics.Gpu/Image/TextureManager.cs @@ -15,6 +15,7 @@ namespace Ryujinx.Graphics.Gpu.Image private readonly TextureBindingsManager _cpBindingsManager; private readonly TextureBindingsManager _gpBindingsManager; + private readonly TextureBindingsArrayCache _bindingsArrayCache; private readonly TexturePoolCache _texturePoolCache; private readonly SamplerPoolCache _samplerPoolCache; @@ -46,8 +47,9 @@ namespace Ryujinx.Graphics.Gpu.Image TexturePoolCache texturePoolCache = new(context); SamplerPoolCache samplerPoolCache = new(context); - _cpBindingsManager = new TextureBindingsManager(context, channel, texturePoolCache, samplerPoolCache, isCompute: true); - _gpBindingsManager = new TextureBindingsManager(context, channel, texturePoolCache, samplerPoolCache, isCompute: false); + _bindingsArrayCache = new TextureBindingsArrayCache(context, channel); + _cpBindingsManager = new TextureBindingsManager(context, channel, _bindingsArrayCache, texturePoolCache, samplerPoolCache, isCompute: true); + _gpBindingsManager = new TextureBindingsManager(context, channel, _bindingsArrayCache, texturePoolCache, samplerPoolCache, isCompute: false); _texturePoolCache = texturePoolCache; _samplerPoolCache = samplerPoolCache; @@ -360,15 +362,16 @@ namespace Ryujinx.Graphics.Gpu.Image /// Commits bindings on the graphics pipeline. /// /// Specialization state for the bound shader + /// True if there is a scale mismatch in the render targets, indicating they must be re-evaluated /// True if all bound textures match the current shader specialization state, false otherwise - public bool CommitGraphicsBindings(ShaderSpecializationState specState) + public bool CommitGraphicsBindings(ShaderSpecializationState specState, out bool scaleMismatch) { _texturePoolCache.Tick(); _samplerPoolCache.Tick(); bool result = _gpBindingsManager.CommitBindings(specState); - UpdateRenderTargets(); + scaleMismatch = UpdateRenderTargets(); return result; } @@ -383,7 +386,7 @@ namespace Ryujinx.Graphics.Gpu.Image { ulong poolAddress = _channel.MemoryManager.Translate(poolGpuVa); - TexturePool texturePool = _texturePoolCache.FindOrCreate(_channel, poolAddress, maximumId); + TexturePool texturePool = _texturePoolCache.FindOrCreate(_channel, poolAddress, maximumId, _bindingsArrayCache); return texturePool; } @@ -426,9 +429,12 @@ namespace Ryujinx.Graphics.Gpu.Image /// /// Update host framebuffer attachments based on currently bound render target buffers. /// - public void UpdateRenderTargets() + /// True if there is a scale mismatch in the render targets, indicating they must be re-evaluated + public bool UpdateRenderTargets() { bool anyChanged = false; + float expectedScale = RenderTargetScale; + bool scaleMismatch = false; Texture dsTexture = _rtDepthStencil; ITexture hostDsTexture = null; @@ -448,6 +454,11 @@ namespace Ryujinx.Graphics.Gpu.Image { _rtHostDs = hostDsTexture; anyChanged = true; + + if (dsTexture != null && dsTexture.ScaleFactor != expectedScale) + { + scaleMismatch = true; + } } for (int index = 0; index < _rtColors.Length; index++) @@ -470,6 +481,11 @@ namespace Ryujinx.Graphics.Gpu.Image { _rtHostColors[index] = hostTexture; anyChanged = true; + + if (texture != null && texture.ScaleFactor != expectedScale) + { + scaleMismatch = true; + } } } @@ -477,6 +493,8 @@ namespace Ryujinx.Graphics.Gpu.Image { _context.Renderer.Pipeline.SetRenderTargets(_rtHostColors, _rtHostDs); } + + return scaleMismatch; } /// diff --git a/src/Ryujinx.Graphics.Gpu/Image/TexturePool.cs b/src/Ryujinx.Graphics.Gpu/Image/TexturePool.cs index a4035577d..be7cb0b89 100644 --- a/src/Ryujinx.Graphics.Gpu/Image/TexturePool.cs +++ b/src/Ryujinx.Graphics.Gpu/Image/TexturePool.cs @@ -6,6 +6,7 @@ using Ryujinx.Memory.Range; using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Numerics; using System.Threading; namespace Ryujinx.Graphics.Gpu.Image @@ -74,6 +75,76 @@ namespace Ryujinx.Graphics.Gpu.Image private readonly ConcurrentQueue _dereferenceQueue = new(); private TextureDescriptor _defaultDescriptor; + /// + /// List of textures that shares the same memory region, but have different formats. + /// + private class TextureAliasList + { + /// + /// Alias texture. + /// + /// Texture format + /// Texture + private readonly record struct Alias(Format Format, Texture Texture); + + /// + /// List of texture aliases. + /// + private readonly List _aliases; + + /// + /// Creates a new instance of the texture alias list. + /// + public TextureAliasList() + { + _aliases = new List(); + } + + /// + /// Adds a new texture alias. + /// + /// Alias format + /// Alias texture + public void Add(Format format, Texture texture) + { + _aliases.Add(new Alias(format, texture)); + texture.IncrementReferenceCount(); + } + + /// + /// Finds a texture with the requested format, or returns null if not found. + /// + /// Format to find + /// Texture with the requested format, or null if not found + public Texture Find(Format format) + { + foreach (var alias in _aliases) + { + if (alias.Format == format) + { + return alias.Texture; + } + } + + return null; + } + + /// + /// Removes all alias textures. + /// + public void Destroy() + { + foreach (var entry in _aliases) + { + entry.Texture.DecrementReferenceCount(); + } + + _aliases.Clear(); + } + } + + private readonly Dictionary _aliasLists; + /// /// Linked list node used on the texture pool cache. /// @@ -94,6 +165,7 @@ namespace Ryujinx.Graphics.Gpu.Image public TexturePool(GpuContext context, GpuChannel channel, ulong address, int maximumId) : base(context, channel.MemoryManager.Physical, address, maximumId) { _channel = channel; + _aliasLists = new Dictionary(); } /// @@ -114,14 +186,13 @@ namespace Ryujinx.Graphics.Gpu.Image if (texture == null) { - TextureInfo info = GetInfo(descriptor, out int layerSize); - // The dereference queue can put our texture back on the cache. if ((texture = ProcessDereferenceQueue(id)) != null) { return ref descriptor; } + TextureInfo info = GetInfo(descriptor, out int layerSize); texture = PhysicalMemory.TextureCache.FindOrCreateTexture(_channel.MemoryManager, TextureSearchFlags.ForSampler, info, layerSize); // If this happens, then the texture address is invalid, we can't add it to the cache. @@ -156,6 +227,17 @@ namespace Ryujinx.Graphics.Gpu.Image /// ID of the texture. This is effectively a zero-based index /// The texture with the given ID public override Texture Get(int id) + { + return Get(id, srgbSampler: true); + } + + /// + /// Gets the texture with the given ID. + /// + /// ID of the texture. This is effectively a zero-based index + /// Whether the texture is being accessed with a sampler that has sRGB conversion enabled + /// The texture with the given ID + public Texture Get(int id, bool srgbSampler) { if ((uint)id >= Items.Length) { @@ -169,7 +251,7 @@ namespace Ryujinx.Graphics.Gpu.Image SynchronizeMemory(); } - GetInternal(id, out Texture texture); + GetForBinding(id, srgbSampler, out Texture texture); return texture; } @@ -181,9 +263,10 @@ namespace Ryujinx.Graphics.Gpu.Image /// This method assumes that the pool has been manually synchronized before doing binding. /// /// ID of the texture. This is effectively a zero-based index + /// Whether the texture is being accessed with a sampler that has sRGB conversion enabled /// The texture with the given ID /// The texture descriptor with the given ID - public ref readonly TextureDescriptor GetForBinding(int id, out Texture texture) + public ref readonly TextureDescriptor GetForBinding(int id, bool srgbSampler, out Texture texture) { if ((uint)id >= Items.Length) { @@ -193,9 +276,66 @@ namespace Ryujinx.Graphics.Gpu.Image // When getting for binding, assume the pool has already been synchronized. + if (!srgbSampler) + { + // If the sampler does not have the sRGB bit enabled, then the texture can't use a sRGB format. + ref readonly TextureDescriptor tempDescriptor = ref GetDescriptorRef(id); + + if (tempDescriptor.UnpackSrgb() && FormatTable.TryGetTextureFormat(tempDescriptor.UnpackFormat(), isSrgb: false, out FormatInfo formatInfo)) + { + // Get a view of the texture with the right format. + return ref GetForBinding(id, formatInfo, out texture); + } + } + return ref GetInternal(id, out texture); } + /// + /// Gets the texture descriptor and texture with the given ID. + /// + /// + /// This method assumes that the pool has been manually synchronized before doing binding. + /// + /// ID of the texture. This is effectively a zero-based index + /// Texture format information + /// The texture with the given ID + /// The texture descriptor with the given ID + public ref readonly TextureDescriptor GetForBinding(int id, FormatInfo formatInfo, out Texture texture) + { + if ((uint)id >= Items.Length) + { + texture = null; + return ref _defaultDescriptor; + } + + ref readonly TextureDescriptor descriptor = ref GetInternal(id, out texture); + + if (texture != null && formatInfo.Format != 0 && texture.Format != formatInfo.Format) + { + if (!_aliasLists.TryGetValue(texture, out TextureAliasList aliasList)) + { + _aliasLists.Add(texture, aliasList = new TextureAliasList()); + } + + texture = aliasList.Find(formatInfo.Format); + + if (texture == null) + { + TextureInfo info = GetInfo(descriptor, out int layerSize); + info = ChangeFormat(info, formatInfo); + texture = PhysicalMemory.TextureCache.FindOrCreateTexture(_channel.MemoryManager, TextureSearchFlags.ForSampler, info, layerSize); + + if (texture != null) + { + aliasList.Add(formatInfo.Format, texture); + } + } + } + + return ref descriptor; + } + /// /// Checks if the pool was modified, and returns the last sequence number where a modification was detected. /// @@ -233,6 +373,7 @@ namespace Ryujinx.Graphics.Gpu.Image else { texture.DecrementReferenceCount(); + RemoveAliasList(texture); } } @@ -326,6 +467,8 @@ namespace Ryujinx.Graphics.Gpu.Image { texture.DecrementReferenceCount(); } + + RemoveAliasList(texture); } return null; @@ -368,6 +511,7 @@ namespace Ryujinx.Graphics.Gpu.Image if (Interlocked.Exchange(ref Items[id], null) != null) { texture.DecrementReferenceCount(this, id); + RemoveAliasList(texture); } } } @@ -490,6 +634,8 @@ namespace Ryujinx.Graphics.Gpu.Image levels = (maxLod - minLod) + 1; } + levels = ClampLevels(target, width, height, depthOrLayers, levels); + SwizzleComponent swizzleR = descriptor.UnpackSwizzleR().Convert(); SwizzleComponent swizzleG = descriptor.UnpackSwizzleG().Convert(); SwizzleComponent swizzleB = descriptor.UnpackSwizzleB().Convert(); @@ -540,6 +686,34 @@ namespace Ryujinx.Graphics.Gpu.Image swizzleA); } + /// + /// Clamps the amount of mipmap levels to the maximum allowed for the given texture dimensions. + /// + /// Number of texture dimensions (1D, 2D, 3D, Cube, etc) + /// Width of the texture + /// Height of the texture, ignored for 1D textures + /// Depth of the texture for 3D textures, otherwise ignored + /// Original amount of mipmap levels + /// Clamped mipmap levels + private static int ClampLevels(Target target, int width, int height, int depthOrLayers, int levels) + { + int maxSize = width; + + if (target != Target.Texture1D && + target != Target.Texture1DArray) + { + maxSize = Math.Max(maxSize, height); + } + + if (target == Target.Texture3D) + { + maxSize = Math.Max(maxSize, depthOrLayers); + } + + int maxLevels = BitOperations.Log2((uint)maxSize) + 1; + return Math.Min(levels, maxLevels); + } + /// /// Gets the texture depth-stencil mode, based on the swizzle components of each color channel. /// The depth-stencil mode is determined based on how the driver sets those parameters. @@ -591,6 +765,57 @@ namespace Ryujinx.Graphics.Gpu.Image component == SwizzleComponent.Green; } + /// + /// Changes the format on the texture information structure, and also adjusts the width for the new format if needed. + /// + /// Texture information + /// New format + /// Texture information with the new format + private static TextureInfo ChangeFormat(in TextureInfo info, FormatInfo dstFormat) + { + int width = info.Width; + + if (info.FormatInfo.BytesPerPixel != dstFormat.BytesPerPixel) + { + int stride = width * info.FormatInfo.BytesPerPixel; + width = stride / dstFormat.BytesPerPixel; + } + + return new TextureInfo( + info.GpuAddress, + width, + info.Height, + info.DepthOrLayers, + info.Levels, + info.SamplesInX, + info.SamplesInY, + info.Stride, + info.IsLinear, + info.GobBlocksInY, + info.GobBlocksInZ, + info.GobBlocksInTileX, + info.Target, + dstFormat, + info.DepthStencilMode, + info.SwizzleR, + info.SwizzleG, + info.SwizzleB, + info.SwizzleA); + } + + /// + /// Removes all aliases for a texture. + /// + /// Texture to have the aliases removed + private void RemoveAliasList(Texture texture) + { + if (_aliasLists.TryGetValue(texture, out TextureAliasList aliasList)) + { + _aliasLists.Remove(texture); + aliasList.Destroy(); + } + } + /// /// Decrements the reference count of the texture. /// This indicates that the texture pool is not using it anymore. @@ -598,7 +823,11 @@ namespace Ryujinx.Graphics.Gpu.Image /// The texture to be deleted protected override void Delete(Texture item) { - item?.DecrementReferenceCount(this); + if (item != null) + { + item.DecrementReferenceCount(this); + RemoveAliasList(item); + } } public override void Dispose() diff --git a/src/Ryujinx.Graphics.Gpu/Memory/Buffer.cs b/src/Ryujinx.Graphics.Gpu/Memory/Buffer.cs index adaacd915..e060e0b4f 100644 --- a/src/Ryujinx.Graphics.Gpu/Memory/Buffer.cs +++ b/src/Ryujinx.Graphics.Gpu/Memory/Buffer.cs @@ -5,9 +5,13 @@ using Ryujinx.Memory.Tracking; using System; using System.Collections.Generic; using System.Linq; +using System.Runtime.CompilerServices; +using System.Threading; namespace Ryujinx.Graphics.Gpu.Memory { + delegate void BufferFlushAction(ulong address, ulong size, ulong syncNumber); + /// /// Buffer, used to store vertex and index data, uniform and storage buffers, and others. /// @@ -21,7 +25,7 @@ namespace Ryujinx.Graphics.Gpu.Memory /// /// Host buffer handle. /// - public BufferHandle Handle { get; } + public BufferHandle Handle { get; private set; } /// /// Start address of the buffer in guest memory. @@ -58,6 +62,17 @@ namespace Ryujinx.Graphics.Gpu.Memory /// private BufferModifiedRangeList _modifiedRanges = null; + /// + /// A structure that is used to flush buffer data back to a host mapped buffer for cached readback. + /// Only used if the buffer data is explicitly owned by device local memory. + /// + private BufferPreFlush _preFlush = null; + + /// + /// Usage tracking state that determines what type of backing the buffer should use. + /// + public BufferBackingState BackingState; + private readonly MultiRegionHandle _memoryTrackingGranular; private readonly RegionHandle _memoryTracking; @@ -65,6 +80,9 @@ namespace Ryujinx.Graphics.Gpu.Memory private readonly Action _loadDelegate; private readonly Action _modifiedDelegate; + private HashSet _virtualDependencies; + private readonly ReaderWriterLockSlim _virtualDependenciesLock; + private int _sequenceNumber; private readonly bool _useGranular; @@ -82,6 +100,7 @@ namespace Ryujinx.Graphics.Gpu.Memory /// Physical memory where the buffer is mapped /// Start address of the buffer /// Size of the buffer in bytes + /// The type of usage that created the buffer /// Indicates if the buffer can be used in a sparse buffer mapping /// Buffers which this buffer contains, and will inherit tracking handles from public Buffer( @@ -89,6 +108,7 @@ namespace Ryujinx.Graphics.Gpu.Memory PhysicalMemory physicalMemory, ulong address, ulong size, + BufferStage stage, bool sparseCompatible, IEnumerable baseBuffers = null) { @@ -98,9 +118,11 @@ namespace Ryujinx.Graphics.Gpu.Memory Size = size; SparseCompatible = sparseCompatible; - BufferAccess access = sparseCompatible ? BufferAccess.SparseCompatible : BufferAccess.Default; + BackingState = new BufferBackingState(_context, this, stage, baseBuffers); - Handle = context.Renderer.CreateBuffer((int)size, access, baseBuffers?.MaxBy(x => x.Size).Handle ?? BufferHandle.Null); + BufferAccess access = BackingState.SwitchAccess(this); + + Handle = context.Renderer.CreateBuffer((int)size, access); _useGranular = size > GranularBufferThreshold; @@ -123,13 +145,13 @@ namespace Ryujinx.Graphics.Gpu.Memory if (_useGranular) { - _memoryTrackingGranular = physicalMemory.BeginGranularTracking(address, size, ResourceKind.Buffer, baseHandles); + _memoryTrackingGranular = physicalMemory.BeginGranularTracking(address, size, ResourceKind.Buffer, RegionFlags.UnalignedAccess, baseHandles); _memoryTrackingGranular.RegisterPreciseAction(address, size, PreciseAction); } else { - _memoryTracking = physicalMemory.BeginTracking(address, size, ResourceKind.Buffer); + _memoryTracking = physicalMemory.BeginTracking(address, size, ResourceKind.Buffer, RegionFlags.UnalignedAccess); if (baseHandles != null) { @@ -152,6 +174,31 @@ namespace Ryujinx.Graphics.Gpu.Memory _externalFlushDelegate = new RegionSignal(ExternalFlush); _loadDelegate = new Action(LoadRegion); _modifiedDelegate = new Action(RegionModified); + + _virtualDependenciesLock = new ReaderWriterLockSlim(); + } + + /// + /// Recreates the backing buffer based on the desired access type + /// reported by the backing state struct. + /// + private void ChangeBacking() + { + BufferAccess access = BackingState.SwitchAccess(this); + + BufferHandle newHandle = _context.Renderer.CreateBuffer((int)Size, access); + + _context.Renderer.Pipeline.CopyBuffer(Handle, newHandle, 0, 0, (int)Size); + + _modifiedRanges?.SelfMigration(); + + // If swtiching from device local to host mapped, pre-flushing data no longer makes sense. + // This is set to null and disposed when the migration fully completes. + _preFlush = null; + + Handle = newHandle; + + _physicalMemory.BufferCache.BufferBackingChanged(this); } /// @@ -220,6 +267,7 @@ namespace Ryujinx.Graphics.Gpu.Memory /// /// Start address of the range to synchronize /// Size in bytes of the range to synchronize + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void SynchronizeMemory(ulong address, ulong size) { if (_useGranular) @@ -238,7 +286,9 @@ namespace Ryujinx.Graphics.Gpu.Memory } else { + BackingState.RecordSet(); _context.Renderer.SetBufferData(Handle, 0, _physicalMemory.GetSpan(Address, (int)Size)); + CopyToDependantVirtualBuffers(); } _sequenceNumber = _context.SequenceNumber; @@ -274,15 +324,35 @@ namespace Ryujinx.Graphics.Gpu.Memory _modifiedRanges ??= new BufferModifiedRangeList(_context, this, Flush); } + /// + /// Checks if a backing change is deemed necessary from the given usage. + /// If it is, queues a backing change to happen on the next sync action. + /// + /// Buffer stage that can change backing type + private void TryQueueBackingChange(BufferStage stage) + { + if (BackingState.ShouldChangeBacking(stage)) + { + if (!_syncActionRegistered) + { + _context.RegisterSyncAction(this); + _syncActionRegistered = true; + } + } + } + /// /// Signal that the given region of the buffer has been modified. /// /// The start address of the modified region /// The size of the modified region - public void SignalModified(ulong address, ulong size) + /// Buffer stage that triggered the modification + public void SignalModified(ulong address, ulong size, BufferStage stage) { EnsureRangeList(); + TryQueueBackingChange(stage); + _modifiedRanges.SignalModified(address, size); if (!_syncActionRegistered) @@ -302,6 +372,37 @@ namespace Ryujinx.Graphics.Gpu.Memory _modifiedRanges?.Clear(address, size); } + /// + /// Action to be performed immediately before sync is created. + /// This will copy any buffer ranges designated for pre-flushing. + /// + /// True if the action is a guest syncpoint + public void SyncPreAction(bool syncpoint) + { + if (_referenceCount == 0) + { + return; + } + + if (BackingState.ShouldChangeBacking()) + { + ChangeBacking(); + } + + if (BackingState.IsDeviceLocal) + { + _preFlush ??= new BufferPreFlush(_context, this, FlushImpl); + + if (_preFlush.ShouldCopy) + { + _modifiedRanges?.GetRangesAtSync(Address, Size, _context.SyncNumber, (address, size) => + { + _preFlush.CopyModified(address, size); + }); + } + } + } + /// /// Action to be performed when a syncpoint is reached after modification. /// This will register read/write tracking to flush the buffer from GPU when its memory is used. @@ -457,9 +558,13 @@ namespace Ryujinx.Graphics.Gpu.Memory /// Size of the modified region private void LoadRegion(ulong mAddress, ulong mSize) { + BackingState.RecordSet(); + int offset = (int)(mAddress - Address); _context.Renderer.SetBufferData(Handle, offset, _physicalMemory.GetSpan(mAddress, (int)mSize)); + + CopyToDependantVirtualBuffers(mAddress, mSize); } /// @@ -520,23 +625,90 @@ namespace Ryujinx.Graphics.Gpu.Memory /// The offset of the destination buffer to copy into public void CopyTo(Buffer destination, int dstOffset) { + CopyFromDependantVirtualBuffers(); _context.Renderer.Pipeline.CopyBuffer(Handle, destination.Handle, 0, dstOffset, (int)Size); } + /// + /// Flushes a range of the buffer. + /// This writes the range data back into guest memory. + /// + /// Buffer handle to flush data from + /// Start address of the range + /// Size in bytes of the range + private void FlushImpl(BufferHandle handle, ulong address, ulong size) + { + int offset = (int)(address - Address); + + using PinnedSpan data = _context.Renderer.GetBufferData(handle, offset, (int)size); + + // TODO: When write tracking shaders, they will need to be aware of changes in overlapping buffers. + _physicalMemory.WriteUntracked(address, CopyFromDependantVirtualBuffers(data.Get(), address, size)); + } + /// /// Flushes a range of the buffer. /// This writes the range data back into guest memory. /// /// Start address of the range /// Size in bytes of the range - public void Flush(ulong address, ulong size) + private void FlushImpl(ulong address, ulong size) { - int offset = (int)(address - Address); + FlushImpl(Handle, address, size); + } - using PinnedSpan data = _context.Renderer.GetBufferData(Handle, offset, (int)size); + /// + /// Flushes a range of the buffer from the most optimal source. + /// This writes the range data back into guest memory. + /// + /// Start address of the range + /// Size in bytes of the range + /// Sync number waited for before flushing the data + public void Flush(ulong address, ulong size, ulong syncNumber) + { + BackingState.RecordFlush(); - // TODO: When write tracking shaders, they will need to be aware of changes in overlapping buffers. - _physicalMemory.WriteUntracked(address, data.Get()); + BufferPreFlush preFlush = _preFlush; + + if (preFlush != null) + { + preFlush.FlushWithAction(address, size, syncNumber); + } + else + { + FlushImpl(address, size); + } + } + /// + /// Gets an action that disposes the backing buffer using its current handle. + /// Useful for deleting an old copy of the buffer after the handle changes. + /// + /// An action that flushes data from the specified range, using the buffer handle at the time the method is generated + public Action GetSnapshotDisposeAction() + { + BufferHandle handle = Handle; + BufferPreFlush preFlush = _preFlush; + + return () => + { + _context.Renderer.DeleteBuffer(handle); + preFlush?.Dispose(); + }; + } + + /// + /// Gets an action that flushes a range of the buffer using its current handle. + /// Useful for flushing data from old copies of the buffer after the handle changes. + /// + /// An action that flushes data from the specified range, using the buffer handle at the time the method is generated + public BufferFlushAction GetSnapshotFlushAction() + { + BufferHandle handle = Handle; + + return (ulong address, ulong size, ulong _) => + { + FlushImpl(handle, address, size); + }; } /// @@ -561,18 +733,6 @@ namespace Ryujinx.Graphics.Gpu.Memory /// Size in bytes public void ExternalFlush(ulong address, ulong size) { - ulong maxAddress = Math.Max(address, Address); - ulong minEndAddress = Math.Min(address + size, Address + Size); - - if (maxAddress >= minEndAddress) - { - // Access doesn't overlap. - return; - } - - address = maxAddress; - size = minEndAddress - address; - _context.Renderer.BackgroundContextAction(() => { var ranges = _modifiedRanges; @@ -629,6 +789,207 @@ namespace Ryujinx.Graphics.Gpu.Memory UnmappedSequence++; } + /// + /// Adds a virtual buffer dependency, indicating that a virtual buffer depends on data from this buffer. + /// + /// Dependant virtual buffer + public void AddVirtualDependency(MultiRangeBuffer virtualBuffer) + { + _virtualDependenciesLock.EnterWriteLock(); + + try + { + (_virtualDependencies ??= new()).Add(virtualBuffer); + } + finally + { + _virtualDependenciesLock.ExitWriteLock(); + } + } + + /// + /// Removes a virtual buffer dependency, indicating that a virtual buffer no longer depends on data from this buffer. + /// + /// Dependant virtual buffer + public void RemoveVirtualDependency(MultiRangeBuffer virtualBuffer) + { + _virtualDependenciesLock.EnterWriteLock(); + + try + { + if (_virtualDependencies != null) + { + _virtualDependencies.Remove(virtualBuffer); + + if (_virtualDependencies.Count == 0) + { + _virtualDependencies = null; + } + } + } + finally + { + _virtualDependenciesLock.ExitWriteLock(); + } + } + + /// + /// Copies the buffer data to all virtual buffers that depends on it. + /// + public void CopyToDependantVirtualBuffers() + { + CopyToDependantVirtualBuffers(Address, Size); + } + + /// + /// Copies the buffer data inside the specifide range to all virtual buffers that depends on it. + /// + /// Address of the range + /// Size of the range in bytes + public void CopyToDependantVirtualBuffers(ulong address, ulong size) + { + if (_virtualDependencies != null) + { + foreach (var virtualBuffer in _virtualDependencies) + { + CopyToDependantVirtualBuffer(virtualBuffer, address, size); + } + } + } + + /// + /// Copies all modified ranges from all virtual buffers back into this buffer. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void CopyFromDependantVirtualBuffers() + { + if (_virtualDependencies != null) + { + CopyFromDependantVirtualBuffersImpl(); + } + } + + /// + /// Copies all modified ranges from all virtual buffers back into this buffer. + /// + [MethodImpl(MethodImplOptions.NoInlining)] + private void CopyFromDependantVirtualBuffersImpl() + { + foreach (var virtualBuffer in _virtualDependencies.OrderBy(x => x.ModificationSequenceNumber)) + { + virtualBuffer.ConsumeModifiedRegion(this, (mAddress, mSize) => + { + // Get offset inside both this and the virtual buffer. + // Note that sometimes there is no right answer for the virtual offset, + // as the same physical range might be mapped multiple times inside a virtual buffer. + // We just assume it does not happen in practice as it can only be implemented correctly + // when the host has support for proper sparse mapping. + + ulong mEndAddress = mAddress + mSize; + mAddress = Math.Max(mAddress, Address); + mSize = Math.Min(mEndAddress, EndAddress) - mAddress; + + int physicalOffset = (int)(mAddress - Address); + int virtualOffset = virtualBuffer.Range.FindOffset(new(mAddress, mSize)); + + _context.Renderer.Pipeline.CopyBuffer(virtualBuffer.Handle, Handle, virtualOffset, physicalOffset, (int)mSize); + }); + } + } + + /// + /// Copies all overlapping modified ranges from all virtual buffers back into this buffer, and returns an updated span with the data. + /// + /// Span where the unmodified data will be taken from for the output + /// Address of the region to copy + /// Size of the region to copy in bytes + /// A span with , and the data for all modified ranges if any + private ReadOnlySpan CopyFromDependantVirtualBuffers(ReadOnlySpan dataSpan, ulong address, ulong size) + { + _virtualDependenciesLock.EnterReadLock(); + + try + { + if (_virtualDependencies != null) + { + byte[] storage = dataSpan.ToArray(); + + foreach (var virtualBuffer in _virtualDependencies.OrderBy(x => x.ModificationSequenceNumber)) + { + virtualBuffer.ConsumeModifiedRegion(address, size, (mAddress, mSize) => + { + // Get offset inside both this and the virtual buffer. + // Note that sometimes there is no right answer for the virtual offset, + // as the same physical range might be mapped multiple times inside a virtual buffer. + // We just assume it does not happen in practice as it can only be implemented correctly + // when the host has support for proper sparse mapping. + + ulong mEndAddress = mAddress + mSize; + mAddress = Math.Max(mAddress, address); + mSize = Math.Min(mEndAddress, address + size) - mAddress; + + int physicalOffset = (int)(mAddress - Address); + int virtualOffset = virtualBuffer.Range.FindOffset(new(mAddress, mSize)); + + _context.Renderer.Pipeline.CopyBuffer(virtualBuffer.Handle, Handle, virtualOffset, physicalOffset, (int)size); + virtualBuffer.GetData(storage.AsSpan().Slice((int)(mAddress - address), (int)mSize), virtualOffset, (int)mSize); + }); + } + + dataSpan = storage; + } + } + finally + { + _virtualDependenciesLock.ExitReadLock(); + } + + return dataSpan; + } + + /// + /// Copies the buffer data to the specified virtual buffer. + /// + /// Virtual buffer to copy the data into + public void CopyToDependantVirtualBuffer(MultiRangeBuffer virtualBuffer) + { + CopyToDependantVirtualBuffer(virtualBuffer, Address, Size); + } + + /// + /// Copies the buffer data inside the given range to the specified virtual buffer. + /// + /// Virtual buffer to copy the data into + /// Address of the range + /// Size of the range in bytes + public void CopyToDependantVirtualBuffer(MultiRangeBuffer virtualBuffer, ulong address, ulong size) + { + // Broadcast data to all ranges of the virtual buffer that are contained inside this buffer. + + ulong lastOffset = 0; + + while (virtualBuffer.TryGetPhysicalOffset(this, lastOffset, out ulong srcOffset, out ulong dstOffset, out ulong copySize)) + { + ulong innerOffset = address - Address; + ulong innerEndOffset = (address + size) - Address; + + lastOffset = dstOffset + copySize; + + // Clamp range to the specified range. + ulong copySrcOffset = Math.Max(srcOffset, innerOffset); + ulong copySrcEndOffset = Math.Min(innerEndOffset, srcOffset + copySize); + + if (copySrcEndOffset > copySrcOffset) + { + copySize = copySrcEndOffset - copySrcOffset; + dstOffset += copySrcOffset - srcOffset; + srcOffset = copySrcOffset; + + _context.Renderer.Pipeline.CopyBuffer(Handle, virtualBuffer.Handle, (int)srcOffset, (int)dstOffset, (int)copySize); + } + } + } + /// /// Increments the buffer reference count. /// @@ -656,6 +1017,8 @@ namespace Ryujinx.Graphics.Gpu.Memory _modifiedRanges?.Clear(); _context.Renderer.DeleteBuffer(Handle); + _preFlush?.Dispose(); + _preFlush = null; UnmappedSequence++; } diff --git a/src/Ryujinx.Graphics.Gpu/Memory/BufferBackingState.cs b/src/Ryujinx.Graphics.Gpu/Memory/BufferBackingState.cs new file mode 100644 index 000000000..3f65131e6 --- /dev/null +++ b/src/Ryujinx.Graphics.Gpu/Memory/BufferBackingState.cs @@ -0,0 +1,294 @@ +using Ryujinx.Graphics.GAL; +using System; +using System.Collections.Generic; + +namespace Ryujinx.Graphics.Gpu.Memory +{ + /// + /// Type of backing memory. + /// In ascending order of priority when merging multiple buffer backing states. + /// + internal enum BufferBackingType + { + HostMemory, + DeviceMemory, + DeviceMemoryWithFlush + } + + /// + /// Keeps track of buffer usage to decide what memory heap that buffer memory is placed on. + /// Dedicated GPUs prefer certain types of resources to be device local, + /// and if we need data to be read back, we might prefer that they're in host memory. + /// + /// The measurements recorded here compare to a set of heruristics (thresholds and conditions) + /// that appear to produce good performance in most software. + /// + internal struct BufferBackingState + { + private const int DeviceLocalSizeThreshold = 256 * 1024; // 256kb + + private const int SetCountThreshold = 100; + private const int WriteCountThreshold = 50; + private const int FlushCountThreshold = 5; + private const int DeviceLocalForceExpiry = 100; + + public readonly bool IsDeviceLocal => _activeType != BufferBackingType.HostMemory; + + private readonly SystemMemoryType _systemMemoryType; + private BufferBackingType _activeType; + private BufferBackingType _desiredType; + + private bool _canSwap; + + private int _setCount; + private int _writeCount; + private int _flushCount; + private int _flushTemp; + private int _lastFlushWrite; + private int _deviceLocalForceCount; + + private readonly int _size; + + /// + /// Initialize the buffer backing state for a given parent buffer. + /// + /// GPU context + /// Parent buffer + /// Initial buffer stage + /// Buffers to inherit state from + public BufferBackingState(GpuContext context, Buffer parent, BufferStage stage, IEnumerable baseBuffers = null) + { + _size = (int)parent.Size; + _systemMemoryType = context.Capabilities.MemoryType; + + // Backend managed is always auto, unified memory is always host. + _desiredType = BufferBackingType.HostMemory; + _canSwap = _systemMemoryType != SystemMemoryType.BackendManaged && _systemMemoryType != SystemMemoryType.UnifiedMemory; + + if (_canSwap) + { + // Might want to start certain buffers as being device local, + // and the usage might also lock those buffers into being device local. + + BufferStage storageFlags = stage & BufferStage.StorageMask; + + if (parent.Size > DeviceLocalSizeThreshold && baseBuffers == null) + { + _desiredType = BufferBackingType.DeviceMemory; + } + + if (storageFlags != 0) + { + // Storage buffer bindings may require special treatment. + + var rawStage = stage & BufferStage.StageMask; + + if (rawStage == BufferStage.Fragment) + { + // Fragment read should start device local. + + _desiredType = BufferBackingType.DeviceMemory; + + if (storageFlags != BufferStage.StorageRead) + { + // Fragment write should stay device local until the use doesn't happen anymore. + + _deviceLocalForceCount = DeviceLocalForceExpiry; + } + } + + // TODO: Might be nice to force atomic access to be device local for any stage. + } + + if (baseBuffers != null) + { + foreach (Buffer buffer in baseBuffers) + { + CombineState(buffer.BackingState); + } + } + } + } + + /// + /// Combine buffer backing types, selecting the one with highest priority. + /// + /// First buffer backing type + /// Second buffer backing type + /// Combined buffer backing type + private static BufferBackingType CombineTypes(BufferBackingType left, BufferBackingType right) + { + return (BufferBackingType)Math.Max((int)left, (int)right); + } + + /// + /// Combine the state from the given buffer backing state with this one, + /// so that the state isn't lost when migrating buffers. + /// + /// Buffer state to combine into this state + private void CombineState(BufferBackingState oldState) + { + _setCount += oldState._setCount; + _writeCount += oldState._writeCount; + _flushCount += oldState._flushCount; + _flushTemp += oldState._flushTemp; + _lastFlushWrite = -1; + _deviceLocalForceCount = Math.Max(_deviceLocalForceCount, oldState._deviceLocalForceCount); + + _canSwap &= oldState._canSwap; + + _desiredType = CombineTypes(_desiredType, oldState._desiredType); + } + + /// + /// Get the buffer access for the desired backing type, and record that type as now being active. + /// + /// Parent buffer + /// Buffer access + public BufferAccess SwitchAccess(Buffer parent) + { + BufferAccess access = parent.SparseCompatible ? BufferAccess.SparseCompatible : BufferAccess.Default; + + bool isBackendManaged = _systemMemoryType == SystemMemoryType.BackendManaged; + + if (!isBackendManaged) + { + switch (_desiredType) + { + case BufferBackingType.HostMemory: + access |= BufferAccess.HostMemory; + break; + case BufferBackingType.DeviceMemory: + access |= BufferAccess.DeviceMemory; + break; + case BufferBackingType.DeviceMemoryWithFlush: + access |= BufferAccess.DeviceMemoryMapped; + break; + } + } + + _activeType = _desiredType; + + return access; + } + + /// + /// Record when data has been uploaded to the buffer. + /// + public void RecordSet() + { + _setCount++; + + ConsiderUseCounts(); + } + + /// + /// Record when data has been flushed from the buffer. + /// + public void RecordFlush() + { + if (_lastFlushWrite != _writeCount) + { + // If it's on the same page as the last flush, ignore it. + _lastFlushWrite = _writeCount; + _flushCount++; + } + } + + /// + /// Determine if the buffer backing should be changed. + /// + /// True if the desired backing type is different from the current type + public readonly bool ShouldChangeBacking() + { + return _desiredType != _activeType; + } + + /// + /// Determine if the buffer backing should be changed, considering a new use with the given buffer stage. + /// + /// Buffer stage for the use + /// True if the desired backing type is different from the current type + public bool ShouldChangeBacking(BufferStage stage) + { + if (!_canSwap) + { + return false; + } + + BufferStage storageFlags = stage & BufferStage.StorageMask; + + if (storageFlags != 0) + { + if (storageFlags != BufferStage.StorageRead) + { + // Storage write. + _writeCount++; + + var rawStage = stage & BufferStage.StageMask; + + if (rawStage == BufferStage.Fragment) + { + // Switch to device memory, swap back only if this use disappears. + + _desiredType = CombineTypes(_desiredType, BufferBackingType.DeviceMemory); + _deviceLocalForceCount = DeviceLocalForceExpiry; + + // TODO: Might be nice to force atomic access to be device local for any stage. + } + } + + ConsiderUseCounts(); + } + + return _desiredType != _activeType; + } + + /// + /// Evaluate the current counts to determine what the buffer's desired backing type is. + /// This method depends on heuristics devised by testing a variety of software. + /// + private void ConsiderUseCounts() + { + if (_canSwap) + { + if (_writeCount >= WriteCountThreshold || _setCount >= SetCountThreshold || _flushCount >= FlushCountThreshold) + { + if (_deviceLocalForceCount > 0 && --_deviceLocalForceCount != 0) + { + // Some buffer usage demanded that the buffer stay device local. + // The desired type was selected when this counter was set. + } + else if (_flushCount > 0 || _flushTemp-- > 0) + { + // Buffers that flush should ideally be mapped in host address space for easy copies. + // If the buffer is large it will do better on GPU memory, as there will be more writes than data flushes (typically individual pages). + // If it is small, then it's likely most of the buffer will be flushed so we want it on host memory, as access is cached. + _desiredType = _size > DeviceLocalSizeThreshold ? BufferBackingType.DeviceMemoryWithFlush : BufferBackingType.HostMemory; + } + else if (_writeCount >= WriteCountThreshold) + { + // Buffers that are written often should ideally be in the device local heap. (Storage buffers) + _desiredType = BufferBackingType.DeviceMemory; + } + else if (_setCount > SetCountThreshold) + { + // Buffers that have their data set often should ideally be host mapped. (Constant buffers) + _desiredType = BufferBackingType.HostMemory; + } + + // It's harder for a buffer that is flushed to revert to another type of mapping. + if (_flushCount > 0) + { + _flushTemp = 1000; + } + + _lastFlushWrite = -1; + _flushCount = 0; + _writeCount = 0; + _setCount = 0; + } + } + } + } +} diff --git a/src/Ryujinx.Graphics.Gpu/Memory/BufferBounds.cs b/src/Ryujinx.Graphics.Gpu/Memory/BufferBounds.cs index aed3268ae..cf783ef2f 100644 --- a/src/Ryujinx.Graphics.Gpu/Memory/BufferBounds.cs +++ b/src/Ryujinx.Graphics.Gpu/Memory/BufferBounds.cs @@ -1,12 +1,13 @@ using Ryujinx.Graphics.Shader; using Ryujinx.Memory.Range; +using System; namespace Ryujinx.Graphics.Gpu.Memory { /// /// Memory range used for buffers. /// - readonly struct BufferBounds + readonly struct BufferBounds : IEquatable { /// /// Physical memory ranges where the buffer is mapped. @@ -33,5 +34,25 @@ namespace Ryujinx.Graphics.Gpu.Memory Range = range; Flags = flags; } + + public override bool Equals(object obj) + { + return obj is BufferBounds bounds && Equals(bounds); + } + + public bool Equals(BufferBounds bounds) + { + return Range == bounds.Range && Flags == bounds.Flags; + } + + public bool Equals(ref BufferBounds bounds) + { + return Range == bounds.Range && Flags == bounds.Flags; + } + + public override int GetHashCode() + { + return HashCode.Combine(Range, Flags); + } } } diff --git a/src/Ryujinx.Graphics.Gpu/Memory/BufferCache.cs b/src/Ryujinx.Graphics.Gpu/Memory/BufferCache.cs index bd9aa39c8..66d2cdb62 100644 --- a/src/Ryujinx.Graphics.Gpu/Memory/BufferCache.cs +++ b/src/Ryujinx.Graphics.Gpu/Memory/BufferCache.cs @@ -3,6 +3,7 @@ using Ryujinx.Memory.Range; using System; using System.Collections.Generic; using System.Linq; +using System.Runtime.CompilerServices; namespace Ryujinx.Graphics.Gpu.Memory { @@ -46,6 +47,7 @@ namespace Ryujinx.Graphics.Gpu.Memory private readonly Dictionary _dirtyCache; private readonly Dictionary _modifiedCache; private bool _pruneCaches; + private int _virtualModifiedSequenceNumber; public event Action NotifyBuffersModified; @@ -105,8 +107,9 @@ namespace Ryujinx.Graphics.Gpu.Memory /// GPU memory manager where the buffer is mapped /// Start GPU virtual address of the buffer /// Size in bytes of the buffer + /// The type of usage that created the buffer /// Contiguous physical range of the buffer, after address translation - public MultiRange TranslateAndCreateBuffer(MemoryManager memoryManager, ulong gpuVa, ulong size) + public MultiRange TranslateAndCreateBuffer(MemoryManager memoryManager, ulong gpuVa, ulong size, BufferStage stage) { if (gpuVa == 0) { @@ -117,7 +120,7 @@ namespace Ryujinx.Graphics.Gpu.Memory if (address != MemoryManager.PteUnmapped) { - CreateBuffer(address, size); + CreateBuffer(address, size, stage); } return new MultiRange(address, size); @@ -125,31 +128,75 @@ namespace Ryujinx.Graphics.Gpu.Memory /// /// Performs address translation of the GPU virtual address, and creates - /// new buffers, if needed, for the specified range. + /// new physical and virtual buffers, if needed, for the specified range. /// /// GPU memory manager where the buffer is mapped /// Start GPU virtual address of the buffer /// Size in bytes of the buffer + /// The type of usage that created the buffer /// Physical ranges of the buffer, after address translation - public MultiRange TranslateAndCreateMultiBuffers(MemoryManager memoryManager, ulong gpuVa, ulong size) + public MultiRange TranslateAndCreateMultiBuffers(MemoryManager memoryManager, ulong gpuVa, ulong size, BufferStage stage) { if (gpuVa == 0) { return new MultiRange(MemoryManager.PteUnmapped, size); } - bool supportsSparse = _context.Capabilities.SupportsSparseBuffer; - // Fast path not taken for non-contiguous ranges, // since multi-range buffers are not coalesced, so a buffer that covers // the entire cached range might not actually exist. - if (memoryManager.VirtualBufferCache.TryGetOrAddRange(gpuVa, size, supportsSparse, out MultiRange range) && + if (memoryManager.VirtualRangeCache.TryGetOrAddRange(gpuVa, size, out MultiRange range) && range.Count == 1) { return range; } - CreateBuffer(range); + CreateBuffer(range, stage); + + return range; + } + + /// + /// Performs address translation of the GPU virtual address, and creates + /// new physical buffers, if needed, for the specified range. + /// + /// GPU memory manager where the buffer is mapped + /// Start GPU virtual address of the buffer + /// Size in bytes of the buffer + /// The type of usage that created the buffer + /// Physical ranges of the buffer, after address translation + public MultiRange TranslateAndCreateMultiBuffersPhysicalOnly(MemoryManager memoryManager, ulong gpuVa, ulong size, BufferStage stage) + { + if (gpuVa == 0) + { + return new MultiRange(MemoryManager.PteUnmapped, size); + } + + // Fast path not taken for non-contiguous ranges, + // since multi-range buffers are not coalesced, so a buffer that covers + // the entire cached range might not actually exist. + if (memoryManager.VirtualRangeCache.TryGetOrAddRange(gpuVa, size, out MultiRange range) && + range.Count == 1) + { + return range; + } + + for (int i = 0; i < range.Count; i++) + { + MemoryRange subRange = range.GetSubRange(i); + + if (subRange.Address != MemoryManager.PteUnmapped) + { + if (range.Count > 1) + { + CreateBuffer(subRange.Address, subRange.Size, stage, SparseBufferAlignmentSize); + } + else + { + CreateBuffer(subRange.Address, subRange.Size, stage); + } + } + } return range; } @@ -159,11 +206,12 @@ namespace Ryujinx.Graphics.Gpu.Memory /// This can be used to ensure the existance of a buffer. /// /// Physical ranges of memory where the buffer data is located - public void CreateBuffer(MultiRange range) + /// The type of usage that created the buffer + public void CreateBuffer(MultiRange range, BufferStage stage) { if (range.Count > 1) { - CreateMultiRangeBuffer(range); + CreateMultiRangeBuffer(range, stage); } else { @@ -171,7 +219,7 @@ namespace Ryujinx.Graphics.Gpu.Memory if (subRange.Address != MemoryManager.PteUnmapped) { - CreateBuffer(subRange.Address, subRange.Size); + CreateBuffer(subRange.Address, subRange.Size, stage); } } } @@ -182,7 +230,8 @@ namespace Ryujinx.Graphics.Gpu.Memory /// /// Address of the buffer in memory /// Size of the buffer in bytes - public void CreateBuffer(ulong address, ulong size) + /// The type of usage that created the buffer + public void CreateBuffer(ulong address, ulong size, BufferStage stage) { ulong endAddress = address + size; @@ -195,7 +244,7 @@ namespace Ryujinx.Graphics.Gpu.Memory alignedEndAddress += BufferAlignmentSize; } - CreateBufferAligned(alignedAddress, alignedEndAddress - alignedAddress); + CreateBufferAligned(alignedAddress, alignedEndAddress - alignedAddress, stage); } /// @@ -204,8 +253,9 @@ namespace Ryujinx.Graphics.Gpu.Memory /// /// Address of the buffer in memory /// Size of the buffer in bytes + /// The type of usage that created the buffer /// Alignment of the start address of the buffer in bytes - public void CreateBuffer(ulong address, ulong size, ulong alignment) + public void CreateBuffer(ulong address, ulong size, BufferStage stage, ulong alignment) { ulong alignmentMask = alignment - 1; ulong pageAlignmentMask = BufferAlignmentMask; @@ -220,7 +270,7 @@ namespace Ryujinx.Graphics.Gpu.Memory alignedEndAddress += pageAlignmentMask; } - CreateBufferAligned(alignedAddress, alignedEndAddress - alignedAddress, alignment); + CreateBufferAligned(alignedAddress, alignedEndAddress - alignedAddress, stage, alignment); } /// @@ -228,7 +278,8 @@ namespace Ryujinx.Graphics.Gpu.Memory /// if it does not exist yet. /// /// Physical ranges of memory - private void CreateMultiRangeBuffer(MultiRange range) + /// The type of usage that created the buffer + private void CreateMultiRangeBuffer(MultiRange range, BufferStage stage) { // Ensure all non-contiguous buffer we might use are sparse aligned. for (int i = 0; i < range.Count; i++) @@ -237,7 +288,7 @@ namespace Ryujinx.Graphics.Gpu.Memory if (subRange.Address != MemoryManager.PteUnmapped) { - CreateBuffer(subRange.Address, subRange.Size, SparseBufferAlignmentSize); + CreateBuffer(subRange.Address, subRange.Size, stage, SparseBufferAlignmentSize); } } @@ -263,41 +314,108 @@ namespace Ryujinx.Graphics.Gpu.Memory } } - BufferRange[] storages = new BufferRange[range.Count]; + MultiRangeBuffer multiRangeBuffer; + MemoryRange[] alignedSubRanges = new MemoryRange[range.Count]; ulong alignmentMask = SparseBufferAlignmentSize - 1; - for (int i = 0; i < range.Count; i++) + if (_context.Capabilities.SupportsSparseBuffer) { - MemoryRange subRange = range.GetSubRange(i); + BufferRange[] storages = new BufferRange[range.Count]; + + for (int i = 0; i < range.Count; i++) + { + MemoryRange subRange = range.GetSubRange(i); + + if (subRange.Address != MemoryManager.PteUnmapped) + { + ulong endAddress = subRange.Address + subRange.Size; + + ulong alignedAddress = subRange.Address & ~alignmentMask; + ulong alignedEndAddress = (endAddress + alignmentMask) & ~alignmentMask; + ulong alignedSize = alignedEndAddress - alignedAddress; + + Buffer buffer = _buffers.FindFirstOverlap(alignedAddress, alignedSize); + BufferRange bufferRange = buffer.GetRange(alignedAddress, alignedSize, false); + + alignedSubRanges[i] = new MemoryRange(alignedAddress, alignedSize); + storages[i] = bufferRange; + } + else + { + ulong alignedSize = (subRange.Size + alignmentMask) & ~alignmentMask; + + alignedSubRanges[i] = new MemoryRange(MemoryManager.PteUnmapped, alignedSize); + storages[i] = new BufferRange(BufferHandle.Null, 0, (int)alignedSize); + } + } + + multiRangeBuffer = new(_context, new MultiRange(alignedSubRanges), storages); + } + else + { + for (int i = 0; i < range.Count; i++) + { + MemoryRange subRange = range.GetSubRange(i); + + if (subRange.Address != MemoryManager.PteUnmapped) + { + ulong endAddress = subRange.Address + subRange.Size; + + ulong alignedAddress = subRange.Address & ~alignmentMask; + ulong alignedEndAddress = (endAddress + alignmentMask) & ~alignmentMask; + ulong alignedSize = alignedEndAddress - alignedAddress; + + alignedSubRanges[i] = new MemoryRange(alignedAddress, alignedSize); + } + else + { + ulong alignedSize = (subRange.Size + alignmentMask) & ~alignmentMask; + + alignedSubRanges[i] = new MemoryRange(MemoryManager.PteUnmapped, alignedSize); + } + } + + multiRangeBuffer = new(_context, new MultiRange(alignedSubRanges)); + + UpdateVirtualBufferDependencies(multiRangeBuffer); + } + + _multiRangeBuffers.Add(multiRangeBuffer); + } + + /// + /// Adds two-way dependencies to all physical buffers contained within a given virtual buffer. + /// + /// Virtual buffer to have dependencies added + private void UpdateVirtualBufferDependencies(MultiRangeBuffer virtualBuffer) + { + virtualBuffer.ClearPhysicalDependencies(); + + ulong dstOffset = 0; + + HashSet physicalBuffers = new(); + + for (int i = 0; i < virtualBuffer.Range.Count; i++) + { + MemoryRange subRange = virtualBuffer.Range.GetSubRange(i); if (subRange.Address != MemoryManager.PteUnmapped) { - ulong endAddress = subRange.Address + subRange.Size; + Buffer buffer = _buffers.FindFirstOverlap(subRange.Address, subRange.Size); - ulong alignedAddress = subRange.Address & ~alignmentMask; - ulong alignedEndAddress = (endAddress + alignmentMask) & ~alignmentMask; - ulong alignedSize = alignedEndAddress - alignedAddress; - - Buffer buffer = _buffers.FindFirstOverlap(alignedAddress, alignedSize); - BufferRange bufferRange = buffer.GetRange(alignedAddress, alignedSize, false); - - storages[i] = bufferRange; - alignedSubRanges[i] = new MemoryRange(alignedAddress, alignedSize); + virtualBuffer.AddPhysicalDependency(buffer, subRange.Address, dstOffset, subRange.Size); + physicalBuffers.Add(buffer); } - else - { - ulong alignedSize = (subRange.Size + alignmentMask) & ~alignmentMask; - storages[i] = new BufferRange(BufferHandle.Null, 0, (int)alignedSize); - alignedSubRanges[i] = new MemoryRange(MemoryManager.PteUnmapped, alignedSize); - } + dstOffset += subRange.Size; } - MultiRangeBuffer multiRangeBuffer = new(_context, new MultiRange(alignedSubRanges), storages); - - _multiRangeBuffers.Add(multiRangeBuffer); + foreach (var buffer in physicalBuffers) + { + buffer.CopyToDependantVirtualBuffer(virtualBuffer); + } } /// @@ -320,9 +438,9 @@ namespace Ryujinx.Graphics.Gpu.Memory result.EndGpuAddress < gpuVa + size || result.UnmappedSequence != result.Buffer.UnmappedSequence) { - MultiRange range = TranslateAndCreateBuffer(memoryManager, gpuVa, size); + MultiRange range = TranslateAndCreateBuffer(memoryManager, gpuVa, size, BufferStage.Internal); ulong address = range.GetSubRange(0).Address; - result = new BufferCacheEntry(address, gpuVa, GetBuffer(address, size)); + result = new BufferCacheEntry(address, gpuVa, GetBuffer(address, size, BufferStage.Internal)); _dirtyCache[gpuVa] = result; } @@ -355,9 +473,9 @@ namespace Ryujinx.Graphics.Gpu.Memory result.EndGpuAddress < alignedEndGpuVa || result.UnmappedSequence != result.Buffer.UnmappedSequence) { - MultiRange range = TranslateAndCreateBuffer(memoryManager, alignedGpuVa, size); + MultiRange range = TranslateAndCreateBuffer(memoryManager, alignedGpuVa, size, BufferStage.None); ulong address = range.GetSubRange(0).Address; - result = new BufferCacheEntry(address, alignedGpuVa, GetBuffer(address, size)); + result = new BufferCacheEntry(address, alignedGpuVa, GetBuffer(address, size, BufferStage.None)); _modifiedCache[alignedGpuVa] = result; } @@ -374,7 +492,8 @@ namespace Ryujinx.Graphics.Gpu.Memory /// /// Address of the buffer in guest memory /// Size in bytes of the buffer - private void CreateBufferAligned(ulong address, ulong size) + /// The type of usage that created the buffer + private void CreateBufferAligned(ulong address, ulong size, BufferStage stage) { Buffer[] overlaps = _bufferOverlaps; int overlapsCount = _buffers.FindOverlapsNonOverlapping(address, size, ref overlaps); @@ -435,13 +554,13 @@ namespace Ryujinx.Graphics.Gpu.Memory ulong newSize = endAddress - address; - CreateBufferAligned(address, newSize, anySparseCompatible, overlaps, overlapsCount); + CreateBufferAligned(address, newSize, stage, anySparseCompatible, overlaps, overlapsCount); } } else { // No overlap, just create a new buffer. - Buffer buffer = new(_context, _physicalMemory, address, size, sparseCompatible: false); + Buffer buffer = new(_context, _physicalMemory, address, size, stage, sparseCompatible: false); lock (_buffers) { @@ -459,8 +578,9 @@ namespace Ryujinx.Graphics.Gpu.Memory /// /// Address of the buffer in guest memory /// Size in bytes of the buffer + /// The type of usage that created the buffer /// Alignment of the start address of the buffer - private void CreateBufferAligned(ulong address, ulong size, ulong alignment) + private void CreateBufferAligned(ulong address, ulong size, BufferStage stage, ulong alignment) { Buffer[] overlaps = _bufferOverlaps; int overlapsCount = _buffers.FindOverlapsNonOverlapping(address, size, ref overlaps); @@ -513,13 +633,13 @@ namespace Ryujinx.Graphics.Gpu.Memory ulong newSize = endAddress - address; - CreateBufferAligned(address, newSize, sparseAligned, overlaps, overlapsCount); + CreateBufferAligned(address, newSize, stage, sparseAligned, overlaps, overlapsCount); } } else { // No overlap, just create a new buffer. - Buffer buffer = new(_context, _physicalMemory, address, size, sparseAligned); + Buffer buffer = new(_context, _physicalMemory, address, size, stage, sparseAligned); lock (_buffers) { @@ -537,12 +657,13 @@ namespace Ryujinx.Graphics.Gpu.Memory /// /// Address of the buffer in guest memory /// Size in bytes of the buffer + /// The type of usage that created the buffer /// Indicates if the buffer can be used in a sparse buffer mapping /// Buffers overlapping the range /// Total of overlaps - private void CreateBufferAligned(ulong address, ulong size, bool sparseCompatible, Buffer[] overlaps, int overlapsCount) + private void CreateBufferAligned(ulong address, ulong size, BufferStage stage, bool sparseCompatible, Buffer[] overlaps, int overlapsCount) { - Buffer newBuffer = new Buffer(_context, _physicalMemory, address, size, sparseCompatible, overlaps.Take(overlapsCount)); + Buffer newBuffer = new Buffer(_context, _physicalMemory, address, size, stage, sparseCompatible, overlaps.Take(overlapsCount)); lock (_buffers) { @@ -593,7 +714,7 @@ namespace Ryujinx.Graphics.Gpu.Memory for (int index = 0; index < overlapCount; index++) { - CreateMultiRangeBuffer(overlaps[index].Range); + CreateMultiRangeBuffer(overlaps[index].Range, BufferStage.None); } } @@ -620,8 +741,8 @@ namespace Ryujinx.Graphics.Gpu.Memory /// Size in bytes of the copy public void CopyBuffer(MemoryManager memoryManager, ulong srcVa, ulong dstVa, ulong size) { - MultiRange srcRange = TranslateAndCreateMultiBuffers(memoryManager, srcVa, size); - MultiRange dstRange = TranslateAndCreateMultiBuffers(memoryManager, dstVa, size); + MultiRange srcRange = TranslateAndCreateMultiBuffersPhysicalOnly(memoryManager, srcVa, size, BufferStage.Copy); + MultiRange dstRange = TranslateAndCreateMultiBuffersPhysicalOnly(memoryManager, dstVa, size, BufferStage.Copy); if (srcRange.Count == 1 && dstRange.Count == 1) { @@ -677,8 +798,8 @@ namespace Ryujinx.Graphics.Gpu.Memory /// Size in bytes of the copy private void CopyBufferSingleRange(MemoryManager memoryManager, ulong srcAddress, ulong dstAddress, ulong size) { - Buffer srcBuffer = GetBuffer(srcAddress, size); - Buffer dstBuffer = GetBuffer(dstAddress, size); + Buffer srcBuffer = GetBuffer(srcAddress, size, BufferStage.Copy); + Buffer dstBuffer = GetBuffer(dstAddress, size, BufferStage.Copy); int srcOffset = (int)(srcAddress - srcBuffer.Address); int dstOffset = (int)(dstAddress - dstBuffer.Address); @@ -692,7 +813,7 @@ namespace Ryujinx.Graphics.Gpu.Memory if (srcBuffer.IsModified(srcAddress, size)) { - dstBuffer.SignalModified(dstAddress, size); + dstBuffer.SignalModified(dstAddress, size, BufferStage.Copy); } else { @@ -701,6 +822,8 @@ namespace Ryujinx.Graphics.Gpu.Memory dstBuffer.ClearModified(dstAddress, size); memoryManager.Physical.WriteTrackedResource(dstAddress, memoryManager.Physical.GetSpan(srcAddress, (int)size), ResourceKind.Buffer); } + + dstBuffer.CopyToDependantVirtualBuffers(dstAddress, size); } /// @@ -715,18 +838,20 @@ namespace Ryujinx.Graphics.Gpu.Memory /// Value to be written into the buffer public void ClearBuffer(MemoryManager memoryManager, ulong gpuVa, ulong size, uint value) { - MultiRange range = TranslateAndCreateMultiBuffers(memoryManager, gpuVa, size); + MultiRange range = TranslateAndCreateMultiBuffersPhysicalOnly(memoryManager, gpuVa, size, BufferStage.Copy); for (int index = 0; index < range.Count; index++) { MemoryRange subRange = range.GetSubRange(index); - Buffer buffer = GetBuffer(subRange.Address, subRange.Size); + Buffer buffer = GetBuffer(subRange.Address, subRange.Size, BufferStage.Copy); int offset = (int)(subRange.Address - buffer.Address); _context.Renderer.Pipeline.ClearBuffer(buffer.Handle, offset, (int)subRange.Size, value); memoryManager.Physical.FillTrackedResource(subRange.Address, subRange.Size, value, ResourceKind.Buffer); + + buffer.CopyToDependantVirtualBuffers(subRange.Address, subRange.Size); } } @@ -734,18 +859,19 @@ namespace Ryujinx.Graphics.Gpu.Memory /// Gets a buffer sub-range starting at a given memory address, aligned to the next page boundary. /// /// Physical regions of memory where the buffer is mapped + /// Buffer stage that triggered the access /// Whether the buffer will be written to by this use /// The buffer sub-range starting at the given memory address - public BufferRange GetBufferRangeAligned(MultiRange range, bool write = false) + public BufferRange GetBufferRangeAligned(MultiRange range, BufferStage stage, bool write = false) { if (range.Count > 1) { - return GetBuffer(range, write).GetRange(range); + return GetBuffer(range, stage, write).GetRange(range); } else { MemoryRange subRange = range.GetSubRange(0); - return GetBuffer(subRange.Address, subRange.Size, write).GetRangeAligned(subRange.Address, subRange.Size, write); + return GetBuffer(subRange.Address, subRange.Size, stage, write).GetRangeAligned(subRange.Address, subRange.Size, write); } } @@ -753,18 +879,19 @@ namespace Ryujinx.Graphics.Gpu.Memory /// Gets a buffer sub-range for a given memory range. /// /// Physical regions of memory where the buffer is mapped + /// Buffer stage that triggered the access /// Whether the buffer will be written to by this use /// The buffer sub-range for the given range - public BufferRange GetBufferRange(MultiRange range, bool write = false) + public BufferRange GetBufferRange(MultiRange range, BufferStage stage, bool write = false) { if (range.Count > 1) { - return GetBuffer(range, write).GetRange(range); + return GetBuffer(range, stage, write).GetRange(range); } else { MemoryRange subRange = range.GetSubRange(0); - return GetBuffer(subRange.Address, subRange.Size, write).GetRange(subRange.Address, subRange.Size, write); + return GetBuffer(subRange.Address, subRange.Size, stage, write).GetRange(subRange.Address, subRange.Size, write); } } @@ -773,9 +900,10 @@ namespace Ryujinx.Graphics.Gpu.Memory /// A buffer overlapping with the specified range is assumed to already exist on the cache. /// /// Physical regions of memory where the buffer is mapped + /// Buffer stage that triggered the access /// Whether the buffer will be written to by this use /// The buffer where the range is fully contained - private MultiRangeBuffer GetBuffer(MultiRange range, bool write = false) + private MultiRangeBuffer GetBuffer(MultiRange range, BufferStage stage, bool write = false) { for (int i = 0; i < range.Count; i++) { @@ -787,7 +915,7 @@ namespace Ryujinx.Graphics.Gpu.Memory if (write) { - subBuffer.SignalModified(subRange.Address, subRange.Size); + subBuffer.SignalModified(subRange.Address, subRange.Size, stage); } } @@ -806,6 +934,11 @@ namespace Ryujinx.Graphics.Gpu.Memory } } + if (write && buffer != null && !_context.Capabilities.SupportsSparseBuffer) + { + buffer.AddModifiedRegion(range, ++_virtualModifiedSequenceNumber); + } + return buffer; } @@ -815,9 +948,10 @@ namespace Ryujinx.Graphics.Gpu.Memory /// /// Start address of the memory range /// Size in bytes of the memory range + /// Buffer stage that triggered the access /// Whether the buffer will be written to by this use /// The buffer where the range is fully contained - private Buffer GetBuffer(ulong address, ulong size, bool write = false) + private Buffer GetBuffer(ulong address, ulong size, BufferStage stage, bool write = false) { Buffer buffer; @@ -825,11 +959,12 @@ namespace Ryujinx.Graphics.Gpu.Memory { buffer = _buffers.FindFirstOverlap(address, size); + buffer.CopyFromDependantVirtualBuffers(); buffer.SynchronizeMemory(address, size); if (write) { - buffer.SignalModified(address, size); + buffer.SignalModified(address, size, stage); } } else @@ -849,14 +984,14 @@ namespace Ryujinx.Graphics.Gpu.Memory if (range.Count == 1) { MemoryRange subRange = range.GetSubRange(0); - SynchronizeBufferRange(subRange.Address, subRange.Size); + SynchronizeBufferRange(subRange.Address, subRange.Size, copyBackVirtual: true); } else { for (int index = 0; index < range.Count; index++) { MemoryRange subRange = range.GetSubRange(index); - SynchronizeBufferRange(subRange.Address, subRange.Size); + SynchronizeBufferRange(subRange.Address, subRange.Size, copyBackVirtual: false); } } } @@ -866,16 +1001,35 @@ namespace Ryujinx.Graphics.Gpu.Memory /// /// Start address of the memory range /// Size in bytes of the memory range - private void SynchronizeBufferRange(ulong address, ulong size) + /// Whether virtual buffers that uses this buffer as backing memory should have its data copied back if modified + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void SynchronizeBufferRange(ulong address, ulong size, bool copyBackVirtual) { if (size != 0) { Buffer buffer = _buffers.FindFirstOverlap(address, size); + if (copyBackVirtual) + { + buffer.CopyFromDependantVirtualBuffers(); + } + buffer.SynchronizeMemory(address, size); } } + /// + /// Signal that the given buffer's handle has changed, + /// forcing rebind and any overlapping multi-range buffers to be recreated. + /// + /// The buffer that has changed handle + public void BufferBackingChanged(Buffer buffer) + { + NotifyBuffersModified?.Invoke(); + + RecreateMultiRangeBuffers(buffer.Address, buffer.Size); + } + /// /// Prune any invalid entries from a quick access dictionary. /// diff --git a/src/Ryujinx.Graphics.Gpu/Memory/BufferManager.cs b/src/Ryujinx.Graphics.Gpu/Memory/BufferManager.cs index c65602b5b..409867e09 100644 --- a/src/Ryujinx.Graphics.Gpu/Memory/BufferManager.cs +++ b/src/Ryujinx.Graphics.Gpu/Memory/BufferManager.cs @@ -27,6 +27,8 @@ namespace Ryujinx.Graphics.Gpu.Memory private readonly VertexBuffer[] _vertexBuffers; private readonly BufferBounds[] _transformFeedbackBuffers; private readonly List _bufferTextures; + private readonly List> _bufferTextureArrays; + private readonly List> _bufferImageArrays; private readonly BufferAssignment[] _ranges; /// @@ -140,11 +142,12 @@ namespace Ryujinx.Graphics.Gpu.Memory } _bufferTextures = new List(); + _bufferTextureArrays = new List>(); + _bufferImageArrays = new List>(); _ranges = new BufferAssignment[Constants.TotalGpUniformBuffers * Constants.ShaderStages]; } - /// /// Sets the memory range with the index buffer data, to be used for subsequent draw calls. /// @@ -153,7 +156,7 @@ namespace Ryujinx.Graphics.Gpu.Memory /// Type of each index buffer element public void SetIndexBuffer(ulong gpuVa, ulong size, IndexType type) { - MultiRange range = _channel.MemoryManager.Physical.BufferCache.TranslateAndCreateBuffer(_channel.MemoryManager, gpuVa, size); + MultiRange range = _channel.MemoryManager.Physical.BufferCache.TranslateAndCreateBuffer(_channel.MemoryManager, gpuVa, size, BufferStage.IndexBuffer); _indexBuffer.Range = range; _indexBuffer.Type = type; @@ -183,7 +186,7 @@ namespace Ryujinx.Graphics.Gpu.Memory /// Vertex divisor of the buffer, for instanced draws public void SetVertexBuffer(int index, ulong gpuVa, ulong size, int stride, int divisor) { - MultiRange range = _channel.MemoryManager.Physical.BufferCache.TranslateAndCreateBuffer(_channel.MemoryManager, gpuVa, size); + MultiRange range = _channel.MemoryManager.Physical.BufferCache.TranslateAndCreateBuffer(_channel.MemoryManager, gpuVa, size, BufferStage.VertexBuffer); _vertexBuffers[index].Range = range; _vertexBuffers[index].Stride = stride; @@ -210,7 +213,7 @@ namespace Ryujinx.Graphics.Gpu.Memory /// Size in bytes of the transform feedback buffer public void SetTransformFeedbackBuffer(int index, ulong gpuVa, ulong size) { - MultiRange range = _channel.MemoryManager.Physical.BufferCache.TranslateAndCreateMultiBuffers(_channel.MemoryManager, gpuVa, size); + MultiRange range = _channel.MemoryManager.Physical.BufferCache.TranslateAndCreateMultiBuffers(_channel.MemoryManager, gpuVa, size, BufferStage.TransformFeedback); _transformFeedbackBuffers[index] = new BufferBounds(range); _transformFeedbackBuffersDirty = true; @@ -257,7 +260,7 @@ namespace Ryujinx.Graphics.Gpu.Memory gpuVa = BitUtils.AlignDown(gpuVa, (ulong)_context.Capabilities.StorageBufferOffsetAlignment); - MultiRange range = _channel.MemoryManager.Physical.BufferCache.TranslateAndCreateMultiBuffers(_channel.MemoryManager, gpuVa, size); + MultiRange range = _channel.MemoryManager.Physical.BufferCache.TranslateAndCreateMultiBuffers(_channel.MemoryManager, gpuVa, size, BufferStageUtils.ComputeStorage(flags)); _cpStorageBuffers.SetBounds(index, range, flags); } @@ -281,7 +284,7 @@ namespace Ryujinx.Graphics.Gpu.Memory gpuVa = BitUtils.AlignDown(gpuVa, (ulong)_context.Capabilities.StorageBufferOffsetAlignment); - MultiRange range = _channel.MemoryManager.Physical.BufferCache.TranslateAndCreateMultiBuffers(_channel.MemoryManager, gpuVa, size); + MultiRange range = _channel.MemoryManager.Physical.BufferCache.TranslateAndCreateMultiBuffers(_channel.MemoryManager, gpuVa, size, BufferStageUtils.GraphicsStorage(stage, flags)); if (!buffers.Buffers[index].Range.Equals(range)) { @@ -300,7 +303,7 @@ namespace Ryujinx.Graphics.Gpu.Memory /// Size in bytes of the storage buffer public void SetComputeUniformBuffer(int index, ulong gpuVa, ulong size) { - MultiRange range = _channel.MemoryManager.Physical.BufferCache.TranslateAndCreateBuffer(_channel.MemoryManager, gpuVa, size); + MultiRange range = _channel.MemoryManager.Physical.BufferCache.TranslateAndCreateBuffer(_channel.MemoryManager, gpuVa, size, BufferStage.Compute); _cpUniformBuffers.SetBounds(index, range); } @@ -315,7 +318,7 @@ namespace Ryujinx.Graphics.Gpu.Memory /// Size in bytes of the storage buffer public void SetGraphicsUniformBuffer(int stage, int index, ulong gpuVa, ulong size) { - MultiRange range = _channel.MemoryManager.Physical.BufferCache.TranslateAndCreateBuffer(_channel.MemoryManager, gpuVa, size); + MultiRange range = _channel.MemoryManager.Physical.BufferCache.TranslateAndCreateBuffer(_channel.MemoryManager, gpuVa, size, BufferStageUtils.FromShaderStage(stage)); _gpUniformBuffers[stage].SetBounds(index, range); _gpUniformBuffersDirty = true; @@ -418,6 +421,16 @@ namespace Ryujinx.Graphics.Gpu.Memory return _cpUniformBuffers.Buffers[index].Range.GetSubRange(0).Address; } + /// + /// Gets the size of the compute uniform buffer currently bound at the given index. + /// + /// Index of the uniform buffer binding + /// The uniform buffer size, or an undefined value if the buffer is not currently bound + public int GetComputeUniformBufferSize(int index) + { + return (int)_cpUniformBuffers.Buffers[index].Range.GetSubRange(0).Size; + } + /// /// Gets the address of the graphics uniform buffer currently bound at the given index. /// @@ -429,6 +442,17 @@ namespace Ryujinx.Graphics.Gpu.Memory return _gpUniformBuffers[stage].Buffers[index].Range.GetSubRange(0).Address; } + /// + /// Gets the size of the graphics uniform buffer currently bound at the given index. + /// + /// Index of the shader stage + /// Index of the uniform buffer binding + /// The uniform buffer size, or an undefined value if the buffer is not currently bound + public int GetGraphicsUniformBufferSize(int stage, int index) + { + return (int)_gpUniformBuffers[stage].Buffers[index].Range.GetSubRange(0).Size; + } + /// /// Gets the bounds of the uniform buffer currently bound at the given index. /// @@ -459,7 +483,7 @@ namespace Ryujinx.Graphics.Gpu.Memory BindBuffers(bufferCache, _cpStorageBuffers, isStorage: true); BindBuffers(bufferCache, _cpUniformBuffers, isStorage: false); - CommitBufferTextureBindings(); + CommitBufferTextureBindings(bufferCache); // Force rebind after doing compute work. Rebind(); @@ -470,21 +494,22 @@ namespace Ryujinx.Graphics.Gpu.Memory /// /// Commit any queued buffer texture bindings. /// - private void CommitBufferTextureBindings() + /// Buffer cache + private void CommitBufferTextureBindings(BufferCache bufferCache) { if (_bufferTextures.Count > 0) { foreach (var binding in _bufferTextures) { var isStore = binding.BindingInfo.Flags.HasFlag(TextureUsageFlags.ImageStore); - var range = _channel.MemoryManager.Physical.BufferCache.GetBufferRange(binding.Range, isStore); + var range = bufferCache.GetBufferRange(binding.Range, BufferStageUtils.TextureBuffer(binding.Stage, binding.BindingInfo.Flags), isStore); binding.Texture.SetStorage(range); // The texture must be rebound to use the new storage if it was updated. if (binding.IsImage) { - _context.Renderer.Pipeline.SetImage(binding.BindingInfo.Binding, binding.Texture, binding.Format); + _context.Renderer.Pipeline.SetImage(binding.Stage, binding.BindingInfo.Binding, binding.Texture); } else { @@ -494,6 +519,33 @@ namespace Ryujinx.Graphics.Gpu.Memory _bufferTextures.Clear(); } + + if (_bufferTextureArrays.Count > 0 || _bufferImageArrays.Count > 0) + { + ITexture[] textureArray = new ITexture[1]; + + foreach (var binding in _bufferTextureArrays) + { + var range = bufferCache.GetBufferRange(binding.Range, BufferStage.None); + binding.Texture.SetStorage(range); + + textureArray[0] = binding.Texture; + binding.Array.SetTextures(binding.Index, textureArray); + } + + foreach (var binding in _bufferImageArrays) + { + var isStore = binding.BindingInfo.Flags.HasFlag(TextureUsageFlags.ImageStore); + var range = bufferCache.GetBufferRange(binding.Range, BufferStage.None, isStore); + binding.Texture.SetStorage(range); + + textureArray[0] = binding.Texture; + binding.Array.SetImages(binding.Index, textureArray); + } + + _bufferTextureArrays.Clear(); + _bufferImageArrays.Clear(); + } } /// @@ -513,7 +565,7 @@ namespace Ryujinx.Graphics.Gpu.Memory if (!_indexBuffer.Range.IsUnmapped) { - BufferRange buffer = bufferCache.GetBufferRange(_indexBuffer.Range); + BufferRange buffer = bufferCache.GetBufferRange(_indexBuffer.Range, BufferStage.IndexBuffer); _context.Renderer.Pipeline.SetIndexBuffer(buffer, _indexBuffer.Type); } @@ -545,7 +597,7 @@ namespace Ryujinx.Graphics.Gpu.Memory continue; } - BufferRange buffer = bufferCache.GetBufferRange(vb.Range); + BufferRange buffer = bufferCache.GetBufferRange(vb.Range, BufferStage.VertexBuffer); vertexBuffers[index] = new VertexBufferDescriptor(buffer, vb.Stride, vb.Divisor); } @@ -585,7 +637,7 @@ namespace Ryujinx.Graphics.Gpu.Memory continue; } - tfbs[index] = bufferCache.GetBufferRange(tfb.Range, write: true); + tfbs[index] = bufferCache.GetBufferRange(tfb.Range, BufferStage.TransformFeedback, write: true); } _context.Renderer.Pipeline.SetTransformFeedbackBuffers(tfbs); @@ -632,7 +684,7 @@ namespace Ryujinx.Graphics.Gpu.Memory _context.SupportBufferUpdater.SetTfeOffset(index, tfeOffset); - buffers[index] = new BufferAssignment(index, bufferCache.GetBufferRange(range, write: true)); + buffers[index] = new BufferAssignment(index, bufferCache.GetBufferRange(range, BufferStage.TransformFeedback, write: true)); } } @@ -676,7 +728,7 @@ namespace Ryujinx.Graphics.Gpu.Memory UpdateBuffers(_gpUniformBuffers); } - CommitBufferTextureBindings(); + CommitBufferTextureBindings(bufferCache); _rebind = false; @@ -699,6 +751,7 @@ namespace Ryujinx.Graphics.Gpu.Memory for (ShaderStage stage = ShaderStage.Vertex; stage <= ShaderStage.Fragment; stage++) { ref var buffers = ref bindings[(int)stage - 1]; + BufferStage bufferStage = BufferStageUtils.FromShaderStage(stage); for (int index = 0; index < buffers.Count; index++) { @@ -710,8 +763,8 @@ namespace Ryujinx.Graphics.Gpu.Memory { var isWrite = bounds.Flags.HasFlag(BufferUsageFlags.Write); var range = isStorage - ? bufferCache.GetBufferRangeAligned(bounds.Range, isWrite) - : bufferCache.GetBufferRange(bounds.Range); + ? bufferCache.GetBufferRangeAligned(bounds.Range, bufferStage | BufferStageUtils.FromUsage(bounds.Flags), isWrite) + : bufferCache.GetBufferRange(bounds.Range, bufferStage); ranges[rangesCount++] = new BufferAssignment(bindingInfo.Binding, range); } @@ -747,8 +800,8 @@ namespace Ryujinx.Graphics.Gpu.Memory { var isWrite = bounds.Flags.HasFlag(BufferUsageFlags.Write); var range = isStorage - ? bufferCache.GetBufferRangeAligned(bounds.Range, isWrite) - : bufferCache.GetBufferRange(bounds.Range); + ? bufferCache.GetBufferRangeAligned(bounds.Range, BufferStageUtils.ComputeStorage(bounds.Flags), isWrite) + : bufferCache.GetBufferRange(bounds.Range, BufferStage.Compute); ranges[rangesCount++] = new BufferAssignment(bindingInfo.Binding, range); } @@ -820,12 +873,57 @@ namespace Ryujinx.Graphics.Gpu.Memory ITexture texture, MultiRange range, TextureBindingInfo bindingInfo, - Format format, bool isImage) { - _channel.MemoryManager.Physical.BufferCache.CreateBuffer(range); + _channel.MemoryManager.Physical.BufferCache.CreateBuffer(range, BufferStageUtils.TextureBuffer(stage, bindingInfo.Flags)); - _bufferTextures.Add(new BufferTextureBinding(stage, texture, range, bindingInfo, format, isImage)); + _bufferTextures.Add(new BufferTextureBinding(stage, texture, range, bindingInfo, isImage)); + } + + /// + /// Sets the buffer storage of a buffer texture array element. This will be bound when the buffer manager commits bindings. + /// + /// Shader stage accessing the texture + /// Texture array where the element will be inserted + /// Buffer texture + /// Physical ranges of memory where the buffer texture data is located + /// Binding info for the buffer texture + /// Index of the binding on the array + /// Format of the buffer texture + public void SetBufferTextureStorage( + ShaderStage stage, + ITextureArray array, + ITexture texture, + MultiRange range, + TextureBindingInfo bindingInfo, + int index) + { + _channel.MemoryManager.Physical.BufferCache.CreateBuffer(range, BufferStageUtils.TextureBuffer(stage, bindingInfo.Flags)); + + _bufferTextureArrays.Add(new BufferTextureArrayBinding(array, texture, range, bindingInfo, index)); + } + + /// + /// Sets the buffer storage of a buffer image array element. This will be bound when the buffer manager commits bindings. + /// + /// Shader stage accessing the texture + /// Image array where the element will be inserted + /// Buffer texture + /// Physical ranges of memory where the buffer texture data is located + /// Binding info for the buffer texture + /// Index of the binding on the array + /// Format of the buffer texture + public void SetBufferTextureStorage( + ShaderStage stage, + IImageArray array, + ITexture texture, + MultiRange range, + TextureBindingInfo bindingInfo, + int index) + { + _channel.MemoryManager.Physical.BufferCache.CreateBuffer(range, BufferStageUtils.TextureBuffer(stage, bindingInfo.Flags)); + + _bufferImageArrays.Add(new BufferTextureArrayBinding(array, texture, range, bindingInfo, index)); } /// diff --git a/src/Ryujinx.Graphics.Gpu/Memory/BufferMigration.cs b/src/Ryujinx.Graphics.Gpu/Memory/BufferMigration.cs index 0a5268031..ce9985318 100644 --- a/src/Ryujinx.Graphics.Gpu/Memory/BufferMigration.cs +++ b/src/Ryujinx.Graphics.Gpu/Memory/BufferMigration.cs @@ -1,37 +1,21 @@ using System; +using System.Threading; namespace Ryujinx.Graphics.Gpu.Memory { /// - /// A record of when buffer data was copied from one buffer to another, along with the SyncNumber when the migration will be complete. - /// Keeps the source buffer alive for data flushes until the migration is complete. + /// A record of when buffer data was copied from multiple buffers to one migration target, + /// along with the SyncNumber when the migration will be complete. + /// Keeps the source buffers alive for data flushes until the migration is complete. + /// All spans cover the full range of the "destination" buffer. /// internal class BufferMigration : IDisposable { /// - /// The offset for the migrated region. + /// Ranges from source buffers that were copied as part of this migration. + /// Ordered by increasing base address. /// - private readonly ulong _offset; - - /// - /// The size for the migrated region. - /// - private readonly ulong _size; - - /// - /// The buffer that was migrated from. - /// - private readonly Buffer _buffer; - - /// - /// The source range action, to be called on overlap with an unreached sync number. - /// - private readonly Action _sourceRangeAction; - - /// - /// The source range list. - /// - private readonly BufferModifiedRangeList _source; + public BufferMigrationSpan[] Spans { get; private set; } /// /// The destination range list. This range list must be updated when flushing the source. @@ -43,55 +27,193 @@ namespace Ryujinx.Graphics.Gpu.Memory /// public readonly ulong SyncNumber; + /// + /// Number of active users there are traversing this migration's spans. + /// + private int _refCount; + + /// + /// Create a new buffer migration. + /// + /// Source spans for the migration + /// Destination buffer range list + /// Sync number where this migration will be complete + public BufferMigration(BufferMigrationSpan[] spans, BufferModifiedRangeList destination, ulong syncNumber) + { + Spans = spans; + Destination = destination; + SyncNumber = syncNumber; + } + + /// + /// Add a span to the migration. Allocates a new array with the target size, and replaces it. + /// + /// + /// The base address for the span is assumed to be higher than all other spans in the migration, + /// to keep the span array ordered. + /// + public void AddSpanToEnd(BufferMigrationSpan span) + { + BufferMigrationSpan[] oldSpans = Spans; + + BufferMigrationSpan[] newSpans = new BufferMigrationSpan[oldSpans.Length + 1]; + + oldSpans.CopyTo(newSpans, 0); + + newSpans[oldSpans.Length] = span; + + Spans = newSpans; + } + + /// + /// Performs the given range action, or one from a migration that overlaps and has not synced yet. + /// + /// The offset to pass to the action + /// The size to pass to the action + /// The sync number that has been reached + /// The action to perform + public void RangeActionWithMigration(ulong offset, ulong size, ulong syncNumber, BufferFlushAction rangeAction) + { + long syncDiff = (long)(syncNumber - SyncNumber); + + if (syncDiff >= 0) + { + // The migration has completed. Run the parent action. + rangeAction(offset, size, syncNumber); + } + else + { + Interlocked.Increment(ref _refCount); + + ulong prevAddress = offset; + ulong endAddress = offset + size; + + foreach (BufferMigrationSpan span in Spans) + { + if (!span.Overlaps(offset, size)) + { + continue; + } + + if (span.Address > prevAddress) + { + // There's a gap between this span and the last (or the start address). Flush the range using the parent action. + + rangeAction(prevAddress, span.Address - prevAddress, syncNumber); + } + + span.RangeActionWithMigration(offset, size, syncNumber); + + prevAddress = span.Address + span.Size; + } + + if (endAddress > prevAddress) + { + // There's a gap at the end of the range with no migration. Flush the range using the parent action. + rangeAction(prevAddress, endAddress - prevAddress, syncNumber); + } + + Interlocked.Decrement(ref _refCount); + } + } + + /// + /// Dispose the buffer migration. This removes the reference from the destination range list, + /// and runs all the dispose buffers for the migration spans. (typically disposes the source buffer) + /// + public void Dispose() + { + while (Volatile.Read(ref _refCount) > 0) + { + // Coming into this method, the sync for the migration will be met, so nothing can increment the ref count. + // However, an existing traversal of the spans for data flush could still be in progress. + // Spin if this is ever the case, so they don't get disposed before the operation is complete. + } + + Destination.RemoveMigration(this); + + foreach (BufferMigrationSpan span in Spans) + { + span.Dispose(); + } + } + } + + /// + /// A record of when buffer data was copied from one buffer to another, for a specific range in a source buffer. + /// Keeps the source buffer alive for data flushes until the migration is complete. + /// + internal readonly struct BufferMigrationSpan : IDisposable + { + /// + /// The offset for the migrated region. + /// + public readonly ulong Address; + + /// + /// The size for the migrated region. + /// + public readonly ulong Size; + + /// + /// The action to perform when the migration isn't needed anymore. + /// + private readonly Action _disposeAction; + + /// + /// The source range action, to be called on overlap with an unreached sync number. + /// + private readonly BufferFlushAction _sourceRangeAction; + + /// + /// Optional migration for the source data. Can chain together if many migrations happen in a short time. + /// If this is null, then _sourceRangeAction will always provide up to date data. + /// + private readonly BufferMigration _source; + /// /// Creates a record for a buffer migration. /// /// The source buffer for this migration + /// The action to perform when the migration isn't needed anymore /// The flush action for the source buffer - /// The modified range list for the source buffer - /// The modified range list for the destination buffer - /// The sync number for when the migration is complete - public BufferMigration( + /// Pending migration for the source buffer + public BufferMigrationSpan( Buffer buffer, - Action sourceRangeAction, - BufferModifiedRangeList source, - BufferModifiedRangeList dest, - ulong syncNumber) + Action disposeAction, + BufferFlushAction sourceRangeAction, + BufferMigration source) { - _offset = buffer.Address; - _size = buffer.Size; - _buffer = buffer; + Address = buffer.Address; + Size = buffer.Size; + _disposeAction = disposeAction; _sourceRangeAction = sourceRangeAction; _source = source; - Destination = dest; - SyncNumber = syncNumber; } + /// + /// Creates a record for a buffer migration, using the default buffer dispose action. + /// + /// The source buffer for this migration + /// The flush action for the source buffer + /// Pending migration for the source buffer + public BufferMigrationSpan( + Buffer buffer, + BufferFlushAction sourceRangeAction, + BufferMigration source) : this(buffer, buffer.DecrementReferenceCount, sourceRangeAction, source) { } + /// /// Determine if the given range overlaps this migration, and has not been completed yet. /// /// Start offset /// Range size - /// The sync number that was waited on /// True if overlapping and in progress, false otherwise - public bool Overlaps(ulong offset, ulong size, ulong syncNumber) + public bool Overlaps(ulong offset, ulong size) { ulong end = offset + size; - ulong destEnd = _offset + _size; - long syncDiff = (long)(syncNumber - SyncNumber); // syncNumber is less if the copy has not completed. + ulong destEnd = Address + Size; - return !(end <= _offset || offset >= destEnd) && syncDiff < 0; - } - - /// - /// Determine if the given range matches this migration. - /// - /// Start offset - /// Range size - /// True if the range exactly matches, false otherwise - public bool FullyMatches(ulong offset, ulong size) - { - return _offset == offset && _size == size; + return !(end <= Address || offset >= destEnd); } /// @@ -100,26 +222,30 @@ namespace Ryujinx.Graphics.Gpu.Memory /// Start offset /// Range size /// Current sync number - /// The modified range list that originally owned this range - public void RangeActionWithMigration(ulong offset, ulong size, ulong syncNumber, BufferModifiedRangeList parent) + public void RangeActionWithMigration(ulong offset, ulong size, ulong syncNumber) { ulong end = offset + size; - end = Math.Min(_offset + _size, end); - offset = Math.Max(_offset, offset); + end = Math.Min(Address + Size, end); + offset = Math.Max(Address, offset); size = end - offset; - _source.RangeActionWithMigration(offset, size, syncNumber, parent, _sourceRangeAction); + if (_source != null) + { + _source.RangeActionWithMigration(offset, size, syncNumber, _sourceRangeAction); + } + else + { + _sourceRangeAction(offset, size, syncNumber); + } } /// - /// Removes this reference to the range list, potentially allowing for the source buffer to be disposed. + /// Removes this migration span, potentially allowing for the source buffer to be disposed. /// public void Dispose() { - Destination.RemoveMigration(this); - - _buffer.DecrementReferenceCount(); + _disposeAction(); } } } diff --git a/src/Ryujinx.Graphics.Gpu/Memory/BufferModifiedRangeList.cs b/src/Ryujinx.Graphics.Gpu/Memory/BufferModifiedRangeList.cs index 6ada8a4b2..d330de638 100644 --- a/src/Ryujinx.Graphics.Gpu/Memory/BufferModifiedRangeList.cs +++ b/src/Ryujinx.Graphics.Gpu/Memory/BufferModifiedRangeList.cs @@ -1,7 +1,6 @@ using Ryujinx.Common.Pools; using Ryujinx.Memory.Range; using System; -using System.Collections.Generic; using System.Linq; namespace Ryujinx.Graphics.Gpu.Memory @@ -72,10 +71,10 @@ namespace Ryujinx.Graphics.Gpu.Memory private readonly GpuContext _context; private readonly Buffer _parent; - private readonly Action _flushAction; + private readonly BufferFlushAction _flushAction; - private List _sources; - private BufferMigration _migrationTarget; + private BufferMigration _source; + private BufferModifiedRangeList _migrationTarget; private readonly object _lock = new(); @@ -99,7 +98,7 @@ namespace Ryujinx.Graphics.Gpu.Memory /// GPU context that the buffer range list belongs to /// The parent buffer that owns this range list /// The flush action for the parent buffer - public BufferModifiedRangeList(GpuContext context, Buffer parent, Action flushAction) : base(BackingInitialSize) + public BufferModifiedRangeList(GpuContext context, Buffer parent, BufferFlushAction flushAction) : base(BackingInitialSize) { _context = context; _parent = parent; @@ -199,6 +198,36 @@ namespace Ryujinx.Graphics.Gpu.Memory } } + /// + /// Gets modified ranges within the specified region, and then fires the given action for each range individually. + /// + /// Start address to query + /// Size to query + /// Sync number required for a range to be signalled + /// The action to call for each modified range + public void GetRangesAtSync(ulong address, ulong size, ulong syncNumber, Action rangeAction) + { + int count = 0; + + ref var overlaps = ref ThreadStaticArray.Get(); + + // Range list must be consistent for this operation. + lock (_lock) + { + count = FindOverlapsNonOverlapping(address, size, ref overlaps); + } + + for (int i = 0; i < count; i++) + { + BufferModifiedRange overlap = overlaps[i]; + + if (overlap.SyncNumber == syncNumber) + { + rangeAction(overlap.Address, overlap.Size); + } + } + } + /// /// Gets modified ranges within the specified region, and then fires the given action for each range individually. /// @@ -245,41 +274,16 @@ namespace Ryujinx.Graphics.Gpu.Memory /// The offset to pass to the action /// The size to pass to the action /// The sync number that has been reached - /// The modified range list that originally owned this range /// The action to perform - public void RangeActionWithMigration(ulong offset, ulong size, ulong syncNumber, BufferModifiedRangeList parent, Action rangeAction) + public void RangeActionWithMigration(ulong offset, ulong size, ulong syncNumber, BufferFlushAction rangeAction) { - bool firstSource = true; - - if (parent != this) + if (_source != null) { - lock (_lock) - { - if (_sources != null) - { - foreach (BufferMigration source in _sources) - { - if (source.Overlaps(offset, size, syncNumber)) - { - if (firstSource && !source.FullyMatches(offset, size)) - { - // Perform this buffer's action first. The migrations will run after. - rangeAction(offset, size); - } - - source.RangeActionWithMigration(offset, size, syncNumber, parent); - - firstSource = false; - } - } - } - } + _source.RangeActionWithMigration(offset, size, syncNumber, rangeAction); } - - if (firstSource) + else { - // No overlapping migrations, or they are not meant for this range, flush the data using the given action. - rangeAction(offset, size); + rangeAction(offset, size, syncNumber); } } @@ -319,7 +323,7 @@ namespace Ryujinx.Graphics.Gpu.Memory ClearPart(overlap, clampAddress, clampEnd); - RangeActionWithMigration(clampAddress, clampEnd - clampAddress, waitSync, overlap.Parent, _flushAction); + RangeActionWithMigration(clampAddress, clampEnd - clampAddress, waitSync, _flushAction); } } @@ -329,7 +333,7 @@ namespace Ryujinx.Graphics.Gpu.Memory // There is a migration target to call instead. This can't be changed after set so accessing it outside the lock is fine. - _migrationTarget.Destination.RemoveRangesAndFlush(overlaps, rangeCount, highestDiff, currentSync, address, endAddress); + _migrationTarget.RemoveRangesAndFlush(overlaps, rangeCount, highestDiff, currentSync, address, endAddress); } /// @@ -367,7 +371,7 @@ namespace Ryujinx.Graphics.Gpu.Memory if (rangeCount == -1) { - _migrationTarget.Destination.WaitForAndFlushRanges(address, size); + _migrationTarget.WaitForAndFlushRanges(address, size); return; } @@ -407,6 +411,9 @@ namespace Ryujinx.Graphics.Gpu.Memory /// /// Inherit ranges from another modified range list. /// + /// + /// Assumes that ranges will be inherited in address ascending order. + /// /// The range list to inherit from /// The action to call for each modified range public void InheritRanges(BufferModifiedRangeList ranges, Action registerRangeAction) @@ -415,18 +422,31 @@ namespace Ryujinx.Graphics.Gpu.Memory lock (ranges._lock) { - BufferMigration migration = new(ranges._parent, ranges._flushAction, ranges, this, _context.SyncNumber); - - ranges._parent.IncrementReferenceCount(); - ranges._migrationTarget = migration; - - _context.RegisterBufferMigration(migration); - inheritRanges = ranges.ToArray(); lock (_lock) { - (_sources ??= new List()).Add(migration); + // Copy over the migration from the previous range list + + BufferMigration oldMigration = ranges._source; + + BufferMigrationSpan span = new BufferMigrationSpan(ranges._parent, ranges._flushAction, oldMigration); + ranges._parent.IncrementReferenceCount(); + + if (_source == null) + { + // Create a new migration. + _source = new BufferMigration(new BufferMigrationSpan[] { span }, this, _context.SyncNumber); + + _context.RegisterBufferMigration(_source); + } + else + { + // Extend the migration + _source.AddSpanToEnd(span); + } + + ranges._migrationTarget = this; foreach (BufferModifiedRange range in inheritRanges) { @@ -445,6 +465,27 @@ namespace Ryujinx.Graphics.Gpu.Memory } } + /// + /// Register a migration from previous buffer storage. This migration is from a snapshot of the buffer's + /// current handle to its handle in the future, and is assumed to be complete when the sync action completes. + /// When the migration completes, the handle is disposed. + /// + public void SelfMigration() + { + lock (_lock) + { + BufferMigrationSpan span = new(_parent, _parent.GetSnapshotDisposeAction(), _parent.GetSnapshotFlushAction(), _source); + BufferMigration migration = new(new BufferMigrationSpan[] { span }, this, _context.SyncNumber); + + // Migration target is used to redirect flush actions to the latest range list, + // so we don't need to set it here. (this range list is still the latest) + + _context.RegisterBufferMigration(migration); + + _source = migration; + } + } + /// /// Removes a source buffer migration, indicating its copy has completed. /// @@ -453,7 +494,10 @@ namespace Ryujinx.Graphics.Gpu.Memory { lock (_lock) { - _sources.Remove(migration); + if (_source == migration) + { + _source = null; + } } } diff --git a/src/Ryujinx.Graphics.Gpu/Memory/BufferPreFlush.cs b/src/Ryujinx.Graphics.Gpu/Memory/BufferPreFlush.cs new file mode 100644 index 000000000..d58b9ea66 --- /dev/null +++ b/src/Ryujinx.Graphics.Gpu/Memory/BufferPreFlush.cs @@ -0,0 +1,295 @@ +using Ryujinx.Common; +using Ryujinx.Graphics.GAL; +using System; + +namespace Ryujinx.Graphics.Gpu.Memory +{ + /// + /// Manages flushing ranges from buffers in advance for easy access, if they are flushed often. + /// Typically, from device local memory to a host mapped target for cached access. + /// + internal class BufferPreFlush : IDisposable + { + private const ulong PageSize = MemoryManager.PageSize; + + /// + /// Threshold for the number of copies without a flush required to disable preflush on a page. + /// + private const int DeactivateCopyThreshold = 200; + + /// + /// Value that indicates whether a page has been flushed or copied before. + /// + private enum PreFlushState + { + None, + HasFlushed, + HasCopied + } + + /// + /// Flush state for each page of the buffer. + /// Controls whether data should be copied to the flush buffer, what sync is expected + /// and unflushed copy counting for stopping copies that are no longer needed. + /// + private struct PreFlushPage + { + public PreFlushState State; + public ulong FirstActivatedSync; + public ulong LastCopiedSync; + public int CopyCount; + } + + /// + /// True if there are ranges that should copy to the flush buffer, false otherwise. + /// + public bool ShouldCopy { get; private set; } + + private readonly GpuContext _context; + private readonly Buffer _buffer; + private readonly PreFlushPage[] _pages; + private readonly ulong _address; + private readonly ulong _size; + private readonly ulong _misalignment; + private readonly Action _flushAction; + + private BufferHandle _flushBuffer; + + public BufferPreFlush(GpuContext context, Buffer parent, Action flushAction) + { + _context = context; + _buffer = parent; + _address = parent.Address; + _size = parent.Size; + _pages = new PreFlushPage[BitUtils.DivRoundUp(_size, PageSize)]; + _misalignment = _address & (PageSize - 1); + + _flushAction = flushAction; + } + + /// + /// Ensure that the flush buffer exists. + /// + private void EnsureFlushBuffer() + { + if (_flushBuffer == BufferHandle.Null) + { + _flushBuffer = _context.Renderer.CreateBuffer((int)_size, BufferAccess.HostMemory); + } + } + + /// + /// Gets a page range from an address and size byte range. + /// + /// Range address + /// Range size + /// A page index and count + private (int index, int count) GetPageRange(ulong address, ulong size) + { + ulong offset = address - _address; + ulong endOffset = offset + size; + + int basePage = (int)(offset / PageSize); + int endPage = (int)((endOffset - 1) / PageSize); + + return (basePage, 1 + endPage - basePage); + } + + /// + /// Gets an offset and size range in the parent buffer from a page index and count. + /// + /// Range start page + /// Range page count + /// Offset and size range + private (int offset, int size) GetOffset(int startPage, int count) + { + int offset = (int)((ulong)startPage * PageSize - _misalignment); + int endOffset = (int)((ulong)(startPage + count) * PageSize - _misalignment); + + offset = Math.Max(0, offset); + endOffset = Math.Min((int)_size, endOffset); + + return (offset, endOffset - offset); + } + + /// + /// Copy a range of pages from the parent buffer into the flush buffer. + /// + /// Range start page + /// Range page count + private void CopyPageRange(int startPage, int count) + { + (int offset, int size) = GetOffset(startPage, count); + + EnsureFlushBuffer(); + + _context.Renderer.Pipeline.CopyBuffer(_buffer.Handle, _flushBuffer, offset, offset, size); + } + + /// + /// Copy a modified range into the flush buffer if it's marked as flushed. + /// Any pages the range overlaps are copied, and copies aren't repeated in the same sync number. + /// + /// Range address + /// Range size + public void CopyModified(ulong address, ulong size) + { + (int baseIndex, int count) = GetPageRange(address, size); + ulong syncNumber = _context.SyncNumber; + + int startPage = -1; + + for (int i = 0; i < count; i++) + { + int pageIndex = baseIndex + i; + ref PreFlushPage page = ref _pages[pageIndex]; + + if (page.State > PreFlushState.None) + { + // Perform the copy, and update the state of each page. + if (startPage == -1) + { + startPage = pageIndex; + } + + if (page.State != PreFlushState.HasCopied) + { + page.FirstActivatedSync = syncNumber; + page.State = PreFlushState.HasCopied; + } + else if (page.CopyCount++ >= DeactivateCopyThreshold) + { + page.CopyCount = 0; + page.State = PreFlushState.None; + } + + if (page.LastCopiedSync != syncNumber) + { + page.LastCopiedSync = syncNumber; + } + } + else if (startPage != -1) + { + CopyPageRange(startPage, pageIndex - startPage); + + startPage = -1; + } + } + + if (startPage != -1) + { + CopyPageRange(startPage, (baseIndex + count) - startPage); + } + } + + /// + /// Flush the given page range back into guest memory, optionally using data from the flush buffer. + /// The actual flushed range is an intersection of the page range and the address range. + /// + /// Address range start + /// Address range size + /// Page range start + /// Page range count + /// True if the data should come from the flush buffer + private void FlushPageRange(ulong address, ulong size, int startPage, int count, bool preFlush) + { + (int pageOffset, int pageSize) = GetOffset(startPage, count); + + int offset = (int)(address - _address); + int end = offset + (int)size; + + offset = Math.Max(offset, pageOffset); + end = Math.Min(end, pageOffset + pageSize); + + if (end >= offset) + { + BufferHandle handle = preFlush ? _flushBuffer : _buffer.Handle; + _flushAction(handle, _address + (ulong)offset, (ulong)(end - offset)); + } + } + + /// + /// Flush the given address range back into guest memory, optionally using data from the flush buffer. + /// When a copy has been performed on or before the waited sync number, the data can come from the flush buffer. + /// Otherwise, it flushes the parent buffer directly. + /// + /// Range address + /// Range size + /// Sync number that has been waited for + public void FlushWithAction(ulong address, ulong size, ulong syncNumber) + { + // Copy the parts of the range that have pre-flush copies that have been completed. + // Run the flush action for ranges that don't have pre-flush copies. + + // If a range doesn't have a pre-flush copy, consider adding one. + + (int baseIndex, int count) = GetPageRange(address, size); + + bool rangePreFlushed = false; + int startPage = -1; + + for (int i = 0; i < count; i++) + { + int pageIndex = baseIndex + i; + ref PreFlushPage page = ref _pages[pageIndex]; + + bool flushPage = false; + page.CopyCount = 0; + + if (page.State == PreFlushState.HasCopied) + { + if (syncNumber >= page.FirstActivatedSync) + { + // After the range is first activated, its data will always be copied to the preflush buffer on each sync. + flushPage = true; + } + } + else if (page.State == PreFlushState.None) + { + page.State = PreFlushState.HasFlushed; + ShouldCopy = true; + } + + if (flushPage) + { + if (!rangePreFlushed || startPage == -1) + { + if (startPage != -1) + { + FlushPageRange(address, size, startPage, pageIndex - startPage, false); + } + + rangePreFlushed = true; + startPage = pageIndex; + } + } + else if (rangePreFlushed || startPage == -1) + { + if (startPage != -1) + { + FlushPageRange(address, size, startPage, pageIndex - startPage, true); + } + + rangePreFlushed = false; + startPage = pageIndex; + } + } + + if (startPage != -1) + { + FlushPageRange(address, size, startPage, (baseIndex + count) - startPage, rangePreFlushed); + } + } + + /// + /// Dispose the flush buffer, if present. + /// + public void Dispose() + { + if (_flushBuffer != BufferHandle.Null) + { + _context.Renderer.DeleteBuffer(_flushBuffer); + } + } + } +} diff --git a/src/Ryujinx.Graphics.Gpu/Memory/BufferStage.cs b/src/Ryujinx.Graphics.Gpu/Memory/BufferStage.cs new file mode 100644 index 000000000..d56abda28 --- /dev/null +++ b/src/Ryujinx.Graphics.Gpu/Memory/BufferStage.cs @@ -0,0 +1,99 @@ +using Ryujinx.Graphics.Shader; +using System.Runtime.CompilerServices; + +namespace Ryujinx.Graphics.Gpu.Memory +{ + /// + /// Pipeline stages that can modify buffer data, as well as flags indicating storage usage. + /// Must match ShaderStage for the shader stages, though anything after that can be in any order. + /// + internal enum BufferStage : byte + { + Compute, + Vertex, + TessellationControl, + TessellationEvaluation, + Geometry, + Fragment, + + Indirect, + VertexBuffer, + IndexBuffer, + Copy, + TransformFeedback, + Internal, + None, + + StageMask = 0x3f, + StorageMask = 0xc0, + + StorageRead = 0x40, + StorageWrite = 0x80, + +#pragma warning disable CA1069 // Enums values should not be duplicated + StorageAtomic = 0xc0 +#pragma warning restore CA1069 // Enums values should not be duplicated + } + + /// + /// Utility methods to convert shader stages and binding flags into buffer stages. + /// + internal static class BufferStageUtils + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static BufferStage FromShaderStage(ShaderStage stage) + { + return (BufferStage)stage; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static BufferStage FromShaderStage(int stageIndex) + { + return (BufferStage)(stageIndex + 1); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static BufferStage FromUsage(BufferUsageFlags flags) + { + if (flags.HasFlag(BufferUsageFlags.Write)) + { + return BufferStage.StorageWrite; + } + else + { + return BufferStage.StorageRead; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static BufferStage FromUsage(TextureUsageFlags flags) + { + if (flags.HasFlag(TextureUsageFlags.ImageStore)) + { + return BufferStage.StorageWrite; + } + else + { + return BufferStage.StorageRead; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static BufferStage TextureBuffer(ShaderStage shaderStage, TextureUsageFlags flags) + { + return FromShaderStage(shaderStage) | FromUsage(flags); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static BufferStage GraphicsStorage(int stageIndex, BufferUsageFlags flags) + { + return FromShaderStage(stageIndex) | FromUsage(flags); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static BufferStage ComputeStorage(BufferUsageFlags flags) + { + return BufferStage.Compute | FromUsage(flags); + } + } +} diff --git a/src/Ryujinx.Graphics.Gpu/Memory/BufferTextureArrayBinding.cs b/src/Ryujinx.Graphics.Gpu/Memory/BufferTextureArrayBinding.cs new file mode 100644 index 000000000..a5338fa55 --- /dev/null +++ b/src/Ryujinx.Graphics.Gpu/Memory/BufferTextureArrayBinding.cs @@ -0,0 +1,59 @@ +using Ryujinx.Graphics.GAL; +using Ryujinx.Graphics.Gpu.Image; +using Ryujinx.Memory.Range; + +namespace Ryujinx.Graphics.Gpu.Memory +{ + /// + /// A buffer binding to apply to a buffer texture array element. + /// + readonly struct BufferTextureArrayBinding + { + /// + /// Backend texture or image array. + /// + public T Array { get; } + + /// + /// The buffer texture. + /// + public ITexture Texture { get; } + + /// + /// Physical ranges of memory where the buffer texture data is located. + /// + public MultiRange Range { get; } + + /// + /// The image or sampler binding info for the buffer texture. + /// + public TextureBindingInfo BindingInfo { get; } + + /// + /// Index of the binding on the array. + /// + public int Index { get; } + + /// + /// Create a new buffer texture binding. + /// + /// Array + /// Buffer texture + /// Physical ranges of memory where the buffer texture data is located + /// Binding info + /// Index of the binding on the array + public BufferTextureArrayBinding( + T array, + ITexture texture, + MultiRange range, + TextureBindingInfo bindingInfo, + int index) + { + Array = array; + Texture = texture; + Range = range; + BindingInfo = bindingInfo; + Index = index; + } + } +} diff --git a/src/Ryujinx.Graphics.Gpu/Memory/BufferTextureBinding.cs b/src/Ryujinx.Graphics.Gpu/Memory/BufferTextureBinding.cs index bf0beffa2..1a3fde5b6 100644 --- a/src/Ryujinx.Graphics.Gpu/Memory/BufferTextureBinding.cs +++ b/src/Ryujinx.Graphics.Gpu/Memory/BufferTextureBinding.cs @@ -30,11 +30,6 @@ namespace Ryujinx.Graphics.Gpu.Memory /// public TextureBindingInfo BindingInfo { get; } - /// - /// The image format for the binding. - /// - public Format Format { get; } - /// /// Whether the binding is for an image or a sampler. /// @@ -47,21 +42,18 @@ namespace Ryujinx.Graphics.Gpu.Memory /// Buffer texture /// Physical ranges of memory where the buffer texture data is located /// Binding info - /// Binding format /// Whether the binding is for an image or a sampler public BufferTextureBinding( ShaderStage stage, ITexture texture, MultiRange range, TextureBindingInfo bindingInfo, - Format format, bool isImage) { Stage = stage; Texture = texture; Range = range; BindingInfo = bindingInfo; - Format = format; IsImage = isImage; } } diff --git a/src/Ryujinx.Graphics.Gpu/Memory/MemoryManager.cs b/src/Ryujinx.Graphics.Gpu/Memory/MemoryManager.cs index 5e19bddc3..59e618c02 100644 --- a/src/Ryujinx.Graphics.Gpu/Memory/MemoryManager.cs +++ b/src/Ryujinx.Graphics.Gpu/Memory/MemoryManager.cs @@ -1,3 +1,5 @@ +using Ryujinx.Common.Memory; +using Ryujinx.Graphics.Gpu.Image; using Ryujinx.Memory; using Ryujinx.Memory.Range; using System; @@ -40,9 +42,9 @@ namespace Ryujinx.Graphics.Gpu.Memory internal PhysicalMemory Physical { get; } /// - /// Virtual buffer cache. + /// Virtual range cache. /// - internal VirtualBufferCache VirtualBufferCache { get; } + internal VirtualRangeCache VirtualRangeCache { get; } /// /// Cache of GPU counters. @@ -53,16 +55,18 @@ namespace Ryujinx.Graphics.Gpu.Memory /// Creates a new instance of the GPU memory manager. /// /// Physical memory that this memory manager will map into - internal MemoryManager(PhysicalMemory physicalMemory) + /// The amount of physical CPU Memory Avaiable on the device. + internal MemoryManager(PhysicalMemory physicalMemory, ulong cpuMemorySize) { Physical = physicalMemory; - VirtualBufferCache = new VirtualBufferCache(this); + VirtualRangeCache = new VirtualRangeCache(this); CounterCache = new CounterCache(); _pageTable = new ulong[PtLvl0Size][]; MemoryUnmapped += Physical.TextureCache.MemoryUnmappedHandler; MemoryUnmapped += Physical.BufferCache.MemoryUnmappedHandler; - MemoryUnmapped += VirtualBufferCache.MemoryUnmappedHandler; + MemoryUnmapped += VirtualRangeCache.MemoryUnmappedHandler; MemoryUnmapped += CounterCache.MemoryUnmappedHandler; + Physical.TextureCache.Initialize(cpuMemorySize); } /// @@ -240,11 +244,11 @@ namespace Ryujinx.Graphics.Gpu.Memory } else { - Memory memory = new byte[size]; + MemoryOwner memoryOwner = MemoryOwner.Rent(size); - GetSpan(va, size).CopyTo(memory.Span); + ReadImpl(va, memoryOwner.Span, tracked); - return new WritableRegion(this, va, memory, tracked); + return new WritableRegion(this, va, memoryOwner, tracked); } } @@ -329,49 +333,6 @@ namespace Ryujinx.Graphics.Gpu.Memory } } - /// - /// Writes data to GPU mapped memory, stopping at the first unmapped page at the memory region, if any. - /// - /// GPU virtual address to write the data into - /// The data to be written - public void WriteMapped(ulong va, ReadOnlySpan data) - { - if (IsContiguous(va, data.Length)) - { - Physical.Write(Translate(va), data); - } - else - { - int offset = 0, size; - - if ((va & PageMask) != 0) - { - ulong pa = Translate(va); - - size = Math.Min(data.Length, (int)PageSize - (int)(va & PageMask)); - - if (pa != PteUnmapped && Physical.IsMapped(pa)) - { - Physical.Write(pa, data[..size]); - } - - offset += size; - } - - for (; offset < data.Length; offset += size) - { - ulong pa = Translate(va + (ulong)offset); - - size = Math.Min(data.Length - offset, (int)PageSize); - - if (pa != PteUnmapped && Physical.IsMapped(pa)) - { - Physical.Write(pa, data.Slice(offset, size)); - } - } - } - } - /// /// Runs remap actions that are added to an unmap event. /// These must run after the mapping completes. diff --git a/src/Ryujinx.Graphics.Gpu/Memory/MultiRangeBuffer.cs b/src/Ryujinx.Graphics.Gpu/Memory/MultiRangeBuffer.cs index e039a7a43..d92b0836e 100644 --- a/src/Ryujinx.Graphics.Gpu/Memory/MultiRangeBuffer.cs +++ b/src/Ryujinx.Graphics.Gpu/Memory/MultiRangeBuffer.cs @@ -1,6 +1,7 @@ using Ryujinx.Graphics.GAL; using Ryujinx.Memory.Range; using System; +using System.Collections.Generic; namespace Ryujinx.Graphics.Gpu.Memory { @@ -21,12 +22,73 @@ namespace Ryujinx.Graphics.Gpu.Memory /// public MultiRange Range { get; } + /// + /// Ever increasing counter value indicating when the buffer was modified relative to other buffers. + /// + public int ModificationSequenceNumber { get; private set; } + + /// + /// Physical buffer dependency entry. + /// + private readonly struct PhysicalDependency + { + /// + /// Physical buffer. + /// + public readonly Buffer PhysicalBuffer; + + /// + /// Offset of the range on the physical buffer. + /// + public readonly ulong PhysicalOffset; + + /// + /// Offset of the range on the virtual buffer. + /// + public readonly ulong VirtualOffset; + + /// + /// Size of the range. + /// + public readonly ulong Size; + + /// + /// Creates a new physical dependency. + /// + /// Physical buffer + /// Offset of the range on the physical buffer + /// Offset of the range on the virtual buffer + /// Size of the range + public PhysicalDependency(Buffer physicalBuffer, ulong physicalOffset, ulong virtualOffset, ulong size) + { + PhysicalBuffer = physicalBuffer; + PhysicalOffset = physicalOffset; + VirtualOffset = virtualOffset; + Size = size; + } + } + + private List _dependencies; + private BufferModifiedRangeList _modifiedRanges = null; + /// /// Creates a new instance of the buffer. /// /// GPU context that the buffer belongs to /// Range of memory where the data is mapped - /// Backing memory for the buffers + public MultiRangeBuffer(GpuContext context, MultiRange range) + { + _context = context; + Range = range; + Handle = context.Renderer.CreateBuffer((int)range.GetSize()); + } + + /// + /// Creates a new instance of the buffer. + /// + /// GPU context that the buffer belongs to + /// Range of memory where the data is mapped + /// Backing memory for the buffer public MultiRangeBuffer(GpuContext context, MultiRange range, ReadOnlySpan storages) { _context = context; @@ -49,11 +111,134 @@ namespace Ryujinx.Graphics.Gpu.Memory return new BufferRange(Handle, offset, (int)range.GetSize()); } + /// + /// Removes all physical buffer dependencies. + /// + public void ClearPhysicalDependencies() + { + _dependencies?.Clear(); + } + + /// + /// Adds a physical buffer dependency. + /// + /// Physical buffer to be added + /// Address inside the physical buffer where the virtual buffer range is located + /// Offset inside the virtual buffer where the physical range is located + /// Size of the range in bytes + public void AddPhysicalDependency(Buffer buffer, ulong rangeAddress, ulong dstOffset, ulong rangeSize) + { + (_dependencies ??= new()).Add(new(buffer, rangeAddress - buffer.Address, dstOffset, rangeSize)); + buffer.AddVirtualDependency(this); + } + + /// + /// Tries to get the physical range corresponding to the given physical buffer. + /// + /// Physical buffer + /// Minimum virtual offset that a range match can have + /// Physical offset of the match + /// Virtual offset of the match, always greater than or equal + /// Size of the range match + /// True if a match was found for the given parameters, false otherwise + public bool TryGetPhysicalOffset(Buffer buffer, ulong minimumVirtOffset, out ulong physicalOffset, out ulong virtualOffset, out ulong size) + { + physicalOffset = 0; + virtualOffset = 0; + size = 0; + + if (_dependencies != null) + { + foreach (var dependency in _dependencies) + { + if (dependency.PhysicalBuffer == buffer && dependency.VirtualOffset >= minimumVirtOffset) + { + physicalOffset = dependency.PhysicalOffset; + virtualOffset = dependency.VirtualOffset; + size = dependency.Size; + + return true; + } + } + } + + return false; + } + + /// + /// Adds a modified virtual memory range. + /// + /// + /// This is only required when the host does not support sparse buffers, otherwise only physical buffers need to track modification. + /// + /// Modified range + /// ModificationSequenceNumber + public void AddModifiedRegion(MultiRange range, int modifiedSequenceNumber) + { + _modifiedRanges ??= new(_context, null, null); + + for (int i = 0; i < range.Count; i++) + { + MemoryRange subRange = range.GetSubRange(i); + + _modifiedRanges.SignalModified(subRange.Address, subRange.Size); + } + + ModificationSequenceNumber = modifiedSequenceNumber; + } + + /// + /// Calls the specified for all modified ranges that overlaps with . + /// + /// Buffer to have its range checked + /// Action to perform for modified ranges + public void ConsumeModifiedRegion(Buffer buffer, Action rangeAction) + { + ConsumeModifiedRegion(buffer.Address, buffer.Size, rangeAction); + } + + /// + /// Calls the specified for all modified ranges that overlaps with and . + /// + /// Address of the region to consume + /// Size of the region to consume + /// Action to perform for modified ranges + public void ConsumeModifiedRegion(ulong address, ulong size, Action rangeAction) + { + if (_modifiedRanges != null) + { + _modifiedRanges.GetRanges(address, size, rangeAction); + _modifiedRanges.Clear(address, size); + } + } + + /// + /// Gets data from the specified region of the buffer, and places it on . + /// + /// Span to put the data into + /// Offset of the buffer to get the data from + /// Size of the data in bytes + public void GetData(Span output, int offset, int size) + { + using PinnedSpan data = _context.Renderer.GetBufferData(Handle, offset, size); + data.Get().CopyTo(output); + } + /// /// Disposes the host buffer. /// public void Dispose() { + if (_dependencies != null) + { + foreach (var dependency in _dependencies) + { + dependency.PhysicalBuffer.RemoveVirtualDependency(this); + } + + _dependencies = null; + } + _context.Renderer.DeleteBuffer(Handle); } } diff --git a/src/Ryujinx.Graphics.Gpu/Memory/PhysicalMemory.cs b/src/Ryujinx.Graphics.Gpu/Memory/PhysicalMemory.cs index 1ca6071bd..b22cc01b8 100644 --- a/src/Ryujinx.Graphics.Gpu/Memory/PhysicalMemory.cs +++ b/src/Ryujinx.Graphics.Gpu/Memory/PhysicalMemory.cs @@ -1,10 +1,13 @@ +using Ryujinx.Common.Memory; using Ryujinx.Cpu; +using Ryujinx.Graphics.Device; using Ryujinx.Graphics.Gpu.Image; using Ryujinx.Graphics.Gpu.Shader; using Ryujinx.Memory; using Ryujinx.Memory.Range; using Ryujinx.Memory.Tracking; using System; +using System.Buffers; using System.Collections.Generic; using System.Linq; using System.Runtime.InteropServices; @@ -22,11 +25,6 @@ namespace Ryujinx.Graphics.Gpu.Memory private readonly IVirtualMemoryManagerTracked _cpuMemory; private int _referenceCount; - /// - /// Indicates whenever the memory manager supports 4KB pages. - /// - public bool Supports4KBPages => _cpuMemory.Supports4KBPages; - /// /// In-memory shader cache. /// @@ -82,6 +80,15 @@ namespace Ryujinx.Graphics.Gpu.Memory } } + /// + /// Creates a new device memory manager. + /// + /// The memory manager + public DeviceMemoryManager CreateDeviceMemoryManager() + { + return new DeviceMemoryManager(_cpuMemory); + } + /// /// Gets a host pointer for a given range of application memory. /// If the memory region is not a single contiguous block, this method returns 0. @@ -185,7 +192,9 @@ namespace Ryujinx.Graphics.Gpu.Memory } else { - Memory memory = new byte[range.GetSize()]; + MemoryOwner memoryOwner = MemoryOwner.Rent(checked((int)range.GetSize())); + + Span memorySpan = memoryOwner.Span; int offset = 0; for (int i = 0; i < range.Count; i++) @@ -194,12 +203,12 @@ namespace Ryujinx.Graphics.Gpu.Memory int size = (int)currentRange.Size; if (currentRange.Address != MemoryManager.PteUnmapped) { - GetSpan(currentRange.Address, size).CopyTo(memory.Span.Slice(offset, size)); + GetSpan(currentRange.Address, size).CopyTo(memorySpan.Slice(offset, size)); } offset += size; } - return new WritableRegion(new MultiRangeWritableBlock(range, this), 0, memory, tracked); + return new WritableRegion(new MultiRangeWritableBlock(range, this), 0, memoryOwner, tracked); } } @@ -358,10 +367,11 @@ namespace Ryujinx.Graphics.Gpu.Memory /// CPU virtual address of the region /// Size of the region /// Kind of the resource being tracked + /// Region flags /// The memory tracking handle - public RegionHandle BeginTracking(ulong address, ulong size, ResourceKind kind) + public RegionHandle BeginTracking(ulong address, ulong size, ResourceKind kind, RegionFlags flags = RegionFlags.None) { - return _cpuMemory.BeginTracking(address, size, (int)kind); + return _cpuMemory.BeginTracking(address, size, (int)kind, flags); } /// @@ -398,12 +408,19 @@ namespace Ryujinx.Graphics.Gpu.Memory /// CPU virtual address of the region /// Size of the region /// Kind of the resource being tracked + /// Region flags /// Handles to inherit state from or reuse /// Desired granularity of write tracking /// The memory tracking handle - public MultiRegionHandle BeginGranularTracking(ulong address, ulong size, ResourceKind kind, IEnumerable handles = null, ulong granularity = 4096) + public MultiRegionHandle BeginGranularTracking( + ulong address, + ulong size, + ResourceKind kind, + RegionFlags flags = RegionFlags.None, + IEnumerable handles = null, + ulong granularity = 4096) { - return _cpuMemory.BeginGranularTracking(address, size, handles, granularity, (int)kind); + return _cpuMemory.BeginGranularTracking(address, size, handles, granularity, (int)kind, flags); } /// diff --git a/src/Ryujinx.Graphics.Gpu/Memory/VirtualBufferCache.cs b/src/Ryujinx.Graphics.Gpu/Memory/VirtualRangeCache.cs similarity index 92% rename from src/Ryujinx.Graphics.Gpu/Memory/VirtualBufferCache.cs rename to src/Ryujinx.Graphics.Gpu/Memory/VirtualRangeCache.cs index 858c5e3b0..964507a21 100644 --- a/src/Ryujinx.Graphics.Gpu/Memory/VirtualBufferCache.cs +++ b/src/Ryujinx.Graphics.Gpu/Memory/VirtualRangeCache.cs @@ -6,9 +6,9 @@ using System.Threading; namespace Ryujinx.Graphics.Gpu.Memory { /// - /// Virtual buffer cache. + /// Virtual range cache. /// - class VirtualBufferCache + class VirtualRangeCache { private readonly MemoryManager _memoryManager; @@ -68,10 +68,10 @@ namespace Ryujinx.Graphics.Gpu.Memory private int _hasDeferredUnmaps; /// - /// Creates a new instance of the virtual buffer cache. + /// Creates a new instance of the virtual range cache. /// - /// Memory manager that the virtual buffer cache belongs to - public VirtualBufferCache(MemoryManager memoryManager) + /// Memory manager that the virtual range cache belongs to + public VirtualRangeCache(MemoryManager memoryManager) { _memoryManager = memoryManager; _virtualRanges = new RangeList(); @@ -102,10 +102,9 @@ namespace Ryujinx.Graphics.Gpu.Memory /// /// GPU virtual address to get the physical range from /// Size in bytes of the region - /// Indicates host support for sparse buffer mapping of non-contiguous ranges /// Physical range for the specified GPU virtual region /// True if the range already existed, false if a new one was created and added - public bool TryGetOrAddRange(ulong gpuVa, ulong size, bool supportsSparse, out MultiRange range) + public bool TryGetOrAddRange(ulong gpuVa, ulong size, out MultiRange range) { VirtualRange[] overlaps = _virtualRangeOverlaps; int overlapsCount; @@ -158,7 +157,7 @@ namespace Ryujinx.Graphics.Gpu.Memory } else { - found = true; + found = overlap0.Range.Count == 1 || IsSparseAligned(overlap0.Range); range = overlap0.Range.Slice(gpuVa - overlap0.Address, size); } } @@ -174,12 +173,11 @@ namespace Ryujinx.Graphics.Gpu.Memory ShrinkOverlapsBufferIfNeeded(); - // If the the range is not properly aligned for sparse mapping, - // or if the host does not support sparse mapping, let's just - // force it to a single range. + // If the range is not properly aligned for sparse mapping, + // let's just force it to a single range. // This might cause issues in some applications that uses sparse // mappings. - if (!IsSparseAligned(range) || !supportsSparse) + if (!IsSparseAligned(range)) { range = new MultiRange(range.GetSubRange(0).Address, size); } diff --git a/src/Ryujinx.Graphics.Gpu/Shader/CachedShaderBindings.cs b/src/Ryujinx.Graphics.Gpu/Shader/CachedShaderBindings.cs index 4e1cb4e12..018c5fdc0 100644 --- a/src/Ryujinx.Graphics.Gpu/Shader/CachedShaderBindings.cs +++ b/src/Ryujinx.Graphics.Gpu/Shader/CachedShaderBindings.cs @@ -17,6 +17,8 @@ namespace Ryujinx.Graphics.Gpu.Shader public BufferDescriptor[][] ConstantBufferBindings { get; } public BufferDescriptor[][] StorageBufferBindings { get; } + public int[] TextureCounts { get; } + public int MaxTextureBinding { get; } public int MaxImageBinding { get; } @@ -34,6 +36,8 @@ namespace Ryujinx.Graphics.Gpu.Shader ConstantBufferBindings = new BufferDescriptor[stageCount][]; StorageBufferBindings = new BufferDescriptor[stageCount][]; + TextureCounts = new int[stageCount]; + int maxTextureBinding = -1; int maxImageBinding = -1; int offset = isCompute ? 0 : 1; @@ -54,18 +58,26 @@ namespace Ryujinx.Graphics.Gpu.Shader TextureBindings[i] = stage.Info.Textures.Select(descriptor => { - Target target = ShaderTexture.GetTarget(descriptor.Type); + Target target = descriptor.Type != SamplerType.None ? ShaderTexture.GetTarget(descriptor.Type) : default; var result = new TextureBindingInfo( target, + descriptor.Set, descriptor.Binding, + descriptor.ArrayLength, descriptor.CbufSlot, descriptor.HandleIndex, - descriptor.Flags); + descriptor.Flags, + descriptor.Type == SamplerType.None); - if (descriptor.Binding > maxTextureBinding) + if (descriptor.ArrayLength <= 1) { - maxTextureBinding = descriptor.Binding; + if (descriptor.Binding > maxTextureBinding) + { + maxTextureBinding = descriptor.Binding; + } + + TextureCounts[i]++; } return result; @@ -74,17 +86,19 @@ namespace Ryujinx.Graphics.Gpu.Shader ImageBindings[i] = stage.Info.Images.Select(descriptor => { Target target = ShaderTexture.GetTarget(descriptor.Type); - Format format = ShaderTexture.GetFormat(descriptor.Format); + FormatInfo formatInfo = ShaderTexture.GetFormatInfo(descriptor.Format); var result = new TextureBindingInfo( target, - format, + formatInfo, + descriptor.Set, descriptor.Binding, + descriptor.ArrayLength, descriptor.CbufSlot, descriptor.HandleIndex, descriptor.Flags); - if (descriptor.Binding > maxImageBinding) + if (descriptor.ArrayLength <= 1 && descriptor.Binding > maxImageBinding) { maxImageBinding = descriptor.Binding; } diff --git a/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/BinarySerializer.cs b/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/BinarySerializer.cs index b08c44d67..3837092c9 100644 --- a/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/BinarySerializer.cs +++ b/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/BinarySerializer.cs @@ -125,9 +125,18 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache CompressionAlgorithm algorithm = CompressionAlgorithm.None; Read(ref algorithm); - if (algorithm == CompressionAlgorithm.Deflate) + switch (algorithm) { - _activeStream = new DeflateStream(_stream, CompressionMode.Decompress, true); + case CompressionAlgorithm.None: + break; + case CompressionAlgorithm.Deflate: + _activeStream = new DeflateStream(_stream, CompressionMode.Decompress, true); + break; + case CompressionAlgorithm.Brotli: + _activeStream = new BrotliStream(_stream, CompressionMode.Decompress, true); + break; + default: + throw new ArgumentException($"Invalid compression algorithm \"{algorithm}\""); } } @@ -139,9 +148,18 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache { Write(ref algorithm); - if (algorithm == CompressionAlgorithm.Deflate) + switch (algorithm) { - _activeStream = new DeflateStream(_stream, CompressionLevel.SmallestSize, true); + case CompressionAlgorithm.None: + break; + case CompressionAlgorithm.Deflate: + _activeStream = new DeflateStream(_stream, CompressionLevel.Fastest, true); + break; + case CompressionAlgorithm.Brotli: + _activeStream = new BrotliStream(_stream, CompressionLevel.Fastest, true); + break; + default: + throw new ArgumentException($"Invalid compression algorithm \"{algorithm}\""); } } @@ -177,7 +195,7 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache switch (algorithm) { case CompressionAlgorithm.None: - stream.Read(data); + stream.ReadExactly(data); break; case CompressionAlgorithm.Deflate: stream = new DeflateStream(stream, CompressionMode.Decompress, true); @@ -187,6 +205,14 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache } stream.Dispose(); break; + case CompressionAlgorithm.Brotli: + stream = new BrotliStream(stream, CompressionMode.Decompress, true); + for (int offset = 0; offset < data.Length;) + { + offset += stream.Read(data[offset..]); + } + stream.Dispose(); + break; } } @@ -206,7 +232,12 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache stream.Write(data); break; case CompressionAlgorithm.Deflate: - stream = new DeflateStream(stream, CompressionLevel.SmallestSize, true); + stream = new DeflateStream(stream, CompressionLevel.Fastest, true); + stream.Write(data); + stream.Dispose(); + break; + case CompressionAlgorithm.Brotli: + stream = new BrotliStream(stream, CompressionLevel.Fastest, true); stream.Write(data); stream.Dispose(); break; diff --git a/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/CompressionAlgorithm.cs b/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/CompressionAlgorithm.cs index 96ddbb513..86d3de07d 100644 --- a/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/CompressionAlgorithm.cs +++ b/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/CompressionAlgorithm.cs @@ -14,5 +14,10 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache /// Deflate compression (RFC 1951). /// Deflate, + + /// + /// Brotli compression (RFC 7932). + /// + Brotli, } } diff --git a/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheCommon.cs b/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheCommon.cs index c4ce0b870..cecfe9acf 100644 --- a/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheCommon.cs +++ b/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheCommon.cs @@ -51,7 +51,7 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache /// Compression algorithm public static CompressionAlgorithm GetCompressionAlgorithm() { - return CompressionAlgorithm.Deflate; + return CompressionAlgorithm.Brotli; } } } diff --git a/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheGpuAccessor.cs b/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheGpuAccessor.cs index de6432bc1..3c7664b77 100644 --- a/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheGpuAccessor.cs +++ b/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheGpuAccessor.cs @@ -18,6 +18,8 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache private readonly ShaderSpecializationState _newSpecState; private readonly int _stageIndex; private readonly bool _isVulkan; + private readonly bool _hasGeometryShader; + private readonly bool _supportsQuads; /// /// Creates a new instance of the cached GPU state accessor for shader translation. @@ -27,7 +29,9 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache /// The constant buffer 1 data of the shader /// Shader specialization state of the cached shader /// Shader specialization state of the recompiled shader + /// Resource counts shared across all shader stages /// Shader stage index + /// Indicates if a geometry shader is present public DiskCacheGpuAccessor( GpuContext context, ReadOnlyMemory data, @@ -35,7 +39,8 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache ShaderSpecializationState oldSpecState, ShaderSpecializationState newSpecState, ResourceCounts counts, - int stageIndex) : base(context, counts, stageIndex) + int stageIndex, + bool hasGeometryShader) : base(context, counts, stageIndex) { _data = data; _cb1Data = cb1Data; @@ -43,6 +48,8 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache _newSpecState = newSpecState; _stageIndex = stageIndex; _isVulkan = context.Capabilities.Api == TargetApi.Vulkan; + _hasGeometryShader = hasGeometryShader; + _supportsQuads = context.Capabilities.SupportsQuads; if (stageIndex == (int)ShaderStage.Geometry - 1) { @@ -99,7 +106,11 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache /// public GpuGraphicsState QueryGraphicsState() { - return _oldSpecState.GraphicsState.CreateShaderGraphicsState(!_isVulkan, _isVulkan || _oldSpecState.GraphicsState.YNegateEnabled); + return _oldSpecState.GraphicsState.CreateShaderGraphicsState( + !_isVulkan, + _supportsQuads, + _hasGeometryShader, + _isVulkan || _oldSpecState.GraphicsState.YNegateEnabled); } /// @@ -109,11 +120,10 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache } /// - public TextureFormat QueryTextureFormat(int handle, int cbufSlot) + /// Pool length is not available on the cache + public int QuerySamplerArrayLengthFromPool() { - _newSpecState.RecordTextureFormat(_stageIndex, handle, cbufSlot); - (uint format, bool formatSrgb) = _oldSpecState.GetFormat(_stageIndex, handle, cbufSlot); - return ConvertToTextureFormat(format, formatSrgb); + return QueryArrayLengthFromPool(isSampler: true); } /// @@ -123,6 +133,36 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache return _oldSpecState.GetTextureTarget(_stageIndex, handle, cbufSlot).ConvertSamplerType(); } + /// + /// Constant buffer derived length is not available on the cache + public int QueryTextureArrayLengthFromBuffer(int slot) + { + if (!_oldSpecState.TextureArrayFromBufferRegistered(_stageIndex, 0, slot)) + { + throw new DiskCacheLoadException(DiskCacheLoadResult.MissingTextureArrayLength); + } + + int arrayLength = _oldSpecState.GetTextureArrayFromBufferLength(_stageIndex, 0, slot); + _newSpecState.RegisterTextureArrayLengthFromBuffer(_stageIndex, 0, slot, arrayLength); + + return arrayLength; + } + + /// + /// Pool length is not available on the cache + public int QueryTextureArrayLengthFromPool() + { + return QueryArrayLengthFromPool(isSampler: false); + } + + /// + public TextureFormat QueryTextureFormat(int handle, int cbufSlot) + { + _newSpecState.RecordTextureFormat(_stageIndex, handle, cbufSlot); + (uint format, bool formatSrgb) = _oldSpecState.GetFormat(_stageIndex, handle, cbufSlot); + return ConvertToTextureFormat(format, formatSrgb); + } + /// public bool QueryTextureCoordNormalized(int handle, int cbufSlot) { @@ -155,6 +195,7 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache } /// + /// Texture information is not available on the cache public void RegisterTexture(int handle, int cbufSlot) { if (!_oldSpecState.TextureRegistered(_stageIndex, handle, cbufSlot)) @@ -167,5 +208,24 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache bool coordNormalized = _oldSpecState.GetCoordNormalized(_stageIndex, handle, cbufSlot); _newSpecState.RegisterTexture(_stageIndex, handle, cbufSlot, format, formatSrgb, target, coordNormalized); } + + /// + /// Gets the cached texture or sampler pool capacity. + /// + /// True to get sampler pool length, false for texture pool length + /// Pool length + /// Pool length is not available on the cache + private int QueryArrayLengthFromPool(bool isSampler) + { + if (!_oldSpecState.TextureArrayFromPoolRegistered(isSampler)) + { + throw new DiskCacheLoadException(DiskCacheLoadResult.MissingTextureArrayLength); + } + + int arrayLength = _oldSpecState.GetTextureArrayFromPoolLength(isSampler); + _newSpecState.RegisterTextureArrayLengthFromPool(isSampler, arrayLength); + + return arrayLength; + } } } diff --git a/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheGuestStorage.cs b/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheGuestStorage.cs index 59d2cfb3f..22af88d31 100644 --- a/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheGuestStorage.cs +++ b/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheGuestStorage.cs @@ -220,7 +220,7 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache } dataFileStream.Seek((long)entry.Offset, SeekOrigin.Begin); - dataFileStream.Read(cb1Data); + dataFileStream.ReadExactly(cb1Data); BinarySerializer.ReadCompressed(dataFileStream, guestCode); _cache[index] = (guestCode, cb1Data); @@ -279,7 +279,7 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache dataFileStream.Seek((long)entry.Offset, SeekOrigin.Begin); byte[] cachedCode = new byte[entry.CodeSize]; byte[] cachedCb1Data = new byte[entry.Cb1DataSize]; - dataFileStream.Read(cachedCb1Data); + dataFileStream.ReadExactly(cachedCb1Data); BinarySerializer.ReadCompressed(dataFileStream, cachedCode); if (data.SequenceEqual(cachedCode) && cb1Data.SequenceEqual(cachedCb1Data)) @@ -453,7 +453,7 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache /// Hash of the data private static uint CalcHash(ReadOnlySpan data) { - return (uint)XXHash128.ComputeHash(data).Low; + return (uint)Hash128.ComputeHash(data).Low; } } } diff --git a/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheHostStorage.cs b/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheHostStorage.cs index 125ab8993..c36fc0ada 100644 --- a/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheHostStorage.cs +++ b/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheHostStorage.cs @@ -22,7 +22,7 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache private const ushort FileFormatVersionMajor = 1; private const ushort FileFormatVersionMinor = 2; private const uint FileFormatVersionPacked = ((uint)FileFormatVersionMajor << 16) | FileFormatVersionMinor; - private const uint CodeGenVersion = 5958; + private const uint CodeGenVersion = 7353; private const string SharedTocFileName = "shared.toc"; private const string SharedDataFileName = "shared.data"; diff --git a/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheLoadResult.cs b/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheLoadResult.cs index ba23f70ee..d5abb9e55 100644 --- a/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheLoadResult.cs +++ b/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheLoadResult.cs @@ -20,6 +20,11 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache /// InvalidCb1DataLength, + /// + /// The cache is missing the length of a texture array used by the shader. + /// + MissingTextureArrayLength, + /// /// The cache is missing the descriptor of a texture used by the shader. /// @@ -60,6 +65,7 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache DiskCacheLoadResult.Success => "No error.", DiskCacheLoadResult.NoAccess => "Could not access the cache file.", DiskCacheLoadResult.InvalidCb1DataLength => "Constant buffer 1 data length is too low.", + DiskCacheLoadResult.MissingTextureArrayLength => "Texture array length missing from the cache file.", DiskCacheLoadResult.MissingTextureDescriptor => "Texture descriptor missing from the cache file.", DiskCacheLoadResult.FileCorruptedGeneric => "The cache file is corrupted.", DiskCacheLoadResult.FileCorruptedInvalidMagic => "Magic check failed, the cache file is corrupted.", diff --git a/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/ParallelDiskCacheLoader.cs b/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/ParallelDiskCacheLoader.cs index 153fc4427..20f96462e 100644 --- a/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/ParallelDiskCacheLoader.cs +++ b/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/ParallelDiskCacheLoader.cs @@ -601,6 +601,8 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache TargetApi api = _context.Capabilities.Api; + bool hasCachedGs = guestShaders[4].HasValue; + for (int stageIndex = Constants.ShaderStages - 1; stageIndex >= 0; stageIndex--) { if (guestShaders[stageIndex + 1].HasValue) @@ -610,7 +612,7 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache byte[] guestCode = shader.Code; byte[] cb1Data = shader.Cb1Data; - DiskCacheGpuAccessor gpuAccessor = new(_context, guestCode, cb1Data, specState, newSpecState, counts, stageIndex); + DiskCacheGpuAccessor gpuAccessor = new(_context, guestCode, cb1Data, specState, newSpecState, counts, stageIndex, hasCachedGs); TranslatorContext currentStage = DecodeGraphicsShader(gpuAccessor, api, DefaultFlags, 0); if (nextStage != null) @@ -623,7 +625,7 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache byte[] guestCodeA = guestShaders[0].Value.Code; byte[] cb1DataA = guestShaders[0].Value.Cb1Data; - DiskCacheGpuAccessor gpuAccessorA = new(_context, guestCodeA, cb1DataA, specState, newSpecState, counts, 0); + DiskCacheGpuAccessor gpuAccessorA = new(_context, guestCodeA, cb1DataA, specState, newSpecState, counts, 0, hasCachedGs); translatorContexts[0] = DecodeGraphicsShader(gpuAccessorA, api, DefaultFlags | TranslationFlags.VertexA, 0); } @@ -711,7 +713,7 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache GuestCodeAndCbData shader = guestShaders[0].Value; ResourceCounts counts = new(); ShaderSpecializationState newSpecState = new(ref specState.ComputeState); - DiskCacheGpuAccessor gpuAccessor = new(_context, shader.Code, shader.Cb1Data, specState, newSpecState, counts, 0); + DiskCacheGpuAccessor gpuAccessor = new(_context, shader.Code, shader.Cb1Data, specState, newSpecState, counts, 0, false); gpuAccessor.InitializeReservedCounts(tfEnabled: false, vertexAsCompute: false); TranslatorContext translatorContext = DecodeComputeShader(gpuAccessor, _context.Capabilities.Api, 0); diff --git a/src/Ryujinx.Graphics.Gpu/Shader/GpuAccessor.cs b/src/Ryujinx.Graphics.Gpu/Shader/GpuAccessor.cs index 95763f31d..1be75f242 100644 --- a/src/Ryujinx.Graphics.Gpu/Shader/GpuAccessor.cs +++ b/src/Ryujinx.Graphics.Gpu/Shader/GpuAccessor.cs @@ -17,6 +17,8 @@ namespace Ryujinx.Graphics.Gpu.Shader private readonly int _stageIndex; private readonly bool _compute; private readonly bool _isVulkan; + private readonly bool _hasGeometryShader; + private readonly bool _supportsQuads; /// /// Creates a new instance of the GPU state accessor for graphics shader translation. @@ -25,12 +27,20 @@ namespace Ryujinx.Graphics.Gpu.Shader /// GPU channel /// Current GPU state /// Graphics shader stage index (0 = Vertex, 4 = Fragment) - public GpuAccessor(GpuContext context, GpuChannel channel, GpuAccessorState state, int stageIndex) : base(context, state.ResourceCounts, stageIndex) + /// Indicates if a geometry shader is present + public GpuAccessor( + GpuContext context, + GpuChannel channel, + GpuAccessorState state, + int stageIndex, + bool hasGeometryShader) : base(context, state.ResourceCounts, stageIndex) { - _isVulkan = context.Capabilities.Api == TargetApi.Vulkan; _channel = channel; _state = state; _stageIndex = stageIndex; + _isVulkan = context.Capabilities.Api == TargetApi.Vulkan; + _hasGeometryShader = hasGeometryShader; + _supportsQuads = context.Capabilities.SupportsQuads; if (stageIndex == (int)ShaderStage.Geometry - 1) { @@ -72,6 +82,7 @@ namespace Ryujinx.Graphics.Gpu.Shader public ReadOnlySpan GetCode(ulong address, int minimumSize) { int size = Math.Max(minimumSize, 0x1000 - (int)(address & 0xfff)); + return MemoryMarshal.Cast(_channel.MemoryManager.GetSpan(address, size)); } @@ -104,7 +115,11 @@ namespace Ryujinx.Graphics.Gpu.Shader /// public GpuGraphicsState QueryGraphicsState() { - return _state.GraphicsState.CreateShaderGraphicsState(!_isVulkan, _isVulkan || _state.GraphicsState.YNegateEnabled); + return _state.GraphicsState.CreateShaderGraphicsState( + !_isVulkan, + _supportsQuads, + _hasGeometryShader, + _isVulkan || _state.GraphicsState.YNegateEnabled); } /// @@ -119,12 +134,13 @@ namespace Ryujinx.Graphics.Gpu.Shader return _state.GraphicsState.HasUnalignedStorageBuffer || _state.ComputeState.HasUnalignedStorageBuffer; } - //// - public TextureFormat QueryTextureFormat(int handle, int cbufSlot) + /// + public int QuerySamplerArrayLengthFromPool() { - _state.SpecializationState?.RecordTextureFormat(_stageIndex, handle, cbufSlot); - var descriptor = GetTextureDescriptor(handle, cbufSlot); - return ConvertToTextureFormat(descriptor.UnpackFormat(), descriptor.UnpackSrgb()); + int length = _state.SamplerPoolMaximumId + 1; + _state.SpecializationState?.RegisterTextureArrayLengthFromPool(isSampler: true, length); + + return length; } /// @@ -134,6 +150,37 @@ namespace Ryujinx.Graphics.Gpu.Shader return GetTextureDescriptor(handle, cbufSlot).UnpackTextureTarget().ConvertSamplerType(); } + /// + public int QueryTextureArrayLengthFromBuffer(int slot) + { + int size = _compute + ? _channel.BufferManager.GetComputeUniformBufferSize(slot) + : _channel.BufferManager.GetGraphicsUniformBufferSize(_stageIndex, slot); + + int arrayLength = size / Constants.TextureHandleSizeInBytes; + + _state.SpecializationState?.RegisterTextureArrayLengthFromBuffer(_stageIndex, 0, slot, arrayLength); + + return arrayLength; + } + + /// + public int QueryTextureArrayLengthFromPool() + { + int length = _state.PoolState.TexturePoolMaximumId + 1; + _state.SpecializationState?.RegisterTextureArrayLengthFromPool(isSampler: false, length); + + return length; + } + + //// + public TextureFormat QueryTextureFormat(int handle, int cbufSlot) + { + _state.SpecializationState?.RecordTextureFormat(_stageIndex, handle, cbufSlot); + var descriptor = GetTextureDescriptor(handle, cbufSlot); + return ConvertToTextureFormat(descriptor.UnpackFormat(), descriptor.UnpackSrgb()); + } + /// public bool QueryTextureCoordNormalized(int handle, int cbufSlot) { diff --git a/src/Ryujinx.Graphics.Gpu/Shader/GpuAccessorBase.cs b/src/Ryujinx.Graphics.Gpu/Shader/GpuAccessorBase.cs index a5b31363b..d89eebabf 100644 --- a/src/Ryujinx.Graphics.Gpu/Shader/GpuAccessorBase.cs +++ b/src/Ryujinx.Graphics.Gpu/Shader/GpuAccessorBase.cs @@ -20,6 +20,9 @@ namespace Ryujinx.Graphics.Gpu.Shader private int _reservedTextures; private int _reservedImages; + private int _staticTexturesCount; + private int _staticImagesCount; + /// /// Creates a new GPU accessor. /// @@ -48,7 +51,7 @@ namespace Ryujinx.Graphics.Gpu.Shader _reservedImages = rrc.ReservedImages; } - public int QueryBindingConstantBuffer(int index) + public SetBindingPair CreateConstantBufferBinding(int index) { int binding; @@ -61,10 +64,42 @@ namespace Ryujinx.Graphics.Gpu.Shader binding = _resourceCounts.UniformBuffersCount++; } - return binding + _reservedConstantBuffers; + return new SetBindingPair(_context.Capabilities.UniformBufferSetIndex, binding + _reservedConstantBuffers); } - public int QueryBindingStorageBuffer(int index) + public SetBindingPair CreateImageBinding(int count, bool isBuffer) + { + int binding; + + if (_context.Capabilities.Api == TargetApi.Vulkan) + { + if (count == 1) + { + int index = _staticImagesCount++; + + if (isBuffer) + { + index += (int)_context.Capabilities.MaximumImagesPerStage; + } + + binding = GetBindingFromIndex(index, _context.Capabilities.MaximumImagesPerStage * 2, "Image"); + } + else + { + binding = (int)GetDynamicBaseIndexDual(_context.Capabilities.MaximumImagesPerStage) + _resourceCounts.ImagesCount++; + } + } + else + { + binding = _resourceCounts.ImagesCount; + + _resourceCounts.ImagesCount += count; + } + + return new SetBindingPair(_context.Capabilities.ImageSetIndex, binding + _reservedImages); + } + + public SetBindingPair CreateStorageBufferBinding(int index) { int binding; @@ -77,49 +112,39 @@ namespace Ryujinx.Graphics.Gpu.Shader binding = _resourceCounts.StorageBuffersCount++; } - return binding + _reservedStorageBuffers; + return new SetBindingPair(_context.Capabilities.StorageBufferSetIndex, binding + _reservedStorageBuffers); } - public int QueryBindingTexture(int index, bool isBuffer) + public SetBindingPair CreateTextureBinding(int count, bool isBuffer) { int binding; if (_context.Capabilities.Api == TargetApi.Vulkan) { - if (isBuffer) + if (count == 1) { - index += (int)_context.Capabilities.MaximumTexturesPerStage; - } + int index = _staticTexturesCount++; - binding = GetBindingFromIndex(index, _context.Capabilities.MaximumTexturesPerStage * 2, "Texture"); + if (isBuffer) + { + index += (int)_context.Capabilities.MaximumTexturesPerStage; + } + + binding = GetBindingFromIndex(index, _context.Capabilities.MaximumTexturesPerStage * 2, "Texture"); + } + else + { + binding = (int)GetDynamicBaseIndexDual(_context.Capabilities.MaximumTexturesPerStage) + _resourceCounts.TexturesCount++; + } } else { - binding = _resourceCounts.TexturesCount++; + binding = _resourceCounts.TexturesCount; + + _resourceCounts.TexturesCount += count; } - return binding + _reservedTextures; - } - - public int QueryBindingImage(int index, bool isBuffer) - { - int binding; - - if (_context.Capabilities.Api == TargetApi.Vulkan) - { - if (isBuffer) - { - index += (int)_context.Capabilities.MaximumImagesPerStage; - } - - binding = GetBindingFromIndex(index, _context.Capabilities.MaximumImagesPerStage * 2, "Image"); - } - else - { - binding = _resourceCounts.ImagesCount++; - } - - return binding + _reservedImages; + return new SetBindingPair(_context.Capabilities.TextureSetIndex, binding + _reservedTextures); } private int GetBindingFromIndex(int index, uint maxPerStage, string resourceName) @@ -148,6 +173,26 @@ namespace Ryujinx.Graphics.Gpu.Shader }; } + private static uint GetDynamicBaseIndexDual(uint maxPerStage) + { + return GetDynamicBaseIndex(maxPerStage) * 2; + } + + private static uint GetDynamicBaseIndex(uint maxPerStage) + { + return maxPerStage * Constants.ShaderStages; + } + + public int CreateExtraSet() + { + if (_resourceCounts.SetsCount >= _context.Capabilities.MaximumExtraSets) + { + return -1; + } + + return _context.Capabilities.ExtraSetBaseIndex + _resourceCounts.SetsCount++; + } + public int QueryHostGatherBiasPrecision() => _context.Capabilities.GatherBiasPrecision; public bool QueryHostReducedPrecision() => _context.Capabilities.ReduceShaderPrecision; @@ -178,6 +223,8 @@ namespace Ryujinx.Graphics.Gpu.Shader public bool QueryHostSupportsScaledVertexFormats() => _context.Capabilities.SupportsScaledVertexFormats; + public bool QueryHostSupportsSeparateSampler() => _context.Capabilities.SupportsSeparateSampler; + public bool QueryHostSupportsShaderBallot() => _context.Capabilities.SupportsShaderBallot; public bool QueryHostSupportsShaderBarrierDivergence() => _context.Capabilities.SupportsShaderBarrierDivergence; diff --git a/src/Ryujinx.Graphics.Gpu/Shader/GpuAccessorState.cs b/src/Ryujinx.Graphics.Gpu/Shader/GpuAccessorState.cs index cfc4a2ccc..808bf1851 100644 --- a/src/Ryujinx.Graphics.Gpu/Shader/GpuAccessorState.cs +++ b/src/Ryujinx.Graphics.Gpu/Shader/GpuAccessorState.cs @@ -5,6 +5,11 @@ namespace Ryujinx.Graphics.Gpu.Shader /// class GpuAccessorState { + /// + /// Maximum ID that a sampler pool entry may have. + /// + public readonly int SamplerPoolMaximumId; + /// /// GPU texture pool state. /// @@ -38,18 +43,21 @@ namespace Ryujinx.Graphics.Gpu.Shader /// /// Creates a new GPU accessor state. /// + /// Maximum ID that a sampler pool entry may have /// GPU texture pool state /// GPU compute state, for compute shaders /// GPU graphics state, for vertex, tessellation, geometry and fragment shaders /// Shader specialization state (shared by all stages) /// Transform feedback information, if the shader uses transform feedback. Otherwise, should be null public GpuAccessorState( + int samplerPoolMaximumId, GpuChannelPoolState poolState, GpuChannelComputeState computeState, GpuChannelGraphicsState graphicsState, ShaderSpecializationState specializationState, TransformFeedbackDescriptor[] transformFeedbackDescriptors = null) { + SamplerPoolMaximumId = samplerPoolMaximumId; PoolState = poolState; GraphicsState = graphicsState; ComputeState = computeState; diff --git a/src/Ryujinx.Graphics.Gpu/Shader/GpuChannelGraphicsState.cs b/src/Ryujinx.Graphics.Gpu/Shader/GpuChannelGraphicsState.cs index b5bc4df3c..765bef7d4 100644 --- a/src/Ryujinx.Graphics.Gpu/Shader/GpuChannelGraphicsState.cs +++ b/src/Ryujinx.Graphics.Gpu/Shader/GpuChannelGraphicsState.cs @@ -106,8 +106,11 @@ namespace Ryujinx.Graphics.Gpu.Shader /// Creates a new graphics state from this state that can be used for shader generation. /// /// Indicates if the host API supports alpha test operations + /// Indicates if the host API supports quad primitives + /// Indicates if a geometry shader is used + /// If true, indicates that the fragment origin is the upper left corner of the viewport, otherwise it is the lower left corner /// GPU graphics state that can be used for shader translation - public readonly GpuGraphicsState CreateShaderGraphicsState(bool hostSupportsAlphaTest, bool originUpperLeft) + public readonly GpuGraphicsState CreateShaderGraphicsState(bool hostSupportsAlphaTest, bool hostSupportsQuads, bool hasGeometryShader, bool originUpperLeft) { AlphaTestOp alphaTestOp; @@ -130,6 +133,9 @@ namespace Ryujinx.Graphics.Gpu.Shader }; } + bool isQuad = Topology == PrimitiveTopology.Quads || Topology == PrimitiveTopology.QuadStrip; + bool halvePrimitiveId = !hostSupportsQuads && !hasGeometryShader && isQuad; + return new GpuGraphicsState( EarlyZForce, ConvertToInputTopology(Topology, TessellationMode), @@ -149,7 +155,8 @@ namespace Ryujinx.Graphics.Gpu.Shader in FragmentOutputTypes, DualSourceBlendEnable, YNegateEnabled, - originUpperLeft); + originUpperLeft, + halvePrimitiveId); } /// diff --git a/src/Ryujinx.Graphics.Gpu/Shader/GpuChannelPoolState.cs b/src/Ryujinx.Graphics.Gpu/Shader/GpuChannelPoolState.cs index ddb45152e..a2ab99335 100644 --- a/src/Ryujinx.Graphics.Gpu/Shader/GpuChannelPoolState.cs +++ b/src/Ryujinx.Graphics.Gpu/Shader/GpuChannelPoolState.cs @@ -2,7 +2,6 @@ using System; namespace Ryujinx.Graphics.Gpu.Shader { -#pragma warning disable CS0659 // Class overrides Object.Equals(object o) but does not override Object.GetHashCode() /// /// State used by the . /// @@ -52,6 +51,10 @@ namespace Ryujinx.Graphics.Gpu.Shader { return obj is GpuChannelPoolState state && Equals(state); } + + public override int GetHashCode() + { + return HashCode.Combine(TexturePoolGpuVa, TexturePoolMaximumId, TextureBufferIndex); + } } -#pragma warning restore CS0659 } diff --git a/src/Ryujinx.Graphics.Gpu/Shader/ResourceCounts.cs b/src/Ryujinx.Graphics.Gpu/Shader/ResourceCounts.cs index 126e3249c..59ab378cf 100644 --- a/src/Ryujinx.Graphics.Gpu/Shader/ResourceCounts.cs +++ b/src/Ryujinx.Graphics.Gpu/Shader/ResourceCounts.cs @@ -24,5 +24,10 @@ namespace Ryujinx.Graphics.Gpu.Shader /// Total of images used by the shaders. /// public int ImagesCount; + + /// + /// Total of extra sets used by the shaders. + /// + public int SetsCount; } } diff --git a/src/Ryujinx.Graphics.Gpu/Shader/ShaderCache.cs b/src/Ryujinx.Graphics.Gpu/Shader/ShaderCache.cs index af682e422..4fc66c4c0 100644 --- a/src/Ryujinx.Graphics.Gpu/Shader/ShaderCache.cs +++ b/src/Ryujinx.Graphics.Gpu/Shader/ShaderCache.cs @@ -161,7 +161,8 @@ namespace Ryujinx.Graphics.Gpu.Shader _graphicsShaderCache, _computeShaderCache, _diskCacheHostStorage, - ShaderCacheStateUpdate, cancellationToken); + ShaderCacheStateUpdate, + cancellationToken); loader.LoadShaders(); @@ -191,12 +192,14 @@ namespace Ryujinx.Graphics.Gpu.Shader /// This automatically translates, compiles and adds the code to the cache if not present. /// /// GPU channel + /// Maximum ID that an entry in the sampler pool may have /// Texture pool state /// Compute engine state /// GPU virtual address of the binary shader code /// Compiled compute shader code public CachedShaderProgram GetComputeShader( GpuChannel channel, + int samplerPoolMaximumId, GpuChannelPoolState poolState, GpuChannelComputeState computeState, ulong gpuVa) @@ -213,7 +216,7 @@ namespace Ryujinx.Graphics.Gpu.Shader } ShaderSpecializationState specState = new(ref computeState); - GpuAccessorState gpuAccessorState = new(poolState, computeState, default, specState); + GpuAccessorState gpuAccessorState = new(samplerPoolMaximumId, poolState, computeState, default, specState); GpuAccessor gpuAccessor = new(_context, channel, gpuAccessorState); gpuAccessor.InitializeReservedCounts(tfEnabled: false, vertexAsCompute: false); @@ -290,6 +293,7 @@ namespace Ryujinx.Graphics.Gpu.Shader /// GPU state /// Pipeline state /// GPU channel + /// Maximum ID that an entry in the sampler pool may have /// Texture pool state /// 3D engine state /// Addresses of the shaders for each stage @@ -298,6 +302,7 @@ namespace Ryujinx.Graphics.Gpu.Shader ref ThreedClassState state, ref ProgramPipelineState pipeline, GpuChannel channel, + int samplerPoolMaximumId, ref GpuChannelPoolState poolState, ref GpuChannelGraphicsState graphicsState, ShaderAddresses addresses) @@ -318,7 +323,7 @@ namespace Ryujinx.Graphics.Gpu.Shader UpdatePipelineInfo(ref state, ref pipeline, graphicsState, channel); ShaderSpecializationState specState = new(ref graphicsState, ref pipeline, transformFeedbackDescriptors); - GpuAccessorState gpuAccessorState = new(poolState, default, graphicsState, specState, transformFeedbackDescriptors); + GpuAccessorState gpuAccessorState = new(samplerPoolMaximumId, poolState, default, graphicsState, specState, transformFeedbackDescriptors); ReadOnlySpan addressesSpan = addresses.AsSpan(); @@ -334,7 +339,7 @@ namespace Ryujinx.Graphics.Gpu.Shader if (gpuVa != 0) { - GpuAccessor gpuAccessor = new(_context, channel, gpuAccessorState, stageIndex); + GpuAccessor gpuAccessor = new(_context, channel, gpuAccessorState, stageIndex, addresses.Geometry != 0); TranslatorContext currentStage = DecodeGraphicsShader(gpuAccessor, api, DefaultFlags, gpuVa); if (nextStage != null) diff --git a/src/Ryujinx.Graphics.Gpu/Shader/ShaderInfoBuilder.cs b/src/Ryujinx.Graphics.Gpu/Shader/ShaderInfoBuilder.cs index c2258026c..49823562f 100644 --- a/src/Ryujinx.Graphics.Gpu/Shader/ShaderInfoBuilder.cs +++ b/src/Ryujinx.Graphics.Gpu/Shader/ShaderInfoBuilder.cs @@ -1,5 +1,6 @@ using Ryujinx.Graphics.GAL; using Ryujinx.Graphics.Shader; +using System; using System.Collections.Generic; namespace Ryujinx.Graphics.Gpu.Shader @@ -9,13 +10,6 @@ namespace Ryujinx.Graphics.Gpu.Shader /// class ShaderInfoBuilder { - private const int TotalSets = 4; - - private const int UniformSetIndex = 0; - private const int StorageSetIndex = 1; - private const int TextureSetIndex = 2; - private const int ImageSetIndex = 3; - private const ResourceStages SupportBufferStages = ResourceStages.Compute | ResourceStages.Vertex | @@ -36,8 +30,8 @@ namespace Ryujinx.Graphics.Gpu.Shader private readonly int _reservedTextures; private readonly int _reservedImages; - private readonly List[] _resourceDescriptors; - private readonly List[] _resourceUsages; + private List[] _resourceDescriptors; + private List[] _resourceUsages; /// /// Creates a new shader info builder. @@ -51,17 +45,27 @@ namespace Ryujinx.Graphics.Gpu.Shader _fragmentOutputMap = -1; - _resourceDescriptors = new List[TotalSets]; - _resourceUsages = new List[TotalSets]; + int uniformSetIndex = context.Capabilities.UniformBufferSetIndex; + int storageSetIndex = context.Capabilities.StorageBufferSetIndex; + int textureSetIndex = context.Capabilities.TextureSetIndex; + int imageSetIndex = context.Capabilities.ImageSetIndex; - for (int index = 0; index < TotalSets; index++) + int totalSets = Math.Max(uniformSetIndex, storageSetIndex); + totalSets = Math.Max(totalSets, textureSetIndex); + totalSets = Math.Max(totalSets, imageSetIndex); + totalSets++; + + _resourceDescriptors = new List[totalSets]; + _resourceUsages = new List[totalSets]; + + for (int index = 0; index < totalSets; index++) { _resourceDescriptors[index] = new(); _resourceUsages[index] = new(); } - AddDescriptor(SupportBufferStages, ResourceType.UniformBuffer, UniformSetIndex, 0, 1); - AddUsage(SupportBufferStages, ResourceType.UniformBuffer, UniformSetIndex, 0, 1); + AddDescriptor(SupportBufferStages, ResourceType.UniformBuffer, uniformSetIndex, 0, 1); + AddUsage(SupportBufferStages, ResourceType.UniformBuffer, uniformSetIndex, 0, 1); ResourceReservationCounts rrc = new(!context.Capabilities.SupportsTransformFeedback && tfEnabled, vertexAsCompute); @@ -73,16 +77,25 @@ namespace Ryujinx.Graphics.Gpu.Shader // TODO: Handle that better? Maybe we should only set the binding that are really needed on each shader. ResourceStages stages = vertexAsCompute ? ResourceStages.Compute | ResourceStages.Vertex : VtgStages; - PopulateDescriptorAndUsages(stages, ResourceType.UniformBuffer, UniformSetIndex, 1, rrc.ReservedConstantBuffers - 1); - PopulateDescriptorAndUsages(stages, ResourceType.StorageBuffer, StorageSetIndex, 0, rrc.ReservedStorageBuffers); - PopulateDescriptorAndUsages(stages, ResourceType.BufferTexture, TextureSetIndex, 0, rrc.ReservedTextures); - PopulateDescriptorAndUsages(stages, ResourceType.BufferImage, ImageSetIndex, 0, rrc.ReservedImages); + PopulateDescriptorAndUsages(stages, ResourceType.UniformBuffer, uniformSetIndex, 1, rrc.ReservedConstantBuffers - 1); + PopulateDescriptorAndUsages(stages, ResourceType.StorageBuffer, storageSetIndex, 0, rrc.ReservedStorageBuffers, true); + PopulateDescriptorAndUsages(stages, ResourceType.BufferTexture, textureSetIndex, 0, rrc.ReservedTextures); + PopulateDescriptorAndUsages(stages, ResourceType.BufferImage, imageSetIndex, 0, rrc.ReservedImages, true); } - private void PopulateDescriptorAndUsages(ResourceStages stages, ResourceType type, int setIndex, int start, int count) + /// + /// Populates descriptors and usages for vertex as compute and transform feedback emulation reserved resources. + /// + /// Shader stages where the resources are used + /// Resource type + /// Resource set index where the resources are used + /// First binding number + /// Amount of bindings + /// True if the binding is written from the shader, false otherwise + private void PopulateDescriptorAndUsages(ResourceStages stages, ResourceType type, int setIndex, int start, int count, bool write = false) { AddDescriptor(stages, type, setIndex, start, count); - AddUsage(stages, type, setIndex, start, count); + AddUsage(stages, type, setIndex, start, count, write); } /// @@ -127,15 +140,23 @@ namespace Ryujinx.Graphics.Gpu.Shader int textureBinding = _reservedTextures + stageIndex * texturesPerStage * 2; int imageBinding = _reservedImages + stageIndex * imagesPerStage * 2; - AddDescriptor(stages, ResourceType.UniformBuffer, UniformSetIndex, uniformBinding, uniformsPerStage); - AddDescriptor(stages, ResourceType.StorageBuffer, StorageSetIndex, storageBinding, storagesPerStage); - AddDualDescriptor(stages, ResourceType.TextureAndSampler, ResourceType.BufferTexture, TextureSetIndex, textureBinding, texturesPerStage); - AddDualDescriptor(stages, ResourceType.Image, ResourceType.BufferImage, ImageSetIndex, imageBinding, imagesPerStage); + int uniformSetIndex = _context.Capabilities.UniformBufferSetIndex; + int storageSetIndex = _context.Capabilities.StorageBufferSetIndex; + int textureSetIndex = _context.Capabilities.TextureSetIndex; + int imageSetIndex = _context.Capabilities.ImageSetIndex; - AddUsage(info.CBuffers, stages, UniformSetIndex, isStorage: false); - AddUsage(info.SBuffers, stages, StorageSetIndex, isStorage: true); - AddUsage(info.Textures, stages, TextureSetIndex, isImage: false); - AddUsage(info.Images, stages, ImageSetIndex, isImage: true); + AddDescriptor(stages, ResourceType.UniformBuffer, uniformSetIndex, uniformBinding, uniformsPerStage); + AddDescriptor(stages, ResourceType.StorageBuffer, storageSetIndex, storageBinding, storagesPerStage); + AddDualDescriptor(stages, ResourceType.TextureAndSampler, ResourceType.BufferTexture, textureSetIndex, textureBinding, texturesPerStage); + AddDualDescriptor(stages, ResourceType.Image, ResourceType.BufferImage, imageSetIndex, imageBinding, imagesPerStage); + + AddArrayDescriptors(info.Textures, stages, isImage: false); + AddArrayDescriptors(info.Images, stages, isImage: true); + + AddUsage(info.CBuffers, stages, isStorage: false); + AddUsage(info.SBuffers, stages, isStorage: true); + AddUsage(info.Textures, stages, isImage: false); + AddUsage(info.Images, stages, isImage: true); } /// @@ -169,6 +190,25 @@ namespace Ryujinx.Graphics.Gpu.Shader AddDescriptor(stages, type2, setIndex, binding + count, count); } + /// + /// Adds all array descriptors (those with an array length greater than one). + /// + /// Textures to be added + /// Stages where the textures are used + /// True for images, false for textures + private void AddArrayDescriptors(IEnumerable textures, ResourceStages stages, bool isImage) + { + foreach (TextureDescriptor texture in textures) + { + if (texture.ArrayLength > 1) + { + ResourceType type = GetTextureResourceType(texture, isImage); + + GetDescriptors(texture.Set).Add(new ResourceDescriptor(texture.Binding, texture.ArrayLength, type, stages)); + } + } + } + /// /// Adds buffer usage information to the list of usages. /// @@ -177,11 +217,12 @@ namespace Ryujinx.Graphics.Gpu.Shader /// Descriptor set number where the resource will be bound /// Binding number where the resource will be bound /// Number of resources bound at the binding location - private void AddUsage(ResourceStages stages, ResourceType type, int setIndex, int binding, int count) + /// True if the binding is written from the shader, false otherwise + private void AddUsage(ResourceStages stages, ResourceType type, int setIndex, int binding, int count, bool write = false) { for (int index = 0; index < count; index++) { - _resourceUsages[setIndex].Add(new ResourceUsage(binding + index, type, stages)); + _resourceUsages[setIndex].Add(new ResourceUsage(binding + index, 1, type, stages, write)); } } @@ -190,16 +231,17 @@ namespace Ryujinx.Graphics.Gpu.Shader /// /// Buffers to be added /// Stages where the buffers are used - /// Descriptor set index where the buffers will be bound /// True for storage buffers, false for uniform buffers - private void AddUsage(IEnumerable buffers, ResourceStages stages, int setIndex, bool isStorage) + private void AddUsage(IEnumerable buffers, ResourceStages stages, bool isStorage) { foreach (BufferDescriptor buffer in buffers) { - _resourceUsages[setIndex].Add(new ResourceUsage( + GetUsages(buffer.Set).Add(new ResourceUsage( buffer.Binding, + 1, isStorage ? ResourceType.StorageBuffer : ResourceType.UniformBuffer, - stages)); + stages, + buffer.Flags.HasFlag(BufferUsageFlags.Write))); } } @@ -208,22 +250,93 @@ namespace Ryujinx.Graphics.Gpu.Shader /// /// Textures to be added /// Stages where the textures are used - /// Descriptor set index where the textures will be bound /// True for images, false for textures - private void AddUsage(IEnumerable textures, ResourceStages stages, int setIndex, bool isImage) + private void AddUsage(IEnumerable textures, ResourceStages stages, bool isImage) { foreach (TextureDescriptor texture in textures) { - bool isBuffer = (texture.Type & SamplerType.Mask) == SamplerType.TextureBuffer; + ResourceType type = GetTextureResourceType(texture, isImage); - ResourceType type = isBuffer - ? (isImage ? ResourceType.BufferImage : ResourceType.BufferTexture) - : (isImage ? ResourceType.Image : ResourceType.TextureAndSampler); - - _resourceUsages[setIndex].Add(new ResourceUsage( + GetUsages(texture.Set).Add(new ResourceUsage( texture.Binding, + texture.ArrayLength, type, - stages)); + stages, + texture.Flags.HasFlag(TextureUsageFlags.ImageStore))); + } + } + + /// + /// Gets the list of resource descriptors for a given set index. A new list will be created if needed. + /// + /// Resource set index + /// List of resource descriptors + private List GetDescriptors(int setIndex) + { + if (_resourceDescriptors.Length <= setIndex) + { + int oldLength = _resourceDescriptors.Length; + Array.Resize(ref _resourceDescriptors, setIndex + 1); + + for (int index = oldLength; index <= setIndex; index++) + { + _resourceDescriptors[index] = new(); + } + } + + return _resourceDescriptors[setIndex]; + } + + /// + /// Gets the list of resource usages for a given set index. A new list will be created if needed. + /// + /// Resource set index + /// List of resource usages + private List GetUsages(int setIndex) + { + if (_resourceUsages.Length <= setIndex) + { + int oldLength = _resourceUsages.Length; + Array.Resize(ref _resourceUsages, setIndex + 1); + + for (int index = oldLength; index <= setIndex; index++) + { + _resourceUsages[index] = new(); + } + } + + return _resourceUsages[setIndex]; + } + + /// + /// Gets a resource type from a texture descriptor. + /// + /// Texture descriptor + /// Whether the texture is a image texture (writable) or not (sampled) + /// Resource type + private static ResourceType GetTextureResourceType(TextureDescriptor texture, bool isImage) + { + bool isBuffer = (texture.Type & SamplerType.Mask) == SamplerType.TextureBuffer; + + if (isBuffer) + { + return isImage ? ResourceType.BufferImage : ResourceType.BufferTexture; + } + else if (isImage) + { + return ResourceType.Image; + } + else if (texture.Type == SamplerType.None) + { + return ResourceType.Sampler; + } + else if (texture.Separate) + { + return ResourceType.Texture; + } + else + { + return ResourceType.TextureAndSampler; } } @@ -235,10 +348,12 @@ namespace Ryujinx.Graphics.Gpu.Shader /// Shader information public ShaderInfo Build(ProgramPipelineState? pipeline, bool fromCache = false) { - var descriptors = new ResourceDescriptorCollection[TotalSets]; - var usages = new ResourceUsageCollection[TotalSets]; + int totalSets = _resourceDescriptors.Length; - for (int index = 0; index < TotalSets; index++) + var descriptors = new ResourceDescriptorCollection[totalSets]; + var usages = new ResourceUsageCollection[totalSets]; + + for (int index = 0; index < totalSets; index++) { descriptors[index] = new ResourceDescriptorCollection(_resourceDescriptors[index].ToArray().AsReadOnly()); usages[index] = new ResourceUsageCollection(_resourceUsages[index].ToArray().AsReadOnly()); diff --git a/src/Ryujinx.Graphics.Gpu/Shader/ShaderSpecializationState.cs b/src/Ryujinx.Graphics.Gpu/Shader/ShaderSpecializationState.cs index 1477b7382..1230c0580 100644 --- a/src/Ryujinx.Graphics.Gpu/Shader/ShaderSpecializationState.cs +++ b/src/Ryujinx.Graphics.Gpu/Shader/ShaderSpecializationState.cs @@ -30,6 +30,8 @@ namespace Ryujinx.Graphics.Gpu.Shader { PrimitiveTopology = 1 << 1, TransformFeedback = 1 << 3, + TextureArrayFromBuffer = 1 << 4, + TextureArrayFromPool = 1 << 5, } private QueriedStateFlags _queriedState; @@ -153,6 +155,8 @@ namespace Ryujinx.Graphics.Gpu.Shader } private readonly Dictionary> _textureSpecialization; + private readonly Dictionary _textureArrayFromBufferSpecialization; + private readonly Dictionary _textureArrayFromPoolSpecialization; private KeyValuePair>[] _allTextures; private Box[][] _textureByBinding; private Box[][] _imageByBinding; @@ -163,6 +167,8 @@ namespace Ryujinx.Graphics.Gpu.Shader private ShaderSpecializationState() { _textureSpecialization = new Dictionary>(); + _textureArrayFromBufferSpecialization = new Dictionary(); + _textureArrayFromPoolSpecialization = new Dictionary(); } /// @@ -323,6 +329,30 @@ namespace Ryujinx.Graphics.Gpu.Shader state.Value.CoordNormalized = coordNormalized; } + /// + /// Registers the length of a texture array calculated from a constant buffer size. + /// + /// Shader stage where the texture is used + /// Offset in words of the texture handle on the texture buffer + /// Slot of the texture buffer constant buffer + /// Number of elements in the texture array + public void RegisterTextureArrayLengthFromBuffer(int stageIndex, int handle, int cbufSlot, int length) + { + _textureArrayFromBufferSpecialization[new TextureKey(stageIndex, handle, cbufSlot)] = length; + _queriedState |= QueriedStateFlags.TextureArrayFromBuffer; + } + + /// + /// Registers the length of a texture array calculated from a texture or sampler pool capacity. + /// + /// True for sampler pool, false for texture pool + /// Number of elements in the texture array + public void RegisterTextureArrayLengthFromPool(bool isSampler, int length) + { + _textureArrayFromPoolSpecialization[isSampler] = length; + _queriedState |= QueriedStateFlags.TextureArrayFromPool; + } + /// /// Indicates that the format of a given texture was used during the shader translation process. /// @@ -369,7 +399,7 @@ namespace Ryujinx.Graphics.Gpu.Shader } /// - /// Checks if a given texture was registerd on this specialization state. + /// Checks if a given texture was registered on this specialization state. /// /// Shader stage where the texture is used /// Offset in words of the texture handle on the texture buffer @@ -379,12 +409,35 @@ namespace Ryujinx.Graphics.Gpu.Shader return GetTextureSpecState(stageIndex, handle, cbufSlot) != null; } + /// + /// Checks if a given texture array (from constant buffer) was registered on this specialization state. + /// + /// Shader stage where the texture is used + /// Offset in words of the texture handle on the texture buffer + /// Slot of the texture buffer constant buffer + /// True if the length for the given buffer and stage exists, false otherwise + public bool TextureArrayFromBufferRegistered(int stageIndex, int handle, int cbufSlot) + { + return _textureArrayFromBufferSpecialization.ContainsKey(new TextureKey(stageIndex, handle, cbufSlot)); + } + + /// + /// Checks if a given texture array (from a sampler pool or texture pool) was registered on this specialization state. + /// + /// True for sampler pool, false for texture pool + /// True if the length for the given pool, false otherwise + public bool TextureArrayFromPoolRegistered(bool isSampler) + { + return _textureArrayFromPoolSpecialization.ContainsKey(isSampler); + } + /// /// Gets the recorded format of a given texture. /// /// Shader stage where the texture is used /// Offset in words of the texture handle on the texture buffer /// Slot of the texture buffer constant buffer + /// Format and sRGB tuple public (uint, bool) GetFormat(int stageIndex, int handle, int cbufSlot) { TextureSpecializationState state = GetTextureSpecState(stageIndex, handle, cbufSlot).Value; @@ -397,6 +450,7 @@ namespace Ryujinx.Graphics.Gpu.Shader /// Shader stage where the texture is used /// Offset in words of the texture handle on the texture buffer /// Slot of the texture buffer constant buffer + /// Texture target public TextureTarget GetTextureTarget(int stageIndex, int handle, int cbufSlot) { return GetTextureSpecState(stageIndex, handle, cbufSlot).Value.TextureTarget; @@ -408,11 +462,34 @@ namespace Ryujinx.Graphics.Gpu.Shader /// Shader stage where the texture is used /// Offset in words of the texture handle on the texture buffer /// Slot of the texture buffer constant buffer + /// True if coordinates are normalized, false otherwise public bool GetCoordNormalized(int stageIndex, int handle, int cbufSlot) { return GetTextureSpecState(stageIndex, handle, cbufSlot).Value.CoordNormalized; } + /// + /// Gets the recorded length of a given texture array (from constant buffer). + /// + /// Shader stage where the texture is used + /// Offset in words of the texture handle on the texture buffer + /// Slot of the texture buffer constant buffer + /// Texture array length + public int GetTextureArrayFromBufferLength(int stageIndex, int handle, int cbufSlot) + { + return _textureArrayFromBufferSpecialization[new TextureKey(stageIndex, handle, cbufSlot)]; + } + + /// + /// Gets the recorded length of a given texture array (from a sampler or texture pool). + /// + /// True to get the sampler pool length, false to get the texture pool length + /// Texture array length + public int GetTextureArrayFromPoolLength(bool isSampler) + { + return _textureArrayFromPoolSpecialization[isSampler]; + } + /// /// Gets texture specialization state for a given texture, or create a new one if not present. /// @@ -548,6 +625,12 @@ namespace Ryujinx.Graphics.Gpu.Shader return Matches(channel, ref poolState, checkTextures, isCompute: false); } + /// + /// Converts special vertex attribute groups to their generic equivalents, for comparison purposes. + /// + /// GPU channel + /// Vertex attribute type + /// Filtered attribute private static AttributeType FilterAttributeType(GpuChannel channel, AttributeType type) { type &= ~(AttributeType.Packed | AttributeType.PackedRgb10A2Signed); @@ -660,7 +743,7 @@ namespace Ryujinx.Graphics.Gpu.Shader constantBufferUsePerStageMask &= ~(1 << index); } - if (checkTextures) + if (checkTextures && _allTextures.Length > 0) { TexturePool pool = channel.TextureManager.GetTexturePool(poolState.TexturePoolGpuVa, poolState.TexturePoolMaximumId); @@ -838,6 +921,38 @@ namespace Ryujinx.Graphics.Gpu.Shader specState._textureSpecialization[textureKey] = textureState; } + if (specState._queriedState.HasFlag(QueriedStateFlags.TextureArrayFromBuffer)) + { + dataReader.Read(ref count); + + for (int index = 0; index < count; index++) + { + TextureKey textureKey = default; + int length = 0; + + dataReader.ReadWithMagicAndSize(ref textureKey, TexkMagic); + dataReader.Read(ref length); + + specState._textureArrayFromBufferSpecialization[textureKey] = length; + } + } + + if (specState._queriedState.HasFlag(QueriedStateFlags.TextureArrayFromPool)) + { + dataReader.Read(ref count); + + for (int index = 0; index < count; index++) + { + bool textureKey = default; + int length = 0; + + dataReader.ReadWithMagicAndSize(ref textureKey, TexkMagic); + dataReader.Read(ref length); + + specState._textureArrayFromPoolSpecialization[textureKey] = length; + } + } + return specState; } @@ -902,6 +1017,36 @@ namespace Ryujinx.Graphics.Gpu.Shader dataWriter.WriteWithMagicAndSize(ref textureKey, TexkMagic); dataWriter.WriteWithMagicAndSize(ref textureState.Value, TexsMagic); } + + if (_queriedState.HasFlag(QueriedStateFlags.TextureArrayFromBuffer)) + { + count = (ushort)_textureArrayFromBufferSpecialization.Count; + dataWriter.Write(ref count); + + foreach (var kv in _textureArrayFromBufferSpecialization) + { + var textureKey = kv.Key; + var length = kv.Value; + + dataWriter.WriteWithMagicAndSize(ref textureKey, TexkMagic); + dataWriter.Write(ref length); + } + } + + if (_queriedState.HasFlag(QueriedStateFlags.TextureArrayFromPool)) + { + count = (ushort)_textureArrayFromPoolSpecialization.Count; + dataWriter.Write(ref count); + + foreach (var kv in _textureArrayFromPoolSpecialization) + { + var textureKey = kv.Key; + var length = kv.Value; + + dataWriter.WriteWithMagicAndSize(ref textureKey, TexkMagic); + dataWriter.Write(ref length); + } + } } } } diff --git a/src/Ryujinx.Graphics.Gpu/Synchronization/SynchronizationManager.cs b/src/Ryujinx.Graphics.Gpu/Synchronization/SynchronizationManager.cs index c2fa4c248..1042a4db8 100644 --- a/src/Ryujinx.Graphics.Gpu/Synchronization/SynchronizationManager.cs +++ b/src/Ryujinx.Graphics.Gpu/Synchronization/SynchronizationManager.cs @@ -1,4 +1,5 @@ using Ryujinx.Common.Logging; +using Ryujinx.Graphics.Device; using System; using System.Threading; @@ -7,7 +8,7 @@ namespace Ryujinx.Graphics.Gpu.Synchronization /// /// GPU synchronization manager. /// - public class SynchronizationManager + public class SynchronizationManager : ISynchronizationManager { /// /// The maximum number of syncpoints supported by the GM20B. @@ -29,12 +30,7 @@ namespace Ryujinx.Graphics.Gpu.Synchronization } } - /// - /// Increment the value of a syncpoint with a given id. - /// - /// The id of the syncpoint - /// Thrown when id >= MaxHardwareSyncpoints - /// The incremented value of the syncpoint + /// public uint IncrementSyncpoint(uint id) { ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(id, (uint)MaxHardwareSyncpoints); @@ -42,12 +38,7 @@ namespace Ryujinx.Graphics.Gpu.Synchronization return _syncpoints[id].Increment(); } - /// - /// Get the value of a syncpoint with a given id. - /// - /// The id of the syncpoint - /// Thrown when id >= MaxHardwareSyncpoints - /// The value of the syncpoint + /// public uint GetSyncpointValue(uint id) { ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(id, (uint)MaxHardwareSyncpoints); @@ -84,15 +75,7 @@ namespace Ryujinx.Graphics.Gpu.Synchronization _syncpoints[id].UnregisterCallback(waiterInformation); } - /// - /// Wait on a syncpoint with a given id at a target threshold. - /// The callback will be called once the threshold is reached and will automatically be unregistered. - /// - /// The id of the syncpoint - /// The target threshold - /// The timeout - /// Thrown when id >= MaxHardwareSyncpoints - /// True if timed out + /// public bool WaitOnSyncpoint(uint id, uint threshold, TimeSpan timeout) { ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(id, (uint)MaxHardwareSyncpoints); diff --git a/src/Ryujinx.Graphics.Gpu/Window.cs b/src/Ryujinx.Graphics.Gpu/Window.cs index 3b2368537..59cd4c8a6 100644 --- a/src/Ryujinx.Graphics.Gpu/Window.cs +++ b/src/Ryujinx.Graphics.Gpu/Window.cs @@ -131,7 +131,7 @@ namespace Ryujinx.Graphics.Gpu bool isLinear, int gobBlocksInY, Format format, - int bytesPerPixel, + byte bytesPerPixel, ImageCrop crop, Action acquireCallback, Action releaseCallback, diff --git a/src/Ryujinx.Graphics.Host1x/Host1xClass.cs b/src/Ryujinx.Graphics.Host1x/Host1xClass.cs index 4327b93c2..3ffc87ba5 100644 --- a/src/Ryujinx.Graphics.Host1x/Host1xClass.cs +++ b/src/Ryujinx.Graphics.Host1x/Host1xClass.cs @@ -1,5 +1,4 @@ using Ryujinx.Graphics.Device; -using Ryujinx.Graphics.Gpu.Synchronization; using System.Collections.Generic; using System.Threading; @@ -7,10 +6,10 @@ namespace Ryujinx.Graphics.Host1x { public class Host1xClass : IDeviceState { - private readonly SynchronizationManager _syncMgr; + private readonly ISynchronizationManager _syncMgr; private readonly DeviceState _state; - public Host1xClass(SynchronizationManager syncMgr) + public Host1xClass(ISynchronizationManager syncMgr) { _syncMgr = syncMgr; _state = new DeviceState(new Dictionary diff --git a/src/Ryujinx.Graphics.Host1x/Host1xDevice.cs b/src/Ryujinx.Graphics.Host1x/Host1xDevice.cs index 6733b32aa..2db74ce5e 100644 --- a/src/Ryujinx.Graphics.Host1x/Host1xDevice.cs +++ b/src/Ryujinx.Graphics.Host1x/Host1xDevice.cs @@ -1,7 +1,6 @@ using Ryujinx.Common; using Ryujinx.Common.Logging; using Ryujinx.Graphics.Device; -using Ryujinx.Graphics.Gpu.Synchronization; using System; using System.Numerics; @@ -35,7 +34,7 @@ namespace Ryujinx.Graphics.Host1x private int _mask; private bool _incrementing; - public Host1xDevice(SynchronizationManager syncMgr) + public Host1xDevice(ISynchronizationManager syncMgr) { _syncptIncrMgr = new SyncptIncrManager(syncMgr); _commandQueue = new AsyncWorkQueue(Process, "Ryujinx.Host1xProcessor"); diff --git a/src/Ryujinx.Graphics.Host1x/Ryujinx.Graphics.Host1x.csproj b/src/Ryujinx.Graphics.Host1x/Ryujinx.Graphics.Host1x.csproj index 22959fad8..d631d039f 100644 --- a/src/Ryujinx.Graphics.Host1x/Ryujinx.Graphics.Host1x.csproj +++ b/src/Ryujinx.Graphics.Host1x/Ryujinx.Graphics.Host1x.csproj @@ -6,7 +6,6 @@ - diff --git a/src/Ryujinx.Graphics.Host1x/SyncptIncrManager.cs b/src/Ryujinx.Graphics.Host1x/SyncptIncrManager.cs index 164d15ec2..a5ee1198c 100644 --- a/src/Ryujinx.Graphics.Host1x/SyncptIncrManager.cs +++ b/src/Ryujinx.Graphics.Host1x/SyncptIncrManager.cs @@ -1,11 +1,11 @@ -using Ryujinx.Graphics.Gpu.Synchronization; +using Ryujinx.Graphics.Device; using System.Collections.Generic; namespace Ryujinx.Graphics.Host1x { class SyncptIncrManager { - private readonly SynchronizationManager _syncMgr; + private readonly ISynchronizationManager _syncMgr; private readonly struct SyncptIncr { @@ -27,7 +27,7 @@ namespace Ryujinx.Graphics.Host1x private uint _currentId; - public SyncptIncrManager(SynchronizationManager syncMgr) + public SyncptIncrManager(ISynchronizationManager syncMgr) { _syncMgr = syncMgr; } diff --git a/src/Ryujinx.Graphics.Nvdec.FFmpeg/FFmpegContext.cs b/src/Ryujinx.Graphics.Nvdec.FFmpeg/FFmpegContext.cs index 0767cc9d6..5c9e3989b 100644 --- a/src/Ryujinx.Graphics.Nvdec.FFmpeg/FFmpegContext.cs +++ b/src/Ryujinx.Graphics.Nvdec.FFmpeg/FFmpegContext.cs @@ -52,7 +52,7 @@ namespace Ryujinx.Graphics.Nvdec.FFmpeg int avCodecMajorVersion = avCodecRawVersion >> 16; int avCodecMinorVersion = (avCodecRawVersion >> 8) & 0xFF; - // libavcodec 59.24 changed AvCodec to move its private API and also move the codec function to an union. + // libavcodec 59.24 changed AvCodec to move its private API and also move the codec function to a union. if (avCodecMajorVersion > 59 || (avCodecMajorVersion == 59 && avCodecMinorVersion > 24)) { _decodeFrame = Marshal.GetDelegateForFunctionPointer(((FFCodec*)_codec)->CodecCallback); @@ -91,7 +91,7 @@ namespace Ryujinx.Graphics.Nvdec.FFmpeg FFmpegApi.av_log_format_line(ptr, level, format, vl, lineBuffer, lineSize, &printPrefix); - string line = Marshal.PtrToStringAnsi((IntPtr)lineBuffer).Trim(); + string line = Marshal.PtrToStringAnsi((nint)lineBuffer).Trim(); switch (level) { diff --git a/src/Ryujinx.Graphics.Nvdec.FFmpeg/H264/Decoder.cs b/src/Ryujinx.Graphics.Nvdec.FFmpeg/H264/Decoder.cs index 14877dd55..3ec99192d 100644 --- a/src/Ryujinx.Graphics.Nvdec.FFmpeg/H264/Decoder.cs +++ b/src/Ryujinx.Graphics.Nvdec.FFmpeg/H264/Decoder.cs @@ -26,8 +26,7 @@ namespace Ryujinx.Graphics.Nvdec.FFmpeg.H264 { Surface outSurf = (Surface)output; - if (outSurf.RequestedWidth != _oldOutputWidth || - outSurf.RequestedHeight != _oldOutputHeight) + if (outSurf.RequestedWidth != _oldOutputWidth || outSurf.RequestedHeight != _oldOutputHeight) { _context.Dispose(); _context = new FFmpegContext(AVCodecID.AV_CODEC_ID_H264); @@ -38,7 +37,7 @@ namespace Ryujinx.Graphics.Nvdec.FFmpeg.H264 Span bs = Prepend(bitstream, SpsAndPpsReconstruction.Reconstruct(ref pictureInfo, _workBuffer)); - return _context.DecodeFrame(outSurf, bs) == 0; + return _context.DecodeFrame(outSurf, bs) is 0; } private static byte[] Prepend(ReadOnlySpan data, ReadOnlySpan prep) diff --git a/src/Ryujinx.Graphics.Nvdec.FFmpeg/H264/H264BitStreamWriter.cs b/src/Ryujinx.Graphics.Nvdec.FFmpeg/H264/H264BitStreamWriter.cs index 57ab9fb53..5cb7a7234 100644 --- a/src/Ryujinx.Graphics.Nvdec.FFmpeg/H264/H264BitStreamWriter.cs +++ b/src/Ryujinx.Graphics.Nvdec.FFmpeg/H264/H264BitStreamWriter.cs @@ -3,23 +3,13 @@ using System.Numerics; namespace Ryujinx.Graphics.Nvdec.FFmpeg.H264 { - struct H264BitStreamWriter + struct H264BitStreamWriter(byte[] workBuffer) { private const int BufferSize = 8; - private readonly byte[] _workBuffer; - - private int _offset; - private int _buffer; - private int _bufferPos; - - public H264BitStreamWriter(byte[] workBuffer) - { - _workBuffer = workBuffer; - _offset = 0; - _buffer = 0; - _bufferPos = 0; - } + private int _offset = 0; + private int _buffer = 0; + private int _bufferPos = 0; public void WriteBit(bool value) { @@ -59,9 +49,7 @@ namespace Ryujinx.Graphics.Nvdec.FFmpeg.H264 private int GetFreeBufferBits() { if (_bufferPos == BufferSize) - { Flush(); - } return BufferSize - _bufferPos; } @@ -70,7 +58,7 @@ namespace Ryujinx.Graphics.Nvdec.FFmpeg.H264 { if (_bufferPos != 0) { - _workBuffer[_offset++] = (byte)_buffer; + workBuffer[_offset++] = (byte)_buffer; _buffer = 0; _bufferPos = 0; @@ -85,9 +73,7 @@ namespace Ryujinx.Graphics.Nvdec.FFmpeg.H264 } public readonly Span AsSpan() - { - return new Span(_workBuffer)[.._offset]; - } + => new Span(workBuffer)[.._offset]; public void WriteU(uint value, int valueSize) => WriteBits((int)value, valueSize); public void WriteSe(int value) => WriteExpGolombCodedInt(value); diff --git a/src/Ryujinx.Graphics.Nvdec.FFmpeg/Native/AVCodec.cs b/src/Ryujinx.Graphics.Nvdec.FFmpeg/Native/AVCodec.cs index 0267000c8..b5ef710b1 100644 --- a/src/Ryujinx.Graphics.Nvdec.FFmpeg/Native/AVCodec.cs +++ b/src/Ryujinx.Graphics.Nvdec.FFmpeg/Native/AVCodec.cs @@ -12,15 +12,15 @@ namespace Ryujinx.Graphics.Nvdec.FFmpeg.Native public int Capabilities; public byte MaxLowRes; public unsafe AVRational* SupportedFramerates; - public IntPtr PixFmts; - public IntPtr SupportedSamplerates; - public IntPtr SampleFmts; + public nint PixFmts; + public nint SupportedSamplerates; + public nint SampleFmts; // Deprecated public unsafe ulong* ChannelLayouts; - public unsafe IntPtr PrivClass; - public IntPtr Profiles; + public unsafe nint PrivClass; + public nint Profiles; public unsafe byte* WrapperName; - public IntPtr ChLayouts; + public nint ChLayouts; #pragma warning restore CS0649 } } diff --git a/src/Ryujinx.Graphics.Nvdec.FFmpeg/Native/AVCodec501.cs b/src/Ryujinx.Graphics.Nvdec.FFmpeg/Native/AVCodec501.cs index 9084f4024..d745e9f04 100644 --- a/src/Ryujinx.Graphics.Nvdec.FFmpeg/Native/AVCodec501.cs +++ b/src/Ryujinx.Graphics.Nvdec.FFmpeg/Native/AVCodec501.cs @@ -12,13 +12,13 @@ namespace Ryujinx.Graphics.Nvdec.FFmpeg.Native public int Capabilities; public byte MaxLowRes; public unsafe AVRational* SupportedFramerates; - public IntPtr PixFmts; - public IntPtr SupportedSamplerates; - public IntPtr SampleFmts; + public nint PixFmts; + public nint SupportedSamplerates; + public nint SampleFmts; // Deprecated public unsafe ulong* ChannelLayouts; - public unsafe IntPtr PrivClass; - public IntPtr Profiles; + public unsafe nint PrivClass; + public nint Profiles; public unsafe byte* WrapperName; #pragma warning restore CS0649 } diff --git a/src/Ryujinx.Graphics.Nvdec.FFmpeg/Native/AVCodecContext.cs b/src/Ryujinx.Graphics.Nvdec.FFmpeg/Native/AVCodecContext.cs index c743ab33e..1de0a13e4 100644 --- a/src/Ryujinx.Graphics.Nvdec.FFmpeg/Native/AVCodecContext.cs +++ b/src/Ryujinx.Graphics.Nvdec.FFmpeg/Native/AVCodecContext.cs @@ -6,22 +6,22 @@ namespace Ryujinx.Graphics.Nvdec.FFmpeg.Native struct AVCodecContext { #pragma warning disable CS0649 // Field is never assigned to - public unsafe IntPtr AvClass; + public unsafe nint AvClass; public int LogLevelOffset; public int CodecType; public unsafe AVCodec* Codec; public AVCodecID CodecId; public uint CodecTag; - public IntPtr PrivData; - public IntPtr Internal; - public IntPtr Opaque; + public nint PrivData; + public nint Internal; + public nint Opaque; public long BitRate; public int BitRateTolerance; public int GlobalQuality; public int CompressionLevel; public int Flags; public int Flags2; - public IntPtr ExtraData; + public nint ExtraData; public int ExtraDataSize; public AVRational TimeBase; public int TicksPerFrame; @@ -32,8 +32,8 @@ namespace Ryujinx.Graphics.Nvdec.FFmpeg.Native public int CodedHeight; public int GopSize; public int PixFmt; - public IntPtr DrawHorizBand; - public IntPtr GetFormat; + public nint DrawHorizBand; + public nint GetFormat; public int MaxBFrames; public float BQuantFactor; public float BQuantOffset; @@ -46,7 +46,7 @@ namespace Ryujinx.Graphics.Nvdec.FFmpeg.Native public float PMasking; public float DarkMasking; public int SliceCount; - public IntPtr SliceOffset; + public nint SliceOffset; public AVRational SampleAspectRatio; public int MeCmp; public int MeSubCmp; @@ -60,8 +60,8 @@ namespace Ryujinx.Graphics.Nvdec.FFmpeg.Native public int MeRange; public int SliceFlags; public int MbDecision; - public IntPtr IntraMatrix; - public IntPtr InterMatrix; + public nint IntraMatrix; + public nint InterMatrix; public int IntraDcPrecision; public int SkipTop; public int SkipBottom; @@ -89,7 +89,7 @@ namespace Ryujinx.Graphics.Nvdec.FFmpeg.Native public ulong RequestChannelLayout; public int AudioServiceType; public int RequestSampleFmt; - public IntPtr GetBuffer2; + public nint GetBuffer2; public float QCompress; public float QBlur; public int QMin; @@ -97,23 +97,23 @@ namespace Ryujinx.Graphics.Nvdec.FFmpeg.Native public int MaxQdiff; public int RcBufferSize; public int RcOverrideCount; - public IntPtr RcOverride; + public nint RcOverride; public long RcMaxRate; public long RcMinRate; public float RcMax_available_vbv_use; public float RcMin_vbv_overflow_use; public int RcInitialBufferOccupancy; public int Trellis; - public IntPtr StatsOut; - public IntPtr StatsIn; + public nint StatsOut; + public nint StatsIn; public int WorkaroundBugs; public int StrictStdCompliance; public int ErrorConcealment; public int Debug; public int ErrRecognition; public long ReorderedOpaque; - public IntPtr HwAccel; - public IntPtr HwAccelContext; + public nint HwAccel; + public nint HwAccelContext; public Array8 Error; public int DctAlgo; public int IdctAlgo; @@ -124,48 +124,48 @@ namespace Ryujinx.Graphics.Nvdec.FFmpeg.Native public int ThreadType; public int ActiveThreadType; public int ThreadSafeCallbacks; - public IntPtr Execute; - public IntPtr Execute2; + public nint Execute; + public nint Execute2; public int NsseWeight; public int Profile; public int Level; public int SkipLoopFilter; public int SkipIdct; public int SkipFrame; - public IntPtr SubtitleHeader; + public nint SubtitleHeader; public int SubtitleHeaderSize; public int InitialPadding; public AVRational Framerate; public int SwPixFmt; public AVRational PktTimebase; - public IntPtr CodecDescriptor; + public nint CodecDescriptor; public long PtsCorrectionNumFaultyPts; public long PtsCorrectionNumFaultyDts; public long PtsCorrectionLastPts; public long PtsCorrectionLastDts; - public IntPtr SubCharenc; + public nint SubCharenc; public int SubCharencMode; public int SkipAlpha; public int SeekPreroll; public int DebugMv; - public IntPtr ChromaIntraMatrix; - public IntPtr DumpSeparator; - public IntPtr CodecWhitelist; + public nint ChromaIntraMatrix; + public nint DumpSeparator; + public nint CodecWhitelist; public uint Properties; - public IntPtr CodedSideData; + public nint CodedSideData; public int NbCodedSideData; - public IntPtr HwFramesCtx; + public nint HwFramesCtx; public int SubTextFormat; public int TrailingPadding; public long MaxPixels; - public IntPtr HwDeviceCtx; + public nint HwDeviceCtx; public int HwAccelFlags; public int applyCropping; public int ExtraHwFrames; public int DiscardDamagedPercentage; public long MaxSamples; public int ExportSideData; - public IntPtr GetEncodeBuffer; + public nint GetEncodeBuffer; #pragma warning restore CS0649 } } diff --git a/src/Ryujinx.Graphics.Nvdec.FFmpeg/Native/AVFrame.cs b/src/Ryujinx.Graphics.Nvdec.FFmpeg/Native/AVFrame.cs index a1eb7a090..97c30c718 100644 --- a/src/Ryujinx.Graphics.Nvdec.FFmpeg/Native/AVFrame.cs +++ b/src/Ryujinx.Graphics.Nvdec.FFmpeg/Native/AVFrame.cs @@ -6,9 +6,9 @@ namespace Ryujinx.Graphics.Nvdec.FFmpeg.Native struct AVFrame { #pragma warning disable CS0649 // Field is never assigned to - public Array8 Data; + public Array8 Data; public Array8 LineSize; - public IntPtr ExtendedData; + public nint ExtendedData; public int Width; public int Height; public int NumSamples; @@ -22,7 +22,7 @@ namespace Ryujinx.Graphics.Nvdec.FFmpeg.Native public int CodedPictureNumber; public int DisplayPictureNumber; public int Quality; - public IntPtr Opaque; + public nint Opaque; public int RepeatPicture; public int InterlacedFrame; public int TopFieldFirst; diff --git a/src/Ryujinx.Graphics.Nvdec.FFmpeg/Native/FFCodec.cs b/src/Ryujinx.Graphics.Nvdec.FFmpeg/Native/FFCodec.cs index ceb8a3b01..95926298c 100644 --- a/src/Ryujinx.Graphics.Nvdec.FFmpeg/Native/FFCodec.cs +++ b/src/Ryujinx.Graphics.Nvdec.FFmpeg/Native/FFCodec.cs @@ -8,12 +8,12 @@ namespace Ryujinx.Graphics.Nvdec.FFmpeg.Native public T Base; public int CapsInternalOrCbType; public int PrivDataSize; - public IntPtr UpdateThreadContext; - public IntPtr UpdateThreadContextForUser; - public IntPtr Defaults; - public IntPtr InitStaticData; - public IntPtr Init; - public IntPtr CodecCallback; + public nint UpdateThreadContext; + public nint UpdateThreadContextForUser; + public nint Defaults; + public nint InitStaticData; + public nint Init; + public nint CodecCallback; #pragma warning restore CS0649 // NOTE: There is more after, but the layout kind of changed a bit and we don't need more than this. This is safe as we only manipulate this behind a reference. diff --git a/src/Ryujinx.Graphics.Nvdec.FFmpeg/Native/FFCodecLegacy.cs b/src/Ryujinx.Graphics.Nvdec.FFmpeg/Native/FFCodecLegacy.cs index 03eba311c..873d2518a 100644 --- a/src/Ryujinx.Graphics.Nvdec.FFmpeg/Native/FFCodecLegacy.cs +++ b/src/Ryujinx.Graphics.Nvdec.FFmpeg/Native/FFCodecLegacy.cs @@ -8,14 +8,14 @@ namespace Ryujinx.Graphics.Nvdec.FFmpeg.Native public T Base; public uint CapsInternalOrCbType; public int PrivDataSize; - public IntPtr UpdateThreadContext; - public IntPtr UpdateThreadContextForUser; - public IntPtr Defaults; - public IntPtr InitStaticData; - public IntPtr Init; - public IntPtr EncodeSub; - public IntPtr Encode2; - public IntPtr Decode; + public nint UpdateThreadContext; + public nint UpdateThreadContextForUser; + public nint Defaults; + public nint InitStaticData; + public nint Init; + public nint EncodeSub; + public nint Encode2; + public nint Decode; #pragma warning restore CS0649 // NOTE: There is more after, but the layout kind of changed a bit and we don't need more than this. This is safe as we only manipulate this behind a reference. diff --git a/src/Ryujinx.Graphics.Nvdec.FFmpeg/Native/FFmpegApi.cs b/src/Ryujinx.Graphics.Nvdec.FFmpeg/Native/FFmpegApi.cs index c0a49b5f7..7b0c2a8ad 100644 --- a/src/Ryujinx.Graphics.Nvdec.FFmpeg/Native/FFmpegApi.cs +++ b/src/Ryujinx.Graphics.Nvdec.FFmpeg/Native/FFmpegApi.cs @@ -26,7 +26,7 @@ namespace Ryujinx.Graphics.Nvdec.FFmpeg.Native { return $"lib{libraryName}.so.{version}"; } - else if (OperatingSystem.IsMacOS() || OperatingSystem.IsIOS()) // TODO: ffmpeg on ios + else if (OperatingSystem.IsMacOS()) { return $"lib{libraryName}.{version}.dylib"; } @@ -37,9 +37,9 @@ namespace Ryujinx.Graphics.Nvdec.FFmpeg.Native } - private static bool TryLoadWhitelistedLibrary(string libraryName, Assembly assembly, DllImportSearchPath? searchPath, out IntPtr handle) + private static bool TryLoadWhitelistedLibrary(string libraryName, Assembly assembly, DllImportSearchPath? searchPath, out nint handle) { - handle = IntPtr.Zero; + handle = nint.Zero; if (_librariesWhitelist.TryGetValue(libraryName, out var value)) { @@ -71,7 +71,7 @@ namespace Ryujinx.Graphics.Nvdec.FFmpeg.Native return handle; } - return IntPtr.Zero; + return nint.Zero; }); } diff --git a/src/Ryujinx.Graphics.Nvdec.FFmpeg/Surface.cs b/src/Ryujinx.Graphics.Nvdec.FFmpeg/Surface.cs index 65fb7b4ad..c13cfe1aa 100644 --- a/src/Ryujinx.Graphics.Nvdec.FFmpeg/Surface.cs +++ b/src/Ryujinx.Graphics.Nvdec.FFmpeg/Surface.cs @@ -11,9 +11,9 @@ namespace Ryujinx.Graphics.Nvdec.FFmpeg public int RequestedWidth { get; } public int RequestedHeight { get; } - public Plane YPlane => new((IntPtr)Frame->Data[0], Stride * Height); - public Plane UPlane => new((IntPtr)Frame->Data[1], UvStride * UvHeight); - public Plane VPlane => new((IntPtr)Frame->Data[2], UvStride * UvHeight); + public Plane YPlane => new((nint)Frame->Data[0], Stride * Height); + public Plane UPlane => new((nint)Frame->Data[1], UvStride * UvHeight); + public Plane VPlane => new((nint)Frame->Data[2], UvStride * UvHeight); public FrameField Field => Frame->InterlacedFrame != 0 ? FrameField.Interlaced : FrameField.Progressive; diff --git a/src/Ryujinx.Graphics.Nvdec.Vp9/Common/MemoryAllocator.cs b/src/Ryujinx.Graphics.Nvdec.Vp9/Common/MemoryAllocator.cs index c75cfeb0f..18ed172f8 100644 --- a/src/Ryujinx.Graphics.Nvdec.Vp9/Common/MemoryAllocator.cs +++ b/src/Ryujinx.Graphics.Nvdec.Vp9/Common/MemoryAllocator.cs @@ -11,7 +11,7 @@ namespace Ryujinx.Graphics.Nvdec.Vp9.Common private struct PoolItem { - public IntPtr Pointer; + public nint Pointer; public int Length; public bool InUse; } @@ -22,7 +22,7 @@ namespace Ryujinx.Graphics.Nvdec.Vp9.Common { int lengthInBytes = Unsafe.SizeOf() * length; - IntPtr ptr = IntPtr.Zero; + nint ptr = nint.Zero; for (int i = 0; i < PoolEntries; i++) { @@ -36,7 +36,7 @@ namespace Ryujinx.Graphics.Nvdec.Vp9.Common } } - if (ptr == IntPtr.Zero) + if (ptr == nint.Zero) { ptr = Marshal.AllocHGlobal(lengthInBytes); @@ -47,7 +47,7 @@ namespace Ryujinx.Graphics.Nvdec.Vp9.Common if (!item.InUse) { item.InUse = true; - if (item.Pointer != IntPtr.Zero) + if (item.Pointer != nint.Zero) { Marshal.FreeHGlobal(item.Pointer); } @@ -63,7 +63,7 @@ namespace Ryujinx.Graphics.Nvdec.Vp9.Common public unsafe void Free(ArrayPtr arr) where T : unmanaged { - IntPtr ptr = (IntPtr)arr.ToPointer(); + nint ptr = (nint)arr.ToPointer(); for (int i = 0; i < PoolEntries; i++) { @@ -83,10 +83,10 @@ namespace Ryujinx.Graphics.Nvdec.Vp9.Common { ref PoolItem item = ref _pool[i]; - if (item.Pointer != IntPtr.Zero) + if (item.Pointer != nint.Zero) { Marshal.FreeHGlobal(item.Pointer); - item.Pointer = IntPtr.Zero; + item.Pointer = nint.Zero; } } } diff --git a/src/Ryujinx.Graphics.Nvdec.Vp9/Types/Surface.cs b/src/Ryujinx.Graphics.Nvdec.Vp9/Types/Surface.cs index 372b1d2b4..d9bda185b 100644 --- a/src/Ryujinx.Graphics.Nvdec.Vp9/Types/Surface.cs +++ b/src/Ryujinx.Graphics.Nvdec.Vp9/Types/Surface.cs @@ -11,9 +11,9 @@ namespace Ryujinx.Graphics.Nvdec.Vp9.Types public ArrayPtr UBuffer; public ArrayPtr VBuffer; - public readonly unsafe Plane YPlane => new((IntPtr)YBuffer.ToPointer(), YBuffer.Length); - public readonly unsafe Plane UPlane => new((IntPtr)UBuffer.ToPointer(), UBuffer.Length); - public readonly unsafe Plane VPlane => new((IntPtr)VBuffer.ToPointer(), VBuffer.Length); + public readonly unsafe Plane YPlane => new((nint)YBuffer.ToPointer(), YBuffer.Length); + public readonly unsafe Plane UPlane => new((nint)UBuffer.ToPointer(), UBuffer.Length); + public readonly unsafe Plane VPlane => new((nint)VBuffer.ToPointer(), VBuffer.Length); public readonly FrameField Field => FrameField.Progressive; @@ -30,7 +30,7 @@ namespace Ryujinx.Graphics.Nvdec.Vp9.Types public bool HighBd { get; } - private readonly IntPtr _pointer; + private readonly nint _pointer; public Surface(int width, int height) { @@ -53,7 +53,7 @@ namespace Ryujinx.Graphics.Nvdec.Vp9.Types int frameSize = (HighBd ? 2 : 1) * (yplaneSize + 2 * uvplaneSize); - IntPtr pointer = Marshal.AllocHGlobal(frameSize); + nint pointer = Marshal.AllocHGlobal(frameSize); _pointer = pointer; Width = width; Height = height; diff --git a/src/Ryujinx.Graphics.Nvdec/H264Decoder.cs b/src/Ryujinx.Graphics.Nvdec/H264Decoder.cs index ef8ab9086..6058f72d6 100644 --- a/src/Ryujinx.Graphics.Nvdec/H264Decoder.cs +++ b/src/Ryujinx.Graphics.Nvdec/H264Decoder.cs @@ -12,10 +12,10 @@ namespace Ryujinx.Graphics.Nvdec public static void Decode(NvdecDecoderContext context, ResourceManager rm, ref NvdecRegisters state) { - PictureInfo pictureInfo = rm.Gmm.DeviceRead(state.SetDrvPicSetupOffset); + PictureInfo pictureInfo = rm.MemoryManager.DeviceRead(state.SetDrvPicSetupOffset); H264PictureInfo info = pictureInfo.Convert(); - ReadOnlySpan bitstream = rm.Gmm.DeviceGetSpan(state.SetInBufBaseOffset, (int)pictureInfo.BitstreamSize); + ReadOnlySpan bitstream = rm.MemoryManager.DeviceGetSpan(state.SetInBufBaseOffset, (int)pictureInfo.BitstreamSize); int width = (int)pictureInfo.PicWidthInMbs * MbSizeInPixels; int height = (int)pictureInfo.PicHeightInMbs * MbSizeInPixels; @@ -34,7 +34,7 @@ namespace Ryujinx.Graphics.Nvdec if (outputSurface.Field == FrameField.Progressive) { SurfaceWriter.Write( - rm.Gmm, + rm.MemoryManager, outputSurface, lumaOffset + pictureInfo.LumaFrameOffset, chromaOffset + pictureInfo.ChromaFrameOffset); @@ -42,7 +42,7 @@ namespace Ryujinx.Graphics.Nvdec else { SurfaceWriter.WriteInterlaced( - rm.Gmm, + rm.MemoryManager, outputSurface, lumaOffset + pictureInfo.LumaTopFieldOffset, chromaOffset + pictureInfo.ChromaTopFieldOffset, diff --git a/src/Ryujinx.Graphics.Nvdec/Image/SurfaceCache.cs b/src/Ryujinx.Graphics.Nvdec/Image/SurfaceCache.cs index 4a4e1a3f6..7359b3309 100644 --- a/src/Ryujinx.Graphics.Nvdec/Image/SurfaceCache.cs +++ b/src/Ryujinx.Graphics.Nvdec/Image/SurfaceCache.cs @@ -1,4 +1,4 @@ -using Ryujinx.Graphics.Gpu.Memory; +using Ryujinx.Graphics.Device; using Ryujinx.Graphics.Video; using System; using System.Diagnostics; @@ -27,11 +27,11 @@ namespace Ryujinx.Graphics.Nvdec.Image private readonly CacheItem[] _pool = new CacheItem[MaxItems]; - private readonly MemoryManager _gmm; + private readonly DeviceMemoryManager _mm; - public SurfaceCache(MemoryManager gmm) + public SurfaceCache(DeviceMemoryManager mm) { - _gmm = gmm; + _mm = mm; } public ISurface Get(IDecoder decoder, uint lumaOffset, uint chromaOffset, int width, int height) @@ -77,7 +77,7 @@ namespace Ryujinx.Graphics.Nvdec.Image if ((lumaOffset | chromaOffset) != 0) { - SurfaceReader.Read(_gmm, surface, lumaOffset, chromaOffset); + SurfaceReader.Read(_mm, surface, lumaOffset, chromaOffset); } MoveToFront(i); @@ -100,7 +100,7 @@ namespace Ryujinx.Graphics.Nvdec.Image if ((lumaOffset | chromaOffset) != 0) { - SurfaceReader.Read(_gmm, surface, lumaOffset, chromaOffset); + SurfaceReader.Read(_mm, surface, lumaOffset, chromaOffset); } MoveToFront(MaxItems - 1); diff --git a/src/Ryujinx.Graphics.Nvdec/Image/SurfaceReader.cs b/src/Ryujinx.Graphics.Nvdec/Image/SurfaceReader.cs index e87956852..f510c128d 100644 --- a/src/Ryujinx.Graphics.Nvdec/Image/SurfaceReader.cs +++ b/src/Ryujinx.Graphics.Nvdec/Image/SurfaceReader.cs @@ -1,5 +1,5 @@ using Ryujinx.Common; -using Ryujinx.Graphics.Gpu.Memory; +using Ryujinx.Graphics.Device; using Ryujinx.Graphics.Texture; using Ryujinx.Graphics.Video; using System; @@ -11,13 +11,13 @@ namespace Ryujinx.Graphics.Nvdec.Image { static class SurfaceReader { - public static void Read(MemoryManager gmm, ISurface surface, uint lumaOffset, uint chromaOffset) + public static void Read(DeviceMemoryManager mm, ISurface surface, uint lumaOffset, uint chromaOffset) { int width = surface.Width; int height = surface.Height; int stride = surface.Stride; - ReadOnlySpan luma = gmm.DeviceGetSpan(lumaOffset, GetBlockLinearSize(width, height, 1)); + ReadOnlySpan luma = mm.DeviceGetSpan(lumaOffset, GetBlockLinearSize(width, height, 1)); ReadLuma(surface.YPlane.AsSpan(), luma, stride, width, height); @@ -25,7 +25,7 @@ namespace Ryujinx.Graphics.Nvdec.Image int uvHeight = surface.UvHeight; int uvStride = surface.UvStride; - ReadOnlySpan chroma = gmm.DeviceGetSpan(chromaOffset, GetBlockLinearSize(uvWidth, uvHeight, 2)); + ReadOnlySpan chroma = mm.DeviceGetSpan(chromaOffset, GetBlockLinearSize(uvWidth, uvHeight, 2)); ReadChroma(surface.UPlane.AsSpan(), surface.VPlane.AsSpan(), chroma, uvStride, uvWidth, uvHeight); } diff --git a/src/Ryujinx.Graphics.Nvdec/Image/SurfaceWriter.cs b/src/Ryujinx.Graphics.Nvdec/Image/SurfaceWriter.cs index b4f028998..043be1f2b 100644 --- a/src/Ryujinx.Graphics.Nvdec/Image/SurfaceWriter.cs +++ b/src/Ryujinx.Graphics.Nvdec/Image/SurfaceWriter.cs @@ -1,5 +1,5 @@ using Ryujinx.Common; -using Ryujinx.Graphics.Gpu.Memory; +using Ryujinx.Graphics.Device; using Ryujinx.Graphics.Texture; using Ryujinx.Graphics.Video; using System; @@ -12,11 +12,11 @@ namespace Ryujinx.Graphics.Nvdec.Image { static class SurfaceWriter { - public static void Write(MemoryManager gmm, ISurface surface, uint lumaOffset, uint chromaOffset) + public static void Write(DeviceMemoryManager mm, ISurface surface, uint lumaOffset, uint chromaOffset) { int lumaSize = GetBlockLinearSize(surface.Width, surface.Height, 1); - using var luma = gmm.GetWritableRegion(ExtendOffset(lumaOffset), lumaSize); + using var luma = mm.GetWritableRegion(ExtendOffset(lumaOffset), lumaSize); WriteLuma( luma.Memory.Span, @@ -27,7 +27,7 @@ namespace Ryujinx.Graphics.Nvdec.Image int chromaSize = GetBlockLinearSize(surface.UvWidth, surface.UvHeight, 2); - using var chroma = gmm.GetWritableRegion(ExtendOffset(chromaOffset), chromaSize); + using var chroma = mm.GetWritableRegion(ExtendOffset(chromaOffset), chromaSize); WriteChroma( chroma.Memory.Span, @@ -39,7 +39,7 @@ namespace Ryujinx.Graphics.Nvdec.Image } public static void WriteInterlaced( - MemoryManager gmm, + DeviceMemoryManager mm, ISurface surface, uint lumaTopOffset, uint chromaTopOffset, @@ -48,8 +48,8 @@ namespace Ryujinx.Graphics.Nvdec.Image { int lumaSize = GetBlockLinearSize(surface.Width, surface.Height / 2, 1); - using var lumaTop = gmm.GetWritableRegion(ExtendOffset(lumaTopOffset), lumaSize); - using var lumaBottom = gmm.GetWritableRegion(ExtendOffset(lumaBottomOffset), lumaSize); + using var lumaTop = mm.GetWritableRegion(ExtendOffset(lumaTopOffset), lumaSize); + using var lumaBottom = mm.GetWritableRegion(ExtendOffset(lumaBottomOffset), lumaSize); WriteLuma( lumaTop.Memory.Span, @@ -67,8 +67,8 @@ namespace Ryujinx.Graphics.Nvdec.Image int chromaSize = GetBlockLinearSize(surface.UvWidth, surface.UvHeight / 2, 2); - using var chromaTop = gmm.GetWritableRegion(ExtendOffset(chromaTopOffset), chromaSize); - using var chromaBottom = gmm.GetWritableRegion(ExtendOffset(chromaBottomOffset), chromaSize); + using var chromaTop = mm.GetWritableRegion(ExtendOffset(chromaTopOffset), chromaSize); + using var chromaBottom = mm.GetWritableRegion(ExtendOffset(chromaBottomOffset), chromaSize); WriteChroma( chromaTop.Memory.Span, diff --git a/src/Ryujinx.Graphics.Nvdec/MemoryExtensions.cs b/src/Ryujinx.Graphics.Nvdec/MemoryExtensions.cs index 28d42a8b4..1477ed914 100644 --- a/src/Ryujinx.Graphics.Nvdec/MemoryExtensions.cs +++ b/src/Ryujinx.Graphics.Nvdec/MemoryExtensions.cs @@ -1,23 +1,23 @@ -using Ryujinx.Graphics.Gpu.Memory; +using Ryujinx.Graphics.Device; using System; namespace Ryujinx.Graphics.Nvdec { static class MemoryExtensions { - public static T DeviceRead(this MemoryManager gmm, uint offset) where T : unmanaged + public static T DeviceRead(this DeviceMemoryManager gmm, uint offset) where T : unmanaged { - return gmm.Read((ulong)offset << 8); + return gmm.Read(ExtendOffset(offset)); } - public static ReadOnlySpan DeviceGetSpan(this MemoryManager gmm, uint offset, int size) + public static ReadOnlySpan DeviceGetSpan(this DeviceMemoryManager gmm, uint offset, int size) { - return gmm.GetSpan((ulong)offset << 8, size); + return gmm.GetSpan(ExtendOffset(offset), size); } - public static void DeviceWrite(this MemoryManager gmm, uint offset, ReadOnlySpan data) + public static void DeviceWrite(this DeviceMemoryManager gmm, uint offset, ReadOnlySpan data) { - gmm.Write((ulong)offset << 8, data); + gmm.Write(ExtendOffset(offset), data); } public static ulong ExtendOffset(uint offset) diff --git a/src/Ryujinx.Graphics.Nvdec/NvdecDevice.cs b/src/Ryujinx.Graphics.Nvdec/NvdecDevice.cs index 77e295544..29e260d63 100644 --- a/src/Ryujinx.Graphics.Nvdec/NvdecDevice.cs +++ b/src/Ryujinx.Graphics.Nvdec/NvdecDevice.cs @@ -1,6 +1,5 @@ using Ryujinx.Common.Logging; using Ryujinx.Graphics.Device; -using Ryujinx.Graphics.Gpu.Memory; using Ryujinx.Graphics.Nvdec.Image; using System.Collections.Concurrent; using System.Collections.Generic; @@ -17,9 +16,9 @@ namespace Ryujinx.Graphics.Nvdec private readonly ConcurrentDictionary _contexts; private NvdecDecoderContext _currentContext; - public NvdecDevice(MemoryManager gmm) + public NvdecDevice(DeviceMemoryManager mm) { - _rm = new ResourceManager(gmm, new SurfaceCache(gmm)); + _rm = new ResourceManager(mm, new SurfaceCache(mm)); _state = new DeviceState(new Dictionary { { nameof(NvdecRegisters.Execute), new RwCallback(Execute, null) }, diff --git a/src/Ryujinx.Graphics.Nvdec/ResourceManager.cs b/src/Ryujinx.Graphics.Nvdec/ResourceManager.cs index 200d3a1bd..da0ded912 100644 --- a/src/Ryujinx.Graphics.Nvdec/ResourceManager.cs +++ b/src/Ryujinx.Graphics.Nvdec/ResourceManager.cs @@ -1,16 +1,16 @@ -using Ryujinx.Graphics.Gpu.Memory; +using Ryujinx.Graphics.Device; using Ryujinx.Graphics.Nvdec.Image; namespace Ryujinx.Graphics.Nvdec { readonly struct ResourceManager { - public MemoryManager Gmm { get; } + public DeviceMemoryManager MemoryManager { get; } public SurfaceCache Cache { get; } - public ResourceManager(MemoryManager gmm, SurfaceCache cache) + public ResourceManager(DeviceMemoryManager mm, SurfaceCache cache) { - Gmm = gmm; + MemoryManager = mm; Cache = cache; } } diff --git a/src/Ryujinx.Graphics.Nvdec/Ryujinx.Graphics.Nvdec.csproj b/src/Ryujinx.Graphics.Nvdec/Ryujinx.Graphics.Nvdec.csproj index fd49a7c80..6c00e9a7c 100644 --- a/src/Ryujinx.Graphics.Nvdec/Ryujinx.Graphics.Nvdec.csproj +++ b/src/Ryujinx.Graphics.Nvdec/Ryujinx.Graphics.Nvdec.csproj @@ -8,7 +8,6 @@ - diff --git a/src/Ryujinx.Graphics.Nvdec/Vp8Decoder.cs b/src/Ryujinx.Graphics.Nvdec/Vp8Decoder.cs index 0a7d5840e..3d2543c49 100644 --- a/src/Ryujinx.Graphics.Nvdec/Vp8Decoder.cs +++ b/src/Ryujinx.Graphics.Nvdec/Vp8Decoder.cs @@ -10,8 +10,8 @@ namespace Ryujinx.Graphics.Nvdec { public static void Decode(NvdecDecoderContext context, ResourceManager rm, ref NvdecRegisters state) { - PictureInfo pictureInfo = rm.Gmm.DeviceRead(state.SetDrvPicSetupOffset); - ReadOnlySpan bitstream = rm.Gmm.DeviceGetSpan(state.SetInBufBaseOffset, (int)pictureInfo.VLDBufferSize); + PictureInfo pictureInfo = rm.MemoryManager.DeviceRead(state.SetDrvPicSetupOffset); + ReadOnlySpan bitstream = rm.MemoryManager.DeviceGetSpan(state.SetInBufBaseOffset, (int)pictureInfo.VLDBufferSize); Decoder decoder = context.GetVp8Decoder(); @@ -24,7 +24,7 @@ namespace Ryujinx.Graphics.Nvdec if (decoder.Decode(ref info, outputSurface, bitstream)) { - SurfaceWriter.Write(rm.Gmm, outputSurface, lumaOffset, chromaOffset); + SurfaceWriter.Write(rm.MemoryManager, outputSurface, lumaOffset, chromaOffset); } rm.Cache.Put(outputSurface); diff --git a/src/Ryujinx.Graphics.Nvdec/Vp9Decoder.cs b/src/Ryujinx.Graphics.Nvdec/Vp9Decoder.cs index 037950562..5ed508647 100644 --- a/src/Ryujinx.Graphics.Nvdec/Vp9Decoder.cs +++ b/src/Ryujinx.Graphics.Nvdec/Vp9Decoder.cs @@ -1,5 +1,5 @@ using Ryujinx.Common; -using Ryujinx.Graphics.Gpu.Memory; +using Ryujinx.Graphics.Device; using Ryujinx.Graphics.Nvdec.Image; using Ryujinx.Graphics.Nvdec.Types.Vp9; using Ryujinx.Graphics.Nvdec.Vp9; @@ -17,8 +17,8 @@ namespace Ryujinx.Graphics.Nvdec public unsafe static void Decode(ResourceManager rm, ref NvdecRegisters state) { - PictureInfo pictureInfo = rm.Gmm.DeviceRead(state.SetDrvPicSetupOffset); - EntropyProbs entropy = rm.Gmm.DeviceRead(state.Vp9SetProbTabBufOffset); + PictureInfo pictureInfo = rm.MemoryManager.DeviceRead(state.SetDrvPicSetupOffset); + EntropyProbs entropy = rm.MemoryManager.DeviceRead(state.Vp9SetProbTabBufOffset); ISurface Rent(uint lumaOffset, uint chromaOffset, FrameSize size) { @@ -38,19 +38,19 @@ namespace Ryujinx.Graphics.Nvdec entropy.Convert(ref info.Entropy); - ReadOnlySpan bitstream = rm.Gmm.DeviceGetSpan(state.SetInBufBaseOffset, (int)pictureInfo.BitstreamSize); + ReadOnlySpan bitstream = rm.MemoryManager.DeviceGetSpan(state.SetInBufBaseOffset, (int)pictureInfo.BitstreamSize); ReadOnlySpan mvsIn = ReadOnlySpan.Empty; if (info.UsePrevInFindMvRefs) { - mvsIn = GetMvsInput(rm.Gmm, pictureInfo.CurrentFrameSize, state.Vp9SetColMvReadBufOffset); + mvsIn = GetMvsInput(rm.MemoryManager, pictureInfo.CurrentFrameSize, state.Vp9SetColMvReadBufOffset); } int miCols = BitUtils.DivRoundUp(pictureInfo.CurrentFrameSize.Width, 8); int miRows = BitUtils.DivRoundUp(pictureInfo.CurrentFrameSize.Height, 8); - using var mvsRegion = rm.Gmm.GetWritableRegion(ExtendOffset(state.Vp9SetColMvWriteBufOffset), miRows * miCols * 16); + using var mvsRegion = rm.MemoryManager.GetWritableRegion(ExtendOffset(state.Vp9SetColMvWriteBufOffset), miRows * miCols * 16); Span mvsOut = MemoryMarshal.Cast(mvsRegion.Memory.Span); @@ -59,10 +59,10 @@ namespace Ryujinx.Graphics.Nvdec if (_decoder.Decode(ref info, currentSurface, bitstream, mvsIn, mvsOut)) { - SurfaceWriter.Write(rm.Gmm, currentSurface, lumaOffset, chromaOffset); + SurfaceWriter.Write(rm.MemoryManager, currentSurface, lumaOffset, chromaOffset); } - WriteBackwardUpdates(rm.Gmm, state.Vp9SetCtxCounterBufOffset, ref info.BackwardUpdateCounts); + WriteBackwardUpdates(rm.MemoryManager, state.Vp9SetCtxCounterBufOffset, ref info.BackwardUpdateCounts); rm.Cache.Put(lastSurface); rm.Cache.Put(goldenSurface); @@ -70,17 +70,17 @@ namespace Ryujinx.Graphics.Nvdec rm.Cache.Put(currentSurface); } - private static ReadOnlySpan GetMvsInput(MemoryManager gmm, FrameSize size, uint offset) + private static ReadOnlySpan GetMvsInput(DeviceMemoryManager mm, FrameSize size, uint offset) { int miCols = BitUtils.DivRoundUp(size.Width, 8); int miRows = BitUtils.DivRoundUp(size.Height, 8); - return MemoryMarshal.Cast(gmm.DeviceGetSpan(offset, miRows * miCols * 16)); + return MemoryMarshal.Cast(mm.DeviceGetSpan(offset, miRows * miCols * 16)); } - private static void WriteBackwardUpdates(MemoryManager gmm, uint offset, ref Vp9BackwardUpdates counts) + private static void WriteBackwardUpdates(DeviceMemoryManager mm, uint offset, ref Vp9BackwardUpdates counts) { - using var backwardUpdatesRegion = gmm.GetWritableRegion(ExtendOffset(offset), Unsafe.SizeOf()); + using var backwardUpdatesRegion = mm.GetWritableRegion(ExtendOffset(offset), Unsafe.SizeOf()); ref var backwardUpdates = ref MemoryMarshal.Cast(backwardUpdatesRegion.Memory.Span)[0]; diff --git a/src/Ryujinx.Graphics.OpenGL/BackgroundContextWorker.cs b/src/Ryujinx.Graphics.OpenGL/BackgroundContextWorker.cs index ae647e388..f22e0df57 100644 --- a/src/Ryujinx.Graphics.OpenGL/BackgroundContextWorker.cs +++ b/src/Ryujinx.Graphics.OpenGL/BackgroundContextWorker.cs @@ -30,6 +30,8 @@ namespace Ryujinx.Graphics.OpenGL _thread.Start(); } + public bool HasContext() => _backgroundContext.HasContext(); + private void Run() { InBackground = true; diff --git a/src/Ryujinx.Graphics.OpenGL/Buffer.cs b/src/Ryujinx.Graphics.OpenGL/Buffer.cs index 2a5143101..33ca25174 100644 --- a/src/Ryujinx.Graphics.OpenGL/Buffer.cs +++ b/src/Ryujinx.Graphics.OpenGL/Buffer.cs @@ -19,11 +19,11 @@ namespace Ryujinx.Graphics.OpenGL GL.ClearBufferSubData( BufferTarget.CopyWriteBuffer, PixelInternalFormat.Rgba8ui, - (IntPtr)offset, - (IntPtr)size, + (nint)offset, + (nint)size, PixelFormat.RgbaInteger, PixelType.UnsignedByte, - (IntPtr)valueArr); + (nint)valueArr); } } @@ -37,7 +37,7 @@ namespace Ryujinx.Graphics.OpenGL int handle = GL.GenBuffer(); GL.BindBuffer(BufferTarget.CopyWriteBuffer, handle); - GL.BufferData(BufferTarget.CopyWriteBuffer, size, IntPtr.Zero, BufferUsageHint.DynamicDraw); + GL.BufferData(BufferTarget.CopyWriteBuffer, size, nint.Zero, BufferUsageHint.DynamicDraw); return Handle.FromInt32(handle); } @@ -47,7 +47,7 @@ namespace Ryujinx.Graphics.OpenGL int handle = GL.GenBuffer(); GL.BindBuffer(BufferTarget.CopyWriteBuffer, handle); - GL.BufferStorage(BufferTarget.CopyWriteBuffer, size, IntPtr.Zero, + GL.BufferStorage(BufferTarget.CopyWriteBuffer, size, nint.Zero, BufferStorageFlags.MapPersistentBit | BufferStorageFlags.MapCoherentBit | BufferStorageFlags.ClientStorageBit | @@ -64,9 +64,9 @@ namespace Ryujinx.Graphics.OpenGL GL.CopyBufferSubData( BufferTarget.CopyReadBuffer, BufferTarget.CopyWriteBuffer, - (IntPtr)srcOffset, - (IntPtr)dstOffset, - (IntPtr)size); + (nint)srcOffset, + (nint)dstOffset, + (nint)size); } public static unsafe PinnedSpan GetData(OpenGLRenderer renderer, BufferHandle buffer, int offset, int size) @@ -74,9 +74,9 @@ namespace Ryujinx.Graphics.OpenGL // Data in the persistent buffer and host array is guaranteed to be available // until the next time the host thread requests data. - if (renderer.PersistentBuffers.TryGet(buffer, out IntPtr ptr)) + if (renderer.PersistentBuffers.TryGet(buffer, out nint ptr)) { - return new PinnedSpan(IntPtr.Add(ptr, offset).ToPointer(), size); + return new PinnedSpan(nint.Add(ptr, offset).ToPointer(), size); } else if (HwCapabilities.UsePersistentBufferForFlush) { @@ -84,11 +84,11 @@ namespace Ryujinx.Graphics.OpenGL } else { - IntPtr target = renderer.PersistentBuffers.Default.GetHostArray(size); + nint target = renderer.PersistentBuffers.Default.GetHostArray(size); GL.BindBuffer(BufferTarget.CopyReadBuffer, buffer.ToInt32()); - GL.GetBufferSubData(BufferTarget.CopyReadBuffer, (IntPtr)offset, size, target); + GL.GetBufferSubData(BufferTarget.CopyReadBuffer, (nint)offset, size, target); return new PinnedSpan(target.ToPointer(), size); } @@ -97,7 +97,7 @@ namespace Ryujinx.Graphics.OpenGL public static void Resize(BufferHandle handle, int size) { GL.BindBuffer(BufferTarget.CopyWriteBuffer, handle.ToInt32()); - GL.BufferData(BufferTarget.CopyWriteBuffer, size, IntPtr.Zero, BufferUsageHint.StreamCopy); + GL.BufferData(BufferTarget.CopyWriteBuffer, size, nint.Zero, BufferUsageHint.StreamCopy); } public static void SetData(BufferHandle buffer, int offset, ReadOnlySpan data) @@ -108,7 +108,7 @@ namespace Ryujinx.Graphics.OpenGL { fixed (byte* ptr = data) { - GL.BufferSubData(BufferTarget.CopyWriteBuffer, (IntPtr)offset, data.Length, (IntPtr)ptr); + GL.BufferSubData(BufferTarget.CopyWriteBuffer, (nint)offset, data.Length, (nint)ptr); } } } diff --git a/src/Ryujinx.Graphics.OpenGL/Debugger.cs b/src/Ryujinx.Graphics.OpenGL/Debugger.cs index 7606bdbfd..c700b3b7c 100644 --- a/src/Ryujinx.Graphics.OpenGL/Debugger.cs +++ b/src/Ryujinx.Graphics.OpenGL/Debugger.cs @@ -21,7 +21,7 @@ namespace Ryujinx.Graphics.OpenGL if (logLevel == GraphicsDebugLevel.None) { GL.Disable(EnableCap.DebugOutputSynchronous); - GL.DebugMessageCallback(null, IntPtr.Zero); + GL.DebugMessageCallback(null, nint.Zero); return; } @@ -45,7 +45,7 @@ namespace Ryujinx.Graphics.OpenGL _counter = 0; _debugCallback = GLDebugHandler; - GL.DebugMessageCallback(_debugCallback, IntPtr.Zero); + GL.DebugMessageCallback(_debugCallback, nint.Zero); Logger.Warning?.Print(LogClass.Gpu, "OpenGL Debugging is enabled. Performance will be negatively impacted."); } @@ -56,8 +56,8 @@ namespace Ryujinx.Graphics.OpenGL int id, DebugSeverity severity, int length, - IntPtr message, - IntPtr userParam) + nint message, + nint userParam) { string msg = Marshal.PtrToStringUTF8(message).Replace('\n', ' '); diff --git a/src/Ryujinx.Graphics.OpenGL/Effects/AreaScalingFilter.cs b/src/Ryujinx.Graphics.OpenGL/Effects/AreaScalingFilter.cs new file mode 100644 index 000000000..9b19f2f26 --- /dev/null +++ b/src/Ryujinx.Graphics.OpenGL/Effects/AreaScalingFilter.cs @@ -0,0 +1,106 @@ +using OpenTK.Graphics.OpenGL; +using Ryujinx.Common; +using Ryujinx.Graphics.GAL; +using Ryujinx.Graphics.OpenGL.Image; +using System; +using static Ryujinx.Graphics.OpenGL.Effects.ShaderHelper; + +namespace Ryujinx.Graphics.OpenGL.Effects +{ + internal class AreaScalingFilter : IScalingFilter + { + private readonly OpenGLRenderer _renderer; + private int _inputUniform; + private int _outputUniform; + private int _srcX0Uniform; + private int _srcX1Uniform; + private int _srcY0Uniform; + private int _scalingShaderProgram; + private int _srcY1Uniform; + private int _dstX0Uniform; + private int _dstX1Uniform; + private int _dstY0Uniform; + private int _dstY1Uniform; + + public float Level { get; set; } + + public AreaScalingFilter(OpenGLRenderer renderer) + { + Initialize(); + + _renderer = renderer; + } + + public void Dispose() + { + if (_scalingShaderProgram != 0) + { + GL.DeleteProgram(_scalingShaderProgram); + } + } + + private void Initialize() + { + var scalingShader = EmbeddedResources.ReadAllText("Ryujinx.Graphics.OpenGL/Effects/Shaders/area_scaling.glsl"); + + _scalingShaderProgram = CompileProgram(scalingShader, ShaderType.ComputeShader); + + _inputUniform = GL.GetUniformLocation(_scalingShaderProgram, "Source"); + _outputUniform = GL.GetUniformLocation(_scalingShaderProgram, "imgOutput"); + + _srcX0Uniform = GL.GetUniformLocation(_scalingShaderProgram, "srcX0"); + _srcX1Uniform = GL.GetUniformLocation(_scalingShaderProgram, "srcX1"); + _srcY0Uniform = GL.GetUniformLocation(_scalingShaderProgram, "srcY0"); + _srcY1Uniform = GL.GetUniformLocation(_scalingShaderProgram, "srcY1"); + _dstX0Uniform = GL.GetUniformLocation(_scalingShaderProgram, "dstX0"); + _dstX1Uniform = GL.GetUniformLocation(_scalingShaderProgram, "dstX1"); + _dstY0Uniform = GL.GetUniformLocation(_scalingShaderProgram, "dstY0"); + _dstY1Uniform = GL.GetUniformLocation(_scalingShaderProgram, "dstY1"); + } + + public void Run( + TextureView view, + TextureView destinationTexture, + int width, + int height, + Extents2D source, + Extents2D destination) + { + int previousProgram = GL.GetInteger(GetPName.CurrentProgram); + int previousUnit = GL.GetInteger(GetPName.ActiveTexture); + GL.ActiveTexture(TextureUnit.Texture0); + int previousTextureBinding = GL.GetInteger(GetPName.TextureBinding2D); + + GL.BindImageTexture(0, destinationTexture.Handle, 0, false, 0, TextureAccess.ReadWrite, SizedInternalFormat.Rgba8); + + int threadGroupWorkRegionDim = 16; + int dispatchX = (width + (threadGroupWorkRegionDim - 1)) / threadGroupWorkRegionDim; + int dispatchY = (height + (threadGroupWorkRegionDim - 1)) / threadGroupWorkRegionDim; + + // Scaling pass + GL.UseProgram(_scalingShaderProgram); + view.Bind(0); + GL.Uniform1(_inputUniform, 0); + GL.Uniform1(_outputUniform, 0); + GL.Uniform1(_srcX0Uniform, (float)source.X1); + GL.Uniform1(_srcX1Uniform, (float)source.X2); + GL.Uniform1(_srcY0Uniform, (float)source.Y1); + GL.Uniform1(_srcY1Uniform, (float)source.Y2); + GL.Uniform1(_dstX0Uniform, (float)destination.X1); + GL.Uniform1(_dstX1Uniform, (float)destination.X2); + GL.Uniform1(_dstY0Uniform, (float)destination.Y1); + GL.Uniform1(_dstY1Uniform, (float)destination.Y2); + GL.DispatchCompute(dispatchX, dispatchY, 1); + + GL.UseProgram(previousProgram); + GL.MemoryBarrier(MemoryBarrierFlags.ShaderImageAccessBarrierBit); + + (_renderer.Pipeline as Pipeline).RestoreImages1And2(); + + GL.ActiveTexture(TextureUnit.Texture0); + GL.BindTexture(TextureTarget.Texture2D, previousTextureBinding); + + GL.ActiveTexture((TextureUnit)previousUnit); + } + } +} diff --git a/src/Ryujinx.Graphics.OpenGL/Effects/FsrScalingFilter.cs b/src/Ryujinx.Graphics.OpenGL/Effects/FsrScalingFilter.cs index 1a130bebb..0522e28e0 100644 --- a/src/Ryujinx.Graphics.OpenGL/Effects/FsrScalingFilter.cs +++ b/src/Ryujinx.Graphics.OpenGL/Effects/FsrScalingFilter.cs @@ -18,7 +18,7 @@ namespace Ryujinx.Graphics.OpenGL.Effects private int _srcY0Uniform; private int _scalingShaderProgram; private int _sharpeningShaderProgram; - private float _scale = 1; + private float _sharpeningLevel = 1; private int _srcY1Uniform; private int _dstX0Uniform; private int _dstX1Uniform; @@ -30,10 +30,10 @@ namespace Ryujinx.Graphics.OpenGL.Effects public float Level { - get => _scale; + get => _sharpeningLevel; set { - _scale = MathF.Max(0.01f, value); + _sharpeningLevel = MathF.Max(0.01f, value); } } diff --git a/src/Ryujinx.Graphics.OpenGL/Effects/ShaderHelper.cs b/src/Ryujinx.Graphics.OpenGL/Effects/ShaderHelper.cs index c25fe5b25..637b2fba8 100644 --- a/src/Ryujinx.Graphics.OpenGL/Effects/ShaderHelper.cs +++ b/src/Ryujinx.Graphics.OpenGL/Effects/ShaderHelper.cs @@ -1,4 +1,5 @@ using OpenTK.Graphics.OpenGL; +using Ryujinx.Common.Logging; namespace Ryujinx.Graphics.OpenGL.Effects { @@ -6,18 +7,7 @@ namespace Ryujinx.Graphics.OpenGL.Effects { public static int CompileProgram(string shaderCode, ShaderType shaderType) { - var shader = GL.CreateShader(shaderType); - GL.ShaderSource(shader, shaderCode); - GL.CompileShader(shader); - - var program = GL.CreateProgram(); - GL.AttachShader(program, shader); - GL.LinkProgram(program); - - GL.DetachShader(program, shader); - GL.DeleteShader(shader); - - return program; + return CompileProgram(new string[] { shaderCode }, shaderType); } public static int CompileProgram(string[] shaders, ShaderType shaderType) @@ -26,6 +16,15 @@ namespace Ryujinx.Graphics.OpenGL.Effects GL.ShaderSource(shader, shaders.Length, shaders, (int[])null); GL.CompileShader(shader); + GL.GetShader(shader, ShaderParameter.CompileStatus, out int isCompiled); + if (isCompiled == 0) + { + string log = GL.GetShaderInfoLog(shader); + Logger.Error?.Print(LogClass.Gpu, $"Failed to compile effect shader:\n\n{log}\n"); + GL.DeleteShader(shader); + return 0; + } + var program = GL.CreateProgram(); GL.AttachShader(program, shader); GL.LinkProgram(program); diff --git a/src/Ryujinx.Graphics.OpenGL/Effects/Shaders/area_scaling.glsl b/src/Ryujinx.Graphics.OpenGL/Effects/Shaders/area_scaling.glsl new file mode 100644 index 000000000..0fe20d3f9 --- /dev/null +++ b/src/Ryujinx.Graphics.OpenGL/Effects/Shaders/area_scaling.glsl @@ -0,0 +1,119 @@ +#version 430 core +precision mediump float; +layout (local_size_x = 16, local_size_y = 16) in; +layout(rgba8, binding = 0, location=0) uniform image2D imgOutput; +layout( location=1 ) uniform sampler2D Source; +layout( location=2 ) uniform float srcX0; +layout( location=3 ) uniform float srcX1; +layout( location=4 ) uniform float srcY0; +layout( location=5 ) uniform float srcY1; +layout( location=6 ) uniform float dstX0; +layout( location=7 ) uniform float dstX1; +layout( location=8 ) uniform float dstY0; +layout( location=9 ) uniform float dstY1; + +/***** Area Sampling *****/ + +// By Sam Belliveau and Filippo Tarpini. Public Domain license. +// Effectively a more accurate sharp bilinear filter when upscaling, +// that also works as a mathematically perfect downscale filter. +// https://entropymine.com/imageworsener/pixelmixing/ +// https://github.com/obsproject/obs-studio/pull/1715 +// https://legacy.imagemagick.org/Usage/filter/ +vec4 AreaSampling(vec2 xy) +{ + // Determine the sizes of the source and target images. + vec2 source_size = vec2(abs(srcX1 - srcX0), abs(srcY1 - srcY0)); + vec2 target_size = vec2(abs(dstX1 - dstX0), abs(dstY1 - dstY0)); + vec2 inverted_target_size = vec2(1.0) / target_size; + + // Compute the top-left and bottom-right corners of the target pixel box. + vec2 t_beg = floor(xy - vec2(dstX0 < dstX1 ? dstX0 : dstX1, dstY0 < dstY1 ? dstY0 : dstY1)); + vec2 t_end = t_beg + vec2(1.0, 1.0); + + // Convert the target pixel box to source pixel box. + vec2 beg = t_beg * inverted_target_size * source_size; + vec2 end = t_end * inverted_target_size * source_size; + + // Compute the top-left and bottom-right corners of the pixel box. + ivec2 f_beg = ivec2(beg); + ivec2 f_end = ivec2(end); + + // Compute how much of the start and end pixels are covered horizontally & vertically. + float area_w = 1.0 - fract(beg.x); + float area_n = 1.0 - fract(beg.y); + float area_e = fract(end.x); + float area_s = fract(end.y); + + // Compute the areas of the corner pixels in the pixel box. + float area_nw = area_n * area_w; + float area_ne = area_n * area_e; + float area_sw = area_s * area_w; + float area_se = area_s * area_e; + + // Initialize the color accumulator. + vec4 avg_color = vec4(0.0, 0.0, 0.0, 0.0); + + // Accumulate corner pixels. + avg_color += area_nw * texelFetch(Source, ivec2(f_beg.x, f_beg.y), 0); + avg_color += area_ne * texelFetch(Source, ivec2(f_end.x, f_beg.y), 0); + avg_color += area_sw * texelFetch(Source, ivec2(f_beg.x, f_end.y), 0); + avg_color += area_se * texelFetch(Source, ivec2(f_end.x, f_end.y), 0); + + // Determine the size of the pixel box. + int x_range = int(f_end.x - f_beg.x - 0.5); + int y_range = int(f_end.y - f_beg.y - 0.5); + + // Accumulate top and bottom edge pixels. + for (int x = f_beg.x + 1; x <= f_beg.x + x_range; ++x) + { + avg_color += area_n * texelFetch(Source, ivec2(x, f_beg.y), 0); + avg_color += area_s * texelFetch(Source, ivec2(x, f_end.y), 0); + } + + // Accumulate left and right edge pixels and all the pixels in between. + for (int y = f_beg.y + 1; y <= f_beg.y + y_range; ++y) + { + avg_color += area_w * texelFetch(Source, ivec2(f_beg.x, y), 0); + avg_color += area_e * texelFetch(Source, ivec2(f_end.x, y), 0); + + for (int x = f_beg.x + 1; x <= f_beg.x + x_range; ++x) + { + avg_color += texelFetch(Source, ivec2(x, y), 0); + } + } + + // Compute the area of the pixel box that was sampled. + float area_corners = area_nw + area_ne + area_sw + area_se; + float area_edges = float(x_range) * (area_n + area_s) + float(y_range) * (area_w + area_e); + float area_center = float(x_range) * float(y_range); + + // Return the normalized average color. + return avg_color / (area_corners + area_edges + area_center); +} + +float insideBox(vec2 v, vec2 bLeft, vec2 tRight) { + vec2 s = step(bLeft, v) - step(tRight, v); + return s.x * s.y; +} + +vec2 translateDest(vec2 pos) { + vec2 translatedPos = vec2(pos.x, pos.y); + translatedPos.x = dstX1 < dstX0 ? dstX1 - translatedPos.x : translatedPos.x; + translatedPos.y = dstY0 > dstY1 ? dstY0 + dstY1 - translatedPos.y - 1 : translatedPos.y; + return translatedPos; +} + +void main() +{ + vec2 bLeft = vec2(dstX0 < dstX1 ? dstX0 : dstX1, dstY0 < dstY1 ? dstY0 : dstY1); + vec2 tRight = vec2(dstX1 > dstX0 ? dstX1 : dstX0, dstY1 > dstY0 ? dstY1 : dstY0); + ivec2 loc = ivec2(gl_GlobalInvocationID.x, gl_GlobalInvocationID.y); + if (insideBox(loc, bLeft, tRight) == 0) { + imageStore(imgOutput, loc, vec4(0, 0, 0, 1)); + return; + } + + vec4 outColor = AreaSampling(loc); + imageStore(imgOutput, ivec2(translateDest(loc)), vec4(outColor.rgb, 1)); +} diff --git a/src/Ryujinx.Graphics.OpenGL/Effects/Shaders/fsr_scaling.glsl b/src/Ryujinx.Graphics.OpenGL/Effects/Shaders/fsr_scaling.glsl index 8e8755db2..3c7d485b1 100644 --- a/src/Ryujinx.Graphics.OpenGL/Effects/Shaders/fsr_scaling.glsl +++ b/src/Ryujinx.Graphics.OpenGL/Effects/Shaders/fsr_scaling.glsl @@ -85,4 +85,4 @@ void main() { CurrFilter(gxy); gxy.x -= 8u; CurrFilter(gxy); -} \ No newline at end of file +} diff --git a/src/Ryujinx.Graphics.OpenGL/Effects/SmaaPostProcessingEffect.cs b/src/Ryujinx.Graphics.OpenGL/Effects/SmaaPostProcessingEffect.cs index 46dda13f2..a6c5e4aca 100644 --- a/src/Ryujinx.Graphics.OpenGL/Effects/SmaaPostProcessingEffect.cs +++ b/src/Ryujinx.Graphics.OpenGL/Effects/SmaaPostProcessingEffect.cs @@ -33,7 +33,8 @@ namespace Ryujinx.Graphics.OpenGL.Effects.Smaa public int Quality { - get => _quality; set + get => _quality; + set { _quality = Math.Clamp(value, 0, _qualities.Length - 1); } @@ -150,8 +151,8 @@ namespace Ryujinx.Graphics.OpenGL.Effects.Smaa _areaTexture = new TextureStorage(_renderer, areaInfo); _searchTexture = new TextureStorage(_renderer, searchInfo); - var areaTexture = EmbeddedResources.Read("Ryujinx.Graphics.OpenGL/Effects/Textures/SmaaAreaTexture.bin"); - var searchTexture = EmbeddedResources.Read("Ryujinx.Graphics.OpenGL/Effects/Textures/SmaaSearchTexture.bin"); + var areaTexture = EmbeddedResources.ReadFileToRentedMemory("Ryujinx.Graphics.OpenGL/Effects/Textures/SmaaAreaTexture.bin"); + var searchTexture = EmbeddedResources.ReadFileToRentedMemory("Ryujinx.Graphics.OpenGL/Effects/Textures/SmaaSearchTexture.bin"); var areaView = _areaTexture.CreateDefaultView(); var searchView = _searchTexture.CreateDefaultView(); diff --git a/src/Ryujinx.Graphics.OpenGL/FormatTable.cs b/src/Ryujinx.Graphics.OpenGL/FormatTable.cs index 3dac33b94..4cf4dc760 100644 --- a/src/Ryujinx.Graphics.OpenGL/FormatTable.cs +++ b/src/Ryujinx.Graphics.OpenGL/FormatTable.cs @@ -68,6 +68,7 @@ namespace Ryujinx.Graphics.OpenGL Add(Format.S8Uint, new FormatInfo(1, false, false, All.StencilIndex8, PixelFormat.StencilIndex, PixelType.UnsignedByte)); Add(Format.D16Unorm, new FormatInfo(1, false, false, All.DepthComponent16, PixelFormat.DepthComponent, PixelType.UnsignedShort)); Add(Format.S8UintD24Unorm, new FormatInfo(1, false, false, All.Depth24Stencil8, PixelFormat.DepthStencil, PixelType.UnsignedInt248)); + Add(Format.X8UintD24Unorm, new FormatInfo(1, false, false, All.DepthComponent24, PixelFormat.DepthComponent, PixelType.UnsignedInt)); Add(Format.D32Float, new FormatInfo(1, false, false, All.DepthComponent32f, PixelFormat.DepthComponent, PixelType.Float)); Add(Format.D24UnormS8Uint, new FormatInfo(1, false, false, All.Depth24Stencil8, PixelFormat.DepthStencil, PixelType.UnsignedInt248)); Add(Format.D32FloatS8Uint, new FormatInfo(1, false, false, All.Depth32fStencil8, PixelFormat.DepthStencil, PixelType.Float32UnsignedInt248Rev)); @@ -161,6 +162,7 @@ namespace Ryujinx.Graphics.OpenGL Add(Format.A1B5G5R5Unorm, new FormatInfo(4, true, false, All.Rgb5A1, PixelFormat.Rgba, PixelType.UnsignedShort5551)); Add(Format.B8G8R8A8Unorm, new FormatInfo(4, true, false, All.Rgba8, PixelFormat.Rgba, PixelType.UnsignedByte)); Add(Format.B8G8R8A8Srgb, new FormatInfo(4, false, false, All.Srgb8Alpha8, PixelFormat.Rgba, PixelType.UnsignedByte)); + Add(Format.B10G10R10A2Unorm, new FormatInfo(4, false, false, All.Rgb10A2, PixelFormat.Rgba, PixelType.UnsignedInt2101010Reversed)); Add(Format.R8Unorm, SizedInternalFormat.R8); Add(Format.R8Uint, SizedInternalFormat.R8ui); @@ -223,5 +225,17 @@ namespace Ryujinx.Graphics.OpenGL { return _tableImage[(int)format]; } + + public static bool IsPackedDepthStencil(Format format) + { + return format == Format.D24UnormS8Uint || + format == Format.D32FloatS8Uint || + format == Format.S8UintD24Unorm; + } + + public static bool IsDepthOnly(Format format) + { + return format == Format.D16Unorm || format == Format.D32Float || format == Format.X8UintD24Unorm; + } } } diff --git a/src/Ryujinx.Graphics.OpenGL/Framebuffer.cs b/src/Ryujinx.Graphics.OpenGL/Framebuffer.cs index 3b79c5d6b..394b8bc76 100644 --- a/src/Ryujinx.Graphics.OpenGL/Framebuffer.cs +++ b/src/Ryujinx.Graphics.OpenGL/Framebuffer.cs @@ -119,11 +119,11 @@ namespace Ryujinx.Graphics.OpenGL private static FramebufferAttachment GetAttachment(Format format) { - if (IsPackedDepthStencilFormat(format)) + if (FormatTable.IsPackedDepthStencil(format)) { return FramebufferAttachment.DepthStencilAttachment; } - else if (IsDepthOnlyFormat(format)) + else if (FormatTable.IsDepthOnly(format)) { return FramebufferAttachment.DepthAttachment; } @@ -133,18 +133,6 @@ namespace Ryujinx.Graphics.OpenGL } } - private static bool IsPackedDepthStencilFormat(Format format) - { - return format == Format.D24UnormS8Uint || - format == Format.D32FloatS8Uint || - format == Format.S8UintD24Unorm; - } - - private static bool IsDepthOnlyFormat(Format format) - { - return format == Format.D16Unorm || format == Format.D32Float; - } - public int GetColorLayerCount(int index) { return _colors[index]?.Info.GetDepthOrLayers() ?? 0; diff --git a/src/Ryujinx.Graphics.OpenGL/Helper/GLXHelper.cs b/src/Ryujinx.Graphics.OpenGL/Helper/GLXHelper.cs index ce2b20f7d..b722bbf04 100644 --- a/src/Ryujinx.Graphics.OpenGL/Helper/GLXHelper.cs +++ b/src/Ryujinx.Graphics.OpenGL/Helper/GLXHelper.cs @@ -15,14 +15,14 @@ namespace Ryujinx.Graphics.OpenGL.Helper { if (name != LibraryName) { - return IntPtr.Zero; + return nint.Zero; } - if (!NativeLibrary.TryLoad("libGL.so.1", assembly, path, out IntPtr result)) + if (!NativeLibrary.TryLoad("libGL.so.1", assembly, path, out nint result)) { if (!NativeLibrary.TryLoad("libGL.so", assembly, path, out result)) { - return IntPtr.Zero; + return nint.Zero; } } @@ -31,6 +31,6 @@ namespace Ryujinx.Graphics.OpenGL.Helper } [LibraryImport(LibraryName, EntryPoint = "glXGetCurrentContext")] - public static partial IntPtr GetCurrentContext(); + public static partial nint GetCurrentContext(); } } diff --git a/src/Ryujinx.Graphics.OpenGL/Helper/WGLHelper.cs b/src/Ryujinx.Graphics.OpenGL/Helper/WGLHelper.cs index be12ff999..7072bbd9f 100644 --- a/src/Ryujinx.Graphics.OpenGL/Helper/WGLHelper.cs +++ b/src/Ryujinx.Graphics.OpenGL/Helper/WGLHelper.cs @@ -10,6 +10,6 @@ namespace Ryujinx.Graphics.OpenGL.Helper private const string LibraryName = "OPENGL32.DLL"; [LibraryImport(LibraryName, EntryPoint = "wglGetCurrentContext")] - public static partial IntPtr GetCurrentContext(); + public static partial nint GetCurrentContext(); } } diff --git a/src/Ryujinx.Graphics.OpenGL/IOpenGLContext.cs b/src/Ryujinx.Graphics.OpenGL/IOpenGLContext.cs index a11b9cb29..525418d74 100644 --- a/src/Ryujinx.Graphics.OpenGL/IOpenGLContext.cs +++ b/src/Ryujinx.Graphics.OpenGL/IOpenGLContext.cs @@ -7,21 +7,6 @@ namespace Ryujinx.Graphics.OpenGL { void MakeCurrent(); - // TODO: Support more APIs per platform. - static bool HasContext() - { - if (OperatingSystem.IsWindows()) - { - return WGLHelper.GetCurrentContext() != IntPtr.Zero; - } - else if (OperatingSystem.IsLinux()) - { - return GLXHelper.GetCurrentContext() != IntPtr.Zero; - } - else - { - return false; - } - } + bool HasContext(); } } diff --git a/src/Ryujinx.Graphics.OpenGL/Image/FormatConverter.cs b/src/Ryujinx.Graphics.OpenGL/Image/FormatConverter.cs index c4bbf7456..490c0c585 100644 --- a/src/Ryujinx.Graphics.OpenGL/Image/FormatConverter.cs +++ b/src/Ryujinx.Graphics.OpenGL/Image/FormatConverter.cs @@ -1,3 +1,4 @@ +using Ryujinx.Common.Memory; using System; using System.Numerics; using System.Runtime.InteropServices; @@ -8,9 +9,11 @@ namespace Ryujinx.Graphics.OpenGL.Image { static class FormatConverter { - public unsafe static byte[] ConvertS8D24ToD24S8(ReadOnlySpan data) + public unsafe static MemoryOwner ConvertS8D24ToD24S8(ReadOnlySpan data) { - byte[] output = new byte[data.Length]; + MemoryOwner outputMemory = MemoryOwner.Rent(data.Length); + + Span output = outputMemory.Span; int start = 0; @@ -74,7 +77,7 @@ namespace Ryujinx.Graphics.OpenGL.Image outSpan[i] = BitOperations.RotateLeft(dataSpan[i], 8); } - return output; + return outputMemory; } public unsafe static byte[] ConvertD24S8ToS8D24(ReadOnlySpan data) diff --git a/src/Ryujinx.Graphics.OpenGL/Image/ImageArray.cs b/src/Ryujinx.Graphics.OpenGL/Image/ImageArray.cs new file mode 100644 index 000000000..3486f29df --- /dev/null +++ b/src/Ryujinx.Graphics.OpenGL/Image/ImageArray.cs @@ -0,0 +1,63 @@ +using OpenTK.Graphics.OpenGL; +using Ryujinx.Graphics.GAL; + +namespace Ryujinx.Graphics.OpenGL.Image +{ + class ImageArray : IImageArray + { + private record struct TextureRef + { + public int Handle; + public Format Format; + } + + private readonly TextureRef[] _images; + + public ImageArray(int size) + { + _images = new TextureRef[size]; + } + + public void SetImages(int index, ITexture[] images) + { + for (int i = 0; i < images.Length; i++) + { + ITexture image = images[i]; + + if (image is TextureBase imageBase) + { + _images[index + i].Handle = imageBase.Handle; + _images[index + i].Format = imageBase.Format; + } + else + { + _images[index + i].Handle = 0; + } + } + } + + public void Bind(int baseBinding) + { + for (int i = 0; i < _images.Length; i++) + { + if (_images[i].Handle == 0) + { + GL.BindImageTexture(baseBinding + i, 0, 0, true, 0, TextureAccess.ReadWrite, SizedInternalFormat.Rgba8); + } + else + { + SizedInternalFormat format = FormatTable.GetImageFormat(_images[i].Format); + + if (format != 0) + { + GL.BindImageTexture(baseBinding + i, _images[i].Handle, 0, true, 0, TextureAccess.ReadWrite, format); + } + } + } + } + + public void Dispose() + { + } + } +} diff --git a/src/Ryujinx.Graphics.OpenGL/Image/TextureArray.cs b/src/Ryujinx.Graphics.OpenGL/Image/TextureArray.cs new file mode 100644 index 000000000..41ac058c1 --- /dev/null +++ b/src/Ryujinx.Graphics.OpenGL/Image/TextureArray.cs @@ -0,0 +1,56 @@ +using Ryujinx.Graphics.GAL; + +namespace Ryujinx.Graphics.OpenGL.Image +{ + class TextureArray : ITextureArray + { + private record struct TextureRef + { + public TextureBase Texture; + public Sampler Sampler; + } + + private readonly TextureRef[] _textureRefs; + + public TextureArray(int size) + { + _textureRefs = new TextureRef[size]; + } + + public void SetSamplers(int index, ISampler[] samplers) + { + for (int i = 0; i < samplers.Length; i++) + { + _textureRefs[index + i].Sampler = samplers[i] as Sampler; + } + } + + public void SetTextures(int index, ITexture[] textures) + { + for (int i = 0; i < textures.Length; i++) + { + _textureRefs[index + i].Texture = textures[i] as TextureBase; + } + } + + public void Bind(int baseBinding) + { + for (int i = 0; i < _textureRefs.Length; i++) + { + if (_textureRefs[i].Texture != null) + { + _textureRefs[i].Texture.Bind(baseBinding + i); + _textureRefs[i].Sampler?.Bind(baseBinding + i); + } + else + { + TextureBase.ClearBinding(baseBinding + i); + } + } + } + + public void Dispose() + { + } + } +} diff --git a/src/Ryujinx.Graphics.OpenGL/Image/TextureBuffer.cs b/src/Ryujinx.Graphics.OpenGL/Image/TextureBuffer.cs index e8f6d9a26..8e728a2bb 100644 --- a/src/Ryujinx.Graphics.OpenGL/Image/TextureBuffer.cs +++ b/src/Ryujinx.Graphics.OpenGL/Image/TextureBuffer.cs @@ -2,7 +2,6 @@ using OpenTK.Graphics.OpenGL; using Ryujinx.Common.Memory; using Ryujinx.Graphics.GAL; using System; -using System.Buffers; namespace Ryujinx.Graphics.OpenGL.Image { @@ -55,24 +54,25 @@ namespace Ryujinx.Graphics.OpenGL.Image throw new NotImplementedException(); } - public void SetData(IMemoryOwner data) + /// + public void SetData(MemoryOwner data) { - var dataSpan = data.Memory.Span; + var dataSpan = data.Span; Buffer.SetData(_buffer, _bufferOffset, dataSpan[..Math.Min(dataSpan.Length, _bufferSize)]); data.Dispose(); } - public void SetData(IMemoryOwner data, int layer, int level) + /// + public void SetData(MemoryOwner data, int layer, int level) { - data.Dispose(); throw new NotSupportedException(); } - public void SetData(IMemoryOwner data, int layer, int level, Rectangle region) + /// + public void SetData(MemoryOwner data, int layer, int level, Rectangle region) { - data.Dispose(); throw new NotSupportedException(); } @@ -97,7 +97,7 @@ namespace Ryujinx.Graphics.OpenGL.Image SizedInternalFormat format = (SizedInternalFormat)FormatTable.GetFormatInfo(Info.Format).PixelInternalFormat; - GL.TexBufferRange(TextureBufferTarget.TextureBuffer, format, _buffer.ToInt32(), (IntPtr)buffer.Offset, buffer.Size); + GL.TexBufferRange(TextureBufferTarget.TextureBuffer, format, _buffer.ToInt32(), (nint)buffer.Offset, buffer.Size); } public void Dispose() diff --git a/src/Ryujinx.Graphics.OpenGL/Image/TextureCopy.cs b/src/Ryujinx.Graphics.OpenGL/Image/TextureCopy.cs index 128f481f6..e08da7013 100644 --- a/src/Ryujinx.Graphics.OpenGL/Image/TextureCopy.cs +++ b/src/Ryujinx.Graphics.OpenGL/Image/TextureCopy.cs @@ -294,7 +294,7 @@ namespace Ryujinx.Graphics.OpenGL.Image { return FramebufferAttachment.DepthStencilAttachment; } - else if (IsDepthOnly(format)) + else if (FormatTable.IsDepthOnly(format)) { return FramebufferAttachment.DepthAttachment; } @@ -324,11 +324,11 @@ namespace Ryujinx.Graphics.OpenGL.Image private static ClearBufferMask GetMask(Format format) { - if (format == Format.D24UnormS8Uint || format == Format.D32FloatS8Uint || format == Format.S8UintD24Unorm) + if (FormatTable.IsPackedDepthStencil(format)) { return ClearBufferMask.DepthBufferBit | ClearBufferMask.StencilBufferBit; } - else if (IsDepthOnly(format)) + else if (FormatTable.IsDepthOnly(format)) { return ClearBufferMask.DepthBufferBit; } @@ -342,11 +342,6 @@ namespace Ryujinx.Graphics.OpenGL.Image } } - private static bool IsDepthOnly(Format format) - { - return format == Format.D16Unorm || format == Format.D32Float; - } - public TextureView BgraSwap(TextureView from) { TextureView to = (TextureView)_renderer.CreateTexture(from.Info); @@ -469,7 +464,7 @@ namespace Ryujinx.Graphics.OpenGL.Image _copyPboSize = requiredSize; GL.BindBuffer(BufferTarget.PixelPackBuffer, _copyPboHandle); - GL.BufferData(BufferTarget.PixelPackBuffer, requiredSize, IntPtr.Zero, BufferUsageHint.DynamicCopy); + GL.BufferData(BufferTarget.PixelPackBuffer, requiredSize, nint.Zero, BufferUsageHint.DynamicCopy); } } diff --git a/src/Ryujinx.Graphics.OpenGL/Image/TextureStorage.cs b/src/Ryujinx.Graphics.OpenGL/Image/TextureStorage.cs index adaa82572..0ebafb04e 100644 --- a/src/Ryujinx.Graphics.OpenGL/Image/TextureStorage.cs +++ b/src/Ryujinx.Graphics.OpenGL/Image/TextureStorage.cs @@ -15,7 +15,7 @@ namespace Ryujinx.Graphics.OpenGL.Image private int _viewsCount; - internal TextureView DefaultView { get; private set; } + internal ITexture DefaultView { get; private set; } public TextureStorage(OpenGLRenderer renderer, TextureCreateInfo info) { @@ -48,7 +48,7 @@ namespace Ryujinx.Graphics.OpenGL.Image internalFormat = (SizedInternalFormat)format.PixelInternalFormat; } - int levels = Info.GetLevelsClamped(); + int levels = Info.Levels; switch (Info.Target) { @@ -144,14 +144,14 @@ namespace Ryujinx.Graphics.OpenGL.Image } } - public TextureView CreateDefaultView() + public ITexture CreateDefaultView() { DefaultView = CreateView(Info, 0, 0); return DefaultView; } - public TextureView CreateView(TextureCreateInfo info, int firstLayer, int firstLevel) + public ITexture CreateView(TextureCreateInfo info, int firstLayer, int firstLevel) { IncrementViewsCount(); diff --git a/src/Ryujinx.Graphics.OpenGL/Image/TextureView.cs b/src/Ryujinx.Graphics.OpenGL/Image/TextureView.cs index 7451fc1d4..a89dd5131 100644 --- a/src/Ryujinx.Graphics.OpenGL/Image/TextureView.cs +++ b/src/Ryujinx.Graphics.OpenGL/Image/TextureView.cs @@ -3,7 +3,6 @@ using Ryujinx.Common; using Ryujinx.Common.Memory; using Ryujinx.Graphics.GAL; using System; -using System.Buffers; using System.Diagnostics; namespace Ryujinx.Graphics.OpenGL.Image @@ -52,7 +51,7 @@ namespace Ryujinx.Graphics.OpenGL.Image pixelInternalFormat = format.PixelInternalFormat; } - int levels = Info.GetLevelsClamped(); + int levels = Info.Levels; GL.TextureView( Handle, @@ -268,7 +267,7 @@ namespace Ryujinx.Graphics.OpenGL.Image public unsafe PinnedSpan GetData() { int size = 0; - int levels = Info.GetLevelsClamped(); + int levels = Info.Levels; for (int level = 0; level < levels; level++) { @@ -283,7 +282,7 @@ namespace Ryujinx.Graphics.OpenGL.Image } else { - IntPtr target = _renderer.PersistentBuffers.Default.GetHostArray(size); + nint target = _renderer.PersistentBuffers.Default.GetHostArray(size); WriteTo(target); @@ -308,7 +307,7 @@ namespace Ryujinx.Graphics.OpenGL.Image } else { - IntPtr target = _renderer.PersistentBuffers.Default.GetHostArray(size); + nint target = _renderer.PersistentBuffers.Default.GetHostArray(size); int offset = WriteTo2D(target, layer, level); @@ -340,15 +339,15 @@ namespace Ryujinx.Graphics.OpenGL.Image public void WriteToPbo(int offset, bool forceBgra) { - WriteTo(IntPtr.Zero + offset, forceBgra); + WriteTo(nint.Zero + offset, forceBgra); } public int WriteToPbo2D(int offset, int layer, int level) { - return WriteTo2D(IntPtr.Zero + offset, layer, level); + return WriteTo2D(nint.Zero + offset, layer, level); } - private int WriteTo2D(IntPtr data, int layer, int level) + private int WriteTo2D(nint data, int layer, int level) { TextureTarget target = Target.Convert(); @@ -391,7 +390,7 @@ namespace Ryujinx.Graphics.OpenGL.Image return 0; } - private void WriteTo(IntPtr data, bool forceBgra = false) + private void WriteTo(nint data, bool forceBgra = false) { TextureTarget target = Target.Convert(); @@ -427,7 +426,7 @@ namespace Ryujinx.Graphics.OpenGL.Image faces = 6; } - int levels = Info.GetLevelsClamped(); + int levels = Info.Levels; for (int level = 0; level < levels; level++) { @@ -449,101 +448,94 @@ namespace Ryujinx.Graphics.OpenGL.Image } } - public void SetData(ReadOnlySpan dataSpan) + public void SetData(MemoryOwner data) { - if (Format == Format.S8UintD24Unorm) + using (data = EnsureDataFormat(data)) { - dataSpan = FormatConverter.ConvertS8D24ToD24S8(dataSpan); - } - - unsafe - { - fixed (byte* ptr = dataSpan) + unsafe { - ReadFrom((IntPtr)ptr, dataSpan.Length); + var dataSpan = data.Span; + fixed (byte* ptr = dataSpan) + { + ReadFrom((nint)ptr, dataSpan.Length); + } } } } - public void SetData(IMemoryOwner data) + public void SetData(MemoryOwner data, int layer, int level) { - SetData(data.Memory.Span); - - data.Dispose(); - } - - public void SetData(IMemoryOwner data, int layer, int level) - { - var dataSpan = data.Memory.Span; - - if (Format == Format.S8UintD24Unorm) + using (data = EnsureDataFormat(data)) { - dataSpan = FormatConverter.ConvertS8D24ToD24S8(dataSpan); - } - - unsafe - { - fixed (byte* ptr = dataSpan) + unsafe { - int width = Math.Max(Info.Width >> level, 1); - int height = Math.Max(Info.Height >> level, 1); + fixed (byte* ptr = data.Span) + { + int width = Math.Max(Info.Width >> level, 1); + int height = Math.Max(Info.Height >> level, 1); - ReadFrom2D((IntPtr)ptr, layer, level, 0, 0, width, height); + ReadFrom2D((nint)ptr, layer, level, 0, 0, width, height); + } } } - - data.Dispose(); } - public void SetData(IMemoryOwner data, int layer, int level, Rectangle region) + public void SetData(MemoryOwner data, int layer, int level, Rectangle region) { - var dataSpan = data.Memory.Span; - - if (Format == Format.S8UintD24Unorm) + using (data = EnsureDataFormat(data)) { - dataSpan = FormatConverter.ConvertS8D24ToD24S8(dataSpan); - } + int wInBlocks = BitUtils.DivRoundUp(region.Width, Info.BlockWidth); + int hInBlocks = BitUtils.DivRoundUp(region.Height, Info.BlockHeight); - int wInBlocks = BitUtils.DivRoundUp(region.Width, Info.BlockWidth); - int hInBlocks = BitUtils.DivRoundUp(region.Height, Info.BlockHeight); - - unsafe - { - fixed (byte* ptr = dataSpan) + unsafe { - ReadFrom2D( - (IntPtr)ptr, - layer, - level, - region.X, - region.Y, - region.Width, - region.Height, - BitUtils.AlignUp(wInBlocks * Info.BytesPerPixel, 4) * hInBlocks); + fixed (byte* ptr = data.Span) + { + ReadFrom2D( + (nint)ptr, + layer, + level, + region.X, + region.Y, + region.Width, + region.Height, + BitUtils.AlignUp(wInBlocks * Info.BytesPerPixel, 4) * hInBlocks); + } } } - - data.Dispose(); } public void ReadFromPbo(int offset, int size) { - ReadFrom(IntPtr.Zero + offset, size); + ReadFrom(nint.Zero + offset, size); } public void ReadFromPbo2D(int offset, int layer, int level, int width, int height) { - ReadFrom2D(IntPtr.Zero + offset, layer, level, 0, 0, width, height); + ReadFrom2D(nint.Zero + offset, layer, level, 0, 0, width, height); } - private void ReadFrom2D(IntPtr data, int layer, int level, int x, int y, int width, int height) + private void ReadFrom2D(nint data, int layer, int level, int x, int y, int width, int height) { int mipSize = Info.GetMipSize2D(level); ReadFrom2D(data, layer, level, x, y, width, height, mipSize); } - private void ReadFrom2D(IntPtr data, int layer, int level, int x, int y, int width, int height, int mipSize) + private MemoryOwner EnsureDataFormat(MemoryOwner data) + { + if (Format == Format.S8UintD24Unorm) + { + using (data) + { + return FormatConverter.ConvertS8D24ToD24S8(data.Span); + } + } + + return data; + } + + private void ReadFrom2D(nint data, int layer, int level, int x, int y, int width, int height, int mipSize) { TextureTarget target = Target.Convert(); @@ -702,7 +694,7 @@ namespace Ryujinx.Graphics.OpenGL.Image } } - private void ReadFrom(IntPtr data, int size) + private void ReadFrom(nint data, int size) { TextureTarget target = Target.Convert(); int baseLevel = 0; @@ -724,7 +716,7 @@ namespace Ryujinx.Graphics.OpenGL.Image int width = Info.Width; int height = Info.Height; int depth = Info.Depth; - int levels = Info.GetLevelsClamped(); + int levels = Info.Levels; int offset = 0; diff --git a/src/Ryujinx.Graphics.OpenGL/OpenGLRenderer.cs b/src/Ryujinx.Graphics.OpenGL/OpenGLRenderer.cs index 64ba4e3ee..6ead314fd 100644 --- a/src/Ryujinx.Graphics.OpenGL/OpenGLRenderer.cs +++ b/src/Ryujinx.Graphics.OpenGL/OpenGLRenderer.cs @@ -29,6 +29,8 @@ namespace Ryujinx.Graphics.OpenGL private readonly Sync _sync; + public uint ProgramCount { get; set; } = 0; + public event EventHandler ScreenCaptured; internal PersistentBuffers PersistentBuffers { get; } @@ -61,7 +63,9 @@ namespace Ryujinx.Graphics.OpenGL { BufferCount++; - if (access.HasFlag(GAL.BufferAccess.FlushPersistent)) + var memType = access & GAL.BufferAccess.MemoryTypeMask; + + if (memType == GAL.BufferAccess.HostMemory) { BufferHandle handle = Buffer.CreatePersistent(size); @@ -75,11 +79,6 @@ namespace Ryujinx.Graphics.OpenGL } } - public BufferHandle CreateBuffer(int size, GAL.BufferAccess access, BufferHandle storageHint) - { - return CreateBuffer(size, access); - } - public BufferHandle CreateBuffer(nint pointer, int size) { throw new NotSupportedException(); @@ -90,8 +89,15 @@ namespace Ryujinx.Graphics.OpenGL throw new NotSupportedException(); } + public IImageArray CreateImageArray(int size, bool isBuffer) + { + return new ImageArray(size); + } + public IProgram CreateProgram(ShaderSource[] shaders, ShaderInfo info) { + ProgramCount++; + return new Program(shaders, info.FragmentOutputMap); } @@ -112,6 +118,11 @@ namespace Ryujinx.Graphics.OpenGL } } + public ITextureArray CreateTextureArray(int size, bool isBuffer) + { + return new TextureArray(size); + } + public void DeleteBuffer(BufferHandle buffer) { PersistentBuffers.Unmap(buffer); @@ -121,7 +132,7 @@ namespace Ryujinx.Graphics.OpenGL public HardwareInfo GetHardwareInfo() { - return new HardwareInfo(GpuVendor, GpuRenderer); + return new HardwareInfo(GpuVendor, GpuRenderer, GpuVendor); // OpenGL does not provide a driver name, vendor name is closest analogue. } public PinnedSpan GetBufferData(BufferHandle buffer, int offset, int size) @@ -138,6 +149,7 @@ namespace Ryujinx.Graphics.OpenGL return new Capabilities( api: TargetApi.OpenGL, vendorName: GpuVendor, + memoryType: SystemMemoryType.BackendManaged, hasFrontFacingBug: intelWindows, hasVectorIndexingBug: amdWindows, needsFragmentOutputSpecialization: false, @@ -151,6 +163,7 @@ namespace Ryujinx.Graphics.OpenGL supportsBgraFormat: false, supportsR4G4Format: false, supportsR4G4B4A4Format: true, + supportsScaledVertexFormats: true, supportsSnormBufferTextureFormat: false, supports5BitComponentFormat: true, supportsSparseBuffer: false, @@ -165,7 +178,8 @@ namespace Ryujinx.Graphics.OpenGL supportsMismatchingViewFormat: HwCapabilities.SupportsMismatchingViewFormat, supportsCubemapView: true, supportsNonConstantTextureOffset: HwCapabilities.SupportsNonConstantTextureOffset, - supportsScaledVertexFormats: true, + supportsQuads: HwCapabilities.SupportsQuads, + supportsSeparateSampler: false, supportsShaderBallot: HwCapabilities.SupportsShaderBallot, supportsShaderBarrierDivergence: !(intelWindows || intelUnix), supportsShaderFloat64: true, @@ -177,6 +191,12 @@ namespace Ryujinx.Graphics.OpenGL supportsViewportSwizzle: HwCapabilities.SupportsViewportSwizzle, supportsIndirectParameters: HwCapabilities.SupportsIndirectParameters, supportsDepthClipControl: true, + uniformBufferSetIndex: 0, + storageBufferSetIndex: 1, + textureSetIndex: 2, + imageSetIndex: 3, + extraSetBaseIndex: 0, + maximumExtraSets: 0, maximumUniformBuffersPerStage: 13, // TODO: Avoid hardcoding those limits here and get from driver? maximumStorageBuffersPerStage: 16, maximumTexturesPerStage: 32, @@ -186,7 +206,8 @@ namespace Ryujinx.Graphics.OpenGL shaderSubgroupSize: Constants.MaxSubgroupSize, storageBufferOffsetAlignment: HwCapabilities.StorageBufferOffsetAlignment, textureBufferOffsetAlignment: HwCapabilities.TextureBufferOffsetAlignment, - gatherBiasPrecision: intelWindows || amdWindows ? 8 : 0); // Precision is 8 for these vendors on Vulkan. + gatherBiasPrecision: intelWindows || amdWindows ? 8 : 0, // Precision is 8 for these vendors on Vulkan. + maximumGpuMemory: 0); } public void SetBufferData(BufferHandle buffer, int offset, ReadOnlySpan data) @@ -226,7 +247,7 @@ namespace Ryujinx.Graphics.OpenGL // This is required to disable [0, 1] clamping for SNorm outputs on compatibility profiles. // This call is expected to fail if we're running with a core profile, // as this clamp target was deprecated, but that's fine as a core profile - // should already have the desired behaviour were outputs are not clamped. + // should already have the desired behaviour where outputs are not clamped. GL.ClampColor(ClampColorTarget.ClampFragmentColor, ClampColorMode.False); } @@ -248,7 +269,7 @@ namespace Ryujinx.Graphics.OpenGL { // alwaysBackground is ignored, since we cannot switch from the current context. - if (IOpenGLContext.HasContext()) + if (_window.BackgroundContext.HasContext()) { action(); // We have a context already - use that (assuming it is the main one). } diff --git a/src/Ryujinx.Graphics.OpenGL/PersistentBuffers.cs b/src/Ryujinx.Graphics.OpenGL/PersistentBuffers.cs index ebfe3ad64..28ebe88a5 100644 --- a/src/Ryujinx.Graphics.OpenGL/PersistentBuffers.cs +++ b/src/Ryujinx.Graphics.OpenGL/PersistentBuffers.cs @@ -14,7 +14,7 @@ namespace Ryujinx.Graphics.OpenGL private readonly PersistentBuffer _main = new(); private readonly PersistentBuffer _background = new(); - private readonly Dictionary _maps = new(); + private readonly Dictionary _maps = new(); public PersistentBuffer Default => BackgroundContextWorker.InBackground ? _background : _main; @@ -27,7 +27,7 @@ namespace Ryujinx.Graphics.OpenGL public void Map(BufferHandle handle, int size) { GL.BindBuffer(BufferTarget.CopyWriteBuffer, handle.ToInt32()); - IntPtr ptr = GL.MapBufferRange(BufferTarget.CopyWriteBuffer, IntPtr.Zero, size, BufferAccessMask.MapReadBit | BufferAccessMask.MapPersistentBit); + nint ptr = GL.MapBufferRange(BufferTarget.CopyWriteBuffer, nint.Zero, size, BufferAccessMask.MapReadBit | BufferAccessMask.MapPersistentBit); _maps[handle] = ptr; } @@ -43,7 +43,7 @@ namespace Ryujinx.Graphics.OpenGL } } - public bool TryGet(BufferHandle handle, out IntPtr ptr) + public bool TryGet(BufferHandle handle, out nint ptr) { return _maps.TryGetValue(handle, out ptr); } @@ -51,12 +51,12 @@ namespace Ryujinx.Graphics.OpenGL class PersistentBuffer : IDisposable { - private IntPtr _bufferMap; + private nint _bufferMap; private int _copyBufferHandle; private int _copyBufferSize; private byte[] _data; - private IntPtr _dataMap; + private nint _dataMap; private void EnsureBuffer(int requiredSize) { @@ -73,19 +73,19 @@ namespace Ryujinx.Graphics.OpenGL _copyBufferSize = requiredSize; GL.BindBuffer(BufferTarget.CopyWriteBuffer, _copyBufferHandle); - GL.BufferStorage(BufferTarget.CopyWriteBuffer, requiredSize, IntPtr.Zero, BufferStorageFlags.MapReadBit | BufferStorageFlags.MapPersistentBit); + GL.BufferStorage(BufferTarget.CopyWriteBuffer, requiredSize, nint.Zero, BufferStorageFlags.MapReadBit | BufferStorageFlags.MapPersistentBit); - _bufferMap = GL.MapBufferRange(BufferTarget.CopyWriteBuffer, IntPtr.Zero, requiredSize, BufferAccessMask.MapReadBit | BufferAccessMask.MapPersistentBit); + _bufferMap = GL.MapBufferRange(BufferTarget.CopyWriteBuffer, nint.Zero, requiredSize, BufferAccessMask.MapReadBit | BufferAccessMask.MapPersistentBit); } } - public unsafe IntPtr GetHostArray(int requiredSize) + public unsafe nint GetHostArray(int requiredSize) { if (_data == null || _data.Length < requiredSize) { _data = GC.AllocateUninitializedArray(requiredSize, true); - _dataMap = (IntPtr)Unsafe.AsPointer(ref MemoryMarshal.GetArrayDataReference(_data)); + _dataMap = (nint)Unsafe.AsPointer(ref MemoryMarshal.GetArrayDataReference(_data)); } return _dataMap; @@ -95,7 +95,7 @@ namespace Ryujinx.Graphics.OpenGL { GL.MemoryBarrier(MemoryBarrierFlags.ClientMappedBufferBarrierBit); - IntPtr sync = GL.FenceSync(SyncCondition.SyncGpuCommandsComplete, WaitSyncFlags.None); + nint sync = GL.FenceSync(SyncCondition.SyncGpuCommandsComplete, WaitSyncFlags.None); WaitSyncStatus syncResult = GL.ClientWaitSync(sync, ClientWaitSyncFlags.SyncFlushCommandsBit, 1000000000); if (syncResult == WaitSyncStatus.TimeoutExpired) @@ -143,7 +143,7 @@ namespace Ryujinx.Graphics.OpenGL GL.BindBuffer(BufferTarget.CopyReadBuffer, buffer.ToInt32()); GL.BindBuffer(BufferTarget.CopyWriteBuffer, _copyBufferHandle); - GL.CopyBufferSubData(BufferTarget.CopyReadBuffer, BufferTarget.CopyWriteBuffer, (IntPtr)offset, IntPtr.Zero, size); + GL.CopyBufferSubData(BufferTarget.CopyReadBuffer, BufferTarget.CopyWriteBuffer, (nint)offset, nint.Zero, size); GL.BindBuffer(BufferTarget.CopyWriteBuffer, 0); diff --git a/src/Ryujinx.Graphics.OpenGL/Pipeline.cs b/src/Ryujinx.Graphics.OpenGL/Pipeline.cs index 923c85d7e..096e2e5eb 100644 --- a/src/Ryujinx.Graphics.OpenGL/Pipeline.cs +++ b/src/Ryujinx.Graphics.OpenGL/Pipeline.cs @@ -23,7 +23,7 @@ namespace Ryujinx.Graphics.OpenGL private VertexArray _vertexArray; private Framebuffer _framebuffer; - private IntPtr _indexBaseOffset; + private nint _indexBaseOffset; private DrawElementsType _elementsType; @@ -45,7 +45,7 @@ namespace Ryujinx.Graphics.OpenGL private readonly Vector4[] _fpIsBgra = new Vector4[SupportBuffer.FragmentIsBgraCount]; - private readonly (TextureBase, Format)[] _images; + private readonly TextureBase[] _images; private TextureBase _unit0Texture; private Sampler _unit0Sampler; @@ -78,7 +78,7 @@ namespace Ryujinx.Graphics.OpenGL _fragmentOutputMap = uint.MaxValue; _componentMasks = uint.MaxValue; - _images = new (TextureBase, Format)[SavedImages]; + _images = new TextureBase[SavedImages]; _tfbs = new BufferHandle[Constants.MaxTransformFeedbackBuffers]; _tfbTargets = new BufferRange[Constants.MaxTransformFeedbackBuffers]; @@ -358,7 +358,7 @@ namespace Ryujinx.Graphics.OpenGL break; } - IntPtr indexBaseOffset = _indexBaseOffset + firstIndex * indexElemSize; + nint indexBaseOffset = _indexBaseOffset + firstIndex * indexElemSize; if (_primitiveType == PrimitiveType.Quads && !HwCapabilities.SupportsQuads) { @@ -396,7 +396,7 @@ namespace Ryujinx.Graphics.OpenGL private void DrawQuadsIndexedImpl( int indexCount, int instanceCount, - IntPtr indexBaseOffset, + nint indexBaseOffset, int indexElemSize, int firstVertex, int firstInstance) @@ -447,7 +447,7 @@ namespace Ryujinx.Graphics.OpenGL } else { - IntPtr[] indices = new IntPtr[quadsCount]; + nint[] indices = new nint[quadsCount]; int[] counts = new int[quadsCount]; @@ -475,7 +475,7 @@ namespace Ryujinx.Graphics.OpenGL private void DrawQuadStripIndexedImpl( int indexCount, int instanceCount, - IntPtr indexBaseOffset, + nint indexBaseOffset, int indexElemSize, int firstVertex, int firstInstance) @@ -483,7 +483,7 @@ namespace Ryujinx.Graphics.OpenGL // TODO: Instanced rendering. int quadsCount = (indexCount - 2) / 2; - IntPtr[] indices = new IntPtr[quadsCount]; + nint[] indices = new nint[quadsCount]; int[] counts = new int[quadsCount]; @@ -516,7 +516,7 @@ namespace Ryujinx.Graphics.OpenGL private void DrawIndexedImpl( int indexCount, int instanceCount, - IntPtr indexBaseOffset, + nint indexBaseOffset, int firstVertex, int firstInstance) { @@ -589,7 +589,7 @@ namespace Ryujinx.Graphics.OpenGL GL.BindBuffer((BufferTarget)All.DrawIndirectBuffer, indirectBuffer.Handle.ToInt32()); - GL.DrawElementsIndirect(_primitiveType, _elementsType, (IntPtr)indirectBuffer.Offset); + GL.DrawElementsIndirect(_primitiveType, _elementsType, (nint)indirectBuffer.Offset); _vertexArray.RestoreIndexBuffer(); @@ -614,8 +614,8 @@ namespace Ryujinx.Graphics.OpenGL GL.MultiDrawElementsIndirectCount( _primitiveType, (All)_elementsType, - (IntPtr)indirectBuffer.Offset, - (IntPtr)parameterBuffer.Offset, + (nint)indirectBuffer.Offset, + (nint)parameterBuffer.Offset, maxDrawCount, stride); @@ -636,7 +636,7 @@ namespace Ryujinx.Graphics.OpenGL GL.BindBuffer((BufferTarget)All.DrawIndirectBuffer, indirectBuffer.Handle.ToInt32()); - GL.DrawArraysIndirect(_primitiveType, (IntPtr)indirectBuffer.Offset); + GL.DrawArraysIndirect(_primitiveType, (nint)indirectBuffer.Offset); PostDraw(); } @@ -656,8 +656,8 @@ namespace Ryujinx.Graphics.OpenGL GL.MultiDrawArraysIndirectCount( _primitiveType, - (IntPtr)indirectBuffer.Offset, - (IntPtr)parameterBuffer.Offset, + (nint)indirectBuffer.Offset, + (nint)parameterBuffer.Offset, maxDrawCount, stride); @@ -935,11 +935,11 @@ namespace Ryujinx.Graphics.OpenGL SetFrontFace(_frontFace = frontFace.Convert()); } - public void SetImage(int binding, ITexture texture, Format imageFormat) + public void SetImage(ShaderStage stage, int binding, ITexture texture) { if ((uint)binding < SavedImages) { - _images[binding] = (texture as TextureBase, imageFormat); + _images[binding] = texture as TextureBase; } if (texture == null) @@ -950,7 +950,7 @@ namespace Ryujinx.Graphics.OpenGL TextureBase texBase = (TextureBase)texture; - SizedInternalFormat format = FormatTable.GetImageFormat(imageFormat); + SizedInternalFormat format = FormatTable.GetImageFormat(texBase.Format); if (format != 0) { @@ -958,11 +958,21 @@ namespace Ryujinx.Graphics.OpenGL } } + public void SetImageArray(ShaderStage stage, int binding, IImageArray array) + { + (array as ImageArray).Bind(binding); + } + + public void SetImageArraySeparate(ShaderStage stage, int setIndex, IImageArray array) + { + throw new NotSupportedException("OpenGL does not support descriptor sets."); + } + public void SetIndexBuffer(BufferRange buffer, IndexType type) { _elementsType = type.Convert(); - _indexBaseOffset = (IntPtr)buffer.Offset; + _indexBaseOffset = (nint)buffer.Offset; EnsureVertexArray(); @@ -1117,7 +1127,7 @@ namespace Ryujinx.Graphics.OpenGL prg.Bind(); } - if (prg.HasFragmentShader && _fragmentOutputMap != (uint)prg.FragmentOutputMap) + if (_fragmentOutputMap != (uint)prg.FragmentOutputMap) { _fragmentOutputMap = (uint)prg.FragmentOutputMap; @@ -1302,6 +1312,15 @@ namespace Ryujinx.Graphics.OpenGL } } + public void SetTextureArray(ShaderStage stage, int binding, ITextureArray array) + { + (array as TextureArray).Bind(binding); + } + + public void SetTextureArraySeparate(ShaderStage stage, int setIndex, ITextureArray array) + { + throw new NotSupportedException("OpenGL does not support descriptor sets."); + } public void SetTransformFeedbackBuffers(ReadOnlySpan buffers) { @@ -1431,11 +1450,11 @@ namespace Ryujinx.Graphics.OpenGL if (buffer.Handle == BufferHandle.Null) { - GL.BindBufferRange(target, assignment.Binding, 0, IntPtr.Zero, 0); + GL.BindBufferRange(target, assignment.Binding, 0, nint.Zero, 0); continue; } - GL.BindBufferRange(target, assignment.Binding, buffer.Handle.ToInt32(), (IntPtr)buffer.Offset, buffer.Size); + GL.BindBufferRange(target, assignment.Binding, buffer.Handle.ToInt32(), (nint)buffer.Offset, buffer.Size); } } @@ -1603,11 +1622,11 @@ namespace Ryujinx.Graphics.OpenGL { for (int i = 0; i < SavedImages; i++) { - (TextureBase texBase, Format imageFormat) = _images[i]; + TextureBase texBase = _images[i]; if (texBase != null) { - SizedInternalFormat format = FormatTable.GetImageFormat(imageFormat); + SizedInternalFormat format = FormatTable.GetImageFormat(texBase.Format); if (format != 0) { diff --git a/src/Ryujinx.Graphics.OpenGL/Program.cs b/src/Ryujinx.Graphics.OpenGL/Program.cs index cc9120c7c..608a03451 100644 --- a/src/Ryujinx.Graphics.OpenGL/Program.cs +++ b/src/Ryujinx.Graphics.OpenGL/Program.cs @@ -30,7 +30,6 @@ namespace Ryujinx.Graphics.OpenGL private ProgramLinkStatus _status = ProgramLinkStatus.Incomplete; private int[] _shaderHandles; - public bool HasFragmentShader; public int FragmentOutputMap { get; } public Program(ShaderSource[] shaders, int fragmentOutputMap) @@ -40,6 +39,7 @@ namespace Ryujinx.Graphics.OpenGL GL.ProgramParameter(Handle, ProgramParameterName.ProgramBinaryRetrievableHint, 1); _shaderHandles = new int[shaders.Length]; + bool hasFragmentShader = false; for (int index = 0; index < shaders.Length; index++) { @@ -47,7 +47,7 @@ namespace Ryujinx.Graphics.OpenGL if (shader.Stage == ShaderStage.Fragment) { - HasFragmentShader = true; + hasFragmentShader = true; } int shaderHandle = GL.CreateShader(shader.Stage.Convert()); @@ -71,7 +71,7 @@ namespace Ryujinx.Graphics.OpenGL GL.LinkProgram(Handle); - FragmentOutputMap = fragmentOutputMap; + FragmentOutputMap = hasFragmentShader ? fragmentOutputMap : 0; } public Program(ReadOnlySpan code, bool hasFragmentShader, int fragmentOutputMap) @@ -86,13 +86,12 @@ namespace Ryujinx.Graphics.OpenGL { fixed (byte* ptr = code) { - GL.ProgramBinary(Handle, binaryFormat, (IntPtr)ptr, code.Length - 4); + GL.ProgramBinary(Handle, binaryFormat, (nint)ptr, code.Length - 4); } } } - HasFragmentShader = hasFragmentShader; - FragmentOutputMap = fragmentOutputMap; + FragmentOutputMap = hasFragmentShader ? fragmentOutputMap : 0; } public void Bind() diff --git a/src/Ryujinx.Graphics.OpenGL/Queries/BufferedQuery.cs b/src/Ryujinx.Graphics.OpenGL/Queries/BufferedQuery.cs index 0a85970d7..a5acd8dce 100644 --- a/src/Ryujinx.Graphics.OpenGL/Queries/BufferedQuery.cs +++ b/src/Ryujinx.Graphics.OpenGL/Queries/BufferedQuery.cs @@ -15,7 +15,7 @@ namespace Ryujinx.Graphics.OpenGL.Queries public int Query { get; } private readonly int _buffer; - private readonly IntPtr _bufferMap; + private readonly nint _bufferMap; private readonly QueryTarget _type; public BufferedQuery(QueryTarget type) @@ -29,9 +29,9 @@ namespace Ryujinx.Graphics.OpenGL.Queries unsafe { long defaultValue = DefaultValue; - GL.BufferStorage(BufferTarget.QueryBuffer, sizeof(long), (IntPtr)(&defaultValue), BufferStorageFlags.MapReadBit | BufferStorageFlags.MapWriteBit | BufferStorageFlags.MapPersistentBit); + GL.BufferStorage(BufferTarget.QueryBuffer, sizeof(long), (nint)(&defaultValue), BufferStorageFlags.MapReadBit | BufferStorageFlags.MapWriteBit | BufferStorageFlags.MapPersistentBit); } - _bufferMap = GL.MapBufferRange(BufferTarget.QueryBuffer, IntPtr.Zero, sizeof(long), BufferAccessMask.MapReadBit | BufferAccessMask.MapWriteBit | BufferAccessMask.MapPersistentBit); + _bufferMap = GL.MapBufferRange(BufferTarget.QueryBuffer, nint.Zero, sizeof(long), BufferAccessMask.MapReadBit | BufferAccessMask.MapWriteBit | BufferAccessMask.MapPersistentBit); } public void Reset() diff --git a/src/Ryujinx.Graphics.OpenGL/Ryujinx.Graphics.OpenGL.csproj b/src/Ryujinx.Graphics.OpenGL/Ryujinx.Graphics.OpenGL.csproj index 3d64da99b..f3071f486 100644 --- a/src/Ryujinx.Graphics.OpenGL/Ryujinx.Graphics.OpenGL.csproj +++ b/src/Ryujinx.Graphics.OpenGL/Ryujinx.Graphics.OpenGL.csproj @@ -21,6 +21,7 @@ + diff --git a/src/Ryujinx.Graphics.OpenGL/Sync.cs b/src/Ryujinx.Graphics.OpenGL/Sync.cs index eba1638a3..e8f7ebc00 100644 --- a/src/Ryujinx.Graphics.OpenGL/Sync.cs +++ b/src/Ryujinx.Graphics.OpenGL/Sync.cs @@ -11,7 +11,7 @@ namespace Ryujinx.Graphics.OpenGL private class SyncHandle { public ulong ID; - public IntPtr Handle; + public nint Handle; } private ulong _firstHandle = 0; @@ -50,7 +50,7 @@ namespace Ryujinx.Graphics.OpenGL { lock (handle) { - if (handle.Handle == IntPtr.Zero) + if (handle.Handle == nint.Zero) { continue; } @@ -96,7 +96,7 @@ namespace Ryujinx.Graphics.OpenGL { lock (result) { - if (result.Handle == IntPtr.Zero) + if (result.Handle == nint.Zero) { return; } @@ -140,7 +140,7 @@ namespace Ryujinx.Graphics.OpenGL _firstHandle = first.ID + 1; _handles.RemoveAt(0); GL.DeleteSync(first.Handle); - first.Handle = IntPtr.Zero; + first.Handle = nint.Zero; } } } @@ -161,7 +161,7 @@ namespace Ryujinx.Graphics.OpenGL lock (handle) { GL.DeleteSync(handle.Handle); - handle.Handle = IntPtr.Zero; + handle.Handle = nint.Zero; } } diff --git a/src/Ryujinx.Graphics.OpenGL/VertexArray.cs b/src/Ryujinx.Graphics.OpenGL/VertexArray.cs index 32211e783..2db84421f 100644 --- a/src/Ryujinx.Graphics.OpenGL/VertexArray.cs +++ b/src/Ryujinx.Graphics.OpenGL/VertexArray.cs @@ -56,7 +56,7 @@ namespace Ryujinx.Graphics.OpenGL minVertexCount = vertexCount; } - GL.BindVertexBuffer(bindingIndex, vb.Buffer.Handle.ToInt32(), (IntPtr)vb.Buffer.Offset, vb.Stride); + GL.BindVertexBuffer(bindingIndex, vb.Buffer.Handle.ToInt32(), (nint)vb.Buffer.Offset, vb.Stride); GL.VertexBindingDivisor(bindingIndex, vb.Divisor); _vertexBuffersInUse |= 1u << bindingIndex; } @@ -64,7 +64,7 @@ namespace Ryujinx.Graphics.OpenGL { if ((_vertexBuffersInUse & (1u << bindingIndex)) != 0) { - GL.BindVertexBuffer(bindingIndex, 0, IntPtr.Zero, 0); + GL.BindVertexBuffer(bindingIndex, 0, nint.Zero, 0); _vertexBuffersInUse &= ~(1u << bindingIndex); } } @@ -188,7 +188,7 @@ namespace Ryujinx.Graphics.OpenGL Buffer.Copy(vb.Buffer.Handle, tempVertexBuffer, vb.Buffer.Offset, currentTempVbOffset, vb.Buffer.Size); Buffer.Clear(tempVertexBuffer, currentTempVbOffset + vb.Buffer.Size, requiredSize - vb.Buffer.Size, 0); - GL.BindVertexBuffer(vbIndex, tempVertexBuffer.ToInt32(), (IntPtr)currentTempVbOffset, vb.Stride); + GL.BindVertexBuffer(vbIndex, tempVertexBuffer.ToInt32(), (nint)currentTempVbOffset, vb.Stride); currentTempVbOffset += requiredSize; _vertexBuffersLimited |= 1u << vbIndex; @@ -234,7 +234,7 @@ namespace Ryujinx.Graphics.OpenGL ref var vb = ref _vertexBuffers[vbIndex]; - GL.BindVertexBuffer(vbIndex, vb.Buffer.Handle.ToInt32(), (IntPtr)vb.Buffer.Offset, vb.Stride); + GL.BindVertexBuffer(vbIndex, vb.Buffer.Handle.ToInt32(), (nint)vb.Buffer.Offset, vb.Stride); buffersLimited &= ~(1u << vbIndex); } diff --git a/src/Ryujinx.Graphics.OpenGL/Window.cs b/src/Ryujinx.Graphics.OpenGL/Window.cs index 6bcfefa4e..1dc8a51f6 100644 --- a/src/Ryujinx.Graphics.OpenGL/Window.cs +++ b/src/Ryujinx.Graphics.OpenGL/Window.cs @@ -54,7 +54,7 @@ namespace Ryujinx.Graphics.OpenGL GL.PixelStore(PixelStoreParameter.UnpackAlignment, 4); } - public void ChangeVSyncMode(bool vsyncEnabled) { } + public void ChangeVSyncMode(VSyncMode vSyncMode) { } public void SetSize(int width, int height) { @@ -373,6 +373,16 @@ namespace Ryujinx.Graphics.OpenGL _isLinear = false; _scalingFilter.Level = _scalingFilterLevel; + RecreateUpscalingTexture(); + break; + case ScalingFilter.Area: + if (_scalingFilter is not AreaScalingFilter) + { + _scalingFilter?.Dispose(); + _scalingFilter = new AreaScalingFilter(_renderer); + } + _isLinear = false; + RecreateUpscalingTexture(); break; } diff --git a/src/Ryujinx.Graphics.Shader/BufferDescriptor.cs b/src/Ryujinx.Graphics.Shader/BufferDescriptor.cs index ead1c5e67..11d4e3c11 100644 --- a/src/Ryujinx.Graphics.Shader/BufferDescriptor.cs +++ b/src/Ryujinx.Graphics.Shader/BufferDescriptor.cs @@ -4,14 +4,16 @@ namespace Ryujinx.Graphics.Shader { // New fields should be added to the end of the struct to keep disk shader cache compatibility. + public readonly int Set; public readonly int Binding; public readonly byte Slot; public readonly byte SbCbSlot; public readonly ushort SbCbOffset; public readonly BufferUsageFlags Flags; - public BufferDescriptor(int binding, int slot) + public BufferDescriptor(int set, int binding, int slot) { + Set = set; Binding = binding; Slot = (byte)slot; SbCbSlot = 0; @@ -19,8 +21,9 @@ namespace Ryujinx.Graphics.Shader Flags = BufferUsageFlags.None; } - public BufferDescriptor(int binding, int slot, int sbCbSlot, int sbCbOffset, BufferUsageFlags flags) + public BufferDescriptor(int set, int binding, int slot, int sbCbSlot, int sbCbOffset, BufferUsageFlags flags) { + Set = set; Binding = binding; Slot = (byte)slot; SbCbSlot = (byte)sbCbSlot; diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/Declarations.cs b/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/Declarations.cs index 500de71f6..eb6c689b8 100644 --- a/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/Declarations.cs +++ b/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/Declarations.cs @@ -6,7 +6,6 @@ using System; using System.Collections.Generic; using System.Globalization; using System.Linq; -using System.Numerics; namespace Ryujinx.Graphics.Shader.CodeGen.Glsl { @@ -339,27 +338,20 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl private static void DeclareSamplers(CodeGenContext context, IEnumerable definitions) { - int arraySize = 0; - foreach (var definition in definitions) { - string indexExpr = string.Empty; + string arrayDecl = string.Empty; - if (definition.Type.HasFlag(SamplerType.Indexed)) + if (definition.ArrayLength > 1) { - if (arraySize == 0) - { - arraySize = ResourceManager.SamplerArraySize; - } - else if (--arraySize != 0) - { - continue; - } - - indexExpr = $"[{NumberFormatter.FormatInt(arraySize)}]"; + arrayDecl = $"[{NumberFormatter.FormatInt(definition.ArrayLength)}]"; + } + else if (definition.ArrayLength == 0) + { + arrayDecl = "[]"; } - string samplerTypeName = definition.Type.ToGlslSamplerType(); + string samplerTypeName = definition.Separate ? definition.Type.ToGlslTextureType() : definition.Type.ToGlslSamplerType(); string layout = string.Empty; @@ -368,30 +360,23 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl layout = $", set = {definition.Set}"; } - context.AppendLine($"layout (binding = {definition.Binding}{layout}) uniform {samplerTypeName} {definition.Name}{indexExpr};"); + context.AppendLine($"layout (binding = {definition.Binding}{layout}) uniform {samplerTypeName} {definition.Name}{arrayDecl};"); } } private static void DeclareImages(CodeGenContext context, IEnumerable definitions) { - int arraySize = 0; - foreach (var definition in definitions) { - string indexExpr = string.Empty; + string arrayDecl = string.Empty; - if (definition.Type.HasFlag(SamplerType.Indexed)) + if (definition.ArrayLength > 1) { - if (arraySize == 0) - { - arraySize = ResourceManager.SamplerArraySize; - } - else if (--arraySize != 0) - { - continue; - } - - indexExpr = $"[{NumberFormatter.FormatInt(arraySize)}]"; + arrayDecl = $"[{NumberFormatter.FormatInt(definition.ArrayLength)}]"; + } + else if (definition.ArrayLength == 0) + { + arrayDecl = "[]"; } string imageTypeName = definition.Type.ToGlslImageType(definition.Format.GetComponentType()); @@ -413,7 +398,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl layout = $", set = {definition.Set}{layout}"; } - context.AppendLine($"layout (binding = {definition.Binding}{layout}) uniform {imageTypeName} {definition.Name}{indexExpr};"); + context.AppendLine($"layout (binding = {definition.Binding}{layout}) uniform {imageTypeName} {definition.Name}{arrayDecl};"); } } diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/Instructions/InstGen.cs b/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/Instructions/InstGen.cs index eb0cb92db..9e7f64b0e 100644 --- a/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/Instructions/InstGen.cs +++ b/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/Instructions/InstGen.cs @@ -38,7 +38,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions AggregateType type = GetSrcVarType(operation.Inst, 0); - string srcExpr = GetSoureExpr(context, src, type); + string srcExpr = GetSourceExpr(context, src, type); string zero; if (type == AggregateType.FP64) @@ -80,7 +80,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions for (int argIndex = operation.SourcesCount - arity + 2; argIndex < operation.SourcesCount; argIndex++) { - builder.Append($", {GetSoureExpr(context, operation.GetSource(argIndex), dstType)}"); + builder.Append($", {GetSourceExpr(context, operation.GetSource(argIndex), dstType)}"); } } else @@ -94,7 +94,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions AggregateType dstType = GetSrcVarType(inst, argIndex); - builder.Append(GetSoureExpr(context, operation.GetSource(argIndex), dstType)); + builder.Append(GetSourceExpr(context, operation.GetSource(argIndex), dstType)); } } @@ -107,7 +107,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions // Return may optionally have a return value (and in this case it is unary). if (inst == Instruction.Return && operation.SourcesCount != 0) { - return $"{op} {GetSoureExpr(context, operation.GetSource(0), context.CurrentFunction.ReturnType)}"; + return $"{op} {GetSourceExpr(context, operation.GetSource(0), context.CurrentFunction.ReturnType)}"; } int arity = (int)(info.Type & InstType.ArityMask); @@ -118,7 +118,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions { IAstNode src = operation.GetSource(index); - string srcExpr = GetSoureExpr(context, src, GetSrcVarType(inst, index)); + string srcExpr = GetSourceExpr(context, src, GetSrcVarType(inst, index)); bool isLhs = arity == 2 && index == 0; diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/Instructions/InstGenBallot.cs b/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/Instructions/InstGenBallot.cs index 6cc7048bd..000d7f797 100644 --- a/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/Instructions/InstGenBallot.cs +++ b/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/Instructions/InstGenBallot.cs @@ -12,7 +12,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions { AggregateType dstType = GetSrcVarType(operation.Inst, 0); - string arg = GetSoureExpr(context, operation.GetSource(0), dstType); + string arg = GetSourceExpr(context, operation.GetSource(0), dstType); char component = "xyzw"[operation.Index]; if (context.HostCapabilities.SupportsShaderBallot) diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/Instructions/InstGenCall.cs b/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/Instructions/InstGenCall.cs index 0618ba8a3..d5448856d 100644 --- a/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/Instructions/InstGenCall.cs +++ b/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/Instructions/InstGenCall.cs @@ -20,7 +20,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions for (int i = 0; i < args.Length; i++) { - args[i] = GetSoureExpr(context, operation.GetSource(i + 1), function.GetArgumentType(i)); + args[i] = GetSourceExpr(context, operation.GetSource(i + 1), function.GetArgumentType(i)); } return $"{function.Name}({string.Join(", ", args)})"; diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/Instructions/InstGenHelper.cs b/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/Instructions/InstGenHelper.cs index 5c2d16f4c..4b28f3878 100644 --- a/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/Instructions/InstGenHelper.cs +++ b/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/Instructions/InstGenHelper.cs @@ -140,7 +140,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions return _infoTable[(int)(inst & Instruction.Mask)]; } - public static string GetSoureExpr(CodeGenContext context, IAstNode node, AggregateType dstType) + public static string GetSourceExpr(CodeGenContext context, IAstNode node, AggregateType dstType) { return ReinterpretCast(context, node, OperandManager.GetNodeDestType(context, node), dstType); } diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/Instructions/InstGenMemory.cs b/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/Instructions/InstGenMemory.cs index 2e90bd16d..56507a2a4 100644 --- a/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/Instructions/InstGenMemory.cs +++ b/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/Instructions/InstGenMemory.cs @@ -14,35 +14,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions { AstTextureOperation texOp = (AstTextureOperation)operation; - bool isBindless = (texOp.Flags & TextureFlags.Bindless) != 0; - - // TODO: Bindless texture support. For now we just return 0/do nothing. - if (isBindless) - { - switch (texOp.Inst) - { - case Instruction.ImageStore: - return "// imageStore(bindless)"; - case Instruction.ImageLoad: - AggregateType componentType = texOp.Format.GetComponentType(); - - NumberFormatter.TryFormat(0, componentType, out string imageConst); - - AggregateType outputType = texOp.GetVectorType(componentType); - - if ((outputType & AggregateType.ElementCountMask) != 0) - { - return $"{Declarations.GetVarTypeName(context, outputType, precise: false)}({imageConst})"; - } - - return imageConst; - default: - return NumberFormatter.FormatInt(0); - } - } - bool isArray = (texOp.Type & SamplerType.Array) != 0; - bool isIndexed = (texOp.Type & SamplerType.Indexed) != 0; var texCallBuilder = new StringBuilder(); @@ -70,21 +42,14 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions texCallBuilder.Append(texOp.Inst == Instruction.ImageLoad ? "imageLoad" : "imageStore"); } - int srcIndex = isBindless ? 1 : 0; + int srcIndex = 0; string Src(AggregateType type) { - return GetSoureExpr(context, texOp.GetSource(srcIndex++), type); + return GetSourceExpr(context, texOp.GetSource(srcIndex++), type); } - string indexExpr = null; - - if (isIndexed) - { - indexExpr = Src(AggregateType.S32); - } - - string imageName = GetImageName(context.Properties, texOp, indexExpr); + string imageName = GetImageName(context, texOp, ref srcIndex); texCallBuilder.Append('('); texCallBuilder.Append(imageName); @@ -198,27 +163,9 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions AstTextureOperation texOp = (AstTextureOperation)operation; int coordsCount = texOp.Type.GetDimensions(); + int coordsIndex = 0; - bool isBindless = (texOp.Flags & TextureFlags.Bindless) != 0; - - // TODO: Bindless texture support. For now we just return 0. - if (isBindless) - { - return NumberFormatter.FormatFloat(0); - } - - bool isIndexed = (texOp.Type & SamplerType.Indexed) != 0; - - string indexExpr = null; - - if (isIndexed) - { - indexExpr = GetSoureExpr(context, texOp.GetSource(0), AggregateType.S32); - } - - string samplerName = GetSamplerName(context.Properties, texOp, indexExpr); - - int coordsIndex = isBindless || isIndexed ? 1 : 0; + string samplerName = GetSamplerName(context, texOp, ref coordsIndex); string coordsExpr; @@ -228,14 +175,14 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions for (int index = 0; index < coordsCount; index++) { - elems[index] = GetSoureExpr(context, texOp.GetSource(coordsIndex + index), AggregateType.FP32); + elems[index] = GetSourceExpr(context, texOp.GetSource(coordsIndex + index), AggregateType.FP32); } coordsExpr = "vec" + coordsCount + "(" + string.Join(", ", elems) + ")"; } else { - coordsExpr = GetSoureExpr(context, texOp.GetSource(coordsIndex), AggregateType.FP32); + coordsExpr = GetSourceExpr(context, texOp.GetSource(coordsIndex), AggregateType.FP32); } return $"textureQueryLod({samplerName}, {coordsExpr}){GetMask(texOp.Index)}"; @@ -250,7 +197,6 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions { AstTextureOperation texOp = (AstTextureOperation)operation; - bool isBindless = (texOp.Flags & TextureFlags.Bindless) != 0; bool isGather = (texOp.Flags & TextureFlags.Gather) != 0; bool hasDerivatives = (texOp.Flags & TextureFlags.Derivatives) != 0; bool intCoords = (texOp.Flags & TextureFlags.IntCoords) != 0; @@ -260,12 +206,9 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions bool hasOffsets = (texOp.Flags & TextureFlags.Offsets) != 0; bool isArray = (texOp.Type & SamplerType.Array) != 0; - bool isIndexed = (texOp.Type & SamplerType.Indexed) != 0; bool isMultisample = (texOp.Type & SamplerType.Multisample) != 0; bool isShadow = (texOp.Type & SamplerType.Shadow) != 0; - bool colorIsVector = isGather || !isShadow; - SamplerType type = texOp.Type & SamplerType.Mask; bool is2D = type == SamplerType.Texture2D; @@ -286,24 +229,6 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions hasLodLevel = false; } - // TODO: Bindless texture support. For now we just return 0. - if (isBindless) - { - string scalarValue = NumberFormatter.FormatFloat(0); - - if (colorIsVector) - { - AggregateType outputType = texOp.GetVectorType(AggregateType.FP32); - - if ((outputType & AggregateType.ElementCountMask) != 0) - { - return $"{Declarations.GetVarTypeName(context, outputType, precise: false)}({scalarValue})"; - } - } - - return scalarValue; - } - string texCall = intCoords ? "texelFetch" : "texture"; if (isGather) @@ -328,21 +253,14 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions texCall += "Offsets"; } - int srcIndex = isBindless ? 1 : 0; + int srcIndex = 0; string Src(AggregateType type) { - return GetSoureExpr(context, texOp.GetSource(srcIndex++), type); + return GetSourceExpr(context, texOp.GetSource(srcIndex++), type); } - string indexExpr = null; - - if (isIndexed) - { - indexExpr = Src(AggregateType.S32); - } - - string samplerName = GetSamplerName(context.Properties, texOp, indexExpr); + string samplerName = GetSamplerName(context, texOp, ref srcIndex); texCall += "(" + samplerName; @@ -512,7 +430,9 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions Append(Src(AggregateType.S32)); } - texCall += ")" + (colorIsVector ? GetMaskMultiDest(texOp.Index) : ""); + bool colorIsVector = isGather || !isShadow; + + texCall += ")" + (colorIsVector ? GetMaskMultiDest(texOp.Index) : string.Empty); return texCall; } @@ -521,24 +441,9 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions { AstTextureOperation texOp = (AstTextureOperation)operation; - bool isBindless = (texOp.Flags & TextureFlags.Bindless) != 0; + int srcIndex = 0; - // TODO: Bindless texture support. For now we just return 0. - if (isBindless) - { - return NumberFormatter.FormatInt(0); - } - - bool isIndexed = (texOp.Type & SamplerType.Indexed) != 0; - - string indexExpr = null; - - if (isIndexed) - { - indexExpr = GetSoureExpr(context, texOp.GetSource(0), AggregateType.S32); - } - - string samplerName = GetSamplerName(context.Properties, texOp, indexExpr); + string samplerName = GetSamplerName(context, texOp, ref srcIndex); return $"textureSamples({samplerName})"; } @@ -547,24 +452,9 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions { AstTextureOperation texOp = (AstTextureOperation)operation; - bool isBindless = (texOp.Flags & TextureFlags.Bindless) != 0; + int srcIndex = 0; - // TODO: Bindless texture support. For now we just return 0. - if (isBindless) - { - return NumberFormatter.FormatInt(0); - } - - bool isIndexed = (texOp.Type & SamplerType.Indexed) != 0; - - string indexExpr = null; - - if (isIndexed) - { - indexExpr = GetSoureExpr(context, texOp.GetSource(0), AggregateType.S32); - } - - string samplerName = GetSamplerName(context.Properties, texOp, indexExpr); + string samplerName = GetSamplerName(context, texOp, ref srcIndex); if (texOp.Index == 3) { @@ -572,15 +462,14 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions } else { - context.Properties.Textures.TryGetValue(texOp.Binding, out TextureDefinition definition); + context.Properties.Textures.TryGetValue(texOp.GetTextureSetAndBinding(), out TextureDefinition definition); bool hasLod = !definition.Type.HasFlag(SamplerType.Multisample) && (definition.Type & SamplerType.Mask) != SamplerType.TextureBuffer; string texCall; if (hasLod) { - int lodSrcIndex = isBindless || isIndexed ? 1 : 0; - IAstNode lod = operation.GetSource(lodSrcIndex); - string lodExpr = GetSoureExpr(context, lod, GetSrcVarType(operation.Inst, lodSrcIndex)); + IAstNode lod = operation.GetSource(srcIndex); + string lodExpr = GetSourceExpr(context, lod, GetSrcVarType(operation.Inst, srcIndex)); texCall = $"textureSize({samplerName}, {lodExpr}){GetMask(texOp.Index)}"; } @@ -697,12 +586,12 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions if (storageKind == StorageKind.Input) { - string expr = GetSoureExpr(context, operation.GetSource(srcIndex++), AggregateType.S32); + string expr = GetSourceExpr(context, operation.GetSource(srcIndex++), AggregateType.S32); varName = $"gl_in[{expr}].{varName}"; } else if (storageKind == StorageKind.Output) { - string expr = GetSoureExpr(context, operation.GetSource(srcIndex++), AggregateType.S32); + string expr = GetSourceExpr(context, operation.GetSource(srcIndex++), AggregateType.S32); varName = $"gl_out[{expr}].{varName}"; } } @@ -735,38 +624,53 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions } else { - varName += $"[{GetSoureExpr(context, src, AggregateType.S32)}]"; + varName += $"[{GetSourceExpr(context, src, AggregateType.S32)}]"; } } if (isStore) { varType &= AggregateType.ElementTypeMask; - varName = $"{varName} = {GetSoureExpr(context, operation.GetSource(srcIndex), varType)}"; + varName = $"{varName} = {GetSourceExpr(context, operation.GetSource(srcIndex), varType)}"; } return varName; } - private static string GetSamplerName(ShaderProperties resourceDefinitions, AstTextureOperation texOp, string indexExpr) + private static string GetSamplerName(CodeGenContext context, AstTextureOperation texOp, ref int srcIndex) { - string name = resourceDefinitions.Textures[texOp.Binding].Name; + TextureDefinition textureDefinition = context.Properties.Textures[texOp.GetTextureSetAndBinding()]; + string name = textureDefinition.Name; - if (texOp.Type.HasFlag(SamplerType.Indexed)) + if (textureDefinition.ArrayLength != 1) { - name = $"{name}[{indexExpr}]"; + name = $"{name}[{GetSourceExpr(context, texOp.GetSource(srcIndex++), AggregateType.S32)}]"; + } + + if (texOp.IsSeparate) + { + TextureDefinition samplerDefinition = context.Properties.Textures[texOp.GetSamplerSetAndBinding()]; + string samplerName = samplerDefinition.Name; + + if (samplerDefinition.ArrayLength != 1) + { + samplerName = $"{samplerName}[{GetSourceExpr(context, texOp.GetSource(srcIndex++), AggregateType.S32)}]"; + } + + name = $"{texOp.Type.ToGlslSamplerType()}({name}, {samplerName})"; } return name; } - private static string GetImageName(ShaderProperties resourceDefinitions, AstTextureOperation texOp, string indexExpr) + private static string GetImageName(CodeGenContext context, AstTextureOperation texOp, ref int srcIndex) { - string name = resourceDefinitions.Images[texOp.Binding].Name; + TextureDefinition definition = context.Properties.Images[texOp.GetTextureSetAndBinding()]; + string name = definition.Name; - if (texOp.Type.HasFlag(SamplerType.Indexed)) + if (definition.ArrayLength != 1) { - name = $"{name}[{indexExpr}]"; + name = $"{name}[{GetSourceExpr(context, texOp.GetSource(srcIndex++), AggregateType.S32)}]"; } return name; diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/Instructions/InstGenPacking.cs b/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/Instructions/InstGenPacking.cs index ad84c4850..4469785d2 100644 --- a/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/Instructions/InstGenPacking.cs +++ b/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/Instructions/InstGenPacking.cs @@ -13,8 +13,8 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions IAstNode src0 = operation.GetSource(0); IAstNode src1 = operation.GetSource(1); - string src0Expr = GetSoureExpr(context, src0, GetSrcVarType(operation.Inst, 0)); - string src1Expr = GetSoureExpr(context, src1, GetSrcVarType(operation.Inst, 1)); + string src0Expr = GetSourceExpr(context, src0, GetSrcVarType(operation.Inst, 0)); + string src1Expr = GetSourceExpr(context, src1, GetSrcVarType(operation.Inst, 1)); return $"packDouble2x32(uvec2({src0Expr}, {src1Expr}))"; } @@ -24,8 +24,8 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions IAstNode src0 = operation.GetSource(0); IAstNode src1 = operation.GetSource(1); - string src0Expr = GetSoureExpr(context, src0, GetSrcVarType(operation.Inst, 0)); - string src1Expr = GetSoureExpr(context, src1, GetSrcVarType(operation.Inst, 1)); + string src0Expr = GetSourceExpr(context, src0, GetSrcVarType(operation.Inst, 0)); + string src1Expr = GetSourceExpr(context, src1, GetSrcVarType(operation.Inst, 1)); return $"packHalf2x16(vec2({src0Expr}, {src1Expr}))"; } @@ -34,7 +34,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions { IAstNode src = operation.GetSource(0); - string srcExpr = GetSoureExpr(context, src, GetSrcVarType(operation.Inst, 0)); + string srcExpr = GetSourceExpr(context, src, GetSrcVarType(operation.Inst, 0)); return $"unpackDouble2x32({srcExpr}){GetMask(operation.Index)}"; } @@ -43,7 +43,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions { IAstNode src = operation.GetSource(0); - string srcExpr = GetSoureExpr(context, src, GetSrcVarType(operation.Inst, 0)); + string srcExpr = GetSourceExpr(context, src, GetSrcVarType(operation.Inst, 0)); return $"unpackHalf2x16({srcExpr}){GetMask(operation.Index)}"; } diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/Instructions/InstGenShuffle.cs b/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/Instructions/InstGenShuffle.cs index 6d3859efd..b72b94d90 100644 --- a/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/Instructions/InstGenShuffle.cs +++ b/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/Instructions/InstGenShuffle.cs @@ -9,8 +9,8 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions { public static string Shuffle(CodeGenContext context, AstOperation operation) { - string value = GetSoureExpr(context, operation.GetSource(0), AggregateType.FP32); - string index = GetSoureExpr(context, operation.GetSource(1), AggregateType.U32); + string value = GetSourceExpr(context, operation.GetSource(0), AggregateType.FP32); + string index = GetSourceExpr(context, operation.GetSource(1), AggregateType.U32); if (context.HostCapabilities.SupportsShaderBallot) { diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/Instructions/InstGenVector.cs b/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/Instructions/InstGenVector.cs index 70174a5ba..a300c7750 100644 --- a/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/Instructions/InstGenVector.cs +++ b/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/Instructions/InstGenVector.cs @@ -13,7 +13,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions IAstNode vector = operation.GetSource(0); IAstNode index = operation.GetSource(1); - string vectorExpr = GetSoureExpr(context, vector, OperandManager.GetNodeDestType(context, vector)); + string vectorExpr = GetSourceExpr(context, vector, OperandManager.GetNodeDestType(context, vector)); if (index is AstOperand indexOperand && indexOperand.Type == OperandType.Constant) { @@ -23,7 +23,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions } else { - string indexExpr = GetSoureExpr(context, index, GetSrcVarType(operation.Inst, 1)); + string indexExpr = GetSourceExpr(context, index, GetSrcVarType(operation.Inst, 1)); return $"{vectorExpr}[{indexExpr}]"; } diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/OperandManager.cs b/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/OperandManager.cs index 53ecc4531..a350b089c 100644 --- a/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/OperandManager.cs +++ b/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/OperandManager.cs @@ -146,9 +146,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl } else if (operation is AstTextureOperation texOp) { - if (texOp.Inst == Instruction.ImageLoad || - texOp.Inst == Instruction.ImageStore || - texOp.Inst == Instruction.ImageAtomic) + if (texOp.Inst.IsImage()) { return texOp.GetVectorType(texOp.Format.GetComponentType()); } diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/CodeGenContext.cs b/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/CodeGenContext.cs index 17c3eefe3..cc7977f84 100644 --- a/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/CodeGenContext.cs +++ b/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/CodeGenContext.cs @@ -33,9 +33,9 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv public Dictionary LocalMemories { get; } = new(); public Dictionary SharedMemories { get; } = new(); - public Dictionary SamplersTypes { get; } = new(); - public Dictionary Samplers { get; } = new(); - public Dictionary Images { get; } = new(); + public Dictionary SamplersTypes { get; } = new(); + public Dictionary Samplers { get; } = new(); + public Dictionary Images { get; } = new(); public Dictionary Inputs { get; } = new(); public Dictionary Outputs { get; } = new(); @@ -98,11 +98,6 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv Logger = parameters.Logger; TargetApi = parameters.TargetApi; - AddCapability(Capability.Shader); - AddCapability(Capability.Float64); - - SetMemoryModel(AddressingModel.Logical, MemoryModel.GLSL450); - Delegates = new SpirvDelegates(this); } diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/Declarations.cs b/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/Declarations.cs index 4ff61d9f2..55d35bf0d 100644 --- a/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/Declarations.cs +++ b/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/Declarations.cs @@ -160,31 +160,61 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv { int setIndex = context.TargetApi == TargetApi.Vulkan ? sampler.Set : 0; - var dim = (sampler.Type & SamplerType.Mask) switch + SpvInstruction imageType; + SpvInstruction sampledImageType; + + if (sampler.Type != SamplerType.None) { - SamplerType.Texture1D => Dim.Dim1D, - SamplerType.Texture2D => Dim.Dim2D, - SamplerType.Texture3D => Dim.Dim3D, - SamplerType.TextureCube => Dim.Cube, - SamplerType.TextureBuffer => Dim.Buffer, - _ => throw new InvalidOperationException($"Invalid sampler type \"{sampler.Type & SamplerType.Mask}\"."), - }; + var dim = (sampler.Type & SamplerType.Mask) switch + { + SamplerType.Texture1D => Dim.Dim1D, + SamplerType.Texture2D => Dim.Dim2D, + SamplerType.Texture3D => Dim.Dim3D, + SamplerType.TextureCube => Dim.Cube, + SamplerType.TextureBuffer => Dim.Buffer, + _ => throw new InvalidOperationException($"Invalid sampler type \"{sampler.Type & SamplerType.Mask}\"."), + }; - var imageType = context.TypeImage( - context.TypeFP32(), - dim, - sampler.Type.HasFlag(SamplerType.Shadow), - sampler.Type.HasFlag(SamplerType.Array), - sampler.Type.HasFlag(SamplerType.Multisample), - 1, - ImageFormat.Unknown); + imageType = context.TypeImage( + context.TypeFP32(), + dim, + sampler.Type.HasFlag(SamplerType.Shadow), + sampler.Type.HasFlag(SamplerType.Array), + sampler.Type.HasFlag(SamplerType.Multisample), + 1, + ImageFormat.Unknown); - var sampledImageType = context.TypeSampledImage(imageType); - var sampledImagePointerType = context.TypePointer(StorageClass.UniformConstant, sampledImageType); - var sampledImageVariable = context.Variable(sampledImagePointerType, StorageClass.UniformConstant); + sampledImageType = context.TypeSampledImage(imageType); + } + else + { + imageType = sampledImageType = context.TypeSampler(); + } - context.Samplers.Add(sampler.Binding, (imageType, sampledImageType, sampledImageVariable)); - context.SamplersTypes.Add(sampler.Binding, sampler.Type); + var sampledOrSeparateImageType = sampler.Separate ? imageType : sampledImageType; + var sampledImagePointerType = context.TypePointer(StorageClass.UniformConstant, sampledOrSeparateImageType); + var sampledImageArrayPointerType = sampledImagePointerType; + + if (sampler.ArrayLength == 0) + { + var sampledImageArrayType = context.TypeRuntimeArray(sampledOrSeparateImageType); + sampledImageArrayPointerType = context.TypePointer(StorageClass.UniformConstant, sampledImageArrayType); + } + else if (sampler.ArrayLength != 1) + { + var sampledImageArrayType = context.TypeArray(sampledOrSeparateImageType, context.Constant(context.TypeU32(), sampler.ArrayLength)); + sampledImageArrayPointerType = context.TypePointer(StorageClass.UniformConstant, sampledImageArrayType); + } + + var sampledImageVariable = context.Variable(sampledImageArrayPointerType, StorageClass.UniformConstant); + + context.Samplers.Add(new(sampler.Set, sampler.Binding), new SamplerDeclaration( + imageType, + sampledImageType, + sampledImagePointerType, + sampledImageVariable, + sampler.ArrayLength != 1)); + context.SamplersTypes.Add(new(sampler.Set, sampler.Binding), sampler.Type); context.Name(sampledImageVariable, sampler.Name); context.Decorate(sampledImageVariable, Decoration.DescriptorSet, (LiteralInteger)setIndex); @@ -211,9 +241,22 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv GetImageFormat(image.Format)); var imagePointerType = context.TypePointer(StorageClass.UniformConstant, imageType); - var imageVariable = context.Variable(imagePointerType, StorageClass.UniformConstant); + var imageArrayPointerType = imagePointerType; - context.Images.Add(image.Binding, (imageType, imageVariable)); + if (image.ArrayLength == 0) + { + var imageArrayType = context.TypeRuntimeArray(imageType); + imageArrayPointerType = context.TypePointer(StorageClass.UniformConstant, imageArrayType); + } + else if (image.ArrayLength != 1) + { + var imageArrayType = context.TypeArray(imageType, context.Constant(context.TypeU32(), image.ArrayLength)); + imageArrayPointerType = context.TypePointer(StorageClass.UniformConstant, imageArrayType); + } + + var imageVariable = context.Variable(imageArrayPointerType, StorageClass.UniformConstant); + + context.Images.Add(new(image.Set, image.Binding), new ImageDeclaration(imageType, imagePointerType, imageVariable, image.ArrayLength != 1)); context.Name(imageVariable, image.Name); context.Decorate(imageVariable, Decoration.DescriptorSet, (LiteralInteger)setIndex); @@ -356,6 +399,16 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv context.AddGlobalVariable(perVertexInputVariable); context.Inputs.Add(new IoDefinition(StorageKind.Input, IoVariable.Position), perVertexInputVariable); + + if (context.Definitions.Stage == ShaderStage.Geometry && + context.Definitions.GpPassthrough && + context.HostCapabilities.SupportsGeometryShaderPassthrough) + { + context.MemberDecorate(perVertexInputStructType, 0, Decoration.PassthroughNV); + context.MemberDecorate(perVertexInputStructType, 1, Decoration.PassthroughNV); + context.MemberDecorate(perVertexInputStructType, 2, Decoration.PassthroughNV); + context.MemberDecorate(perVertexInputStructType, 3, Decoration.PassthroughNV); + } } var perVertexOutputStructType = CreatePerVertexStructType(context); diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/ImageDeclaration.cs b/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/ImageDeclaration.cs new file mode 100644 index 000000000..1e0aee734 --- /dev/null +++ b/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/ImageDeclaration.cs @@ -0,0 +1,20 @@ +using Spv.Generator; + +namespace Ryujinx.Graphics.Shader.CodeGen.Spirv +{ + readonly struct ImageDeclaration + { + public readonly Instruction ImageType; + public readonly Instruction ImagePointerType; + public readonly Instruction Image; + public readonly bool IsIndexed; + + public ImageDeclaration(Instruction imageType, Instruction imagePointerType, Instruction image, bool isIndexed) + { + ImageType = imageType; + ImagePointerType = imagePointerType; + Image = image; + IsIndexed = isIndexed; + } + } +} diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/Instructions.cs b/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/Instructions.cs index 601753cb0..6206985d8 100644 --- a/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/Instructions.cs +++ b/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/Instructions.cs @@ -591,34 +591,28 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv { AstTextureOperation texOp = (AstTextureOperation)operation; - bool isBindless = (texOp.Flags & TextureFlags.Bindless) != 0; - var componentType = texOp.Format.GetComponentType(); - // TODO: Bindless texture support. For now we just return 0/do nothing. - if (isBindless) - { - return new OperationResult(componentType, componentType switch - { - AggregateType.S32 => context.Constant(context.TypeS32(), 0), - AggregateType.U32 => context.Constant(context.TypeU32(), 0u), - _ => context.Constant(context.TypeFP32(), 0f), - }); - } - bool isArray = (texOp.Type & SamplerType.Array) != 0; - bool isIndexed = (texOp.Type & SamplerType.Indexed) != 0; - int srcIndex = isBindless ? 1 : 0; + int srcIndex = 0; SpvInstruction Src(AggregateType type) { return context.Get(type, texOp.GetSource(srcIndex++)); } - if (isIndexed) + ImageDeclaration declaration = context.Images[texOp.GetTextureSetAndBinding()]; + SpvInstruction image = declaration.Image; + + SpvInstruction resultType = context.GetType(componentType); + SpvInstruction imagePointerType = context.TypePointer(StorageClass.Image, resultType); + + if (declaration.IsIndexed) { - Src(AggregateType.S32); + SpvInstruction textureIndex = Src(AggregateType.S32); + + image = context.AccessChain(imagePointerType, image, textureIndex); } int coordsCount = texOp.Type.GetDimensions(); @@ -646,14 +640,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv SpvInstruction value = Src(componentType); - (var imageType, var imageVariable) = context.Images[texOp.Binding]; - - context.Load(imageType, imageVariable); - - SpvInstruction resultType = context.GetType(componentType); - SpvInstruction imagePointerType = context.TypePointer(StorageClass.Image, resultType); - - var pointer = context.ImageTexelPointer(imagePointerType, imageVariable, pCoords, context.Constant(context.TypeU32(), 0)); + var pointer = context.ImageTexelPointer(imagePointerType, image, pCoords, context.Constant(context.TypeU32(), 0)); var one = context.Constant(context.TypeU32(), 1); var zero = context.Constant(context.TypeU32(), 0); @@ -683,31 +670,29 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv { AstTextureOperation texOp = (AstTextureOperation)operation; - bool isBindless = (texOp.Flags & TextureFlags.Bindless) != 0; - var componentType = texOp.Format.GetComponentType(); - // TODO: Bindless texture support. For now we just return 0/do nothing. - if (isBindless) - { - return GetZeroOperationResult(context, texOp, componentType, isVector: true); - } - bool isArray = (texOp.Type & SamplerType.Array) != 0; - bool isIndexed = (texOp.Type & SamplerType.Indexed) != 0; - int srcIndex = isBindless ? 1 : 0; + int srcIndex = 0; SpvInstruction Src(AggregateType type) { return context.Get(type, texOp.GetSource(srcIndex++)); } - if (isIndexed) + ImageDeclaration declaration = context.Images[texOp.GetTextureSetAndBinding()]; + SpvInstruction image = declaration.Image; + + if (declaration.IsIndexed) { - Src(AggregateType.S32); + SpvInstruction textureIndex = Src(AggregateType.S32); + + image = context.AccessChain(declaration.ImagePointerType, image, textureIndex); } + image = context.Load(declaration.ImageType, image); + int coordsCount = texOp.Type.GetDimensions(); int pCount = coordsCount + (isArray ? 1 : 0); @@ -731,9 +716,6 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv pCoords = Src(AggregateType.S32); } - (var imageType, var imageVariable) = context.Images[texOp.Binding]; - - var image = context.Load(imageType, imageVariable); var imageComponentType = context.GetType(componentType); var swizzledResultType = texOp.GetVectorType(componentType); @@ -747,29 +729,27 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv { AstTextureOperation texOp = (AstTextureOperation)operation; - bool isBindless = (texOp.Flags & TextureFlags.Bindless) != 0; - - // TODO: Bindless texture support. For now we just return 0/do nothing. - if (isBindless) - { - return OperationResult.Invalid; - } - bool isArray = (texOp.Type & SamplerType.Array) != 0; - bool isIndexed = (texOp.Type & SamplerType.Indexed) != 0; - int srcIndex = isBindless ? 1 : 0; + int srcIndex = 0; SpvInstruction Src(AggregateType type) { return context.Get(type, texOp.GetSource(srcIndex++)); } - if (isIndexed) + ImageDeclaration declaration = context.Images[texOp.GetTextureSetAndBinding()]; + SpvInstruction image = declaration.Image; + + if (declaration.IsIndexed) { - Src(AggregateType.S32); + SpvInstruction textureIndex = Src(AggregateType.S32); + + image = context.AccessChain(declaration.ImagePointerType, image, textureIndex); } + image = context.Load(declaration.ImageType, image); + int coordsCount = texOp.Type.GetDimensions(); int pCount = coordsCount + (isArray ? 1 : 0); @@ -818,10 +798,6 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv var texel = context.CompositeConstruct(context.TypeVector(context.GetType(componentType), ComponentsCount), cElems); - (var imageType, var imageVariable) = context.Images[texOp.Binding]; - - var image = context.Load(imageType, imageVariable); - context.ImageWrite(image, pCoords, texel, ImageOperandsMask.MaskNone); return OperationResult.Invalid; @@ -854,16 +830,6 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv { AstTextureOperation texOp = (AstTextureOperation)operation; - bool isBindless = (texOp.Flags & TextureFlags.Bindless) != 0; - - bool isIndexed = (texOp.Type & SamplerType.Indexed) != 0; - - // TODO: Bindless texture support. For now we just return 0. - if (isBindless) - { - return new OperationResult(AggregateType.S32, context.Constant(context.TypeS32(), 0)); - } - int srcIndex = 0; SpvInstruction Src(AggregateType type) @@ -871,10 +837,8 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv return context.Get(type, texOp.GetSource(srcIndex++)); } - if (isIndexed) - { - Src(AggregateType.S32); - } + SamplerDeclaration declaration = context.Samplers[texOp.GetTextureSetAndBinding()]; + SpvInstruction image = GenerateSampledImageLoad(context, texOp, declaration, ref srcIndex); int pCount = texOp.Type.GetDimensions(); @@ -897,10 +861,6 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv pCoords = Src(AggregateType.FP32); } - (_, var sampledImageType, var sampledImageVariable) = context.Samplers[texOp.Binding]; - - var image = context.Load(sampledImageType, sampledImageVariable); - var resultType = context.TypeVector(context.TypeFP32(), 2); var packed = context.ImageQueryLod(resultType, image, pCoords); var result = context.CompositeExtract(context.TypeFP32(), packed, (SpvLiteralInteger)texOp.Index); @@ -1182,7 +1142,6 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv { AstTextureOperation texOp = (AstTextureOperation)operation; - bool isBindless = (texOp.Flags & TextureFlags.Bindless) != 0; bool isGather = (texOp.Flags & TextureFlags.Gather) != 0; bool hasDerivatives = (texOp.Flags & TextureFlags.Derivatives) != 0; bool intCoords = (texOp.Flags & TextureFlags.IntCoords) != 0; @@ -1192,29 +1151,18 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv bool hasOffsets = (texOp.Flags & TextureFlags.Offsets) != 0; bool isArray = (texOp.Type & SamplerType.Array) != 0; - bool isIndexed = (texOp.Type & SamplerType.Indexed) != 0; bool isMultisample = (texOp.Type & SamplerType.Multisample) != 0; bool isShadow = (texOp.Type & SamplerType.Shadow) != 0; - bool colorIsVector = isGather || !isShadow; - - // TODO: Bindless texture support. For now we just return 0. - if (isBindless) - { - return GetZeroOperationResult(context, texOp, AggregateType.FP32, colorIsVector); - } - - int srcIndex = isBindless ? 1 : 0; + int srcIndex = 0; SpvInstruction Src(AggregateType type) { return context.Get(type, texOp.GetSource(srcIndex++)); } - if (isIndexed) - { - Src(AggregateType.S32); - } + SamplerDeclaration declaration = context.Samplers[texOp.GetTextureSetAndBinding()]; + SpvInstruction image = GenerateSampledImageLoad(context, texOp, declaration, ref srcIndex); int coordsCount = texOp.Type.GetDimensions(); @@ -1419,15 +1367,13 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv operandsList.Add(sample); } + bool colorIsVector = isGather || !isShadow; + var resultType = colorIsVector ? context.TypeVector(context.TypeFP32(), 4) : context.TypeFP32(); - (var imageType, var sampledImageType, var sampledImageVariable) = context.Samplers[texOp.Binding]; - - var image = context.Load(sampledImageType, sampledImageVariable); - if (intCoords) { - image = context.Image(imageType, image); + image = context.Image(declaration.ImageType, image); } var operands = operandsList.ToArray(); @@ -1485,25 +1431,12 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv { AstTextureOperation texOp = (AstTextureOperation)operation; - bool isBindless = (texOp.Flags & TextureFlags.Bindless) != 0; + int srcIndex = 0; - // TODO: Bindless texture support. For now we just return 0. - if (isBindless) - { - return new OperationResult(AggregateType.S32, context.Constant(context.TypeS32(), 0)); - } + SamplerDeclaration declaration = context.Samplers[texOp.GetTextureSetAndBinding()]; + SpvInstruction image = GenerateSampledImageLoad(context, texOp, declaration, ref srcIndex); - bool isIndexed = (texOp.Type & SamplerType.Indexed) != 0; - - if (isIndexed) - { - context.GetS32(texOp.GetSource(0)); - } - - (var imageType, var sampledImageType, var sampledImageVariable) = context.Samplers[texOp.Binding]; - - var image = context.Load(sampledImageType, sampledImageVariable); - image = context.Image(imageType, image); + image = context.Image(declaration.ImageType, image); SpvInstruction result = context.ImageQuerySamples(context.TypeS32(), image); @@ -1514,25 +1447,12 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv { AstTextureOperation texOp = (AstTextureOperation)operation; - bool isBindless = (texOp.Flags & TextureFlags.Bindless) != 0; + int srcIndex = 0; - // TODO: Bindless texture support. For now we just return 0. - if (isBindless) - { - return new OperationResult(AggregateType.S32, context.Constant(context.TypeS32(), 0)); - } + SamplerDeclaration declaration = context.Samplers[texOp.GetTextureSetAndBinding()]; + SpvInstruction image = GenerateSampledImageLoad(context, texOp, declaration, ref srcIndex); - bool isIndexed = (texOp.Type & SamplerType.Indexed) != 0; - - if (isIndexed) - { - context.GetS32(texOp.GetSource(0)); - } - - (var imageType, var sampledImageType, var sampledImageVariable) = context.Samplers[texOp.Binding]; - - var image = context.Load(sampledImageType, sampledImageVariable); - image = context.Image(imageType, image); + image = context.Image(declaration.ImageType, image); if (texOp.Index == 3) { @@ -1540,7 +1460,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv } else { - var type = context.SamplersTypes[texOp.Binding]; + var type = context.SamplersTypes[texOp.GetTextureSetAndBinding()]; bool hasLod = !type.HasFlag(SamplerType.Multisample) && type != SamplerType.TextureBuffer; int dimensions = (type & SamplerType.Mask) == SamplerType.TextureCube ? 2 : type.GetDimensions(); @@ -1556,8 +1476,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv if (hasLod) { - int lodSrcIndex = isBindless || isIndexed ? 1 : 0; - var lod = context.GetS32(operation.GetSource(lodSrcIndex)); + var lod = context.GetS32(operation.GetSource(srcIndex)); result = context.ImageQuerySizeLod(resultType, image, lod); } else @@ -1929,38 +1848,6 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv return context.Load(context.GetType(varType), context.Inputs[ioDefinition]); } - private static OperationResult GetZeroOperationResult( - CodeGenContext context, - AstTextureOperation texOp, - AggregateType scalarType, - bool isVector) - { - var zero = scalarType switch - { - AggregateType.S32 => context.Constant(context.TypeS32(), 0), - AggregateType.U32 => context.Constant(context.TypeU32(), 0u), - _ => context.Constant(context.TypeFP32(), 0f), - }; - - if (isVector) - { - AggregateType outputType = texOp.GetVectorType(scalarType); - - if ((outputType & AggregateType.ElementCountMask) != 0) - { - int componentsCount = BitOperations.PopCount((uint)texOp.Index); - - SpvInstruction[] values = new SpvInstruction[componentsCount]; - - values.AsSpan().Fill(zero); - - return new OperationResult(outputType, context.ConstantComposite(context.GetType(outputType), values)); - } - } - - return new OperationResult(scalarType, zero); - } - private static SpvInstruction GetSwizzledResult(CodeGenContext context, SpvInstruction vector, AggregateType swizzledResultType, int mask) { if ((swizzledResultType & AggregateType.ElementCountMask) != 0) @@ -1987,6 +1874,43 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv } } + private static SpvInstruction GenerateSampledImageLoad(CodeGenContext context, AstTextureOperation texOp, SamplerDeclaration declaration, ref int srcIndex) + { + SpvInstruction image = declaration.Image; + + if (declaration.IsIndexed) + { + SpvInstruction textureIndex = context.Get(AggregateType.S32, texOp.GetSource(srcIndex++)); + + image = context.AccessChain(declaration.SampledImagePointerType, image, textureIndex); + } + + if (texOp.IsSeparate) + { + image = context.Load(declaration.ImageType, image); + + SamplerDeclaration samplerDeclaration = context.Samplers[texOp.GetSamplerSetAndBinding()]; + + SpvInstruction sampler = samplerDeclaration.Image; + + if (samplerDeclaration.IsIndexed) + { + SpvInstruction samplerIndex = context.Get(AggregateType.S32, texOp.GetSource(srcIndex++)); + + sampler = context.AccessChain(samplerDeclaration.SampledImagePointerType, sampler, samplerIndex); + } + + sampler = context.Load(samplerDeclaration.ImageType, sampler); + image = context.SampledImage(declaration.SampledImageType, image, sampler); + } + else + { + image = context.Load(declaration.SampledImageType, image); + } + + return image; + } + private static OperationResult GenerateUnary( CodeGenContext context, AstOperation operation, diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/SamplerDeclaration.cs b/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/SamplerDeclaration.cs new file mode 100644 index 000000000..9e0ecd794 --- /dev/null +++ b/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/SamplerDeclaration.cs @@ -0,0 +1,27 @@ +using Spv.Generator; + +namespace Ryujinx.Graphics.Shader.CodeGen.Spirv +{ + readonly struct SamplerDeclaration + { + public readonly Instruction ImageType; + public readonly Instruction SampledImageType; + public readonly Instruction SampledImagePointerType; + public readonly Instruction Image; + public readonly bool IsIndexed; + + public SamplerDeclaration( + Instruction imageType, + Instruction sampledImageType, + Instruction sampledImagePointerType, + Instruction image, + bool isIndexed) + { + ImageType = imageType; + SampledImageType = sampledImageType; + SampledImagePointerType = sampledImagePointerType; + Image = image; + IsIndexed = isIndexed; + } + } +} diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/SpirvGenerator.cs b/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/SpirvGenerator.cs index ccfdc46d0..b259dde28 100644 --- a/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/SpirvGenerator.cs +++ b/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/SpirvGenerator.cs @@ -43,6 +43,10 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv CodeGenContext context = new(info, parameters, instPool, integerPool); + context.AddCapability(Capability.Shader); + + context.SetMemoryModel(AddressingModel.Logical, MemoryModel.GLSL450); + context.AddCapability(Capability.GroupNonUniformBallot); context.AddCapability(Capability.GroupNonUniformShuffle); context.AddCapability(Capability.GroupNonUniformVote); @@ -51,6 +55,11 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv context.AddCapability(Capability.ImageQuery); context.AddCapability(Capability.SampledBuffer); + if (parameters.HostCapabilities.SupportsShaderFloat64) + { + context.AddCapability(Capability.Float64); + } + if (parameters.Definitions.TransformFeedbackEnabled && parameters.Definitions.LastInVertexPipeline) { context.AddCapability(Capability.TransformFeedback); @@ -58,7 +67,8 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv if (parameters.Definitions.Stage == ShaderStage.Fragment) { - if (context.Info.IoDefinitions.Contains(new IoDefinition(StorageKind.Input, IoVariable.Layer))) + if (context.Info.IoDefinitions.Contains(new IoDefinition(StorageKind.Input, IoVariable.Layer)) || + context.Info.IoDefinitions.Contains(new IoDefinition(StorageKind.Input, IoVariable.PrimitiveId))) { context.AddCapability(Capability.Geometry); } diff --git a/src/Ryujinx.Graphics.Shader/GpuGraphicsState.cs b/src/Ryujinx.Graphics.Shader/GpuGraphicsState.cs index f16c71d55..38684002c 100644 --- a/src/Ryujinx.Graphics.Shader/GpuGraphicsState.cs +++ b/src/Ryujinx.Graphics.Shader/GpuGraphicsState.cs @@ -102,6 +102,11 @@ namespace Ryujinx.Graphics.Shader /// public readonly bool OriginUpperLeft; + /// + /// Indicates that the primitive ID values on the shader should be halved due to quad to triangles conversion. + /// + public readonly bool HalvePrimitiveId; + /// /// Creates a new GPU graphics state. /// @@ -124,6 +129,7 @@ namespace Ryujinx.Graphics.Shader /// Indicates whether dual source blend is enabled /// Indicates if negation of the viewport Y axis is enabled /// If true, indicates that the fragment origin is the upper left corner of the viewport, otherwise it is the lower left corner + /// Indicates that the primitive ID values on the shader should be halved due to quad to triangles conversion public GpuGraphicsState( bool earlyZForce, InputTopology topology, @@ -143,7 +149,8 @@ namespace Ryujinx.Graphics.Shader in Array8 fragmentOutputTypes, bool dualSourceBlendEnable, bool yNegateEnabled, - bool originUpperLeft) + bool originUpperLeft, + bool halvePrimitiveId) { EarlyZForce = earlyZForce; Topology = topology; @@ -164,6 +171,7 @@ namespace Ryujinx.Graphics.Shader DualSourceBlendEnable = dualSourceBlendEnable; YNegateEnabled = yNegateEnabled; OriginUpperLeft = originUpperLeft; + HalvePrimitiveId = halvePrimitiveId; } } } diff --git a/src/Ryujinx.Graphics.Shader/IGpuAccessor.cs b/src/Ryujinx.Graphics.Shader/IGpuAccessor.cs index df6d29dc5..4e6d6edf9 100644 --- a/src/Ryujinx.Graphics.Shader/IGpuAccessor.cs +++ b/src/Ryujinx.Graphics.Shader/IGpuAccessor.cs @@ -27,45 +27,42 @@ namespace Ryujinx.Graphics.Shader ReadOnlySpan GetCode(ulong address, int minimumSize); /// - /// Queries the binding number of a constant buffer. + /// Gets the binding number of a constant buffer. /// /// Constant buffer index /// Binding number - int QueryBindingConstantBuffer(int index) - { - return index + 1; - } + SetBindingPair CreateConstantBufferBinding(int index); /// - /// Queries the binding number of a storage buffer. + /// Gets the binding number of an image. + /// + /// For array of images, the number of elements of the array, otherwise it should be 1 + /// Indicates if the image is a buffer image + /// Binding number + SetBindingPair CreateImageBinding(int count, bool isBuffer); + + /// + /// Gets the binding number of a storage buffer. /// /// Storage buffer index /// Binding number - int QueryBindingStorageBuffer(int index) - { - return index; - } + SetBindingPair CreateStorageBufferBinding(int index); /// - /// Queries the binding number of a texture. + /// Gets the binding number of a texture. /// - /// Texture index + /// For array of textures, the number of elements of the array, otherwise it should be 1 /// Indicates if the texture is a buffer texture /// Binding number - int QueryBindingTexture(int index, bool isBuffer) - { - return index; - } + SetBindingPair CreateTextureBinding(int count, bool isBuffer); /// - /// Queries the binding number of an image. + /// Gets the set index for an additional set, or -1 if there's no extra set available. /// - /// Image index - /// Indicates if the image is a buffer image - /// Binding number - int QueryBindingImage(int index, bool isBuffer) + /// Extra set index, or -1 if not available + int CreateExtraSet() { - return index; + return -1; } /// @@ -147,6 +144,7 @@ namespace Ryujinx.Graphics.Shader default, false, false, + false, false); } @@ -303,6 +301,15 @@ namespace Ryujinx.Graphics.Shader return true; } + /// + /// Queries host API support for separate textures and samplers. + /// + /// True if the API supports samplers and textures to be combined on the shader, false otherwise + bool QueryHostSupportsSeparateSampler() + { + return true; + } + /// /// Queries host GPU shader ballot support. /// @@ -393,6 +400,12 @@ namespace Ryujinx.Graphics.Shader return true; } + /// + /// Gets the maximum number of samplers that the bound texture pool may have. + /// + /// Maximum amount of samplers that the pool may have + int QuerySamplerArrayLengthFromPool(); + /// /// Queries sampler type information. /// @@ -404,6 +417,19 @@ namespace Ryujinx.Graphics.Shader return SamplerType.Texture2D; } + /// + /// Gets the size in bytes of a bound constant buffer for the current shader stage. + /// + /// The number of the constant buffer to get the size from + /// Size in bytes + int QueryTextureArrayLengthFromBuffer(int slot); + + /// + /// Gets the maximum number of textures that the bound texture pool may have. + /// + /// Maximum amount of textures that the pool may have + int QueryTextureArrayLengthFromPool(); + /// /// Queries texture coordinate normalization information. /// diff --git a/src/Ryujinx.Graphics.Shader/Instructions/InstEmitAttribute.cs b/src/Ryujinx.Graphics.Shader/Instructions/InstEmitAttribute.cs index 63ce38e25..c704156bc 100644 --- a/src/Ryujinx.Graphics.Shader/Instructions/InstEmitAttribute.cs +++ b/src/Ryujinx.Graphics.Shader/Instructions/InstEmitAttribute.cs @@ -84,6 +84,10 @@ namespace Ryujinx.Graphics.Shader.Instructions value = context.IConvertU32ToFP32(value); } } + else if (offset == AttributeConsts.PrimitiveId && context.TranslatorContext.Definitions.HalvePrimitiveId) + { + value = context.ShiftRightS32(value, Const(1)); + } context.Copy(Register(rd), value); } @@ -187,6 +191,12 @@ namespace Ryujinx.Graphics.Shader.Instructions } } } + else if (op.Imm10 == AttributeConsts.PrimitiveId && context.TranslatorContext.Definitions.HalvePrimitiveId) + { + // If quads are used, but the host does not support them, they need to be converted to triangles. + // Since each quad becomes 2 triangles, we need to compensate here and divide primitive ID by 2. + res = context.ShiftRightS32(res, Const(1)); + } else if (op.Imm10 == AttributeConsts.FrontFacing && context.TranslatorContext.GpuAccessor.QueryHostHasFrontFacingBug()) { // gl_FrontFacing sometimes has incorrect (flipped) values depending how it is accessed on Intel GPUs. diff --git a/src/Ryujinx.Graphics.Shader/Instructions/InstEmitMemory.cs b/src/Ryujinx.Graphics.Shader/Instructions/InstEmitMemory.cs index 40129252a..3fcb821d3 100644 --- a/src/Ryujinx.Graphics.Shader/Instructions/InstEmitMemory.cs +++ b/src/Ryujinx.Graphics.Shader/Instructions/InstEmitMemory.cs @@ -222,30 +222,14 @@ namespace Ryujinx.Graphics.Shader.Instructions context.TranslatorContext.GpuAccessor.Log($"Invalid reduction type: {type}."); } break; - case AtomOp.And: - if (type == AtomSize.S32 || type == AtomSize.U32) + case AtomOp.Min: + if (type == AtomSize.S32) { - res = context.AtomicAnd(storageKind, e0, e1, value); + res = context.AtomicMinS32(storageKind, e0, e1, value); } - else + else if (type == AtomSize.U32) { - context.TranslatorContext.GpuAccessor.Log($"Invalid reduction type: {type}."); - } - break; - case AtomOp.Xor: - if (type == AtomSize.S32 || type == AtomSize.U32) - { - res = context.AtomicXor(storageKind, e0, e1, value); - } - else - { - context.TranslatorContext.GpuAccessor.Log($"Invalid reduction type: {type}."); - } - break; - case AtomOp.Or: - if (type == AtomSize.S32 || type == AtomSize.U32) - { - res = context.AtomicOr(storageKind, e0, e1, value); + res = context.AtomicMinU32(storageKind, e0, e1, value); } else { @@ -266,20 +250,49 @@ namespace Ryujinx.Graphics.Shader.Instructions context.TranslatorContext.GpuAccessor.Log($"Invalid reduction type: {type}."); } break; - case AtomOp.Min: - if (type == AtomSize.S32) + case AtomOp.And: + if (type == AtomSize.S32 || type == AtomSize.U32) { - res = context.AtomicMinS32(storageKind, e0, e1, value); - } - else if (type == AtomSize.U32) - { - res = context.AtomicMinU32(storageKind, e0, e1, value); + res = context.AtomicAnd(storageKind, e0, e1, value); } else { context.TranslatorContext.GpuAccessor.Log($"Invalid reduction type: {type}."); } break; + case AtomOp.Or: + if (type == AtomSize.S32 || type == AtomSize.U32) + { + res = context.AtomicOr(storageKind, e0, e1, value); + } + else + { + context.TranslatorContext.GpuAccessor.Log($"Invalid reduction type: {type}."); + } + break; + case AtomOp.Xor: + if (type == AtomSize.S32 || type == AtomSize.U32) + { + res = context.AtomicXor(storageKind, e0, e1, value); + } + else + { + context.TranslatorContext.GpuAccessor.Log($"Invalid reduction type: {type}."); + } + break; + case AtomOp.Exch: + if (type == AtomSize.S32 || type == AtomSize.U32) + { + res = context.AtomicSwap(storageKind, e0, e1, value); + } + else + { + context.TranslatorContext.GpuAccessor.Log($"Invalid reduction type: {type}."); + } + break; + default: + context.TranslatorContext.GpuAccessor.Log($"Invalid atomic operation: {op}."); + break; } return res; diff --git a/src/Ryujinx.Graphics.Shader/Instructions/InstEmitPredicate.cs b/src/Ryujinx.Graphics.Shader/Instructions/InstEmitPredicate.cs index 630162ade..1d8651254 100644 --- a/src/Ryujinx.Graphics.Shader/Instructions/InstEmitPredicate.cs +++ b/src/Ryujinx.Graphics.Shader/Instructions/InstEmitPredicate.cs @@ -24,7 +24,7 @@ namespace Ryujinx.Graphics.Shader.Instructions if (op.BVal) { - context.Copy(dest, context.ConditionalSelect(res, ConstF(1), Const(0))); + context.Copy(dest, context.ConditionalSelect(res, ConstF(1), ConstF(0))); } else { diff --git a/src/Ryujinx.Graphics.Shader/Instructions/InstEmitSurface.cs b/src/Ryujinx.Graphics.Shader/Instructions/InstEmitSurface.cs index 0aac0ffa8..383e82c69 100644 --- a/src/Ryujinx.Graphics.Shader/Instructions/InstEmitSurface.cs +++ b/src/Ryujinx.Graphics.Shader/Instructions/InstEmitSurface.cs @@ -278,7 +278,7 @@ namespace Ryujinx.Graphics.Shader.Instructions flags |= TextureFlags.Bindless; } - int binding = isBindless ? 0 : context.ResourceManager.GetTextureOrImageBinding( + SetBindingPair setAndBinding = isBindless ? default : context.ResourceManager.GetTextureOrImageBinding( Instruction.ImageAtomic, type, format, @@ -286,7 +286,7 @@ namespace Ryujinx.Graphics.Shader.Instructions TextureOperation.DefaultCbufSlot, imm); - Operand res = context.ImageAtomic(type, format, flags, binding, sources); + Operand res = context.ImageAtomic(type, format, flags, setAndBinding, sources); context.Copy(d, res); } @@ -389,7 +389,7 @@ namespace Ryujinx.Graphics.Shader.Instructions TextureFormat format = isBindless ? TextureFormat.Unknown : ShaderProperties.GetTextureFormat(context.TranslatorContext.GpuAccessor, handle); - int binding = isBindless ? 0 : context.ResourceManager.GetTextureOrImageBinding( + SetBindingPair setAndBinding = isBindless ? default : context.ResourceManager.GetTextureOrImageBinding( Instruction.ImageLoad, type, format, @@ -397,7 +397,7 @@ namespace Ryujinx.Graphics.Shader.Instructions TextureOperation.DefaultCbufSlot, handle); - context.ImageLoad(type, format, flags, binding, (int)componentMask, dests, sources); + context.ImageLoad(type, format, flags, setAndBinding, (int)componentMask, dests, sources); } else { @@ -432,7 +432,7 @@ namespace Ryujinx.Graphics.Shader.Instructions TextureFormat format = GetTextureFormat(size); - int binding = isBindless ? 0 : context.ResourceManager.GetTextureOrImageBinding( + SetBindingPair setAndBinding = isBindless ? default : context.ResourceManager.GetTextureOrImageBinding( Instruction.ImageLoad, type, format, @@ -440,7 +440,7 @@ namespace Ryujinx.Graphics.Shader.Instructions TextureOperation.DefaultCbufSlot, handle); - context.ImageLoad(type, format, flags, binding, compMask, dests, sources); + context.ImageLoad(type, format, flags, setAndBinding, compMask, dests, sources); switch (size) { @@ -552,7 +552,7 @@ namespace Ryujinx.Graphics.Shader.Instructions flags |= TextureFlags.Bindless; } - int binding = isBindless ? 0 : context.ResourceManager.GetTextureOrImageBinding( + SetBindingPair setAndBinding = isBindless ? default : context.ResourceManager.GetTextureOrImageBinding( Instruction.ImageAtomic, type, format, @@ -560,7 +560,7 @@ namespace Ryujinx.Graphics.Shader.Instructions TextureOperation.DefaultCbufSlot, imm); - context.ImageAtomic(type, format, flags, binding, sources); + context.ImageAtomic(type, format, flags, setAndBinding, sources); } private static void EmitSust( @@ -679,7 +679,7 @@ namespace Ryujinx.Graphics.Shader.Instructions flags |= TextureFlags.Coherent; } - int binding = isBindless ? 0 : context.ResourceManager.GetTextureOrImageBinding( + SetBindingPair setAndBinding = isBindless ? default : context.ResourceManager.GetTextureOrImageBinding( Instruction.ImageStore, type, format, @@ -687,7 +687,7 @@ namespace Ryujinx.Graphics.Shader.Instructions TextureOperation.DefaultCbufSlot, handle); - context.ImageStore(type, format, flags, binding, sources); + context.ImageStore(type, format, flags, setAndBinding, sources); } private static int GetComponentSizeInBytesLog2(SuatomSize size) diff --git a/src/Ryujinx.Graphics.Shader/Instructions/InstEmitTexture.cs b/src/Ryujinx.Graphics.Shader/Instructions/InstEmitTexture.cs index 55f7d5778..2076262da 100644 --- a/src/Ryujinx.Graphics.Shader/Instructions/InstEmitTexture.cs +++ b/src/Ryujinx.Graphics.Shader/Instructions/InstEmitTexture.cs @@ -578,12 +578,7 @@ namespace Ryujinx.Graphics.Shader.Instructions type = SamplerType.Texture2D; flags = TextureFlags.Gather; - if (tld4sOp.Dc) - { - sourcesList.Add(Rb()); - - type |= SamplerType.Shadow; - } + int depthCompareIndex = sourcesList.Count; if (tld4sOp.Aoffi) { @@ -592,7 +587,16 @@ namespace Ryujinx.Graphics.Shader.Instructions flags |= TextureFlags.Offset; } - sourcesList.Add(Const((int)tld4sOp.TexComp)); + if (tld4sOp.Dc) + { + sourcesList.Insert(depthCompareIndex, Rb()); + + type |= SamplerType.Shadow; + } + else + { + sourcesList.Add(Const((int)tld4sOp.TexComp)); + } } else { @@ -881,7 +885,7 @@ namespace Ryujinx.Graphics.Shader.Instructions return Register(dest++, RegisterType.Gpr); } - int binding = isBindless ? 0 : context.ResourceManager.GetTextureOrImageBinding( + SetBindingPair setAndBinding = isBindless ? default : context.ResourceManager.GetTextureOrImageBinding( Instruction.Lod, type, TextureFormat.Unknown, @@ -909,7 +913,7 @@ namespace Ryujinx.Graphics.Shader.Instructions else { // The instruction component order is the inverse of GLSL's. - Operand res = context.Lod(type, flags, binding, compIndex ^ 1, sources); + Operand res = context.Lod(type, flags, setAndBinding, compIndex ^ 1, sources); res = context.FPMultiply(res, ConstF(256.0f)); @@ -1112,12 +1116,12 @@ namespace Ryujinx.Graphics.Shader.Instructions } TextureFlags flags = isBindless ? TextureFlags.Bindless : TextureFlags.None; - int binding; + SetBindingPair setAndBinding; switch (query) { case TexQuery.TexHeaderDimension: - binding = isBindless ? 0 : context.ResourceManager.GetTextureOrImageBinding( + setAndBinding = isBindless ? default : context.ResourceManager.GetTextureOrImageBinding( Instruction.TextureQuerySize, type, TextureFormat.Unknown, @@ -1136,13 +1140,13 @@ namespace Ryujinx.Graphics.Shader.Instructions break; } - context.Copy(d, context.TextureQuerySize(type, flags, binding, compIndex, sources)); + context.Copy(d, context.TextureQuerySize(type, flags, setAndBinding, compIndex, sources)); } } break; case TexQuery.TexHeaderTextureType: - binding = isBindless ? 0 : context.ResourceManager.GetTextureOrImageBinding( + setAndBinding = isBindless ? default : context.ResourceManager.GetTextureOrImageBinding( Instruction.TextureQuerySamples, type, TextureFormat.Unknown, @@ -1167,7 +1171,7 @@ namespace Ryujinx.Graphics.Shader.Instructions if (d != null) { - context.Copy(d, context.TextureQuerySamples(type, flags, binding, sources)); + context.Copy(d, context.TextureQuerySamples(type, flags, setAndBinding, sources)); } } break; @@ -1187,7 +1191,7 @@ namespace Ryujinx.Graphics.Shader.Instructions Operand[] dests, Operand[] sources) { - int binding = flags.HasFlag(TextureFlags.Bindless) ? 0 : context.ResourceManager.GetTextureOrImageBinding( + SetBindingPair setAndBinding = flags.HasFlag(TextureFlags.Bindless) ? default : context.ResourceManager.GetTextureOrImageBinding( Instruction.TextureSample, type, TextureFormat.Unknown, @@ -1195,7 +1199,7 @@ namespace Ryujinx.Graphics.Shader.Instructions TextureOperation.DefaultCbufSlot, handle); - context.TextureSample(type, flags, binding, componentMask, dests, sources); + context.TextureSample(type, flags, setAndBinding, componentMask, dests, sources); } private static SamplerType ConvertSamplerType(TexDim dimensions) diff --git a/src/Ryujinx.Graphics.Shader/IntermediateRepresentation/Instruction.cs b/src/Ryujinx.Graphics.Shader/IntermediateRepresentation/Instruction.cs index e5695ebc2..273a38a5b 100644 --- a/src/Ryujinx.Graphics.Shader/IntermediateRepresentation/Instruction.cs +++ b/src/Ryujinx.Graphics.Shader/IntermediateRepresentation/Instruction.cs @@ -156,10 +156,42 @@ namespace Ryujinx.Graphics.Shader.IntermediateRepresentation return false; } + public static bool IsComparison(this Instruction inst) + { + switch (inst & Instruction.Mask) + { + case Instruction.CompareEqual: + case Instruction.CompareGreater: + case Instruction.CompareGreaterOrEqual: + case Instruction.CompareGreaterOrEqualU32: + case Instruction.CompareGreaterU32: + case Instruction.CompareLess: + case Instruction.CompareLessOrEqual: + case Instruction.CompareLessOrEqualU32: + case Instruction.CompareLessU32: + case Instruction.CompareNotEqual: + return true; + } + + return false; + } + public static bool IsTextureQuery(this Instruction inst) { inst &= Instruction.Mask; return inst == Instruction.Lod || inst == Instruction.TextureQuerySamples || inst == Instruction.TextureQuerySize; } + + public static bool IsImage(this Instruction inst) + { + inst &= Instruction.Mask; + return inst == Instruction.ImageAtomic || inst == Instruction.ImageLoad || inst == Instruction.ImageStore; + } + + public static bool IsImageStore(this Instruction inst) + { + inst &= Instruction.Mask; + return inst == Instruction.ImageAtomic || inst == Instruction.ImageStore; + } } } diff --git a/src/Ryujinx.Graphics.Shader/IntermediateRepresentation/Operation.cs b/src/Ryujinx.Graphics.Shader/IntermediateRepresentation/Operation.cs index f5396a884..713e8a4fb 100644 --- a/src/Ryujinx.Graphics.Shader/IntermediateRepresentation/Operation.cs +++ b/src/Ryujinx.Graphics.Shader/IntermediateRepresentation/Operation.cs @@ -20,13 +20,13 @@ namespace Ryujinx.Graphics.Shader.IntermediateRepresentation } set { - if (value != null && value.Type == OperandType.LocalVariable) - { - value.AsgOp = this; - } - if (value != null) { + if (value.Type == OperandType.LocalVariable) + { + value.AsgOp = this; + } + _dests = new[] { value }; } else @@ -216,6 +216,11 @@ namespace Ryujinx.Graphics.Shader.IntermediateRepresentation newSources[index] = source; + if (source != null && source.Type == OperandType.LocalVariable) + { + source.UseOps.Add(this); + } + _sources = newSources; } diff --git a/src/Ryujinx.Graphics.Shader/IntermediateRepresentation/TextureOperation.cs b/src/Ryujinx.Graphics.Shader/IntermediateRepresentation/TextureOperation.cs index fa5550a64..7eee8f2e9 100644 --- a/src/Ryujinx.Graphics.Shader/IntermediateRepresentation/TextureOperation.cs +++ b/src/Ryujinx.Graphics.Shader/IntermediateRepresentation/TextureOperation.cs @@ -8,13 +8,17 @@ namespace Ryujinx.Graphics.Shader.IntermediateRepresentation public TextureFormat Format { get; set; } public TextureFlags Flags { get; private set; } + public int Set { get; private set; } public int Binding { get; private set; } + public int SamplerSet { get; private set; } + public int SamplerBinding { get; private set; } public TextureOperation( Instruction inst, SamplerType type, TextureFormat format, TextureFlags flags, + int set, int binding, int compIndex, Operand[] dests, @@ -23,17 +27,28 @@ namespace Ryujinx.Graphics.Shader.IntermediateRepresentation Type = type; Format = format; Flags = flags; + Set = set; Binding = binding; + SamplerSet = -1; + SamplerBinding = -1; } - public void TurnIntoIndexed(int binding) + public void TurnIntoArray(SetBindingPair setAndBinding) { - Type |= SamplerType.Indexed; Flags &= ~TextureFlags.Bindless; - Binding = binding; + Set = setAndBinding.SetIndex; + Binding = setAndBinding.Binding; } - public void SetBinding(int binding) + public void TurnIntoArray(SetBindingPair textureSetAndBinding, SetBindingPair samplerSetAndBinding) + { + TurnIntoArray(textureSetAndBinding); + + SamplerSet = samplerSetAndBinding.SetIndex; + SamplerBinding = samplerSetAndBinding.Binding; + } + + public void SetBinding(SetBindingPair setAndBinding) { if ((Flags & TextureFlags.Bindless) != 0) { @@ -42,7 +57,8 @@ namespace Ryujinx.Graphics.Shader.IntermediateRepresentation RemoveSource(0); } - Binding = binding; + Set = setAndBinding.SetIndex; + Binding = setAndBinding.Binding; } public void SetLodLevelFlag() diff --git a/src/Ryujinx.Graphics.Shader/SamplerType.cs b/src/Ryujinx.Graphics.Shader/SamplerType.cs index 85e97368f..a693495fa 100644 --- a/src/Ryujinx.Graphics.Shader/SamplerType.cs +++ b/src/Ryujinx.Graphics.Shader/SamplerType.cs @@ -16,9 +16,8 @@ namespace Ryujinx.Graphics.Shader Mask = 0xff, Array = 1 << 8, - Indexed = 1 << 9, - Multisample = 1 << 10, - Shadow = 1 << 11, + Multisample = 1 << 9, + Shadow = 1 << 10, } static class SamplerTypeExtensions @@ -36,10 +35,41 @@ namespace Ryujinx.Graphics.Shader }; } + public static string ToShortSamplerType(this SamplerType type) + { + string typeName = (type & SamplerType.Mask) switch + { + SamplerType.Texture1D => "1d", + SamplerType.TextureBuffer => "b", + SamplerType.Texture2D => "2d", + SamplerType.Texture3D => "3d", + SamplerType.TextureCube => "cube", + _ => throw new ArgumentException($"Invalid sampler type \"{type}\"."), + }; + + if ((type & SamplerType.Multisample) != 0) + { + typeName += "ms"; + } + + if ((type & SamplerType.Array) != 0) + { + typeName += "a"; + } + + if ((type & SamplerType.Shadow) != 0) + { + typeName += "s"; + } + + return typeName; + } + public static string ToGlslSamplerType(this SamplerType type) { string typeName = (type & SamplerType.Mask) switch { + SamplerType.None => "sampler", SamplerType.Texture1D => "sampler1D", SamplerType.TextureBuffer => "samplerBuffer", SamplerType.Texture2D => "sampler2D", @@ -66,6 +96,31 @@ namespace Ryujinx.Graphics.Shader return typeName; } + public static string ToGlslTextureType(this SamplerType type) + { + string typeName = (type & SamplerType.Mask) switch + { + SamplerType.Texture1D => "texture1D", + SamplerType.TextureBuffer => "textureBuffer", + SamplerType.Texture2D => "texture2D", + SamplerType.Texture3D => "texture3D", + SamplerType.TextureCube => "textureCube", + _ => throw new ArgumentException($"Invalid texture type \"{type}\"."), + }; + + if ((type & SamplerType.Multisample) != 0) + { + typeName += "MS"; + } + + if ((type & SamplerType.Array) != 0) + { + typeName += "Array"; + } + + return typeName; + } + public static string ToGlslImageType(this SamplerType type, AggregateType componentType) { string typeName = (type & SamplerType.Mask) switch diff --git a/src/Ryujinx.Graphics.Shader/SetBindingPair.cs b/src/Ryujinx.Graphics.Shader/SetBindingPair.cs new file mode 100644 index 000000000..1e8a4f9c6 --- /dev/null +++ b/src/Ryujinx.Graphics.Shader/SetBindingPair.cs @@ -0,0 +1,4 @@ +namespace Ryujinx.Graphics.Shader +{ + public readonly record struct SetBindingPair(int SetIndex, int Binding); +} diff --git a/src/Ryujinx.Graphics.Shader/StructuredIr/AstTextureOperation.cs b/src/Ryujinx.Graphics.Shader/StructuredIr/AstTextureOperation.cs index 3970df1e9..867cae853 100644 --- a/src/Ryujinx.Graphics.Shader/StructuredIr/AstTextureOperation.cs +++ b/src/Ryujinx.Graphics.Shader/StructuredIr/AstTextureOperation.cs @@ -8,21 +8,42 @@ namespace Ryujinx.Graphics.Shader.StructuredIr public TextureFormat Format { get; } public TextureFlags Flags { get; } + public int Set { get; } public int Binding { get; } + public int SamplerSet { get; } + public int SamplerBinding { get; } + + public bool IsSeparate => SamplerBinding >= 0; public AstTextureOperation( Instruction inst, SamplerType type, TextureFormat format, TextureFlags flags, + int set, int binding, + int samplerSet, + int samplerBinding, int index, params IAstNode[] sources) : base(inst, StorageKind.None, false, index, sources, sources.Length) { Type = type; Format = format; Flags = flags; + Set = set; Binding = binding; + SamplerSet = samplerSet; + SamplerBinding = samplerBinding; + } + + public SetBindingPair GetTextureSetAndBinding() + { + return new SetBindingPair(Set, Binding); + } + + public SetBindingPair GetSamplerSetAndBinding() + { + return new SetBindingPair(SamplerSet, SamplerBinding); } } } diff --git a/src/Ryujinx.Graphics.Shader/StructuredIr/PhiFunctions.cs b/src/Ryujinx.Graphics.Shader/StructuredIr/PhiFunctions.cs index 8b1cb9c56..90f1f2f6d 100644 --- a/src/Ryujinx.Graphics.Shader/StructuredIr/PhiFunctions.cs +++ b/src/Ryujinx.Graphics.Shader/StructuredIr/PhiFunctions.cs @@ -24,17 +24,21 @@ namespace Ryujinx.Graphics.Shader.StructuredIr continue; } + Operand temp = OperandHelper.Local(); + for (int index = 0; index < phi.SourcesCount; index++) { Operand src = phi.GetSource(index); - BasicBlock srcBlock = phi.GetBlock(index); - Operation copyOp = new(Instruction.Copy, phi.Dest, src); + Operation copyOp = new(Instruction.Copy, temp, src); srcBlock.Append(copyOp); } + Operation copyOp2 = new(Instruction.Copy, phi.Dest, temp); + + nextNode = block.Operations.AddAfter(node, copyOp2).Next; block.Operations.Remove(node); node = nextNode; diff --git a/src/Ryujinx.Graphics.Shader/StructuredIr/ShaderProperties.cs b/src/Ryujinx.Graphics.Shader/StructuredIr/ShaderProperties.cs index 8c12c2aaf..53ed6bfcc 100644 --- a/src/Ryujinx.Graphics.Shader/StructuredIr/ShaderProperties.cs +++ b/src/Ryujinx.Graphics.Shader/StructuredIr/ShaderProperties.cs @@ -6,15 +6,15 @@ namespace Ryujinx.Graphics.Shader.StructuredIr { private readonly Dictionary _constantBuffers; private readonly Dictionary _storageBuffers; - private readonly Dictionary _textures; - private readonly Dictionary _images; + private readonly Dictionary _textures; + private readonly Dictionary _images; private readonly Dictionary _localMemories; private readonly Dictionary _sharedMemories; public IReadOnlyDictionary ConstantBuffers => _constantBuffers; public IReadOnlyDictionary StorageBuffers => _storageBuffers; - public IReadOnlyDictionary Textures => _textures; - public IReadOnlyDictionary Images => _images; + public IReadOnlyDictionary Textures => _textures; + public IReadOnlyDictionary Images => _images; public IReadOnlyDictionary LocalMemories => _localMemories; public IReadOnlyDictionary SharedMemories => _sharedMemories; @@ -22,8 +22,8 @@ namespace Ryujinx.Graphics.Shader.StructuredIr { _constantBuffers = new Dictionary(); _storageBuffers = new Dictionary(); - _textures = new Dictionary(); - _images = new Dictionary(); + _textures = new Dictionary(); + _images = new Dictionary(); _localMemories = new Dictionary(); _sharedMemories = new Dictionary(); } @@ -40,12 +40,12 @@ namespace Ryujinx.Graphics.Shader.StructuredIr public void AddOrUpdateTexture(TextureDefinition definition) { - _textures[definition.Binding] = definition; + _textures[new(definition.Set, definition.Binding)] = definition; } public void AddOrUpdateImage(TextureDefinition definition) { - _images[definition.Binding] = definition; + _images[new(definition.Set, definition.Binding)] = definition; } public int AddLocalMemory(MemoryDefinition definition) diff --git a/src/Ryujinx.Graphics.Shader/StructuredIr/StructuredProgram.cs b/src/Ryujinx.Graphics.Shader/StructuredIr/StructuredProgram.cs index 2e2df7546..88053658d 100644 --- a/src/Ryujinx.Graphics.Shader/StructuredIr/StructuredProgram.cs +++ b/src/Ryujinx.Graphics.Shader/StructuredIr/StructuredProgram.cs @@ -169,7 +169,17 @@ namespace Ryujinx.Graphics.Shader.StructuredIr AstTextureOperation GetAstTextureOperation(TextureOperation texOp) { - return new AstTextureOperation(inst, texOp.Type, texOp.Format, texOp.Flags, texOp.Binding, texOp.Index, sources); + return new AstTextureOperation( + inst, + texOp.Type, + texOp.Format, + texOp.Flags, + texOp.Set, + texOp.Binding, + texOp.SamplerSet, + texOp.SamplerBinding, + texOp.Index, + sources); } int componentsCount = BitOperations.PopCount((uint)operation.Index); diff --git a/src/Ryujinx.Graphics.Shader/StructuredIr/TextureDefinition.cs b/src/Ryujinx.Graphics.Shader/StructuredIr/TextureDefinition.cs index e45c82854..1021dff0e 100644 --- a/src/Ryujinx.Graphics.Shader/StructuredIr/TextureDefinition.cs +++ b/src/Ryujinx.Graphics.Shader/StructuredIr/TextureDefinition.cs @@ -4,24 +4,40 @@ namespace Ryujinx.Graphics.Shader { public int Set { get; } public int Binding { get; } + public int ArrayLength { get; } + public bool Separate { get; } public string Name { get; } public SamplerType Type { get; } public TextureFormat Format { get; } public TextureUsageFlags Flags { get; } - public TextureDefinition(int set, int binding, string name, SamplerType type, TextureFormat format, TextureUsageFlags flags) + public TextureDefinition( + int set, + int binding, + int arrayLength, + bool separate, + string name, + SamplerType type, + TextureFormat format, + TextureUsageFlags flags) { Set = set; Binding = binding; + ArrayLength = arrayLength; + Separate = separate; Name = name; Type = type; Format = format; Flags = flags; } + public TextureDefinition(int set, int binding, string name, SamplerType type) : this(set, binding, 1, false, name, type, TextureFormat.Unknown, TextureUsageFlags.None) + { + } + public TextureDefinition SetFlag(TextureUsageFlags flag) { - return new TextureDefinition(Set, Binding, Name, Type, Format, Flags | flag); + return new TextureDefinition(Set, Binding, ArrayLength, Separate, Name, Type, Format, Flags | flag); } } } diff --git a/src/Ryujinx.Graphics.Shader/TextureDescriptor.cs b/src/Ryujinx.Graphics.Shader/TextureDescriptor.cs index 1130b63b8..1e387407d 100644 --- a/src/Ryujinx.Graphics.Shader/TextureDescriptor.cs +++ b/src/Ryujinx.Graphics.Shader/TextureDescriptor.cs @@ -4,6 +4,7 @@ namespace Ryujinx.Graphics.Shader { // New fields should be added to the end of the struct to keep disk shader cache compatibility. + public readonly int Set; public readonly int Binding; public readonly SamplerType Type; @@ -11,16 +12,31 @@ namespace Ryujinx.Graphics.Shader public readonly int CbufSlot; public readonly int HandleIndex; + public readonly int ArrayLength; + + public readonly bool Separate; public readonly TextureUsageFlags Flags; - public TextureDescriptor(int binding, SamplerType type, TextureFormat format, int cbufSlot, int handleIndex, TextureUsageFlags flags) + public TextureDescriptor( + int set, + int binding, + SamplerType type, + TextureFormat format, + int cbufSlot, + int handleIndex, + int arrayLength, + bool separate, + TextureUsageFlags flags) { + Set = set; Binding = binding; Type = type; Format = format; CbufSlot = cbufSlot; HandleIndex = handleIndex; + ArrayLength = arrayLength; + Separate = separate; Flags = flags; } } diff --git a/src/Ryujinx.Graphics.Shader/TextureHandle.cs b/src/Ryujinx.Graphics.Shader/TextureHandle.cs index fc9ab2d67..3aaceac48 100644 --- a/src/Ryujinx.Graphics.Shader/TextureHandle.cs +++ b/src/Ryujinx.Graphics.Shader/TextureHandle.cs @@ -9,6 +9,7 @@ namespace Ryujinx.Graphics.Shader SeparateSamplerHandle = 1, SeparateSamplerId = 2, SeparateConstantSamplerHandle = 3, + Direct = 4, } public static class TextureHandle @@ -88,7 +89,7 @@ namespace Ryujinx.Graphics.Shader { (int textureWordOffset, int samplerWordOffset, TextureHandleType handleType) = UnpackOffsets(wordOffset); - int handle = cachedTextureBuffer.Length != 0 ? cachedTextureBuffer[textureWordOffset] : 0; + int handle = textureWordOffset < cachedTextureBuffer.Length ? cachedTextureBuffer[textureWordOffset] : 0; // The "wordOffset" (which is really the immediate value used on texture instructions on the shader) // is a 13-bit value. However, in order to also support separate samplers and textures (which uses @@ -102,7 +103,7 @@ namespace Ryujinx.Graphics.Shader if (handleType != TextureHandleType.SeparateConstantSamplerHandle) { - samplerHandle = cachedSamplerBuffer.Length != 0 ? cachedSamplerBuffer[samplerWordOffset] : 0; + samplerHandle = samplerWordOffset < cachedSamplerBuffer.Length ? cachedSamplerBuffer[samplerWordOffset] : 0; } else { diff --git a/src/Ryujinx.Graphics.Shader/Translation/EmitterContext.cs b/src/Ryujinx.Graphics.Shader/Translation/EmitterContext.cs index f1dffb351..5e07b39f1 100644 --- a/src/Ryujinx.Graphics.Shader/Translation/EmitterContext.cs +++ b/src/Ryujinx.Graphics.Shader/Translation/EmitterContext.cs @@ -80,9 +80,10 @@ namespace Ryujinx.Graphics.Shader.Translation return; } - if (TranslatorContext.Definitions.Stage == ShaderStage.Vertex && TranslatorContext.Options.TargetApi == TargetApi.Vulkan) + // Vulkan requires the point size to be always written on the shader if the primitive topology is points. + // OpenGL requires the point size to be always written on the shader if PROGRAM_POINT_SIZE is set. + if (TranslatorContext.Definitions.Stage == ShaderStage.Vertex) { - // Vulkan requires the point size to be always written on the shader if the primitive topology is points. this.Store(StorageKind.Output, IoVariable.PointSize, null, ConstF(TranslatorContext.Definitions.PointSize)); } @@ -123,7 +124,7 @@ namespace Ryujinx.Graphics.Shader.Translation this.TextureSample( SamplerType.TextureBuffer, TextureFlags.IntCoords, - ResourceManager.Reservations.IndexBufferTextureBinding, + ResourceManager.Reservations.GetIndexBufferTextureSetAndBinding(), 1, new[] { vertexIndexVr }, new[] { this.IAdd(ibBaseOffset, outputVertexOffset) }); @@ -144,7 +145,7 @@ namespace Ryujinx.Graphics.Shader.Translation this.TextureSample( SamplerType.TextureBuffer, TextureFlags.IntCoords, - ResourceManager.Reservations.TopologyRemapBufferTextureBinding, + ResourceManager.Reservations.GetTopologyRemapBufferTextureSetAndBinding(), 1, new[] { vertexIndex }, new[] { this.IAdd(baseVertex, Const(index)) }); diff --git a/src/Ryujinx.Graphics.Shader/Translation/EmitterContextInsts.cs b/src/Ryujinx.Graphics.Shader/Translation/EmitterContextInsts.cs index 9e314c620..5bdbb0025 100644 --- a/src/Ryujinx.Graphics.Shader/Translation/EmitterContextInsts.cs +++ b/src/Ryujinx.Graphics.Shader/Translation/EmitterContextInsts.cs @@ -618,12 +618,21 @@ namespace Ryujinx.Graphics.Shader.Translation SamplerType type, TextureFormat format, TextureFlags flags, - int binding, + SetBindingPair setAndBinding, Operand[] sources) { Operand dest = Local(); - context.Add(new TextureOperation(Instruction.ImageAtomic, type, format, flags, binding, 0, new[] { dest }, sources)); + context.Add(new TextureOperation( + Instruction.ImageAtomic, + type, + format, + flags, + setAndBinding.SetIndex, + setAndBinding.Binding, + 0, + new[] { dest }, + sources)); return dest; } @@ -633,12 +642,21 @@ namespace Ryujinx.Graphics.Shader.Translation SamplerType type, TextureFormat format, TextureFlags flags, - int binding, + SetBindingPair setAndBinding, int compMask, Operand[] dests, Operand[] sources) { - context.Add(new TextureOperation(Instruction.ImageLoad, type, format, flags, binding, compMask, dests, sources)); + context.Add(new TextureOperation( + Instruction.ImageLoad, + type, + format, + flags, + setAndBinding.SetIndex, + setAndBinding.Binding, + compMask, + dests, + sources)); } public static void ImageStore( @@ -646,10 +664,19 @@ namespace Ryujinx.Graphics.Shader.Translation SamplerType type, TextureFormat format, TextureFlags flags, - int binding, + SetBindingPair setAndBinding, Operand[] sources) { - context.Add(new TextureOperation(Instruction.ImageStore, type, format, flags, binding, 0, null, sources)); + context.Add(new TextureOperation( + Instruction.ImageStore, + type, + format, + flags, + setAndBinding.SetIndex, + setAndBinding.Binding, + 0, + null, + sources)); } public static Operand IsNan(this EmitterContext context, Operand a, Instruction fpType = Instruction.FP32) @@ -718,13 +745,22 @@ namespace Ryujinx.Graphics.Shader.Translation this EmitterContext context, SamplerType type, TextureFlags flags, - int binding, + SetBindingPair setAndBinding, int compIndex, Operand[] sources) { Operand dest = Local(); - context.Add(new TextureOperation(Instruction.Lod, type, TextureFormat.Unknown, flags, binding, compIndex, new[] { dest }, sources)); + context.Add(new TextureOperation( + Instruction.Lod, + type, + TextureFormat.Unknown, + flags, + setAndBinding.SetIndex, + setAndBinding.Binding, + compIndex, + new[] { dest }, + sources)); return dest; } @@ -889,24 +925,42 @@ namespace Ryujinx.Graphics.Shader.Translation this EmitterContext context, SamplerType type, TextureFlags flags, - int binding, + SetBindingPair setAndBinding, int compMask, Operand[] dests, Operand[] sources) { - context.Add(new TextureOperation(Instruction.TextureSample, type, TextureFormat.Unknown, flags, binding, compMask, dests, sources)); + context.Add(new TextureOperation( + Instruction.TextureSample, + type, + TextureFormat.Unknown, + flags, + setAndBinding.SetIndex, + setAndBinding.Binding, + compMask, + dests, + sources)); } public static Operand TextureQuerySamples( this EmitterContext context, SamplerType type, TextureFlags flags, - int binding, + SetBindingPair setAndBinding, Operand[] sources) { Operand dest = Local(); - context.Add(new TextureOperation(Instruction.TextureQuerySamples, type, TextureFormat.Unknown, flags, binding, 0, new[] { dest }, sources)); + context.Add(new TextureOperation( + Instruction.TextureQuerySamples, + type, + TextureFormat.Unknown, + flags, + setAndBinding.SetIndex, + setAndBinding.Binding, + 0, + new[] { dest }, + sources)); return dest; } @@ -915,13 +969,22 @@ namespace Ryujinx.Graphics.Shader.Translation this EmitterContext context, SamplerType type, TextureFlags flags, - int binding, + SetBindingPair setAndBinding, int compIndex, Operand[] sources) { Operand dest = Local(); - context.Add(new TextureOperation(Instruction.TextureQuerySize, type, TextureFormat.Unknown, flags, binding, compIndex, new[] { dest }, sources)); + context.Add(new TextureOperation( + Instruction.TextureQuerySize, + type, + TextureFormat.Unknown, + flags, + setAndBinding.SetIndex, + setAndBinding.Binding, + compIndex, + new[] { dest }, + sources)); return dest; } diff --git a/src/Ryujinx.Graphics.Shader/Translation/FunctionMatch.cs b/src/Ryujinx.Graphics.Shader/Translation/FunctionMatch.cs index 714a9d68c..b792776df 100644 --- a/src/Ryujinx.Graphics.Shader/Translation/FunctionMatch.cs +++ b/src/Ryujinx.Graphics.Shader/Translation/FunctionMatch.cs @@ -830,12 +830,12 @@ namespace Ryujinx.Graphics.Shader.Translation if (use.Node != null) { - Console.Write($"{indentation} {separator}- ({(use.Inverted ? "INV " : "")}{use.Index})"); + Console.Write($"{indentation} {separator}- ({(use.Inverted ? "INV " : string.Empty)}{use.Index})"); PrintTreeNode(use.Node, indentation + (last ? " " : " | ")); } else { - Console.WriteLine($"{indentation} {separator}- ({(use.Inverted ? "INV " : "")}{use.Index}) NULL"); + Console.WriteLine($"{indentation} {separator}- ({(use.Inverted ? "INV " : string.Empty)}{use.Index}) NULL"); } } } @@ -852,12 +852,12 @@ namespace Ryujinx.Graphics.Shader.Translation if (use.Node != null) { - Console.Write($"{indentation} {separator}- ({(use.Inverted ? "INV " : "")}{use.Index})"); + Console.Write($"{indentation} {separator}- ({(use.Inverted ? "INV " : string.Empty)}{use.Index})"); PrintTreeNode(use.Node, indentation + (last ? " " : " | ")); } else { - Console.WriteLine($"{indentation} {separator}- ({(use.Inverted ? "INV " : "")}{use.Index}) NULL"); + Console.WriteLine($"{indentation} {separator}- ({(use.Inverted ? "INV " : string.Empty)}{use.Index}) NULL"); } } } diff --git a/src/Ryujinx.Graphics.Shader/Translation/HostCapabilities.cs b/src/Ryujinx.Graphics.Shader/Translation/HostCapabilities.cs index 2523272b0..11fe6599d 100644 --- a/src/Ryujinx.Graphics.Shader/Translation/HostCapabilities.cs +++ b/src/Ryujinx.Graphics.Shader/Translation/HostCapabilities.cs @@ -8,6 +8,7 @@ namespace Ryujinx.Graphics.Shader.Translation public readonly bool SupportsGeometryShaderPassthrough; public readonly bool SupportsShaderBallot; public readonly bool SupportsShaderBarrierDivergence; + public readonly bool SupportsShaderFloat64; public readonly bool SupportsTextureShadowLod; public readonly bool SupportsViewportMask; @@ -18,6 +19,7 @@ namespace Ryujinx.Graphics.Shader.Translation bool supportsGeometryShaderPassthrough, bool supportsShaderBallot, bool supportsShaderBarrierDivergence, + bool supportsShaderFloat64, bool supportsTextureShadowLod, bool supportsViewportMask) { @@ -27,6 +29,7 @@ namespace Ryujinx.Graphics.Shader.Translation SupportsGeometryShaderPassthrough = supportsGeometryShaderPassthrough; SupportsShaderBallot = supportsShaderBallot; SupportsShaderBarrierDivergence = supportsShaderBarrierDivergence; + SupportsShaderFloat64 = supportsShaderFloat64; SupportsTextureShadowLod = supportsTextureShadowLod; SupportsViewportMask = supportsViewportMask; } diff --git a/src/Ryujinx.Graphics.Shader/Translation/Optimizations/BindlessElimination.cs b/src/Ryujinx.Graphics.Shader/Translation/Optimizations/BindlessElimination.cs index a88903274..1f2f79a2d 100644 --- a/src/Ryujinx.Graphics.Shader/Translation/Optimizations/BindlessElimination.cs +++ b/src/Ryujinx.Graphics.Shader/Translation/Optimizations/BindlessElimination.cs @@ -1,6 +1,7 @@ using Ryujinx.Graphics.Shader.Instructions; using Ryujinx.Graphics.Shader.IntermediateRepresentation; using Ryujinx.Graphics.Shader.StructuredIr; +using System; using System.Collections.Generic; namespace Ryujinx.Graphics.Shader.Translation.Optimizations @@ -15,8 +16,12 @@ namespace Ryujinx.Graphics.Shader.Translation.Optimizations // - The handle is a constant buffer value. // - The handle is the result of a bitwise OR logical operation. // - Both sources of the OR operation comes from a constant buffer. - for (LinkedListNode node = block.Operations.First; node != null; node = node.Next) + LinkedListNode nextNode; + + for (LinkedListNode node = block.Operations.First; node != null; node = nextNode) { + nextNode = node.Next; + if (node.Value is not TextureOperation texOp) { continue; @@ -27,185 +32,325 @@ namespace Ryujinx.Graphics.Shader.Translation.Optimizations continue; } - if (texOp.Inst == Instruction.TextureSample || texOp.Inst.IsTextureQuery()) + if (!TryConvertBindless(block, resourceManager, gpuAccessor, texOp) && + !GenerateBindlessAccess(block, resourceManager, gpuAccessor, texOp, node)) { - Operand bindlessHandle = texOp.GetSource(0); + // If we can't do bindless elimination, remove the texture operation. + // Set any destination variables to zero. - // In some cases the compiler uses a shuffle operation to get the handle, - // for some textureGrad implementations. In those cases, we can skip the shuffle. - if (bindlessHandle.AsgOp is Operation shuffleOp && shuffleOp.Inst == Instruction.Shuffle) + string typeName = texOp.Inst.IsImage() + ? texOp.Type.ToGlslImageType(texOp.Format.GetComponentType()) + : texOp.Type.ToGlslTextureType(); + + gpuAccessor.Log($"Failed to find handle source for bindless access of type \"{typeName}\"."); + + for (int destIndex = 0; destIndex < texOp.DestsCount; destIndex++) { - bindlessHandle = shuffleOp.GetSource(0); + block.Operations.AddBefore(node, new Operation(Instruction.Copy, texOp.GetDest(destIndex), OperandHelper.Const(0))); } - bindlessHandle = Utils.FindLastOperation(bindlessHandle, block); - - // Some instructions do not encode an accurate sampler type: - // - Most instructions uses the same type for 1D and Buffer. - // - Query instructions may not have any type. - // For those cases, we need to try getting the type from current GPU state, - // as long bindless elimination is successful and we know where the texture descriptor is located. - bool rewriteSamplerType = - texOp.Type == SamplerType.TextureBuffer || - texOp.Inst == Instruction.TextureQuerySamples || - texOp.Inst == Instruction.TextureQuerySize; - - if (bindlessHandle.Type == OperandType.ConstantBuffer) - { - SetHandle( - resourceManager, - gpuAccessor, - texOp, - bindlessHandle.GetCbufOffset(), - bindlessHandle.GetCbufSlot(), - rewriteSamplerType, - isImage: false); - - continue; - } - - if (!TryGetOperation(bindlessHandle.AsgOp, out Operation handleCombineOp)) - { - continue; - } - - if (handleCombineOp.Inst != Instruction.BitwiseOr) - { - continue; - } - - Operand src0 = Utils.FindLastOperation(handleCombineOp.GetSource(0), block); - Operand src1 = Utils.FindLastOperation(handleCombineOp.GetSource(1), block); - - // For cases where we have a constant, ensure that the constant is always - // the second operand. - // Since this is a commutative operation, both are fine, - // and having a "canonical" representation simplifies some checks below. - if (src0.Type == OperandType.Constant && src1.Type != OperandType.Constant) - { - (src0, src1) = (src1, src0); - } - - TextureHandleType handleType = TextureHandleType.SeparateSamplerHandle; - - // Try to match the following patterns: - // Masked pattern: - // - samplerHandle = samplerHandle & 0xFFF00000; - // - textureHandle = textureHandle & 0xFFFFF; - // - combinedHandle = samplerHandle | textureHandle; - // Where samplerHandle and textureHandle comes from a constant buffer. - // Shifted pattern: - // - samplerHandle = samplerId << 20; - // - combinedHandle = samplerHandle | textureHandle; - // Where samplerId and textureHandle comes from a constant buffer. - // Constant pattern: - // - combinedHandle = samplerHandleConstant | textureHandle; - // Where samplerHandleConstant is a constant value, and textureHandle comes from a constant buffer. - if (src0.AsgOp is Operation src0AsgOp) - { - if (src1.AsgOp is Operation src1AsgOp && - src0AsgOp.Inst == Instruction.BitwiseAnd && - src1AsgOp.Inst == Instruction.BitwiseAnd) - { - src0 = GetSourceForMaskedHandle(src0AsgOp, 0xFFFFF); - src1 = GetSourceForMaskedHandle(src1AsgOp, 0xFFF00000); - - // The OR operation is commutative, so we can also try to swap the operands to get a match. - if (src0 == null || src1 == null) - { - src0 = GetSourceForMaskedHandle(src1AsgOp, 0xFFFFF); - src1 = GetSourceForMaskedHandle(src0AsgOp, 0xFFF00000); - } - - if (src0 == null || src1 == null) - { - continue; - } - } - else if (src0AsgOp.Inst == Instruction.ShiftLeft) - { - Operand shift = src0AsgOp.GetSource(1); - - if (shift.Type == OperandType.Constant && shift.Value == 20) - { - src0 = src1; - src1 = src0AsgOp.GetSource(0); - handleType = TextureHandleType.SeparateSamplerId; - } - } - } - else if (src1.AsgOp is Operation src1AsgOp && src1AsgOp.Inst == Instruction.ShiftLeft) - { - Operand shift = src1AsgOp.GetSource(1); - - if (shift.Type == OperandType.Constant && shift.Value == 20) - { - src1 = src1AsgOp.GetSource(0); - handleType = TextureHandleType.SeparateSamplerId; - } - } - else if (src1.Type == OperandType.Constant && (src1.Value & 0xfffff) == 0) - { - handleType = TextureHandleType.SeparateConstantSamplerHandle; - } - - if (src0.Type != OperandType.ConstantBuffer) - { - continue; - } - - if (handleType == TextureHandleType.SeparateConstantSamplerHandle) - { - SetHandle( - resourceManager, - gpuAccessor, - texOp, - TextureHandle.PackOffsets(src0.GetCbufOffset(), ((src1.Value >> 20) & 0xfff), handleType), - TextureHandle.PackSlots(src0.GetCbufSlot(), 0), - rewriteSamplerType, - isImage: false); - } - else if (src1.Type == OperandType.ConstantBuffer) - { - SetHandle( - resourceManager, - gpuAccessor, - texOp, - TextureHandle.PackOffsets(src0.GetCbufOffset(), src1.GetCbufOffset(), handleType), - TextureHandle.PackSlots(src0.GetCbufSlot(), src1.GetCbufSlot()), - rewriteSamplerType, - isImage: false); - } + Utils.DeleteNode(node, texOp); } - else if (texOp.Inst == Instruction.ImageLoad || - texOp.Inst == Instruction.ImageStore || - texOp.Inst == Instruction.ImageAtomic) + } + } + + private static bool GenerateBindlessAccess( + BasicBlock block, + ResourceManager resourceManager, + IGpuAccessor gpuAccessor, + TextureOperation texOp, + LinkedListNode node) + { + if (!gpuAccessor.QueryHostSupportsSeparateSampler()) + { + // We depend on combining samplers and textures in the shader being supported for this. + + return false; + } + + Operand bindlessHandle = texOp.GetSource(0); + + if (bindlessHandle.AsgOp is PhiNode phi) + { + for (int srcIndex = 0; srcIndex < phi.SourcesCount; srcIndex++) { - Operand src0 = Utils.FindLastOperation(texOp.GetSource(0), block); + Operand phiSource = phi.GetSource(srcIndex); - if (src0.Type == OperandType.ConstantBuffer) + if (phiSource.AsgOp is not PhiNode && !IsBindlessAccessAllowed(phiSource)) { - int cbufOffset = src0.GetCbufOffset(); - int cbufSlot = src0.GetCbufSlot(); - - if (texOp.Format == TextureFormat.Unknown) - { - if (texOp.Inst == Instruction.ImageAtomic) - { - texOp.Format = ShaderProperties.GetTextureFormatAtomic(gpuAccessor, cbufOffset, cbufSlot); - } - else - { - texOp.Format = ShaderProperties.GetTextureFormat(gpuAccessor, cbufOffset, cbufSlot); - } - } - - bool rewriteSamplerType = texOp.Type == SamplerType.TextureBuffer; - - SetHandle(resourceManager, gpuAccessor, texOp, cbufOffset, cbufSlot, rewriteSamplerType, isImage: true); + return false; } } } + else if (!IsBindlessAccessAllowed(bindlessHandle)) + { + return false; + } + + Operand textureHandle = OperandHelper.Local(); + Operand samplerHandle = OperandHelper.Local(); + Operand textureIndex = OperandHelper.Local(); + + block.Operations.AddBefore(node, new Operation(Instruction.BitwiseAnd, textureHandle, bindlessHandle, OperandHelper.Const(0xfffff))); + block.Operations.AddBefore(node, new Operation(Instruction.ShiftRightU32, samplerHandle, bindlessHandle, OperandHelper.Const(20))); + + int texturePoolLength = Math.Max(BindlessToArray.MinimumArrayLength, gpuAccessor.QueryTextureArrayLengthFromPool()); + + block.Operations.AddBefore(node, new Operation(Instruction.MinimumU32, textureIndex, textureHandle, OperandHelper.Const(texturePoolLength - 1))); + + texOp.SetSource(0, textureIndex); + + bool hasSampler = !texOp.Inst.IsImage(); + + SetBindingPair textureSetAndBinding = resourceManager.GetTextureOrImageBinding( + texOp.Inst, + texOp.Type, + texOp.Format, + texOp.Flags & ~TextureFlags.Bindless, + 0, + TextureHandle.PackOffsets(0, 0, TextureHandleType.Direct), + texturePoolLength, + hasSampler); + + if (hasSampler) + { + Operand samplerIndex = OperandHelper.Local(); + + int samplerPoolLength = Math.Max(BindlessToArray.MinimumArrayLength, gpuAccessor.QuerySamplerArrayLengthFromPool()); + + block.Operations.AddBefore(node, new Operation(Instruction.MinimumU32, samplerIndex, samplerHandle, OperandHelper.Const(samplerPoolLength - 1))); + + texOp.InsertSource(1, samplerIndex); + + SetBindingPair samplerSetAndBinding = resourceManager.GetTextureOrImageBinding( + texOp.Inst, + SamplerType.None, + texOp.Format, + TextureFlags.None, + 0, + TextureHandle.PackOffsets(0, 0, TextureHandleType.Direct), + samplerPoolLength); + + texOp.TurnIntoArray(textureSetAndBinding, samplerSetAndBinding); + } + else + { + texOp.TurnIntoArray(textureSetAndBinding); + } + + return true; + } + + private static bool IsBindlessAccessAllowed(Operand bindlessHandle) + { + if (bindlessHandle.Type == OperandType.ConstantBuffer) + { + // Bindless access with handles from constant buffer is allowed. + + return true; + } + + if (bindlessHandle.AsgOp is not Operation handleOp || + handleOp.Inst != Instruction.Load || + (handleOp.StorageKind != StorageKind.Input && handleOp.StorageKind != StorageKind.StorageBuffer)) + { + // Right now, we only allow bindless access when the handle comes from a shader input or storage buffer. + // This is an artificial limitation to prevent it from being used in cases where it + // would have a large performance impact of loading all textures in the pool. + // It might be removed in the future, if we can mitigate the performance impact. + + return false; + } + + return true; + } + + private static bool TryConvertBindless(BasicBlock block, ResourceManager resourceManager, IGpuAccessor gpuAccessor, TextureOperation texOp) + { + if (texOp.Inst == Instruction.TextureSample || texOp.Inst.IsTextureQuery()) + { + Operand bindlessHandle = texOp.GetSource(0); + + // In some cases the compiler uses a shuffle operation to get the handle, + // for some textureGrad implementations. In those cases, we can skip the shuffle. + if (bindlessHandle.AsgOp is Operation shuffleOp && shuffleOp.Inst == Instruction.Shuffle) + { + bindlessHandle = shuffleOp.GetSource(0); + } + + bindlessHandle = Utils.FindLastOperation(bindlessHandle, block); + + // Some instructions do not encode an accurate sampler type: + // - Most instructions uses the same type for 1D and Buffer. + // - Query instructions may not have any type. + // For those cases, we need to try getting the type from current GPU state, + // as long bindless elimination is successful and we know where the texture descriptor is located. + bool rewriteSamplerType = + texOp.Type == SamplerType.TextureBuffer || + texOp.Inst == Instruction.TextureQuerySamples || + texOp.Inst == Instruction.TextureQuerySize; + + if (bindlessHandle.Type == OperandType.ConstantBuffer) + { + SetHandle( + resourceManager, + gpuAccessor, + texOp, + bindlessHandle.GetCbufOffset(), + bindlessHandle.GetCbufSlot(), + rewriteSamplerType, + isImage: false); + + return true; + } + + if (!TryGetOperation(bindlessHandle.AsgOp, out Operation handleCombineOp)) + { + return false; + } + + if (handleCombineOp.Inst != Instruction.BitwiseOr) + { + return false; + } + + Operand src0 = Utils.FindLastOperation(handleCombineOp.GetSource(0), block); + Operand src1 = Utils.FindLastOperation(handleCombineOp.GetSource(1), block); + + // For cases where we have a constant, ensure that the constant is always + // the second operand. + // Since this is a commutative operation, both are fine, + // and having a "canonical" representation simplifies some checks below. + if (src0.Type == OperandType.Constant && src1.Type != OperandType.Constant) + { + (src0, src1) = (src1, src0); + } + + TextureHandleType handleType = TextureHandleType.SeparateSamplerHandle; + + // Try to match the following patterns: + // Masked pattern: + // - samplerHandle = samplerHandle & 0xFFF00000; + // - textureHandle = textureHandle & 0xFFFFF; + // - combinedHandle = samplerHandle | textureHandle; + // Where samplerHandle and textureHandle comes from a constant buffer. + // Shifted pattern: + // - samplerHandle = samplerId << 20; + // - combinedHandle = samplerHandle | textureHandle; + // Where samplerId and textureHandle comes from a constant buffer. + // Constant pattern: + // - combinedHandle = samplerHandleConstant | textureHandle; + // Where samplerHandleConstant is a constant value, and textureHandle comes from a constant buffer. + if (src0.AsgOp is Operation src0AsgOp) + { + if (src1.AsgOp is Operation src1AsgOp && + src0AsgOp.Inst == Instruction.BitwiseAnd && + src1AsgOp.Inst == Instruction.BitwiseAnd) + { + src0 = GetSourceForMaskedHandle(src0AsgOp, 0xFFFFF); + src1 = GetSourceForMaskedHandle(src1AsgOp, 0xFFF00000); + + // The OR operation is commutative, so we can also try to swap the operands to get a match. + if (src0 == null || src1 == null) + { + src0 = GetSourceForMaskedHandle(src1AsgOp, 0xFFFFF); + src1 = GetSourceForMaskedHandle(src0AsgOp, 0xFFF00000); + } + + if (src0 == null || src1 == null) + { + return false; + } + } + else if (src0AsgOp.Inst == Instruction.ShiftLeft) + { + Operand shift = src0AsgOp.GetSource(1); + + if (shift.Type == OperandType.Constant && shift.Value == 20) + { + src0 = src1; + src1 = src0AsgOp.GetSource(0); + handleType = TextureHandleType.SeparateSamplerId; + } + } + } + else if (src1.AsgOp is Operation src1AsgOp && src1AsgOp.Inst == Instruction.ShiftLeft) + { + Operand shift = src1AsgOp.GetSource(1); + + if (shift.Type == OperandType.Constant && shift.Value == 20) + { + src1 = src1AsgOp.GetSource(0); + handleType = TextureHandleType.SeparateSamplerId; + } + } + else if (src1.Type == OperandType.Constant && (src1.Value & 0xfffff) == 0) + { + handleType = TextureHandleType.SeparateConstantSamplerHandle; + } + + if (src0.Type != OperandType.ConstantBuffer) + { + return false; + } + + if (handleType == TextureHandleType.SeparateConstantSamplerHandle) + { + SetHandle( + resourceManager, + gpuAccessor, + texOp, + TextureHandle.PackOffsets(src0.GetCbufOffset(), (src1.Value >> 20) & 0xfff, handleType), + TextureHandle.PackSlots(src0.GetCbufSlot(), 0), + rewriteSamplerType, + isImage: false); + + return true; + } + else if (src1.Type == OperandType.ConstantBuffer) + { + SetHandle( + resourceManager, + gpuAccessor, + texOp, + TextureHandle.PackOffsets(src0.GetCbufOffset(), src1.GetCbufOffset(), handleType), + TextureHandle.PackSlots(src0.GetCbufSlot(), src1.GetCbufSlot()), + rewriteSamplerType, + isImage: false); + + return true; + } + } + else if (texOp.Inst.IsImage()) + { + Operand src0 = Utils.FindLastOperation(texOp.GetSource(0), block); + + if (src0.Type == OperandType.ConstantBuffer) + { + int cbufOffset = src0.GetCbufOffset(); + int cbufSlot = src0.GetCbufSlot(); + + if (texOp.Format == TextureFormat.Unknown) + { + if (texOp.Inst == Instruction.ImageAtomic) + { + texOp.Format = ShaderProperties.GetTextureFormatAtomic(gpuAccessor, cbufOffset, cbufSlot); + } + else + { + texOp.Format = ShaderProperties.GetTextureFormat(gpuAccessor, cbufOffset, cbufSlot); + } + } + + bool rewriteSamplerType = texOp.Type == SamplerType.TextureBuffer; + + SetHandle(resourceManager, gpuAccessor, texOp, cbufOffset, cbufSlot, rewriteSamplerType, isImage: true); + + return true; + } + } + + return false; } private static bool TryGetOperation(INode asgOp, out Operation outOperation) @@ -335,7 +480,7 @@ namespace Ryujinx.Graphics.Shader.Translation.Optimizations } } - int binding = resourceManager.GetTextureOrImageBinding( + SetBindingPair setAndBinding = resourceManager.GetTextureOrImageBinding( texOp.Inst, texOp.Type, texOp.Format, @@ -343,7 +488,7 @@ namespace Ryujinx.Graphics.Shader.Translation.Optimizations cbufSlot, cbufOffset); - texOp.SetBinding(binding); + texOp.SetBinding(setAndBinding); } } } diff --git a/src/Ryujinx.Graphics.Shader/Translation/Optimizations/BindlessToArray.cs b/src/Ryujinx.Graphics.Shader/Translation/Optimizations/BindlessToArray.cs new file mode 100644 index 000000000..1e0b3b645 --- /dev/null +++ b/src/Ryujinx.Graphics.Shader/Translation/Optimizations/BindlessToArray.cs @@ -0,0 +1,238 @@ +using Ryujinx.Graphics.Shader.IntermediateRepresentation; +using System; +using System.Collections.Generic; +using static Ryujinx.Graphics.Shader.IntermediateRepresentation.OperandHelper; + +namespace Ryujinx.Graphics.Shader.Translation.Optimizations +{ + static class BindlessToArray + { + private const int NvnTextureBufferIndex = 2; + private const int HardcodedArrayLengthOgl = 4; + + // 1 and 0 elements are not considered arrays anymore. + public const int MinimumArrayLength = 2; + + public static void RunPassOgl(BasicBlock block, ResourceManager resourceManager) + { + // We can turn a bindless texture access into a indexed access, + // as long the following conditions are true: + // - The handle is loaded using a LDC instruction. + // - The handle is loaded from the constant buffer with the handles (CB2 for NVN). + // - The load has a constant offset. + // The base offset of the array of handles on the constant buffer is the constant offset. + for (LinkedListNode node = block.Operations.First; node != null; node = node.Next) + { + if (node.Value is not TextureOperation texOp) + { + continue; + } + + if ((texOp.Flags & TextureFlags.Bindless) == 0) + { + continue; + } + + if (texOp.GetSource(0).AsgOp is not Operation handleAsgOp) + { + continue; + } + + if (handleAsgOp.Inst != Instruction.Load || + handleAsgOp.StorageKind != StorageKind.ConstantBuffer || + handleAsgOp.SourcesCount != 4) + { + continue; + } + + Operand ldcSrc0 = handleAsgOp.GetSource(0); + + if (ldcSrc0.Type != OperandType.Constant || + !resourceManager.TryGetConstantBufferSlot(ldcSrc0.Value, out int src0CbufSlot) || + src0CbufSlot != NvnTextureBufferIndex) + { + continue; + } + + Operand ldcSrc1 = handleAsgOp.GetSource(1); + + // We expect field index 0 to be accessed. + if (ldcSrc1.Type != OperandType.Constant || ldcSrc1.Value != 0) + { + continue; + } + + Operand ldcSrc2 = handleAsgOp.GetSource(2); + + // FIXME: This is missing some checks, for example, a check to ensure that the shift value is 2. + // Might be not worth fixing since if that doesn't kick in, the result will be no texture + // to access anyway which is also wrong. + // Plus this whole transform is fundamentally flawed as-is since we have no way to know the array size. + // Eventually, this should be entirely removed in favor of a implementation that supports true bindless + // texture access. + if (ldcSrc2.AsgOp is not Operation shrOp || shrOp.Inst != Instruction.ShiftRightU32) + { + continue; + } + + if (shrOp.GetSource(0).AsgOp is not Operation shrOp2 || shrOp2.Inst != Instruction.ShiftRightU32) + { + continue; + } + + if (shrOp2.GetSource(0).AsgOp is not Operation addOp || addOp.Inst != Instruction.Add) + { + continue; + } + + Operand addSrc1 = addOp.GetSource(1); + + if (addSrc1.Type != OperandType.Constant) + { + continue; + } + + TurnIntoArray(resourceManager, texOp, NvnTextureBufferIndex, addSrc1.Value / 4, HardcodedArrayLengthOgl); + + Operand index = Local(); + + Operand source = addOp.GetSource(0); + + Operation shrBy3 = new(Instruction.ShiftRightU32, index, source, Const(3)); + + block.Operations.AddBefore(node, shrBy3); + + texOp.SetSource(0, index); + } + } + + public static void RunPass(BasicBlock block, ResourceManager resourceManager, IGpuAccessor gpuAccessor) + { + // We can turn a bindless texture access into a indexed access, + // as long the following conditions are true: + // - The handle is loaded using a LDC instruction. + // - The handle is loaded from the constant buffer with the handles (CB2 for NVN). + // - The load has a constant offset. + // The base offset of the array of handles on the constant buffer is the constant offset. + for (LinkedListNode node = block.Operations.First; node != null; node = node.Next) + { + if (node.Value is not TextureOperation texOp) + { + continue; + } + + if ((texOp.Flags & TextureFlags.Bindless) == 0) + { + continue; + } + + Operand bindlessHandle = Utils.FindLastOperation(texOp.GetSource(0), block); + + if (bindlessHandle.AsgOp is not Operation handleAsgOp) + { + continue; + } + + int secondaryCbufSlot = 0; + int secondaryCbufOffset = 0; + bool hasSecondaryHandle = false; + + if (handleAsgOp.Inst == Instruction.BitwiseOr) + { + Operand src0 = Utils.FindLastOperation(handleAsgOp.GetSource(0), block); + Operand src1 = Utils.FindLastOperation(handleAsgOp.GetSource(1), block); + + if (src0.Type == OperandType.ConstantBuffer && src1.AsgOp is Operation) + { + handleAsgOp = src1.AsgOp as Operation; + secondaryCbufSlot = src0.GetCbufSlot(); + secondaryCbufOffset = src0.GetCbufOffset(); + hasSecondaryHandle = true; + } + else if (src0.AsgOp is Operation && src1.Type == OperandType.ConstantBuffer) + { + handleAsgOp = src0.AsgOp as Operation; + secondaryCbufSlot = src1.GetCbufSlot(); + secondaryCbufOffset = src1.GetCbufOffset(); + hasSecondaryHandle = true; + } + } + + if (handleAsgOp.Inst != Instruction.Load || + handleAsgOp.StorageKind != StorageKind.ConstantBuffer || + handleAsgOp.SourcesCount != 4) + { + continue; + } + + Operand ldcSrc0 = handleAsgOp.GetSource(0); + + if (ldcSrc0.Type != OperandType.Constant || + !resourceManager.TryGetConstantBufferSlot(ldcSrc0.Value, out int src0CbufSlot)) + { + continue; + } + + Operand ldcSrc1 = handleAsgOp.GetSource(1); + + // We expect field index 0 to be accessed. + if (ldcSrc1.Type != OperandType.Constant || ldcSrc1.Value != 0) + { + continue; + } + + Operand ldcVecIndex = handleAsgOp.GetSource(2); + Operand ldcElemIndex = handleAsgOp.GetSource(3); + + if (ldcVecIndex.Type != OperandType.LocalVariable || ldcElemIndex.Type != OperandType.LocalVariable) + { + continue; + } + + int cbufSlot; + int handleIndex; + + if (hasSecondaryHandle) + { + cbufSlot = TextureHandle.PackSlots(src0CbufSlot, secondaryCbufSlot); + handleIndex = TextureHandle.PackOffsets(0, secondaryCbufOffset, TextureHandleType.SeparateSamplerHandle); + } + else + { + cbufSlot = src0CbufSlot; + handleIndex = 0; + } + + int length = Math.Max(MinimumArrayLength, gpuAccessor.QueryTextureArrayLengthFromBuffer(src0CbufSlot)); + + TurnIntoArray(resourceManager, texOp, cbufSlot, handleIndex, length); + + Operand vecIndex = Local(); + Operand elemIndex = Local(); + Operand index = Local(); + Operand indexMin = Local(); + + block.Operations.AddBefore(node, new Operation(Instruction.ShiftLeft, vecIndex, ldcVecIndex, Const(1))); + block.Operations.AddBefore(node, new Operation(Instruction.ShiftRightU32, elemIndex, ldcElemIndex, Const(1))); + block.Operations.AddBefore(node, new Operation(Instruction.Add, index, vecIndex, elemIndex)); + block.Operations.AddBefore(node, new Operation(Instruction.MinimumU32, indexMin, index, Const(length - 1))); + + texOp.SetSource(0, indexMin); + } + } + + private static void TurnIntoArray(ResourceManager resourceManager, TextureOperation texOp, int cbufSlot, int handleIndex, int length) + { + SetBindingPair setAndBinding = resourceManager.GetTextureOrImageBinding( + texOp.Inst, + texOp.Type, + texOp.Format, + texOp.Flags & ~TextureFlags.Bindless, + cbufSlot, + handleIndex, + length); + + texOp.TurnIntoArray(setAndBinding); + } + } +} diff --git a/src/Ryujinx.Graphics.Shader/Translation/Optimizations/BindlessToIndexed.cs b/src/Ryujinx.Graphics.Shader/Translation/Optimizations/BindlessToIndexed.cs deleted file mode 100644 index 2bd31fe1b..000000000 --- a/src/Ryujinx.Graphics.Shader/Translation/Optimizations/BindlessToIndexed.cs +++ /dev/null @@ -1,118 +0,0 @@ -using Ryujinx.Graphics.Shader.IntermediateRepresentation; -using System.Collections.Generic; - -using static Ryujinx.Graphics.Shader.IntermediateRepresentation.OperandHelper; - -namespace Ryujinx.Graphics.Shader.Translation.Optimizations -{ - static class BindlessToIndexed - { - private const int NvnTextureBufferIndex = 2; - - public static void RunPass(BasicBlock block, ResourceManager resourceManager) - { - // We can turn a bindless texture access into a indexed access, - // as long the following conditions are true: - // - The handle is loaded using a LDC instruction. - // - The handle is loaded from the constant buffer with the handles (CB2 for NVN). - // - The load has a constant offset. - // The base offset of the array of handles on the constant buffer is the constant offset. - for (LinkedListNode node = block.Operations.First; node != null; node = node.Next) - { - if (node.Value is not TextureOperation texOp) - { - continue; - } - - if ((texOp.Flags & TextureFlags.Bindless) == 0) - { - continue; - } - - if (texOp.GetSource(0).AsgOp is not Operation handleAsgOp) - { - continue; - } - - if (handleAsgOp.Inst != Instruction.Load || - handleAsgOp.StorageKind != StorageKind.ConstantBuffer || - handleAsgOp.SourcesCount != 4) - { - continue; - } - - Operand ldcSrc0 = handleAsgOp.GetSource(0); - - if (ldcSrc0.Type != OperandType.Constant || - !resourceManager.TryGetConstantBufferSlot(ldcSrc0.Value, out int src0CbufSlot) || - src0CbufSlot != NvnTextureBufferIndex) - { - continue; - } - - Operand ldcSrc1 = handleAsgOp.GetSource(1); - - // We expect field index 0 to be accessed. - if (ldcSrc1.Type != OperandType.Constant || ldcSrc1.Value != 0) - { - continue; - } - - Operand ldcSrc2 = handleAsgOp.GetSource(2); - - // FIXME: This is missing some checks, for example, a check to ensure that the shift value is 2. - // Might be not worth fixing since if that doesn't kick in, the result will be no texture - // to access anyway which is also wrong. - // Plus this whole transform is fundamentally flawed as-is since we have no way to know the array size. - // Eventually, this should be entirely removed in favor of a implementation that supports true bindless - // texture access. - if (ldcSrc2.AsgOp is not Operation shrOp || shrOp.Inst != Instruction.ShiftRightU32) - { - continue; - } - - if (shrOp.GetSource(0).AsgOp is not Operation shrOp2 || shrOp2.Inst != Instruction.ShiftRightU32) - { - continue; - } - - if (shrOp2.GetSource(0).AsgOp is not Operation addOp || addOp.Inst != Instruction.Add) - { - continue; - } - - Operand addSrc1 = addOp.GetSource(1); - - if (addSrc1.Type != OperandType.Constant) - { - continue; - } - - TurnIntoIndexed(resourceManager, texOp, addSrc1.Value / 4); - - Operand index = Local(); - - Operand source = addOp.GetSource(0); - - Operation shrBy3 = new(Instruction.ShiftRightU32, index, source, Const(3)); - - block.Operations.AddBefore(node, shrBy3); - - texOp.SetSource(0, index); - } - } - - private static void TurnIntoIndexed(ResourceManager resourceManager, TextureOperation texOp, int handle) - { - int binding = resourceManager.GetTextureOrImageBinding( - texOp.Inst, - texOp.Type | SamplerType.Indexed, - texOp.Format, - texOp.Flags & ~TextureFlags.Bindless, - NvnTextureBufferIndex, - handle); - - texOp.TurnIntoIndexed(binding); - } - } -} diff --git a/src/Ryujinx.Graphics.Shader/Translation/Optimizations/Optimizer.cs b/src/Ryujinx.Graphics.Shader/Translation/Optimizations/Optimizer.cs index 17427a5f9..1be7c5c52 100644 --- a/src/Ryujinx.Graphics.Shader/Translation/Optimizations/Optimizer.cs +++ b/src/Ryujinx.Graphics.Shader/Translation/Optimizations/Optimizer.cs @@ -20,7 +20,15 @@ namespace Ryujinx.Graphics.Shader.Translation.Optimizations // Those passes are looking for specific patterns and only needs to run once. for (int blkIndex = 0; blkIndex < context.Blocks.Length; blkIndex++) { - BindlessToIndexed.RunPass(context.Blocks[blkIndex], context.ResourceManager); + if (context.TargetApi == TargetApi.OpenGL) + { + BindlessToArray.RunPassOgl(context.Blocks[blkIndex], context.ResourceManager); + } + else + { + BindlessToArray.RunPass(context.Blocks[blkIndex], context.ResourceManager, context.GpuAccessor); + } + BindlessElimination.RunPass(context.Blocks[blkIndex], context.ResourceManager, context.GpuAccessor); // FragmentCoord only exists on fragment shaders, so we don't need to check other stages. @@ -144,18 +152,14 @@ namespace Ryujinx.Graphics.Shader.Translation.Optimizations { // If all phi sources are the same, we can propagate it and remove the phi. - Operand firstSrc = phi.GetSource(0); - - for (int index = 1; index < phi.SourcesCount; index++) + if (!Utils.AreAllSourcesTheSameOperand(phi)) { - if (!IsSameOperand(firstSrc, phi.GetSource(index))) - { - return false; - } + return false; } // All sources are equal, we can propagate the value. + Operand firstSrc = phi.GetSource(0); Operand dest = phi.Dest; INode[] uses = dest.UseOps.ToArray(); @@ -174,17 +178,6 @@ namespace Ryujinx.Graphics.Shader.Translation.Optimizations return true; } - private static bool IsSameOperand(Operand x, Operand y) - { - if (x.Type != y.Type || x.Value != y.Value) - { - return false; - } - - // TODO: Handle Load operations with the same storage and the same constant parameters. - return x.Type == OperandType.Constant || x.Type == OperandType.ConstantBuffer; - } - private static bool PropagatePack(Operation packOp) { // Propagate pack source operands to uses by unpack @@ -322,7 +315,7 @@ namespace Ryujinx.Graphics.Shader.Translation.Optimizations Operand lhs = operation.GetSource(0); Operand rhs = operation.GetSource(1); - // Check LHS of the the main multiplication operation. We expect an input being multiplied by gl_FragCoord.w. + // Check LHS of the main multiplication operation. We expect an input being multiplied by gl_FragCoord.w. if (lhs.AsgOp is not Operation attrMulOp || attrMulOp.Inst != (Instruction.FP32 | Instruction.Multiply)) { return; diff --git a/src/Ryujinx.Graphics.Shader/Translation/Optimizations/Simplification.cs b/src/Ryujinx.Graphics.Shader/Translation/Optimizations/Simplification.cs index a509fcb42..097c8aa88 100644 --- a/src/Ryujinx.Graphics.Shader/Translation/Optimizations/Simplification.cs +++ b/src/Ryujinx.Graphics.Shader/Translation/Optimizations/Simplification.cs @@ -31,6 +31,10 @@ namespace Ryujinx.Graphics.Shader.Translation.Optimizations TryEliminateBitwiseOr(operation); break; + case Instruction.CompareNotEqual: + TryEliminateCompareNotEqual(operation); + break; + case Instruction.ConditionalSelect: TryEliminateConditionalSelect(operation); break; @@ -174,6 +178,32 @@ namespace Ryujinx.Graphics.Shader.Translation.Optimizations } } + private static void TryEliminateCompareNotEqual(Operation operation) + { + // Comparison instruction returns 0 if the result is false, and -1 if true. + // Doing a not equal zero comparison on the result is redundant, so we can just copy the first result in this case. + + Operand lhs = operation.GetSource(0); + Operand rhs = operation.GetSource(1); + + if (lhs.Type == OperandType.Constant) + { + (lhs, rhs) = (rhs, lhs); + } + + if (rhs.Type != OperandType.Constant || rhs.Value != 0) + { + return; + } + + if (lhs.AsgOp is not Operation compareOp || !compareOp.Inst.IsComparison()) + { + return; + } + + operation.TurnIntoCopy(lhs); + } + private static void TryEliminateConditionalSelect(Operation operation) { Operand cond = operation.GetSource(0); diff --git a/src/Ryujinx.Graphics.Shader/Translation/Optimizations/Utils.cs b/src/Ryujinx.Graphics.Shader/Translation/Optimizations/Utils.cs index 74a6d5a1e..6ec90fa3c 100644 --- a/src/Ryujinx.Graphics.Shader/Translation/Optimizations/Utils.cs +++ b/src/Ryujinx.Graphics.Shader/Translation/Optimizations/Utils.cs @@ -34,6 +34,50 @@ namespace Ryujinx.Graphics.Shader.Translation.Optimizations return elemIndexSrc.Type == OperandType.Constant && elemIndexSrc.Value == elemIndex; } + private static bool IsSameOperand(Operand x, Operand y) + { + if (x.Type != y.Type || x.Value != y.Value) + { + return false; + } + + // TODO: Handle Load operations with the same storage and the same constant parameters. + return x == y || x.Type == OperandType.Constant || x.Type == OperandType.ConstantBuffer; + } + + private static bool AreAllSourcesEqual(INode node, INode otherNode) + { + if (node.SourcesCount != otherNode.SourcesCount) + { + return false; + } + + for (int index = 0; index < node.SourcesCount; index++) + { + if (!IsSameOperand(node.GetSource(index), otherNode.GetSource(index))) + { + return false; + } + } + + return true; + } + + public static bool AreAllSourcesTheSameOperand(INode node) + { + Operand firstSrc = node.GetSource(0); + + for (int index = 1; index < node.SourcesCount; index++) + { + if (!IsSameOperand(firstSrc, node.GetSource(index))) + { + return false; + } + } + + return true; + } + private static Operation FindBranchSource(BasicBlock block) { foreach (BasicBlock sourceBlock in block.Predecessors) @@ -55,6 +99,19 @@ namespace Ryujinx.Graphics.Shader.Translation.Optimizations return inst == Instruction.BranchIfFalse || inst == Instruction.BranchIfTrue; } + private static bool IsSameCondition(Operand currentCondition, Operand queryCondition) + { + if (currentCondition == queryCondition) + { + return true; + } + + return currentCondition.AsgOp is Operation currentOperation && + queryCondition.AsgOp is Operation queryOperation && + currentOperation.Inst == queryOperation.Inst && + AreAllSourcesEqual(currentOperation, queryOperation); + } + private static bool BlockConditionsMatch(BasicBlock currentBlock, BasicBlock queryBlock) { // Check if all the conditions for the query block are satisfied by the current block. @@ -70,10 +127,10 @@ namespace Ryujinx.Graphics.Shader.Translation.Optimizations return currentBranch != null && queryBranch != null && currentBranch.Inst == queryBranch.Inst && - currentCondition == queryCondition; + IsSameCondition(currentCondition, queryCondition); } - public static Operand FindLastOperation(Operand source, BasicBlock block) + public static Operand FindLastOperation(Operand source, BasicBlock block, bool recurse = true) { if (source.AsgOp is PhiNode phiNode) { @@ -81,13 +138,48 @@ namespace Ryujinx.Graphics.Shader.Translation.Optimizations // Ensure that conditions met for that branch are also met for the current one. // Prefer the latest sources for the phi node. + int undefCount = 0; + for (int i = phiNode.SourcesCount - 1; i >= 0; i--) { BasicBlock phiBlock = phiNode.GetBlock(i); + Operand phiSource = phiNode.GetSource(i); if (BlockConditionsMatch(block, phiBlock)) { - return phiNode.GetSource(i); + return phiSource; + } + else if (recurse && phiSource.AsgOp is PhiNode) + { + // Phi source is another phi. + // Let's check if that phi has a block that matches our condition. + + Operand match = FindLastOperation(phiSource, block, false); + + if (match != phiSource) + { + return match; + } + } + else if (phiSource.Type == OperandType.Undefined) + { + undefCount++; + } + } + + // If all sources but one are undefined, we can assume that the one + // that is not undefined is the right one. + + if (undefCount == phiNode.SourcesCount - 1) + { + for (int i = phiNode.SourcesCount - 1; i >= 0; i--) + { + Operand phiSource = phiNode.GetSource(i); + + if (phiSource.Type != OperandType.Undefined) + { + return phiSource; + } } } } diff --git a/src/Ryujinx.Graphics.Shader/Translation/RegisterUsage.cs b/src/Ryujinx.Graphics.Shader/Translation/RegisterUsage.cs index e27e47070..1c724223c 100644 --- a/src/Ryujinx.Graphics.Shader/Translation/RegisterUsage.cs +++ b/src/Ryujinx.Graphics.Shader/Translation/RegisterUsage.cs @@ -155,9 +155,14 @@ namespace Ryujinx.Graphics.Shader.Translation localInputs[block.Index] |= GetMask(register) & ~localOutputs[block.Index]; } - if (operation.Dest != null && operation.Dest.Type == OperandType.Register) + for (int dstIndex = 0; dstIndex < operation.DestsCount; dstIndex++) { - localOutputs[block.Index] |= GetMask(operation.Dest.GetRegister()); + Operand dest = operation.GetDest(dstIndex); + + if (dest != null && dest.Type == OperandType.Register) + { + localOutputs[block.Index] |= GetMask(dest.GetRegister()); + } } } } diff --git a/src/Ryujinx.Graphics.Shader/Translation/ResourceManager.cs b/src/Ryujinx.Graphics.Shader/Translation/ResourceManager.cs index 83332711f..94691a5b4 100644 --- a/src/Ryujinx.Graphics.Shader/Translation/ResourceManager.cs +++ b/src/Ryujinx.Graphics.Shader/Translation/ResourceManager.cs @@ -14,17 +14,14 @@ namespace Ryujinx.Graphics.Shader.Translation private const int DefaultLocalMemorySize = 128; private const int DefaultSharedMemorySize = 4096; - // TODO: Non-hardcoded array size. - public const int SamplerArraySize = 4; - private static readonly string[] _stagePrefixes = new string[] { "cp", "vp", "tcp", "tep", "gp", "fp" }; private readonly IGpuAccessor _gpuAccessor; private readonly ShaderStage _stage; private readonly string _stagePrefix; - private readonly int[] _cbSlotToBindingMap; - private readonly int[] _sbSlotToBindingMap; + private readonly SetBindingPair[] _cbSlotToBindingMap; + private readonly SetBindingPair[] _sbSlotToBindingMap; private uint _sbSlotWritten; private readonly Dictionary _sbSlots; @@ -32,10 +29,11 @@ namespace Ryujinx.Graphics.Shader.Translation private readonly HashSet _usedConstantBufferBindings; - private readonly record struct TextureInfo(int CbufSlot, int Handle, bool Indexed, TextureFormat Format); + private readonly record struct TextureInfo(int CbufSlot, int Handle, int ArrayLength, bool Separate, SamplerType Type, TextureFormat Format); private struct TextureMeta { + public int Set; public int Binding; public bool AccurateType; public SamplerType Type; @@ -67,10 +65,10 @@ namespace Ryujinx.Graphics.Shader.Translation _stage = stage; _stagePrefix = GetShaderStagePrefix(stage); - _cbSlotToBindingMap = new int[18]; - _sbSlotToBindingMap = new int[16]; - _cbSlotToBindingMap.AsSpan().Fill(-1); - _sbSlotToBindingMap.AsSpan().Fill(-1); + _cbSlotToBindingMap = new SetBindingPair[18]; + _sbSlotToBindingMap = new SetBindingPair[16]; + _cbSlotToBindingMap.AsSpan().Fill(new(-1, -1)); + _sbSlotToBindingMap.AsSpan().Fill(new(-1, -1)); _sbSlots = new(); _sbSlotsReverse = new(); @@ -149,16 +147,16 @@ namespace Ryujinx.Graphics.Shader.Translation public int GetConstantBufferBinding(int slot) { - int binding = _cbSlotToBindingMap[slot]; - if (binding < 0) + SetBindingPair setAndBinding = _cbSlotToBindingMap[slot]; + if (setAndBinding.Binding < 0) { - binding = _gpuAccessor.QueryBindingConstantBuffer(slot); - _cbSlotToBindingMap[slot] = binding; + setAndBinding = _gpuAccessor.CreateConstantBufferBinding(slot); + _cbSlotToBindingMap[slot] = setAndBinding; string slotNumber = slot.ToString(CultureInfo.InvariantCulture); - AddNewConstantBuffer(binding, $"{_stagePrefix}_c{slotNumber}"); + AddNewConstantBuffer(setAndBinding.SetIndex, setAndBinding.Binding, $"{_stagePrefix}_c{slotNumber}"); } - return binding; + return setAndBinding.Binding; } public bool TryGetStorageBufferBinding(int sbCbSlot, int sbCbOffset, bool write, out int binding) @@ -169,14 +167,14 @@ namespace Ryujinx.Graphics.Shader.Translation return false; } - binding = _sbSlotToBindingMap[slot]; + SetBindingPair setAndBinding = _sbSlotToBindingMap[slot]; - if (binding < 0) + if (setAndBinding.Binding < 0) { - binding = _gpuAccessor.QueryBindingStorageBuffer(slot); - _sbSlotToBindingMap[slot] = binding; + setAndBinding = _gpuAccessor.CreateStorageBufferBinding(slot); + _sbSlotToBindingMap[slot] = setAndBinding; string slotNumber = slot.ToString(CultureInfo.InvariantCulture); - AddNewStorageBuffer(binding, $"{_stagePrefix}_s{slotNumber}"); + AddNewStorageBuffer(setAndBinding.SetIndex, setAndBinding.Binding, $"{_stagePrefix}_s{slotNumber}"); } if (write) @@ -184,6 +182,7 @@ namespace Ryujinx.Graphics.Shader.Translation _sbSlotWritten |= 1u << slot; } + binding = setAndBinding.Binding; return true; } @@ -211,7 +210,7 @@ namespace Ryujinx.Graphics.Shader.Translation { for (slot = 0; slot < _cbSlotToBindingMap.Length; slot++) { - if (_cbSlotToBindingMap[slot] == binding) + if (_cbSlotToBindingMap[slot].Binding == binding) { return true; } @@ -221,17 +220,19 @@ namespace Ryujinx.Graphics.Shader.Translation return false; } - public int GetTextureOrImageBinding( + public SetBindingPair GetTextureOrImageBinding( Instruction inst, SamplerType type, TextureFormat format, TextureFlags flags, int cbufSlot, - int handle) + int handle, + int arrayLength = 1, + bool separate = false) { inst &= Instruction.Mask; - bool isImage = inst == Instruction.ImageLoad || inst == Instruction.ImageStore || inst == Instruction.ImageAtomic; - bool isWrite = inst == Instruction.ImageStore || inst == Instruction.ImageAtomic; + bool isImage = inst.IsImage(); + bool isWrite = inst.IsImageStore(); bool accurateType = !inst.IsTextureQuery(); bool intCoords = isImage || flags.HasFlag(TextureFlags.IntCoords) || inst == Instruction.TextureQuerySize; bool coherent = flags.HasFlag(TextureFlags.Coherent); @@ -241,26 +242,38 @@ namespace Ryujinx.Graphics.Shader.Translation format = TextureFormat.Unknown; } - int binding = GetTextureOrImageBinding(cbufSlot, handle, type, format, isImage, intCoords, isWrite, accurateType, coherent); + SetBindingPair setAndBinding = GetTextureOrImageBinding( + cbufSlot, + handle, + arrayLength, + type, + format, + isImage, + intCoords, + isWrite, + accurateType, + coherent, + separate); _gpuAccessor.RegisterTexture(handle, cbufSlot); - return binding; + return setAndBinding; } - private int GetTextureOrImageBinding( + private SetBindingPair GetTextureOrImageBinding( int cbufSlot, int handle, + int arrayLength, SamplerType type, TextureFormat format, bool isImage, bool intCoords, bool write, bool accurateType, - bool coherent) + bool coherent, + bool separate) { - var dimensions = type.GetDimensions(); - var isIndexed = type.HasFlag(SamplerType.Indexed); + var dimensions = type == SamplerType.None ? 0 : type.GetDimensions(); var dict = isImage ? _usedImages : _usedTextures; var usageFlags = TextureUsageFlags.None; @@ -269,7 +282,7 @@ namespace Ryujinx.Graphics.Shader.Translation { usageFlags |= TextureUsageFlags.NeedsScaleValue; - var canScale = _stage.SupportsRenderScale() && !isIndexed && !write && dimensions == 2; + var canScale = _stage.SupportsRenderScale() && arrayLength == 1 && !write && dimensions == 2; if (!canScale) { @@ -289,80 +302,102 @@ namespace Ryujinx.Graphics.Shader.Translation usageFlags |= TextureUsageFlags.ImageCoherent; } - int arraySize = isIndexed ? SamplerArraySize : 1; - int firstBinding = -1; - - for (int layer = 0; layer < arraySize; layer++) + // For array textures, we also want to use type as key, + // since we may have texture handles stores in the same buffer, but for textures with different types. + var keyType = arrayLength > 1 ? type : SamplerType.None; + var info = new TextureInfo(cbufSlot, handle, arrayLength, separate, keyType, format); + var meta = new TextureMeta() { - var info = new TextureInfo(cbufSlot, handle + layer * 2, isIndexed, format); - var meta = new TextureMeta() - { - AccurateType = accurateType, - Type = type, - UsageFlags = usageFlags, - }; + AccurateType = accurateType, + Type = type, + UsageFlags = usageFlags, + }; - int binding; + int setIndex; + int binding; - if (dict.TryGetValue(info, out var existingMeta)) + if (dict.TryGetValue(info, out var existingMeta)) + { + dict[info] = MergeTextureMeta(meta, existingMeta); + setIndex = existingMeta.Set; + binding = existingMeta.Binding; + } + else + { + if (arrayLength > 1 && (setIndex = _gpuAccessor.CreateExtraSet()) >= 0) { - dict[info] = MergeTextureMeta(meta, existingMeta); - binding = existingMeta.Binding; + // We reserved an "extra set" for the array. + // In this case the binding is always the first one (0). + // Using separate sets for array is better as we need to do less descriptor set updates. + + binding = 0; } else { bool isBuffer = (type & SamplerType.Mask) == SamplerType.TextureBuffer; - binding = isImage - ? _gpuAccessor.QueryBindingImage(dict.Count, isBuffer) - : _gpuAccessor.QueryBindingTexture(dict.Count, isBuffer); + SetBindingPair setAndBinding = isImage + ? _gpuAccessor.CreateImageBinding(arrayLength, isBuffer) + : _gpuAccessor.CreateTextureBinding(arrayLength, isBuffer); - meta.Binding = binding; - - dict.Add(info, meta); + setIndex = setAndBinding.SetIndex; + binding = setAndBinding.Binding; } - string nameSuffix; + meta.Set = setIndex; + meta.Binding = binding; - if (isImage) - { - nameSuffix = cbufSlot < 0 - ? $"i_tcb_{handle:X}_{format.ToGlslFormat()}" - : $"i_cb{cbufSlot}_{handle:X}_{format.ToGlslFormat()}"; - } - else - { - nameSuffix = cbufSlot < 0 ? $"t_tcb_{handle:X}" : $"t_cb{cbufSlot}_{handle:X}"; - } - - var definition = new TextureDefinition( - isImage ? 3 : 2, - binding, - $"{_stagePrefix}_{nameSuffix}", - meta.Type, - info.Format, - meta.UsageFlags); - - if (isImage) - { - Properties.AddOrUpdateImage(definition); - } - else - { - Properties.AddOrUpdateTexture(definition); - } - - if (layer == 0) - { - firstBinding = binding; - } + dict.Add(info, meta); } - return firstBinding; + string nameSuffix; + string prefix = isImage ? "i" : "t"; + + if (arrayLength != 1 && type != SamplerType.None) + { + prefix += type.ToShortSamplerType(); + } + + if (isImage) + { + nameSuffix = cbufSlot < 0 + ? $"{prefix}_tcb_{handle:X}_{format.ToGlslFormat()}" + : $"{prefix}_cb{cbufSlot}_{handle:X}_{format.ToGlslFormat()}"; + } + else if (type == SamplerType.None) + { + nameSuffix = cbufSlot < 0 ? $"s_tcb_{handle:X}" : $"s_cb{cbufSlot}_{handle:X}"; + } + else + { + nameSuffix = cbufSlot < 0 ? $"{prefix}_tcb_{handle:X}" : $"{prefix}_cb{cbufSlot}_{handle:X}"; + } + + var definition = new TextureDefinition( + setIndex, + binding, + arrayLength, + separate, + $"{_stagePrefix}_{nameSuffix}", + meta.Type, + info.Format, + meta.UsageFlags); + + if (isImage) + { + Properties.AddOrUpdateImage(definition); + } + else + { + Properties.AddOrUpdateTexture(definition); + } + + return new SetBindingPair(setIndex, binding); } private static TextureMeta MergeTextureMeta(TextureMeta meta, TextureMeta existingMeta) { + meta.Set = existingMeta.Set; meta.Binding = existingMeta.Binding; meta.UsageFlags |= existingMeta.UsageFlags; @@ -399,8 +434,7 @@ namespace Ryujinx.Graphics.Shader.Translation selectedMeta.UsageFlags |= TextureUsageFlags.NeedsScaleValue; var dimensions = type.GetDimensions(); - var isIndexed = type.HasFlag(SamplerType.Indexed); - var canScale = _stage.SupportsRenderScale() && !isIndexed && dimensions == 2; + var canScale = _stage.SupportsRenderScale() && selectedInfo.ArrayLength == 1 && dimensions == 2; if (!canScale) { @@ -426,11 +460,11 @@ namespace Ryujinx.Graphics.Shader.Translation for (int slot = 0; slot < _cbSlotToBindingMap.Length; slot++) { - int binding = _cbSlotToBindingMap[slot]; + SetBindingPair setAndBinding = _cbSlotToBindingMap[slot]; - if (binding >= 0 && _usedConstantBufferBindings.Contains(binding)) + if (setAndBinding.Binding >= 0 && _usedConstantBufferBindings.Contains(setAndBinding.Binding)) { - descriptors[descriptorIndex++] = new BufferDescriptor(binding, slot); + descriptors[descriptorIndex++] = new BufferDescriptor(setAndBinding.SetIndex, setAndBinding.Binding, slot); } } @@ -450,13 +484,13 @@ namespace Ryujinx.Graphics.Shader.Translation foreach ((int key, int slot) in _sbSlots) { - int binding = _sbSlotToBindingMap[slot]; + SetBindingPair setAndBinding = _sbSlotToBindingMap[slot]; - if (binding >= 0) + if (setAndBinding.Binding >= 0) { (int sbCbSlot, int sbCbOffset) = UnpackSbCbInfo(key); BufferUsageFlags flags = (_sbSlotWritten & (1u << slot)) != 0 ? BufferUsageFlags.Write : BufferUsageFlags.None; - descriptors[descriptorIndex++] = new BufferDescriptor(binding, slot, sbCbSlot, sbCbOffset, flags); + descriptors[descriptorIndex++] = new BufferDescriptor(setAndBinding.SetIndex, setAndBinding.Binding, slot, sbCbSlot, sbCbOffset, flags); } } @@ -468,34 +502,65 @@ namespace Ryujinx.Graphics.Shader.Translation return descriptors; } - public TextureDescriptor[] GetTextureDescriptors() + public TextureDescriptor[] GetTextureDescriptors(bool includeArrays = true) { - return GetDescriptors(_usedTextures, _usedTextures.Count); + return GetDescriptors(_usedTextures, includeArrays); } - public TextureDescriptor[] GetImageDescriptors() + public TextureDescriptor[] GetImageDescriptors(bool includeArrays = true) { - return GetDescriptors(_usedImages, _usedImages.Count); + return GetDescriptors(_usedImages, includeArrays); } - private static TextureDescriptor[] GetDescriptors(IReadOnlyDictionary usedResources, int count) + private static TextureDescriptor[] GetDescriptors(IReadOnlyDictionary usedResources, bool includeArrays) { - TextureDescriptor[] descriptors = new TextureDescriptor[count]; + List descriptors = new(); - int descriptorIndex = 0; + bool hasAnyArray = false; foreach ((TextureInfo info, TextureMeta meta) in usedResources) { - descriptors[descriptorIndex++] = new TextureDescriptor( + if (info.ArrayLength > 1) + { + hasAnyArray = true; + continue; + } + + descriptors.Add(new TextureDescriptor( + meta.Set, meta.Binding, meta.Type, info.Format, info.CbufSlot, info.Handle, - meta.UsageFlags); + info.ArrayLength, + info.Separate, + meta.UsageFlags)); } - return descriptors; + if (hasAnyArray && includeArrays) + { + foreach ((TextureInfo info, TextureMeta meta) in usedResources) + { + if (info.ArrayLength <= 1) + { + continue; + } + + descriptors.Add(new TextureDescriptor( + meta.Set, + meta.Binding, + meta.Type, + info.Format, + info.CbufSlot, + info.Handle, + info.ArrayLength, + info.Separate, + meta.UsageFlags)); + } + } + + return descriptors.ToArray(); } public bool TryGetCbufSlotAndHandleForTexture(int binding, out int cbufSlot, out int handle) @@ -531,24 +596,37 @@ namespace Ryujinx.Graphics.Shader.Translation return FindDescriptorIndex(GetImageDescriptors(), binding); } - private void AddNewConstantBuffer(int binding, string name) + public bool IsArrayOfTexturesOrImages(int binding, bool isImage) + { + foreach ((TextureInfo info, TextureMeta meta) in isImage ? _usedImages : _usedTextures) + { + if (meta.Binding == binding) + { + return info.ArrayLength != 1; + } + } + + return false; + } + + private void AddNewConstantBuffer(int setIndex, int binding, string name) { StructureType type = new(new[] { new StructureField(AggregateType.Array | AggregateType.Vector4 | AggregateType.FP32, "data", Constants.ConstantBufferSize / 16), }); - Properties.AddOrUpdateConstantBuffer(new(BufferLayout.Std140, 0, binding, name, type)); + Properties.AddOrUpdateConstantBuffer(new(BufferLayout.Std140, setIndex, binding, name, type)); } - private void AddNewStorageBuffer(int binding, string name) + private void AddNewStorageBuffer(int setIndex, int binding, string name) { StructureType type = new(new[] { new StructureField(AggregateType.Array | AggregateType.U32, "data", 0), }); - Properties.AddOrUpdateStorageBuffer(new(BufferLayout.Std430, 1, binding, name, type)); + Properties.AddOrUpdateStorageBuffer(new(BufferLayout.Std430, setIndex, binding, name, type)); } public static string GetShaderStagePrefix(ShaderStage stage) diff --git a/src/Ryujinx.Graphics.Shader/Translation/ResourceReservations.cs b/src/Ryujinx.Graphics.Shader/Translation/ResourceReservations.cs index d559f6699..c89c4d0b6 100644 --- a/src/Ryujinx.Graphics.Shader/Translation/ResourceReservations.cs +++ b/src/Ryujinx.Graphics.Shader/Translation/ResourceReservations.cs @@ -11,6 +11,8 @@ namespace Ryujinx.Graphics.Shader.Translation public const int MaxVertexBufferTextures = 32; + private const int TextureSetIndex = 2; // TODO: Get from GPU accessor. + public int VertexInfoConstantBufferBinding { get; } public int VertexOutputStorageBufferBinding { get; } public int GeometryVertexOutputStorageBufferBinding { get; } @@ -163,6 +165,21 @@ namespace Ryujinx.Graphics.Shader.Translation return _vertexBufferTextureBaseBinding + vaLocation; } + public SetBindingPair GetVertexBufferTextureSetAndBinding(int vaLocation) + { + return new SetBindingPair(TextureSetIndex, GetVertexBufferTextureBinding(vaLocation)); + } + + public SetBindingPair GetIndexBufferTextureSetAndBinding() + { + return new SetBindingPair(TextureSetIndex, IndexBufferTextureBinding); + } + + public SetBindingPair GetTopologyRemapBufferTextureSetAndBinding() + { + return new SetBindingPair(TextureSetIndex, TopologyRemapBufferTextureBinding); + } + internal bool TryGetOffset(StorageKind storageKind, int location, int component, out int offset) { return _offsets.TryGetValue(new IoDefinition(storageKind, IoVariable.UserDefined, location, component), out offset); diff --git a/src/Ryujinx.Graphics.Shader/Translation/ShaderDefinitions.cs b/src/Ryujinx.Graphics.Shader/Translation/ShaderDefinitions.cs index 3246e2594..f831ec940 100644 --- a/src/Ryujinx.Graphics.Shader/Translation/ShaderDefinitions.cs +++ b/src/Ryujinx.Graphics.Shader/Translation/ShaderDefinitions.cs @@ -45,6 +45,8 @@ namespace Ryujinx.Graphics.Shader.Translation public bool YNegateEnabled => _graphicsState.YNegateEnabled; public bool OriginUpperLeft => _graphicsState.OriginUpperLeft; + public bool HalvePrimitiveId => _graphicsState.HalvePrimitiveId; + public ImapPixelType[] ImapTypes { get; } public bool IaIndexing { get; private set; } public bool OaIndexing { get; private set; } diff --git a/src/Ryujinx.Graphics.Shader/Translation/TransformContext.cs b/src/Ryujinx.Graphics.Shader/Translation/TransformContext.cs index 87ebb8e7c..1e87585f1 100644 --- a/src/Ryujinx.Graphics.Shader/Translation/TransformContext.cs +++ b/src/Ryujinx.Graphics.Shader/Translation/TransformContext.cs @@ -9,6 +9,7 @@ namespace Ryujinx.Graphics.Shader.Translation public readonly ShaderDefinitions Definitions; public readonly ResourceManager ResourceManager; public readonly IGpuAccessor GpuAccessor; + public readonly TargetApi TargetApi; public readonly TargetLanguage TargetLanguage; public readonly ShaderStage Stage; public readonly ref FeatureFlags UsedFeatures; @@ -19,6 +20,7 @@ namespace Ryujinx.Graphics.Shader.Translation ShaderDefinitions definitions, ResourceManager resourceManager, IGpuAccessor gpuAccessor, + TargetApi targetApi, TargetLanguage targetLanguage, ShaderStage stage, ref FeatureFlags usedFeatures) @@ -28,6 +30,7 @@ namespace Ryujinx.Graphics.Shader.Translation Definitions = definitions; ResourceManager = resourceManager; GpuAccessor = gpuAccessor; + TargetApi = targetApi; TargetLanguage = targetLanguage; Stage = stage; UsedFeatures = ref usedFeatures; diff --git a/src/Ryujinx.Graphics.Shader/Translation/Transforms/TexturePass.cs b/src/Ryujinx.Graphics.Shader/Translation/Transforms/TexturePass.cs index 495ea8a94..6ba8cb44a 100644 --- a/src/Ryujinx.Graphics.Shader/Translation/Transforms/TexturePass.cs +++ b/src/Ryujinx.Graphics.Shader/Translation/Transforms/TexturePass.cs @@ -23,7 +23,7 @@ namespace Ryujinx.Graphics.Shader.Translation.Transforms { node = InsertCoordNormalization(context.Hfm, node, context.ResourceManager, context.GpuAccessor, context.Stage); node = InsertCoordGatherBias(node, context.ResourceManager, context.GpuAccessor); - node = InsertConstOffsets(node, context.GpuAccessor, context.Stage); + node = InsertConstOffsets(node, context.ResourceManager, context.GpuAccessor, context.Stage); if (texOp.Type == SamplerType.TextureBuffer && !context.GpuAccessor.QueryHostSupportsSnormBufferTextureFormat()) { @@ -45,13 +45,9 @@ namespace Ryujinx.Graphics.Shader.Translation.Transforms bool isBindless = (texOp.Flags & TextureFlags.Bindless) != 0; bool intCoords = (texOp.Flags & TextureFlags.IntCoords) != 0; - bool isIndexed = (texOp.Type & SamplerType.Indexed) != 0; - - int coordsCount = texOp.Type.GetDimensions(); - - int coordsIndex = isBindless || isIndexed ? 1 : 0; bool isImage = IsImageInstructionWithScale(texOp.Inst); + bool isIndexed = resourceManager.IsArrayOfTexturesOrImages(texOp.Binding, isImage); if ((texOp.Inst == Instruction.TextureSample || isImage) && (intCoords || isImage) && @@ -62,9 +58,12 @@ namespace Ryujinx.Graphics.Shader.Translation.Transforms { int functionId = hfm.GetOrCreateFunctionId(HelperFunctionName.TexelFetchScale); int samplerIndex = isImage - ? resourceManager.GetTextureDescriptors().Length + resourceManager.FindImageDescriptorIndex(texOp.Binding) + ? resourceManager.GetTextureDescriptors(includeArrays: false).Length + resourceManager.FindImageDescriptorIndex(texOp.Binding) : resourceManager.FindTextureDescriptorIndex(texOp.Binding); + int coordsCount = texOp.Type.GetDimensions(); + int coordsIndex = isBindless ? 1 : 0; + for (int index = 0; index < coordsCount; index++) { Operand scaledCoord = Local(); @@ -97,7 +96,7 @@ namespace Ryujinx.Graphics.Shader.Translation.Transforms TextureOperation texOp = (TextureOperation)node.Value; bool isBindless = (texOp.Flags & TextureFlags.Bindless) != 0; - bool isIndexed = (texOp.Type & SamplerType.Indexed) != 0; + bool isIndexed = resourceManager.IsArrayOfTexturesOrImages(texOp.Binding, isImage: false); if (texOp.Inst == Instruction.TextureQuerySize && texOp.Index < 2 && @@ -152,8 +151,9 @@ namespace Ryujinx.Graphics.Shader.Translation.Transforms TextureOperation texOp = (TextureOperation)node.Value; bool isBindless = (texOp.Flags & TextureFlags.Bindless) != 0; + bool isIndexed = resourceManager.IsArrayOfTexturesOrImages(texOp.Binding, isImage: false); - if (isBindless || !resourceManager.TryGetCbufSlotAndHandleForTexture(texOp.Binding, out int cbufSlot, out int handle)) + if (isBindless || isIndexed || !resourceManager.TryGetCbufSlotAndHandleForTexture(texOp.Binding, out int cbufSlot, out int handle)) { return node; } @@ -167,10 +167,7 @@ namespace Ryujinx.Graphics.Shader.Translation.Transforms return node; } - bool isIndexed = (texOp.Type & SamplerType.Indexed) != 0; - int coordsCount = texOp.Type.GetDimensions(); - int coordsIndex = isBindless || isIndexed ? 1 : 0; int normCoordsCount = (texOp.Type & SamplerType.Mask) == SamplerType.TextureCube ? 2 : coordsCount; @@ -178,22 +175,14 @@ namespace Ryujinx.Graphics.Shader.Translation.Transforms { Operand coordSize = Local(); - Operand[] texSizeSources; - - if (isBindless || isIndexed) - { - texSizeSources = new Operand[] { texOp.GetSource(0), Const(0) }; - } - else - { - texSizeSources = new Operand[] { Const(0) }; - } + Operand[] texSizeSources = new Operand[] { Const(0) }; LinkedListNode textureSizeNode = node.List.AddBefore(node, new TextureOperation( Instruction.TextureQuerySize, texOp.Type, texOp.Format, texOp.Flags, + texOp.Set, texOp.Binding, index, new[] { coordSize }, @@ -201,13 +190,13 @@ namespace Ryujinx.Graphics.Shader.Translation.Transforms resourceManager.SetUsageFlagsForTextureQuery(texOp.Binding, texOp.Type); - Operand source = texOp.GetSource(coordsIndex + index); + Operand source = texOp.GetSource(index); Operand coordNormalized = Local(); node.List.AddBefore(node, new Operation(Instruction.FP32 | Instruction.Divide, coordNormalized, source, GenerateI2f(node, coordSize))); - texOp.SetSource(coordsIndex + index, coordNormalized); + texOp.SetSource(index, coordNormalized); InsertTextureSizeUnscale(hfm, textureSizeNode, resourceManager, stage); } @@ -234,7 +223,7 @@ namespace Ryujinx.Graphics.Shader.Translation.Transforms return node; } - bool isIndexed = (texOp.Type & SamplerType.Indexed) != 0; + bool isIndexed = resourceManager.IsArrayOfTexturesOrImages(texOp.Binding, isImage: false); int coordsCount = texOp.Type.GetDimensions(); int coordsIndex = isBindless || isIndexed ? 1 : 0; @@ -263,6 +252,7 @@ namespace Ryujinx.Graphics.Shader.Translation.Transforms texOp.Type, texOp.Format, texOp.Flags, + texOp.Set, texOp.Binding, index, new[] { coordSize }, @@ -287,7 +277,7 @@ namespace Ryujinx.Graphics.Shader.Translation.Transforms return node; } - private static LinkedListNode InsertConstOffsets(LinkedListNode node, IGpuAccessor gpuAccessor, ShaderStage stage) + private static LinkedListNode InsertConstOffsets(LinkedListNode node, ResourceManager resourceManager, IGpuAccessor gpuAccessor, ShaderStage stage) { // Non-constant texture offsets are not allowed (according to the spec), // however some GPUs does support that. @@ -321,7 +311,6 @@ namespace Ryujinx.Graphics.Shader.Translation.Transforms bool hasLodLevel = (texOp.Flags & TextureFlags.LodLevel) != 0; bool isArray = (texOp.Type & SamplerType.Array) != 0; - bool isIndexed = (texOp.Type & SamplerType.Indexed) != 0; bool isMultisample = (texOp.Type & SamplerType.Multisample) != 0; bool isShadow = (texOp.Type & SamplerType.Shadow) != 0; @@ -342,6 +331,8 @@ namespace Ryujinx.Graphics.Shader.Translation.Transforms offsetsCount = 0; } + bool isIndexed = resourceManager.IsArrayOfTexturesOrImages(texOp.Binding, isImage: false); + Operand[] offsets = new Operand[offsetsCount]; Operand[] sources = new Operand[texOp.SourcesCount - offsetsCount]; @@ -482,6 +473,7 @@ namespace Ryujinx.Graphics.Shader.Translation.Transforms texOp.Type, texOp.Format, texOp.Flags & ~(TextureFlags.Offset | TextureFlags.Offsets), + texOp.Set, texOp.Binding, 1 << 3, // W component: i=0, j=0 new[] { dests[destIndex++] }, @@ -538,6 +530,7 @@ namespace Ryujinx.Graphics.Shader.Translation.Transforms texOp.Type, texOp.Format, texOp.Flags & ~(TextureFlags.Offset | TextureFlags.Offsets), + texOp.Set, texOp.Binding, componentIndex, dests, @@ -584,6 +577,7 @@ namespace Ryujinx.Graphics.Shader.Translation.Transforms texOp.Type, texOp.Format, texOp.Flags, + texOp.Set, texOp.Binding, index, new[] { texSizes[index] }, @@ -614,6 +608,7 @@ namespace Ryujinx.Graphics.Shader.Translation.Transforms texOp.Type, texOp.Format, texOp.Flags, + texOp.Set, texOp.Binding, 0, new[] { lod }, @@ -644,6 +639,7 @@ namespace Ryujinx.Graphics.Shader.Translation.Transforms texOp.Type, texOp.Format, texOp.Flags, + texOp.Set, texOp.Binding, index, new[] { texSizes[index] }, diff --git a/src/Ryujinx.Graphics.Shader/Translation/Transforms/VertexToCompute.cs b/src/Ryujinx.Graphics.Shader/Translation/Transforms/VertexToCompute.cs index d71ada865..ddd2134d2 100644 --- a/src/Ryujinx.Graphics.Shader/Translation/Transforms/VertexToCompute.cs +++ b/src/Ryujinx.Graphics.Shader/Translation/Transforms/VertexToCompute.cs @@ -54,6 +54,7 @@ namespace Ryujinx.Graphics.Shader.Translation.Transforms { bool needsSextNorm = context.Definitions.IsAttributePackedRgb10A2Signed(location); + SetBindingPair setAndBinding = context.ResourceManager.Reservations.GetVertexBufferTextureSetAndBinding(location); Operand temp = needsSextNorm ? Local() : dest; Operand vertexElemOffset = GenerateVertexOffset(context.ResourceManager, node, location, 0); @@ -62,7 +63,8 @@ namespace Ryujinx.Graphics.Shader.Translation.Transforms SamplerType.TextureBuffer, TextureFormat.Unknown, TextureFlags.IntCoords, - context.ResourceManager.Reservations.GetVertexBufferTextureBinding(location), + setAndBinding.SetIndex, + setAndBinding.Binding, 1 << component, new[] { temp }, new[] { vertexElemOffset })); @@ -75,6 +77,7 @@ namespace Ryujinx.Graphics.Shader.Translation.Transforms } else { + SetBindingPair setAndBinding = context.ResourceManager.Reservations.GetVertexBufferTextureSetAndBinding(location); Operand temp = component > 0 ? Local() : dest; Operand vertexElemOffset = GenerateVertexOffset(context.ResourceManager, node, location, component); @@ -83,7 +86,8 @@ namespace Ryujinx.Graphics.Shader.Translation.Transforms SamplerType.TextureBuffer, TextureFormat.Unknown, TextureFlags.IntCoords, - context.ResourceManager.Reservations.GetVertexBufferTextureBinding(location), + setAndBinding.SetIndex, + setAndBinding.Binding, 1, new[] { temp }, new[] { vertexElemOffset })); diff --git a/src/Ryujinx.Graphics.Shader/Translation/Translator.cs b/src/Ryujinx.Graphics.Shader/Translation/Translator.cs index 6a31ea2e7..d1fbca0eb 100644 --- a/src/Ryujinx.Graphics.Shader/Translation/Translator.cs +++ b/src/Ryujinx.Graphics.Shader/Translation/Translator.cs @@ -190,7 +190,7 @@ namespace Ryujinx.Graphics.Shader.Translation if (stage == ShaderStage.Vertex) { - InitializePositionOutput(context); + InitializeVertexOutputs(context); } UInt128 usedAttributes = context.TranslatorContext.AttributeUsage.NextInputAttributesComponents; @@ -236,12 +236,20 @@ namespace Ryujinx.Graphics.Shader.Translation } } - private static void InitializePositionOutput(EmitterContext context) + private static void InitializeVertexOutputs(EmitterContext context) { for (int c = 0; c < 4; c++) { context.Store(StorageKind.Output, IoVariable.Position, null, Const(c), ConstF(c == 3 ? 1f : 0f)); } + + if (context.Program.ClipDistancesWritten != 0) + { + for (int i = 0; i < 8; i++) + { + context.Store(StorageKind.Output, IoVariable.ClipDistance, null, Const(i), ConstF(0f)); + } + } } private static void InitializeOutput(EmitterContext context, int location, bool perPatch) diff --git a/src/Ryujinx.Graphics.Shader/Translation/TranslatorContext.cs b/src/Ryujinx.Graphics.Shader/Translation/TranslatorContext.cs index a193ab3c4..a579433f9 100644 --- a/src/Ryujinx.Graphics.Shader/Translation/TranslatorContext.cs +++ b/src/Ryujinx.Graphics.Shader/Translation/TranslatorContext.cs @@ -294,6 +294,7 @@ namespace Ryujinx.Graphics.Shader.Translation Definitions, resourceManager, GpuAccessor, + Options.TargetApi, Options.TargetLanguage, Definitions.Stage, ref usedFeatures); @@ -362,6 +363,7 @@ namespace Ryujinx.Graphics.Shader.Translation GpuAccessor.QueryHostSupportsGeometryShaderPassthrough(), GpuAccessor.QueryHostSupportsShaderBallot(), GpuAccessor.QueryHostSupportsShaderBarrierDivergence(), + GpuAccessor.QueryHostSupportsShaderFloat64(), GpuAccessor.QueryHostSupportsTextureShadowLod(), GpuAccessor.QueryHostSupportsViewportMask()); @@ -411,8 +413,8 @@ namespace Ryujinx.Graphics.Shader.Translation if (Stage == ShaderStage.Vertex) { - int ibBinding = resourceManager.Reservations.IndexBufferTextureBinding; - TextureDefinition indexBuffer = new(2, ibBinding, "ib_data", SamplerType.TextureBuffer, TextureFormat.Unknown, TextureUsageFlags.None); + SetBindingPair ibSetAndBinding = resourceManager.Reservations.GetIndexBufferTextureSetAndBinding(); + TextureDefinition indexBuffer = new(ibSetAndBinding.SetIndex, ibSetAndBinding.Binding, "ib_data", SamplerType.TextureBuffer); resourceManager.Properties.AddOrUpdateTexture(indexBuffer); int inputMap = _program.AttributeUsage.UsedInputAttributes; @@ -420,8 +422,8 @@ namespace Ryujinx.Graphics.Shader.Translation while (inputMap != 0) { int location = BitOperations.TrailingZeroCount(inputMap); - int binding = resourceManager.Reservations.GetVertexBufferTextureBinding(location); - TextureDefinition vaBuffer = new(2, binding, $"vb_data{location}", SamplerType.TextureBuffer, TextureFormat.Unknown, TextureUsageFlags.None); + SetBindingPair setAndBinding = resourceManager.Reservations.GetVertexBufferTextureSetAndBinding(location); + TextureDefinition vaBuffer = new(setAndBinding.SetIndex, setAndBinding.Binding, $"vb_data{location}", SamplerType.TextureBuffer); resourceManager.Properties.AddOrUpdateTexture(vaBuffer); inputMap &= ~(1 << location); @@ -429,8 +431,8 @@ namespace Ryujinx.Graphics.Shader.Translation } else if (Stage == ShaderStage.Geometry) { - int trbBinding = resourceManager.Reservations.TopologyRemapBufferTextureBinding; - TextureDefinition remapBuffer = new(2, trbBinding, "trb_data", SamplerType.TextureBuffer, TextureFormat.Unknown, TextureUsageFlags.None); + SetBindingPair trbSetAndBinding = resourceManager.Reservations.GetTopologyRemapBufferTextureSetAndBinding(); + TextureDefinition remapBuffer = new(trbSetAndBinding.SetIndex, trbSetAndBinding.Binding, "trb_data", SamplerType.TextureBuffer); resourceManager.Properties.AddOrUpdateTexture(remapBuffer); int geometryVbOutputSbBinding = resourceManager.Reservations.GeometryVertexOutputStorageBufferBinding; diff --git a/src/Ryujinx.Graphics.Texture/Astc/AstcDecoder.cs b/src/Ryujinx.Graphics.Texture/Astc/AstcDecoder.cs index 3f65e1225..92e39d2e0 100644 --- a/src/Ryujinx.Graphics.Texture/Astc/AstcDecoder.cs +++ b/src/Ryujinx.Graphics.Texture/Astc/AstcDecoder.cs @@ -1,7 +1,6 @@ using Ryujinx.Common.Memory; using Ryujinx.Common.Utilities; using System; -using System.Buffers; using System.Diagnostics; using System.Linq; using System.Runtime.CompilerServices; @@ -293,9 +292,9 @@ namespace Ryujinx.Graphics.Texture.Astc int depth, int levels, int layers, - out IMemoryOwner decoded) + out MemoryOwner decoded) { - decoded = ByteMemoryPool.Rent(QueryDecompressedSize(width, height, depth, levels, layers)); + decoded = MemoryOwner.Rent(QueryDecompressedSize(width, height, depth, levels, layers)); AstcDecoder decoder = new(data, decoded.Memory, blockWidth, blockHeight, width, height, depth, levels, layers); diff --git a/src/Ryujinx.Graphics.Texture/Astc/IntegerEncoded.cs b/src/Ryujinx.Graphics.Texture/Astc/IntegerEncoded.cs index fedd90ee2..dc99de2b6 100644 --- a/src/Ryujinx.Graphics.Texture/Astc/IntegerEncoded.cs +++ b/src/Ryujinx.Graphics.Texture/Astc/IntegerEncoded.cs @@ -3,7 +3,7 @@ using System.Numerics; namespace Ryujinx.Graphics.Texture.Astc { - internal struct IntegerEncoded + internal struct IntegerEncoded(IntegerEncoded.EIntegerEncoding encoding, int numBits) { internal const int StructSize = 8; private static readonly IntegerEncoded[] _encodings; @@ -15,11 +15,11 @@ namespace Ryujinx.Graphics.Texture.Astc Trit, } - readonly EIntegerEncoding _encoding; - public byte NumberBits { get; private set; } - public byte TritValue { get; private set; } - public byte QuintValue { get; private set; } - public int BitValue { get; private set; } + readonly EIntegerEncoding _encoding = encoding; + public byte NumberBits { get; } = (byte)numBits; + public byte TritValue { get; private set; } = 0; + public byte QuintValue { get; private set; } = 0; + public int BitValue { get; private set; } = 0; static IntegerEncoded() { @@ -31,15 +31,6 @@ namespace Ryujinx.Graphics.Texture.Astc } } - public IntegerEncoded(EIntegerEncoding encoding, int numBits) - { - _encoding = encoding; - NumberBits = (byte)numBits; - BitValue = 0; - TritValue = 0; - QuintValue = 0; - } - public readonly bool MatchesEncoding(IntegerEncoded other) { return _encoding == other._encoding && NumberBits == other.NumberBits; diff --git a/src/Ryujinx.Graphics.Texture/BC6Decoder.cs b/src/Ryujinx.Graphics.Texture/BC6Decoder.cs index ae33e9cf9..f4d933643 100644 --- a/src/Ryujinx.Graphics.Texture/BC6Decoder.cs +++ b/src/Ryujinx.Graphics.Texture/BC6Decoder.cs @@ -45,7 +45,7 @@ namespace Ryujinx.Graphics.Texture if (subsetCount == 0) { // Mode is invalid, the spec mandates that hardware fills the block with - // a opaque black color. + // an opaque black color. for (int ty = 0; ty < h; ty++) { int baseOffs = ty * width; diff --git a/src/Ryujinx.Graphics.Texture/BCnDecoder.cs b/src/Ryujinx.Graphics.Texture/BCnDecoder.cs index fef3b1f03..d7b1f0fa9 100644 --- a/src/Ryujinx.Graphics.Texture/BCnDecoder.cs +++ b/src/Ryujinx.Graphics.Texture/BCnDecoder.cs @@ -1,7 +1,6 @@ using Ryujinx.Common; using Ryujinx.Common.Memory; using System; -using System.Buffers; using System.Buffers.Binary; using System.Runtime.InteropServices; using System.Runtime.Intrinsics; @@ -14,7 +13,7 @@ namespace Ryujinx.Graphics.Texture private const int BlockWidth = 4; private const int BlockHeight = 4; - public static IMemoryOwner DecodeBC1(ReadOnlySpan data, int width, int height, int depth, int levels, int layers) + public static MemoryOwner DecodeBC1(ReadOnlySpan data, int width, int height, int depth, int levels, int layers) { int size = 0; @@ -23,12 +22,12 @@ namespace Ryujinx.Graphics.Texture size += Math.Max(1, width >> l) * Math.Max(1, height >> l) * Math.Max(1, depth >> l) * layers * 4; } - IMemoryOwner output = ByteMemoryPool.Rent(size); + MemoryOwner output = MemoryOwner.Rent(size); Span tile = stackalloc byte[BlockWidth * BlockHeight * 4]; Span tileAsUint = MemoryMarshal.Cast(tile); - Span outputAsUint = MemoryMarshal.Cast(output.Memory.Span); + Span outputAsUint = MemoryMarshal.Cast(output.Span); Span> tileAsVector128 = MemoryMarshal.Cast>(tile); @@ -102,7 +101,7 @@ namespace Ryujinx.Graphics.Texture return output; } - public static IMemoryOwner DecodeBC2(ReadOnlySpan data, int width, int height, int depth, int levels, int layers) + public static MemoryOwner DecodeBC2(ReadOnlySpan data, int width, int height, int depth, int levels, int layers) { int size = 0; @@ -111,12 +110,12 @@ namespace Ryujinx.Graphics.Texture size += Math.Max(1, width >> l) * Math.Max(1, height >> l) * Math.Max(1, depth >> l) * layers * 4; } - IMemoryOwner output = ByteMemoryPool.Rent(size); + MemoryOwner output = MemoryOwner.Rent(size); Span tile = stackalloc byte[BlockWidth * BlockHeight * 4]; Span tileAsUint = MemoryMarshal.Cast(tile); - Span outputAsUint = MemoryMarshal.Cast(output.Memory.Span); + Span outputAsUint = MemoryMarshal.Cast(output.Span); Span> tileAsVector128 = MemoryMarshal.Cast>(tile); @@ -197,7 +196,7 @@ namespace Ryujinx.Graphics.Texture return output; } - public static IMemoryOwner DecodeBC3(ReadOnlySpan data, int width, int height, int depth, int levels, int layers) + public static MemoryOwner DecodeBC3(ReadOnlySpan data, int width, int height, int depth, int levels, int layers) { int size = 0; @@ -206,13 +205,13 @@ namespace Ryujinx.Graphics.Texture size += Math.Max(1, width >> l) * Math.Max(1, height >> l) * Math.Max(1, depth >> l) * layers * 4; } - IMemoryOwner output = ByteMemoryPool.Rent(size); + MemoryOwner output = MemoryOwner.Rent(size); Span tile = stackalloc byte[BlockWidth * BlockHeight * 4]; Span rPal = stackalloc byte[8]; Span tileAsUint = MemoryMarshal.Cast(tile); - Span outputAsUint = MemoryMarshal.Cast(output.Memory.Span); + Span outputAsUint = MemoryMarshal.Cast(output.Span); Span> tileAsVector128 = MemoryMarshal.Cast>(tile); @@ -294,7 +293,7 @@ namespace Ryujinx.Graphics.Texture return output; } - public static IMemoryOwner DecodeBC4(ReadOnlySpan data, int width, int height, int depth, int levels, int layers, bool signed) + public static MemoryOwner DecodeBC4(ReadOnlySpan data, int width, int height, int depth, int levels, int layers, bool signed) { int size = 0; @@ -306,8 +305,8 @@ namespace Ryujinx.Graphics.Texture // Backends currently expect a stride alignment of 4 bytes, so output width must be aligned. int alignedWidth = BitUtils.AlignUp(width, 4); - IMemoryOwner output = ByteMemoryPool.Rent(size); - Span outputSpan = output.Memory.Span; + MemoryOwner output = MemoryOwner.Rent(size); + Span outputSpan = output.Span; ReadOnlySpan data64 = MemoryMarshal.Cast(data); @@ -402,7 +401,7 @@ namespace Ryujinx.Graphics.Texture return output; } - public static IMemoryOwner DecodeBC5(ReadOnlySpan data, int width, int height, int depth, int levels, int layers, bool signed) + public static MemoryOwner DecodeBC5(ReadOnlySpan data, int width, int height, int depth, int levels, int layers, bool signed) { int size = 0; @@ -414,7 +413,7 @@ namespace Ryujinx.Graphics.Texture // Backends currently expect a stride alignment of 4 bytes, so output width must be aligned. int alignedWidth = BitUtils.AlignUp(width, 2); - IMemoryOwner output = ByteMemoryPool.Rent(size); + MemoryOwner output = MemoryOwner.Rent(size); ReadOnlySpan data64 = MemoryMarshal.Cast(data); @@ -423,7 +422,7 @@ namespace Ryujinx.Graphics.Texture Span rPal = stackalloc byte[8]; Span gPal = stackalloc byte[8]; - Span outputAsUshort = MemoryMarshal.Cast(output.Memory.Span); + Span outputAsUshort = MemoryMarshal.Cast(output.Span); Span rTileAsUint = MemoryMarshal.Cast(rTile); Span gTileAsUint = MemoryMarshal.Cast(gTile); @@ -527,7 +526,7 @@ namespace Ryujinx.Graphics.Texture return output; } - public static IMemoryOwner DecodeBC6(ReadOnlySpan data, int width, int height, int depth, int levels, int layers, bool signed) + public static MemoryOwner DecodeBC6(ReadOnlySpan data, int width, int height, int depth, int levels, int layers, bool signed) { int size = 0; @@ -536,7 +535,8 @@ namespace Ryujinx.Graphics.Texture size += Math.Max(1, width >> l) * Math.Max(1, height >> l) * Math.Max(1, depth >> l) * layers * 8; } - IMemoryOwner output = ByteMemoryPool.Rent(size); + MemoryOwner output = MemoryOwner.Rent(size); + Span outputSpan = output.Span; int inputOffset = 0; int outputOffset = 0; @@ -550,7 +550,7 @@ namespace Ryujinx.Graphics.Texture { for (int z = 0; z < depth; z++) { - BC6Decoder.Decode(output.Memory.Span[outputOffset..], data[inputOffset..], width, height, signed); + BC6Decoder.Decode(outputSpan[outputOffset..], data[inputOffset..], width, height, signed); inputOffset += w * h * 16; outputOffset += width * height * 8; @@ -565,7 +565,7 @@ namespace Ryujinx.Graphics.Texture return output; } - public static IMemoryOwner DecodeBC7(ReadOnlySpan data, int width, int height, int depth, int levels, int layers) + public static MemoryOwner DecodeBC7(ReadOnlySpan data, int width, int height, int depth, int levels, int layers) { int size = 0; @@ -574,7 +574,8 @@ namespace Ryujinx.Graphics.Texture size += Math.Max(1, width >> l) * Math.Max(1, height >> l) * Math.Max(1, depth >> l) * layers * 4; } - IMemoryOwner output = ByteMemoryPool.Rent(size); + MemoryOwner output = MemoryOwner.Rent(size); + Span outputSpan = output.Span; int inputOffset = 0; int outputOffset = 0; @@ -588,7 +589,7 @@ namespace Ryujinx.Graphics.Texture { for (int z = 0; z < depth; z++) { - BC7Decoder.Decode(output.Memory.Span[outputOffset..], data[inputOffset..], width, height); + BC7Decoder.Decode(outputSpan[outputOffset..], data[inputOffset..], width, height); inputOffset += w * h * 16; outputOffset += width * height * 4; diff --git a/src/Ryujinx.Graphics.Texture/BCnEncoder.cs b/src/Ryujinx.Graphics.Texture/BCnEncoder.cs index 74df038b3..4db8a182b 100644 --- a/src/Ryujinx.Graphics.Texture/BCnEncoder.cs +++ b/src/Ryujinx.Graphics.Texture/BCnEncoder.cs @@ -2,7 +2,6 @@ using Ryujinx.Common; using Ryujinx.Common.Memory; using Ryujinx.Graphics.Texture.Encoders; using System; -using System.Buffers; namespace Ryujinx.Graphics.Texture { @@ -11,7 +10,7 @@ namespace Ryujinx.Graphics.Texture private const int BlockWidth = 4; private const int BlockHeight = 4; - public static IMemoryOwner EncodeBC7(Memory data, int width, int height, int depth, int levels, int layers) + public static MemoryOwner EncodeBC7(Memory data, int width, int height, int depth, int levels, int layers) { int size = 0; @@ -23,7 +22,8 @@ namespace Ryujinx.Graphics.Texture size += w * h * 16 * Math.Max(1, depth >> l) * layers; } - IMemoryOwner output = ByteMemoryPool.Rent(size); + MemoryOwner output = MemoryOwner.Rent(size); + Memory outputMemory = output.Memory; int imageBaseIOffs = 0; int imageBaseOOffs = 0; @@ -38,7 +38,7 @@ namespace Ryujinx.Graphics.Texture for (int z = 0; z < depth; z++) { BC7Encoder.Encode( - output.Memory[imageBaseOOffs..], + outputMemory[imageBaseOOffs..], data[imageBaseIOffs..], width, height, diff --git a/src/Ryujinx.Graphics.Texture/ETC2Decoder.cs b/src/Ryujinx.Graphics.Texture/ETC2Decoder.cs index 52801ff47..49e7154c8 100644 --- a/src/Ryujinx.Graphics.Texture/ETC2Decoder.cs +++ b/src/Ryujinx.Graphics.Texture/ETC2Decoder.cs @@ -1,7 +1,6 @@ using Ryujinx.Common; using Ryujinx.Common.Memory; using System; -using System.Buffers; using System.Buffers.Binary; using System.Runtime.InteropServices; @@ -51,15 +50,15 @@ namespace Ryujinx.Graphics.Texture new int[] { -3, -5, -7, -9, 2, 4, 6, 8 }, }; - public static IMemoryOwner DecodeRgb(ReadOnlySpan data, int width, int height, int depth, int levels, int layers) + public static MemoryOwner DecodeRgb(ReadOnlySpan data, int width, int height, int depth, int levels, int layers) { ReadOnlySpan dataUlong = MemoryMarshal.Cast(data); int inputOffset = 0; - IMemoryOwner output = ByteMemoryPool.Rent(CalculateOutputSize(width, height, depth, levels, layers)); + MemoryOwner output = MemoryOwner.Rent(CalculateOutputSize(width, height, depth, levels, layers)); - Span outputUint = MemoryMarshal.Cast(output.Memory.Span); + Span outputUint = MemoryMarshal.Cast(output.Span); Span tile = stackalloc uint[BlockWidth * BlockHeight]; int imageBaseOOffs = 0; @@ -113,15 +112,15 @@ namespace Ryujinx.Graphics.Texture return output; } - public static IMemoryOwner DecodePta(ReadOnlySpan data, int width, int height, int depth, int levels, int layers) + public static MemoryOwner DecodePta(ReadOnlySpan data, int width, int height, int depth, int levels, int layers) { ReadOnlySpan dataUlong = MemoryMarshal.Cast(data); int inputOffset = 0; - IMemoryOwner output = ByteMemoryPool.Rent(CalculateOutputSize(width, height, depth, levels, layers)); + MemoryOwner output = MemoryOwner.Rent(CalculateOutputSize(width, height, depth, levels, layers)); - Span outputUint = MemoryMarshal.Cast(output.Memory.Span); + Span outputUint = MemoryMarshal.Cast(output.Span); Span tile = stackalloc uint[BlockWidth * BlockHeight]; int imageBaseOOffs = 0; @@ -170,15 +169,15 @@ namespace Ryujinx.Graphics.Texture return output; } - public static IMemoryOwner DecodeRgba(ReadOnlySpan data, int width, int height, int depth, int levels, int layers) + public static MemoryOwner DecodeRgba(ReadOnlySpan data, int width, int height, int depth, int levels, int layers) { ReadOnlySpan dataUlong = MemoryMarshal.Cast(data); int inputOffset = 0; - IMemoryOwner output = ByteMemoryPool.Rent(CalculateOutputSize(width, height, depth, levels, layers)); + MemoryOwner output = MemoryOwner.Rent(CalculateOutputSize(width, height, depth, levels, layers)); - Span outputUint = MemoryMarshal.Cast(output.Memory.Span); + Span outputUint = MemoryMarshal.Cast(output.Span); Span tile = stackalloc uint[BlockWidth * BlockHeight]; int imageBaseOOffs = 0; diff --git a/src/Ryujinx.Graphics.Texture/LayoutConverter.cs b/src/Ryujinx.Graphics.Texture/LayoutConverter.cs index d6732674b..5426af205 100644 --- a/src/Ryujinx.Graphics.Texture/LayoutConverter.cs +++ b/src/Ryujinx.Graphics.Texture/LayoutConverter.cs @@ -1,7 +1,6 @@ using Ryujinx.Common; using Ryujinx.Common.Memory; using System; -using System.Buffers; using System.Runtime.Intrinsics; using static Ryujinx.Graphics.Texture.BlockLinearConstants; @@ -95,7 +94,7 @@ namespace Ryujinx.Graphics.Texture }; } - public static IMemoryOwner ConvertBlockLinearToLinear( + public static MemoryOwner ConvertBlockLinearToLinear( int width, int height, int depth, @@ -121,8 +120,8 @@ namespace Ryujinx.Graphics.Texture blockHeight, bytesPerPixel); - IMemoryOwner outputOwner = ByteMemoryPool.Rent(outSize); - Span output = outputOwner.Memory.Span; + MemoryOwner outputOwner = MemoryOwner.Rent(outSize); + Span output = outputOwner.Span; int outOffs = 0; @@ -249,7 +248,7 @@ namespace Ryujinx.Graphics.Texture return outputOwner; } - public static IMemoryOwner ConvertLinearStridedToLinear( + public static MemoryOwner ConvertLinearStridedToLinear( int width, int height, int blockWidth, @@ -265,8 +264,8 @@ namespace Ryujinx.Graphics.Texture int outStride = BitUtils.AlignUp(w * bytesPerPixel, HostStrideAlignment); lineSize = Math.Min(lineSize, outStride); - IMemoryOwner output = ByteMemoryPool.Rent(h * outStride); - Span outSpan = output.Memory.Span; + MemoryOwner output = MemoryOwner.Rent(h * outStride); + Span outSpan = output.Span; int outOffs = 0; int inOffs = 0; diff --git a/src/Ryujinx.Graphics.Texture/OffsetCalculator.cs b/src/Ryujinx.Graphics.Texture/OffsetCalculator.cs index 48d36cdfb..e519341e6 100644 --- a/src/Ryujinx.Graphics.Texture/OffsetCalculator.cs +++ b/src/Ryujinx.Graphics.Texture/OffsetCalculator.cs @@ -15,7 +15,7 @@ namespace Ryujinx.Graphics.Texture private readonly BlockLinearLayout _layoutConverter; - // Variables for built in iteration. + // Variables for built-in iteration. private int _yPart; public OffsetCalculator( @@ -73,69 +73,50 @@ namespace Ryujinx.Graphics.Texture public int GetOffset(int x, int y) { if (_isLinear) - { return x * _bytesPerPixel + y * _stride; - } - else - { - return _layoutConverter.GetOffset(x, y, 0); - } + + return _layoutConverter.GetOffset(x, y, 0); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public int GetOffset(int x) { if (_isLinear) - { return x * _bytesPerPixel + _yPart; - } - else - { - return _layoutConverter.GetOffset(x); - } + + return _layoutConverter.GetOffset(x); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public int GetOffsetWithLineOffset64(int x) { if (_isLinear) - { return x + _yPart; - } - else - { - return _layoutConverter.GetOffsetWithLineOffset64(x); - } + + return _layoutConverter.GetOffsetWithLineOffset64(x); } public (int offset, int size) GetRectangleRange(int x, int y, int width, int height) { - if (_isLinear) - { - int start = y * Math.Abs(_stride) + x * _bytesPerPixel; - int end = (y + height - 1) * Math.Abs(_stride) + (x + width) * _bytesPerPixel; - return (y * _stride + x * _bytesPerPixel, end - start); - } - else - { + if (!_isLinear) return _layoutConverter.GetRectangleRange(x, y, width, height); - } + + int start = y * Math.Abs(_stride) + x * _bytesPerPixel; + int end = (y + height - 1) * Math.Abs(_stride) + (x + width) * _bytesPerPixel; + return (y * _stride + x * _bytesPerPixel, end - start); } public bool LayoutMatches(OffsetCalculator other) { if (_isLinear) - { return other._isLinear && _width == other._width && _height == other._height && _stride == other._stride && _bytesPerPixel == other._bytesPerPixel; - } - else - { - return !other._isLinear && _layoutConverter.LayoutMatches(other._layoutConverter); - } + + + return !other._isLinear && _layoutConverter.LayoutMatches(other._layoutConverter); } } } diff --git a/src/Ryujinx.Graphics.Texture/PixelConverter.cs b/src/Ryujinx.Graphics.Texture/PixelConverter.cs index 27d134074..3676d9199 100644 --- a/src/Ryujinx.Graphics.Texture/PixelConverter.cs +++ b/src/Ryujinx.Graphics.Texture/PixelConverter.cs @@ -1,7 +1,6 @@ using Ryujinx.Common; using Ryujinx.Common.Memory; using System; -using System.Buffers; using System.Runtime.InteropServices; using System.Runtime.Intrinsics; using System.Runtime.Intrinsics.X86; @@ -21,13 +20,14 @@ namespace Ryujinx.Graphics.Texture return (remainder, outRemainder, length / stride); } - public unsafe static IMemoryOwner ConvertR4G4ToR4G4B4A4(ReadOnlySpan data, int width) + public unsafe static MemoryOwner ConvertR4G4ToR4G4B4A4(ReadOnlySpan data, int width) { - IMemoryOwner output = ByteMemoryPool.Rent(data.Length * 2); + MemoryOwner output = MemoryOwner.Rent(data.Length * 2); + Span outputSpan = output.Span; (int remainder, int outRemainder, int height) = GetLineRemainders(data.Length, width, 1, 2); - Span outputSpan = MemoryMarshal.Cast(output.Memory.Span); + Span outputSpanUInt16 = MemoryMarshal.Cast(outputSpan); if (remainder == 0) { @@ -38,7 +38,7 @@ namespace Ryujinx.Graphics.Texture int sizeTrunc = data.Length & ~7; start = sizeTrunc; - fixed (byte* inputPtr = data, outputPtr = output.Memory.Span) + fixed (byte* inputPtr = data, outputPtr = outputSpan) { for (ulong offset = 0; offset < (ulong)sizeTrunc; offset += 8) { @@ -49,7 +49,7 @@ namespace Ryujinx.Graphics.Texture for (int i = start; i < data.Length; i++) { - outputSpan[i] = (ushort)data[i]; + outputSpanUInt16[i] = data[i]; } } else @@ -61,7 +61,7 @@ namespace Ryujinx.Graphics.Texture { for (int x = 0; x < width; x++) { - outputSpan[outOffset++] = data[offset++]; + outputSpanUInt16[outOffset++] = data[offset++]; } offset += remainder; @@ -72,16 +72,16 @@ namespace Ryujinx.Graphics.Texture return output; } - public unsafe static IMemoryOwner ConvertR5G6B5ToR8G8B8A8(ReadOnlySpan data, int width) + public static MemoryOwner ConvertR5G6B5ToR8G8B8A8(ReadOnlySpan data, int width) { - IMemoryOwner output = ByteMemoryPool.Rent(data.Length * 2); + MemoryOwner output = MemoryOwner.Rent(data.Length * 2); int offset = 0; int outOffset = 0; (int remainder, int outRemainder, int height) = GetLineRemainders(data.Length, width, 2, 4); ReadOnlySpan inputSpan = MemoryMarshal.Cast(data); - Span outputSpan = MemoryMarshal.Cast(output.Memory.Span); + Span outputSpan = MemoryMarshal.Cast(output.Span); for (int y = 0; y < height; y++) { @@ -109,16 +109,16 @@ namespace Ryujinx.Graphics.Texture return output; } - public unsafe static IMemoryOwner ConvertR5G5B5ToR8G8B8A8(ReadOnlySpan data, int width, bool forceAlpha) + public static MemoryOwner ConvertR5G5B5ToR8G8B8A8(ReadOnlySpan data, int width, bool forceAlpha) { - IMemoryOwner output = ByteMemoryPool.Rent(data.Length * 2); + MemoryOwner output = MemoryOwner.Rent(data.Length * 2); int offset = 0; int outOffset = 0; (int remainder, int outRemainder, int height) = GetLineRemainders(data.Length, width, 2, 4); ReadOnlySpan inputSpan = MemoryMarshal.Cast(data); - Span outputSpan = MemoryMarshal.Cast(output.Memory.Span); + Span outputSpan = MemoryMarshal.Cast(output.Span); for (int y = 0; y < height; y++) { @@ -146,16 +146,16 @@ namespace Ryujinx.Graphics.Texture return output; } - public unsafe static IMemoryOwner ConvertA1B5G5R5ToR8G8B8A8(ReadOnlySpan data, int width) + public static MemoryOwner ConvertA1B5G5R5ToR8G8B8A8(ReadOnlySpan data, int width) { - IMemoryOwner output = ByteMemoryPool.Rent(data.Length * 2); + MemoryOwner output = MemoryOwner.Rent(data.Length * 2); int offset = 0; int outOffset = 0; (int remainder, int outRemainder, int height) = GetLineRemainders(data.Length, width, 2, 4); ReadOnlySpan inputSpan = MemoryMarshal.Cast(data); - Span outputSpan = MemoryMarshal.Cast(output.Memory.Span); + Span outputSpan = MemoryMarshal.Cast(output.Span); for (int y = 0; y < height; y++) { @@ -183,16 +183,16 @@ namespace Ryujinx.Graphics.Texture return output; } - public unsafe static IMemoryOwner ConvertR4G4B4A4ToR8G8B8A8(ReadOnlySpan data, int width) + public static MemoryOwner ConvertR4G4B4A4ToR8G8B8A8(ReadOnlySpan data, int width) { - IMemoryOwner output = ByteMemoryPool.Rent(data.Length * 2); + MemoryOwner output = MemoryOwner.Rent(data.Length * 2); int offset = 0; int outOffset = 0; (int remainder, int outRemainder, int height) = GetLineRemainders(data.Length, width, 2, 4); ReadOnlySpan inputSpan = MemoryMarshal.Cast(data); - Span outputSpan = MemoryMarshal.Cast(output.Memory.Span); + Span outputSpan = MemoryMarshal.Cast(output.Span); for (int y = 0; y < height; y++) { diff --git a/src/Ryujinx.Graphics.Vic/Image/SurfaceReader.cs b/src/Ryujinx.Graphics.Vic/Image/SurfaceReader.cs index 8a9acd912..83f00f345 100644 --- a/src/Ryujinx.Graphics.Vic/Image/SurfaceReader.cs +++ b/src/Ryujinx.Graphics.Vic/Image/SurfaceReader.cs @@ -454,7 +454,7 @@ namespace Ryujinx.Graphics.Vic.Image int srcStride = GetPitch(width, bytesPerPixel); int inSize = srcStride * height; - ReadOnlySpan src = rm.Gmm.GetSpan(ExtendOffset(offset), inSize); + ReadOnlySpan src = rm.MemoryManager.GetSpan(ExtendOffset(offset), inSize); int outSize = dstStride * height; int bufferIndex = rm.BufferPool.RentMinimum(outSize, out byte[] buffer); @@ -481,7 +481,7 @@ namespace Ryujinx.Graphics.Vic.Image { int inSize = GetBlockLinearSize(width, height, bytesPerPixel, gobBlocksInY); - ReadOnlySpan src = rm.Gmm.GetSpan(ExtendOffset(offset), inSize); + ReadOnlySpan src = rm.MemoryManager.GetSpan(ExtendOffset(offset), inSize); int outSize = dstStride * height; int bufferIndex = rm.BufferPool.RentMinimum(outSize, out byte[] buffer); diff --git a/src/Ryujinx.Graphics.Vic/Image/SurfaceWriter.cs b/src/Ryujinx.Graphics.Vic/Image/SurfaceWriter.cs index b06640499..b5008b7b5 100644 --- a/src/Ryujinx.Graphics.Vic/Image/SurfaceWriter.cs +++ b/src/Ryujinx.Graphics.Vic/Image/SurfaceWriter.cs @@ -636,7 +636,7 @@ namespace Ryujinx.Graphics.Vic.Image { if (linear) { - rm.Gmm.WriteMapped(ExtendOffset(offset), src); + rm.MemoryManager.Write(ExtendOffset(offset), src); return; } @@ -659,7 +659,7 @@ namespace Ryujinx.Graphics.Vic.Image LayoutConverter.ConvertLinearToBlockLinear(dst, width, height, dstStride, bytesPerPixel, gobBlocksInY, src); - rm.Gmm.WriteMapped(ExtendOffset(offset), dst); + rm.MemoryManager.Write(ExtendOffset(offset), dst); rm.BufferPool.Return(dstIndex); } diff --git a/src/Ryujinx.Graphics.Vic/ResourceManager.cs b/src/Ryujinx.Graphics.Vic/ResourceManager.cs index b0ff8e10e..e7d7ef74a 100644 --- a/src/Ryujinx.Graphics.Vic/ResourceManager.cs +++ b/src/Ryujinx.Graphics.Vic/ResourceManager.cs @@ -1,17 +1,17 @@ -using Ryujinx.Graphics.Gpu.Memory; +using Ryujinx.Graphics.Device; using Ryujinx.Graphics.Vic.Image; namespace Ryujinx.Graphics.Vic { readonly struct ResourceManager { - public MemoryManager Gmm { get; } + public DeviceMemoryManager MemoryManager { get; } public BufferPool SurfacePool { get; } public BufferPool BufferPool { get; } - public ResourceManager(MemoryManager gmm, BufferPool surfacePool, BufferPool bufferPool) + public ResourceManager(DeviceMemoryManager mm, BufferPool surfacePool, BufferPool bufferPool) { - Gmm = gmm; + MemoryManager = mm; SurfacePool = surfacePool; BufferPool = bufferPool; } diff --git a/src/Ryujinx.Graphics.Vic/Ryujinx.Graphics.Vic.csproj b/src/Ryujinx.Graphics.Vic/Ryujinx.Graphics.Vic.csproj index cfebcfa2a..a6c4fb2bb 100644 --- a/src/Ryujinx.Graphics.Vic/Ryujinx.Graphics.Vic.csproj +++ b/src/Ryujinx.Graphics.Vic/Ryujinx.Graphics.Vic.csproj @@ -8,8 +8,6 @@ - - diff --git a/src/Ryujinx.Graphics.Vic/VicDevice.cs b/src/Ryujinx.Graphics.Vic/VicDevice.cs index 2ddb94a4e..2b25a74c8 100644 --- a/src/Ryujinx.Graphics.Vic/VicDevice.cs +++ b/src/Ryujinx.Graphics.Vic/VicDevice.cs @@ -1,5 +1,4 @@ using Ryujinx.Graphics.Device; -using Ryujinx.Graphics.Gpu.Memory; using Ryujinx.Graphics.Vic.Image; using Ryujinx.Graphics.Vic.Types; using System; @@ -9,14 +8,14 @@ namespace Ryujinx.Graphics.Vic { public class VicDevice : IDeviceState { - private readonly MemoryManager _gmm; + private readonly DeviceMemoryManager _mm; private readonly ResourceManager _rm; private readonly DeviceState _state; - public VicDevice(MemoryManager gmm) + public VicDevice(DeviceMemoryManager mm) { - _gmm = gmm; - _rm = new ResourceManager(gmm, new BufferPool(), new BufferPool()); + _mm = mm; + _rm = new ResourceManager(mm, new BufferPool(), new BufferPool()); _state = new DeviceState(new Dictionary { { nameof(VicRegisters.Execute), new RwCallback(Execute, null) }, @@ -68,7 +67,7 @@ namespace Ryujinx.Graphics.Vic private T ReadIndirect(uint offset) where T : unmanaged { - return _gmm.Read((ulong)offset << 8); + return _mm.Read((ulong)offset << 8); } } } diff --git a/src/Ryujinx.Graphics.Video/Plane.cs b/src/Ryujinx.Graphics.Video/Plane.cs index b895cad90..4e4e65b32 100644 --- a/src/Ryujinx.Graphics.Video/Plane.cs +++ b/src/Ryujinx.Graphics.Video/Plane.cs @@ -2,5 +2,5 @@ using System; namespace Ryujinx.Graphics.Video { - public readonly record struct Plane(IntPtr Pointer, int Length); + public readonly record struct Plane(nint Pointer, int Length); } diff --git a/src/Ryujinx.Graphics.Vulkan/BackgroundResources.cs b/src/Ryujinx.Graphics.Vulkan/BackgroundResources.cs index 24e600a26..0290987fd 100644 --- a/src/Ryujinx.Graphics.Vulkan/BackgroundResources.cs +++ b/src/Ryujinx.Graphics.Vulkan/BackgroundResources.cs @@ -29,7 +29,14 @@ namespace Ryujinx.Graphics.Vulkan lock (queueLock) { - _pool = new CommandBufferPool(_gd.Api, _device, queue, queueLock, _gd.QueueFamilyIndex, isLight: true); + _pool = new CommandBufferPool( + _gd.Api, + _device, + queue, + queueLock, + _gd.QueueFamilyIndex, + _gd.IsQualcommProprietary, + isLight: true); } } diff --git a/src/Ryujinx.Graphics.Vulkan/BarrierBatch.cs b/src/Ryujinx.Graphics.Vulkan/BarrierBatch.cs new file mode 100644 index 000000000..bcfb3dbfe --- /dev/null +++ b/src/Ryujinx.Graphics.Vulkan/BarrierBatch.cs @@ -0,0 +1,458 @@ +using Silk.NET.Vulkan; +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +namespace Ryujinx.Graphics.Vulkan +{ + internal class BarrierBatch : IDisposable + { + private const int MaxBarriersPerCall = 16; + + private const AccessFlags BaseAccess = AccessFlags.ShaderReadBit | AccessFlags.ShaderWriteBit; + private const AccessFlags BufferAccess = AccessFlags.IndexReadBit | AccessFlags.VertexAttributeReadBit | AccessFlags.UniformReadBit; + private const AccessFlags CommandBufferAccess = AccessFlags.IndirectCommandReadBit; + + private readonly VulkanRenderer _gd; + + private readonly NativeArray _memoryBarrierBatch = new(MaxBarriersPerCall); + private readonly NativeArray _bufferBarrierBatch = new(MaxBarriersPerCall); + private readonly NativeArray _imageBarrierBatch = new(MaxBarriersPerCall); + + private readonly List> _memoryBarriers = new(); + private readonly List> _bufferBarriers = new(); + private readonly List> _imageBarriers = new(); + private int _queuedBarrierCount; + + private enum IncoherentBarrierType + { + None, + Texture, + All, + CommandBuffer + } + + private bool _feedbackLoopActive; + private PipelineStageFlags _incoherentBufferWriteStages; + private PipelineStageFlags _incoherentTextureWriteStages; + private PipelineStageFlags _extraStages; + private IncoherentBarrierType _queuedIncoherentBarrier; + private bool _queuedFeedbackLoopBarrier; + + public BarrierBatch(VulkanRenderer gd) + { + _gd = gd; + } + + public static (AccessFlags Access, PipelineStageFlags Stages) GetSubpassAccessSuperset(VulkanRenderer gd) + { + AccessFlags access = BufferAccess; + PipelineStageFlags stages = PipelineStageFlags.AllGraphicsBit; + + if (gd.TransformFeedbackApi != null) + { + access |= AccessFlags.TransformFeedbackWriteBitExt; + stages |= PipelineStageFlags.TransformFeedbackBitExt; + } + + return (access, stages); + } + + private readonly record struct StageFlags : IEquatable + { + public readonly PipelineStageFlags Source; + public readonly PipelineStageFlags Dest; + + public StageFlags(PipelineStageFlags source, PipelineStageFlags dest) + { + Source = source; + Dest = dest; + } + } + + private readonly struct BarrierWithStageFlags where T : unmanaged + { + public readonly StageFlags Flags; + public readonly T Barrier; + public readonly T2 Resource; + + public BarrierWithStageFlags(StageFlags flags, T barrier) + { + Flags = flags; + Barrier = barrier; + Resource = default; + } + + public BarrierWithStageFlags(PipelineStageFlags srcStageFlags, PipelineStageFlags dstStageFlags, T barrier, T2 resource) + { + Flags = new StageFlags(srcStageFlags, dstStageFlags); + Barrier = barrier; + Resource = resource; + } + } + + private void QueueBarrier(List> list, T barrier, T2 resource, PipelineStageFlags srcStageFlags, PipelineStageFlags dstStageFlags) where T : unmanaged + { + list.Add(new BarrierWithStageFlags(srcStageFlags, dstStageFlags, barrier, resource)); + _queuedBarrierCount++; + } + + public void QueueBarrier(MemoryBarrier barrier, PipelineStageFlags srcStageFlags, PipelineStageFlags dstStageFlags) + { + QueueBarrier(_memoryBarriers, barrier, default, srcStageFlags, dstStageFlags); + } + + public void QueueBarrier(BufferMemoryBarrier barrier, PipelineStageFlags srcStageFlags, PipelineStageFlags dstStageFlags) + { + QueueBarrier(_bufferBarriers, barrier, default, srcStageFlags, dstStageFlags); + } + + public void QueueBarrier(ImageMemoryBarrier barrier, TextureStorage resource, PipelineStageFlags srcStageFlags, PipelineStageFlags dstStageFlags) + { + QueueBarrier(_imageBarriers, barrier, resource, srcStageFlags, dstStageFlags); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe void FlushMemoryBarrier(ShaderCollection program, bool inRenderPass) + { + if (_queuedIncoherentBarrier > IncoherentBarrierType.None) + { + // We should emit a memory barrier if there's a write access in the program (current program, or program since last barrier) + bool hasTextureWrite = _incoherentTextureWriteStages != PipelineStageFlags.None; + bool hasBufferWrite = _incoherentBufferWriteStages != PipelineStageFlags.None; + bool hasBufferBarrier = _queuedIncoherentBarrier > IncoherentBarrierType.Texture; + + if (hasTextureWrite || (hasBufferBarrier && hasBufferWrite)) + { + AccessFlags access = BaseAccess; + + PipelineStageFlags stages = inRenderPass ? PipelineStageFlags.AllGraphicsBit : PipelineStageFlags.AllCommandsBit; + + if (hasBufferBarrier && hasBufferWrite) + { + access |= BufferAccess; + + if (_gd.TransformFeedbackApi != null) + { + access |= AccessFlags.TransformFeedbackWriteBitExt; + stages |= PipelineStageFlags.TransformFeedbackBitExt; + } + } + + if (_queuedIncoherentBarrier == IncoherentBarrierType.CommandBuffer) + { + access |= CommandBufferAccess; + stages |= PipelineStageFlags.DrawIndirectBit; + } + + MemoryBarrier barrier = new MemoryBarrier() + { + SType = StructureType.MemoryBarrier, + SrcAccessMask = access, + DstAccessMask = access + }; + + QueueBarrier(barrier, stages, stages); + + _incoherentTextureWriteStages = program?.IncoherentTextureWriteStages ?? PipelineStageFlags.None; + + if (_queuedIncoherentBarrier > IncoherentBarrierType.Texture) + { + if (program != null) + { + _incoherentBufferWriteStages = program.IncoherentBufferWriteStages | _extraStages; + } + else + { + _incoherentBufferWriteStages = PipelineStageFlags.None; + } + } + + _queuedIncoherentBarrier = IncoherentBarrierType.None; + _queuedFeedbackLoopBarrier = false; + } + else if (_feedbackLoopActive && _queuedFeedbackLoopBarrier) + { + // Feedback loop barrier. + + MemoryBarrier barrier = new MemoryBarrier() + { + SType = StructureType.MemoryBarrier, + SrcAccessMask = AccessFlags.ShaderWriteBit, + DstAccessMask = AccessFlags.ShaderReadBit + }; + + QueueBarrier(barrier, PipelineStageFlags.FragmentShaderBit, PipelineStageFlags.AllGraphicsBit); + + _queuedFeedbackLoopBarrier = false; + } + + _feedbackLoopActive = false; + } + } + + public unsafe void Flush(CommandBufferScoped cbs, bool inRenderPass, RenderPassHolder rpHolder, Action endRenderPass) + { + Flush(cbs, null, false, inRenderPass, rpHolder, endRenderPass); + } + + public unsafe void Flush(CommandBufferScoped cbs, ShaderCollection program, bool feedbackLoopActive, bool inRenderPass, RenderPassHolder rpHolder, Action endRenderPass) + { + if (program != null) + { + _incoherentBufferWriteStages |= program.IncoherentBufferWriteStages | _extraStages; + _incoherentTextureWriteStages |= program.IncoherentTextureWriteStages; + } + + _feedbackLoopActive |= feedbackLoopActive; + + FlushMemoryBarrier(program, inRenderPass); + + if (!inRenderPass && rpHolder != null) + { + // Render pass is about to begin. Queue any fences that normally interrupt the pass. + rpHolder.InsertForcedFences(cbs); + } + + while (_queuedBarrierCount > 0) + { + int memoryCount = 0; + int bufferCount = 0; + int imageCount = 0; + + bool hasBarrier = false; + StageFlags flags = default; + + static void AddBarriers( + Span target, + ref int queuedBarrierCount, + ref bool hasBarrier, + ref StageFlags flags, + ref int count, + List> list) where T : unmanaged + { + int firstMatch = -1; + int end = list.Count; + + for (int i = 0; i < list.Count; i++) + { + BarrierWithStageFlags barrier = list[i]; + + if (!hasBarrier) + { + flags = barrier.Flags; + hasBarrier = true; + + target[count++] = barrier.Barrier; + queuedBarrierCount--; + firstMatch = i; + + if (count >= target.Length) + { + end = i + 1; + break; + } + } + else + { + if (flags.Equals(barrier.Flags)) + { + target[count++] = barrier.Barrier; + queuedBarrierCount--; + + if (firstMatch == -1) + { + firstMatch = i; + } + + if (count >= target.Length) + { + end = i + 1; + break; + } + } + else + { + // Delete consumed barriers from the first match to the current non-match. + if (firstMatch != -1) + { + int deleteCount = i - firstMatch; + list.RemoveRange(firstMatch, deleteCount); + i -= deleteCount; + + firstMatch = -1; + end = list.Count; + } + } + } + } + + if (firstMatch == 0 && end == list.Count) + { + list.Clear(); + } + else if (firstMatch != -1) + { + int deleteCount = end - firstMatch; + + list.RemoveRange(firstMatch, deleteCount); + } + } + + if (inRenderPass && _imageBarriers.Count > 0) + { + // Image barriers queued in the batch are meant to be globally scoped, + // but inside a render pass they're scoped to just the range of the render pass. + + // On MoltenVK, we just break the rules and always use image barrier. + // On desktop GPUs, all barriers are globally scoped, so we just replace it with a generic memory barrier. + // Generally, we want to avoid this from happening in the future, so flag the texture to immediately + // emit a barrier whenever the current render pass is bound again. + + bool anyIsNonAttachment = false; + + foreach (BarrierWithStageFlags barrier in _imageBarriers) + { + // If the binding is an attachment, don't add it as a forced fence. + bool isAttachment = rpHolder.ContainsAttachment(barrier.Resource); + + if (!isAttachment) + { + rpHolder.AddForcedFence(barrier.Resource, barrier.Flags.Dest); + anyIsNonAttachment = true; + } + } + + if (_gd.IsTBDR) + { + if (!_gd.IsMoltenVk) + { + if (!anyIsNonAttachment) + { + // This case is a feedback loop. To prevent this from causing an absolute performance disaster, + // remove the barriers entirely. + // If this is not here, there will be a lot of single draw render passes. + // TODO: explicit handling for feedback loops, likely outside this class. + + _queuedBarrierCount -= _imageBarriers.Count; + _imageBarriers.Clear(); + } + else + { + // TBDR GPUs are sensitive to barriers, so we need to end the pass to ensure the data is available. + // Metal already has hazard tracking so MVK doesn't need this. + endRenderPass(); + inRenderPass = false; + } + } + } + else + { + // Generic pipeline memory barriers will work for desktop GPUs. + // They do require a few more access flags on the subpass dependency, though. + foreach (var barrier in _imageBarriers) + { + _memoryBarriers.Add(new BarrierWithStageFlags( + barrier.Flags, + new MemoryBarrier() + { + SType = StructureType.MemoryBarrier, + SrcAccessMask = barrier.Barrier.SrcAccessMask, + DstAccessMask = barrier.Barrier.DstAccessMask + })); + } + + _imageBarriers.Clear(); + } + } + + if (inRenderPass && _memoryBarriers.Count > 0) + { + PipelineStageFlags allFlags = PipelineStageFlags.None; + + foreach (var barrier in _memoryBarriers) + { + allFlags |= barrier.Flags.Dest; + } + + if (allFlags.HasFlag(PipelineStageFlags.DrawIndirectBit) || !_gd.SupportsRenderPassBarrier(allFlags)) + { + endRenderPass(); + inRenderPass = false; + } + } + + AddBarriers(_memoryBarrierBatch.AsSpan(), ref _queuedBarrierCount, ref hasBarrier, ref flags, ref memoryCount, _memoryBarriers); + AddBarriers(_bufferBarrierBatch.AsSpan(), ref _queuedBarrierCount, ref hasBarrier, ref flags, ref bufferCount, _bufferBarriers); + AddBarriers(_imageBarrierBatch.AsSpan(), ref _queuedBarrierCount, ref hasBarrier, ref flags, ref imageCount, _imageBarriers); + + if (hasBarrier) + { + PipelineStageFlags srcStageFlags = flags.Source; + + if (inRenderPass) + { + // Inside a render pass, barrier stages can only be from rasterization. + srcStageFlags &= ~PipelineStageFlags.ComputeShaderBit; + } + + _gd.Api.CmdPipelineBarrier( + cbs.CommandBuffer, + srcStageFlags, + flags.Dest, + 0, + (uint)memoryCount, + _memoryBarrierBatch.Pointer, + (uint)bufferCount, + _bufferBarrierBatch.Pointer, + (uint)imageCount, + _imageBarrierBatch.Pointer); + } + } + } + + private void QueueIncoherentBarrier(IncoherentBarrierType type) + { + if (type > _queuedIncoherentBarrier) + { + _queuedIncoherentBarrier = type; + } + + _queuedFeedbackLoopBarrier = true; + } + + public void QueueTextureBarrier() + { + QueueIncoherentBarrier(IncoherentBarrierType.Texture); + } + + public void QueueMemoryBarrier() + { + QueueIncoherentBarrier(IncoherentBarrierType.All); + } + + public void QueueCommandBufferBarrier() + { + QueueIncoherentBarrier(IncoherentBarrierType.CommandBuffer); + } + + public void EnableTfbBarriers(bool enable) + { + if (enable) + { + _extraStages |= PipelineStageFlags.TransformFeedbackBitExt; + } + else + { + _extraStages &= ~PipelineStageFlags.TransformFeedbackBitExt; + } + } + + public void Dispose() + { + _memoryBarrierBatch.Dispose(); + _bufferBarrierBatch.Dispose(); + _imageBarrierBatch.Dispose(); + } + } +} diff --git a/src/Ryujinx.Graphics.Vulkan/BufferHolder.cs b/src/Ryujinx.Graphics.Vulkan/BufferHolder.cs index b54ff3ab6..6dce6abb5 100644 --- a/src/Ryujinx.Graphics.Vulkan/BufferHolder.cs +++ b/src/Ryujinx.Graphics.Vulkan/BufferHolder.cs @@ -1,9 +1,9 @@ -using Ryujinx.Common.Logging; using Ryujinx.Graphics.GAL; using Silk.NET.Vulkan; using System; using System.Collections.Generic; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using System.Threading; using VkBuffer = Silk.NET.Vulkan.Buffer; using VkFormat = Silk.NET.Vulkan.Format; @@ -30,40 +30,29 @@ namespace Ryujinx.Graphics.Vulkan private readonly VulkanRenderer _gd; private readonly Device _device; - private MemoryAllocation _allocation; - private Auto _buffer; - private Auto _allocationAuto; + private readonly MemoryAllocation _allocation; + private readonly Auto _buffer; + private readonly Auto _allocationAuto; private readonly bool _allocationImported; - private ulong _bufferHandle; + private readonly ulong _bufferHandle; private CacheByRange _cachedConvertedBuffers; public int Size { get; } - private IntPtr _map; + private readonly nint _map; - private MultiFenceHolder _waitable; + private readonly MultiFenceHolder _waitable; private bool _lastAccessIsWrite; - private BufferAllocationType _baseType; - private BufferAllocationType _currentType; - private bool _swapQueued; - - public BufferAllocationType DesiredType { get; private set; } - - private int _setCount; - private int _writeCount; - private int _flushCount; - private int _flushTemp; - private int _lastFlushWrite = -1; + private readonly BufferAllocationType _baseType; + private readonly BufferAllocationType _activeType; private readonly ReaderWriterLockSlim _flushLock; private FenceHolder _flushFence; private int _flushWaiting; - private List _swapActions; - private byte[] _pendingData; private BufferMirrorRangeList _pendingDataRanges; private Dictionary _mirrors; @@ -82,8 +71,7 @@ namespace Ryujinx.Graphics.Vulkan _map = allocation.HostPointer; _baseType = type; - _currentType = currentType; - DesiredType = currentType; + _activeType = currentType; _flushLock = new ReaderWriterLockSlim(); _useMirrors = gd.IsTBDR; @@ -103,8 +91,7 @@ namespace Ryujinx.Graphics.Vulkan _map = _allocation.HostPointer + offset; _baseType = type; - _currentType = currentType; - DesiredType = currentType; + _activeType = currentType; _flushLock = new ReaderWriterLockSlim(); } @@ -119,164 +106,11 @@ namespace Ryujinx.Graphics.Vulkan Size = size; _baseType = BufferAllocationType.Sparse; - _currentType = BufferAllocationType.Sparse; - DesiredType = BufferAllocationType.Sparse; + _activeType = BufferAllocationType.Sparse; _flushLock = new ReaderWriterLockSlim(); } - public bool TryBackingSwap(ref CommandBufferScoped? cbs) - { - if (_swapQueued && DesiredType != _currentType) - { - // Only swap if the buffer is not used in any queued command buffer. - bool isRented = _buffer.HasRentedCommandBufferDependency(_gd.CommandBufferPool); - - if (!isRented && _gd.CommandBufferPool.OwnedByCurrentThread && !_flushLock.IsReadLockHeld && (_pendingData == null || cbs != null)) - { - var currentAllocation = _allocationAuto; - var currentBuffer = _buffer; - IntPtr currentMap = _map; - - (VkBuffer buffer, MemoryAllocation allocation, BufferAllocationType resultType) = _gd.BufferManager.CreateBacking(_gd, Size, DesiredType, false, false, _currentType); - - if (buffer.Handle != 0) - { - if (cbs != null) - { - ClearMirrors(cbs.Value, 0, Size); - } - - _flushLock.EnterWriteLock(); - - ClearFlushFence(); - - _waitable = new MultiFenceHolder(Size); - - _allocation = allocation; - _allocationAuto = new Auto(allocation); - _buffer = new Auto(new DisposableBuffer(_gd.Api, _device, buffer), this, _waitable, _allocationAuto); - _bufferHandle = buffer.Handle; - _map = allocation.HostPointer; - - if (_map != IntPtr.Zero && currentMap != IntPtr.Zero) - { - // Copy data directly. Readbacks don't have to wait if this is done. - - unsafe - { - new Span((void*)currentMap, Size).CopyTo(new Span((void*)_map, Size)); - } - } - else - { - cbs ??= _gd.CommandBufferPool.Rent(); - - CommandBufferScoped cbsV = cbs.Value; - - Copy(_gd, cbsV, currentBuffer, _buffer, 0, 0, Size); - - // Need to wait for the data to reach the new buffer before data can be flushed. - - _flushFence = _gd.CommandBufferPool.GetFence(cbsV.CommandBufferIndex); - _flushFence.Get(); - } - - Logger.Debug?.PrintMsg(LogClass.Gpu, $"Converted {Size} buffer {_currentType} to {resultType}"); - - _currentType = resultType; - - if (_swapActions != null) - { - foreach (var action in _swapActions) - { - action(); - } - - _swapActions.Clear(); - } - - currentBuffer.Dispose(); - currentAllocation.Dispose(); - - _gd.PipelineInternal.SwapBuffer(currentBuffer, _buffer); - - _flushLock.ExitWriteLock(); - } - - _swapQueued = false; - - return true; - } - - return false; - } - - _swapQueued = false; - - return true; - } - - private void ConsiderBackingSwap() - { - if (_baseType == BufferAllocationType.Auto) - { - // When flushed, wait for a bit more info to make a decision. - bool wasFlushed = _flushTemp > 0; - int multiplier = wasFlushed ? 2 : 0; - if (_writeCount >= (WriteCountThreshold << multiplier) || _setCount >= (SetCountThreshold << multiplier) || _flushCount >= (FlushCountThreshold << multiplier)) - { - if (_flushCount > 0 || _flushTemp-- > 0) - { - // Buffers that flush should ideally be mapped in host address space for easy copies. - // If the buffer is large it will do better on GPU memory, as there will be more writes than data flushes (typically individual pages). - // If it is small, then it's likely most of the buffer will be flushed so we want it on host memory, as access is cached. - - bool hostMappingSensitive = _gd.Vendor == Vendor.Nvidia; - bool deviceLocalMapped = Size > DeviceLocalSizeThreshold || (wasFlushed && _writeCount > _flushCount * 10 && hostMappingSensitive) || _currentType == BufferAllocationType.DeviceLocalMapped; - - DesiredType = deviceLocalMapped ? BufferAllocationType.DeviceLocalMapped : BufferAllocationType.HostMapped; - - // It's harder for a buffer that is flushed to revert to another type of mapping. - if (_flushCount > 0) - { - _flushTemp = 1000; - } - } - else if (_writeCount >= (WriteCountThreshold << multiplier)) - { - // Buffers that are written often should ideally be in the device local heap. (Storage buffers) - DesiredType = BufferAllocationType.DeviceLocal; - } - else if (_setCount > (SetCountThreshold << multiplier)) - { - // Buffers that have their data set often should ideally be host mapped. (Constant buffers) - DesiredType = BufferAllocationType.HostMapped; - } - - _lastFlushWrite = -1; - _flushCount = 0; - _writeCount = 0; - _setCount = 0; - } - - if (!_swapQueued && DesiredType != _currentType) - { - _swapQueued = true; - - _gd.PipelineInternal.AddBackingSwap(this); - } - } - } - - public void Pin() - { - if (_baseType == BufferAllocationType.Auto) - { - _baseType = _currentType; - } - } - public unsafe Auto CreateView(VkFormat format, int offset, int size, Action invalidateView) { var bufferViewCreateInfo = new BufferViewCreateInfo @@ -288,21 +122,11 @@ namespace Ryujinx.Graphics.Vulkan Range = (uint)size, }; - _gd.Api.CreateBufferView(_device, bufferViewCreateInfo, null, out var bufferView).ThrowOnError(); - - (_swapActions ??= new List()).Add(invalidateView); + _gd.Api.CreateBufferView(_device, in bufferViewCreateInfo, null, out var bufferView).ThrowOnError(); return new Auto(new DisposableBufferView(_gd.Api, _device, bufferView), this, _waitable, _buffer); } - public void InheritMetrics(BufferHolder other) - { - _setCount = other._setCount; - _writeCount = other._writeCount; - _flushCount = other._flushCount; - _flushTemp = other._flushTemp; - } - public unsafe void InsertBarrier(CommandBuffer commandBuffer, bool isWrite) { // If the last access is write, we always need a barrier to be sure we will read or modify @@ -329,7 +153,7 @@ namespace Ryujinx.Graphics.Vulkan PipelineStageFlags.AllCommandsBit, DependencyFlags.DeviceGroupBit, 1, - memoryBarrier, + in memoryBarrier, 0, null, 0, @@ -384,7 +208,7 @@ namespace Ryujinx.Graphics.Vulkan var baseData = new Span((void*)(_map + offset), size); var modData = _pendingData.AsSpan(offset, size); - StagingBufferReserved? newMirror = _gd.BufferManager.StagingBuffer.TryReserveData(cbs, size, (int)_gd.Capabilities.MinResourceAlignment); + StagingBufferReserved? newMirror = _gd.BufferManager.StagingBuffer.TryReserveData(cbs, size); if (newMirror != null) { @@ -422,18 +246,8 @@ namespace Ryujinx.Graphics.Vulkan { if (isWrite) { - _writeCount++; - SignalWrite(0, Size); } - else if (isSSBO) - { - // Always consider SSBO access for swapping to device local memory. - - _writeCount++; - - ConsiderBackingSwap(); - } return _buffer; } @@ -442,8 +256,6 @@ namespace Ryujinx.Graphics.Vulkan { if (isWrite) { - _writeCount++; - SignalWrite(offset, size); } @@ -520,7 +332,7 @@ namespace Ryujinx.Graphics.Vulkan if (_gd.PipelineInternal.CurrentCommandBuffer.CommandBuffer.Handle == cbs.CommandBuffer.Handle) { - SetData(rangeOffset, _pendingData.AsSpan(rangeOffset, rangeSize), cbs, _gd.PipelineInternal.EndRenderPass, false); + SetData(rangeOffset, _pendingData.AsSpan(rangeOffset, rangeSize), cbs, _gd.PipelineInternal.EndRenderPassDelegate, false); } else { @@ -542,8 +354,6 @@ namespace Ryujinx.Graphics.Vulkan public void SignalWrite(int offset, int size) { - ConsiderBackingSwap(); - if (offset == 0 && size == Size) { _cachedConvertedBuffers.Clear(); @@ -560,7 +370,7 @@ namespace Ryujinx.Graphics.Vulkan return Unsafe.As(ref handle); } - public IntPtr Map(int offset, int mappingSize) + public nint Map(int offset, int mappingSize) { return _map; } @@ -623,16 +433,9 @@ namespace Ryujinx.Graphics.Vulkan WaitForFlushFence(); - if (_lastFlushWrite != _writeCount) - { - // If it's on the same page as the last flush, ignore it. - _lastFlushWrite = _writeCount; - _flushCount++; - } - Span result; - if (_map != IntPtr.Zero) + if (_map != nint.Zero) { result = GetDataStorage(offset, size); @@ -667,7 +470,7 @@ namespace Ryujinx.Graphics.Vulkan { int mappingSize = Math.Min(size, Size - offset); - if (_map != IntPtr.Zero) + if (_map != nint.Zero) { return new Span((void*)(_map + offset), mappingSize); } @@ -710,10 +513,9 @@ namespace Ryujinx.Graphics.Vulkan return; } - _setCount++; - bool allowMirror = _useMirrors && allowCbsWait && cbs != null && _currentType <= BufferAllocationType.HostMapped; + bool allowMirror = _useMirrors && allowCbsWait && cbs != null && _activeType <= BufferAllocationType.HostMapped; - if (_map != IntPtr.Zero) + if (_map != nint.Zero) { // If persistently mapped, set the data directly if the buffer is not currently in use. bool isRented = _buffer.HasRentedCommandBufferDependency(_gd.CommandBufferPool); @@ -828,7 +630,7 @@ namespace Ryujinx.Graphics.Vulkan return; } - if (_map != IntPtr.Zero) + if (_map != nint.Zero) { data[..dataSize].CopyTo(new Span((void*)(_map + offset), dataSize)); } @@ -838,6 +640,11 @@ namespace Ryujinx.Graphics.Vulkan } } + public unsafe void SetDataUnchecked(int offset, ReadOnlySpan data) where T : unmanaged + { + SetDataUnchecked(offset, MemoryMarshal.AsBytes(data)); + } + public void SetDataInline(CommandBufferScoped cbs, Action endRenderPass, int dstOffset, ReadOnlySpan data) { if (!TryPushData(cbs, endRenderPass, dstOffset, data)) @@ -857,8 +664,6 @@ namespace Ryujinx.Graphics.Vulkan var dstBuffer = GetBuffer(cbs.CommandBuffer, dstOffset, data.Length, true).Get(cbs, dstOffset, data.Length, true).Value; - _writeCount--; - InsertBufferBarrier( _gd, cbs.CommandBuffer, @@ -965,7 +770,7 @@ namespace Ryujinx.Graphics.Vulkan 0, null, 1, - memoryBarrier, + in memoryBarrier, 0, null); } @@ -1094,8 +899,6 @@ namespace Ryujinx.Graphics.Vulkan public void Dispose() { - _swapQueued = false; - _gd.PipelineInternal?.FlushCommandsIfWeightExceeding(_buffer, (ulong)Size); _buffer.Dispose(); diff --git a/src/Ryujinx.Graphics.Vulkan/BufferManager.cs b/src/Ryujinx.Graphics.Vulkan/BufferManager.cs index e9ac98847..7523913ec 100644 --- a/src/Ryujinx.Graphics.Vulkan/BufferManager.cs +++ b/src/Ryujinx.Graphics.Vulkan/BufferManager.cs @@ -9,6 +9,36 @@ using VkFormat = Silk.NET.Vulkan.Format; namespace Ryujinx.Graphics.Vulkan { + readonly struct ScopedTemporaryBuffer : IDisposable + { + private readonly BufferManager _bufferManager; + private readonly bool _isReserved; + + public readonly BufferRange Range; + public readonly BufferHolder Holder; + + public BufferHandle Handle => Range.Handle; + public int Offset => Range.Offset; + + public ScopedTemporaryBuffer(BufferManager bufferManager, BufferHolder holder, BufferHandle handle, int offset, int size, bool isReserved) + { + _bufferManager = bufferManager; + + Range = new BufferRange(handle, offset, size); + Holder = holder; + + _isReserved = isReserved; + } + + public void Dispose() + { + if (!_isReserved) + { + _bufferManager.Delete(Range.Handle); + } + } + } + class BufferManager : IDisposable { public const MemoryPropertyFlags DefaultBufferMemoryFlags = @@ -73,12 +103,19 @@ namespace Ryujinx.Graphics.Vulkan usage |= BufferUsageFlags.IndirectBufferBit; } + var externalMemoryBuffer = new ExternalMemoryBufferCreateInfo + { + SType = StructureType.ExternalMemoryBufferCreateInfo, + HandleTypes = ExternalMemoryHandleTypeFlags.HostAllocationBitExt, + }; + var bufferCreateInfo = new BufferCreateInfo { SType = StructureType.BufferCreateInfo, Size = (ulong)size, Usage = usage, SharingMode = SharingMode.Exclusive, + PNext = &externalMemoryBuffer, }; gd.Api.CreateBuffer(_device, in bufferCreateInfo, null, out var buffer).ThrowOnError(); @@ -135,10 +172,6 @@ namespace Ryujinx.Graphics.Vulkan if (TryGetBuffer(range.Handle, out var existingHolder)) { - // Since this buffer now also owns the memory from the referenced buffer, - // we pin it to ensure the memory location will not change. - existingHolder.Pin(); - (var memory, var offset) = existingHolder.GetDeviceMemoryAndOffset(); memoryBinds[index] = new SparseMemoryBind() @@ -188,7 +221,7 @@ namespace Ryujinx.Graphics.Vulkan PBufferBinds = &bufferBind }; - gd.Api.QueueBindSparse(gd.Queue, 1, bindSparseInfo, default).ThrowOnError(); + gd.Api.QueueBindSparse(gd.Queue, 1, in bindSparseInfo, default).ThrowOnError(); } var holder = new BufferHolder(gd, _device, buffer, (int)size, storageAllocations); @@ -205,10 +238,9 @@ namespace Ryujinx.Graphics.Vulkan int size, bool sparseCompatible = false, BufferAllocationType baseType = BufferAllocationType.HostMapped, - BufferHandle storageHint = default, bool forceMirrors = false) { - return CreateWithHandle(gd, size, out _, sparseCompatible, baseType, storageHint, forceMirrors); + return CreateWithHandle(gd, size, out _, sparseCompatible, baseType, forceMirrors); } public BufferHandle CreateWithHandle( @@ -217,10 +249,9 @@ namespace Ryujinx.Graphics.Vulkan out BufferHolder holder, bool sparseCompatible = false, BufferAllocationType baseType = BufferAllocationType.HostMapped, - BufferHandle storageHint = default, bool forceMirrors = false) { - holder = Create(gd, size, forConditionalRendering: false, sparseCompatible, baseType, storageHint); + holder = Create(gd, size, forConditionalRendering: false, sparseCompatible, baseType); if (holder == null) { return BufferHandle.Null; @@ -238,6 +269,23 @@ namespace Ryujinx.Graphics.Vulkan return Unsafe.As(ref handle64); } + public ScopedTemporaryBuffer ReserveOrCreate(VulkanRenderer gd, CommandBufferScoped cbs, int size) + { + StagingBufferReserved? result = StagingBuffer.TryReserveData(cbs, size); + + if (result.HasValue) + { + return new ScopedTemporaryBuffer(this, result.Value.Buffer, StagingBuffer.Handle, result.Value.Offset, result.Value.Size, true); + } + else + { + // Create a temporary buffer. + BufferHandle handle = CreateWithHandle(gd, size, out BufferHolder holder); + + return new ScopedTemporaryBuffer(this, holder, handle, 0, size, false); + } + } + public unsafe MemoryRequirements GetHostImportedUsageRequirements(VulkanRenderer gd) { var usage = HostImportedBufferUsageFlags; @@ -340,31 +388,13 @@ namespace Ryujinx.Graphics.Vulkan int size, bool forConditionalRendering = false, bool sparseCompatible = false, - BufferAllocationType baseType = BufferAllocationType.HostMapped, - BufferHandle storageHint = default) + BufferAllocationType baseType = BufferAllocationType.HostMapped) { BufferAllocationType type = baseType; - BufferHolder storageHintHolder = null; if (baseType == BufferAllocationType.Auto) { - if (gd.IsSharedMemory) - { - baseType = BufferAllocationType.HostMapped; - type = baseType; - } - else - { - type = size >= BufferHolder.DeviceLocalSizeThreshold ? BufferAllocationType.DeviceLocal : BufferAllocationType.HostMapped; - } - - if (storageHint != BufferHandle.Null) - { - if (TryGetBuffer(storageHint, out storageHintHolder)) - { - type = storageHintHolder.DesiredType; - } - } + type = BufferAllocationType.HostMapped; } (VkBuffer buffer, MemoryAllocation allocation, BufferAllocationType resultType) = @@ -374,11 +404,6 @@ namespace Ryujinx.Graphics.Vulkan { var holder = new BufferHolder(gd, _device, buffer, allocation, size, baseType, resultType); - if (storageHintHolder != null) - { - holder.InheritMetrics(storageHintHolder); - } - return holder; } @@ -635,13 +660,14 @@ namespace Ryujinx.Graphics.Vulkan { if (disposing) { + StagingBuffer.Dispose(); + foreach (BufferHolder buffer in _buffers) { buffer.Dispose(); } _buffers.Clear(); - StagingBuffer.Dispose(); } } diff --git a/src/Ryujinx.Graphics.Vulkan/BufferState.cs b/src/Ryujinx.Graphics.Vulkan/BufferState.cs index d585dd53c..e49df765d 100644 --- a/src/Ryujinx.Graphics.Vulkan/BufferState.cs +++ b/src/Ryujinx.Graphics.Vulkan/BufferState.cs @@ -25,7 +25,10 @@ namespace Ryujinx.Graphics.Vulkan { var buffer = _buffer.Get(cbs, _offset, _size, true).Value; - gd.TransformFeedbackApi.CmdBindTransformFeedbackBuffers(cbs.CommandBuffer, binding, 1, buffer, (ulong)_offset, (ulong)_size); + ulong offset = (ulong)_offset; + ulong size = (ulong)_size; + + gd.TransformFeedbackApi.CmdBindTransformFeedbackBuffers(cbs.CommandBuffer, binding, 1, in buffer, in offset, in size); } } diff --git a/src/Ryujinx.Graphics.Vulkan/CommandBufferPool.cs b/src/Ryujinx.Graphics.Vulkan/CommandBufferPool.cs index 61cfbb6ec..e1fd3fb9d 100644 --- a/src/Ryujinx.Graphics.Vulkan/CommandBufferPool.cs +++ b/src/Ryujinx.Graphics.Vulkan/CommandBufferPool.cs @@ -18,6 +18,7 @@ namespace Ryujinx.Graphics.Vulkan private readonly Device _device; private readonly Queue _queue; private readonly object _queueLock; + private readonly bool _concurrentFenceWaitUnsupported; private readonly CommandPool _pool; private readonly Thread _owner; @@ -30,11 +31,9 @@ namespace Ryujinx.Graphics.Vulkan public int SubmissionCount; public CommandBuffer CommandBuffer; public FenceHolder Fence; - public SemaphoreHolder Semaphore; public List Dependants; public List Waitables; - public HashSet Dependencies; public void Initialize(Vk api, Device device, CommandPool pool) { @@ -46,11 +45,10 @@ namespace Ryujinx.Graphics.Vulkan Level = CommandBufferLevel.Primary, }; - api.AllocateCommandBuffers(device, allocateInfo, out CommandBuffer); + api.AllocateCommandBuffers(device, in allocateInfo, out CommandBuffer); Dependants = new List(); Waitables = new List(); - Dependencies = new HashSet(); } } @@ -61,12 +59,20 @@ namespace Ryujinx.Graphics.Vulkan private int _queuedCount; private int _inUseCount; - public unsafe CommandBufferPool(Vk api, Device device, Queue queue, object queueLock, uint queueFamilyIndex, bool isLight = false) + public unsafe CommandBufferPool( + Vk api, + Device device, + Queue queue, + object queueLock, + uint queueFamilyIndex, + bool concurrentFenceWaitUnsupported, + bool isLight = false) { _api = api; _device = device; _queue = queue; _queueLock = queueLock; + _concurrentFenceWaitUnsupported = concurrentFenceWaitUnsupported; _owner = Thread.CurrentThread; var commandPoolCreateInfo = new CommandPoolCreateInfo @@ -77,7 +83,7 @@ namespace Ryujinx.Graphics.Vulkan CommandPoolCreateFlags.ResetCommandBufferBit, }; - api.CreateCommandPool(device, commandPoolCreateInfo, null, out _pool).ThrowOnError(); + api.CreateCommandPool(device, in commandPoolCreateInfo, null, out _pool).ThrowOnError(); // We need at least 2 command buffers to get texture data in some cases. _totalCommandBuffers = isLight ? 2 : MaxCommandBuffers; @@ -134,14 +140,6 @@ namespace Ryujinx.Graphics.Vulkan } } - public void AddDependency(int cbIndex, CommandBufferScoped dependencyCbs) - { - Debug.Assert(_commandBuffers[cbIndex].InUse); - var semaphoreHolder = _commandBuffers[dependencyCbs.CommandBufferIndex].Semaphore; - semaphoreHolder.Get(); - _commandBuffers[cbIndex].Dependencies.Add(semaphoreHolder); - } - public void AddWaitable(int cbIndex, MultiFenceHolder waitable) { ref var entry = ref _commandBuffers[cbIndex]; @@ -255,7 +253,7 @@ namespace Ryujinx.Graphics.Vulkan SType = StructureType.CommandBufferBeginInfo, }; - _api.BeginCommandBuffer(entry.CommandBuffer, commandBufferBeginInfo).ThrowOnError(); + _api.BeginCommandBuffer(entry.CommandBuffer, in commandBufferBeginInfo).ThrowOnError(); return new CommandBufferScoped(this, entry.CommandBuffer, cursor); } @@ -302,18 +300,18 @@ namespace Ryujinx.Graphics.Vulkan SubmitInfo sInfo = new() { SType = StructureType.SubmitInfo, - WaitSemaphoreCount = waitSemaphores != null ? (uint)waitSemaphores.Length : 0, + WaitSemaphoreCount = !waitSemaphores.IsEmpty ? (uint)waitSemaphores.Length : 0, PWaitSemaphores = pWaitSemaphores, PWaitDstStageMask = pWaitDstStageMask, CommandBufferCount = 1, PCommandBuffers = &commandBuffer, - SignalSemaphoreCount = signalSemaphores != null ? (uint)signalSemaphores.Length : 0, + SignalSemaphoreCount = !signalSemaphores.IsEmpty ? (uint)signalSemaphores.Length : 0, PSignalSemaphores = pSignalSemaphores, }; lock (_queueLock) { - _api.QueueSubmit(_queue, 1, sInfo, entry.Fence.GetUnsafe()).ThrowOnError(); + _api.QueueSubmit(_queue, 1, in sInfo, entry.Fence.GetUnsafe()).ThrowOnError(); } } } @@ -345,19 +343,13 @@ namespace Ryujinx.Graphics.Vulkan waitable.RemoveBufferUses(cbIndex); } - foreach (var dependency in entry.Dependencies) - { - dependency.Put(); - } - entry.Dependants.Clear(); entry.Waitables.Clear(); - entry.Dependencies.Clear(); entry.Fence?.Dispose(); if (refreshFence) { - entry.Fence = new FenceHolder(_api, _device); + entry.Fence = new FenceHolder(_api, _device, _concurrentFenceWaitUnsupported); } else { diff --git a/src/Ryujinx.Graphics.Vulkan/CommandBufferScoped.cs b/src/Ryujinx.Graphics.Vulkan/CommandBufferScoped.cs index 270cdc6e6..2accd69b2 100644 --- a/src/Ryujinx.Graphics.Vulkan/CommandBufferScoped.cs +++ b/src/Ryujinx.Graphics.Vulkan/CommandBufferScoped.cs @@ -26,11 +26,6 @@ namespace Ryujinx.Graphics.Vulkan _pool.AddWaitable(CommandBufferIndex, waitable); } - public void AddDependency(CommandBufferScoped dependencyCbs) - { - _pool.AddDependency(CommandBufferIndex, dependencyCbs); - } - public FenceHolder GetFence() { return _pool.GetFence(CommandBufferIndex); diff --git a/src/Ryujinx.Graphics.Vulkan/Constants.cs b/src/Ryujinx.Graphics.Vulkan/Constants.cs index cd6122112..20ce65818 100644 --- a/src/Ryujinx.Graphics.Vulkan/Constants.cs +++ b/src/Ryujinx.Graphics.Vulkan/Constants.cs @@ -16,6 +16,7 @@ namespace Ryujinx.Graphics.Vulkan public const int MaxStorageBufferBindings = MaxStorageBuffersPerStage * MaxShaderStages; public const int MaxTextureBindings = MaxTexturesPerStage * MaxShaderStages; public const int MaxImageBindings = MaxImagesPerStage * MaxShaderStages; + public const int MaxPushDescriptorBinding = 64; public const ulong SparseBufferAlignment = 0x10000; } diff --git a/src/Ryujinx.Graphics.Vulkan/DescriptorSetCollection.cs b/src/Ryujinx.Graphics.Vulkan/DescriptorSetCollection.cs index ae5167334..40fc01b24 100644 --- a/src/Ryujinx.Graphics.Vulkan/DescriptorSetCollection.cs +++ b/src/Ryujinx.Graphics.Vulkan/DescriptorSetCollection.cs @@ -43,66 +43,37 @@ namespace Ryujinx.Graphics.Vulkan PBufferInfo = &bufferInfo, }; - _holder.Api.UpdateDescriptorSets(_holder.Device, 1, writeDescriptorSet, 0, null); + _holder.Api.UpdateDescriptorSets(_holder.Device, 1, in writeDescriptorSet, 0, null); } } public unsafe void UpdateBuffers(int setIndex, int baseBinding, ReadOnlySpan bufferInfo, DescriptorType type) -{ - - /* - - // DEBUG: Validate inputs - if (bufferInfo.Length == 0) - { - Console.WriteLine("bufferInfo is empty."); - return; - } - - // DEBUG: Check if _descriptorSets and _holder.Device are properly initialized - if (_descriptorSets == null || _descriptorSets.Length <= setIndex) - { - throw new Exception("Descriptor set at the specified index is null or out of range."); - } - - if (_holder?.Device == null) - { - throw new Exception("_holder.Device is null or uninitialized."); - } - - // DEBUG: Check each DescriptorBufferInfo in the span - foreach (var info in bufferInfo) - { - if (info.Buffer.Handle == 0) { - throw new Exception("One of the buffers in bufferInfo is null or uninitialized."); + if (bufferInfo.Length == 0) + { + return; + } + + fixed (DescriptorBufferInfo* pBufferInfo = bufferInfo) + { + var writeDescriptorSet = new WriteDescriptorSet + { + SType = StructureType.WriteDescriptorSet, + DstSet = _descriptorSets[setIndex], + DstBinding = (uint)baseBinding, + DescriptorType = type, + DescriptorCount = (uint)bufferInfo.Length, + PBufferInfo = pBufferInfo, + }; + + _holder.Api.UpdateDescriptorSets(_holder.Device, 1, in writeDescriptorSet, 0, null); + } } - } - */ - - // Proceed if all checks pass - fixed (DescriptorBufferInfo* pBufferInfo = bufferInfo) - { - var writeDescriptorSet = new WriteDescriptorSet - { - SType = StructureType.WriteDescriptorSet, - DstSet = _descriptorSets[setIndex], - DstBinding = (uint)baseBinding, - DescriptorType = type, - DescriptorCount = (uint)bufferInfo.Length, - PBufferInfo = pBufferInfo - }; - - // Update descriptor sets - _holder.Api.UpdateDescriptorSets(_holder.Device, 1, writeDescriptorSet, 0, null); - } -} public unsafe void UpdateImage(int setIndex, int bindingIndex, DescriptorImageInfo imageInfo, DescriptorType type) { if (imageInfo.ImageView.Handle != 0UL) { - var writeDescriptorSet = new WriteDescriptorSet { SType = StructureType.WriteDescriptorSet, @@ -113,31 +84,17 @@ namespace Ryujinx.Graphics.Vulkan PImageInfo = &imageInfo, }; - _holder.Api.UpdateDescriptorSets(_holder.Device, 1, writeDescriptorSet, 0, null); + _holder.Api.UpdateDescriptorSets(_holder.Device, 1, in writeDescriptorSet, 0, null); } } public unsafe void UpdateImages(int setIndex, int baseBinding, ReadOnlySpan imageInfo, DescriptorType type) { - /* - - // DEBUG: Check if imageInfo is Empty if (imageInfo.Length == 0) { - - Console.WriteLine("Error: imageInfo is empty."); return; } - // DEBUG: Check the values inside imageInfo - foreach (var info in imageInfo) - { - Console.WriteLine($"Buffer Handle: {info.ImageView.Handle}"); - } - Console.WriteLine($"SetIndex: {setIndex}, BaseBinding: {baseBinding}, DescriptorType: {type}, ImageInfo Count: {imageInfo.Length}"); - */ - - fixed (DescriptorImageInfo* pImageInfo = imageInfo) { var writeDescriptorSet = new WriteDescriptorSet @@ -150,9 +107,7 @@ namespace Ryujinx.Graphics.Vulkan PImageInfo = pImageInfo, }; - - - _holder.Api.UpdateDescriptorSets(_holder.Device, 1, writeDescriptorSet, 0, null); + _holder.Api.UpdateDescriptorSets(_holder.Device, 1, in writeDescriptorSet, 0, null); } } @@ -185,11 +140,11 @@ namespace Ryujinx.Graphics.Vulkan DstSet = _descriptorSets[setIndex], DstBinding = (uint)(baseBinding + i), DescriptorType = DescriptorType.CombinedImageSampler, - DescriptorCount = 1, + DescriptorCount = (uint)count, PImageInfo = pImageInfo, }; - _holder.Api.UpdateDescriptorSets(_holder.Device, 1, writeDescriptorSet, 0, null); + _holder.Api.UpdateDescriptorSets(_holder.Device, 1, in writeDescriptorSet, 0, null); i += count - 1; } @@ -211,7 +166,7 @@ namespace Ryujinx.Graphics.Vulkan PTexelBufferView = &texelBufferView, }; - _holder.Api.UpdateDescriptorSets(_holder.Device, 1, writeDescriptorSet, 0, null); + _holder.Api.UpdateDescriptorSets(_holder.Device, 1, in writeDescriptorSet, 0, null); } } @@ -241,11 +196,11 @@ namespace Ryujinx.Graphics.Vulkan DstSet = _descriptorSets[setIndex], DstBinding = (uint)baseBinding + i, DescriptorType = type, - DescriptorCount = 1, + DescriptorCount = count, PTexelBufferView = pTexelBufferView + i, }; - _holder.Api.UpdateDescriptorSets(_holder.Device, 1, writeDescriptorSet, 0, null); + _holder.Api.UpdateDescriptorSets(_holder.Device, 1, in writeDescriptorSet, 0, null); } i += count; diff --git a/src/Ryujinx.Graphics.Vulkan/DescriptorSetManager.cs b/src/Ryujinx.Graphics.Vulkan/DescriptorSetManager.cs index 7594384d6..97669942c 100644 --- a/src/Ryujinx.Graphics.Vulkan/DescriptorSetManager.cs +++ b/src/Ryujinx.Graphics.Vulkan/DescriptorSetManager.cs @@ -6,7 +6,7 @@ namespace Ryujinx.Graphics.Vulkan { class DescriptorSetManager : IDisposable { - public const uint MaxSets = 16; + public const uint MaxSets = 8; public class DescriptorPoolHolder : IDisposable { @@ -40,7 +40,7 @@ namespace Ryujinx.Graphics.Vulkan PPoolSizes = pPoolsSize, }; - Api.CreateDescriptorPool(device, descriptorPoolCreateInfo, null, out _pool).ThrowOnError(); + Api.CreateDescriptorPool(device, in descriptorPoolCreateInfo, null, out _pool).ThrowOnError(); } } diff --git a/src/Ryujinx.Graphics.Vulkan/DescriptorSetTemplate.cs b/src/Ryujinx.Graphics.Vulkan/DescriptorSetTemplate.cs new file mode 100644 index 000000000..117f79bb4 --- /dev/null +++ b/src/Ryujinx.Graphics.Vulkan/DescriptorSetTemplate.cs @@ -0,0 +1,210 @@ +using Ryujinx.Graphics.GAL; +using Silk.NET.Vulkan; +using System; +using System.Numerics; +using System.Runtime.CompilerServices; + +namespace Ryujinx.Graphics.Vulkan +{ + class DescriptorSetTemplate : IDisposable + { + /// + /// Renderdoc seems to crash when doing a templated uniform update with count > 1 on a push descriptor. + /// When this is true, consecutive buffers are always updated individually. + /// + private const bool RenderdocPushCountBug = true; + + private readonly VulkanRenderer _gd; + private readonly Device _device; + + public readonly DescriptorUpdateTemplate Template; + public readonly int Size; + + public unsafe DescriptorSetTemplate( + VulkanRenderer gd, + Device device, + ResourceBindingSegment[] segments, + PipelineLayoutCacheEntry plce, + PipelineBindPoint pbp, + int setIndex) + { + _gd = gd; + _device = device; + + // Create a template from the set usages. Assumes the descriptor set is updated in segment order then binding order. + + DescriptorUpdateTemplateEntry* entries = stackalloc DescriptorUpdateTemplateEntry[segments.Length]; + nuint structureOffset = 0; + + for (int seg = 0; seg < segments.Length; seg++) + { + ResourceBindingSegment segment = segments[seg]; + + int binding = segment.Binding; + int count = segment.Count; + + if (IsBufferType(segment.Type)) + { + entries[seg] = new DescriptorUpdateTemplateEntry() + { + DescriptorType = segment.Type.Convert(), + DstBinding = (uint)binding, + DescriptorCount = (uint)count, + Offset = structureOffset, + Stride = (nuint)Unsafe.SizeOf() + }; + + structureOffset += (nuint)(Unsafe.SizeOf() * count); + } + else if (IsBufferTextureType(segment.Type)) + { + entries[seg] = new DescriptorUpdateTemplateEntry() + { + DescriptorType = segment.Type.Convert(), + DstBinding = (uint)binding, + DescriptorCount = (uint)count, + Offset = structureOffset, + Stride = (nuint)Unsafe.SizeOf() + }; + + structureOffset += (nuint)(Unsafe.SizeOf() * count); + } + else + { + entries[seg] = new DescriptorUpdateTemplateEntry() + { + DescriptorType = segment.Type.Convert(), + DstBinding = (uint)binding, + DescriptorCount = (uint)count, + Offset = structureOffset, + Stride = (nuint)Unsafe.SizeOf() + }; + + structureOffset += (nuint)(Unsafe.SizeOf() * count); + } + } + + Size = (int)structureOffset; + + var info = new DescriptorUpdateTemplateCreateInfo() + { + SType = StructureType.DescriptorUpdateTemplateCreateInfo, + DescriptorUpdateEntryCount = (uint)segments.Length, + PDescriptorUpdateEntries = entries, + + TemplateType = DescriptorUpdateTemplateType.DescriptorSet, + DescriptorSetLayout = plce.DescriptorSetLayouts[setIndex], + PipelineBindPoint = pbp, + PipelineLayout = plce.PipelineLayout, + Set = (uint)setIndex, + }; + + DescriptorUpdateTemplate result; + gd.Api.CreateDescriptorUpdateTemplate(device, &info, null, &result).ThrowOnError(); + + Template = result; + } + + public unsafe DescriptorSetTemplate( + VulkanRenderer gd, + Device device, + ResourceDescriptorCollection descriptors, + long updateMask, + PipelineLayoutCacheEntry plce, + PipelineBindPoint pbp, + int setIndex) + { + _gd = gd; + _device = device; + + // Create a template from the set usages. Assumes the descriptor set is updated in segment order then binding order. + int segmentCount = BitOperations.PopCount((ulong)updateMask); + + DescriptorUpdateTemplateEntry* entries = stackalloc DescriptorUpdateTemplateEntry[segmentCount]; + int entry = 0; + nuint structureOffset = 0; + + void AddBinding(int binding, int count) + { + entries[entry++] = new DescriptorUpdateTemplateEntry() + { + DescriptorType = DescriptorType.UniformBuffer, + DstBinding = (uint)binding, + DescriptorCount = (uint)count, + Offset = structureOffset, + Stride = (nuint)Unsafe.SizeOf() + }; + + structureOffset += (nuint)(Unsafe.SizeOf() * count); + } + + int startBinding = 0; + int bindingCount = 0; + + foreach (ResourceDescriptor descriptor in descriptors.Descriptors) + { + for (int i = 0; i < descriptor.Count; i++) + { + int binding = descriptor.Binding + i; + + if ((updateMask & (1L << binding)) != 0) + { + if (bindingCount > 0 && (RenderdocPushCountBug || startBinding + bindingCount != binding)) + { + AddBinding(startBinding, bindingCount); + + bindingCount = 0; + } + + if (bindingCount == 0) + { + startBinding = binding; + } + + bindingCount++; + } + } + } + + if (bindingCount > 0) + { + AddBinding(startBinding, bindingCount); + } + + Size = (int)structureOffset; + + var info = new DescriptorUpdateTemplateCreateInfo() + { + SType = StructureType.DescriptorUpdateTemplateCreateInfo, + DescriptorUpdateEntryCount = (uint)entry, + PDescriptorUpdateEntries = entries, + + TemplateType = DescriptorUpdateTemplateType.PushDescriptorsKhr, + DescriptorSetLayout = plce.DescriptorSetLayouts[setIndex], + PipelineBindPoint = pbp, + PipelineLayout = plce.PipelineLayout, + Set = (uint)setIndex, + }; + + DescriptorUpdateTemplate result; + gd.Api.CreateDescriptorUpdateTemplate(device, &info, null, &result).ThrowOnError(); + + Template = result; + } + + private static bool IsBufferType(ResourceType type) + { + return type == ResourceType.UniformBuffer || type == ResourceType.StorageBuffer; + } + + private static bool IsBufferTextureType(ResourceType type) + { + return type == ResourceType.BufferTexture || type == ResourceType.BufferImage; + } + + public unsafe void Dispose() + { + _gd.Api.DestroyDescriptorUpdateTemplate(_device, Template, null); + } + } +} diff --git a/src/Ryujinx.Graphics.Vulkan/DescriptorSetTemplateUpdater.cs b/src/Ryujinx.Graphics.Vulkan/DescriptorSetTemplateUpdater.cs new file mode 100644 index 000000000..88db7e769 --- /dev/null +++ b/src/Ryujinx.Graphics.Vulkan/DescriptorSetTemplateUpdater.cs @@ -0,0 +1,77 @@ +using Ryujinx.Common; +using Silk.NET.Vulkan; +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace Ryujinx.Graphics.Vulkan +{ + ref struct DescriptorSetTemplateWriter + { + private Span _data; + + public DescriptorSetTemplateWriter(Span data) + { + _data = data; + } + + public void Push(ReadOnlySpan values) where T : unmanaged + { + Span target = MemoryMarshal.Cast(_data); + + values.CopyTo(target); + + _data = _data[(Unsafe.SizeOf() * values.Length)..]; + } + } + + unsafe class DescriptorSetTemplateUpdater : IDisposable + { + private const int SizeGranularity = 512; + + private DescriptorSetTemplate _activeTemplate; + private NativeArray _data; + + private void EnsureSize(int size) + { + if (_data == null || _data.Length < size) + { + _data?.Dispose(); + + int dataSize = BitUtils.AlignUp(size, SizeGranularity); + _data = new NativeArray(dataSize); + } + } + + public DescriptorSetTemplateWriter Begin(DescriptorSetTemplate template) + { + _activeTemplate = template; + + EnsureSize(template.Size); + + return new DescriptorSetTemplateWriter(new Span(_data.Pointer, template.Size)); + } + + public DescriptorSetTemplateWriter Begin(int maxSize) + { + EnsureSize(maxSize); + + return new DescriptorSetTemplateWriter(new Span(_data.Pointer, maxSize)); + } + + public void Commit(VulkanRenderer gd, Device device, DescriptorSet set) + { + gd.Api.UpdateDescriptorSetWithTemplate(device, set, _activeTemplate.Template, _data.Pointer); + } + + public void CommitPushDescriptor(VulkanRenderer gd, CommandBufferScoped cbs, DescriptorSetTemplate template, PipelineLayout layout) + { + gd.PushDescriptorApi.CmdPushDescriptorSetWithTemplate(cbs.CommandBuffer, template.Template, layout, 0, _data.Pointer); + } + + public void Dispose() + { + _data?.Dispose(); + } + } +} diff --git a/src/Ryujinx.Graphics.Vulkan/DescriptorSetUpdater.cs b/src/Ryujinx.Graphics.Vulkan/DescriptorSetUpdater.cs index 95fc72492..3780dc174 100644 --- a/src/Ryujinx.Graphics.Vulkan/DescriptorSetUpdater.cs +++ b/src/Ryujinx.Graphics.Vulkan/DescriptorSetUpdater.cs @@ -3,7 +3,10 @@ using Ryujinx.Graphics.GAL; using Ryujinx.Graphics.Shader; using Silk.NET.Vulkan; using System; +using System.Buffers; +using System.Collections.Generic; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using CompareOp = Ryujinx.Graphics.GAL.CompareOp; using Format = Ryujinx.Graphics.GAL.Format; using SamplerCreateInfo = Ryujinx.Graphics.GAL.SamplerCreateInfo; @@ -13,6 +16,9 @@ namespace Ryujinx.Graphics.Vulkan class DescriptorSetUpdater { private const ulong StorageBufferMaxMirrorable = 0x2000; + + private const int ArrayGrowthSize = 16; + private record struct BufferRef { public Auto Buffer; @@ -34,18 +40,54 @@ namespace Ryujinx.Graphics.Vulkan } } + private record struct TextureRef + { + public ShaderStage Stage; + public TextureView View; + public Auto ImageView; + public Auto Sampler; + + public TextureRef(ShaderStage stage, TextureView view, Auto imageView, Auto sampler) + { + Stage = stage; + View = view; + ImageView = imageView; + Sampler = sampler; + } + } + + private record struct ImageRef + { + public ShaderStage Stage; + public TextureView View; + public Auto ImageView; + + public ImageRef(ShaderStage stage, TextureView view, Auto imageView) + { + Stage = stage; + View = view; + ImageView = imageView; + } + } + + private readonly record struct ArrayRef(ShaderStage Stage, T Array); + private readonly VulkanRenderer _gd; - private readonly PipelineBase _pipeline; + private readonly Device _device; private ShaderCollection _program; private readonly BufferRef[] _uniformBufferRefs; private readonly BufferRef[] _storageBufferRefs; - private readonly Auto[] _textureRefs; - private readonly Auto[] _samplerRefs; - private readonly Auto[] _imageRefs; + private readonly TextureRef[] _textureRefs; + private readonly ImageRef[] _imageRefs; private readonly TextureBuffer[] _bufferTextureRefs; private readonly TextureBuffer[] _bufferImageRefs; - private readonly Format[] _bufferImageFormats; + + private ArrayRef[] _textureArrayRefs; + private ArrayRef[] _imageArrayRefs; + + private ArrayRef[] _textureArrayExtraRefs; + private ArrayRef[] _imageArrayExtraRefs; private readonly DescriptorBufferInfo[] _uniformBuffers; private readonly DescriptorBufferInfo[] _storageBuffers; @@ -54,10 +96,14 @@ namespace Ryujinx.Graphics.Vulkan private readonly BufferView[] _bufferTextures; private readonly BufferView[] _bufferImages; + private readonly DescriptorSetTemplateUpdater _templateUpdater; + private BitMapStruct> _uniformSet; private BitMapStruct> _storageSet; private BitMapStruct> _uniformMirrored; private BitMapStruct> _storageMirrored; + private readonly int[] _uniformSetPd; + private int _pdSequence = 1; private bool _updateDescriptorCacheCbIndex; @@ -78,22 +124,28 @@ namespace Ryujinx.Graphics.Vulkan private readonly TextureView _dummyTexture; private readonly SamplerHolder _dummySampler; - public DescriptorSetUpdater(VulkanRenderer gd, PipelineBase pipeline) + public List FeedbackLoopHazards { get; private set; } + + public DescriptorSetUpdater(VulkanRenderer gd, Device device) { _gd = gd; - _pipeline = pipeline; + _device = device; // Some of the bindings counts needs to be multiplied by 2 because we have buffer and // regular textures/images interleaved on the same descriptor set. _uniformBufferRefs = new BufferRef[Constants.MaxUniformBufferBindings]; _storageBufferRefs = new BufferRef[Constants.MaxStorageBufferBindings]; - _textureRefs = new Auto[Constants.MaxTextureBindings * 2]; - _samplerRefs = new Auto[Constants.MaxTextureBindings * 2]; - _imageRefs = new Auto[Constants.MaxImageBindings * 2]; + _textureRefs = new TextureRef[Constants.MaxTextureBindings * 2]; + _imageRefs = new ImageRef[Constants.MaxImageBindings * 2]; _bufferTextureRefs = new TextureBuffer[Constants.MaxTextureBindings * 2]; _bufferImageRefs = new TextureBuffer[Constants.MaxImageBindings * 2]; - _bufferImageFormats = new Format[Constants.MaxImageBindings * 2]; + + _textureArrayRefs = Array.Empty>(); + _imageArrayRefs = Array.Empty>(); + + _textureArrayExtraRefs = Array.Empty>(); + _imageArrayExtraRefs = Array.Empty>(); _uniformBuffers = new DescriptorBufferInfo[Constants.MaxUniformBufferBindings]; _storageBuffers = new DescriptorBufferInfo[Constants.MaxStorageBufferBindings]; @@ -102,6 +154,8 @@ namespace Ryujinx.Graphics.Vulkan _bufferTextures = new BufferView[Constants.MaxTexturesPerStage]; _bufferImages = new BufferView[Constants.MaxImagesPerStage]; + _uniformSetPd = new int[Constants.MaxUniformBufferBindings]; + var initialImageInfo = new DescriptorImageInfo { ImageLayout = ImageLayout.General, @@ -110,7 +164,7 @@ namespace Ryujinx.Graphics.Vulkan _textures.AsSpan().Fill(initialImageInfo); _images.AsSpan().Fill(initialImageInfo); - if (gd.Capabilities.SupportsNullDescriptors && !OperatingSystem.IsIOS()) + if (gd.Capabilities.SupportsNullDescriptors) { // If null descriptors are supported, we can pass null as the handle. _dummyBuffer = null; @@ -152,12 +206,19 @@ namespace Ryujinx.Graphics.Vulkan 0, 0, 1f)); + + _templateUpdater = new(); } - public void Initialize() + public void Initialize(bool isMainPipeline) { - Span dummyTextureData = stackalloc byte[4]; + MemoryOwner dummyTextureData = MemoryOwner.RentCleared(4); _dummyTexture.SetData(dummyTextureData); + + if (isMainPipeline) + { + FeedbackLoopHazards = new(); + } } private static bool BindingOverlaps(ref DescriptorBufferInfo info, int bindingOffset, int offset, int size) @@ -187,6 +248,7 @@ namespace Ryujinx.Graphics.Vulkan if (BindingOverlaps(ref info, bindingOffset, offset, size)) { _uniformSet.Clear(binding); + _uniformSetPd[binding] = 0; SignalDirty(DirtyFlags.Uniform); } } @@ -217,29 +279,135 @@ namespace Ryujinx.Graphics.Vulkan }); } - public void SetProgram(ShaderCollection program) + public void InsertBindingBarriers(CommandBufferScoped cbs) { + if ((FeedbackLoopHazards?.Count ?? 0) > 0) + { + // Clear existing hazards - they will be rebuilt. + + foreach (TextureView hazard in FeedbackLoopHazards) + { + hazard.DecrementHazardUses(); + } + + FeedbackLoopHazards.Clear(); + } + + foreach (ResourceBindingSegment segment in _program.BindingSegments[PipelineBase.TextureSetIndex]) + { + if (segment.Type == ResourceType.TextureAndSampler) + { + if (!segment.IsArray) + { + for (int i = 0; i < segment.Count; i++) + { + ref var texture = ref _textureRefs[segment.Binding + i]; + texture.View?.PrepareForUsage(cbs, texture.Stage.ConvertToPipelineStageFlags(), FeedbackLoopHazards); + } + } + else + { + ref var arrayRef = ref _textureArrayRefs[segment.Binding]; + PipelineStageFlags stageFlags = arrayRef.Stage.ConvertToPipelineStageFlags(); + arrayRef.Array?.QueueWriteToReadBarriers(cbs, stageFlags); + } + } + } + + foreach (ResourceBindingSegment segment in _program.BindingSegments[PipelineBase.ImageSetIndex]) + { + if (segment.Type == ResourceType.Image) + { + if (!segment.IsArray) + { + for (int i = 0; i < segment.Count; i++) + { + ref var image = ref _imageRefs[segment.Binding + i]; + image.View?.PrepareForUsage(cbs, image.Stage.ConvertToPipelineStageFlags(), FeedbackLoopHazards); + } + } + else + { + ref var arrayRef = ref _imageArrayRefs[segment.Binding]; + PipelineStageFlags stageFlags = arrayRef.Stage.ConvertToPipelineStageFlags(); + arrayRef.Array?.QueueWriteToReadBarriers(cbs, stageFlags); + } + } + } + + for (int setIndex = PipelineBase.DescriptorSetLayouts; setIndex < _program.BindingSegments.Length; setIndex++) + { + var bindingSegments = _program.BindingSegments[setIndex]; + + if (bindingSegments.Length == 0) + { + continue; + } + + ResourceBindingSegment segment = bindingSegments[0]; + + if (segment.IsArray) + { + if (segment.Type == ResourceType.Texture || + segment.Type == ResourceType.Sampler || + segment.Type == ResourceType.TextureAndSampler || + segment.Type == ResourceType.BufferTexture) + { + ref var arrayRef = ref _textureArrayExtraRefs[setIndex - PipelineBase.DescriptorSetLayouts]; + PipelineStageFlags stageFlags = arrayRef.Stage.ConvertToPipelineStageFlags(); + arrayRef.Array?.QueueWriteToReadBarriers(cbs, stageFlags); + } + else if (segment.Type == ResourceType.Image || segment.Type == ResourceType.BufferImage) + { + ref var arrayRef = ref _imageArrayExtraRefs[setIndex - PipelineBase.DescriptorSetLayouts]; + PipelineStageFlags stageFlags = arrayRef.Stage.ConvertToPipelineStageFlags(); + arrayRef.Array?.QueueWriteToReadBarriers(cbs, stageFlags); + } + } + } + } + + public void AdvancePdSequence() + { + if (++_pdSequence == 0) + { + _pdSequence = 1; + } + } + + public void SetProgram(CommandBufferScoped cbs, ShaderCollection program, bool isBound) + { + if (!program.HasSameLayout(_program)) + { + // When the pipeline layout changes, push descriptor bindings are invalidated. + + AdvancePdSequence(); + } + _program = program; _updateDescriptorCacheCbIndex = true; _dirty = DirtyFlags.All; } - public void SetImage(int binding, ITexture image, Format imageFormat) + public void SetImage(CommandBufferScoped cbs, ShaderStage stage, int binding, ITexture image) { if (image is TextureBuffer imageBuffer) { _bufferImageRefs[binding] = imageBuffer; - _bufferImageFormats[binding] = imageFormat; } else if (image is TextureView view) { - _imageRefs[binding] = view.GetView(imageFormat).GetIdentityImageView(); + ref ImageRef iRef = ref _imageRefs[binding]; + + iRef.View?.ClearUsage(FeedbackLoopHazards); + view?.PrepareForUsage(cbs, stage.ConvertToPipelineStageFlags(), FeedbackLoopHazards); + + iRef = new(stage, view, view.GetIdentityImageView()); } else { - _imageRefs[binding] = null; + _imageRefs[binding] = default; _bufferImageRefs[binding] = null; - _bufferImageFormats[binding] = default; } SignalDirty(DirtyFlags.Image); @@ -247,7 +415,7 @@ namespace Ryujinx.Graphics.Vulkan public void SetImage(int binding, Auto image) { - _imageRefs[binding] = image; + _imageRefs[binding] = new(ShaderStage.Compute, null, image); SignalDirty(DirtyFlags.Image); } @@ -332,15 +500,16 @@ namespace Ryujinx.Graphics.Vulkan } else if (texture is TextureView view) { - view.Storage.InsertWriteToReadBarrier(cbs, AccessFlags.ShaderReadBit, stage.ConvertToPipelineStageFlags()); + ref TextureRef iRef = ref _textureRefs[binding]; - _textureRefs[binding] = view.GetImageView(); - _samplerRefs[binding] = ((SamplerHolder)sampler)?.GetSampler(); + iRef.View?.ClearUsage(FeedbackLoopHazards); + view?.PrepareForUsage(cbs, stage.ConvertToPipelineStageFlags(), FeedbackLoopHazards); + + iRef = new(stage, view, view.GetImageView(), ((SamplerHolder)sampler)?.GetSampler()); } else { - _textureRefs[binding] = null; - _samplerRefs[binding] = null; + _textureRefs[binding] = default; _bufferTextureRefs[binding] = null; } @@ -356,10 +525,9 @@ namespace Ryujinx.Graphics.Vulkan { if (texture is TextureView view) { - view.Storage.InsertWriteToReadBarrier(cbs, AccessFlags.ShaderReadBit, stage.ConvertToPipelineStageFlags()); + view.Storage.QueueWriteToReadBarrier(cbs, AccessFlags.ShaderReadBit, stage.ConvertToPipelineStageFlags()); - _textureRefs[binding] = view.GetIdentityImageView(); - _samplerRefs[binding] = ((SamplerHolder)sampler)?.GetSampler(); + _textureRefs[binding] = new(stage, view, view.GetIdentityImageView(), ((SamplerHolder)sampler)?.GetSampler()); SignalDirty(DirtyFlags.Texture); } @@ -369,6 +537,98 @@ namespace Ryujinx.Graphics.Vulkan } } + public void SetTextureArray(CommandBufferScoped cbs, ShaderStage stage, int binding, ITextureArray array) + { + ref ArrayRef arrayRef = ref GetArrayRef(ref _textureArrayRefs, binding, ArrayGrowthSize); + + if (arrayRef.Stage != stage || arrayRef.Array != array) + { + arrayRef.Array?.DecrementBindCount(); + + if (array is TextureArray textureArray) + { + textureArray.IncrementBindCount(); + textureArray.QueueWriteToReadBarriers(cbs, stage.ConvertToPipelineStageFlags()); + } + + arrayRef = new ArrayRef(stage, array as TextureArray); + + SignalDirty(DirtyFlags.Texture); + } + } + + public void SetTextureArraySeparate(CommandBufferScoped cbs, ShaderStage stage, int setIndex, ITextureArray array) + { + ref ArrayRef arrayRef = ref GetArrayRef(ref _textureArrayExtraRefs, setIndex - PipelineBase.DescriptorSetLayouts); + + if (arrayRef.Stage != stage || arrayRef.Array != array) + { + arrayRef.Array?.DecrementBindCount(); + + if (array is TextureArray textureArray) + { + textureArray.IncrementBindCount(); + textureArray.QueueWriteToReadBarriers(cbs, stage.ConvertToPipelineStageFlags()); + } + + arrayRef = new ArrayRef(stage, array as TextureArray); + + SignalDirty(DirtyFlags.Texture); + } + } + + public void SetImageArray(CommandBufferScoped cbs, ShaderStage stage, int binding, IImageArray array) + { + ref ArrayRef arrayRef = ref GetArrayRef(ref _imageArrayRefs, binding, ArrayGrowthSize); + + if (arrayRef.Stage != stage || arrayRef.Array != array) + { + arrayRef.Array?.DecrementBindCount(); + + if (array is ImageArray imageArray) + { + imageArray.IncrementBindCount(); + imageArray.QueueWriteToReadBarriers(cbs, stage.ConvertToPipelineStageFlags()); + } + + arrayRef = new ArrayRef(stage, array as ImageArray); + + SignalDirty(DirtyFlags.Image); + } + } + + public void SetImageArraySeparate(CommandBufferScoped cbs, ShaderStage stage, int setIndex, IImageArray array) + { + ref ArrayRef arrayRef = ref GetArrayRef(ref _imageArrayExtraRefs, setIndex - PipelineBase.DescriptorSetLayouts); + + if (arrayRef.Stage != stage || arrayRef.Array != array) + { + arrayRef.Array?.DecrementBindCount(); + + if (array is ImageArray imageArray) + { + imageArray.IncrementBindCount(); + imageArray.QueueWriteToReadBarriers(cbs, stage.ConvertToPipelineStageFlags()); + } + + arrayRef = new ArrayRef(stage, array as ImageArray); + + SignalDirty(DirtyFlags.Image); + } + } + + private static ref ArrayRef GetArrayRef(ref ArrayRef[] array, int index, int growthSize = 1) + { + ArgumentOutOfRangeException.ThrowIfNegative(index); + + if (array.Length <= index) + { + Array.Resize(ref array, index + growthSize); + } + + return ref array[index]; + } + public void SetUniformBuffers(CommandBuffer commandBuffer, ReadOnlySpan buffers) { for (int i = 0; i < buffers.Length; i++) @@ -396,6 +656,7 @@ namespace Ryujinx.Graphics.Vulkan if (!currentBufferRef.Equals(newRef) || currentInfo.Range != info.Range) { _uniformSet.Clear(index); + _uniformSetPd[index] = 0; currentInfo = info; currentBufferRef = newRef; @@ -417,31 +678,47 @@ namespace Ryujinx.Graphics.Vulkan return; } + var program = _program; + if (_dirty.HasFlag(DirtyFlags.Uniform)) { - if (_program.UsePushDescriptors) + if (program.UsePushDescriptors) { - UpdateAndBindUniformBufferPd(cbs, pbp); + UpdateAndBindUniformBufferPd(cbs); } else { - UpdateAndBind(cbs, PipelineBase.UniformSetIndex, pbp); + UpdateAndBind(cbs, program, PipelineBase.UniformSetIndex, pbp); } } if (_dirty.HasFlag(DirtyFlags.Storage)) { - UpdateAndBind(cbs, PipelineBase.StorageSetIndex, pbp); + UpdateAndBind(cbs, program, PipelineBase.StorageSetIndex, pbp); } if (_dirty.HasFlag(DirtyFlags.Texture)) { - UpdateAndBind(cbs, PipelineBase.TextureSetIndex, pbp); + if (program.UpdateTexturesWithoutTemplate) + { + UpdateAndBindTexturesWithoutTemplate(cbs, program, pbp); + } + else + { + UpdateAndBind(cbs, program, PipelineBase.TextureSetIndex, pbp); + } } if (_dirty.HasFlag(DirtyFlags.Image)) { - UpdateAndBind(cbs, PipelineBase.ImageSetIndex, pbp); + UpdateAndBind(cbs, program, PipelineBase.ImageSetIndex, pbp); + } + + if (program.BindingSegments.Length > PipelineBase.DescriptorSetLayouts) + { + // Program is using extra sets, we need to bind those too. + + BindExtraSets(cbs, program, pbp); } _dirty = DirtyFlags.None; @@ -481,9 +758,8 @@ namespace Ryujinx.Graphics.Vulkan } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void UpdateAndBind(CommandBufferScoped cbs, int setIndex, PipelineBindPoint pbp) + private void UpdateAndBind(CommandBufferScoped cbs, ShaderCollection program, int setIndex, PipelineBindPoint pbp) { - var program = _program; var bindingSegments = program.BindingSegments[setIndex]; if (bindingSegments.Length == 0) @@ -509,6 +785,10 @@ namespace Ryujinx.Graphics.Vulkan } } + DescriptorSetTemplate template = program.Templates[setIndex]; + + DescriptorSetTemplateWriter tu = _templateUpdater.Begin(template); + foreach (ResourceBindingSegment segment in bindingSegments) { int binding = segment.Binding; @@ -531,7 +811,8 @@ namespace Ryujinx.Graphics.Vulkan } ReadOnlySpan uniformBuffers = _uniformBuffers; - dsc.UpdateBuffers(0, binding, uniformBuffers.Slice(binding, count), DescriptorType.UniformBuffer); + + tu.Push(uniformBuffers.Slice(binding, count)); } else if (setIndex == PipelineBase.StorageSetIndex) { @@ -556,9 +837,133 @@ namespace Ryujinx.Graphics.Vulkan } ReadOnlySpan storageBuffers = _storageBuffers; - dsc.UpdateBuffers(0, binding, storageBuffers.Slice(binding, count), DescriptorType.StorageBuffer); + + tu.Push(storageBuffers.Slice(binding, count)); } else if (setIndex == PipelineBase.TextureSetIndex) + { + if (!segment.IsArray) + { + if (segment.Type != ResourceType.BufferTexture) + { + Span textures = _textures; + + for (int i = 0; i < count; i++) + { + ref var texture = ref textures[i]; + ref var refs = ref _textureRefs[binding + i]; + + texture.ImageView = refs.ImageView?.Get(cbs).Value ?? default; + texture.Sampler = refs.Sampler?.Get(cbs).Value ?? default; + + if (texture.ImageView.Handle == 0) + { + texture.ImageView = _dummyTexture.GetImageView().Get(cbs).Value; + } + + if (texture.Sampler.Handle == 0) + { + texture.Sampler = _dummySampler.GetSampler().Get(cbs).Value; + } + } + + tu.Push(textures[..count]); + } + else + { + Span bufferTextures = _bufferTextures; + + for (int i = 0; i < count; i++) + { + bufferTextures[i] = _bufferTextureRefs[binding + i]?.GetBufferView(cbs, false) ?? default; + } + + tu.Push(bufferTextures[..count]); + } + } + else + { + if (segment.Type != ResourceType.BufferTexture) + { + tu.Push(_textureArrayRefs[binding].Array.GetImageInfos(_gd, cbs, _dummyTexture, _dummySampler)); + } + else + { + tu.Push(_textureArrayRefs[binding].Array.GetBufferViews(cbs)); + } + } + } + else if (setIndex == PipelineBase.ImageSetIndex) + { + if (!segment.IsArray) + { + if (segment.Type != ResourceType.BufferImage) + { + Span images = _images; + + for (int i = 0; i < count; i++) + { + images[i].ImageView = _imageRefs[binding + i].ImageView?.Get(cbs).Value ?? default; + } + + tu.Push(images[..count]); + } + else + { + Span bufferImages = _bufferImages; + + for (int i = 0; i < count; i++) + { + bufferImages[i] = _bufferImageRefs[binding + i]?.GetBufferView(cbs, true) ?? default; + } + + tu.Push(bufferImages[..count]); + } + } + else + { + if (segment.Type != ResourceType.BufferTexture) + { + tu.Push(_imageArrayRefs[binding].Array.GetImageInfos(_gd, cbs, _dummyTexture)); + } + else + { + tu.Push(_imageArrayRefs[binding].Array.GetBufferViews(cbs)); + } + } + } + } + + var sets = dsc.GetSets(); + _templateUpdater.Commit(_gd, _device, sets[0]); + + _gd.Api.CmdBindDescriptorSets(cbs.CommandBuffer, pbp, _program.PipelineLayout, (uint)setIndex, 1, sets, 0, ReadOnlySpan.Empty); + } + + private void UpdateAndBindTexturesWithoutTemplate(CommandBufferScoped cbs, ShaderCollection program, PipelineBindPoint pbp) + { + int setIndex = PipelineBase.TextureSetIndex; + var bindingSegments = program.BindingSegments[setIndex]; + + if (bindingSegments.Length == 0) + { + return; + } + + if (_updateDescriptorCacheCbIndex) + { + _updateDescriptorCacheCbIndex = false; + program.UpdateDescriptorCacheCommandBufferIndex(cbs.CommandBufferIndex); + } + + var dsc = program.GetNewDescriptorSetCollection(setIndex, out _).Get(cbs); + + foreach (ResourceBindingSegment segment in bindingSegments) + { + int binding = segment.Binding; + int count = segment.Count; + + if (!segment.IsArray) { if (segment.Type != ResourceType.BufferTexture) { @@ -567,9 +972,10 @@ namespace Ryujinx.Graphics.Vulkan for (int i = 0; i < count; i++) { ref var texture = ref textures[i]; + ref var refs = ref _textureRefs[binding + i]; - texture.ImageView = _textureRefs[binding + i]?.Get(cbs).Value ?? default; - texture.Sampler = _samplerRefs[binding + i]?.Get(cbs).Value ?? default; + texture.ImageView = refs.ImageView?.Get(cbs).Value ?? default; + texture.Sampler = refs.Sampler?.Get(cbs).Value ?? default; if (texture.ImageView.Handle == 0) { @@ -580,14 +986,9 @@ namespace Ryujinx.Graphics.Vulkan { texture.Sampler = _dummySampler.GetSampler().Get(cbs).Value; } - if (OperatingSystem.IsIOS()) { - Span singleTexture = textures.Slice(i, 1); - dsc.UpdateImages(0, binding + i, singleTexture, DescriptorType.CombinedImageSampler); - } - } - if (!OperatingSystem.IsIOS()) { - dsc.UpdateImages(0, binding, textures[..count], DescriptorType.CombinedImageSampler); } + + dsc.UpdateImages(0, binding, textures[..count], DescriptorType.CombinedImageSampler); } else { @@ -601,35 +1002,15 @@ namespace Ryujinx.Graphics.Vulkan dsc.UpdateBufferImages(0, binding, bufferTextures[..count], DescriptorType.UniformTexelBuffer); } } - else if (setIndex == PipelineBase.ImageSetIndex) + else { - if (segment.Type != ResourceType.BufferImage) + if (segment.Type != ResourceType.BufferTexture) { - Span images = _images; - - for (int i = 0; i < count; i++) - { - images[i].ImageView = _imageRefs[binding + i]?.Get(cbs).Value ?? default; - if (OperatingSystem.IsIOS()) { - Span singleImage = images.Slice(i, 1); - dsc.UpdateImages(0, binding + i, singleImage, DescriptorType.StorageImage); - } - } - - if (!OperatingSystem.IsIOS()) { - dsc.UpdateImages(0, binding, images[..count], DescriptorType.StorageImage); - } + dsc.UpdateImages(0, binding, _textureArrayRefs[binding].Array.GetImageInfos(_gd, cbs, _dummyTexture, _dummySampler), DescriptorType.CombinedImageSampler); } else { - Span bufferImages = _bufferImages; - - for (int i = 0; i < count; i++) - { - bufferImages[i] = _bufferImageRefs[binding + i]?.GetBufferView(cbs, _bufferImageFormats[binding + i], true) ?? default; - } - - dsc.UpdateBufferImages(0, binding, bufferImages[..count], DescriptorType.StorageTexelBuffer); + dsc.UpdateBufferImages(0, binding, _textureArrayRefs[binding].Array.GetBufferViews(cbs), DescriptorType.UniformTexelBuffer); } } } @@ -639,45 +1020,22 @@ namespace Ryujinx.Graphics.Vulkan _gd.Api.CmdBindDescriptorSets(cbs.CommandBuffer, pbp, _program.PipelineLayout, (uint)setIndex, 1, sets, 0, ReadOnlySpan.Empty); } - private unsafe void UpdateBuffers( - CommandBufferScoped cbs, - PipelineBindPoint pbp, - int baseBinding, - ReadOnlySpan bufferInfo, - DescriptorType type) - { - if (bufferInfo.Length == 0) - { - return; - } - - fixed (DescriptorBufferInfo* pBufferInfo = bufferInfo) - { - var writeDescriptorSet = new WriteDescriptorSet - { - SType = StructureType.WriteDescriptorSet, - DstBinding = (uint)baseBinding, - DescriptorType = type, - DescriptorCount = (uint)bufferInfo.Length, - PBufferInfo = pBufferInfo, - }; - - _gd.PushDescriptorApi.CmdPushDescriptorSet(cbs.CommandBuffer, pbp, _program.PipelineLayout, 0, 1, &writeDescriptorSet); - } - } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void UpdateAndBindUniformBufferPd(CommandBufferScoped cbs, PipelineBindPoint pbp) + private void UpdateAndBindUniformBufferPd(CommandBufferScoped cbs) { + int sequence = _pdSequence; var bindingSegments = _program.BindingSegments[PipelineBase.UniformSetIndex]; var dummyBuffer = _dummyBuffer?.GetBuffer(); + long updatedBindings = 0; + DescriptorSetTemplateWriter writer = _templateUpdater.Begin(32 * Unsafe.SizeOf()); + foreach (ResourceBindingSegment segment in bindingSegments) { int binding = segment.Binding; int count = segment.Count; - bool doUpdate = false; + ReadOnlySpan uniformBuffers = _uniformBuffers; for (int i = 0; i < count; i++) { @@ -686,16 +1044,28 @@ namespace Ryujinx.Graphics.Vulkan if (_uniformSet.Set(index)) { ref BufferRef buffer = ref _uniformBufferRefs[index]; - UpdateBuffer(cbs, ref _uniformBuffers[index], ref buffer, dummyBuffer, true); - doUpdate = true; + + bool mirrored = UpdateBuffer(cbs, ref _uniformBuffers[index], ref buffer, dummyBuffer, true); + + _uniformMirrored.Set(index, mirrored); + } + + if (_uniformSetPd[index] != sequence) + { + // Need to set this push descriptor (even if the buffer binding has not changed) + + _uniformSetPd[index] = sequence; + updatedBindings |= 1L << index; + + writer.Push(MemoryMarshal.CreateReadOnlySpan(ref _uniformBuffers[index], 1)); } } + } - if (doUpdate) - { - ReadOnlySpan uniformBuffers = _uniformBuffers; - UpdateBuffers(cbs, pbp, binding, uniformBuffers.Slice(binding, count), DescriptorType.UniformBuffer); - } + if (updatedBindings > 0) + { + DescriptorSetTemplate template = _program.GetPushDescriptorTemplate(updatedBindings); + _templateUpdater.CommitPushDescriptor(_gd, cbs, template, _program.PipelineLayout); } } @@ -715,6 +1085,56 @@ namespace Ryujinx.Graphics.Vulkan } } + private void BindExtraSets(CommandBufferScoped cbs, ShaderCollection program, PipelineBindPoint pbp) + { + for (int setIndex = PipelineBase.DescriptorSetLayouts; setIndex < program.BindingSegments.Length; setIndex++) + { + var bindingSegments = program.BindingSegments[setIndex]; + + if (bindingSegments.Length == 0) + { + continue; + } + + ResourceBindingSegment segment = bindingSegments[0]; + + if (segment.IsArray) + { + DescriptorSet[] sets = null; + + if (segment.Type == ResourceType.Texture || + segment.Type == ResourceType.Sampler || + segment.Type == ResourceType.TextureAndSampler || + segment.Type == ResourceType.BufferTexture) + { + sets = _textureArrayExtraRefs[setIndex - PipelineBase.DescriptorSetLayouts].Array.GetDescriptorSets( + _device, + cbs, + _templateUpdater, + program, + setIndex, + _dummyTexture, + _dummySampler); + } + else if (segment.Type == ResourceType.Image || segment.Type == ResourceType.BufferImage) + { + sets = _imageArrayExtraRefs[setIndex - PipelineBase.DescriptorSetLayouts].Array.GetDescriptorSets( + _device, + cbs, + _templateUpdater, + program, + setIndex, + _dummyTexture); + } + + if (sets != null) + { + _gd.Api.CmdBindDescriptorSets(cbs.CommandBuffer, pbp, _program.PipelineLayout, (uint)setIndex, 1, sets, 0, ReadOnlySpan.Empty); + } + } + } + } + public void SignalCommandBufferChange() { _updateDescriptorCacheCbIndex = true; @@ -722,6 +1142,17 @@ namespace Ryujinx.Graphics.Vulkan _uniformSet.Clear(); _storageSet.Clear(); + AdvancePdSequence(); + } + + public void ForceTextureDirty() + { + SignalDirty(DirtyFlags.Texture); + } + + public void ForceImageDirty() + { + SignalDirty(DirtyFlags.Image); } private static void SwapBuffer(BufferRef[] list, Auto from, Auto to) @@ -747,6 +1178,7 @@ namespace Ryujinx.Graphics.Vulkan { _dummyTexture.Dispose(); _dummySampler.Dispose(); + _templateUpdater.Dispose(); } } diff --git a/src/Ryujinx.Graphics.Vulkan/Effects/AreaScalingFilter.cs b/src/Ryujinx.Graphics.Vulkan/Effects/AreaScalingFilter.cs new file mode 100644 index 000000000..87b46df80 --- /dev/null +++ b/src/Ryujinx.Graphics.Vulkan/Effects/AreaScalingFilter.cs @@ -0,0 +1,101 @@ +using Ryujinx.Common; +using Ryujinx.Graphics.GAL; +using Ryujinx.Graphics.Shader; +using Ryujinx.Graphics.Shader.Translation; +using Silk.NET.Vulkan; +using System; +using Extent2D = Ryujinx.Graphics.GAL.Extents2D; +using Format = Silk.NET.Vulkan.Format; +using SamplerCreateInfo = Ryujinx.Graphics.GAL.SamplerCreateInfo; + +namespace Ryujinx.Graphics.Vulkan.Effects +{ + internal class AreaScalingFilter : IScalingFilter + { + private readonly VulkanRenderer _renderer; + private PipelineHelperShader _pipeline; + private ISampler _sampler; + private ShaderCollection _scalingProgram; + private Device _device; + + public float Level { get; set; } + + public AreaScalingFilter(VulkanRenderer renderer, Device device) + { + _device = device; + _renderer = renderer; + + Initialize(); + } + + public void Dispose() + { + _pipeline.Dispose(); + _scalingProgram.Dispose(); + _sampler.Dispose(); + } + + public void Initialize() + { + _pipeline = new PipelineHelperShader(_renderer, _device); + + _pipeline.Initialize(); + + var scalingShader = EmbeddedResources.Read("Ryujinx.Graphics.Vulkan/Effects/Shaders/AreaScaling.spv"); + + var scalingResourceLayout = new ResourceLayoutBuilder() + .Add(ResourceStages.Compute, ResourceType.UniformBuffer, 2) + .Add(ResourceStages.Compute, ResourceType.TextureAndSampler, 1) + .Add(ResourceStages.Compute, ResourceType.Image, 0, true).Build(); + + _sampler = _renderer.CreateSampler(SamplerCreateInfo.Create(MinFilter.Linear, MagFilter.Linear)); + + _scalingProgram = _renderer.CreateProgramWithMinimalLayout(new[] + { + new ShaderSource(scalingShader, ShaderStage.Compute, TargetLanguage.Spirv), + }, scalingResourceLayout); + } + + public void Run( + TextureView view, + CommandBufferScoped cbs, + Auto destinationTexture, + Format format, + int width, + int height, + Extent2D source, + Extent2D destination) + { + _pipeline.SetCommandBuffer(cbs); + _pipeline.SetProgram(_scalingProgram); + _pipeline.SetTextureAndSampler(ShaderStage.Compute, 1, view, _sampler); + + ReadOnlySpan dimensionsBuffer = stackalloc float[] + { + source.X1, + source.X2, + source.Y1, + source.Y2, + destination.X1, + destination.X2, + destination.Y1, + destination.Y2, + }; + + int rangeSize = dimensionsBuffer.Length * sizeof(float); + using var buffer = _renderer.BufferManager.ReserveOrCreate(_renderer, cbs, rangeSize); + buffer.Holder.SetDataUnchecked(buffer.Offset, dimensionsBuffer); + + int threadGroupWorkRegionDim = 16; + int dispatchX = (width + (threadGroupWorkRegionDim - 1)) / threadGroupWorkRegionDim; + int dispatchY = (height + (threadGroupWorkRegionDim - 1)) / threadGroupWorkRegionDim; + + _pipeline.SetUniformBuffers(stackalloc[] { new BufferAssignment(2, buffer.Range) }); + _pipeline.SetImage(0, destinationTexture); + _pipeline.DispatchCompute(dispatchX, dispatchY, 1); + _pipeline.ComputeBarrier(); + + _pipeline.Finish(); + } + } +} diff --git a/src/Ryujinx.Graphics.Vulkan/Effects/FsrScalingFilter.cs b/src/Ryujinx.Graphics.Vulkan/Effects/FsrScalingFilter.cs index 23acdcf8f..080dde5e5 100644 --- a/src/Ryujinx.Graphics.Vulkan/Effects/FsrScalingFilter.cs +++ b/src/Ryujinx.Graphics.Vulkan/Effects/FsrScalingFilter.cs @@ -59,14 +59,14 @@ namespace Ryujinx.Graphics.Vulkan.Effects var scalingResourceLayout = new ResourceLayoutBuilder() .Add(ResourceStages.Compute, ResourceType.UniformBuffer, 2) .Add(ResourceStages.Compute, ResourceType.TextureAndSampler, 1) - .Add(ResourceStages.Compute, ResourceType.Image, 0).Build(); + .Add(ResourceStages.Compute, ResourceType.Image, 0, true).Build(); var sharpeningResourceLayout = new ResourceLayoutBuilder() .Add(ResourceStages.Compute, ResourceType.UniformBuffer, 2) .Add(ResourceStages.Compute, ResourceType.UniformBuffer, 3) .Add(ResourceStages.Compute, ResourceType.UniformBuffer, 4) .Add(ResourceStages.Compute, ResourceType.TextureAndSampler, 1) - .Add(ResourceStages.Compute, ResourceType.Image, 0).Build(); + .Add(ResourceStages.Compute, ResourceType.Image, 0, true).Build(); _sampler = _renderer.CreateSampler(SamplerCreateInfo.Create(MinFilter.Linear, MagFilter.Linear)); @@ -142,36 +142,31 @@ namespace Ryujinx.Graphics.Vulkan.Effects }; int rangeSize = dimensionsBuffer.Length * sizeof(float); - var bufferHandle = _renderer.BufferManager.CreateWithHandle(_renderer, rangeSize); - _renderer.BufferManager.SetData(bufferHandle, 0, dimensionsBuffer); + using var buffer = _renderer.BufferManager.ReserveOrCreate(_renderer, cbs, rangeSize); + buffer.Holder.SetDataUnchecked(buffer.Offset, dimensionsBuffer); - ReadOnlySpan sharpeningBuffer = stackalloc float[] { 1.5f - (Level * 0.01f * 1.5f) }; - var sharpeningBufferHandle = _renderer.BufferManager.CreateWithHandle(_renderer, sizeof(float)); - _renderer.BufferManager.SetData(sharpeningBufferHandle, 0, sharpeningBuffer); + ReadOnlySpan sharpeningBufferData = stackalloc float[] { 1.5f - (Level * 0.01f * 1.5f) }; + using var sharpeningBuffer = _renderer.BufferManager.ReserveOrCreate(_renderer, cbs, sizeof(float)); + sharpeningBuffer.Holder.SetDataUnchecked(sharpeningBuffer.Offset, sharpeningBufferData); int threadGroupWorkRegionDim = 16; int dispatchX = (width + (threadGroupWorkRegionDim - 1)) / threadGroupWorkRegionDim; int dispatchY = (height + (threadGroupWorkRegionDim - 1)) / threadGroupWorkRegionDim; - var bufferRanges = new BufferRange(bufferHandle, 0, rangeSize); - _pipeline.SetUniformBuffers(stackalloc[] { new BufferAssignment(2, bufferRanges) }); - _pipeline.SetImage(0, _intermediaryTexture, FormatTable.ConvertRgba8SrgbToUnorm(view.Info.Format)); + _pipeline.SetUniformBuffers(stackalloc[] { new BufferAssignment(2, buffer.Range) }); + _pipeline.SetImage(ShaderStage.Compute, 0, _intermediaryTexture.GetView(FormatTable.ConvertRgba8SrgbToUnorm(view.Info.Format))); _pipeline.DispatchCompute(dispatchX, dispatchY, 1); _pipeline.ComputeBarrier(); // Sharpening pass _pipeline.SetProgram(_sharpeningProgram); _pipeline.SetTextureAndSampler(ShaderStage.Compute, 1, _intermediaryTexture, _sampler); - var sharpeningRange = new BufferRange(sharpeningBufferHandle, 0, sizeof(float)); - _pipeline.SetUniformBuffers(stackalloc[] { new BufferAssignment(4, sharpeningRange) }); + _pipeline.SetUniformBuffers(stackalloc[] { new BufferAssignment(4, sharpeningBuffer.Range) }); _pipeline.SetImage(0, destinationTexture); _pipeline.DispatchCompute(dispatchX, dispatchY, 1); _pipeline.ComputeBarrier(); _pipeline.Finish(); - - _renderer.BufferManager.Delete(bufferHandle); - _renderer.BufferManager.Delete(sharpeningBufferHandle); } } } diff --git a/src/Ryujinx.Graphics.Vulkan/Effects/FxaaPostProcessingEffect.cs b/src/Ryujinx.Graphics.Vulkan/Effects/FxaaPostProcessingEffect.cs index 67e461e51..26314b7bf 100644 --- a/src/Ryujinx.Graphics.Vulkan/Effects/FxaaPostProcessingEffect.cs +++ b/src/Ryujinx.Graphics.Vulkan/Effects/FxaaPostProcessingEffect.cs @@ -42,7 +42,7 @@ namespace Ryujinx.Graphics.Vulkan.Effects var resourceLayout = new ResourceLayoutBuilder() .Add(ResourceStages.Compute, ResourceType.UniformBuffer, 2) .Add(ResourceStages.Compute, ResourceType.TextureAndSampler, 1) - .Add(ResourceStages.Compute, ResourceType.Image, 0).Build(); + .Add(ResourceStages.Compute, ResourceType.Image, 0, true).Build(); _samplerLinear = _renderer.CreateSampler(SamplerCreateInfo.Create(MinFilter.Linear, MagFilter.Linear)); @@ -66,20 +66,18 @@ namespace Ryujinx.Graphics.Vulkan.Effects ReadOnlySpan resolutionBuffer = stackalloc float[] { view.Width, view.Height }; int rangeSize = resolutionBuffer.Length * sizeof(float); - var bufferHandle = _renderer.BufferManager.CreateWithHandle(_renderer, rangeSize); + using var buffer = _renderer.BufferManager.ReserveOrCreate(_renderer, cbs, rangeSize); - _renderer.BufferManager.SetData(bufferHandle, 0, resolutionBuffer); + buffer.Holder.SetDataUnchecked(buffer.Offset, resolutionBuffer); - var bufferRanges = new BufferRange(bufferHandle, 0, rangeSize); - _pipeline.SetUniformBuffers(stackalloc[] { new BufferAssignment(2, bufferRanges) }); + _pipeline.SetUniformBuffers(stackalloc[] { new BufferAssignment(2, buffer.Range) }); var dispatchX = BitUtils.DivRoundUp(view.Width, IPostProcessingEffect.LocalGroupSize); var dispatchY = BitUtils.DivRoundUp(view.Height, IPostProcessingEffect.LocalGroupSize); - _pipeline.SetImage(0, _texture, FormatTable.ConvertRgba8SrgbToUnorm(view.Info.Format)); + _pipeline.SetImage(ShaderStage.Compute, 0, _texture.GetView(FormatTable.ConvertRgba8SrgbToUnorm(view.Info.Format))); _pipeline.DispatchCompute(dispatchX, dispatchY, 1); - _renderer.BufferManager.Delete(bufferHandle); _pipeline.ComputeBarrier(); _pipeline.Finish(); diff --git a/src/Ryujinx.Graphics.Vulkan/Effects/Shaders/AreaScaling.glsl b/src/Ryujinx.Graphics.Vulkan/Effects/Shaders/AreaScaling.glsl new file mode 100644 index 000000000..e34dd77dd --- /dev/null +++ b/src/Ryujinx.Graphics.Vulkan/Effects/Shaders/AreaScaling.glsl @@ -0,0 +1,122 @@ +// Scaling + +#version 430 core +layout (local_size_x = 16, local_size_y = 16) in; +layout( rgba8, binding = 0, set = 3) uniform image2D imgOutput; +layout( binding = 1, set = 2) uniform sampler2D Source; +layout( binding = 2 ) uniform dimensions{ + float srcX0; + float srcX1; + float srcY0; + float srcY1; + float dstX0; + float dstX1; + float dstY0; + float dstY1; +}; + +/***** Area Sampling *****/ + +// By Sam Belliveau and Filippo Tarpini. Public Domain license. +// Effectively a more accurate sharp bilinear filter when upscaling, +// that also works as a mathematically perfect downscale filter. +// https://entropymine.com/imageworsener/pixelmixing/ +// https://github.com/obsproject/obs-studio/pull/1715 +// https://legacy.imagemagick.org/Usage/filter/ +vec4 AreaSampling(vec2 xy) +{ + // Determine the sizes of the source and target images. + vec2 source_size = vec2(abs(srcX1 - srcX0), abs(srcY1 - srcY0)); + vec2 target_size = vec2(abs(dstX1 - dstX0), abs(dstY1 - dstY0)); + vec2 inverted_target_size = vec2(1.0) / target_size; + + // Compute the top-left and bottom-right corners of the target pixel box. + vec2 t_beg = floor(xy - vec2(dstX0 < dstX1 ? dstX0 : dstX1, dstY0 < dstY1 ? dstY0 : dstY1)); + vec2 t_end = t_beg + vec2(1.0, 1.0); + + // Convert the target pixel box to source pixel box. + vec2 beg = t_beg * inverted_target_size * source_size; + vec2 end = t_end * inverted_target_size * source_size; + + // Compute the top-left and bottom-right corners of the pixel box. + ivec2 f_beg = ivec2(beg); + ivec2 f_end = ivec2(end); + + // Compute how much of the start and end pixels are covered horizontally & vertically. + float area_w = 1.0 - fract(beg.x); + float area_n = 1.0 - fract(beg.y); + float area_e = fract(end.x); + float area_s = fract(end.y); + + // Compute the areas of the corner pixels in the pixel box. + float area_nw = area_n * area_w; + float area_ne = area_n * area_e; + float area_sw = area_s * area_w; + float area_se = area_s * area_e; + + // Initialize the color accumulator. + vec4 avg_color = vec4(0.0, 0.0, 0.0, 0.0); + + // Accumulate corner pixels. + avg_color += area_nw * texelFetch(Source, ivec2(f_beg.x, f_beg.y), 0); + avg_color += area_ne * texelFetch(Source, ivec2(f_end.x, f_beg.y), 0); + avg_color += area_sw * texelFetch(Source, ivec2(f_beg.x, f_end.y), 0); + avg_color += area_se * texelFetch(Source, ivec2(f_end.x, f_end.y), 0); + + // Determine the size of the pixel box. + int x_range = int(f_end.x - f_beg.x - 0.5); + int y_range = int(f_end.y - f_beg.y - 0.5); + + // Accumulate top and bottom edge pixels. + for (int x = f_beg.x + 1; x <= f_beg.x + x_range; ++x) + { + avg_color += area_n * texelFetch(Source, ivec2(x, f_beg.y), 0); + avg_color += area_s * texelFetch(Source, ivec2(x, f_end.y), 0); + } + + // Accumulate left and right edge pixels and all the pixels in between. + for (int y = f_beg.y + 1; y <= f_beg.y + y_range; ++y) + { + avg_color += area_w * texelFetch(Source, ivec2(f_beg.x, y), 0); + avg_color += area_e * texelFetch(Source, ivec2(f_end.x, y), 0); + + for (int x = f_beg.x + 1; x <= f_beg.x + x_range; ++x) + { + avg_color += texelFetch(Source, ivec2(x, y), 0); + } + } + + // Compute the area of the pixel box that was sampled. + float area_corners = area_nw + area_ne + area_sw + area_se; + float area_edges = float(x_range) * (area_n + area_s) + float(y_range) * (area_w + area_e); + float area_center = float(x_range) * float(y_range); + + // Return the normalized average color. + return avg_color / (area_corners + area_edges + area_center); +} + +float insideBox(vec2 v, vec2 bLeft, vec2 tRight) { + vec2 s = step(bLeft, v) - step(tRight, v); + return s.x * s.y; +} + +vec2 translateDest(vec2 pos) { + vec2 translatedPos = vec2(pos.x, pos.y); + translatedPos.x = dstX1 < dstX0 ? dstX1 - translatedPos.x : translatedPos.x; + translatedPos.y = dstY0 < dstY1 ? dstY1 + dstY0 - translatedPos.y - 1 : translatedPos.y; + return translatedPos; +} + +void main() +{ + vec2 bLeft = vec2(dstX0 < dstX1 ? dstX0 : dstX1, dstY0 < dstY1 ? dstY0 : dstY1); + vec2 tRight = vec2(dstX1 > dstX0 ? dstX1 : dstX0, dstY1 > dstY0 ? dstY1 : dstY0); + ivec2 loc = ivec2(gl_GlobalInvocationID.x, gl_GlobalInvocationID.y); + if (insideBox(loc, bLeft, tRight) == 0) { + imageStore(imgOutput, loc, vec4(0, 0, 0, 1)); + return; + } + + vec4 outColor = AreaSampling(loc); + imageStore(imgOutput, ivec2(translateDest(loc)), vec4(outColor.rgb, 1)); +} diff --git a/src/Ryujinx.Graphics.Vulkan/Effects/Shaders/AreaScaling.spv b/src/Ryujinx.Graphics.Vulkan/Effects/Shaders/AreaScaling.spv new file mode 100644 index 000000000..7d097280f Binary files /dev/null and b/src/Ryujinx.Graphics.Vulkan/Effects/Shaders/AreaScaling.spv differ diff --git a/src/Ryujinx.Graphics.Vulkan/Effects/SmaaPostProcessingEffect.cs b/src/Ryujinx.Graphics.Vulkan/Effects/SmaaPostProcessingEffect.cs index c521f2273..a8e68f429 100644 --- a/src/Ryujinx.Graphics.Vulkan/Effects/SmaaPostProcessingEffect.cs +++ b/src/Ryujinx.Graphics.Vulkan/Effects/SmaaPostProcessingEffect.cs @@ -81,20 +81,20 @@ namespace Ryujinx.Graphics.Vulkan.Effects var edgeResourceLayout = new ResourceLayoutBuilder() .Add(ResourceStages.Compute, ResourceType.UniformBuffer, 2) .Add(ResourceStages.Compute, ResourceType.TextureAndSampler, 1) - .Add(ResourceStages.Compute, ResourceType.Image, 0).Build(); + .Add(ResourceStages.Compute, ResourceType.Image, 0, true).Build(); var blendResourceLayout = new ResourceLayoutBuilder() .Add(ResourceStages.Compute, ResourceType.UniformBuffer, 2) .Add(ResourceStages.Compute, ResourceType.TextureAndSampler, 1) .Add(ResourceStages.Compute, ResourceType.TextureAndSampler, 3) .Add(ResourceStages.Compute, ResourceType.TextureAndSampler, 4) - .Add(ResourceStages.Compute, ResourceType.Image, 0).Build(); + .Add(ResourceStages.Compute, ResourceType.Image, 0, true).Build(); var neighbourResourceLayout = new ResourceLayoutBuilder() .Add(ResourceStages.Compute, ResourceType.UniformBuffer, 2) .Add(ResourceStages.Compute, ResourceType.TextureAndSampler, 1) .Add(ResourceStages.Compute, ResourceType.TextureAndSampler, 3) - .Add(ResourceStages.Compute, ResourceType.Image, 0).Build(); + .Add(ResourceStages.Compute, ResourceType.Image, 0, true).Build(); _samplerLinear = _renderer.CreateSampler(SamplerCreateInfo.Create(MinFilter.Linear, MagFilter.Linear)); @@ -174,8 +174,8 @@ namespace Ryujinx.Graphics.Vulkan.Effects SwizzleComponent.Blue, SwizzleComponent.Alpha); - var areaTexture = EmbeddedResources.Read("Ryujinx.Graphics.Vulkan/Effects/Textures/SmaaAreaTexture.bin"); - var searchTexture = EmbeddedResources.Read("Ryujinx.Graphics.Vulkan/Effects/Textures/SmaaSearchTexture.bin"); + var areaTexture = EmbeddedResources.ReadFileToRentedMemory("Ryujinx.Graphics.Vulkan/Effects/Textures/SmaaAreaTexture.bin"); + var searchTexture = EmbeddedResources.ReadFileToRentedMemory("Ryujinx.Graphics.Vulkan/Effects/Textures/SmaaSearchTexture.bin"); _areaTexture = _renderer.CreateTexture(areaInfo) as TextureView; _searchTexture = _renderer.CreateTexture(searchInfo) as TextureView; @@ -215,12 +215,11 @@ namespace Ryujinx.Graphics.Vulkan.Effects ReadOnlySpan resolutionBuffer = stackalloc float[] { view.Width, view.Height }; int rangeSize = resolutionBuffer.Length * sizeof(float); - var bufferHandle = _renderer.BufferManager.CreateWithHandle(_renderer, rangeSize); + using var buffer = _renderer.BufferManager.ReserveOrCreate(_renderer, cbs, rangeSize); - _renderer.BufferManager.SetData(bufferHandle, 0, resolutionBuffer); - var bufferRanges = new BufferRange(bufferHandle, 0, rangeSize); - _pipeline.SetUniformBuffers(stackalloc[] { new BufferAssignment(2, bufferRanges) }); - _pipeline.SetImage(0, _edgeOutputTexture, FormatTable.ConvertRgba8SrgbToUnorm(view.Info.Format)); + buffer.Holder.SetDataUnchecked(buffer.Offset, resolutionBuffer); + _pipeline.SetUniformBuffers(stackalloc[] { new BufferAssignment(2, buffer.Range) }); + _pipeline.SetImage(ShaderStage.Compute, 0, _edgeOutputTexture.GetView(FormatTable.ConvertRgba8SrgbToUnorm(view.Info.Format))); _pipeline.DispatchCompute(dispatchX, dispatchY, 1); _pipeline.ComputeBarrier(); @@ -230,7 +229,7 @@ namespace Ryujinx.Graphics.Vulkan.Effects _pipeline.SetTextureAndSampler(ShaderStage.Compute, 1, _edgeOutputTexture, _samplerLinear); _pipeline.SetTextureAndSampler(ShaderStage.Compute, 3, _areaTexture, _samplerLinear); _pipeline.SetTextureAndSampler(ShaderStage.Compute, 4, _searchTexture, _samplerLinear); - _pipeline.SetImage(0, _blendOutputTexture, FormatTable.ConvertRgba8SrgbToUnorm(view.Info.Format)); + _pipeline.SetImage(ShaderStage.Compute, 0, _blendOutputTexture.GetView(FormatTable.ConvertRgba8SrgbToUnorm(view.Info.Format))); _pipeline.DispatchCompute(dispatchX, dispatchY, 1); _pipeline.ComputeBarrier(); @@ -239,14 +238,12 @@ namespace Ryujinx.Graphics.Vulkan.Effects _pipeline.Specialize(_specConstants); _pipeline.SetTextureAndSampler(ShaderStage.Compute, 3, _blendOutputTexture, _samplerLinear); _pipeline.SetTextureAndSampler(ShaderStage.Compute, 1, view, _samplerLinear); - _pipeline.SetImage(0, _outputTexture, FormatTable.ConvertRgba8SrgbToUnorm(view.Info.Format)); + _pipeline.SetImage(ShaderStage.Compute, 0, _outputTexture.GetView(FormatTable.ConvertRgba8SrgbToUnorm(view.Info.Format))); _pipeline.DispatchCompute(dispatchX, dispatchY, 1); _pipeline.ComputeBarrier(); _pipeline.Finish(); - _renderer.BufferManager.Delete(bufferHandle); - return _outputTexture; } @@ -260,7 +257,7 @@ namespace Ryujinx.Graphics.Vulkan.Effects scissors[0] = new Rectangle(0, 0, texture.Width, texture.Height); - _pipeline.SetRenderTarget(texture.GetImageViewForAttachment(), (uint)texture.Width, (uint)texture.Height, false, texture.VkFormat); + _pipeline.SetRenderTarget(texture, (uint)texture.Width, (uint)texture.Height); _pipeline.SetRenderTargetColorMasks(colorMasks); _pipeline.SetScissors(scissors); _pipeline.ClearRenderTargetColor(0, 0, 1, new ColorF(0f, 0f, 0f, 1f)); diff --git a/src/Ryujinx.Graphics.Vulkan/EnumConversion.cs b/src/Ryujinx.Graphics.Vulkan/EnumConversion.cs index e10027057..9d1fd9ffd 100644 --- a/src/Ryujinx.Graphics.Vulkan/EnumConversion.cs +++ b/src/Ryujinx.Graphics.Vulkan/EnumConversion.cs @@ -376,7 +376,7 @@ namespace Ryujinx.Graphics.Vulkan { return format switch { - Format.D16Unorm or Format.D32Float => ImageAspectFlags.DepthBit, + Format.D16Unorm or Format.D32Float or Format.X8UintD24Unorm => ImageAspectFlags.DepthBit, Format.S8Uint => ImageAspectFlags.StencilBit, Format.D24UnormS8Uint or Format.D32FloatS8Uint or @@ -389,7 +389,7 @@ namespace Ryujinx.Graphics.Vulkan { return format switch { - Format.D16Unorm or Format.D32Float => ImageAspectFlags.DepthBit, + Format.D16Unorm or Format.D32Float or Format.X8UintD24Unorm => ImageAspectFlags.DepthBit, Format.S8Uint => ImageAspectFlags.StencilBit, Format.D24UnormS8Uint or Format.D32FloatS8Uint or @@ -424,10 +424,20 @@ namespace Ryujinx.Graphics.Vulkan public static BufferAllocationType Convert(this BufferAccess access) { - if (access.HasFlag(BufferAccess.FlushPersistent) || access.HasFlag(BufferAccess.Stream)) + BufferAccess memType = access & BufferAccess.MemoryTypeMask; + + if (memType == BufferAccess.HostMemory || access.HasFlag(BufferAccess.Stream)) { return BufferAllocationType.HostMapped; } + else if (memType == BufferAccess.DeviceMemory) + { + return BufferAllocationType.DeviceLocal; + } + else if (memType == BufferAccess.DeviceMemoryMapped) + { + return BufferAllocationType.DeviceLocalMapped; + } return BufferAllocationType.Auto; } diff --git a/src/Ryujinx.Graphics.Vulkan/FeedbackLoopAspects.cs b/src/Ryujinx.Graphics.Vulkan/FeedbackLoopAspects.cs new file mode 100644 index 000000000..22f73679d --- /dev/null +++ b/src/Ryujinx.Graphics.Vulkan/FeedbackLoopAspects.cs @@ -0,0 +1,12 @@ +using System; + +namespace Ryujinx.Graphics.Vulkan +{ + [Flags] + internal enum FeedbackLoopAspects + { + None = 0, + Color = 1 << 0, + Depth = 1 << 1, + } +} diff --git a/src/Ryujinx.Graphics.Vulkan/FenceHolder.cs b/src/Ryujinx.Graphics.Vulkan/FenceHolder.cs index 4f0a87160..0cdb93f20 100644 --- a/src/Ryujinx.Graphics.Vulkan/FenceHolder.cs +++ b/src/Ryujinx.Graphics.Vulkan/FenceHolder.cs @@ -10,12 +10,15 @@ namespace Ryujinx.Graphics.Vulkan private readonly Device _device; private Fence _fence; private int _referenceCount; + private int _lock; + private readonly bool _concurrentWaitUnsupported; private bool _disposed; - public unsafe FenceHolder(Vk api, Device device) + public unsafe FenceHolder(Vk api, Device device, bool concurrentWaitUnsupported) { _api = api; _device = device; + _concurrentWaitUnsupported = concurrentWaitUnsupported; var fenceCreateInfo = new FenceCreateInfo { @@ -47,6 +50,11 @@ namespace Ryujinx.Graphics.Vulkan } while (Interlocked.CompareExchange(ref _referenceCount, lastValue + 1, lastValue) != lastValue); + if (_concurrentWaitUnsupported) + { + AcquireLock(); + } + fence = _fence; return true; } @@ -57,6 +65,16 @@ namespace Ryujinx.Graphics.Vulkan return _fence; } + public void PutLock() + { + Put(); + + if (_concurrentWaitUnsupported) + { + ReleaseLock(); + } + } + public void Put() { if (Interlocked.Decrement(ref _referenceCount) == 0) @@ -66,24 +84,67 @@ namespace Ryujinx.Graphics.Vulkan } } + private void AcquireLock() + { + while (!TryAcquireLock()) + { + Thread.SpinWait(32); + } + } + + private bool TryAcquireLock() + { + return Interlocked.Exchange(ref _lock, 1) == 0; + } + + private void ReleaseLock() + { + Interlocked.Exchange(ref _lock, 0); + } + public void Wait() { - Span fences = stackalloc Fence[] + if (_concurrentWaitUnsupported) { - _fence, - }; + AcquireLock(); - FenceHelper.WaitAllIndefinitely(_api, _device, fences); + try + { + FenceHelper.WaitAllIndefinitely(_api, _device, stackalloc Fence[] { _fence }); + } + finally + { + ReleaseLock(); + } + } + else + { + FenceHelper.WaitAllIndefinitely(_api, _device, stackalloc Fence[] { _fence }); + } } public bool IsSignaled() { - Span fences = stackalloc Fence[] + if (_concurrentWaitUnsupported) { - _fence, - }; + if (!TryAcquireLock()) + { + return false; + } - return FenceHelper.AllSignaled(_api, _device, fences); + try + { + return FenceHelper.AllSignaled(_api, _device, stackalloc Fence[] { _fence }); + } + finally + { + ReleaseLock(); + } + } + else + { + return FenceHelper.AllSignaled(_api, _device, stackalloc Fence[] { _fence }); + } } public void Dispose() diff --git a/src/Ryujinx.Graphics.Vulkan/FormatCapabilities.cs b/src/Ryujinx.Graphics.Vulkan/FormatCapabilities.cs index 7307a0ee0..9ea8cec4b 100644 --- a/src/Ryujinx.Graphics.Vulkan/FormatCapabilities.cs +++ b/src/Ryujinx.Graphics.Vulkan/FormatCapabilities.cs @@ -220,7 +220,7 @@ namespace Ryujinx.Graphics.Vulkan public static bool IsD24S8(Format format) { - return format == Format.D24UnormS8Uint || format == Format.S8UintD24Unorm; + return format == Format.D24UnormS8Uint || format == Format.S8UintD24Unorm || format == Format.X8UintD24Unorm; } private static bool IsRGB16IntFloat(Format format) diff --git a/src/Ryujinx.Graphics.Vulkan/FormatTable.cs b/src/Ryujinx.Graphics.Vulkan/FormatTable.cs index 5f767df16..98796d9bf 100644 --- a/src/Ryujinx.Graphics.Vulkan/FormatTable.cs +++ b/src/Ryujinx.Graphics.Vulkan/FormatTable.cs @@ -1,5 +1,6 @@ using Ryujinx.Graphics.GAL; using System; +using System.Collections.Generic; using VkFormat = Silk.NET.Vulkan.Format; namespace Ryujinx.Graphics.Vulkan @@ -7,10 +8,12 @@ namespace Ryujinx.Graphics.Vulkan static class FormatTable { private static readonly VkFormat[] _table; + private static readonly Dictionary _reverseMap; static FormatTable() { _table = new VkFormat[Enum.GetNames(typeof(Format)).Length]; + _reverseMap = new Dictionary(); #pragma warning disable IDE0055 // Disable formatting Add(Format.R8Unorm, VkFormat.R8Unorm); @@ -64,6 +67,7 @@ namespace Ryujinx.Graphics.Vulkan Add(Format.S8Uint, VkFormat.S8Uint); Add(Format.D16Unorm, VkFormat.D16Unorm); Add(Format.S8UintD24Unorm, VkFormat.D24UnormS8Uint); + Add(Format.X8UintD24Unorm, VkFormat.X8D24UnormPack32); Add(Format.D32Float, VkFormat.D32Sfloat); Add(Format.D24UnormS8Uint, VkFormat.D24UnormS8Uint); Add(Format.D32FloatS8Uint, VkFormat.D32SfloatS8Uint); @@ -158,12 +162,14 @@ namespace Ryujinx.Graphics.Vulkan Add(Format.A1B5G5R5Unorm, VkFormat.R5G5B5A1UnormPack16); Add(Format.B8G8R8A8Unorm, VkFormat.B8G8R8A8Unorm); Add(Format.B8G8R8A8Srgb, VkFormat.B8G8R8A8Srgb); + Add(Format.B10G10R10A2Unorm, VkFormat.A2R10G10B10UnormPack32); #pragma warning restore IDE0055 } private static void Add(Format format, VkFormat vkFormat) { _table[(int)format] = vkFormat; + _reverseMap[vkFormat] = format; } public static VkFormat GetFormat(Format format) @@ -171,6 +177,16 @@ namespace Ryujinx.Graphics.Vulkan return _table[(int)format]; } + public static Format GetFormat(VkFormat format) + { + if (!_reverseMap.TryGetValue(format, out Format result)) + { + return Format.B8G8R8A8Unorm; + } + + return result; + } + public static Format ConvertRgba8SrgbToUnorm(Format format) { return format switch diff --git a/src/Ryujinx.Graphics.Vulkan/FramebufferParams.cs b/src/Ryujinx.Graphics.Vulkan/FramebufferParams.cs index 458a16464..8d80e9d05 100644 --- a/src/Ryujinx.Graphics.Vulkan/FramebufferParams.cs +++ b/src/Ryujinx.Graphics.Vulkan/FramebufferParams.cs @@ -12,6 +12,8 @@ namespace Ryujinx.Graphics.Vulkan private readonly Auto[] _attachments; private readonly TextureView[] _colors; private readonly TextureView _depthStencil; + private readonly TextureView[] _colorsCanonical; + private readonly TextureView _baseAttachment; private readonly uint _validColorAttachments; public uint Width { get; } @@ -22,32 +24,43 @@ namespace Ryujinx.Graphics.Vulkan public VkFormat[] AttachmentFormats { get; } public int[] AttachmentIndices { get; } public uint AttachmentIntegerFormatMask { get; } + public bool LogicOpsAllowed { get; } public int AttachmentsCount { get; } public int MaxColorAttachmentIndex => AttachmentIndices.Length > 0 ? AttachmentIndices[^1] : -1; public bool HasDepthStencil { get; } public int ColorAttachmentsCount => AttachmentsCount - (HasDepthStencil ? 1 : 0); - public FramebufferParams( - Device device, - Auto view, - uint width, - uint height, - uint samples, - bool isDepthStencil, - VkFormat format) + public FramebufferParams(Device device, TextureView view, uint width, uint height) { + var format = view.Info.Format; + + bool isDepthStencil = format.IsDepthOrStencil(); + _device = device; - _attachments = new[] { view }; + _attachments = new[] { view.GetImageViewForAttachment() }; _validColorAttachments = isDepthStencil ? 0u : 1u; + _baseAttachment = view; + + if (isDepthStencil) + { + _depthStencil = view; + } + else + { + _colors = new TextureView[] { view }; + _colorsCanonical = _colors; + } Width = width; Height = height; Layers = 1; - AttachmentSamples = new[] { samples }; - AttachmentFormats = new[] { format }; + AttachmentSamples = new[] { (uint)view.Info.Samples }; + AttachmentFormats = new[] { view.VkFormat }; AttachmentIndices = isDepthStencil ? Array.Empty() : new[] { 0 }; + AttachmentIntegerFormatMask = format.IsInteger() ? 1u : 0u; + LogicOpsAllowed = !format.IsFloatOrSrgb(); AttachmentsCount = 1; @@ -64,6 +77,7 @@ namespace Ryujinx.Graphics.Vulkan _attachments = new Auto[count]; _colors = new TextureView[colorsCount]; + _colorsCanonical = colors.Select(color => color is TextureView view && view.Valid ? view : null).ToArray(); AttachmentSamples = new uint[count]; AttachmentFormats = new VkFormat[count]; @@ -76,6 +90,7 @@ namespace Ryujinx.Graphics.Vulkan int index = 0; int bindIndex = 0; uint attachmentIntegerFormatMask = 0; + bool allFormatsFloatOrSrgb = colorsCount != 0; foreach (ITexture color in colors) { @@ -86,16 +101,21 @@ namespace Ryujinx.Graphics.Vulkan _attachments[index] = texture.GetImageViewForAttachment(); _colors[index] = texture; _validColorAttachments |= 1u << bindIndex; + _baseAttachment = texture; AttachmentSamples[index] = (uint)texture.Info.Samples; AttachmentFormats[index] = texture.VkFormat; AttachmentIndices[index] = bindIndex; - if (texture.Info.Format.IsInteger()) + var format = texture.Info.Format; + + if (format.IsInteger()) { attachmentIntegerFormatMask |= 1u << bindIndex; } + allFormatsFloatOrSrgb &= format.IsFloatOrSrgb(); + width = Math.Min(width, (uint)texture.Width); height = Math.Min(height, (uint)texture.Height); layers = Math.Min(layers, (uint)texture.Layers); @@ -110,11 +130,13 @@ namespace Ryujinx.Graphics.Vulkan } AttachmentIntegerFormatMask = attachmentIntegerFormatMask; + LogicOpsAllowed = !allFormatsFloatOrSrgb; if (depthStencil is TextureView dsTexture && dsTexture.Valid) { _attachments[count - 1] = dsTexture.GetImageViewForAttachment(); _depthStencil = dsTexture; + _baseAttachment ??= dsTexture; AttachmentSamples[count - 1] = (uint)dsTexture.Info.Samples; AttachmentFormats[count - 1] = dsTexture.VkFormat; @@ -228,51 +250,95 @@ namespace Ryujinx.Graphics.Vulkan Layers = Layers, }; - api.CreateFramebuffer(_device, framebufferCreateInfo, null, out var framebuffer).ThrowOnError(); + api.CreateFramebuffer(_device, in framebufferCreateInfo, null, out var framebuffer).ThrowOnError(); return new Auto(new DisposableFramebuffer(api, _device, framebuffer), null, _attachments); } - public void UpdateModifications() + public TextureView[] GetAttachmentViews() + { + var result = new TextureView[_attachments.Length]; + + _colors?.CopyTo(result, 0); + + if (_depthStencil != null) + { + result[^1] = _depthStencil; + } + + return result; + } + + public RenderPassCacheKey GetRenderPassCacheKey() + { + return new RenderPassCacheKey(_depthStencil, _colorsCanonical); + } + + public void InsertLoadOpBarriers(VulkanRenderer gd, CommandBufferScoped cbs) { if (_colors != null) { - for (int index = 0; index < _colors.Length; index++) + foreach (var color in _colors) { - _colors[index].Storage.SetModification( - AccessFlags.ColorAttachmentWriteBit, - PipelineStageFlags.ColorAttachmentOutputBit); + // If Clear or DontCare were used, this would need to be write bit. + color.Storage?.QueueLoadOpBarrier(cbs, false); } } - _depthStencil?.Storage.SetModification( - AccessFlags.DepthStencilAttachmentWriteBit, - PipelineStageFlags.LateFragmentTestsBit); + _depthStencil?.Storage?.QueueLoadOpBarrier(cbs, true); + + gd.Barriers.Flush(cbs, false, null, null); } - public void InsertClearBarrier(CommandBufferScoped cbs, int index) + public void AddStoreOpUsage() { if (_colors != null) { - int realIndex = Array.IndexOf(AttachmentIndices, index); - - if (realIndex != -1) + foreach (var color in _colors) { - _colors[realIndex].Storage?.InsertReadToWriteBarrier( - cbs, - AccessFlags.ColorAttachmentWriteBit, - PipelineStageFlags.ColorAttachmentOutputBit, - insideRenderPass: true); + color.Storage?.AddStoreOpUsage(false); } } + + _depthStencil?.Storage?.AddStoreOpUsage(true); + } + + public void ClearBindings() + { + _depthStencil?.Storage.ClearBindings(); + + for (int i = 0; i < _colorsCanonical.Length; i++) + { + _colorsCanonical[i]?.Storage.ClearBindings(); + } } - public void InsertClearBarrierDS(CommandBufferScoped cbs) + public void AddBindings() { - _depthStencil?.Storage?.InsertReadToWriteBarrier( - cbs, - AccessFlags.DepthStencilAttachmentWriteBit, - PipelineStageFlags.LateFragmentTestsBit, - insideRenderPass: true); + _depthStencil?.Storage.AddBinding(_depthStencil); + + for (int i = 0; i < _colorsCanonical.Length; i++) + { + TextureView color = _colorsCanonical[i]; + color?.Storage.AddBinding(color); + } + } + + public (RenderPassHolder rpHolder, Auto framebuffer) GetPassAndFramebuffer( + VulkanRenderer gd, + Device device, + CommandBufferScoped cbs) + { + return _baseAttachment.GetPassAndFramebuffer(gd, device, cbs, this); + } + + public TextureView GetColorView(int index) + { + return _colorsCanonical[index]; + } + + public TextureView GetDepthStencilView() + { + return _depthStencil; } } } diff --git a/src/Ryujinx.Graphics.Vulkan/HardwareCapabilities.cs b/src/Ryujinx.Graphics.Vulkan/HardwareCapabilities.cs index c421a662a..bd17867b1 100644 --- a/src/Ryujinx.Graphics.Vulkan/HardwareCapabilities.cs +++ b/src/Ryujinx.Graphics.Vulkan/HardwareCapabilities.cs @@ -34,6 +34,7 @@ namespace Ryujinx.Graphics.Vulkan public readonly bool SupportsMultiView; public readonly bool SupportsNullDescriptors; public readonly bool SupportsPushDescriptors; + public readonly uint MaxPushDescriptors; public readonly bool SupportsPrimitiveTopologyListRestart; public readonly bool SupportsPrimitiveTopologyPatchListRestart; public readonly bool SupportsTransformFeedback; @@ -45,6 +46,8 @@ namespace Ryujinx.Graphics.Vulkan public readonly bool SupportsViewportArray2; public readonly bool SupportsHostImportedMemory; public readonly bool SupportsDepthClipControl; + public readonly bool SupportsAttachmentFeedbackLoop; + public readonly bool SupportsDynamicAttachmentFeedbackLoop; public readonly uint SubgroupSize; public readonly SampleCountFlags SupportedSampleCounts; public readonly PortabilitySubsetFlags PortabilitySubset; @@ -71,6 +74,7 @@ namespace Ryujinx.Graphics.Vulkan bool supportsMultiView, bool supportsNullDescriptors, bool supportsPushDescriptors, + uint maxPushDescriptors, bool supportsPrimitiveTopologyListRestart, bool supportsPrimitiveTopologyPatchListRestart, bool supportsTransformFeedback, @@ -82,6 +86,8 @@ namespace Ryujinx.Graphics.Vulkan bool supportsViewportArray2, bool supportsHostImportedMemory, bool supportsDepthClipControl, + bool supportsAttachmentFeedbackLoop, + bool supportsDynamicAttachmentFeedbackLoop, uint subgroupSize, SampleCountFlags supportedSampleCounts, PortabilitySubsetFlags portabilitySubset, @@ -103,14 +109,11 @@ namespace Ryujinx.Graphics.Vulkan SupportsShaderStencilExport = supportsShaderStencilExport; SupportsShaderStorageImageMultisample = supportsShaderStorageImageMultisample; SupportsConditionalRendering = supportsConditionalRendering; - if (OperatingSystem.IsIOS()) { - SupportsExtendedDynamicState = (OperatingSystem.IsOSPlatformVersionAtLeast("iOS", 17) ? supportsExtendedDynamicState : false); - } else { - SupportsExtendedDynamicState = supportsExtendedDynamicState; - } + SupportsExtendedDynamicState = supportsExtendedDynamicState; SupportsMultiView = supportsMultiView; - SupportsNullDescriptors = (OperatingSystem.IsIOS() ? false : supportsNullDescriptors); + SupportsNullDescriptors = supportsNullDescriptors; SupportsPushDescriptors = supportsPushDescriptors; + MaxPushDescriptors = maxPushDescriptors; SupportsPrimitiveTopologyListRestart = supportsPrimitiveTopologyListRestart; SupportsPrimitiveTopologyPatchListRestart = supportsPrimitiveTopologyPatchListRestart; SupportsTransformFeedback = supportsTransformFeedback; @@ -122,6 +125,8 @@ namespace Ryujinx.Graphics.Vulkan SupportsViewportArray2 = supportsViewportArray2; SupportsHostImportedMemory = supportsHostImportedMemory; SupportsDepthClipControl = supportsDepthClipControl; + SupportsAttachmentFeedbackLoop = supportsAttachmentFeedbackLoop; + SupportsDynamicAttachmentFeedbackLoop = supportsDynamicAttachmentFeedbackLoop; SubgroupSize = subgroupSize; SupportedSampleCounts = supportedSampleCounts; PortabilitySubset = portabilitySubset; diff --git a/src/Ryujinx.Graphics.Vulkan/HashTableSlim.cs b/src/Ryujinx.Graphics.Vulkan/HashTableSlim.cs index ff4eb7890..3796e3c52 100644 --- a/src/Ryujinx.Graphics.Vulkan/HashTableSlim.cs +++ b/src/Ryujinx.Graphics.Vulkan/HashTableSlim.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Runtime.CompilerServices; namespace Ryujinx.Graphics.Vulkan { @@ -20,20 +21,29 @@ namespace Ryujinx.Graphics.Vulkan public TValue Value; } - private readonly Entry[][] _hashTable = new Entry[TotalBuckets][]; + private struct Bucket + { + public int Length; + public Entry[] Entries; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Span AsSpan() + { + return Entries == null ? Span.Empty : Entries.AsSpan(0, Length); + } + } + + private readonly Bucket[] _hashTable = new Bucket[TotalBuckets]; public IEnumerable Keys { get { - foreach (Entry[] bucket in _hashTable) + foreach (Bucket bucket in _hashTable) { - if (bucket != null) + for (int i = 0; i < bucket.Length; i++) { - foreach (Entry entry in bucket) - { - yield return entry.Key; - } + yield return bucket.Entries[i].Key; } } } @@ -43,14 +53,11 @@ namespace Ryujinx.Graphics.Vulkan { get { - foreach (Entry[] bucket in _hashTable) + foreach (Bucket bucket in _hashTable) { - if (bucket != null) + for (int i = 0; i < bucket.Length; i++) { - foreach (Entry entry in bucket) - { - yield return entry.Value; - } + yield return bucket.Entries[i].Value; } } } @@ -68,40 +75,64 @@ namespace Ryujinx.Graphics.Vulkan int hashCode = key.GetHashCode(); int bucketIndex = hashCode & TotalBucketsMask; - var bucket = _hashTable[bucketIndex]; - if (bucket != null) + ref var bucket = ref _hashTable[bucketIndex]; + if (bucket.Entries != null) { int index = bucket.Length; - Array.Resize(ref _hashTable[bucketIndex], index + 1); + if (index >= bucket.Entries.Length) + { + Array.Resize(ref bucket.Entries, index + 1); + } - _hashTable[bucketIndex][index] = entry; + bucket.Entries[index] = entry; } else { - _hashTable[bucketIndex] = new[] + bucket.Entries = new[] { entry, }; } + + bucket.Length++; + } + + public bool Remove(ref TKey key) + { + int hashCode = key.GetHashCode(); + + ref var bucket = ref _hashTable[hashCode & TotalBucketsMask]; + var entries = bucket.AsSpan(); + for (int i = 0; i < entries.Length; i++) + { + ref var entry = ref entries[i]; + + if (entry.Hash == hashCode && entry.Key.Equals(ref key)) + { + entries[(i + 1)..].CopyTo(entries[i..]); + bucket.Length--; + + return true; + } + } + + return false; } public bool TryGetValue(ref TKey key, out TValue value) { int hashCode = key.GetHashCode(); - var bucket = _hashTable[hashCode & TotalBucketsMask]; - if (bucket != null) + var entries = _hashTable[hashCode & TotalBucketsMask].AsSpan(); + for (int i = 0; i < entries.Length; i++) { - for (int i = 0; i < bucket.Length; i++) - { - ref var entry = ref bucket[i]; + ref var entry = ref entries[i]; - if (entry.Hash == hashCode && entry.Key.Equals(ref key)) - { - value = entry.Value; - return true; - } + if (entry.Hash == hashCode && entry.Key.Equals(ref key)) + { + value = entry.Value; + return true; } } diff --git a/src/Ryujinx.Graphics.Vulkan/HelperShader.cs b/src/Ryujinx.Graphics.Vulkan/HelperShader.cs index deaf81625..b7c42aff0 100644 --- a/src/Ryujinx.Graphics.Vulkan/HelperShader.cs +++ b/src/Ryujinx.Graphics.Vulkan/HelperShader.cs @@ -115,7 +115,7 @@ namespace Ryujinx.Graphics.Vulkan var strideChangeResourceLayout = new ResourceLayoutBuilder() .Add(ResourceStages.Compute, ResourceType.UniformBuffer, 0) .Add(ResourceStages.Compute, ResourceType.StorageBuffer, 1) - .Add(ResourceStages.Compute, ResourceType.StorageBuffer, 2).Build(); + .Add(ResourceStages.Compute, ResourceType.StorageBuffer, 2, true).Build(); _programStrideChange = gd.CreateProgramWithMinimalLayout(new[] { @@ -125,7 +125,7 @@ namespace Ryujinx.Graphics.Vulkan var colorCopyResourceLayout = new ResourceLayoutBuilder() .Add(ResourceStages.Compute, ResourceType.UniformBuffer, 0) .Add(ResourceStages.Compute, ResourceType.TextureAndSampler, 0) - .Add(ResourceStages.Compute, ResourceType.Image, 0).Build(); + .Add(ResourceStages.Compute, ResourceType.Image, 0, true).Build(); _programColorCopyShortening = gd.CreateProgramWithMinimalLayout(new[] { @@ -155,7 +155,7 @@ namespace Ryujinx.Graphics.Vulkan var convertD32S8ToD24S8ResourceLayout = new ResourceLayoutBuilder() .Add(ResourceStages.Compute, ResourceType.UniformBuffer, 0) .Add(ResourceStages.Compute, ResourceType.StorageBuffer, 1) - .Add(ResourceStages.Compute, ResourceType.StorageBuffer, 2).Build(); + .Add(ResourceStages.Compute, ResourceType.StorageBuffer, 2, true).Build(); _programConvertD32S8ToD24S8 = gd.CreateProgramWithMinimalLayout(new[] { @@ -165,7 +165,7 @@ namespace Ryujinx.Graphics.Vulkan var convertIndexBufferResourceLayout = new ResourceLayoutBuilder() .Add(ResourceStages.Compute, ResourceType.UniformBuffer, 0) .Add(ResourceStages.Compute, ResourceType.StorageBuffer, 1) - .Add(ResourceStages.Compute, ResourceType.StorageBuffer, 2).Build(); + .Add(ResourceStages.Compute, ResourceType.StorageBuffer, 2, true).Build(); _programConvertIndexBuffer = gd.CreateProgramWithMinimalLayout(new[] { @@ -175,7 +175,7 @@ namespace Ryujinx.Graphics.Vulkan var convertIndirectDataResourceLayout = new ResourceLayoutBuilder() .Add(ResourceStages.Compute, ResourceType.UniformBuffer, 0) .Add(ResourceStages.Compute, ResourceType.StorageBuffer, 1) - .Add(ResourceStages.Compute, ResourceType.StorageBuffer, 2) + .Add(ResourceStages.Compute, ResourceType.StorageBuffer, 2, true) .Add(ResourceStages.Compute, ResourceType.StorageBuffer, 3).Build(); _programConvertIndirectData = gd.CreateProgramWithMinimalLayout(new[] @@ -256,17 +256,8 @@ namespace Ryujinx.Graphics.Vulkan using var cbs = gd.CommandBufferPool.Rent(); - var dstFormat = dst.VkFormat; - var dstSamples = dst.Info.Samples; - for (int l = 0; l < levels; l++) { - int srcWidth = Math.Max(1, src.Width >> l); - int srcHeight = Math.Max(1, src.Height >> l); - - int dstWidth = Math.Max(1, dst.Width >> l); - int dstHeight = Math.Max(1, dst.Height >> l); - var mipSrcRegion = new Extents2D( srcRegion.X1 >> l, srcRegion.Y1 >> l, @@ -290,11 +281,7 @@ namespace Ryujinx.Graphics.Vulkan gd, cbs, srcView, - dst.GetImageViewForAttachment(), - dstWidth, - dstHeight, - dstSamples, - dstFormat, + dstView, mipSrcRegion, mipDstRegion); } @@ -304,12 +291,7 @@ namespace Ryujinx.Graphics.Vulkan gd, cbs, srcView, - dst.GetImageViewForAttachment(), - dstWidth, - dstHeight, - dstSamples, - dstFormat, - false, + dstView, mipSrcRegion, mipDstRegion, linearFilter, @@ -367,12 +349,7 @@ namespace Ryujinx.Graphics.Vulkan gd, cbs, srcView, - dstView.GetImageViewForAttachment(), - dstView.Width, - dstView.Height, - dstView.Info.Samples, - dstView.VkFormat, - dstView.Info.Format.IsDepthOrStencil(), + dstView, extents, extents, false); @@ -394,12 +371,7 @@ namespace Ryujinx.Graphics.Vulkan VulkanRenderer gd, CommandBufferScoped cbs, TextureView src, - Auto dst, - int dstWidth, - int dstHeight, - int dstSamples, - VkFormat dstFormat, - bool dstIsDepthOrStencil, + TextureView dst, Extents2D srcRegion, Extents2D dstRegion, bool linearFilter, @@ -430,11 +402,11 @@ namespace Ryujinx.Graphics.Vulkan (region[2], region[3]) = (region[3], region[2]); } - var bufferHandle = gd.BufferManager.CreateWithHandle(gd, RegionBufferSize); + using var buffer = gd.BufferManager.ReserveOrCreate(gd, cbs, RegionBufferSize); - gd.BufferManager.SetData(bufferHandle, 0, region); + buffer.Holder.SetDataUnchecked(buffer.Offset, region); - _pipeline.SetUniformBuffers(stackalloc[] { new BufferAssignment(1, new BufferRange(bufferHandle, 0, RegionBufferSize)) }); + _pipeline.SetUniformBuffers(stackalloc[] { new BufferAssignment(1, buffer.Range) }); Span viewports = stackalloc Viewport[1]; @@ -453,6 +425,8 @@ namespace Ryujinx.Graphics.Vulkan 0f, 1f); + bool dstIsDepthOrStencil = dst.Info.Format.IsDepthOrStencil(); + if (dstIsDepthOrStencil) { _pipeline.SetProgram(src.Info.Target.IsMultisample() ? _programDepthBlitMs : _programDepthBlit); @@ -471,7 +445,10 @@ namespace Ryujinx.Graphics.Vulkan _pipeline.SetProgram(_programColorBlit); } - _pipeline.SetRenderTarget(dst, (uint)dstWidth, (uint)dstHeight, (uint)dstSamples, dstIsDepthOrStencil, dstFormat); + int dstWidth = dst.Width; + int dstHeight = dst.Height; + + _pipeline.SetRenderTarget(dst, (uint)dstWidth, (uint)dstHeight); _pipeline.SetRenderTargetColorMasks(new uint[] { 0xf }); _pipeline.SetScissors(stackalloc Rectangle[] { new Rectangle(0, 0, dstWidth, dstHeight) }); @@ -490,19 +467,13 @@ namespace Ryujinx.Graphics.Vulkan } _pipeline.Finish(gd, cbs); - - gd.BufferManager.Delete(bufferHandle); } private void BlitDepthStencil( VulkanRenderer gd, CommandBufferScoped cbs, TextureView src, - Auto dst, - int dstWidth, - int dstHeight, - int dstSamples, - VkFormat dstFormat, + TextureView dst, Extents2D srcRegion, Extents2D dstRegion) { @@ -527,11 +498,11 @@ namespace Ryujinx.Graphics.Vulkan (region[2], region[3]) = (region[3], region[2]); } - var bufferHandle = gd.BufferManager.CreateWithHandle(gd, RegionBufferSize); + using var buffer = gd.BufferManager.ReserveOrCreate(gd, cbs, RegionBufferSize); - gd.BufferManager.SetData(bufferHandle, 0, region); + buffer.Holder.SetDataUnchecked(buffer.Offset, region); - _pipeline.SetUniformBuffers(stackalloc[] { new BufferAssignment(1, new BufferRange(bufferHandle, 0, RegionBufferSize)) }); + _pipeline.SetUniformBuffers(stackalloc[] { new BufferAssignment(1, buffer.Range) }); Span viewports = stackalloc Viewport[1]; @@ -550,7 +521,10 @@ namespace Ryujinx.Graphics.Vulkan 0f, 1f); - _pipeline.SetRenderTarget(dst, (uint)dstWidth, (uint)dstHeight, (uint)dstSamples, true, dstFormat); + int dstWidth = dst.Width; + int dstHeight = dst.Height; + + _pipeline.SetRenderTarget(dst, (uint)dstWidth, (uint)dstHeight); _pipeline.SetScissors(stackalloc Rectangle[] { new Rectangle(0, 0, dstWidth, dstHeight) }); _pipeline.SetViewports(viewports); _pipeline.SetPrimitiveTopology(PrimitiveTopology.TriangleStrip); @@ -582,8 +556,6 @@ namespace Ryujinx.Graphics.Vulkan } _pipeline.Finish(gd, cbs); - - gd.BufferManager.Delete(bufferHandle); } private static TextureView CreateDepthOrStencilView(TextureView depthStencilTexture, DepthStencilMode depthStencilMode) @@ -664,12 +636,11 @@ namespace Ryujinx.Graphics.Vulkan public void Clear( VulkanRenderer gd, - Auto dst, + TextureView dst, ReadOnlySpan clearColor, uint componentMask, int dstWidth, int dstHeight, - VkFormat dstFormat, ComponentType type, Rectangle scissor) { @@ -681,11 +652,11 @@ namespace Ryujinx.Graphics.Vulkan _pipeline.SetCommandBuffer(cbs); - var bufferHandle = gd.BufferManager.CreateWithHandle(gd, ClearColorBufferSize); + using var buffer = gd.BufferManager.ReserveOrCreate(gd, cbs, ClearColorBufferSize); - gd.BufferManager.SetData(bufferHandle, 0, clearColor); + buffer.Holder.SetDataUnchecked(buffer.Offset, clearColor); - _pipeline.SetUniformBuffers(stackalloc[] { new BufferAssignment(1, new BufferRange(bufferHandle, 0, ClearColorBufferSize)) }); + _pipeline.SetUniformBuffers(stackalloc[] { new BufferAssignment(1, buffer.Range) }); Span viewports = stackalloc Viewport[1]; @@ -714,20 +685,18 @@ namespace Ryujinx.Graphics.Vulkan } _pipeline.SetProgram(program); - _pipeline.SetRenderTarget(dst, (uint)dstWidth, (uint)dstHeight, false, dstFormat); + _pipeline.SetRenderTarget(dst, (uint)dstWidth, (uint)dstHeight); _pipeline.SetRenderTargetColorMasks(new[] { componentMask }); _pipeline.SetViewports(viewports); _pipeline.SetScissors(stackalloc Rectangle[] { scissor }); _pipeline.SetPrimitiveTopology(PrimitiveTopology.TriangleStrip); _pipeline.Draw(4, 1, 0, 0); _pipeline.Finish(); - - gd.BufferManager.Delete(bufferHandle); } public void Clear( VulkanRenderer gd, - Auto dst, + TextureView dst, float depthValue, bool depthMask, int stencilValue, @@ -745,11 +714,11 @@ namespace Ryujinx.Graphics.Vulkan _pipeline.SetCommandBuffer(cbs); - var bufferHandle = gd.BufferManager.CreateWithHandle(gd, ClearColorBufferSize); + using var buffer = gd.BufferManager.ReserveOrCreate(gd, cbs, ClearColorBufferSize); - gd.BufferManager.SetData(bufferHandle, 0, stackalloc float[] { depthValue }); + buffer.Holder.SetDataUnchecked(buffer.Offset, stackalloc float[] { depthValue }); - _pipeline.SetUniformBuffers(stackalloc[] { new BufferAssignment(1, new BufferRange(bufferHandle, 0, ClearColorBufferSize)) }); + _pipeline.SetUniformBuffers(stackalloc[] { new BufferAssignment(1, buffer.Range) }); Span viewports = stackalloc Viewport[1]; @@ -763,7 +732,7 @@ namespace Ryujinx.Graphics.Vulkan 1f); _pipeline.SetProgram(_programDepthStencilClear); - _pipeline.SetRenderTarget(dst, (uint)dstWidth, (uint)dstHeight, true, dstFormat); + _pipeline.SetRenderTarget(dst, (uint)dstWidth, (uint)dstHeight); _pipeline.SetViewports(viewports); _pipeline.SetScissors(stackalloc Rectangle[] { scissor }); _pipeline.SetPrimitiveTopology(PrimitiveTopology.TriangleStrip); @@ -771,8 +740,6 @@ namespace Ryujinx.Graphics.Vulkan _pipeline.SetStencilTest(CreateStencilTestDescriptor(stencilMask != 0, stencilValue, 0xff, stencilMask)); _pipeline.Draw(4, 1, 0, 0); _pipeline.Finish(); - - gd.BufferManager.Delete(bufferHandle); } public void DrawTexture( @@ -878,13 +845,13 @@ namespace Ryujinx.Graphics.Vulkan shaderParams[2] = size; shaderParams[3] = srcOffset; - var bufferHandle = gd.BufferManager.CreateWithHandle(gd, ParamsBufferSize); + using var buffer = gd.BufferManager.ReserveOrCreate(gd, cbs, ParamsBufferSize); - gd.BufferManager.SetData(bufferHandle, 0, shaderParams); + buffer.Holder.SetDataUnchecked(buffer.Offset, shaderParams); _pipeline.SetCommandBuffer(cbs); - _pipeline.SetUniformBuffers(stackalloc[] { new BufferAssignment(0, new BufferRange(bufferHandle, 0, ParamsBufferSize)) }); + _pipeline.SetUniformBuffers(stackalloc[] { new BufferAssignment(0, buffer.Range) }); Span> sbRanges = new Auto[2]; @@ -896,8 +863,6 @@ namespace Ryujinx.Graphics.Vulkan _pipeline.SetProgram(_programStrideChange); _pipeline.DispatchCompute(1 + elems / ConvertElementsPerWorkgroup, 1, 1); - gd.BufferManager.Delete(bufferHandle); - _pipeline.Finish(gd, cbs); } else @@ -1025,7 +990,7 @@ namespace Ryujinx.Graphics.Vulkan { const int ParamsBufferSize = 4; - Span shaderParams = stackalloc int[sizeof(int)]; + Span shaderParams = stackalloc int[ParamsBufferSize / sizeof(int)]; int srcBpp = src.Info.BytesPerPixel; int dstBpp = dst.Info.BytesPerPixel; @@ -1034,9 +999,9 @@ namespace Ryujinx.Graphics.Vulkan shaderParams[0] = BitOperations.Log2((uint)ratio); - var bufferHandle = gd.BufferManager.CreateWithHandle(gd, ParamsBufferSize); + using var buffer = gd.BufferManager.ReserveOrCreate(gd, cbs, ParamsBufferSize); - gd.BufferManager.SetData(bufferHandle, 0, shaderParams); + buffer.Holder.SetDataUnchecked(buffer.Offset, shaderParams); TextureView.InsertImageBarrier( gd.Api, @@ -1064,7 +1029,7 @@ namespace Ryujinx.Graphics.Vulkan var srcFormat = GetFormat(componentSize, srcBpp / componentSize); var dstFormat = GetFormat(componentSize, dstBpp / componentSize); - _pipeline.SetUniformBuffers(stackalloc[] { new BufferAssignment(0, new BufferRange(bufferHandle, 0, ParamsBufferSize)) }); + _pipeline.SetUniformBuffers(stackalloc[] { new BufferAssignment(0, buffer.Range) }); for (int l = 0; l < levels; l++) { @@ -1074,7 +1039,7 @@ namespace Ryujinx.Graphics.Vulkan var dstView = Create2DLayerView(dst, dstLayer + z, dstLevel + l); _pipeline.SetTextureAndSamplerIdentitySwizzle(ShaderStage.Compute, 0, srcView, null); - _pipeline.SetImage(0, dstView, dstFormat); + _pipeline.SetImage(ShaderStage.Compute, 0, dstView.GetView(dstFormat)); int dispatchX = (Math.Min(srcView.Info.Width, dstView.Info.Width) + 31) / 32; int dispatchY = (Math.Min(srcView.Info.Height, dstView.Info.Height) + 31) / 32; @@ -1093,8 +1058,6 @@ namespace Ryujinx.Graphics.Vulkan } } - gd.BufferManager.Delete(bufferHandle); - _pipeline.Finish(gd, cbs); TextureView.InsertImageBarrier( @@ -1128,9 +1091,9 @@ namespace Ryujinx.Graphics.Vulkan (shaderParams[0], shaderParams[1]) = GetSampleCountXYLog2(samples); (shaderParams[2], shaderParams[3]) = GetSampleCountXYLog2((int)TextureStorage.ConvertToSampleCountFlags(gd.Capabilities.SupportedSampleCounts, (uint)samples)); - var bufferHandle = gd.BufferManager.CreateWithHandle(gd, ParamsBufferSize); + using var buffer = gd.BufferManager.ReserveOrCreate(gd, cbs, ParamsBufferSize); - gd.BufferManager.SetData(bufferHandle, 0, shaderParams); + buffer.Holder.SetDataUnchecked(buffer.Offset, shaderParams); TextureView.InsertImageBarrier( gd.Api, @@ -1147,7 +1110,7 @@ namespace Ryujinx.Graphics.Vulkan 1); _pipeline.SetCommandBuffer(cbs); - _pipeline.SetUniformBuffers(stackalloc[] { new BufferAssignment(0, new BufferRange(bufferHandle, 0, ParamsBufferSize)) }); + _pipeline.SetUniformBuffers(stackalloc[] { new BufferAssignment(0, buffer.Range) }); if (isDepthOrStencil) { @@ -1175,12 +1138,7 @@ namespace Ryujinx.Graphics.Vulkan var srcView = Create2DLayerView(src, srcLayer + z, 0); var dstView = Create2DLayerView(dst, dstLayer + z, 0); - _pipeline.SetRenderTarget( - dstView.GetImageViewForAttachment(), - (uint)dst.Width, - (uint)dst.Height, - true, - dst.VkFormat); + _pipeline.SetRenderTarget(dstView, (uint)dst.Width, (uint)dst.Height); CopyMSDraw(srcView, aspectFlags, fromMS: true); @@ -1210,7 +1168,7 @@ namespace Ryujinx.Graphics.Vulkan var dstView = Create2DLayerView(dst, dstLayer + z, 0); _pipeline.SetTextureAndSamplerIdentitySwizzle(ShaderStage.Compute, 0, srcView, null); - _pipeline.SetImage(0, dstView, format); + _pipeline.SetImage(ShaderStage.Compute, 0, dstView.GetView(format)); _pipeline.DispatchCompute(dispatchX, dispatchY, 1); @@ -1226,8 +1184,6 @@ namespace Ryujinx.Graphics.Vulkan } } - gd.BufferManager.Delete(bufferHandle); - _pipeline.Finish(gd, cbs); TextureView.InsertImageBarrier( @@ -1261,9 +1217,9 @@ namespace Ryujinx.Graphics.Vulkan (shaderParams[0], shaderParams[1]) = GetSampleCountXYLog2(samples); (shaderParams[2], shaderParams[3]) = GetSampleCountXYLog2((int)TextureStorage.ConvertToSampleCountFlags(gd.Capabilities.SupportedSampleCounts, (uint)samples)); - var bufferHandle = gd.BufferManager.CreateWithHandle(gd, ParamsBufferSize); + using var buffer = gd.BufferManager.ReserveOrCreate(gd, cbs, ParamsBufferSize); - gd.BufferManager.SetData(bufferHandle, 0, shaderParams); + buffer.Holder.SetDataUnchecked(buffer.Offset, shaderParams); TextureView.InsertImageBarrier( gd.Api, @@ -1299,7 +1255,7 @@ namespace Ryujinx.Graphics.Vulkan _pipeline.SetViewports(viewports); _pipeline.SetPrimitiveTopology(PrimitiveTopology.TriangleStrip); - _pipeline.SetUniformBuffers(stackalloc[] { new BufferAssignment(0, new BufferRange(bufferHandle, 0, ParamsBufferSize)) }); + _pipeline.SetUniformBuffers(stackalloc[] { new BufferAssignment(0, buffer.Range) }); if (isDepthOrStencil) { @@ -1308,13 +1264,7 @@ namespace Ryujinx.Graphics.Vulkan var srcView = Create2DLayerView(src, srcLayer + z, 0); var dstView = Create2DLayerView(dst, dstLayer + z, 0); - _pipeline.SetRenderTarget( - dstView.GetImageViewForAttachment(), - (uint)dst.Width, - (uint)dst.Height, - (uint)samples, - true, - dst.VkFormat); + _pipeline.SetRenderTarget(dstView, (uint)dst.Width, (uint)dst.Height); CopyMSDraw(srcView, aspectFlags, fromMS: false); @@ -1342,13 +1292,7 @@ namespace Ryujinx.Graphics.Vulkan var dstView = Create2DLayerView(dst, dstLayer + z, 0); _pipeline.SetTextureAndSamplerIdentitySwizzle(ShaderStage.Fragment, 0, srcView, null); - _pipeline.SetRenderTarget( - dstView.GetView(format).GetImageViewForAttachment(), - (uint)dst.Width, - (uint)dst.Height, - (uint)samples, - false, - vkFormat); + _pipeline.SetRenderTarget(dstView.GetView(format), (uint)dst.Width, (uint)dst.Height); _pipeline.Draw(4, 1, 0, 0); @@ -1364,8 +1308,6 @@ namespace Ryujinx.Graphics.Vulkan } } - gd.BufferManager.Delete(bufferHandle); - _pipeline.Finish(gd, cbs); TextureView.InsertImageBarrier( @@ -1487,9 +1429,9 @@ namespace Ryujinx.Graphics.Vulkan }; var info = new TextureCreateInfo( - from.Info.Width, - from.Info.Height, - from.Info.Depth, + Math.Max(1, from.Info.Width >> level), + Math.Max(1, from.Info.Height >> level), + 1, 1, from.Info.Samples, from.Info.BlockWidth, @@ -1616,10 +1558,11 @@ namespace Ryujinx.Graphics.Vulkan pattern.OffsetIndex.CopyTo(shaderParams[..pattern.OffsetIndex.Length]); - var patternBufferHandle = gd.BufferManager.CreateWithHandle(gd, ParamsBufferSize, out var patternBuffer); + using var patternScoped = gd.BufferManager.ReserveOrCreate(gd, cbs, ParamsBufferSize); + var patternBuffer = patternScoped.Holder; var patternBufferAuto = patternBuffer.GetBuffer(); - gd.BufferManager.SetData(patternBufferHandle, 0, shaderParams); + patternBuffer.SetDataUnchecked(patternScoped.Offset, shaderParams); _pipeline.SetCommandBuffer(cbs); @@ -1635,7 +1578,8 @@ namespace Ryujinx.Graphics.Vulkan indirectDataSize); _pipeline.SetUniformBuffers(stackalloc[] { new BufferAssignment(0, drawCountBufferAligned) }); - _pipeline.SetStorageBuffers(1, new[] { srcIndirectBuffer.GetBuffer(), dstIndirectBuffer.GetBuffer(), patternBuffer.GetBuffer() }); + _pipeline.SetStorageBuffers(1, new[] { srcIndirectBuffer.GetBuffer(), dstIndirectBuffer.GetBuffer() }); + _pipeline.SetStorageBuffers(stackalloc[] { new BufferAssignment(3, patternScoped.Range) }); _pipeline.SetProgram(_programConvertIndirectData); _pipeline.DispatchCompute(1, 1, 1); @@ -1643,12 +1587,12 @@ namespace Ryujinx.Graphics.Vulkan BufferHolder.InsertBufferBarrier( gd, cbs.CommandBuffer, - patternBufferAuto.Get(cbs, ParamsIndirectDispatchOffset, ParamsIndirectDispatchSize).Value, + patternBufferAuto.Get(cbs, patternScoped.Offset + ParamsIndirectDispatchOffset, ParamsIndirectDispatchSize).Value, AccessFlags.ShaderWriteBit, AccessFlags.IndirectCommandReadBit, PipelineStageFlags.ComputeShaderBit, PipelineStageFlags.DrawIndirectBit, - ParamsIndirectDispatchOffset, + patternScoped.Offset + ParamsIndirectDispatchOffset, ParamsIndirectDispatchSize); BufferHolder.InsertBufferBarrier( @@ -1662,11 +1606,11 @@ namespace Ryujinx.Graphics.Vulkan 0, convertedCount * outputIndexSize); - _pipeline.SetUniformBuffers(stackalloc[] { new BufferAssignment(0, new BufferRange(patternBufferHandle, 0, ParamsBufferSize)) }); + _pipeline.SetUniformBuffers(stackalloc[] { new BufferAssignment(0, new BufferRange(patternScoped.Handle, patternScoped.Offset, ParamsBufferSize)) }); _pipeline.SetStorageBuffers(1, new[] { srcIndexBuffer.GetBuffer(), dstIndexBuffer.GetBuffer() }); _pipeline.SetProgram(_programConvertIndexBuffer); - _pipeline.DispatchComputeIndirect(patternBufferAuto, ParamsIndirectDispatchOffset); + _pipeline.DispatchComputeIndirect(patternBufferAuto, patternScoped.Offset + ParamsIndirectDispatchOffset); BufferHolder.InsertBufferBarrier( gd, @@ -1679,8 +1623,6 @@ namespace Ryujinx.Graphics.Vulkan 0, convertedCount * outputIndexSize); - gd.BufferManager.Delete(patternBufferHandle); - _pipeline.Finish(gd, cbs); } @@ -1726,13 +1668,13 @@ namespace Ryujinx.Graphics.Vulkan shaderParams[0] = pixelCount; shaderParams[1] = dstOffset; - var bufferHandle = gd.BufferManager.CreateWithHandle(gd, ParamsBufferSize); + using var buffer = gd.BufferManager.ReserveOrCreate(gd, cbs, ParamsBufferSize); - gd.BufferManager.SetData(bufferHandle, 0, shaderParams); + buffer.Holder.SetDataUnchecked(buffer.Offset, shaderParams); _pipeline.SetCommandBuffer(cbs); - _pipeline.SetUniformBuffers(stackalloc[] { new BufferAssignment(0, new BufferRange(bufferHandle, 0, ParamsBufferSize)) }); + _pipeline.SetUniformBuffers(stackalloc[] { new BufferAssignment(0, buffer.Range) }); Span> sbRanges = new Auto[2]; @@ -1744,8 +1686,6 @@ namespace Ryujinx.Graphics.Vulkan _pipeline.SetProgram(_programConvertD32S8ToD24S8); _pipeline.DispatchCompute(1 + inSize / ConvertElementsPerWorkgroup, 1, 1); - gd.BufferManager.Delete(bufferHandle); - _pipeline.Finish(gd, cbs); BufferHolder.InsertBufferBarrier( diff --git a/src/Ryujinx.Graphics.Vulkan/HostMemoryAllocator.cs b/src/Ryujinx.Graphics.Vulkan/HostMemoryAllocator.cs index baccc698f..77c5f95c9 100644 --- a/src/Ryujinx.Graphics.Vulkan/HostMemoryAllocator.cs +++ b/src/Ryujinx.Graphics.Vulkan/HostMemoryAllocator.cs @@ -13,13 +13,13 @@ namespace Ryujinx.Graphics.Vulkan private readonly struct HostMemoryAllocation { public readonly Auto Allocation; - public readonly IntPtr Pointer; + public readonly nint Pointer; public readonly ulong Size; public ulong Start => (ulong)Pointer; public ulong End => (ulong)Pointer + Size; - public HostMemoryAllocation(Auto allocation, IntPtr pointer, ulong size) + public HostMemoryAllocation(Auto allocation, nint pointer, ulong size) { Allocation = allocation; Pointer = pointer; @@ -50,7 +50,7 @@ namespace Ryujinx.Graphics.Vulkan public unsafe bool TryImport( MemoryRequirements requirements, MemoryPropertyFlags flags, - IntPtr pointer, + nint pointer, ulong size) { lock (_lock) @@ -115,7 +115,7 @@ namespace Ryujinx.Graphics.Vulkan PNext = &importInfo, }; - Result result = _api.AllocateMemory(_device, memoryAllocateInfo, null, out var deviceMemory); + Result result = _api.AllocateMemory(_device, in memoryAllocateInfo, null, out var deviceMemory); if (result < Result.Success) { @@ -139,7 +139,7 @@ namespace Ryujinx.Graphics.Vulkan return true; } - public (Auto, ulong) GetExistingAllocation(IntPtr pointer, ulong size) + public (Auto, ulong) GetExistingAllocation(nint pointer, ulong size) { lock (_lock) { diff --git a/src/Ryujinx.Graphics.Vulkan/ImageArray.cs b/src/Ryujinx.Graphics.Vulkan/ImageArray.cs new file mode 100644 index 000000000..019286d28 --- /dev/null +++ b/src/Ryujinx.Graphics.Vulkan/ImageArray.cs @@ -0,0 +1,207 @@ +using Ryujinx.Graphics.GAL; +using Silk.NET.Vulkan; +using System; +using System.Collections.Generic; + +namespace Ryujinx.Graphics.Vulkan +{ + class ImageArray : ResourceArray, IImageArray + { + private readonly VulkanRenderer _gd; + + private record struct TextureRef + { + public TextureStorage Storage; + public TextureView View; + } + + private readonly TextureRef[] _textureRefs; + private readonly TextureBuffer[] _bufferTextureRefs; + + private readonly DescriptorImageInfo[] _textures; + private readonly BufferView[] _bufferTextures; + + private HashSet _storages; + + private int _cachedCommandBufferIndex; + private int _cachedSubmissionCount; + + private readonly bool _isBuffer; + + public ImageArray(VulkanRenderer gd, int size, bool isBuffer) + { + _gd = gd; + + if (isBuffer) + { + _bufferTextureRefs = new TextureBuffer[size]; + _bufferTextures = new BufferView[size]; + } + else + { + _textureRefs = new TextureRef[size]; + _textures = new DescriptorImageInfo[size]; + } + + _storages = null; + + _cachedCommandBufferIndex = -1; + _cachedSubmissionCount = 0; + + _isBuffer = isBuffer; + } + + public void SetImages(int index, ITexture[] images) + { + for (int i = 0; i < images.Length; i++) + { + ITexture image = images[i]; + + if (image is TextureBuffer textureBuffer) + { + _bufferTextureRefs[index + i] = textureBuffer; + } + else if (image is TextureView view) + { + _textureRefs[index + i].Storage = view.Storage; + _textureRefs[index + i].View = view; + } + else if (!_isBuffer) + { + _textureRefs[index + i].Storage = null; + _textureRefs[index + i].View = default; + } + else + { + _bufferTextureRefs[index + i] = null; + } + } + + SetDirty(); + } + + private void SetDirty() + { + _cachedCommandBufferIndex = -1; + _storages = null; + SetDirty(_gd, isImage: true); + } + + public void QueueWriteToReadBarriers(CommandBufferScoped cbs, PipelineStageFlags stageFlags) + { + HashSet storages = _storages; + + if (storages == null) + { + storages = new HashSet(); + + for (int index = 0; index < _textureRefs.Length; index++) + { + if (_textureRefs[index].Storage != null) + { + storages.Add(_textureRefs[index].Storage); + } + } + + _storages = storages; + } + + foreach (TextureStorage storage in storages) + { + storage.QueueWriteToReadBarrier(cbs, AccessFlags.ShaderReadBit, stageFlags); + } + } + + public ReadOnlySpan GetImageInfos(VulkanRenderer gd, CommandBufferScoped cbs, TextureView dummyTexture) + { + int submissionCount = gd.CommandBufferPool.GetSubmissionCount(cbs.CommandBufferIndex); + + Span textures = _textures; + + if (cbs.CommandBufferIndex == _cachedCommandBufferIndex && submissionCount == _cachedSubmissionCount) + { + return textures; + } + + _cachedCommandBufferIndex = cbs.CommandBufferIndex; + _cachedSubmissionCount = submissionCount; + + for (int i = 0; i < textures.Length; i++) + { + ref var texture = ref textures[i]; + ref var refs = ref _textureRefs[i]; + + if (i > 0 && _textureRefs[i - 1].View == refs.View) + { + texture = textures[i - 1]; + + continue; + } + + texture.ImageLayout = ImageLayout.General; + texture.ImageView = refs.View?.GetIdentityImageView().Get(cbs).Value ?? default; + + if (texture.ImageView.Handle == 0) + { + texture.ImageView = dummyTexture.GetImageView().Get(cbs).Value; + } + } + + return textures; + } + + public ReadOnlySpan GetBufferViews(CommandBufferScoped cbs) + { + Span bufferTextures = _bufferTextures; + + for (int i = 0; i < bufferTextures.Length; i++) + { + bufferTextures[i] = _bufferTextureRefs[i]?.GetBufferView(cbs, true) ?? default; + } + + return bufferTextures; + } + + public DescriptorSet[] GetDescriptorSets( + Device device, + CommandBufferScoped cbs, + DescriptorSetTemplateUpdater templateUpdater, + ShaderCollection program, + int setIndex, + TextureView dummyTexture) + { + if (TryGetCachedDescriptorSets(cbs, program, setIndex, out DescriptorSet[] sets)) + { + // We still need to ensure the current command buffer holds a reference to all used textures. + + if (!_isBuffer) + { + GetImageInfos(_gd, cbs, dummyTexture); + } + else + { + GetBufferViews(cbs); + } + + return sets; + } + + DescriptorSetTemplate template = program.Templates[setIndex]; + + DescriptorSetTemplateWriter tu = templateUpdater.Begin(template); + + if (!_isBuffer) + { + tu.Push(GetImageInfos(_gd, cbs, dummyTexture)); + } + else + { + tu.Push(GetBufferViews(cbs)); + } + + templateUpdater.Commit(_gd, device, sets[0]); + + return sets; + } + } +} diff --git a/src/Ryujinx.Graphics.Vulkan/MemoryAllocation.cs b/src/Ryujinx.Graphics.Vulkan/MemoryAllocation.cs index 3f134e289..d0d0ac1e7 100644 --- a/src/Ryujinx.Graphics.Vulkan/MemoryAllocation.cs +++ b/src/Ryujinx.Graphics.Vulkan/MemoryAllocation.cs @@ -10,7 +10,7 @@ namespace Ryujinx.Graphics.Vulkan private readonly HostMemoryAllocator _hostMemory; public DeviceMemory Memory { get; } - public IntPtr HostPointer { get; } + public nint HostPointer { get; } public ulong Offset { get; } public ulong Size { get; } @@ -18,7 +18,7 @@ namespace Ryujinx.Graphics.Vulkan MemoryAllocatorBlockList owner, MemoryAllocatorBlockList.Block block, DeviceMemory memory, - IntPtr hostPointer, + nint hostPointer, ulong offset, ulong size) { @@ -33,7 +33,7 @@ namespace Ryujinx.Graphics.Vulkan public MemoryAllocation( HostMemoryAllocator hostMemory, DeviceMemory memory, - IntPtr hostPointer, + nint hostPointer, ulong offset, ulong size) { diff --git a/src/Ryujinx.Graphics.Vulkan/MemoryAllocator.cs b/src/Ryujinx.Graphics.Vulkan/MemoryAllocator.cs index 339754db1..a28322a25 100644 --- a/src/Ryujinx.Graphics.Vulkan/MemoryAllocator.cs +++ b/src/Ryujinx.Graphics.Vulkan/MemoryAllocator.cs @@ -76,9 +76,7 @@ namespace Ryujinx.Graphics.Vulkan } } - internal int FindSuitableMemoryTypeIndex( - uint memoryTypeBits, - MemoryPropertyFlags flags) + internal int FindSuitableMemoryTypeIndex(uint memoryTypeBits, MemoryPropertyFlags flags) { for (int i = 0; i < _physicalDevice.PhysicalDeviceMemoryProperties.MemoryTypeCount; i++) { diff --git a/src/Ryujinx.Graphics.Vulkan/MemoryAllocatorBlockList.cs b/src/Ryujinx.Graphics.Vulkan/MemoryAllocatorBlockList.cs index a1acc90f9..4a0cb2a74 100644 --- a/src/Ryujinx.Graphics.Vulkan/MemoryAllocatorBlockList.cs +++ b/src/Ryujinx.Graphics.Vulkan/MemoryAllocatorBlockList.cs @@ -14,9 +14,9 @@ namespace Ryujinx.Graphics.Vulkan public class Block : IComparable { public DeviceMemory Memory { get; private set; } - public IntPtr HostPointer { get; private set; } + public nint HostPointer { get; private set; } public ulong Size { get; } - public bool Mapped => HostPointer != IntPtr.Zero; + public bool Mapped => HostPointer != nint.Zero; private readonly struct Range : IComparable { @@ -37,7 +37,7 @@ namespace Ryujinx.Graphics.Vulkan private readonly List _freeRanges; - public Block(DeviceMemory memory, IntPtr hostPointer, ulong size) + public Block(DeviceMemory memory, nint hostPointer, ulong size) { Memory = memory; HostPointer = hostPointer; @@ -146,7 +146,7 @@ namespace Ryujinx.Graphics.Vulkan if (Mapped) { api.UnmapMemory(device, Memory); - HostPointer = IntPtr.Zero; + HostPointer = nint.Zero; } if (Memory.Handle != 0) @@ -220,15 +220,15 @@ namespace Ryujinx.Graphics.Vulkan MemoryTypeIndex = (uint)MemoryTypeIndex, }; - _api.AllocateMemory(_device, memoryAllocateInfo, null, out var deviceMemory).ThrowOnError(); + _api.AllocateMemory(_device, in memoryAllocateInfo, null, out var deviceMemory).ThrowOnError(); - IntPtr hostPointer = IntPtr.Zero; + nint hostPointer = nint.Zero; if (map) { void* pointer = null; _api.MapMemory(_device, deviceMemory, 0, blockAlignedSize, 0, ref pointer).ThrowOnError(); - hostPointer = (IntPtr)pointer; + hostPointer = (nint)pointer; } var newBlock = new Block(deviceMemory, hostPointer, blockAlignedSize); @@ -241,14 +241,14 @@ namespace Ryujinx.Graphics.Vulkan return new MemoryAllocation(this, newBlock, deviceMemory, GetHostPointer(newBlock, newBlockOffset), newBlockOffset, size); } - private static IntPtr GetHostPointer(Block block, ulong offset) + private static nint GetHostPointer(Block block, ulong offset) { - if (block.HostPointer == IntPtr.Zero) + if (block.HostPointer == nint.Zero) { - return IntPtr.Zero; + return nint.Zero; } - return (IntPtr)((nuint)block.HostPointer + offset); + return (nint)((nuint)block.HostPointer + offset); } public void Free(Block block, ulong offset, ulong size) diff --git a/src/Ryujinx.Graphics.Vulkan/MoltenVK/MVKConfiguration.cs b/src/Ryujinx.Graphics.Vulkan/MoltenVK/MVKConfiguration.cs index bdf606e82..271999375 100644 --- a/src/Ryujinx.Graphics.Vulkan/MoltenVK/MVKConfiguration.cs +++ b/src/Ryujinx.Graphics.Vulkan/MoltenVK/MVKConfiguration.cs @@ -90,7 +90,7 @@ namespace Ryujinx.Graphics.Vulkan.MoltenVK public Bool32 SemaphoreUseMTLFence; public MVKVkSemaphoreSupportStyle SemaphoreSupportStyle; public MVKConfigAutoGPUCaptureScope AutoGPUCaptureScope; - public IntPtr AutoGPUCaptureOutputFilepath; + public nint AutoGPUCaptureOutputFilepath; public Bool32 Texture1DAs2D; public Bool32 PreallocateDescriptors; public Bool32 UseCommandPooling; diff --git a/src/Ryujinx.Graphics.Vulkan/MoltenVK/MVKInitialization.cs b/src/Ryujinx.Graphics.Vulkan/MoltenVK/MVKInitialization.cs index fadfc66dd..086c4e1df 100644 --- a/src/Ryujinx.Graphics.Vulkan/MoltenVK/MVKInitialization.cs +++ b/src/Ryujinx.Graphics.Vulkan/MoltenVK/MVKInitialization.cs @@ -1,3 +1,4 @@ +using Silk.NET.Core.Loader; using Silk.NET.Vulkan; using System; using System.Runtime.InteropServices; @@ -6,20 +7,21 @@ using System.Runtime.Versioning; namespace Ryujinx.Graphics.Vulkan.MoltenVK { [SupportedOSPlatform("macos")] - [SupportedOSPlatform("ios")] public static partial class MVKInitialization { - [LibraryImport("libMoltenVK.dylib")] - private static partial Result vkGetMoltenVKConfigurationMVK(IntPtr unusedInstance, out MVKConfiguration config, in IntPtr configSize); + private const string VulkanLib = "libvulkan.dylib"; [LibraryImport("libMoltenVK.dylib")] - private static partial Result vkSetMoltenVKConfigurationMVK(IntPtr unusedInstance, in MVKConfiguration config, in IntPtr configSize); + private static partial Result vkGetMoltenVKConfigurationMVK(nint unusedInstance, out MVKConfiguration config, in nint configSize); + + [LibraryImport("libMoltenVK.dylib")] + private static partial Result vkSetMoltenVKConfigurationMVK(nint unusedInstance, in MVKConfiguration config, in nint configSize); public static void Initialize() { - var configSize = (IntPtr)Marshal.SizeOf(); + var configSize = (nint)Marshal.SizeOf(); - vkGetMoltenVKConfigurationMVK(IntPtr.Zero, out MVKConfiguration config, configSize); + vkGetMoltenVKConfigurationMVK(nint.Zero, out MVKConfiguration config, configSize); config.UseMetalArgumentBuffers = true; @@ -28,7 +30,22 @@ namespace Ryujinx.Graphics.Vulkan.MoltenVK config.ResumeLostDevice = true; - vkSetMoltenVKConfigurationMVK(IntPtr.Zero, config, configSize); + vkSetMoltenVKConfigurationMVK(nint.Zero, config, configSize); + } + + private static string[] Resolver(string path) + { + if (path.EndsWith(VulkanLib)) + { + path = path[..^VulkanLib.Length] + "libMoltenVK.dylib"; + return [path]; + } + return Array.Empty(); + } + + public static void InitializeResolver() + { + ((DefaultPathResolver)PathResolver.Default).Resolvers.Insert(0, Resolver); } } } diff --git a/src/Ryujinx.Graphics.Vulkan/MultiFenceHolder.cs b/src/Ryujinx.Graphics.Vulkan/MultiFenceHolder.cs index 0bce3b72d..b42524712 100644 --- a/src/Ryujinx.Graphics.Vulkan/MultiFenceHolder.cs +++ b/src/Ryujinx.Graphics.Vulkan/MultiFenceHolder.cs @@ -1,3 +1,4 @@ +using Ryujinx.Common.Memory; using Silk.NET.Vulkan; using System; @@ -165,14 +166,15 @@ namespace Ryujinx.Graphics.Vulkan /// True if all fences were signaled before the timeout expired, false otherwise private bool WaitForFencesImpl(Vk api, Device device, int offset, int size, bool hasTimeout, ulong timeout) { - Span fenceHolders = new FenceHolder[CommandBufferPool.MaxCommandBuffers]; + using SpanOwner fenceHoldersOwner = SpanOwner.Rent(CommandBufferPool.MaxCommandBuffers); + Span fenceHolders = fenceHoldersOwner.Span; int count = size != 0 ? GetOverlappingFences(fenceHolders, offset, size) : GetFences(fenceHolders); Span fences = stackalloc Fence[count]; int fenceCount = 0; - for (int i = 0; i < count; i++) + for (int i = 0; i < fences.Length; i++) { if (fenceHolders[i].TryGet(out Fence fence)) { @@ -194,18 +196,23 @@ namespace Ryujinx.Graphics.Vulkan bool signaled = true; - if (hasTimeout) + try { - signaled = FenceHelper.AllSignaled(api, device, fences[..fenceCount], timeout); + if (hasTimeout) + { + signaled = FenceHelper.AllSignaled(api, device, fences[..fenceCount], timeout); + } + else + { + FenceHelper.WaitAllIndefinitely(api, device, fences[..fenceCount]); + } } - else + finally { - FenceHelper.WaitAllIndefinitely(api, device, fences[..fenceCount]); - } - - for (int i = 0; i < fenceCount; i++) - { - fenceHolders[i].Put(); + for (int i = 0; i < fenceCount; i++) + { + fenceHolders[i].PutLock(); + } } return signaled; diff --git a/src/Ryujinx.Graphics.Vulkan/NativeArray.cs b/src/Ryujinx.Graphics.Vulkan/NativeArray.cs index 7678b63c8..33377962b 100644 --- a/src/Ryujinx.Graphics.Vulkan/NativeArray.cs +++ b/src/Ryujinx.Graphics.Vulkan/NativeArray.cs @@ -40,7 +40,7 @@ namespace Ryujinx.Graphics.Vulkan { if (Pointer != null) { - Marshal.FreeHGlobal((IntPtr)Pointer); + Marshal.FreeHGlobal((nint)Pointer); Pointer = null; } } diff --git a/src/Ryujinx.Graphics.Vulkan/PipelineBase.cs b/src/Ryujinx.Graphics.Vulkan/PipelineBase.cs index 28d05a526..addad83fd 100644 --- a/src/Ryujinx.Graphics.Vulkan/PipelineBase.cs +++ b/src/Ryujinx.Graphics.Vulkan/PipelineBase.cs @@ -2,6 +2,7 @@ using Ryujinx.Graphics.GAL; using Ryujinx.Graphics.Shader; using Silk.NET.Vulkan; using System; +using System.Collections.Generic; using System.Linq; using System.Numerics; using System.Runtime.CompilerServices; @@ -30,10 +31,14 @@ namespace Ryujinx.Graphics.Vulkan public readonly PipelineCache PipelineCache; public readonly AutoFlushCounter AutoFlush; + public readonly Action EndRenderPassDelegate; protected PipelineDynamicState DynamicState; + protected bool IsMainPipeline; private PipelineState _newState; - private bool _stateDirty; + private bool _graphicsStateDirty; + private bool _computeStateDirty; + private bool _bindingBarriersDirty; private PrimitiveTopology _topology; private ulong _currentPipelineHandle; @@ -52,7 +57,9 @@ namespace Ryujinx.Graphics.Vulkan protected FramebufferParams FramebufferParams; private Auto _framebuffer; + private RenderPassHolder _rpHolder; private Auto _renderPass; + private RenderPassHolder _nullRenderPass; private int _writtenAttachmentCount; private bool _framebufferUsingColorWriteMask; @@ -80,9 +87,10 @@ namespace Ryujinx.Graphics.Vulkan private bool _tfEnabled; private bool _tfActive; - private readonly PipelineColorBlendAttachmentState[] _storedBlend; + private FeedbackLoopAspects _feedbackLoop; + private bool _passWritesDepthStencil; - private ulong _drawCountSinceBarrier; + private readonly PipelineColorBlendAttachmentState[] _storedBlend; public ulong DrawCount { get; private set; } public bool RenderPassActive { get; private set; } @@ -92,15 +100,16 @@ namespace Ryujinx.Graphics.Vulkan Device = device; AutoFlush = new AutoFlushCounter(gd); + EndRenderPassDelegate = EndRenderPass; var pipelineCacheCreateInfo = new PipelineCacheCreateInfo { SType = StructureType.PipelineCacheCreateInfo, }; - gd.Api.CreatePipelineCache(device, pipelineCacheCreateInfo, null, out PipelineCache).ThrowOnError(); + gd.Api.CreatePipelineCache(device, in pipelineCacheCreateInfo, null, out PipelineCache).ThrowOnError(); - _descriptorSetUpdater = new DescriptorSetUpdater(gd, this); + _descriptorSetUpdater = new DescriptorSetUpdater(gd, device); _vertexBufferUpdater = new VertexBufferUpdater(gd); _transformFeedbackBuffers = new BufferState[Constants.MaxTransformFeedbackBuffers]; @@ -122,7 +131,7 @@ namespace Ryujinx.Graphics.Vulkan public void Initialize() { - _descriptorSetUpdater.Initialize(); + _descriptorSetUpdater.Initialize(IsMainPipeline); QuadsToTrisPattern = new IndexBufferPattern(Gd, 4, 6, 0, new[] { 0, 1, 2, 0, 2, 3 }, 4, false); TriFanToTrisPattern = new IndexBufferPattern(Gd, 3, 3, 2, new[] { int.MinValue, -1, 0 }, 1, true); @@ -130,48 +139,7 @@ namespace Ryujinx.Graphics.Vulkan public unsafe void Barrier() { - if (_drawCountSinceBarrier != DrawCount) - { - _drawCountSinceBarrier = DrawCount; - - // Barriers are not supported inside a render pass on Apple GPUs. - // As a workaround, end the render pass. - if (Gd.Vendor == Vendor.Apple) - { - EndRenderPass(); - } - } - - MemoryBarrier memoryBarrier = new() - { - SType = StructureType.MemoryBarrier, - SrcAccessMask = AccessFlags.MemoryReadBit | AccessFlags.MemoryWriteBit, - DstAccessMask = AccessFlags.MemoryReadBit | AccessFlags.MemoryWriteBit, - }; - - PipelineStageFlags pipelineStageFlags = PipelineStageFlags.VertexShaderBit | PipelineStageFlags.FragmentShaderBit; - - if (Gd.Capabilities.SupportsGeometryShader) - { - pipelineStageFlags |= PipelineStageFlags.GeometryShaderBit; - } - - if (Gd.Capabilities.SupportsTessellationShader) - { - pipelineStageFlags |= PipelineStageFlags.TessellationControlShaderBit | PipelineStageFlags.TessellationEvaluationShaderBit; - } - - Gd.Api.CmdPipelineBarrier( - CommandBuffer, - pipelineStageFlags, - pipelineStageFlags, - 0, - 1, - memoryBarrier, - 0, - null, - 0, - null); + Gd.Barriers.QueueMemoryBarrier(); } public void ComputeBarrier() @@ -198,6 +166,7 @@ namespace Ryujinx.Graphics.Vulkan public void BeginTransformFeedback(PrimitiveTopology topology) { + Gd.Barriers.EnableTfbBarriers(true); _tfEnabled = true; } @@ -244,14 +213,14 @@ namespace Ryujinx.Graphics.Vulkan CreateRenderPass(); } + Gd.Barriers.Flush(Cbs, RenderPassActive, _rpHolder, EndRenderPassDelegate); + BeginRenderPass(); var clearValue = new ClearValue(new ClearColorValue(color.Red, color.Green, color.Blue, color.Alpha)); var attachment = new ClearAttachment(ImageAspectFlags.ColorBit, (uint)index, clearValue); var clearRect = FramebufferParams.GetClearRect(ClearScissor, layer, layerCount); - FramebufferParams.InsertClearBarrier(Cbs, index); - Gd.Api.CmdClearAttachments(CommandBuffer, 1, &attachment, 1, &clearRect); } @@ -282,36 +251,19 @@ namespace Ryujinx.Graphics.Vulkan CreateRenderPass(); } + Gd.Barriers.Flush(Cbs, RenderPassActive, _rpHolder, EndRenderPassDelegate); + BeginRenderPass(); var attachment = new ClearAttachment(flags, 0, clearValue); var clearRect = FramebufferParams.GetClearRect(ClearScissor, layer, layerCount); - FramebufferParams.InsertClearBarrierDS(Cbs); - Gd.Api.CmdClearAttachments(CommandBuffer, 1, &attachment, 1, &clearRect); } public unsafe void CommandBufferBarrier() { - MemoryBarrier memoryBarrier = new() - { - SType = StructureType.MemoryBarrier, - SrcAccessMask = BufferHolder.DefaultAccessFlags, - DstAccessMask = AccessFlags.IndirectCommandReadBit, - }; - - Gd.Api.CmdPipelineBarrier( - CommandBuffer, - PipelineStageFlags.AllCommandsBit, - PipelineStageFlags.DrawIndirectBit, - 0, - 1, - memoryBarrier, - 0, - null, - 0, - null); + Gd.Barriers.QueueCommandBufferBarrier(); } public void CopyBuffer(BufferHandle source, BufferHandle destination, int srcOffset, int dstOffset, int size) @@ -351,7 +303,7 @@ namespace Ryujinx.Graphics.Vulkan } EndRenderPass(); - RecreatePipelineIfNeeded(PipelineBindPoint.Compute); + RecreateComputePipelineIfNeeded(); Gd.Api.CmdDispatch(CommandBuffer, (uint)groupsX, (uint)groupsY, (uint)groupsZ); } @@ -364,19 +316,23 @@ namespace Ryujinx.Graphics.Vulkan } EndRenderPass(); - RecreatePipelineIfNeeded(PipelineBindPoint.Compute); + RecreateComputePipelineIfNeeded(); Gd.Api.CmdDispatchIndirect(CommandBuffer, indirectBuffer.Get(Cbs, indirectBufferOffset, 12).Value, (ulong)indirectBufferOffset); } public void Draw(int vertexCount, int instanceCount, int firstVertex, int firstInstance) { - if (!_program.IsLinked || vertexCount == 0) + if (vertexCount == 0) + { + return; + } + + if (!RecreateGraphicsPipelineIfNeeded()) { return; } - RecreatePipelineIfNeeded(PipelineBindPoint.Graphics); BeginRenderPass(); DrawCount++; @@ -435,13 +391,18 @@ namespace Ryujinx.Graphics.Vulkan public void DrawIndexed(int indexCount, int instanceCount, int firstIndex, int firstVertex, int firstInstance) { - if (!_program.IsLinked || indexCount == 0) + if (indexCount == 0) { return; } UpdateIndexBufferPattern(); - RecreatePipelineIfNeeded(PipelineBindPoint.Graphics); + + if (!RecreateGraphicsPipelineIfNeeded()) + { + return; + } + BeginRenderPass(); DrawCount++; @@ -474,17 +435,17 @@ namespace Ryujinx.Graphics.Vulkan public void DrawIndexedIndirect(BufferRange indirectBuffer) { - if (!_program.IsLinked) - { - return; - } - var buffer = Gd.BufferManager .GetBuffer(CommandBuffer, indirectBuffer.Handle, indirectBuffer.Offset, indirectBuffer.Size, false) .Get(Cbs, indirectBuffer.Offset, indirectBuffer.Size).Value; UpdateIndexBufferPattern(); - RecreatePipelineIfNeeded(PipelineBindPoint.Graphics); + + if (!RecreateGraphicsPipelineIfNeeded()) + { + return; + } + BeginRenderPass(); DrawCount++; @@ -520,11 +481,6 @@ namespace Ryujinx.Graphics.Vulkan public void DrawIndexedIndirectCount(BufferRange indirectBuffer, BufferRange parameterBuffer, int maxDrawCount, int stride) { - if (!_program.IsLinked) - { - return; - } - var countBuffer = Gd.BufferManager .GetBuffer(CommandBuffer, parameterBuffer.Handle, parameterBuffer.Offset, parameterBuffer.Size, false) .Get(Cbs, parameterBuffer.Offset, parameterBuffer.Size).Value; @@ -534,7 +490,12 @@ namespace Ryujinx.Graphics.Vulkan .Get(Cbs, indirectBuffer.Offset, indirectBuffer.Size).Value; UpdateIndexBufferPattern(); - RecreatePipelineIfNeeded(PipelineBindPoint.Graphics); + + if (!RecreateGraphicsPipelineIfNeeded()) + { + return; + } + BeginRenderPass(); DrawCount++; @@ -612,18 +573,17 @@ namespace Ryujinx.Graphics.Vulkan public void DrawIndirect(BufferRange indirectBuffer) { - if (!_program.IsLinked) - { - return; - } - // TODO: Support quads and other unsupported topologies. var buffer = Gd.BufferManager .GetBuffer(CommandBuffer, indirectBuffer.Handle, indirectBuffer.Offset, indirectBuffer.Size, false) .Get(Cbs, indirectBuffer.Offset, indirectBuffer.Size, false).Value; - RecreatePipelineIfNeeded(PipelineBindPoint.Graphics); + if (!RecreateGraphicsPipelineIfNeeded()) + { + return; + } + BeginRenderPass(); ResumeTransformFeedbackInternal(); DrawCount++; @@ -639,11 +599,6 @@ namespace Ryujinx.Graphics.Vulkan throw new NotSupportedException(); } - if (!_program.IsLinked) - { - return; - } - var buffer = Gd.BufferManager .GetBuffer(CommandBuffer, indirectBuffer.Handle, indirectBuffer.Offset, indirectBuffer.Size, false) .Get(Cbs, indirectBuffer.Offset, indirectBuffer.Size, false).Value; @@ -654,7 +609,11 @@ namespace Ryujinx.Graphics.Vulkan // TODO: Support quads and other unsupported topologies. - RecreatePipelineIfNeeded(PipelineBindPoint.Graphics); + if (!RecreateGraphicsPipelineIfNeeded()) + { + return; + } + BeginRenderPass(); ResumeTransformFeedbackInternal(); DrawCount++; @@ -677,9 +636,9 @@ namespace Ryujinx.Graphics.Vulkan var oldStencilTestEnable = _newState.StencilTestEnable; var oldDepthTestEnable = _newState.DepthTestEnable; var oldDepthWriteEnable = _newState.DepthWriteEnable; - var oldTopology = _newState.Topology; var oldViewports = DynamicState.Viewports; var oldViewportsCount = _newState.ViewportsCount; + var oldTopology = _topology; _newState.CullMode = CullModeFlags.None; _newState.StencilTestEnable = false; @@ -699,7 +658,7 @@ namespace Ryujinx.Graphics.Vulkan _newState.StencilTestEnable = oldStencilTestEnable; _newState.DepthTestEnable = oldDepthTestEnable; _newState.DepthWriteEnable = oldDepthWriteEnable; - _newState.Topology = oldTopology; + SetPrimitiveTopology(oldTopology); DynamicState.SetViewports(ref oldViewports, oldViewportsCount); @@ -710,6 +669,7 @@ namespace Ryujinx.Graphics.Vulkan public void EndTransformFeedback() { + Gd.Barriers.EnableTfbBarriers(false); PauseTransformFeedbackInternal(); _tfEnabled = false; } @@ -739,14 +699,12 @@ namespace Ryujinx.Graphics.Vulkan _vertexBufferUpdater.Commit(Cbs); } -#pragma warning disable CA1822 // Mark member as static public void SetAlphaTest(bool enable, float reference, CompareOp op) { // This is currently handled using shader specialization, as Vulkan does not support alpha test. // In the future, we may want to use this to write the reference value into the support buffer, // to avoid creating one version of the shader per reference value used. } -#pragma warning restore CA1822 public void SetBlendState(AdvancedBlendDescriptor blend) { @@ -861,6 +819,8 @@ namespace Ryujinx.Graphics.Vulkan _newState.DepthTestEnable = depthTest.TestEnable; _newState.DepthWriteEnable = depthTest.WriteEnable; _newState.DepthCompareOp = depthTest.Func.Convert(); + + UpdatePassDepthStencil(); SignalStateChange(); } @@ -876,9 +836,9 @@ namespace Ryujinx.Graphics.Vulkan SignalStateChange(); } - public void SetImage(int binding, ITexture image, Format imageFormat) + public void SetImage(ShaderStage stage, int binding, ITexture image) { - _descriptorSetUpdater.SetImage(binding, image, imageFormat); + _descriptorSetUpdater.SetImage(Cbs, stage, binding, image); } public void SetImage(int binding, Auto image) @@ -886,6 +846,16 @@ namespace Ryujinx.Graphics.Vulkan _descriptorSetUpdater.SetImage(binding, image); } + public void SetImageArray(ShaderStage stage, int binding, IImageArray array) + { + _descriptorSetUpdater.SetImageArray(Cbs, stage, binding, array); + } + + public void SetImageArraySeparate(ShaderStage stage, int setIndex, IImageArray array) + { + _descriptorSetUpdater.SetImageArraySeparate(Cbs, stage, setIndex, array); + } + public void SetIndexBuffer(BufferRange buffer, IndexType type) { if (buffer.Handle != BufferHandle.Null) @@ -928,7 +898,6 @@ namespace Ryujinx.Graphics.Vulkan // TODO: Default levels (likely needs emulation on shaders?) } -#pragma warning disable CA1822 // Mark member as static public void SetPointParameters(float size, bool isProgramPointSize, bool enablePointSprite, Origin origin) { // TODO. @@ -938,7 +907,6 @@ namespace Ryujinx.Graphics.Vulkan { // TODO. } -#pragma warning restore CA1822 public void SetPrimitiveRestart(bool enable, int index) { @@ -965,9 +933,11 @@ namespace Ryujinx.Graphics.Vulkan _program = internalProgram; - _descriptorSetUpdater.SetProgram(internalProgram); + _descriptorSetUpdater.SetProgram(Cbs, internalProgram, _currentPipelineHandle != 0); + _bindingBarriersDirty = true; _newState.PipelineLayout = internalProgram.PipelineLayout; + _newState.HasTessellationControlShader = internalProgram.HasTessellationControlShader; _newState.StagesCount = (uint)stages.Length; stages.CopyTo(_newState.Stages.AsSpan()[..stages.Length]); @@ -1000,6 +970,13 @@ namespace Ryujinx.Graphics.Vulkan { _newState.RasterizerDiscardEnable = discard; SignalStateChange(); + + if (!discard && Gd.IsQualcommProprietary) + { + // On Adreno, enabling rasterizer discard somehow corrupts the viewport state. + // Force it to be updated on next use to work around this bug. + DynamicState.ForceAllDirty(); + } } public void SetRenderTargetColorMasks(ReadOnlySpan componentMask) @@ -1055,7 +1032,6 @@ namespace Ryujinx.Graphics.Vulkan private void SetRenderTargetsInternal(ITexture[] colors, ITexture depthStencil, bool filterWriteMasked) { CreateFramebuffer(colors, depthStencil, filterWriteMasked); - FramebufferParams?.UpdateModifications(); CreateRenderPass(); SignalStateChange(); SignalAttachmentChange(); @@ -1110,6 +1086,8 @@ namespace Ryujinx.Graphics.Vulkan _newState.StencilFrontPassOp = stencilTest.FrontDpPass.Convert(); _newState.StencilFrontDepthFailOp = stencilTest.FrontDpFail.Convert(); _newState.StencilFrontCompareOp = stencilTest.FrontFunc.Convert(); + + UpdatePassDepthStencil(); SignalStateChange(); } @@ -1133,6 +1111,16 @@ namespace Ryujinx.Graphics.Vulkan _descriptorSetUpdater.SetTextureAndSamplerIdentitySwizzle(Cbs, stage, binding, texture, sampler); } + public void SetTextureArray(ShaderStage stage, int binding, ITextureArray array) + { + _descriptorSetUpdater.SetTextureArray(Cbs, stage, binding, array); + } + + public void SetTextureArraySeparate(ShaderStage stage, int setIndex, ITextureArray array) + { + _descriptorSetUpdater.SetTextureArraySeparate(Cbs, stage, setIndex, array); + } + public void SetTransformFeedbackBuffers(ReadOnlySpan buffers) { PauseTransformFeedbackInternal(); @@ -1163,12 +1151,10 @@ namespace Ryujinx.Graphics.Vulkan _descriptorSetUpdater.SetUniformBuffers(CommandBuffer, buffers); } -#pragma warning disable CA1822 // Mark member as static public void SetUserClipDistance(int index, bool enableClip) { // TODO. } -#pragma warning restore CA1822 public void SetVertexAttribs(ReadOnlySpan vertexAttribs) { @@ -1362,26 +1348,19 @@ namespace Ryujinx.Graphics.Vulkan SignalCommandBufferChange(); } + public void ForceTextureDirty() + { + _descriptorSetUpdater.ForceTextureDirty(); + } + + public void ForceImageDirty() + { + _descriptorSetUpdater.ForceImageDirty(); + } + public unsafe void TextureBarrier() { - MemoryBarrier memoryBarrier = new() - { - SType = StructureType.MemoryBarrier, - SrcAccessMask = AccessFlags.MemoryReadBit | AccessFlags.MemoryWriteBit, - DstAccessMask = AccessFlags.MemoryReadBit | AccessFlags.MemoryWriteBit, - }; - - Gd.Api.CmdPipelineBarrier( - CommandBuffer, - PipelineStageFlags.FragmentShaderBit, - PipelineStageFlags.FragmentShaderBit, - 0, - 1, - memoryBarrier, - 0, - null, - 0, - null); + Gd.Barriers.QueueTextureBarrier(); } public void TextureBarrierTiled() @@ -1456,7 +1435,23 @@ namespace Ryujinx.Graphics.Vulkan } } + if (IsMainPipeline) + { + FramebufferParams?.ClearBindings(); + } + FramebufferParams = new FramebufferParams(Device, colors, depthStencil); + + if (IsMainPipeline) + { + FramebufferParams.AddBindings(); + + _newState.FeedbackLoopAspects = FeedbackLoopAspects.None; + _bindingBarriersDirty = true; + } + + _passWritesDepthStencil = false; + UpdatePassDepthStencil(); UpdatePipelineAttachmentFormats(); } @@ -1465,6 +1460,7 @@ namespace Ryujinx.Graphics.Vulkan var dstAttachmentFormats = _newState.Internal.AttachmentFormats.AsSpan(); FramebufferParams.AttachmentFormats.CopyTo(dstAttachmentFormats); _newState.Internal.AttachmentIntegerFormatMask = FramebufferParams.AttachmentIntegerFormatMask; + _newState.Internal.LogicOpsAllowed = FramebufferParams.LogicOpsAllowed; for (int i = FramebufferParams.AttachmentFormats.Length; i < dstAttachmentFormats.Length; i++) { @@ -1478,113 +1474,134 @@ namespace Ryujinx.Graphics.Vulkan protected unsafe void CreateRenderPass() { - const int MaxAttachments = Constants.MaxRenderTargets + 1; - - AttachmentDescription[] attachmentDescs = null; - - var subpass = new SubpassDescription - { - PipelineBindPoint = PipelineBindPoint.Graphics, - }; - - AttachmentReference* attachmentReferences = stackalloc AttachmentReference[MaxAttachments]; - var hasFramebuffer = FramebufferParams != null; - if (hasFramebuffer && FramebufferParams.AttachmentsCount != 0) - { - attachmentDescs = new AttachmentDescription[FramebufferParams.AttachmentsCount]; - - for (int i = 0; i < FramebufferParams.AttachmentsCount; i++) - { - attachmentDescs[i] = new AttachmentDescription( - 0, - FramebufferParams.AttachmentFormats[i], - TextureStorage.ConvertToSampleCountFlags(Gd.Capabilities.SupportedSampleCounts, FramebufferParams.AttachmentSamples[i]), - AttachmentLoadOp.Load, - AttachmentStoreOp.Store, - AttachmentLoadOp.Load, - AttachmentStoreOp.Store, - ImageLayout.General, - ImageLayout.General); - } - - int colorAttachmentsCount = FramebufferParams.ColorAttachmentsCount; - - if (colorAttachmentsCount > MaxAttachments - 1) - { - colorAttachmentsCount = MaxAttachments - 1; - } - - if (colorAttachmentsCount != 0) - { - int maxAttachmentIndex = FramebufferParams.MaxColorAttachmentIndex; - subpass.ColorAttachmentCount = (uint)maxAttachmentIndex + 1; - subpass.PColorAttachments = &attachmentReferences[0]; - - // Fill with VK_ATTACHMENT_UNUSED to cover any gaps. - for (int i = 0; i <= maxAttachmentIndex; i++) - { - subpass.PColorAttachments[i] = new AttachmentReference(Vk.AttachmentUnused, ImageLayout.Undefined); - } - - for (int i = 0; i < colorAttachmentsCount; i++) - { - int bindIndex = FramebufferParams.AttachmentIndices[i]; - - subpass.PColorAttachments[bindIndex] = new AttachmentReference((uint)i, ImageLayout.General); - } - } - - if (FramebufferParams.HasDepthStencil) - { - uint dsIndex = (uint)FramebufferParams.AttachmentsCount - 1; - - subpass.PDepthStencilAttachment = &attachmentReferences[MaxAttachments - 1]; - *subpass.PDepthStencilAttachment = new AttachmentReference(dsIndex, ImageLayout.General); - } - } - - var subpassDependency = PipelineConverter.CreateSubpassDependency(); - - fixed (AttachmentDescription* pAttachmentDescs = attachmentDescs) - { - var renderPassCreateInfo = new RenderPassCreateInfo - { - SType = StructureType.RenderPassCreateInfo, - PAttachments = pAttachmentDescs, - AttachmentCount = attachmentDescs != null ? (uint)attachmentDescs.Length : 0, - PSubpasses = &subpass, - SubpassCount = 1, - PDependencies = &subpassDependency, - DependencyCount = 1, - }; - - Gd.Api.CreateRenderPass(Device, renderPassCreateInfo, null, out var renderPass).ThrowOnError(); - - _renderPass?.Dispose(); - _renderPass = new Auto(new DisposableRenderPass(Gd.Api, Device, renderPass)); - } - EndRenderPass(); - _framebuffer?.Dispose(); - _framebuffer = hasFramebuffer ? FramebufferParams.Create(Gd.Api, Cbs, _renderPass) : null; + if (!hasFramebuffer || FramebufferParams.AttachmentsCount == 0) + { + // Use the null framebuffer. + _nullRenderPass ??= new RenderPassHolder(Gd, Device, new RenderPassCacheKey(), FramebufferParams); + + _rpHolder = _nullRenderPass; + _renderPass = _nullRenderPass.GetRenderPass(); + _framebuffer = _nullRenderPass.GetFramebuffer(Gd, Cbs, FramebufferParams); + } + else + { + (_rpHolder, _framebuffer) = FramebufferParams.GetPassAndFramebuffer(Gd, Device, Cbs); + + _renderPass = _rpHolder.GetRenderPass(); + } } protected void SignalStateChange() { - _stateDirty = true; + _graphicsStateDirty = true; + _computeStateDirty = true; } - private void RecreatePipelineIfNeeded(PipelineBindPoint pbp) + private void RecreateComputePipelineIfNeeded() + { + if (_computeStateDirty || Pbp != PipelineBindPoint.Compute) + { + CreatePipeline(PipelineBindPoint.Compute); + _computeStateDirty = false; + Pbp = PipelineBindPoint.Compute; + + if (_bindingBarriersDirty) + { + // Stale barriers may have been activated by switching program. Emit any that are relevant. + _descriptorSetUpdater.InsertBindingBarriers(Cbs); + + _bindingBarriersDirty = false; + } + } + + Gd.Barriers.Flush(Cbs, _program, _feedbackLoop != 0, RenderPassActive, _rpHolder, EndRenderPassDelegate); + + _descriptorSetUpdater.UpdateAndBindDescriptorSets(Cbs, PipelineBindPoint.Compute); + } + + private bool ChangeFeedbackLoop(FeedbackLoopAspects aspects) + { + if (_feedbackLoop != aspects) + { + if (Gd.Capabilities.SupportsDynamicAttachmentFeedbackLoop) + { + DynamicState.SetFeedbackLoop(aspects); + } + else + { + _newState.FeedbackLoopAspects = aspects; + } + + _feedbackLoop = aspects; + + return true; + } + + return false; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool UpdateFeedbackLoop() + { + List hazards = _descriptorSetUpdater.FeedbackLoopHazards; + + if ((hazards?.Count ?? 0) > 0) + { + FeedbackLoopAspects aspects = 0; + + foreach (TextureView view in hazards) + { + // May need to enforce feedback loop layout here in the future. + // Though technically, it should always work with the general layout. + + if (view.Info.Format.IsDepthOrStencil()) + { + if (_passWritesDepthStencil) + { + // If depth/stencil isn't written in the pass, it doesn't count as a feedback loop. + + aspects |= FeedbackLoopAspects.Depth; + } + } + else + { + aspects |= FeedbackLoopAspects.Color; + } + } + + return ChangeFeedbackLoop(aspects); + } + else if (_feedbackLoop != 0) + { + return ChangeFeedbackLoop(FeedbackLoopAspects.None); + } + + return false; + } + + private void UpdatePassDepthStencil() + { + if (!RenderPassActive) + { + _passWritesDepthStencil = false; + } + + // Stencil test being enabled doesn't necessarily mean a write, but it's not critical to check. + _passWritesDepthStencil |= (_newState.DepthTestEnable && _newState.DepthWriteEnable) || _newState.StencilTestEnable; + } + + private bool RecreateGraphicsPipelineIfNeeded() { if (AutoFlush.ShouldFlushDraw(DrawCount)) { Gd.FlushAllCommands(); } - DynamicState.ReplayIfDirty(Gd.Api, CommandBuffer); + DynamicState.ReplayIfDirty(Gd, CommandBuffer); if (_needsIndexBufferRebind && _indexBufferPattern == null) { @@ -1618,17 +1635,33 @@ namespace Ryujinx.Graphics.Vulkan _vertexBufferUpdater.Commit(Cbs); } - if (_stateDirty || Pbp != pbp) + if (_bindingBarriersDirty) { - CreatePipeline(pbp); - _stateDirty = false; - Pbp = pbp; + // Stale barriers may have been activated by switching program. Emit any that are relevant. + _descriptorSetUpdater.InsertBindingBarriers(Cbs); + + _bindingBarriersDirty = false; } - _descriptorSetUpdater.UpdateAndBindDescriptorSets(Cbs, pbp); + if (UpdateFeedbackLoop() || _graphicsStateDirty || Pbp != PipelineBindPoint.Graphics) + { + if (!CreatePipeline(PipelineBindPoint.Graphics)) + { + return false; + } + + _graphicsStateDirty = false; + Pbp = PipelineBindPoint.Graphics; + } + + Gd.Barriers.Flush(Cbs, _program, _feedbackLoop != 0, RenderPassActive, _rpHolder, EndRenderPassDelegate); + + _descriptorSetUpdater.UpdateAndBindDescriptorSets(Cbs, PipelineBindPoint.Graphics); + + return true; } - private void CreatePipeline(PipelineBindPoint pbp) + private bool CreatePipeline(PipelineBindPoint pbp) { // We can only create a pipeline if the have the shader stages set. if (_newState.Stages != null) @@ -1638,10 +1671,25 @@ namespace Ryujinx.Graphics.Vulkan CreateRenderPass(); } + if (!_program.IsLinked) + { + // Background compile failed, we likely can't create the pipeline because the shader is broken + // or the driver failed to compile it. + + return false; + } + var pipeline = pbp == PipelineBindPoint.Compute ? _newState.CreateComputePipeline(Gd, Device, _program, PipelineCache) : _newState.CreateGraphicsPipeline(Gd, Device, _program, PipelineCache, _renderPass.Get(Cbs).Value); + if (pipeline == null) + { + // Host failed to create the pipeline, likely due to driver bugs. + + return false; + } + ulong pipelineHandle = pipeline.GetUnsafe().Value.Handle; if (_currentPipelineHandle != pipelineHandle) @@ -1653,12 +1701,16 @@ namespace Ryujinx.Graphics.Vulkan Gd.Api.CmdBindPipeline(CommandBuffer, pbp, Pipeline.Get(Cbs).Value); } } + + return true; } private unsafe void BeginRenderPass() { if (!RenderPassActive) { + FramebufferParams.InsertLoadOpBarriers(Gd, Cbs); + var renderArea = new Rect2D(null, new Extent2D(FramebufferParams.Width, FramebufferParams.Height)); var clearValue = new ClearValue(); @@ -1672,7 +1724,7 @@ namespace Ryujinx.Graphics.Vulkan ClearValueCount = 1, }; - Gd.Api.CmdBeginRenderPass(CommandBuffer, renderPassBeginInfo, SubpassContents.Inline); + Gd.Api.CmdBeginRenderPass(CommandBuffer, in renderPassBeginInfo, SubpassContents.Inline); RenderPassActive = true; } } @@ -1681,6 +1733,8 @@ namespace Ryujinx.Graphics.Vulkan { if (RenderPassActive) { + FramebufferParams.AddStoreOpUsage(); + PauseTransformFeedbackInternal(); Gd.Api.CmdEndRenderPass(CommandBuffer); SignalRenderPassEnd(); @@ -1724,8 +1778,7 @@ namespace Ryujinx.Graphics.Vulkan { if (disposing) { - _renderPass?.Dispose(); - _framebuffer?.Dispose(); + _nullRenderPass?.Dispose(); _newState.Dispose(); _descriptorSetUpdater.Dispose(); _vertexBufferUpdater.Dispose(); diff --git a/src/Ryujinx.Graphics.Vulkan/PipelineConverter.cs b/src/Ryujinx.Graphics.Vulkan/PipelineConverter.cs index 95b480a5e..85069c6b2 100644 --- a/src/Ryujinx.Graphics.Vulkan/PipelineConverter.cs +++ b/src/Ryujinx.Graphics.Vulkan/PipelineConverter.cs @@ -9,13 +9,6 @@ namespace Ryujinx.Graphics.Vulkan { static class PipelineConverter { - private const AccessFlags SubpassAccessMask = - AccessFlags.MemoryReadBit | - AccessFlags.MemoryWriteBit | - AccessFlags.ShaderReadBit | - AccessFlags.ColorAttachmentWriteBit | - AccessFlags.DepthStencilAttachmentWriteBit; - public static unsafe DisposableRenderPass ToRenderPass(this ProgramPipelineState state, VulkanRenderer gd, Device device) { const int MaxAttachments = Constants.MaxRenderTargets + 1; @@ -108,7 +101,7 @@ namespace Ryujinx.Graphics.Vulkan } } - var subpassDependency = CreateSubpassDependency(); + var subpassDependency = CreateSubpassDependency(gd); fixed (AttachmentDescription* pAttachmentDescs = attachmentDescs) { @@ -123,35 +116,39 @@ namespace Ryujinx.Graphics.Vulkan DependencyCount = 1, }; - gd.Api.CreateRenderPass(device, renderPassCreateInfo, null, out var renderPass).ThrowOnError(); + gd.Api.CreateRenderPass(device, in renderPassCreateInfo, null, out var renderPass).ThrowOnError(); return new DisposableRenderPass(gd.Api, device, renderPass); } } - public static SubpassDependency CreateSubpassDependency() + public static SubpassDependency CreateSubpassDependency(VulkanRenderer gd) { + var (access, stages) = BarrierBatch.GetSubpassAccessSuperset(gd); + return new SubpassDependency( 0, 0, - PipelineStageFlags.AllGraphicsBit, - PipelineStageFlags.AllGraphicsBit, - SubpassAccessMask, - SubpassAccessMask, + stages, + stages, + access, + access, 0); } - public unsafe static SubpassDependency2 CreateSubpassDependency2() + public unsafe static SubpassDependency2 CreateSubpassDependency2(VulkanRenderer gd) { + var (access, stages) = BarrierBatch.GetSubpassAccessSuperset(gd); + return new SubpassDependency2( StructureType.SubpassDependency2, null, 0, 0, - PipelineStageFlags.AllGraphicsBit, - PipelineStageFlags.AllGraphicsBit, - SubpassAccessMask, - SubpassAccessMask, + stages, + stages, + access, + access, 0); } @@ -180,9 +177,6 @@ namespace Ryujinx.Graphics.Vulkan pipeline.LogicOpEnable = state.LogicOpEnable; pipeline.LogicOp = state.LogicOp.Convert(); - pipeline.MinDepthBounds = 0f; // Not implemented. - pipeline.MaxDepthBounds = 0f; // Not implemented. - pipeline.PatchControlPoints = state.PatchControlPoints; pipeline.PolygonMode = PolygonMode.Fill; // Not implemented. pipeline.PrimitiveRestartEnable = state.PrimitiveRestartEnable; @@ -208,17 +202,11 @@ namespace Ryujinx.Graphics.Vulkan pipeline.StencilFrontPassOp = state.StencilTest.FrontDpPass.Convert(); pipeline.StencilFrontDepthFailOp = state.StencilTest.FrontDpFail.Convert(); pipeline.StencilFrontCompareOp = state.StencilTest.FrontFunc.Convert(); - pipeline.StencilFrontCompareMask = 0; - pipeline.StencilFrontWriteMask = 0; - pipeline.StencilFrontReference = 0; pipeline.StencilBackFailOp = state.StencilTest.BackSFail.Convert(); pipeline.StencilBackPassOp = state.StencilTest.BackDpPass.Convert(); pipeline.StencilBackDepthFailOp = state.StencilTest.BackDpFail.Convert(); pipeline.StencilBackCompareOp = state.StencilTest.BackFunc.Convert(); - pipeline.StencilBackCompareMask = 0; - pipeline.StencilBackWriteMask = 0; - pipeline.StencilBackReference = 0; pipeline.StencilTestEnable = state.StencilTest.TestEnable; @@ -302,6 +290,7 @@ namespace Ryujinx.Graphics.Vulkan int attachmentCount = 0; int maxColorAttachmentIndex = -1; uint attachmentIntegerFormatMask = 0; + bool allFormatsFloatOrSrgb = true; for (int i = 0; i < Constants.MaxRenderTargets; i++) { @@ -314,6 +303,8 @@ namespace Ryujinx.Graphics.Vulkan { attachmentIntegerFormatMask |= 1u << i; } + + allFormatsFloatOrSrgb &= state.AttachmentFormats[i].IsFloatOrSrgb(); } } @@ -325,6 +316,7 @@ namespace Ryujinx.Graphics.Vulkan pipeline.ColorBlendAttachmentStateCount = (uint)(maxColorAttachmentIndex + 1); pipeline.VertexAttributeDescriptionsCount = (uint)Math.Min(Constants.MaxVertexAttributes, state.VertexAttribCount); pipeline.Internal.AttachmentIntegerFormatMask = attachmentIntegerFormatMask; + pipeline.Internal.LogicOpsAllowed = attachmentCount == 0 || !allFormatsFloatOrSrgb; return pipeline; } diff --git a/src/Ryujinx.Graphics.Vulkan/PipelineDynamicState.cs b/src/Ryujinx.Graphics.Vulkan/PipelineDynamicState.cs index 1cc33f728..ad26ff7b3 100644 --- a/src/Ryujinx.Graphics.Vulkan/PipelineDynamicState.cs +++ b/src/Ryujinx.Graphics.Vulkan/PipelineDynamicState.cs @@ -1,5 +1,6 @@ using Ryujinx.Common.Memory; using Silk.NET.Vulkan; +using Silk.NET.Vulkan.Extensions.EXT; namespace Ryujinx.Graphics.Vulkan { @@ -21,6 +22,8 @@ namespace Ryujinx.Graphics.Vulkan private Array4 _blendConstants; + private FeedbackLoopAspects _feedbackLoopAspects; + public uint ViewportsCount; public Array16 Viewports; @@ -32,7 +35,8 @@ namespace Ryujinx.Graphics.Vulkan Scissor = 1 << 2, Stencil = 1 << 3, Viewport = 1 << 4, - All = Blend | DepthBias | Scissor | Stencil | Viewport, + FeedbackLoop = 1 << 5, + All = Blend | DepthBias | Scissor | Stencil | Viewport | FeedbackLoop, } private DirtyFlags _dirty; @@ -99,13 +103,22 @@ namespace Ryujinx.Graphics.Vulkan } } + public void SetFeedbackLoop(FeedbackLoopAspects aspects) + { + _feedbackLoopAspects = aspects; + + _dirty |= DirtyFlags.FeedbackLoop; + } + public void ForceAllDirty() { _dirty = DirtyFlags.All; } - public void ReplayIfDirty(Vk api, CommandBuffer commandBuffer) + public void ReplayIfDirty(VulkanRenderer gd, CommandBuffer commandBuffer) { + Vk api = gd.Api; + if (_dirty.HasFlag(DirtyFlags.Blend)) { RecordBlend(api, commandBuffer); @@ -131,6 +144,11 @@ namespace Ryujinx.Graphics.Vulkan RecordViewport(api, commandBuffer); } + if (_dirty.HasFlag(DirtyFlags.FeedbackLoop) && gd.Capabilities.SupportsDynamicAttachmentFeedbackLoop) + { + RecordFeedbackLoop(gd.DynamicFeedbackLoopApi, commandBuffer); + } + _dirty = DirtyFlags.None; } @@ -169,5 +187,17 @@ namespace Ryujinx.Graphics.Vulkan api.CmdSetViewport(commandBuffer, 0, ViewportsCount, Viewports.AsSpan()); } } + + private readonly void RecordFeedbackLoop(ExtAttachmentFeedbackLoopDynamicState api, CommandBuffer commandBuffer) + { + ImageAspectFlags aspects = (_feedbackLoopAspects & FeedbackLoopAspects.Color) != 0 ? ImageAspectFlags.ColorBit : 0; + + if ((_feedbackLoopAspects & FeedbackLoopAspects.Depth) != 0) + { + aspects |= ImageAspectFlags.DepthBit | ImageAspectFlags.StencilBit; + } + + api.CmdSetAttachmentFeedbackLoopEnable(commandBuffer, aspects); + } } } diff --git a/src/Ryujinx.Graphics.Vulkan/PipelineFull.cs b/src/Ryujinx.Graphics.Vulkan/PipelineFull.cs index 24ca715fe..54d43bdba 100644 --- a/src/Ryujinx.Graphics.Vulkan/PipelineFull.cs +++ b/src/Ryujinx.Graphics.Vulkan/PipelineFull.cs @@ -28,6 +28,8 @@ namespace Ryujinx.Graphics.Vulkan _activeBufferMirrors = new(); CommandBuffer = (Cbs = gd.CommandBufferPool.Rent()).CommandBuffer; + + IsMainPipeline = true; } private void CopyPendingQuery() @@ -47,11 +49,12 @@ namespace Ryujinx.Graphics.Vulkan return; } - if (componentMask != 0xf) + if (componentMask != 0xf || Gd.IsQualcommProprietary) { // We can't use CmdClearAttachments if not writing all components, // because on Vulkan, the pipeline state does not affect clears. - var dstTexture = FramebufferParams.GetAttachment(index); + // On proprietary Adreno drivers, CmdClearAttachments appears to execute out of order, so it's better to not use it at all. + var dstTexture = FramebufferParams.GetColorView(index); if (dstTexture == null) { return; @@ -71,7 +74,6 @@ namespace Ryujinx.Graphics.Vulkan componentMask, (int)FramebufferParams.Width, (int)FramebufferParams.Height, - FramebufferParams.AttachmentFormats[index], FramebufferParams.GetAttachmentComponentType(index), ClearScissor); } @@ -88,11 +90,12 @@ namespace Ryujinx.Graphics.Vulkan return; } - if (stencilMask != 0 && stencilMask != 0xff) + if ((stencilMask != 0 && stencilMask != 0xff) || Gd.IsQualcommProprietary) { // We can't use CmdClearAttachments if not clearing all (mask is all ones, 0xFF) or none (mask is 0) of the stencil bits, // because on Vulkan, the pipeline state does not affect clears. - var dstTexture = FramebufferParams.GetDepthStencilAttachment(); + // On proprietary Adreno drivers, CmdClearAttachments appears to execute out of order, so it's better to not use it at all. + var dstTexture = FramebufferParams.GetDepthStencilView(); if (dstTexture == null) { return; @@ -223,20 +226,6 @@ namespace Ryujinx.Graphics.Vulkan } } - private void TryBackingSwaps() - { - CommandBufferScoped? cbs = null; - - _backingSwaps.RemoveAll(holder => holder.TryBackingSwap(ref cbs)); - - cbs?.Dispose(); - } - - public void AddBackingSwap(BufferHolder holder) - { - _backingSwaps.Add(holder); - } - public void Restore() { if (Pipeline != null) @@ -246,7 +235,10 @@ namespace Ryujinx.Graphics.Vulkan SignalCommandBufferChange(); - DynamicState.ReplayIfDirty(Gd.Api, CommandBuffer); + if (Pipeline != null && Pbp == PipelineBindPoint.Graphics) + { + DynamicState.ReplayIfDirty(Gd, CommandBuffer); + } } public void FlushCommandsImpl() @@ -267,6 +259,7 @@ namespace Ryujinx.Graphics.Vulkan PreloadCbs = null; } + Gd.Barriers.Flush(Cbs, false, null, null); CommandBuffer = (Cbs = Gd.CommandBufferPool.ReturnAndRent(Cbs)).CommandBuffer; Gd.RegisterFlush(); @@ -288,8 +281,6 @@ namespace Ryujinx.Graphics.Vulkan Gd.ResetCounterPool(); - TryBackingSwaps(); - Restore(); } diff --git a/src/Ryujinx.Graphics.Vulkan/PipelineHelperShader.cs b/src/Ryujinx.Graphics.Vulkan/PipelineHelperShader.cs index 0a871a5c8..dfbf19013 100644 --- a/src/Ryujinx.Graphics.Vulkan/PipelineHelperShader.cs +++ b/src/Ryujinx.Graphics.Vulkan/PipelineHelperShader.cs @@ -9,21 +9,16 @@ namespace Ryujinx.Graphics.Vulkan { } - public void SetRenderTarget(Auto view, uint width, uint height, bool isDepthStencil, VkFormat format) + public void SetRenderTarget(TextureView view, uint width, uint height) { - SetRenderTarget(view, width, height, 1u, isDepthStencil, format); - } - - public void SetRenderTarget(Auto view, uint width, uint height, uint samples, bool isDepthStencil, VkFormat format) - { - CreateFramebuffer(view, width, height, samples, isDepthStencil, format); + CreateFramebuffer(view, width, height); CreateRenderPass(); SignalStateChange(); } - private void CreateFramebuffer(Auto view, uint width, uint height, uint samples, bool isDepthStencil, VkFormat format) + private void CreateFramebuffer(TextureView view, uint width, uint height) { - FramebufferParams = new FramebufferParams(Device, view, width, height, samples, isDepthStencil, format); + FramebufferParams = new FramebufferParams(Device, view, width, height); UpdatePipelineAttachmentFormats(); } diff --git a/src/Ryujinx.Graphics.Vulkan/PipelineLayoutCacheEntry.cs b/src/Ryujinx.Graphics.Vulkan/PipelineLayoutCacheEntry.cs index 2840dda0f..ae296b033 100644 --- a/src/Ryujinx.Graphics.Vulkan/PipelineLayoutCacheEntry.cs +++ b/src/Ryujinx.Graphics.Vulkan/PipelineLayoutCacheEntry.cs @@ -3,27 +3,26 @@ using Silk.NET.Vulkan; using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Diagnostics; +using System.Runtime.InteropServices; namespace Ryujinx.Graphics.Vulkan { class PipelineLayoutCacheEntry { - // Those were adjusted based on current descriptor usage and the descriptor counts usually used on pipeline layouts. - // It might be a good idea to tweak them again if those change, or maybe find a way to calculate an optimal value dynamically. - private const uint DefaultUniformBufferPoolCapacity = 19 * DescriptorSetManager.MaxSets; - private const uint DefaultStorageBufferPoolCapacity = 16 * DescriptorSetManager.MaxSets; - private const uint DefaultTexturePoolCapacity = 128 * DescriptorSetManager.MaxSets; - private const uint DefaultImagePoolCapacity = 8 * DescriptorSetManager.MaxSets; - - private const int MaxPoolSizesPerSet = 2; + private const int MaxPoolSizesPerSet = 8; private readonly VulkanRenderer _gd; private readonly Device _device; public DescriptorSetLayout[] DescriptorSetLayouts { get; } + public bool[] DescriptorSetLayoutsUpdateAfterBind { get; } public PipelineLayout PipelineLayout { get; } private readonly int[] _consumedDescriptorsPerSet; + private readonly DescriptorPoolSize[][] _poolSizes; + + private readonly DescriptorSetManager _descriptorSetManager; private readonly List>[][] _dsCache; private List>[] _currentDsCache; @@ -31,6 +30,46 @@ namespace Ryujinx.Graphics.Vulkan private int _dsLastCbIndex; private int _dsLastSubmissionCount; + private struct ManualDescriptorSetEntry + { + public Auto DescriptorSet; + public uint CbRefMask; + public bool InUse; + + public ManualDescriptorSetEntry(Auto descriptorSet, int cbIndex) + { + DescriptorSet = descriptorSet; + CbRefMask = 1u << cbIndex; + InUse = true; + } + } + + private readonly struct PendingManualDsConsumption + { + public FenceHolder Fence { get; } + public int CommandBufferIndex { get; } + public int SetIndex { get; } + public int CacheIndex { get; } + + public PendingManualDsConsumption(FenceHolder fence, int commandBufferIndex, int setIndex, int cacheIndex) + { + Fence = fence; + CommandBufferIndex = commandBufferIndex; + SetIndex = setIndex; + CacheIndex = cacheIndex; + fence.Get(); + } + } + + private readonly List[] _manualDsCache; + private readonly Queue _pendingManualDsConsumptions; + private readonly Queue[] _freeManualDsCacheEntries; + + private readonly Dictionary _pdTemplates; + private readonly ResourceDescriptorCollection _pdDescriptors; + private long _lastPdUsage; + private DescriptorSetTemplate _lastPdTemplate; + private PipelineLayoutCacheEntry(VulkanRenderer gd, Device device, int setsCount) { _gd = gd; @@ -49,6 +88,9 @@ namespace Ryujinx.Graphics.Vulkan } _dsCacheCursor = new int[setsCount]; + _manualDsCache = new List[setsCount]; + _pendingManualDsConsumptions = new Queue(); + _freeManualDsCacheEntries = new Queue[setsCount]; } public PipelineLayoutCacheEntry( @@ -57,9 +99,16 @@ namespace Ryujinx.Graphics.Vulkan ReadOnlyCollection setDescriptors, bool usePushDescriptors) : this(gd, device, setDescriptors.Count) { - (DescriptorSetLayouts, PipelineLayout) = PipelineLayoutFactory.Create(gd, device, setDescriptors, usePushDescriptors); + ResourceLayouts layouts = PipelineLayoutFactory.Create(gd, device, setDescriptors, usePushDescriptors); + + DescriptorSetLayouts = layouts.DescriptorSetLayouts; + DescriptorSetLayoutsUpdateAfterBind = layouts.DescriptorSetLayoutsUpdateAfterBind; + PipelineLayout = layouts.PipelineLayout; _consumedDescriptorsPerSet = new int[setDescriptors.Count]; + _poolSizes = new DescriptorPoolSize[setDescriptors.Count][]; + + Span poolSizes = stackalloc DescriptorPoolSize[MaxPoolSizesPerSet]; for (int setIndex = 0; setIndex < setDescriptors.Count; setIndex++) { @@ -71,7 +120,16 @@ namespace Ryujinx.Graphics.Vulkan } _consumedDescriptorsPerSet[setIndex] = count; + _poolSizes[setIndex] = GetDescriptorPoolSizes(poolSizes, setDescriptors[setIndex], DescriptorSetManager.MaxSets).ToArray(); } + + if (usePushDescriptors) + { + _pdDescriptors = setDescriptors[0]; + _pdTemplates = new(); + } + + _descriptorSetManager = new DescriptorSetManager(_device, setDescriptors.Count); } public void UpdateCommandBufferIndex(int commandBufferIndex) @@ -94,18 +152,13 @@ namespace Ryujinx.Graphics.Vulkan int index = _dsCacheCursor[setIndex]++; if (index == list.Count) { - Span poolSizes = stackalloc DescriptorPoolSize[MaxPoolSizesPerSet]; - poolSizes = GetDescriptorPoolSizes(poolSizes, setIndex); - - int consumedDescriptors = _consumedDescriptorsPerSet[setIndex]; - - var dsc = _gd.DescriptorSetManager.AllocateDescriptorSet( + var dsc = _descriptorSetManager.AllocateDescriptorSet( _gd.Api, DescriptorSetLayouts[setIndex], - poolSizes, + _poolSizes[setIndex], setIndex, - consumedDescriptors, - false); + _consumedDescriptorsPerSet[setIndex], + DescriptorSetLayoutsUpdateAfterBind[setIndex]); list.Add(dsc); isNew = true; @@ -116,37 +169,168 @@ namespace Ryujinx.Graphics.Vulkan return list[index]; } - private static Span GetDescriptorPoolSizes(Span output, int setIndex) + public Auto GetNewManualDescriptorSetCollection(CommandBufferScoped cbs, int setIndex, out int cacheIndex) { - int count = 1; + FreeCompletedManualDescriptorSets(); - switch (setIndex) + var list = _manualDsCache[setIndex] ??= new(); + var span = CollectionsMarshal.AsSpan(list); + + Queue freeQueue = _freeManualDsCacheEntries[setIndex]; + + // Do we have at least one freed descriptor set? If so, just use that. + if (freeQueue != null && freeQueue.TryDequeue(out int freeIndex)) { - case PipelineBase.UniformSetIndex: - output[0] = new(DescriptorType.UniformBuffer, DefaultUniformBufferPoolCapacity); - break; - case PipelineBase.StorageSetIndex: - output[0] = new(DescriptorType.StorageBuffer, DefaultStorageBufferPoolCapacity); - break; - case PipelineBase.TextureSetIndex: - output[0] = new(DescriptorType.CombinedImageSampler, DefaultTexturePoolCapacity); - output[1] = new(DescriptorType.UniformTexelBuffer, DefaultTexturePoolCapacity); - count = 2; - break; - case PipelineBase.ImageSetIndex: - output[0] = new(DescriptorType.StorageImage, DefaultImagePoolCapacity); - output[1] = new(DescriptorType.StorageTexelBuffer, DefaultImagePoolCapacity); - count = 2; - break; + ref ManualDescriptorSetEntry entry = ref span[freeIndex]; + + Debug.Assert(!entry.InUse && entry.CbRefMask == 0); + + entry.InUse = true; + entry.CbRefMask = 1u << cbs.CommandBufferIndex; + cacheIndex = freeIndex; + + _pendingManualDsConsumptions.Enqueue(new PendingManualDsConsumption(cbs.GetFence(), cbs.CommandBufferIndex, setIndex, freeIndex)); + + return entry.DescriptorSet; + } + + // Otherwise create a new descriptor set, and add to our pending queue for command buffer consumption tracking. + var dsc = _descriptorSetManager.AllocateDescriptorSet( + _gd.Api, + DescriptorSetLayouts[setIndex], + _poolSizes[setIndex], + setIndex, + _consumedDescriptorsPerSet[setIndex], + DescriptorSetLayoutsUpdateAfterBind[setIndex]); + + cacheIndex = list.Count; + list.Add(new ManualDescriptorSetEntry(dsc, cbs.CommandBufferIndex)); + _pendingManualDsConsumptions.Enqueue(new PendingManualDsConsumption(cbs.GetFence(), cbs.CommandBufferIndex, setIndex, cacheIndex)); + + return dsc; + } + + public void UpdateManualDescriptorSetCollectionOwnership(CommandBufferScoped cbs, int setIndex, int cacheIndex) + { + FreeCompletedManualDescriptorSets(); + + var list = _manualDsCache[setIndex]; + var span = CollectionsMarshal.AsSpan(list); + ref var entry = ref span[cacheIndex]; + + uint cbMask = 1u << cbs.CommandBufferIndex; + + if ((entry.CbRefMask & cbMask) == 0) + { + entry.CbRefMask |= cbMask; + + _pendingManualDsConsumptions.Enqueue(new PendingManualDsConsumption(cbs.GetFence(), cbs.CommandBufferIndex, setIndex, cacheIndex)); + } + } + + private void FreeCompletedManualDescriptorSets() + { + FenceHolder signalledFence = null; + while (_pendingManualDsConsumptions.TryPeek(out var pds) && (pds.Fence == signalledFence || pds.Fence.IsSignaled())) + { + signalledFence = pds.Fence; // Already checked - don't need to do it again. + var dequeued = _pendingManualDsConsumptions.Dequeue(); + Debug.Assert(dequeued.Fence == pds.Fence); + pds.Fence.Put(); + + var span = CollectionsMarshal.AsSpan(_manualDsCache[dequeued.SetIndex]); + ref var entry = ref span[dequeued.CacheIndex]; + entry.CbRefMask &= ~(1u << dequeued.CommandBufferIndex); + + if (!entry.InUse && entry.CbRefMask == 0) + { + // If not in use by any array, and not bound to any command buffer, the descriptor set can be re-used immediately. + (_freeManualDsCacheEntries[dequeued.SetIndex] ??= new()).Enqueue(dequeued.CacheIndex); + } + } + } + + public void ReleaseManualDescriptorSetCollection(int setIndex, int cacheIndex) + { + var list = _manualDsCache[setIndex]; + var span = CollectionsMarshal.AsSpan(list); + + span[cacheIndex].InUse = false; + + if (span[cacheIndex].CbRefMask == 0) + { + // This is no longer in use by any array, so if not bound to any command buffer, the descriptor set can be re-used immediately. + (_freeManualDsCacheEntries[setIndex] ??= new()).Enqueue(cacheIndex); + } + } + + private static Span GetDescriptorPoolSizes(Span output, ResourceDescriptorCollection setDescriptor, uint multiplier) + { + int count = 0; + + for (int index = 0; index < setDescriptor.Descriptors.Count; index++) + { + ResourceDescriptor descriptor = setDescriptor.Descriptors[index]; + DescriptorType descriptorType = descriptor.Type.Convert(); + + bool found = false; + + for (int poolSizeIndex = 0; poolSizeIndex < count; poolSizeIndex++) + { + if (output[poolSizeIndex].Type == descriptorType) + { + output[poolSizeIndex].DescriptorCount += (uint)descriptor.Count * multiplier; + found = true; + break; + } + } + + if (!found) + { + output[count++] = new DescriptorPoolSize() + { + Type = descriptorType, + DescriptorCount = (uint)descriptor.Count, + }; + } } return output[..count]; } + public DescriptorSetTemplate GetPushDescriptorTemplate(PipelineBindPoint pbp, long updateMask) + { + if (_lastPdUsage == updateMask && _lastPdTemplate != null) + { + // Most likely result is that it asks to update the same buffers. + return _lastPdTemplate; + } + + if (!_pdTemplates.TryGetValue(updateMask, out DescriptorSetTemplate template)) + { + template = new DescriptorSetTemplate(_gd, _device, _pdDescriptors, updateMask, this, pbp, 0); + + _pdTemplates.Add(updateMask, template); + } + + _lastPdUsage = updateMask; + _lastPdTemplate = template; + + return template; + } + protected virtual unsafe void Dispose(bool disposing) { if (disposing) { + if (_pdTemplates != null) + { + foreach (DescriptorSetTemplate template in _pdTemplates.Values) + { + template.Dispose(); + } + } + for (int i = 0; i < _dsCache.Length; i++) { for (int j = 0; j < _dsCache[i].Length; j++) @@ -160,12 +344,34 @@ namespace Ryujinx.Graphics.Vulkan } } + for (int i = 0; i < _manualDsCache.Length; i++) + { + if (_manualDsCache[i] == null) + { + continue; + } + + for (int j = 0; j < _manualDsCache[i].Count; j++) + { + _manualDsCache[i][j].DescriptorSet.Dispose(); + } + + _manualDsCache[i].Clear(); + } + _gd.Api.DestroyPipelineLayout(_device, PipelineLayout, null); for (int i = 0; i < DescriptorSetLayouts.Length; i++) { _gd.Api.DestroyDescriptorSetLayout(_device, DescriptorSetLayouts[i], null); } + + while (_pendingManualDsConsumptions.TryDequeue(out var pds)) + { + pds.Fence.Put(); + } + + _descriptorSetManager.Dispose(); } } diff --git a/src/Ryujinx.Graphics.Vulkan/PipelineLayoutFactory.cs b/src/Ryujinx.Graphics.Vulkan/PipelineLayoutFactory.cs index 8bf286c65..8d7815616 100644 --- a/src/Ryujinx.Graphics.Vulkan/PipelineLayoutFactory.cs +++ b/src/Ryujinx.Graphics.Vulkan/PipelineLayoutFactory.cs @@ -1,18 +1,23 @@ +using Ryujinx.Common.Memory; using Ryujinx.Graphics.GAL; using Silk.NET.Vulkan; +using System; using System.Collections.ObjectModel; namespace Ryujinx.Graphics.Vulkan { + record struct ResourceLayouts(DescriptorSetLayout[] DescriptorSetLayouts, bool[] DescriptorSetLayoutsUpdateAfterBind, PipelineLayout PipelineLayout); + static class PipelineLayoutFactory { - public static unsafe (DescriptorSetLayout[], PipelineLayout) Create( + public static unsafe ResourceLayouts Create( VulkanRenderer gd, Device device, ReadOnlyCollection setDescriptors, bool usePushDescriptors) { DescriptorSetLayout[] layouts = new DescriptorSetLayout[setDescriptors.Count]; + bool[] updateAfterBindFlags = new bool[setDescriptors.Count]; bool isMoltenVk = gd.IsMoltenVk; @@ -32,10 +37,11 @@ namespace Ryujinx.Graphics.Vulkan DescriptorSetLayoutBinding[] layoutBindings = new DescriptorSetLayoutBinding[rdc.Descriptors.Count]; + bool hasArray = false; + for (int descIndex = 0; descIndex < rdc.Descriptors.Count; descIndex++) { ResourceDescriptor descriptor = rdc.Descriptors[descIndex]; - ResourceStages stages = descriptor.Stages; if (descriptor.Type == ResourceType.StorageBuffer && isMoltenVk) @@ -52,19 +58,40 @@ namespace Ryujinx.Graphics.Vulkan DescriptorCount = (uint)descriptor.Count, StageFlags = stages.Convert(), }; + + if (descriptor.Count > 1) + { + hasArray = true; + } } fixed (DescriptorSetLayoutBinding* pLayoutBindings = layoutBindings) { + DescriptorSetLayoutCreateFlags flags = DescriptorSetLayoutCreateFlags.None; + + if (usePushDescriptors && setIndex == 0) + { + flags = DescriptorSetLayoutCreateFlags.PushDescriptorBitKhr; + } + + if (gd.Vendor == Vendor.Intel && hasArray) + { + // Some vendors (like Intel) have low per-stage limits. + // We must set the flag if we exceed those limits. + flags |= DescriptorSetLayoutCreateFlags.UpdateAfterBindPoolBit; + + updateAfterBindFlags[setIndex] = true; + } + var descriptorSetLayoutCreateInfo = new DescriptorSetLayoutCreateInfo { SType = StructureType.DescriptorSetLayoutCreateInfo, PBindings = pLayoutBindings, BindingCount = (uint)layoutBindings.Length, - Flags = usePushDescriptors && setIndex == 0 ? DescriptorSetLayoutCreateFlags.PushDescriptorBitKhr : DescriptorSetLayoutCreateFlags.None, + Flags = flags, }; - gd.Api.CreateDescriptorSetLayout(device, descriptorSetLayoutCreateInfo, null, out layouts[setIndex]).ThrowOnError(); + gd.Api.CreateDescriptorSetLayout(device, in descriptorSetLayoutCreateInfo, null, out layouts[setIndex]).ThrowOnError(); } } @@ -82,7 +109,7 @@ namespace Ryujinx.Graphics.Vulkan gd.Api.CreatePipelineLayout(device, &pipelineLayoutCreateInfo, null, out layout).ThrowOnError(); } - return (layouts, layout); + return new ResourceLayouts(layouts, updateAfterBindFlags, layout); } } } diff --git a/src/Ryujinx.Graphics.Vulkan/PipelineState.cs b/src/Ryujinx.Graphics.Vulkan/PipelineState.cs index 11f532510..a726b9edb 100644 --- a/src/Ryujinx.Graphics.Vulkan/PipelineState.cs +++ b/src/Ryujinx.Graphics.Vulkan/PipelineState.cs @@ -8,6 +8,7 @@ namespace Ryujinx.Graphics.Vulkan struct PipelineState : IDisposable { private const int RequiredSubgroupSize = 32; + private const int MaxDynamicStatesCount = 9; public PipelineUid Internal; @@ -71,248 +72,242 @@ namespace Ryujinx.Graphics.Vulkan set => Internal.Id4 = (Internal.Id4 & 0xFFFFFFFF) | ((ulong)value << 32); } - public float MinDepthBounds - { - readonly get => BitConverter.Int32BitsToSingle((int)((Internal.Id5 >> 0) & 0xFFFFFFFF)); - set => Internal.Id5 = (Internal.Id5 & 0xFFFFFFFF00000000) | ((ulong)(uint)BitConverter.SingleToInt32Bits(value) << 0); - } - - public float MaxDepthBounds - { - readonly get => BitConverter.Int32BitsToSingle((int)((Internal.Id5 >> 32) & 0xFFFFFFFF)); - set => Internal.Id5 = (Internal.Id5 & 0xFFFFFFFF) | ((ulong)(uint)BitConverter.SingleToInt32Bits(value) << 32); - } - public PolygonMode PolygonMode { - readonly get => (PolygonMode)((Internal.Id6 >> 0) & 0x3FFFFFFF); - set => Internal.Id6 = (Internal.Id6 & 0xFFFFFFFFC0000000) | ((ulong)value << 0); + readonly get => (PolygonMode)((Internal.Id5 >> 0) & 0x3FFFFFFF); + set => Internal.Id5 = (Internal.Id5 & 0xFFFFFFFFC0000000) | ((ulong)value << 0); } public uint StagesCount { - readonly get => (byte)((Internal.Id6 >> 30) & 0xFF); - set => Internal.Id6 = (Internal.Id6 & 0xFFFFFFC03FFFFFFF) | ((ulong)value << 30); + readonly get => (byte)((Internal.Id5 >> 30) & 0xFF); + set => Internal.Id5 = (Internal.Id5 & 0xFFFFFFC03FFFFFFF) | ((ulong)value << 30); } public uint VertexAttributeDescriptionsCount { - readonly get => (byte)((Internal.Id6 >> 38) & 0xFF); - set => Internal.Id6 = (Internal.Id6 & 0xFFFFC03FFFFFFFFF) | ((ulong)value << 38); + readonly get => (byte)((Internal.Id5 >> 38) & 0xFF); + set => Internal.Id5 = (Internal.Id5 & 0xFFFFC03FFFFFFFFF) | ((ulong)value << 38); } public uint VertexBindingDescriptionsCount { - readonly get => (byte)((Internal.Id6 >> 46) & 0xFF); - set => Internal.Id6 = (Internal.Id6 & 0xFFC03FFFFFFFFFFF) | ((ulong)value << 46); + readonly get => (byte)((Internal.Id5 >> 46) & 0xFF); + set => Internal.Id5 = (Internal.Id5 & 0xFFC03FFFFFFFFFFF) | ((ulong)value << 46); } public uint ViewportsCount { - readonly get => (byte)((Internal.Id6 >> 54) & 0xFF); - set => Internal.Id6 = (Internal.Id6 & 0xC03FFFFFFFFFFFFF) | ((ulong)value << 54); + readonly get => (byte)((Internal.Id5 >> 54) & 0xFF); + set => Internal.Id5 = (Internal.Id5 & 0xC03FFFFFFFFFFFFF) | ((ulong)value << 54); } public uint ScissorsCount { - readonly get => (byte)((Internal.Id7 >> 0) & 0xFF); - set => Internal.Id7 = (Internal.Id7 & 0xFFFFFFFFFFFFFF00) | ((ulong)value << 0); + readonly get => (byte)((Internal.Id6 >> 0) & 0xFF); + set => Internal.Id6 = (Internal.Id6 & 0xFFFFFFFFFFFFFF00) | ((ulong)value << 0); } public uint ColorBlendAttachmentStateCount { - readonly get => (byte)((Internal.Id7 >> 8) & 0xFF); - set => Internal.Id7 = (Internal.Id7 & 0xFFFFFFFFFFFF00FF) | ((ulong)value << 8); + readonly get => (byte)((Internal.Id6 >> 8) & 0xFF); + set => Internal.Id6 = (Internal.Id6 & 0xFFFFFFFFFFFF00FF) | ((ulong)value << 8); } public PrimitiveTopology Topology { - readonly get => (PrimitiveTopology)((Internal.Id7 >> 16) & 0xF); - set => Internal.Id7 = (Internal.Id7 & 0xFFFFFFFFFFF0FFFF) | ((ulong)value << 16); + readonly get => (PrimitiveTopology)((Internal.Id6 >> 16) & 0xF); + set => Internal.Id6 = (Internal.Id6 & 0xFFFFFFFFFFF0FFFF) | ((ulong)value << 16); } public LogicOp LogicOp { - readonly get => (LogicOp)((Internal.Id7 >> 20) & 0xF); - set => Internal.Id7 = (Internal.Id7 & 0xFFFFFFFFFF0FFFFF) | ((ulong)value << 20); + readonly get => (LogicOp)((Internal.Id6 >> 20) & 0xF); + set => Internal.Id6 = (Internal.Id6 & 0xFFFFFFFFFF0FFFFF) | ((ulong)value << 20); } public CompareOp DepthCompareOp { - readonly get => (CompareOp)((Internal.Id7 >> 24) & 0x7); - set => Internal.Id7 = (Internal.Id7 & 0xFFFFFFFFF8FFFFFF) | ((ulong)value << 24); + readonly get => (CompareOp)((Internal.Id6 >> 24) & 0x7); + set => Internal.Id6 = (Internal.Id6 & 0xFFFFFFFFF8FFFFFF) | ((ulong)value << 24); } public StencilOp StencilFrontFailOp { - readonly get => (StencilOp)((Internal.Id7 >> 27) & 0x7); - set => Internal.Id7 = (Internal.Id7 & 0xFFFFFFFFC7FFFFFF) | ((ulong)value << 27); + readonly get => (StencilOp)((Internal.Id6 >> 27) & 0x7); + set => Internal.Id6 = (Internal.Id6 & 0xFFFFFFFFC7FFFFFF) | ((ulong)value << 27); } public StencilOp StencilFrontPassOp { - readonly get => (StencilOp)((Internal.Id7 >> 30) & 0x7); - set => Internal.Id7 = (Internal.Id7 & 0xFFFFFFFE3FFFFFFF) | ((ulong)value << 30); + readonly get => (StencilOp)((Internal.Id6 >> 30) & 0x7); + set => Internal.Id6 = (Internal.Id6 & 0xFFFFFFFE3FFFFFFF) | ((ulong)value << 30); } public StencilOp StencilFrontDepthFailOp { - readonly get => (StencilOp)((Internal.Id7 >> 33) & 0x7); - set => Internal.Id7 = (Internal.Id7 & 0xFFFFFFF1FFFFFFFF) | ((ulong)value << 33); + readonly get => (StencilOp)((Internal.Id6 >> 33) & 0x7); + set => Internal.Id6 = (Internal.Id6 & 0xFFFFFFF1FFFFFFFF) | ((ulong)value << 33); } public CompareOp StencilFrontCompareOp { - readonly get => (CompareOp)((Internal.Id7 >> 36) & 0x7); - set => Internal.Id7 = (Internal.Id7 & 0xFFFFFF8FFFFFFFFF) | ((ulong)value << 36); + readonly get => (CompareOp)((Internal.Id6 >> 36) & 0x7); + set => Internal.Id6 = (Internal.Id6 & 0xFFFFFF8FFFFFFFFF) | ((ulong)value << 36); } public StencilOp StencilBackFailOp { - readonly get => (StencilOp)((Internal.Id7 >> 39) & 0x7); - set => Internal.Id7 = (Internal.Id7 & 0xFFFFFC7FFFFFFFFF) | ((ulong)value << 39); + readonly get => (StencilOp)((Internal.Id6 >> 39) & 0x7); + set => Internal.Id6 = (Internal.Id6 & 0xFFFFFC7FFFFFFFFF) | ((ulong)value << 39); } public StencilOp StencilBackPassOp { - readonly get => (StencilOp)((Internal.Id7 >> 42) & 0x7); - set => Internal.Id7 = (Internal.Id7 & 0xFFFFE3FFFFFFFFFF) | ((ulong)value << 42); + readonly get => (StencilOp)((Internal.Id6 >> 42) & 0x7); + set => Internal.Id6 = (Internal.Id6 & 0xFFFFE3FFFFFFFFFF) | ((ulong)value << 42); } public StencilOp StencilBackDepthFailOp { - readonly get => (StencilOp)((Internal.Id7 >> 45) & 0x7); - set => Internal.Id7 = (Internal.Id7 & 0xFFFF1FFFFFFFFFFF) | ((ulong)value << 45); + readonly get => (StencilOp)((Internal.Id6 >> 45) & 0x7); + set => Internal.Id6 = (Internal.Id6 & 0xFFFF1FFFFFFFFFFF) | ((ulong)value << 45); } public CompareOp StencilBackCompareOp { - readonly get => (CompareOp)((Internal.Id7 >> 48) & 0x7); - set => Internal.Id7 = (Internal.Id7 & 0xFFF8FFFFFFFFFFFF) | ((ulong)value << 48); + readonly get => (CompareOp)((Internal.Id6 >> 48) & 0x7); + set => Internal.Id6 = (Internal.Id6 & 0xFFF8FFFFFFFFFFFF) | ((ulong)value << 48); } public CullModeFlags CullMode { - readonly get => (CullModeFlags)((Internal.Id7 >> 51) & 0x3); - set => Internal.Id7 = (Internal.Id7 & 0xFFE7FFFFFFFFFFFF) | ((ulong)value << 51); + readonly get => (CullModeFlags)((Internal.Id6 >> 51) & 0x3); + set => Internal.Id6 = (Internal.Id6 & 0xFFE7FFFFFFFFFFFF) | ((ulong)value << 51); } public bool PrimitiveRestartEnable { - readonly get => ((Internal.Id7 >> 53) & 0x1) != 0UL; - set => Internal.Id7 = (Internal.Id7 & 0xFFDFFFFFFFFFFFFF) | ((value ? 1UL : 0UL) << 53); + readonly get => ((Internal.Id6 >> 53) & 0x1) != 0UL; + set => Internal.Id6 = (Internal.Id6 & 0xFFDFFFFFFFFFFFFF) | ((value ? 1UL : 0UL) << 53); } public bool DepthClampEnable { - readonly get => ((Internal.Id7 >> 54) & 0x1) != 0UL; - set => Internal.Id7 = (Internal.Id7 & 0xFFBFFFFFFFFFFFFF) | ((value ? 1UL : 0UL) << 54); + readonly get => ((Internal.Id6 >> 54) & 0x1) != 0UL; + set => Internal.Id6 = (Internal.Id6 & 0xFFBFFFFFFFFFFFFF) | ((value ? 1UL : 0UL) << 54); } public bool RasterizerDiscardEnable { - readonly get => ((Internal.Id7 >> 55) & 0x1) != 0UL; - set => Internal.Id7 = (Internal.Id7 & 0xFF7FFFFFFFFFFFFF) | ((value ? 1UL : 0UL) << 55); + readonly get => ((Internal.Id6 >> 55) & 0x1) != 0UL; + set => Internal.Id6 = (Internal.Id6 & 0xFF7FFFFFFFFFFFFF) | ((value ? 1UL : 0UL) << 55); } public FrontFace FrontFace { - readonly get => (FrontFace)((Internal.Id7 >> 56) & 0x1); - set => Internal.Id7 = (Internal.Id7 & 0xFEFFFFFFFFFFFFFF) | ((ulong)value << 56); + readonly get => (FrontFace)((Internal.Id6 >> 56) & 0x1); + set => Internal.Id6 = (Internal.Id6 & 0xFEFFFFFFFFFFFFFF) | ((ulong)value << 56); } public bool DepthBiasEnable { - readonly get => ((Internal.Id7 >> 57) & 0x1) != 0UL; - set => Internal.Id7 = (Internal.Id7 & 0xFDFFFFFFFFFFFFFF) | ((value ? 1UL : 0UL) << 57); + readonly get => ((Internal.Id6 >> 57) & 0x1) != 0UL; + set => Internal.Id6 = (Internal.Id6 & 0xFDFFFFFFFFFFFFFF) | ((value ? 1UL : 0UL) << 57); } public bool DepthTestEnable { - readonly get => ((Internal.Id7 >> 58) & 0x1) != 0UL; - set => Internal.Id7 = (Internal.Id7 & 0xFBFFFFFFFFFFFFFF) | ((value ? 1UL : 0UL) << 58); + readonly get => ((Internal.Id6 >> 58) & 0x1) != 0UL; + set => Internal.Id6 = (Internal.Id6 & 0xFBFFFFFFFFFFFFFF) | ((value ? 1UL : 0UL) << 58); } public bool DepthWriteEnable { - readonly get => ((Internal.Id7 >> 59) & 0x1) != 0UL; - set => Internal.Id7 = (Internal.Id7 & 0xF7FFFFFFFFFFFFFF) | ((value ? 1UL : 0UL) << 59); + readonly get => ((Internal.Id6 >> 59) & 0x1) != 0UL; + set => Internal.Id6 = (Internal.Id6 & 0xF7FFFFFFFFFFFFFF) | ((value ? 1UL : 0UL) << 59); } public bool DepthBoundsTestEnable { - readonly get => ((Internal.Id7 >> 60) & 0x1) != 0UL; - set => Internal.Id7 = (Internal.Id7 & 0xEFFFFFFFFFFFFFFF) | ((value ? 1UL : 0UL) << 60); + readonly get => ((Internal.Id6 >> 60) & 0x1) != 0UL; + set => Internal.Id6 = (Internal.Id6 & 0xEFFFFFFFFFFFFFFF) | ((value ? 1UL : 0UL) << 60); } public bool StencilTestEnable { - readonly get => ((Internal.Id7 >> 61) & 0x1) != 0UL; - set => Internal.Id7 = (Internal.Id7 & 0xDFFFFFFFFFFFFFFF) | ((value ? 1UL : 0UL) << 61); + readonly get => ((Internal.Id6 >> 61) & 0x1) != 0UL; + set => Internal.Id6 = (Internal.Id6 & 0xDFFFFFFFFFFFFFFF) | ((value ? 1UL : 0UL) << 61); } public bool LogicOpEnable { - readonly get => ((Internal.Id7 >> 62) & 0x1) != 0UL; - set => Internal.Id7 = (Internal.Id7 & 0xBFFFFFFFFFFFFFFF) | ((value ? 1UL : 0UL) << 62); + readonly get => ((Internal.Id6 >> 62) & 0x1) != 0UL; + set => Internal.Id6 = (Internal.Id6 & 0xBFFFFFFFFFFFFFFF) | ((value ? 1UL : 0UL) << 62); } public bool HasDepthStencil { - readonly get => ((Internal.Id7 >> 63) & 0x1) != 0UL; - set => Internal.Id7 = (Internal.Id7 & 0x7FFFFFFFFFFFFFFF) | ((value ? 1UL : 0UL) << 63); + readonly get => ((Internal.Id6 >> 63) & 0x1) != 0UL; + set => Internal.Id6 = (Internal.Id6 & 0x7FFFFFFFFFFFFFFF) | ((value ? 1UL : 0UL) << 63); } public uint PatchControlPoints { - readonly get => (uint)((Internal.Id8 >> 0) & 0xFFFFFFFF); - set => Internal.Id8 = (Internal.Id8 & 0xFFFFFFFF00000000) | ((ulong)value << 0); + readonly get => (uint)((Internal.Id7 >> 0) & 0xFFFFFFFF); + set => Internal.Id7 = (Internal.Id7 & 0xFFFFFFFF00000000) | ((ulong)value << 0); } public uint SamplesCount { - readonly get => (uint)((Internal.Id8 >> 32) & 0xFFFFFFFF); - set => Internal.Id8 = (Internal.Id8 & 0xFFFFFFFF) | ((ulong)value << 32); + readonly get => (uint)((Internal.Id7 >> 32) & 0xFFFFFFFF); + set => Internal.Id7 = (Internal.Id7 & 0xFFFFFFFF) | ((ulong)value << 32); } public bool AlphaToCoverageEnable { - readonly get => ((Internal.Id9 >> 0) & 0x1) != 0UL; - set => Internal.Id9 = (Internal.Id9 & 0xFFFFFFFFFFFFFFFE) | ((value ? 1UL : 0UL) << 0); + readonly get => ((Internal.Id8 >> 0) & 0x1) != 0UL; + set => Internal.Id8 = (Internal.Id8 & 0xFFFFFFFFFFFFFFFE) | ((value ? 1UL : 0UL) << 0); } public bool AlphaToOneEnable { - readonly get => ((Internal.Id9 >> 1) & 0x1) != 0UL; - set => Internal.Id9 = (Internal.Id9 & 0xFFFFFFFFFFFFFFFD) | ((value ? 1UL : 0UL) << 1); + readonly get => ((Internal.Id8 >> 1) & 0x1) != 0UL; + set => Internal.Id8 = (Internal.Id8 & 0xFFFFFFFFFFFFFFFD) | ((value ? 1UL : 0UL) << 1); } public bool AdvancedBlendSrcPreMultiplied { - readonly get => ((Internal.Id9 >> 2) & 0x1) != 0UL; - set => Internal.Id9 = (Internal.Id9 & 0xFFFFFFFFFFFFFFFB) | ((value ? 1UL : 0UL) << 2); + readonly get => ((Internal.Id8 >> 2) & 0x1) != 0UL; + set => Internal.Id8 = (Internal.Id8 & 0xFFFFFFFFFFFFFFFB) | ((value ? 1UL : 0UL) << 2); } public bool AdvancedBlendDstPreMultiplied { - readonly get => ((Internal.Id9 >> 3) & 0x1) != 0UL; - set => Internal.Id9 = (Internal.Id9 & 0xFFFFFFFFFFFFFFF7) | ((value ? 1UL : 0UL) << 3); + readonly get => ((Internal.Id8 >> 3) & 0x1) != 0UL; + set => Internal.Id8 = (Internal.Id8 & 0xFFFFFFFFFFFFFFF7) | ((value ? 1UL : 0UL) << 3); } public BlendOverlapEXT AdvancedBlendOverlap { - readonly get => (BlendOverlapEXT)((Internal.Id9 >> 4) & 0x3); - set => Internal.Id9 = (Internal.Id9 & 0xFFFFFFFFFFFFFFCF) | ((ulong)value << 4); + readonly get => (BlendOverlapEXT)((Internal.Id8 >> 4) & 0x3); + set => Internal.Id8 = (Internal.Id8 & 0xFFFFFFFFFFFFFFCF) | ((ulong)value << 4); } public bool DepthMode { - readonly get => ((Internal.Id9 >> 6) & 0x1) != 0UL; - set => Internal.Id9 = (Internal.Id9 & 0xFFFFFFFFFFFFFFBF) | ((value ? 1UL : 0UL) << 6); + readonly get => ((Internal.Id8 >> 6) & 0x1) != 0UL; + set => Internal.Id8 = (Internal.Id8 & 0xFFFFFFFFFFFFFFBF) | ((value ? 1UL : 0UL) << 6); } + public FeedbackLoopAspects FeedbackLoopAspects + { + readonly get => (FeedbackLoopAspects)((Internal.Id8 >> 7) & 0x3); + set => Internal.Id8 = (Internal.Id8 & 0xFFFFFFFFFFFFFE7F) | (((ulong)value) << 7); + } + + public bool HasTessellationControlShader; public NativeArray Stages; - public NativeArray StageRequiredSubgroupSizes; public PipelineLayout PipelineLayout; public SpecData SpecializationData; @@ -320,17 +315,8 @@ namespace Ryujinx.Graphics.Vulkan public void Initialize() { + HasTessellationControlShader = false; Stages = new NativeArray(Constants.MaxShaderStages); - StageRequiredSubgroupSizes = new NativeArray(Constants.MaxShaderStages); - - for (int index = 0; index < Constants.MaxShaderStages; index++) - { - StageRequiredSubgroupSizes[index] = new PipelineShaderStageRequiredSubgroupSizeCreateInfoEXT - { - SType = StructureType.PipelineShaderStageRequiredSubgroupSizeCreateInfoExt, - RequiredSubgroupSize = RequiredSubgroupSize, - }; - } AdvancedBlendSrcPreMultiplied = true; AdvancedBlendDstPreMultiplied = true; @@ -397,7 +383,8 @@ namespace Ryujinx.Graphics.Vulkan Device device, ShaderCollection program, PipelineCache cache, - RenderPass renderPass) + RenderPass renderPass, + bool throwOnError = false) { if (program.TryGetGraphicsPipeline(ref Internal, out var pipeline)) { @@ -416,8 +403,6 @@ namespace Ryujinx.Graphics.Vulkan fixed (VertexInputAttributeDescription* pVertexAttributeDescriptions = &Internal.VertexAttributeDescriptions[0]) fixed (VertexInputAttributeDescription* pVertexAttributeDescriptions2 = &_vertexAttributeDescriptions2[0]) fixed (VertexInputBindingDescription* pVertexBindingDescriptions = &Internal.VertexBindingDescriptions[0]) - fixed (Viewport* pViewports = &Internal.Viewports[0]) - fixed (Rect2D* pScissors = &Internal.Scissors[0]) fixed (PipelineColorBlendAttachmentState* pColorBlendAttachmentState = &Internal.ColorBlendAttachmentState[0]) { var vertexInputState = new PipelineVertexInputStateCreateInfo @@ -429,6 +414,15 @@ namespace Ryujinx.Graphics.Vulkan PVertexBindingDescriptions = pVertexBindingDescriptions, }; + // Using patches topology without a tessellation shader is invalid. + // If we find such a case, return null pipeline to skip the draw. + if (Topology == PrimitiveTopology.PatchList && !HasTessellationControlShader) + { + program.AddGraphicsPipeline(ref Internal, null); + + return null; + } + bool primitiveRestartEnable = PrimitiveRestartEnable; bool topologySupportsRestart; @@ -452,7 +446,7 @@ namespace Ryujinx.Graphics.Vulkan { SType = StructureType.PipelineInputAssemblyStateCreateInfo, PrimitiveRestartEnable = primitiveRestartEnable, - Topology = Topology, + Topology = HasTessellationControlShader ? PrimitiveTopology.PatchList : Topology, }; var tessellationState = new PipelineTessellationStateCreateInfo @@ -471,18 +465,13 @@ namespace Ryujinx.Graphics.Vulkan CullMode = CullMode, FrontFace = FrontFace, DepthBiasEnable = DepthBiasEnable, - DepthBiasClamp = DepthBiasClamp, - DepthBiasConstantFactor = DepthBiasConstantFactor, - DepthBiasSlopeFactor = DepthBiasSlopeFactor, }; var viewportState = new PipelineViewportStateCreateInfo { SType = StructureType.PipelineViewportStateCreateInfo, ViewportCount = ViewportsCount, - PViewports = pViewports, ScissorCount = ScissorsCount, - PScissors = pScissors, }; if (gd.Capabilities.SupportsDepthClipControl) @@ -510,19 +499,13 @@ namespace Ryujinx.Graphics.Vulkan StencilFrontFailOp, StencilFrontPassOp, StencilFrontDepthFailOp, - StencilFrontCompareOp, - StencilFrontCompareMask, - StencilFrontWriteMask, - StencilFrontReference); + StencilFrontCompareOp); var stencilBack = new StencilOpState( StencilBackFailOp, StencilBackPassOp, StencilBackDepthFailOp, - StencilBackCompareOp, - StencilBackCompareMask, - StencilBackWriteMask, - StencilBackReference); + StencilBackCompareOp); var depthStencilState = new PipelineDepthStencilStateCreateInfo { @@ -530,12 +513,10 @@ namespace Ryujinx.Graphics.Vulkan DepthTestEnable = DepthTestEnable, DepthWriteEnable = DepthWriteEnable, DepthCompareOp = DepthCompareOp, - DepthBoundsTestEnable = DepthBoundsTestEnable, + DepthBoundsTestEnable = false, StencilTestEnable = StencilTestEnable, Front = stencilFront, Back = stencilBack, - MinDepthBounds = MinDepthBounds, - MaxDepthBounds = MaxDepthBounds, }; uint blendEnables = 0; @@ -559,10 +540,14 @@ namespace Ryujinx.Graphics.Vulkan } } + // Vendors other than NVIDIA have a bug where it enables logical operations even for float formats, + // so we need to force disable them here. + bool logicOpEnable = LogicOpEnable && (gd.Vendor == Vendor.Nvidia || Internal.LogicOpsAllowed); + var colorBlendState = new PipelineColorBlendStateCreateInfo { SType = StructureType.PipelineColorBlendStateCreateInfo, - LogicOpEnable = LogicOpEnable, + LogicOpEnable = logicOpEnable, LogicOp = LogicOp, AttachmentCount = ColorBlendAttachmentStateCount, PAttachments = pColorBlendAttachmentState, @@ -586,22 +571,28 @@ namespace Ryujinx.Graphics.Vulkan } bool supportsExtDynamicState = gd.Capabilities.SupportsExtendedDynamicState; - int dynamicStatesCount = supportsExtDynamicState ? 9 : 8; + bool supportsFeedbackLoopDynamicState = gd.Capabilities.SupportsDynamicAttachmentFeedbackLoop; - DynamicState* dynamicStates = stackalloc DynamicState[dynamicStatesCount]; + DynamicState* dynamicStates = stackalloc DynamicState[MaxDynamicStatesCount]; + + int dynamicStatesCount = 7; dynamicStates[0] = DynamicState.Viewport; dynamicStates[1] = DynamicState.Scissor; dynamicStates[2] = DynamicState.DepthBias; - dynamicStates[3] = DynamicState.DepthBounds; - dynamicStates[4] = DynamicState.StencilCompareMask; - dynamicStates[5] = DynamicState.StencilWriteMask; - dynamicStates[6] = DynamicState.StencilReference; - dynamicStates[7] = DynamicState.BlendConstants; + dynamicStates[3] = DynamicState.StencilCompareMask; + dynamicStates[4] = DynamicState.StencilWriteMask; + dynamicStates[5] = DynamicState.StencilReference; + dynamicStates[6] = DynamicState.BlendConstants; if (supportsExtDynamicState) { - dynamicStates[8] = DynamicState.VertexInputBindingStrideExt; + dynamicStates[dynamicStatesCount++] = DynamicState.VertexInputBindingStrideExt; + } + + if (supportsFeedbackLoopDynamicState) + { + dynamicStates[dynamicStatesCount++] = DynamicState.AttachmentFeedbackLoopEnableExt; } var pipelineDynamicStateCreateInfo = new PipelineDynamicStateCreateInfo @@ -611,9 +602,27 @@ namespace Ryujinx.Graphics.Vulkan PDynamicStates = dynamicStates, }; + PipelineCreateFlags flags = 0; + + if (gd.Capabilities.SupportsAttachmentFeedbackLoop) + { + FeedbackLoopAspects aspects = FeedbackLoopAspects; + + if ((aspects & FeedbackLoopAspects.Color) != 0) + { + flags |= PipelineCreateFlags.CreateColorAttachmentFeedbackLoopBitExt; + } + + if ((aspects & FeedbackLoopAspects.Depth) != 0) + { + flags |= PipelineCreateFlags.CreateDepthStencilAttachmentFeedbackLoopBitExt; + } + } + var pipelineCreateInfo = new GraphicsPipelineCreateInfo { SType = StructureType.GraphicsPipelineCreateInfo, + Flags = flags, StageCount = StagesCount, PStages = Stages.Pointer, PVertexInputState = &vertexInputState, @@ -627,10 +636,20 @@ namespace Ryujinx.Graphics.Vulkan PDynamicState = &pipelineDynamicStateCreateInfo, Layout = PipelineLayout, RenderPass = renderPass, - BasePipelineIndex = -1, }; - gd.Api.CreateGraphicsPipelines(device, cache, 1, &pipelineCreateInfo, null, &pipelineHandle).ThrowOnError(); + Result result = gd.Api.CreateGraphicsPipelines(device, cache, 1, &pipelineCreateInfo, null, &pipelineHandle); + + if (throwOnError) + { + result.ThrowOnError(); + } + else if (result.IsError()) + { + program.AddGraphicsPipeline(ref Internal, null); + + return null; + } // Restore previous blend enable values if we changed it. while (blendEnables != 0) @@ -708,7 +727,6 @@ namespace Ryujinx.Graphics.Vulkan public readonly void Dispose() { Stages.Dispose(); - StageRequiredSubgroupSizes.Dispose(); } } } diff --git a/src/Ryujinx.Graphics.Vulkan/PipelineUid.cs b/src/Ryujinx.Graphics.Vulkan/PipelineUid.cs index 3448d9743..c56224216 100644 --- a/src/Ryujinx.Graphics.Vulkan/PipelineUid.cs +++ b/src/Ryujinx.Graphics.Vulkan/PipelineUid.cs @@ -17,23 +17,21 @@ namespace Ryujinx.Graphics.Vulkan public ulong Id4; public ulong Id5; public ulong Id6; + public ulong Id7; - public ulong Id8; - public ulong Id9; - private readonly uint VertexAttributeDescriptionsCount => (byte)((Id6 >> 38) & 0xFF); - private readonly uint VertexBindingDescriptionsCount => (byte)((Id6 >> 46) & 0xFF); - private readonly uint ColorBlendAttachmentStateCount => (byte)((Id7 >> 8) & 0xFF); - private readonly bool HasDepthStencil => ((Id7 >> 63) & 0x1) != 0UL; + private readonly uint VertexAttributeDescriptionsCount => (byte)((Id5 >> 38) & 0xFF); + private readonly uint VertexBindingDescriptionsCount => (byte)((Id5 >> 46) & 0xFF); + private readonly uint ColorBlendAttachmentStateCount => (byte)((Id6 >> 8) & 0xFF); + private readonly bool HasDepthStencil => ((Id6 >> 63) & 0x1) != 0UL; public Array32 VertexAttributeDescriptions; public Array33 VertexBindingDescriptions; - public Array16 Viewports; - public Array16 Scissors; public Array8 ColorBlendAttachmentState; public Array9 AttachmentFormats; public uint AttachmentIntegerFormatMask; + public bool LogicOpsAllowed; public readonly override bool Equals(object obj) { @@ -44,7 +42,7 @@ namespace Ryujinx.Graphics.Vulkan { if (!Unsafe.As>(ref Id0).Equals(Unsafe.As>(ref other.Id0)) || !Unsafe.As>(ref Id4).Equals(Unsafe.As>(ref other.Id4)) || - !Unsafe.As>(ref Id8).Equals(Unsafe.As>(ref other.Id8))) + !Unsafe.As>(ref Id7).Equals(Unsafe.As>(ref other.Id7))) { return false; } @@ -87,8 +85,7 @@ namespace Ryujinx.Graphics.Vulkan Id5 * 23 ^ Id6 * 23 ^ Id7 * 23 ^ - Id8 * 23 ^ - Id9 * 23; + Id8 * 23; for (int i = 0; i < (int)VertexAttributeDescriptionsCount; i++) { diff --git a/src/Ryujinx.Graphics.Vulkan/Queries/BufferedQuery.cs b/src/Ryujinx.Graphics.Vulkan/Queries/BufferedQuery.cs index 3fdc5afa5..5d48a6622 100644 --- a/src/Ryujinx.Graphics.Vulkan/Queries/BufferedQuery.cs +++ b/src/Ryujinx.Graphics.Vulkan/Queries/BufferedQuery.cs @@ -10,8 +10,8 @@ namespace Ryujinx.Graphics.Vulkan.Queries class BufferedQuery : IDisposable { private const int MaxQueryRetries = 5000; - private const long DefaultValue = -1; - private const long DefaultValueInt = 0xFFFFFFFF; + private const long DefaultValue = unchecked((long)0xFFFFFFFEFFFFFFFE); + private const long DefaultValueInt = 0xFFFFFFFE; private const ulong HighMask = 0xFFFFFFFF00000000; private readonly Vk _api; @@ -21,7 +21,7 @@ namespace Ryujinx.Graphics.Vulkan.Queries private QueryPool _queryPool; private readonly BufferHolder _buffer; - private readonly IntPtr _bufferMap; + private readonly nint _bufferMap; private readonly CounterType _type; private readonly bool _result32Bit; private readonly bool _isSupported; @@ -52,7 +52,7 @@ namespace Ryujinx.Graphics.Vulkan.Queries PipelineStatistics = flags, }; - gd.Api.CreateQueryPool(device, queryPoolCreateInfo, null, out _queryPool).ThrowOnError(); + gd.Api.CreateQueryPool(device, in queryPoolCreateInfo, null, out _queryPool).ThrowOnError(); } var buffer = gd.BufferManager.Create(gd, sizeof(long), forConditionalRendering: true); diff --git a/src/Ryujinx.Graphics.Vulkan/Queries/CounterQueue.cs b/src/Ryujinx.Graphics.Vulkan/Queries/CounterQueue.cs index 3984e2826..0d133e50e 100644 --- a/src/Ryujinx.Graphics.Vulkan/Queries/CounterQueue.cs +++ b/src/Ryujinx.Graphics.Vulkan/Queries/CounterQueue.cs @@ -67,9 +67,18 @@ namespace Ryujinx.Graphics.Vulkan.Queries lock (_queryPool) { count = Math.Min(count, _queryPool.Count); - for (int i = 0; i < count; i++) + + if (count > 0) { - _queryPool.ElementAt(i).PoolReset(cmd, ResetSequence); + foreach (BufferedQuery query in _queryPool) + { + query.PoolReset(cmd, ResetSequence); + + if (--count == 0) + { + break; + } + } } } } diff --git a/src/Ryujinx.Graphics.Vulkan/RenderPassCacheKey.cs b/src/Ryujinx.Graphics.Vulkan/RenderPassCacheKey.cs new file mode 100644 index 000000000..7c57b8feb --- /dev/null +++ b/src/Ryujinx.Graphics.Vulkan/RenderPassCacheKey.cs @@ -0,0 +1,43 @@ +using System; +using System.Linq; + +namespace Ryujinx.Graphics.Vulkan +{ + internal readonly struct RenderPassCacheKey : IRefEquatable + { + private readonly TextureView _depthStencil; + private readonly TextureView[] _colors; + + public RenderPassCacheKey(TextureView depthStencil, TextureView[] colors) + { + _depthStencil = depthStencil; + _colors = colors; + } + + public override int GetHashCode() + { + HashCode hc = new(); + + hc.Add(_depthStencil); + + if (_colors != null) + { + foreach (var color in _colors) + { + hc.Add(color); + } + } + + return hc.ToHashCode(); + } + + public bool Equals(ref RenderPassCacheKey other) + { + bool colorsNull = _colors == null; + bool otherNull = other._colors == null; + return other._depthStencil == _depthStencil && + colorsNull == otherNull && + (colorsNull || other._colors.SequenceEqual(_colors)); + } + } +} diff --git a/src/Ryujinx.Graphics.Vulkan/RenderPassHolder.cs b/src/Ryujinx.Graphics.Vulkan/RenderPassHolder.cs new file mode 100644 index 000000000..a364c5716 --- /dev/null +++ b/src/Ryujinx.Graphics.Vulkan/RenderPassHolder.cs @@ -0,0 +1,221 @@ +using Silk.NET.Vulkan; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Ryujinx.Graphics.Vulkan +{ + internal class RenderPassHolder + { + private readonly struct FramebufferCacheKey : IRefEquatable + { + private readonly uint _width; + private readonly uint _height; + private readonly uint _layers; + + public FramebufferCacheKey(uint width, uint height, uint layers) + { + _width = width; + _height = height; + _layers = layers; + } + + public override int GetHashCode() + { + return HashCode.Combine(_width, _height, _layers); + } + + public bool Equals(ref FramebufferCacheKey other) + { + return other._width == _width && other._height == _height && other._layers == _layers; + } + } + + private readonly record struct ForcedFence(TextureStorage Texture, PipelineStageFlags StageFlags); + + private readonly TextureView[] _textures; + private readonly Auto _renderPass; + private readonly HashTableSlim> _framebuffers; + private readonly RenderPassCacheKey _key; + private readonly List _forcedFences; + + public unsafe RenderPassHolder(VulkanRenderer gd, Device device, RenderPassCacheKey key, FramebufferParams fb) + { + // Create render pass using framebuffer params. + + const int MaxAttachments = Constants.MaxRenderTargets + 1; + + AttachmentDescription[] attachmentDescs = null; + + var subpass = new SubpassDescription + { + PipelineBindPoint = PipelineBindPoint.Graphics, + }; + + AttachmentReference* attachmentReferences = stackalloc AttachmentReference[MaxAttachments]; + + var hasFramebuffer = fb != null; + + if (hasFramebuffer && fb.AttachmentsCount != 0) + { + attachmentDescs = new AttachmentDescription[fb.AttachmentsCount]; + + for (int i = 0; i < fb.AttachmentsCount; i++) + { + attachmentDescs[i] = new AttachmentDescription( + 0, + fb.AttachmentFormats[i], + TextureStorage.ConvertToSampleCountFlags(gd.Capabilities.SupportedSampleCounts, fb.AttachmentSamples[i]), + AttachmentLoadOp.Load, + AttachmentStoreOp.Store, + AttachmentLoadOp.Load, + AttachmentStoreOp.Store, + ImageLayout.General, + ImageLayout.General); + } + + int colorAttachmentsCount = fb.ColorAttachmentsCount; + + if (colorAttachmentsCount > MaxAttachments - 1) + { + colorAttachmentsCount = MaxAttachments - 1; + } + + if (colorAttachmentsCount != 0) + { + int maxAttachmentIndex = fb.MaxColorAttachmentIndex; + subpass.ColorAttachmentCount = (uint)maxAttachmentIndex + 1; + subpass.PColorAttachments = &attachmentReferences[0]; + + // Fill with VK_ATTACHMENT_UNUSED to cover any gaps. + for (int i = 0; i <= maxAttachmentIndex; i++) + { + subpass.PColorAttachments[i] = new AttachmentReference(Vk.AttachmentUnused, ImageLayout.Undefined); + } + + for (int i = 0; i < colorAttachmentsCount; i++) + { + int bindIndex = fb.AttachmentIndices[i]; + + subpass.PColorAttachments[bindIndex] = new AttachmentReference((uint)i, ImageLayout.General); + } + } + + if (fb.HasDepthStencil) + { + uint dsIndex = (uint)fb.AttachmentsCount - 1; + + subpass.PDepthStencilAttachment = &attachmentReferences[MaxAttachments - 1]; + *subpass.PDepthStencilAttachment = new AttachmentReference(dsIndex, ImageLayout.General); + } + } + + var subpassDependency = PipelineConverter.CreateSubpassDependency(gd); + + fixed (AttachmentDescription* pAttachmentDescs = attachmentDescs) + { + var renderPassCreateInfo = new RenderPassCreateInfo + { + SType = StructureType.RenderPassCreateInfo, + PAttachments = pAttachmentDescs, + AttachmentCount = attachmentDescs != null ? (uint)attachmentDescs.Length : 0, + PSubpasses = &subpass, + SubpassCount = 1, + PDependencies = &subpassDependency, + DependencyCount = 1, + }; + + gd.Api.CreateRenderPass(device, in renderPassCreateInfo, null, out var renderPass).ThrowOnError(); + + _renderPass = new Auto(new DisposableRenderPass(gd.Api, device, renderPass)); + } + + _framebuffers = new HashTableSlim>(); + + // Register this render pass with all render target views. + + var textures = fb.GetAttachmentViews(); + + foreach (var texture in textures) + { + texture.AddRenderPass(key, this); + } + + _textures = textures; + _key = key; + + _forcedFences = new List(); + } + + public Auto GetFramebuffer(VulkanRenderer gd, CommandBufferScoped cbs, FramebufferParams fb) + { + var key = new FramebufferCacheKey(fb.Width, fb.Height, fb.Layers); + + if (!_framebuffers.TryGetValue(ref key, out Auto result)) + { + result = fb.Create(gd.Api, cbs, _renderPass); + + _framebuffers.Add(ref key, result); + } + + return result; + } + + public Auto GetRenderPass() + { + return _renderPass; + } + + public void AddForcedFence(TextureStorage storage, PipelineStageFlags stageFlags) + { + if (!_forcedFences.Any(fence => fence.Texture == storage)) + { + _forcedFences.Add(new ForcedFence(storage, stageFlags)); + } + } + + public void InsertForcedFences(CommandBufferScoped cbs) + { + if (_forcedFences.Count > 0) + { + _forcedFences.RemoveAll((entry) => + { + if (entry.Texture.Disposed) + { + return true; + } + + entry.Texture.QueueWriteToReadBarrier(cbs, AccessFlags.ShaderReadBit, entry.StageFlags); + + return false; + }); + } + } + + public bool ContainsAttachment(TextureStorage storage) + { + return _textures.Any(view => view.Storage == storage); + } + + public void Dispose() + { + // Dispose all framebuffers. + + foreach (var fb in _framebuffers.Values) + { + fb.Dispose(); + } + + // Notify all texture views that this render pass has been disposed. + + foreach (var texture in _textures) + { + texture.RemoveRenderPass(_key); + } + + // Dispose render pass. + + _renderPass.Dispose(); + } + } +} diff --git a/src/Ryujinx.Graphics.Vulkan/ResourceArray.cs b/src/Ryujinx.Graphics.Vulkan/ResourceArray.cs new file mode 100644 index 000000000..f96b4a845 --- /dev/null +++ b/src/Ryujinx.Graphics.Vulkan/ResourceArray.cs @@ -0,0 +1,81 @@ +using Silk.NET.Vulkan; +using System; +using System.Diagnostics; + +namespace Ryujinx.Graphics.Vulkan +{ + class ResourceArray : IDisposable + { + private DescriptorSet[] _cachedDescriptorSets; + + private ShaderCollection _cachedDscProgram; + private int _cachedDscSetIndex; + private int _cachedDscIndex; + + private int _bindCount; + + protected void SetDirty(VulkanRenderer gd, bool isImage) + { + ReleaseDescriptorSet(); + + if (_bindCount != 0) + { + if (isImage) + { + gd.PipelineInternal.ForceImageDirty(); + } + else + { + gd.PipelineInternal.ForceTextureDirty(); + } + } + } + + public bool TryGetCachedDescriptorSets(CommandBufferScoped cbs, ShaderCollection program, int setIndex, out DescriptorSet[] sets) + { + if (_cachedDescriptorSets != null) + { + _cachedDscProgram.UpdateManualDescriptorSetCollectionOwnership(cbs, _cachedDscSetIndex, _cachedDscIndex); + + sets = _cachedDescriptorSets; + + return true; + } + + var dsc = program.GetNewManualDescriptorSetCollection(cbs, setIndex, out _cachedDscIndex).Get(cbs); + + sets = dsc.GetSets(); + + _cachedDescriptorSets = sets; + _cachedDscProgram = program; + _cachedDscSetIndex = setIndex; + + return false; + } + + public void IncrementBindCount() + { + _bindCount++; + } + + public void DecrementBindCount() + { + int newBindCount = --_bindCount; + Debug.Assert(newBindCount >= 0); + } + + private void ReleaseDescriptorSet() + { + if (_cachedDescriptorSets != null) + { + _cachedDscProgram.ReleaseManualDescriptorSetCollection(_cachedDscSetIndex, _cachedDscIndex); + _cachedDescriptorSets = null; + } + } + + public void Dispose() + { + ReleaseDescriptorSet(); + } + } +} diff --git a/src/Ryujinx.Graphics.Vulkan/ResourceBindingSegment.cs b/src/Ryujinx.Graphics.Vulkan/ResourceBindingSegment.cs index 8902f13e6..6e27da4a6 100644 --- a/src/Ryujinx.Graphics.Vulkan/ResourceBindingSegment.cs +++ b/src/Ryujinx.Graphics.Vulkan/ResourceBindingSegment.cs @@ -8,13 +8,15 @@ namespace Ryujinx.Graphics.Vulkan public readonly int Count; public readonly ResourceType Type; public readonly ResourceStages Stages; + public readonly bool IsArray; - public ResourceBindingSegment(int binding, int count, ResourceType type, ResourceStages stages) + public ResourceBindingSegment(int binding, int count, ResourceType type, ResourceStages stages, bool isArray) { Binding = binding; Count = count; Type = type; Stages = stages; + IsArray = isArray; } } } diff --git a/src/Ryujinx.Graphics.Vulkan/ResourceLayoutBuilder.cs b/src/Ryujinx.Graphics.Vulkan/ResourceLayoutBuilder.cs index f5ac39684..730a0a2f9 100644 --- a/src/Ryujinx.Graphics.Vulkan/ResourceLayoutBuilder.cs +++ b/src/Ryujinx.Graphics.Vulkan/ResourceLayoutBuilder.cs @@ -23,7 +23,7 @@ namespace Ryujinx.Graphics.Vulkan } } - public ResourceLayoutBuilder Add(ResourceStages stages, ResourceType type, int binding) + public ResourceLayoutBuilder Add(ResourceStages stages, ResourceType type, int binding, bool write = false) { int setIndex = type switch { @@ -35,7 +35,7 @@ namespace Ryujinx.Graphics.Vulkan }; _resourceDescriptors[setIndex].Add(new ResourceDescriptor(binding, 1, type, stages)); - _resourceUsages[setIndex].Add(new ResourceUsage(binding, type, stages)); + _resourceUsages[setIndex].Add(new ResourceUsage(binding, 1, type, stages, write)); return this; } diff --git a/src/Ryujinx.Graphics.Vulkan/Ryujinx.Graphics.Vulkan.csproj b/src/Ryujinx.Graphics.Vulkan/Ryujinx.Graphics.Vulkan.csproj index f6a7be91e..aae28733f 100644 --- a/src/Ryujinx.Graphics.Vulkan/Ryujinx.Graphics.Vulkan.csproj +++ b/src/Ryujinx.Graphics.Vulkan/Ryujinx.Graphics.Vulkan.csproj @@ -15,6 +15,7 @@ + diff --git a/src/Ryujinx.Graphics.Vulkan/SamplerHolder.cs b/src/Ryujinx.Graphics.Vulkan/SamplerHolder.cs index f67daeecc..7f37ab139 100644 --- a/src/Ryujinx.Graphics.Vulkan/SamplerHolder.cs +++ b/src/Ryujinx.Graphics.Vulkan/SamplerHolder.cs @@ -68,7 +68,7 @@ namespace Ryujinx.Graphics.Vulkan samplerCreateInfo.BorderColor = BorderColor.FloatCustomExt; } - gd.Api.CreateSampler(device, samplerCreateInfo, null, out var sampler).ThrowOnError(); + gd.Api.CreateSampler(device, in samplerCreateInfo, null, out var sampler).ThrowOnError(); _sampler = new Auto(new DisposableSampler(gd.Api, device, sampler)); } diff --git a/src/Ryujinx.Graphics.Vulkan/SemaphoreHolder.cs b/src/Ryujinx.Graphics.Vulkan/SemaphoreHolder.cs deleted file mode 100644 index 618a7d488..000000000 --- a/src/Ryujinx.Graphics.Vulkan/SemaphoreHolder.cs +++ /dev/null @@ -1,60 +0,0 @@ -using Silk.NET.Vulkan; -using System; -using System.Threading; -using VkSemaphore = Silk.NET.Vulkan.Semaphore; - -namespace Ryujinx.Graphics.Vulkan -{ - class SemaphoreHolder : IDisposable - { - private readonly Vk _api; - private readonly Device _device; - private VkSemaphore _semaphore; - private int _referenceCount; - private bool _disposed; - - public unsafe SemaphoreHolder(Vk api, Device device) - { - _api = api; - _device = device; - - var semaphoreCreateInfo = new SemaphoreCreateInfo - { - SType = StructureType.SemaphoreCreateInfo, - }; - - api.CreateSemaphore(device, in semaphoreCreateInfo, null, out _semaphore).ThrowOnError(); - - _referenceCount = 1; - } - - public VkSemaphore GetUnsafe() - { - return _semaphore; - } - - public VkSemaphore Get() - { - Interlocked.Increment(ref _referenceCount); - return _semaphore; - } - - public unsafe void Put() - { - if (Interlocked.Decrement(ref _referenceCount) == 0) - { - _api.DestroySemaphore(_device, _semaphore, null); - _semaphore = default; - } - } - - public void Dispose() - { - if (!_disposed) - { - Put(); - _disposed = true; - } - } - } -} diff --git a/src/Ryujinx.Graphics.Vulkan/Shader.cs b/src/Ryujinx.Graphics.Vulkan/Shader.cs index 06f3499db..f23b78f51 100644 --- a/src/Ryujinx.Graphics.Vulkan/Shader.cs +++ b/src/Ryujinx.Graphics.Vulkan/Shader.cs @@ -15,7 +15,7 @@ namespace Ryujinx.Graphics.Vulkan // Take this lock when using them. private static readonly object _shaderOptionsLock = new(); - private static readonly IntPtr _ptrMainEntryPointName = Marshal.StringToHGlobalAnsi("main"); + private static readonly nint _ptrMainEntryPointName = Marshal.StringToHGlobalAnsi("main"); private readonly Vk _api; private readonly Device _device; @@ -64,7 +64,7 @@ namespace Ryujinx.Graphics.Vulkan PCode = (uint*)pCode, }; - api.CreateShaderModule(device, shaderModuleCreateInfo, null, out _module).ThrowOnError(); + api.CreateShaderModule(device, in shaderModuleCreateInfo, null, out _module).ThrowOnError(); } CompileStatus = ProgramLinkStatus.Success; diff --git a/src/Ryujinx.Graphics.Vulkan/ShaderCollection.cs b/src/Ryujinx.Graphics.Vulkan/ShaderCollection.cs index d01eebf3a..c9aab4018 100644 --- a/src/Ryujinx.Graphics.Vulkan/ShaderCollection.cs +++ b/src/Ryujinx.Graphics.Vulkan/ShaderCollection.cs @@ -21,11 +21,18 @@ namespace Ryujinx.Graphics.Vulkan public bool HasMinimalLayout { get; } public bool UsePushDescriptors { get; } public bool IsCompute { get; } + public bool HasTessellationControlShader => (Stages & (1u << 3)) != 0; + + public bool UpdateTexturesWithoutTemplate { get; } public uint Stages { get; } + public PipelineStageFlags IncoherentBufferWriteStages { get; } + public PipelineStageFlags IncoherentTextureWriteStages { get; } + public ResourceBindingSegment[][] ClearSegments { get; } public ResourceBindingSegment[][] BindingSegments { get; } + public DescriptorSetTemplate[] Templates { get; } public ProgramLinkStatus LinkStatus { get; private set; } @@ -107,17 +114,30 @@ namespace Ryujinx.Graphics.Vulkan _shaders = internalShaders; - bool usePushDescriptors = !isMinimal && VulkanConfiguration.UsePushDescriptors && _gd.Capabilities.SupportsPushDescriptors; + bool usePushDescriptors = !isMinimal && + VulkanConfiguration.UsePushDescriptors && + _gd.Capabilities.SupportsPushDescriptors && + !IsCompute && + !HasPushDescriptorsBug(gd) && + CanUsePushDescriptors(gd, resourceLayout, IsCompute); - _plce = gd.PipelineLayoutCache.GetOrCreate(gd, device, resourceLayout.Sets, usePushDescriptors); + ReadOnlyCollection sets = usePushDescriptors ? + BuildPushDescriptorSets(gd, resourceLayout.Sets) : resourceLayout.Sets; + + _plce = gd.PipelineLayoutCache.GetOrCreate(gd, device, sets, usePushDescriptors); HasMinimalLayout = isMinimal; UsePushDescriptors = usePushDescriptors; Stages = stages; - ClearSegments = BuildClearSegments(resourceLayout.Sets); - BindingSegments = BuildBindingSegments(resourceLayout.SetUsages); + ClearSegments = BuildClearSegments(sets); + BindingSegments = BuildBindingSegments(resourceLayout.SetUsages, out bool usesBufferTextures); + Templates = BuildTemplates(usePushDescriptors); + (IncoherentBufferWriteStages, IncoherentTextureWriteStages) = BuildIncoherentStages(resourceLayout.SetUsages); + + // Updating buffer texture bindings using template updates crashes the Adreno driver on Windows. + UpdateTexturesWithoutTemplate = gd.IsQualcommProprietary && usesBufferTextures; _compileTask = Task.CompletedTask; _firstBackgroundUse = false; @@ -137,6 +157,82 @@ namespace Ryujinx.Graphics.Vulkan _firstBackgroundUse = !fromCache; } + private static bool HasPushDescriptorsBug(VulkanRenderer gd) + { + // Those GPUs/drivers do not work properly with push descriptors, so we must force disable them. + return gd.IsNvidiaPreTuring || (gd.IsIntelArc && gd.IsIntelWindows); + } + + private static bool CanUsePushDescriptors(VulkanRenderer gd, ResourceLayout layout, bool isCompute) + { + // If binding 3 is immediately used, use an alternate set of reserved bindings. + ReadOnlyCollection uniformUsage = layout.SetUsages[0].Usages; + bool hasBinding3 = uniformUsage.Any(x => x.Binding == 3); + int[] reserved = isCompute ? Array.Empty() : gd.GetPushDescriptorReservedBindings(hasBinding3); + + // Can't use any of the reserved usages. + for (int i = 0; i < uniformUsage.Count; i++) + { + var binding = uniformUsage[i].Binding; + + if (reserved.Contains(binding) || + binding >= Constants.MaxPushDescriptorBinding || + binding >= gd.Capabilities.MaxPushDescriptors + reserved.Count(id => id < binding)) + { + return false; + } + } + + return true; + } + + private static ReadOnlyCollection BuildPushDescriptorSets( + VulkanRenderer gd, + ReadOnlyCollection sets) + { + // The reserved bindings were selected when determining if push descriptors could be used. + int[] reserved = gd.GetPushDescriptorReservedBindings(false); + + var result = new ResourceDescriptorCollection[sets.Count]; + + for (int i = 0; i < sets.Count; i++) + { + if (i == 0) + { + // Push descriptors apply here. Remove reserved bindings. + ResourceDescriptorCollection original = sets[i]; + + var pdUniforms = new ResourceDescriptor[original.Descriptors.Count]; + int j = 0; + + foreach (ResourceDescriptor descriptor in original.Descriptors) + { + if (reserved.Contains(descriptor.Binding)) + { + // If the binding is reserved, set its descriptor count to 0. + pdUniforms[j++] = new ResourceDescriptor( + descriptor.Binding, + 0, + descriptor.Type, + descriptor.Stages); + } + else + { + pdUniforms[j++] = descriptor; + } + } + + result[i] = new ResourceDescriptorCollection(new(pdUniforms)); + } + else + { + result[i] = sets[i]; + } + } + + return new(result); + } + private static ResourceBindingSegment[][] BuildClearSegments(ReadOnlyCollection sets) { ResourceBindingSegment[][] segments = new ResourceBindingSegment[sets.Count][]; @@ -154,7 +250,9 @@ namespace Ryujinx.Graphics.Vulkan if (currentDescriptor.Binding + currentCount != descriptor.Binding || currentDescriptor.Type != descriptor.Type || - currentDescriptor.Stages != descriptor.Stages) + currentDescriptor.Stages != descriptor.Stages || + currentDescriptor.Count > 1 || + descriptor.Count > 1) { if (currentCount != 0) { @@ -162,7 +260,8 @@ namespace Ryujinx.Graphics.Vulkan currentDescriptor.Binding, currentCount, currentDescriptor.Type, - currentDescriptor.Stages)); + currentDescriptor.Stages, + currentDescriptor.Count > 1)); } currentDescriptor = descriptor; @@ -180,7 +279,8 @@ namespace Ryujinx.Graphics.Vulkan currentDescriptor.Binding, currentCount, currentDescriptor.Type, - currentDescriptor.Stages)); + currentDescriptor.Stages, + currentDescriptor.Count > 1)); } segments[setIndex] = currentSegments.ToArray(); @@ -189,8 +289,10 @@ namespace Ryujinx.Graphics.Vulkan return segments; } - private static ResourceBindingSegment[][] BuildBindingSegments(ReadOnlyCollection setUsages) + private static ResourceBindingSegment[][] BuildBindingSegments(ReadOnlyCollection setUsages, out bool usesBufferTextures) { + usesBufferTextures = false; + ResourceBindingSegment[][] segments = new ResourceBindingSegment[setUsages.Count][]; for (int setIndex = 0; setIndex < setUsages.Count; setIndex++) @@ -204,9 +306,16 @@ namespace Ryujinx.Graphics.Vulkan { ResourceUsage usage = setUsages[setIndex].Usages[index]; + if (usage.Type == ResourceType.BufferTexture) + { + usesBufferTextures = true; + } + if (currentUsage.Binding + currentCount != usage.Binding || currentUsage.Type != usage.Type || - currentUsage.Stages != usage.Stages) + currentUsage.Stages != usage.Stages || + currentUsage.ArrayLength > 1 || + usage.ArrayLength > 1) { if (currentCount != 0) { @@ -214,11 +323,12 @@ namespace Ryujinx.Graphics.Vulkan currentUsage.Binding, currentCount, currentUsage.Type, - currentUsage.Stages)); + currentUsage.Stages, + currentUsage.ArrayLength > 1)); } currentUsage = usage; - currentCount = 1; + currentCount = usage.ArrayLength; } else { @@ -232,7 +342,8 @@ namespace Ryujinx.Graphics.Vulkan currentUsage.Binding, currentCount, currentUsage.Type, - currentUsage.Stages)); + currentUsage.Stages, + currentUsage.ArrayLength > 1)); } segments[setIndex] = currentSegments.ToArray(); @@ -241,6 +352,102 @@ namespace Ryujinx.Graphics.Vulkan return segments; } + private DescriptorSetTemplate[] BuildTemplates(bool usePushDescriptors) + { + var templates = new DescriptorSetTemplate[BindingSegments.Length]; + + for (int setIndex = 0; setIndex < BindingSegments.Length; setIndex++) + { + if (usePushDescriptors && setIndex == 0) + { + // Push descriptors get updated using templates owned by the pipeline layout. + continue; + } + + ResourceBindingSegment[] segments = BindingSegments[setIndex]; + + if (segments != null && segments.Length > 0) + { + templates[setIndex] = new DescriptorSetTemplate( + _gd, + _device, + segments, + _plce, + IsCompute ? PipelineBindPoint.Compute : PipelineBindPoint.Graphics, + setIndex); + } + } + + return templates; + } + + private PipelineStageFlags GetPipelineStages(ResourceStages stages) + { + PipelineStageFlags result = 0; + + if ((stages & ResourceStages.Compute) != 0) + { + result |= PipelineStageFlags.ComputeShaderBit; + } + + if ((stages & ResourceStages.Vertex) != 0) + { + result |= PipelineStageFlags.VertexShaderBit; + } + + if ((stages & ResourceStages.Fragment) != 0) + { + result |= PipelineStageFlags.FragmentShaderBit; + } + + if ((stages & ResourceStages.Geometry) != 0) + { + result |= PipelineStageFlags.GeometryShaderBit; + } + + if ((stages & ResourceStages.TessellationControl) != 0) + { + result |= PipelineStageFlags.TessellationControlShaderBit; + } + + if ((stages & ResourceStages.TessellationEvaluation) != 0) + { + result |= PipelineStageFlags.TessellationEvaluationShaderBit; + } + + return result; + } + + private (PipelineStageFlags Buffer, PipelineStageFlags Texture) BuildIncoherentStages(ReadOnlyCollection setUsages) + { + PipelineStageFlags buffer = PipelineStageFlags.None; + PipelineStageFlags texture = PipelineStageFlags.None; + + foreach (var set in setUsages) + { + foreach (var range in set.Usages) + { + if (range.Write) + { + PipelineStageFlags stages = GetPipelineStages(range.Stages); + + switch (range.Type) + { + case ResourceType.Image: + texture |= stages; + break; + case ResourceType.StorageBuffer: + case ResourceType.BufferImage: + buffer |= stages; + break; + } + } + } + } + + return (buffer, texture); + } + private async Task BackgroundCompilation() { await Task.WhenAll(_shaders.Select(shader => shader.CompileTask)); @@ -352,10 +559,11 @@ namespace Ryujinx.Graphics.Vulkan stages[i] = _shaders[i].GetInfo(); } + pipeline.HasTessellationControlShader = HasTessellationControlShader; pipeline.StagesCount = (uint)_shaders.Length; pipeline.PipelineLayout = PipelineLayout; - pipeline.CreateGraphicsPipeline(_gd, _device, this, (_gd.Pipeline as PipelineBase).PipelineCache, renderPass.Value); + pipeline.CreateGraphicsPipeline(_gd, _device, this, (_gd.Pipeline as PipelineBase).PipelineCache, renderPass.Value, throwOnError: true); pipeline.Dispose(); } @@ -414,6 +622,11 @@ namespace Ryujinx.Graphics.Vulkan return null; } + public DescriptorSetTemplate GetPushDescriptorTemplate(long updateMask) + { + return _plce.GetPushDescriptorTemplate(IsCompute ? PipelineBindPoint.Compute : PipelineBindPoint.Graphics, updateMask); + } + public void AddComputePipeline(ref SpecData key, Auto pipeline) { (_computePipelineCache ??= new()).Add(ref key, pipeline); @@ -474,6 +687,26 @@ namespace Ryujinx.Graphics.Vulkan return _plce.GetNewDescriptorSetCollection(setIndex, out isNew); } + public Auto GetNewManualDescriptorSetCollection(CommandBufferScoped cbs, int setIndex, out int cacheIndex) + { + return _plce.GetNewManualDescriptorSetCollection(cbs, setIndex, out cacheIndex); + } + + public void UpdateManualDescriptorSetCollectionOwnership(CommandBufferScoped cbs, int setIndex, int cacheIndex) + { + _plce.UpdateManualDescriptorSetCollectionOwnership(cbs, setIndex, cacheIndex); + } + + public void ReleaseManualDescriptorSetCollection(int setIndex, int cacheIndex) + { + _plce.ReleaseManualDescriptorSetCollection(setIndex, cacheIndex); + } + + public bool HasSameLayout(ShaderCollection other) + { + return other != null && _plce == other._plce; + } + protected virtual void Dispose(bool disposing) { if (disposing) @@ -492,7 +725,7 @@ namespace Ryujinx.Graphics.Vulkan { foreach (Auto pipeline in _graphicsPipelineCache.Values) { - pipeline.Dispose(); + pipeline?.Dispose(); } } @@ -504,6 +737,11 @@ namespace Ryujinx.Graphics.Vulkan } } + for (int i = 0; i < Templates.Length; i++) + { + Templates[i]?.Dispose(); + } + if (_dummyRenderPass.Value.Handle != 0) { _dummyRenderPass.Dispose(); diff --git a/src/Ryujinx.Graphics.Vulkan/StagingBuffer.cs b/src/Ryujinx.Graphics.Vulkan/StagingBuffer.cs index 3a02a28dc..90a47bb67 100644 --- a/src/Ryujinx.Graphics.Vulkan/StagingBuffer.cs +++ b/src/Ryujinx.Graphics.Vulkan/StagingBuffer.cs @@ -1,5 +1,6 @@ using Ryujinx.Common; using Ryujinx.Common.Logging; +using Ryujinx.Graphics.GAL; using System; using System.Collections.Generic; using System.Diagnostics; @@ -29,6 +30,9 @@ namespace Ryujinx.Graphics.Vulkan private readonly VulkanRenderer _gd; private readonly BufferHolder _buffer; + private readonly int _resourceAlignment; + + public readonly BufferHandle Handle; private readonly struct PendingCopy { @@ -48,9 +52,10 @@ namespace Ryujinx.Graphics.Vulkan public StagingBuffer(VulkanRenderer gd, BufferManager bufferManager) { _gd = gd; - _buffer = bufferManager.Create(gd, BufferSize); + Handle = bufferManager.CreateWithHandle(gd, BufferSize, out _buffer); _pendingCopies = new Queue(); _freeSize = BufferSize; + _resourceAlignment = (int)gd.Capabilities.MinResourceAlignment; } public void PushData(CommandBufferPool cbp, CommandBufferScoped? cbs, Action endRenderPass, BufferHolder dst, int dstOffset, ReadOnlySpan data) @@ -197,7 +202,7 @@ namespace Ryujinx.Graphics.Vulkan /// Reserve a range on the staging buffer for the current command buffer and upload data to it. /// /// Command buffer to reserve the data on - /// The data to upload + /// The minimum size the reserved data requires /// The required alignment for the buffer offset /// The reserved range of the staging buffer public unsafe StagingBufferReserved? TryReserveData(CommandBufferScoped cbs, int size, int alignment) @@ -223,6 +228,18 @@ namespace Ryujinx.Graphics.Vulkan return ReserveDataImpl(cbs, size, alignment); } + /// + /// Reserve a range on the staging buffer for the current command buffer and upload data to it. + /// Uses the most permissive byte alignment. + /// + /// Command buffer to reserve the data on + /// The minimum size the reserved data requires + /// The reserved range of the staging buffer + public unsafe StagingBufferReserved? TryReserveData(CommandBufferScoped cbs, int size) + { + return TryReserveData(cbs, size, _resourceAlignment); + } + private bool WaitFreeCompleted(CommandBufferPool cbp) { if (_pendingCopies.TryPeek(out var pc)) @@ -263,7 +280,7 @@ namespace Ryujinx.Graphics.Vulkan { if (disposing) { - _buffer.Dispose(); + _gd.BufferManager.Delete(Handle); while (_pendingCopies.TryDequeue(out var pc)) { diff --git a/src/Ryujinx.Graphics.Vulkan/TextureArray.cs b/src/Ryujinx.Graphics.Vulkan/TextureArray.cs new file mode 100644 index 000000000..99238b1f5 --- /dev/null +++ b/src/Ryujinx.Graphics.Vulkan/TextureArray.cs @@ -0,0 +1,234 @@ +using Ryujinx.Graphics.GAL; +using Silk.NET.Vulkan; +using System; +using System.Collections.Generic; + +namespace Ryujinx.Graphics.Vulkan +{ + class TextureArray : ResourceArray, ITextureArray + { + private readonly VulkanRenderer _gd; + + private struct TextureRef + { + public TextureStorage Storage; + public Auto View; + public Auto Sampler; + } + + private readonly TextureRef[] _textureRefs; + private readonly TextureBuffer[] _bufferTextureRefs; + + private readonly DescriptorImageInfo[] _textures; + private readonly BufferView[] _bufferTextures; + + private HashSet _storages; + + private int _cachedCommandBufferIndex; + private int _cachedSubmissionCount; + + private readonly bool _isBuffer; + + public TextureArray(VulkanRenderer gd, int size, bool isBuffer) + { + _gd = gd; + + if (isBuffer) + { + _bufferTextureRefs = new TextureBuffer[size]; + _bufferTextures = new BufferView[size]; + } + else + { + _textureRefs = new TextureRef[size]; + _textures = new DescriptorImageInfo[size]; + } + + _storages = null; + + _cachedCommandBufferIndex = -1; + _cachedSubmissionCount = 0; + + _isBuffer = isBuffer; + } + + public void SetSamplers(int index, ISampler[] samplers) + { + for (int i = 0; i < samplers.Length; i++) + { + ISampler sampler = samplers[i]; + + if (sampler is SamplerHolder samplerHolder) + { + _textureRefs[index + i].Sampler = samplerHolder.GetSampler(); + } + else + { + _textureRefs[index + i].Sampler = default; + } + } + + SetDirty(); + } + + public void SetTextures(int index, ITexture[] textures) + { + for (int i = 0; i < textures.Length; i++) + { + ITexture texture = textures[i]; + + if (texture is TextureBuffer textureBuffer) + { + _bufferTextureRefs[index + i] = textureBuffer; + } + else if (texture is TextureView view) + { + _textureRefs[index + i].Storage = view.Storage; + _textureRefs[index + i].View = view.GetImageView(); + } + else if (!_isBuffer) + { + _textureRefs[index + i].Storage = null; + _textureRefs[index + i].View = default; + } + else + { + _bufferTextureRefs[index + i] = null; + } + } + + SetDirty(); + } + + private void SetDirty() + { + _cachedCommandBufferIndex = -1; + _storages = null; + SetDirty(_gd, isImage: false); + } + + public void QueueWriteToReadBarriers(CommandBufferScoped cbs, PipelineStageFlags stageFlags) + { + HashSet storages = _storages; + + if (storages == null) + { + storages = new HashSet(); + + for (int index = 0; index < _textureRefs.Length; index++) + { + if (_textureRefs[index].Storage != null) + { + storages.Add(_textureRefs[index].Storage); + } + } + + _storages = storages; + } + + foreach (TextureStorage storage in storages) + { + storage.QueueWriteToReadBarrier(cbs, AccessFlags.ShaderReadBit, stageFlags); + } + } + + public ReadOnlySpan GetImageInfos(VulkanRenderer gd, CommandBufferScoped cbs, TextureView dummyTexture, SamplerHolder dummySampler) + { + int submissionCount = gd.CommandBufferPool.GetSubmissionCount(cbs.CommandBufferIndex); + + Span textures = _textures; + + if (cbs.CommandBufferIndex == _cachedCommandBufferIndex && submissionCount == _cachedSubmissionCount) + { + return textures; + } + + _cachedCommandBufferIndex = cbs.CommandBufferIndex; + _cachedSubmissionCount = submissionCount; + + for (int i = 0; i < textures.Length; i++) + { + ref var texture = ref textures[i]; + ref var refs = ref _textureRefs[i]; + + if (i > 0 && _textureRefs[i - 1].View == refs.View && _textureRefs[i - 1].Sampler == refs.Sampler) + { + texture = textures[i - 1]; + + continue; + } + + texture.ImageLayout = ImageLayout.General; + texture.ImageView = refs.View?.Get(cbs).Value ?? default; + texture.Sampler = refs.Sampler?.Get(cbs).Value ?? default; + + if (texture.ImageView.Handle == 0) + { + texture.ImageView = dummyTexture.GetImageView().Get(cbs).Value; + } + + if (texture.Sampler.Handle == 0) + { + texture.Sampler = dummySampler.GetSampler().Get(cbs).Value; + } + } + + return textures; + } + + public ReadOnlySpan GetBufferViews(CommandBufferScoped cbs) + { + Span bufferTextures = _bufferTextures; + + for (int i = 0; i < bufferTextures.Length; i++) + { + bufferTextures[i] = _bufferTextureRefs[i]?.GetBufferView(cbs, false) ?? default; + } + + return bufferTextures; + } + + public DescriptorSet[] GetDescriptorSets( + Device device, + CommandBufferScoped cbs, + DescriptorSetTemplateUpdater templateUpdater, + ShaderCollection program, + int setIndex, + TextureView dummyTexture, + SamplerHolder dummySampler) + { + if (TryGetCachedDescriptorSets(cbs, program, setIndex, out DescriptorSet[] sets)) + { + // We still need to ensure the current command buffer holds a reference to all used textures. + + if (!_isBuffer) + { + GetImageInfos(_gd, cbs, dummyTexture, dummySampler); + } + else + { + GetBufferViews(cbs); + } + + return sets; + } + + DescriptorSetTemplate template = program.Templates[setIndex]; + + DescriptorSetTemplateWriter tu = templateUpdater.Begin(template); + + if (!_isBuffer) + { + tu.Push(GetImageInfos(_gd, cbs, dummyTexture, dummySampler)); + } + else + { + tu.Push(GetBufferViews(cbs)); + } + + templateUpdater.Commit(_gd, device, sets[0]); + + return sets; + } + } +} diff --git a/src/Ryujinx.Graphics.Vulkan/TextureBuffer.cs b/src/Ryujinx.Graphics.Vulkan/TextureBuffer.cs index 5cb8b8f49..073eee2ca 100644 --- a/src/Ryujinx.Graphics.Vulkan/TextureBuffer.cs +++ b/src/Ryujinx.Graphics.Vulkan/TextureBuffer.cs @@ -2,7 +2,6 @@ using Ryujinx.Common.Memory; using Ryujinx.Graphics.GAL; using Silk.NET.Vulkan; using System; -using System.Buffers; using System.Collections.Generic; using Format = Ryujinx.Graphics.GAL.Format; using VkFormat = Silk.NET.Vulkan.Format; @@ -17,7 +16,6 @@ namespace Ryujinx.Graphics.Vulkan private int _offset; private int _size; private Auto _bufferView; - private Dictionary> _selfManagedViews; private int _bufferCount; @@ -81,35 +79,26 @@ namespace Ryujinx.Graphics.Vulkan private void ReleaseImpl() { - if (_selfManagedViews != null) - { - foreach (var bufferView in _selfManagedViews.Values) - { - bufferView.Dispose(); - } - - _selfManagedViews = null; - } - _bufferView?.Dispose(); _bufferView = null; } - public void SetData(IMemoryOwner data) + /// + public void SetData(MemoryOwner data) { - _gd.SetBufferData(_bufferHandle, _offset, data.Memory.Span); + _gd.SetBufferData(_bufferHandle, _offset, data.Span); data.Dispose(); } - public void SetData(IMemoryOwner data, int layer, int level) + /// + public void SetData(MemoryOwner data, int layer, int level) { - data.Dispose(); throw new NotSupportedException(); } - public void SetData(IMemoryOwner data, int layer, int level, Rectangle region) + /// + public void SetData(MemoryOwner data, int layer, int level, Rectangle region) { - data.Dispose(); throw new NotSupportedException(); } @@ -137,28 +126,5 @@ namespace Ryujinx.Graphics.Vulkan return _bufferView?.Get(cbs, _offset, _size, write).Value ?? default; } - - public BufferView GetBufferView(CommandBufferScoped cbs, Format format, bool write) - { - var vkFormat = FormatTable.GetFormat(format); - if (vkFormat == VkFormat) - { - return GetBufferView(cbs, write); - } - - if (_selfManagedViews != null && _selfManagedViews.TryGetValue(format, out var bufferView)) - { - return bufferView.Get(cbs, _offset, _size, write).Value; - } - - bufferView = _gd.BufferManager.CreateView(_bufferHandle, vkFormat, _offset, _size, ReleaseImpl); - - if (bufferView != null) - { - (_selfManagedViews ??= new Dictionary>()).Add(format, bufferView); - } - - return bufferView?.Get(cbs, _offset, _size, write).Value ?? default; - } } } diff --git a/src/Ryujinx.Graphics.Vulkan/TextureCopy.cs b/src/Ryujinx.Graphics.Vulkan/TextureCopy.cs index 7c06a5df6..45cddd772 100644 --- a/src/Ryujinx.Graphics.Vulkan/TextureCopy.cs +++ b/src/Ryujinx.Graphics.Vulkan/TextureCopy.cs @@ -88,7 +88,7 @@ namespace Ryujinx.Graphics.Vulkan DstOffsets = dstOffsets, }; - api.CmdBlitImage(commandBuffer, srcImage, ImageLayout.General, dstImage, ImageLayout.General, 1, region, filter); + api.CmdBlitImage(commandBuffer, srcImage, ImageLayout.General, dstImage, ImageLayout.General, 1, in region, filter); copySrcLevel++; copyDstLevel++; @@ -320,13 +320,13 @@ namespace Ryujinx.Graphics.Vulkan { var region = new ImageResolve(srcSl, new Offset3D(0, 0, srcZ), dstSl, new Offset3D(0, 0, dstZ), extent); - api.CmdResolveImage(commandBuffer, srcImage, ImageLayout.General, dstImage, ImageLayout.General, 1, region); + api.CmdResolveImage(commandBuffer, srcImage, ImageLayout.General, dstImage, ImageLayout.General, 1, in region); } else { var region = new ImageCopy(srcSl, new Offset3D(0, 0, srcZ), dstSl, new Offset3D(0, 0, dstZ), extent); - api.CmdCopyImage(commandBuffer, srcImage, ImageLayout.General, dstImage, ImageLayout.General, 1, region); + api.CmdCopyImage(commandBuffer, srcImage, ImageLayout.General, dstImage, ImageLayout.General, 1, in region); } width = Math.Max(1, width >> 1); @@ -407,7 +407,7 @@ namespace Ryujinx.Graphics.Vulkan ImageLayout.General, ImageLayout.General); - var subpassDependency = PipelineConverter.CreateSubpassDependency2(); + var subpassDependency = PipelineConverter.CreateSubpassDependency2(gd); fixed (AttachmentDescription2* pAttachmentDescs = attachmentDescs) { @@ -422,7 +422,7 @@ namespace Ryujinx.Graphics.Vulkan DependencyCount = 1, }; - gd.Api.CreateRenderPass2(device, renderPassCreateInfo, null, out var renderPass).ThrowOnError(); + gd.Api.CreateRenderPass2(device, in renderPassCreateInfo, null, out var renderPass).ThrowOnError(); using var rp = new Auto(new DisposableRenderPass(gd.Api, device, renderPass)); @@ -445,7 +445,7 @@ namespace Ryujinx.Graphics.Vulkan Layers = (uint)src.Layers, }; - gd.Api.CreateFramebuffer(device, framebufferCreateInfo, null, out var framebuffer).ThrowOnError(); + gd.Api.CreateFramebuffer(device, in framebufferCreateInfo, null, out var framebuffer).ThrowOnError(); using var fb = new Auto(new DisposableFramebuffer(gd.Api, device, framebuffer), null, srcView, dstView); var renderArea = new Rect2D(null, new Extent2D((uint)src.Info.Width, (uint)src.Info.Height)); @@ -465,7 +465,7 @@ namespace Ryujinx.Graphics.Vulkan // to resolve the depth-stencil texture. // TODO: Do speculative resolve and part of the same render pass as the draw to avoid // ending the current render pass? - gd.Api.CmdBeginRenderPass(cbs.CommandBuffer, renderPassBeginInfo, SubpassContents.Inline); + gd.Api.CmdBeginRenderPass(cbs.CommandBuffer, in renderPassBeginInfo, SubpassContents.Inline); gd.Api.CmdEndRenderPass(cbs.CommandBuffer); } } diff --git a/src/Ryujinx.Graphics.Vulkan/TextureStorage.cs b/src/Ryujinx.Graphics.Vulkan/TextureStorage.cs index b763fa987..10b36a3f9 100644 --- a/src/Ryujinx.Graphics.Vulkan/TextureStorage.cs +++ b/src/Ryujinx.Graphics.Vulkan/TextureStorage.cs @@ -4,6 +4,7 @@ using Silk.NET.Vulkan; using System; using System.Collections.Generic; using System.Numerics; +using System.Runtime.CompilerServices; using Format = Ryujinx.Graphics.GAL.Format; using VkBuffer = Silk.NET.Vulkan.Buffer; using VkFormat = Silk.NET.Vulkan.Format; @@ -12,6 +13,11 @@ namespace Ryujinx.Graphics.Vulkan { class TextureStorage : IDisposable { + private struct TextureSliceInfo + { + public int BindCount; + } + private const MemoryPropertyFlags DefaultImageMemoryFlags = MemoryPropertyFlags.DeviceLocalBit; @@ -38,9 +44,12 @@ namespace Ryujinx.Graphics.Vulkan public TextureCreateInfo Info => _info; + public bool Disposed { get; private set; } + private readonly Image _image; private readonly Auto _imageAuto; private readonly Auto _allocationAuto; + private readonly int _depthOrLayers; private Auto _foreignAllocationAuto; private Dictionary _aliasedStorages; @@ -53,6 +62,9 @@ namespace Ryujinx.Graphics.Vulkan private int _viewsCount; private readonly ulong _size; + private int _bindCount; + private readonly TextureSliceInfo[] _slices; + public VkFormat VkFormat { get; } public unsafe TextureStorage( @@ -71,6 +83,7 @@ namespace Ryujinx.Graphics.Vulkan var depth = (uint)(info.Target == Target.Texture3D ? info.Depth : 1); VkFormat = format; + _depthOrLayers = info.GetDepthOrLayers(); var type = info.Target.Convert(); @@ -78,9 +91,9 @@ namespace Ryujinx.Graphics.Vulkan var sampleCountFlags = ConvertToSampleCountFlags(gd.Capabilities.SupportedSampleCounts, (uint)info.Samples); - var usage = GetImageUsage(info.Format, info.Target, gd.Capabilities.SupportsShaderStorageImageMultisample); + var usage = GetImageUsage(info.Format, info.Target, gd.Capabilities); - var flags = ImageCreateFlags.CreateMutableFormatBit; + var flags = ImageCreateFlags.CreateMutableFormatBit | ImageCreateFlags.CreateExtendedUsageBit; // This flag causes mipmapped texture arrays to break on AMD GCN, so for that copy dependencies are forced for aliasing as cube. bool isCube = info.Target == Target.Cubemap || info.Target == Target.CubemapArray; @@ -112,7 +125,7 @@ namespace Ryujinx.Graphics.Vulkan Flags = flags, }; - gd.Api.CreateImage(device, imageCreateInfo, null, out _image).ThrowOnError(); + gd.Api.CreateImage(device, in imageCreateInfo, null, out _image).ThrowOnError(); if (foreignAllocation == null) { @@ -146,6 +159,8 @@ namespace Ryujinx.Graphics.Vulkan InitialTransition(ImageLayout.Preinitialized, ImageLayout.General); } + + _slices = new TextureSliceInfo[levels * _depthOrLayers]; } public TextureStorage CreateAliasedColorForDepthStorageUnsafe(Format format) @@ -154,9 +169,8 @@ namespace Ryujinx.Graphics.Vulkan { Format.S8Uint => Format.R8Unorm, Format.D16Unorm => Format.R16Unorm, - Format.S8UintD24Unorm => Format.R8G8B8A8Unorm, + Format.D24UnormS8Uint or Format.S8UintD24Unorm or Format.X8UintD24Unorm => Format.R8G8B8A8Unorm, Format.D32Float => Format.R32Float, - Format.D24UnormS8Uint => Format.R8G8B8A8Unorm, Format.D32FloatS8Uint => Format.R32G32Float, _ => throw new ArgumentException($"\"{format}\" is not a supported depth or stencil format."), }; @@ -283,7 +297,7 @@ namespace Ryujinx.Graphics.Vulkan 0, null, 1, - barrier); + in barrier); if (useTempCbs) { @@ -291,7 +305,7 @@ namespace Ryujinx.Graphics.Vulkan } } - public static ImageUsageFlags GetImageUsage(Format format, Target target, bool supportsMsStorage) + public static ImageUsageFlags GetImageUsage(Format format, Target target, in HardwareCapabilities capabilities) { var usage = DefaultUsageFlags; @@ -304,11 +318,19 @@ namespace Ryujinx.Graphics.Vulkan usage |= ImageUsageFlags.ColorAttachmentBit; } + bool supportsMsStorage = capabilities.SupportsShaderStorageImageMultisample; + if (format.IsImageCompatible() && (supportsMsStorage || !target.IsMultisample())) { usage |= ImageUsageFlags.StorageBit; } + if (capabilities.SupportsAttachmentFeedbackLoop && + (usage & (ImageUsageFlags.DepthStencilAttachmentBit | ImageUsageFlags.ColorAttachmentBit)) != 0) + { + usage |= ImageUsageFlags.AttachmentFeedbackLoopBitExt; + } + return usage; } @@ -400,11 +422,11 @@ namespace Ryujinx.Graphics.Vulkan if (to) { - _gd.Api.CmdCopyImageToBuffer(commandBuffer, image, ImageLayout.General, buffer, 1, region); + _gd.Api.CmdCopyImageToBuffer(commandBuffer, image, ImageLayout.General, buffer, 1, in region); } else { - _gd.Api.CmdCopyBufferToImage(commandBuffer, buffer, image, ImageLayout.General, 1, region); + _gd.Api.CmdCopyBufferToImage(commandBuffer, buffer, image, ImageLayout.General, 1, in region); } offset += mipSize; @@ -434,104 +456,130 @@ namespace Ryujinx.Graphics.Vulkan return FormatCapabilities.IsD24S8(Info.Format) && VkFormat == VkFormat.D32SfloatS8Uint; } - public void SetModification(AccessFlags accessFlags, PipelineStageFlags stage) + public void AddStoreOpUsage(bool depthStencil) { - _lastModificationAccess = accessFlags; - _lastModificationStage = stage; + _lastModificationStage = depthStencil ? + PipelineStageFlags.LateFragmentTestsBit : + PipelineStageFlags.ColorAttachmentOutputBit; + + _lastModificationAccess = depthStencil ? + AccessFlags.DepthStencilAttachmentWriteBit : + AccessFlags.ColorAttachmentWriteBit; } - public void InsertReadToWriteBarrier(CommandBufferScoped cbs, AccessFlags dstAccessFlags, PipelineStageFlags dstStageFlags, bool insideRenderPass) + public void QueueLoadOpBarrier(CommandBufferScoped cbs, bool depthStencil) { - var lastReadStage = _lastReadStage; + PipelineStageFlags srcStageFlags = _lastReadStage | _lastModificationStage; + PipelineStageFlags dstStageFlags = depthStencil ? + PipelineStageFlags.EarlyFragmentTestsBit | PipelineStageFlags.LateFragmentTestsBit : + PipelineStageFlags.ColorAttachmentOutputBit; - if (insideRenderPass) + AccessFlags srcAccessFlags = _lastModificationAccess | _lastReadAccess; + AccessFlags dstAccessFlags = depthStencil ? + AccessFlags.DepthStencilAttachmentWriteBit | AccessFlags.DepthStencilAttachmentReadBit : + AccessFlags.ColorAttachmentWriteBit | AccessFlags.ColorAttachmentReadBit; + + if (srcAccessFlags != AccessFlags.None) { - // We can't have barrier from compute inside a render pass, - // as it is invalid to specify compute in the subpass dependency stage mask. + ImageAspectFlags aspectFlags = Info.Format.ConvertAspectFlags(); + ImageMemoryBarrier barrier = TextureView.GetImageBarrier( + _imageAuto.Get(cbs).Value, + srcAccessFlags, + dstAccessFlags, + aspectFlags, + 0, + 0, + _info.GetLayers(), + _info.Levels); - lastReadStage &= ~PipelineStageFlags.ComputeShaderBit; - } + _gd.Barriers.QueueBarrier(barrier, this, srcStageFlags, dstStageFlags); - if (lastReadStage != PipelineStageFlags.None) - { - // This would result in a validation error, but is - // required on MoltenVK as the generic barrier results in - // severe texture flickering in some scenarios. - if (_gd.IsMoltenVk) - { - ImageAspectFlags aspectFlags = Info.Format.ConvertAspectFlags(); - TextureView.InsertImageBarrier( - _gd.Api, - cbs.CommandBuffer, - _imageAuto.Get(cbs).Value, - _lastReadAccess, - dstAccessFlags, - _lastReadStage, - dstStageFlags, - aspectFlags, - 0, - 0, - _info.GetLayers(), - _info.Levels); - } - else - { - TextureView.InsertMemoryBarrier( - _gd.Api, - cbs.CommandBuffer, - _lastReadAccess, - dstAccessFlags, - lastReadStage, - dstStageFlags); - } - - _lastReadAccess = AccessFlags.None; _lastReadStage = PipelineStageFlags.None; + _lastReadAccess = AccessFlags.None; } + + _lastModificationStage = depthStencil ? + PipelineStageFlags.LateFragmentTestsBit : + PipelineStageFlags.ColorAttachmentOutputBit; + + _lastModificationAccess = depthStencil ? + AccessFlags.DepthStencilAttachmentWriteBit : + AccessFlags.ColorAttachmentWriteBit; } - public void InsertWriteToReadBarrier(CommandBufferScoped cbs, AccessFlags dstAccessFlags, PipelineStageFlags dstStageFlags) + public void QueueWriteToReadBarrier(CommandBufferScoped cbs, AccessFlags dstAccessFlags, PipelineStageFlags dstStageFlags) { _lastReadAccess |= dstAccessFlags; _lastReadStage |= dstStageFlags; if (_lastModificationAccess != AccessFlags.None) { - // This would result in a validation error, but is - // required on MoltenVK as the generic barrier results in - // severe texture flickering in some scenarios. - if (_gd.IsMoltenVk) - { - ImageAspectFlags aspectFlags = Info.Format.ConvertAspectFlags(); - TextureView.InsertImageBarrier( - _gd.Api, - cbs.CommandBuffer, - _imageAuto.Get(cbs).Value, - _lastModificationAccess, - dstAccessFlags, - _lastModificationStage, - dstStageFlags, - aspectFlags, - 0, - 0, - _info.GetLayers(), - _info.Levels); - } - else - { - TextureView.InsertMemoryBarrier( - _gd.Api, - cbs.CommandBuffer, - _lastModificationAccess, - dstAccessFlags, - _lastModificationStage, - dstStageFlags); - } + ImageAspectFlags aspectFlags = Info.Format.ConvertAspectFlags(); + ImageMemoryBarrier barrier = TextureView.GetImageBarrier( + _imageAuto.Get(cbs).Value, + _lastModificationAccess, + dstAccessFlags, + aspectFlags, + 0, + 0, + _info.GetLayers(), + _info.Levels); + + _gd.Barriers.QueueBarrier(barrier, this, _lastModificationStage, dstStageFlags); _lastModificationAccess = AccessFlags.None; } } + public void AddBinding(TextureView view) + { + // Assumes a view only has a first level. + + int index = view.FirstLevel * _depthOrLayers + view.FirstLayer; + int layers = view.Layers; + + for (int i = 0; i < layers; i++) + { + ref TextureSliceInfo info = ref _slices[index++]; + + info.BindCount++; + } + + _bindCount++; + } + + public void ClearBindings() + { + if (_bindCount != 0) + { + Array.Clear(_slices, 0, _slices.Length); + + _bindCount = 0; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool IsBound(TextureView view) + { + if (_bindCount != 0) + { + int index = view.FirstLevel * _depthOrLayers + view.FirstLayer; + int layers = view.Layers; + + for (int i = 0; i < layers; i++) + { + ref TextureSliceInfo info = ref _slices[index++]; + + if (info.BindCount != 0) + { + return true; + } + } + } + + return false; + } + public void IncrementViewsCount() { _viewsCount++; @@ -549,6 +597,8 @@ namespace Ryujinx.Graphics.Vulkan public void Dispose() { + Disposed = true; + if (_aliasedStorages != null) { foreach (var storage in _aliasedStorages.Values) diff --git a/src/Ryujinx.Graphics.Vulkan/TextureView.cs b/src/Ryujinx.Graphics.Vulkan/TextureView.cs index 96e113f07..b7b936809 100644 --- a/src/Ryujinx.Graphics.Vulkan/TextureView.cs +++ b/src/Ryujinx.Graphics.Vulkan/TextureView.cs @@ -2,8 +2,9 @@ using Ryujinx.Common.Memory; using Ryujinx.Graphics.GAL; using Silk.NET.Vulkan; using System; -using System.Buffers; using System.Collections.Generic; +using System.Linq; +using System.Threading; using Format = Ryujinx.Graphics.GAL.Format; using VkBuffer = Silk.NET.Vulkan.Buffer; using VkFormat = Silk.NET.Vulkan.Format; @@ -22,8 +23,12 @@ namespace Ryujinx.Graphics.Vulkan private readonly Auto _imageView2dArray; private Dictionary _selfManagedViews; + private int _hazardUses; + private readonly TextureCreateInfo _info; + private HashTableSlim _renderPasses; + public TextureCreateInfo Info => _info; public TextureStorage Storage { get; } @@ -34,7 +39,8 @@ namespace Ryujinx.Graphics.Vulkan public int FirstLayer { get; } public int FirstLevel { get; } public VkFormat VkFormat { get; } - public bool Valid { get; private set; } + private int _isValid; + public bool Valid => Volatile.Read(ref _isValid) != 0; public TextureView( VulkanRenderer gd, @@ -56,7 +62,7 @@ namespace Ryujinx.Graphics.Vulkan gd.Textures.Add(this); var format = _gd.FormatCapabilities.ConvertToVkFormat(info.Format); - var usage = TextureStorage.GetImageUsage(info.Format, info.Target, gd.Capabilities.SupportsShaderStorageImageMultisample); + var usage = TextureStorage.GetImageUsage(info.Format, info.Target, gd.Capabilities); var levels = (uint)info.Levels; var layers = (uint)info.GetLayers(); @@ -96,7 +102,7 @@ namespace Ryujinx.Graphics.Vulkan unsafe Auto CreateImageView(ComponentMapping cm, ImageSubresourceRange sr, ImageViewType viewType, ImageUsageFlags usageFlags) { - var usage = new ImageViewUsageCreateInfo + var imageViewUsage = new ImageViewUsageCreateInfo { SType = StructureType.ImageViewUsageCreateInfo, Usage = usageFlags, @@ -110,16 +116,16 @@ namespace Ryujinx.Graphics.Vulkan Format = format, Components = cm, SubresourceRange = sr, - PNext = &usage, + PNext = &imageViewUsage, }; - gd.Api.CreateImageView(device, imageCreateInfo, null, out var imageView).ThrowOnError(); + gd.Api.CreateImageView(device, in imageCreateInfo, null, out var imageView).ThrowOnError(); return new Auto(new DisposableImageView(gd.Api, device, imageView), null, storage.GetImage()); } ImageUsageFlags shaderUsage = ImageUsageFlags.SampledBit; - if (info.Format.IsImageCompatible()) + if (info.Format.IsImageCompatible() && (_gd.Capabilities.SupportsShaderStorageImageMultisample || !info.Target.IsMultisample())) { shaderUsage |= ImageUsageFlags.StorageBit; } @@ -150,13 +156,33 @@ namespace Ryujinx.Graphics.Vulkan } else { - subresourceRange = new ImageSubresourceRange(aspectFlags, (uint)firstLevel, levels, (uint)firstLayer, (uint)info.Depth); + subresourceRange = new ImageSubresourceRange(aspectFlags, (uint)firstLevel, 1, (uint)firstLayer, (uint)info.Depth); _imageView2dArray = CreateImageView(identityComponentMapping, subresourceRange, ImageViewType.Type2DArray, usage); } } - Valid = true; + _isValid = 1; + } + + /// + /// Create a texture view for an existing swapchain image view. + /// Does not set storage, so only appropriate for swapchain use. + /// + /// Do not use this for normal textures, and make sure uses do not try to read storage. + public TextureView(VulkanRenderer gd, Device device, DisposableImageView view, TextureCreateInfo info, VkFormat format) + { + _gd = gd; + _device = device; + + _imageView = new Auto(view); + _imageViewDraw = _imageView; + _imageViewIdentity = _imageView; + _info = info; + + VkFormat = format; + + _isValid = 1; } public Auto GetImage() @@ -468,13 +494,37 @@ namespace Ryujinx.Graphics.Vulkan dstStageMask, DependencyFlags.None, 1, - memoryBarrier, + in memoryBarrier, 0, null, 0, null); } + public static ImageMemoryBarrier GetImageBarrier( + Image image, + AccessFlags srcAccessMask, + AccessFlags dstAccessMask, + ImageAspectFlags aspectFlags, + int firstLayer, + int firstLevel, + int layers, + int levels) + { + return new() + { + SType = StructureType.ImageMemoryBarrier, + SrcAccessMask = srcAccessMask, + DstAccessMask = dstAccessMask, + SrcQueueFamilyIndex = Vk.QueueFamilyIgnored, + DstQueueFamilyIndex = Vk.QueueFamilyIgnored, + Image = image, + OldLayout = ImageLayout.General, + NewLayout = ImageLayout.General, + SubresourceRange = new ImageSubresourceRange(aspectFlags, (uint)firstLevel, (uint)levels, (uint)firstLayer, (uint)layers), + }; + } + public static unsafe void InsertImageBarrier( Vk api, CommandBuffer commandBuffer, @@ -489,18 +539,15 @@ namespace Ryujinx.Graphics.Vulkan int layers, int levels) { - ImageMemoryBarrier memoryBarrier = new() - { - SType = StructureType.ImageMemoryBarrier, - SrcAccessMask = srcAccessMask, - DstAccessMask = dstAccessMask, - SrcQueueFamilyIndex = Vk.QueueFamilyIgnored, - DstQueueFamilyIndex = Vk.QueueFamilyIgnored, - Image = image, - OldLayout = ImageLayout.General, - NewLayout = ImageLayout.General, - SubresourceRange = new ImageSubresourceRange(aspectFlags, (uint)firstLevel, (uint)levels, (uint)firstLayer, (uint)layers), - }; + ImageMemoryBarrier memoryBarrier = GetImageBarrier( + image, + srcAccessMask, + dstAccessMask, + aspectFlags, + firstLayer, + firstLevel, + layers, + levels); api.CmdPipelineBarrier( commandBuffer, @@ -512,7 +559,7 @@ namespace Ryujinx.Graphics.Vulkan 0, null, 1, - memoryBarrier); + in memoryBarrier); } public TextureView GetView(Format format) @@ -622,8 +669,36 @@ namespace Ryujinx.Graphics.Vulkan if (PrepareOutputBuffer(cbs, hostSize, buffer, out VkBuffer copyToBuffer, out BufferHolder tempCopyHolder)) { + // No barrier necessary, as this is a temporary copy buffer. offset = 0; } + else + { + BufferHolder.InsertBufferBarrier( + _gd, + cbs.CommandBuffer, + copyToBuffer, + BufferHolder.DefaultAccessFlags, + AccessFlags.TransferWriteBit, + PipelineStageFlags.AllCommandsBit, + PipelineStageFlags.TransferBit, + offset, + outSize); + } + + InsertImageBarrier( + _gd.Api, + cbs.CommandBuffer, + image, + TextureStorage.DefaultAccessMask, + AccessFlags.TransferReadBit, + PipelineStageFlags.AllCommandsBit, + PipelineStageFlags.TransferBit, + Info.Format.ConvertAspectFlags(), + FirstLayer + layer, + FirstLevel + level, + 1, + 1); CopyFromOrToBuffer(cbs.CommandBuffer, copyToBuffer, image, hostSize, true, layer, level, 1, 1, singleSlice: true, offset, stride); @@ -632,6 +707,19 @@ namespace Ryujinx.Graphics.Vulkan CopyDataToOutputBuffer(cbs, tempCopyHolder, autoBuffer, hostSize, range.Offset); tempCopyHolder.Dispose(); } + else + { + BufferHolder.InsertBufferBarrier( + _gd, + cbs.CommandBuffer, + copyToBuffer, + AccessFlags.TransferWriteBit, + BufferHolder.DefaultAccessFlags, + PipelineStageFlags.TransferBit, + PipelineStageFlags.AllCommandsBit, + offset, + outSize); + } } private ReadOnlySpan GetData(CommandBufferPool cbp, PersistentFlushBuffer flushBuffer) @@ -657,26 +745,24 @@ namespace Ryujinx.Graphics.Vulkan return GetDataFromBuffer(result, size, result); } - public void SetData(ReadOnlySpan data) + /// + public void SetData(MemoryOwner data) { - SetData(data, 0, 0, Info.GetLayers(), Info.Levels, singleSlice: false); - } - - public void SetData(IMemoryOwner data) - { - SetData(data.Memory.Span); + SetData(data.Span, 0, 0, Info.GetLayers(), Info.Levels, singleSlice: false); data.Dispose(); } - public void SetData(IMemoryOwner data, int layer, int level) + /// + public void SetData(MemoryOwner data, int layer, int level) { - SetData(data.Memory.Span, layer, level, 1, 1, singleSlice: true); + SetData(data.Span, layer, level, 1, 1, singleSlice: true); data.Dispose(); } - public void SetData(IMemoryOwner data, int layer, int level, Rectangle region) + /// + public void SetData(MemoryOwner data, int layer, int level, Rectangle region) { - SetData(data.Memory.Span, layer, level, 1, 1, singleSlice: true, region); + SetData(data.Span, layer, level, 1, 1, singleSlice: true, region); data.Dispose(); } @@ -825,7 +911,9 @@ namespace Ryujinx.Graphics.Vulkan for (int level = 0; level < levels; level++) { - int mipSize = GetBufferDataLength(Info.GetMipSize2D(dstLevel + level) * dstLayers); + int mipSize = GetBufferDataLength(is3D && !singleSlice + ? Info.GetMipSize(dstLevel + level) + : Info.GetMipSize2D(dstLevel + level) * dstLayers); int endOffset = offset + mipSize; @@ -863,11 +951,11 @@ namespace Ryujinx.Graphics.Vulkan if (to) { - _gd.Api.CmdCopyImageToBuffer(commandBuffer, image, ImageLayout.General, buffer, 1, region); + _gd.Api.CmdCopyImageToBuffer(commandBuffer, image, ImageLayout.General, buffer, 1, in region); } else { - _gd.Api.CmdCopyBufferToImage(commandBuffer, buffer, image, ImageLayout.General, 1, region); + _gd.Api.CmdCopyBufferToImage(commandBuffer, buffer, image, ImageLayout.General, 1, in region); } offset += mipSize; @@ -924,11 +1012,11 @@ namespace Ryujinx.Graphics.Vulkan if (to) { - _gd.Api.CmdCopyImageToBuffer(commandBuffer, image, ImageLayout.General, buffer, 1, region); + _gd.Api.CmdCopyImageToBuffer(commandBuffer, image, ImageLayout.General, buffer, 1, in region); } else { - _gd.Api.CmdCopyBufferToImage(commandBuffer, buffer, image, ImageLayout.General, 1, region); + _gd.Api.CmdCopyBufferToImage(commandBuffer, buffer, image, ImageLayout.General, 1, in region); } } @@ -948,40 +1036,111 @@ namespace Ryujinx.Graphics.Vulkan throw new NotImplementedException(); } + public void PrepareForUsage(CommandBufferScoped cbs, PipelineStageFlags flags, List feedbackLoopHazards) + { + Storage.QueueWriteToReadBarrier(cbs, AccessFlags.ShaderReadBit, flags); + + if (feedbackLoopHazards != null && Storage.IsBound(this)) + { + feedbackLoopHazards.Add(this); + _hazardUses++; + } + } + + public void ClearUsage(List feedbackLoopHazards) + { + if (_hazardUses != 0 && feedbackLoopHazards != null) + { + feedbackLoopHazards.Remove(this); + _hazardUses--; + } + } + + public void DecrementHazardUses() + { + if (_hazardUses != 0) + { + _hazardUses--; + } + } + + public (RenderPassHolder rpHolder, Auto framebuffer) GetPassAndFramebuffer( + VulkanRenderer gd, + Device device, + CommandBufferScoped cbs, + FramebufferParams fb) + { + var key = fb.GetRenderPassCacheKey(); + + if (_renderPasses == null || !_renderPasses.TryGetValue(ref key, out RenderPassHolder rpHolder)) + { + rpHolder = new RenderPassHolder(gd, device, key, fb); + } + + return (rpHolder, rpHolder.GetFramebuffer(gd, cbs, fb)); + } + + public void AddRenderPass(RenderPassCacheKey key, RenderPassHolder renderPass) + { + _renderPasses ??= new HashTableSlim(); + + _renderPasses.Add(ref key, renderPass); + } + + public void RemoveRenderPass(RenderPassCacheKey key) + { + _renderPasses.Remove(ref key); + } + protected virtual void Dispose(bool disposing) { if (disposing) { - Valid = false; - - if (_gd.Textures.Remove(this)) + bool wasValid = Interlocked.Exchange(ref _isValid, 0) != 0; + if (wasValid) { + _gd.Textures.Remove(this); + _imageView.Dispose(); - _imageViewIdentity.Dispose(); _imageView2dArray?.Dispose(); + if (_imageViewIdentity != _imageView) + { + _imageViewIdentity.Dispose(); + } + if (_imageViewDraw != _imageViewIdentity) { _imageViewDraw.Dispose(); } - Storage.DecrementViewsCount(); + Storage?.DecrementViewsCount(); + + if (_renderPasses != null) + { + var renderPasses = _renderPasses.Values.ToArray(); + + foreach (var pass in renderPasses) + { + pass.Dispose(); + } + } + + if (_selfManagedViews != null) + { + foreach (var view in _selfManagedViews.Values) + { + view.Dispose(); + } + + _selfManagedViews = null; + } } } } public void Dispose() { - if (_selfManagedViews != null) - { - foreach (var view in _selfManagedViews.Values) - { - view.Dispose(); - } - - _selfManagedViews = null; - } - Dispose(true); } diff --git a/src/Ryujinx.Graphics.Vulkan/Vendor.cs b/src/Ryujinx.Graphics.Vulkan/Vendor.cs index 2d2f17b25..55ae0cd81 100644 --- a/src/Ryujinx.Graphics.Vulkan/Vendor.cs +++ b/src/Ryujinx.Graphics.Vulkan/Vendor.cs @@ -1,3 +1,4 @@ +using Silk.NET.Vulkan; using System.Text.RegularExpressions; namespace Ryujinx.Graphics.Vulkan @@ -20,6 +21,9 @@ namespace Ryujinx.Graphics.Vulkan [GeneratedRegex("Radeon (((HD|R(5|7|9|X)) )?((M?[2-6]\\d{2}(\\D|$))|([7-8]\\d{3}(\\D|$))|Fury|Nano))|(Pro Duo)")] public static partial Regex AmdGcnRegex(); + [GeneratedRegex("NVIDIA GeForce (R|G)?TX? (\\d{3}\\d?)M?")] + public static partial Regex NvidiaConsumerClassRegex(); + public static Vendor FromId(uint id) { return id switch @@ -58,5 +62,39 @@ namespace Ryujinx.Graphics.Vulkan _ => $"0x{id:X}", }; } + + public static string GetFriendlyDriverName(DriverId id) + { + return id switch + { + DriverId.AmdProprietary => "AMD", + DriverId.AmdOpenSource => "AMD (Open)", + DriverId.MesaRadv => "RADV", + DriverId.NvidiaProprietary => "NVIDIA", + DriverId.IntelProprietaryWindows => "Intel", + DriverId.IntelOpenSourceMesa => "Intel (Open)", + DriverId.ImaginationProprietary => "Imagination", + DriverId.QualcommProprietary => "Qualcomm", + DriverId.ArmProprietary => "ARM", + DriverId.GoogleSwiftshader => "SwiftShader", + DriverId.GgpProprietary => "GGP", + DriverId.BroadcomProprietary => "Broadcom", + DriverId.MesaLlvmpipe => "LLVMpipe", + DriverId.Moltenvk => "MoltenVK", + DriverId.CoreaviProprietary => "CoreAVI", + DriverId.JuiceProprietary => "Juice", + DriverId.VerisiliconProprietary => "Verisilicon", + DriverId.MesaTurnip => "Turnip", + DriverId.MesaV3DV => "V3DV", + DriverId.MesaPanvk => "PanVK", + DriverId.SamsungProprietary => "Samsung", + DriverId.MesaVenus => "Venus", + DriverId.MesaDozen => "Dozen", + DriverId.MesaNvk => "NVK", + DriverId.ImaginationOpenSourceMesa => "Imagination (Open)", + DriverId.MesaAgxv => "Honeykrisp", + _ => id.ToString(), + }; + } } } diff --git a/src/Ryujinx.Graphics.Vulkan/VertexBufferState.cs b/src/Ryujinx.Graphics.Vulkan/VertexBufferState.cs index 6f27bb68b..ce1293589 100644 --- a/src/Ryujinx.Graphics.Vulkan/VertexBufferState.cs +++ b/src/Ryujinx.Graphics.Vulkan/VertexBufferState.cs @@ -55,8 +55,10 @@ namespace Ryujinx.Graphics.Vulkan if (_handle != BufferHandle.Null) { // May need to restride the vertex buffer. - - if (gd.NeedsVertexBufferAlignment(AttributeScalarAlignment, out int alignment) && (_stride % alignment) != 0) + // + // Fix divide by zero when recovering from missed draw (Oct. 16 2024) + // (fixes crash in 'Baldo: The Guardian Owls' opening cutscene) + if (gd.NeedsVertexBufferAlignment(AttributeScalarAlignment, out int alignment) && alignment != 0 && (_stride % alignment) != 0) { autoBuffer = gd.BufferManager.GetAlignedVertexBuffer(cbs, _handle, _offset, _size, _stride, alignment); diff --git a/src/Ryujinx.Graphics.Vulkan/VulkanConfiguration.cs b/src/Ryujinx.Graphics.Vulkan/VulkanConfiguration.cs index a1fdc4aed..596c0e176 100644 --- a/src/Ryujinx.Graphics.Vulkan/VulkanConfiguration.cs +++ b/src/Ryujinx.Graphics.Vulkan/VulkanConfiguration.cs @@ -4,7 +4,7 @@ namespace Ryujinx.Graphics.Vulkan { public const bool UseFastBufferUpdates = true; public const bool UseUnsafeBlit = true; - public const bool UsePushDescriptors = false; + public const bool UsePushDescriptors = true; public const bool ForceD24S8Unsupported = false; public const bool ForceRGB16IntFloatUnsupported = false; diff --git a/src/Ryujinx.Graphics.Vulkan/VulkanDebugMessenger.cs b/src/Ryujinx.Graphics.Vulkan/VulkanDebugMessenger.cs index 496a90fbe..6dfcd8b6e 100644 --- a/src/Ryujinx.Graphics.Vulkan/VulkanDebugMessenger.cs +++ b/src/Ryujinx.Graphics.Vulkan/VulkanDebugMessenger.cs @@ -95,7 +95,7 @@ namespace Ryujinx.Graphics.Vulkan DebugUtilsMessengerCallbackDataEXT* pCallbackData, void* pUserData) { - var msg = Marshal.PtrToStringAnsi((IntPtr)pCallbackData->PMessage); + var msg = Marshal.PtrToStringAnsi((nint)pCallbackData->PMessage); if (messageSeverity.HasFlag(DebugUtilsMessageSeverityFlagsEXT.ErrorBitExt)) { diff --git a/src/Ryujinx.Graphics.Vulkan/VulkanException.cs b/src/Ryujinx.Graphics.Vulkan/VulkanException.cs index 0d4036802..e203a3a21 100644 --- a/src/Ryujinx.Graphics.Vulkan/VulkanException.cs +++ b/src/Ryujinx.Graphics.Vulkan/VulkanException.cs @@ -6,10 +6,16 @@ namespace Ryujinx.Graphics.Vulkan { static class ResultExtensions { + public static bool IsError(this Result result) + { + // Only negative result codes are errors. + return result < Result.Success; + } + public static void ThrowOnError(this Result result) { // Only negative result codes are errors. - if ((int)result < (int)Result.Success) + if (result.IsError()) { throw new VulkanException(result); } diff --git a/src/Ryujinx.Graphics.Vulkan/VulkanInitialization.cs b/src/Ryujinx.Graphics.Vulkan/VulkanInitialization.cs index dd7bcf10f..352f271cc 100644 --- a/src/Ryujinx.Graphics.Vulkan/VulkanInitialization.cs +++ b/src/Ryujinx.Graphics.Vulkan/VulkanInitialization.cs @@ -42,6 +42,10 @@ namespace Ryujinx.Graphics.Vulkan "VK_EXT_depth_clip_control", "VK_KHR_portability_subset", // As per spec, we should enable this if present. "VK_EXT_4444_formats", + "VK_KHR_8bit_storage", + "VK_KHR_maintenance2", + "VK_EXT_attachment_feedback_loop_layout", + "VK_EXT_attachment_feedback_loop_dynamic_state", }; private static readonly string[] _requiredExtensions = { @@ -90,8 +94,8 @@ namespace Ryujinx.Graphics.Vulkan ApiVersion = _maximumVulkanVersion, }; - IntPtr* ppEnabledExtensions = stackalloc IntPtr[enabledExtensions.Length]; - IntPtr* ppEnabledLayers = stackalloc IntPtr[enabledLayers.Count]; + nint* ppEnabledExtensions = stackalloc nint[enabledExtensions.Length]; + nint* ppEnabledLayers = stackalloc nint[enabledLayers.Count]; for (int i = 0; i < enabledExtensions.Length; i++) { @@ -136,7 +140,7 @@ namespace Ryujinx.Graphics.Vulkan { instance.EnumeratePhysicalDevices(out var physicalDevices).ThrowOnError(); - // First we try to pick the the user preferred GPU. + // First we try to pick the user preferred GPU. for (int i = 0; i < physicalDevices.Length; i++) { if (IsPreferredAndSuitableDevice(api, physicalDevices[i], surface, preferredGpuId)) @@ -355,6 +359,36 @@ namespace Ryujinx.Graphics.Vulkan features2.PNext = &supportedFeaturesDepthClipControl; } + PhysicalDeviceAttachmentFeedbackLoopLayoutFeaturesEXT supportedFeaturesAttachmentFeedbackLoopLayout = new() + { + SType = StructureType.PhysicalDeviceAttachmentFeedbackLoopLayoutFeaturesExt, + PNext = features2.PNext, + }; + + if (physicalDevice.IsDeviceExtensionPresent("VK_EXT_attachment_feedback_loop_layout")) + { + features2.PNext = &supportedFeaturesAttachmentFeedbackLoopLayout; + } + + PhysicalDeviceAttachmentFeedbackLoopDynamicStateFeaturesEXT supportedFeaturesDynamicAttachmentFeedbackLoopLayout = new() + { + SType = StructureType.PhysicalDeviceAttachmentFeedbackLoopDynamicStateFeaturesExt, + PNext = features2.PNext, + }; + + if (physicalDevice.IsDeviceExtensionPresent("VK_EXT_attachment_feedback_loop_dynamic_state")) + { + features2.PNext = &supportedFeaturesDynamicAttachmentFeedbackLoopLayout; + } + + PhysicalDeviceVulkan12Features supportedPhysicalDeviceVulkan12Features = new() + { + SType = StructureType.PhysicalDeviceVulkan12Features, + PNext = features2.PNext, + }; + + features2.PNext = &supportedPhysicalDeviceVulkan12Features; + api.GetPhysicalDeviceFeatures2(physicalDevice.PhysicalDevice, &features2); var supportedFeatures = features2.Features; @@ -382,6 +416,7 @@ namespace Ryujinx.Graphics.Vulkan TessellationShader = supportedFeatures.TessellationShader, VertexPipelineStoresAndAtomics = supportedFeatures.VertexPipelineStoresAndAtomics, RobustBufferAccess = useRobustBufferAccess, + SampleRateShading = supportedFeatures.SampleRateShading, }; void* pExtendedFeatures = null; @@ -451,9 +486,11 @@ namespace Ryujinx.Graphics.Vulkan { SType = StructureType.PhysicalDeviceVulkan12Features, PNext = pExtendedFeatures, - DescriptorIndexing = physicalDevice.IsDeviceExtensionPresent("VK_EXT_descriptor_indexing"), - DrawIndirectCount = physicalDevice.IsDeviceExtensionPresent(KhrDrawIndirectCount.ExtensionName), - UniformBufferStandardLayout = physicalDevice.IsDeviceExtensionPresent("VK_KHR_uniform_buffer_standard_layout"), + DescriptorIndexing = supportedPhysicalDeviceVulkan12Features.DescriptorIndexing, + DrawIndirectCount = supportedPhysicalDeviceVulkan12Features.DrawIndirectCount, + UniformBufferStandardLayout = supportedPhysicalDeviceVulkan12Features.UniformBufferStandardLayout, + UniformAndStorageBuffer8BitAccess = supportedPhysicalDeviceVulkan12Features.UniformAndStorageBuffer8BitAccess, + StorageBuffer8BitAccess = supportedPhysicalDeviceVulkan12Features.StorageBuffer8BitAccess, }; pExtendedFeatures = &featuresVk12; @@ -486,20 +523,6 @@ namespace Ryujinx.Graphics.Vulkan pExtendedFeatures = &featuresFragmentShaderInterlock; } - PhysicalDeviceSubgroupSizeControlFeaturesEXT featuresSubgroupSizeControl; - - if (physicalDevice.IsDeviceExtensionPresent("VK_EXT_subgroup_size_control")) - { - featuresSubgroupSizeControl = new PhysicalDeviceSubgroupSizeControlFeaturesEXT - { - SType = StructureType.PhysicalDeviceSubgroupSizeControlFeaturesExt, - PNext = pExtendedFeatures, - SubgroupSizeControl = true, - }; - - pExtendedFeatures = &featuresSubgroupSizeControl; - } - PhysicalDeviceCustomBorderColorFeaturesEXT featuresCustomBorderColor; if (physicalDevice.IsDeviceExtensionPresent("VK_EXT_custom_border_color") && @@ -532,9 +555,39 @@ namespace Ryujinx.Graphics.Vulkan pExtendedFeatures = &featuresDepthClipControl; } + PhysicalDeviceAttachmentFeedbackLoopLayoutFeaturesEXT featuresAttachmentFeedbackLoopLayout; + + if (physicalDevice.IsDeviceExtensionPresent("VK_EXT_attachment_feedback_loop_layout") && + supportedFeaturesAttachmentFeedbackLoopLayout.AttachmentFeedbackLoopLayout) + { + featuresAttachmentFeedbackLoopLayout = new() + { + SType = StructureType.PhysicalDeviceAttachmentFeedbackLoopLayoutFeaturesExt, + PNext = pExtendedFeatures, + AttachmentFeedbackLoopLayout = true, + }; + + pExtendedFeatures = &featuresAttachmentFeedbackLoopLayout; + } + + PhysicalDeviceAttachmentFeedbackLoopDynamicStateFeaturesEXT featuresDynamicAttachmentFeedbackLoopLayout; + + if (physicalDevice.IsDeviceExtensionPresent("VK_EXT_attachment_feedback_loop_dynamic_state") && + supportedFeaturesDynamicAttachmentFeedbackLoopLayout.AttachmentFeedbackLoopDynamicState) + { + featuresDynamicAttachmentFeedbackLoopLayout = new() + { + SType = StructureType.PhysicalDeviceAttachmentFeedbackLoopDynamicStateFeaturesExt, + PNext = pExtendedFeatures, + AttachmentFeedbackLoopDynamicState = true, + }; + + pExtendedFeatures = &featuresDynamicAttachmentFeedbackLoopLayout; + } + var enabledExtensions = _requiredExtensions.Union(_desirableExtensions.Intersect(physicalDevice.DeviceExtensions)).ToArray(); - IntPtr* ppEnabledExtensions = stackalloc IntPtr[enabledExtensions.Length]; + nint* ppEnabledExtensions = stackalloc nint[enabledExtensions.Length]; for (int i = 0; i < enabledExtensions.Length; i++) { diff --git a/src/Ryujinx.Graphics.Vulkan/VulkanInstance.cs b/src/Ryujinx.Graphics.Vulkan/VulkanInstance.cs index 843d34125..69b75925e 100644 --- a/src/Ryujinx.Graphics.Vulkan/VulkanInstance.cs +++ b/src/Ryujinx.Graphics.Vulkan/VulkanInstance.cs @@ -22,7 +22,7 @@ namespace Ryujinx.Graphics.Vulkan _api = api; Instance = instance; - if (api.GetInstanceProcAddr(instance, "vkEnumerateInstanceVersion") == IntPtr.Zero) + if (api.GetInstanceProcAddr(instance, "vkEnumerateInstanceVersion") == nint.Zero) { InstanceVersion = Vk.Version10; } @@ -94,7 +94,7 @@ namespace Ryujinx.Graphics.Vulkan unsafe { - return extensionProperties.Select(x => Marshal.PtrToStringAnsi((IntPtr)x.ExtensionName)).ToImmutableHashSet(); + return extensionProperties.Select(x => Marshal.PtrToStringAnsi((nint)x.ExtensionName)).ToImmutableHashSet(); } } @@ -110,7 +110,7 @@ namespace Ryujinx.Graphics.Vulkan unsafe { - return layerProperties.Select(x => Marshal.PtrToStringAnsi((IntPtr)x.LayerName)).ToImmutableHashSet(); + return layerProperties.Select(x => Marshal.PtrToStringAnsi((nint)x.LayerName)).ToImmutableHashSet(); } } diff --git a/src/Ryujinx.Graphics.Vulkan/VulkanPhysicalDevice.cs b/src/Ryujinx.Graphics.Vulkan/VulkanPhysicalDevice.cs index 547f36543..b3f8fd756 100644 --- a/src/Ryujinx.Graphics.Vulkan/VulkanPhysicalDevice.cs +++ b/src/Ryujinx.Graphics.Vulkan/VulkanPhysicalDevice.cs @@ -31,7 +31,7 @@ namespace Ryujinx.Graphics.Vulkan unsafe { - DeviceName = Marshal.PtrToStringAnsi((IntPtr)physicalDeviceProperties.DeviceName); + DeviceName = Marshal.PtrToStringAnsi((nint)physicalDeviceProperties.DeviceName); } uint propertiesCount = 0; @@ -50,7 +50,7 @@ namespace Ryujinx.Graphics.Vulkan unsafe { - DeviceExtensions = extensionProperties.Select(x => Marshal.PtrToStringAnsi((IntPtr)x.ExtensionName)).ToImmutableHashSet(); + DeviceExtensions = extensionProperties.Select(x => Marshal.PtrToStringAnsi((nint)x.ExtensionName)).ToImmutableHashSet(); } } @@ -58,6 +58,33 @@ namespace Ryujinx.Graphics.Vulkan public bool IsDeviceExtensionPresent(string extension) => DeviceExtensions.Contains(extension); + public unsafe bool TryGetPhysicalDeviceDriverPropertiesKHR(Vk api, out PhysicalDeviceDriverPropertiesKHR res) + { + if (!IsDeviceExtensionPresent("VK_KHR_driver_properties")) + { + res = default; + + return false; + } + + PhysicalDeviceDriverPropertiesKHR physicalDeviceDriverProperties = new() + { + SType = StructureType.PhysicalDeviceDriverPropertiesKhr + }; + + PhysicalDeviceProperties2 physicalDeviceProperties2 = new() + { + SType = StructureType.PhysicalDeviceProperties2, + PNext = &physicalDeviceDriverProperties + }; + + api.GetPhysicalDeviceProperties2(PhysicalDevice, &physicalDeviceProperties2); + + res = physicalDeviceDriverProperties; + + return true; + } + public DeviceInfo ToDeviceInfo() { return new DeviceInfo( diff --git a/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs b/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs index 3a2b104ce..cc2bc36c2 100644 --- a/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs +++ b/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs @@ -27,6 +27,8 @@ namespace Ryujinx.Graphics.Vulkan private bool _initialized; + public uint ProgramCount { get; set; } = 0; + internal FormatCapabilities FormatCapabilities { get; private set; } internal HardwareCapabilities Capabilities; @@ -38,6 +40,7 @@ namespace Ryujinx.Graphics.Vulkan internal KhrPushDescriptor PushDescriptorApi { get; private set; } internal ExtTransformFeedback TransformFeedbackApi { get; private set; } internal KhrDrawIndirectCount DrawIndirectCountApi { get; private set; } + internal ExtAttachmentFeedbackLoopDynamicState DynamicFeedbackLoopApi { get; private set; } internal uint QueueFamilyIndex { get; private set; } internal Queue Queue { get; private set; } @@ -48,7 +51,6 @@ namespace Ryujinx.Graphics.Vulkan internal MemoryAllocator MemoryAllocator { get; private set; } internal HostMemoryAllocator HostMemoryAllocator { get; private set; } internal CommandBufferPool CommandBufferPool { get; private set; } - internal DescriptorSetManager DescriptorSetManager { get; private set; } internal PipelineLayoutCache PipelineLayoutCache { get; private set; } internal BackgroundResources BackgroundResources { get; private set; } internal Action InterruptAction { get; private set; } @@ -68,6 +70,8 @@ namespace Ryujinx.Graphics.Vulkan internal HelperShader HelperShader { get; private set; } internal PipelineFull PipelineInternal => _pipeline; + internal BarrierBatch Barriers { get; private set; } + public IPipeline Pipeline => _pipeline; public IWindow Window => _window; @@ -76,14 +80,23 @@ namespace Ryujinx.Graphics.Vulkan private readonly Func _getRequiredExtensions; private readonly string _preferredGpuId; + private int[] _pdReservedBindings; + private readonly static int[] _pdReservedBindingsNvn = { 3, 18, 21, 36, 30 }; + private readonly static int[] _pdReservedBindingsOgl = { 17, 18, 34, 35, 36 }; + internal Vendor Vendor { get; private set; } internal bool IsAmdWindows { get; private set; } internal bool IsIntelWindows { get; private set; } internal bool IsAmdGcn { get; private set; } + internal bool IsNvidiaPreTuring { get; private set; } + internal bool IsIntelArc { get; private set; } + internal bool IsQualcommProprietary { get; private set; } internal bool IsMoltenVk { get; private set; } internal bool IsTBDR { get; private set; } internal bool IsSharedMemory { get; private set; } + public string GpuVendor { get; private set; } + public string GpuDriver { get; private set; } public string GpuRenderer { get; private set; } public string GpuVersion { get; private set; } @@ -91,25 +104,27 @@ namespace Ryujinx.Graphics.Vulkan public event EventHandler ScreenCaptured; - public VulkanRenderer(Vk api, Func surfaceFunc, Func requiredExtensionsFunc, string preferredGpuId) + public VulkanRenderer(Vk api, Func getSurface, Func requiredExtensionsFunc, string preferredGpuId) { - _getSurface = surfaceFunc; + _getSurface = getSurface; _getRequiredExtensions = requiredExtensionsFunc; _preferredGpuId = preferredGpuId; Api = api; - Shaders = new HashSet(); - Textures = new HashSet(); - Samplers = new HashSet(); + Shaders = []; + Textures = []; + Samplers = []; - if (OperatingSystem.IsMacOS() || OperatingSystem.IsIOS()) - { + // Any device running on MacOS is using MoltenVK, even Intel and AMD vendors. + if (IsMoltenVk = OperatingSystem.IsMacOS()) MVKInitialization.Initialize(); - - // Any device running on MacOS is using MoltenVK, even Intel and AMD vendors. - IsMoltenVk = true; - } } + public static VulkanRenderer Create( + string preferredGpuId, + Func getSurface, + Func getRequiredExtensions + ) => new(Vk.GetApi(), getSurface, getRequiredExtensions, preferredGpuId); + private unsafe void LoadFeatures(uint maxQueueCount, uint queueFamilyIndex) { FormatCapabilities = new FormatCapabilities(Api, _physicalDevice.PhysicalDevice); @@ -139,6 +154,11 @@ namespace Ryujinx.Graphics.Vulkan DrawIndirectCountApi = drawIndirectCountApi; } + if (Api.TryGetDeviceExtension(_instance.Instance, _device, out ExtAttachmentFeedbackLoopDynamicState dynamicFeedbackLoopApi)) + { + DynamicFeedbackLoopApi = dynamicFeedbackLoopApi; + } + if (maxQueueCount >= 2) { Api.GetDeviceQueue(_device, queueFamilyIndex, 1, out var backgroundQueue); @@ -190,6 +210,19 @@ namespace Ryujinx.Graphics.Vulkan SType = StructureType.PhysicalDevicePortabilitySubsetPropertiesKhr, }; + bool supportsPushDescriptors = _physicalDevice.IsDeviceExtensionPresent(KhrPushDescriptor.ExtensionName); + + PhysicalDevicePushDescriptorPropertiesKHR propertiesPushDescriptor = new PhysicalDevicePushDescriptorPropertiesKHR() + { + SType = StructureType.PhysicalDevicePushDescriptorPropertiesKhr + }; + + if (supportsPushDescriptors) + { + propertiesPushDescriptor.PNext = properties2.PNext; + properties2.PNext = &propertiesPushDescriptor; + } + PhysicalDeviceFeatures2 features2 = new() { SType = StructureType.PhysicalDeviceFeatures2, @@ -220,6 +253,16 @@ namespace Ryujinx.Graphics.Vulkan SType = StructureType.PhysicalDeviceDepthClipControlFeaturesExt, }; + PhysicalDeviceAttachmentFeedbackLoopLayoutFeaturesEXT featuresAttachmentFeedbackLoop = new() + { + SType = StructureType.PhysicalDeviceAttachmentFeedbackLoopLayoutFeaturesExt, + }; + + PhysicalDeviceAttachmentFeedbackLoopDynamicStateFeaturesEXT featuresDynamicAttachmentFeedbackLoop = new() + { + SType = StructureType.PhysicalDeviceAttachmentFeedbackLoopDynamicStateFeaturesExt, + }; + PhysicalDevicePortabilitySubsetFeaturesKHR featuresPortabilitySubset = new() { SType = StructureType.PhysicalDevicePortabilitySubsetFeaturesKhr, @@ -256,6 +299,22 @@ namespace Ryujinx.Graphics.Vulkan features2.PNext = &featuresDepthClipControl; } + bool supportsAttachmentFeedbackLoop = _physicalDevice.IsDeviceExtensionPresent("VK_EXT_attachment_feedback_loop_layout"); + + if (supportsAttachmentFeedbackLoop) + { + featuresAttachmentFeedbackLoop.PNext = features2.PNext; + features2.PNext = &featuresAttachmentFeedbackLoop; + } + + bool supportsDynamicAttachmentFeedbackLoop = _physicalDevice.IsDeviceExtensionPresent("VK_EXT_attachment_feedback_loop_dynamic_state"); + + if (supportsDynamicAttachmentFeedbackLoop) + { + featuresDynamicAttachmentFeedbackLoop.PNext = features2.PNext; + features2.PNext = &featuresDynamicAttachmentFeedbackLoop; + } + bool usePortability = _physicalDevice.IsDeviceExtensionPresent("VK_KHR_portability_subset"); if (usePortability) @@ -289,6 +348,52 @@ namespace Ryujinx.Graphics.Vulkan ref var properties = ref properties2.Properties; + var hasDriverProperties = _physicalDevice.TryGetPhysicalDeviceDriverPropertiesKHR(Api, out var driverProperties); + + Vendor = VendorUtils.FromId(properties.VendorID); + + IsAmdWindows = Vendor == Vendor.Amd && OperatingSystem.IsWindows(); + IsIntelWindows = Vendor == Vendor.Intel && OperatingSystem.IsWindows(); + IsTBDR = + Vendor == Vendor.Apple || + Vendor == Vendor.Qualcomm || + Vendor == Vendor.ARM || + Vendor == Vendor.Broadcom || + Vendor == Vendor.ImgTec; + + GpuVendor = VendorUtils.GetNameFromId(properties.VendorID); + GpuDriver = hasDriverProperties && !OperatingSystem.IsMacOS() ? + VendorUtils.GetFriendlyDriverName(driverProperties.DriverID) : GpuVendor; // Fallback to vendor name if driver is unavailable or on MacOS where vendor is preferred. + + fixed (byte* deviceName = properties.DeviceName) + { + GpuRenderer = Marshal.PtrToStringAnsi((nint)deviceName); + } + + GpuVersion = $"Vulkan v{ParseStandardVulkanVersion(properties.ApiVersion)}, Driver v{ParseDriverVersion(ref properties)}"; + + IsAmdGcn = !IsMoltenVk && Vendor == Vendor.Amd && VendorUtils.AmdGcnRegex().IsMatch(GpuRenderer); + + if (Vendor == Vendor.Nvidia) + { + var match = VendorUtils.NvidiaConsumerClassRegex().Match(GpuRenderer); + + if (match != null && int.TryParse(match.Groups[2].Value, out int gpuNumber)) + { + IsNvidiaPreTuring = gpuNumber < 2000; + } + else if (GpuRenderer.Contains("TITAN") && !GpuRenderer.Contains("RTX")) + { + IsNvidiaPreTuring = true; + } + } + else if (Vendor == Vendor.Intel) + { + IsIntelArc = GpuRenderer.StartsWith("Intel(R) Arc(TM)"); + } + + IsQualcommProprietary = hasDriverProperties && driverProperties.DriverID == DriverId.QualcommProprietary; + ulong minResourceAlignment = Math.Max( Math.Max( properties.Limits.MinStorageBufferOffsetAlignment, @@ -318,8 +423,9 @@ namespace Ryujinx.Graphics.Vulkan _physicalDevice.IsDeviceExtensionPresent(ExtConditionalRendering.ExtensionName), _physicalDevice.IsDeviceExtensionPresent(ExtExtendedDynamicState.ExtensionName), features2.Features.MultiViewport && !(IsMoltenVk && Vendor == Vendor.Amd), // Workaround for AMD on MoltenVK issue - featuresRobustness2.NullDescriptor || !IsMoltenVk, - _physicalDevice.IsDeviceExtensionPresent(KhrPushDescriptor.ExtensionName), + featuresRobustness2.NullDescriptor || IsMoltenVk, + supportsPushDescriptors && !IsMoltenVk, + propertiesPushDescriptor.MaxPushDescriptors, featuresPrimitiveTopologyListRestart.PrimitiveTopologyListRestart, featuresPrimitiveTopologyListRestart.PrimitiveTopologyPatchListRestart, supportsTransformFeedback, @@ -331,6 +437,8 @@ namespace Ryujinx.Graphics.Vulkan _physicalDevice.IsDeviceExtensionPresent("VK_NV_viewport_array2"), _physicalDevice.IsDeviceExtensionPresent(ExtExternalMemoryHost.ExtensionName), supportsDepthClipControl && featuresDepthClipControl.DepthClipControl, + supportsAttachmentFeedbackLoop && featuresAttachmentFeedbackLoop.AttachmentFeedbackLoopLayout, + supportsDynamicAttachmentFeedbackLoop && featuresDynamicAttachmentFeedbackLoop.AttachmentFeedbackLoopDynamicState, propertiesSubgroup.SubgroupSize, supportedSampleCounts, portabilityFlags, @@ -345,9 +453,7 @@ namespace Ryujinx.Graphics.Vulkan Api.TryGetDeviceExtension(_instance.Instance, _device, out ExtExternalMemoryHost hostMemoryApi); HostMemoryAllocator = new HostMemoryAllocator(MemoryAllocator, Api, hostMemoryApi, _device); - CommandBufferPool = new CommandBufferPool(Api, _device, Queue, QueueLock, queueFamilyIndex); - - DescriptorSetManager = new DescriptorSetManager(_device, PipelineBase.DescriptorSetLayouts); + CommandBufferPool = new CommandBufferPool(Api, _device, Queue, QueueLock, queueFamilyIndex, IsQualcommProprietary); PipelineLayoutCache = new PipelineLayoutCache(); @@ -361,6 +467,8 @@ namespace Ryujinx.Graphics.Vulkan HelperShader = new HelperShader(this, _device); + Barriers = new BarrierBatch(this); + _counters = new Counters(this, _device, _pipeline); } @@ -399,14 +507,28 @@ namespace Ryujinx.Graphics.Vulkan _initialized = true; } - public BufferHandle CreateBuffer(int size, BufferAccess access) + internal int[] GetPushDescriptorReservedBindings(bool isOgl) { - return BufferManager.CreateWithHandle(this, size, access.HasFlag(BufferAccess.SparseCompatible), access.Convert(), default, access == BufferAccess.Stream); + // The first call of this method determines what push descriptor layout is used for all shaders on this renderer. + // This is chosen to minimize shaders that can't fit their uniforms on the device's max number of push descriptors. + if (_pdReservedBindings == null) + { + if (Capabilities.MaxPushDescriptors <= Constants.MaxUniformBuffersPerStage * 2) + { + _pdReservedBindings = isOgl ? _pdReservedBindingsOgl : _pdReservedBindingsNvn; + } + else + { + _pdReservedBindings = Array.Empty(); + } + } + + return _pdReservedBindings; } - public BufferHandle CreateBuffer(int size, BufferAccess access, BufferHandle storageHint) + public BufferHandle CreateBuffer(int size, BufferAccess access) { - return BufferManager.CreateWithHandle(this, size, access.HasFlag(BufferAccess.SparseCompatible), access.Convert(), storageHint); + return BufferManager.CreateWithHandle(this, size, access.HasFlag(BufferAccess.SparseCompatible), access.Convert(), access.HasFlag(BufferAccess.Stream)); } public BufferHandle CreateBuffer(nint pointer, int size) @@ -419,8 +541,15 @@ namespace Ryujinx.Graphics.Vulkan return BufferManager.CreateSparse(this, storageBuffers); } + public IImageArray CreateImageArray(int size, bool isBuffer) + { + return new ImageArray(this, size, isBuffer); + } + public IProgram CreateProgram(ShaderSource[] sources, ShaderInfo info) { + ProgramCount++; + bool isCompute = sources.Length == 1 && sources[0].Stage == ShaderStage.Compute; if (info.State.HasValue || isCompute) @@ -451,6 +580,11 @@ namespace Ryujinx.Graphics.Vulkan return CreateTextureView(info); } + public ITextureArray CreateTextureArray(int size, bool isBuffer) + { + return new TextureArray(this, size, isBuffer); + } + internal TextureView CreateTextureView(TextureCreateInfo info) { // This should be disposed when all views are destroyed. @@ -580,11 +714,25 @@ namespace Ryujinx.Graphics.Vulkan var limits = _physicalDevice.PhysicalDeviceProperties.Limits; var mainQueueProperties = _physicalDevice.QueueFamilyProperties[QueueFamilyIndex]; + SystemMemoryType memoryType; + + if (IsSharedMemory) + { + memoryType = SystemMemoryType.UnifiedMemory; + } + else + { + memoryType = Vendor == Vendor.Nvidia ? + SystemMemoryType.DedicatedMemorySlowStorage : + SystemMemoryType.DedicatedMemory; + } + return new Capabilities( api: TargetApi.Vulkan, GpuVendor, + memoryType: memoryType, hasFrontFacingBug: IsIntelWindows, - hasVectorIndexingBug: Vendor == Vendor.Qualcomm, + hasVectorIndexingBug: IsQualcommProprietary, needsFragmentOutputSpecialization: IsMoltenVk, reduceShaderPrecision: IsMoltenVk, supportsAstcCompression: features2.Features.TextureCompressionAstcLdr && supportsAstcFormats, @@ -596,6 +744,7 @@ namespace Ryujinx.Graphics.Vulkan supportsBgraFormat: true, supportsR4G4Format: false, supportsR4G4B4A4Format: supportsR4G4B4A4Format, + supportsScaledVertexFormats: FormatCapabilities.SupportsScaledVertexFormats(), supportsSnormBufferTextureFormat: true, supports5BitComponentFormat: supports5BitComponentFormat, supportsSparseBuffer: features2.Features.SparseBinding && mainQueueProperties.QueueFlags.HasFlag(QueueFlags.SparseBindingBit), @@ -610,7 +759,8 @@ namespace Ryujinx.Graphics.Vulkan supportsMismatchingViewFormat: true, supportsCubemapView: !IsAmdGcn, supportsNonConstantTextureOffset: false, - supportsScaledVertexFormats: FormatCapabilities.SupportsScaledVertexFormats(), + supportsQuads: false, + supportsSeparateSampler: true, supportsShaderBallot: false, supportsShaderBarrierDivergence: Vendor != Vendor.Intel, supportsShaderFloat64: Capabilities.SupportsShaderFloat64, @@ -622,6 +772,12 @@ namespace Ryujinx.Graphics.Vulkan supportsViewportSwizzle: false, supportsIndirectParameters: true, supportsDepthClipControl: Capabilities.SupportsDepthClipControl, + uniformBufferSetIndex: PipelineBase.UniformSetIndex, + storageBufferSetIndex: PipelineBase.StorageSetIndex, + textureSetIndex: PipelineBase.TextureSetIndex, + imageSetIndex: PipelineBase.ImageSetIndex, + extraSetBaseIndex: PipelineBase.DescriptorSetLayouts, + maximumExtraSets: Math.Max(0, (int)limits.MaxBoundDescriptorSets - PipelineBase.DescriptorSetLayouts), maximumUniformBuffersPerStage: Constants.MaxUniformBuffersPerStage, maximumStorageBuffersPerStage: Constants.MaxStorageBuffersPerStage, maximumTexturesPerStage: Constants.MaxTexturesPerStage, @@ -631,12 +787,31 @@ namespace Ryujinx.Graphics.Vulkan shaderSubgroupSize: (int)Capabilities.SubgroupSize, storageBufferOffsetAlignment: (int)limits.MinStorageBufferOffsetAlignment, textureBufferOffsetAlignment: (int)limits.MinTexelBufferOffsetAlignment, - gatherBiasPrecision: IsIntelWindows || IsAmdWindows ? (int)Capabilities.SubTexelPrecisionBits : 0); + gatherBiasPrecision: IsIntelWindows || IsAmdWindows ? (int)Capabilities.SubTexelPrecisionBits : 0, + maximumGpuMemory: GetTotalGPUMemory()); + } + + private ulong GetTotalGPUMemory() + { + ulong totalMemory = 0; + + Api.GetPhysicalDeviceMemoryProperties(_physicalDevice.PhysicalDevice, out PhysicalDeviceMemoryProperties memoryProperties); + + for (int i = 0; i < memoryProperties.MemoryHeapCount; i++) + { + var heap = memoryProperties.MemoryHeaps[i]; + if ((heap.Flags & MemoryHeapFlags.DeviceLocalBit) == MemoryHeapFlags.DeviceLocalBit) + { + totalMemory += heap.Size; + } + } + + return totalMemory; } public HardwareInfo GetHardwareInfo() { - return new HardwareInfo(GpuVendor, GpuRenderer); + return new HardwareInfo(GpuVendor, GpuRenderer, GpuDriver); } /// @@ -689,39 +864,15 @@ namespace Ryujinx.Graphics.Vulkan return ParseStandardVulkanVersion(driverVersionRaw); } - private unsafe void PrintGpuInformation() - { - var properties = _physicalDevice.PhysicalDeviceProperties; - - string vendorName = VendorUtils.GetNameFromId(properties.VendorID); - - Vendor = VendorUtils.FromId(properties.VendorID); - - IsAmdWindows = Vendor == Vendor.Amd && OperatingSystem.IsWindows(); - IsIntelWindows = Vendor == Vendor.Intel && OperatingSystem.IsWindows(); - IsTBDR = - Vendor == Vendor.Apple || - Vendor == Vendor.Qualcomm || - Vendor == Vendor.ARM || - Vendor == Vendor.Broadcom || - Vendor == Vendor.ImgTec; - - GpuVendor = vendorName; - GpuRenderer = Marshal.PtrToStringAnsi((IntPtr)properties.DeviceName); - GpuVersion = $"Vulkan v{ParseStandardVulkanVersion(properties.ApiVersion)}, Driver v{ParseDriverVersion(ref properties)}"; - - IsAmdGcn = !IsMoltenVk && Vendor == Vendor.Amd && VendorUtils.AmdGcnRegex().IsMatch(GpuRenderer); - - Logger.Notice.Print(LogClass.Gpu, $"{GpuVendor} {GpuRenderer} ({GpuVersion})"); - } - internal PrimitiveTopology TopologyRemap(PrimitiveTopology topology) { return topology switch { PrimitiveTopology.Quads => PrimitiveTopology.Triangles, PrimitiveTopology.QuadStrip => PrimitiveTopology.TriangleStrip, - PrimitiveTopology.TriangleFan => Capabilities.PortabilitySubset.HasFlag(PortabilitySubsetFlags.NoTriangleFans) ? PrimitiveTopology.Triangles : topology, + PrimitiveTopology.TriangleFan or PrimitiveTopology.Polygon => Capabilities.PortabilitySubset.HasFlag(PortabilitySubsetFlags.NoTriangleFans) + ? PrimitiveTopology.Triangles + : topology, _ => topology, }; } @@ -731,11 +882,17 @@ namespace Ryujinx.Graphics.Vulkan return topology switch { PrimitiveTopology.Quads => true, - PrimitiveTopology.TriangleFan => Capabilities.PortabilitySubset.HasFlag(PortabilitySubsetFlags.NoTriangleFans), + PrimitiveTopology.TriangleFan or PrimitiveTopology.Polygon => Capabilities.PortabilitySubset.HasFlag(PortabilitySubsetFlags.NoTriangleFans), _ => false, }; } + private void PrintGpuInformation() + { + Logger.Notice.Print(LogClass.Gpu, $"{GpuVendor} {GpuRenderer} ({GpuVersion})"); + Logger.Notice.Print(LogClass.Gpu, $"GPU Memory: {GetTotalGPUMemory() / (1024 * 1024)} MiB"); + } + public void Initialize(GraphicsDebugLevel logLevel) { SetupContext(logLevel); @@ -784,7 +941,7 @@ namespace Ryujinx.Graphics.Vulkan public void SetBufferData(BufferHandle buffer, int offset, ReadOnlySpan data) { - BufferManager.SetData(buffer, offset, data, _pipeline.CurrentCommandBuffer, _pipeline.EndRenderPass); + BufferManager.SetData(buffer, offset, data, _pipeline.CurrentCommandBuffer, _pipeline.EndRenderPassDelegate); } public void UpdateCounters() @@ -842,6 +999,11 @@ namespace Ryujinx.Graphics.Vulkan ScreenCaptured?.Invoke(this, bitmap); } + public bool SupportsRenderPassBarrier(PipelineStageFlags flags) + { + return !(IsMoltenVk || IsQualcommProprietary); + } + public unsafe void Dispose() { if (!_initialized) @@ -856,8 +1018,8 @@ namespace Ryujinx.Graphics.Vulkan HelperShader.Dispose(); _pipeline.Dispose(); BufferManager.Dispose(); - DescriptorSetManager.Dispose(); PipelineLayoutCache.Dispose(); + Barriers.Dispose(); MemoryAllocator.Dispose(); diff --git a/src/Ryujinx.Graphics.Vulkan/Window.cs b/src/Ryujinx.Graphics.Vulkan/Window.cs index 2c5764a99..3e8d3b375 100644 --- a/src/Ryujinx.Graphics.Vulkan/Window.cs +++ b/src/Ryujinx.Graphics.Vulkan/Window.cs @@ -20,7 +20,7 @@ namespace Ryujinx.Graphics.Vulkan private SwapchainKHR _swapchain; private Image[] _swapchainImages; - private Auto[] _swapchainImageViews; + private TextureView[] _swapchainImageViews; private Semaphore[] _imageAvailableSemaphores; private Semaphore[] _renderFinishedSemaphores; @@ -29,7 +29,7 @@ namespace Ryujinx.Graphics.Vulkan private int _width; private int _height; - private bool _vsyncEnabled; + private VSyncMode _vSyncMode; private bool _swapchainIsDirty; private VkFormat _format; private AntiAliasing _currentAntiAliasing; @@ -139,11 +139,28 @@ namespace Ryujinx.Graphics.Vulkan ImageArrayLayers = 1, PreTransform = capabilities.CurrentTransform, CompositeAlpha = ChooseCompositeAlpha(capabilities.SupportedCompositeAlpha), - PresentMode = ChooseSwapPresentMode(presentModes, _vsyncEnabled), + PresentMode = ChooseSwapPresentMode(presentModes, _vSyncMode), Clipped = true, }; - _gd.SwapchainApi.CreateSwapchain(_device, swapchainCreateInfo, null, out _swapchain).ThrowOnError(); + var textureCreateInfo = new TextureCreateInfo( + _width, + _height, + 1, + 1, + 1, + 1, + 1, + 1, + FormatTable.GetFormat(surfaceFormat.Format), + DepthStencilMode.Depth, + Target.Texture2D, + SwizzleComponent.Red, + SwizzleComponent.Green, + SwizzleComponent.Blue, + SwizzleComponent.Alpha); + + _gd.SwapchainApi.CreateSwapchain(_device, in swapchainCreateInfo, null, out _swapchain).ThrowOnError(); _gd.SwapchainApi.GetSwapchainImages(_device, _swapchain, &imageCount, null); @@ -154,11 +171,11 @@ namespace Ryujinx.Graphics.Vulkan _gd.SwapchainApi.GetSwapchainImages(_device, _swapchain, &imageCount, pSwapchainImages); } - _swapchainImageViews = new Auto[imageCount]; + _swapchainImageViews = new TextureView[imageCount]; for (int i = 0; i < _swapchainImageViews.Length; i++) { - _swapchainImageViews[i] = CreateSwapchainImageView(_swapchainImages[i], surfaceFormat.Format); + _swapchainImageViews[i] = CreateSwapchainImageView(_swapchainImages[i], surfaceFormat.Format, textureCreateInfo); } var semaphoreCreateInfo = new SemaphoreCreateInfo @@ -170,18 +187,18 @@ namespace Ryujinx.Graphics.Vulkan for (int i = 0; i < _imageAvailableSemaphores.Length; i++) { - _gd.Api.CreateSemaphore(_device, semaphoreCreateInfo, null, out _imageAvailableSemaphores[i]).ThrowOnError(); + _gd.Api.CreateSemaphore(_device, in semaphoreCreateInfo, null, out _imageAvailableSemaphores[i]).ThrowOnError(); } _renderFinishedSemaphores = new Semaphore[imageCount]; for (int i = 0; i < _renderFinishedSemaphores.Length; i++) { - _gd.Api.CreateSemaphore(_device, semaphoreCreateInfo, null, out _renderFinishedSemaphores[i]).ThrowOnError(); + _gd.Api.CreateSemaphore(_device, in semaphoreCreateInfo, null, out _renderFinishedSemaphores[i]).ThrowOnError(); } } - private unsafe Auto CreateSwapchainImageView(Image swapchainImage, VkFormat format) + private unsafe TextureView CreateSwapchainImageView(Image swapchainImage, VkFormat format, TextureCreateInfo info) { var componentMapping = new ComponentMapping( ComponentSwizzle.R, @@ -203,8 +220,9 @@ namespace Ryujinx.Graphics.Vulkan SubresourceRange = subresourceRange, }; - _gd.Api.CreateImageView(_device, imageCreateInfo, null, out var imageView).ThrowOnError(); - return new Auto(new DisposableImageView(_gd.Api, _device, imageView)); + _gd.Api.CreateImageView(_device, in imageCreateInfo, null, out var imageView).ThrowOnError(); + + return new TextureView(_gd, _device, new DisposableImageView(_gd.Api, _device, imageView), info, format); } private static SurfaceFormatKHR ChooseSwapSurfaceFormat(SurfaceFormatKHR[] availableFormats, bool colorSpacePassthroughEnabled) @@ -261,9 +279,9 @@ namespace Ryujinx.Graphics.Vulkan } } - private static PresentModeKHR ChooseSwapPresentMode(PresentModeKHR[] availablePresentModes, bool vsyncEnabled) + private static PresentModeKHR ChooseSwapPresentMode(PresentModeKHR[] availablePresentModes, VSyncMode vSyncMode) { - if (!vsyncEnabled && availablePresentModes.Contains(PresentModeKHR.ImmediateKhr)) + if (vSyncMode == VSyncMode.Unbounded && availablePresentModes.Contains(PresentModeKHR.ImmediateKhr)) { return PresentModeKHR.ImmediateKhr; } @@ -312,6 +330,7 @@ namespace Ryujinx.Graphics.Vulkan _swapchainIsDirty) { RecreateSwapchain(); + semaphoreIndex = (_frameIndex - 1) % _imageAvailableSemaphores.Length; } else { @@ -406,7 +425,7 @@ namespace Ryujinx.Graphics.Vulkan _scalingFilter.Run( view, cbs, - _swapchainImageViews[nextImage], + _swapchainImageViews[nextImage].GetImageViewForAttachment(), _format, _width, _height, @@ -421,11 +440,6 @@ namespace Ryujinx.Graphics.Vulkan cbs, view, _swapchainImageViews[nextImage], - _width, - _height, - 1, - _format, - false, new Extents2D(srcX0, srcY0, srcX1, srcY1), new Extents2D(dstX0, dstY1, dstX1, dstY0), _isLinear, @@ -465,7 +479,7 @@ namespace Ryujinx.Graphics.Vulkan lock (_gd.QueueLock) { - _gd.SwapchainApi.QueuePresent(_gd.Queue, presentInfo); + _gd.SwapchainApi.QueuePresent(_gd.Queue, in presentInfo); } } @@ -554,6 +568,13 @@ namespace Ryujinx.Graphics.Vulkan _scalingFilter.Level = _scalingFilterLevel; break; + case ScalingFilter.Area: + if (_scalingFilter is not AreaScalingFilter) + { + _scalingFilter?.Dispose(); + _scalingFilter = new AreaScalingFilter(_gd, _device); + } + break; } } } @@ -597,7 +618,7 @@ namespace Ryujinx.Graphics.Vulkan 0, null, 1, - barrier); + in barrier); } private void CaptureFrame(TextureView texture, int x, int y, int width, int height, bool isBgra, bool flipX, bool flipY) @@ -609,12 +630,14 @@ namespace Ryujinx.Graphics.Vulkan public override void SetSize(int width, int height) { - // Not needed as we can get the size from the surface. + // We don't need to use width and height as we can get the size from the surface. + _swapchainIsDirty = true; } - public override void ChangeVSyncMode(bool vsyncEnabled) + public override void ChangeVSyncMode(VSyncMode vSyncMode) { - _vsyncEnabled = vsyncEnabled; + _vSyncMode = vSyncMode; + //present mode may change, so mark the swapchain for recreation _swapchainIsDirty = true; } diff --git a/src/Ryujinx.Graphics.Vulkan/WindowBase.cs b/src/Ryujinx.Graphics.Vulkan/WindowBase.cs index edb9c688c..ca06ec0b8 100644 --- a/src/Ryujinx.Graphics.Vulkan/WindowBase.cs +++ b/src/Ryujinx.Graphics.Vulkan/WindowBase.cs @@ -10,7 +10,7 @@ namespace Ryujinx.Graphics.Vulkan public abstract void Dispose(); public abstract void Present(ITexture texture, ImageCrop crop, Action swapBuffersCallback); public abstract void SetSize(int width, int height); - public abstract void ChangeVSyncMode(bool vsyncEnabled); + public abstract void ChangeVSyncMode(VSyncMode vSyncMode); public abstract void SetAntiAliasing(AntiAliasing effect); public abstract void SetScalingFilter(ScalingFilter scalerType); public abstract void SetScalingFilterLevel(float scale); diff --git a/src/Ryujinx.HLE.Generators/CodeGenerator.cs b/src/Ryujinx.HLE.Generators/CodeGenerator.cs index 726a07f0c..7e4848ad3 100644 --- a/src/Ryujinx.HLE.Generators/CodeGenerator.cs +++ b/src/Ryujinx.HLE.Generators/CodeGenerator.cs @@ -1,4 +1,4 @@ -using System.Text; +using System.Text; namespace Ryujinx.HLE.Generators { diff --git a/src/Ryujinx.HLE.Generators/IpcServiceGenerator.cs b/src/Ryujinx.HLE.Generators/IpcServiceGenerator.cs index bfeb972fc..5cac4d13a 100644 --- a/src/Ryujinx.HLE.Generators/IpcServiceGenerator.cs +++ b/src/Ryujinx.HLE.Generators/IpcServiceGenerator.cs @@ -1,4 +1,4 @@ -using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using System.Linq; @@ -13,25 +13,7 @@ namespace Ryujinx.HLE.Generators var syntaxReceiver = (ServiceSyntaxReceiver)context.SyntaxReceiver; CodeGenerator generator = new CodeGenerator(); - generator.EnterScope($"namespace Ryujinx.rd"); - generator.EnterScope($"public class Rd"); - - generator.AppendLine($"public string rd = \"\"\""); - foreach (var className in syntaxReceiver.Types) - { - if (className.Modifiers.Any(SyntaxKind.AbstractKeyword) || className.Modifiers.Any(SyntaxKind.PrivateKeyword)) - continue; - - var name = GetFullName(className, context).Replace("global::", ""); - generator.AppendLine($""); - } - generator.AppendLine($"\"\"\";"); - - generator.LeaveScope(); - generator.LeaveScope(); - context.AddSource($"rd.g.cs", generator.ToString()); - generator = new CodeGenerator(); - + generator.AppendLine("#nullable enable"); generator.AppendLine("using System;"); generator.EnterScope($"namespace Ryujinx.HLE.HOS.Services.Sm"); generator.EnterScope($"partial class IUserInterface"); @@ -41,10 +23,10 @@ namespace Ryujinx.HLE.Generators { if (className.Modifiers.Any(SyntaxKind.AbstractKeyword) || className.Modifiers.Any(SyntaxKind.PrivateKeyword) || !className.AttributeLists.Any(x => x.Attributes.Any(y => y.ToString().StartsWith("Service")))) continue; - var name = GetFullName(className, context).Replace("global::", ""); + var name = GetFullName(className, context).Replace("global::", string.Empty); if (!name.StartsWith("Ryujinx.HLE.HOS.Services")) continue; - var constructors = className.ChildNodes().Where(x => x.IsKind(SyntaxKind.ConstructorDeclaration)).Select(y => y as ConstructorDeclarationSyntax); + var constructors = className.ChildNodes().Where(x => x.IsKind(SyntaxKind.ConstructorDeclaration)).Select(y => y as ConstructorDeclarationSyntax).ToArray(); if (!constructors.Any(x => x.ParameterList.Parameters.Count >= 1)) continue; @@ -77,6 +59,7 @@ namespace Ryujinx.HLE.Generators generator.LeaveScope(); generator.LeaveScope(); + generator.AppendLine("#nullable disable"); context.AddSource($"IUserInterface.g.cs", generator.ToString()); } @@ -86,6 +69,7 @@ namespace Ryujinx.HLE.Generators return typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); } + public void Initialize(GeneratorInitializationContext context) { context.RegisterForSyntaxNotifications(() => new ServiceSyntaxReceiver()); diff --git a/src/Ryujinx.HLE.Generators/ServiceSyntaxReceiver.cs b/src/Ryujinx.HLE.Generators/ServiceSyntaxReceiver.cs index 6fc1c7199..e4269cb9a 100644 --- a/src/Ryujinx.HLE.Generators/ServiceSyntaxReceiver.cs +++ b/src/Ryujinx.HLE.Generators/ServiceSyntaxReceiver.cs @@ -1,10 +1,6 @@ -using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.CSharp; -using System; using System.Collections.Generic; -using System.Text; -using System.Linq; namespace Ryujinx.HLE.Generators { @@ -21,8 +17,6 @@ namespace Ryujinx.HLE.Generators return; } - var name = classDeclaration.Identifier.ToString(); - Types.Add(classDeclaration); } } diff --git a/src/Ryujinx.HLE/FileSystem/ContentManager.cs b/src/Ryujinx.HLE/FileSystem/ContentManager.cs index b27eb5ead..fc8def9d2 100644 --- a/src/Ryujinx.HLE/FileSystem/ContentManager.cs +++ b/src/Ryujinx.HLE/FileSystem/ContentManager.cs @@ -14,6 +14,7 @@ using Ryujinx.Common.Utilities; using Ryujinx.HLE.Exceptions; using Ryujinx.HLE.HOS.Services.Ssl; using Ryujinx.HLE.HOS.Services.Time; +using Ryujinx.HLE.Utilities; using System; using System.Collections.Generic; using System.IO; @@ -104,20 +105,15 @@ namespace Ryujinx.HLE.FileSystem foreach (StorageId storageId in Enum.GetValues()) { - string contentDirectory = null; - string contentPathString = null; - string registeredDirectory = null; - - try - { - contentPathString = ContentPath.GetContentPath(storageId); - contentDirectory = ContentPath.GetRealPath(contentPathString); - registeredDirectory = Path.Combine(contentDirectory, "registered"); - } - catch (NotSupportedException) + if (!ContentPath.TryGetContentPath(storageId, out var contentPathString)) { continue; } + if (!ContentPath.TryGetRealPath(contentPathString, out var contentDirectory)) + { + continue; + } + var registeredDirectory = Path.Combine(contentDirectory, "registered"); Directory.CreateDirectory(registeredDirectory); @@ -189,41 +185,6 @@ namespace Ryujinx.HLE.FileSystem } } - // fs must contain AOC nca files in its root - public void AddAocData(IFileSystem fs, string containerPath, ulong aocBaseId, IntegrityCheckLevel integrityCheckLevel) - { - _virtualFileSystem.ImportTickets(fs); - - foreach (var ncaPath in fs.EnumerateEntries("*.cnmt.nca", SearchOptions.Default)) - { - using var ncaFile = new UniqueRef(); - - fs.OpenFile(ref ncaFile.Ref, ncaPath.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); - var nca = new Nca(_virtualFileSystem.KeySet, ncaFile.Get.AsStorage()); - if (nca.Header.ContentType != NcaContentType.Meta) - { - Logger.Warning?.Print(LogClass.Application, $"{ncaPath} is not a valid metadata file"); - - continue; - } - - using var pfs0 = nca.OpenFileSystem(0, integrityCheckLevel); - using var cnmtFile = new UniqueRef(); - - pfs0.OpenFile(ref cnmtFile.Ref, pfs0.EnumerateEntries().Single().FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); - - var cnmt = new Cnmt(cnmtFile.Get.AsStream()); - if (cnmt.Type != ContentMetaType.AddOnContent || (cnmt.TitleId & 0xFFFFFFFFFFFFE000) != aocBaseId) - { - continue; - } - - string ncaId = Convert.ToHexString(cnmt.ContentEntries[0].NcaId).ToLower(); - - AddAocItem(cnmt.TitleId, containerPath, $"/{ncaId}.nca", true); - } - } - public void AddAocItem(ulong titleId, string containerPath, string ncaPath, bool mergedToContainer = false) { // TODO: Check Aoc version. @@ -237,11 +198,7 @@ namespace Ryujinx.HLE.FileSystem if (!mergedToContainer) { - using FileStream fileStream = File.OpenRead(containerPath); - using PartitionFileSystem partitionFileSystem = new(); - partitionFileSystem.Initialize(fileStream.AsStorage()).ThrowIfFailure(); - - _virtualFileSystem.ImportTickets(partitionFileSystem); + using var pfs = PartitionFileSystemUtils.OpenApplicationFileSystem(containerPath, _virtualFileSystem); } } } @@ -471,8 +428,8 @@ namespace Ryujinx.HLE.FileSystem public void InstallFirmware(string firmwareSource) { - string contentPathString = ContentPath.GetContentPath(StorageId.BuiltInSystem); - string contentDirectory = ContentPath.GetRealPath(contentPathString); + ContentPath.TryGetContentPath(StorageId.BuiltInSystem, out var contentPathString); + ContentPath.TryGetRealPath(contentPathString, out var contentDirectory); string registeredDirectory = Path.Combine(contentDirectory, "registered"); string temporaryDirectory = Path.Combine(contentDirectory, "temp"); @@ -566,7 +523,7 @@ namespace Ryujinx.HLE.FileSystem { // Clean up the name and get the NcaId - string[] pathComponents = entry.FullName.Replace(".cnmt", "").Split('/'); + string[] pathComponents = entry.FullName.Replace(".cnmt", string.Empty).Split('/'); string ncaId = pathComponents[^1]; @@ -631,21 +588,15 @@ namespace Ryujinx.HLE.FileSystem // LibHac.NcaHeader's DecryptHeader doesn't check if HeaderKey is empty and throws InvalidDataException instead // So, we check it early for a better user experience. if (_virtualFileSystem.KeySet.HeaderKey.IsZeros()) - { throw new MissingKeyException("HeaderKey is empty. Cannot decrypt NCA headers."); - } Dictionary> updateNcas = new(); if (Directory.Exists(firmwarePackage)) - { return VerifyAndGetVersionDirectory(firmwarePackage); - } if (!File.Exists(firmwarePackage)) - { throw new FileNotFoundException("Firmware file does not exist."); - } FileInfo info = new(firmwarePackage); @@ -655,30 +606,22 @@ namespace Ryujinx.HLE.FileSystem { case ".zip": using (ZipArchive archive = ZipFile.OpenRead(firmwarePackage)) - { return VerifyAndGetVersionZip(archive); - } case ".xci": Xci xci = new(_virtualFileSystem.KeySet, file.AsStorage()); - if (xci.HasPartition(XciPartitionType.Update)) - { - XciPartition partition = xci.OpenPartition(XciPartitionType.Update); - - return VerifyAndGetVersion(partition); - } - else - { + if (!xci.HasPartition(XciPartitionType.Update)) throw new InvalidFirmwarePackageException("Update not found in xci file."); - } - default: - break; + + XciPartition partition = xci.OpenPartition(XciPartitionType.Update); + + return VerifyAndGetVersion(partition); } + return null; + SystemVersion VerifyAndGetVersionDirectory(string firmwareDirectory) - { - return VerifyAndGetVersion(new LocalFileSystem(firmwareDirectory)); - } + => VerifyAndGetVersion(new LocalFileSystem(firmwareDirectory)); SystemVersion VerifyAndGetVersionZip(ZipArchive archive) { @@ -968,8 +911,6 @@ namespace Ryujinx.HLE.FileSystem return systemVersion; } - - return null; } public SystemVersion GetCurrentFirmwareVersion() diff --git a/src/Ryujinx.HLE/FileSystem/ContentMetaData.cs b/src/Ryujinx.HLE/FileSystem/ContentMetaData.cs new file mode 100644 index 000000000..aebcf7988 --- /dev/null +++ b/src/Ryujinx.HLE/FileSystem/ContentMetaData.cs @@ -0,0 +1,61 @@ +using LibHac.Common.Keys; +using LibHac.Fs.Fsa; +using LibHac.Ncm; +using LibHac.Tools.FsSystem.NcaUtils; +using LibHac.Tools.Ncm; +using Ryujinx.HLE.Loaders.Processes.Extensions; +using System; + +namespace Ryujinx.HLE.FileSystem +{ + /// + /// Thin wrapper around + /// + public class ContentMetaData + { + private readonly IFileSystem _pfs; + private readonly Cnmt _cnmt; + + public ulong Id => _cnmt.TitleId; + public TitleVersion Version => _cnmt.TitleVersion; + public ContentMetaType Type => _cnmt.Type; + public ulong ApplicationId => _cnmt.ApplicationTitleId; + public ulong PatchId => _cnmt.PatchTitleId; + public TitleVersion RequiredSystemVersion => _cnmt.MinimumSystemVersion; + public TitleVersion RequiredApplicationVersion => _cnmt.MinimumApplicationVersion; + public byte[] Digest => _cnmt.Hash; + + public ulong ProgramBaseId => Id & ~0x1FFFUL; + public bool IsSystemTitle => _cnmt.Type < ContentMetaType.Application; + + public ContentMetaData(IFileSystem pfs, Cnmt cnmt) + { + _pfs = pfs; + _cnmt = cnmt; + } + + public Nca GetNcaByType(KeySet keySet, ContentType type, int programIndex = 0) + { + // TODO: Replace this with a check for IdOffset as soon as LibHac supports it: + // && entry.IdOffset == programIndex + + foreach (var entry in _cnmt.ContentEntries) + { + if (entry.Type != type) + { + continue; + } + + string ncaId = BitConverter.ToString(entry.NcaId).Replace("-", null).ToLower(); + Nca nca = _pfs.GetNca(keySet, $"/{ncaId}.nca"); + + if (nca.GetProgramIndex() == programIndex) + { + return nca; + } + } + + return null; + } + } +} diff --git a/src/Ryujinx.HLE/FileSystem/ContentPath.cs b/src/Ryujinx.HLE/FileSystem/ContentPath.cs index 02539c5c6..ffc212f77 100644 --- a/src/Ryujinx.HLE/FileSystem/ContentPath.cs +++ b/src/Ryujinx.HLE/FileSystem/ContentPath.cs @@ -26,17 +26,19 @@ namespace Ryujinx.HLE.FileSystem public const string Nintendo = "Nintendo"; public const string Contents = "Contents"; - public static string GetRealPath(string switchContentPath) + public static bool TryGetRealPath(string switchContentPath, out string realPath) { - return switchContentPath switch + realPath = switchContentPath switch { SystemContent => Path.Combine(AppDataManager.BaseDirPath, SystemNandPath, Contents), UserContent => Path.Combine(AppDataManager.BaseDirPath, UserNandPath, Contents), SdCardContent => Path.Combine(GetSdCardPath(), Nintendo, Contents), System => Path.Combine(AppDataManager.BaseDirPath, SystemNandPath), User => Path.Combine(AppDataManager.BaseDirPath, UserNandPath), - _ => throw new NotSupportedException($"Content Path \"`{switchContentPath}`\" is not supported."), + _ => null, }; + + return realPath != null; } public static string GetContentPath(ContentStorageId contentStorageId) @@ -50,15 +52,17 @@ namespace Ryujinx.HLE.FileSystem }; } - public static string GetContentPath(StorageId storageId) + public static bool TryGetContentPath(StorageId storageId, out string contentPath) { - return storageId switch + contentPath = storageId switch { StorageId.BuiltInSystem => SystemContent, StorageId.BuiltInUser => UserContent, StorageId.SdCard => SdCardContent, - _ => throw new NotSupportedException($"Storage Id \"`{storageId}`\" is not supported."), + _ => null, }; + + return contentPath != null; } public static StorageId GetStorageId(string contentPathString) diff --git a/src/Ryujinx.HLE/FileSystem/VirtualFileSystem.cs b/src/Ryujinx.HLE/FileSystem/VirtualFileSystem.cs index 43bd27761..39c544eac 100644 --- a/src/Ryujinx.HLE/FileSystem/VirtualFileSystem.cs +++ b/src/Ryujinx.HLE/FileSystem/VirtualFileSystem.cs @@ -132,7 +132,7 @@ namespace Ryujinx.HLE.FileSystem if (systemPath.StartsWith(baseSystemPath)) { - string rawPath = systemPath.Replace(baseSystemPath, ""); + string rawPath = systemPath.Replace(baseSystemPath, string.Empty); int firstSeparatorOffset = rawPath.IndexOf(Path.DirectorySeparatorChar); if (firstSeparatorOffset == -1) @@ -186,7 +186,12 @@ namespace Ryujinx.HLE.FileSystem public void InitializeFsServer(LibHac.Horizon horizon, out HorizonClient fsServerClient) { - LocalFileSystem serverBaseFs = new(AppDataManager.BaseDirPath); + LocalFileSystem serverBaseFs = new(useUnixTimeStamps: true); + Result result = serverBaseFs.Initialize(AppDataManager.BaseDirPath, LocalFileSystem.PathMode.DefaultCaseSensitivity, ensurePathExists: true); + if (result.IsFailure()) + { + throw new HorizonResultException(result, "Error creating LocalFileSystem."); + } fsServerClient = horizon.CreatePrivilegedHorizonClient(); var fsServer = new FileSystemServer(fsServerClient); @@ -624,8 +629,8 @@ namespace Ryujinx.HLE.FileSystem } private static readonly ExtraDataFixInfo[] _systemExtraDataFixInfo = - { - new ExtraDataFixInfo() + [ + new() { StaticSaveDataId = 0x8000000000000030, OwnerId = 0x010000000000001F, @@ -633,15 +638,15 @@ namespace Ryujinx.HLE.FileSystem DataSize = 0x10000, JournalSize = 0x10000, }, - new ExtraDataFixInfo() + new() { StaticSaveDataId = 0x8000000000001040, OwnerId = 0x0100000000001009, Flags = SaveDataFlags.None, DataSize = 0xC000, JournalSize = 0xC000, - }, - }; + } + ]; public void Dispose() { diff --git a/src/Ryujinx.HLE/HLEConfiguration.cs b/src/Ryujinx.HLE/HLEConfiguration.cs index f589bfdda..f75ead588 100644 --- a/src/Ryujinx.HLE/HLEConfiguration.cs +++ b/src/Ryujinx.HLE/HLEConfiguration.cs @@ -7,8 +7,9 @@ using Ryujinx.HLE.FileSystem; using Ryujinx.HLE.HOS; using Ryujinx.HLE.HOS.Services.Account.Acc; using Ryujinx.HLE.HOS.SystemState; -using Ryujinx.HLE.Ui; +using Ryujinx.HLE.UI; using System; +using VSyncMode = Ryujinx.Common.Configuration.VSyncMode; namespace Ryujinx.HLE { @@ -63,7 +64,7 @@ namespace Ryujinx.HLE /// The handler for various UI related operations needed outside of HLE. /// /// This cannot be changed after instantiation. - internal readonly IHostUiHandler HostUiHandler; + internal readonly IHostUIHandler HostUIHandler; /// /// Control the memory configuration used by the emulation context. @@ -84,9 +85,14 @@ namespace Ryujinx.HLE internal readonly RegionCode Region; /// - /// Control the initial state of the vertical sync in the SurfaceFlinger service. + /// Control the initial state of the present interval in the SurfaceFlinger service (previously Vsync). /// - internal readonly bool EnableVsync; + internal readonly VSyncMode VSyncMode; + + /// + /// Control the custom VSync interval, if enabled and active. + /// + internal readonly int CustomVSyncInterval; /// /// Control the initial state of the docked mode. @@ -164,6 +170,21 @@ namespace Ryujinx.HLE /// public MultiplayerMode MultiplayerMode { internal get; set; } + /// + /// Disable P2P mode + /// + public bool MultiplayerDisableP2p { internal get; set; } + + /// + /// Multiplayer Passphrase + /// + public string MultiplayerLdnPassphrase { internal get; set; } + + /// + /// LDN Server + /// + public string MultiplayerLdnServer { internal get; set; } + /// /// An action called when HLE force a refresh of output after docked mode changed. /// @@ -177,10 +198,10 @@ namespace Ryujinx.HLE IRenderer gpuRenderer, IHardwareDeviceDriver audioDeviceDriver, MemoryConfiguration memoryConfiguration, - IHostUiHandler hostUiHandler, + IHostUIHandler hostUIHandler, SystemLanguage systemLanguage, RegionCode region, - bool enableVsync, + VSyncMode vSyncMode, bool enableDockedMode, bool enablePtc, bool enableInternetAccess, @@ -194,7 +215,11 @@ namespace Ryujinx.HLE float audioVolume, bool useHypervisor, string multiplayerLanInterfaceId, - MultiplayerMode multiplayerMode) + MultiplayerMode multiplayerMode, + bool multiplayerDisableP2p, + string multiplayerLdnPassphrase, + string multiplayerLdnServer, + int customVSyncInterval) { VirtualFileSystem = virtualFileSystem; LibHacHorizonManager = libHacHorizonManager; @@ -204,10 +229,11 @@ namespace Ryujinx.HLE GpuRenderer = gpuRenderer; AudioDeviceDriver = audioDeviceDriver; MemoryConfiguration = memoryConfiguration; - HostUiHandler = hostUiHandler; + HostUIHandler = hostUIHandler; SystemLanguage = systemLanguage; Region = region; - EnableVsync = enableVsync; + VSyncMode = vSyncMode; + CustomVSyncInterval = customVSyncInterval; EnableDockedMode = enableDockedMode; EnablePtc = enablePtc; EnableInternetAccess = enableInternetAccess; @@ -222,6 +248,9 @@ namespace Ryujinx.HLE UseHypervisor = useHypervisor; MultiplayerLanInterfaceId = multiplayerLanInterfaceId; MultiplayerMode = multiplayerMode; + MultiplayerDisableP2p = multiplayerDisableP2p; + MultiplayerLdnPassphrase = multiplayerLdnPassphrase; + MultiplayerLdnServer = multiplayerLdnServer; } } } diff --git a/src/Ryujinx.HLE/HOS/Applets/AppletManager.cs b/src/Ryujinx.HLE/HOS/Applets/AppletManager.cs index 3c34d5c78..da4d2e51b 100644 --- a/src/Ryujinx.HLE/HOS/Applets/AppletManager.cs +++ b/src/Ryujinx.HLE/HOS/Applets/AppletManager.cs @@ -1,4 +1,6 @@ +using Ryujinx.Common.Logging; using Ryujinx.HLE.HOS.Applets.Browser; +using Ryujinx.HLE.HOS.Applets.Dummy; using Ryujinx.HLE.HOS.Applets.Error; using Ryujinx.HLE.HOS.Services.Am.AppletAE; using System; @@ -26,9 +28,13 @@ namespace Ryujinx.HLE.HOS.Applets return new BrowserApplet(system); case AppletId.LibAppletOff: return new BrowserApplet(system); + case AppletId.MiiEdit: + Logger.Warning?.Print(LogClass.Application, $"Please use the MiiEdit inside File/Open Applet"); + return new DummyApplet(system); } - throw new NotImplementedException($"{applet} applet is not implemented."); + Logger.Warning?.Print(LogClass.Application, $"Applet {applet} not implemented!"); + return new DummyApplet(system); } } } diff --git a/src/Ryujinx.HLE/HOS/Applets/Controller/ControllerApplet.cs b/src/Ryujinx.HLE/HOS/Applets/Controller/ControllerApplet.cs index 867202178..5ec9d4b08 100644 --- a/src/Ryujinx.HLE/HOS/Applets/Controller/ControllerApplet.cs +++ b/src/Ryujinx.HLE/HOS/Applets/Controller/ControllerApplet.cs @@ -86,7 +86,7 @@ namespace Ryujinx.HLE.HOS.Applets PlayerIndex primaryIndex; while (!_system.Device.Hid.Npads.Validate(playerMin, playerMax, (ControllerType)privateArg.NpadStyleSet, out configuredCount, out primaryIndex)) { - ControllerAppletUiArgs uiArgs = new() + ControllerAppletUIArgs uiArgs = new() { PlayerCountMin = playerMin, PlayerCountMax = playerMax, @@ -95,7 +95,7 @@ namespace Ryujinx.HLE.HOS.Applets IsDocked = _system.State.DockedMode, }; - if (!_system.Device.UiHandler.DisplayMessageDialog(uiArgs)) + if (!_system.Device.UIHandler.DisplayMessageDialog(uiArgs)) { break; } diff --git a/src/Ryujinx.HLE/HOS/Applets/Controller/ControllerAppletUiArgs.cs b/src/Ryujinx.HLE/HOS/Applets/Controller/ControllerAppletUiArgs.cs index bf440515b..10cba58ba 100644 --- a/src/Ryujinx.HLE/HOS/Applets/Controller/ControllerAppletUiArgs.cs +++ b/src/Ryujinx.HLE/HOS/Applets/Controller/ControllerAppletUiArgs.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; namespace Ryujinx.HLE.HOS.Applets { - public struct ControllerAppletUiArgs + public struct ControllerAppletUIArgs { public int PlayerCountMin; public int PlayerCountMax; diff --git a/src/Ryujinx.HLE/HOS/Applets/Dummy/DummyApplet.cs b/src/Ryujinx.HLE/HOS/Applets/Dummy/DummyApplet.cs new file mode 100644 index 000000000..75df7a373 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Applets/Dummy/DummyApplet.cs @@ -0,0 +1,43 @@ +using Ryujinx.Common.Logging; +using Ryujinx.Common.Memory; +using Ryujinx.HLE.HOS.Applets; +using Ryujinx.HLE.HOS.Services.Am.AppletAE; +using System; +using System.IO; +using System.Runtime.InteropServices; +namespace Ryujinx.HLE.HOS.Applets.Dummy +{ + internal class DummyApplet : IApplet + { + private readonly Horizon _system; + private AppletSession _normalSession; + public event EventHandler AppletStateChanged; + public DummyApplet(Horizon system) + { + _system = system; + } + public ResultCode Start(AppletSession normalSession, AppletSession interactiveSession) + { + _normalSession = normalSession; + _normalSession.Push(BuildResponse()); + AppletStateChanged?.Invoke(this, null); + _system.ReturnFocus(); + return ResultCode.Success; + } + private static T ReadStruct(byte[] data) where T : struct + { + return MemoryMarshal.Read(data.AsSpan()); + } + private static byte[] BuildResponse() + { + using MemoryStream stream = MemoryStreamManager.Shared.GetStream(); + using BinaryWriter writer = new(stream); + writer.Write((ulong)ResultCode.Success); + return stream.ToArray(); + } + public ResultCode GetResult() + { + return ResultCode.Success; + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Applets/Error/ErrorApplet.cs b/src/Ryujinx.HLE/HOS/Applets/Error/ErrorApplet.cs index 5c474f229..87d88fc65 100644 --- a/src/Ryujinx.HLE/HOS/Applets/Error/ErrorApplet.cs +++ b/src/Ryujinx.HLE/HOS/Applets/Error/ErrorApplet.cs @@ -107,7 +107,7 @@ namespace Ryujinx.HLE.HOS.Applets.Error private static string CleanText(string value) { - return CleanTextRegex().Replace(value, "").Replace("\0", ""); + return CleanTextRegex().Replace(value, string.Empty).Replace("\0", string.Empty); } private string GetMessageText(uint module, uint description, string key) @@ -129,17 +129,15 @@ namespace Ryujinx.HLE.HOS.Applets.Error return CleanText(reader.ReadToEnd()); } - else - { - return ""; - } + + return string.Empty; } private string[] GetButtonsText(uint module, uint description, string key) { string buttonsText = GetMessageText(module, description, key); - return (buttonsText == "") ? null : buttonsText.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None); + return (buttonsText == string.Empty) ? null : buttonsText.Split(["\r\n", "\r", "\n"], StringSplitOptions.None); } private void ParseErrorCommonArg() @@ -156,23 +154,20 @@ namespace Ryujinx.HLE.HOS.Applets.Error string message = GetMessageText(module, description, "DlgMsg"); - if (message == "") + if (message == string.Empty) { - message = "An error has occured.\n\n" - + "Please try again later.\n\n" - + "If the problem persists, please refer to the Ryujinx website.\n" - + "www.ryujinx.org"; + message = "An error has occured.\n\nPlease try again later."; } string[] buttons = GetButtonsText(module, description, "DlgBtn"); - bool showDetails = _horizon.Device.UiHandler.DisplayErrorAppletDialog($"Error Code: {module}-{description:0000}", "\n" + message, buttons); + bool showDetails = _horizon.Device.UIHandler.DisplayErrorAppletDialog($"Error Code: {module}-{description:0000}", "\n" + message, buttons); if (showDetails) { message = GetMessageText(module, description, "FlvMsg"); buttons = GetButtonsText(module, description, "FlvBtn"); - _horizon.Device.UiHandler.DisplayErrorAppletDialog($"Details: {module}-{description:0000}", "\n" + message, buttons); + _horizon.Device.UIHandler.DisplayErrorAppletDialog($"Details: {module}-{description:0000}", "\n" + message, buttons); } } @@ -193,19 +188,19 @@ namespace Ryujinx.HLE.HOS.Applets.Error // TODO: Handle the LanguageCode to return the translated "OK" and "Details". - if (detailsText.Trim() != "") + if (detailsText.Trim() != string.Empty) { buttons.Add("Details"); } buttons.Add("OK"); - bool showDetails = _horizon.Device.UiHandler.DisplayErrorAppletDialog($"Error Number: {applicationErrorArg.ErrorNumber}", "\n" + messageText, buttons.ToArray()); + bool showDetails = _horizon.Device.UIHandler.DisplayErrorAppletDialog($"Error Number: {applicationErrorArg.ErrorNumber}", "\n" + messageText, buttons.ToArray()); if (showDetails) { buttons.RemoveAt(0); - _horizon.Device.UiHandler.DisplayErrorAppletDialog($"Error Number: {applicationErrorArg.ErrorNumber} (Details)", "\n" + detailsText, buttons.ToArray()); + _horizon.Device.UIHandler.DisplayErrorAppletDialog($"Error Number: {applicationErrorArg.ErrorNumber} (Details)", "\n" + detailsText, buttons.ToArray()); } } diff --git a/src/Ryujinx.HLE/HOS/Applets/IApplet.cs b/src/Ryujinx.HLE/HOS/Applets/IApplet.cs index 5ccf3994f..bc5353841 100644 --- a/src/Ryujinx.HLE/HOS/Applets/IApplet.cs +++ b/src/Ryujinx.HLE/HOS/Applets/IApplet.cs @@ -1,5 +1,5 @@ using Ryujinx.HLE.HOS.Services.Am.AppletAE; -using Ryujinx.HLE.Ui; +using Ryujinx.HLE.UI; using Ryujinx.Memory; using System; using System.Runtime.InteropServices; @@ -15,14 +15,8 @@ namespace Ryujinx.HLE.HOS.Applets ResultCode GetResult(); - bool DrawTo(RenderingSurfaceInfo surfaceInfo, IVirtualMemoryManager destination, ulong position) - { - return false; - } + bool DrawTo(RenderingSurfaceInfo surfaceInfo, IVirtualMemoryManager destination, ulong position) => false; - static T ReadStruct(ReadOnlySpan data) where T : unmanaged - { - return MemoryMarshal.Cast(data)[0]; - } + static T ReadStruct(ReadOnlySpan data) where T : unmanaged => MemoryMarshal.Cast(data)[0]; } } diff --git a/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardApplet.cs b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardApplet.cs index 432bf6a8a..e04fc64fe 100644 --- a/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardApplet.cs +++ b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardApplet.cs @@ -4,8 +4,8 @@ using Ryujinx.Common.Logging; using Ryujinx.HLE.HOS.Applets.SoftwareKeyboard; using Ryujinx.HLE.HOS.Services.Am.AppletAE; using Ryujinx.HLE.HOS.Services.Hid.Types.SharedMemory.Npad; -using Ryujinx.HLE.Ui; -using Ryujinx.HLE.Ui.Input; +using Ryujinx.HLE.UI; +using Ryujinx.HLE.UI.Input; using Ryujinx.Memory; using System; using System.Diagnostics; @@ -51,7 +51,7 @@ namespace Ryujinx.HLE.HOS.Applets private byte[] _transferMemory; - private string _textValue = ""; + private string _textValue = string.Empty; private int _cursorBegin = 0; private Encoding _encoding = Encoding.Unicode; private KeyboardResult _lastResult = KeyboardResult.NotSet; @@ -92,14 +92,14 @@ namespace Ryujinx.HLE.HOS.Applets _keyboardBackgroundInitialize = MemoryMarshal.Read(keyboardConfig); _backgroundState = InlineKeyboardState.Uninitialized; - if (_device.UiHandler == null) + if (_device.UIHandler == null) { Logger.Error?.Print(LogClass.ServiceAm, "GUI Handler is not set, software keyboard applet will not work properly"); } else { // Create a text handler that converts keyboard strokes to strings. - _dynamicTextInputHandler = _device.UiHandler.CreateDynamicTextInputHandler(); + _dynamicTextInputHandler = _device.UIHandler.CreateDynamicTextInputHandler(); _dynamicTextInputHandler.TextChangedEvent += HandleTextChangedEvent; _dynamicTextInputHandler.KeyPressedEvent += HandleKeyPressedEvent; @@ -107,7 +107,7 @@ namespace Ryujinx.HLE.HOS.Applets _npads.NpadButtonDownEvent += HandleNpadButtonDownEvent; _npads.NpadButtonUpEvent += HandleNpadButtonUpEvent; - _keyboardRenderer = new SoftwareKeyboardRenderer(_device.UiHandler.HostUiTheme); + _keyboardRenderer = new SoftwareKeyboardRenderer(_device.UIHandler.HostUITheme); } return ResultCode.Success; @@ -199,7 +199,7 @@ namespace Ryujinx.HLE.HOS.Applets _keyboardForegroundConfig.StringLengthMax = 100; } - if (_device.UiHandler == null) + if (_device.UIHandler == null) { Logger.Warning?.Print(LogClass.Application, "GUI Handler is not set. Falling back to default"); @@ -209,7 +209,7 @@ namespace Ryujinx.HLE.HOS.Applets else { // Call the configured GUI handler to get user's input. - var args = new SoftwareKeyboardUiArgs + var args = new SoftwareKeyboardUIArgs { KeyboardMode = _keyboardForegroundConfig.Mode, HeaderText = StripUnicodeControlCodes(_keyboardForegroundConfig.HeaderText), @@ -222,7 +222,7 @@ namespace Ryujinx.HLE.HOS.Applets InitialText = initialText, }; - _lastResult = _device.UiHandler.DisplayInputDialog(args, out _textValue) ? KeyboardResult.Accept : KeyboardResult.Cancel; + _lastResult = _device.UIHandler.DisplayInputDialog(args, out _textValue) ? KeyboardResult.Accept : KeyboardResult.Cancel; _textValue ??= initialText ?? DefaultInputText; } diff --git a/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardRenderer.cs b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardRenderer.cs index f76cce295..239535ad5 100644 --- a/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardRenderer.cs +++ b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardRenderer.cs @@ -1,4 +1,4 @@ -using Ryujinx.HLE.Ui; +using Ryujinx.HLE.UI; using Ryujinx.Memory; using System; using System.Threading; @@ -15,13 +15,13 @@ namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard private readonly object _stateLock = new(); - private readonly SoftwareKeyboardUiState _state = new(); + private readonly SoftwareKeyboardUIState _state = new(); private readonly SoftwareKeyboardRendererBase _renderer; private readonly TimedAction _textBoxBlinkTimedAction = new(); private readonly TimedAction _renderAction = new(); - public SoftwareKeyboardRenderer(IHostUiTheme uiTheme) + public SoftwareKeyboardRenderer(IHostUITheme uiTheme) { _renderer = new SoftwareKeyboardRendererBase(uiTheme); @@ -29,7 +29,7 @@ namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard StartRenderer(_renderAction, _renderer, _state, _stateLock); } - private static void StartTextBoxBlinker(TimedAction timedAction, SoftwareKeyboardUiState state, object stateLock) + private static void StartTextBoxBlinker(TimedAction timedAction, SoftwareKeyboardUIState state, object stateLock) { timedAction.Reset(() => { @@ -45,9 +45,9 @@ namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard }, TextBoxBlinkSleepMilliseconds); } - private static void StartRenderer(TimedAction timedAction, SoftwareKeyboardRendererBase renderer, SoftwareKeyboardUiState state, object stateLock) + private static void StartRenderer(TimedAction timedAction, SoftwareKeyboardRendererBase renderer, SoftwareKeyboardUIState state, object stateLock) { - SoftwareKeyboardUiState internalState = new(); + SoftwareKeyboardUIState internalState = new(); bool canCreateSurface = false; bool needsUpdate = true; @@ -112,11 +112,16 @@ namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard { // Update the parameters that were provided. _state.InputText = inputText ?? _state.InputText; - _state.CursorBegin = cursorBegin.GetValueOrDefault(_state.CursorBegin); - _state.CursorEnd = cursorEnd.GetValueOrDefault(_state.CursorEnd); + _state.CursorBegin = Math.Max(0, cursorBegin.GetValueOrDefault(_state.CursorBegin)); + _state.CursorEnd = Math.Min(cursorEnd.GetValueOrDefault(_state.CursorEnd), _state.InputText.Length); _state.OverwriteMode = overwriteMode.GetValueOrDefault(_state.OverwriteMode); _state.TypingEnabled = typingEnabled.GetValueOrDefault(_state.TypingEnabled); + var begin = _state.CursorBegin; + var end = _state.CursorEnd; + _state.CursorBegin = Math.Min(begin, end); + _state.CursorEnd = Math.Max(begin, end); + // Reset the cursor blink. _state.TextBoxBlinkCounter = 0; diff --git a/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardRendererBase.cs b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardRendererBase.cs index d3831d8f3..67b5f2b1f 100644 --- a/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardRendererBase.cs +++ b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardRendererBase.cs @@ -1,14 +1,9 @@ -using Ryujinx.HLE.Ui; +using Ryujinx.HLE.UI; using Ryujinx.Memory; -using SixLabors.Fonts; -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing; +using SkiaSharp; using System; using System.Diagnostics; using System.IO; -using System.Numerics; using System.Reflection; using System.Runtime.InteropServices; @@ -29,41 +24,42 @@ namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard private readonly object _bufferLock = new(); private RenderingSurfaceInfo _surfaceInfo = null; - private Image _surface = null; + private SKImageInfo _imageInfo; + private SKSurface _surface = null; private byte[] _bufferData = null; - private readonly Image _ryujinxLogo = null; - private readonly Image _padAcceptIcon = null; - private readonly Image _padCancelIcon = null; - private readonly Image _keyModeIcon = null; + private readonly SKBitmap _ryujinxLogo = null; + private readonly SKBitmap _padAcceptIcon = null; + private readonly SKBitmap _padCancelIcon = null; + private readonly SKBitmap _keyModeIcon = null; private readonly float _textBoxOutlineWidth; private readonly float _padPressedPenWidth; - private readonly Color _textNormalColor; - private readonly Color _textSelectedColor; - private readonly Color _textOverCursorColor; + private readonly SKColor _textNormalColor; + private readonly SKColor _textSelectedColor; + private readonly SKColor _textOverCursorColor; - private readonly IBrush _panelBrush; - private readonly IBrush _disabledBrush; - private readonly IBrush _cursorBrush; - private readonly IBrush _selectionBoxBrush; + private readonly SKPaint _panelBrush; + private readonly SKPaint _disabledBrush; + private readonly SKPaint _cursorBrush; + private readonly SKPaint _selectionBoxBrush; - private readonly Pen _textBoxOutlinePen; - private readonly Pen _cursorPen; - private readonly Pen _selectionBoxPen; - private readonly Pen _padPressedPen; + private readonly SKPaint _textBoxOutlinePen; + private readonly SKPaint _cursorPen; + private readonly SKPaint _selectionBoxPen; + private readonly SKPaint _padPressedPen; private readonly int _inputTextFontSize; - private Font _messageFont; - private Font _inputTextFont; - private Font _labelsTextFont; + private SKFont _messageFont; + private SKFont _inputTextFont; + private SKFont _labelsTextFont; - private RectangleF _panelRectangle; - private Point _logoPosition; + private SKRect _panelRectangle; + private SKPoint _logoPosition; private float _messagePositionY; - public SoftwareKeyboardRendererBase(IHostUiTheme uiTheme) + public SoftwareKeyboardRendererBase(IHostUITheme uiTheme) { int ryujinxLogoSize = 32; @@ -78,10 +74,10 @@ namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard _padCancelIcon = LoadResource(typeof(SoftwareKeyboardRendererBase).Assembly, padCancelIconPath, 0, 0); _keyModeIcon = LoadResource(typeof(SoftwareKeyboardRendererBase).Assembly, keyModeIconPath, 0, 0); - Color panelColor = ToColor(uiTheme.DefaultBackgroundColor, 255); - Color panelTransparentColor = ToColor(uiTheme.DefaultBackgroundColor, 150); - Color borderColor = ToColor(uiTheme.DefaultBorderColor); - Color selectionBackgroundColor = ToColor(uiTheme.SelectionBackgroundColor); + var panelColor = ToColor(uiTheme.DefaultBackgroundColor, 255); + var panelTransparentColor = ToColor(uiTheme.DefaultBackgroundColor, 150); + var borderColor = ToColor(uiTheme.DefaultBorderColor); + var selectionBackgroundColor = ToColor(uiTheme.SelectionBackgroundColor); _textNormalColor = ToColor(uiTheme.DefaultForegroundColor); _textSelectedColor = ToColor(uiTheme.SelectionForegroundColor); @@ -92,70 +88,67 @@ namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard _textBoxOutlineWidth = 2; _padPressedPenWidth = 2; - _panelBrush = new SolidBrush(panelColor); - _disabledBrush = new SolidBrush(panelTransparentColor); - _cursorBrush = new SolidBrush(_textNormalColor); - _selectionBoxBrush = new SolidBrush(selectionBackgroundColor); + _panelBrush = new SKPaint() + { + Color = panelColor, + IsAntialias = true + }; + _disabledBrush = new SKPaint() + { + Color = panelTransparentColor, + IsAntialias = true + }; + _cursorBrush = new SKPaint() { Color = _textNormalColor, IsAntialias = true }; + _selectionBoxBrush = new SKPaint() { Color = selectionBackgroundColor, IsAntialias = true }; - _textBoxOutlinePen = new Pen(borderColor, _textBoxOutlineWidth); - _cursorPen = new Pen(_textNormalColor, cursorWidth); - _selectionBoxPen = new Pen(selectionBackgroundColor, cursorWidth); - _padPressedPen = new Pen(borderColor, _padPressedPenWidth); + _textBoxOutlinePen = new SKPaint() + { + Color = borderColor, + StrokeWidth = _textBoxOutlineWidth, + IsStroke = true, + IsAntialias = true + }; + _cursorPen = new SKPaint() { Color = _textNormalColor, StrokeWidth = cursorWidth, IsStroke = true, IsAntialias = true }; + _selectionBoxPen = new SKPaint() { Color = selectionBackgroundColor, StrokeWidth = cursorWidth, IsStroke = true, IsAntialias = true }; + _padPressedPen = new SKPaint() { Color = borderColor, StrokeWidth = _padPressedPenWidth, IsStroke = true, IsAntialias = true }; _inputTextFontSize = 20; CreateFonts(uiTheme.FontFamily); } -private void CreateFonts(string uiThemeFontFamily) -{ - // Try a list of fonts in case any of them is not available in the system. - string[] availableFonts = { uiThemeFontFamily }; - - // If it's iOS, we'll want to use a more appropriate set of fonts. - if (OperatingSystem.IsIOS()) - { - availableFonts = new string[] { - "Chalkboard", - "Chalkboard", // San Francisco is the default font on iOS - "Chalkboard", // Legacy iOS font - "Chalkboard" // Common system font - }; - } - else - { - // Fallback for other platforms (e.g., Android, Windows, etc.) - availableFonts = new string[] { - uiThemeFontFamily, - "Liberation Sans", - "FreeSans", - "DejaVu Sans", - "Lucida Grande" - }; - } - - // Try to create the fonts with the selected font families - foreach (string fontFamily in availableFonts) - { - try + private void CreateFonts(string uiThemeFontFamily) { - _messageFont = SystemFonts.CreateFont(fontFamily, 26, FontStyle.Regular); - _inputTextFont = SystemFonts.CreateFont(fontFamily, _inputTextFontSize, FontStyle.Regular); - _labelsTextFont = SystemFonts.CreateFont(fontFamily, 24, FontStyle.Regular); + // Try a list of fonts in case any of them is not available in the system. - return; + string[] availableFonts = { + uiThemeFontFamily, + "Liberation Sans", + "FreeSans", + "DejaVu Sans", + "Lucida Grande", + }; + + foreach (string fontFamily in availableFonts) + { + try + { + using var typeface = SKTypeface.FromFamilyName(fontFamily, SKFontStyle.Normal); + _messageFont = new SKFont(typeface, 26); + _inputTextFont = new SKFont(typeface, _inputTextFontSize); + _labelsTextFont = new SKFont(typeface, 24); + + return; + } + catch + { + } + } + + throw new Exception($"None of these fonts were found in the system: {String.Join(", ", availableFonts)}!"); } - catch - { - // If the font creation fails, try the next font family - } - } - throw new Exception($"None of these fonts were found in the system: {String.Join(", ", availableFonts)}!"); -} - - - private static Color ToColor(ThemeColor color, byte? overrideAlpha = null, bool flipRgb = false) + private static SKColor ToColor(ThemeColor color, byte? overrideAlpha = null, bool flipRgb = false) { var a = (byte)(color.A * 255); var r = (byte)(color.R * 255); @@ -169,34 +162,33 @@ private void CreateFonts(string uiThemeFontFamily) b = (byte)(255 - b); } - return Color.FromRgba(r, g, b, overrideAlpha.GetValueOrDefault(a)); + return new SKColor(r, g, b, overrideAlpha.GetValueOrDefault(a)); } - private static Image LoadResource(Assembly assembly, string resourcePath, int newWidth, int newHeight) + private static SKBitmap LoadResource(Assembly assembly, string resourcePath, int newWidth, int newHeight) { Stream resourceStream = assembly.GetManifestResourceStream(resourcePath); return LoadResource(resourceStream, newWidth, newHeight); } - private static Image LoadResource(Stream resourceStream, int newWidth, int newHeight) + private static SKBitmap LoadResource(Stream resourceStream, int newWidth, int newHeight) { Debug.Assert(resourceStream != null); - var image = Image.Load(resourceStream); + var bitmap = SKBitmap.Decode(resourceStream); if (newHeight != 0 && newWidth != 0) { - image.Mutate(x => x.Resize(newWidth, newHeight, KnownResamplers.Lanczos3)); + var resized = bitmap.Resize(new SKImageInfo(newWidth, newHeight), SKFilterQuality.High); + if (resized != null) + { + bitmap.Dispose(); + bitmap = resized; + } } - return image; - } - - private static void SetGraphicsOptions(IImageProcessingContext context) - { - context.GetGraphicsOptions().Antialias = true; - context.GetShapeGraphicsOptions().GraphicsOptions.Antialias = true; + return bitmap; } private void DrawImmutableElements() @@ -205,65 +197,64 @@ private void CreateFonts(string uiThemeFontFamily) { return; } + var canvas = _surface.Canvas; - _surface.Mutate(context => - { - SetGraphicsOptions(context); + canvas.Clear(SKColors.Transparent); + canvas.DrawRect(_panelRectangle, _panelBrush); + canvas.DrawBitmap(_ryujinxLogo, _logoPosition); - context.Clear(Color.Transparent); - context.Fill(_panelBrush, _panelRectangle); - context.DrawImage(_ryujinxLogo, _logoPosition, 1); + float halfWidth = _panelRectangle.Width / 2; + float buttonsY = _panelRectangle.Top + 185; - float halfWidth = _panelRectangle.Width / 2; - float buttonsY = _panelRectangle.Y + 185; + SKPoint disableButtonPosition = new(halfWidth + 180, buttonsY); - PointF disableButtonPosition = new(halfWidth + 180, buttonsY); - - DrawControllerToggle(context, disableButtonPosition); - }); + DrawControllerToggle(canvas, disableButtonPosition); } - public void DrawMutableElements(SoftwareKeyboardUiState state) + public void DrawMutableElements(SoftwareKeyboardUIState state) { if (_surface == null) { return; } - _surface.Mutate(context => + using var paint = new SKPaint(_messageFont) { - var messageRectangle = MeasureString(MessageText, _messageFont); - float messagePositionX = (_panelRectangle.Width - messageRectangle.Width) / 2 - messageRectangle.X; - float messagePositionY = _messagePositionY - messageRectangle.Y; - var messagePosition = new PointF(messagePositionX, messagePositionY); - var messageBoundRectangle = new RectangleF(messagePositionX, messagePositionY, messageRectangle.Width, messageRectangle.Height); + Color = _textNormalColor, + IsAntialias = true + }; - SetGraphicsOptions(context); + var canvas = _surface.Canvas; + var messageRectangle = MeasureString(MessageText, paint); + float messagePositionX = (_panelRectangle.Width - messageRectangle.Width) / 2 - messageRectangle.Left; + float messagePositionY = _messagePositionY - messageRectangle.Top; + var messagePosition = new SKPoint(messagePositionX, messagePositionY); + var messageBoundRectangle = SKRect.Create(messagePositionX, messagePositionY, messageRectangle.Width, messageRectangle.Height); - context.Fill(_panelBrush, messageBoundRectangle); + canvas.DrawRect(messageBoundRectangle, _panelBrush); - context.DrawText(MessageText, _messageFont, _textNormalColor, messagePosition); + canvas.DrawText(MessageText, messagePosition.X, messagePosition.Y + _messageFont.Metrics.XHeight + _messageFont.Metrics.Descent, paint); - if (!state.TypingEnabled) - { - // Just draw a semi-transparent rectangle on top to fade the component with the background. - // TODO (caian): This will not work if one decides to add make background semi-transparent as well. + if (!state.TypingEnabled) + { + // Just draw a semi-transparent rectangle on top to fade the component with the background. + // TODO (caian): This will not work if one decides to add make background semi-transparent as well. - context.Fill(_disabledBrush, messageBoundRectangle); - } + canvas.DrawRect(messageBoundRectangle, _disabledBrush); + } - DrawTextBox(context, state); + DrawTextBox(canvas, state); - float halfWidth = _panelRectangle.Width / 2; - float buttonsY = _panelRectangle.Y + 185; + float halfWidth = _panelRectangle.Width / 2; + float buttonsY = _panelRectangle.Top + 185; - PointF acceptButtonPosition = new(halfWidth - 180, buttonsY); - PointF cancelButtonPosition = new(halfWidth, buttonsY); - PointF disableButtonPosition = new(halfWidth + 180, buttonsY); + SKPoint acceptButtonPosition = new(halfWidth - 180, buttonsY); + SKPoint cancelButtonPosition = new(halfWidth, buttonsY); + SKPoint disableButtonPosition = new(halfWidth + 180, buttonsY); + + DrawPadButton(canvas, acceptButtonPosition, _padAcceptIcon, AcceptText, state.AcceptPressed, state.ControllerEnabled); + DrawPadButton(canvas, cancelButtonPosition, _padCancelIcon, CancelText, state.CancelPressed, state.ControllerEnabled); - DrawPadButton(context, acceptButtonPosition, _padAcceptIcon, AcceptText, state.AcceptPressed, state.ControllerEnabled); - DrawPadButton(context, cancelButtonPosition, _padCancelIcon, CancelText, state.CancelPressed, state.ControllerEnabled); - }); } public void CreateSurface(RenderingSurfaceInfo surfaceInfo) @@ -286,7 +277,8 @@ private void CreateFonts(string uiThemeFontFamily) Debug.Assert(_surfaceInfo.Height <= totalHeight); Debug.Assert(_surfaceInfo.Pitch * _surfaceInfo.Height <= _surfaceInfo.Size); - _surface = new Image((int)totalWidth, (int)totalHeight); + _imageInfo = new SKImageInfo((int)totalWidth, (int)totalHeight, SKColorType.Rgba8888); + _surface = SKSurface.Create(_imageInfo); ComputeConstants(); DrawImmutableElements(); @@ -300,76 +292,81 @@ private void CreateFonts(string uiThemeFontFamily) int panelHeight = 240; int panelPositionY = totalHeight - panelHeight; - _panelRectangle = new RectangleF(0, panelPositionY, totalWidth, panelHeight); + _panelRectangle = SKRect.Create(0, panelPositionY, totalWidth, panelHeight); _messagePositionY = panelPositionY + 60; int logoPositionX = (totalWidth - _ryujinxLogo.Width) / 2; int logoPositionY = panelPositionY + 18; - _logoPosition = new Point(logoPositionX, logoPositionY); + _logoPosition = new SKPoint(logoPositionX, logoPositionY); } - private static RectangleF MeasureString(string text, Font font) + private static SKRect MeasureString(string text, SKPaint paint) { - RendererOptions options = new(font); + SKRect bounds = SKRect.Empty; - if (text == "") + if (text == string.Empty) { - FontRectangle emptyRectangle = TextMeasurer.Measure(" ", options); - - return new RectangleF(0, emptyRectangle.Y, 0, emptyRectangle.Height); + paint.MeasureText(" ", ref bounds); + } + else + { + paint.MeasureText(text, ref bounds); } - FontRectangle rectangle = TextMeasurer.Measure(text, options); - - return new RectangleF(rectangle.X, rectangle.Y, rectangle.Width, rectangle.Height); + return bounds; } - private static RectangleF MeasureString(ReadOnlySpan text, Font font) + private static SKRect MeasureString(ReadOnlySpan text, SKPaint paint) { - RendererOptions options = new(font); + SKRect bounds = SKRect.Empty; - if (text == "") + if (text == string.Empty) { - FontRectangle emptyRectangle = TextMeasurer.Measure(" ", options); - return new RectangleF(0, emptyRectangle.Y, 0, emptyRectangle.Height); + paint.MeasureText(" ", ref bounds); + } + else + { + paint.MeasureText(text, ref bounds); } - FontRectangle rectangle = TextMeasurer.Measure(text, options); - - return new RectangleF(rectangle.X, rectangle.Y, rectangle.Width, rectangle.Height); + return bounds; } - private void DrawTextBox(IImageProcessingContext context, SoftwareKeyboardUiState state) + private void DrawTextBox(SKCanvas canvas, SoftwareKeyboardUIState state) { - var inputTextRectangle = MeasureString(state.InputText, _inputTextFont); + using var textPaint = new SKPaint(_labelsTextFont) + { + IsAntialias = true, + Color = _textNormalColor + }; + var inputTextRectangle = MeasureString(state.InputText, textPaint); - float boxWidth = (int)(Math.Max(300, inputTextRectangle.Width + inputTextRectangle.X + 8)); + float boxWidth = (int)(Math.Max(300, inputTextRectangle.Width + inputTextRectangle.Left + 8)); float boxHeight = 32; - float boxY = _panelRectangle.Y + 110; + float boxY = _panelRectangle.Top + 110; float boxX = (int)((_panelRectangle.Width - boxWidth) / 2); - RectangleF boxRectangle = new(boxX, boxY, boxWidth, boxHeight); + SKRect boxRectangle = SKRect.Create(boxX, boxY, boxWidth, boxHeight); - RectangleF boundRectangle = new(_panelRectangle.X, boxY - _textBoxOutlineWidth, + SKRect boundRectangle = SKRect.Create(_panelRectangle.Left, boxY - _textBoxOutlineWidth, _panelRectangle.Width, boxHeight + 2 * _textBoxOutlineWidth); - context.Fill(_panelBrush, boundRectangle); + canvas.DrawRect(boundRectangle, _panelBrush); - context.Draw(_textBoxOutlinePen, boxRectangle); + canvas.DrawRect(boxRectangle, _textBoxOutlinePen); - float inputTextX = (_panelRectangle.Width - inputTextRectangle.Width) / 2 - inputTextRectangle.X; + float inputTextX = (_panelRectangle.Width - inputTextRectangle.Width) / 2 - inputTextRectangle.Left; float inputTextY = boxY + 5; - var inputTextPosition = new PointF(inputTextX, inputTextY); - - context.DrawText(state.InputText, _inputTextFont, _textNormalColor, inputTextPosition); + var inputTextPosition = new SKPoint(inputTextX, inputTextY); + canvas.DrawText(state.InputText, inputTextPosition.X, inputTextPosition.Y + (_labelsTextFont.Metrics.XHeight + _labelsTextFont.Metrics.Descent), textPaint); // Draw the cursor on top of the text and redraw the text with a different color if necessary. - Color cursorTextColor; - IBrush cursorBrush; - Pen cursorPen; + SKColor cursorTextColor; + SKPaint cursorBrush; + SKPaint cursorPen; float cursorPositionYTop = inputTextY + 1; float cursorPositionYBottom = cursorPositionYTop + _inputTextFontSize + 1; @@ -389,12 +386,12 @@ private void CreateFonts(string uiThemeFontFamily) ReadOnlySpan textUntilBegin = state.InputText.AsSpan(0, state.CursorBegin); ReadOnlySpan textUntilEnd = state.InputText.AsSpan(0, state.CursorEnd); - var selectionBeginRectangle = MeasureString(textUntilBegin, _inputTextFont); - var selectionEndRectangle = MeasureString(textUntilEnd, _inputTextFont); + var selectionBeginRectangle = MeasureString(textUntilBegin, textPaint); + var selectionEndRectangle = MeasureString(textUntilEnd, textPaint); cursorVisible = true; - cursorPositionXLeft = inputTextX + selectionBeginRectangle.Width + selectionBeginRectangle.X; - cursorPositionXRight = inputTextX + selectionEndRectangle.Width + selectionEndRectangle.X; + cursorPositionXLeft = inputTextX + selectionBeginRectangle.Width + selectionBeginRectangle.Left; + cursorPositionXRight = inputTextX + selectionEndRectangle.Width + selectionEndRectangle.Left; } else { @@ -408,10 +405,10 @@ private void CreateFonts(string uiThemeFontFamily) int cursorBegin = Math.Min(state.InputText.Length, state.CursorBegin); ReadOnlySpan textUntilCursor = state.InputText.AsSpan(0, cursorBegin); - var cursorTextRectangle = MeasureString(textUntilCursor, _inputTextFont); + var cursorTextRectangle = MeasureString(textUntilCursor, textPaint); cursorVisible = true; - cursorPositionXLeft = inputTextX + cursorTextRectangle.Width + cursorTextRectangle.X; + cursorPositionXLeft = inputTextX + cursorTextRectangle.Width + cursorTextRectangle.Left; if (state.OverwriteMode) { @@ -420,8 +417,8 @@ private void CreateFonts(string uiThemeFontFamily) if (state.CursorBegin < state.InputText.Length) { textUntilCursor = state.InputText.AsSpan(0, cursorBegin + 1); - cursorTextRectangle = MeasureString(textUntilCursor, _inputTextFont); - cursorPositionXRight = inputTextX + cursorTextRectangle.Width + cursorTextRectangle.X; + cursorTextRectangle = MeasureString(textUntilCursor, textPaint); + cursorPositionXRight = inputTextX + cursorTextRectangle.Width + cursorTextRectangle.Left; } else { @@ -448,29 +445,32 @@ private void CreateFonts(string uiThemeFontFamily) if (cursorWidth == 0) { - PointF[] points = { - new PointF(cursorPositionXLeft, cursorPositionYTop), - new PointF(cursorPositionXLeft, cursorPositionYBottom), - }; - - context.DrawLines(cursorPen, points); + canvas.DrawLine(new SKPoint(cursorPositionXLeft, cursorPositionYTop), + new SKPoint(cursorPositionXLeft, cursorPositionYBottom), + cursorPen); } else { - var cursorRectangle = new RectangleF(cursorPositionXLeft, cursorPositionYTop, cursorWidth, cursorHeight); + var cursorRectangle = SKRect.Create(cursorPositionXLeft, cursorPositionYTop, cursorWidth, cursorHeight); - context.Draw(cursorPen, cursorRectangle); - context.Fill(cursorBrush, cursorRectangle); + canvas.DrawRect(cursorRectangle, cursorPen); + canvas.DrawRect(cursorRectangle, cursorBrush); - Image textOverCursor = new((int)cursorRectangle.Width, (int)cursorRectangle.Height); - textOverCursor.Mutate(context => + using var textOverCursor = SKSurface.Create(new SKImageInfo((int)cursorRectangle.Width, (int)cursorRectangle.Height, SKColorType.Rgba8888)); + var textOverCanvas = textOverCursor.Canvas; + var textRelativePosition = new SKPoint(inputTextPosition.X - cursorRectangle.Left, inputTextPosition.Y - cursorRectangle.Top); + + using var cursorPaint = new SKPaint(_inputTextFont) { - var textRelativePosition = new PointF(inputTextPosition.X - cursorRectangle.X, inputTextPosition.Y - cursorRectangle.Y); - context.DrawText(state.InputText, _inputTextFont, cursorTextColor, textRelativePosition); - }); + Color = cursorTextColor, + IsAntialias = true + }; - var cursorPosition = new Point((int)cursorRectangle.X, (int)cursorRectangle.Y); - context.DrawImage(textOverCursor, cursorPosition, 1); + textOverCanvas.DrawText(state.InputText, textRelativePosition.X, textRelativePosition.Y + _inputTextFont.Metrics.XHeight + _inputTextFont.Metrics.Descent, cursorPaint); + + var cursorPosition = new SKPoint((int)cursorRectangle.Left, (int)cursorRectangle.Top); + textOverCursor.Flush(); + canvas.DrawSurface(textOverCursor, cursorPosition); } } else if (!state.TypingEnabled) @@ -478,25 +478,31 @@ private void CreateFonts(string uiThemeFontFamily) // Just draw a semi-transparent rectangle on top to fade the component with the background. // TODO (caian): This will not work if one decides to add make background semi-transparent as well. - context.Fill(_disabledBrush, boundRectangle); + canvas.DrawRect(boundRectangle, _disabledBrush); } } - private void DrawPadButton(IImageProcessingContext context, PointF point, Image icon, string label, bool pressed, bool enabled) + private void DrawPadButton(SKCanvas canvas, SKPoint point, SKBitmap icon, string label, bool pressed, bool enabled) { - // Use relative positions so we can center the the entire drawing later. + // Use relative positions so we can center the entire drawing later. float iconX = 0; float iconY = 0; float iconWidth = icon.Width; float iconHeight = icon.Height; - var labelRectangle = MeasureString(label, _labelsTextFont); + using var paint = new SKPaint(_labelsTextFont) + { + Color = _textNormalColor, + IsAntialias = true + }; - float labelPositionX = iconWidth + 8 - labelRectangle.X; + var labelRectangle = MeasureString(label, paint); + + float labelPositionX = iconWidth + 8 - labelRectangle.Left; float labelPositionY = 3; - float fullWidth = labelPositionX + labelRectangle.Width + labelRectangle.X; + float fullWidth = labelPositionX + labelRectangle.Width + labelRectangle.Left; float fullHeight = iconHeight; // Convert all relative positions into absolute. @@ -507,24 +513,24 @@ private void CreateFonts(string uiThemeFontFamily) iconX += originX; iconY += originY; - var iconPosition = new Point((int)iconX, (int)iconY); - var labelPosition = new PointF(labelPositionX + originX, labelPositionY + originY); + var iconPosition = new SKPoint((int)iconX, (int)iconY); + var labelPosition = new SKPoint(labelPositionX + originX, labelPositionY + originY); - var selectedRectangle = new RectangleF(originX - 2 * _padPressedPenWidth, originY - 2 * _padPressedPenWidth, + var selectedRectangle = SKRect.Create(originX - 2 * _padPressedPenWidth, originY - 2 * _padPressedPenWidth, fullWidth + 4 * _padPressedPenWidth, fullHeight + 4 * _padPressedPenWidth); - var boundRectangle = new RectangleF(originX, originY, fullWidth, fullHeight); + var boundRectangle = SKRect.Create(originX, originY, fullWidth, fullHeight); boundRectangle.Inflate(4 * _padPressedPenWidth, 4 * _padPressedPenWidth); - context.Fill(_panelBrush, boundRectangle); - context.DrawImage(icon, iconPosition, 1); - context.DrawText(label, _labelsTextFont, _textNormalColor, labelPosition); + canvas.DrawRect(boundRectangle, _panelBrush); + canvas.DrawBitmap(icon, iconPosition); + canvas.DrawText(label, labelPosition.X, labelPosition.Y + _labelsTextFont.Metrics.XHeight + _labelsTextFont.Metrics.Descent, paint); if (enabled) { if (pressed) { - context.Draw(_padPressedPen, selectedRectangle); + canvas.DrawRect(selectedRectangle, _padPressedPen); } } else @@ -532,21 +538,26 @@ private void CreateFonts(string uiThemeFontFamily) // Just draw a semi-transparent rectangle on top to fade the component with the background. // TODO (caian): This will not work if one decides to add make background semi-transparent as well. - context.Fill(_disabledBrush, boundRectangle); + canvas.DrawRect(boundRectangle, _disabledBrush); } } - private void DrawControllerToggle(IImageProcessingContext context, PointF point) + private void DrawControllerToggle(SKCanvas canvas, SKPoint point) { - var labelRectangle = MeasureString(ControllerToggleText, _labelsTextFont); + using var paint = new SKPaint(_labelsTextFont) + { + IsAntialias = true, + Color = _textNormalColor + }; + var labelRectangle = MeasureString(ControllerToggleText, paint); - // Use relative positions so we can center the the entire drawing later. + // Use relative positions so we can center the entire drawing later. float keyWidth = _keyModeIcon.Width; float keyHeight = _keyModeIcon.Height; - float labelPositionX = keyWidth + 8 - labelRectangle.X; - float labelPositionY = -labelRectangle.Y - 1; + float labelPositionX = keyWidth + 8 - labelRectangle.Left; + float labelPositionY = -labelRectangle.Top - 1; float keyX = 0; float keyY = (int)((labelPositionY + labelRectangle.Height - keyHeight) / 2); @@ -562,14 +573,14 @@ private void CreateFonts(string uiThemeFontFamily) keyX += originX; keyY += originY; - var labelPosition = new PointF(labelPositionX + originX, labelPositionY + originY); - var overlayPosition = new Point((int)keyX, (int)keyY); + var labelPosition = new SKPoint(labelPositionX + originX, labelPositionY + originY); + var overlayPosition = new SKPoint((int)keyX, (int)keyY); - context.DrawImage(_keyModeIcon, overlayPosition, 1); - context.DrawText(ControllerToggleText, _labelsTextFont, _textNormalColor, labelPosition); + canvas.DrawBitmap(_keyModeIcon, overlayPosition); + canvas.DrawText(ControllerToggleText, labelPosition.X, labelPosition.Y + _labelsTextFont.Metrics.XHeight, paint); } - public void CopyImageToBuffer() + public unsafe void CopyImageToBuffer() { lock (_bufferLock) { @@ -579,21 +590,20 @@ private void CreateFonts(string uiThemeFontFamily) } // Convert the pixel format used in the image to the one used in the Switch surface. + _surface.Flush(); - if (!_surface.TryGetSinglePixelSpan(out Span pixels)) + var buffer = new byte[_imageInfo.BytesSize]; + fixed (byte* bufferPtr = buffer) { - return; + if (!_surface.ReadPixels(_imageInfo, (nint)bufferPtr, _imageInfo.RowBytes, 0, 0)) + { + return; + } } - _bufferData = MemoryMarshal.AsBytes(pixels).ToArray(); - Span dataConvert = MemoryMarshal.Cast(_bufferData); + _bufferData = buffer; - Debug.Assert(_bufferData.Length == _surfaceInfo.Size); - - for (int i = 0; i < dataConvert.Length; i++) - { - dataConvert[i] = BitOperations.RotateRight(dataConvert[i], 8); - } + Debug.Assert(buffer.Length == _surfaceInfo.Size); } } diff --git a/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardUiArgs.cs b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardUiArgs.cs index 52fa7ed85..854f04a3b 100644 --- a/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardUiArgs.cs +++ b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardUiArgs.cs @@ -2,7 +2,7 @@ using Ryujinx.HLE.HOS.Applets.SoftwareKeyboard; namespace Ryujinx.HLE.HOS.Applets { - public struct SoftwareKeyboardUiArgs + public struct SoftwareKeyboardUIArgs { public KeyboardMode KeyboardMode; public string HeaderText; diff --git a/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardUiState.cs b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardUiState.cs index 608d51f32..da5220364 100644 --- a/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardUiState.cs +++ b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardUiState.cs @@ -1,13 +1,13 @@ -using Ryujinx.HLE.Ui; +using Ryujinx.HLE.UI; namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard { /// /// TODO /// - internal class SoftwareKeyboardUiState + internal class SoftwareKeyboardUIState { - public string InputText = ""; + public string InputText = string.Empty; public int CursorBegin = 0; public int CursorEnd = 0; public bool AcceptPressed = false; diff --git a/src/Ryujinx.HLE/HOS/ArmProcessContext.cs b/src/Ryujinx.HLE/HOS/ArmProcessContext.cs index 476e5d6a9..09a721644 100644 --- a/src/Ryujinx.HLE/HOS/ArmProcessContext.cs +++ b/src/Ryujinx.HLE/HOS/ArmProcessContext.cs @@ -13,7 +13,8 @@ namespace Ryujinx.HLE.HOS string displayVersion, bool diskCacheEnabled, ulong codeAddress, - ulong codeSize); + ulong codeSize, + string cacheSelector); } class ArmProcessContext : IArmProcessContext where T : class, IVirtualMemoryManagerTracked, IMemoryManager @@ -57,6 +58,8 @@ namespace Ryujinx.HLE.HOS public void Execute(IExecutionContext context, ulong codeAddress) { + // We must wait until shader cache is loaded, among other things, before executing CPU code. + _gpuContext.WaitUntilGpuReady(); _cpuContext.Execute(context, codeAddress); } @@ -65,10 +68,11 @@ namespace Ryujinx.HLE.HOS string displayVersion, bool diskCacheEnabled, ulong codeAddress, - ulong codeSize) + ulong codeSize, + string cacheSelector) { _cpuContext.PrepareCodeRange(codeAddress, codeSize); - return _cpuContext.LoadDiskCache(titleIdText, displayVersion, diskCacheEnabled); + return _cpuContext.LoadDiskCache(titleIdText, displayVersion, diskCacheEnabled, cacheSelector); } public void InvalidateCacheRegion(ulong address, ulong size) diff --git a/src/Ryujinx.HLE/HOS/ArmProcessContextFactory.cs b/src/Ryujinx.HLE/HOS/ArmProcessContextFactory.cs index a6fbb5b24..14775fb1d 100644 --- a/src/Ryujinx.HLE/HOS/ArmProcessContextFactory.cs +++ b/src/Ryujinx.HLE/HOS/ArmProcessContextFactory.cs @@ -1,4 +1,4 @@ -using Ryujinx.Common.Configuration; +using Ryujinx.Common.Configuration; using Ryujinx.Common.Logging; using Ryujinx.Cpu; using Ryujinx.Cpu.AppleHv; @@ -72,9 +72,10 @@ namespace Ryujinx.HLE.HOS AddressSpace addressSpace = null; - if ((mode == MemoryManagerMode.HostMapped || mode == MemoryManagerMode.HostMappedUnsafe) && (MemoryBlock.GetPageSize() <= 0x1000)) + // We want to use host tracked mode if the host page size is > 4KB. + if ((mode == MemoryManagerMode.HostMapped || mode == MemoryManagerMode.HostMappedUnsafe) && MemoryBlock.GetPageSize() <= 0x1000) { - if (!AddressSpace.TryCreate(context.Memory, addressSpaceSize, MemoryBlock.GetPageSize() == MemoryManagerHostMapped.PageSize, out addressSpace)) + if (!AddressSpace.TryCreate(context.Memory, addressSpaceSize, out addressSpace)) { Logger.Warning?.Print(LogClass.Cpu, "Address space creation failed, falling back to software page table"); @@ -93,7 +94,7 @@ namespace Ryujinx.HLE.HOS case MemoryManagerMode.HostMappedUnsafe: if (addressSpace == null) { - var memoryManagerHostTracked = new MemoryManagerHostTracked(context.Memory, addressSpaceSize, invalidAccessHandler); + var memoryManagerHostTracked = new MemoryManagerHostTracked(context.Memory, addressSpaceSize, mode == MemoryManagerMode.HostMappedUnsafe, invalidAccessHandler); processContext = new ArmProcessContext(pid, cpuEngine, _gpu, memoryManagerHostTracked, addressSpaceSize, for64Bit); } else @@ -113,7 +114,7 @@ namespace Ryujinx.HLE.HOS } } - DiskCacheLoadState = processContext.Initialize(_titleIdText, _displayVersion, _diskCacheEnabled, _codeAddress, _codeSize); + DiskCacheLoadState = processContext.Initialize(_titleIdText, _displayVersion, _diskCacheEnabled, _codeAddress, _codeSize, "default"); //Ready for exefs profiles return processContext; } diff --git a/src/Ryujinx.HLE/HOS/Diagnostics/Demangler/Demangler.cs b/src/Ryujinx.HLE/HOS/Diagnostics/Demangler/Demangler.cs index 171a083f3..2e7b8ee76 100644 --- a/src/Ryujinx.HLE/HOS/Diagnostics/Demangler/Demangler.cs +++ b/src/Ryujinx.HLE/HOS/Diagnostics/Demangler/Demangler.cs @@ -2463,7 +2463,7 @@ namespace Ryujinx.HLE.HOS.Diagnostics.Demangler return ParseIntegerLiteral("unsigned short"); case 'i': _position++; - return ParseIntegerLiteral(""); + return ParseIntegerLiteral(string.Empty); case 'j': _position++; return ParseIntegerLiteral("u"); diff --git a/src/Ryujinx.HLE/HOS/Horizon.cs b/src/Ryujinx.HLE/HOS/Horizon.cs index 1a402240f..64b08e309 100644 --- a/src/Ryujinx.HLE/HOS/Horizon.cs +++ b/src/Ryujinx.HLE/HOS/Horizon.cs @@ -4,12 +4,6 @@ using LibHac.Fs; using LibHac.Fs.Shim; using LibHac.FsSystem; using LibHac.Tools.FsSystem; -using Ryujinx.Audio; -using Ryujinx.Audio.Input; -using Ryujinx.Audio.Integration; -using Ryujinx.Audio.Output; -using Ryujinx.Audio.Renderer.Device; -using Ryujinx.Audio.Renderer.Server; using Ryujinx.Cpu; using Ryujinx.HLE.FileSystem; using Ryujinx.HLE.HOS.Kernel; @@ -20,7 +14,6 @@ using Ryujinx.HLE.HOS.Services; using Ryujinx.HLE.HOS.Services.Account.Acc; using Ryujinx.HLE.HOS.Services.Am.AppletAE.AllSystemAppletProxiesService.SystemAppletProxy; using Ryujinx.HLE.HOS.Services.Apm; -using Ryujinx.HLE.HOS.Services.Audio.AudioRenderer; using Ryujinx.HLE.HOS.Services.Caps; using Ryujinx.HLE.HOS.Services.Mii; using Ryujinx.HLE.HOS.Services.Nfc.Nfp.NfpManager; @@ -61,11 +54,6 @@ namespace Ryujinx.HLE.HOS internal ITickSource TickSource { get; } internal SurfaceFlinger SurfaceFlinger { get; private set; } - internal AudioManager AudioManager { get; private set; } - internal AudioOutputManager AudioOutputManager { get; private set; } - internal AudioInputManager AudioInputManager { get; private set; } - internal AudioRendererManager AudioRendererManager { get; private set; } - internal VirtualDeviceSessionRegistry AudioDeviceSessionRegistry { get; private set; } public SystemStateMgr State { get; private set; } @@ -79,8 +67,6 @@ namespace Ryujinx.HLE.HOS internal ServerBase SmServer { get; private set; } internal ServerBase BsdServer { get; private set; } - internal ServerBase AudRenServer { get; private set; } - internal ServerBase AudOutServer { get; private set; } internal ServerBase FsServer { get; private set; } internal ServerBase HidServer { get; private set; } internal ServerBase NvDrvServer { get; private set; } @@ -248,60 +234,9 @@ namespace Ryujinx.HLE.HOS HostSyncpoint = new NvHostSyncpt(device); SurfaceFlinger = new SurfaceFlinger(device); - - InitializeAudioRenderer(TickSource); - InitializeServices(); } - private void InitializeAudioRenderer(ITickSource tickSource) - { - AudioManager = new AudioManager(); - AudioOutputManager = new AudioOutputManager(); - AudioInputManager = new AudioInputManager(); - AudioRendererManager = new AudioRendererManager(tickSource); - AudioRendererManager.SetVolume(Device.Configuration.AudioVolume); - AudioDeviceSessionRegistry = new VirtualDeviceSessionRegistry(Device.AudioDeviceDriver); - - IWritableEvent[] audioOutputRegisterBufferEvents = new IWritableEvent[Constants.AudioOutSessionCountMax]; - - for (int i = 0; i < audioOutputRegisterBufferEvents.Length; i++) - { - KEvent registerBufferEvent = new(KernelContext); - - audioOutputRegisterBufferEvents[i] = new AudioKernelEvent(registerBufferEvent); - } - - AudioOutputManager.Initialize(Device.AudioDeviceDriver, audioOutputRegisterBufferEvents); - AudioOutputManager.SetVolume(Device.Configuration.AudioVolume); - - IWritableEvent[] audioInputRegisterBufferEvents = new IWritableEvent[Constants.AudioInSessionCountMax]; - - for (int i = 0; i < audioInputRegisterBufferEvents.Length; i++) - { - KEvent registerBufferEvent = new(KernelContext); - - audioInputRegisterBufferEvents[i] = new AudioKernelEvent(registerBufferEvent); - } - - AudioInputManager.Initialize(Device.AudioDeviceDriver, audioInputRegisterBufferEvents); - - IWritableEvent[] systemEvents = new IWritableEvent[Constants.AudioRendererSessionCountMax]; - - for (int i = 0; i < systemEvents.Length; i++) - { - KEvent systemEvent = new(KernelContext); - - systemEvents[i] = new AudioKernelEvent(systemEvent); - } - - AudioManager.Initialize(Device.AudioDeviceDriver.GetUpdateRequiredEvent(), AudioOutputManager.Update, AudioInputManager.Update); - - AudioRendererManager.Initialize(systemEvents, Device.AudioDeviceDriver); - - AudioManager.Start(); - } - - private void InitializeServices() + public void InitializeServices() { SmRegistry = new SmRegistry(); SmServer = new ServerBase(KernelContext, "SmServer", () => new IUserInterface(KernelContext, SmRegistry)); @@ -311,8 +246,6 @@ namespace Ryujinx.HLE.HOS SmServer.InitDone.WaitOne(); BsdServer = new ServerBase(KernelContext, "BsdServer"); - AudRenServer = new ServerBase(KernelContext, "AudioRendererServer"); - AudOutServer = new ServerBase(KernelContext, "AudioOutServer"); FsServer = new ServerBase(KernelContext, "FsServer"); HidServer = new ServerBase(KernelContext, "HidServer"); NvDrvServer = new ServerBase(KernelContext, "NvservicesServer"); @@ -330,7 +263,13 @@ namespace Ryujinx.HLE.HOS HorizonFsClient fsClient = new(this); ServiceTable = new ServiceTable(); - var services = ServiceTable.GetServices(new HorizonOptions(Device.Configuration.IgnoreMissingServices, LibHacHorizonManager.BcatClient, fsClient)); + var services = ServiceTable.GetServices(new HorizonOptions + (Device.Configuration.IgnoreMissingServices, + LibHacHorizonManager.BcatClient, + fsClient, + AccountManager, + Device.AudioDeviceDriver, + TickSource)); foreach (var service in services) { @@ -385,17 +324,6 @@ namespace Ryujinx.HLE.HOS } } - public void SetVolume(float volume) - { - AudioOutputManager.SetVolume(volume); - AudioRendererManager.SetVolume(volume); - } - - public float GetVolume() - { - return AudioOutputManager.GetVolume() == 0 ? AudioRendererManager.GetVolume() : AudioOutputManager.GetVolume(); - } - public void ReturnFocus() { AppletState.SetFocus(true); @@ -459,11 +387,7 @@ namespace Ryujinx.HLE.HOS // "Soft" stops AudioRenderer and AudioManager to avoid some sound between resume and stop. if (IsPaused) { - AudioManager.StopUpdates(); - TogglePauseEmulation(false); - - AudioRendererManager.StopSendingCommands(); } KProcess terminationProcess = new(KernelContext); @@ -514,12 +438,6 @@ namespace Ryujinx.HLE.HOS // This is safe as KThread that are likely to call ioctls are going to be terminated by the post handler hook on the SVC facade. INvDrvServices.Destroy(); - AudioManager.Dispose(); - AudioOutputManager.Dispose(); - AudioInputManager.Dispose(); - - AudioRendererManager.Dispose(); - if (LibHacHorizonManager.ApplicationClient != null) { LibHacHorizonManager.PmClient.Fs.UnregisterProgram(LibHacHorizonManager.ApplicationClient.Os.GetCurrentProcessId().Value).ThrowIfFailure(); diff --git a/src/Ryujinx.HLE/HOS/Kernel/Common/KSystemControl.cs b/src/Ryujinx.HLE/HOS/Kernel/Common/KSystemControl.cs index 10f0b6f78..119d18d27 100644 --- a/src/Ryujinx.HLE/HOS/Kernel/Common/KSystemControl.cs +++ b/src/Ryujinx.HLE/HOS/Kernel/Common/KSystemControl.cs @@ -28,8 +28,9 @@ namespace Ryujinx.HLE.HOS.Kernel.Common MemoryArrange.MemoryArrange4GiBSystemDev or MemoryArrange.MemoryArrange6GiBAppletDev => 3285 * MiB, MemoryArrange.MemoryArrange4GiBAppletDev => 2048 * MiB, - MemoryArrange.MemoryArrange6GiB or - MemoryArrange.MemoryArrange8GiB => 4916 * MiB, + MemoryArrange.MemoryArrange6GiB => 4916 * MiB, + MemoryArrange.MemoryArrange8GiB => 6964 * MiB, + MemoryArrange.MemoryArrange12GiB => 11060 * MiB, _ => throw new ArgumentException($"Invalid memory arrange \"{arrange}\"."), }; } @@ -42,8 +43,9 @@ namespace Ryujinx.HLE.HOS.Kernel.Common MemoryArrange.MemoryArrange4GiBAppletDev => 1554 * MiB, MemoryArrange.MemoryArrange4GiBSystemDev => 448 * MiB, MemoryArrange.MemoryArrange6GiB => 562 * MiB, - MemoryArrange.MemoryArrange6GiBAppletDev or - MemoryArrange.MemoryArrange8GiB => 2193 * MiB, + MemoryArrange.MemoryArrange6GiBAppletDev => 2193 * MiB, + MemoryArrange.MemoryArrange8GiB or + MemoryArrange.MemoryArrange12GiB => 562 * MiB, _ => throw new ArgumentException($"Invalid memory arrange \"{arrange}\"."), }; } @@ -71,6 +73,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Common MemorySize.MemorySize4GiB => 4 * GiB, MemorySize.MemorySize6GiB => 6 * GiB, MemorySize.MemorySize8GiB => 8 * GiB, + MemorySize.MemorySize12GiB => 12 * GiB, _ => throw new ArgumentException($"Invalid memory size \"{size}\"."), }; } diff --git a/src/Ryujinx.HLE/HOS/Kernel/Common/MemoryArrange.cs b/src/Ryujinx.HLE/HOS/Kernel/Common/MemoryArrange.cs index 2c88d8b35..d06c97b68 100644 --- a/src/Ryujinx.HLE/HOS/Kernel/Common/MemoryArrange.cs +++ b/src/Ryujinx.HLE/HOS/Kernel/Common/MemoryArrange.cs @@ -8,5 +8,6 @@ namespace Ryujinx.HLE.HOS.Kernel.Common MemoryArrange6GiB, MemoryArrange6GiBAppletDev, MemoryArrange8GiB, + MemoryArrange12GiB, } } diff --git a/src/Ryujinx.HLE/HOS/Kernel/Common/MemorySize.cs b/src/Ryujinx.HLE/HOS/Kernel/Common/MemorySize.cs index 7cc34a722..f92859db4 100644 --- a/src/Ryujinx.HLE/HOS/Kernel/Common/MemorySize.cs +++ b/src/Ryujinx.HLE/HOS/Kernel/Common/MemorySize.cs @@ -5,5 +5,6 @@ namespace Ryujinx.HLE.HOS.Kernel.Common MemorySize4GiB = 0, MemorySize6GiB = 1, MemorySize8GiB = 2, + MemorySize12GiB = 3, } } diff --git a/src/Ryujinx.HLE/HOS/Kernel/Ipc/KServerSession.cs b/src/Ryujinx.HLE/HOS/Kernel/Ipc/KServerSession.cs index 7e41a3f3a..3b4280855 100644 --- a/src/Ryujinx.HLE/HOS/Kernel/Ipc/KServerSession.cs +++ b/src/Ryujinx.HLE/HOS/Kernel/Ipc/KServerSession.cs @@ -570,7 +570,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Ipc } else { - serverProcess.CpuMemory.Write(copyDst, clientProcess.CpuMemory.GetSpan(copySrc, (int)copySize)); + serverProcess.CpuMemory.Write(copyDst, clientProcess.CpuMemory.GetReadOnlySequence(copySrc, (int)copySize)); } if (clientResult != Result.Success) @@ -858,7 +858,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Ipc } else { - clientProcess.CpuMemory.Write(copyDst, serverProcess.CpuMemory.GetSpan(copySrc, (int)copySize)); + clientProcess.CpuMemory.Write(copyDst, serverProcess.CpuMemory.GetReadOnlySequence(copySrc, (int)copySize)); } } diff --git a/src/Ryujinx.HLE/HOS/Kernel/Memory/AddressSpaceType.cs b/src/Ryujinx.HLE/HOS/Kernel/Memory/AddressSpaceType.cs deleted file mode 100644 index 8dfa4303f..000000000 --- a/src/Ryujinx.HLE/HOS/Kernel/Memory/AddressSpaceType.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Ryujinx.HLE.HOS.Kernel.Memory -{ - enum AddressSpaceType - { - Addr32Bits = 0, - Addr36Bits = 1, - Addr32BitsNoMap = 2, - Addr39Bits = 3, - } -} diff --git a/src/Ryujinx.HLE/HOS/Kernel/Memory/KPageTable.cs b/src/Ryujinx.HLE/HOS/Kernel/Memory/KPageTable.cs index 4c8858bb0..4ffa447dd 100644 --- a/src/Ryujinx.HLE/HOS/Kernel/Memory/KPageTable.cs +++ b/src/Ryujinx.HLE/HOS/Kernel/Memory/KPageTable.cs @@ -2,6 +2,7 @@ using Ryujinx.Horizon.Common; using Ryujinx.Memory; using Ryujinx.Memory.Range; using System; +using System.Buffers; using System.Collections.Generic; using System.Diagnostics; @@ -11,7 +12,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Memory { private readonly IVirtualMemoryManager _cpuMemory; - protected override bool Supports4KBPages => _cpuMemory.Supports4KBPages; + protected override bool UsesPrivateAllocations => _cpuMemory.UsesPrivateAllocations; public KPageTable(KernelContext context, IVirtualMemoryManager cpuMemory, ulong reservedAddressSpaceSize) : base(context, reservedAddressSpaceSize) { @@ -34,6 +35,12 @@ namespace Ryujinx.HLE.HOS.Kernel.Memory } } + /// + protected override ReadOnlySequence GetReadOnlySequence(ulong va, int size) + { + return _cpuMemory.GetReadOnlySequence(va, size); + } + /// protected override ReadOnlySpan GetSpan(ulong va, int size) { @@ -119,7 +126,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Memory Context.MemoryManager.IncrementPagesReferenceCount(srcPa, pagesCount); } - if (shouldFillPages && (Supports4KBPages || !flags.HasFlag(MemoryMapFlags.Private) || fillValue != 0)) + if (shouldFillPages) { _cpuMemory.Fill(dstVa, size, fillValue); } @@ -149,7 +156,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Memory _cpuMemory.Map(currentVa, addr, size, flags); - if (shouldFillPages && (Supports4KBPages || !flags.HasFlag(MemoryMapFlags.Private) || fillValue != 0)) + if (shouldFillPages) { _cpuMemory.Fill(currentVa, size, fillValue); } @@ -247,6 +254,12 @@ namespace Ryujinx.HLE.HOS.Kernel.Memory _cpuMemory.SignalMemoryTracking(va, size, write); } + /// + protected override void Write(ulong va, ReadOnlySequence data) + { + _cpuMemory.Write(va, data); + } + /// protected override void Write(ulong va, ReadOnlySpan data) { diff --git a/src/Ryujinx.HLE/HOS/Kernel/Memory/KPageTableBase.cs b/src/Ryujinx.HLE/HOS/Kernel/Memory/KPageTableBase.cs index b065e9c58..bf2bbb97b 100644 --- a/src/Ryujinx.HLE/HOS/Kernel/Memory/KPageTableBase.cs +++ b/src/Ryujinx.HLE/HOS/Kernel/Memory/KPageTableBase.cs @@ -5,6 +5,7 @@ using Ryujinx.Horizon.Common; using Ryujinx.Memory; using Ryujinx.Memory.Range; using System; +using System.Buffers; using System.Collections.Generic; using System.Diagnostics; @@ -32,7 +33,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Memory private const int MaxBlocksNeededForInsertion = 2; protected readonly KernelContext Context; - protected virtual bool Supports4KBPages => true; + protected virtual bool UsesPrivateAllocations => false; public ulong AddrSpaceStart { get; private set; } public ulong AddrSpaceEnd { get; private set; } @@ -57,11 +58,10 @@ namespace Ryujinx.HLE.HOS.Kernel.Memory public ulong AslrRegionStart { get; private set; } public ulong AslrRegionEnd { get; private set; } -#pragma warning disable IDE0052 // Remove unread private member private ulong _heapCapacity; -#pragma warning restore IDE0052 public ulong PhysicalMemoryUsage { get; private set; } + public ulong AliasRegionExtraSize { get; private set; } private readonly KMemoryBlockManager _blockManager; @@ -97,30 +97,21 @@ namespace Ryujinx.HLE.HOS.Kernel.Memory _reservedAddressSpaceSize = reservedAddressSpaceSize; } - private static readonly int[] _addrSpaceSizes = { 32, 36, 32, 39 }; - public Result InitializeForProcess( - AddressSpaceType addrSpaceType, - bool aslrEnabled, + ProcessCreationFlags flags, bool fromBack, MemoryRegion memRegion, ulong address, ulong size, KMemoryBlockSlabManager slabManager) { - if ((uint)addrSpaceType > (uint)AddressSpaceType.Addr39Bits) - { - throw new ArgumentException($"AddressSpaceType bigger than {(uint)AddressSpaceType.Addr39Bits}: {(uint)addrSpaceType}", nameof(addrSpaceType)); - } - _contextId = Context.ContextIdManager.GetId(); ulong addrSpaceBase = 0; - ulong addrSpaceSize = 1UL << _addrSpaceSizes[(int)addrSpaceType]; + ulong addrSpaceSize = 1UL << GetAddressSpaceWidth(flags); Result result = CreateUserAddressSpace( - addrSpaceType, - aslrEnabled, + flags, fromBack, addrSpaceBase, addrSpaceSize, @@ -137,6 +128,22 @@ namespace Ryujinx.HLE.HOS.Kernel.Memory return result; } + private static int GetAddressSpaceWidth(ProcessCreationFlags flags) + { + switch (flags & ProcessCreationFlags.AddressSpaceMask) + { + case ProcessCreationFlags.AddressSpace32Bit: + case ProcessCreationFlags.AddressSpace32BitWithoutAlias: + return 32; + case ProcessCreationFlags.AddressSpace64BitDeprecated: + return 36; + case ProcessCreationFlags.AddressSpace64Bit: + return 39; + } + + throw new ArgumentException($"Invalid process flags {flags}", nameof(flags)); + } + private struct Region { public ulong Start; @@ -146,8 +153,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Memory } private Result CreateUserAddressSpace( - AddressSpaceType addrSpaceType, - bool aslrEnabled, + ProcessCreationFlags flags, bool fromBack, ulong addrSpaceStart, ulong addrSpaceEnd, @@ -167,9 +173,11 @@ namespace Ryujinx.HLE.HOS.Kernel.Memory ulong stackAndTlsIoStart; ulong stackAndTlsIoEnd; - switch (addrSpaceType) + AliasRegionExtraSize = 0; + + switch (flags & ProcessCreationFlags.AddressSpaceMask) { - case AddressSpaceType.Addr32Bits: + case ProcessCreationFlags.AddressSpace32Bit: aliasRegion.Size = 0x40000000; heapRegion.Size = 0x40000000; stackRegion.Size = 0; @@ -182,7 +190,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Memory stackAndTlsIoEnd = 0x40000000; break; - case AddressSpaceType.Addr36Bits: + case ProcessCreationFlags.AddressSpace64BitDeprecated: aliasRegion.Size = 0x180000000; heapRegion.Size = 0x180000000; stackRegion.Size = 0; @@ -195,7 +203,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Memory stackAndTlsIoEnd = 0x80000000; break; - case AddressSpaceType.Addr32BitsNoMap: + case ProcessCreationFlags.AddressSpace32BitWithoutAlias: aliasRegion.Size = 0; heapRegion.Size = 0x80000000; stackRegion.Size = 0; @@ -208,7 +216,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Memory stackAndTlsIoEnd = 0x40000000; break; - case AddressSpaceType.Addr39Bits: + case ProcessCreationFlags.AddressSpace64Bit: if (_reservedAddressSpaceSize < addrSpaceEnd) { int addressSpaceWidth = (int)ulong.Log2(_reservedAddressSpaceSize); @@ -217,8 +225,8 @@ namespace Ryujinx.HLE.HOS.Kernel.Memory heapRegion.Size = 0x180000000; stackRegion.Size = 1UL << (addressSpaceWidth - 8); tlsIoRegion.Size = 1UL << (addressSpaceWidth - 3); - CodeRegionStart = BitUtils.AlignDown(address, RegionAlignment); - codeRegionSize = BitUtils.AlignUp(endAddr, RegionAlignment) - CodeRegionStart; + CodeRegionStart = BitUtils.AlignDown(address, RegionAlignment); + codeRegionSize = BitUtils.AlignUp(endAddr, RegionAlignment) - CodeRegionStart; stackAndTlsIoStart = 0; stackAndTlsIoEnd = 0; AslrRegionStart = 0x8000000; @@ -238,9 +246,16 @@ namespace Ryujinx.HLE.HOS.Kernel.Memory stackAndTlsIoStart = 0; stackAndTlsIoEnd = 0; } + + if (flags.HasFlag(ProcessCreationFlags.EnableAliasRegionExtraSize)) + { + AliasRegionExtraSize = addrSpaceEnd / 8; + aliasRegion.Size += AliasRegionExtraSize; + } break; + default: - throw new ArgumentException($"AddressSpaceType bigger than {(uint)AddressSpaceType.Addr39Bits}: {(uint)addrSpaceType}", nameof(addrSpaceType)); + throw new ArgumentException($"Invalid process flags {flags}", nameof(flags)); } CodeRegionEnd = CodeRegionStart + codeRegionSize; @@ -265,6 +280,8 @@ namespace Ryujinx.HLE.HOS.Kernel.Memory ulong aslrMaxOffset = mapAvailableSize - mapTotalSize; + bool aslrEnabled = flags.HasFlag(ProcessCreationFlags.EnableAslr); + _aslrEnabled = aslrEnabled; AddrSpaceStart = addrSpaceStart; @@ -673,9 +690,9 @@ namespace Ryujinx.HLE.HOS.Kernel.Memory MemoryState.UnmapProcessCodeMemoryAllowed, KMemoryPermission.None, KMemoryPermission.None, - MemoryAttribute.Mask, + MemoryAttribute.Mask & ~MemoryAttribute.PermissionLocked, MemoryAttribute.None, - MemoryAttribute.IpcAndDeviceMapped | MemoryAttribute.PermissionLocked, + MemoryAttribute.IpcAndDeviceMapped, out MemoryState state, out _, out _); @@ -724,7 +741,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Memory { address = 0; - if (size > HeapRegionEnd - HeapRegionStart) + if (size > HeapRegionEnd - HeapRegionStart || size > _heapCapacity) { return KernelResult.OutOfMemory; } @@ -1568,7 +1585,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Memory while (size > 0) { - ulong copySize = 0x100000; // Copy chunck size. Any value will do, moderate sizes are recommended. + ulong copySize = int.MaxValue; if (copySize > size) { @@ -1577,11 +1594,11 @@ namespace Ryujinx.HLE.HOS.Kernel.Memory if (toServer) { - currentProcess.CpuMemory.Write(serverAddress, GetSpan(clientAddress, (int)copySize)); + currentProcess.CpuMemory.Write(serverAddress, GetReadOnlySequence(clientAddress, (int)copySize)); } else { - Write(clientAddress, currentProcess.CpuMemory.GetSpan(serverAddress, (int)copySize)); + Write(clientAddress, currentProcess.CpuMemory.GetReadOnlySequence(serverAddress, (int)copySize)); } serverAddress += copySize; @@ -1911,9 +1928,9 @@ namespace Ryujinx.HLE.HOS.Kernel.Memory Context.Memory.Fill(GetDramAddressFromPa(dstFirstPagePa), unusedSizeBefore, (byte)_ipcFillValue); ulong copySize = addressRounded <= endAddr ? addressRounded - address : size; - var data = srcPageTable.GetSpan(addressTruncated + unusedSizeBefore, (int)copySize); + var data = srcPageTable.GetReadOnlySequence(addressTruncated + unusedSizeBefore, (int)copySize); - Context.Memory.Write(GetDramAddressFromPa(dstFirstPagePa + unusedSizeBefore), data); + ((IWritableBlock)Context.Memory).Write(GetDramAddressFromPa(dstFirstPagePa + unusedSizeBefore), data); firstPageFillAddress += unusedSizeBefore + copySize; @@ -1947,17 +1964,17 @@ namespace Ryujinx.HLE.HOS.Kernel.Memory Result result; - if (srcPageTable.Supports4KBPages) + if (srcPageTable.UsesPrivateAllocations) + { + result = MapForeign(srcPageTable.GetHostRegions(addressRounded, alignedSize), currentVa, alignedSize); + } + else { KPageList pageList = new(); srcPageTable.GetPhysicalRegions(addressRounded, alignedSize, pageList); result = MapPages(currentVa, pageList, permission, MemoryMapFlags.None); } - else - { - result = MapForeign(srcPageTable.GetHostRegions(addressRounded, alignedSize), currentVa, alignedSize); - } if (result != Result.Success) { @@ -1977,9 +1994,9 @@ namespace Ryujinx.HLE.HOS.Kernel.Memory if (send) { ulong copySize = endAddr - endAddrTruncated; - var data = srcPageTable.GetSpan(endAddrTruncated, (int)copySize); + var data = srcPageTable.GetReadOnlySequence(endAddrTruncated, (int)copySize); - Context.Memory.Write(GetDramAddressFromPa(dstLastPagePa), data); + ((IWritableBlock)Context.Memory).Write(GetDramAddressFromPa(dstLastPagePa), data); lastPageFillAddr += copySize; @@ -2943,6 +2960,18 @@ namespace Ryujinx.HLE.HOS.Kernel.Memory /// Page list where the ranges will be added protected abstract void GetPhysicalRegions(ulong va, ulong size, KPageList pageList); + /// + /// Gets a read-only sequence of data from CPU mapped memory. + /// + /// + /// Allows reading non-contiguous memory without first copying it to a newly allocated single contiguous block. + /// + /// Virtual address of the data + /// Size of the data + /// A read-only sequence of the data + /// Throw for unhandled invalid or unmapped memory accesses + protected abstract ReadOnlySequence GetReadOnlySequence(ulong va, int size); + /// /// Gets a read-only span of data from CPU mapped memory. /// @@ -2952,7 +2981,6 @@ namespace Ryujinx.HLE.HOS.Kernel.Memory /// /// Virtual address of the data /// Size of the data - /// True if read tracking is triggered on the span /// A read-only span of the data /// Throw for unhandled invalid or unmapped memory accesses protected abstract ReadOnlySpan GetSpan(ulong va, int size); @@ -3060,6 +3088,14 @@ namespace Ryujinx.HLE.HOS.Kernel.Memory /// Size of the region protected abstract void SignalMemoryTracking(ulong va, ulong size, bool write); + /// + /// Writes data to CPU mapped memory, with write tracking. + /// + /// Virtual address to write the data into + /// Data to be written + /// Throw for unhandled invalid or unmapped memory accesses + protected abstract void Write(ulong va, ReadOnlySequence data); + /// /// Writes data to CPU mapped memory, with write tracking. /// diff --git a/src/Ryujinx.HLE/HOS/Kernel/Memory/KSharedMemory.cs b/src/Ryujinx.HLE/HOS/Kernel/Memory/KSharedMemory.cs index e302ee443..e593a7e13 100644 --- a/src/Ryujinx.HLE/HOS/Kernel/Memory/KSharedMemory.cs +++ b/src/Ryujinx.HLE/HOS/Kernel/Memory/KSharedMemory.cs @@ -2,7 +2,6 @@ using Ryujinx.Common; using Ryujinx.HLE.HOS.Kernel.Common; using Ryujinx.HLE.HOS.Kernel.Process; using Ryujinx.Horizon.Common; -using Ryujinx.Memory; namespace Ryujinx.HLE.HOS.Kernel.Memory { @@ -49,17 +48,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Memory return KernelResult.InvalidPermission; } - // On platforms with page size > 4 KB, this can fail due to the address not being page aligned, - // we can return an error to force the application to retry with a different address. - - try - { - return memoryManager.MapPages(address, _pageList, MemoryState.SharedMemory, permission); - } - catch (InvalidMemoryRegionException) - { - return KernelResult.InvalidMemState; - } + return memoryManager.MapPages(address, _pageList, MemoryState.SharedMemory, permission); } public Result UnmapFromProcess(KPageTableBase memoryManager, ulong address, ulong size, KProcess process) diff --git a/src/Ryujinx.HLE/HOS/Kernel/Process/HleProcessDebugger.cs b/src/Ryujinx.HLE/HOS/Kernel/Process/HleProcessDebugger.cs index 7578f1d2f..ff3de4a17 100644 --- a/src/Ryujinx.HLE/HOS/Kernel/Process/HleProcessDebugger.cs +++ b/src/Ryujinx.HLE/HOS/Kernel/Process/HleProcessDebugger.cs @@ -238,7 +238,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Process } else { - info.SubName = ""; + info.SubName = string.Empty; } info.ImageName = GetGuessedNsoNameFromIndex(imageIndex); diff --git a/src/Ryujinx.HLE/HOS/Kernel/Process/KProcess.cs b/src/Ryujinx.HLE/HOS/Kernel/Process/KProcess.cs index 6008548be..422f03c64 100644 --- a/src/Ryujinx.HLE/HOS/Kernel/Process/KProcess.cs +++ b/src/Ryujinx.HLE/HOS/Kernel/Process/KProcess.cs @@ -126,8 +126,6 @@ namespace Ryujinx.HLE.HOS.Kernel.Process _contextFactory = contextFactory ?? new ProcessContextFactory(); _customThreadStart = customThreadStart; - AddressSpaceType addrSpaceType = (AddressSpaceType)((int)(creationInfo.Flags & ProcessCreationFlags.AddressSpaceMask) >> (int)ProcessCreationFlags.AddressSpaceShift); - Pid = KernelContext.NewKipId(); if (Pid == 0 || Pid >= KernelConstants.InitialProcessId) @@ -137,8 +135,6 @@ namespace Ryujinx.HLE.HOS.Kernel.Process InitializeMemoryManager(creationInfo.Flags); - bool aslrEnabled = creationInfo.Flags.HasFlag(ProcessCreationFlags.EnableAslr); - ulong codeAddress = creationInfo.CodeAddress; ulong codeSize = (ulong)creationInfo.CodePagesCount * KPageTableBase.PageSize; @@ -148,9 +144,8 @@ namespace Ryujinx.HLE.HOS.Kernel.Process : KernelContext.SmallMemoryBlockSlabManager; Result result = MemoryManager.InitializeForProcess( - addrSpaceType, - aslrEnabled, - !aslrEnabled, + creationInfo.Flags, + !creationInfo.Flags.HasFlag(ProcessCreationFlags.EnableAslr), memRegion, codeAddress, codeSize, @@ -234,8 +229,6 @@ namespace Ryujinx.HLE.HOS.Kernel.Process : KernelContext.SmallMemoryBlockSlabManager; } - AddressSpaceType addrSpaceType = (AddressSpaceType)((int)(creationInfo.Flags & ProcessCreationFlags.AddressSpaceMask) >> (int)ProcessCreationFlags.AddressSpaceShift); - Pid = KernelContext.NewProcessId(); if (Pid == ulong.MaxValue || Pid < KernelConstants.InitialProcessId) @@ -245,16 +238,13 @@ namespace Ryujinx.HLE.HOS.Kernel.Process InitializeMemoryManager(creationInfo.Flags); - bool aslrEnabled = creationInfo.Flags.HasFlag(ProcessCreationFlags.EnableAslr); - ulong codeAddress = creationInfo.CodeAddress; ulong codeSize = codePagesCount * KPageTableBase.PageSize; Result result = MemoryManager.InitializeForProcess( - addrSpaceType, - aslrEnabled, - !aslrEnabled, + creationInfo.Flags, + !creationInfo.Flags.HasFlag(ProcessCreationFlags.EnableAslr), memRegion, codeAddress, codeSize, @@ -309,8 +299,8 @@ namespace Ryujinx.HLE.HOS.Kernel.Process private Result ParseProcessInfo(ProcessCreationInfo creationInfo) { // Ensure that the current kernel version is equal or above to the minimum required. - uint requiredKernelVersionMajor = (uint)Capabilities.KernelReleaseVersion >> 19; - uint requiredKernelVersionMinor = ((uint)Capabilities.KernelReleaseVersion >> 15) & 0xf; + uint requiredKernelVersionMajor = Capabilities.KernelReleaseVersion >> 19; + uint requiredKernelVersionMinor = (Capabilities.KernelReleaseVersion >> 15) & 0xf; if (KernelContext.EnableVersionChecks) { @@ -519,12 +509,10 @@ namespace Ryujinx.HLE.HOS.Kernel.Process return result; } -#pragma warning disable CA1822 // Mark member as static private void GenerateRandomEntropy() { // TODO. } -#pragma warning restore CA1822 public Result Start(int mainThreadPriority, ulong stackSize) { @@ -1182,5 +1170,10 @@ namespace Ryujinx.HLE.HOS.Kernel.Process // TODO return false; } + + public bool IsSvcPermitted(int svcId) + { + return Capabilities.IsSvcPermitted(svcId); + } } } diff --git a/src/Ryujinx.HLE/HOS/Kernel/Process/KProcessCapabilities.cs b/src/Ryujinx.HLE/HOS/Kernel/Process/KProcessCapabilities.cs index 314aadf36..ebab67bb8 100644 --- a/src/Ryujinx.HLE/HOS/Kernel/Process/KProcessCapabilities.cs +++ b/src/Ryujinx.HLE/HOS/Kernel/Process/KProcessCapabilities.cs @@ -8,6 +8,8 @@ namespace Ryujinx.HLE.HOS.Kernel.Process { class KProcessCapabilities { + private const int SvcMaskElementBits = 8; + public byte[] SvcAccessMask { get; } public byte[] IrqAccessMask { get; } @@ -22,7 +24,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Process public KProcessCapabilities() { // length / number of bits of the underlying type - SvcAccessMask = new byte[KernelConstants.SupervisorCallCount / 8]; + SvcAccessMask = new byte[KernelConstants.SupervisorCallCount / SvcMaskElementBits]; IrqAccessMask = new byte[0x80]; } @@ -208,7 +210,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Process return KernelResult.MaximumExceeded; } - SvcAccessMask[svcId / 8] |= (byte)(1 << (svcId & 7)); + SvcAccessMask[svcId / SvcMaskElementBits] |= (byte)(1 << (svcId % SvcMaskElementBits)); } break; @@ -324,5 +326,13 @@ namespace Ryujinx.HLE.HOS.Kernel.Process return mask << (int)min; } + + public bool IsSvcPermitted(int svcId) + { + int index = svcId / SvcMaskElementBits; + int mask = 1 << (svcId % SvcMaskElementBits); + + return (uint)svcId < KernelConstants.SupervisorCallCount && (SvcAccessMask[index] & mask) != 0; + } } } diff --git a/src/Ryujinx.HLE/HOS/Kernel/Process/ProcessCreationFlags.cs b/src/Ryujinx.HLE/HOS/Kernel/Process/ProcessCreationFlags.cs index f0e43e023..1b62a29d4 100644 --- a/src/Ryujinx.HLE/HOS/Kernel/Process/ProcessCreationFlags.cs +++ b/src/Ryujinx.HLE/HOS/Kernel/Process/ProcessCreationFlags.cs @@ -29,6 +29,8 @@ namespace Ryujinx.HLE.HOS.Kernel.Process PoolPartitionMask = 0xf << PoolPartitionShift, OptimizeMemoryAllocation = 1 << 11, + DisableDeviceAddressSpaceMerge = 1 << 12, + EnableAliasRegionExtraSize = 1 << 13, All = Is64Bit | @@ -38,6 +40,8 @@ namespace Ryujinx.HLE.HOS.Kernel.Process IsApplication | DeprecatedUseSecureMemory | PoolPartitionMask | - OptimizeMemoryAllocation, + OptimizeMemoryAllocation | + DisableDeviceAddressSpaceMerge | + EnableAliasRegionExtraSize, } } diff --git a/src/Ryujinx.HLE/HOS/Kernel/SupervisorCall/ExternalEvent.cs b/src/Ryujinx.HLE/HOS/Kernel/SupervisorCall/ExternalEvent.cs new file mode 100644 index 000000000..738d6b64a --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Kernel/SupervisorCall/ExternalEvent.cs @@ -0,0 +1,25 @@ +using Ryujinx.HLE.HOS.Kernel.Threading; +using Ryujinx.Horizon.Common; + +namespace Ryujinx.HLE.HOS.Kernel.SupervisorCall +{ + readonly struct ExternalEvent : IExternalEvent + { + private readonly KWritableEvent _writableEvent; + + public ExternalEvent(KWritableEvent writableEvent) + { + _writableEvent = writableEvent; + } + + public readonly void Signal() + { + _writableEvent.Signal(); + } + + public readonly void Clear() + { + _writableEvent.Clear(); + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Kernel/SupervisorCall/InfoType.cs b/src/Ryujinx.HLE/HOS/Kernel/SupervisorCall/InfoType.cs index c0db82105..cbaae8780 100644 --- a/src/Ryujinx.HLE/HOS/Kernel/SupervisorCall/InfoType.cs +++ b/src/Ryujinx.HLE/HOS/Kernel/SupervisorCall/InfoType.cs @@ -21,14 +21,17 @@ namespace Ryujinx.HLE.HOS.Kernel.SupervisorCall SystemResourceSizeTotal, SystemResourceSizeUsed, ProgramId, - // NOTE: Added in 4.0.0, removed in 5.0.0. - InitialProcessIdRange, + InitialProcessIdRange, // NOTE: Added in 4.0.0, removed in 5.0.0. UserExceptionContextAddress, TotalNonSystemMemorySize, UsedNonSystemMemorySize, IsApplication, FreeThreadCount, ThreadTickCount, + IsSvcPermitted, + IoRegionHint, + AliasRegionExtraSize, + MesosphereCurrentProcess = 65001, } } diff --git a/src/Ryujinx.HLE/HOS/Kernel/SupervisorCall/Syscall.cs b/src/Ryujinx.HLE/HOS/Kernel/SupervisorCall/Syscall.cs index b07f5194e..2f487243d 100644 --- a/src/Ryujinx.HLE/HOS/Kernel/SupervisorCall/Syscall.cs +++ b/src/Ryujinx.HLE/HOS/Kernel/SupervisorCall/Syscall.cs @@ -8,6 +8,7 @@ using Ryujinx.HLE.HOS.Kernel.Memory; using Ryujinx.HLE.HOS.Kernel.Process; using Ryujinx.HLE.HOS.Kernel.Threading; using Ryujinx.Horizon.Common; +using Ryujinx.Memory; using System; using System.Buffers; using System.Threading; @@ -83,6 +84,17 @@ namespace Ryujinx.HLE.HOS.Kernel.SupervisorCall return KernelResult.InvalidSize; } + if (info.Flags.HasFlag(ProcessCreationFlags.EnableAliasRegionExtraSize)) + { + if ((info.Flags & ProcessCreationFlags.AddressSpaceMask) != ProcessCreationFlags.AddressSpace64Bit || + info.SystemResourcePagesCount <= 0) + { + return KernelResult.InvalidState; + } + + // TODO: Check that we are in debug mode. + } + if (info.Flags.HasFlag(ProcessCreationFlags.OptimizeMemoryAllocation) && !info.Flags.HasFlag(ProcessCreationFlags.IsApplication)) { @@ -138,7 +150,6 @@ namespace Ryujinx.HLE.HOS.Kernel.SupervisorCall return handleTable.GenerateHandle(process, out handle); } -#pragma warning disable CA1822 // Mark member as static public Result StartProcess(int handle, int priority, int cpuCore, ulong mainThreadStackSize) { KProcess process = KernelStatic.GetCurrentProcess().HandleTable.GetObject(handle); @@ -171,17 +182,14 @@ namespace Ryujinx.HLE.HOS.Kernel.SupervisorCall return Result.Success; } -#pragma warning restore CA1822 [Svc(0x5f)] -#pragma warning disable CA1822 // Mark member as static public Result FlushProcessDataCache(int processHandle, ulong address, ulong size) { // FIXME: This needs to be implemented as ARMv7 doesn't have any way to do cache maintenance operations on EL0. // As we don't support (and don't actually need) to flush the cache, this is stubbed. return Result.Success; } -#pragma warning restore CA1822 // IPC @@ -255,7 +263,6 @@ namespace Ryujinx.HLE.HOS.Kernel.SupervisorCall } [Svc(0x22)] -#pragma warning disable CA1822 // Mark member as static public Result SendSyncRequestWithUserBuffer( [PointerSized] ulong messagePtr, [PointerSized] ulong messageSize, @@ -305,7 +312,6 @@ namespace Ryujinx.HLE.HOS.Kernel.SupervisorCall return result; } -#pragma warning restore CA1822 [Svc(0x23)] public Result SendAsyncRequestWithUserBuffer( @@ -615,7 +621,7 @@ namespace Ryujinx.HLE.HOS.Kernel.SupervisorCall } } - ArrayPool.Shared.Return(syncObjsArray); + ArrayPool.Shared.Return(syncObjsArray, true); return result; } @@ -895,7 +901,6 @@ namespace Ryujinx.HLE.HOS.Kernel.SupervisorCall } [Svc(2)] -#pragma warning disable CA1822 // Mark member as static public Result SetMemoryPermission([PointerSized] ulong address, [PointerSized] ulong size, KMemoryPermission permission) { if (!PageAligned(address)) @@ -927,10 +932,8 @@ namespace Ryujinx.HLE.HOS.Kernel.SupervisorCall return currentProcess.MemoryManager.SetMemoryPermission(address, size, permission); } -#pragma warning restore CA1822 [Svc(3)] -#pragma warning disable CA1822 // Mark member as static public Result SetMemoryAttribute( [PointerSized] ulong address, [PointerSized] ulong size, @@ -978,10 +981,8 @@ namespace Ryujinx.HLE.HOS.Kernel.SupervisorCall return result; } -#pragma warning restore CA1822 [Svc(4)] -#pragma warning disable CA1822 // Mark member as static public Result MapMemory([PointerSized] ulong dst, [PointerSized] ulong src, [PointerSized] ulong size) { if (!PageAligned(src | dst)) @@ -1017,10 +1018,8 @@ namespace Ryujinx.HLE.HOS.Kernel.SupervisorCall return process.MemoryManager.Map(dst, src, size); } -#pragma warning restore CA1822 [Svc(5)] -#pragma warning disable CA1822 // Mark member as static public Result UnmapMemory([PointerSized] ulong dst, [PointerSized] ulong src, [PointerSized] ulong size) { if (!PageAligned(src | dst)) @@ -1056,7 +1055,6 @@ namespace Ryujinx.HLE.HOS.Kernel.SupervisorCall return process.MemoryManager.Unmap(dst, src, size); } -#pragma warning restore CA1822 [Svc(6)] public Result QueryMemory([PointerSized] ulong infoPtr, [PointerSized] out ulong pageInfo, [PointerSized] ulong address) @@ -1073,7 +1071,6 @@ namespace Ryujinx.HLE.HOS.Kernel.SupervisorCall return result; } -#pragma warning disable CA1822 // Mark member as static public Result QueryMemory(out MemoryInfo info, out ulong pageInfo, ulong address) { KProcess process = KernelStatic.GetCurrentProcess(); @@ -1093,10 +1090,8 @@ namespace Ryujinx.HLE.HOS.Kernel.SupervisorCall return Result.Success; } -#pragma warning restore CA1822 [Svc(0x13)] -#pragma warning disable CA1822 // Mark member as static public Result MapSharedMemory(int handle, [PointerSized] ulong address, [PointerSized] ulong size, KMemoryPermission permission) { if (!PageAligned(address)) @@ -1142,10 +1137,8 @@ namespace Ryujinx.HLE.HOS.Kernel.SupervisorCall currentProcess, permission); } -#pragma warning restore CA1822 [Svc(0x14)] -#pragma warning disable CA1822 // Mark member as static public Result UnmapSharedMemory(int handle, [PointerSized] ulong address, [PointerSized] ulong size) { if (!PageAligned(address)) @@ -1185,7 +1178,6 @@ namespace Ryujinx.HLE.HOS.Kernel.SupervisorCall size, currentProcess); } -#pragma warning restore CA1822 [Svc(0x15)] public Result CreateTransferMemory(out int handle, [PointerSized] ulong address, [PointerSized] ulong size, KMemoryPermission permission) @@ -1252,7 +1244,6 @@ namespace Ryujinx.HLE.HOS.Kernel.SupervisorCall } [Svc(0x51)] -#pragma warning disable CA1822 // Mark member as static public Result MapTransferMemory(int handle, [PointerSized] ulong address, [PointerSized] ulong size, KMemoryPermission permission) { if (!PageAligned(address)) @@ -1298,10 +1289,8 @@ namespace Ryujinx.HLE.HOS.Kernel.SupervisorCall currentProcess, permission); } -#pragma warning restore CA1822 [Svc(0x52)] -#pragma warning disable CA1822 // Mark member as static public Result UnmapTransferMemory(int handle, [PointerSized] ulong address, [PointerSized] ulong size) { if (!PageAligned(address)) @@ -1341,10 +1330,8 @@ namespace Ryujinx.HLE.HOS.Kernel.SupervisorCall size, currentProcess); } -#pragma warning restore CA1822 [Svc(0x2c)] -#pragma warning disable CA1822 // Mark member as static public Result MapPhysicalMemory([PointerSized] ulong address, [PointerSized] ulong size) { if (!PageAligned(address)) @@ -1379,10 +1366,8 @@ namespace Ryujinx.HLE.HOS.Kernel.SupervisorCall return process.MemoryManager.MapPhysicalMemory(address, size); } -#pragma warning restore CA1822 [Svc(0x2d)] -#pragma warning disable CA1822 // Mark member as static public Result UnmapPhysicalMemory([PointerSized] ulong address, [PointerSized] ulong size) { if (!PageAligned(address)) @@ -1417,7 +1402,6 @@ namespace Ryujinx.HLE.HOS.Kernel.SupervisorCall return process.MemoryManager.UnmapPhysicalMemory(address, size); } -#pragma warning restore CA1822 [Svc(0x4b)] public Result CreateCodeMemory(out int handle, [PointerSized] ulong address, [PointerSized] ulong size) @@ -1461,7 +1445,6 @@ namespace Ryujinx.HLE.HOS.Kernel.SupervisorCall } [Svc(0x4c)] -#pragma warning disable CA1822 // Mark member as static public Result ControlCodeMemory( int handle, CodeMemoryOperation op, @@ -1539,14 +1522,12 @@ namespace Ryujinx.HLE.HOS.Kernel.SupervisorCall return KernelResult.InvalidEnumValue; } } -#pragma warning restore CA1822 [Svc(0x73)] -#pragma warning disable CA1822 // Mark member as static public Result SetProcessMemoryPermission( int handle, - [PointerSized] ulong src, - [PointerSized] ulong size, + ulong src, + ulong size, KMemoryPermission permission) { if (!PageAligned(src)) @@ -1583,10 +1564,8 @@ namespace Ryujinx.HLE.HOS.Kernel.SupervisorCall return targetProcess.MemoryManager.SetProcessMemoryPermission(src, size, permission); } -#pragma warning restore CA1822 [Svc(0x74)] -#pragma warning disable CA1822 // Mark member as static public Result MapProcessMemory( [PointerSized] ulong dst, int handle, @@ -1642,10 +1621,8 @@ namespace Ryujinx.HLE.HOS.Kernel.SupervisorCall return dstProcess.MemoryManager.MapPages(dst, pageList, MemoryState.ProcessMemory, KMemoryPermission.ReadAndWrite); } -#pragma warning restore CA1822 [Svc(0x75)] -#pragma warning disable CA1822 // Mark member as static public Result UnmapProcessMemory( [PointerSized] ulong dst, int handle, @@ -1690,10 +1667,8 @@ namespace Ryujinx.HLE.HOS.Kernel.SupervisorCall return Result.Success; } -#pragma warning restore CA1822 [Svc(0x77)] -#pragma warning disable CA1822 // Mark member as static public Result MapProcessCodeMemory(int handle, ulong dst, ulong src, ulong size) { if (!PageAligned(dst) || !PageAligned(src)) @@ -1730,10 +1705,8 @@ namespace Ryujinx.HLE.HOS.Kernel.SupervisorCall return targetProcess.MemoryManager.MapProcessCodeMemory(dst, src, size); } -#pragma warning restore CA1822 [Svc(0x78)] -#pragma warning disable CA1822 // Mark member as static public Result UnmapProcessCodeMemory(int handle, ulong dst, ulong src, ulong size) { if (!PageAligned(dst) || !PageAligned(src)) @@ -1770,7 +1743,6 @@ namespace Ryujinx.HLE.HOS.Kernel.SupervisorCall return targetProcess.MemoryManager.UnmapProcessCodeMemory(dst, src, size); } -#pragma warning restore CA1822 private static bool PageAligned(ulong address) { @@ -1780,7 +1752,6 @@ namespace Ryujinx.HLE.HOS.Kernel.SupervisorCall // System [Svc(0x7b)] -#pragma warning disable CA1822 // Mark member as static public Result TerminateProcess(int handle) { KProcess process = KernelStatic.GetCurrentProcess(); @@ -1809,15 +1780,12 @@ namespace Ryujinx.HLE.HOS.Kernel.SupervisorCall return result; } -#pragma warning restore CA1822 [Svc(7)] -#pragma warning disable CA1822 // Mark member as static public void ExitProcess() { KernelStatic.GetCurrentProcess().TerminateCurrentProcess(); } -#pragma warning restore CA1822 [Svc(0x11)] public Result SignalEvent(int handle) @@ -1910,7 +1878,6 @@ namespace Ryujinx.HLE.HOS.Kernel.SupervisorCall } [Svc(0x26)] -#pragma warning disable CA1822 // Mark member as static public void Break(ulong reason) { KThread currentThread = KernelStatic.GetCurrentThread(); @@ -1936,10 +1903,8 @@ namespace Ryujinx.HLE.HOS.Kernel.SupervisorCall Logger.Debug?.Print(LogClass.KernelSvc, "Debugger triggered."); } } -#pragma warning restore CA1822 [Svc(0x27)] -#pragma warning disable CA1822 // Mark member as static public void OutputDebugString([PointerSized] ulong strPtr, [PointerSized] ulong size) { KProcess process = KernelStatic.GetCurrentProcess(); @@ -1948,7 +1913,6 @@ namespace Ryujinx.HLE.HOS.Kernel.SupervisorCall Logger.Warning?.Print(LogClass.KernelSvc, str); } -#pragma warning restore CA1822 [Svc(0x29)] public Result GetInfo(out ulong value, InfoType id, int handle, long subId) @@ -1977,6 +1941,7 @@ namespace Ryujinx.HLE.HOS.Kernel.SupervisorCall case InfoType.UsedNonSystemMemorySize: case InfoType.IsApplication: case InfoType.FreeThreadCount: + case InfoType.AliasRegionExtraSize: { if (subId != 0) { @@ -2005,22 +1970,19 @@ namespace Ryujinx.HLE.HOS.Kernel.SupervisorCall value = process.MemoryManager.AliasRegionStart; break; case InfoType.AliasRegionSize: - value = (process.MemoryManager.AliasRegionEnd - - process.MemoryManager.AliasRegionStart); + value = process.MemoryManager.AliasRegionEnd - process.MemoryManager.AliasRegionStart; break; case InfoType.HeapRegionAddress: value = process.MemoryManager.HeapRegionStart; break; case InfoType.HeapRegionSize: - value = (process.MemoryManager.HeapRegionEnd - - process.MemoryManager.HeapRegionStart); + value = process.MemoryManager.HeapRegionEnd - process.MemoryManager.HeapRegionStart; break; case InfoType.TotalMemorySize: value = process.GetMemoryCapacity(); break; - case InfoType.UsedMemorySize: value = process.GetMemoryUsage(); break; @@ -2028,7 +1990,6 @@ namespace Ryujinx.HLE.HOS.Kernel.SupervisorCall case InfoType.AslrRegionAddress: value = process.MemoryManager.GetAddrSpaceBaseAddr(); break; - case InfoType.AslrRegionSize: value = process.MemoryManager.GetAddrSpaceSize(); break; @@ -2037,20 +1998,17 @@ namespace Ryujinx.HLE.HOS.Kernel.SupervisorCall value = process.MemoryManager.StackRegionStart; break; case InfoType.StackRegionSize: - value = (process.MemoryManager.StackRegionEnd - - process.MemoryManager.StackRegionStart); + value = process.MemoryManager.StackRegionEnd - process.MemoryManager.StackRegionStart; break; case InfoType.SystemResourceSizeTotal: value = process.PersonalMmHeapPagesCount * KPageTableBase.PageSize; break; - case InfoType.SystemResourceSizeUsed: if (process.PersonalMmHeapPagesCount != 0) { value = process.MemoryManager.GetMmUsedPages() * KPageTableBase.PageSize; } - break; case InfoType.ProgramId: @@ -2064,7 +2022,6 @@ namespace Ryujinx.HLE.HOS.Kernel.SupervisorCall case InfoType.TotalNonSystemMemorySize: value = process.GetMemoryCapacityWithoutPersonalMmHeap(); break; - case InfoType.UsedNonSystemMemorySize: value = process.GetMemoryUsageWithoutPersonalMmHeap(); break; @@ -2083,10 +2040,12 @@ namespace Ryujinx.HLE.HOS.Kernel.SupervisorCall { value = 0; } + break; + case InfoType.AliasRegionExtraSize: + value = process.MemoryManager.AliasRegionExtraSize; break; } - break; } @@ -2103,7 +2062,6 @@ namespace Ryujinx.HLE.HOS.Kernel.SupervisorCall } value = KernelStatic.GetCurrentProcess().Debug ? 1UL : 0UL; - break; } @@ -2135,7 +2093,6 @@ namespace Ryujinx.HLE.HOS.Kernel.SupervisorCall value = (uint)resLimHandle; } - break; } @@ -2154,7 +2111,6 @@ namespace Ryujinx.HLE.HOS.Kernel.SupervisorCall } value = (ulong)KTimeManager.ConvertHostTicksToTicks(_context.Schedulers[currentCore].TotalIdleTimeTicks); - break; } @@ -2173,7 +2129,6 @@ namespace Ryujinx.HLE.HOS.Kernel.SupervisorCall KProcess currentProcess = KernelStatic.GetCurrentProcess(); value = currentProcess.RandomEntropy[subId]; - break; } @@ -2219,7 +2174,22 @@ namespace Ryujinx.HLE.HOS.Kernel.SupervisorCall value = (ulong)KTimeManager.ConvertHostTicksToTicks(totalTimeRunning); } + break; + } + case InfoType.IsSvcPermitted: + { + if (handle != 0) + { + return KernelResult.InvalidHandle; + } + + if (subId != 0x36) + { + return KernelResult.InvalidCombination; + } + + value = KernelStatic.GetCurrentProcess().IsSvcPermitted((int)subId) ? 1UL : 0UL; break; } @@ -2230,7 +2200,7 @@ namespace Ryujinx.HLE.HOS.Kernel.SupervisorCall return KernelResult.InvalidHandle; } - if ((ulong)subId != 0) + if (subId != 0) { return KernelResult.InvalidCombination; } @@ -2245,8 +2215,7 @@ namespace Ryujinx.HLE.HOS.Kernel.SupervisorCall return result; } - value = (ulong)outHandle; - + value = (uint)outHandle; break; } @@ -2397,7 +2366,6 @@ namespace Ryujinx.HLE.HOS.Kernel.SupervisorCall } [Svc(0x30)] -#pragma warning disable CA1822 // Mark member as static public Result GetResourceLimitLimitValue(out long limitValue, int handle, LimitableResource resource) { limitValue = 0; @@ -2418,10 +2386,8 @@ namespace Ryujinx.HLE.HOS.Kernel.SupervisorCall return Result.Success; } -#pragma warning restore CA1822 [Svc(0x31)] -#pragma warning disable CA1822 // Mark member as static public Result GetResourceLimitCurrentValue(out long limitValue, int handle, LimitableResource resource) { limitValue = 0; @@ -2442,10 +2408,8 @@ namespace Ryujinx.HLE.HOS.Kernel.SupervisorCall return Result.Success; } -#pragma warning restore CA1822 [Svc(0x37)] -#pragma warning disable CA1822 // Mark member as static public Result GetResourceLimitPeakValue(out long peak, int handle, LimitableResource resource) { peak = 0; @@ -2466,7 +2430,6 @@ namespace Ryujinx.HLE.HOS.Kernel.SupervisorCall return Result.Success; } -#pragma warning restore CA1822 [Svc(0x7d)] public Result CreateResourceLimit(out int handle) @@ -2479,7 +2442,6 @@ namespace Ryujinx.HLE.HOS.Kernel.SupervisorCall } [Svc(0x7e)] -#pragma warning disable CA1822 // Mark member as static public Result SetResourceLimitLimitValue(int handle, LimitableResource resource, long limitValue) { if (resource >= LimitableResource.Count) @@ -2496,7 +2458,6 @@ namespace Ryujinx.HLE.HOS.Kernel.SupervisorCall return resourceLimit.SetLimitValue(resource, limitValue); } -#pragma warning restore CA1822 // Thread @@ -2576,7 +2537,6 @@ namespace Ryujinx.HLE.HOS.Kernel.SupervisorCall } [Svc(9)] -#pragma warning disable CA1822 // Mark member as static public Result StartThread(int handle) { KProcess process = KernelStatic.GetCurrentProcess(); @@ -2603,17 +2563,14 @@ namespace Ryujinx.HLE.HOS.Kernel.SupervisorCall return KernelResult.InvalidHandle; } } -#pragma warning restore CA1822 [Svc(0xa)] -#pragma warning disable CA1822 // Mark member as static public void ExitThread() { KThread currentThread = KernelStatic.GetCurrentThread(); currentThread.Exit(); } -#pragma warning restore CA1822 [Svc(0xb)] public void SleepThread(long timeout) @@ -2640,7 +2597,6 @@ namespace Ryujinx.HLE.HOS.Kernel.SupervisorCall } [Svc(0xc)] -#pragma warning disable CA1822 // Mark member as static public Result GetThreadPriority(out int priority, int handle) { KProcess process = KernelStatic.GetCurrentProcess(); @@ -2660,10 +2616,8 @@ namespace Ryujinx.HLE.HOS.Kernel.SupervisorCall return KernelResult.InvalidHandle; } } -#pragma warning restore CA1822 [Svc(0xd)] -#pragma warning disable CA1822 // Mark member as static public Result SetThreadPriority(int handle, int priority) { // TODO: NPDM check. @@ -2681,10 +2635,8 @@ namespace Ryujinx.HLE.HOS.Kernel.SupervisorCall return Result.Success; } -#pragma warning restore CA1822 [Svc(0xe)] -#pragma warning disable CA1822 // Mark member as static public Result GetThreadCoreMask(out int preferredCore, out ulong affinityMask, int handle) { KProcess process = KernelStatic.GetCurrentProcess(); @@ -2706,10 +2658,8 @@ namespace Ryujinx.HLE.HOS.Kernel.SupervisorCall return KernelResult.InvalidHandle; } } -#pragma warning restore CA1822 [Svc(0xf)] -#pragma warning disable CA1822 // Mark member as static public Result SetThreadCoreMask(int handle, int preferredCore, ulong affinityMask) { KProcess currentProcess = KernelStatic.GetCurrentProcess(); @@ -2757,18 +2707,14 @@ namespace Ryujinx.HLE.HOS.Kernel.SupervisorCall return thread.SetCoreAndAffinityMask(preferredCore, affinityMask); } -#pragma warning restore CA1822 [Svc(0x10)] -#pragma warning disable CA1822 // Mark member as static public int GetCurrentProcessorNumber() { return KernelStatic.GetCurrentThread().CurrentCore; } -#pragma warning restore CA1822 [Svc(0x25)] -#pragma warning disable CA1822 // Mark member as static public Result GetThreadId(out ulong threadUid, int handle) { KProcess process = KernelStatic.GetCurrentProcess(); @@ -2788,10 +2734,8 @@ namespace Ryujinx.HLE.HOS.Kernel.SupervisorCall return KernelResult.InvalidHandle; } } -#pragma warning restore CA1822 [Svc(0x32)] -#pragma warning disable CA1822 // Mark member as static public Result SetThreadActivity(int handle, bool pause) { KProcess process = KernelStatic.GetCurrentProcess(); @@ -2815,10 +2759,8 @@ namespace Ryujinx.HLE.HOS.Kernel.SupervisorCall return thread.SetActivity(pause); } -#pragma warning restore CA1822 [Svc(0x33)] -#pragma warning disable CA1822 // Mark member as static public Result GetThreadContext3([PointerSized] ulong address, int handle) { KProcess currentProcess = KernelStatic.GetCurrentProcess(); @@ -2852,7 +2794,6 @@ namespace Ryujinx.HLE.HOS.Kernel.SupervisorCall return result; } -#pragma warning restore CA1822 // Thread synchronization @@ -2985,7 +2926,6 @@ namespace Ryujinx.HLE.HOS.Kernel.SupervisorCall } [Svc(0x1a)] -#pragma warning disable CA1822 // Mark member as static public Result ArbitrateLock(int ownerHandle, [PointerSized] ulong mutexAddress, int requesterHandle) { if (IsPointingInsideKernel(mutexAddress)) @@ -3002,10 +2942,8 @@ namespace Ryujinx.HLE.HOS.Kernel.SupervisorCall return currentProcess.AddressArbiter.ArbitrateLock(ownerHandle, mutexAddress, requesterHandle); } -#pragma warning restore CA1822 [Svc(0x1b)] -#pragma warning disable CA1822 // Mark member as static public Result ArbitrateUnlock([PointerSized] ulong mutexAddress) { if (IsPointingInsideKernel(mutexAddress)) @@ -3022,10 +2960,8 @@ namespace Ryujinx.HLE.HOS.Kernel.SupervisorCall return currentProcess.AddressArbiter.ArbitrateUnlock(mutexAddress); } -#pragma warning restore CA1822 [Svc(0x1c)] -#pragma warning disable CA1822 // Mark member as static public Result WaitProcessWideKeyAtomic( [PointerSized] ulong mutexAddress, [PointerSized] ulong condVarAddress, @@ -3055,10 +2991,8 @@ namespace Ryujinx.HLE.HOS.Kernel.SupervisorCall handle, timeout); } -#pragma warning restore CA1822 [Svc(0x1d)] -#pragma warning disable CA1822 // Mark member as static public Result SignalProcessWideKey([PointerSized] ulong address, int count) { KProcess currentProcess = KernelStatic.GetCurrentProcess(); @@ -3067,10 +3001,8 @@ namespace Ryujinx.HLE.HOS.Kernel.SupervisorCall return Result.Success; } -#pragma warning restore CA1822 [Svc(0x34)] -#pragma warning disable CA1822 // Mark member as static public Result WaitForAddress([PointerSized] ulong address, ArbitrationType type, int value, long timeout) { if (IsPointingInsideKernel(address)) @@ -3101,10 +3033,8 @@ namespace Ryujinx.HLE.HOS.Kernel.SupervisorCall _ => KernelResult.InvalidEnumValue, }; } -#pragma warning restore CA1822 [Svc(0x35)] -#pragma warning disable CA1822 // Mark member as static public Result SignalToAddress([PointerSized] ulong address, SignalType type, int value, int count) { if (IsPointingInsideKernel(address)) @@ -3130,17 +3060,45 @@ namespace Ryujinx.HLE.HOS.Kernel.SupervisorCall _ => KernelResult.InvalidEnumValue, }; } -#pragma warning restore CA1822 [Svc(0x36)] -#pragma warning disable CA1822 // Mark member as static public Result SynchronizePreemptionState() { KernelStatic.GetCurrentThread().SynchronizePreemptionState(); return Result.Success; } -#pragma warning restore CA1822 + + // Not actual syscalls, used by HLE services and such. + + public IExternalEvent GetExternalEvent(int handle) + { + KWritableEvent writableEvent = KernelStatic.GetCurrentProcess().HandleTable.GetObject(handle); + + if (writableEvent == null) + { + return null; + } + + return new ExternalEvent(writableEvent); + } + + public IVirtualMemoryManager GetMemoryManagerByProcessHandle(int handle) + { + return KernelStatic.GetCurrentProcess().HandleTable.GetKProcess(handle).CpuMemory; + } + + public ulong GetTransferMemoryAddress(int handle) + { + KTransferMemory transferMemory = KernelStatic.GetCurrentProcess().HandleTable.GetObject(handle); + + if (transferMemory == null) + { + return 0; + } + + return transferMemory.Address; + } private static bool IsPointingInsideKernel(ulong address) { diff --git a/src/Ryujinx.HLE/HOS/Kernel/Threading/KScheduler.cs b/src/Ryujinx.HLE/HOS/Kernel/Threading/KScheduler.cs index 905c61d66..8ef77902c 100644 --- a/src/Ryujinx.HLE/HOS/Kernel/Threading/KScheduler.cs +++ b/src/Ryujinx.HLE/HOS/Kernel/Threading/KScheduler.cs @@ -28,42 +28,25 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading private SchedulingState _state; - private AutoResetEvent _idleInterruptEvent; - private readonly object _idleInterruptEventLock; - private KThread _previousThread; private KThread _currentThread; - private readonly KThread _idleThread; + + private int _coreIdleLock; + private bool _idleSignalled = true; + private bool _idleActive = true; + private long _idleTimeRunning; public KThread PreviousThread => _previousThread; public KThread CurrentThread => _currentThread; public long LastContextSwitchTime { get; private set; } - public long TotalIdleTimeTicks => _idleThread.TotalTimeRunning; + public long TotalIdleTimeTicks => _idleTimeRunning; public KScheduler(KernelContext context, int coreId) { _context = context; _coreId = coreId; - _idleInterruptEvent = new AutoResetEvent(false); - _idleInterruptEventLock = new object(); - - KThread idleThread = CreateIdleThread(context, coreId); - - _currentThread = idleThread; - _idleThread = idleThread; - - idleThread.StartHostThread(); - idleThread.SchedulerWaitEvent.Set(); - } - - private KThread CreateIdleThread(KernelContext context, int cpuCore) - { - KThread idleThread = new(context); - - idleThread.Initialize(0UL, 0UL, 0UL, PrioritiesCount, cpuCore, null, ThreadType.Dummy, IdleThreadLoop); - - return idleThread; + _currentThread = null; } public static ulong SelectThreads(KernelContext context) @@ -237,39 +220,64 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading KThread threadToSignal = context.Schedulers[coreToSignal]._currentThread; // Request the thread running on that core to stop and reschedule, if we have one. - if (threadToSignal != context.Schedulers[coreToSignal]._idleThread) - { - threadToSignal.Context.RequestInterrupt(); - } + threadToSignal?.Context.RequestInterrupt(); // If the core is idle, ensure that the idle thread is awaken. - context.Schedulers[coreToSignal]._idleInterruptEvent.Set(); + context.Schedulers[coreToSignal].NotifyIdleThread(); scheduledCoresMask &= ~(1UL << coreToSignal); } } - private void IdleThreadLoop() + private void ActivateIdleThread() { - while (_context.Running) + while (Interlocked.CompareExchange(ref _coreIdleLock, 1, 0) != 0) + { + Thread.SpinWait(1); + } + + Thread.MemoryBarrier(); + + // Signals that idle thread is now active on this core. + _idleActive = true; + + TryLeaveIdle(); + + Interlocked.Exchange(ref _coreIdleLock, 0); + } + + private void NotifyIdleThread() + { + while (Interlocked.CompareExchange(ref _coreIdleLock, 1, 0) != 0) + { + Thread.SpinWait(1); + } + + Thread.MemoryBarrier(); + + // Signals that the idle core may be able to exit idle. + _idleSignalled = true; + + TryLeaveIdle(); + + Interlocked.Exchange(ref _coreIdleLock, 0); + } + + public void TryLeaveIdle() + { + if (_idleSignalled && _idleActive) { _state.NeedsScheduling = false; Thread.MemoryBarrier(); - KThread nextThread = PickNextThread(_state.SelectedThread); + KThread nextThread = PickNextThread(null, _state.SelectedThread); - if (_idleThread != nextThread) + if (nextThread != null) { - _idleThread.SchedulerWaitEvent.Reset(); - WaitHandle.SignalAndWait(nextThread.SchedulerWaitEvent, _idleThread.SchedulerWaitEvent); + _idleActive = false; + nextThread.SchedulerWaitEvent.Set(); } - _idleInterruptEvent.WaitOne(); - } - - lock (_idleInterruptEventLock) - { - _idleInterruptEvent.Dispose(); - _idleInterruptEvent = null; + _idleSignalled = false; } } @@ -292,20 +300,37 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading // Wake all the threads that might be waiting until this thread context is unlocked. for (int core = 0; core < CpuCoresCount; core++) { - _context.Schedulers[core]._idleInterruptEvent.Set(); + _context.Schedulers[core].NotifyIdleThread(); } - KThread nextThread = PickNextThread(selectedThread); + KThread nextThread = PickNextThread(KernelStatic.GetCurrentThread(), selectedThread); if (currentThread.Context.Running) { // Wait until this thread is scheduled again, and allow the next thread to run. - WaitHandle.SignalAndWait(nextThread.SchedulerWaitEvent, currentThread.SchedulerWaitEvent); + + if (nextThread == null) + { + ActivateIdleThread(); + currentThread.SchedulerWaitEvent.WaitOne(); + } + else + { + WaitHandle.SignalAndWait(nextThread.SchedulerWaitEvent, currentThread.SchedulerWaitEvent); + } } else { // Allow the next thread to run. - nextThread.SchedulerWaitEvent.Set(); + + if (nextThread == null) + { + ActivateIdleThread(); + } + else + { + nextThread.SchedulerWaitEvent.Set(); + } // We don't need to wait since the thread is exiting, however we need to // make sure this thread will never call the scheduler again, since it is @@ -319,7 +344,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading } } - private KThread PickNextThread(KThread selectedThread) + private KThread PickNextThread(KThread currentThread, KThread selectedThread) { while (true) { @@ -335,7 +360,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading // on the core, as the scheduled thread will handle the next switch. if (selectedThread.ThreadContext.Lock()) { - SwitchTo(selectedThread); + SwitchTo(currentThread, selectedThread); if (!_state.NeedsScheduling) { @@ -346,15 +371,15 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading } else { - return _idleThread; + return null; } } else { // The core is idle now, make sure that the idle thread can run // and switch the core when a thread is available. - SwitchTo(null); - return _idleThread; + SwitchTo(currentThread, null); + return null; } _state.NeedsScheduling = false; @@ -363,12 +388,9 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading } } - private void SwitchTo(KThread nextThread) + private void SwitchTo(KThread currentThread, KThread nextThread) { - KProcess currentProcess = KernelStatic.GetCurrentProcess(); - KThread currentThread = KernelStatic.GetCurrentThread(); - - nextThread ??= _idleThread; + KProcess currentProcess = currentThread?.Owner; if (currentThread != nextThread) { @@ -376,7 +398,14 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading long currentTicks = PerformanceCounter.ElapsedTicks; long ticksDelta = currentTicks - previousTicks; - currentThread.AddCpuTime(ticksDelta); + if (currentThread == null) + { + Interlocked.Add(ref _idleTimeRunning, ticksDelta); + } + else + { + currentThread.AddCpuTime(ticksDelta); + } currentProcess?.AddCpuTime(ticksDelta); @@ -386,13 +415,13 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading { _previousThread = !currentThread.TerminationRequested && currentThread.ActiveCore == _coreId ? currentThread : null; } - else if (currentThread == _idleThread) + else if (currentThread == null) { _previousThread = null; } } - if (nextThread.CurrentCore != _coreId) + if (nextThread != null && nextThread.CurrentCore != _coreId) { nextThread.CurrentCore = _coreId; } @@ -645,11 +674,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading public void Dispose() { - // Ensure that the idle thread is not blocked and can exit. - lock (_idleInterruptEventLock) - { - _idleInterruptEvent?.Set(); - } + // No resources to dispose for now. } } } diff --git a/src/Ryujinx.HLE/HOS/Kernel/Threading/KSynchronization.cs b/src/Ryujinx.HLE/HOS/Kernel/Threading/KSynchronization.cs index b1af06b0d..21c2730bf 100644 --- a/src/Ryujinx.HLE/HOS/Kernel/Threading/KSynchronization.cs +++ b/src/Ryujinx.HLE/HOS/Kernel/Threading/KSynchronization.cs @@ -104,7 +104,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading } } - ArrayPool>.Shared.Return(syncNodesArray); + ArrayPool>.Shared.Return(syncNodesArray, true); } _context.CriticalSection.Leave(); diff --git a/src/Ryujinx.HLE/HOS/Kernel/Threading/KThread.cs b/src/Ryujinx.HLE/HOS/Kernel/Threading/KThread.cs index 12383fb8a..835bf5d40 100644 --- a/src/Ryujinx.HLE/HOS/Kernel/Threading/KThread.cs +++ b/src/Ryujinx.HLE/HOS/Kernel/Threading/KThread.cs @@ -143,9 +143,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading PreferredCore = cpuCore; AffinityMask |= 1UL << cpuCore; - SchedFlags = type == ThreadType.Dummy - ? ThreadSchedState.Running - : ThreadSchedState.None; + SchedFlags = ThreadSchedState.None; ActiveCore = cpuCore; ObjSyncResult = KernelResult.ThreadNotStarted; @@ -1055,6 +1053,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading // If the thread is not schedulable, we want to just run or pause // it directly as we don't care about priority or the core it is // running on in this case. + if (SchedFlags == ThreadSchedState.Running) { _schedulerWaitEvent.Set(); diff --git a/src/Ryujinx.HLE/HOS/Kernel/Threading/ThreadType.cs b/src/Ryujinx.HLE/HOS/Kernel/Threading/ThreadType.cs index 83093570b..e2dfd2ffb 100644 --- a/src/Ryujinx.HLE/HOS/Kernel/Threading/ThreadType.cs +++ b/src/Ryujinx.HLE/HOS/Kernel/Threading/ThreadType.cs @@ -2,7 +2,6 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading { enum ThreadType { - Dummy, Kernel, Kernel2, User, diff --git a/src/Ryujinx.HLE/HOS/ModLoader.cs b/src/Ryujinx.HLE/HOS/ModLoader.cs index 834bc0595..7cbe1afca 100644 --- a/src/Ryujinx.HLE/HOS/ModLoader.cs +++ b/src/Ryujinx.HLE/HOS/ModLoader.cs @@ -7,6 +7,7 @@ using LibHac.Tools.FsSystem; using LibHac.Tools.FsSystem.RomFs; using Ryujinx.Common.Configuration; using Ryujinx.Common.Logging; +using Ryujinx.Common.Utilities; using Ryujinx.HLE.HOS.Kernel.Process; using Ryujinx.HLE.Loaders.Executables; using Ryujinx.HLE.Loaders.Mods; @@ -17,6 +18,7 @@ using System.Collections.Specialized; using System.Globalization; using System.IO; using System.Linq; +using LazyFile = Ryujinx.HLE.HOS.Services.Fs.FileSystemProxy.LazyFile; using Path = System.IO.Path; namespace Ryujinx.HLE.HOS @@ -37,15 +39,19 @@ namespace Ryujinx.HLE.HOS private const string AmsNroPatchDir = "nro_patches"; private const string AmsKipPatchDir = "kip_patches"; + private static readonly ModMetadataJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions()); + public readonly struct Mod where T : FileSystemInfo { public readonly string Name; public readonly T Path; + public readonly bool Enabled; - public Mod(string name, T path) + public Mod(string name, T path, bool enabled) { Name = name; Path = path; + Enabled = enabled; } } @@ -67,7 +73,7 @@ namespace Ryujinx.HLE.HOS } } - // Title dependent mods + // Application dependent mods public class ModCache { public List> RomfsContainers { get; } @@ -88,7 +94,7 @@ namespace Ryujinx.HLE.HOS } } - // Title independent mods + // Application independent mods private class PatchCache { public List> NsoPatches { get; } @@ -107,21 +113,16 @@ namespace Ryujinx.HLE.HOS } } - private readonly Dictionary _appMods; // key is TitleId + private readonly Dictionary _appMods; // key is ApplicationId private PatchCache _patches; - private static readonly EnumerationOptions _dirEnumOptions; - - static ModLoader() + private static readonly EnumerationOptions _dirEnumOptions = new() { - _dirEnumOptions = new EnumerationOptions - { - MatchCasing = MatchCasing.CaseInsensitive, - MatchType = MatchType.Simple, - RecurseSubdirectories = false, - ReturnSpecialDirectories = false, - }; - } + MatchCasing = MatchCasing.CaseInsensitive, + MatchType = MatchType.Simple, + RecurseSubdirectories = false, + ReturnSpecialDirectories = false, + }; public ModLoader() { @@ -153,26 +154,32 @@ namespace Ryujinx.HLE.HOS return modsDir.FullName; } - private static DirectoryInfo FindTitleDir(DirectoryInfo contentsDir, string titleId) - => contentsDir.EnumerateDirectories(titleId, _dirEnumOptions).FirstOrDefault(); + private static DirectoryInfo FindApplicationDir(DirectoryInfo contentsDir, string applicationId) + => contentsDir.EnumerateDirectories(applicationId, _dirEnumOptions).FirstOrDefault(); - private static void AddModsFromDirectory(ModCache mods, DirectoryInfo dir, string titleId) + private static void AddModsFromDirectory(ModCache mods, DirectoryInfo dir, ModMetadata modMetadata) { System.Text.StringBuilder types = new(); foreach (var modDir in dir.EnumerateDirectories()) { types.Clear(); - Mod mod = new("", null); + Mod mod = new(string.Empty, null, true); if (StrEquals(RomfsDir, modDir.Name)) { - mods.RomfsDirs.Add(mod = new Mod(dir.Name, modDir)); + var modData = modMetadata.Mods.Find(x => modDir.FullName.Contains(x.Path)); + var enabled = modData?.Enabled ?? true; + + mods.RomfsDirs.Add(mod = new Mod(dir.Name, modDir, enabled)); types.Append('R'); } else if (StrEquals(ExefsDir, modDir.Name)) { - mods.ExefsDirs.Add(mod = new Mod(dir.Name, modDir)); + var modData = modMetadata.Mods.Find(x => modDir.FullName.Contains(x.Path)); + var enabled = modData?.Enabled ?? true; + + mods.ExefsDirs.Add(mod = new Mod(dir.Name, modDir, enabled)); types.Append('E'); } else if (StrEquals(CheatDir, modDir.Name)) @@ -181,28 +188,28 @@ namespace Ryujinx.HLE.HOS } else { - AddModsFromDirectory(mods, modDir, titleId); + AddModsFromDirectory(mods, modDir, modMetadata); } if (types.Length > 0) { - Logger.Info?.Print(LogClass.ModLoader, $"Found mod '{mod.Name}' [{types}]"); + Logger.Info?.Print(LogClass.ModLoader, $"Found {(mod.Enabled ? "enabled" : "disabled")} mod '{mod.Name}' [{types}]"); } } } - public static string GetTitleDir(string modsBasePath, string titleId) + public static string GetApplicationDir(string modsBasePath, string applicationId) { var contentsDir = new DirectoryInfo(Path.Combine(modsBasePath, AmsContentsDir)); - var titleModsPath = FindTitleDir(contentsDir, titleId); + var applicationModsPath = FindApplicationDir(contentsDir, applicationId); - if (titleModsPath == null) + if (applicationModsPath == null) { - Logger.Info?.Print(LogClass.ModLoader, $"Creating mods directory for Title {titleId.ToUpper()}"); - titleModsPath = contentsDir.CreateSubdirectory(titleId); + Logger.Info?.Print(LogClass.ModLoader, $"Creating mods directory for Application {applicationId.ToUpper()}"); + applicationModsPath = contentsDir.CreateSubdirectory(applicationId); } - return titleModsPath.FullName; + return applicationModsPath.FullName; } // Static Query Methods @@ -238,47 +245,68 @@ namespace Ryujinx.HLE.HOS foreach (var modDir in patchDir.EnumerateDirectories()) { - patches.Add(new Mod(modDir.Name, modDir)); + patches.Add(new Mod(modDir.Name, modDir, true)); Logger.Info?.Print(LogClass.ModLoader, $"Found {type} patch '{modDir.Name}'"); } } - private static void QueryTitleDir(ModCache mods, DirectoryInfo titleDir) + private static void QueryApplicationDir(ModCache mods, DirectoryInfo applicationDir, ulong applicationId) { - if (!titleDir.Exists) + if (!applicationDir.Exists) { return; } - var fsFile = new FileInfo(Path.Combine(titleDir.FullName, RomfsContainer)); - if (fsFile.Exists) + string modJsonPath = Path.Combine(AppDataManager.GamesDirPath, applicationId.ToString("x16"), "mods.json"); + ModMetadata modMetadata = new(); + + if (File.Exists(modJsonPath)) { - mods.RomfsContainers.Add(new Mod($"<{titleDir.Name} RomFs>", fsFile)); + try + { + modMetadata = JsonHelper.DeserializeFromFile(modJsonPath, _serializerContext.ModMetadata); + } + catch + { + Logger.Warning?.Print(LogClass.ModLoader, $"Failed to deserialize mod data for {applicationId:X16} at {modJsonPath}"); + } } - fsFile = new FileInfo(Path.Combine(titleDir.FullName, ExefsContainer)); + var fsFile = new FileInfo(Path.Combine(applicationDir.FullName, RomfsContainer)); if (fsFile.Exists) { - mods.ExefsContainers.Add(new Mod($"<{titleDir.Name} ExeFs>", fsFile)); + var modData = modMetadata.Mods.Find(x => fsFile.FullName.Contains(x.Path)); + var enabled = modData == null || modData.Enabled; + + mods.RomfsContainers.Add(new Mod($"<{applicationDir.Name} RomFs>", fsFile, enabled)); } - AddModsFromDirectory(mods, titleDir, titleDir.Name); + fsFile = new FileInfo(Path.Combine(applicationDir.FullName, ExefsContainer)); + if (fsFile.Exists) + { + var modData = modMetadata.Mods.Find(x => fsFile.FullName.Contains(x.Path)); + var enabled = modData == null || modData.Enabled; + + mods.ExefsContainers.Add(new Mod($"<{applicationDir.Name} ExeFs>", fsFile, enabled)); + } + + AddModsFromDirectory(mods, applicationDir, modMetadata); } - public static void QueryContentsDir(ModCache mods, DirectoryInfo contentsDir, ulong titleId) + public static void QueryContentsDir(ModCache mods, DirectoryInfo contentsDir, ulong applicationId) { if (!contentsDir.Exists) { return; } - Logger.Info?.Print(LogClass.ModLoader, $"Searching mods for {((titleId & 0x1000) != 0 ? "DLC" : "Title")} {titleId:X16}"); + Logger.Info?.Print(LogClass.ModLoader, $"Searching mods for {((applicationId & 0x1000) != 0 ? "DLC" : "Application")} {applicationId:X16} in \"{contentsDir.FullName}\""); - var titleDir = FindTitleDir(contentsDir, $"{titleId:x16}"); + var applicationDir = FindApplicationDir(contentsDir, $"{applicationId:x16}"); - if (titleDir != null) + if (applicationDir != null) { - QueryTitleDir(mods, titleDir); + QueryApplicationDir(mods, applicationDir, applicationId); } } @@ -387,9 +415,9 @@ namespace Ryujinx.HLE.HOS { if (IsContentsDir(searchDir.Name)) { - foreach ((ulong titleId, ModCache cache) in modCaches) + foreach ((ulong applicationId, ModCache cache) in modCaches) { - QueryContentsDir(cache, searchDir, titleId); + QueryContentsDir(cache, searchDir, applicationId); } return true; @@ -410,7 +438,7 @@ namespace Ryujinx.HLE.HOS if (!searchDir.Exists) { Logger.Warning?.Print(LogClass.ModLoader, $"Mod Search Dir '{searchDir.FullName}' doesn't exist"); - continue; + return; } if (!TryQuery(searchDir, patches, modCaches)) @@ -425,21 +453,21 @@ namespace Ryujinx.HLE.HOS patches.Initialized = true; } - public void CollectMods(IEnumerable titles, params string[] searchDirPaths) + public void CollectMods(IEnumerable applications, params string[] searchDirPaths) { Clear(); - foreach (ulong titleId in titles) + foreach (ulong applicationId in applications) { - _appMods[titleId] = new ModCache(); + _appMods[applicationId] = new ModCache(); } CollectMods(_appMods, _patches, searchDirPaths); } - internal IStorage ApplyRomFsMods(ulong titleId, IStorage baseStorage) + internal IStorage ApplyRomFsMods(ulong applicationId, IStorage baseStorage) { - if (!_appMods.TryGetValue(titleId, out ModCache mods) || mods.RomfsDirs.Count + mods.RomfsContainers.Count == 0) + if (!_appMods.TryGetValue(applicationId, out ModCache mods) || mods.RomfsDirs.Count + mods.RomfsContainers.Count == 0) { return baseStorage; } @@ -448,14 +476,19 @@ namespace Ryujinx.HLE.HOS var builder = new RomFsBuilder(); int count = 0; - Logger.Info?.Print(LogClass.ModLoader, $"Applying RomFS mods for Title {titleId:X16}"); + Logger.Info?.Print(LogClass.ModLoader, $"Applying RomFS mods for Application {applicationId:X16}"); // Prioritize loose files first foreach (var mod in mods.RomfsDirs) { + if (!mod.Enabled) + { + continue; + } + using (IFileSystem fs = new LocalFileSystem(mod.Path.FullName)) { - AddFiles(fs, mod.Name, fileSet, builder); + AddFiles(fs, mod.Name, mod.Path.FullName, fileSet, builder); } count++; } @@ -463,10 +496,15 @@ namespace Ryujinx.HLE.HOS // Then files inside images foreach (var mod in mods.RomfsContainers) { - Logger.Info?.Print(LogClass.ModLoader, $"Found 'romfs.bin' for Title {titleId:X16}"); + if (!mod.Enabled) + { + continue; + } + + Logger.Info?.Print(LogClass.ModLoader, $"Found 'romfs.bin' for Application {applicationId:X16}"); using (IFileSystem fs = new RomFsFileSystem(mod.Path.OpenRead().AsStorage())) { - AddFiles(fs, mod.Name, fileSet, builder); + AddFiles(fs, mod.Name, mod.Path.FullName, fileSet, builder); } count++; } @@ -499,18 +537,18 @@ namespace Ryujinx.HLE.HOS return newStorage; } - private static void AddFiles(IFileSystem fs, string modName, ISet fileSet, RomFsBuilder builder) + private static void AddFiles(IFileSystem fs, string modName, string rootPath, ISet fileSet, RomFsBuilder builder) { foreach (var entry in fs.EnumerateEntries() + .AsParallel() .Where(f => f.Type == DirectoryEntryType.File) .OrderBy(f => f.FullPath, StringComparer.Ordinal)) { - using var file = new UniqueRef(); + var file = new LazyFile(entry.FullPath, rootPath, fs); - fs.OpenFile(ref file.Ref, entry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); if (fileSet.Add(entry.FullPath)) { - builder.AddFile(entry.FullPath, file.Release()); + builder.AddFile(entry.FullPath, file); } else { @@ -519,9 +557,9 @@ namespace Ryujinx.HLE.HOS } } - internal bool ReplaceExefsPartition(ulong titleId, ref IFileSystem exefs) + internal bool ReplaceExefsPartition(ulong applicationId, ref IFileSystem exefs) { - if (!_appMods.TryGetValue(titleId, out ModCache mods) || mods.ExefsContainers.Count == 0) + if (!_appMods.TryGetValue(applicationId, out ModCache mods) || mods.ExefsContainers.Count == 0) { return false; } @@ -549,7 +587,7 @@ namespace Ryujinx.HLE.HOS public bool Modified => (Stubs.Data | Replaces.Data) != 0; } - internal ModLoadResult ApplyExefsMods(ulong titleId, NsoExecutable[] nsos) + internal ModLoadResult ApplyExefsMods(ulong applicationId, NsoExecutable[] nsos) { ModLoadResult modLoadResult = new() { @@ -557,7 +595,7 @@ namespace Ryujinx.HLE.HOS Replaces = new BitVector32(), }; - if (!_appMods.TryGetValue(titleId, out ModCache mods) || mods.ExefsDirs.Count == 0) + if (!_appMods.TryGetValue(applicationId, out ModCache mods) || mods.ExefsDirs.Count == 0) { return modLoadResult; } @@ -571,6 +609,11 @@ namespace Ryujinx.HLE.HOS foreach (var mod in exeMods) { + if (!mod.Enabled) + { + continue; + } + for (int i = 0; i < ProcessConst.ExeFsPrefixes.Length; ++i) { var nsoName = ProcessConst.ExeFsPrefixes[i]; @@ -637,11 +680,11 @@ namespace Ryujinx.HLE.HOS ApplyProgramPatches(nroPatches, 0, nro); } - internal bool ApplyNsoPatches(ulong titleId, params IExecutable[] programs) + internal bool ApplyNsoPatches(ulong applicationId, params IExecutable[] programs) { IEnumerable> nsoMods = _patches.NsoPatches; - if (_appMods.TryGetValue(titleId, out ModCache mods)) + if (_appMods.TryGetValue(applicationId, out ModCache mods)) { nsoMods = nsoMods.Concat(mods.ExefsDirs); } @@ -651,7 +694,7 @@ namespace Ryujinx.HLE.HOS return ApplyProgramPatches(nsoMods, 0x100, programs); } - internal void LoadCheats(ulong titleId, ProcessTamperInfo tamperInfo, TamperMachine tamperMachine) + internal void LoadCheats(ulong applicationId, ProcessTamperInfo tamperInfo, TamperMachine tamperMachine) { if (tamperInfo?.BuildIds == null || tamperInfo.CodeAddresses == null) { @@ -660,9 +703,9 @@ namespace Ryujinx.HLE.HOS return; } - Logger.Info?.Print(LogClass.ModLoader, $"Build ids found for title {titleId:X16}:\n {String.Join("\n ", tamperInfo.BuildIds)}"); + Logger.Info?.Print(LogClass.ModLoader, $"Build ids found for application {applicationId:X16}:\n {String.Join("\n ", tamperInfo.BuildIds)}"); - if (!_appMods.TryGetValue(titleId, out ModCache mods) || mods.Cheats.Count == 0) + if (!_appMods.TryGetValue(applicationId, out ModCache mods) || mods.Cheats.Count == 0) { return; } @@ -687,12 +730,12 @@ namespace Ryujinx.HLE.HOS tamperMachine.InstallAtmosphereCheat(cheat.Name, cheatId, cheat.Instructions, tamperInfo, exeAddress); } - EnableCheats(titleId, tamperMachine); + EnableCheats(applicationId, tamperMachine); } - internal static void EnableCheats(ulong titleId, TamperMachine tamperMachine) + internal static void EnableCheats(ulong applicationId, TamperMachine tamperMachine) { - var contentDirectory = FindTitleDir(new DirectoryInfo(Path.Combine(GetModsBasePath(), AmsContentsDir)), $"{titleId:x16}"); + var contentDirectory = FindApplicationDir(new DirectoryInfo(Path.Combine(GetModsBasePath(), AmsContentsDir)), $"{applicationId:x16}"); string enabledCheatsPath = Path.Combine(contentDirectory.FullName, CheatDir, "enabled.txt"); if (File.Exists(enabledCheatsPath)) @@ -724,6 +767,11 @@ namespace Ryujinx.HLE.HOS // Collect patches foreach (var mod in mods) { + if (!mod.Enabled) + { + continue; + } + var patchDir = mod.Path; foreach (var patchFile in patchDir.EnumerateFiles()) { diff --git a/src/Ryujinx.HLE/HOS/Services/Account/Acc/AccountManager.cs b/src/Ryujinx.HLE/HOS/Services/Account/Acc/AccountManager.cs index 924ac3fb9..d2da9e248 100644 --- a/src/Ryujinx.HLE/HOS/Services/Account/Acc/AccountManager.cs +++ b/src/Ryujinx.HLE/HOS/Services/Account/Acc/AccountManager.cs @@ -4,6 +4,7 @@ using LibHac.Fs; using LibHac.Fs.Shim; using Ryujinx.Common; using Ryujinx.Common.Logging; +using Ryujinx.Horizon.Sdk.Account; using System; using System.Collections.Concurrent; using System.Collections.Generic; @@ -11,7 +12,7 @@ using System.Linq; namespace Ryujinx.HLE.HOS.Services.Account.Acc { - public class AccountManager + public class AccountManager : IEmulatorAccountManager { public static readonly UserId DefaultUserId = new("00000000000000010000000000000000"); @@ -63,7 +64,7 @@ namespace Ryujinx.HLE.HOS.Services.Account.Acc { if (userId.IsNull) { - userId = new UserId(Guid.NewGuid().ToString().Replace("-", "")); + userId = new UserId(Guid.NewGuid().ToString().Replace("-", string.Empty)); } UserProfile profile = new(userId, name, image); @@ -106,6 +107,11 @@ namespace Ryujinx.HLE.HOS.Services.Account.Acc _accountSaveDataManager.Save(_profiles); } + public void OpenUserOnlinePlay(Uid userId) + { + OpenUserOnlinePlay(new UserId((long)userId.Low, (long)userId.High)); + } + public void OpenUserOnlinePlay(UserId userId) { if (_profiles.TryGetValue(userId.ToString(), out UserProfile profile)) @@ -127,6 +133,11 @@ namespace Ryujinx.HLE.HOS.Services.Account.Acc _accountSaveDataManager.Save(_profiles); } + public void CloseUserOnlinePlay(Uid userId) + { + CloseUserOnlinePlay(new UserId((long)userId.Low, (long)userId.High)); + } + public void CloseUserOnlinePlay(UserId userId) { if (_profiles.TryGetValue(userId.ToString(), out UserProfile profile)) diff --git a/src/Ryujinx.HLE/HOS/Services/Account/Acc/AccountService/ManagerServer.cs b/src/Ryujinx.HLE/HOS/Services/Account/Acc/AccountService/ManagerServer.cs index 4c75d430f..75bad0e3f 100644 --- a/src/Ryujinx.HLE/HOS/Services/Account/Acc/AccountService/ManagerServer.cs +++ b/src/Ryujinx.HLE/HOS/Services/Account/Acc/AccountService/ManagerServer.cs @@ -1,10 +1,12 @@ +using Microsoft.IdentityModel.JsonWebTokens; using Microsoft.IdentityModel.Tokens; using Ryujinx.Common.Logging; using Ryujinx.HLE.HOS.Kernel.Threading; using Ryujinx.HLE.HOS.Services.Account.Acc.AsyncContext; using System; -using System.IdentityModel.Tokens.Jwt; +using System.Collections.Generic; using System.Security.Cryptography; +using System.Security.Principal; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -20,6 +22,9 @@ namespace Ryujinx.HLE.HOS.Services.Account.Acc.AccountService private readonly UserId _userId; #pragma warning restore IDE0052 + private byte[] _cachedTokenData; + private DateTime _cachedTokenExpiry; + public ManagerServer(UserId userId) { _userId = userId; @@ -37,11 +42,6 @@ namespace Ryujinx.HLE.HOS.Services.Account.Acc.AccountService credentials.Key.KeyId = parameters.ToString(); - var header = new JwtHeader(credentials) - { - { "jku", "https://e0d67c509fb203858ebcb2fe3f88c2aa.baas.nintendo.com/1.0.0/certificates" }, - }; - byte[] rawUserId = new byte[0x10]; RandomNumberGenerator.Fill(rawUserId); @@ -51,23 +51,25 @@ namespace Ryujinx.HLE.HOS.Services.Account.Acc.AccountService byte[] deviceAccountId = new byte[0x10]; RandomNumberGenerator.Fill(deviceId); - var payload = new JwtPayload + var descriptor = new SecurityTokenDescriptor { - { "sub", Convert.ToHexString(rawUserId).ToLower() }, - { "aud", "ed9e2f05d286f7b8" }, - { "di", Convert.ToHexString(deviceId).ToLower() }, - { "sn", "XAW10000000000" }, - { "bs:did", Convert.ToHexString(deviceAccountId).ToLower() }, - { "iss", "https://e0d67c509fb203858ebcb2fe3f88c2aa.baas.nintendo.com" }, - { "typ", "id_token" }, - { "iat", DateTimeOffset.UtcNow.ToUnixTimeSeconds() }, - { "jti", Guid.NewGuid().ToString() }, - { "exp", (DateTimeOffset.UtcNow + TimeSpan.FromHours(3)).ToUnixTimeSeconds() }, + Subject = new GenericIdentity(Convert.ToHexString(rawUserId).ToLower()), + SigningCredentials = credentials, + Audience = "ed9e2f05d286f7b8", + Issuer = "https://e0d67c509fb203858ebcb2fe3f88c2aa.baas.nintendo.com", + TokenType = "id_token", + IssuedAt = DateTime.UtcNow, + Expires = DateTime.UtcNow + TimeSpan.FromHours(3), + Claims = new Dictionary + { + { "jku", "https://e0d67c509fb203858ebcb2fe3f88c2aa.baas.nintendo.com/1.0.0/certificates" }, + { "di", Convert.ToHexString(deviceId).ToLower() }, + { "sn", "XAW10000000000" }, + { "bs:did", Convert.ToHexString(deviceAccountId).ToLower() } + } }; - JwtSecurityToken securityToken = new(header, payload); - - return new JwtSecurityTokenHandler().WriteToken(securityToken); + return new JsonWebTokenHandler().CreateToken(descriptor); } public ResultCode CheckAvailability(ServiceCtx context) @@ -145,7 +147,13 @@ namespace Ryujinx.HLE.HOS.Services.Account.Acc.AccountService } */ - byte[] tokenData = Encoding.ASCII.GetBytes(GenerateIdToken()); + if (_cachedTokenData == null || DateTime.UtcNow > _cachedTokenExpiry) + { + _cachedTokenExpiry = DateTime.UtcNow + TimeSpan.FromHours(3); + _cachedTokenData = Encoding.ASCII.GetBytes(GenerateIdToken()); + } + + byte[] tokenData = _cachedTokenData; context.Memory.Write(bufferPosition, tokenData); context.ResponseData.Write(tokenData.Length); diff --git a/src/Ryujinx.HLE/HOS/Services/Am/AppletAE/AllSystemAppletProxiesService/LibraryAppletCreator/ILibraryAppletAccessor.cs b/src/Ryujinx.HLE/HOS/Services/Am/AppletAE/AllSystemAppletProxiesService/LibraryAppletCreator/ILibraryAppletAccessor.cs index 148628136..ffeddbb72 100644 --- a/src/Ryujinx.HLE/HOS/Services/Am/AppletAE/AllSystemAppletProxiesService/LibraryAppletCreator/ILibraryAppletAccessor.cs +++ b/src/Ryujinx.HLE/HOS/Services/Am/AppletAE/AllSystemAppletProxiesService/LibraryAppletCreator/ILibraryAppletAccessor.cs @@ -111,7 +111,7 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletAE.AllSystemAppletProxiesService.Lib { // NOTE: This call reset two internal fields to 0 and one internal field to "true". // It seems to be used only with software keyboard inline. - // Since we doesn't support applets for now, it's fine to stub it. + // Since we don't support applets for now, it's fine to stub it. Logger.Stub?.PrintStub(LogClass.ServiceAm); diff --git a/src/Ryujinx.HLE/HOS/Services/Am/AppletAE/IAllSystemAppletProxiesService.cs b/src/Ryujinx.HLE/HOS/Services/Am/AppletAE/IAllSystemAppletProxiesService.cs index 0a032562a..b8741b22b 100644 --- a/src/Ryujinx.HLE/HOS/Services/Am/AppletAE/IAllSystemAppletProxiesService.cs +++ b/src/Ryujinx.HLE/HOS/Services/Am/AppletAE/IAllSystemAppletProxiesService.cs @@ -1,4 +1,5 @@ using Ryujinx.HLE.HOS.Services.Am.AppletAE.AllSystemAppletProxiesService; +using Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService; namespace Ryujinx.HLE.HOS.Services.Am.AppletAE { @@ -25,5 +26,14 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletAE return ResultCode.Success; } + + [CommandCmif(350)] + // OpenSystemApplicationProxy(u64, pid, handle) -> object + public ResultCode OpenSystemApplicationProxy(ServiceCtx context) + { + MakeObject(context, new IApplicationProxy(context.Request.HandleDesc.PId)); + + return ResultCode.Success; + } } } diff --git a/src/Ryujinx.HLE/HOS/Services/Am/AppletOE/ApplicationProxyService/ApplicationProxy/IApplicationFunctions.cs b/src/Ryujinx.HLE/HOS/Services/Am/AppletOE/ApplicationProxyService/ApplicationProxy/IApplicationFunctions.cs index 271d00605..12c046f56 100644 --- a/src/Ryujinx.HLE/HOS/Services/Am/AppletOE/ApplicationProxyService/ApplicationProxy/IApplicationFunctions.cs +++ b/src/Ryujinx.HLE/HOS/Services/Am/AppletOE/ApplicationProxyService/ApplicationProxy/IApplicationFunctions.cs @@ -97,7 +97,7 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.Applicati if (titleId == 0) { - context.Device.UiHandler.ExecuteProgram(context.Device, ProgramSpecifyKind.RestartProgram, titleId); + context.Device.UIHandler.ExecuteProgram(context.Device, ProgramSpecifyKind.RestartProgram, titleId); } else { @@ -524,7 +524,7 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.Applicati Logger.Stub?.PrintStub(LogClass.ServiceAm, new { kind, value }); - context.Device.UiHandler.ExecuteProgram(context.Device, kind, value); + context.Device.UIHandler.ExecuteProgram(context.Device, kind, value); return ResultCode.Success; } @@ -659,7 +659,7 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.Applicati if (string.IsNullOrWhiteSpace(filePath)) { - throw new InvalidSystemResourceException("JIT (010000000000003B) system title not found! The JIT will not work, provide the system archive to fix this error. (See https://github.com/Ryujinx/Ryujinx#requirements for more information)"); + throw new InvalidSystemResourceException("JIT (010000000000003B) system title not found! The JIT will not work, provide the system archive to fix this error. (See https://github.com/GreemDev/Ryujinx#requirements for more information)"); } context.Device.LoadNca(filePath); diff --git a/src/Ryujinx.HLE/HOS/Services/Arp/IReader.cs b/src/Ryujinx.HLE/HOS/Services/Arp/IReader.cs deleted file mode 100644 index 611310421..000000000 --- a/src/Ryujinx.HLE/HOS/Services/Arp/IReader.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Ryujinx.HLE.HOS.Services.Arp -{ - [Service("arp:r")] - class IReader : IpcService - { - public IReader(ServiceCtx context) { } - } -} diff --git a/src/Ryujinx.HLE/HOS/Services/Arp/IWriter.cs b/src/Ryujinx.HLE/HOS/Services/Arp/IWriter.cs deleted file mode 100644 index 22d081b9b..000000000 --- a/src/Ryujinx.HLE/HOS/Services/Arp/IWriter.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Ryujinx.HLE.HOS.Services.Arp -{ - [Service("arp:w")] - class IWriter : IpcService - { - public IWriter(ServiceCtx context) { } - } -} diff --git a/src/Ryujinx.HLE/HOS/Services/Audio/AudioIn/AudioIn.cs b/src/Ryujinx.HLE/HOS/Services/Audio/AudioIn/AudioIn.cs deleted file mode 100644 index acf83f488..000000000 --- a/src/Ryujinx.HLE/HOS/Services/Audio/AudioIn/AudioIn.cs +++ /dev/null @@ -1,108 +0,0 @@ -using Ryujinx.Audio.Common; -using Ryujinx.Audio.Input; -using Ryujinx.Audio.Integration; -using Ryujinx.HLE.HOS.Kernel; -using Ryujinx.HLE.HOS.Kernel.Threading; -using Ryujinx.HLE.HOS.Services.Audio.AudioRenderer; -using System; - -namespace Ryujinx.HLE.HOS.Services.Audio.AudioIn -{ - class AudioIn : IAudioIn - { - private readonly AudioInputSystem _system; - private readonly uint _processHandle; - private readonly KernelContext _kernelContext; - - public AudioIn(AudioInputSystem system, KernelContext kernelContext, uint processHandle) - { - _system = system; - _kernelContext = kernelContext; - _processHandle = processHandle; - } - - public ResultCode AppendBuffer(ulong bufferTag, ref AudioUserBuffer buffer) - { - return (ResultCode)_system.AppendBuffer(bufferTag, ref buffer); - } - - public ResultCode AppendUacBuffer(ulong bufferTag, ref AudioUserBuffer buffer, uint handle) - { - return (ResultCode)_system.AppendUacBuffer(bufferTag, ref buffer, handle); - } - - public bool ContainsBuffer(ulong bufferTag) - { - return _system.ContainsBuffer(bufferTag); - } - - public void Dispose() - { - Dispose(true); - } - - protected virtual void Dispose(bool disposing) - { - if (disposing) - { - _system.Dispose(); - - _kernelContext.Syscall.CloseHandle((int)_processHandle); - } - } - - public bool FlushBuffers() - { - return _system.FlushBuffers(); - } - - public uint GetBufferCount() - { - return _system.GetBufferCount(); - } - - public ResultCode GetReleasedBuffers(Span releasedBuffers, out uint releasedCount) - { - return (ResultCode)_system.GetReleasedBuffers(releasedBuffers, out releasedCount); - } - - public AudioDeviceState GetState() - { - return _system.GetState(); - } - - public float GetVolume() - { - return _system.GetVolume(); - } - - public KEvent RegisterBufferEvent() - { - IWritableEvent outEvent = _system.RegisterBufferEvent(); - - if (outEvent is AudioKernelEvent kernelEvent) - { - return kernelEvent.Event; - } - else - { - throw new NotImplementedException(); - } - } - - public void SetVolume(float volume) - { - _system.SetVolume(volume); - } - - public ResultCode Start() - { - return (ResultCode)_system.Start(); - } - - public ResultCode Stop() - { - return (ResultCode)_system.Stop(); - } - } -} diff --git a/src/Ryujinx.HLE/HOS/Services/Audio/AudioIn/AudioInServer.cs b/src/Ryujinx.HLE/HOS/Services/Audio/AudioIn/AudioInServer.cs deleted file mode 100644 index 3f138021c..000000000 --- a/src/Ryujinx.HLE/HOS/Services/Audio/AudioIn/AudioInServer.cs +++ /dev/null @@ -1,200 +0,0 @@ -using Ryujinx.Audio.Common; -using Ryujinx.Cpu; -using Ryujinx.HLE.HOS.Ipc; -using Ryujinx.HLE.HOS.Kernel.Threading; -using Ryujinx.Horizon.Common; -using Ryujinx.Memory; -using System; -using System.Runtime.InteropServices; - -namespace Ryujinx.HLE.HOS.Services.Audio.AudioIn -{ - class AudioInServer : DisposableIpcService - { - private readonly IAudioIn _impl; - - public AudioInServer(IAudioIn impl) - { - _impl = impl; - } - - [CommandCmif(0)] - // GetAudioInState() -> u32 state - public ResultCode GetAudioInState(ServiceCtx context) - { - context.ResponseData.Write((uint)_impl.GetState()); - - return ResultCode.Success; - } - - [CommandCmif(1)] - // Start() - public ResultCode Start(ServiceCtx context) - { - return _impl.Start(); - } - - [CommandCmif(2)] - // Stop() - public ResultCode StopAudioIn(ServiceCtx context) - { - return _impl.Stop(); - } - - [CommandCmif(3)] - // AppendAudioInBuffer(u64 tag, buffer) - public ResultCode AppendAudioInBuffer(ServiceCtx context) - { - ulong position = context.Request.SendBuff[0].Position; - - ulong bufferTag = context.RequestData.ReadUInt64(); - - AudioUserBuffer data = MemoryHelper.Read(context.Memory, position); - - return _impl.AppendBuffer(bufferTag, ref data); - } - - [CommandCmif(4)] - // RegisterBufferEvent() -> handle - public ResultCode RegisterBufferEvent(ServiceCtx context) - { - KEvent bufferEvent = _impl.RegisterBufferEvent(); - - if (context.Process.HandleTable.GenerateHandle(bufferEvent.ReadableEvent, out int handle) != Result.Success) - { - throw new InvalidOperationException("Out of handles!"); - } - - context.Response.HandleDesc = IpcHandleDesc.MakeCopy(handle); - - return ResultCode.Success; - } - - [CommandCmif(5)] - // GetReleasedAudioInBuffers() -> (u32 count, buffer tags) - public ResultCode GetReleasedAudioInBuffers(ServiceCtx context) - { - ulong position = context.Request.ReceiveBuff[0].Position; - ulong size = context.Request.ReceiveBuff[0].Size; - - using WritableRegion outputRegion = context.Memory.GetWritableRegion((ulong)position, (int)size); - ResultCode result = _impl.GetReleasedBuffers(MemoryMarshal.Cast(outputRegion.Memory.Span), out uint releasedCount); - - context.ResponseData.Write(releasedCount); - - return result; - } - - [CommandCmif(6)] - // ContainsAudioInBuffer(u64 tag) -> b8 - public ResultCode ContainsAudioInBuffer(ServiceCtx context) - { - ulong bufferTag = context.RequestData.ReadUInt64(); - - context.ResponseData.Write(_impl.ContainsBuffer(bufferTag)); - - return ResultCode.Success; - } - - [CommandCmif(7)] // 3.0.0+ - // AppendUacInBuffer(u64 tag, handle, buffer) - public ResultCode AppendUacInBuffer(ServiceCtx context) - { - ulong position = context.Request.SendBuff[0].Position; - - ulong bufferTag = context.RequestData.ReadUInt64(); - uint handle = (uint)context.Request.HandleDesc.ToCopy[0]; - - AudioUserBuffer data = MemoryHelper.Read(context.Memory, position); - - return _impl.AppendUacBuffer(bufferTag, ref data, handle); - } - - [CommandCmif(8)] // 3.0.0+ - // AppendAudioInBufferAuto(u64 tag, buffer) - public ResultCode AppendAudioInBufferAuto(ServiceCtx context) - { - (ulong position, _) = context.Request.GetBufferType0x21(); - - ulong bufferTag = context.RequestData.ReadUInt64(); - - AudioUserBuffer data = MemoryHelper.Read(context.Memory, position); - - return _impl.AppendBuffer(bufferTag, ref data); - } - - [CommandCmif(9)] // 3.0.0+ - // GetReleasedAudioInBuffersAuto() -> (u32 count, buffer tags) - public ResultCode GetReleasedAudioInBuffersAuto(ServiceCtx context) - { - (ulong position, ulong size) = context.Request.GetBufferType0x22(); - - using WritableRegion outputRegion = context.Memory.GetWritableRegion(position, (int)size); - ResultCode result = _impl.GetReleasedBuffers(MemoryMarshal.Cast(outputRegion.Memory.Span), out uint releasedCount); - - context.ResponseData.Write(releasedCount); - - return result; - } - - [CommandCmif(10)] // 3.0.0+ - // AppendUacInBufferAuto(u64 tag, handle, buffer) - public ResultCode AppendUacInBufferAuto(ServiceCtx context) - { - (ulong position, _) = context.Request.GetBufferType0x21(); - - ulong bufferTag = context.RequestData.ReadUInt64(); - uint handle = (uint)context.Request.HandleDesc.ToCopy[0]; - - AudioUserBuffer data = MemoryHelper.Read(context.Memory, position); - - return _impl.AppendUacBuffer(bufferTag, ref data, handle); - } - - [CommandCmif(11)] // 4.0.0+ - // GetAudioInBufferCount() -> u32 - public ResultCode GetAudioInBufferCount(ServiceCtx context) - { - context.ResponseData.Write(_impl.GetBufferCount()); - - return ResultCode.Success; - } - - [CommandCmif(12)] // 4.0.0+ - // SetAudioInVolume(s32) - public ResultCode SetAudioInVolume(ServiceCtx context) - { - float volume = context.RequestData.ReadSingle(); - - _impl.SetVolume(volume); - - return ResultCode.Success; - } - - [CommandCmif(13)] // 4.0.0+ - // GetAudioInVolume() -> s32 - public ResultCode GetAudioInVolume(ServiceCtx context) - { - context.ResponseData.Write(_impl.GetVolume()); - - return ResultCode.Success; - } - - [CommandCmif(14)] // 6.0.0+ - // FlushAudioInBuffers() -> b8 - public ResultCode FlushAudioInBuffers(ServiceCtx context) - { - context.ResponseData.Write(_impl.FlushBuffers()); - - return ResultCode.Success; - } - - protected override void Dispose(bool isDisposing) - { - if (isDisposing) - { - _impl.Dispose(); - } - } - } -} diff --git a/src/Ryujinx.HLE/HOS/Services/Audio/AudioIn/IAudioIn.cs b/src/Ryujinx.HLE/HOS/Services/Audio/AudioIn/IAudioIn.cs deleted file mode 100644 index 4e67303df..000000000 --- a/src/Ryujinx.HLE/HOS/Services/Audio/AudioIn/IAudioIn.cs +++ /dev/null @@ -1,34 +0,0 @@ -using Ryujinx.Audio.Common; -using Ryujinx.HLE.HOS.Kernel.Threading; -using System; - -namespace Ryujinx.HLE.HOS.Services.Audio.AudioIn -{ - interface IAudioIn : IDisposable - { - AudioDeviceState GetState(); - - ResultCode Start(); - - ResultCode Stop(); - - ResultCode AppendBuffer(ulong bufferTag, ref AudioUserBuffer buffer); - - // NOTE: This is broken by design... not quite sure what it's used for (if anything in production). - ResultCode AppendUacBuffer(ulong bufferTag, ref AudioUserBuffer buffer, uint handle); - - KEvent RegisterBufferEvent(); - - ResultCode GetReleasedBuffers(Span releasedBuffers, out uint releasedCount); - - bool ContainsBuffer(ulong bufferTag); - - uint GetBufferCount(); - - bool FlushBuffers(); - - void SetVolume(float volume); - - float GetVolume(); - } -} diff --git a/src/Ryujinx.HLE/HOS/Services/Audio/AudioInManager.cs b/src/Ryujinx.HLE/HOS/Services/Audio/AudioInManager.cs deleted file mode 100644 index 1e759e0ca..000000000 --- a/src/Ryujinx.HLE/HOS/Services/Audio/AudioInManager.cs +++ /dev/null @@ -1,40 +0,0 @@ -using Ryujinx.Audio.Common; -using Ryujinx.Audio.Input; -using Ryujinx.HLE.HOS.Services.Audio.AudioIn; -using AudioInManagerImpl = Ryujinx.Audio.Input.AudioInputManager; - -namespace Ryujinx.HLE.HOS.Services.Audio -{ - class AudioInManager : IAudioInManager - { - private readonly AudioInManagerImpl _impl; - - public AudioInManager(AudioInManagerImpl impl) - { - _impl = impl; - } - - public string[] ListAudioIns(bool filtered) - { - return _impl.ListAudioIns(filtered); - } - - public ResultCode OpenAudioIn(ServiceCtx context, out string outputDeviceName, out AudioOutputConfiguration outputConfiguration, out IAudioIn obj, string inputDeviceName, ref AudioInputConfiguration parameter, ulong appletResourceUserId, uint processHandle) - { - var memoryManager = context.Process.HandleTable.GetKProcess((int)processHandle).CpuMemory; - - ResultCode result = (ResultCode)_impl.OpenAudioIn(out outputDeviceName, out outputConfiguration, out AudioInputSystem inSystem, memoryManager, inputDeviceName, SampleFormat.PcmInt16, ref parameter, appletResourceUserId, processHandle); - - if (result == ResultCode.Success) - { - obj = new AudioIn.AudioIn(inSystem, context.Device.System.KernelContext, processHandle); - } - else - { - obj = null; - } - - return result; - } - } -} diff --git a/src/Ryujinx.HLE/HOS/Services/Audio/AudioInManagerServer.cs b/src/Ryujinx.HLE/HOS/Services/Audio/AudioInManagerServer.cs deleted file mode 100644 index 1b35a62d8..000000000 --- a/src/Ryujinx.HLE/HOS/Services/Audio/AudioInManagerServer.cs +++ /dev/null @@ -1,243 +0,0 @@ -using Ryujinx.Audio.Common; -using Ryujinx.Common; -using Ryujinx.Common.Logging; -using Ryujinx.Cpu; -using Ryujinx.HLE.HOS.Services.Audio.AudioIn; -using System.Text; - -namespace Ryujinx.HLE.HOS.Services.Audio -{ - [Service("audin:u")] - class AudioInManagerServer : IpcService - { - private const int AudioInNameSize = 0x100; - - private readonly IAudioInManager _impl; - - public AudioInManagerServer(ServiceCtx context) : this(context, new AudioInManager(context.Device.System.AudioInputManager)) { } - - public AudioInManagerServer(ServiceCtx context, IAudioInManager impl) : base(context.Device.System.AudOutServer) - { - _impl = impl; - } - - [CommandCmif(0)] - // ListAudioIns() -> (u32, buffer) - public ResultCode ListAudioIns(ServiceCtx context) - { - string[] deviceNames = _impl.ListAudioIns(false); - - ulong position = context.Request.ReceiveBuff[0].Position; - ulong size = context.Request.ReceiveBuff[0].Size; - - ulong basePosition = position; - - int count = 0; - - foreach (string name in deviceNames) - { - byte[] buffer = Encoding.ASCII.GetBytes(name); - - if ((position - basePosition) + (ulong)buffer.Length > size) - { - Logger.Error?.Print(LogClass.ServiceAudio, $"Output buffer size {size} too small!"); - - break; - } - - context.Memory.Write(position, buffer); - MemoryHelper.FillWithZeros(context.Memory, position + (ulong)buffer.Length, AudioInNameSize - buffer.Length); - - position += AudioInNameSize; - count++; - } - - context.ResponseData.Write(count); - - return ResultCode.Success; - } - - [CommandCmif(1)] - // OpenAudioIn(AudioInInputConfiguration input_config, nn::applet::AppletResourceUserId, pid, handle, buffer name) - // -> (u32 sample_rate, u32 channel_count, u32 pcm_format, u32, object, buffer name) - public ResultCode OpenAudioIn(ServiceCtx context) - { - AudioInputConfiguration inputConfiguration = context.RequestData.ReadStruct(); - ulong appletResourceUserId = context.RequestData.ReadUInt64(); - - ulong deviceNameInputPosition = context.Request.SendBuff[0].Position; - ulong deviceNameInputSize = context.Request.SendBuff[0].Size; - - ulong deviceNameOutputPosition = context.Request.ReceiveBuff[0].Position; -#pragma warning disable IDE0059 // Remove unnecessary value assignment - ulong deviceNameOutputSize = context.Request.ReceiveBuff[0].Size; -#pragma warning restore IDE0059 - - uint processHandle = (uint)context.Request.HandleDesc.ToCopy[0]; - - string inputDeviceName = MemoryHelper.ReadAsciiString(context.Memory, deviceNameInputPosition, (long)deviceNameInputSize); - - ResultCode resultCode = _impl.OpenAudioIn(context, out string outputDeviceName, out AudioOutputConfiguration outputConfiguration, out IAudioIn obj, inputDeviceName, ref inputConfiguration, appletResourceUserId, processHandle); - - if (resultCode == ResultCode.Success) - { - context.ResponseData.WriteStruct(outputConfiguration); - - byte[] outputDeviceNameRaw = Encoding.ASCII.GetBytes(outputDeviceName); - - context.Memory.Write(deviceNameOutputPosition, outputDeviceNameRaw); - MemoryHelper.FillWithZeros(context.Memory, deviceNameOutputPosition + (ulong)outputDeviceNameRaw.Length, AudioInNameSize - outputDeviceNameRaw.Length); - - MakeObject(context, new AudioInServer(obj)); - } - - return resultCode; - } - - [CommandCmif(2)] // 3.0.0+ - // ListAudioInsAuto() -> (u32, buffer) - public ResultCode ListAudioInsAuto(ServiceCtx context) - { - string[] deviceNames = _impl.ListAudioIns(false); - - (ulong position, ulong size) = context.Request.GetBufferType0x22(); - - ulong basePosition = position; - - int count = 0; - - foreach (string name in deviceNames) - { - byte[] buffer = Encoding.ASCII.GetBytes(name); - - if ((position - basePosition) + (ulong)buffer.Length > size) - { - Logger.Error?.Print(LogClass.ServiceAudio, $"Output buffer size {size} too small!"); - - break; - } - - context.Memory.Write(position, buffer); - MemoryHelper.FillWithZeros(context.Memory, position + (ulong)buffer.Length, AudioInNameSize - buffer.Length); - - position += AudioInNameSize; - count++; - } - - context.ResponseData.Write(count); - - return ResultCode.Success; - } - - [CommandCmif(3)] // 3.0.0+ - // OpenAudioInAuto(AudioInInputConfiguration input_config, nn::applet::AppletResourceUserId, pid, handle, buffer) - // -> (u32 sample_rate, u32 channel_count, u32 pcm_format, u32, object, buffer name) - public ResultCode OpenAudioInAuto(ServiceCtx context) - { - AudioInputConfiguration inputConfiguration = context.RequestData.ReadStruct(); - ulong appletResourceUserId = context.RequestData.ReadUInt64(); - - (ulong deviceNameInputPosition, ulong deviceNameInputSize) = context.Request.GetBufferType0x21(); -#pragma warning disable IDE0059 // Remove unnecessary value assignment - (ulong deviceNameOutputPosition, ulong deviceNameOutputSize) = context.Request.GetBufferType0x22(); -#pragma warning restore IDE0059 - - uint processHandle = (uint)context.Request.HandleDesc.ToCopy[0]; - - string inputDeviceName = MemoryHelper.ReadAsciiString(context.Memory, deviceNameInputPosition, (long)deviceNameInputSize); - - ResultCode resultCode = _impl.OpenAudioIn(context, out string outputDeviceName, out AudioOutputConfiguration outputConfiguration, out IAudioIn obj, inputDeviceName, ref inputConfiguration, appletResourceUserId, processHandle); - - if (resultCode == ResultCode.Success) - { - context.ResponseData.WriteStruct(outputConfiguration); - - byte[] outputDeviceNameRaw = Encoding.ASCII.GetBytes(outputDeviceName); - - context.Memory.Write(deviceNameOutputPosition, outputDeviceNameRaw); - MemoryHelper.FillWithZeros(context.Memory, deviceNameOutputPosition + (ulong)outputDeviceNameRaw.Length, AudioInNameSize - outputDeviceNameRaw.Length); - - MakeObject(context, new AudioInServer(obj)); - } - - return resultCode; - } - - [CommandCmif(4)] // 3.0.0+ - // ListAudioInsAutoFiltered() -> (u32, buffer) - public ResultCode ListAudioInsAutoFiltered(ServiceCtx context) - { - string[] deviceNames = _impl.ListAudioIns(true); - - (ulong position, ulong size) = context.Request.GetBufferType0x22(); - - ulong basePosition = position; - - int count = 0; - - foreach (string name in deviceNames) - { - byte[] buffer = Encoding.ASCII.GetBytes(name); - - if ((position - basePosition) + (ulong)buffer.Length > size) - { - Logger.Error?.Print(LogClass.ServiceAudio, $"Output buffer size {size} too small!"); - - break; - } - - context.Memory.Write(position, buffer); - MemoryHelper.FillWithZeros(context.Memory, position + (ulong)buffer.Length, AudioInNameSize - buffer.Length); - - position += AudioInNameSize; - count++; - } - - context.ResponseData.Write(count); - - return ResultCode.Success; - } - - [CommandCmif(5)] // 5.0.0+ - // OpenAudioInProtocolSpecified(b64 protocol_specified_related, AudioInInputConfiguration input_config, nn::applet::AppletResourceUserId, pid, handle, buffer name) - // -> (u32 sample_rate, u32 channel_count, u32 pcm_format, u32, object, buffer name) - public ResultCode OpenAudioInProtocolSpecified(ServiceCtx context) - { - // NOTE: We always assume that only the default device will be plugged (we never report any USB Audio Class type devices). -#pragma warning disable IDE0059 // Remove unnecessary value assignment - bool protocolSpecifiedRelated = context.RequestData.ReadUInt64() == 1; -#pragma warning restore IDE0059 - - AudioInputConfiguration inputConfiguration = context.RequestData.ReadStruct(); - ulong appletResourceUserId = context.RequestData.ReadUInt64(); - - ulong deviceNameInputPosition = context.Request.SendBuff[0].Position; - ulong deviceNameInputSize = context.Request.SendBuff[0].Size; - - ulong deviceNameOutputPosition = context.Request.ReceiveBuff[0].Position; -#pragma warning disable IDE0051, IDE0059 // Remove unused private member - ulong deviceNameOutputSize = context.Request.ReceiveBuff[0].Size; -#pragma warning restore IDE0051, IDE0059 - - uint processHandle = (uint)context.Request.HandleDesc.ToCopy[0]; - - string inputDeviceName = MemoryHelper.ReadAsciiString(context.Memory, deviceNameInputPosition, (long)deviceNameInputSize); - - ResultCode resultCode = _impl.OpenAudioIn(context, out string outputDeviceName, out AudioOutputConfiguration outputConfiguration, out IAudioIn obj, inputDeviceName, ref inputConfiguration, appletResourceUserId, processHandle); - - if (resultCode == ResultCode.Success) - { - context.ResponseData.WriteStruct(outputConfiguration); - - byte[] outputDeviceNameRaw = Encoding.ASCII.GetBytes(outputDeviceName); - - context.Memory.Write(deviceNameOutputPosition, outputDeviceNameRaw); - MemoryHelper.FillWithZeros(context.Memory, deviceNameOutputPosition + (ulong)outputDeviceNameRaw.Length, AudioInNameSize - outputDeviceNameRaw.Length); - - MakeObject(context, new AudioInServer(obj)); - } - - return resultCode; - } - } -} diff --git a/src/Ryujinx.HLE/HOS/Services/Audio/AudioOut/AudioOut.cs b/src/Ryujinx.HLE/HOS/Services/Audio/AudioOut/AudioOut.cs deleted file mode 100644 index 2ccf0657f..000000000 --- a/src/Ryujinx.HLE/HOS/Services/Audio/AudioOut/AudioOut.cs +++ /dev/null @@ -1,108 +0,0 @@ -using Ryujinx.Audio.Common; -using Ryujinx.Audio.Integration; -using Ryujinx.Audio.Output; -using Ryujinx.HLE.HOS.Kernel; -using Ryujinx.HLE.HOS.Kernel.Threading; -using Ryujinx.HLE.HOS.Services.Audio.AudioRenderer; -using System; - -namespace Ryujinx.HLE.HOS.Services.Audio.AudioOut -{ - class AudioOut : IAudioOut - { - private readonly AudioOutputSystem _system; - private readonly uint _processHandle; - private readonly KernelContext _kernelContext; - - public AudioOut(AudioOutputSystem system, KernelContext kernelContext, uint processHandle) - { - _system = system; - _kernelContext = kernelContext; - _processHandle = processHandle; - } - - public ResultCode AppendBuffer(ulong bufferTag, ref AudioUserBuffer buffer) - { - return (ResultCode)_system.AppendBuffer(bufferTag, ref buffer); - } - - public bool ContainsBuffer(ulong bufferTag) - { - return _system.ContainsBuffer(bufferTag); - } - - public void Dispose() - { - Dispose(true); - } - - protected virtual void Dispose(bool disposing) - { - if (disposing) - { - _system.Dispose(); - - _kernelContext.Syscall.CloseHandle((int)_processHandle); - } - } - - public bool FlushBuffers() - { - return _system.FlushBuffers(); - } - - public uint GetBufferCount() - { - return _system.GetBufferCount(); - } - - public ulong GetPlayedSampleCount() - { - return _system.GetPlayedSampleCount(); - } - - public ResultCode GetReleasedBuffers(Span releasedBuffers, out uint releasedCount) - { - return (ResultCode)_system.GetReleasedBuffer(releasedBuffers, out releasedCount); - } - - public AudioDeviceState GetState() - { - return _system.GetState(); - } - - public float GetVolume() - { - return _system.GetVolume(); - } - - public KEvent RegisterBufferEvent() - { - IWritableEvent outEvent = _system.RegisterBufferEvent(); - - if (outEvent is AudioKernelEvent kernelEvent) - { - return kernelEvent.Event; - } - else - { - throw new NotImplementedException(); - } - } - - public void SetVolume(float volume) - { - _system.SetVolume(volume); - } - - public ResultCode Start() - { - return (ResultCode)_system.Start(); - } - - public ResultCode Stop() - { - return (ResultCode)_system.Stop(); - } - } -} diff --git a/src/Ryujinx.HLE/HOS/Services/Audio/AudioOut/AudioOutServer.cs b/src/Ryujinx.HLE/HOS/Services/Audio/AudioOut/AudioOutServer.cs deleted file mode 100644 index e1b252631..000000000 --- a/src/Ryujinx.HLE/HOS/Services/Audio/AudioOut/AudioOutServer.cs +++ /dev/null @@ -1,181 +0,0 @@ -using Ryujinx.Audio.Common; -using Ryujinx.Cpu; -using Ryujinx.HLE.HOS.Ipc; -using Ryujinx.HLE.HOS.Kernel.Threading; -using Ryujinx.Horizon.Common; -using Ryujinx.Memory; -using System; -using System.Runtime.InteropServices; - -namespace Ryujinx.HLE.HOS.Services.Audio.AudioOut -{ - class AudioOutServer : DisposableIpcService - { - private readonly IAudioOut _impl; - - public AudioOutServer(IAudioOut impl) - { - _impl = impl; - } - - [CommandCmif(0)] - // GetAudioOutState() -> u32 state - public ResultCode GetAudioOutState(ServiceCtx context) - { - context.ResponseData.Write((uint)_impl.GetState()); - - return ResultCode.Success; - } - - [CommandCmif(1)] - // Start() - public ResultCode Start(ServiceCtx context) - { - return _impl.Start(); - } - - [CommandCmif(2)] - // Stop() - public ResultCode Stop(ServiceCtx context) - { - return _impl.Stop(); - } - - [CommandCmif(3)] - // AppendAudioOutBuffer(u64 bufferTag, buffer buffer) - public ResultCode AppendAudioOutBuffer(ServiceCtx context) - { - ulong position = context.Request.SendBuff[0].Position; - - ulong bufferTag = context.RequestData.ReadUInt64(); - - AudioUserBuffer data = MemoryHelper.Read(context.Memory, position); - - return _impl.AppendBuffer(bufferTag, ref data); - } - - [CommandCmif(4)] - // RegisterBufferEvent() -> handle - public ResultCode RegisterBufferEvent(ServiceCtx context) - { - KEvent bufferEvent = _impl.RegisterBufferEvent(); - - if (context.Process.HandleTable.GenerateHandle(bufferEvent.ReadableEvent, out int handle) != Result.Success) - { - throw new InvalidOperationException("Out of handles!"); - } - - context.Response.HandleDesc = IpcHandleDesc.MakeCopy(handle); - - return ResultCode.Success; - } - - [CommandCmif(5)] - // GetReleasedAudioOutBuffers() -> (u32 count, buffer tags) - public ResultCode GetReleasedAudioOutBuffers(ServiceCtx context) - { - ulong position = context.Request.ReceiveBuff[0].Position; - ulong size = context.Request.ReceiveBuff[0].Size; - - using WritableRegion outputRegion = context.Memory.GetWritableRegion(position, (int)size); - ResultCode result = _impl.GetReleasedBuffers(MemoryMarshal.Cast(outputRegion.Memory.Span), out uint releasedCount); - - context.ResponseData.Write(releasedCount); - - return result; - } - - [CommandCmif(6)] - // ContainsAudioOutBuffer(u64 tag) -> b8 - public ResultCode ContainsAudioOutBuffer(ServiceCtx context) - { - ulong bufferTag = context.RequestData.ReadUInt64(); - - context.ResponseData.Write(_impl.ContainsBuffer(bufferTag)); - - return ResultCode.Success; - } - - [CommandCmif(7)] // 3.0.0+ - // AppendAudioOutBufferAuto(u64 tag, buffer) - public ResultCode AppendAudioOutBufferAuto(ServiceCtx context) - { - (ulong position, _) = context.Request.GetBufferType0x21(); - - ulong bufferTag = context.RequestData.ReadUInt64(); - - AudioUserBuffer data = MemoryHelper.Read(context.Memory, position); - - return _impl.AppendBuffer(bufferTag, ref data); - } - - [CommandCmif(8)] // 3.0.0+ - // GetReleasedAudioOutBuffersAuto() -> (u32 count, buffer tags) - public ResultCode GetReleasedAudioOutBuffersAuto(ServiceCtx context) - { - (ulong position, ulong size) = context.Request.GetBufferType0x22(); - - using WritableRegion outputRegion = context.Memory.GetWritableRegion(position, (int)size); - ResultCode result = _impl.GetReleasedBuffers(MemoryMarshal.Cast(outputRegion.Memory.Span), out uint releasedCount); - - context.ResponseData.Write(releasedCount); - - return result; - } - - [CommandCmif(9)] // 4.0.0+ - // GetAudioOutBufferCount() -> u32 - public ResultCode GetAudioOutBufferCount(ServiceCtx context) - { - context.ResponseData.Write(_impl.GetBufferCount()); - - return ResultCode.Success; - } - - [CommandCmif(10)] // 4.0.0+ - // GetAudioOutPlayedSampleCount() -> u64 - public ResultCode GetAudioOutPlayedSampleCount(ServiceCtx context) - { - context.ResponseData.Write(_impl.GetPlayedSampleCount()); - - return ResultCode.Success; - } - - [CommandCmif(11)] // 4.0.0+ - // FlushAudioOutBuffers() -> b8 - public ResultCode FlushAudioOutBuffers(ServiceCtx context) - { - context.ResponseData.Write(_impl.FlushBuffers()); - - return ResultCode.Success; - } - - [CommandCmif(12)] // 6.0.0+ - // SetAudioOutVolume(s32) - public ResultCode SetAudioOutVolume(ServiceCtx context) - { - float volume = context.RequestData.ReadSingle(); - - _impl.SetVolume(volume); - - return ResultCode.Success; - } - - [CommandCmif(13)] // 6.0.0+ - // GetAudioOutVolume() -> s32 - public ResultCode GetAudioOutVolume(ServiceCtx context) - { - context.ResponseData.Write(_impl.GetVolume()); - - return ResultCode.Success; - } - - protected override void Dispose(bool isDisposing) - { - if (isDisposing) - { - _impl.Dispose(); - } - } - } -} diff --git a/src/Ryujinx.HLE/HOS/Services/Audio/AudioOut/IAudioOut.cs b/src/Ryujinx.HLE/HOS/Services/Audio/AudioOut/IAudioOut.cs deleted file mode 100644 index 8c8c68629..000000000 --- a/src/Ryujinx.HLE/HOS/Services/Audio/AudioOut/IAudioOut.cs +++ /dev/null @@ -1,33 +0,0 @@ -using Ryujinx.Audio.Common; -using Ryujinx.HLE.HOS.Kernel.Threading; -using System; - -namespace Ryujinx.HLE.HOS.Services.Audio.AudioOut -{ - interface IAudioOut : IDisposable - { - AudioDeviceState GetState(); - - ResultCode Start(); - - ResultCode Stop(); - - ResultCode AppendBuffer(ulong bufferTag, ref AudioUserBuffer buffer); - - KEvent RegisterBufferEvent(); - - ResultCode GetReleasedBuffers(Span releasedBuffers, out uint releasedCount); - - bool ContainsBuffer(ulong bufferTag); - - uint GetBufferCount(); - - ulong GetPlayedSampleCount(); - - bool FlushBuffers(); - - void SetVolume(float volume); - - float GetVolume(); - } -} diff --git a/src/Ryujinx.HLE/HOS/Services/Audio/AudioOutManager.cs b/src/Ryujinx.HLE/HOS/Services/Audio/AudioOutManager.cs deleted file mode 100644 index c45a485bc..000000000 --- a/src/Ryujinx.HLE/HOS/Services/Audio/AudioOutManager.cs +++ /dev/null @@ -1,40 +0,0 @@ -using Ryujinx.Audio.Common; -using Ryujinx.Audio.Output; -using Ryujinx.HLE.HOS.Services.Audio.AudioOut; -using AudioOutManagerImpl = Ryujinx.Audio.Output.AudioOutputManager; - -namespace Ryujinx.HLE.HOS.Services.Audio -{ - class AudioOutManager : IAudioOutManager - { - private readonly AudioOutManagerImpl _impl; - - public AudioOutManager(AudioOutManagerImpl impl) - { - _impl = impl; - } - - public string[] ListAudioOuts() - { - return _impl.ListAudioOuts(); - } - - public ResultCode OpenAudioOut(ServiceCtx context, out string outputDeviceName, out AudioOutputConfiguration outputConfiguration, out IAudioOut obj, string inputDeviceName, ref AudioInputConfiguration parameter, ulong appletResourceUserId, uint processHandle, float volume) - { - var memoryManager = context.Process.HandleTable.GetKProcess((int)processHandle).CpuMemory; - - ResultCode result = (ResultCode)_impl.OpenAudioOut(out outputDeviceName, out outputConfiguration, out AudioOutputSystem outSystem, memoryManager, inputDeviceName, SampleFormat.PcmInt16, ref parameter, appletResourceUserId, processHandle, volume); - - if (result == ResultCode.Success) - { - obj = new AudioOut.AudioOut(outSystem, context.Device.System.KernelContext, processHandle); - } - else - { - obj = null; - } - - return result; - } - } -} diff --git a/src/Ryujinx.HLE/HOS/Services/Audio/AudioOutManagerServer.cs b/src/Ryujinx.HLE/HOS/Services/Audio/AudioOutManagerServer.cs deleted file mode 100644 index 79ae6a141..000000000 --- a/src/Ryujinx.HLE/HOS/Services/Audio/AudioOutManagerServer.cs +++ /dev/null @@ -1,166 +0,0 @@ -using Ryujinx.Audio.Common; -using Ryujinx.Common; -using Ryujinx.Common.Logging; -using Ryujinx.Cpu; -using Ryujinx.HLE.HOS.Services.Audio.AudioOut; -using System.Text; - -namespace Ryujinx.HLE.HOS.Services.Audio -{ - [Service("audout:u")] - class AudioOutManagerServer : IpcService - { - private const int AudioOutNameSize = 0x100; - - private readonly IAudioOutManager _impl; - - public AudioOutManagerServer(ServiceCtx context) : this(context, new AudioOutManager(context.Device.System.AudioOutputManager)) { } - - public AudioOutManagerServer(ServiceCtx context, IAudioOutManager impl) : base(context.Device.System.AudOutServer) - { - _impl = impl; - } - - [CommandCmif(0)] - // ListAudioOuts() -> (u32, buffer) - public ResultCode ListAudioOuts(ServiceCtx context) - { - string[] deviceNames = _impl.ListAudioOuts(); - - ulong position = context.Request.ReceiveBuff[0].Position; - ulong size = context.Request.ReceiveBuff[0].Size; - - ulong basePosition = position; - - int count = 0; - - foreach (string name in deviceNames) - { - byte[] buffer = Encoding.ASCII.GetBytes(name); - - if ((position - basePosition) + (ulong)buffer.Length > size) - { - Logger.Error?.Print(LogClass.ServiceAudio, $"Output buffer size {size} too small!"); - - break; - } - - context.Memory.Write(position, buffer); - MemoryHelper.FillWithZeros(context.Memory, position + (ulong)buffer.Length, AudioOutNameSize - buffer.Length); - - position += AudioOutNameSize; - count++; - } - - context.ResponseData.Write(count); - - return ResultCode.Success; - } - - [CommandCmif(1)] - // OpenAudioOut(AudioOutInputConfiguration input_config, nn::applet::AppletResourceUserId, pid, handle process_handle, buffer name_in) - // -> (AudioOutInputConfiguration output_config, object, buffer name_out) - public ResultCode OpenAudioOut(ServiceCtx context) - { - AudioInputConfiguration inputConfiguration = context.RequestData.ReadStruct(); - ulong appletResourceUserId = context.RequestData.ReadUInt64(); - - ulong deviceNameInputPosition = context.Request.SendBuff[0].Position; - ulong deviceNameInputSize = context.Request.SendBuff[0].Size; - - ulong deviceNameOutputPosition = context.Request.ReceiveBuff[0].Position; -#pragma warning disable IDE0059 // Remove unnecessary value assignment - ulong deviceNameOutputSize = context.Request.ReceiveBuff[0].Size; -#pragma warning restore IDE0059 - - uint processHandle = (uint)context.Request.HandleDesc.ToCopy[0]; - - string inputDeviceName = MemoryHelper.ReadAsciiString(context.Memory, deviceNameInputPosition, (long)deviceNameInputSize); - - ResultCode resultCode = _impl.OpenAudioOut(context, out string outputDeviceName, out AudioOutputConfiguration outputConfiguration, out IAudioOut obj, inputDeviceName, ref inputConfiguration, appletResourceUserId, processHandle, context.Device.Configuration.AudioVolume); - - if (resultCode == ResultCode.Success) - { - context.ResponseData.WriteStruct(outputConfiguration); - - byte[] outputDeviceNameRaw = Encoding.ASCII.GetBytes(outputDeviceName); - - context.Memory.Write(deviceNameOutputPosition, outputDeviceNameRaw); - MemoryHelper.FillWithZeros(context.Memory, deviceNameOutputPosition + (ulong)outputDeviceNameRaw.Length, AudioOutNameSize - outputDeviceNameRaw.Length); - - MakeObject(context, new AudioOutServer(obj)); - } - - return resultCode; - } - - [CommandCmif(2)] // 3.0.0+ - // ListAudioOutsAuto() -> (u32, buffer) - public ResultCode ListAudioOutsAuto(ServiceCtx context) - { - string[] deviceNames = _impl.ListAudioOuts(); - - (ulong position, ulong size) = context.Request.GetBufferType0x22(); - - ulong basePosition = position; - - int count = 0; - - foreach (string name in deviceNames) - { - byte[] buffer = Encoding.ASCII.GetBytes(name); - - if ((position - basePosition) + (ulong)buffer.Length > size) - { - Logger.Error?.Print(LogClass.ServiceAudio, $"Output buffer size {size} too small!"); - - break; - } - - context.Memory.Write(position, buffer); - MemoryHelper.FillWithZeros(context.Memory, position + (ulong)buffer.Length, AudioOutNameSize - buffer.Length); - - position += AudioOutNameSize; - count++; - } - - context.ResponseData.Write(count); - - return ResultCode.Success; - } - - [CommandCmif(3)] // 3.0.0+ - // OpenAudioOut(AudioOutInputConfiguration input_config, nn::applet::AppletResourceUserId, pid, handle process_handle, buffer name_in) - // -> (AudioOutInputConfiguration output_config, object, buffer name_out) - public ResultCode OpenAudioOutAuto(ServiceCtx context) - { - AudioInputConfiguration inputConfiguration = context.RequestData.ReadStruct(); - ulong appletResourceUserId = context.RequestData.ReadUInt64(); - - (ulong deviceNameInputPosition, ulong deviceNameInputSize) = context.Request.GetBufferType0x21(); -#pragma warning disable IDE0059 // Remove unnecessary value assignment - (ulong deviceNameOutputPosition, ulong deviceNameOutputSize) = context.Request.GetBufferType0x22(); -#pragma warning restore IDE0059 - - uint processHandle = (uint)context.Request.HandleDesc.ToCopy[0]; - - string inputDeviceName = MemoryHelper.ReadAsciiString(context.Memory, deviceNameInputPosition, (long)deviceNameInputSize); - - ResultCode resultCode = _impl.OpenAudioOut(context, out string outputDeviceName, out AudioOutputConfiguration outputConfiguration, out IAudioOut obj, inputDeviceName, ref inputConfiguration, appletResourceUserId, processHandle, context.Device.Configuration.AudioVolume); - - if (resultCode == ResultCode.Success) - { - context.ResponseData.WriteStruct(outputConfiguration); - - byte[] outputDeviceNameRaw = Encoding.ASCII.GetBytes(outputDeviceName); - - context.Memory.Write(deviceNameOutputPosition, outputDeviceNameRaw); - MemoryHelper.FillWithZeros(context.Memory, deviceNameOutputPosition + (ulong)outputDeviceNameRaw.Length, AudioOutNameSize - outputDeviceNameRaw.Length); - - MakeObject(context, new AudioOutServer(obj)); - } - - return resultCode; - } - } -} diff --git a/src/Ryujinx.HLE/HOS/Services/Audio/AudioRenderer/AudioDevice.cs b/src/Ryujinx.HLE/HOS/Services/Audio/AudioRenderer/AudioDevice.cs deleted file mode 100644 index 6497a3b84..000000000 --- a/src/Ryujinx.HLE/HOS/Services/Audio/AudioRenderer/AudioDevice.cs +++ /dev/null @@ -1,174 +0,0 @@ -using Ryujinx.Audio.Renderer.Device; -using Ryujinx.Audio.Renderer.Server; -using Ryujinx.HLE.HOS.Kernel; -using Ryujinx.HLE.HOS.Kernel.Threading; - -namespace Ryujinx.HLE.HOS.Services.Audio.AudioRenderer -{ - class AudioDevice : IAudioDevice - { - private readonly VirtualDeviceSession[] _sessions; -#pragma warning disable IDE0052 // Remove unread private member - private readonly ulong _appletResourceId; - private readonly int _revision; -#pragma warning restore IDE0052 - private readonly bool _isUsbDeviceSupported; - - private readonly VirtualDeviceSessionRegistry _registry; - private readonly KEvent _systemEvent; - - public AudioDevice(VirtualDeviceSessionRegistry registry, KernelContext context, ulong appletResourceId, int revision) - { - _registry = registry; - _appletResourceId = appletResourceId; - _revision = revision; - - BehaviourContext behaviourContext = new(); - behaviourContext.SetUserRevision(revision); - - _isUsbDeviceSupported = behaviourContext.IsAudioUsbDeviceOutputSupported(); - _sessions = _registry.GetSessionByAppletResourceId(appletResourceId); - - // TODO: support the 3 different events correctly when we will have hot plugable audio devices. - _systemEvent = new KEvent(context); - _systemEvent.ReadableEvent.Signal(); - } - - private bool TryGetDeviceByName(out VirtualDeviceSession result, string name, bool ignoreRevLimitation = false) - { - result = null; - - foreach (VirtualDeviceSession session in _sessions) - { - if (session.Device.Name.Equals(name)) - { - if (!ignoreRevLimitation && !_isUsbDeviceSupported && session.Device.IsUsbDevice()) - { - return false; - } - - result = session; - - return true; - } - } - - return false; - } - - public string GetActiveAudioDeviceName() - { - VirtualDevice device = _registry.ActiveDevice; - - if (!_isUsbDeviceSupported && device.IsUsbDevice()) - { - device = _registry.DefaultDevice; - } - - return device.Name; - } - - public uint GetActiveChannelCount() - { - VirtualDevice device = _registry.ActiveDevice; - - if (!_isUsbDeviceSupported && device.IsUsbDevice()) - { - device = _registry.DefaultDevice; - } - - return device.ChannelCount; - } - - public ResultCode GetAudioDeviceOutputVolume(string name, out float volume) - { - if (TryGetDeviceByName(out VirtualDeviceSession result, name)) - { - volume = result.Volume; - } - else - { - volume = 0.0f; - } - - return ResultCode.Success; - } - - public ResultCode SetAudioDeviceOutputVolume(string name, float volume) - { - if (TryGetDeviceByName(out VirtualDeviceSession result, name, true)) - { - if (!_isUsbDeviceSupported && result.Device.IsUsbDevice()) - { - result = _sessions[0]; - } - - result.Volume = volume; - } - - return ResultCode.Success; - } - - public string GetActiveAudioOutputDeviceName() - { - return _registry.ActiveDevice.GetOutputDeviceName(); - } - - public string[] ListAudioDeviceName() - { - int deviceCount = _sessions.Length; - - if (!_isUsbDeviceSupported) - { - deviceCount--; - } - - string[] result = new string[deviceCount]; - - int i = 0; - - foreach (VirtualDeviceSession session in _sessions) - { - if (!_isUsbDeviceSupported && session.Device.IsUsbDevice()) - { - continue; - } - - result[i] = session.Device.Name; - - i++; - } - - return result; - } - - public string[] ListAudioOutputDeviceName() - { - int deviceCount = _sessions.Length; - - string[] result = new string[deviceCount]; - - for (int i = 0; i < deviceCount; i++) - { - result[i] = _sessions[i].Device.GetOutputDeviceName(); - } - - return result; - } - - public KEvent QueryAudioDeviceInputEvent() - { - return _systemEvent; - } - - public KEvent QueryAudioDeviceOutputEvent() - { - return _systemEvent; - } - - public KEvent QueryAudioDeviceSystemEvent() - { - return _systemEvent; - } - } -} diff --git a/src/Ryujinx.HLE/HOS/Services/Audio/AudioRenderer/AudioDeviceServer.cs b/src/Ryujinx.HLE/HOS/Services/Audio/AudioRenderer/AudioDeviceServer.cs deleted file mode 100644 index 6206215d5..000000000 --- a/src/Ryujinx.HLE/HOS/Services/Audio/AudioRenderer/AudioDeviceServer.cs +++ /dev/null @@ -1,320 +0,0 @@ -using Ryujinx.Common.Logging; -using Ryujinx.Cpu; -using Ryujinx.HLE.HOS.Ipc; -using Ryujinx.HLE.HOS.Kernel.Threading; -using Ryujinx.Horizon.Common; -using System; -using System.Text; - -namespace Ryujinx.HLE.HOS.Services.Audio.AudioRenderer -{ - class AudioDeviceServer : IpcService - { - private const int AudioDeviceNameSize = 0x100; - - private readonly IAudioDevice _impl; - - public AudioDeviceServer(IAudioDevice impl) - { - _impl = impl; - } - - [CommandCmif(0)] - // ListAudioDeviceName() -> (u32, buffer) - public ResultCode ListAudioDeviceName(ServiceCtx context) - { - string[] deviceNames = _impl.ListAudioDeviceName(); - - ulong position = context.Request.ReceiveBuff[0].Position; - ulong size = context.Request.ReceiveBuff[0].Size; - - ulong basePosition = position; - - int count = 0; - - foreach (string name in deviceNames) - { - byte[] buffer = Encoding.ASCII.GetBytes(name); - - if ((position - basePosition) + (ulong)buffer.Length > size) - { - break; - } - - context.Memory.Write(position, buffer); - MemoryHelper.FillWithZeros(context.Memory, position + (ulong)buffer.Length, AudioDeviceNameSize - buffer.Length); - - position += AudioDeviceNameSize; - count++; - } - - context.ResponseData.Write(count); - - return ResultCode.Success; - } - - [CommandCmif(1)] - // SetAudioDeviceOutputVolume(f32 volume, buffer name) - public ResultCode SetAudioDeviceOutputVolume(ServiceCtx context) - { - float volume = context.RequestData.ReadSingle(); - - ulong position = context.Request.SendBuff[0].Position; - ulong size = context.Request.SendBuff[0].Size; - - string deviceName = MemoryHelper.ReadAsciiString(context.Memory, position, (long)size); - - return _impl.SetAudioDeviceOutputVolume(deviceName, volume); - } - - [CommandCmif(2)] - // GetAudioDeviceOutputVolume(buffer name) -> f32 volume - public ResultCode GetAudioDeviceOutputVolume(ServiceCtx context) - { - ulong position = context.Request.SendBuff[0].Position; - ulong size = context.Request.SendBuff[0].Size; - - string deviceName = MemoryHelper.ReadAsciiString(context.Memory, position, (long)size); - - ResultCode result = _impl.GetAudioDeviceOutputVolume(deviceName, out float volume); - - if (result == ResultCode.Success) - { - context.ResponseData.Write(volume); - } - - return result; - } - - [CommandCmif(3)] - // GetActiveAudioDeviceName() -> buffer - public ResultCode GetActiveAudioDeviceName(ServiceCtx context) - { - string name = _impl.GetActiveAudioDeviceName(); - - ulong position = context.Request.ReceiveBuff[0].Position; - ulong size = context.Request.ReceiveBuff[0].Size; - - byte[] deviceNameBuffer = Encoding.ASCII.GetBytes(name + "\0"); - - if ((ulong)deviceNameBuffer.Length <= size) - { - context.Memory.Write(position, deviceNameBuffer); - } - else - { - Logger.Error?.Print(LogClass.ServiceAudio, $"Output buffer size {size} too small!"); - } - - return ResultCode.Success; - } - - [CommandCmif(4)] - // QueryAudioDeviceSystemEvent() -> handle - public ResultCode QueryAudioDeviceSystemEvent(ServiceCtx context) - { - KEvent deviceSystemEvent = _impl.QueryAudioDeviceSystemEvent(); - - if (context.Process.HandleTable.GenerateHandle(deviceSystemEvent.ReadableEvent, out int handle) != Result.Success) - { - throw new InvalidOperationException("Out of handles!"); - } - - context.Response.HandleDesc = IpcHandleDesc.MakeCopy(handle); - - Logger.Stub?.PrintStub(LogClass.ServiceAudio); - - return ResultCode.Success; - } - - [CommandCmif(5)] - // GetActiveChannelCount() -> u32 - public ResultCode GetActiveChannelCount(ServiceCtx context) - { - context.ResponseData.Write(_impl.GetActiveChannelCount()); - - Logger.Stub?.PrintStub(LogClass.ServiceAudio); - - return ResultCode.Success; - } - - [CommandCmif(6)] // 3.0.0+ - // ListAudioDeviceNameAuto() -> (u32, buffer) - public ResultCode ListAudioDeviceNameAuto(ServiceCtx context) - { - string[] deviceNames = _impl.ListAudioDeviceName(); - - (ulong position, ulong size) = context.Request.GetBufferType0x22(); - - ulong basePosition = position; - - int count = 0; - - foreach (string name in deviceNames) - { - byte[] buffer = Encoding.ASCII.GetBytes(name); - - if ((position - basePosition) + (ulong)buffer.Length > size) - { - break; - } - - context.Memory.Write(position, buffer); - MemoryHelper.FillWithZeros(context.Memory, position + (ulong)buffer.Length, AudioDeviceNameSize - buffer.Length); - - position += AudioDeviceNameSize; - count++; - } - - context.ResponseData.Write(count); - - return ResultCode.Success; - } - - [CommandCmif(7)] // 3.0.0+ - // SetAudioDeviceOutputVolumeAuto(f32 volume, buffer name) - public ResultCode SetAudioDeviceOutputVolumeAuto(ServiceCtx context) - { - float volume = context.RequestData.ReadSingle(); - - (ulong position, ulong size) = context.Request.GetBufferType0x21(); - - string deviceName = MemoryHelper.ReadAsciiString(context.Memory, position, (long)size); - - return _impl.SetAudioDeviceOutputVolume(deviceName, volume); - } - - [CommandCmif(8)] // 3.0.0+ - // GetAudioDeviceOutputVolumeAuto(buffer name) -> f32 - public ResultCode GetAudioDeviceOutputVolumeAuto(ServiceCtx context) - { - (ulong position, ulong size) = context.Request.GetBufferType0x21(); - - string deviceName = MemoryHelper.ReadAsciiString(context.Memory, position, (long)size); - - ResultCode result = _impl.GetAudioDeviceOutputVolume(deviceName, out float volume); - - if (result == ResultCode.Success) - { - context.ResponseData.Write(volume); - } - - return ResultCode.Success; - } - - [CommandCmif(10)] // 3.0.0+ - // GetActiveAudioDeviceNameAuto() -> buffer - public ResultCode GetActiveAudioDeviceNameAuto(ServiceCtx context) - { - string name = _impl.GetActiveAudioDeviceName(); - - (ulong position, ulong size) = context.Request.GetBufferType0x22(); - - byte[] deviceNameBuffer = Encoding.UTF8.GetBytes(name + '\0'); - - if ((ulong)deviceNameBuffer.Length <= size) - { - context.Memory.Write(position, deviceNameBuffer); - } - else - { - Logger.Error?.Print(LogClass.ServiceAudio, $"Output buffer size {size} too small!"); - } - - return ResultCode.Success; - } - - [CommandCmif(11)] // 3.0.0+ - // QueryAudioDeviceInputEvent() -> handle - public ResultCode QueryAudioDeviceInputEvent(ServiceCtx context) - { - KEvent deviceInputEvent = _impl.QueryAudioDeviceInputEvent(); - - if (context.Process.HandleTable.GenerateHandle(deviceInputEvent.ReadableEvent, out int handle) != Result.Success) - { - throw new InvalidOperationException("Out of handles!"); - } - - context.Response.HandleDesc = IpcHandleDesc.MakeCopy(handle); - - Logger.Stub?.PrintStub(LogClass.ServiceAudio); - - return ResultCode.Success; - } - - [CommandCmif(12)] // 3.0.0+ - // QueryAudioDeviceOutputEvent() -> handle - public ResultCode QueryAudioDeviceOutputEvent(ServiceCtx context) - { - KEvent deviceOutputEvent = _impl.QueryAudioDeviceOutputEvent(); - - if (context.Process.HandleTable.GenerateHandle(deviceOutputEvent.ReadableEvent, out int handle) != Result.Success) - { - throw new InvalidOperationException("Out of handles!"); - } - - context.Response.HandleDesc = IpcHandleDesc.MakeCopy(handle); - - Logger.Stub?.PrintStub(LogClass.ServiceAudio); - - return ResultCode.Success; - } - - [CommandCmif(13)] // 13.0.0+ - // GetActiveAudioOutputDeviceName() -> buffer - public ResultCode GetActiveAudioOutputDeviceName(ServiceCtx context) - { - string name = _impl.GetActiveAudioOutputDeviceName(); - - ulong position = context.Request.ReceiveBuff[0].Position; - ulong size = context.Request.ReceiveBuff[0].Size; - - byte[] deviceNameBuffer = Encoding.ASCII.GetBytes(name + "\0"); - - if ((ulong)deviceNameBuffer.Length <= size) - { - context.Memory.Write(position, deviceNameBuffer); - } - else - { - Logger.Error?.Print(LogClass.ServiceAudio, $"Output buffer size {size} too small!"); - } - - return ResultCode.Success; - } - - [CommandCmif(14)] // 13.0.0+ - // ListAudioOutputDeviceName() -> (u32, buffer) - public ResultCode ListAudioOutputDeviceName(ServiceCtx context) - { - string[] deviceNames = _impl.ListAudioOutputDeviceName(); - - ulong position = context.Request.ReceiveBuff[0].Position; - ulong size = context.Request.ReceiveBuff[0].Size; - - ulong basePosition = position; - - int count = 0; - - foreach (string name in deviceNames) - { - byte[] buffer = Encoding.ASCII.GetBytes(name); - - if ((position - basePosition) + (ulong)buffer.Length > size) - { - break; - } - - context.Memory.Write(position, buffer); - MemoryHelper.FillWithZeros(context.Memory, position + (ulong)buffer.Length, AudioDeviceNameSize - buffer.Length); - - position += AudioDeviceNameSize; - count++; - } - - context.ResponseData.Write(count); - - return ResultCode.Success; - } - } -} diff --git a/src/Ryujinx.HLE/HOS/Services/Audio/AudioRenderer/AudioKernelEvent.cs b/src/Ryujinx.HLE/HOS/Services/Audio/AudioRenderer/AudioKernelEvent.cs deleted file mode 100644 index 414c70a43..000000000 --- a/src/Ryujinx.HLE/HOS/Services/Audio/AudioRenderer/AudioKernelEvent.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Ryujinx.Audio.Integration; -using Ryujinx.HLE.HOS.Kernel.Threading; - -namespace Ryujinx.HLE.HOS.Services.Audio.AudioRenderer -{ - class AudioKernelEvent : IWritableEvent - { - public KEvent Event { get; } - - public AudioKernelEvent(KEvent evnt) - { - Event = evnt; - } - - public void Clear() - { - Event.WritableEvent.Clear(); - } - - public void Signal() - { - Event.WritableEvent.Signal(); - } - } -} diff --git a/src/Ryujinx.HLE/HOS/Services/Audio/AudioRenderer/AudioRenderer.cs b/src/Ryujinx.HLE/HOS/Services/Audio/AudioRenderer/AudioRenderer.cs deleted file mode 100644 index 88456be3e..000000000 --- a/src/Ryujinx.HLE/HOS/Services/Audio/AudioRenderer/AudioRenderer.cs +++ /dev/null @@ -1,122 +0,0 @@ -using Ryujinx.Audio.Integration; -using Ryujinx.Audio.Renderer.Server; -using Ryujinx.HLE.HOS.Kernel.Threading; -using System; - -namespace Ryujinx.HLE.HOS.Services.Audio.AudioRenderer -{ - class AudioRenderer : IAudioRenderer - { - private readonly AudioRenderSystem _impl; - - public AudioRenderer(AudioRenderSystem impl) - { - _impl = impl; - } - - public ResultCode ExecuteAudioRendererRendering() - { - return (ResultCode)_impl.ExecuteAudioRendererRendering(); - } - - public uint GetMixBufferCount() - { - return _impl.GetMixBufferCount(); - } - - public uint GetRenderingTimeLimit() - { - return _impl.GetRenderingTimeLimit(); - } - - public uint GetSampleCount() - { - return _impl.GetSampleCount(); - } - - public uint GetSampleRate() - { - return _impl.GetSampleRate(); - } - - public int GetState() - { - if (_impl.IsActive()) - { - return 0; - } - - return 1; - } - - public ResultCode QuerySystemEvent(out KEvent systemEvent) - { - ResultCode resultCode = (ResultCode)_impl.QuerySystemEvent(out IWritableEvent outEvent); - - if (resultCode == ResultCode.Success) - { - if (outEvent is AudioKernelEvent kernelEvent) - { - systemEvent = kernelEvent.Event; - } - else - { - throw new NotImplementedException(); - } - } - else - { - systemEvent = null; - } - - return resultCode; - } - - public ResultCode RequestUpdate(Memory output, Memory performanceOutput, ReadOnlyMemory input) - { - return (ResultCode)_impl.Update(output, performanceOutput, input); - } - - public void SetRenderingTimeLimit(uint percent) - { - _impl.SetRenderingTimeLimitPercent(percent); - } - - public ResultCode Start() - { - _impl.Start(); - - return ResultCode.Success; - } - - public ResultCode Stop() - { - _impl.Stop(); - - return ResultCode.Success; - } - - public void Dispose() - { - Dispose(true); - } - - protected virtual void Dispose(bool disposing) - { - if (disposing) - { - _impl.Dispose(); - } - } - - public void SetVoiceDropParameter(float voiceDropParameter) - { - _impl.SetVoiceDropParameter(voiceDropParameter); - } - - public float GetVoiceDropParameter() - { - return _impl.GetVoiceDropParameter(); - } - } -} diff --git a/src/Ryujinx.HLE/HOS/Services/Audio/AudioRenderer/AudioRendererServer.cs b/src/Ryujinx.HLE/HOS/Services/Audio/AudioRenderer/AudioRendererServer.cs deleted file mode 100644 index baea01072..000000000 --- a/src/Ryujinx.HLE/HOS/Services/Audio/AudioRenderer/AudioRendererServer.cs +++ /dev/null @@ -1,215 +0,0 @@ -using Ryujinx.Common.Logging; -using Ryujinx.Common.Memory; -using Ryujinx.HLE.HOS.Ipc; -using Ryujinx.HLE.HOS.Kernel.Threading; -using Ryujinx.Horizon.Common; -using System; -using System.Buffers; - -namespace Ryujinx.HLE.HOS.Services.Audio.AudioRenderer -{ - class AudioRendererServer : DisposableIpcService - { - private readonly IAudioRenderer _impl; - - public AudioRendererServer(IAudioRenderer impl) - { - _impl = impl; - } - - [CommandCmif(0)] - // GetSampleRate() -> u32 - public ResultCode GetSampleRate(ServiceCtx context) - { - context.ResponseData.Write(_impl.GetSampleRate()); - - return ResultCode.Success; - } - - [CommandCmif(1)] - // GetSampleCount() -> u32 - public ResultCode GetSampleCount(ServiceCtx context) - { - context.ResponseData.Write(_impl.GetSampleCount()); - - return ResultCode.Success; - } - - [CommandCmif(2)] - // GetMixBufferCount() -> u32 - public ResultCode GetMixBufferCount(ServiceCtx context) - { - context.ResponseData.Write(_impl.GetMixBufferCount()); - - return ResultCode.Success; - } - - [CommandCmif(3)] - // GetState() -> u32 - public ResultCode GetState(ServiceCtx context) - { - context.ResponseData.Write(_impl.GetState()); - - return ResultCode.Success; - } - - [CommandCmif(4)] - // RequestUpdate(buffer input) - // -> (buffer output, buffer performanceOutput) - public ResultCode RequestUpdate(ServiceCtx context) - { - ulong inputPosition = context.Request.SendBuff[0].Position; - ulong inputSize = context.Request.SendBuff[0].Size; - - ulong outputPosition = context.Request.ReceiveBuff[0].Position; - ulong outputSize = context.Request.ReceiveBuff[0].Size; - - ulong performanceOutputPosition = context.Request.ReceiveBuff[1].Position; - ulong performanceOutputSize = context.Request.ReceiveBuff[1].Size; - - ReadOnlyMemory input = context.Memory.GetSpan(inputPosition, (int)inputSize).ToArray(); - - using IMemoryOwner outputOwner = ByteMemoryPool.RentCleared(outputSize); - using IMemoryOwner performanceOutputOwner = ByteMemoryPool.RentCleared(performanceOutputSize); - Memory output = outputOwner.Memory; - Memory performanceOutput = performanceOutputOwner.Memory; - - using MemoryHandle outputHandle = output.Pin(); - using MemoryHandle performanceOutputHandle = performanceOutput.Pin(); - - ResultCode result = _impl.RequestUpdate(output, performanceOutput, input); - - if (result == ResultCode.Success) - { - context.Memory.Write(outputPosition, output.Span); - context.Memory.Write(performanceOutputPosition, performanceOutput.Span); - } - else - { - Logger.Error?.Print(LogClass.ServiceAudio, $"Error while processing renderer update: 0x{(int)result:X}"); - } - - return result; - } - - [CommandCmif(5)] - // Start() - public ResultCode Start(ServiceCtx context) - { - return _impl.Start(); - } - - [CommandCmif(6)] - // Stop() - public ResultCode Stop(ServiceCtx context) - { - return _impl.Stop(); - } - - [CommandCmif(7)] - // QuerySystemEvent() -> handle - public ResultCode QuerySystemEvent(ServiceCtx context) - { - ResultCode result = _impl.QuerySystemEvent(out KEvent systemEvent); - - if (result == ResultCode.Success) - { - if (context.Process.HandleTable.GenerateHandle(systemEvent.ReadableEvent, out int handle) != Result.Success) - { - throw new InvalidOperationException("Out of handles!"); - } - - context.Response.HandleDesc = IpcHandleDesc.MakeCopy(handle); - } - - return result; - } - - [CommandCmif(8)] - // SetAudioRendererRenderingTimeLimit(u32 limit) - public ResultCode SetAudioRendererRenderingTimeLimit(ServiceCtx context) - { - uint limit = context.RequestData.ReadUInt32(); - - _impl.SetRenderingTimeLimit(limit); - - return ResultCode.Success; - } - - [CommandCmif(9)] - // GetAudioRendererRenderingTimeLimit() -> u32 limit - public ResultCode GetAudioRendererRenderingTimeLimit(ServiceCtx context) - { - uint limit = _impl.GetRenderingTimeLimit(); - - context.ResponseData.Write(limit); - - return ResultCode.Success; - } - - [CommandCmif(10)] // 3.0.0+ - // RequestUpdateAuto(buffer input) - // -> (buffer output, buffer performanceOutput) - public ResultCode RequestUpdateAuto(ServiceCtx context) - { - (ulong inputPosition, ulong inputSize) = context.Request.GetBufferType0x21(); - (ulong outputPosition, ulong outputSize) = context.Request.GetBufferType0x22(0); - (ulong performanceOutputPosition, ulong performanceOutputSize) = context.Request.GetBufferType0x22(1); - - ReadOnlyMemory input = context.Memory.GetSpan(inputPosition, (int)inputSize).ToArray(); - - Memory output = new byte[outputSize]; - Memory performanceOutput = new byte[performanceOutputSize]; - - using MemoryHandle outputHandle = output.Pin(); - using MemoryHandle performanceOutputHandle = performanceOutput.Pin(); - - ResultCode result = _impl.RequestUpdate(output, performanceOutput, input); - - if (result == ResultCode.Success) - { - context.Memory.Write(outputPosition, output.Span); - context.Memory.Write(performanceOutputPosition, performanceOutput.Span); - } - - return result; - } - - [CommandCmif(11)] // 3.0.0+ - // ExecuteAudioRendererRendering() - public ResultCode ExecuteAudioRendererRendering(ServiceCtx context) - { - return _impl.ExecuteAudioRendererRendering(); - } - - [CommandCmif(12)] // 15.0.0+ - // SetVoiceDropParameter(f32 voiceDropParameter) - public ResultCode SetVoiceDropParameter(ServiceCtx context) - { - float voiceDropParameter = context.RequestData.ReadSingle(); - - _impl.SetVoiceDropParameter(voiceDropParameter); - - return ResultCode.Success; - } - - [CommandCmif(13)] // 15.0.0+ - // GetVoiceDropParameter() -> f32 voiceDropParameter - public ResultCode GetVoiceDropParameter(ServiceCtx context) - { - float voiceDropParameter = _impl.GetVoiceDropParameter(); - - context.ResponseData.Write(voiceDropParameter); - - return ResultCode.Success; - } - - protected override void Dispose(bool isDisposing) - { - if (isDisposing) - { - _impl.Dispose(); - } - } - } -} diff --git a/src/Ryujinx.HLE/HOS/Services/Audio/AudioRenderer/IAudioDevice.cs b/src/Ryujinx.HLE/HOS/Services/Audio/AudioRenderer/IAudioDevice.cs deleted file mode 100644 index 0f181a477..000000000 --- a/src/Ryujinx.HLE/HOS/Services/Audio/AudioRenderer/IAudioDevice.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Ryujinx.HLE.HOS.Kernel.Threading; - -namespace Ryujinx.HLE.HOS.Services.Audio.AudioRenderer -{ - interface IAudioDevice - { - string[] ListAudioDeviceName(); - ResultCode SetAudioDeviceOutputVolume(string name, float volume); - ResultCode GetAudioDeviceOutputVolume(string name, out float volume); - string GetActiveAudioDeviceName(); - KEvent QueryAudioDeviceSystemEvent(); - uint GetActiveChannelCount(); - KEvent QueryAudioDeviceInputEvent(); - KEvent QueryAudioDeviceOutputEvent(); - string GetActiveAudioOutputDeviceName(); - string[] ListAudioOutputDeviceName(); - } -} diff --git a/src/Ryujinx.HLE/HOS/Services/Audio/AudioRenderer/IAudioRenderer.cs b/src/Ryujinx.HLE/HOS/Services/Audio/AudioRenderer/IAudioRenderer.cs deleted file mode 100644 index 6bb4a5dec..000000000 --- a/src/Ryujinx.HLE/HOS/Services/Audio/AudioRenderer/IAudioRenderer.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Ryujinx.HLE.HOS.Kernel.Threading; -using System; - -namespace Ryujinx.HLE.HOS.Services.Audio.AudioRenderer -{ - interface IAudioRenderer : IDisposable - { - uint GetSampleRate(); - uint GetSampleCount(); - uint GetMixBufferCount(); - int GetState(); - ResultCode RequestUpdate(Memory output, Memory performanceOutput, ReadOnlyMemory input); - ResultCode Start(); - ResultCode Stop(); - ResultCode QuerySystemEvent(out KEvent systemEvent); - void SetRenderingTimeLimit(uint percent); - uint GetRenderingTimeLimit(); - ResultCode ExecuteAudioRendererRendering(); - void SetVoiceDropParameter(float voiceDropParameter); - float GetVoiceDropParameter(); - } -} diff --git a/src/Ryujinx.HLE/HOS/Services/Audio/AudioRendererManager.cs b/src/Ryujinx.HLE/HOS/Services/Audio/AudioRendererManager.cs deleted file mode 100644 index 87d0001e3..000000000 --- a/src/Ryujinx.HLE/HOS/Services/Audio/AudioRendererManager.cs +++ /dev/null @@ -1,67 +0,0 @@ -using Ryujinx.Audio.Renderer.Device; -using Ryujinx.Audio.Renderer.Parameter; -using Ryujinx.Audio.Renderer.Server; -using Ryujinx.HLE.HOS.Kernel.Memory; -using Ryujinx.HLE.HOS.Services.Audio.AudioRenderer; - -using AudioRendererManagerImpl = Ryujinx.Audio.Renderer.Server.AudioRendererManager; - -namespace Ryujinx.HLE.HOS.Services.Audio -{ - class AudioRendererManager : IAudioRendererManager - { - private readonly AudioRendererManagerImpl _impl; - private readonly VirtualDeviceSessionRegistry _registry; - - public AudioRendererManager(AudioRendererManagerImpl impl, VirtualDeviceSessionRegistry registry) - { - _impl = impl; - _registry = registry; - } - - public ResultCode GetAudioDeviceServiceWithRevisionInfo(ServiceCtx context, out IAudioDevice outObject, int revision, ulong appletResourceUserId) - { - outObject = new AudioDevice(_registry, context.Device.System.KernelContext, appletResourceUserId, revision); - - return ResultCode.Success; - } - - public ulong GetWorkBufferSize(ref AudioRendererConfiguration parameter) - { - return AudioRendererManagerImpl.GetWorkBufferSize(ref parameter); - } - - public ResultCode OpenAudioRenderer( - ServiceCtx context, - out IAudioRenderer obj, - ref AudioRendererConfiguration parameter, - ulong workBufferSize, - ulong appletResourceUserId, - KTransferMemory workBufferTransferMemory, - uint processHandle) - { - var memoryManager = context.Process.HandleTable.GetKProcess((int)processHandle).CpuMemory; - - ResultCode result = (ResultCode)_impl.OpenAudioRenderer( - out AudioRenderSystem renderer, - memoryManager, - ref parameter, - appletResourceUserId, - workBufferTransferMemory.Address, - workBufferTransferMemory.Size, - processHandle, - context.Device.Configuration.AudioVolume); - - if (result == ResultCode.Success) - { - obj = new AudioRenderer.AudioRenderer(renderer); - } - else - { - obj = null; - } - - return result; - } - } -} diff --git a/src/Ryujinx.HLE/HOS/Services/Audio/AudioRendererManagerServer.cs b/src/Ryujinx.HLE/HOS/Services/Audio/AudioRendererManagerServer.cs deleted file mode 100644 index 38a841d82..000000000 --- a/src/Ryujinx.HLE/HOS/Services/Audio/AudioRendererManagerServer.cs +++ /dev/null @@ -1,116 +0,0 @@ -using Ryujinx.Audio.Renderer.Parameter; -using Ryujinx.Audio.Renderer.Server; -using Ryujinx.Common; -using Ryujinx.Common.Logging; -using Ryujinx.HLE.HOS.Kernel.Memory; -using Ryujinx.HLE.HOS.Services.Audio.AudioRenderer; - -namespace Ryujinx.HLE.HOS.Services.Audio -{ - [Service("audren:u")] - class AudioRendererManagerServer : IpcService - { - private const int InitialRevision = ('R' << 0) | ('E' << 8) | ('V' << 16) | ('1' << 24); - - private readonly IAudioRendererManager _impl; - - public AudioRendererManagerServer(ServiceCtx context) : this(context, new AudioRendererManager(context.Device.System.AudioRendererManager, context.Device.System.AudioDeviceSessionRegistry)) { } - - public AudioRendererManagerServer(ServiceCtx context, IAudioRendererManager impl) : base(context.Device.System.AudRenServer) - { - _impl = impl; - } - - [CommandCmif(0)] - // OpenAudioRenderer(nn::audio::detail::AudioRendererParameterInternal parameter, u64 workBufferSize, nn::applet::AppletResourceUserId appletResourceId, pid, handle workBuffer, handle processHandle) - // -> object - public ResultCode OpenAudioRenderer(ServiceCtx context) - { - AudioRendererConfiguration parameter = context.RequestData.ReadStruct(); - ulong workBufferSize = context.RequestData.ReadUInt64(); - ulong appletResourceUserId = context.RequestData.ReadUInt64(); - - int transferMemoryHandle = context.Request.HandleDesc.ToCopy[0]; - KTransferMemory workBufferTransferMemory = context.Process.HandleTable.GetObject(transferMemoryHandle); - uint processHandle = (uint)context.Request.HandleDesc.ToCopy[1]; - - ResultCode result = _impl.OpenAudioRenderer( - context, - out IAudioRenderer renderer, - ref parameter, - workBufferSize, - appletResourceUserId, - workBufferTransferMemory, - processHandle); - - if (result == ResultCode.Success) - { - MakeObject(context, new AudioRendererServer(renderer)); - } - - context.Device.System.KernelContext.Syscall.CloseHandle(transferMemoryHandle); - context.Device.System.KernelContext.Syscall.CloseHandle((int)processHandle); - - return result; - } - - [CommandCmif(1)] - // GetWorkBufferSize(nn::audio::detail::AudioRendererParameterInternal parameter) -> u64 workBufferSize - public ResultCode GetAudioRendererWorkBufferSize(ServiceCtx context) - { - AudioRendererConfiguration parameter = context.RequestData.ReadStruct(); - - if (BehaviourContext.CheckValidRevision(parameter.Revision)) - { - ulong size = _impl.GetWorkBufferSize(ref parameter); - - context.ResponseData.Write(size); - - Logger.Debug?.Print(LogClass.ServiceAudio, $"WorkBufferSize is 0x{size:x16}."); - - return ResultCode.Success; - } - else - { - context.ResponseData.Write(0L); - - Logger.Warning?.Print(LogClass.ServiceAudio, $"Library Revision REV{BehaviourContext.GetRevisionNumber(parameter.Revision)} is not supported!"); - - return ResultCode.UnsupportedRevision; - } - } - - [CommandCmif(2)] - // GetAudioDeviceService(nn::applet::AppletResourceUserId) -> object - public ResultCode GetAudioDeviceService(ServiceCtx context) - { - ulong appletResourceUserId = context.RequestData.ReadUInt64(); - - ResultCode result = _impl.GetAudioDeviceServiceWithRevisionInfo(context, out IAudioDevice device, InitialRevision, appletResourceUserId); - - if (result == ResultCode.Success) - { - MakeObject(context, new AudioDeviceServer(device)); - } - - return result; - } - - [CommandCmif(4)] // 4.0.0+ - // GetAudioDeviceServiceWithRevisionInfo(s32 revision, nn::applet::AppletResourceUserId appletResourceId) -> object - public ResultCode GetAudioDeviceServiceWithRevisionInfo(ServiceCtx context) - { - int revision = context.RequestData.ReadInt32(); - ulong appletResourceUserId = context.RequestData.ReadUInt64(); - - ResultCode result = _impl.GetAudioDeviceServiceWithRevisionInfo(context, out IAudioDevice device, revision, appletResourceUserId); - - if (result == ResultCode.Success) - { - MakeObject(context, new AudioDeviceServer(device)); - } - - return result; - } - } -} diff --git a/src/Ryujinx.HLE/HOS/Services/Audio/HardwareOpusDecoderManager/Decoder.cs b/src/Ryujinx.HLE/HOS/Services/Audio/HardwareOpusDecoderManager/Decoder.cs deleted file mode 100644 index c5dd2f00d..000000000 --- a/src/Ryujinx.HLE/HOS/Services/Audio/HardwareOpusDecoderManager/Decoder.cs +++ /dev/null @@ -1,27 +0,0 @@ -using Concentus.Structs; - -namespace Ryujinx.HLE.HOS.Services.Audio.HardwareOpusDecoderManager -{ - class Decoder : IDecoder - { - private readonly OpusDecoder _decoder; - - public int SampleRate => _decoder.SampleRate; - public int ChannelsCount => _decoder.NumChannels; - - public Decoder(int sampleRate, int channelsCount) - { - _decoder = new OpusDecoder(sampleRate, channelsCount); - } - - public int Decode(byte[] inData, int inDataOffset, int len, short[] outPcm, int outPcmOffset, int frameSize) - { - return _decoder.Decode(inData, inDataOffset, len, outPcm, outPcmOffset, frameSize); - } - - public void ResetState() - { - _decoder.ResetState(); - } - } -} diff --git a/src/Ryujinx.HLE/HOS/Services/Audio/HardwareOpusDecoderManager/DecoderCommon.cs b/src/Ryujinx.HLE/HOS/Services/Audio/HardwareOpusDecoderManager/DecoderCommon.cs deleted file mode 100644 index 9ff511a50..000000000 --- a/src/Ryujinx.HLE/HOS/Services/Audio/HardwareOpusDecoderManager/DecoderCommon.cs +++ /dev/null @@ -1,92 +0,0 @@ -using Concentus; -using Concentus.Enums; -using Concentus.Structs; -using Ryujinx.HLE.HOS.Services.Audio.Types; -using System; -using System.Runtime.CompilerServices; - -namespace Ryujinx.HLE.HOS.Services.Audio.HardwareOpusDecoderManager -{ - static class DecoderCommon - { - private static ResultCode GetPacketNumSamples(this IDecoder decoder, out int numSamples, byte[] packet) - { - int result = OpusPacketInfo.GetNumSamples(packet, 0, packet.Length, decoder.SampleRate); - - numSamples = result; - - if (result == OpusError.OPUS_INVALID_PACKET) - { - return ResultCode.OpusInvalidInput; - } - else if (result == OpusError.OPUS_BAD_ARG) - { - return ResultCode.OpusInvalidInput; - } - - return ResultCode.Success; - } - - public static ResultCode DecodeInterleaved( - this IDecoder decoder, - bool reset, - ReadOnlySpan input, - out short[] outPcmData, - ulong outputSize, - out uint outConsumed, - out int outSamples) - { - outPcmData = null; - outConsumed = 0; - outSamples = 0; - - int streamSize = input.Length; - - if (streamSize < Unsafe.SizeOf()) - { - return ResultCode.OpusInvalidInput; - } - - OpusPacketHeader header = OpusPacketHeader.FromSpan(input); - int headerSize = Unsafe.SizeOf(); - uint totalSize = header.length + (uint)headerSize; - - if (totalSize > streamSize) - { - return ResultCode.OpusInvalidInput; - } - - byte[] opusData = input.Slice(headerSize, (int)header.length).ToArray(); - - ResultCode result = decoder.GetPacketNumSamples(out int numSamples, opusData); - - if (result == ResultCode.Success) - { - if ((uint)numSamples * (uint)decoder.ChannelsCount * sizeof(short) > outputSize) - { - return ResultCode.OpusInvalidInput; - } - - outPcmData = new short[numSamples * decoder.ChannelsCount]; - - if (reset) - { - decoder.ResetState(); - } - - try - { - outSamples = decoder.Decode(opusData, 0, opusData.Length, outPcmData, 0, outPcmData.Length / decoder.ChannelsCount); - outConsumed = totalSize; - } - catch (OpusException) - { - // TODO: as OpusException doesn't provide us the exact error code, this is kind of inaccurate in some cases... - return ResultCode.OpusInvalidInput; - } - } - - return ResultCode.Success; - } - } -} diff --git a/src/Ryujinx.HLE/HOS/Services/Audio/HardwareOpusDecoderManager/IDecoder.cs b/src/Ryujinx.HLE/HOS/Services/Audio/HardwareOpusDecoderManager/IDecoder.cs deleted file mode 100644 index cc83be5e3..000000000 --- a/src/Ryujinx.HLE/HOS/Services/Audio/HardwareOpusDecoderManager/IDecoder.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace Ryujinx.HLE.HOS.Services.Audio.HardwareOpusDecoderManager -{ - interface IDecoder - { - int SampleRate { get; } - int ChannelsCount { get; } - - int Decode(byte[] inData, int inDataOffset, int len, short[] outPcm, int outPcmOffset, int frameSize); - void ResetState(); - } -} diff --git a/src/Ryujinx.HLE/HOS/Services/Audio/HardwareOpusDecoderManager/IHardwareOpusDecoder.cs b/src/Ryujinx.HLE/HOS/Services/Audio/HardwareOpusDecoderManager/IHardwareOpusDecoder.cs deleted file mode 100644 index 3d5d2839f..000000000 --- a/src/Ryujinx.HLE/HOS/Services/Audio/HardwareOpusDecoderManager/IHardwareOpusDecoder.cs +++ /dev/null @@ -1,116 +0,0 @@ -using Ryujinx.HLE.HOS.Services.Audio.Types; -using System; -using System.Runtime.InteropServices; - -namespace Ryujinx.HLE.HOS.Services.Audio.HardwareOpusDecoderManager -{ - class IHardwareOpusDecoder : IpcService - { - private readonly IDecoder _decoder; - private readonly OpusDecoderFlags _flags; - - public IHardwareOpusDecoder(int sampleRate, int channelsCount, OpusDecoderFlags flags) - { - _decoder = new Decoder(sampleRate, channelsCount); - _flags = flags; - } - - public IHardwareOpusDecoder(int sampleRate, int channelsCount, int streams, int coupledStreams, OpusDecoderFlags flags, byte[] mapping) - { - _decoder = new MultiSampleDecoder(sampleRate, channelsCount, streams, coupledStreams, mapping); - _flags = flags; - } - - [CommandCmif(0)] - // DecodeInterleavedOld(buffer) -> (u32, u32, buffer) - public ResultCode DecodeInterleavedOld(ServiceCtx context) - { - return DecodeInterleavedInternal(context, OpusDecoderFlags.None, reset: false, withPerf: false); - } - - [CommandCmif(2)] - // DecodeInterleavedForMultiStreamOld(buffer) -> (u32, u32, buffer) - public ResultCode DecodeInterleavedForMultiStreamOld(ServiceCtx context) - { - return DecodeInterleavedInternal(context, OpusDecoderFlags.None, reset: false, withPerf: false); - } - - [CommandCmif(4)] // 6.0.0+ - // DecodeInterleavedWithPerfOld(buffer) -> (u32, u32, u64, buffer) - public ResultCode DecodeInterleavedWithPerfOld(ServiceCtx context) - { - return DecodeInterleavedInternal(context, OpusDecoderFlags.None, reset: false, withPerf: true); - } - - [CommandCmif(5)] // 6.0.0+ - // DecodeInterleavedForMultiStreamWithPerfOld(buffer) -> (u32, u32, u64, buffer) - public ResultCode DecodeInterleavedForMultiStreamWithPerfOld(ServiceCtx context) - { - return DecodeInterleavedInternal(context, OpusDecoderFlags.None, reset: false, withPerf: true); - } - - [CommandCmif(6)] // 6.0.0+ - // DecodeInterleavedWithPerfAndResetOld(bool reset, buffer) -> (u32, u32, u64, buffer) - public ResultCode DecodeInterleavedWithPerfAndResetOld(ServiceCtx context) - { - bool reset = context.RequestData.ReadBoolean(); - - return DecodeInterleavedInternal(context, OpusDecoderFlags.None, reset, withPerf: true); - } - - [CommandCmif(7)] // 6.0.0+ - // DecodeInterleavedForMultiStreamWithPerfAndResetOld(bool reset, buffer) -> (u32, u32, u64, buffer) - public ResultCode DecodeInterleavedForMultiStreamWithPerfAndResetOld(ServiceCtx context) - { - bool reset = context.RequestData.ReadBoolean(); - - return DecodeInterleavedInternal(context, OpusDecoderFlags.None, reset, withPerf: true); - } - - [CommandCmif(8)] // 7.0.0+ - // DecodeInterleaved(bool reset, buffer) -> (u32, u32, u64, buffer) - public ResultCode DecodeInterleaved(ServiceCtx context) - { - bool reset = context.RequestData.ReadBoolean(); - - return DecodeInterleavedInternal(context, _flags, reset, withPerf: true); - } - - [CommandCmif(9)] // 7.0.0+ - // DecodeInterleavedForMultiStream(bool reset, buffer) -> (u32, u32, u64, buffer) - public ResultCode DecodeInterleavedForMultiStream(ServiceCtx context) - { - bool reset = context.RequestData.ReadBoolean(); - - return DecodeInterleavedInternal(context, _flags, reset, withPerf: true); - } - - private ResultCode DecodeInterleavedInternal(ServiceCtx context, OpusDecoderFlags flags, bool reset, bool withPerf) - { - ulong inPosition = context.Request.SendBuff[0].Position; - ulong inSize = context.Request.SendBuff[0].Size; - ulong outputPosition = context.Request.ReceiveBuff[0].Position; - ulong outputSize = context.Request.ReceiveBuff[0].Size; - - ReadOnlySpan input = context.Memory.GetSpan(inPosition, (int)inSize); - - ResultCode result = _decoder.DecodeInterleaved(reset, input, out short[] outPcmData, outputSize, out uint outConsumed, out int outSamples); - - if (result == ResultCode.Success) - { - context.Memory.Write(outputPosition, MemoryMarshal.Cast(outPcmData.AsSpan())); - - context.ResponseData.Write(outConsumed); - context.ResponseData.Write(outSamples); - - if (withPerf) - { - // This is the time the DSP took to process the request, TODO: fill this. - context.ResponseData.Write(0UL); - } - } - - return result; - } - } -} diff --git a/src/Ryujinx.HLE/HOS/Services/Audio/HardwareOpusDecoderManager/MultiSampleDecoder.cs b/src/Ryujinx.HLE/HOS/Services/Audio/HardwareOpusDecoderManager/MultiSampleDecoder.cs deleted file mode 100644 index 910bb23ee..000000000 --- a/src/Ryujinx.HLE/HOS/Services/Audio/HardwareOpusDecoderManager/MultiSampleDecoder.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Concentus.Structs; - -namespace Ryujinx.HLE.HOS.Services.Audio.HardwareOpusDecoderManager -{ - class MultiSampleDecoder : IDecoder - { - private readonly OpusMSDecoder _decoder; - - public int SampleRate => _decoder.SampleRate; - public int ChannelsCount { get; } - - public MultiSampleDecoder(int sampleRate, int channelsCount, int streams, int coupledStreams, byte[] mapping) - { - ChannelsCount = channelsCount; - _decoder = new OpusMSDecoder(sampleRate, channelsCount, streams, coupledStreams, mapping); - } - - public int Decode(byte[] inData, int inDataOffset, int len, short[] outPcm, int outPcmOffset, int frameSize) - { - return _decoder.DecodeMultistream(inData, inDataOffset, len, outPcm, outPcmOffset, frameSize, 0); - } - - public void ResetState() - { - _decoder.ResetState(); - } - } -} diff --git a/src/Ryujinx.HLE/HOS/Services/Audio/IAudioController.cs b/src/Ryujinx.HLE/HOS/Services/Audio/IAudioController.cs deleted file mode 100644 index a250ec799..000000000 --- a/src/Ryujinx.HLE/HOS/Services/Audio/IAudioController.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Ryujinx.HLE.HOS.Services.Audio -{ - [Service("audctl")] - class IAudioController : IpcService - { - public IAudioController(ServiceCtx context) { } - } -} diff --git a/src/Ryujinx.HLE/HOS/Services/Audio/IAudioInManager.cs b/src/Ryujinx.HLE/HOS/Services/Audio/IAudioInManager.cs deleted file mode 100644 index 861e9f2dc..000000000 --- a/src/Ryujinx.HLE/HOS/Services/Audio/IAudioInManager.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Ryujinx.Audio.Common; -using Ryujinx.HLE.HOS.Services.Audio.AudioIn; - -namespace Ryujinx.HLE.HOS.Services.Audio -{ - interface IAudioInManager - { - public string[] ListAudioIns(bool filtered); - - public ResultCode OpenAudioIn(ServiceCtx context, out string outputDeviceName, out AudioOutputConfiguration outputConfiguration, out IAudioIn obj, string inputDeviceName, ref AudioInputConfiguration parameter, ulong appletResourceUserId, uint processHandle); - } -} diff --git a/src/Ryujinx.HLE/HOS/Services/Audio/IAudioInManagerForApplet.cs b/src/Ryujinx.HLE/HOS/Services/Audio/IAudioInManagerForApplet.cs deleted file mode 100644 index d0c385b56..000000000 --- a/src/Ryujinx.HLE/HOS/Services/Audio/IAudioInManagerForApplet.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Ryujinx.HLE.HOS.Services.Audio -{ - [Service("audin:a")] - class IAudioInManagerForApplet : IpcService - { - public IAudioInManagerForApplet(ServiceCtx context) { } - } -} diff --git a/src/Ryujinx.HLE/HOS/Services/Audio/IAudioInManagerForDebugger.cs b/src/Ryujinx.HLE/HOS/Services/Audio/IAudioInManagerForDebugger.cs deleted file mode 100644 index 120136158..000000000 --- a/src/Ryujinx.HLE/HOS/Services/Audio/IAudioInManagerForDebugger.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Ryujinx.HLE.HOS.Services.Audio -{ - [Service("audin:d")] - class IAudioInManagerForDebugger : IpcService - { - public IAudioInManagerForDebugger(ServiceCtx context) { } - } -} diff --git a/src/Ryujinx.HLE/HOS/Services/Audio/IAudioOutManager.cs b/src/Ryujinx.HLE/HOS/Services/Audio/IAudioOutManager.cs deleted file mode 100644 index cd7cbe41c..000000000 --- a/src/Ryujinx.HLE/HOS/Services/Audio/IAudioOutManager.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Ryujinx.Audio.Common; -using Ryujinx.HLE.HOS.Services.Audio.AudioOut; - -namespace Ryujinx.HLE.HOS.Services.Audio -{ - interface IAudioOutManager - { - public string[] ListAudioOuts(); - - public ResultCode OpenAudioOut(ServiceCtx context, out string outputDeviceName, out AudioOutputConfiguration outputConfiguration, out IAudioOut obj, string inputDeviceName, ref AudioInputConfiguration parameter, ulong appletResourceUserId, uint processHandle, float volume); - } -} diff --git a/src/Ryujinx.HLE/HOS/Services/Audio/IAudioOutManagerForApplet.cs b/src/Ryujinx.HLE/HOS/Services/Audio/IAudioOutManagerForApplet.cs deleted file mode 100644 index 9925777e2..000000000 --- a/src/Ryujinx.HLE/HOS/Services/Audio/IAudioOutManagerForApplet.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Ryujinx.HLE.HOS.Services.Audio -{ - [Service("audout:a")] - class IAudioOutManagerForApplet : IpcService - { - public IAudioOutManagerForApplet(ServiceCtx context) { } - } -} diff --git a/src/Ryujinx.HLE/HOS/Services/Audio/IAudioOutManagerForDebugger.cs b/src/Ryujinx.HLE/HOS/Services/Audio/IAudioOutManagerForDebugger.cs deleted file mode 100644 index c41767a01..000000000 --- a/src/Ryujinx.HLE/HOS/Services/Audio/IAudioOutManagerForDebugger.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Ryujinx.HLE.HOS.Services.Audio -{ - [Service("audout:d")] - class IAudioOutManagerForDebugger : IpcService - { - public IAudioOutManagerForDebugger(ServiceCtx context) { } - } -} diff --git a/src/Ryujinx.HLE/HOS/Services/Audio/IAudioRendererManager.cs b/src/Ryujinx.HLE/HOS/Services/Audio/IAudioRendererManager.cs deleted file mode 100644 index 112e246c0..000000000 --- a/src/Ryujinx.HLE/HOS/Services/Audio/IAudioRendererManager.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Ryujinx.Audio.Renderer.Parameter; -using Ryujinx.HLE.HOS.Kernel.Memory; -using Ryujinx.HLE.HOS.Services.Audio.AudioRenderer; - -namespace Ryujinx.HLE.HOS.Services.Audio -{ - interface IAudioRendererManager - { - // TODO: Remove ServiceCtx argument - // BODY: This is only needed by the legacy backend. Refactor this when removing the legacy backend. - ResultCode GetAudioDeviceServiceWithRevisionInfo(ServiceCtx context, out IAudioDevice outObject, int revision, ulong appletResourceUserId); - - // TODO: Remove ServiceCtx argument - // BODY: This is only needed by the legacy backend. Refactor this when removing the legacy backend. - ResultCode OpenAudioRenderer(ServiceCtx context, out IAudioRenderer obj, ref AudioRendererConfiguration parameter, ulong workBufferSize, ulong appletResourceUserId, KTransferMemory workBufferTransferMemory, uint processHandle); - - ulong GetWorkBufferSize(ref AudioRendererConfiguration parameter); - } -} diff --git a/src/Ryujinx.HLE/HOS/Services/Audio/IAudioRendererManagerForApplet.cs b/src/Ryujinx.HLE/HOS/Services/Audio/IAudioRendererManagerForApplet.cs deleted file mode 100644 index dd767993d..000000000 --- a/src/Ryujinx.HLE/HOS/Services/Audio/IAudioRendererManagerForApplet.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Ryujinx.HLE.HOS.Services.Audio -{ - [Service("audren:a")] - class IAudioRendererManagerForApplet : IpcService - { - public IAudioRendererManagerForApplet(ServiceCtx context) { } - } -} diff --git a/src/Ryujinx.HLE/HOS/Services/Audio/IAudioRendererManagerForDebugger.cs b/src/Ryujinx.HLE/HOS/Services/Audio/IAudioRendererManagerForDebugger.cs deleted file mode 100644 index cd2af09b2..000000000 --- a/src/Ryujinx.HLE/HOS/Services/Audio/IAudioRendererManagerForDebugger.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Ryujinx.HLE.HOS.Services.Audio -{ - [Service("audren:d")] - class IAudioRendererManagerForDebugger : IpcService - { - public IAudioRendererManagerForDebugger(ServiceCtx context) { } - } -} diff --git a/src/Ryujinx.HLE/HOS/Services/Audio/IAudioSnoopManager.cs b/src/Ryujinx.HLE/HOS/Services/Audio/IAudioSnoopManager.cs deleted file mode 100644 index aa9789ac5..000000000 --- a/src/Ryujinx.HLE/HOS/Services/Audio/IAudioSnoopManager.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Ryujinx.HLE.HOS.Services.Audio -{ - [Service("auddev")] // 6.0.0+ - class IAudioSnoopManager : IpcService - { - public IAudioSnoopManager(ServiceCtx context) { } - } -} diff --git a/src/Ryujinx.HLE/HOS/Services/Audio/IFinalOutputRecorderManager.cs b/src/Ryujinx.HLE/HOS/Services/Audio/IFinalOutputRecorderManager.cs deleted file mode 100644 index 9b58213e9..000000000 --- a/src/Ryujinx.HLE/HOS/Services/Audio/IFinalOutputRecorderManager.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Ryujinx.HLE.HOS.Services.Audio -{ - [Service("audrec:u")] - class IFinalOutputRecorderManager : IpcService - { - public IFinalOutputRecorderManager(ServiceCtx context) { } - } -} diff --git a/src/Ryujinx.HLE/HOS/Services/Audio/IFinalOutputRecorderManagerForApplet.cs b/src/Ryujinx.HLE/HOS/Services/Audio/IFinalOutputRecorderManagerForApplet.cs deleted file mode 100644 index e2d62eee3..000000000 --- a/src/Ryujinx.HLE/HOS/Services/Audio/IFinalOutputRecorderManagerForApplet.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Ryujinx.HLE.HOS.Services.Audio -{ - [Service("audrec:a")] - class IFinalOutputRecorderManagerForApplet : IpcService - { - public IFinalOutputRecorderManagerForApplet(ServiceCtx context) { } - } -} diff --git a/src/Ryujinx.HLE/HOS/Services/Audio/IFinalOutputRecorderManagerForDebugger.cs b/src/Ryujinx.HLE/HOS/Services/Audio/IFinalOutputRecorderManagerForDebugger.cs deleted file mode 100644 index 7ded79435..000000000 --- a/src/Ryujinx.HLE/HOS/Services/Audio/IFinalOutputRecorderManagerForDebugger.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Ryujinx.HLE.HOS.Services.Audio -{ - [Service("audrec:d")] - class IFinalOutputRecorderManagerForDebugger : IpcService - { - public IFinalOutputRecorderManagerForDebugger(ServiceCtx context) { } - } -} diff --git a/src/Ryujinx.HLE/HOS/Services/Audio/IHardwareOpusDecoderManager.cs b/src/Ryujinx.HLE/HOS/Services/Audio/IHardwareOpusDecoderManager.cs deleted file mode 100644 index 514b51a51..000000000 --- a/src/Ryujinx.HLE/HOS/Services/Audio/IHardwareOpusDecoderManager.cs +++ /dev/null @@ -1,227 +0,0 @@ -using Ryujinx.Common; -using Ryujinx.HLE.HOS.Services.Audio.HardwareOpusDecoderManager; -using Ryujinx.HLE.HOS.Services.Audio.Types; -using System.Runtime.InteropServices; - -namespace Ryujinx.HLE.HOS.Services.Audio -{ - [Service("hwopus")] - class IHardwareOpusDecoderManager : IpcService - { - public IHardwareOpusDecoderManager(ServiceCtx context) { } - - [CommandCmif(0)] - // Initialize(bytes<8, 4>, u32, handle) -> object - public ResultCode Initialize(ServiceCtx context) - { - int sampleRate = context.RequestData.ReadInt32(); - int channelsCount = context.RequestData.ReadInt32(); - - MakeObject(context, new IHardwareOpusDecoder(sampleRate, channelsCount, OpusDecoderFlags.None)); - - // Close transfer memory immediately as we don't use it. - context.Device.System.KernelContext.Syscall.CloseHandle(context.Request.HandleDesc.ToCopy[0]); - - return ResultCode.Success; - } - - [CommandCmif(1)] - // GetWorkBufferSize(bytes<8, 4>) -> u32 - public ResultCode GetWorkBufferSize(ServiceCtx context) - { - int sampleRate = context.RequestData.ReadInt32(); - int channelsCount = context.RequestData.ReadInt32(); - - int opusDecoderSize = GetOpusDecoderSize(channelsCount); - - int frameSize = BitUtils.AlignUp(channelsCount * 1920 / (48000 / sampleRate), 64); - int totalSize = opusDecoderSize + 1536 + frameSize; - - context.ResponseData.Write(totalSize); - - return ResultCode.Success; - } - - [CommandCmif(2)] // 3.0.0+ - // InitializeForMultiStream(u32, handle, buffer, 0x19>) -> object - public ResultCode InitializeForMultiStream(ServiceCtx context) - { - ulong parametersAddress = context.Request.PtrBuff[0].Position; - - OpusMultiStreamParameters parameters = context.Memory.Read(parametersAddress); - - MakeObject(context, new IHardwareOpusDecoder(parameters.SampleRate, parameters.ChannelsCount, OpusDecoderFlags.None)); - - // Close transfer memory immediately as we don't use it. - context.Device.System.KernelContext.Syscall.CloseHandle(context.Request.HandleDesc.ToCopy[0]); - - return ResultCode.Success; - } - - [CommandCmif(3)] // 3.0.0+ - // GetWorkBufferSizeForMultiStream(buffer, 0x19>) -> u32 - public ResultCode GetWorkBufferSizeForMultiStream(ServiceCtx context) - { - ulong parametersAddress = context.Request.PtrBuff[0].Position; - - OpusMultiStreamParameters parameters = context.Memory.Read(parametersAddress); - - int opusDecoderSize = GetOpusMultistreamDecoderSize(parameters.NumberOfStreams, parameters.NumberOfStereoStreams); - - int streamSize = BitUtils.AlignUp(parameters.NumberOfStreams * 1500, 64); - int frameSize = BitUtils.AlignUp(parameters.ChannelsCount * 1920 / (48000 / parameters.SampleRate), 64); - int totalSize = opusDecoderSize + streamSize + frameSize; - - context.ResponseData.Write(totalSize); - - return ResultCode.Success; - } - - [CommandCmif(4)] // 12.0.0+ - // InitializeEx(OpusParametersEx, u32, handle) -> object - public ResultCode InitializeEx(ServiceCtx context) - { - OpusParametersEx parameters = context.RequestData.ReadStruct(); - - // UseLargeFrameSize can be ignored due to not relying on fixed size buffers for storing the decoded result. - MakeObject(context, new IHardwareOpusDecoder(parameters.SampleRate, parameters.ChannelsCount, parameters.Flags)); - - // Close transfer memory immediately as we don't use it. - context.Device.System.KernelContext.Syscall.CloseHandle(context.Request.HandleDesc.ToCopy[0]); - - return ResultCode.Success; - } - - [CommandCmif(5)] // 12.0.0+ - // GetWorkBufferSizeEx(OpusParametersEx) -> u32 - public ResultCode GetWorkBufferSizeEx(ServiceCtx context) - { - OpusParametersEx parameters = context.RequestData.ReadStruct(); - - int opusDecoderSize = GetOpusDecoderSize(parameters.ChannelsCount); - - int frameSizeMono48KHz = parameters.Flags.HasFlag(OpusDecoderFlags.LargeFrameSize) ? 5760 : 1920; - int frameSize = BitUtils.AlignUp(parameters.ChannelsCount * frameSizeMono48KHz / (48000 / parameters.SampleRate), 64); - int totalSize = opusDecoderSize + 1536 + frameSize; - - context.ResponseData.Write(totalSize); - - return ResultCode.Success; - } - - [CommandCmif(6)] // 12.0.0+ - // InitializeForMultiStreamEx(u32, handle, buffer, 0x19>) -> object - public ResultCode InitializeForMultiStreamEx(ServiceCtx context) - { - ulong parametersAddress = context.Request.PtrBuff[0].Position; - - OpusMultiStreamParametersEx parameters = context.Memory.Read(parametersAddress); - - byte[] mappings = MemoryMarshal.Cast(parameters.ChannelMappings.AsSpan()).ToArray(); - - // UseLargeFrameSize can be ignored due to not relying on fixed size buffers for storing the decoded result. - MakeObject(context, new IHardwareOpusDecoder( - parameters.SampleRate, - parameters.ChannelsCount, - parameters.NumberOfStreams, - parameters.NumberOfStereoStreams, - parameters.Flags, - mappings)); - - // Close transfer memory immediately as we don't use it. - context.Device.System.KernelContext.Syscall.CloseHandle(context.Request.HandleDesc.ToCopy[0]); - - return ResultCode.Success; - } - - [CommandCmif(7)] // 12.0.0+ - // GetWorkBufferSizeForMultiStreamEx(buffer, 0x19>) -> u32 - public ResultCode GetWorkBufferSizeForMultiStreamEx(ServiceCtx context) - { - ulong parametersAddress = context.Request.PtrBuff[0].Position; - - OpusMultiStreamParametersEx parameters = context.Memory.Read(parametersAddress); - - int opusDecoderSize = GetOpusMultistreamDecoderSize(parameters.NumberOfStreams, parameters.NumberOfStereoStreams); - - int frameSizeMono48KHz = parameters.Flags.HasFlag(OpusDecoderFlags.LargeFrameSize) ? 5760 : 1920; - int streamSize = BitUtils.AlignUp(parameters.NumberOfStreams * 1500, 64); - int frameSize = BitUtils.AlignUp(parameters.ChannelsCount * frameSizeMono48KHz / (48000 / parameters.SampleRate), 64); - int totalSize = opusDecoderSize + streamSize + frameSize; - - context.ResponseData.Write(totalSize); - - return ResultCode.Success; - } - - [CommandCmif(8)] // 16.0.0+ - // GetWorkBufferSizeExEx(OpusParametersEx) -> u32 - public ResultCode GetWorkBufferSizeExEx(ServiceCtx context) - { - // NOTE: GetWorkBufferSizeEx use hardcoded values to compute the returned size. - // GetWorkBufferSizeExEx fixes that by using dynamic values. - // Since we're already doing that, it's fine to call it directly. - - return GetWorkBufferSizeEx(context); - } - - [CommandCmif(9)] // 16.0.0+ - // GetWorkBufferSizeForMultiStreamExEx(buffer, 0x19>) -> u32 - public ResultCode GetWorkBufferSizeForMultiStreamExEx(ServiceCtx context) - { - // NOTE: GetWorkBufferSizeForMultiStreamEx use hardcoded values to compute the returned size. - // GetWorkBufferSizeForMultiStreamExEx fixes that by using dynamic values. - // Since we're already doing that, it's fine to call it directly. - - return GetWorkBufferSizeForMultiStreamEx(context); - } - - private static int GetOpusMultistreamDecoderSize(int streams, int coupledStreams) - { - if (streams < 1 || coupledStreams > streams || coupledStreams < 0) - { - return 0; - } - - int coupledSize = GetOpusDecoderSize(2); - int monoSize = GetOpusDecoderSize(1); - - return Align4(monoSize - GetOpusDecoderAllocSize(1)) * (streams - coupledStreams) + - Align4(coupledSize - GetOpusDecoderAllocSize(2)) * coupledStreams + 0xb90c; - } - - private static int Align4(int value) - { - return BitUtils.AlignUp(value, 4); - } - - private static int GetOpusDecoderSize(int channelsCount) - { - const int SilkDecoderSize = 0x2160; - - if (channelsCount < 1 || channelsCount > 2) - { - return 0; - } - - int celtDecoderSize = GetCeltDecoderSize(channelsCount); - int opusDecoderSize = GetOpusDecoderAllocSize(channelsCount) | 0x4c; - - return opusDecoderSize + SilkDecoderSize + celtDecoderSize; - } - - private static int GetOpusDecoderAllocSize(int channelsCount) - { - return (channelsCount * 0x800 + 0x4803) & -0x800; - } - - private static int GetCeltDecoderSize(int channelsCount) - { - const int DecodeBufferSize = 0x2030; - const int Overlap = 120; - const int EBandsCount = 21; - - return (DecodeBufferSize + Overlap * 4) * channelsCount + EBandsCount * 16 + 0x50; - } - } -} diff --git a/src/Ryujinx.HLE/HOS/Services/Audio/ResultCode.cs b/src/Ryujinx.HLE/HOS/Services/Audio/ResultCode.cs deleted file mode 100644 index c1d49109c..000000000 --- a/src/Ryujinx.HLE/HOS/Services/Audio/ResultCode.cs +++ /dev/null @@ -1,21 +0,0 @@ -namespace Ryujinx.HLE.HOS.Services.Audio -{ - enum ResultCode - { - ModuleId = 153, - ErrorCodeShift = 9, - - Success = 0, - - DeviceNotFound = (1 << ErrorCodeShift) | ModuleId, - UnsupportedRevision = (2 << ErrorCodeShift) | ModuleId, - UnsupportedSampleRate = (3 << ErrorCodeShift) | ModuleId, - BufferSizeTooSmall = (4 << ErrorCodeShift) | ModuleId, - OpusInvalidInput = (6 << ErrorCodeShift) | ModuleId, - TooManyBuffersInUse = (8 << ErrorCodeShift) | ModuleId, - InvalidChannelCount = (10 << ErrorCodeShift) | ModuleId, - InvalidOperation = (513 << ErrorCodeShift) | ModuleId, - InvalidHandle = (1536 << ErrorCodeShift) | ModuleId, - OutputAlreadyStarted = (1540 << ErrorCodeShift) | ModuleId, - } -} diff --git a/src/Ryujinx.HLE/HOS/Services/Audio/Types/OpusPacketHeader.cs b/src/Ryujinx.HLE/HOS/Services/Audio/Types/OpusPacketHeader.cs deleted file mode 100644 index 099769b3a..000000000 --- a/src/Ryujinx.HLE/HOS/Services/Audio/Types/OpusPacketHeader.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System; -using System.Buffers.Binary; -using System.Runtime.InteropServices; - -namespace Ryujinx.HLE.HOS.Services.Audio.Types -{ - [StructLayout(LayoutKind.Sequential)] - struct OpusPacketHeader - { - public uint length; - public uint finalRange; - - public static OpusPacketHeader FromSpan(ReadOnlySpan data) - { - OpusPacketHeader header = MemoryMarshal.Cast(data)[0]; - - header.length = BitConverter.IsLittleEndian ? BinaryPrimitives.ReverseEndianness(header.length) : header.length; - header.finalRange = BitConverter.IsLittleEndian ? BinaryPrimitives.ReverseEndianness(header.finalRange) : header.finalRange; - - return header; - } - } -} diff --git a/src/Ryujinx.HLE/HOS/Services/Audio/Types/OpusParametersEx.cs b/src/Ryujinx.HLE/HOS/Services/Audio/Types/OpusParametersEx.cs deleted file mode 100644 index 4d1e0c824..000000000 --- a/src/Ryujinx.HLE/HOS/Services/Audio/Types/OpusParametersEx.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Ryujinx.Common.Memory; -using System.Runtime.InteropServices; - -namespace Ryujinx.HLE.HOS.Services.Audio.Types -{ - [StructLayout(LayoutKind.Sequential, Size = 0x10)] - struct OpusParametersEx - { - public int SampleRate; - public int ChannelsCount; - public OpusDecoderFlags Flags; - - Array4 Padding1; - } -} diff --git a/src/Ryujinx.HLE/HOS/Services/Caps/CaptureManager.cs b/src/Ryujinx.HLE/HOS/Services/Caps/CaptureManager.cs index 91a8958e6..bf0c7e9dc 100644 --- a/src/Ryujinx.HLE/HOS/Services/Caps/CaptureManager.cs +++ b/src/Ryujinx.HLE/HOS/Services/Caps/CaptureManager.cs @@ -1,10 +1,10 @@ using Ryujinx.Common.Memory; using Ryujinx.HLE.HOS.Services.Caps.Types; -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.PixelFormats; +using SkiaSharp; using System; using System.IO; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using System.Security.Cryptography; namespace Ryujinx.HLE.HOS.Services.Caps @@ -118,7 +118,11 @@ namespace Ryujinx.HLE.HOS.Services.Caps } // NOTE: The saved JPEG file doesn't have the limitation in the extra EXIF data. - Image.LoadPixelData(screenshotData, 1280, 720).SaveAsJpegAsync(filePath); + using var bitmap = new SKBitmap(new SKImageInfo(1280, 720, SKColorType.Rgba8888)); + Marshal.Copy(screenshotData, 0, bitmap.GetPixels(), screenshotData.Length); + using var data = bitmap.Encode(SKEncodedImageFormat.Jpeg, 80); + using var file = File.OpenWrite(filePath); + data.SaveTo(file); return ResultCode.Success; } diff --git a/src/Ryujinx.HLE/HOS/Services/Fatal/IService.cs b/src/Ryujinx.HLE/HOS/Services/Fatal/IService.cs index 21daf8758..155077745 100644 --- a/src/Ryujinx.HLE/HOS/Services/Fatal/IService.cs +++ b/src/Ryujinx.HLE/HOS/Services/Fatal/IService.cs @@ -60,7 +60,7 @@ namespace Ryujinx.HLE.HOS.Services.Fatal errorReport.AppendLine($"\tResultCode: {((int)resultCode & 0x1FF) + 2000}-{((int)resultCode >> 9) & 0x3FFF:d4}"); errorReport.AppendLine($"\tFatalPolicy: {fatalPolicy}"); - if (cpuContext != null) + if (!cpuContext.IsEmpty) { errorReport.AppendLine("CPU Context:"); diff --git a/src/Ryujinx.HLE/HOS/Services/Friend/IServiceCreator.cs b/src/Ryujinx.HLE/HOS/Services/Friend/IServiceCreator.cs deleted file mode 100644 index 3f15f3fc6..000000000 --- a/src/Ryujinx.HLE/HOS/Services/Friend/IServiceCreator.cs +++ /dev/null @@ -1,55 +0,0 @@ -using Ryujinx.Common; -using Ryujinx.HLE.HOS.Services.Account.Acc; -using Ryujinx.HLE.HOS.Services.Friend.ServiceCreator; - -namespace Ryujinx.HLE.HOS.Services.Friend -{ - [Service("friend:a", FriendServicePermissionLevel.Administrator)] - [Service("friend:m", FriendServicePermissionLevel.Manager)] - [Service("friend:s", FriendServicePermissionLevel.System)] - [Service("friend:u", FriendServicePermissionLevel.User)] - [Service("friend:v", FriendServicePermissionLevel.Viewer)] - class IServiceCreator : IpcService - { - private readonly FriendServicePermissionLevel _permissionLevel; - - public IServiceCreator(ServiceCtx context, FriendServicePermissionLevel permissionLevel) - { - _permissionLevel = permissionLevel; - } - - [CommandCmif(0)] - // CreateFriendService() -> object - public ResultCode CreateFriendService(ServiceCtx context) - { - MakeObject(context, new IFriendService(_permissionLevel)); - - return ResultCode.Success; - } - - [CommandCmif(1)] // 2.0.0+ - // CreateNotificationService(nn::account::Uid userId) -> object - public ResultCode CreateNotificationService(ServiceCtx context) - { - UserId userId = context.RequestData.ReadStruct(); - - if (userId.IsNull) - { - return ResultCode.InvalidArgument; - } - - MakeObject(context, new INotificationService(context, userId, _permissionLevel)); - - return ResultCode.Success; - } - - [CommandCmif(2)] // 4.0.0+ - // CreateDaemonSuspendSessionService() -> object - public ResultCode CreateDaemonSuspendSessionService(ServiceCtx context) - { - MakeObject(context, new IDaemonSuspendSessionService(_permissionLevel)); - - return ResultCode.Success; - } - } -} diff --git a/src/Ryujinx.HLE/HOS/Services/Friend/ResultCode.cs b/src/Ryujinx.HLE/HOS/Services/Friend/ResultCode.cs deleted file mode 100644 index 9f612059c..000000000 --- a/src/Ryujinx.HLE/HOS/Services/Friend/ResultCode.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Ryujinx.HLE.HOS.Services.Friend -{ - enum ResultCode - { - ModuleId = 121, - ErrorCodeShift = 9, - - Success = 0, - - InvalidArgument = (2 << ErrorCodeShift) | ModuleId, - InternetRequestDenied = (6 << ErrorCodeShift) | ModuleId, - NotificationQueueEmpty = (15 << ErrorCodeShift) | ModuleId, - } -} diff --git a/src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/FriendService/Types/Friend.cs b/src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/FriendService/Types/Friend.cs deleted file mode 100644 index 28745c3f2..000000000 --- a/src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/FriendService/Types/Friend.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Ryujinx.HLE.HOS.Services.Account.Acc; -using System.Runtime.InteropServices; - -namespace Ryujinx.HLE.HOS.Services.Friend.ServiceCreator.FriendService -{ - [StructLayout(LayoutKind.Sequential, Pack = 0x8, Size = 0x200, CharSet = CharSet.Ansi)] - struct Friend - { - public UserId UserId; - public long NetworkUserId; - - [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 0x21)] - public string Nickname; - - public UserPresence presence; - - [MarshalAs(UnmanagedType.I1)] - public bool IsFavourite; - - [MarshalAs(UnmanagedType.I1)] - public bool IsNew; - - [MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x6)] - readonly char[] Unknown; - - [MarshalAs(UnmanagedType.I1)] - public bool IsValid; - } -} diff --git a/src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/FriendService/Types/FriendFilter.cs b/src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/FriendService/Types/FriendFilter.cs deleted file mode 100644 index 5f13f3136..000000000 --- a/src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/FriendService/Types/FriendFilter.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System.Runtime.InteropServices; - -namespace Ryujinx.HLE.HOS.Services.Friend.ServiceCreator.FriendService -{ - [StructLayout(LayoutKind.Sequential)] - struct FriendFilter - { - public PresenceStatusFilter PresenceStatus; - - [MarshalAs(UnmanagedType.I1)] - public bool IsFavoriteOnly; - - [MarshalAs(UnmanagedType.I1)] - public bool IsSameAppPresenceOnly; - - [MarshalAs(UnmanagedType.I1)] - public bool IsSameAppPlayedOnly; - - [MarshalAs(UnmanagedType.I1)] - public bool IsArbitraryAppPlayedOnly; - - public long PresenceGroupId; - } -} diff --git a/src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/FriendService/Types/UserPresence.cs b/src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/FriendService/Types/UserPresence.cs deleted file mode 100644 index 80d142059..000000000 --- a/src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/FriendService/Types/UserPresence.cs +++ /dev/null @@ -1,34 +0,0 @@ -using Ryujinx.Common.Memory; -using Ryujinx.HLE.HOS.Services.Account.Acc; -using System; -using System.Runtime.InteropServices; - -namespace Ryujinx.HLE.HOS.Services.Friend.ServiceCreator.FriendService -{ - [StructLayout(LayoutKind.Sequential, Pack = 0x8)] - struct UserPresence - { - public UserId UserId; - public long LastTimeOnlineTimestamp; - public PresenceStatus Status; - - [MarshalAs(UnmanagedType.I1)] - public bool SamePresenceGroupApplication; - - public Array3 Unknown; - private AppKeyValueStorageHolder _appKeyValueStorage; - - public Span AppKeyValueStorage => MemoryMarshal.Cast(MemoryMarshal.CreateSpan(ref _appKeyValueStorage, AppKeyValueStorageHolder.Size)); - - [StructLayout(LayoutKind.Sequential, Pack = 0x1, Size = Size)] - private struct AppKeyValueStorageHolder - { - public const int Size = 0xC0; - } - - public readonly override string ToString() - { - return $"UserPresence {{ UserId: {UserId}, LastTimeOnlineTimestamp: {LastTimeOnlineTimestamp}, Status: {Status} }}"; - } - } -} diff --git a/src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/IDaemonSuspendSessionService.cs b/src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/IDaemonSuspendSessionService.cs deleted file mode 100644 index 3b1601abb..000000000 --- a/src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/IDaemonSuspendSessionService.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Ryujinx.HLE.HOS.Services.Friend.ServiceCreator -{ - class IDaemonSuspendSessionService : IpcService - { -#pragma warning disable IDE0052 // Remove unread private member - private readonly FriendServicePermissionLevel _permissionLevel; -#pragma warning restore IDE0052 - - public IDaemonSuspendSessionService(FriendServicePermissionLevel permissionLevel) - { - _permissionLevel = permissionLevel; - } - } -} diff --git a/src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/IFriendService.cs b/src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/IFriendService.cs deleted file mode 100644 index 54d23e88c..000000000 --- a/src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/IFriendService.cs +++ /dev/null @@ -1,374 +0,0 @@ -using LibHac.Ns; -using Ryujinx.Common; -using Ryujinx.Common.Logging; -using Ryujinx.Common.Memory; -using Ryujinx.Common.Utilities; -using Ryujinx.HLE.HOS.Ipc; -using Ryujinx.HLE.HOS.Kernel.Threading; -using Ryujinx.HLE.HOS.Services.Account.Acc; -using Ryujinx.HLE.HOS.Services.Friend.ServiceCreator.FriendService; -using Ryujinx.Horizon.Common; -using System; -using System.Runtime.InteropServices; - -namespace Ryujinx.HLE.HOS.Services.Friend.ServiceCreator -{ - class IFriendService : IpcService - { -#pragma warning disable IDE0052 // Remove unread private member - private readonly FriendServicePermissionLevel _permissionLevel; -#pragma warning restore IDE0052 - private KEvent _completionEvent; - - public IFriendService(FriendServicePermissionLevel permissionLevel) - { - _permissionLevel = permissionLevel; - } - - [CommandCmif(0)] - // GetCompletionEvent() -> handle - public ResultCode GetCompletionEvent(ServiceCtx context) - { - _completionEvent ??= new KEvent(context.Device.System.KernelContext); - - if (context.Process.HandleTable.GenerateHandle(_completionEvent.ReadableEvent, out int completionEventHandle) != Result.Success) - { - throw new InvalidOperationException("Out of handles!"); - } - - _completionEvent.WritableEvent.Signal(); - - context.Response.HandleDesc = IpcHandleDesc.MakeCopy(completionEventHandle); - - return ResultCode.Success; - } - - [CommandCmif(1)] - // nn::friends::Cancel() - public ResultCode Cancel(ServiceCtx context) - { - // TODO: Original service sets an internal field to 1 here. Determine usage. - Logger.Stub?.PrintStub(LogClass.ServiceFriend); - - return ResultCode.Success; - } - - [CommandCmif(10100)] - // nn::friends::GetFriendListIds(int offset, nn::account::Uid userId, nn::friends::detail::ipc::SizedFriendFilter friendFilter, ulong pidPlaceHolder, pid) - // -> int outCount, array - public ResultCode GetFriendListIds(ServiceCtx context) - { - int offset = context.RequestData.ReadInt32(); - - // Padding - context.RequestData.ReadInt32(); - - UserId userId = context.RequestData.ReadStruct(); - FriendFilter filter = context.RequestData.ReadStruct(); - - // Pid placeholder - context.RequestData.ReadInt64(); - - if (userId.IsNull) - { - return ResultCode.InvalidArgument; - } - - // There are no friends online, so we return 0 because the nn::account::NetworkServiceAccountId array is empty. - context.ResponseData.Write(0); - - Logger.Stub?.PrintStub(LogClass.ServiceFriend, new - { - UserId = userId.ToString(), - offset, - filter.PresenceStatus, - filter.IsFavoriteOnly, - filter.IsSameAppPresenceOnly, - filter.IsSameAppPlayedOnly, - filter.IsArbitraryAppPlayedOnly, - filter.PresenceGroupId, - }); - - return ResultCode.Success; - } - - [CommandCmif(10101)] - // nn::friends::GetFriendList(int offset, nn::account::Uid userId, nn::friends::detail::ipc::SizedFriendFilter friendFilter, ulong pidPlaceHolder, pid) - // -> int outCount, array - public ResultCode GetFriendList(ServiceCtx context) - { - int offset = context.RequestData.ReadInt32(); - - // Padding - context.RequestData.ReadInt32(); - - UserId userId = context.RequestData.ReadStruct(); - FriendFilter filter = context.RequestData.ReadStruct(); - - // Pid placeholder - context.RequestData.ReadInt64(); - - if (userId.IsNull) - { - return ResultCode.InvalidArgument; - } - - // There are no friends online, so we return 0 because the nn::account::NetworkServiceAccountId array is empty. - context.ResponseData.Write(0); - - Logger.Stub?.PrintStub(LogClass.ServiceFriend, new - { - UserId = userId.ToString(), - offset, - filter.PresenceStatus, - filter.IsFavoriteOnly, - filter.IsSameAppPresenceOnly, - filter.IsSameAppPlayedOnly, - filter.IsArbitraryAppPlayedOnly, - filter.PresenceGroupId, - }); - - return ResultCode.Success; - } - - [CommandCmif(10120)] // 10.0.0+ - // nn::friends::IsFriendListCacheAvailable(nn::account::Uid userId) -> bool - public ResultCode IsFriendListCacheAvailable(ServiceCtx context) - { - UserId userId = context.RequestData.ReadStruct(); - - if (userId.IsNull) - { - return ResultCode.InvalidArgument; - } - - // TODO: Service mount the friends:/ system savedata and try to load friend.cache file, returns true if exists, false otherwise. - // NOTE: If no cache is available, guest then calls nn::friends::EnsureFriendListAvailable, we can avoid that by faking the cache check. - context.ResponseData.Write(true); - - // TODO: Since we don't support friend features, it's fine to stub it for now. - Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { UserId = userId.ToString() }); - - return ResultCode.Success; - } - - [CommandCmif(10121)] // 10.0.0+ - // nn::friends::EnsureFriendListAvailable(nn::account::Uid userId) - public ResultCode EnsureFriendListAvailable(ServiceCtx context) - { - UserId userId = context.RequestData.ReadStruct(); - - if (userId.IsNull) - { - return ResultCode.InvalidArgument; - } - - // TODO: Service mount the friends:/ system savedata and create a friend.cache file for the given user id. - // Since we don't support friend features, it's fine to stub it for now. - Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { UserId = userId.ToString() }); - - return ResultCode.Success; - } - - [CommandCmif(10400)] - // nn::friends::GetBlockedUserListIds(int offset, nn::account::Uid userId) -> (u32, buffer) - public ResultCode GetBlockedUserListIds(ServiceCtx context) - { - int offset = context.RequestData.ReadInt32(); - - // Padding - context.RequestData.ReadInt32(); - - UserId userId = context.RequestData.ReadStruct(); - - // There are no friends blocked, so we return 0 because the nn::account::NetworkServiceAccountId array is empty. - context.ResponseData.Write(0); - - Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { offset, UserId = userId.ToString() }); - - return ResultCode.Success; - } - - [CommandCmif(10420)] - // nn::friends::CheckBlockedUserListAvailability(nn::account::Uid userId) -> bool - public ResultCode CheckBlockedUserListAvailability(ServiceCtx context) - { - UserId userId = context.RequestData.ReadStruct(); - - // Yes, it is available. - context.ResponseData.Write(true); - - Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { UserId = userId.ToString() }); - - return ResultCode.Success; - } - - [CommandCmif(10600)] - // nn::friends::DeclareOpenOnlinePlaySession(nn::account::Uid userId) - public ResultCode DeclareOpenOnlinePlaySession(ServiceCtx context) - { - UserId userId = context.RequestData.ReadStruct(); - - if (userId.IsNull) - { - return ResultCode.InvalidArgument; - } - - context.Device.System.AccountManager.OpenUserOnlinePlay(userId); - - Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { UserId = userId.ToString() }); - - return ResultCode.Success; - } - - [CommandCmif(10601)] - // nn::friends::DeclareCloseOnlinePlaySession(nn::account::Uid userId) - public ResultCode DeclareCloseOnlinePlaySession(ServiceCtx context) - { - UserId userId = context.RequestData.ReadStruct(); - - if (userId.IsNull) - { - return ResultCode.InvalidArgument; - } - - context.Device.System.AccountManager.CloseUserOnlinePlay(userId); - - Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { UserId = userId.ToString() }); - - return ResultCode.Success; - } - - [CommandCmif(10610)] - // nn::friends::UpdateUserPresence(nn::account::Uid, u64, pid, buffer) - public ResultCode UpdateUserPresence(ServiceCtx context) - { - UserId uuid = context.RequestData.ReadStruct(); - - // Pid placeholder - context.RequestData.ReadInt64(); - - ulong position = context.Request.PtrBuff[0].Position; - ulong size = context.Request.PtrBuff[0].Size; - - ReadOnlySpan userPresenceInputArray = MemoryMarshal.Cast(context.Memory.GetSpan(position, (int)size)); - - if (uuid.IsNull) - { - return ResultCode.InvalidArgument; - } - - Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { UserId = uuid.ToString(), userPresenceInputArray = userPresenceInputArray.ToArray() }); - - return ResultCode.Success; - } - - [CommandCmif(10700)] - // nn::friends::GetPlayHistoryRegistrationKey(b8 unknown, nn::account::Uid) -> buffer - public ResultCode GetPlayHistoryRegistrationKey(ServiceCtx context) - { - bool unknownBool = context.RequestData.ReadBoolean(); - UserId userId = context.RequestData.ReadStruct(); - - context.Response.PtrBuff[0] = context.Response.PtrBuff[0].WithSize(0x40UL); - - ulong bufferPosition = context.Request.RecvListBuff[0].Position; - - if (userId.IsNull) - { - return ResultCode.InvalidArgument; - } - - // NOTE: Calls nn::friends::detail::service::core::PlayHistoryManager::GetInstance and stores the instance. - - byte[] randomBytes = new byte[8]; - - Random.Shared.NextBytes(randomBytes); - - // NOTE: Calls nn::friends::detail::service::core::UuidManager::GetInstance and stores the instance. - // Then call nn::friends::detail::service::core::AccountStorageManager::GetInstance and store the instance. - // Then it checks if an Uuid is already stored for the UserId, if not it generates a random Uuid. - // And store it in the savedata 8000000000000080 in the friends:/uid.bin file. - - Array16 randomGuid = new(); - - Guid.NewGuid().ToByteArray().AsSpan().CopyTo(randomGuid.AsSpan()); - - PlayHistoryRegistrationKey playHistoryRegistrationKey = new() - { - Type = 0x101, - KeyIndex = (byte)(randomBytes[0] & 7), - UserIdBool = 0, // TODO: Find it. - UnknownBool = (byte)(unknownBool ? 1 : 0), // TODO: Find it. - Reserved = new Array11(), - Uuid = randomGuid, - }; - - ReadOnlySpan playHistoryRegistrationKeyBuffer = SpanHelpers.AsByteSpan(ref playHistoryRegistrationKey); - - /* - - NOTE: The service uses the KeyIndex to get a random key from a keys buffer (since the key index is stored in the returned buffer). - We currently don't support play history and online services so we can use a blank key for now. - Code for reference: - - byte[] hmacKey = new byte[0x20]; - - HMACSHA256 hmacSha256 = new HMACSHA256(hmacKey); - byte[] hmacHash = hmacSha256.ComputeHash(playHistoryRegistrationKeyBuffer); - - */ - - context.Memory.Write(bufferPosition, playHistoryRegistrationKeyBuffer); - context.Memory.Write(bufferPosition + 0x20, new byte[0x20]); // HmacHash - - return ResultCode.Success; - } - - [CommandCmif(10702)] - // nn::friends::AddPlayHistory(nn::account::Uid, u64, pid, buffer, buffer, buffer) - public ResultCode AddPlayHistory(ServiceCtx context) - { - UserId userId = context.RequestData.ReadStruct(); - - // Pid placeholder - context.RequestData.ReadInt64(); -#pragma warning disable IDE0059 // Remove unnecessary value assignment - ulong pid = context.Request.HandleDesc.PId; - - ulong playHistoryRegistrationKeyPosition = context.Request.PtrBuff[0].Position; - ulong playHistoryRegistrationKeySize = context.Request.PtrBuff[0].Size; - - ulong inAppScreenName1Position = context.Request.PtrBuff[1].Position; -#pragma warning restore IDE0059 - ulong inAppScreenName1Size = context.Request.PtrBuff[1].Size; - -#pragma warning disable IDE0059 // Remove unnecessary value assignment - ulong inAppScreenName2Position = context.Request.PtrBuff[2].Position; -#pragma warning restore IDE0059 - ulong inAppScreenName2Size = context.Request.PtrBuff[2].Size; - - if (userId.IsNull || inAppScreenName1Size > 0x48 || inAppScreenName2Size > 0x48) - { - return ResultCode.InvalidArgument; - } - - // TODO: Call nn::arp::GetApplicationControlProperty here when implemented. -#pragma warning disable IDE0059 // Remove unnecessary value assignment - ApplicationControlProperty controlProperty = context.Device.Processes.ActiveApplication.ApplicationControlProperties; -#pragma warning restore IDE0059 - - /* - - NOTE: The service calls nn::friends::detail::service::core::PlayHistoryManager to store informations using the registration key computed in GetPlayHistoryRegistrationKey. - Then calls nn::friends::detail::service::core::FriendListManager to update informations on the friend list. - We currently don't support play history and online services so it's fine to do nothing. - - */ - - Logger.Stub?.PrintStub(LogClass.ServiceFriend); - - return ResultCode.Success; - } - } -} diff --git a/src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/INotificationService.cs b/src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/INotificationService.cs deleted file mode 100644 index 8fc7a4609..000000000 --- a/src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/INotificationService.cs +++ /dev/null @@ -1,178 +0,0 @@ -using Ryujinx.Common; -using Ryujinx.HLE.HOS.Ipc; -using Ryujinx.HLE.HOS.Kernel.Threading; -using Ryujinx.HLE.HOS.Services.Account.Acc; -using Ryujinx.HLE.HOS.Services.Friend.ServiceCreator.NotificationService; -using Ryujinx.Horizon.Common; -using System; -using System.Collections.Generic; - -namespace Ryujinx.HLE.HOS.Services.Friend.ServiceCreator -{ - class INotificationService : DisposableIpcService - { - private readonly UserId _userId; - private readonly FriendServicePermissionLevel _permissionLevel; - - private readonly object _lock = new(); - - private readonly KEvent _notificationEvent; - private int _notificationEventHandle = 0; - - private readonly LinkedList _notifications; - - private bool _hasNewFriendRequest; - private bool _hasFriendListUpdate; - - public INotificationService(ServiceCtx context, UserId userId, FriendServicePermissionLevel permissionLevel) - { - _userId = userId; - _permissionLevel = permissionLevel; - _notifications = new LinkedList(); - _notificationEvent = new KEvent(context.Device.System.KernelContext); - - _hasNewFriendRequest = false; - _hasFriendListUpdate = false; - - NotificationEventHandler.Instance.RegisterNotificationService(this); - } - - [CommandCmif(0)] //2.0.0+ - // nn::friends::detail::ipc::INotificationService::GetEvent() -> handle - public ResultCode GetEvent(ServiceCtx context) - { - if (_notificationEventHandle == 0) - { - if (context.Process.HandleTable.GenerateHandle(_notificationEvent.ReadableEvent, out _notificationEventHandle) != Result.Success) - { - throw new InvalidOperationException("Out of handles!"); - } - } - - context.Response.HandleDesc = IpcHandleDesc.MakeCopy(_notificationEventHandle); - - return ResultCode.Success; - } - - [CommandCmif(1)] //2.0.0+ - // nn::friends::detail::ipc::INotificationService::Clear() - public ResultCode Clear(ServiceCtx context) - { - lock (_lock) - { - _hasNewFriendRequest = false; - _hasFriendListUpdate = false; - - _notifications.Clear(); - } - - return ResultCode.Success; - } - - [CommandCmif(2)] // 2.0.0+ - // nn::friends::detail::ipc::INotificationService::Pop() -> nn::friends::detail::ipc::SizedNotificationInfo - public ResultCode Pop(ServiceCtx context) - { - lock (_lock) - { - if (_notifications.Count >= 1) - { - NotificationInfo notificationInfo = _notifications.First.Value; - _notifications.RemoveFirst(); - - if (notificationInfo.Type == NotificationEventType.FriendListUpdate) - { - _hasFriendListUpdate = false; - } - else if (notificationInfo.Type == NotificationEventType.NewFriendRequest) - { - _hasNewFriendRequest = false; - } - - context.ResponseData.WriteStruct(notificationInfo); - - return ResultCode.Success; - } - } - - return ResultCode.NotificationQueueEmpty; - } - - public void SignalFriendListUpdate(UserId targetId) - { - lock (_lock) - { - if (_userId == targetId) - { - if (!_hasFriendListUpdate) - { - NotificationInfo friendListNotification = new(); - - if (_notifications.Count != 0) - { - friendListNotification = _notifications.First.Value; - _notifications.RemoveFirst(); - } - - friendListNotification.Type = NotificationEventType.FriendListUpdate; - _hasFriendListUpdate = true; - - if (_hasNewFriendRequest) - { - NotificationInfo newFriendRequestNotification = new(); - - if (_notifications.Count != 0) - { - newFriendRequestNotification = _notifications.First.Value; - _notifications.RemoveFirst(); - } - - newFriendRequestNotification.Type = NotificationEventType.NewFriendRequest; - _notifications.AddFirst(newFriendRequestNotification); - } - - // We defer this to make sure we are on top of the queue. - _notifications.AddFirst(friendListNotification); - } - - _notificationEvent.ReadableEvent.Signal(); - } - } - } - - public void SignalNewFriendRequest(UserId targetId) - { - lock (_lock) - { - if ((_permissionLevel & FriendServicePermissionLevel.ViewerMask) != 0 && _userId == targetId) - { - if (!_hasNewFriendRequest) - { - if (_notifications.Count == 100) - { - SignalFriendListUpdate(targetId); - } - - NotificationInfo newFriendRequestNotification = new() - { - Type = NotificationEventType.NewFriendRequest, - }; - - _notifications.AddLast(newFriendRequestNotification); - _hasNewFriendRequest = true; - } - - _notificationEvent.ReadableEvent.Signal(); - } - } - } - - protected override void Dispose(bool isDisposing) - { - if (isDisposing) - { - NotificationEventHandler.Instance.UnregisterNotificationService(this); - } - } - } -} diff --git a/src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/NotificationService/NotificationEventHandler.cs b/src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/NotificationService/NotificationEventHandler.cs deleted file mode 100644 index 88627fd78..000000000 --- a/src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/NotificationService/NotificationEventHandler.cs +++ /dev/null @@ -1,74 +0,0 @@ -using Ryujinx.HLE.HOS.Services.Account.Acc; - -namespace Ryujinx.HLE.HOS.Services.Friend.ServiceCreator.NotificationService -{ - public sealed class NotificationEventHandler - { - private static NotificationEventHandler _instance; - private static readonly object _instanceLock = new(); - - private readonly INotificationService[] _registry; - - public static NotificationEventHandler Instance - { - get - { - lock (_instanceLock) - { - _instance ??= new NotificationEventHandler(); - - return _instance; - } - } - } - - NotificationEventHandler() - { - _registry = new INotificationService[0x20]; - } - - internal void RegisterNotificationService(INotificationService service) - { - // NOTE: in case there isn't space anymore in the registry array, Nintendo doesn't return any errors. - for (int i = 0; i < _registry.Length; i++) - { - if (_registry[i] == null) - { - _registry[i] = service; - break; - } - } - } - - internal void UnregisterNotificationService(INotificationService service) - { - // NOTE: in case there isn't the entry in the registry array, Nintendo doesn't return any errors. - for (int i = 0; i < _registry.Length; i++) - { - if (_registry[i] == service) - { - _registry[i] = null; - break; - } - } - } - - // TODO: Use this when we will have enough things to go online. - public void SignalFriendListUpdate(UserId targetId) - { - for (int i = 0; i < _registry.Length; i++) - { - _registry[i]?.SignalFriendListUpdate(targetId); - } - } - - // TODO: Use this when we will have enough things to go online. - public void SignalNewFriendRequest(UserId targetId) - { - for (int i = 0; i < _registry.Length; i++) - { - _registry[i]?.SignalNewFriendRequest(targetId); - } - } - } -} diff --git a/src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/NotificationService/Types/NotificationInfo.cs b/src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/NotificationService/Types/NotificationInfo.cs deleted file mode 100644 index aa58433d8..000000000 --- a/src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/NotificationService/Types/NotificationInfo.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Ryujinx.Common.Memory; -using System.Runtime.InteropServices; - -namespace Ryujinx.HLE.HOS.Services.Friend.ServiceCreator.NotificationService -{ - [StructLayout(LayoutKind.Sequential, Size = 0x10)] - struct NotificationInfo - { - public NotificationEventType Type; - private Array4 _padding; - public long NetworkUserIdPlaceholder; - } -} diff --git a/src/Ryujinx.HLE/HOS/Services/Fs/FileSystemProxy/IFileSystem.cs b/src/Ryujinx.HLE/HOS/Services/Fs/FileSystemProxy/IFileSystem.cs index 66020d57b..e19d17912 100644 --- a/src/Ryujinx.HLE/HOS/Services/Fs/FileSystemProxy/IFileSystem.cs +++ b/src/Ryujinx.HLE/HOS/Services/Fs/FileSystemProxy/IFileSystem.cs @@ -2,6 +2,7 @@ using LibHac; using LibHac.Common; using LibHac.Fs; using LibHac.Fs.Fsa; +using Ryujinx.Common.Logging; using Path = LibHac.FsSrv.Sf.Path; namespace Ryujinx.HLE.HOS.Services.Fs.FileSystemProxy @@ -149,7 +150,13 @@ namespace Ryujinx.HLE.HOS.Services.Fs.FileSystemProxy // Commit() public ResultCode Commit(ServiceCtx context) { - return (ResultCode)_fileSystem.Get.Commit().Value; + ResultCode resultCode = (ResultCode)_fileSystem.Get.Commit().Value; + if (resultCode == ResultCode.PathAlreadyInUse) + { + Logger.Warning?.Print(LogClass.ServiceFs, "The file system is already in use by another process."); + } + + return resultCode; } [CommandCmif(11)] diff --git a/src/Ryujinx.HLE/HOS/Services/Fs/FileSystemProxy/LazyFile.cs b/src/Ryujinx.HLE/HOS/Services/Fs/FileSystemProxy/LazyFile.cs new file mode 100644 index 000000000..a179e8e38 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Fs/FileSystemProxy/LazyFile.cs @@ -0,0 +1,65 @@ +using LibHac; +using LibHac.Common; +using LibHac.Fs; +using System; +using System.IO; + +namespace Ryujinx.HLE.HOS.Services.Fs.FileSystemProxy +{ + class LazyFile : LibHac.Fs.Fsa.IFile + { + private readonly LibHac.Fs.Fsa.IFileSystem _fs; + private readonly string _filePath; + private readonly UniqueRef _fileReference = new(); + private readonly FileInfo _fileInfo; + + public LazyFile(string filePath, string prefix, LibHac.Fs.Fsa.IFileSystem fs) + { + _fs = fs; + _filePath = filePath; + _fileInfo = new FileInfo(prefix + "/" + filePath); + } + + private void PrepareFile() + { + if (_fileReference.Get == null) + { + _fs.OpenFile(ref _fileReference.Ref, _filePath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); + } + } + + protected override Result DoRead(out long bytesRead, long offset, Span destination, in ReadOption option) + { + PrepareFile(); + + return _fileReference.Get!.Read(out bytesRead, offset, destination); + } + + protected override Result DoWrite(long offset, ReadOnlySpan source, in WriteOption option) + { + throw new NotSupportedException(); + } + + protected override Result DoFlush() + { + throw new NotSupportedException(); + } + + protected override Result DoSetSize(long size) + { + throw new NotSupportedException(); + } + + protected override Result DoGetSize(out long size) + { + size = _fileInfo.Length; + + return Result.Success; + } + + protected override Result DoOperateRange(Span outBuffer, OperationId operationId, long offset, long size, ReadOnlySpan inBuffer) + { + throw new NotSupportedException(); + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Hid/Hid.cs b/src/Ryujinx.HLE/HOS/Services/Hid/Hid.cs index dbcbe1870..66b5a5cba 100644 --- a/src/Ryujinx.HLE/HOS/Services/Hid/Hid.cs +++ b/src/Ryujinx.HLE/HOS/Services/Hid/Hid.cs @@ -5,6 +5,7 @@ using Ryujinx.HLE.Exceptions; using Ryujinx.HLE.HOS.Kernel.Memory; using Ryujinx.HLE.HOS.Services.Hid.Types.SharedMemory; using Ryujinx.HLE.HOS.Services.Hid.Types.SharedMemory.Common; +using Ryujinx.HLE.HOS.Services.Hid.Types.SharedMemory.DebugMouse; using Ryujinx.HLE.HOS.Services.Hid.Types.SharedMemory.DebugPad; using Ryujinx.HLE.HOS.Services.Hid.Types.SharedMemory.Keyboard; using Ryujinx.HLE.HOS.Services.Hid.Types.SharedMemory.Mouse; @@ -28,6 +29,7 @@ namespace Ryujinx.HLE.HOS.Services.Hid public DebugPadDevice DebugPad; public TouchDevice Touchscreen; public MouseDevice Mouse; + public DebugMouseDevice DebugMouse; public KeyboardDevice Keyboard; public NpadDevices Npads; @@ -44,6 +46,7 @@ namespace Ryujinx.HLE.HOS.Services.Hid CheckTypeSizeOrThrow>(0x2c8); CheckTypeSizeOrThrow>(0x2C38); CheckTypeSizeOrThrow>(0x350); + CheckTypeSizeOrThrow>(0x350); CheckTypeSizeOrThrow>(0x3D8); CheckTypeSizeOrThrow>(0x32000); CheckTypeSizeOrThrow(Horizon.HidSize); @@ -64,6 +67,7 @@ namespace Ryujinx.HLE.HOS.Services.Hid DebugPad = new DebugPadDevice(_device, true); Touchscreen = new TouchDevice(_device, true); Mouse = new MouseDevice(_device, false); + DebugMouse = new DebugMouseDevice(_device, false); Keyboard = new KeyboardDevice(_device, false); Npads = new NpadDevices(_device, true); } diff --git a/src/Ryujinx.HLE/HOS/Services/Hid/HidDevices/DebugMouseDevice.cs b/src/Ryujinx.HLE/HOS/Services/Hid/HidDevices/DebugMouseDevice.cs new file mode 100644 index 000000000..cb917444b --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Hid/HidDevices/DebugMouseDevice.cs @@ -0,0 +1,29 @@ +using Ryujinx.HLE.HOS.Services.Hid.Types.SharedMemory.Common; +using Ryujinx.HLE.HOS.Services.Hid.Types.SharedMemory.DebugMouse; + +namespace Ryujinx.HLE.HOS.Services.Hid +{ + public class DebugMouseDevice : BaseDevice + { + public DebugMouseDevice(Switch device, bool active) : base(device, active) { } + + public void Update() + { + ref RingLifo lifo = ref _device.Hid.SharedMemory.DebugMouse; + + ref DebugMouseState previousEntry = ref lifo.GetCurrentEntryRef(); + + DebugMouseState newState = new() + { + SamplingNumber = previousEntry.SamplingNumber + 1, + }; + + if (Active) + { + // TODO: This is a debug device only present in dev environment, do we want to support it? + } + + lifo.Write(ref newState); + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Hid/HidDevices/MouseDevice.cs b/src/Ryujinx.HLE/HOS/Services/Hid/HidDevices/MouseDevice.cs index b2dd3feaf..2e62d206b 100644 --- a/src/Ryujinx.HLE/HOS/Services/Hid/HidDevices/MouseDevice.cs +++ b/src/Ryujinx.HLE/HOS/Services/Hid/HidDevices/MouseDevice.cs @@ -23,8 +23,8 @@ namespace Ryujinx.HLE.HOS.Services.Hid newState.Buttons = (MouseButton)buttons; newState.X = mouseX; newState.Y = mouseY; - newState.DeltaX = mouseX - previousEntry.DeltaX; - newState.DeltaY = mouseY - previousEntry.DeltaY; + newState.DeltaX = mouseX - previousEntry.X; + newState.DeltaY = mouseY - previousEntry.Y; newState.WheelDeltaX = scrollX; newState.WheelDeltaY = scrollY; newState.Attributes = connected ? MouseAttribute.IsConnected : MouseAttribute.None; diff --git a/src/Ryujinx.HLE/HOS/Services/Hid/IHidServer.cs b/src/Ryujinx.HLE/HOS/Services/Hid/IHidServer.cs index 1d1b145cc..556e35ea6 100644 --- a/src/Ryujinx.HLE/HOS/Services/Hid/IHidServer.cs +++ b/src/Ryujinx.HLE/HOS/Services/Hid/IHidServer.cs @@ -22,6 +22,7 @@ namespace Ryujinx.HLE.HOS.Services.Hid private bool _sixAxisSensorFusionEnabled; private bool _unintendedHomeButtonInputProtectionEnabled; + private bool _npadAnalogStickCenterClampEnabled; private bool _vibrationPermitted; private bool _usbFullKeyControllerEnabled; private readonly bool _isFirmwareUpdateAvailableForSixAxisSensor; @@ -129,6 +130,26 @@ namespace Ryujinx.HLE.HOS.Services.Hid return ResultCode.Success; } + + [CommandCmif(26)] + // ActivateDebugMouse(nn::applet::AppletResourceUserId) + public ResultCode ActivateDebugMouse(ServiceCtx context) + { + long appletResourceUserId = context.RequestData.ReadInt64(); + + context.Device.Hid.DebugMouse.Active = true; + + // Initialize entries to avoid issues with some games. + + for (int entry = 0; entry < Hid.SharedMemEntryCount; entry++) + { + context.Device.Hid.DebugMouse.Update(); + } + + Logger.Stub?.PrintStub(LogClass.ServiceHid, new { appletResourceUserId }); + + return ResultCode.Success; + } [CommandCmif(31)] // ActivateKeyboard(nn::applet::AppletResourceUserId) @@ -1107,6 +1128,19 @@ namespace Ryujinx.HLE.HOS.Services.Hid // If not, it returns nothing. } + [CommandCmif(134)] // 6.1.0+ + // SetNpadUseAnalogStickUseCenterClamp(bool Enable, nn::applet::AppletResourceUserId) + public ResultCode SetNpadUseAnalogStickUseCenterClamp(ServiceCtx context) + { + ulong pid = context.RequestData.ReadUInt64(); + _npadAnalogStickCenterClampEnabled = context.RequestData.ReadUInt32() != 0; + long appletResourceUserId = context.RequestData.ReadInt64(); + + Logger.Stub?.PrintStub(LogClass.ServiceHid, new { pid, appletResourceUserId, _npadAnalogStickCenterClampEnabled }); + + return ResultCode.Success; + } + [CommandCmif(200)] // GetVibrationDeviceInfo(nn::hid::VibrationDeviceHandle) -> nn::hid::VibrationDeviceInfo public ResultCode GetVibrationDeviceInfo(ServiceCtx context) @@ -1821,5 +1855,18 @@ namespace Ryujinx.HLE.HOS.Services.Hid return ResultCode.Success; } + + [CommandCmif(1004)] // 17.0.0+ + // SetTouchScreenResolution(int width, int height, nn::applet::AppletResourceUserId) + public ResultCode SetTouchScreenResolution(ServiceCtx context) + { + int width = context.RequestData.ReadInt32(); + int height = context.RequestData.ReadInt32(); + long appletResourceUserId = context.RequestData.ReadInt64(); + + Logger.Stub?.PrintStub(LogClass.ServiceHid, new { width, height, appletResourceUserId }); + + return ResultCode.Success; + } } } diff --git a/src/Ryujinx.HLE/HOS/Services/Hid/Types/SharedMemory/DebugMouse/DebugMouseAttribute.cs b/src/Ryujinx.HLE/HOS/Services/Hid/Types/SharedMemory/DebugMouse/DebugMouseAttribute.cs new file mode 100644 index 000000000..0b55277d4 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Hid/Types/SharedMemory/DebugMouse/DebugMouseAttribute.cs @@ -0,0 +1,12 @@ +using System; + +namespace Ryujinx.HLE.HOS.Services.Hid.Types.SharedMemory.DebugMouse +{ + [Flags] + enum DebugMouseAttribute : uint + { + None = 0, + Transferable = 1 << 0, + IsConnected = 1 << 1, + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Hid/Types/SharedMemory/DebugMouse/DebugMouseButton.cs b/src/Ryujinx.HLE/HOS/Services/Hid/Types/SharedMemory/DebugMouse/DebugMouseButton.cs new file mode 100644 index 000000000..c07fa84af --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Hid/Types/SharedMemory/DebugMouse/DebugMouseButton.cs @@ -0,0 +1,15 @@ +using System; + +namespace Ryujinx.HLE.HOS.Services.Hid.Types.SharedMemory.DebugMouse +{ + [Flags] + enum DebugMouseButton : uint + { + None = 0, + Left = 1 << 0, + Right = 1 << 1, + Middle = 1 << 2, + Forward = 1 << 3, + Back = 1 << 4, + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Hid/Types/SharedMemory/DebugMouse/DebugMouseState.cs b/src/Ryujinx.HLE/HOS/Services/Hid/Types/SharedMemory/DebugMouse/DebugMouseState.cs new file mode 100644 index 000000000..e2860c9f5 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Hid/Types/SharedMemory/DebugMouse/DebugMouseState.cs @@ -0,0 +1,19 @@ +using Ryujinx.HLE.HOS.Services.Hid.Types.SharedMemory.Common; +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Hid.Types.SharedMemory.DebugMouse +{ + [StructLayout(LayoutKind.Sequential, Pack = 1)] + struct DebugMouseState : ISampledDataStruct + { + public ulong SamplingNumber; + public int X; + public int Y; + public int DeltaX; + public int DeltaY; + public int WheelDeltaX; + public int WheelDeltaY; + public DebugMouseButton Buttons; + public DebugMouseAttribute Attributes; + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Hid/Types/SharedMemory/SharedMemory.cs b/src/Ryujinx.HLE/HOS/Services/Hid/Types/SharedMemory/SharedMemory.cs index d6283eb57..59d8f4489 100644 --- a/src/Ryujinx.HLE/HOS/Services/Hid/Types/SharedMemory/SharedMemory.cs +++ b/src/Ryujinx.HLE/HOS/Services/Hid/Types/SharedMemory/SharedMemory.cs @@ -1,5 +1,6 @@ using Ryujinx.Common.Memory; using Ryujinx.HLE.HOS.Services.Hid.Types.SharedMemory.Common; +using Ryujinx.HLE.HOS.Services.Hid.Types.SharedMemory.DebugMouse; using Ryujinx.HLE.HOS.Services.Hid.Types.SharedMemory.DebugPad; using Ryujinx.HLE.HOS.Services.Hid.Types.SharedMemory.Keyboard; using Ryujinx.HLE.HOS.Services.Hid.Types.SharedMemory.Mouse; @@ -44,6 +45,12 @@ namespace Ryujinx.HLE.HOS.Services.Hid.Types.SharedMemory /// [FieldOffset(0x9A00)] public Array10 Npads; + + /// + /// Debug mouse. + /// + [FieldOffset(0x3DC00)] + public RingLifo DebugMouse; public static SharedMemory Create() { diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NetworkConfig.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NetworkConfig.cs index 4da5fe42b..c6d6ac944 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NetworkConfig.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NetworkConfig.cs @@ -3,7 +3,7 @@ using System.Runtime.InteropServices; namespace Ryujinx.HLE.HOS.Services.Ldn.Types { - [StructLayout(LayoutKind.Sequential, Size = 0x20)] + [StructLayout(LayoutKind.Sequential, Size = 0x20, Pack = 8)] struct NetworkConfig { public IntentId IntentId; diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/ScanFilter.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/ScanFilter.cs index 449c923cc..f3ab1edd5 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/ScanFilter.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/ScanFilter.cs @@ -3,7 +3,7 @@ using System.Runtime.InteropServices; namespace Ryujinx.HLE.HOS.Services.Ldn.Types { - [StructLayout(LayoutKind.Sequential, Size = 0x60)] + [StructLayout(LayoutKind.Sequential, Size = 0x60, Pack = 8)] struct ScanFilter { public NetworkId NetworkId; diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/SecurityConfig.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/SecurityConfig.cs index 5939a1394..f3968aab4 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/SecurityConfig.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/SecurityConfig.cs @@ -3,7 +3,7 @@ using System.Runtime.InteropServices; namespace Ryujinx.HLE.HOS.Services.Ldn.Types { - [StructLayout(LayoutKind.Sequential, Size = 0x44)] + [StructLayout(LayoutKind.Sequential, Size = 0x44, Pack = 2)] struct SecurityConfig { public SecurityMode SecurityMode; diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/SecurityParameter.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/SecurityParameter.cs index dbcaa9eeb..e564a2ec9 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/SecurityParameter.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/SecurityParameter.cs @@ -3,7 +3,7 @@ using System.Runtime.InteropServices; namespace Ryujinx.HLE.HOS.Services.Ldn.Types { - [StructLayout(LayoutKind.Sequential, Size = 0x20)] + [StructLayout(LayoutKind.Sequential, Size = 0x20, Pack = 1)] struct SecurityParameter { public Array16 Data; diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/UserConfig.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/UserConfig.cs index 3820f936e..7246f6f80 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/UserConfig.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/UserConfig.cs @@ -3,7 +3,7 @@ using System.Runtime.InteropServices; namespace Ryujinx.HLE.HOS.Services.Ldn.Types { - [StructLayout(LayoutKind.Sequential, Size = 0x30)] + [StructLayout(LayoutKind.Sequential, Size = 0x30, Pack = 1)] struct UserConfig { public Array33 UserName; diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/AccessPoint.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/AccessPoint.cs index 78ebcac82..bd00a3139 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/AccessPoint.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/AccessPoint.cs @@ -15,6 +15,8 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator public Array8 LatestUpdates = new(); public bool Connected { get; private set; } + public ProxyConfig Config => _parent.NetworkClient.Config; + public AccessPoint(IUserLocalCommunicationService parent) { _parent = parent; @@ -24,9 +26,12 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator public void Dispose() { - _parent.NetworkClient.DisconnectNetwork(); + if (_parent?.NetworkClient != null) + { + _parent.NetworkClient.DisconnectNetwork(); - _parent.NetworkClient.NetworkChange -= NetworkChanged; + _parent.NetworkClient.NetworkChange -= NetworkChanged; + } } private void NetworkChanged(object sender, NetworkChangeEventArgs e) diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/INetworkClient.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/INetworkClient.cs index 7ad6de51d..028ab6cfc 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/INetworkClient.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/INetworkClient.cs @@ -6,6 +6,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator { interface INetworkClient : IDisposable { + ProxyConfig Config { get; } bool NeedsRealId { get; } event EventHandler NetworkChange; diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/IUserLocalCommunicationService.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/IUserLocalCommunicationService.cs index 1d4b5485e..9f65aed4b 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/IUserLocalCommunicationService.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/IUserLocalCommunicationService.cs @@ -9,6 +9,8 @@ using Ryujinx.HLE.HOS.Ipc; using Ryujinx.HLE.HOS.Kernel.Threading; using Ryujinx.HLE.HOS.Services.Ldn.Types; using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm; +using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu; +using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types; using Ryujinx.Horizon.Common; using Ryujinx.Memory; using System; @@ -21,6 +23,9 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator { class IUserLocalCommunicationService : IpcService, IDisposable { + public static string DefaultLanPlayHost = "ryuldn.vudjun.com"; + public static short LanPlayPort = 30456; + public INetworkClient NetworkClient { get; private set; } private const int NifmRequestID = 90; @@ -175,19 +180,37 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator if (_state == NetworkState.AccessPointCreated || _state == NetworkState.StationConnected) { - (_, UnicastIPAddressInformation unicastAddress) = NetworkHelpers.GetLocalInterface(context.Device.Configuration.MultiplayerLanInterfaceId); - - if (unicastAddress == null) + ProxyConfig config = _state switch { - context.ResponseData.Write(NetworkHelpers.ConvertIpv4Address(DefaultIPAddress)); - context.ResponseData.Write(NetworkHelpers.ConvertIpv4Address(DefaultSubnetMask)); + NetworkState.AccessPointCreated => _accessPoint.Config, + NetworkState.StationConnected => _station.Config, + + _ => default + }; + + if (config.ProxyIp == 0) + { + (_, UnicastIPAddressInformation unicastAddress) = NetworkHelpers.GetLocalInterface(context.Device.Configuration.MultiplayerLanInterfaceId); + + if (unicastAddress == null) + { + context.ResponseData.Write(NetworkHelpers.ConvertIpv4Address(DefaultIPAddress)); + context.ResponseData.Write(NetworkHelpers.ConvertIpv4Address(DefaultSubnetMask)); + } + else + { + Logger.Info?.Print(LogClass.ServiceLdn, $"Console's LDN IP is \"{unicastAddress.Address}\"."); + + context.ResponseData.Write(NetworkHelpers.ConvertIpv4Address(unicastAddress.Address)); + context.ResponseData.Write(NetworkHelpers.ConvertIpv4Address(unicastAddress.IPv4Mask)); + } } else { - Logger.Info?.Print(LogClass.ServiceLdn, $"Console's LDN IP is \"{unicastAddress.Address}\"."); + Logger.Info?.Print(LogClass.ServiceLdn, $"LDN obtained proxy IP."); - context.ResponseData.Write(NetworkHelpers.ConvertIpv4Address(unicastAddress.Address)); - context.ResponseData.Write(NetworkHelpers.ConvertIpv4Address(unicastAddress.IPv4Mask)); + context.ResponseData.Write(config.ProxyIp); + context.ResponseData.Write(config.ProxySubnetMask); } } else @@ -1066,6 +1089,27 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator switch (mode) { + case MultiplayerMode.LdnRyu: + try + { + string ldnServer = context.Device.Configuration.MultiplayerLdnServer; + if (string.IsNullOrEmpty(ldnServer)) + { + ldnServer = DefaultLanPlayHost; + } + if (!IPAddress.TryParse(ldnServer, out IPAddress ipAddress)) + { + ipAddress = Dns.GetHostEntry(ldnServer).AddressList[0]; + } + NetworkClient = new LdnMasterProxyClient(ipAddress.ToString(), LanPlayPort, context.Device.Configuration); + } + catch (Exception ex) + { + Logger.Error?.Print(LogClass.ServiceLdn, "Could not locate LdnRyu server. Defaulting to stubbed wireless."); + Logger.Error?.Print(LogClass.ServiceLdn, ex.Message); + NetworkClient = new LdnDisabledClient(); + } + break; case MultiplayerMode.LdnMitm: NetworkClient = new LdnMitmClient(context.Device.Configuration); break; @@ -1103,7 +1147,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator _accessPoint?.Dispose(); _accessPoint = null; - NetworkClient?.Dispose(); + NetworkClient?.DisconnectAndStop(); NetworkClient = null; } } diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnDisabledClient.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnDisabledClient.cs index e3385a1ed..2e8bb8d83 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnDisabledClient.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnDisabledClient.cs @@ -1,3 +1,4 @@ +using Ryujinx.Common.Logging; using Ryujinx.HLE.HOS.Services.Ldn.Types; using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types; using System; @@ -6,12 +7,14 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator { class LdnDisabledClient : INetworkClient { + public ProxyConfig Config { get; } public bool NeedsRealId => true; public event EventHandler NetworkChange; public NetworkError Connect(ConnectRequest request) { + Logger.Warning?.PrintMsg(LogClass.ServiceLdn, "Attempted to connect to a network, but Multiplayer is disabled!"); NetworkChange?.Invoke(this, new NetworkChangeEventArgs(new NetworkInfo(), false)); return NetworkError.None; @@ -19,6 +22,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator public NetworkError ConnectPrivate(ConnectPrivateRequest request) { + Logger.Warning?.PrintMsg(LogClass.ServiceLdn, "Attempted to connect to a network, but Multiplayer is disabled!"); NetworkChange?.Invoke(this, new NetworkChangeEventArgs(new NetworkInfo(), false)); return NetworkError.None; @@ -26,6 +30,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator public bool CreateNetwork(CreateAccessPointRequest request, byte[] advertiseData) { + Logger.Warning?.PrintMsg(LogClass.ServiceLdn, "Attempted to create a network, but Multiplayer is disabled!"); NetworkChange?.Invoke(this, new NetworkChangeEventArgs(new NetworkInfo(), false)); return true; @@ -33,6 +38,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator public bool CreateNetworkPrivate(CreateAccessPointPrivateRequest request, byte[] advertiseData) { + Logger.Warning?.PrintMsg(LogClass.ServiceLdn, "Attempted to create a network, but Multiplayer is disabled!"); NetworkChange?.Invoke(this, new NetworkChangeEventArgs(new NetworkInfo(), false)); return true; @@ -49,6 +55,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator public NetworkInfo[] Scan(ushort channel, ScanFilter scanFilter) { + Logger.Warning?.PrintMsg(LogClass.ServiceLdn, "Attempted to scan for networks, but Multiplayer is disabled!"); return Array.Empty(); } diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/LanProtocol.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/LanProtocol.cs index 4b01bfe31..b60b70d80 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/LanProtocol.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/LanProtocol.cs @@ -1,3 +1,4 @@ +using Gommon; using Ryujinx.Common.Logging; using Ryujinx.Common.Memory; using Ryujinx.Common.Utilities; @@ -143,7 +144,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm if (decompressedLdnData.Length != header.DecompressLength) { Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"Decompress error: length does not match. ({decompressedLdnData.Length} != {header.DecompressLength})"); - Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"Decompress error data: '{string.Join("", decompressedLdnData.Select(x => (int)x).ToArray())}'"); + Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"Decompress error data: '{decompressedLdnData.Select(x => (int)x).JoinToString(string.Empty)}'"); return; } diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/LdnMitmClient.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/LdnMitmClient.cs index 273acdd5e..40697d122 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/LdnMitmClient.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/LdnMitmClient.cs @@ -12,6 +12,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm /// internal class LdnMitmClient : INetworkClient { + public ProxyConfig Config { get; } public bool NeedsRealId => false; public event EventHandler NetworkChange; diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/IProxyClient.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/IProxyClient.cs new file mode 100644 index 000000000..a7c435506 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/IProxyClient.cs @@ -0,0 +1,7 @@ +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu +{ + interface IProxyClient + { + bool SendAsync(byte[] buffer); + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/LdnMasterProxyClient.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/LdnMasterProxyClient.cs new file mode 100644 index 000000000..4c7814b8e --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/LdnMasterProxyClient.cs @@ -0,0 +1,645 @@ +using Ryujinx.Common.Logging; +using Ryujinx.Common.Utilities; +using Ryujinx.HLE.HOS.Services.Ldn.Types; +using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Proxy; +using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types; +using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types; +using Ryujinx.HLE.HOS.Services.Sockets.Bsd.Proxy; +using Ryujinx.HLE.Utilities; +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.NetworkInformation; +using System.Net.Sockets; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using TcpClient = NetCoreServer.TcpClient; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu +{ + class LdnMasterProxyClient : TcpClient, INetworkClient, IProxyClient + { + public bool NeedsRealId => true; + + private static InitializeMessage InitializeMemory = new InitializeMessage(); + + private const int InactiveTimeout = 6000; + private const int FailureTimeout = 4000; + private const int ScanTimeout = 1000; + + private bool _useP2pProxy; + private NetworkError _lastError; + + private readonly ManualResetEvent _connected = new ManualResetEvent(false); + private readonly ManualResetEvent _error = new ManualResetEvent(false); + private readonly ManualResetEvent _scan = new ManualResetEvent(false); + private readonly ManualResetEvent _reject = new ManualResetEvent(false); + private readonly AutoResetEvent _apConnected = new AutoResetEvent(false); + + private readonly RyuLdnProtocol _protocol; + private readonly NetworkTimeout _timeout; + + private readonly List _availableGames = new List(); + private DisconnectReason _disconnectReason; + + private P2pProxyServer _hostedProxy; + private P2pProxyClient _connectedProxy; + + private bool _networkConnected; + + private string _passphrase; + private byte[] _gameVersion = new byte[0x10]; + + private readonly HLEConfiguration _config; + + public event EventHandler NetworkChange; + + public ProxyConfig Config { get; private set; } + + public LdnMasterProxyClient(string address, int port, HLEConfiguration config) : base(address, port) + { + if (ProxyHelpers.SupportsNoDelay()) + { + OptionNoDelay = true; + } + + _protocol = new RyuLdnProtocol(); + _timeout = new NetworkTimeout(InactiveTimeout, TimeoutConnection); + + _protocol.Initialize += HandleInitialize; + _protocol.Connected += HandleConnected; + _protocol.Reject += HandleReject; + _protocol.RejectReply += HandleRejectReply; + _protocol.SyncNetwork += HandleSyncNetwork; + _protocol.ProxyConfig += HandleProxyConfig; + _protocol.Disconnected += HandleDisconnected; + + _protocol.ScanReply += HandleScanReply; + _protocol.ScanReplyEnd += HandleScanReplyEnd; + _protocol.ExternalProxy += HandleExternalProxy; + + _protocol.Ping += HandlePing; + _protocol.NetworkError += HandleNetworkError; + + _config = config; + _useP2pProxy = !config.MultiplayerDisableP2p; + } + + private void TimeoutConnection() + { + _connected.Reset(); + + DisconnectAsync(); + + while (IsConnected) + { + Thread.Yield(); + } + } + + private bool EnsureConnected() + { + if (IsConnected) + { + return true; + } + + _error.Reset(); + + ConnectAsync(); + + int index = WaitHandle.WaitAny(new WaitHandle[] { _connected, _error }, FailureTimeout); + + if (IsConnected) + { + SendAsync(_protocol.Encode(PacketId.Initialize, InitializeMemory)); + } + + return index == 0 && IsConnected; + } + + private void UpdatePassphraseIfNeeded() + { + string passphrase = _config.MultiplayerLdnPassphrase ?? ""; + if (passphrase != _passphrase) + { + _passphrase = passphrase; + + SendAsync(_protocol.Encode(PacketId.Passphrase, StringUtils.GetFixedLengthBytes(passphrase, 0x80, Encoding.UTF8))); + } + } + + protected override void OnConnected() + { + Logger.Info?.PrintMsg(LogClass.ServiceLdn, $"LDN TCP client connected a new session with Id {Id}"); + + UpdatePassphraseIfNeeded(); + + _connected.Set(); + } + + protected override void OnDisconnected() + { + Logger.Info?.PrintMsg(LogClass.ServiceLdn, $"LDN TCP client disconnected a session with Id {Id}"); + + _passphrase = null; + + _connected.Reset(); + + if (_networkConnected) + { + DisconnectInternal(); + } + } + + public void DisconnectAndStop() + { + _timeout.Dispose(); + + DisconnectAsync(); + + while (IsConnected) + { + Thread.Yield(); + } + + Dispose(); + } + + protected override void OnReceived(byte[] buffer, long offset, long size) + { + _protocol.Read(buffer, (int)offset, (int)size); + } + + protected override void OnError(SocketError error) + { + Logger.Info?.PrintMsg(LogClass.ServiceLdn, $"LDN TCP client caught an error with code {error}"); + + _error.Set(); + } + + + + private void HandleInitialize(LdnHeader header, InitializeMessage initialize) + { + InitializeMemory = initialize; + } + + private void HandleExternalProxy(LdnHeader header, ExternalProxyConfig config) + { + int length = config.AddressFamily switch + { + AddressFamily.InterNetwork => 4, + AddressFamily.InterNetworkV6 => 16, + _ => 0 + }; + + if (length == 0) + { + return; // Invalid external proxy. + } + + IPAddress address = new(config.ProxyIp.AsSpan()[..length].ToArray()); + P2pProxyClient proxy = new(address.ToString(), config.ProxyPort); + + _connectedProxy = proxy; + + bool success = proxy.PerformAuth(config); + + if (!success) + { + DisconnectInternal(); + } + } + + private void HandlePing(LdnHeader header, PingMessage ping) + { + if (ping.Requester == 0) // Server requested. + { + // Send the ping message back. + + SendAsync(_protocol.Encode(PacketId.Ping, ping)); + } + } + + private void HandleNetworkError(LdnHeader header, NetworkErrorMessage error) + { + if (error.Error == NetworkError.PortUnreachable) + { + _useP2pProxy = false; + } + else + { + _lastError = error.Error; + } + } + + private NetworkError ConsumeNetworkError() + { + NetworkError result = _lastError; + + _lastError = NetworkError.None; + + return result; + } + + private void HandleSyncNetwork(LdnHeader header, NetworkInfo info) + { + NetworkChange?.Invoke(this, new NetworkChangeEventArgs(info, true)); + } + + private void HandleConnected(LdnHeader header, NetworkInfo info) + { + _networkConnected = true; + _disconnectReason = DisconnectReason.None; + + _apConnected.Set(); + + NetworkChange?.Invoke(this, new NetworkChangeEventArgs(info, true)); + } + + private void HandleDisconnected(LdnHeader header, DisconnectMessage message) + { + DisconnectInternal(); + } + + private void HandleReject(LdnHeader header, RejectRequest reject) + { + // When the client receives a Reject request, we have been rejected and will be disconnected shortly. + _disconnectReason = reject.DisconnectReason; + } + + private void HandleRejectReply(LdnHeader header) + { + _reject.Set(); + } + + private void HandleScanReply(LdnHeader header, NetworkInfo info) + { + _availableGames.Add(info); + } + + private void HandleScanReplyEnd(LdnHeader obj) + { + _scan.Set(); + } + + private void DisconnectInternal() + { + if (_networkConnected) + { + _networkConnected = false; + + _hostedProxy?.Dispose(); + _hostedProxy = null; + + _connectedProxy?.Dispose(); + _connectedProxy = null; + + _apConnected.Reset(); + + NetworkChange?.Invoke(this, new NetworkChangeEventArgs(new NetworkInfo(), false, _disconnectReason)); + + if (IsConnected) + { + _timeout.RefreshTimeout(); + } + } + } + + public void DisconnectNetwork() + { + if (_networkConnected) + { + SendAsync(_protocol.Encode(PacketId.Disconnect, new DisconnectMessage())); + + DisconnectInternal(); + } + } + + public ResultCode Reject(DisconnectReason disconnectReason, uint nodeId) + { + if (_networkConnected) + { + _reject.Reset(); + + SendAsync(_protocol.Encode(PacketId.Reject, new RejectRequest(disconnectReason, nodeId))); + + int index = WaitHandle.WaitAny(new WaitHandle[] { _reject, _error }, InactiveTimeout); + + if (index == 0) + { + return (ConsumeNetworkError() != NetworkError.None) ? ResultCode.InvalidState : ResultCode.Success; + } + } + + return ResultCode.InvalidState; + } + + public void SetAdvertiseData(byte[] data) + { + // TODO: validate we're the owner (the server will do this anyways tho) + if (_networkConnected) + { + SendAsync(_protocol.Encode(PacketId.SetAdvertiseData, data)); + } + } + + public void SetGameVersion(byte[] versionString) + { + _gameVersion = versionString; + + if (_gameVersion.Length < 0x10) + { + Array.Resize(ref _gameVersion, 0x10); + } + } + + public void SetStationAcceptPolicy(AcceptPolicy acceptPolicy) + { + // TODO: validate we're the owner (the server will do this anyways tho) + if (_networkConnected) + { + SendAsync(_protocol.Encode(PacketId.SetAcceptPolicy, new SetAcceptPolicyRequest + { + StationAcceptPolicy = acceptPolicy + })); + } + } + + private void DisposeProxy() + { + _hostedProxy?.Dispose(); + _hostedProxy = null; + } + + private void ConfigureAccessPoint(ref RyuNetworkConfig request) + { + _gameVersion.AsSpan().CopyTo(request.GameVersion.AsSpan()); + + if (_useP2pProxy) + { + // Before sending the request, attempt to set up a proxy server. + // This can be on a range of private ports, which can be exposed on a range of public + // ports via UPnP. If any of this fails, we just fall back to using the master server. + + int i = 0; + for (; i < P2pProxyServer.PrivatePortRange; i++) + { + _hostedProxy = new P2pProxyServer(this, (ushort)(P2pProxyServer.PrivatePortBase + i), _protocol); + + try + { + _hostedProxy.Start(); + + break; + } + catch (SocketException e) + { + _hostedProxy.Dispose(); + _hostedProxy = null; + + if (e.SocketErrorCode != SocketError.AddressAlreadyInUse) + { + i = P2pProxyServer.PrivatePortRange; // Immediately fail. + } + } + } + + bool openSuccess = i < P2pProxyServer.PrivatePortRange; + + if (openSuccess) + { + Task natPunchResult = _hostedProxy.NatPunch(); + + try + { + if (natPunchResult.Result != 0) + { + // Tell the server that we are hosting the proxy. + request.ExternalProxyPort = natPunchResult.Result; + } + } + catch (Exception) { } + + if (request.ExternalProxyPort == 0) + { + Logger.Warning?.Print(LogClass.ServiceLdn, "Failed to open a port with UPnP for P2P connection. Proxying through the master server instead. Expect higher latency."); + _hostedProxy.Dispose(); + } + else + { + Logger.Info?.Print(LogClass.ServiceLdn, $"Created a wireless P2P network on port {request.ExternalProxyPort}."); + _hostedProxy.Start(); + + (_, UnicastIPAddressInformation unicastAddress) = NetworkHelpers.GetLocalInterface(); + + unicastAddress.Address.GetAddressBytes().AsSpan().CopyTo(request.PrivateIp.AsSpan()); + request.InternalProxyPort = _hostedProxy.PrivatePort; + request.AddressFamily = unicastAddress.Address.AddressFamily; + } + } + else + { + Logger.Warning?.Print(LogClass.ServiceLdn, "Cannot create a P2P server. Proxying through the master server instead. Expect higher latency."); + } + } + } + + private bool CreateNetworkCommon() + { + bool signalled = _apConnected.WaitOne(FailureTimeout); + + if (!_useP2pProxy && _hostedProxy != null) + { + Logger.Warning?.Print(LogClass.ServiceLdn, "Locally hosted proxy server was not externally reachable. Proxying through the master server instead. Expect higher latency."); + + DisposeProxy(); + } + + if (signalled && _connectedProxy != null) + { + _connectedProxy.EnsureProxyReady(); + + Config = _connectedProxy.ProxyConfig; + } + else + { + DisposeProxy(); + } + + return signalled; + } + + public bool CreateNetwork(CreateAccessPointRequest request, byte[] advertiseData) + { + _timeout.DisableTimeout(); + + ConfigureAccessPoint(ref request.RyuNetworkConfig); + + if (!EnsureConnected()) + { + DisposeProxy(); + + return false; + } + + UpdatePassphraseIfNeeded(); + + SendAsync(_protocol.Encode(PacketId.CreateAccessPoint, request, advertiseData)); + + // Send a network change event with dummy data immediately. Necessary to avoid crashes in some games + var networkChangeEvent = new NetworkChangeEventArgs(new NetworkInfo() + { + Common = new CommonNetworkInfo() + { + MacAddress = InitializeMemory.MacAddress, + Channel = request.NetworkConfig.Channel, + LinkLevel = 3, + NetworkType = 2, + Ssid = new Ssid() + { + Length = 32 + } + }, + Ldn = new LdnNetworkInfo() + { + AdvertiseDataSize = (ushort)advertiseData.Length, + AuthenticationId = 0, + NodeCount = 1, + NodeCountMax = request.NetworkConfig.NodeCountMax, + SecurityMode = (ushort)request.SecurityConfig.SecurityMode + } + }, true); + networkChangeEvent.Info.Ldn.Nodes[0] = new NodeInfo() + { + Ipv4Address = 175243265, + IsConnected = 1, + LocalCommunicationVersion = request.NetworkConfig.LocalCommunicationVersion, + MacAddress = InitializeMemory.MacAddress, + NodeId = 0, + UserName = request.UserConfig.UserName + }; + "12345678123456781234567812345678"u8.ToArray().CopyTo(networkChangeEvent.Info.Common.Ssid.Name.AsSpan()); + NetworkChange?.Invoke(this, networkChangeEvent); + + return CreateNetworkCommon(); + } + + public bool CreateNetworkPrivate(CreateAccessPointPrivateRequest request, byte[] advertiseData) + { + _timeout.DisableTimeout(); + + ConfigureAccessPoint(ref request.RyuNetworkConfig); + + if (!EnsureConnected()) + { + DisposeProxy(); + + return false; + } + + UpdatePassphraseIfNeeded(); + + SendAsync(_protocol.Encode(PacketId.CreateAccessPointPrivate, request, advertiseData)); + + return CreateNetworkCommon(); + } + + public NetworkInfo[] Scan(ushort channel, ScanFilter scanFilter) + { + if (!_networkConnected) + { + _timeout.RefreshTimeout(); + } + + _availableGames.Clear(); + + int index = -1; + + if (EnsureConnected()) + { + UpdatePassphraseIfNeeded(); + + _scan.Reset(); + + SendAsync(_protocol.Encode(PacketId.Scan, scanFilter)); + + index = WaitHandle.WaitAny(new WaitHandle[] { _scan, _error }, ScanTimeout); + } + + if (index != 0) + { + // An error occurred or timeout. Write 0 games. + return Array.Empty(); + } + + return _availableGames.ToArray(); + } + + private NetworkError ConnectCommon() + { + bool signalled = _apConnected.WaitOne(FailureTimeout); + + NetworkError error = ConsumeNetworkError(); + + if (error != NetworkError.None) + { + return error; + } + + if (signalled && _connectedProxy != null) + { + _connectedProxy.EnsureProxyReady(); + + Config = _connectedProxy.ProxyConfig; + } + + return signalled ? NetworkError.None : NetworkError.ConnectTimeout; + } + + public NetworkError Connect(ConnectRequest request) + { + _timeout.DisableTimeout(); + + if (!EnsureConnected()) + { + return NetworkError.Unknown; + } + + SendAsync(_protocol.Encode(PacketId.Connect, request)); + + var networkChangeEvent = new NetworkChangeEventArgs(new NetworkInfo() + { + Common = request.NetworkInfo.Common, + Ldn = request.NetworkInfo.Ldn + }, true); + + NetworkChange?.Invoke(this, networkChangeEvent); + + return ConnectCommon(); + } + + public NetworkError ConnectPrivate(ConnectPrivateRequest request) + { + _timeout.DisableTimeout(); + + if (!EnsureConnected()) + { + return NetworkError.Unknown; + } + + SendAsync(_protocol.Encode(PacketId.ConnectPrivate, request)); + + return ConnectCommon(); + } + + private void HandleProxyConfig(LdnHeader header, ProxyConfig config) + { + Config = config; + + SocketHelpers.RegisterProxy(new LdnProxy(config, this, _protocol)); + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/NetworkTimeout.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/NetworkTimeout.cs new file mode 100644 index 000000000..5012d5d81 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/NetworkTimeout.cs @@ -0,0 +1,83 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu +{ + class NetworkTimeout : IDisposable + { + private readonly int _idleTimeout; + private readonly Action _timeoutCallback; + private CancellationTokenSource _cancel; + + private readonly object _lock = new object(); + + public NetworkTimeout(int idleTimeout, Action timeoutCallback) + { + _idleTimeout = idleTimeout; + _timeoutCallback = timeoutCallback; + } + + private async Task TimeoutTask() + { + CancellationTokenSource cts; + + lock (_lock) + { + cts = _cancel; + } + + if (cts == null) + { + return; + } + + try + { + await Task.Delay(_idleTimeout, cts.Token); + } + catch (TaskCanceledException) + { + return; // Timeout cancelled. + } + + lock (_lock) + { + // Run the timeout callback. If the cancel token source has been replaced, we have _just_ been cancelled. + if (cts == _cancel) + { + _timeoutCallback(); + } + } + } + + public bool RefreshTimeout() + { + lock (_lock) + { + _cancel?.Cancel(); + + _cancel = new CancellationTokenSource(); + + Task.Run(TimeoutTask); + } + + return true; + } + + public void DisableTimeout() + { + lock (_lock) + { + _cancel?.Cancel(); + + _cancel = new CancellationTokenSource(); + } + } + + public void Dispose() + { + DisableTimeout(); + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/EphemeralPortPool.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/EphemeralPortPool.cs new file mode 100644 index 000000000..bc3a5edf2 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/EphemeralPortPool.cs @@ -0,0 +1,53 @@ +using System.Collections.Generic; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Proxy +{ + public class EphemeralPortPool + { + private const ushort EphemeralBase = 49152; + + private readonly List _ephemeralPorts = new List(); + + private readonly object _lock = new object(); + + public ushort Get() + { + ushort port = EphemeralBase; + lock (_lock) + { + // Starting at the ephemeral port base, return an ephemeral port that is not in use. + // Returns 0 if the range is exhausted. + + for (int i = 0; i < _ephemeralPorts.Count; i++) + { + ushort existingPort = _ephemeralPorts[i]; + + if (existingPort > port) + { + // The port was free - take it. + _ephemeralPorts.Insert(i, port); + + return port; + } + + port++; + } + + if (port != 0) + { + _ephemeralPorts.Add(port); + } + + return port; + } + } + + public void Return(ushort port) + { + lock (_lock) + { + _ephemeralPorts.Remove(port); + } + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/LdnProxy.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/LdnProxy.cs new file mode 100644 index 000000000..bb390d49a --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/LdnProxy.cs @@ -0,0 +1,254 @@ +using Ryujinx.Common.Logging; +using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types; +using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types; +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Sockets; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Proxy +{ + class LdnProxy : IDisposable + { + public EndPoint LocalEndpoint { get; } + public IPAddress LocalAddress { get; } + + private readonly List _sockets = new List(); + private readonly Dictionary _ephemeralPorts = new Dictionary(); + + private readonly IProxyClient _parent; + private RyuLdnProtocol _protocol; + private readonly uint _subnetMask; + private readonly uint _localIp; + private readonly uint _broadcast; + + public LdnProxy(ProxyConfig config, IProxyClient client, RyuLdnProtocol protocol) + { + _parent = client; + _protocol = protocol; + + _ephemeralPorts[ProtocolType.Udp] = new EphemeralPortPool(); + _ephemeralPorts[ProtocolType.Tcp] = new EphemeralPortPool(); + + byte[] address = BitConverter.GetBytes(config.ProxyIp); + Array.Reverse(address); + LocalAddress = new IPAddress(address); + + _subnetMask = config.ProxySubnetMask; + _localIp = config.ProxyIp; + _broadcast = _localIp | (~_subnetMask); + + RegisterHandlers(protocol); + } + + public bool Supported(AddressFamily domain, SocketType type, ProtocolType protocol) + { + if (protocol == ProtocolType.Tcp) + { + Logger.Error?.PrintMsg(LogClass.ServiceLdn, "Tcp proxy networking is untested. Please report this game so that it can be tested."); + } + return domain == AddressFamily.InterNetwork && (protocol == ProtocolType.Tcp || protocol == ProtocolType.Udp); + } + + private void RegisterHandlers(RyuLdnProtocol protocol) + { + protocol.ProxyConnect += HandleConnectionRequest; + protocol.ProxyConnectReply += HandleConnectionResponse; + protocol.ProxyData += HandleData; + protocol.ProxyDisconnect += HandleDisconnect; + + _protocol = protocol; + } + + public void UnregisterHandlers(RyuLdnProtocol protocol) + { + protocol.ProxyConnect -= HandleConnectionRequest; + protocol.ProxyConnectReply -= HandleConnectionResponse; + protocol.ProxyData -= HandleData; + protocol.ProxyDisconnect -= HandleDisconnect; + } + + public ushort GetEphemeralPort(ProtocolType type) + { + return _ephemeralPorts[type].Get(); + } + + public void ReturnEphemeralPort(ProtocolType type, ushort port) + { + _ephemeralPorts[type].Return(port); + } + + public void RegisterSocket(LdnProxySocket socket) + { + lock (_sockets) + { + _sockets.Add(socket); + } + } + + public void UnregisterSocket(LdnProxySocket socket) + { + lock (_sockets) + { + _sockets.Remove(socket); + } + } + + private void ForRoutedSockets(ProxyInfo info, Action action) + { + lock (_sockets) + { + foreach (LdnProxySocket socket in _sockets) + { + // Must match protocol and destination port. + if (socket.ProtocolType != info.Protocol || socket.LocalEndPoint is not IPEndPoint endpoint || endpoint.Port != info.DestPort) + { + continue; + } + + // We can assume packets routed to us have been sent to our destination. + // They will either be sent to us, or broadcast packets. + + action(socket); + } + } + } + + public void HandleConnectionRequest(LdnHeader header, ProxyConnectRequest request) + { + ForRoutedSockets(request.Info, (socket) => + { + socket.HandleConnectRequest(request); + }); + } + + public void HandleConnectionResponse(LdnHeader header, ProxyConnectResponse response) + { + ForRoutedSockets(response.Info, (socket) => + { + socket.HandleConnectResponse(response); + }); + } + + public void HandleData(LdnHeader header, ProxyDataHeader proxyHeader, byte[] data) + { + ProxyDataPacket packet = new ProxyDataPacket() { Header = proxyHeader, Data = data }; + + ForRoutedSockets(proxyHeader.Info, (socket) => + { + socket.IncomingData(packet); + }); + } + + public void HandleDisconnect(LdnHeader header, ProxyDisconnectMessage disconnect) + { + ForRoutedSockets(disconnect.Info, (socket) => + { + socket.HandleDisconnect(disconnect); + }); + } + + private uint GetIpV4(IPEndPoint endpoint) + { + if (endpoint.AddressFamily != AddressFamily.InterNetwork) + { + throw new NotSupportedException(); + } + + byte[] address = endpoint.Address.GetAddressBytes(); + Array.Reverse(address); + + return BitConverter.ToUInt32(address); + } + + private ProxyInfo MakeInfo(IPEndPoint localEp, IPEndPoint remoteEP, ProtocolType type) + { + return new ProxyInfo + { + SourceIpV4 = GetIpV4(localEp), + SourcePort = (ushort)localEp.Port, + + DestIpV4 = GetIpV4(remoteEP), + DestPort = (ushort)remoteEP.Port, + + Protocol = type + }; + } + + public void RequestConnection(IPEndPoint localEp, IPEndPoint remoteEp, ProtocolType type) + { + // We must ask the other side to initialize a connection, so they can accept a socket for us. + + ProxyConnectRequest request = new ProxyConnectRequest + { + Info = MakeInfo(localEp, remoteEp, type) + }; + + _parent.SendAsync(_protocol.Encode(PacketId.ProxyConnect, request)); + } + + public void SignalConnected(IPEndPoint localEp, IPEndPoint remoteEp, ProtocolType type) + { + // We must tell the other side that we have accepted their request for connection. + + ProxyConnectResponse request = new ProxyConnectResponse + { + Info = MakeInfo(localEp, remoteEp, type) + }; + + _parent.SendAsync(_protocol.Encode(PacketId.ProxyConnectReply, request)); + } + + public void EndConnection(IPEndPoint localEp, IPEndPoint remoteEp, ProtocolType type) + { + // We must tell the other side that our connection is dropped. + + ProxyDisconnectMessage request = new ProxyDisconnectMessage + { + Info = MakeInfo(localEp, remoteEp, type), + DisconnectReason = 0 // TODO + }; + + _parent.SendAsync(_protocol.Encode(PacketId.ProxyDisconnect, request)); + } + + public int SendTo(ReadOnlySpan buffer, SocketFlags flags, IPEndPoint localEp, IPEndPoint remoteEp, ProtocolType type) + { + // We send exactly as much as the user wants us to, currently instantly. + // TODO: handle over "virtual mtu" (we have a max packet size to worry about anyways). fragment if tcp? throw if udp? + + ProxyDataHeader request = new ProxyDataHeader + { + Info = MakeInfo(localEp, remoteEp, type), + DataLength = (uint)buffer.Length + }; + + _parent.SendAsync(_protocol.Encode(PacketId.ProxyData, request, buffer.ToArray())); + + return buffer.Length; + } + + public bool IsBroadcast(uint ip) + { + return ip == _broadcast; + } + + public bool IsMyself(uint ip) + { + return ip == _localIp; + } + + public void Dispose() + { + UnregisterHandlers(_protocol); + + lock (_sockets) + { + foreach (LdnProxySocket socket in _sockets) + { + socket.ProxyDestroyed(); + } + } + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/LdnProxySocket.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/LdnProxySocket.cs new file mode 100644 index 000000000..ed7a9c751 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/LdnProxySocket.cs @@ -0,0 +1,797 @@ +using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types; +using Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl; +using Ryujinx.HLE.HOS.Services.Sockets.Bsd.Proxy; +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Sockets; +using System.Threading; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Proxy +{ + /// + /// This socket is forwarded through a TCP stream that goes through the Ldn server. + /// The Ldn server will then route the packets we send (or need to receive) within the virtual adhoc network. + /// + class LdnProxySocket : ISocketImpl + { + private readonly LdnProxy _proxy; + + private bool _isListening; + private readonly List _listenSockets = new List(); + + private readonly Queue _connectRequests = new Queue(); + + private readonly AutoResetEvent _acceptEvent = new AutoResetEvent(false); + private readonly int _acceptTimeout = -1; + + private readonly Queue _errors = new Queue(); + + private readonly AutoResetEvent _connectEvent = new AutoResetEvent(false); + private ProxyConnectResponse _connectResponse; + + private int _receiveTimeout = -1; + private readonly AutoResetEvent _receiveEvent = new AutoResetEvent(false); + private readonly Queue _receiveQueue = new Queue(); + + // private int _sendTimeout = -1; // Sends are techically instant right now, so not _really_ used. + + private bool _connecting; + private bool _broadcast; + private bool _readShutdown; + // private bool _writeShutdown; + private bool _closed; + + private readonly Dictionary _socketOptions = new Dictionary() + { + { SocketOptionName.Broadcast, 0 }, //TODO: honor this value + { SocketOptionName.DontLinger, 0 }, + { SocketOptionName.Debug, 0 }, + { SocketOptionName.Error, 0 }, + { SocketOptionName.KeepAlive, 0 }, + { SocketOptionName.OutOfBandInline, 0 }, + { SocketOptionName.ReceiveBuffer, 131072 }, + { SocketOptionName.ReceiveTimeout, -1 }, + { SocketOptionName.SendBuffer, 131072 }, + { SocketOptionName.SendTimeout, -1 }, + { SocketOptionName.Type, 0 }, + { SocketOptionName.ReuseAddress, 0 } //TODO: honor this value + }; + + public EndPoint RemoteEndPoint { get; private set; } + + public EndPoint LocalEndPoint { get; private set; } + + public bool Connected { get; private set; } + + public bool IsBound { get; private set; } + + public AddressFamily AddressFamily { get; } + + public SocketType SocketType { get; } + + public ProtocolType ProtocolType { get; } + + public bool Blocking { get; set; } + + public int Available + { + get + { + int result = 0; + + lock (_receiveQueue) + { + foreach (ProxyDataPacket data in _receiveQueue) + { + result += data.Data.Length; + } + } + + return result; + } + } + + public bool Readable + { + get + { + if (_isListening) + { + lock (_connectRequests) + { + return _connectRequests.Count > 0; + } + } + else + { + if (_readShutdown) + { + return true; + } + + lock (_receiveQueue) + { + return _receiveQueue.Count > 0; + } + } + + } + } + public bool Writable => Connected || ProtocolType == ProtocolType.Udp; + public bool Error => false; + + public LdnProxySocket(AddressFamily addressFamily, SocketType socketType, ProtocolType protocolType, LdnProxy proxy) + { + AddressFamily = addressFamily; + SocketType = socketType; + ProtocolType = protocolType; + + _proxy = proxy; + _socketOptions[SocketOptionName.Type] = (int)socketType; + + proxy.RegisterSocket(this); + } + + private IPEndPoint EnsureLocalEndpoint(bool replace) + { + if (LocalEndPoint != null) + { + if (replace) + { + _proxy.ReturnEphemeralPort(ProtocolType, (ushort)((IPEndPoint)LocalEndPoint).Port); + } + else + { + return (IPEndPoint)LocalEndPoint; + } + } + + IPEndPoint localEp = new IPEndPoint(_proxy.LocalAddress, _proxy.GetEphemeralPort(ProtocolType)); + LocalEndPoint = localEp; + + return localEp; + } + + public LdnProxySocket AsAccepted(IPEndPoint remoteEp) + { + Connected = true; + RemoteEndPoint = remoteEp; + + IPEndPoint localEp = EnsureLocalEndpoint(true); + + _proxy.SignalConnected(localEp, remoteEp, ProtocolType); + + return this; + } + + private void SignalError(WsaError error) + { + lock (_errors) + { + _errors.Enqueue((int)error); + } + } + + private IPEndPoint GetEndpoint(uint ipv4, ushort port) + { + byte[] address = BitConverter.GetBytes(ipv4); + Array.Reverse(address); + + return new IPEndPoint(new IPAddress(address), port); + } + + public void IncomingData(ProxyDataPacket packet) + { + bool isBroadcast = _proxy.IsBroadcast(packet.Header.Info.DestIpV4); + + if (!_closed && (_broadcast || !isBroadcast)) + { + lock (_receiveQueue) + { + _receiveQueue.Enqueue(packet); + } + } + } + + public ISocketImpl Accept() + { + if (!_isListening) + { + throw new InvalidOperationException(); + } + + // Accept a pending request to this socket. + + lock (_connectRequests) + { + if (!Blocking && _connectRequests.Count == 0) + { + throw new SocketException((int)WsaError.WSAEWOULDBLOCK); + } + } + + while (true) + { + _acceptEvent.WaitOne(_acceptTimeout); + + lock (_connectRequests) + { + while (_connectRequests.Count > 0) + { + ProxyConnectRequest request = _connectRequests.Dequeue(); + + if (_connectRequests.Count > 0) + { + _acceptEvent.Set(); // Still more accepts to do. + } + + // Is this request made for us? + IPEndPoint endpoint = GetEndpoint(request.Info.DestIpV4, request.Info.DestPort); + + if (Equals(endpoint, LocalEndPoint)) + { + // Yes - let's accept. + IPEndPoint remoteEndpoint = GetEndpoint(request.Info.SourceIpV4, request.Info.SourcePort); + + LdnProxySocket socket = new LdnProxySocket(AddressFamily, SocketType, ProtocolType, _proxy).AsAccepted(remoteEndpoint); + + lock (_listenSockets) + { + _listenSockets.Add(socket); + } + + return socket; + } + } + } + } + } + + public void Bind(EndPoint localEP) + { + ArgumentNullException.ThrowIfNull(localEP); + + if (LocalEndPoint != null) + { + _proxy.ReturnEphemeralPort(ProtocolType, (ushort)((IPEndPoint)LocalEndPoint).Port); + } + var asIPEndpoint = (IPEndPoint)localEP; + if (asIPEndpoint.Port == 0) + { + asIPEndpoint.Port = (ushort)_proxy.GetEphemeralPort(ProtocolType); + } + + LocalEndPoint = (IPEndPoint)localEP; + + IsBound = true; + } + + public void Close() + { + _closed = true; + + _proxy.UnregisterSocket(this); + + if (Connected) + { + Disconnect(false); + } + + lock (_listenSockets) + { + foreach (LdnProxySocket socket in _listenSockets) + { + socket.Close(); + } + } + + _isListening = false; + } + + public void Connect(EndPoint remoteEP) + { + if (_isListening || !IsBound) + { + throw new InvalidOperationException(); + } + + if (remoteEP is not IPEndPoint) + { + throw new NotSupportedException(); + } + + IPEndPoint localEp = EnsureLocalEndpoint(true); + + _connecting = true; + + _proxy.RequestConnection(localEp, (IPEndPoint)remoteEP, ProtocolType); + + if (!Blocking && ProtocolType == ProtocolType.Tcp) + { + throw new SocketException((int)WsaError.WSAEWOULDBLOCK); + } + + _connectEvent.WaitOne(); //timeout? + + if (_connectResponse.Info.SourceIpV4 == 0) + { + throw new SocketException((int)WsaError.WSAECONNREFUSED); + } + + _connectResponse = default; + } + + public void HandleConnectResponse(ProxyConnectResponse obj) + { + if (!_connecting) + { + return; + } + + _connecting = false; + + if (_connectResponse.Info.SourceIpV4 != 0) + { + IPEndPoint remoteEp = GetEndpoint(obj.Info.SourceIpV4, obj.Info.SourcePort); + RemoteEndPoint = remoteEp; + + Connected = true; + } + else + { + // Connection failed + + SignalError(WsaError.WSAECONNREFUSED); + } + } + + public void Disconnect(bool reuseSocket) + { + if (Connected) + { + ConnectionEnded(); + + // The other side needs to be notified that connection ended. + _proxy.EndConnection(LocalEndPoint as IPEndPoint, RemoteEndPoint as IPEndPoint, ProtocolType); + } + } + + private void ConnectionEnded() + { + if (Connected) + { + RemoteEndPoint = null; + Connected = false; + } + } + + public void GetSocketOption(SocketOptionLevel optionLevel, SocketOptionName optionName, byte[] optionValue) + { + if (optionLevel != SocketOptionLevel.Socket) + { + throw new NotImplementedException(); + } + + if (_socketOptions.TryGetValue(optionName, out int result)) + { + byte[] data = BitConverter.GetBytes(result); + Array.Copy(data, 0, optionValue, 0, Math.Min(data.Length, optionValue.Length)); + } + else + { + throw new NotImplementedException(); + } + } + + public void Listen(int backlog) + { + if (!IsBound) + { + throw new SocketException(); + } + + _isListening = true; + } + + public void HandleConnectRequest(ProxyConnectRequest obj) + { + lock (_connectRequests) + { + _connectRequests.Enqueue(obj); + } + + _connectEvent.Set(); + } + + public void HandleDisconnect(ProxyDisconnectMessage message) + { + Disconnect(false); + } + + public int Receive(Span buffer) + { + EndPoint dummy = new IPEndPoint(IPAddress.Any, 0); + + return ReceiveFrom(buffer, SocketFlags.None, ref dummy); + } + + public int Receive(Span buffer, SocketFlags flags) + { + EndPoint dummy = new IPEndPoint(IPAddress.Any, 0); + + return ReceiveFrom(buffer, flags, ref dummy); + } + + public int Receive(Span buffer, SocketFlags flags, out SocketError socketError) + { + EndPoint dummy = new IPEndPoint(IPAddress.Any, 0); + + return ReceiveFrom(buffer, flags, out socketError, ref dummy); + } + + public int ReceiveFrom(Span buffer, SocketFlags flags, ref EndPoint remoteEp) + { + // We just receive all packets meant for us anyways regardless of EP in the actual implementation. + // The point is mostly to return the endpoint that we got the data from. + + if (!Connected && ProtocolType == ProtocolType.Tcp) + { + throw new SocketException((int)WsaError.WSAECONNRESET); + } + + lock (_receiveQueue) + { + if (_receiveQueue.Count > 0) + { + return ReceiveFromQueue(buffer, flags, ref remoteEp); + } + else if (_readShutdown) + { + return 0; + } + else if (!Blocking) + { + throw new SocketException((int)WsaError.WSAEWOULDBLOCK); + } + } + + int timeout = _receiveTimeout; + + _receiveEvent.WaitOne(timeout == 0 ? -1 : timeout); + + if (!Connected && ProtocolType == ProtocolType.Tcp) + { + throw new SocketException((int)WsaError.WSAECONNRESET); + } + + lock (_receiveQueue) + { + if (_receiveQueue.Count > 0) + { + return ReceiveFromQueue(buffer, flags, ref remoteEp); + } + else if (_readShutdown) + { + return 0; + } + else + { + throw new SocketException((int)WsaError.WSAETIMEDOUT); + } + } + } + + public int ReceiveFrom(Span buffer, SocketFlags flags, out SocketError socketError, ref EndPoint remoteEp) + { + // We just receive all packets meant for us anyways regardless of EP in the actual implementation. + // The point is mostly to return the endpoint that we got the data from. + + if (!Connected && ProtocolType == ProtocolType.Tcp) + { + socketError = SocketError.ConnectionReset; + return -1; + } + + lock (_receiveQueue) + { + if (_receiveQueue.Count > 0) + { + return ReceiveFromQueue(buffer, flags, out socketError, ref remoteEp); + } + else if (_readShutdown) + { + socketError = SocketError.Success; + return 0; + } + else if (!Blocking) + { + throw new SocketException((int)WsaError.WSAEWOULDBLOCK); + } + } + + int timeout = _receiveTimeout; + + _receiveEvent.WaitOne(timeout == 0 ? -1 : timeout); + + if (!Connected && ProtocolType == ProtocolType.Tcp) + { + throw new SocketException((int)WsaError.WSAECONNRESET); + } + + lock (_receiveQueue) + { + if (_receiveQueue.Count > 0) + { + return ReceiveFromQueue(buffer, flags, out socketError, ref remoteEp); + } + else if (_readShutdown) + { + socketError = SocketError.Success; + return 0; + } + else + { + socketError = SocketError.TimedOut; + return -1; + } + } + } + + private int ReceiveFromQueue(Span buffer, SocketFlags flags, ref EndPoint remoteEp) + { + int size = buffer.Length; + + // Assumes we have the receive queue lock, and at least one item in the queue. + ProxyDataPacket packet = _receiveQueue.Peek(); + + remoteEp = GetEndpoint(packet.Header.Info.SourceIpV4, packet.Header.Info.SourcePort); + + bool peek = (flags & SocketFlags.Peek) != 0; + + int read; + + if (packet.Data.Length > size) + { + read = size; + + // Cannot fit in the output buffer. Copy up to what we've got. + packet.Data.AsSpan(0, size).CopyTo(buffer); + + if (ProtocolType == ProtocolType.Udp) + { + // Udp overflows, loses the data, then throws an exception. + + if (!peek) + { + _receiveQueue.Dequeue(); + } + + throw new SocketException((int)WsaError.WSAEMSGSIZE); + } + else if (ProtocolType == ProtocolType.Tcp) + { + // Split the data at the buffer boundary. It will stay on the recieve queue. + + byte[] newData = new byte[packet.Data.Length - size]; + Array.Copy(packet.Data, size, newData, 0, newData.Length); + + packet.Data = newData; + } + } + else + { + read = packet.Data.Length; + + packet.Data.AsSpan(0, packet.Data.Length).CopyTo(buffer); + + if (!peek) + { + _receiveQueue.Dequeue(); + } + } + + return read; + } + + private int ReceiveFromQueue(Span buffer, SocketFlags flags, out SocketError socketError, ref EndPoint remoteEp) + { + int size = buffer.Length; + + // Assumes we have the receive queue lock, and at least one item in the queue. + ProxyDataPacket packet = _receiveQueue.Peek(); + + remoteEp = GetEndpoint(packet.Header.Info.SourceIpV4, packet.Header.Info.SourcePort); + + bool peek = (flags & SocketFlags.Peek) != 0; + + int read; + + if (packet.Data.Length > size) + { + read = size; + + // Cannot fit in the output buffer. Copy up to what we've got. + packet.Data.AsSpan(0, size).CopyTo(buffer); + + if (ProtocolType == ProtocolType.Udp) + { + // Udp overflows, loses the data, then throws an exception. + + if (!peek) + { + _receiveQueue.Dequeue(); + } + + socketError = SocketError.MessageSize; + return -1; + } + else if (ProtocolType == ProtocolType.Tcp) + { + // Split the data at the buffer boundary. It will stay on the recieve queue. + + byte[] newData = new byte[packet.Data.Length - size]; + Array.Copy(packet.Data, size, newData, 0, newData.Length); + + packet.Data = newData; + } + } + else + { + read = packet.Data.Length; + + packet.Data.AsSpan(0, packet.Data.Length).CopyTo(buffer); + + if (!peek) + { + _receiveQueue.Dequeue(); + } + } + + socketError = SocketError.Success; + + return read; + } + + public int Send(ReadOnlySpan buffer) + { + // Send to the remote host chosen when we "connect" or "accept". + if (!Connected) + { + throw new SocketException(); + } + + return SendTo(buffer, SocketFlags.None, RemoteEndPoint); + } + + public int Send(ReadOnlySpan buffer, SocketFlags flags) + { + // Send to the remote host chosen when we "connect" or "accept". + if (!Connected) + { + throw new SocketException(); + } + + return SendTo(buffer, flags, RemoteEndPoint); + } + + public int Send(ReadOnlySpan buffer, SocketFlags flags, out SocketError socketError) + { + // Send to the remote host chosen when we "connect" or "accept". + if (!Connected) + { + throw new SocketException(); + } + + return SendTo(buffer, flags, out socketError, RemoteEndPoint); + } + + public int SendTo(ReadOnlySpan buffer, SocketFlags flags, EndPoint remoteEP) + { + if (!Connected && ProtocolType == ProtocolType.Tcp) + { + throw new SocketException((int)WsaError.WSAECONNRESET); + } + + IPEndPoint localEp = EnsureLocalEndpoint(false); + + if (remoteEP is not IPEndPoint) + { + throw new NotSupportedException(); + } + + return _proxy.SendTo(buffer, flags, localEp, (IPEndPoint)remoteEP, ProtocolType); + } + + public int SendTo(ReadOnlySpan buffer, SocketFlags flags, out SocketError socketError, EndPoint remoteEP) + { + if (!Connected && ProtocolType == ProtocolType.Tcp) + { + socketError = SocketError.ConnectionReset; + return -1; + } + + IPEndPoint localEp = EnsureLocalEndpoint(false); + + if (remoteEP is not IPEndPoint) + { + // throw new NotSupportedException(); + socketError = SocketError.OperationNotSupported; + return -1; + } + + socketError = SocketError.Success; + + return _proxy.SendTo(buffer, flags, localEp, (IPEndPoint)remoteEP, ProtocolType); + } + + public bool Poll(int microSeconds, SelectMode mode) + { + return mode switch + { + SelectMode.SelectRead => Readable, + SelectMode.SelectWrite => Writable, + SelectMode.SelectError => Error, + _ => false + }; + } + + public void SetSocketOption(SocketOptionLevel optionLevel, SocketOptionName optionName, int optionValue) + { + if (optionLevel != SocketOptionLevel.Socket) + { + throw new NotImplementedException(); + } + + switch (optionName) + { + case SocketOptionName.SendTimeout: + //_sendTimeout = optionValue; + break; + case SocketOptionName.ReceiveTimeout: + _receiveTimeout = optionValue; + break; + case SocketOptionName.Broadcast: + _broadcast = optionValue != 0; + break; + } + + lock (_socketOptions) + { + _socketOptions[optionName] = optionValue; + } + } + + public void SetSocketOption(SocketOptionLevel optionLevel, SocketOptionName optionName, object optionValue) + { + // Just linger uses this for now in BSD, which we ignore. + } + + public void Shutdown(SocketShutdown how) + { + switch (how) + { + case SocketShutdown.Both: + _readShutdown = true; + // _writeShutdown = true; + break; + case SocketShutdown.Receive: + _readShutdown = true; + break; + case SocketShutdown.Send: + // _writeShutdown = true; + break; + } + } + + public void ProxyDestroyed() + { + // Do nothing, for now. Will likely be more useful with TCP. + } + + public void Dispose() + { + + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/P2pProxyClient.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/P2pProxyClient.cs new file mode 100644 index 000000000..7da1aa998 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/P2pProxyClient.cs @@ -0,0 +1,93 @@ +using Ryujinx.Common.Logging; +using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types; +using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types; +using Ryujinx.HLE.HOS.Services.Sockets.Bsd.Proxy; +using System.Net.Sockets; +using System.Threading; +using TcpClient = NetCoreServer.TcpClient; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Proxy +{ + class P2pProxyClient : TcpClient, IProxyClient + { + private const int FailureTimeout = 4000; + + public ProxyConfig ProxyConfig { get; private set; } + + private readonly RyuLdnProtocol _protocol; + + private readonly ManualResetEvent _connected = new ManualResetEvent(false); + private readonly ManualResetEvent _ready = new ManualResetEvent(false); + private readonly AutoResetEvent _error = new AutoResetEvent(false); + + public P2pProxyClient(string address, int port) : base(address, port) + { + if (ProxyHelpers.SupportsNoDelay()) + { + OptionNoDelay = true; + } + + _protocol = new RyuLdnProtocol(); + + _protocol.ProxyConfig += HandleProxyConfig; + + ConnectAsync(); + } + + protected override void OnConnected() + { + Logger.Info?.PrintMsg(LogClass.ServiceLdn, $"Proxy TCP client connected a new session with Id {Id}"); + + _connected.Set(); + } + + protected override void OnDisconnected() + { + Logger.Info?.PrintMsg(LogClass.ServiceLdn, $"Proxy TCP client disconnected a session with Id {Id}"); + + SocketHelpers.UnregisterProxy(); + + _connected.Reset(); + } + + protected override void OnReceived(byte[] buffer, long offset, long size) + { + _protocol.Read(buffer, (int)offset, (int)size); + } + + protected override void OnError(SocketError error) + { + Logger.Info?.PrintMsg(LogClass.ServiceLdn, $"Proxy TCP client caught an error with code {error}"); + + _error.Set(); + } + + private void HandleProxyConfig(LdnHeader header, ProxyConfig config) + { + ProxyConfig = config; + + SocketHelpers.RegisterProxy(new LdnProxy(config, this, _protocol)); + + _ready.Set(); + } + + public bool EnsureProxyReady() + { + return _ready.WaitOne(FailureTimeout); + } + + public bool PerformAuth(ExternalProxyConfig config) + { + bool signalled = _connected.WaitOne(FailureTimeout); + + if (!signalled) + { + return false; + } + + SendAsync(_protocol.Encode(PacketId.ExternalProxy, config)); + + return true; + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/P2pProxyServer.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/P2pProxyServer.cs new file mode 100644 index 000000000..598fb654f --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/P2pProxyServer.cs @@ -0,0 +1,388 @@ +using NetCoreServer; +using Open.Nat; +using Ryujinx.Common.Logging; +using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types; +using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Proxy +{ + class P2pProxyServer : TcpServer, IDisposable + { + public const ushort PrivatePortBase = 39990; + public const int PrivatePortRange = 10; + + private const ushort PublicPortBase = 39990; + private const int PublicPortRange = 10; + + private const ushort PortLeaseLength = 60; + private const ushort PortLeaseRenew = 50; + + private const ushort AuthWaitSeconds = 1; + + private readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion); + + public ushort PrivatePort { get; } + + private ushort _publicPort; + + private bool _disposed; + private readonly CancellationTokenSource _disposedCancellation = new CancellationTokenSource(); + + private NatDevice _natDevice; + private Mapping _portMapping; + + private readonly List _players = new List(); + + private readonly List _waitingTokens = new List(); + private readonly AutoResetEvent _tokenEvent = new AutoResetEvent(false); + + private uint _broadcastAddress; + + private readonly LdnMasterProxyClient _master; + private readonly RyuLdnProtocol _masterProtocol; + private readonly RyuLdnProtocol _protocol; + + public P2pProxyServer(LdnMasterProxyClient master, ushort port, RyuLdnProtocol masterProtocol) : base(IPAddress.Any, port) + { + if (ProxyHelpers.SupportsNoDelay()) + { + OptionNoDelay = true; + } + + PrivatePort = port; + + _master = master; + _masterProtocol = masterProtocol; + + _masterProtocol.ExternalProxyState += HandleStateChange; + _masterProtocol.ExternalProxyToken += HandleToken; + + _protocol = new RyuLdnProtocol(); + } + + private void HandleToken(LdnHeader header, ExternalProxyToken token) + { + _lock.EnterWriteLock(); + + _waitingTokens.Add(token); + + _lock.ExitWriteLock(); + + _tokenEvent.Set(); + } + + private void HandleStateChange(LdnHeader header, ExternalProxyConnectionState state) + { + if (!state.Connected) + { + _lock.EnterWriteLock(); + + _waitingTokens.RemoveAll(token => token.VirtualIp == state.IpAddress); + + _players.RemoveAll(player => + { + if (player.VirtualIpAddress == state.IpAddress) + { + player.DisconnectAndStop(); + + return true; + } + + return false; + }); + + _lock.ExitWriteLock(); + } + } + + public void Configure(ProxyConfig config) + { + _broadcastAddress = config.ProxyIp | (~config.ProxySubnetMask); + } + + public async Task NatPunch() + { + NatDiscoverer discoverer = new NatDiscoverer(); + CancellationTokenSource cts = new CancellationTokenSource(1000); + + NatDevice device; + + try + { + device = await discoverer.DiscoverDeviceAsync(PortMapper.Upnp, cts); + } + catch (NatDeviceNotFoundException) + { + return 0; + } + + _publicPort = PublicPortBase; + + for (int i = 0; i < PublicPortRange; i++) + { + try + { + _portMapping = new Mapping(Protocol.Tcp, PrivatePort, _publicPort, PortLeaseLength, "Ryujinx Local Multiplayer"); + + await device.CreatePortMapAsync(_portMapping); + + break; + } + catch (MappingException) + { + _publicPort++; + } + catch (Exception) + { + return 0; + } + + if (i == PublicPortRange - 1) + { + _publicPort = 0; + } + } + + if (_publicPort != 0) + { + _ = Task.Delay(PortLeaseRenew * 1000, _disposedCancellation.Token).ContinueWith((task) => Task.Run(RefreshLease)); + } + + _natDevice = device; + + return _publicPort; + } + + // Proxy handlers + + private void RouteMessage(P2pProxySession sender, ref ProxyInfo info, Action action) + { + if (info.SourceIpV4 == 0) + { + // If they sent from a connection bound on 0.0.0.0, make others see it as them. + info.SourceIpV4 = sender.VirtualIpAddress; + } + else if (info.SourceIpV4 != sender.VirtualIpAddress) + { + // Can't pretend to be somebody else. + return; + } + + uint destIp = info.DestIpV4; + + if (destIp == 0xc0a800ff) + { + destIp = _broadcastAddress; + } + + bool isBroadcast = destIp == _broadcastAddress; + + _lock.EnterReadLock(); + + if (isBroadcast) + { + _players.ForEach(player => + { + action(player); + }); + } + else + { + P2pProxySession target = _players.FirstOrDefault(player => player.VirtualIpAddress == destIp); + + if (target != null) + { + action(target); + } + } + + _lock.ExitReadLock(); + } + + public void HandleProxyDisconnect(P2pProxySession sender, LdnHeader header, ProxyDisconnectMessage message) + { + RouteMessage(sender, ref message.Info, (target) => + { + target.SendAsync(sender.Protocol.Encode(PacketId.ProxyDisconnect, message)); + }); + } + + public void HandleProxyData(P2pProxySession sender, LdnHeader header, ProxyDataHeader message, byte[] data) + { + RouteMessage(sender, ref message.Info, (target) => + { + target.SendAsync(sender.Protocol.Encode(PacketId.ProxyData, message, data)); + }); + } + + public void HandleProxyConnectReply(P2pProxySession sender, LdnHeader header, ProxyConnectResponse message) + { + RouteMessage(sender, ref message.Info, (target) => + { + target.SendAsync(sender.Protocol.Encode(PacketId.ProxyConnectReply, message)); + }); + } + + public void HandleProxyConnect(P2pProxySession sender, LdnHeader header, ProxyConnectRequest message) + { + RouteMessage(sender, ref message.Info, (target) => + { + target.SendAsync(sender.Protocol.Encode(PacketId.ProxyConnect, message)); + }); + } + + // End proxy handlers + + private async Task RefreshLease() + { + if (_disposed || _natDevice == null) + { + return; + } + + try + { + await _natDevice.CreatePortMapAsync(_portMapping); + } + catch (Exception) + { + + } + + _ = Task.Delay(PortLeaseRenew, _disposedCancellation.Token).ContinueWith((task) => Task.Run(RefreshLease)); + } + + public bool TryRegisterUser(P2pProxySession session, ExternalProxyConfig config) + { + _lock.EnterWriteLock(); + + // Attempt to find matching configuration. If we don't find one, wait for a bit and try again. + // Woken by new tokens coming in from the master server. + + IPAddress address = (session.Socket.RemoteEndPoint as IPEndPoint).Address; + byte[] addressBytes = ProxyHelpers.AddressTo16Byte(address); + + long time; + long endTime = Stopwatch.GetTimestamp() + Stopwatch.Frequency * AuthWaitSeconds; + + do + { + for (int i = 0; i < _waitingTokens.Count; i++) + { + ExternalProxyToken waitToken = _waitingTokens[i]; + + // Allow any client that has a private IP to connect. (indicated by the server as all 0 in the token) + + bool isPrivate = waitToken.PhysicalIp.AsSpan().SequenceEqual(new byte[16]); + bool ipEqual = isPrivate || waitToken.AddressFamily == address.AddressFamily && waitToken.PhysicalIp.AsSpan().SequenceEqual(addressBytes); + + if (ipEqual && waitToken.Token.AsSpan().SequenceEqual(config.Token.AsSpan())) + { + // This is a match. + + _waitingTokens.RemoveAt(i); + + session.SetIpv4(waitToken.VirtualIp); + + ProxyConfig pconfig = new ProxyConfig + { + ProxyIp = session.VirtualIpAddress, + ProxySubnetMask = 0xFFFF0000 // TODO: Use from server. + }; + + if (_players.Count == 0) + { + Configure(pconfig); + } + + _players.Add(session); + + session.SendAsync(_protocol.Encode(PacketId.ProxyConfig, pconfig)); + + _lock.ExitWriteLock(); + + return true; + } + } + + // Couldn't find the token. + // It may not have arrived yet, so wait for one to arrive. + + _lock.ExitWriteLock(); + + time = Stopwatch.GetTimestamp(); + int remainingMs = (int)((endTime - time) / (Stopwatch.Frequency / 1000)); + + if (remainingMs < 0) + { + remainingMs = 0; + } + + _tokenEvent.WaitOne(remainingMs); + + _lock.EnterWriteLock(); + + } while (time < endTime); + + _lock.ExitWriteLock(); + + return false; + } + + public void DisconnectProxyClient(P2pProxySession session) + { + _lock.EnterWriteLock(); + + bool removed = _players.Remove(session); + + if (removed) + { + _master.SendAsync(_masterProtocol.Encode(PacketId.ExternalProxyState, new ExternalProxyConnectionState + { + IpAddress = session.VirtualIpAddress, + Connected = false + })); + } + + _lock.ExitWriteLock(); + } + + public new void Dispose() + { + base.Dispose(); + + _disposed = true; + _disposedCancellation.Cancel(); + + try + { + Task delete = _natDevice?.DeletePortMapAsync(new Mapping(Protocol.Tcp, PrivatePort, _publicPort, 60, "Ryujinx Local Multiplayer")); + + // Just absorb any exceptions. + delete?.ContinueWith((task) => { }); + } + catch (Exception) + { + // Fail silently. + } + } + + protected override TcpSession CreateSession() + { + return new P2pProxySession(this); + } + + protected override void OnError(SocketError error) + { + Logger.Info?.PrintMsg(LogClass.ServiceLdn, $"Proxy TCP server caught an error with code {error}"); + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/P2pProxySession.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/P2pProxySession.cs new file mode 100644 index 000000000..515feeac5 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/P2pProxySession.cs @@ -0,0 +1,90 @@ +using NetCoreServer; +using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types; +using System; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Proxy +{ + class P2pProxySession : TcpSession + { + public uint VirtualIpAddress { get; private set; } + public RyuLdnProtocol Protocol { get; } + + private readonly P2pProxyServer _parent; + + private bool _masterClosed; + + public P2pProxySession(P2pProxyServer server) : base(server) + { + _parent = server; + + Protocol = new RyuLdnProtocol(); + + Protocol.ProxyDisconnect += HandleProxyDisconnect; + Protocol.ProxyData += HandleProxyData; + Protocol.ProxyConnectReply += HandleProxyConnectReply; + Protocol.ProxyConnect += HandleProxyConnect; + + Protocol.ExternalProxy += HandleAuthentication; + } + + private void HandleAuthentication(LdnHeader header, ExternalProxyConfig token) + { + if (!_parent.TryRegisterUser(this, token)) + { + Disconnect(); + } + } + + public void SetIpv4(uint ip) + { + VirtualIpAddress = ip; + } + + public void DisconnectAndStop() + { + _masterClosed = true; + + Disconnect(); + } + + protected override void OnDisconnected() + { + if (!_masterClosed) + { + _parent.DisconnectProxyClient(this); + } + } + + protected override void OnReceived(byte[] buffer, long offset, long size) + { + try + { + Protocol.Read(buffer, (int)offset, (int)size); + } + catch (Exception) + { + Disconnect(); + } + } + + private void HandleProxyDisconnect(LdnHeader header, ProxyDisconnectMessage message) + { + _parent.HandleProxyDisconnect(this, header, message); + } + + private void HandleProxyData(LdnHeader header, ProxyDataHeader message, byte[] data) + { + _parent.HandleProxyData(this, header, message, data); + } + + private void HandleProxyConnectReply(LdnHeader header, ProxyConnectResponse data) + { + _parent.HandleProxyConnectReply(this, header, data); + } + + private void HandleProxyConnect(LdnHeader header, ProxyConnectRequest message) + { + _parent.HandleProxyConnect(this, header, message); + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/ProxyHelpers.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/ProxyHelpers.cs new file mode 100644 index 000000000..42b1ab6a2 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/ProxyHelpers.cs @@ -0,0 +1,24 @@ +using System; +using System.Net; +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Proxy +{ + static class ProxyHelpers + { + public static byte[] AddressTo16Byte(IPAddress address) + { + byte[] ipBytes = new byte[16]; + byte[] srcBytes = address.GetAddressBytes(); + + Array.Copy(srcBytes, 0, ipBytes, 0, srcBytes.Length); + + return ipBytes; + } + + public static bool SupportsNoDelay() + { + return RuntimeInformation.IsOSPlatform(OSPlatform.Windows); + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/RyuLdnProtocol.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/RyuLdnProtocol.cs new file mode 100644 index 000000000..d0eeaf125 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/RyuLdnProtocol.cs @@ -0,0 +1,380 @@ +using Ryujinx.Common.Utilities; +using Ryujinx.HLE.HOS.Services.Ldn.Types; +using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types; +using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types; +using System; +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu +{ + class RyuLdnProtocol + { + private const byte CurrentProtocolVersion = 1; + private const int Magic = ('R' << 0) | ('L' << 8) | ('D' << 16) | ('N' << 24); + private const int MaxPacketSize = 131072; + + private readonly int _headerSize = Marshal.SizeOf(); + + private readonly byte[] _buffer = new byte[MaxPacketSize]; + private int _bufferEnd = 0; + + // Client Packets. + public event Action Initialize; + public event Action Passphrase; + public event Action Connected; + public event Action SyncNetwork; + public event Action ScanReply; + public event Action ScanReplyEnd; + public event Action Disconnected; + + // External Proxy Packets. + public event Action ExternalProxy; + public event Action ExternalProxyState; + public event Action ExternalProxyToken; + + // Server Packets. + public event Action CreateAccessPoint; + public event Action CreateAccessPointPrivate; + public event Action Reject; + public event Action RejectReply; + public event Action SetAcceptPolicy; + public event Action SetAdvertiseData; + public event Action Connect; + public event Action ConnectPrivate; + public event Action Scan; + + // Proxy Packets. + public event Action ProxyConfig; + public event Action ProxyConnect; + public event Action ProxyConnectReply; + public event Action ProxyData; + public event Action ProxyDisconnect; + + // Lifecycle Packets. + public event Action NetworkError; + public event Action Ping; + + public RyuLdnProtocol() { } + + public void Reset() + { + _bufferEnd = 0; + } + + public void Read(byte[] data, int offset, int size) + { + int index = 0; + + while (index < size) + { + if (_bufferEnd < _headerSize) + { + // Assemble the header first. + + int copyable = Math.Min(size - index, Math.Min(size, _headerSize - _bufferEnd)); + + Array.Copy(data, index + offset, _buffer, _bufferEnd, copyable); + + index += copyable; + _bufferEnd += copyable; + } + + if (_bufferEnd >= _headerSize) + { + // The header is available. Make sure we received all the data (size specified in the header) + + LdnHeader ldnHeader = MemoryMarshal.Cast(_buffer)[0]; + + if (ldnHeader.Magic != Magic) + { + throw new InvalidOperationException("Invalid magic number in received packet."); + } + + if (ldnHeader.Version != CurrentProtocolVersion) + { + throw new InvalidOperationException($"Protocol version mismatch. Expected ${CurrentProtocolVersion}, was ${ldnHeader.Version}."); + } + + int finalSize = _headerSize + ldnHeader.DataSize; + + if (finalSize >= MaxPacketSize) + { + throw new InvalidOperationException($"Max packet size {MaxPacketSize} exceeded."); + } + + int copyable = Math.Min(size - index, Math.Min(size, finalSize - _bufferEnd)); + + Array.Copy(data, index + offset, _buffer, _bufferEnd, copyable); + + index += copyable; + _bufferEnd += copyable; + + if (finalSize == _bufferEnd) + { + // The full packet has been retrieved. Send it to be decoded. + + byte[] ldnData = new byte[ldnHeader.DataSize]; + + Array.Copy(_buffer, _headerSize, ldnData, 0, ldnData.Length); + + DecodeAndHandle(ldnHeader, ldnData); + + Reset(); + } + } + } + } + + private (T, byte[]) ParseWithData(byte[] data) where T : struct + { + T str = default; + int size = Marshal.SizeOf(str); + + byte[] remainder = new byte[data.Length - size]; + + if (remainder.Length > 0) + { + Array.Copy(data, size, remainder, 0, remainder.Length); + } + + return (MemoryMarshal.Read(data), remainder); + } + + private void DecodeAndHandle(LdnHeader header, byte[] data) + { + switch ((PacketId)header.Type) + { + // Client Packets. + case PacketId.Initialize: + { + Initialize?.Invoke(header, MemoryMarshal.Read(data)); + + break; + } + case PacketId.Passphrase: + { + Passphrase?.Invoke(header, MemoryMarshal.Read(data)); + + break; + } + case PacketId.Connected: + { + Connected?.Invoke(header, MemoryMarshal.Read(data)); + + break; + } + case PacketId.SyncNetwork: + { + SyncNetwork?.Invoke(header, MemoryMarshal.Read(data)); + + break; + } + case PacketId.ScanReply: + { + ScanReply?.Invoke(header, MemoryMarshal.Read(data)); + + break; + } + + case PacketId.ScanReplyEnd: + { + ScanReplyEnd?.Invoke(header); + + break; + } + case PacketId.Disconnect: + { + Disconnected?.Invoke(header, MemoryMarshal.Read(data)); + + break; + } + + // External Proxy Packets. + case PacketId.ExternalProxy: + { + ExternalProxy?.Invoke(header, MemoryMarshal.Read(data)); + + break; + } + case PacketId.ExternalProxyState: + { + ExternalProxyState?.Invoke(header, MemoryMarshal.Read(data)); + + break; + } + case PacketId.ExternalProxyToken: + { + ExternalProxyToken?.Invoke(header, MemoryMarshal.Read(data)); + + break; + } + + // Server Packets. + case PacketId.CreateAccessPoint: + { + (CreateAccessPointRequest packet, byte[] extraData) = ParseWithData(data); + CreateAccessPoint?.Invoke(header, packet, extraData); + break; + } + case PacketId.CreateAccessPointPrivate: + { + (CreateAccessPointPrivateRequest packet, byte[] extraData) = ParseWithData(data); + CreateAccessPointPrivate?.Invoke(header, packet, extraData); + break; + } + case PacketId.Reject: + { + Reject?.Invoke(header, MemoryMarshal.Read(data)); + + break; + } + case PacketId.RejectReply: + { + RejectReply?.Invoke(header); + + break; + } + case PacketId.SetAcceptPolicy: + { + SetAcceptPolicy?.Invoke(header, MemoryMarshal.Read(data)); + + break; + } + case PacketId.SetAdvertiseData: + { + SetAdvertiseData?.Invoke(header, data); + + break; + } + case PacketId.Connect: + { + Connect?.Invoke(header, MemoryMarshal.Read(data)); + + break; + } + case PacketId.ConnectPrivate: + { + ConnectPrivate?.Invoke(header, MemoryMarshal.Read(data)); + + break; + } + case PacketId.Scan: + { + Scan?.Invoke(header, MemoryMarshal.Read(data)); + + break; + } + + // Proxy Packets + case PacketId.ProxyConfig: + { + ProxyConfig?.Invoke(header, MemoryMarshal.Read(data)); + + break; + } + case PacketId.ProxyConnect: + { + ProxyConnect?.Invoke(header, MemoryMarshal.Read(data)); + + break; + } + case PacketId.ProxyConnectReply: + { + ProxyConnectReply?.Invoke(header, MemoryMarshal.Read(data)); + + break; + } + case PacketId.ProxyData: + { + (ProxyDataHeader packet, byte[] extraData) = ParseWithData(data); + + ProxyData?.Invoke(header, packet, extraData); + + break; + } + case PacketId.ProxyDisconnect: + { + ProxyDisconnect?.Invoke(header, MemoryMarshal.Read(data)); + + break; + } + + // Lifecycle Packets. + case PacketId.Ping: + { + Ping?.Invoke(header, MemoryMarshal.Read(data)); + + break; + } + case PacketId.NetworkError: + { + NetworkError?.Invoke(header, MemoryMarshal.Read(data)); + + break; + } + + default: + break; + } + } + + private static LdnHeader GetHeader(PacketId type, int dataSize) + { + return new LdnHeader() + { + Magic = Magic, + Version = CurrentProtocolVersion, + Type = (byte)type, + DataSize = dataSize + }; + } + + public byte[] Encode(PacketId type) + { + LdnHeader header = GetHeader(type, 0); + + return SpanHelpers.AsSpan(ref header).ToArray(); + } + + public byte[] Encode(PacketId type, byte[] data) + { + LdnHeader header = GetHeader(type, data.Length); + + byte[] result = SpanHelpers.AsSpan(ref header).ToArray(); + + Array.Resize(ref result, result.Length + data.Length); + Array.Copy(data, 0, result, Marshal.SizeOf(), data.Length); + + return result; + } + + public byte[] Encode(PacketId type, T packet) where T : unmanaged + { + byte[] packetData = SpanHelpers.AsSpan(ref packet).ToArray(); + + LdnHeader header = GetHeader(type, packetData.Length); + + byte[] result = SpanHelpers.AsSpan(ref header).ToArray(); + + Array.Resize(ref result, result.Length + packetData.Length); + Array.Copy(packetData, 0, result, Marshal.SizeOf(), packetData.Length); + + return result; + } + + public byte[] Encode(PacketId type, T packet, byte[] data) where T : unmanaged + { + byte[] packetData = SpanHelpers.AsSpan(ref packet).ToArray(); + + LdnHeader header = GetHeader(type, packetData.Length + data.Length); + + byte[] result = SpanHelpers.AsSpan(ref header).ToArray(); + + Array.Resize(ref result, result.Length + packetData.Length + data.Length); + Array.Copy(packetData, 0, result, Marshal.SizeOf(), packetData.Length); + Array.Copy(data, 0, result, Marshal.SizeOf() + packetData.Length, data.Length); + + return result; + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/DisconnectMessage.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/DisconnectMessage.cs new file mode 100644 index 000000000..448d33f29 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/DisconnectMessage.cs @@ -0,0 +1,10 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types +{ + [StructLayout(LayoutKind.Sequential, Size = 0x4)] + struct DisconnectMessage + { + public uint DisconnectIP; + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ExternalProxyConfig.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ExternalProxyConfig.cs new file mode 100644 index 000000000..9cbb80242 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ExternalProxyConfig.cs @@ -0,0 +1,19 @@ +using Ryujinx.Common.Memory; +using System.Net.Sockets; +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types +{ + /// + /// Sent by the server to point a client towards an external server being used as a proxy. + /// The client then forwards this to the external proxy after connecting, to verify the connection worked. + /// + [StructLayout(LayoutKind.Sequential, Size = 0x26, Pack = 1)] + struct ExternalProxyConfig + { + public Array16 ProxyIp; + public AddressFamily AddressFamily; + public ushort ProxyPort; + public Array16 Token; + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ExternalProxyConnectionState.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ExternalProxyConnectionState.cs new file mode 100644 index 000000000..ecf4e14f7 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ExternalProxyConnectionState.cs @@ -0,0 +1,18 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types +{ + /// + /// Indicates a change in connection state for the given client. + /// Is sent to notify the master server when connection is first established. + /// Can be sent by the external proxy to the master server to notify it of a proxy disconnect. + /// Can be sent by the master server to notify the external proxy of a user leaving a room. + /// Both will result in a force kick. + /// + [StructLayout(LayoutKind.Sequential, Size = 0x8, Pack = 4)] + struct ExternalProxyConnectionState + { + public uint IpAddress; + public bool Connected; + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ExternalProxyToken.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ExternalProxyToken.cs new file mode 100644 index 000000000..0a8980c37 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ExternalProxyToken.cs @@ -0,0 +1,20 @@ +using Ryujinx.Common.Memory; +using System.Net.Sockets; +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types +{ + /// + /// Sent by the master server to an external proxy to tell them someone is going to connect. + /// This drives authentication, and lets the proxy know what virtual IP to give to each joiner, + /// as these are managed by the master server. + /// + [StructLayout(LayoutKind.Sequential, Size = 0x28)] + struct ExternalProxyToken + { + public uint VirtualIp; + public Array16 Token; + public Array16 PhysicalIp; + public AddressFamily AddressFamily; + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/InitializeMessage.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/InitializeMessage.cs new file mode 100644 index 000000000..36ddc65fe --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/InitializeMessage.cs @@ -0,0 +1,20 @@ +using Ryujinx.Common.Memory; +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types +{ + /// + /// This message is first sent by the client to identify themselves. + /// If the server has a token+mac combo that matches the submission, then they are returned their new ID and mac address. (the mac is also reassigned to the new id) + /// Otherwise, they are returned a random mac address. + /// + [StructLayout(LayoutKind.Sequential, Size = 0x16)] + struct InitializeMessage + { + // All 0 if we don't have an ID yet. + public Array16 Id; + + // All 0 if we don't have a mac yet. + public Array6 MacAddress; + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/LdnHeader.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/LdnHeader.cs new file mode 100644 index 000000000..f41f15ab4 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/LdnHeader.cs @@ -0,0 +1,13 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types +{ + [StructLayout(LayoutKind.Sequential, Size = 0xA)] + struct LdnHeader + { + public uint Magic; + public byte Type; + public byte Version; + public int DataSize; + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/PacketId.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/PacketId.cs new file mode 100644 index 000000000..b8ef5fbc1 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/PacketId.cs @@ -0,0 +1,36 @@ +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types +{ + enum PacketId + { + Initialize, + Passphrase, + + CreateAccessPoint, + CreateAccessPointPrivate, + ExternalProxy, + ExternalProxyToken, + ExternalProxyState, + SyncNetwork, + Reject, + RejectReply, + Scan, + ScanReply, + ScanReplyEnd, + Connect, + ConnectPrivate, + Connected, + Disconnect, + + ProxyConfig, + ProxyConnect, + ProxyConnectReply, + ProxyData, + ProxyDisconnect, + + SetAcceptPolicy, + SetAdvertiseData, + + Ping = 254, + NetworkError = 255 + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/PassphraseMessage.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/PassphraseMessage.cs new file mode 100644 index 000000000..0deba0b07 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/PassphraseMessage.cs @@ -0,0 +1,11 @@ +using Ryujinx.Common.Memory; +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types +{ + [StructLayout(LayoutKind.Sequential, Size = 0x80)] + struct PassphraseMessage + { + public Array128 Passphrase; + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/PingMessage.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/PingMessage.cs new file mode 100644 index 000000000..135e39caa --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/PingMessage.cs @@ -0,0 +1,11 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types +{ + [StructLayout(LayoutKind.Sequential, Size = 0x2)] + struct PingMessage + { + public byte Requester; + public byte Id; + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ProxyConnectRequest.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ProxyConnectRequest.cs new file mode 100644 index 000000000..ffce77791 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ProxyConnectRequest.cs @@ -0,0 +1,10 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types +{ + [StructLayout(LayoutKind.Sequential, Size = 0x10)] + struct ProxyConnectRequest + { + public ProxyInfo Info; + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ProxyConnectResponse.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ProxyConnectResponse.cs new file mode 100644 index 000000000..de2e430fb --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ProxyConnectResponse.cs @@ -0,0 +1,10 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types +{ + [StructLayout(LayoutKind.Sequential, Size = 0x10)] + struct ProxyConnectResponse + { + public ProxyInfo Info; + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ProxyDataHeader.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ProxyDataHeader.cs new file mode 100644 index 000000000..e46a40692 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ProxyDataHeader.cs @@ -0,0 +1,14 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types +{ + /// + /// Represents data sent over a transport layer. + /// + [StructLayout(LayoutKind.Sequential, Size = 0x14)] + struct ProxyDataHeader + { + public ProxyInfo Info; + public uint DataLength; // Followed by the data with the specified byte length. + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ProxyDataPacket.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ProxyDataPacket.cs new file mode 100644 index 000000000..eb3648413 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ProxyDataPacket.cs @@ -0,0 +1,8 @@ +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types +{ + class ProxyDataPacket + { + public ProxyDataHeader Header; + public byte[] Data; + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ProxyDisconnectMessage.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ProxyDisconnectMessage.cs new file mode 100644 index 000000000..2154ae109 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ProxyDisconnectMessage.cs @@ -0,0 +1,11 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types +{ + [StructLayout(LayoutKind.Sequential, Size = 0x14)] + struct ProxyDisconnectMessage + { + public ProxyInfo Info; + public int DisconnectReason; + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ProxyInfo.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ProxyInfo.cs new file mode 100644 index 000000000..d9338f244 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ProxyInfo.cs @@ -0,0 +1,20 @@ +using System.Net.Sockets; +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types +{ + /// + /// Information included in all proxied communication. + /// + [StructLayout(LayoutKind.Sequential, Size = 0x10, Pack = 1)] + struct ProxyInfo + { + public uint SourceIpV4; + public ushort SourcePort; + + public uint DestIpV4; + public ushort DestPort; + + public ProtocolType Protocol; + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/RejectRequest.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/RejectRequest.cs new file mode 100644 index 000000000..1c2ce1f8b --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/RejectRequest.cs @@ -0,0 +1,18 @@ +using Ryujinx.HLE.HOS.Services.Ldn.Types; +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types +{ + [StructLayout(LayoutKind.Sequential, Size = 0x8)] + struct RejectRequest + { + public uint NodeId; + public DisconnectReason DisconnectReason; + + public RejectRequest(DisconnectReason disconnectReason, uint nodeId) + { + DisconnectReason = disconnectReason; + NodeId = nodeId; + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/RyuNetworkConfig.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/RyuNetworkConfig.cs new file mode 100644 index 000000000..f3bd72023 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/RyuNetworkConfig.cs @@ -0,0 +1,23 @@ +using Ryujinx.Common.Memory; +using System.Net.Sockets; +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types +{ + [StructLayout(LayoutKind.Sequential, Size = 0x28, Pack = 1)] + struct RyuNetworkConfig + { + public Array16 GameVersion; + + // PrivateIp is included for external proxies for the case where a client attempts to join from + // their own LAN. UPnP forwarding can fail when connecting devices on the same network over the public IP, + // so if their public IP is identical, the internal address should be sent instead. + + // The fields below are 0 if not hosting a p2p proxy. + + public Array16 PrivateIp; + public AddressFamily AddressFamily; + public ushort ExternalProxyPort; + public ushort InternalProxyPort; + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/SetAcceptPolicyRequest.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/SetAcceptPolicyRequest.cs new file mode 100644 index 000000000..c4a969901 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/SetAcceptPolicyRequest.cs @@ -0,0 +1,11 @@ +using Ryujinx.HLE.HOS.Services.Ldn.Types; +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types +{ + [StructLayout(LayoutKind.Sequential, Size = 0x1, Pack = 1)] + struct SetAcceptPolicyRequest + { + public AcceptPolicy StationAcceptPolicy; + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Station.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Station.cs index e39c01978..fa43f789e 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Station.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Station.cs @@ -14,6 +14,8 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator public bool Connected { get; private set; } + public ProxyConfig Config => _parent.NetworkClient.Config; + public Station(IUserLocalCommunicationService parent) { _parent = parent; @@ -48,9 +50,12 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator public void Dispose() { - _parent.NetworkClient.DisconnectNetwork(); + if (_parent.NetworkClient != null) + { + _parent.NetworkClient.DisconnectNetwork(); - _parent.NetworkClient.NetworkChange -= NetworkChanged; + _parent.NetworkClient.NetworkChange -= NetworkChanged; + } } private ResultCode NetworkErrorToResult(NetworkError error) diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/CreateAccessPointPrivateRequest.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/CreateAccessPointPrivateRequest.cs index ac0ff7d94..0972c21c0 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/CreateAccessPointPrivateRequest.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/CreateAccessPointPrivateRequest.cs @@ -1,4 +1,5 @@ using Ryujinx.HLE.HOS.Services.Ldn.Types; +using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types; using System.Runtime.InteropServices; namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types @@ -14,5 +15,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types public UserConfig UserConfig; public NetworkConfig NetworkConfig; public AddressList AddressList; + + public RyuNetworkConfig RyuNetworkConfig; } } diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/CreateAccessPointRequest.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/CreateAccessPointRequest.cs index f67f0aac9..d2dc5b698 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/CreateAccessPointRequest.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/CreateAccessPointRequest.cs @@ -1,4 +1,5 @@ using Ryujinx.HLE.HOS.Services.Ldn.Types; +using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types; using System.Runtime.InteropServices; namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types @@ -6,11 +7,13 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types /// /// Advertise data is appended separately (remaining data in the buffer). /// - [StructLayout(LayoutKind.Sequential, Size = 0x94, CharSet = CharSet.Ansi)] + [StructLayout(LayoutKind.Sequential, Size = 0xBC, Pack = 1)] struct CreateAccessPointRequest { public SecurityConfig SecurityConfig; public UserConfig UserConfig; public NetworkConfig NetworkConfig; + + public RyuNetworkConfig RyuNetworkConfig; } } diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/ProxyConfig.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/ProxyConfig.cs new file mode 100644 index 000000000..c89c08bbe --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/ProxyConfig.cs @@ -0,0 +1,11 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types +{ + [StructLayout(LayoutKind.Sequential, Size = 0x8)] + struct ProxyConfig + { + public uint ProxyIp; + public uint ProxySubnetMask; + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Mii/UtilityImpl.cs b/src/Ryujinx.HLE/HOS/Services/Mii/UtilityImpl.cs index 32dbb4946..ea54a6ad6 100644 --- a/src/Ryujinx.HLE/HOS/Services/Mii/UtilityImpl.cs +++ b/src/Ryujinx.HLE/HOS/Services/Mii/UtilityImpl.cs @@ -63,7 +63,7 @@ namespace Ryujinx.HLE.HOS.Services.Mii public CreateId MakeCreateId() { - UInt128 value = UInt128Utils.CreateRandom(); + UInt128 value = Random.Shared.NextUInt128(); // Ensure the random ID generated is valid as a create id. value &= ~new UInt128(0xC0, 0); diff --git a/src/Ryujinx.HLE/HOS/Services/Nfc/Nfp/NfpManager/Types/VirtualAmiiboFile.cs b/src/Ryujinx.HLE/HOS/Services/Nfc/Nfp/NfpManager/Types/VirtualAmiiboFile.cs index 65d380979..e1db98e5f 100644 --- a/src/Ryujinx.HLE/HOS/Services/Nfc/Nfp/NfpManager/Types/VirtualAmiiboFile.cs +++ b/src/Ryujinx.HLE/HOS/Services/Nfc/Nfp/NfpManager/Types/VirtualAmiiboFile.cs @@ -8,6 +8,7 @@ namespace Ryujinx.HLE.HOS.Services.Nfc.Nfp.NfpManager public uint FileVersion { get; set; } public byte[] TagUuid { get; set; } public string AmiiboId { get; set; } + public string NickName { get; set; } public DateTime FirstWriteDate { get; set; } public DateTime LastWriteDate { get; set; } public ushort WriteCounter { get; set; } diff --git a/src/Ryujinx.HLE/HOS/Services/Nfc/Nfp/VirtualAmiibo.cs b/src/Ryujinx.HLE/HOS/Services/Nfc/Nfp/VirtualAmiibo.cs index ba4a81e0e..7ce749d1a 100644 --- a/src/Ryujinx.HLE/HOS/Services/Nfc/Nfp/VirtualAmiibo.cs +++ b/src/Ryujinx.HLE/HOS/Services/Nfc/Nfp/VirtualAmiibo.cs @@ -64,16 +64,17 @@ namespace Ryujinx.HLE.HOS.Services.Nfc.Nfp }; } - public static RegisterInfo GetRegisterInfo(ITickSource tickSource, string amiiboId, string nickname) + public static RegisterInfo GetRegisterInfo(ITickSource tickSource, string amiiboId, string userName) { VirtualAmiiboFile amiiboFile = LoadAmiiboFile(amiiboId); - + string nickname = amiiboFile.NickName ?? "Ryujinx"; UtilityImpl utilityImpl = new(tickSource); CharInfo charInfo = new(); charInfo.SetFromStoreData(StoreData.BuildDefault(utilityImpl, 0)); - charInfo.Nickname = Nickname.FromString(nickname); + // This is the player's name + charInfo.Nickname = Nickname.FromString(userName); RegisterInfo registerInfo = new() { @@ -85,7 +86,9 @@ namespace Ryujinx.HLE.HOS.Services.Nfc.Nfp Reserved1 = new Array64(), Reserved2 = new Array58(), }; - "Ryujinx"u8.CopyTo(registerInfo.Nickname.AsSpan()); + // This is the amiibo's name + byte[] nicknameBytes = System.Text.Encoding.UTF8.GetBytes(nickname); + nicknameBytes.CopyTo(registerInfo.Nickname.AsSpan()); return registerInfo; } diff --git a/src/Ryujinx.HLE/HOS/Services/Ngct/NgctServer.cs b/src/Ryujinx.HLE/HOS/Services/Ngct/NgctServer.cs index f652ecda8..b076958eb 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ngct/NgctServer.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ngct/NgctServer.cs @@ -15,7 +15,7 @@ namespace Ryujinx.HLE.HOS.Services.Ngct ulong bufferSize = context.Request.PtrBuff[0].Size; bool isMatch = false; - string text = ""; + string text = string.Empty; if (bufferSize != 0) { @@ -57,8 +57,8 @@ namespace Ryujinx.HLE.HOS.Services.Ngct ulong bufferFilteredPosition = context.Request.RecvListBuff[0].Position; - string text = ""; - string textFiltered = ""; + string text = string.Empty; + string textFiltered = string.Empty; if (bufferSize != 0) { diff --git a/src/Ryujinx.HLE/HOS/Services/Nifm/StaticService/IGeneralService.cs b/src/Ryujinx.HLE/HOS/Services/Nifm/StaticService/IGeneralService.cs index 581a2906b..a5a822db3 100644 --- a/src/Ryujinx.HLE/HOS/Services/Nifm/StaticService/IGeneralService.cs +++ b/src/Ryujinx.HLE/HOS/Services/Nifm/StaticService/IGeneralService.cs @@ -78,7 +78,7 @@ namespace Ryujinx.HLE.HOS.Services.Nifm.StaticService NetworkProfileData networkProfile = new() { - Uuid = UInt128Utils.CreateRandom(), + Uuid = Random.Shared.NextUInt128(), }; networkProfile.IpSettingData.IpAddressSetting = new IpAddressSetting(interfaceProperties, unicastAddress); diff --git a/src/Ryujinx.HLE/HOS/Services/Nifm/StaticService/Types/DnsSetting.cs b/src/Ryujinx.HLE/HOS/Services/Nifm/StaticService/Types/DnsSetting.cs index e0348ddc3..af80db480 100644 --- a/src/Ryujinx.HLE/HOS/Services/Nifm/StaticService/Types/DnsSetting.cs +++ b/src/Ryujinx.HLE/HOS/Services/Nifm/StaticService/Types/DnsSetting.cs @@ -1,7 +1,6 @@ using System; using System.Net.NetworkInformation; using System.Runtime.InteropServices; -using System.Net; namespace Ryujinx.HLE.HOS.Services.Nifm.StaticService.Types { @@ -16,23 +15,16 @@ namespace Ryujinx.HLE.HOS.Services.Nifm.StaticService.Types public DnsSetting(IPInterfaceProperties interfaceProperties) { IsDynamicDnsEnabled = OperatingSystem.IsWindows() && interfaceProperties.IsDynamicDnsEnabled; - - IPAddress ip = IPAddress.Parse("1.1.1.1"); - if (OperatingSystem.IsIOS()) { - PrimaryDns = new IpV4Address(ip); - SecondaryDns = new IpV4Address(ip); - } else { - if (interfaceProperties.DnsAddresses.Count == 0) - { - PrimaryDns = new IpV4Address(); - SecondaryDns = new IpV4Address(); - } - else - { - PrimaryDns = new IpV4Address(interfaceProperties.DnsAddresses[0]); - SecondaryDns = new IpV4Address(interfaceProperties.DnsAddresses[interfaceProperties.DnsAddresses.Count > 1 ? 1 : 0]); - } + if (interfaceProperties.DnsAddresses.Count == 0) + { + PrimaryDns = new IpV4Address(); + SecondaryDns = new IpV4Address(); + } + else + { + PrimaryDns = new IpV4Address(interfaceProperties.DnsAddresses[0]); + SecondaryDns = new IpV4Address(interfaceProperties.DnsAddresses[interfaceProperties.DnsAddresses.Count > 1 ? 1 : 0]); } } } diff --git a/src/Ryujinx.HLE/HOS/Services/Nifm/StaticService/Types/IpAddressSetting.cs b/src/Ryujinx.HLE/HOS/Services/Nifm/StaticService/Types/IpAddressSetting.cs index 359c9b75e..4fa674de9 100644 --- a/src/Ryujinx.HLE/HOS/Services/Nifm/StaticService/Types/IpAddressSetting.cs +++ b/src/Ryujinx.HLE/HOS/Services/Nifm/StaticService/Types/IpAddressSetting.cs @@ -15,7 +15,7 @@ namespace Ryujinx.HLE.HOS.Services.Nifm.StaticService.Types public IpAddressSetting(IPInterfaceProperties interfaceProperties, UnicastIPAddressInformation unicastIPAddressInformation) { - IsDhcpEnabled = OperatingSystem.IsMacOS() || OperatingSystem.IsIOS() || interfaceProperties.DhcpServerAddresses.Count != 0; + IsDhcpEnabled = OperatingSystem.IsMacOS() || interfaceProperties.DhcpServerAddresses.Count != 0; Address = new IpV4Address(unicastIPAddressInformation.Address); IPv4Mask = new IpV4Address(unicastIPAddressInformation.IPv4Mask); GatewayAddress = (interfaceProperties.GatewayAddresses.Count == 0) ? new IpV4Address() : new IpV4Address(interfaceProperties.GatewayAddresses[0].Address); diff --git a/src/Ryujinx.HLE/HOS/Services/Nim/IShopServiceAccessServerInterface.cs b/src/Ryujinx.HLE/HOS/Services/Nim/IShopServiceAccessServerInterface.cs index 52412489a..d7e276ea0 100644 --- a/src/Ryujinx.HLE/HOS/Services/Nim/IShopServiceAccessServerInterface.cs +++ b/src/Ryujinx.HLE/HOS/Services/Nim/IShopServiceAccessServerInterface.cs @@ -40,5 +40,12 @@ namespace Ryujinx.HLE.HOS.Services.Nim return ResultCode.Success; } + + [CommandCmif(5)] // 17.0.0+ + // CreateServerInterface2(pid, handle, u64) -> object + public ResultCode CreateServerInterface2(ServiceCtx context) + { + return CreateServerInterface(context); + } } } diff --git a/src/Ryujinx.HLE/HOS/Services/Nv/Host1xContext.cs b/src/Ryujinx.HLE/HOS/Services/Nv/Host1xContext.cs index 371edbecd..7c7ebf22d 100644 --- a/src/Ryujinx.HLE/HOS/Services/Nv/Host1xContext.cs +++ b/src/Ryujinx.HLE/HOS/Services/Nv/Host1xContext.cs @@ -1,4 +1,4 @@ -using Ryujinx.Graphics.Gpu.Memory; +using Ryujinx.Graphics.Device; using Ryujinx.Graphics.Host1x; using Ryujinx.Graphics.Nvdec; using Ryujinx.Graphics.Vic; @@ -9,7 +9,7 @@ namespace Ryujinx.HLE.HOS.Services.Nv { class Host1xContext : IDisposable { - public MemoryManager Smmu { get; } + public DeviceMemoryManager Smmu { get; } public NvMemoryAllocator MemoryAllocator { get; } public Host1xDevice Host1x { get; } @@ -17,7 +17,7 @@ namespace Ryujinx.HLE.HOS.Services.Nv { MemoryAllocator = new NvMemoryAllocator(); Host1x = new Host1xDevice(gpu.Synchronization); - Smmu = gpu.CreateMemoryManager(pid); + Smmu = gpu.CreateDeviceMemoryManager(pid); var nvdec = new NvdecDevice(Smmu); var vic = new VicDevice(Smmu); Host1x.RegisterDevice(ClassId.Nvdec, nvdec); diff --git a/src/Ryujinx.HLE/HOS/Services/Nv/NvDrvServices/NvHostAsGpu/NvHostAsGpuDeviceFile.cs b/src/Ryujinx.HLE/HOS/Services/Nv/NvDrvServices/NvHostAsGpu/NvHostAsGpuDeviceFile.cs index 03c4ed860..0f5d7547c 100644 --- a/src/Ryujinx.HLE/HOS/Services/Nv/NvDrvServices/NvHostAsGpu/NvHostAsGpuDeviceFile.cs +++ b/src/Ryujinx.HLE/HOS/Services/Nv/NvDrvServices/NvHostAsGpu/NvHostAsGpuDeviceFile.cs @@ -42,7 +42,7 @@ namespace Ryujinx.HLE.HOS.Services.Nv.NvDrvServices.NvHostAsGpu public NvHostAsGpuDeviceFile(ServiceCtx context, IVirtualMemoryManager memory, ulong owner) : base(context, owner) { - _asContext = new AddressSpaceContext(context.Device.Gpu.CreateMemoryManager(owner)); + _asContext = new AddressSpaceContext(context.Device.Gpu.CreateMemoryManager(owner, context.Device.Memory.Size)); _memoryAllocator = new NvMemoryAllocator(); } @@ -266,7 +266,7 @@ namespace Ryujinx.HLE.HOS.Services.Nv.NvDrvServices.NvHostAsGpu if (size == 0) { - size = (uint)map.Size; + size = map.Size; } NvInternalResult result = NvInternalResult.Success; diff --git a/src/Ryujinx.HLE/HOS/Services/Nv/NvDrvServices/NvHostChannel/NvHostChannelDeviceFile.cs b/src/Ryujinx.HLE/HOS/Services/Nv/NvDrvServices/NvHostChannel/NvHostChannelDeviceFile.cs index 53db5eca4..bc70b05cf 100644 --- a/src/Ryujinx.HLE/HOS/Services/Nv/NvDrvServices/NvHostChannel/NvHostChannelDeviceFile.cs +++ b/src/Ryujinx.HLE/HOS/Services/Nv/NvDrvServices/NvHostChannel/NvHostChannelDeviceFile.cs @@ -250,12 +250,12 @@ namespace Ryujinx.HLE.HOS.Services.Nv.NvDrvServices.NvHostChannel { if (map.DmaMapAddress == 0) { - ulong va = _host1xContext.MemoryAllocator.GetFreeAddress((ulong)map.Size, out ulong freeAddressStartPosition, 1, MemoryManager.PageSize); + ulong va = _host1xContext.MemoryAllocator.GetFreeAddress(map.Size, out ulong freeAddressStartPosition, 1, MemoryManager.PageSize); - if (va != NvMemoryAllocator.PteUnmapped && va <= uint.MaxValue && (va + (uint)map.Size) <= uint.MaxValue) + if (va != NvMemoryAllocator.PteUnmapped && va <= uint.MaxValue && (va + map.Size) <= uint.MaxValue) { - _host1xContext.MemoryAllocator.AllocateRange(va, (uint)map.Size, freeAddressStartPosition); - _host1xContext.Smmu.Map(map.Address, va, (uint)map.Size, PteKind.Pitch); // FIXME: This should not use the GMMU. + _host1xContext.MemoryAllocator.AllocateRange(va, map.Size, freeAddressStartPosition); + _host1xContext.Smmu.Map(map.Address, va, map.Size); map.DmaMapAddress = va; } else diff --git a/src/Ryujinx.HLE/HOS/Services/Nv/NvDrvServices/NvHostCtrlGpu/NvHostCtrlGpuDeviceFile.cs b/src/Ryujinx.HLE/HOS/Services/Nv/NvDrvServices/NvHostCtrlGpu/NvHostCtrlGpuDeviceFile.cs index d287c6d9e..29198617f 100644 --- a/src/Ryujinx.HLE/HOS/Services/Nv/NvDrvServices/NvHostCtrlGpu/NvHostCtrlGpuDeviceFile.cs +++ b/src/Ryujinx.HLE/HOS/Services/Nv/NvDrvServices/NvHostCtrlGpu/NvHostCtrlGpuDeviceFile.cs @@ -50,6 +50,12 @@ namespace Ryujinx.HLE.HOS.Services.Nv.NvDrvServices.NvHostCtrlGpu case 0x06: result = CallIoctlMethod(GetTpcMasks, arguments); break; + case 0x12: + result = CallIoctlMethod(NumVsms, arguments); + break; + case 0x13: + result = CallIoctlMethod(VsmsMapping, arguments); + break; case 0x14: result = CallIoctlMethod(GetActiveSlotMask, arguments); break; @@ -76,6 +82,12 @@ namespace Ryujinx.HLE.HOS.Services.Nv.NvDrvServices.NvHostCtrlGpu case 0x06: result = CallIoctlMethod(GetTpcMasks, arguments, inlineOutBuffer); break; + case 0x12: + result = CallIoctlMethod(NumVsms, arguments); + break; + case 0x13: + result = CallIoctlMethod(VsmsMapping, arguments); + break; } } @@ -216,6 +228,27 @@ namespace Ryujinx.HLE.HOS.Services.Nv.NvDrvServices.NvHostCtrlGpu return NvInternalResult.Success; } + private NvInternalResult NumVsms(ref NumVsmsArguments arguments) + { + Logger.Stub?.PrintStub(LogClass.ServiceNv); + + arguments.NumVsms = 2; + + return NvInternalResult.Success; + } + + private NvInternalResult VsmsMapping(ref VsmsMappingArguments arguments) + { + Logger.Stub?.PrintStub(LogClass.ServiceNv); + + arguments.Sm0GpcIndex = 0; + arguments.Sm0TpcIndex = 0; + arguments.Sm1GpcIndex = 0; + arguments.Sm1TpcIndex = 1; + + return NvInternalResult.Success; + } + private NvInternalResult GetActiveSlotMask(ref GetActiveSlotMaskArguments arguments) { Logger.Stub?.PrintStub(LogClass.ServiceNv); diff --git a/src/Ryujinx.HLE/HOS/Services/Nv/NvDrvServices/NvHostCtrlGpu/Types/NumVsmsArguments.cs b/src/Ryujinx.HLE/HOS/Services/Nv/NvDrvServices/NvHostCtrlGpu/Types/NumVsmsArguments.cs new file mode 100644 index 000000000..fb5013a70 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Nv/NvDrvServices/NvHostCtrlGpu/Types/NumVsmsArguments.cs @@ -0,0 +1,11 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Nv.NvDrvServices.NvHostCtrlGpu.Types +{ + [StructLayout(LayoutKind.Sequential)] + struct NumVsmsArguments + { + public uint NumVsms; + public uint Reserved; + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Nv/NvDrvServices/NvHostCtrlGpu/Types/VsmsMappingArguments.cs b/src/Ryujinx.HLE/HOS/Services/Nv/NvDrvServices/NvHostCtrlGpu/Types/VsmsMappingArguments.cs new file mode 100644 index 000000000..baada9197 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Nv/NvDrvServices/NvHostCtrlGpu/Types/VsmsMappingArguments.cs @@ -0,0 +1,14 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Nv.NvDrvServices.NvHostCtrlGpu.Types +{ + [StructLayout(LayoutKind.Sequential)] + struct VsmsMappingArguments + { + public byte Sm0GpcIndex; + public byte Sm0TpcIndex; + public byte Sm1GpcIndex; + public byte Sm1TpcIndex; + public uint Reserved; + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Nv/NvDrvServices/NvMap/NvMapDeviceFile.cs b/src/Ryujinx.HLE/HOS/Services/Nv/NvDrvServices/NvMap/NvMapDeviceFile.cs index abe0a4de8..6a0ac58bd 100644 --- a/src/Ryujinx.HLE/HOS/Services/Nv/NvDrvServices/NvMap/NvMapDeviceFile.cs +++ b/src/Ryujinx.HLE/HOS/Services/Nv/NvDrvServices/NvMap/NvMapDeviceFile.cs @@ -69,7 +69,7 @@ namespace Ryujinx.HLE.HOS.Services.Nv.NvDrvServices.NvMap return NvInternalResult.InvalidInput; } - int size = BitUtils.AlignUp(arguments.Size, (int)MemoryManager.PageSize); + uint size = BitUtils.AlignUp(arguments.Size, (uint)MemoryManager.PageSize); arguments.Handle = CreateHandleFromMap(new NvMapHandle(size)); @@ -128,7 +128,7 @@ namespace Ryujinx.HLE.HOS.Services.Nv.NvDrvServices.NvMap map.Align = arguments.Align; map.Kind = (byte)arguments.Kind; - int size = BitUtils.AlignUp(map.Size, (int)MemoryManager.PageSize); + uint size = BitUtils.AlignUp(map.Size, (uint)MemoryManager.PageSize); ulong address = arguments.Address; @@ -191,7 +191,7 @@ namespace Ryujinx.HLE.HOS.Services.Nv.NvDrvServices.NvMap switch (arguments.Param) { case NvMapHandleParam.Size: - arguments.Result = map.Size; + arguments.Result = (int)map.Size; break; case NvMapHandleParam.Align: arguments.Result = map.Align; diff --git a/src/Ryujinx.HLE/HOS/Services/Nv/NvDrvServices/NvMap/Types/NvMapCreate.cs b/src/Ryujinx.HLE/HOS/Services/Nv/NvDrvServices/NvMap/Types/NvMapCreate.cs index 5380c45c7..f4047497a 100644 --- a/src/Ryujinx.HLE/HOS/Services/Nv/NvDrvServices/NvMap/Types/NvMapCreate.cs +++ b/src/Ryujinx.HLE/HOS/Services/Nv/NvDrvServices/NvMap/Types/NvMapCreate.cs @@ -5,7 +5,7 @@ namespace Ryujinx.HLE.HOS.Services.Nv.NvDrvServices.NvMap [StructLayout(LayoutKind.Sequential)] struct NvMapCreate { - public int Size; + public uint Size; public int Handle; } } diff --git a/src/Ryujinx.HLE/HOS/Services/Nv/NvDrvServices/NvMap/Types/NvMapFree.cs b/src/Ryujinx.HLE/HOS/Services/Nv/NvDrvServices/NvMap/Types/NvMapFree.cs index b0b3fa2d6..ce93e9e5e 100644 --- a/src/Ryujinx.HLE/HOS/Services/Nv/NvDrvServices/NvMap/Types/NvMapFree.cs +++ b/src/Ryujinx.HLE/HOS/Services/Nv/NvDrvServices/NvMap/Types/NvMapFree.cs @@ -8,7 +8,7 @@ namespace Ryujinx.HLE.HOS.Services.Nv.NvDrvServices.NvMap public int Handle; public int Padding; public ulong Address; - public int Size; + public uint Size; public int Flags; } } diff --git a/src/Ryujinx.HLE/HOS/Services/Nv/NvDrvServices/NvMap/Types/NvMapHandle.cs b/src/Ryujinx.HLE/HOS/Services/Nv/NvDrvServices/NvMap/Types/NvMapHandle.cs index 301179747..e821b571d 100644 --- a/src/Ryujinx.HLE/HOS/Services/Nv/NvDrvServices/NvMap/Types/NvMapHandle.cs +++ b/src/Ryujinx.HLE/HOS/Services/Nv/NvDrvServices/NvMap/Types/NvMapHandle.cs @@ -8,7 +8,7 @@ namespace Ryujinx.HLE.HOS.Services.Nv.NvDrvServices.NvMap public int Handle; public int Id; #pragma warning restore CS0649 - public int Size; + public uint Size; public int Align; public int Kind; public ulong Address; @@ -22,7 +22,7 @@ namespace Ryujinx.HLE.HOS.Services.Nv.NvDrvServices.NvMap _dupes = 1; } - public NvMapHandle(int size) : this() + public NvMapHandle(uint size) : this() { Size = size; } diff --git a/src/Ryujinx.HLE/HOS/Services/Pctl/ParentalControlServiceFactory/IParentalControlService.cs b/src/Ryujinx.HLE/HOS/Services/Pctl/ParentalControlServiceFactory/IParentalControlService.cs index cf8c1f78d..9b026a1c3 100644 --- a/src/Ryujinx.HLE/HOS/Services/Pctl/ParentalControlServiceFactory/IParentalControlService.cs +++ b/src/Ryujinx.HLE/HOS/Services/Pctl/ParentalControlServiceFactory/IParentalControlService.cs @@ -159,9 +159,7 @@ namespace Ryujinx.HLE.HOS.Services.Pctl.ParentalControlServiceFactory } else { -#pragma warning disable CS0162 // Unreachable code return ResultCode.StereoVisionRestrictionConfigurableDisabled; -#pragma warning restore CS0162 } } diff --git a/src/Ryujinx.HLE/HOS/Services/Ptm/Ts/IMeasurementServer.cs b/src/Ryujinx.HLE/HOS/Services/Ptm/Ts/IMeasurementServer.cs deleted file mode 100644 index 66ffd0a49..000000000 --- a/src/Ryujinx.HLE/HOS/Services/Ptm/Ts/IMeasurementServer.cs +++ /dev/null @@ -1,39 +0,0 @@ -using Ryujinx.Common.Logging; -using Ryujinx.HLE.HOS.Services.Ptm.Ts.Types; - -namespace Ryujinx.HLE.HOS.Services.Ptm.Ts -{ - [Service("ts")] - class IMeasurementServer : IpcService - { - private const uint DefaultTemperature = 42u; - - public IMeasurementServer(ServiceCtx context) { } - - [CommandCmif(1)] - // GetTemperature(Location location) -> u32 - public ResultCode GetTemperature(ServiceCtx context) - { - Location location = (Location)context.RequestData.ReadByte(); - - Logger.Stub?.PrintStub(LogClass.ServicePtm, new { location }); - - context.ResponseData.Write(DefaultTemperature); - - return ResultCode.Success; - } - - [CommandCmif(3)] - // GetTemperatureMilliC(Location location) -> u32 - public ResultCode GetTemperatureMilliC(ServiceCtx context) - { - Location location = (Location)context.RequestData.ReadByte(); - - Logger.Stub?.PrintStub(LogClass.ServicePtm, new { location }); - - context.ResponseData.Write(DefaultTemperature * 1000); - - return ResultCode.Success; - } - } -} diff --git a/src/Ryujinx.HLE/HOS/Services/Sdb/Pl/SharedFontManager.cs b/src/Ryujinx.HLE/HOS/Services/Sdb/Pl/SharedFontManager.cs index 641795890..ea3bd84df 100644 --- a/src/Ryujinx.HLE/HOS/Services/Sdb/Pl/SharedFontManager.cs +++ b/src/Ryujinx.HLE/HOS/Services/Sdb/Pl/SharedFontManager.cs @@ -105,7 +105,7 @@ namespace Ryujinx.HLE.HOS.Services.Sdb.Pl titleName = "Unknown"; } - throw new InvalidSystemResourceException($"{titleName} ({fontTitle:x8}) system title not found! This font will not work, provide the system archive to fix this error. (See https://github.com/Ryujinx/Ryujinx#requirements for more information)"); + throw new InvalidSystemResourceException($"{titleName} ({fontTitle:x8}) system title not found! This font will not work, provide the system archive to fix this error. (See https://github.com/GreemDev/Ryujinx#requirements for more information)"); } } else diff --git a/src/Ryujinx.HLE/HOS/Services/ServerBase.cs b/src/Ryujinx.HLE/HOS/Services/ServerBase.cs index e892d6ab6..f67699b90 100644 --- a/src/Ryujinx.HLE/HOS/Services/ServerBase.cs +++ b/src/Ryujinx.HLE/HOS/Services/ServerBase.cs @@ -287,6 +287,10 @@ namespace Ryujinx.HLE.HOS.Services _wakeEvent.WritableEvent.Clear(); } } + else if (rc == KernelResult.PortRemoteClosed && signaledIndex >= 0 && SmObjectFactory != null) + { + DestroySession(handles[signaledIndex]); + } _selfProcess.CpuMemory.Write(messagePtr + 0x0, 0); _selfProcess.CpuMemory.Write(messagePtr + 0x4, 2 << 10); @@ -299,6 +303,16 @@ namespace Ryujinx.HLE.HOS.Services Dispose(); } + private void DestroySession(int serverSessionHandle) + { + _context.Syscall.CloseHandle(serverSessionHandle); + + if (RemoveSessionObj(serverSessionHandle, out var session)) + { + (session as IDisposable)?.Dispose(); + } + } + private bool Process(int serverSessionHandle, ulong recvListAddr) { IpcMessage request = ReadRequest(); @@ -360,7 +374,7 @@ namespace Ryujinx.HLE.HOS.Services response.RawData = _responseDataStream.ToArray(); } else if (request.Type == IpcMessageType.CmifControl || - request.Type == IpcMessageType.CmifControlWithContext) + request.Type == IpcMessageType.CmifControlWithContext) { #pragma warning disable IDE0059 // Remove unnecessary value assignment uint magic = (uint)_requestDataReader.ReadUInt64(); @@ -412,11 +426,7 @@ namespace Ryujinx.HLE.HOS.Services } else if (request.Type == IpcMessageType.CmifCloseSession || request.Type == IpcMessageType.TipcCloseSession) { - _context.Syscall.CloseHandle(serverSessionHandle); - if (RemoveSessionObj(serverSessionHandle, out var session)) - { - (session as IDisposable)?.Dispose(); - } + DestroySession(serverSessionHandle); shouldReply = false; } // If the type is past 0xF, we are using TIPC @@ -464,9 +474,9 @@ namespace Ryujinx.HLE.HOS.Services { const int MessageSize = 0x100; - using IMemoryOwner reqDataOwner = ByteMemoryPool.Rent(MessageSize); + using SpanOwner reqDataOwner = SpanOwner.Rent(MessageSize); - Span reqDataSpan = reqDataOwner.Memory.Span; + Span reqDataSpan = reqDataOwner.Span; _selfProcess.CpuMemory.Read(_selfThread.TlsAddress, reqDataSpan); diff --git a/src/Ryujinx.HLE/HOS/Services/Settings/NxSettings.cs b/src/Ryujinx.HLE/HOS/Services/Settings/NxSettings.cs index b2d4d55cc..b0b854f74 100644 --- a/src/Ryujinx.HLE/HOS/Services/Settings/NxSettings.cs +++ b/src/Ryujinx.HLE/HOS/Services/Settings/NxSettings.cs @@ -73,1529 +73,1529 @@ namespace Ryujinx.HLE.HOS.Services.Settings { "ns.sdcard!compare_sdcard", 0 }, { "ns.gamecard!mount_gamecard_result_value", 0 }, { "ns.gamecard!try_gamecard_access_result_value", 0 }, - { "nv!00008600", "" }, - { "nv!0007b25e", "" }, - { "nv!0083e1", "" }, - { "nv!01621887", "" }, - { "nv!03134743", "" }, - { "nv!0356afd0", "" }, - { "nv!0356afd1", "" }, - { "nv!0356afd2", "" }, - { "nv!0356afd3", "" }, - { "nv!094313", "" }, - { "nv!0x04dc09", "" }, - { "nv!0x111133", "" }, - { "nv!0x1aa483", "" }, - { "nv!0x1cb1cf", "" }, - { "nv!0x1cb1d0", "" }, - { "nv!0x1e3221", "" }, - { "nv!0x300fc8", "" }, - { "nv!0x301fc8", "" }, - { "nv!0x302fc8", "" }, - { "nv!0x3eec59", "" }, - { "nv!0x46b3ed", "" }, - { "nv!0x523dc0", "" }, - { "nv!0x523dc1", "" }, - { "nv!0x523dc2", "" }, - { "nv!0x523dc3", "" }, - { "nv!0x523dc4", "" }, - { "nv!0x523dc5", "" }, - { "nv!0x523dc6", "" }, - { "nv!0x523dd0", "" }, - { "nv!0x523dd1", "" }, - { "nv!0x523dd3", "" }, - { "nv!0x5344bb", "" }, - { "nv!0x555237", "" }, - { "nv!0x58a234", "" }, - { "nv!0x7b4428", "" }, - { "nv!0x923dc0", "" }, - { "nv!0x923dc1", "" }, - { "nv!0x923dc2", "" }, - { "nv!0x923dc3", "" }, - { "nv!0x923dc4", "" }, - { "nv!0x923dd3", "" }, - { "nv!0x9abdc5", "" }, - { "nv!0x9abdc6", "" }, - { "nv!0xaaa36c", "" }, - { "nv!0xb09da0", "" }, - { "nv!0xb09da1", "" }, - { "nv!0xb09da2", "" }, - { "nv!0xb09da3", "" }, - { "nv!0xb09da4", "" }, - { "nv!0xb09da5", "" }, - { "nv!0xb0b348", "" }, - { "nv!0xb0b349", "" }, - { "nv!0xbb558f", "" }, - { "nv!0xbd10fb", "" }, - { "nv!0xc32ad3", "" }, - { "nv!0xce2348", "" }, - { "nv!0xcfd81f", "" }, - { "nv!0xe0036b", "" }, - { "nv!0xe01f2d", "" }, - { "nv!0xe17212", "" }, - { "nv!0xeae966", "" }, - { "nv!0xed4f82", "" }, - { "nv!0xf12335", "" }, - { "nv!0xf12336", "" }, - { "nv!10261989", "" }, - { "nv!1042d483", "" }, - { "nv!10572898", "" }, - { "nv!115631", "" }, - { "nv!12950094", "" }, - { "nv!1314f311", "" }, - { "nv!1314f312", "" }, - { "nv!13279512", "" }, - { "nv!13813496", "" }, - { "nv!14507179", "" }, - { "nv!15694569", "" }, - { "nv!16936964", "" }, - { "nv!17aa230c", "" }, - { "nv!182054", "" }, - { "nv!18273275", "" }, - { "nv!18273276", "" }, - { "nv!1854d03b", "" }, - { "nv!18add00d", "" }, - { "nv!19156670", "" }, - { "nv!19286545", "" }, - { "nv!1a298e9f", "" }, - { "nv!1acf43fe", "" }, - { "nv!1bda43fe", "" }, - { "nv!1c3b92", "" }, - { "nv!21509920", "" }, - { "nv!215323457", "" }, - { "nv!2165ad", "" }, - { "nv!2165ae", "" }, - { "nv!21be9c", "" }, - { "nv!233264316", "" }, - { "nv!234557580", "" }, - { "nv!23cd0e", "" }, - { "nv!24189123", "" }, - { "nv!2443266", "" }, - { "nv!25025519", "" }, - { "nv!255e39", "" }, - { "nv!2583364", "" }, - { "nv!2888c1", "" }, - { "nv!28ca3e", "" }, - { "nv!29871243", "" }, - { "nv!2a1f64", "" }, - { "nv!2dc432", "" }, - { "nv!2de437", "" }, - { "nv!2f3bb89c", "" }, - { "nv!2fd652", "" }, - { "nv!3001ac", "" }, - { "nv!31298772", "" }, - { "nv!313233", "" }, - { "nv!31f7d603", "" }, - { "nv!320ce4", "" }, - { "nv!32153248", "" }, - { "nv!32153249", "" }, - { "nv!335bca", "" }, - { "nv!342abb", "" }, - { "nv!34dfe6", "" }, - { "nv!34dfe7", "" }, - { "nv!34dfe8", "" }, - { "nv!34dfe9", "" }, - { "nv!35201578", "" }, - { "nv!359278", "" }, - { "nv!37f53a", "" }, - { "nv!38144972", "" }, - { "nv!38542646", "" }, - { "nv!3b74c9", "" }, - { "nv!3c136f", "" }, - { "nv!3cf72823", "" }, - { "nv!3d7af029", "" }, - { "nv!3ff34782", "" }, - { "nv!4129618", "" }, - { "nv!4189fac3", "" }, - { "nv!420bd4", "" }, - { "nv!42a699", "" }, - { "nv!441369", "" }, - { "nv!4458713e", "" }, - { "nv!4554b6", "" }, - { "nv!457425", "" }, - { "nv!4603b207", "" }, - { "nv!46574957", "" }, - { "nv!46574958", "" }, - { "nv!46813529", "" }, - { "nv!46f1e13d", "" }, - { "nv!47534c43", "" }, - { "nv!48550336", "" }, - { "nv!48576893", "" }, - { "nv!48576894", "" }, - { "nv!4889ac02", "" }, - { "nv!49005740", "" }, - { "nv!49867584", "" }, - { "nv!49960973", "" }, - { "nv!4a5341", "" }, - { "nv!4f4e48", "" }, - { "nv!4f8a0a", "" }, - { "nv!50299698", "" }, - { "nv!50299699", "" }, - { "nv!50361291", "" }, - { "nv!5242ae", "" }, - { "nv!53d30c", "" }, - { "nv!56347a", "" }, - { "nv!563a95f1", "" }, - { "nv!573823", "" }, - { "nv!58027529", "" }, - { "nv!5d2d63", "" }, - { "nv!5f7e3b", "" }, - { "nv!60461793", "" }, - { "nv!60d355", "" }, - { "nv!616627aa", "" }, - { "nv!62317182", "" }, - { "nv!6253fa2e", "" }, - { "nv!64100768", "" }, - { "nv!64100769", "" }, - { "nv!64100770", "" }, - { "nv!647395", "" }, - { "nv!66543234", "" }, - { "nv!67674763", "" }, - { "nv!67739784", "" }, - { "nv!68fb9c", "" }, - { "nv!69801276", "" }, - { "nv!6af9fa2f", "" }, - { "nv!6af9fa3f", "" }, - { "nv!6af9fa4f", "" }, - { "nv!6bd8c7", "" }, - { "nv!6c7691", "" }, - { "nv!6d4296ce", "" }, - { "nv!6dd7e7", "" }, - { "nv!6dd7e8", "" }, - { "nv!6fe11ec1", "" }, - { "nv!716511763", "" }, - { "nv!72504593", "" }, - { "nv!73304097", "" }, - { "nv!73314098", "" }, - { "nv!74095213", "" }, - { "nv!74095213a", "" }, - { "nv!74095213b", "" }, - { "nv!74095214", "" }, - { "nv!748f9649", "" }, - { "nv!75494732", "" }, - { "nv!78452832", "" }, - { "nv!784561", "" }, - { "nv!78e16b9c", "" }, - { "nv!79251225", "" }, - { "nv!7c128b", "" }, - { "nv!7ccd93", "" }, - { "nv!7df8d1", "" }, - { "nv!800c2310", "" }, - { "nv!80546710", "" }, - { "nv!80772310", "" }, - { "nv!808ee280", "" }, - { "nv!81131154", "" }, - { "nv!81274457", "" }, - { "nv!8292291f", "" }, - { "nv!83498426", "" }, - { "nv!84993794", "" }, - { "nv!84995585", "" }, - { "nv!84a0a0", "" }, - { "nv!852142", "" }, - { "nv!85612309", "" }, - { "nv!85612310", "" }, - { "nv!85612311", "" }, - { "nv!85612312", "" }, - { "nv!8623ff27", "" }, - { "nv!87364952", "" }, - { "nv!87f6275666", "" }, - { "nv!886748", "" }, - { "nv!89894423", "" }, - { "nv!8ad8a75", "" }, - { "nv!8ad8ad00", "" }, - { "nv!8bb815", "" }, - { "nv!8bb817", "" }, - { "nv!8bb818", "" }, - { "nv!8bb819", "" }, - { "nv!8e640cd1", "" }, - { "nv!8f34971a", "" }, - { "nv!8f773984", "" }, - { "nv!8f7a7d", "" }, - { "nv!902486209", "" }, - { "nv!90482571", "" }, - { "nv!91214835", "" }, - { "nv!912848290", "" }, - { "nv!915e56", "" }, - { "nv!92179063", "" }, - { "nv!92179064", "" }, - { "nv!92179065", "" }, - { "nv!92179066", "" }, - { "nv!92350358", "" }, - { "nv!92809063", "" }, - { "nv!92809064", "" }, - { "nv!92809065", "" }, - { "nv!92809066", "" }, - { "nv!92920143", "" }, - { "nv!93a89b12", "" }, - { "nv!93a89c0b", "" }, - { "nv!94812574", "" }, - { "nv!95282304", "" }, - { "nv!95394027", "" }, - { "nv!959b1f", "" }, - { "nv!9638af", "" }, - { "nv!96fd59", "" }, - { "nv!97f6275666", "" }, - { "nv!97f6275667", "" }, - { "nv!97f6275668", "" }, - { "nv!97f6275669", "" }, - { "nv!97f627566a", "" }, - { "nv!97f627566b", "" }, - { "nv!97f627566d", "" }, - { "nv!97f627566e", "" }, - { "nv!97f627566f", "" }, - { "nv!97f6275670", "" }, - { "nv!97f6275671", "" }, - { "nv!97f727566e", "" }, - { "nv!98480775", "" }, - { "nv!98480776", "" }, - { "nv!98480777", "" }, - { "nv!992431", "" }, - { "nv!9aa29065", "" }, - { "nv!9af32c", "" }, - { "nv!9af32d", "" }, - { "nv!9af32e", "" }, - { "nv!9c108b71", "" }, - { "nv!9f279065", "" }, - { "nv!a01bc728", "" }, - { "nv!a13b46c80", "" }, - { "nv!a22eb0", "" }, - { "nv!a2fb451e", "" }, - { "nv!a3456abe", "" }, - { "nv!a7044887", "" }, - { "nv!a7149200", "" }, - { "nv!a766215670", "" }, - { "nv!aac_drc_boost", "" }, - { "nv!aac_drc_cut", "" }, - { "nv!aac_drc_enc_target_level", "" }, - { "nv!aac_drc_heavy", "" }, - { "nv!aac_drc_reference_level", "" }, - { "nv!aalinegamma", "" }, - { "nv!aalinetweaks", "" }, - { "nv!ab34ee01", "" }, - { "nv!ab34ee02", "" }, - { "nv!ab34ee03", "" }, - { "nv!ac0274", "" }, - { "nv!af73c63e", "" }, - { "nv!af73c63f", "" }, - { "nv!af9927", "" }, - { "nv!afoverride", "" }, - { "nv!allocdeviceevents", "" }, - { "nv!applicationkey", "" }, - { "nv!appreturnonlybasicglsltype", "" }, - { "nv!app_softimage", "" }, - { "nv!app_supportbits2", "" }, - { "nv!assumetextureismipmappedatcreation", "" }, - { "nv!b1fb0f01", "" }, - { "nv!b3edd5", "" }, - { "nv!b40d9e03d", "" }, - { "nv!b7f6275666", "" }, - { "nv!b812c1", "" }, - { "nv!ba14ba1a", "" }, - { "nv!ba14ba1b", "" }, - { "nv!bd7559", "" }, - { "nv!bd755a", "" }, - { "nv!bd755c", "" }, - { "nv!bd755d", "" }, - { "nv!be58bb", "" }, - { "nv!be92cb", "" }, - { "nv!beefcba3", "" }, - { "nv!beefcba4", "" }, - { "nv!c023777f", "" }, - { "nv!c09dc8", "" }, - { "nv!c0d340", "" }, - { "nv!c2ff374c", "" }, - { "nv!c5e9d7a3", "" }, - { "nv!c5e9d7a4", "" }, - { "nv!c5e9d7b4", "" }, - { "nv!c618f9", "" }, - { "nv!ca345840", "" }, - { "nv!cachedisable", "" }, - { "nv!cast.on", "" }, - { "nv!cde", "" }, - { "nv!channelpriorityoverride", "" }, - { "nv!cleardatastorevidmem", "" }, - { "nv!cmdbufmemoryspaceenables", "" }, - { "nv!cmdbufminwords", "" }, - { "nv!cmdbufsizewords", "" }, - { "nv!conformantblitframebufferscissor", "" }, - { "nv!conformantincompletetextures", "" }, - { "nv!copybuffermethod", "" }, - { "nv!cubemapaniso", "" }, - { "nv!cubemapfiltering", "" }, - { "nv!d0e9a4d7", "" }, - { "nv!d13733f12", "" }, - { "nv!d1b399", "" }, - { "nv!d2983c32", "" }, - { "nv!d2983c33", "" }, - { "nv!d2e71b", "" }, - { "nv!d377dc", "" }, - { "nv!d377dd", "" }, - { "nv!d489f4", "" }, - { "nv!d4bce1", "" }, - { "nv!d518cb", "" }, - { "nv!d518cd", "" }, - { "nv!d518ce", "" }, - { "nv!d518d0", "" }, - { "nv!d518d1", "" }, - { "nv!d518d2", "" }, - { "nv!d518d3", "" }, - { "nv!d518d4", "" }, - { "nv!d518d5", "" }, - { "nv!d59eda", "" }, - { "nv!d83cbd", "" }, - { "nv!d8e777", "" }, - { "nv!debug_level", "" }, - { "nv!debug_mask", "" }, - { "nv!debug_options", "" }, - { "nv!devshmpageableallocations", "" }, - { "nv!df1f9812", "" }, - { "nv!df783c", "" }, - { "nv!diagenable", "" }, - { "nv!disallowcemask", "" }, - { "nv!disallowz16", "" }, - { "nv!dlmemoryspaceenables", "" }, - { "nv!e0bfec", "" }, - { "nv!e433456d", "" }, - { "nv!e435563f", "" }, - { "nv!e4cd9c", "" }, - { "nv!e5c972", "" }, - { "nv!e639ef", "" }, - { "nv!e802af", "" }, - { "nv!eae964", "" }, - { "nv!earlytexturehwallocation", "" }, - { "nv!eb92a3", "" }, - { "nv!ebca56", "" }, - { "nv!enable-noaud", "" }, - { "nv!enable-noavs", "" }, - { "nv!enable-prof", "" }, - { "nv!enable-sxesmode", "" }, - { "nv!enable-ulld", "" }, - { "nv!expert_detail_level", "" }, - { "nv!expert_output_mask", "" }, - { "nv!expert_report_mask", "" }, - { "nv!extensionstringnvarch", "" }, - { "nv!extensionstringversion", "" }, - { "nv!f00f1938", "" }, - { "nv!f10736", "" }, - { "nv!f1846870", "" }, - { "nv!f33bc370", "" }, - { "nv!f392a874", "" }, - { "nv!f49ae8", "" }, - { "nv!fa345cce", "" }, - { "nv!fa35cc4", "" }, - { "nv!faa14a", "" }, - { "nv!faf8a723", "" }, - { "nv!fastgs", "" }, - { "nv!fbf4ac45", "" }, - { "nv!fbo_blit_ignore_srgb", "" }, - { "nv!fc64c7", "" }, - { "nv!ff54ec97", "" }, - { "nv!ff54ec98", "" }, - { "nv!forceexitprocessdetach", "" }, - { "nv!forcerequestedesversion", "" }, - { "nv!__gl_", "" }, - { "nv!__gl_00008600", "" }, - { "nv!__gl_0007b25e", "" }, - { "nv!__gl_0083e1", "" }, - { "nv!__gl_01621887", "" }, - { "nv!__gl_03134743", "" }, - { "nv!__gl_0356afd0", "" }, - { "nv!__gl_0356afd1", "" }, - { "nv!__gl_0356afd2", "" }, - { "nv!__gl_0356afd3", "" }, - { "nv!__gl_094313", "" }, - { "nv!__gl_0x04dc09", "" }, - { "nv!__gl_0x111133", "" }, - { "nv!__gl_0x1aa483", "" }, - { "nv!__gl_0x1cb1cf", "" }, - { "nv!__gl_0x1cb1d0", "" }, - { "nv!__gl_0x1e3221", "" }, - { "nv!__gl_0x300fc8", "" }, - { "nv!__gl_0x301fc8", "" }, - { "nv!__gl_0x302fc8", "" }, - { "nv!__gl_0x3eec59", "" }, - { "nv!__gl_0x46b3ed", "" }, - { "nv!__gl_0x523dc0", "" }, - { "nv!__gl_0x523dc1", "" }, - { "nv!__gl_0x523dc2", "" }, - { "nv!__gl_0x523dc3", "" }, - { "nv!__gl_0x523dc4", "" }, - { "nv!__gl_0x523dc5", "" }, - { "nv!__gl_0x523dc6", "" }, - { "nv!__gl_0x523dd0", "" }, - { "nv!__gl_0x523dd1", "" }, - { "nv!__gl_0x523dd3", "" }, - { "nv!__gl_0x5344bb", "" }, - { "nv!__gl_0x555237", "" }, - { "nv!__gl_0x58a234", "" }, - { "nv!__gl_0x7b4428", "" }, - { "nv!__gl_0x923dc0", "" }, - { "nv!__gl_0x923dc1", "" }, - { "nv!__gl_0x923dc2", "" }, - { "nv!__gl_0x923dc3", "" }, - { "nv!__gl_0x923dc4", "" }, - { "nv!__gl_0x923dd3", "" }, - { "nv!__gl_0x9abdc5", "" }, - { "nv!__gl_0x9abdc6", "" }, - { "nv!__gl_0xaaa36c", "" }, - { "nv!__gl_0xb09da0", "" }, - { "nv!__gl_0xb09da1", "" }, - { "nv!__gl_0xb09da2", "" }, - { "nv!__gl_0xb09da3", "" }, - { "nv!__gl_0xb09da4", "" }, - { "nv!__gl_0xb09da5", "" }, - { "nv!__gl_0xb0b348", "" }, - { "nv!__gl_0xb0b349", "" }, - { "nv!__gl_0xbb558f", "" }, - { "nv!__gl_0xbd10fb", "" }, - { "nv!__gl_0xc32ad3", "" }, - { "nv!__gl_0xce2348", "" }, - { "nv!__gl_0xcfd81f", "" }, - { "nv!__gl_0xe0036b", "" }, - { "nv!__gl_0xe01f2d", "" }, - { "nv!__gl_0xe17212", "" }, - { "nv!__gl_0xeae966", "" }, - { "nv!__gl_0xed4f82", "" }, - { "nv!__gl_0xf12335", "" }, - { "nv!__gl_0xf12336", "" }, - { "nv!__gl_10261989", "" }, - { "nv!__gl_1042d483", "" }, - { "nv!__gl_10572898", "" }, - { "nv!__gl_115631", "" }, - { "nv!__gl_12950094", "" }, - { "nv!__gl_1314f311", "" }, - { "nv!__gl_1314f312", "" }, - { "nv!__gl_13279512", "" }, - { "nv!__gl_13813496", "" }, - { "nv!__gl_14507179", "" }, - { "nv!__gl_15694569", "" }, - { "nv!__gl_16936964", "" }, - { "nv!__gl_17aa230c", "" }, - { "nv!__gl_182054", "" }, - { "nv!__gl_18273275", "" }, - { "nv!__gl_18273276", "" }, - { "nv!__gl_1854d03b", "" }, - { "nv!__gl_18add00d", "" }, - { "nv!__gl_19156670", "" }, - { "nv!__gl_19286545", "" }, - { "nv!__gl_1a298e9f", "" }, - { "nv!__gl_1acf43fe", "" }, - { "nv!__gl_1bda43fe", "" }, - { "nv!__gl_1c3b92", "" }, - { "nv!__gl_21509920", "" }, - { "nv!__gl_215323457", "" }, - { "nv!__gl_2165ad", "" }, - { "nv!__gl_2165ae", "" }, - { "nv!__gl_21be9c", "" }, - { "nv!__gl_233264316", "" }, - { "nv!__gl_234557580", "" }, - { "nv!__gl_23cd0e", "" }, - { "nv!__gl_24189123", "" }, - { "nv!__gl_2443266", "" }, - { "nv!__gl_25025519", "" }, - { "nv!__gl_255e39", "" }, - { "nv!__gl_2583364", "" }, - { "nv!__gl_2888c1", "" }, - { "nv!__gl_28ca3e", "" }, - { "nv!__gl_29871243", "" }, - { "nv!__gl_2a1f64", "" }, - { "nv!__gl_2dc432", "" }, - { "nv!__gl_2de437", "" }, - { "nv!__gl_2f3bb89c", "" }, - { "nv!__gl_2fd652", "" }, - { "nv!__gl_3001ac", "" }, - { "nv!__gl_31298772", "" }, - { "nv!__gl_313233", "" }, - { "nv!__gl_31f7d603", "" }, - { "nv!__gl_320ce4", "" }, - { "nv!__gl_32153248", "" }, - { "nv!__gl_32153249", "" }, - { "nv!__gl_335bca", "" }, - { "nv!__gl_342abb", "" }, - { "nv!__gl_34dfe6", "" }, - { "nv!__gl_34dfe7", "" }, - { "nv!__gl_34dfe8", "" }, - { "nv!__gl_34dfe9", "" }, - { "nv!__gl_35201578", "" }, - { "nv!__gl_359278", "" }, - { "nv!__gl_37f53a", "" }, - { "nv!__gl_38144972", "" }, - { "nv!__gl_38542646", "" }, - { "nv!__gl_3b74c9", "" }, - { "nv!__gl_3c136f", "" }, - { "nv!__gl_3cf72823", "" }, - { "nv!__gl_3d7af029", "" }, - { "nv!__gl_3ff34782", "" }, - { "nv!__gl_4129618", "" }, - { "nv!__gl_4189fac3", "" }, - { "nv!__gl_420bd4", "" }, - { "nv!__gl_42a699", "" }, - { "nv!__gl_441369", "" }, - { "nv!__gl_4458713e", "" }, - { "nv!__gl_4554b6", "" }, - { "nv!__gl_457425", "" }, - { "nv!__gl_4603b207", "" }, - { "nv!__gl_46574957", "" }, - { "nv!__gl_46574958", "" }, - { "nv!__gl_46813529", "" }, - { "nv!__gl_46f1e13d", "" }, - { "nv!__gl_47534c43", "" }, - { "nv!__gl_48550336", "" }, - { "nv!__gl_48576893", "" }, - { "nv!__gl_48576894", "" }, - { "nv!__gl_4889ac02", "" }, - { "nv!__gl_49005740", "" }, - { "nv!__gl_49867584", "" }, - { "nv!__gl_49960973", "" }, - { "nv!__gl_4a5341", "" }, - { "nv!__gl_4f4e48", "" }, - { "nv!__gl_4f8a0a", "" }, - { "nv!__gl_50299698", "" }, - { "nv!__gl_50299699", "" }, - { "nv!__gl_50361291", "" }, - { "nv!__gl_5242ae", "" }, - { "nv!__gl_53d30c", "" }, - { "nv!__gl_56347a", "" }, - { "nv!__gl_563a95f1", "" }, - { "nv!__gl_573823", "" }, - { "nv!__gl_58027529", "" }, - { "nv!__gl_5d2d63", "" }, - { "nv!__gl_5f7e3b", "" }, - { "nv!__gl_60461793", "" }, - { "nv!__gl_60d355", "" }, - { "nv!__gl_616627aa", "" }, - { "nv!__gl_62317182", "" }, - { "nv!__gl_6253fa2e", "" }, - { "nv!__gl_64100768", "" }, - { "nv!__gl_64100769", "" }, - { "nv!__gl_64100770", "" }, - { "nv!__gl_647395", "" }, - { "nv!__gl_66543234", "" }, - { "nv!__gl_67674763", "" }, - { "nv!__gl_67739784", "" }, - { "nv!__gl_68fb9c", "" }, - { "nv!__gl_69801276", "" }, - { "nv!__gl_6af9fa2f", "" }, - { "nv!__gl_6af9fa3f", "" }, - { "nv!__gl_6af9fa4f", "" }, - { "nv!__gl_6bd8c7", "" }, - { "nv!__gl_6c7691", "" }, - { "nv!__gl_6d4296ce", "" }, - { "nv!__gl_6dd7e7", "" }, - { "nv!__gl_6dd7e8", "" }, - { "nv!__gl_6fe11ec1", "" }, - { "nv!__gl_716511763", "" }, - { "nv!__gl_72504593", "" }, - { "nv!__gl_73304097", "" }, - { "nv!__gl_73314098", "" }, - { "nv!__gl_74095213", "" }, - { "nv!__gl_74095213a", "" }, - { "nv!__gl_74095213b", "" }, - { "nv!__gl_74095214", "" }, - { "nv!__gl_748f9649", "" }, - { "nv!__gl_75494732", "" }, - { "nv!__gl_78452832", "" }, - { "nv!__gl_784561", "" }, - { "nv!__gl_78e16b9c", "" }, - { "nv!__gl_79251225", "" }, - { "nv!__gl_7c128b", "" }, - { "nv!__gl_7ccd93", "" }, - { "nv!__gl_7df8d1", "" }, - { "nv!__gl_800c2310", "" }, - { "nv!__gl_80546710", "" }, - { "nv!__gl_80772310", "" }, - { "nv!__gl_808ee280", "" }, - { "nv!__gl_81131154", "" }, - { "nv!__gl_81274457", "" }, - { "nv!__gl_8292291f", "" }, - { "nv!__gl_83498426", "" }, - { "nv!__gl_84993794", "" }, - { "nv!__gl_84995585", "" }, - { "nv!__gl_84a0a0", "" }, - { "nv!__gl_852142", "" }, - { "nv!__gl_85612309", "" }, - { "nv!__gl_85612310", "" }, - { "nv!__gl_85612311", "" }, - { "nv!__gl_85612312", "" }, - { "nv!__gl_8623ff27", "" }, - { "nv!__gl_87364952", "" }, - { "nv!__gl_87f6275666", "" }, - { "nv!__gl_886748", "" }, - { "nv!__gl_89894423", "" }, - { "nv!__gl_8ad8a75", "" }, - { "nv!__gl_8ad8ad00", "" }, - { "nv!__gl_8bb815", "" }, - { "nv!__gl_8bb817", "" }, - { "nv!__gl_8bb818", "" }, - { "nv!__gl_8bb819", "" }, - { "nv!__gl_8e640cd1", "" }, - { "nv!__gl_8f34971a", "" }, - { "nv!__gl_8f773984", "" }, - { "nv!__gl_8f7a7d", "" }, - { "nv!__gl_902486209", "" }, - { "nv!__gl_90482571", "" }, - { "nv!__gl_91214835", "" }, - { "nv!__gl_912848290", "" }, - { "nv!__gl_915e56", "" }, - { "nv!__gl_92179063", "" }, - { "nv!__gl_92179064", "" }, - { "nv!__gl_92179065", "" }, - { "nv!__gl_92179066", "" }, - { "nv!__gl_92350358", "" }, - { "nv!__gl_92809063", "" }, - { "nv!__gl_92809064", "" }, - { "nv!__gl_92809065", "" }, - { "nv!__gl_92809066", "" }, - { "nv!__gl_92920143", "" }, - { "nv!__gl_93a89b12", "" }, - { "nv!__gl_93a89c0b", "" }, - { "nv!__gl_94812574", "" }, - { "nv!__gl_95282304", "" }, - { "nv!__gl_95394027", "" }, - { "nv!__gl_959b1f", "" }, - { "nv!__gl_9638af", "" }, - { "nv!__gl_96fd59", "" }, - { "nv!__gl_97f6275666", "" }, - { "nv!__gl_97f6275667", "" }, - { "nv!__gl_97f6275668", "" }, - { "nv!__gl_97f6275669", "" }, - { "nv!__gl_97f627566a", "" }, - { "nv!__gl_97f627566b", "" }, - { "nv!__gl_97f627566d", "" }, - { "nv!__gl_97f627566e", "" }, - { "nv!__gl_97f627566f", "" }, - { "nv!__gl_97f6275670", "" }, - { "nv!__gl_97f6275671", "" }, - { "nv!__gl_97f727566e", "" }, - { "nv!__gl_98480775", "" }, - { "nv!__gl_98480776", "" }, - { "nv!__gl_98480777", "" }, - { "nv!__gl_992431", "" }, - { "nv!__gl_9aa29065", "" }, - { "nv!__gl_9af32c", "" }, - { "nv!__gl_9af32d", "" }, - { "nv!__gl_9af32e", "" }, - { "nv!__gl_9c108b71", "" }, - { "nv!__gl_9f279065", "" }, - { "nv!__gl_a01bc728", "" }, - { "nv!__gl_a13b46c80", "" }, - { "nv!__gl_a22eb0", "" }, - { "nv!__gl_a2fb451e", "" }, - { "nv!__gl_a3456abe", "" }, - { "nv!__gl_a7044887", "" }, - { "nv!__gl_a7149200", "" }, - { "nv!__gl_a766215670", "" }, - { "nv!__gl_aalinegamma", "" }, - { "nv!__gl_aalinetweaks", "" }, - { "nv!__gl_ab34ee01", "" }, - { "nv!__gl_ab34ee02", "" }, - { "nv!__gl_ab34ee03", "" }, - { "nv!__gl_ac0274", "" }, - { "nv!__gl_af73c63e", "" }, - { "nv!__gl_af73c63f", "" }, - { "nv!__gl_af9927", "" }, - { "nv!__gl_afoverride", "" }, - { "nv!__gl_allocdeviceevents", "" }, - { "nv!__gl_applicationkey", "" }, - { "nv!__gl_appreturnonlybasicglsltype", "" }, - { "nv!__gl_app_softimage", "" }, - { "nv!__gl_app_supportbits2", "" }, - { "nv!__gl_assumetextureismipmappedatcreation", "" }, - { "nv!__gl_b1fb0f01", "" }, - { "nv!__gl_b3edd5", "" }, - { "nv!__gl_b40d9e03d", "" }, - { "nv!__gl_b7f6275666", "" }, - { "nv!__gl_b812c1", "" }, - { "nv!__gl_ba14ba1a", "" }, - { "nv!__gl_ba14ba1b", "" }, - { "nv!__gl_bd7559", "" }, - { "nv!__gl_bd755a", "" }, - { "nv!__gl_bd755c", "" }, - { "nv!__gl_bd755d", "" }, - { "nv!__gl_be58bb", "" }, - { "nv!__gl_be92cb", "" }, - { "nv!__gl_beefcba3", "" }, - { "nv!__gl_beefcba4", "" }, - { "nv!__gl_c023777f", "" }, - { "nv!__gl_c09dc8", "" }, - { "nv!__gl_c0d340", "" }, - { "nv!__gl_c2ff374c", "" }, - { "nv!__gl_c5e9d7a3", "" }, - { "nv!__gl_c5e9d7a4", "" }, - { "nv!__gl_c5e9d7b4", "" }, - { "nv!__gl_c618f9", "" }, - { "nv!__gl_ca345840", "" }, - { "nv!__gl_cachedisable", "" }, - { "nv!__gl_channelpriorityoverride", "" }, - { "nv!__gl_cleardatastorevidmem", "" }, - { "nv!__gl_cmdbufmemoryspaceenables", "" }, - { "nv!__gl_cmdbufminwords", "" }, - { "nv!__gl_cmdbufsizewords", "" }, - { "nv!__gl_conformantblitframebufferscissor", "" }, - { "nv!__gl_conformantincompletetextures", "" }, - { "nv!__gl_copybuffermethod", "" }, - { "nv!__gl_cubemapaniso", "" }, - { "nv!__gl_cubemapfiltering", "" }, - { "nv!__gl_d0e9a4d7", "" }, - { "nv!__gl_d13733f12", "" }, - { "nv!__gl_d1b399", "" }, - { "nv!__gl_d2983c32", "" }, - { "nv!__gl_d2983c33", "" }, - { "nv!__gl_d2e71b", "" }, - { "nv!__gl_d377dc", "" }, - { "nv!__gl_d377dd", "" }, - { "nv!__gl_d489f4", "" }, - { "nv!__gl_d4bce1", "" }, - { "nv!__gl_d518cb", "" }, - { "nv!__gl_d518cd", "" }, - { "nv!__gl_d518ce", "" }, - { "nv!__gl_d518d0", "" }, - { "nv!__gl_d518d1", "" }, - { "nv!__gl_d518d2", "" }, - { "nv!__gl_d518d3", "" }, - { "nv!__gl_d518d4", "" }, - { "nv!__gl_d518d5", "" }, - { "nv!__gl_d59eda", "" }, - { "nv!__gl_d83cbd", "" }, - { "nv!__gl_d8e777", "" }, - { "nv!__gl_debug_level", "" }, - { "nv!__gl_debug_mask", "" }, - { "nv!__gl_debug_options", "" }, - { "nv!__gl_devshmpageableallocations", "" }, - { "nv!__gl_df1f9812", "" }, - { "nv!__gl_df783c", "" }, - { "nv!__gl_diagenable", "" }, - { "nv!__gl_disallowcemask", "" }, - { "nv!__gl_disallowz16", "" }, - { "nv!__gl_dlmemoryspaceenables", "" }, - { "nv!__gl_e0bfec", "" }, - { "nv!__gl_e433456d", "" }, - { "nv!__gl_e435563f", "" }, - { "nv!__gl_e4cd9c", "" }, - { "nv!__gl_e5c972", "" }, - { "nv!__gl_e639ef", "" }, - { "nv!__gl_e802af", "" }, - { "nv!__gl_eae964", "" }, - { "nv!__gl_earlytexturehwallocation", "" }, - { "nv!__gl_eb92a3", "" }, - { "nv!__gl_ebca56", "" }, - { "nv!__gl_expert_detail_level", "" }, - { "nv!__gl_expert_output_mask", "" }, - { "nv!__gl_expert_report_mask", "" }, - { "nv!__gl_extensionstringnvarch", "" }, - { "nv!__gl_extensionstringversion", "" }, - { "nv!__gl_f00f1938", "" }, - { "nv!__gl_f10736", "" }, - { "nv!__gl_f1846870", "" }, - { "nv!__gl_f33bc370", "" }, - { "nv!__gl_f392a874", "" }, - { "nv!__gl_f49ae8", "" }, - { "nv!__gl_fa345cce", "" }, - { "nv!__gl_fa35cc4", "" }, - { "nv!__gl_faa14a", "" }, - { "nv!__gl_faf8a723", "" }, - { "nv!__gl_fastgs", "" }, - { "nv!__gl_fbf4ac45", "" }, - { "nv!__gl_fbo_blit_ignore_srgb", "" }, - { "nv!__gl_fc64c7", "" }, - { "nv!__gl_ff54ec97", "" }, - { "nv!__gl_ff54ec98", "" }, - { "nv!__gl_forceexitprocessdetach", "" }, - { "nv!__gl_forcerequestedesversion", "" }, - { "nv!__gl_glsynctovblank", "" }, - { "nv!__gl_gvitimeoutcontrol", "" }, - { "nv!__gl_hcctrl", "" }, - { "nv!__gl_hwstate_per_ctx", "" }, - { "nv!__gl_machinecachelimit", "" }, - { "nv!__gl_maxframesallowed", "" }, - { "nv!__gl_memmgrcachedalloclimit", "" }, - { "nv!__gl_memmgrcachedalloclimitratio", "" }, - { "nv!__gl_memmgrsysheapalloclimit", "" }, - { "nv!__gl_memmgrsysheapalloclimitratio", "" }, - { "nv!__gl_memmgrvidheapalloclimit", "" }, - { "nv!__gl_mosaic_clip_to_subdev", "" }, - { "nv!__gl_mosaic_clip_to_subdev_h_overlap", "" }, - { "nv!__gl_mosaic_clip_to_subdev_v_overlap", "" }, - { "nv!__gl_overlaymergeblittimerms", "" }, - { "nv!__gl_perfmon_mode", "" }, - { "nv!__gl_pixbar_mode", "" }, - { "nv!__gl_qualityenhancements", "" }, - { "nv!__gl_r27s18q28", "" }, - { "nv!__gl_r2d7c1d8", "" }, - { "nv!__gl_renderer", "" }, - { "nv!__gl_renderqualityflags", "" }, - { "nv!__gl_s3tcquality", "" }, - { "nv!__gl_shaderatomics", "" }, - { "nv!__gl_shadercacheinitsize", "" }, - { "nv!__gl_shader_disk_cache_path", "" }, - { "nv!__gl_shader_disk_cache_read_only", "" }, - { "nv!__gl_shaderobjects", "" }, - { "nv!__gl_shaderportabilitywarnings", "" }, - { "nv!__gl_shaderwarningsaserrors", "" }, - { "nv!__gl_skiptexturehostcopies", "" }, - { "nv!__glslc_debug_level", "" }, - { "nv!__glslc_debug_mask", "" }, - { "nv!__glslc_debug_options", "" }, - { "nv!__glslc_debug_filename", "" }, - { "nv!__gl_sli_dli_control", "" }, - { "nv!__gl_sparsetexture", "" }, - { "nv!__gl_spinlooptimeout", "" }, - { "nv!__gl_sync_to_vblank", "" }, - { "nv!glsynctovblank", "" }, - { "nv!__gl_sysheapreuseratio", "" }, - { "nv!__gl_sysmemtexturepromotion", "" }, - { "nv!__gl_targetflushcount", "" }, - { "nv!__gl_tearingfreeswappresent", "" }, - { "nv!__gl_texclampbehavior", "" }, - { "nv!__gl_texlodbias", "" }, - { "nv!__gl_texmemoryspaceenables", "" }, - { "nv!__gl_textureprecache", "" }, - { "nv!__gl_threadcontrol", "" }, - { "nv!__gl_threadcontrol2", "" }, - { "nv!__gl_usegvievents", "" }, - { "nv!__gl_vbomemoryspaceenables", "" }, - { "nv!__gl_vertexlimit", "" }, - { "nv!__gl_vidheapreuseratio", "" }, - { "nv!__gl_vpipe", "" }, - { "nv!__gl_vpipeformatbloatlimit", "" }, - { "nv!__gl_wglmessageboxonabort", "" }, - { "nv!__gl_writeinfolog", "" }, - { "nv!__gl_writeprogramobjectassembly", "" }, - { "nv!__gl_writeprogramobjectsource", "" }, - { "nv!__gl_xnvadapterpresent", "" }, - { "nv!__gl_yield", "" }, - { "nv!__gl_yieldfunction", "" }, - { "nv!__gl_yieldfunctionfast", "" }, - { "nv!__gl_yieldfunctionslow", "" }, - { "nv!__gl_yieldfunctionwaitfordcqueue", "" }, - { "nv!__gl_yieldfunctionwaitforframe", "" }, - { "nv!__gl_yieldfunctionwaitforgpu", "" }, - { "nv!__gl_zbctableaddhysteresis", "" }, - { "nv!gpu_debug_mode", "" }, - { "nv!gpu_stay_on", "" }, - { "nv!gpu_timeout_ms_max", "" }, - { "nv!gvitimeoutcontrol", "" }, - { "nv!hcctrl", "" }, - { "nv!hwstate_per_ctx", "" }, - { "nv!libandroid_enable_log", "" }, - { "nv!machinecachelimit", "" }, - { "nv!maxframesallowed", "" }, - { "nv!media.aac_51_output_enabled", "" }, - { "nv!memmgrcachedalloclimit", "" }, - { "nv!memmgrcachedalloclimitratio", "" }, - { "nv!memmgrsysheapalloclimit", "" }, - { "nv!memmgrsysheapalloclimitratio", "" }, - { "nv!memmgrvidheapalloclimit", "" }, - { "nv!mosaic_clip_to_subdev", "" }, - { "nv!mosaic_clip_to_subdev_h_overlap", "" }, - { "nv!mosaic_clip_to_subdev_v_overlap", "" }, - { "nv!nvblit.dump", "" }, - { "nv!nvblit.profile", "" }, - { "nv!nvblit.twod", "" }, - { "nv!nvblit.vic", "" }, - { "nv!nvddk_vic_prevent_use", "" }, - { "nv!nv_decompression", "" }, + { "nv!00008600", string.Empty }, + { "nv!0007b25e", string.Empty }, + { "nv!0083e1", string.Empty }, + { "nv!01621887", string.Empty }, + { "nv!03134743", string.Empty }, + { "nv!0356afd0", string.Empty }, + { "nv!0356afd1", string.Empty }, + { "nv!0356afd2", string.Empty }, + { "nv!0356afd3", string.Empty }, + { "nv!094313", string.Empty }, + { "nv!0x04dc09", string.Empty }, + { "nv!0x111133", string.Empty }, + { "nv!0x1aa483", string.Empty }, + { "nv!0x1cb1cf", string.Empty }, + { "nv!0x1cb1d0", string.Empty }, + { "nv!0x1e3221", string.Empty }, + { "nv!0x300fc8", string.Empty }, + { "nv!0x301fc8", string.Empty }, + { "nv!0x302fc8", string.Empty }, + { "nv!0x3eec59", string.Empty }, + { "nv!0x46b3ed", string.Empty }, + { "nv!0x523dc0", string.Empty }, + { "nv!0x523dc1", string.Empty }, + { "nv!0x523dc2", string.Empty }, + { "nv!0x523dc3", string.Empty }, + { "nv!0x523dc4", string.Empty }, + { "nv!0x523dc5", string.Empty }, + { "nv!0x523dc6", string.Empty }, + { "nv!0x523dd0", string.Empty }, + { "nv!0x523dd1", string.Empty }, + { "nv!0x523dd3", string.Empty }, + { "nv!0x5344bb", string.Empty }, + { "nv!0x555237", string.Empty }, + { "nv!0x58a234", string.Empty }, + { "nv!0x7b4428", string.Empty }, + { "nv!0x923dc0", string.Empty }, + { "nv!0x923dc1", string.Empty }, + { "nv!0x923dc2", string.Empty }, + { "nv!0x923dc3", string.Empty }, + { "nv!0x923dc4", string.Empty }, + { "nv!0x923dd3", string.Empty }, + { "nv!0x9abdc5", string.Empty }, + { "nv!0x9abdc6", string.Empty }, + { "nv!0xaaa36c", string.Empty }, + { "nv!0xb09da0", string.Empty }, + { "nv!0xb09da1", string.Empty }, + { "nv!0xb09da2", string.Empty }, + { "nv!0xb09da3", string.Empty }, + { "nv!0xb09da4", string.Empty }, + { "nv!0xb09da5", string.Empty }, + { "nv!0xb0b348", string.Empty }, + { "nv!0xb0b349", string.Empty }, + { "nv!0xbb558f", string.Empty }, + { "nv!0xbd10fb", string.Empty }, + { "nv!0xc32ad3", string.Empty }, + { "nv!0xce2348", string.Empty }, + { "nv!0xcfd81f", string.Empty }, + { "nv!0xe0036b", string.Empty }, + { "nv!0xe01f2d", string.Empty }, + { "nv!0xe17212", string.Empty }, + { "nv!0xeae966", string.Empty }, + { "nv!0xed4f82", string.Empty }, + { "nv!0xf12335", string.Empty }, + { "nv!0xf12336", string.Empty }, + { "nv!10261989", string.Empty }, + { "nv!1042d483", string.Empty }, + { "nv!10572898", string.Empty }, + { "nv!115631", string.Empty }, + { "nv!12950094", string.Empty }, + { "nv!1314f311", string.Empty }, + { "nv!1314f312", string.Empty }, + { "nv!13279512", string.Empty }, + { "nv!13813496", string.Empty }, + { "nv!14507179", string.Empty }, + { "nv!15694569", string.Empty }, + { "nv!16936964", string.Empty }, + { "nv!17aa230c", string.Empty }, + { "nv!182054", string.Empty }, + { "nv!18273275", string.Empty }, + { "nv!18273276", string.Empty }, + { "nv!1854d03b", string.Empty }, + { "nv!18add00d", string.Empty }, + { "nv!19156670", string.Empty }, + { "nv!19286545", string.Empty }, + { "nv!1a298e9f", string.Empty }, + { "nv!1acf43fe", string.Empty }, + { "nv!1bda43fe", string.Empty }, + { "nv!1c3b92", string.Empty }, + { "nv!21509920", string.Empty }, + { "nv!215323457", string.Empty }, + { "nv!2165ad", string.Empty }, + { "nv!2165ae", string.Empty }, + { "nv!21be9c", string.Empty }, + { "nv!233264316", string.Empty }, + { "nv!234557580", string.Empty }, + { "nv!23cd0e", string.Empty }, + { "nv!24189123", string.Empty }, + { "nv!2443266", string.Empty }, + { "nv!25025519", string.Empty }, + { "nv!255e39", string.Empty }, + { "nv!2583364", string.Empty }, + { "nv!2888c1", string.Empty }, + { "nv!28ca3e", string.Empty }, + { "nv!29871243", string.Empty }, + { "nv!2a1f64", string.Empty }, + { "nv!2dc432", string.Empty }, + { "nv!2de437", string.Empty }, + { "nv!2f3bb89c", string.Empty }, + { "nv!2fd652", string.Empty }, + { "nv!3001ac", string.Empty }, + { "nv!31298772", string.Empty }, + { "nv!313233", string.Empty }, + { "nv!31f7d603", string.Empty }, + { "nv!320ce4", string.Empty }, + { "nv!32153248", string.Empty }, + { "nv!32153249", string.Empty }, + { "nv!335bca", string.Empty }, + { "nv!342abb", string.Empty }, + { "nv!34dfe6", string.Empty }, + { "nv!34dfe7", string.Empty }, + { "nv!34dfe8", string.Empty }, + { "nv!34dfe9", string.Empty }, + { "nv!35201578", string.Empty }, + { "nv!359278", string.Empty }, + { "nv!37f53a", string.Empty }, + { "nv!38144972", string.Empty }, + { "nv!38542646", string.Empty }, + { "nv!3b74c9", string.Empty }, + { "nv!3c136f", string.Empty }, + { "nv!3cf72823", string.Empty }, + { "nv!3d7af029", string.Empty }, + { "nv!3ff34782", string.Empty }, + { "nv!4129618", string.Empty }, + { "nv!4189fac3", string.Empty }, + { "nv!420bd4", string.Empty }, + { "nv!42a699", string.Empty }, + { "nv!441369", string.Empty }, + { "nv!4458713e", string.Empty }, + { "nv!4554b6", string.Empty }, + { "nv!457425", string.Empty }, + { "nv!4603b207", string.Empty }, + { "nv!46574957", string.Empty }, + { "nv!46574958", string.Empty }, + { "nv!46813529", string.Empty }, + { "nv!46f1e13d", string.Empty }, + { "nv!47534c43", string.Empty }, + { "nv!48550336", string.Empty }, + { "nv!48576893", string.Empty }, + { "nv!48576894", string.Empty }, + { "nv!4889ac02", string.Empty }, + { "nv!49005740", string.Empty }, + { "nv!49867584", string.Empty }, + { "nv!49960973", string.Empty }, + { "nv!4a5341", string.Empty }, + { "nv!4f4e48", string.Empty }, + { "nv!4f8a0a", string.Empty }, + { "nv!50299698", string.Empty }, + { "nv!50299699", string.Empty }, + { "nv!50361291", string.Empty }, + { "nv!5242ae", string.Empty }, + { "nv!53d30c", string.Empty }, + { "nv!56347a", string.Empty }, + { "nv!563a95f1", string.Empty }, + { "nv!573823", string.Empty }, + { "nv!58027529", string.Empty }, + { "nv!5d2d63", string.Empty }, + { "nv!5f7e3b", string.Empty }, + { "nv!60461793", string.Empty }, + { "nv!60d355", string.Empty }, + { "nv!616627aa", string.Empty }, + { "nv!62317182", string.Empty }, + { "nv!6253fa2e", string.Empty }, + { "nv!64100768", string.Empty }, + { "nv!64100769", string.Empty }, + { "nv!64100770", string.Empty }, + { "nv!647395", string.Empty }, + { "nv!66543234", string.Empty }, + { "nv!67674763", string.Empty }, + { "nv!67739784", string.Empty }, + { "nv!68fb9c", string.Empty }, + { "nv!69801276", string.Empty }, + { "nv!6af9fa2f", string.Empty }, + { "nv!6af9fa3f", string.Empty }, + { "nv!6af9fa4f", string.Empty }, + { "nv!6bd8c7", string.Empty }, + { "nv!6c7691", string.Empty }, + { "nv!6d4296ce", string.Empty }, + { "nv!6dd7e7", string.Empty }, + { "nv!6dd7e8", string.Empty }, + { "nv!6fe11ec1", string.Empty }, + { "nv!716511763", string.Empty }, + { "nv!72504593", string.Empty }, + { "nv!73304097", string.Empty }, + { "nv!73314098", string.Empty }, + { "nv!74095213", string.Empty }, + { "nv!74095213a", string.Empty }, + { "nv!74095213b", string.Empty }, + { "nv!74095214", string.Empty }, + { "nv!748f9649", string.Empty }, + { "nv!75494732", string.Empty }, + { "nv!78452832", string.Empty }, + { "nv!784561", string.Empty }, + { "nv!78e16b9c", string.Empty }, + { "nv!79251225", string.Empty }, + { "nv!7c128b", string.Empty }, + { "nv!7ccd93", string.Empty }, + { "nv!7df8d1", string.Empty }, + { "nv!800c2310", string.Empty }, + { "nv!80546710", string.Empty }, + { "nv!80772310", string.Empty }, + { "nv!808ee280", string.Empty }, + { "nv!81131154", string.Empty }, + { "nv!81274457", string.Empty }, + { "nv!8292291f", string.Empty }, + { "nv!83498426", string.Empty }, + { "nv!84993794", string.Empty }, + { "nv!84995585", string.Empty }, + { "nv!84a0a0", string.Empty }, + { "nv!852142", string.Empty }, + { "nv!85612309", string.Empty }, + { "nv!85612310", string.Empty }, + { "nv!85612311", string.Empty }, + { "nv!85612312", string.Empty }, + { "nv!8623ff27", string.Empty }, + { "nv!87364952", string.Empty }, + { "nv!87f6275666", string.Empty }, + { "nv!886748", string.Empty }, + { "nv!89894423", string.Empty }, + { "nv!8ad8a75", string.Empty }, + { "nv!8ad8ad00", string.Empty }, + { "nv!8bb815", string.Empty }, + { "nv!8bb817", string.Empty }, + { "nv!8bb818", string.Empty }, + { "nv!8bb819", string.Empty }, + { "nv!8e640cd1", string.Empty }, + { "nv!8f34971a", string.Empty }, + { "nv!8f773984", string.Empty }, + { "nv!8f7a7d", string.Empty }, + { "nv!902486209", string.Empty }, + { "nv!90482571", string.Empty }, + { "nv!91214835", string.Empty }, + { "nv!912848290", string.Empty }, + { "nv!915e56", string.Empty }, + { "nv!92179063", string.Empty }, + { "nv!92179064", string.Empty }, + { "nv!92179065", string.Empty }, + { "nv!92179066", string.Empty }, + { "nv!92350358", string.Empty }, + { "nv!92809063", string.Empty }, + { "nv!92809064", string.Empty }, + { "nv!92809065", string.Empty }, + { "nv!92809066", string.Empty }, + { "nv!92920143", string.Empty }, + { "nv!93a89b12", string.Empty }, + { "nv!93a89c0b", string.Empty }, + { "nv!94812574", string.Empty }, + { "nv!95282304", string.Empty }, + { "nv!95394027", string.Empty }, + { "nv!959b1f", string.Empty }, + { "nv!9638af", string.Empty }, + { "nv!96fd59", string.Empty }, + { "nv!97f6275666", string.Empty }, + { "nv!97f6275667", string.Empty }, + { "nv!97f6275668", string.Empty }, + { "nv!97f6275669", string.Empty }, + { "nv!97f627566a", string.Empty }, + { "nv!97f627566b", string.Empty }, + { "nv!97f627566d", string.Empty }, + { "nv!97f627566e", string.Empty }, + { "nv!97f627566f", string.Empty }, + { "nv!97f6275670", string.Empty }, + { "nv!97f6275671", string.Empty }, + { "nv!97f727566e", string.Empty }, + { "nv!98480775", string.Empty }, + { "nv!98480776", string.Empty }, + { "nv!98480777", string.Empty }, + { "nv!992431", string.Empty }, + { "nv!9aa29065", string.Empty }, + { "nv!9af32c", string.Empty }, + { "nv!9af32d", string.Empty }, + { "nv!9af32e", string.Empty }, + { "nv!9c108b71", string.Empty }, + { "nv!9f279065", string.Empty }, + { "nv!a01bc728", string.Empty }, + { "nv!a13b46c80", string.Empty }, + { "nv!a22eb0", string.Empty }, + { "nv!a2fb451e", string.Empty }, + { "nv!a3456abe", string.Empty }, + { "nv!a7044887", string.Empty }, + { "nv!a7149200", string.Empty }, + { "nv!a766215670", string.Empty }, + { "nv!aac_drc_boost", string.Empty }, + { "nv!aac_drc_cut", string.Empty }, + { "nv!aac_drc_enc_target_level", string.Empty }, + { "nv!aac_drc_heavy", string.Empty }, + { "nv!aac_drc_reference_level", string.Empty }, + { "nv!aalinegamma", string.Empty }, + { "nv!aalinetweaks", string.Empty }, + { "nv!ab34ee01", string.Empty }, + { "nv!ab34ee02", string.Empty }, + { "nv!ab34ee03", string.Empty }, + { "nv!ac0274", string.Empty }, + { "nv!af73c63e", string.Empty }, + { "nv!af73c63f", string.Empty }, + { "nv!af9927", string.Empty }, + { "nv!afoverride", string.Empty }, + { "nv!allocdeviceevents", string.Empty }, + { "nv!applicationkey", string.Empty }, + { "nv!appreturnonlybasicglsltype", string.Empty }, + { "nv!app_softimage", string.Empty }, + { "nv!app_supportbits2", string.Empty }, + { "nv!assumetextureismipmappedatcreation", string.Empty }, + { "nv!b1fb0f01", string.Empty }, + { "nv!b3edd5", string.Empty }, + { "nv!b40d9e03d", string.Empty }, + { "nv!b7f6275666", string.Empty }, + { "nv!b812c1", string.Empty }, + { "nv!ba14ba1a", string.Empty }, + { "nv!ba14ba1b", string.Empty }, + { "nv!bd7559", string.Empty }, + { "nv!bd755a", string.Empty }, + { "nv!bd755c", string.Empty }, + { "nv!bd755d", string.Empty }, + { "nv!be58bb", string.Empty }, + { "nv!be92cb", string.Empty }, + { "nv!beefcba3", string.Empty }, + { "nv!beefcba4", string.Empty }, + { "nv!c023777f", string.Empty }, + { "nv!c09dc8", string.Empty }, + { "nv!c0d340", string.Empty }, + { "nv!c2ff374c", string.Empty }, + { "nv!c5e9d7a3", string.Empty }, + { "nv!c5e9d7a4", string.Empty }, + { "nv!c5e9d7b4", string.Empty }, + { "nv!c618f9", string.Empty }, + { "nv!ca345840", string.Empty }, + { "nv!cachedisable", string.Empty }, + { "nv!cast.on", string.Empty }, + { "nv!cde", string.Empty }, + { "nv!channelpriorityoverride", string.Empty }, + { "nv!cleardatastorevidmem", string.Empty }, + { "nv!cmdbufmemoryspaceenables", string.Empty }, + { "nv!cmdbufminwords", string.Empty }, + { "nv!cmdbufsizewords", string.Empty }, + { "nv!conformantblitframebufferscissor", string.Empty }, + { "nv!conformantincompletetextures", string.Empty }, + { "nv!copybuffermethod", string.Empty }, + { "nv!cubemapaniso", string.Empty }, + { "nv!cubemapfiltering", string.Empty }, + { "nv!d0e9a4d7", string.Empty }, + { "nv!d13733f12", string.Empty }, + { "nv!d1b399", string.Empty }, + { "nv!d2983c32", string.Empty }, + { "nv!d2983c33", string.Empty }, + { "nv!d2e71b", string.Empty }, + { "nv!d377dc", string.Empty }, + { "nv!d377dd", string.Empty }, + { "nv!d489f4", string.Empty }, + { "nv!d4bce1", string.Empty }, + { "nv!d518cb", string.Empty }, + { "nv!d518cd", string.Empty }, + { "nv!d518ce", string.Empty }, + { "nv!d518d0", string.Empty }, + { "nv!d518d1", string.Empty }, + { "nv!d518d2", string.Empty }, + { "nv!d518d3", string.Empty }, + { "nv!d518d4", string.Empty }, + { "nv!d518d5", string.Empty }, + { "nv!d59eda", string.Empty }, + { "nv!d83cbd", string.Empty }, + { "nv!d8e777", string.Empty }, + { "nv!debug_level", string.Empty }, + { "nv!debug_mask", string.Empty }, + { "nv!debug_options", string.Empty }, + { "nv!devshmpageableallocations", string.Empty }, + { "nv!df1f9812", string.Empty }, + { "nv!df783c", string.Empty }, + { "nv!diagenable", string.Empty }, + { "nv!disallowcemask", string.Empty }, + { "nv!disallowz16", string.Empty }, + { "nv!dlmemoryspaceenables", string.Empty }, + { "nv!e0bfec", string.Empty }, + { "nv!e433456d", string.Empty }, + { "nv!e435563f", string.Empty }, + { "nv!e4cd9c", string.Empty }, + { "nv!e5c972", string.Empty }, + { "nv!e639ef", string.Empty }, + { "nv!e802af", string.Empty }, + { "nv!eae964", string.Empty }, + { "nv!earlytexturehwallocation", string.Empty }, + { "nv!eb92a3", string.Empty }, + { "nv!ebca56", string.Empty }, + { "nv!enable-noaud", string.Empty }, + { "nv!enable-noavs", string.Empty }, + { "nv!enable-prof", string.Empty }, + { "nv!enable-sxesmode", string.Empty }, + { "nv!enable-ulld", string.Empty }, + { "nv!expert_detail_level", string.Empty }, + { "nv!expert_output_mask", string.Empty }, + { "nv!expert_report_mask", string.Empty }, + { "nv!extensionstringnvarch", string.Empty }, + { "nv!extensionstringversion", string.Empty }, + { "nv!f00f1938", string.Empty }, + { "nv!f10736", string.Empty }, + { "nv!f1846870", string.Empty }, + { "nv!f33bc370", string.Empty }, + { "nv!f392a874", string.Empty }, + { "nv!f49ae8", string.Empty }, + { "nv!fa345cce", string.Empty }, + { "nv!fa35cc4", string.Empty }, + { "nv!faa14a", string.Empty }, + { "nv!faf8a723", string.Empty }, + { "nv!fastgs", string.Empty }, + { "nv!fbf4ac45", string.Empty }, + { "nv!fbo_blit_ignore_srgb", string.Empty }, + { "nv!fc64c7", string.Empty }, + { "nv!ff54ec97", string.Empty }, + { "nv!ff54ec98", string.Empty }, + { "nv!forceexitprocessdetach", string.Empty }, + { "nv!forcerequestedesversion", string.Empty }, + { "nv!__gl_", string.Empty }, + { "nv!__gl_00008600", string.Empty }, + { "nv!__gl_0007b25e", string.Empty }, + { "nv!__gl_0083e1", string.Empty }, + { "nv!__gl_01621887", string.Empty }, + { "nv!__gl_03134743", string.Empty }, + { "nv!__gl_0356afd0", string.Empty }, + { "nv!__gl_0356afd1", string.Empty }, + { "nv!__gl_0356afd2", string.Empty }, + { "nv!__gl_0356afd3", string.Empty }, + { "nv!__gl_094313", string.Empty }, + { "nv!__gl_0x04dc09", string.Empty }, + { "nv!__gl_0x111133", string.Empty }, + { "nv!__gl_0x1aa483", string.Empty }, + { "nv!__gl_0x1cb1cf", string.Empty }, + { "nv!__gl_0x1cb1d0", string.Empty }, + { "nv!__gl_0x1e3221", string.Empty }, + { "nv!__gl_0x300fc8", string.Empty }, + { "nv!__gl_0x301fc8", string.Empty }, + { "nv!__gl_0x302fc8", string.Empty }, + { "nv!__gl_0x3eec59", string.Empty }, + { "nv!__gl_0x46b3ed", string.Empty }, + { "nv!__gl_0x523dc0", string.Empty }, + { "nv!__gl_0x523dc1", string.Empty }, + { "nv!__gl_0x523dc2", string.Empty }, + { "nv!__gl_0x523dc3", string.Empty }, + { "nv!__gl_0x523dc4", string.Empty }, + { "nv!__gl_0x523dc5", string.Empty }, + { "nv!__gl_0x523dc6", string.Empty }, + { "nv!__gl_0x523dd0", string.Empty }, + { "nv!__gl_0x523dd1", string.Empty }, + { "nv!__gl_0x523dd3", string.Empty }, + { "nv!__gl_0x5344bb", string.Empty }, + { "nv!__gl_0x555237", string.Empty }, + { "nv!__gl_0x58a234", string.Empty }, + { "nv!__gl_0x7b4428", string.Empty }, + { "nv!__gl_0x923dc0", string.Empty }, + { "nv!__gl_0x923dc1", string.Empty }, + { "nv!__gl_0x923dc2", string.Empty }, + { "nv!__gl_0x923dc3", string.Empty }, + { "nv!__gl_0x923dc4", string.Empty }, + { "nv!__gl_0x923dd3", string.Empty }, + { "nv!__gl_0x9abdc5", string.Empty }, + { "nv!__gl_0x9abdc6", string.Empty }, + { "nv!__gl_0xaaa36c", string.Empty }, + { "nv!__gl_0xb09da0", string.Empty }, + { "nv!__gl_0xb09da1", string.Empty }, + { "nv!__gl_0xb09da2", string.Empty }, + { "nv!__gl_0xb09da3", string.Empty }, + { "nv!__gl_0xb09da4", string.Empty }, + { "nv!__gl_0xb09da5", string.Empty }, + { "nv!__gl_0xb0b348", string.Empty }, + { "nv!__gl_0xb0b349", string.Empty }, + { "nv!__gl_0xbb558f", string.Empty }, + { "nv!__gl_0xbd10fb", string.Empty }, + { "nv!__gl_0xc32ad3", string.Empty }, + { "nv!__gl_0xce2348", string.Empty }, + { "nv!__gl_0xcfd81f", string.Empty }, + { "nv!__gl_0xe0036b", string.Empty }, + { "nv!__gl_0xe01f2d", string.Empty }, + { "nv!__gl_0xe17212", string.Empty }, + { "nv!__gl_0xeae966", string.Empty }, + { "nv!__gl_0xed4f82", string.Empty }, + { "nv!__gl_0xf12335", string.Empty }, + { "nv!__gl_0xf12336", string.Empty }, + { "nv!__gl_10261989", string.Empty }, + { "nv!__gl_1042d483", string.Empty }, + { "nv!__gl_10572898", string.Empty }, + { "nv!__gl_115631", string.Empty }, + { "nv!__gl_12950094", string.Empty }, + { "nv!__gl_1314f311", string.Empty }, + { "nv!__gl_1314f312", string.Empty }, + { "nv!__gl_13279512", string.Empty }, + { "nv!__gl_13813496", string.Empty }, + { "nv!__gl_14507179", string.Empty }, + { "nv!__gl_15694569", string.Empty }, + { "nv!__gl_16936964", string.Empty }, + { "nv!__gl_17aa230c", string.Empty }, + { "nv!__gl_182054", string.Empty }, + { "nv!__gl_18273275", string.Empty }, + { "nv!__gl_18273276", string.Empty }, + { "nv!__gl_1854d03b", string.Empty }, + { "nv!__gl_18add00d", string.Empty }, + { "nv!__gl_19156670", string.Empty }, + { "nv!__gl_19286545", string.Empty }, + { "nv!__gl_1a298e9f", string.Empty }, + { "nv!__gl_1acf43fe", string.Empty }, + { "nv!__gl_1bda43fe", string.Empty }, + { "nv!__gl_1c3b92", string.Empty }, + { "nv!__gl_21509920", string.Empty }, + { "nv!__gl_215323457", string.Empty }, + { "nv!__gl_2165ad", string.Empty }, + { "nv!__gl_2165ae", string.Empty }, + { "nv!__gl_21be9c", string.Empty }, + { "nv!__gl_233264316", string.Empty }, + { "nv!__gl_234557580", string.Empty }, + { "nv!__gl_23cd0e", string.Empty }, + { "nv!__gl_24189123", string.Empty }, + { "nv!__gl_2443266", string.Empty }, + { "nv!__gl_25025519", string.Empty }, + { "nv!__gl_255e39", string.Empty }, + { "nv!__gl_2583364", string.Empty }, + { "nv!__gl_2888c1", string.Empty }, + { "nv!__gl_28ca3e", string.Empty }, + { "nv!__gl_29871243", string.Empty }, + { "nv!__gl_2a1f64", string.Empty }, + { "nv!__gl_2dc432", string.Empty }, + { "nv!__gl_2de437", string.Empty }, + { "nv!__gl_2f3bb89c", string.Empty }, + { "nv!__gl_2fd652", string.Empty }, + { "nv!__gl_3001ac", string.Empty }, + { "nv!__gl_31298772", string.Empty }, + { "nv!__gl_313233", string.Empty }, + { "nv!__gl_31f7d603", string.Empty }, + { "nv!__gl_320ce4", string.Empty }, + { "nv!__gl_32153248", string.Empty }, + { "nv!__gl_32153249", string.Empty }, + { "nv!__gl_335bca", string.Empty }, + { "nv!__gl_342abb", string.Empty }, + { "nv!__gl_34dfe6", string.Empty }, + { "nv!__gl_34dfe7", string.Empty }, + { "nv!__gl_34dfe8", string.Empty }, + { "nv!__gl_34dfe9", string.Empty }, + { "nv!__gl_35201578", string.Empty }, + { "nv!__gl_359278", string.Empty }, + { "nv!__gl_37f53a", string.Empty }, + { "nv!__gl_38144972", string.Empty }, + { "nv!__gl_38542646", string.Empty }, + { "nv!__gl_3b74c9", string.Empty }, + { "nv!__gl_3c136f", string.Empty }, + { "nv!__gl_3cf72823", string.Empty }, + { "nv!__gl_3d7af029", string.Empty }, + { "nv!__gl_3ff34782", string.Empty }, + { "nv!__gl_4129618", string.Empty }, + { "nv!__gl_4189fac3", string.Empty }, + { "nv!__gl_420bd4", string.Empty }, + { "nv!__gl_42a699", string.Empty }, + { "nv!__gl_441369", string.Empty }, + { "nv!__gl_4458713e", string.Empty }, + { "nv!__gl_4554b6", string.Empty }, + { "nv!__gl_457425", string.Empty }, + { "nv!__gl_4603b207", string.Empty }, + { "nv!__gl_46574957", string.Empty }, + { "nv!__gl_46574958", string.Empty }, + { "nv!__gl_46813529", string.Empty }, + { "nv!__gl_46f1e13d", string.Empty }, + { "nv!__gl_47534c43", string.Empty }, + { "nv!__gl_48550336", string.Empty }, + { "nv!__gl_48576893", string.Empty }, + { "nv!__gl_48576894", string.Empty }, + { "nv!__gl_4889ac02", string.Empty }, + { "nv!__gl_49005740", string.Empty }, + { "nv!__gl_49867584", string.Empty }, + { "nv!__gl_49960973", string.Empty }, + { "nv!__gl_4a5341", string.Empty }, + { "nv!__gl_4f4e48", string.Empty }, + { "nv!__gl_4f8a0a", string.Empty }, + { "nv!__gl_50299698", string.Empty }, + { "nv!__gl_50299699", string.Empty }, + { "nv!__gl_50361291", string.Empty }, + { "nv!__gl_5242ae", string.Empty }, + { "nv!__gl_53d30c", string.Empty }, + { "nv!__gl_56347a", string.Empty }, + { "nv!__gl_563a95f1", string.Empty }, + { "nv!__gl_573823", string.Empty }, + { "nv!__gl_58027529", string.Empty }, + { "nv!__gl_5d2d63", string.Empty }, + { "nv!__gl_5f7e3b", string.Empty }, + { "nv!__gl_60461793", string.Empty }, + { "nv!__gl_60d355", string.Empty }, + { "nv!__gl_616627aa", string.Empty }, + { "nv!__gl_62317182", string.Empty }, + { "nv!__gl_6253fa2e", string.Empty }, + { "nv!__gl_64100768", string.Empty }, + { "nv!__gl_64100769", string.Empty }, + { "nv!__gl_64100770", string.Empty }, + { "nv!__gl_647395", string.Empty }, + { "nv!__gl_66543234", string.Empty }, + { "nv!__gl_67674763", string.Empty }, + { "nv!__gl_67739784", string.Empty }, + { "nv!__gl_68fb9c", string.Empty }, + { "nv!__gl_69801276", string.Empty }, + { "nv!__gl_6af9fa2f", string.Empty }, + { "nv!__gl_6af9fa3f", string.Empty }, + { "nv!__gl_6af9fa4f", string.Empty }, + { "nv!__gl_6bd8c7", string.Empty }, + { "nv!__gl_6c7691", string.Empty }, + { "nv!__gl_6d4296ce", string.Empty }, + { "nv!__gl_6dd7e7", string.Empty }, + { "nv!__gl_6dd7e8", string.Empty }, + { "nv!__gl_6fe11ec1", string.Empty }, + { "nv!__gl_716511763", string.Empty }, + { "nv!__gl_72504593", string.Empty }, + { "nv!__gl_73304097", string.Empty }, + { "nv!__gl_73314098", string.Empty }, + { "nv!__gl_74095213", string.Empty }, + { "nv!__gl_74095213a", string.Empty }, + { "nv!__gl_74095213b", string.Empty }, + { "nv!__gl_74095214", string.Empty }, + { "nv!__gl_748f9649", string.Empty }, + { "nv!__gl_75494732", string.Empty }, + { "nv!__gl_78452832", string.Empty }, + { "nv!__gl_784561", string.Empty }, + { "nv!__gl_78e16b9c", string.Empty }, + { "nv!__gl_79251225", string.Empty }, + { "nv!__gl_7c128b", string.Empty }, + { "nv!__gl_7ccd93", string.Empty }, + { "nv!__gl_7df8d1", string.Empty }, + { "nv!__gl_800c2310", string.Empty }, + { "nv!__gl_80546710", string.Empty }, + { "nv!__gl_80772310", string.Empty }, + { "nv!__gl_808ee280", string.Empty }, + { "nv!__gl_81131154", string.Empty }, + { "nv!__gl_81274457", string.Empty }, + { "nv!__gl_8292291f", string.Empty }, + { "nv!__gl_83498426", string.Empty }, + { "nv!__gl_84993794", string.Empty }, + { "nv!__gl_84995585", string.Empty }, + { "nv!__gl_84a0a0", string.Empty }, + { "nv!__gl_852142", string.Empty }, + { "nv!__gl_85612309", string.Empty }, + { "nv!__gl_85612310", string.Empty }, + { "nv!__gl_85612311", string.Empty }, + { "nv!__gl_85612312", string.Empty }, + { "nv!__gl_8623ff27", string.Empty }, + { "nv!__gl_87364952", string.Empty }, + { "nv!__gl_87f6275666", string.Empty }, + { "nv!__gl_886748", string.Empty }, + { "nv!__gl_89894423", string.Empty }, + { "nv!__gl_8ad8a75", string.Empty }, + { "nv!__gl_8ad8ad00", string.Empty }, + { "nv!__gl_8bb815", string.Empty }, + { "nv!__gl_8bb817", string.Empty }, + { "nv!__gl_8bb818", string.Empty }, + { "nv!__gl_8bb819", string.Empty }, + { "nv!__gl_8e640cd1", string.Empty }, + { "nv!__gl_8f34971a", string.Empty }, + { "nv!__gl_8f773984", string.Empty }, + { "nv!__gl_8f7a7d", string.Empty }, + { "nv!__gl_902486209", string.Empty }, + { "nv!__gl_90482571", string.Empty }, + { "nv!__gl_91214835", string.Empty }, + { "nv!__gl_912848290", string.Empty }, + { "nv!__gl_915e56", string.Empty }, + { "nv!__gl_92179063", string.Empty }, + { "nv!__gl_92179064", string.Empty }, + { "nv!__gl_92179065", string.Empty }, + { "nv!__gl_92179066", string.Empty }, + { "nv!__gl_92350358", string.Empty }, + { "nv!__gl_92809063", string.Empty }, + { "nv!__gl_92809064", string.Empty }, + { "nv!__gl_92809065", string.Empty }, + { "nv!__gl_92809066", string.Empty }, + { "nv!__gl_92920143", string.Empty }, + { "nv!__gl_93a89b12", string.Empty }, + { "nv!__gl_93a89c0b", string.Empty }, + { "nv!__gl_94812574", string.Empty }, + { "nv!__gl_95282304", string.Empty }, + { "nv!__gl_95394027", string.Empty }, + { "nv!__gl_959b1f", string.Empty }, + { "nv!__gl_9638af", string.Empty }, + { "nv!__gl_96fd59", string.Empty }, + { "nv!__gl_97f6275666", string.Empty }, + { "nv!__gl_97f6275667", string.Empty }, + { "nv!__gl_97f6275668", string.Empty }, + { "nv!__gl_97f6275669", string.Empty }, + { "nv!__gl_97f627566a", string.Empty }, + { "nv!__gl_97f627566b", string.Empty }, + { "nv!__gl_97f627566d", string.Empty }, + { "nv!__gl_97f627566e", string.Empty }, + { "nv!__gl_97f627566f", string.Empty }, + { "nv!__gl_97f6275670", string.Empty }, + { "nv!__gl_97f6275671", string.Empty }, + { "nv!__gl_97f727566e", string.Empty }, + { "nv!__gl_98480775", string.Empty }, + { "nv!__gl_98480776", string.Empty }, + { "nv!__gl_98480777", string.Empty }, + { "nv!__gl_992431", string.Empty }, + { "nv!__gl_9aa29065", string.Empty }, + { "nv!__gl_9af32c", string.Empty }, + { "nv!__gl_9af32d", string.Empty }, + { "nv!__gl_9af32e", string.Empty }, + { "nv!__gl_9c108b71", string.Empty }, + { "nv!__gl_9f279065", string.Empty }, + { "nv!__gl_a01bc728", string.Empty }, + { "nv!__gl_a13b46c80", string.Empty }, + { "nv!__gl_a22eb0", string.Empty }, + { "nv!__gl_a2fb451e", string.Empty }, + { "nv!__gl_a3456abe", string.Empty }, + { "nv!__gl_a7044887", string.Empty }, + { "nv!__gl_a7149200", string.Empty }, + { "nv!__gl_a766215670", string.Empty }, + { "nv!__gl_aalinegamma", string.Empty }, + { "nv!__gl_aalinetweaks", string.Empty }, + { "nv!__gl_ab34ee01", string.Empty }, + { "nv!__gl_ab34ee02", string.Empty }, + { "nv!__gl_ab34ee03", string.Empty }, + { "nv!__gl_ac0274", string.Empty }, + { "nv!__gl_af73c63e", string.Empty }, + { "nv!__gl_af73c63f", string.Empty }, + { "nv!__gl_af9927", string.Empty }, + { "nv!__gl_afoverride", string.Empty }, + { "nv!__gl_allocdeviceevents", string.Empty }, + { "nv!__gl_applicationkey", string.Empty }, + { "nv!__gl_appreturnonlybasicglsltype", string.Empty }, + { "nv!__gl_app_softimage", string.Empty }, + { "nv!__gl_app_supportbits2", string.Empty }, + { "nv!__gl_assumetextureismipmappedatcreation", string.Empty }, + { "nv!__gl_b1fb0f01", string.Empty }, + { "nv!__gl_b3edd5", string.Empty }, + { "nv!__gl_b40d9e03d", string.Empty }, + { "nv!__gl_b7f6275666", string.Empty }, + { "nv!__gl_b812c1", string.Empty }, + { "nv!__gl_ba14ba1a", string.Empty }, + { "nv!__gl_ba14ba1b", string.Empty }, + { "nv!__gl_bd7559", string.Empty }, + { "nv!__gl_bd755a", string.Empty }, + { "nv!__gl_bd755c", string.Empty }, + { "nv!__gl_bd755d", string.Empty }, + { "nv!__gl_be58bb", string.Empty }, + { "nv!__gl_be92cb", string.Empty }, + { "nv!__gl_beefcba3", string.Empty }, + { "nv!__gl_beefcba4", string.Empty }, + { "nv!__gl_c023777f", string.Empty }, + { "nv!__gl_c09dc8", string.Empty }, + { "nv!__gl_c0d340", string.Empty }, + { "nv!__gl_c2ff374c", string.Empty }, + { "nv!__gl_c5e9d7a3", string.Empty }, + { "nv!__gl_c5e9d7a4", string.Empty }, + { "nv!__gl_c5e9d7b4", string.Empty }, + { "nv!__gl_c618f9", string.Empty }, + { "nv!__gl_ca345840", string.Empty }, + { "nv!__gl_cachedisable", string.Empty }, + { "nv!__gl_channelpriorityoverride", string.Empty }, + { "nv!__gl_cleardatastorevidmem", string.Empty }, + { "nv!__gl_cmdbufmemoryspaceenables", string.Empty }, + { "nv!__gl_cmdbufminwords", string.Empty }, + { "nv!__gl_cmdbufsizewords", string.Empty }, + { "nv!__gl_conformantblitframebufferscissor", string.Empty }, + { "nv!__gl_conformantincompletetextures", string.Empty }, + { "nv!__gl_copybuffermethod", string.Empty }, + { "nv!__gl_cubemapaniso", string.Empty }, + { "nv!__gl_cubemapfiltering", string.Empty }, + { "nv!__gl_d0e9a4d7", string.Empty }, + { "nv!__gl_d13733f12", string.Empty }, + { "nv!__gl_d1b399", string.Empty }, + { "nv!__gl_d2983c32", string.Empty }, + { "nv!__gl_d2983c33", string.Empty }, + { "nv!__gl_d2e71b", string.Empty }, + { "nv!__gl_d377dc", string.Empty }, + { "nv!__gl_d377dd", string.Empty }, + { "nv!__gl_d489f4", string.Empty }, + { "nv!__gl_d4bce1", string.Empty }, + { "nv!__gl_d518cb", string.Empty }, + { "nv!__gl_d518cd", string.Empty }, + { "nv!__gl_d518ce", string.Empty }, + { "nv!__gl_d518d0", string.Empty }, + { "nv!__gl_d518d1", string.Empty }, + { "nv!__gl_d518d2", string.Empty }, + { "nv!__gl_d518d3", string.Empty }, + { "nv!__gl_d518d4", string.Empty }, + { "nv!__gl_d518d5", string.Empty }, + { "nv!__gl_d59eda", string.Empty }, + { "nv!__gl_d83cbd", string.Empty }, + { "nv!__gl_d8e777", string.Empty }, + { "nv!__gl_debug_level", string.Empty }, + { "nv!__gl_debug_mask", string.Empty }, + { "nv!__gl_debug_options", string.Empty }, + { "nv!__gl_devshmpageableallocations", string.Empty }, + { "nv!__gl_df1f9812", string.Empty }, + { "nv!__gl_df783c", string.Empty }, + { "nv!__gl_diagenable", string.Empty }, + { "nv!__gl_disallowcemask", string.Empty }, + { "nv!__gl_disallowz16", string.Empty }, + { "nv!__gl_dlmemoryspaceenables", string.Empty }, + { "nv!__gl_e0bfec", string.Empty }, + { "nv!__gl_e433456d", string.Empty }, + { "nv!__gl_e435563f", string.Empty }, + { "nv!__gl_e4cd9c", string.Empty }, + { "nv!__gl_e5c972", string.Empty }, + { "nv!__gl_e639ef", string.Empty }, + { "nv!__gl_e802af", string.Empty }, + { "nv!__gl_eae964", string.Empty }, + { "nv!__gl_earlytexturehwallocation", string.Empty }, + { "nv!__gl_eb92a3", string.Empty }, + { "nv!__gl_ebca56", string.Empty }, + { "nv!__gl_expert_detail_level", string.Empty }, + { "nv!__gl_expert_output_mask", string.Empty }, + { "nv!__gl_expert_report_mask", string.Empty }, + { "nv!__gl_extensionstringnvarch", string.Empty }, + { "nv!__gl_extensionstringversion", string.Empty }, + { "nv!__gl_f00f1938", string.Empty }, + { "nv!__gl_f10736", string.Empty }, + { "nv!__gl_f1846870", string.Empty }, + { "nv!__gl_f33bc370", string.Empty }, + { "nv!__gl_f392a874", string.Empty }, + { "nv!__gl_f49ae8", string.Empty }, + { "nv!__gl_fa345cce", string.Empty }, + { "nv!__gl_fa35cc4", string.Empty }, + { "nv!__gl_faa14a", string.Empty }, + { "nv!__gl_faf8a723", string.Empty }, + { "nv!__gl_fastgs", string.Empty }, + { "nv!__gl_fbf4ac45", string.Empty }, + { "nv!__gl_fbo_blit_ignore_srgb", string.Empty }, + { "nv!__gl_fc64c7", string.Empty }, + { "nv!__gl_ff54ec97", string.Empty }, + { "nv!__gl_ff54ec98", string.Empty }, + { "nv!__gl_forceexitprocessdetach", string.Empty }, + { "nv!__gl_forcerequestedesversion", string.Empty }, + { "nv!__gl_glsynctovblank", string.Empty }, + { "nv!__gl_gvitimeoutcontrol", string.Empty }, + { "nv!__gl_hcctrl", string.Empty }, + { "nv!__gl_hwstate_per_ctx", string.Empty }, + { "nv!__gl_machinecachelimit", string.Empty }, + { "nv!__gl_maxframesallowed", string.Empty }, + { "nv!__gl_memmgrcachedalloclimit", string.Empty }, + { "nv!__gl_memmgrcachedalloclimitratio", string.Empty }, + { "nv!__gl_memmgrsysheapalloclimit", string.Empty }, + { "nv!__gl_memmgrsysheapalloclimitratio", string.Empty }, + { "nv!__gl_memmgrvidheapalloclimit", string.Empty }, + { "nv!__gl_mosaic_clip_to_subdev", string.Empty }, + { "nv!__gl_mosaic_clip_to_subdev_h_overlap", string.Empty }, + { "nv!__gl_mosaic_clip_to_subdev_v_overlap", string.Empty }, + { "nv!__gl_overlaymergeblittimerms", string.Empty }, + { "nv!__gl_perfmon_mode", string.Empty }, + { "nv!__gl_pixbar_mode", string.Empty }, + { "nv!__gl_qualityenhancements", string.Empty }, + { "nv!__gl_r27s18q28", string.Empty }, + { "nv!__gl_r2d7c1d8", string.Empty }, + { "nv!__gl_renderer", string.Empty }, + { "nv!__gl_renderqualityflags", string.Empty }, + { "nv!__gl_s3tcquality", string.Empty }, + { "nv!__gl_shaderatomics", string.Empty }, + { "nv!__gl_shadercacheinitsize", string.Empty }, + { "nv!__gl_shader_disk_cache_path", string.Empty }, + { "nv!__gl_shader_disk_cache_read_only", string.Empty }, + { "nv!__gl_shaderobjects", string.Empty }, + { "nv!__gl_shaderportabilitywarnings", string.Empty }, + { "nv!__gl_shaderwarningsaserrors", string.Empty }, + { "nv!__gl_skiptexturehostcopies", string.Empty }, + { "nv!__glslc_debug_level", string.Empty }, + { "nv!__glslc_debug_mask", string.Empty }, + { "nv!__glslc_debug_options", string.Empty }, + { "nv!__glslc_debug_filename", string.Empty }, + { "nv!__gl_sli_dli_control", string.Empty }, + { "nv!__gl_sparsetexture", string.Empty }, + { "nv!__gl_spinlooptimeout", string.Empty }, + { "nv!__gl_sync_to_vblank", string.Empty }, + { "nv!glsynctovblank", string.Empty }, + { "nv!__gl_sysheapreuseratio", string.Empty }, + { "nv!__gl_sysmemtexturepromotion", string.Empty }, + { "nv!__gl_targetflushcount", string.Empty }, + { "nv!__gl_tearingfreeswappresent", string.Empty }, + { "nv!__gl_texclampbehavior", string.Empty }, + { "nv!__gl_texlodbias", string.Empty }, + { "nv!__gl_texmemoryspaceenables", string.Empty }, + { "nv!__gl_textureprecache", string.Empty }, + { "nv!__gl_threadcontrol", string.Empty }, + { "nv!__gl_threadcontrol2", string.Empty }, + { "nv!__gl_usegvievents", string.Empty }, + { "nv!__gl_vbomemoryspaceenables", string.Empty }, + { "nv!__gl_vertexlimit", string.Empty }, + { "nv!__gl_vidheapreuseratio", string.Empty }, + { "nv!__gl_vpipe", string.Empty }, + { "nv!__gl_vpipeformatbloatlimit", string.Empty }, + { "nv!__gl_wglmessageboxonabort", string.Empty }, + { "nv!__gl_writeinfolog", string.Empty }, + { "nv!__gl_writeprogramobjectassembly", string.Empty }, + { "nv!__gl_writeprogramobjectsource", string.Empty }, + { "nv!__gl_xnvadapterpresent", string.Empty }, + { "nv!__gl_yield", string.Empty }, + { "nv!__gl_yieldfunction", string.Empty }, + { "nv!__gl_yieldfunctionfast", string.Empty }, + { "nv!__gl_yieldfunctionslow", string.Empty }, + { "nv!__gl_yieldfunctionwaitfordcqueue", string.Empty }, + { "nv!__gl_yieldfunctionwaitforframe", string.Empty }, + { "nv!__gl_yieldfunctionwaitforgpu", string.Empty }, + { "nv!__gl_zbctableaddhysteresis", string.Empty }, + { "nv!gpu_debug_mode", string.Empty }, + { "nv!gpu_stay_on", string.Empty }, + { "nv!gpu_timeout_ms_max", string.Empty }, + { "nv!gvitimeoutcontrol", string.Empty }, + { "nv!hcctrl", string.Empty }, + { "nv!hwstate_per_ctx", string.Empty }, + { "nv!libandroid_enable_log", string.Empty }, + { "nv!machinecachelimit", string.Empty }, + { "nv!maxframesallowed", string.Empty }, + { "nv!media.aac_51_output_enabled", string.Empty }, + { "nv!memmgrcachedalloclimit", string.Empty }, + { "nv!memmgrcachedalloclimitratio", string.Empty }, + { "nv!memmgrsysheapalloclimit", string.Empty }, + { "nv!memmgrsysheapalloclimitratio", string.Empty }, + { "nv!memmgrvidheapalloclimit", string.Empty }, + { "nv!mosaic_clip_to_subdev", string.Empty }, + { "nv!mosaic_clip_to_subdev_h_overlap", string.Empty }, + { "nv!mosaic_clip_to_subdev_v_overlap", string.Empty }, + { "nv!nvblit.dump", string.Empty }, + { "nv!nvblit.profile", string.Empty }, + { "nv!nvblit.twod", string.Empty }, + { "nv!nvblit.vic", string.Empty }, + { "nv!nvddk_vic_prevent_use", string.Empty }, + { "nv!nv_decompression", string.Empty }, { "nv!nvdisp_bl_ctrl", "0" }, - { "nv!nvdisp_debug_mask", "" }, + { "nv!nvdisp_debug_mask", string.Empty }, { "nv!nvdisp_enable_ts", "0" }, { "nv!nvhdcp_timeout_ms", "12000" }, { "nv!nvhdcp_max_retries", "5" }, - { "nv!nv_emc_dvfs_test", "" }, - { "nv!nv_emc_init_rate_hz", "" }, - { "nv!nv_gmmu_va_page_split", "" }, - { "nv!nv_gmmu_va_range", "" }, - { "nv!nvhost_debug_mask", "" }, - { "nv!nvidia.hwc.dump_config", "" }, - { "nv!nvidia.hwc.dump_layerlist", "" }, - { "nv!nvidia.hwc.dump_windows", "" }, - { "nv!nvidia.hwc.enable_disp_trans", "" }, - { "nv!nvidia.hwc.ftrace_enable", "" }, - { "nv!nvidia.hwc.hdcp_enable", "" }, - { "nv!nvidia.hwc.hidden_window_mask0", "" }, - { "nv!nvidia.hwc.hidden_window_mask1", "" }, - { "nv!nvidia.hwc.immediate_modeset", "" }, - { "nv!nvidia.hwc.imp_enable", "" }, - { "nv!nvidia.hwc.no_egl", "" }, - { "nv!nvidia.hwc.no_scratchblit", "" }, - { "nv!nvidia.hwc.no_vic", "" }, - { "nv!nvidia.hwc.null_display", "" }, - { "nv!nvidia.hwc.scan_props", "" }, - { "nv!nvidia.hwc.swap_interval", "" }, + { "nv!nv_emc_dvfs_test", string.Empty }, + { "nv!nv_emc_init_rate_hz", string.Empty }, + { "nv!nv_gmmu_va_page_split", string.Empty }, + { "nv!nv_gmmu_va_range", string.Empty }, + { "nv!nvhost_debug_mask", string.Empty }, + { "nv!nvidia.hwc.dump_config", string.Empty }, + { "nv!nvidia.hwc.dump_layerlist", string.Empty }, + { "nv!nvidia.hwc.dump_windows", string.Empty }, + { "nv!nvidia.hwc.enable_disp_trans", string.Empty }, + { "nv!nvidia.hwc.ftrace_enable", string.Empty }, + { "nv!nvidia.hwc.hdcp_enable", string.Empty }, + { "nv!nvidia.hwc.hidden_window_mask0", string.Empty }, + { "nv!nvidia.hwc.hidden_window_mask1", string.Empty }, + { "nv!nvidia.hwc.immediate_modeset", string.Empty }, + { "nv!nvidia.hwc.imp_enable", string.Empty }, + { "nv!nvidia.hwc.no_egl", string.Empty }, + { "nv!nvidia.hwc.no_scratchblit", string.Empty }, + { "nv!nvidia.hwc.no_vic", string.Empty }, + { "nv!nvidia.hwc.null_display", string.Empty }, + { "nv!nvidia.hwc.scan_props", string.Empty }, + { "nv!nvidia.hwc.swap_interval", string.Empty }, { "nv!nvidia.hwc.war_1515812", "0" }, - { "nv!nvmap_debug_mask", "" }, - { "nv!nv_memory_profiler", "" }, - { "nv!nvnflinger_enable_log", "" }, - { "nv!nvnflinger_flip_policy", "" }, + { "nv!nvmap_debug_mask", string.Empty }, + { "nv!nv_memory_profiler", string.Empty }, + { "nv!nvnflinger_enable_log", string.Empty }, + { "nv!nvnflinger_flip_policy", string.Empty }, { "nv!nvnflinger_hotplug_autoswitch", "0" }, { "nv!nvnflinger_prefer_primary_layer", "0" }, - { "nv!nvnflinger_service_priority", "" }, - { "nv!nvnflinger_service_threads", "" }, - { "nv!nvnflinger_swap_interval", "" }, - { "nv!nvnflinger_track_perf", "" }, + { "nv!nvnflinger_service_priority", string.Empty }, + { "nv!nvnflinger_service_threads", string.Empty }, + { "nv!nvnflinger_swap_interval", string.Empty }, + { "nv!nvnflinger_track_perf", string.Empty }, { "nv!nvnflinger_virtualdisplay_policy", "60hz" }, { "nv!nvn_no_vsync_capability", false }, - { "nv!nvn_through_opengl", "" }, - { "nv!nv_pllcx_always_on", "" }, - { "nv!nv_pllcx_safe_div", "" }, - { "nv!nvrm_gpu_channel_interleave", "" }, - { "nv!nvrm_gpu_channel_priority", "" }, - { "nv!nvrm_gpu_channel_timeslice", "" }, - { "nv!nvrm_gpu_default_device_index", "" }, - { "nv!nvrm_gpu_dummy", "" }, - { "nv!nvrm_gpu_help", "" }, - { "nv!nvrm_gpu_nvgpu_disable", "" }, - { "nv!nvrm_gpu_nvgpu_do_nfa_partial_map", "" }, - { "nv!nvrm_gpu_nvgpu_ecc_overrides", "" }, - { "nv!nvrm_gpu_nvgpu_no_as_get_va_regions", "" }, - { "nv!nvrm_gpu_nvgpu_no_channel_abort", "" }, - { "nv!nvrm_gpu_nvgpu_no_cyclestats", "" }, - { "nv!nvrm_gpu_nvgpu_no_fixed", "" }, - { "nv!nvrm_gpu_nvgpu_no_gpu_characteristics", "" }, - { "nv!nvrm_gpu_nvgpu_no_ioctl_mutex", "" }, - { "nv!nvrm_gpu_nvgpu_no_map_buffer_ex", "" }, - { "nv!nvrm_gpu_nvgpu_no_robustness", "" }, - { "nv!nvrm_gpu_nvgpu_no_sparse", "" }, - { "nv!nvrm_gpu_nvgpu_no_syncpoints", "" }, - { "nv!nvrm_gpu_nvgpu_no_tsg", "" }, - { "nv!nvrm_gpu_nvgpu_no_zbc", "" }, - { "nv!nvrm_gpu_nvgpu_no_zcull", "" }, - { "nv!nvrm_gpu_nvgpu_wrap_channels_in_tsgs", "" }, - { "nv!nvrm_gpu_prevent_use", "" }, - { "nv!nvrm_gpu_trace", "" }, - { "nv!nvsched_debug_mask", "" }, - { "nv!nvsched_force_enable", "" }, - { "nv!nvsched_force_log", "" }, - { "nv!nv_usb_plls_hw_ctrl", "" }, - { "nv!nv_winsys", "" }, - { "nv!nvwsi_dump", "" }, - { "nv!nvwsi_fill", "" }, - { "nv!ogl_", "" }, - { "nv!ogl_0356afd0", "" }, - { "nv!ogl_0356afd1", "" }, - { "nv!ogl_0356afd2", "" }, - { "nv!ogl_0356afd3", "" }, - { "nv!ogl_0x923dc0", "" }, - { "nv!ogl_0x923dc1", "" }, - { "nv!ogl_0x923dc2", "" }, - { "nv!ogl_0x923dc3", "" }, - { "nv!ogl_0x923dc4", "" }, - { "nv!ogl_0x923dd3", "" }, - { "nv!ogl_0x9abdc5", "" }, - { "nv!ogl_0x9abdc6", "" }, - { "nv!ogl_0xbd10fb", "" }, - { "nv!ogl_0xce2348", "" }, - { "nv!ogl_10261989", "" }, - { "nv!ogl_1042d483", "" }, - { "nv!ogl_10572898", "" }, - { "nv!ogl_115631", "" }, - { "nv!ogl_12950094", "" }, - { "nv!ogl_1314f311", "" }, - { "nv!ogl_1314f312", "" }, - { "nv!ogl_13279512", "" }, - { "nv!ogl_13813496", "" }, - { "nv!ogl_14507179", "" }, - { "nv!ogl_15694569", "" }, - { "nv!ogl_16936964", "" }, - { "nv!ogl_17aa230c", "" }, - { "nv!ogl_182054", "" }, - { "nv!ogl_18273275", "" }, - { "nv!ogl_18273276", "" }, - { "nv!ogl_1854d03b", "" }, - { "nv!ogl_18add00d", "" }, - { "nv!ogl_19156670", "" }, - { "nv!ogl_19286545", "" }, - { "nv!ogl_1a298e9f", "" }, - { "nv!ogl_1acf43fe", "" }, - { "nv!ogl_1bda43fe", "" }, - { "nv!ogl_1c3b92", "" }, - { "nv!ogl_21509920", "" }, - { "nv!ogl_215323457", "" }, - { "nv!ogl_2165ad", "" }, - { "nv!ogl_2165ae", "" }, - { "nv!ogl_21be9c", "" }, - { "nv!ogl_233264316", "" }, - { "nv!ogl_234557580", "" }, - { "nv!ogl_23cd0e", "" }, - { "nv!ogl_24189123", "" }, - { "nv!ogl_2443266", "" }, - { "nv!ogl_25025519", "" }, - { "nv!ogl_255e39", "" }, - { "nv!ogl_2583364", "" }, - { "nv!ogl_2888c1", "" }, - { "nv!ogl_28ca3e", "" }, - { "nv!ogl_29871243", "" }, - { "nv!ogl_2a1f64", "" }, - { "nv!ogl_2dc432", "" }, - { "nv!ogl_2de437", "" }, - { "nv!ogl_2f3bb89c", "" }, - { "nv!ogl_2fd652", "" }, - { "nv!ogl_3001ac", "" }, - { "nv!ogl_31298772", "" }, - { "nv!ogl_313233", "" }, - { "nv!ogl_31f7d603", "" }, - { "nv!ogl_320ce4", "" }, - { "nv!ogl_32153248", "" }, - { "nv!ogl_32153249", "" }, - { "nv!ogl_335bca", "" }, - { "nv!ogl_342abb", "" }, - { "nv!ogl_34dfe6", "" }, - { "nv!ogl_34dfe7", "" }, - { "nv!ogl_34dfe8", "" }, - { "nv!ogl_34dfe9", "" }, - { "nv!ogl_35201578", "" }, - { "nv!ogl_359278", "" }, - { "nv!ogl_37f53a", "" }, - { "nv!ogl_38144972", "" }, - { "nv!ogl_38542646", "" }, - { "nv!ogl_3b74c9", "" }, - { "nv!ogl_3c136f", "" }, - { "nv!ogl_3cf72823", "" }, - { "nv!ogl_3d7af029", "" }, - { "nv!ogl_3ff34782", "" }, - { "nv!ogl_4129618", "" }, - { "nv!ogl_4189fac3", "" }, - { "nv!ogl_420bd4", "" }, - { "nv!ogl_42a699", "" }, - { "nv!ogl_441369", "" }, - { "nv!ogl_4458713e", "" }, - { "nv!ogl_4554b6", "" }, - { "nv!ogl_457425", "" }, - { "nv!ogl_4603b207", "" }, - { "nv!ogl_46574957", "" }, - { "nv!ogl_46574958", "" }, - { "nv!ogl_46813529", "" }, - { "nv!ogl_46f1e13d", "" }, - { "nv!ogl_47534c43", "" }, - { "nv!ogl_48550336", "" }, - { "nv!ogl_48576893", "" }, - { "nv!ogl_48576894", "" }, - { "nv!ogl_4889ac02", "" }, - { "nv!ogl_49005740", "" }, - { "nv!ogl_49867584", "" }, - { "nv!ogl_49960973", "" }, - { "nv!ogl_4a5341", "" }, - { "nv!ogl_4f4e48", "" }, - { "nv!ogl_4f8a0a", "" }, - { "nv!ogl_50299698", "" }, - { "nv!ogl_50299699", "" }, - { "nv!ogl_50361291", "" }, - { "nv!ogl_5242ae", "" }, - { "nv!ogl_53d30c", "" }, - { "nv!ogl_56347a", "" }, - { "nv!ogl_563a95f1", "" }, - { "nv!ogl_573823", "" }, - { "nv!ogl_58027529", "" }, - { "nv!ogl_5d2d63", "" }, - { "nv!ogl_5f7e3b", "" }, - { "nv!ogl_60461793", "" }, - { "nv!ogl_60d355", "" }, - { "nv!ogl_616627aa", "" }, - { "nv!ogl_62317182", "" }, - { "nv!ogl_6253fa2e", "" }, - { "nv!ogl_64100768", "" }, - { "nv!ogl_64100769", "" }, - { "nv!ogl_64100770", "" }, - { "nv!ogl_647395", "" }, - { "nv!ogl_66543234", "" }, - { "nv!ogl_67674763", "" }, - { "nv!ogl_67739784", "" }, - { "nv!ogl_68fb9c", "" }, - { "nv!ogl_69801276", "" }, - { "nv!ogl_6af9fa2f", "" }, - { "nv!ogl_6af9fa3f", "" }, - { "nv!ogl_6af9fa4f", "" }, - { "nv!ogl_6bd8c7", "" }, - { "nv!ogl_6c7691", "" }, - { "nv!ogl_6d4296ce", "" }, - { "nv!ogl_6dd7e7", "" }, - { "nv!ogl_6dd7e8", "" }, - { "nv!ogl_6fe11ec1", "" }, - { "nv!ogl_716511763", "" }, - { "nv!ogl_72504593", "" }, - { "nv!ogl_73304097", "" }, - { "nv!ogl_73314098", "" }, - { "nv!ogl_74095213", "" }, - { "nv!ogl_74095213a", "" }, - { "nv!ogl_74095213b", "" }, - { "nv!ogl_74095214", "" }, - { "nv!ogl_748f9649", "" }, - { "nv!ogl_75494732", "" }, - { "nv!ogl_78452832", "" }, - { "nv!ogl_784561", "" }, - { "nv!ogl_78e16b9c", "" }, - { "nv!ogl_79251225", "" }, - { "nv!ogl_7c128b", "" }, - { "nv!ogl_7ccd93", "" }, - { "nv!ogl_7df8d1", "" }, - { "nv!ogl_800c2310", "" }, - { "nv!ogl_80546710", "" }, - { "nv!ogl_80772310", "" }, - { "nv!ogl_808ee280", "" }, - { "nv!ogl_81131154", "" }, - { "nv!ogl_81274457", "" }, - { "nv!ogl_8292291f", "" }, - { "nv!ogl_83498426", "" }, - { "nv!ogl_84993794", "" }, - { "nv!ogl_84995585", "" }, - { "nv!ogl_84a0a0", "" }, - { "nv!ogl_852142", "" }, - { "nv!ogl_85612309", "" }, - { "nv!ogl_85612310", "" }, - { "nv!ogl_85612311", "" }, - { "nv!ogl_85612312", "" }, - { "nv!ogl_8623ff27", "" }, - { "nv!ogl_87364952", "" }, - { "nv!ogl_87f6275666", "" }, - { "nv!ogl_886748", "" }, - { "nv!ogl_89894423", "" }, - { "nv!ogl_8ad8a75", "" }, - { "nv!ogl_8ad8ad00", "" }, - { "nv!ogl_8bb815", "" }, - { "nv!ogl_8bb817", "" }, - { "nv!ogl_8bb818", "" }, - { "nv!ogl_8bb819", "" }, - { "nv!ogl_8e640cd1", "" }, - { "nv!ogl_8f34971a", "" }, - { "nv!ogl_8f773984", "" }, - { "nv!ogl_8f7a7d", "" }, - { "nv!ogl_902486209", "" }, - { "nv!ogl_90482571", "" }, - { "nv!ogl_91214835", "" }, - { "nv!ogl_912848290", "" }, - { "nv!ogl_915e56", "" }, - { "nv!ogl_92179063", "" }, - { "nv!ogl_92179064", "" }, - { "nv!ogl_92179065", "" }, - { "nv!ogl_92179066", "" }, - { "nv!ogl_92350358", "" }, - { "nv!ogl_92809063", "" }, - { "nv!ogl_92809064", "" }, - { "nv!ogl_92809065", "" }, - { "nv!ogl_92809066", "" }, - { "nv!ogl_92920143", "" }, - { "nv!ogl_93a89b12", "" }, - { "nv!ogl_93a89c0b", "" }, - { "nv!ogl_94812574", "" }, - { "nv!ogl_95282304", "" }, - { "nv!ogl_95394027", "" }, - { "nv!ogl_959b1f", "" }, - { "nv!ogl_9638af", "" }, - { "nv!ogl_96fd59", "" }, - { "nv!ogl_97f6275666", "" }, - { "nv!ogl_97f6275667", "" }, - { "nv!ogl_97f6275668", "" }, - { "nv!ogl_97f6275669", "" }, - { "nv!ogl_97f627566a", "" }, - { "nv!ogl_97f627566b", "" }, - { "nv!ogl_97f627566d", "" }, - { "nv!ogl_97f627566e", "" }, - { "nv!ogl_97f627566f", "" }, - { "nv!ogl_97f6275670", "" }, - { "nv!ogl_97f6275671", "" }, - { "nv!ogl_97f727566e", "" }, - { "nv!ogl_98480775", "" }, - { "nv!ogl_98480776", "" }, - { "nv!ogl_98480777", "" }, - { "nv!ogl_992431", "" }, - { "nv!ogl_9aa29065", "" }, - { "nv!ogl_9af32c", "" }, - { "nv!ogl_9af32d", "" }, - { "nv!ogl_9af32e", "" }, - { "nv!ogl_9c108b71", "" }, - { "nv!ogl_9f279065", "" }, - { "nv!ogl_a01bc728", "" }, - { "nv!ogl_a13b46c80", "" }, - { "nv!ogl_a22eb0", "" }, - { "nv!ogl_a2fb451e", "" }, - { "nv!ogl_a3456abe", "" }, - { "nv!ogl_a7044887", "" }, - { "nv!ogl_a7149200", "" }, - { "nv!ogl_a766215670", "" }, - { "nv!ogl_aalinegamma", "" }, - { "nv!ogl_aalinetweaks", "" }, - { "nv!ogl_ab34ee01", "" }, - { "nv!ogl_ab34ee02", "" }, - { "nv!ogl_ab34ee03", "" }, - { "nv!ogl_ac0274", "" }, - { "nv!ogl_af73c63e", "" }, - { "nv!ogl_af73c63f", "" }, - { "nv!ogl_af9927", "" }, - { "nv!ogl_afoverride", "" }, - { "nv!ogl_allocdeviceevents", "" }, - { "nv!ogl_applicationkey", "" }, - { "nv!ogl_appreturnonlybasicglsltype", "" }, - { "nv!ogl_app_softimage", "" }, - { "nv!ogl_app_supportbits2", "" }, - { "nv!ogl_assumetextureismipmappedatcreation", "" }, - { "nv!ogl_b1fb0f01", "" }, - { "nv!ogl_b3edd5", "" }, - { "nv!ogl_b40d9e03d", "" }, - { "nv!ogl_b7f6275666", "" }, - { "nv!ogl_b812c1", "" }, - { "nv!ogl_ba14ba1a", "" }, - { "nv!ogl_ba14ba1b", "" }, - { "nv!ogl_bd7559", "" }, - { "nv!ogl_bd755a", "" }, - { "nv!ogl_bd755c", "" }, - { "nv!ogl_bd755d", "" }, - { "nv!ogl_be58bb", "" }, - { "nv!ogl_be92cb", "" }, - { "nv!ogl_beefcba3", "" }, - { "nv!ogl_beefcba4", "" }, - { "nv!ogl_c023777f", "" }, - { "nv!ogl_c09dc8", "" }, - { "nv!ogl_c0d340", "" }, - { "nv!ogl_c2ff374c", "" }, - { "nv!ogl_c5e9d7a3", "" }, - { "nv!ogl_c5e9d7a4", "" }, - { "nv!ogl_c5e9d7b4", "" }, - { "nv!ogl_c618f9", "" }, - { "nv!ogl_ca345840", "" }, - { "nv!ogl_cachedisable", "" }, - { "nv!ogl_channelpriorityoverride", "" }, - { "nv!ogl_cleardatastorevidmem", "" }, - { "nv!ogl_cmdbufmemoryspaceenables", "" }, - { "nv!ogl_cmdbufminwords", "" }, - { "nv!ogl_cmdbufsizewords", "" }, - { "nv!ogl_conformantblitframebufferscissor", "" }, - { "nv!ogl_conformantincompletetextures", "" }, - { "nv!ogl_copybuffermethod", "" }, - { "nv!ogl_cubemapaniso", "" }, - { "nv!ogl_cubemapfiltering", "" }, - { "nv!ogl_d0e9a4d7", "" }, - { "nv!ogl_d13733f12", "" }, - { "nv!ogl_d1b399", "" }, - { "nv!ogl_d2983c32", "" }, - { "nv!ogl_d2983c33", "" }, - { "nv!ogl_d2e71b", "" }, - { "nv!ogl_d377dc", "" }, - { "nv!ogl_d377dd", "" }, - { "nv!ogl_d489f4", "" }, - { "nv!ogl_d4bce1", "" }, - { "nv!ogl_d518cb", "" }, - { "nv!ogl_d518cd", "" }, - { "nv!ogl_d518ce", "" }, - { "nv!ogl_d518d0", "" }, - { "nv!ogl_d518d1", "" }, - { "nv!ogl_d518d2", "" }, - { "nv!ogl_d518d3", "" }, - { "nv!ogl_d518d4", "" }, - { "nv!ogl_d518d5", "" }, - { "nv!ogl_d59eda", "" }, - { "nv!ogl_d83cbd", "" }, - { "nv!ogl_d8e777", "" }, - { "nv!ogl_debug_level", "" }, - { "nv!ogl_debug_mask", "" }, - { "nv!ogl_debug_options", "" }, - { "nv!ogl_devshmpageableallocations", "" }, - { "nv!ogl_df1f9812", "" }, - { "nv!ogl_df783c", "" }, - { "nv!ogl_diagenable", "" }, - { "nv!ogl_disallowcemask", "" }, - { "nv!ogl_disallowz16", "" }, - { "nv!ogl_dlmemoryspaceenables", "" }, - { "nv!ogl_e0bfec", "" }, - { "nv!ogl_e433456d", "" }, - { "nv!ogl_e435563f", "" }, - { "nv!ogl_e4cd9c", "" }, - { "nv!ogl_e5c972", "" }, - { "nv!ogl_e639ef", "" }, - { "nv!ogl_e802af", "" }, - { "nv!ogl_eae964", "" }, - { "nv!ogl_earlytexturehwallocation", "" }, - { "nv!ogl_eb92a3", "" }, - { "nv!ogl_ebca56", "" }, - { "nv!ogl_expert_detail_level", "" }, - { "nv!ogl_expert_output_mask", "" }, - { "nv!ogl_expert_report_mask", "" }, - { "nv!ogl_extensionstringnvarch", "" }, - { "nv!ogl_extensionstringversion", "" }, - { "nv!ogl_f00f1938", "" }, - { "nv!ogl_f10736", "" }, - { "nv!ogl_f1846870", "" }, - { "nv!ogl_f33bc370", "" }, - { "nv!ogl_f392a874", "" }, - { "nv!ogl_f49ae8", "" }, - { "nv!ogl_fa345cce", "" }, - { "nv!ogl_fa35cc4", "" }, - { "nv!ogl_faa14a", "" }, - { "nv!ogl_faf8a723", "" }, - { "nv!ogl_fastgs", "" }, - { "nv!ogl_fbf4ac45", "" }, - { "nv!ogl_fbo_blit_ignore_srgb", "" }, - { "nv!ogl_fc64c7", "" }, - { "nv!ogl_ff54ec97", "" }, - { "nv!ogl_ff54ec98", "" }, - { "nv!ogl_forceexitprocessdetach", "" }, - { "nv!ogl_forcerequestedesversion", "" }, - { "nv!ogl_glsynctovblank", "" }, - { "nv!ogl_gvitimeoutcontrol", "" }, - { "nv!ogl_hcctrl", "" }, - { "nv!ogl_hwstate_per_ctx", "" }, - { "nv!ogl_machinecachelimit", "" }, - { "nv!ogl_maxframesallowed", "" }, - { "nv!ogl_memmgrcachedalloclimit", "" }, - { "nv!ogl_memmgrcachedalloclimitratio", "" }, - { "nv!ogl_memmgrsysheapalloclimit", "" }, - { "nv!ogl_memmgrsysheapalloclimitratio", "" }, - { "nv!ogl_memmgrvidheapalloclimit", "" }, - { "nv!ogl_mosaic_clip_to_subdev", "" }, - { "nv!ogl_mosaic_clip_to_subdev_h_overlap", "" }, - { "nv!ogl_mosaic_clip_to_subdev_v_overlap", "" }, - { "nv!ogl_overlaymergeblittimerms", "" }, - { "nv!ogl_perfmon_mode", "" }, - { "nv!ogl_pixbar_mode", "" }, - { "nv!ogl_qualityenhancements", "" }, - { "nv!ogl_r27s18q28", "" }, - { "nv!ogl_r2d7c1d8", "" }, - { "nv!ogl_renderer", "" }, - { "nv!ogl_renderqualityflags", "" }, - { "nv!ogl_s3tcquality", "" }, - { "nv!ogl_shaderatomics", "" }, - { "nv!ogl_shadercacheinitsize", "" }, - { "nv!ogl_shader_disk_cache_path", "" }, - { "nv!ogl_shader_disk_cache_read_only", "" }, - { "nv!ogl_shaderobjects", "" }, - { "nv!ogl_shaderportabilitywarnings", "" }, - { "nv!ogl_shaderwarningsaserrors", "" }, - { "nv!ogl_skiptexturehostcopies", "" }, - { "nv!ogl_sli_dli_control", "" }, - { "nv!ogl_sparsetexture", "" }, - { "nv!ogl_spinlooptimeout", "" }, - { "nv!ogl_sync_to_vblank", "" }, - { "nv!ogl_sysheapreuseratio", "" }, - { "nv!ogl_sysmemtexturepromotion", "" }, - { "nv!ogl_targetflushcount", "" }, - { "nv!ogl_tearingfreeswappresent", "" }, - { "nv!ogl_texclampbehavior", "" }, - { "nv!ogl_texlodbias", "" }, - { "nv!ogl_texmemoryspaceenables", "" }, - { "nv!ogl_textureprecache", "" }, - { "nv!ogl_threadcontrol", "" }, - { "nv!ogl_threadcontrol2", "" }, - { "nv!ogl_usegvievents", "" }, - { "nv!ogl_vbomemoryspaceenables", "" }, - { "nv!ogl_vertexlimit", "" }, - { "nv!ogl_vidheapreuseratio", "" }, - { "nv!ogl_vpipe", "" }, - { "nv!ogl_vpipeformatbloatlimit", "" }, - { "nv!ogl_wglmessageboxonabort", "" }, - { "nv!ogl_writeinfolog", "" }, - { "nv!ogl_writeprogramobjectassembly", "" }, - { "nv!ogl_writeprogramobjectsource", "" }, - { "nv!ogl_xnvadapterpresent", "" }, - { "nv!ogl_yield", "" }, - { "nv!ogl_yieldfunction", "" }, - { "nv!ogl_yieldfunctionfast", "" }, - { "nv!ogl_yieldfunctionslow", "" }, - { "nv!ogl_yieldfunctionwaitfordcqueue", "" }, - { "nv!ogl_yieldfunctionwaitforframe", "" }, - { "nv!ogl_yieldfunctionwaitforgpu", "" }, - { "nv!ogl_zbctableaddhysteresis", "" }, - { "nv!overlaymergeblittimerms", "" }, - { "nv!perfmon_mode", "" }, - { "nv!persist.sys.display.resolution", "" }, - { "nv!persist.tegra.composite.fallb", "" }, - { "nv!persist.tegra.composite.policy", "" }, - { "nv!persist.tegra.composite.range", "" }, - { "nv!persist.tegra.compositor", "" }, - { "nv!persist.tegra.compositor.virt", "" }, - { "nv!persist.tegra.compression", "" }, - { "nv!persist.tegra.cursor.enable", "" }, - { "nv!persist.tegra.didim.enable", "" }, - { "nv!persist.tegra.didim.normal", "" }, - { "nv!persist.tegra.didim.video", "" }, - { "nv!persist.tegra.disp.heads", "" }, - { "nv!persist.tegra.gamma_correction", "" }, - { "nv!persist.tegra.gpu_mapping_cache", "" }, - { "nv!persist.tegra.grlayout", "" }, - { "nv!persist.tegra.hdmi.2020.10", "" }, - { "nv!persist.tegra.hdmi.2020.fake", "" }, - { "nv!persist.tegra.hdmi.2020.force", "" }, - { "nv!persist.tegra.hdmi.autorotate", "" }, - { "nv!persist.tegra.hdmi.hdr.fake", "" }, - { "nv!persist.tegra.hdmi.ignore_ratio", "" }, - { "nv!persist.tegra.hdmi.limit.clock", "" }, - { "nv!persist.tegra.hdmi.only_16_9", "" }, - { "nv!persist.tegra.hdmi.range", "" }, - { "nv!persist.tegra.hdmi.resolution", "" }, - { "nv!persist.tegra.hdmi.underscan", "" }, - { "nv!persist.tegra.hdmi.yuv.422", "" }, - { "nv!persist.tegra.hdmi.yuv.444", "" }, - { "nv!persist.tegra.hdmi.yuv.enable", "" }, - { "nv!persist.tegra.hdmi.yuv.force", "" }, - { "nv!persist.tegra.hwc.nvdc", "" }, - { "nv!persist.tegra.idle.minimum_fps", "" }, - { "nv!persist.tegra.panel.rotation", "" }, - { "nv!persist.tegra.scan_props", "" }, - { "nv!persist.tegra.stb.mode", "" }, - { "nv!persist.tegra.zbc_override", "" }, - { "nv!pixbar_mode", "" }, - { "nv!qualityenhancements", "" }, - { "nv!r27s18q28", "" }, - { "nv!r2d7c1d8", "" }, - { "nv!renderer", "" }, - { "nv!renderqualityflags", "" }, - { "nv!rmos_debug_mask", "" }, - { "nv!rmos_set_production_mode", "" }, - { "nv!s3tcquality", "" }, - { "nv!shaderatomics", "" }, - { "nv!shadercacheinitsize", "" }, - { "nv!shader_disk_cache_path", "" }, - { "nv!shader_disk_cache_read_only", "" }, - { "nv!shaderobjects", "" }, - { "nv!shaderportabilitywarnings", "" }, - { "nv!shaderwarningsaserrors", "" }, - { "nv!skiptexturehostcopies", "" }, - { "nv!sli_dli_control", "" }, - { "nv!sparsetexture", "" }, - { "nv!spinlooptimeout", "" }, - { "nv!sync_to_vblank", "" }, - { "nv!sysheapreuseratio", "" }, - { "nv!sysmemtexturepromotion", "" }, - { "nv!targetflushcount", "" }, - { "nv!tearingfreeswappresent", "" }, - { "nv!tegra.refresh", "" }, - { "nv!texclampbehavior", "" }, - { "nv!texlodbias", "" }, - { "nv!texmemoryspaceenables", "" }, - { "nv!textureprecache", "" }, - { "nv!threadcontrol", "" }, - { "nv!threadcontrol2", "" }, - { "nv!tvmr.avp.logs", "" }, - { "nv!tvmr.buffer.logs", "" }, - { "nv!tvmr.dec.prof", "" }, - { "nv!tvmr.deint.logs", "" }, - { "nv!tvmr.dfs.logs", "" }, - { "nv!tvmr.ffprof.logs", "" }, - { "nv!tvmr.game.stream", "" }, - { "nv!tvmr.general.logs", "" }, - { "nv!tvmr.input.dump", "" }, - { "nv!tvmr.seeking.logs", "" }, - { "nv!tvmr.ts_pulldown", "" }, - { "nv!usegvievents", "" }, - { "nv!vbomemoryspaceenables", "" }, - { "nv!vcc_debug_ip", "" }, - { "nv!vcc_verbose_level", "" }, - { "nv!vertexlimit", "" }, - { "nv!viccomposer.filter", "" }, - { "nv!videostats-enable", "" }, - { "nv!vidheapreuseratio", "" }, - { "nv!vpipe", "" }, - { "nv!vpipeformatbloatlimit", "" }, - { "nv!wglmessageboxonabort", "" }, - { "nv!writeinfolog", "" }, - { "nv!writeprogramobjectassembly", "" }, - { "nv!writeprogramobjectsource", "" }, - { "nv!xnvadapterpresent", "" }, - { "nv!yield", "" }, - { "nv!yieldfunction", "" }, - { "nv!yieldfunctionfast", "" }, - { "nv!yieldfunctionslow", "" }, - { "nv!yieldfunctionwaitfordcqueue", "" }, - { "nv!yieldfunctionwaitforframe", "" }, - { "nv!yieldfunctionwaitforgpu", "" }, - { "nv!zbctableaddhysteresis", "" }, + { "nv!nvn_through_opengl", string.Empty }, + { "nv!nv_pllcx_always_on", string.Empty }, + { "nv!nv_pllcx_safe_div", string.Empty }, + { "nv!nvrm_gpu_channel_interleave", string.Empty }, + { "nv!nvrm_gpu_channel_priority", string.Empty }, + { "nv!nvrm_gpu_channel_timeslice", string.Empty }, + { "nv!nvrm_gpu_default_device_index", string.Empty }, + { "nv!nvrm_gpu_dummy", string.Empty }, + { "nv!nvrm_gpu_help", string.Empty }, + { "nv!nvrm_gpu_nvgpu_disable", string.Empty }, + { "nv!nvrm_gpu_nvgpu_do_nfa_partial_map", string.Empty }, + { "nv!nvrm_gpu_nvgpu_ecc_overrides", string.Empty }, + { "nv!nvrm_gpu_nvgpu_no_as_get_va_regions", string.Empty }, + { "nv!nvrm_gpu_nvgpu_no_channel_abort", string.Empty }, + { "nv!nvrm_gpu_nvgpu_no_cyclestats", string.Empty }, + { "nv!nvrm_gpu_nvgpu_no_fixed", string.Empty }, + { "nv!nvrm_gpu_nvgpu_no_gpu_characteristics", string.Empty }, + { "nv!nvrm_gpu_nvgpu_no_ioctl_mutex", string.Empty }, + { "nv!nvrm_gpu_nvgpu_no_map_buffer_ex", string.Empty }, + { "nv!nvrm_gpu_nvgpu_no_robustness", string.Empty }, + { "nv!nvrm_gpu_nvgpu_no_sparse", string.Empty }, + { "nv!nvrm_gpu_nvgpu_no_syncpoints", string.Empty }, + { "nv!nvrm_gpu_nvgpu_no_tsg", string.Empty }, + { "nv!nvrm_gpu_nvgpu_no_zbc", string.Empty }, + { "nv!nvrm_gpu_nvgpu_no_zcull", string.Empty }, + { "nv!nvrm_gpu_nvgpu_wrap_channels_in_tsgs", string.Empty }, + { "nv!nvrm_gpu_prevent_use", string.Empty }, + { "nv!nvrm_gpu_trace", string.Empty }, + { "nv!nvsched_debug_mask", string.Empty }, + { "nv!nvsched_force_enable", string.Empty }, + { "nv!nvsched_force_log", string.Empty }, + { "nv!nv_usb_plls_hw_ctrl", string.Empty }, + { "nv!nv_winsys", string.Empty }, + { "nv!nvwsi_dump", string.Empty }, + { "nv!nvwsi_fill", string.Empty }, + { "nv!ogl_", string.Empty }, + { "nv!ogl_0356afd0", string.Empty }, + { "nv!ogl_0356afd1", string.Empty }, + { "nv!ogl_0356afd2", string.Empty }, + { "nv!ogl_0356afd3", string.Empty }, + { "nv!ogl_0x923dc0", string.Empty }, + { "nv!ogl_0x923dc1", string.Empty }, + { "nv!ogl_0x923dc2", string.Empty }, + { "nv!ogl_0x923dc3", string.Empty }, + { "nv!ogl_0x923dc4", string.Empty }, + { "nv!ogl_0x923dd3", string.Empty }, + { "nv!ogl_0x9abdc5", string.Empty }, + { "nv!ogl_0x9abdc6", string.Empty }, + { "nv!ogl_0xbd10fb", string.Empty }, + { "nv!ogl_0xce2348", string.Empty }, + { "nv!ogl_10261989", string.Empty }, + { "nv!ogl_1042d483", string.Empty }, + { "nv!ogl_10572898", string.Empty }, + { "nv!ogl_115631", string.Empty }, + { "nv!ogl_12950094", string.Empty }, + { "nv!ogl_1314f311", string.Empty }, + { "nv!ogl_1314f312", string.Empty }, + { "nv!ogl_13279512", string.Empty }, + { "nv!ogl_13813496", string.Empty }, + { "nv!ogl_14507179", string.Empty }, + { "nv!ogl_15694569", string.Empty }, + { "nv!ogl_16936964", string.Empty }, + { "nv!ogl_17aa230c", string.Empty }, + { "nv!ogl_182054", string.Empty }, + { "nv!ogl_18273275", string.Empty }, + { "nv!ogl_18273276", string.Empty }, + { "nv!ogl_1854d03b", string.Empty }, + { "nv!ogl_18add00d", string.Empty }, + { "nv!ogl_19156670", string.Empty }, + { "nv!ogl_19286545", string.Empty }, + { "nv!ogl_1a298e9f", string.Empty }, + { "nv!ogl_1acf43fe", string.Empty }, + { "nv!ogl_1bda43fe", string.Empty }, + { "nv!ogl_1c3b92", string.Empty }, + { "nv!ogl_21509920", string.Empty }, + { "nv!ogl_215323457", string.Empty }, + { "nv!ogl_2165ad", string.Empty }, + { "nv!ogl_2165ae", string.Empty }, + { "nv!ogl_21be9c", string.Empty }, + { "nv!ogl_233264316", string.Empty }, + { "nv!ogl_234557580", string.Empty }, + { "nv!ogl_23cd0e", string.Empty }, + { "nv!ogl_24189123", string.Empty }, + { "nv!ogl_2443266", string.Empty }, + { "nv!ogl_25025519", string.Empty }, + { "nv!ogl_255e39", string.Empty }, + { "nv!ogl_2583364", string.Empty }, + { "nv!ogl_2888c1", string.Empty }, + { "nv!ogl_28ca3e", string.Empty }, + { "nv!ogl_29871243", string.Empty }, + { "nv!ogl_2a1f64", string.Empty }, + { "nv!ogl_2dc432", string.Empty }, + { "nv!ogl_2de437", string.Empty }, + { "nv!ogl_2f3bb89c", string.Empty }, + { "nv!ogl_2fd652", string.Empty }, + { "nv!ogl_3001ac", string.Empty }, + { "nv!ogl_31298772", string.Empty }, + { "nv!ogl_313233", string.Empty }, + { "nv!ogl_31f7d603", string.Empty }, + { "nv!ogl_320ce4", string.Empty }, + { "nv!ogl_32153248", string.Empty }, + { "nv!ogl_32153249", string.Empty }, + { "nv!ogl_335bca", string.Empty }, + { "nv!ogl_342abb", string.Empty }, + { "nv!ogl_34dfe6", string.Empty }, + { "nv!ogl_34dfe7", string.Empty }, + { "nv!ogl_34dfe8", string.Empty }, + { "nv!ogl_34dfe9", string.Empty }, + { "nv!ogl_35201578", string.Empty }, + { "nv!ogl_359278", string.Empty }, + { "nv!ogl_37f53a", string.Empty }, + { "nv!ogl_38144972", string.Empty }, + { "nv!ogl_38542646", string.Empty }, + { "nv!ogl_3b74c9", string.Empty }, + { "nv!ogl_3c136f", string.Empty }, + { "nv!ogl_3cf72823", string.Empty }, + { "nv!ogl_3d7af029", string.Empty }, + { "nv!ogl_3ff34782", string.Empty }, + { "nv!ogl_4129618", string.Empty }, + { "nv!ogl_4189fac3", string.Empty }, + { "nv!ogl_420bd4", string.Empty }, + { "nv!ogl_42a699", string.Empty }, + { "nv!ogl_441369", string.Empty }, + { "nv!ogl_4458713e", string.Empty }, + { "nv!ogl_4554b6", string.Empty }, + { "nv!ogl_457425", string.Empty }, + { "nv!ogl_4603b207", string.Empty }, + { "nv!ogl_46574957", string.Empty }, + { "nv!ogl_46574958", string.Empty }, + { "nv!ogl_46813529", string.Empty }, + { "nv!ogl_46f1e13d", string.Empty }, + { "nv!ogl_47534c43", string.Empty }, + { "nv!ogl_48550336", string.Empty }, + { "nv!ogl_48576893", string.Empty }, + { "nv!ogl_48576894", string.Empty }, + { "nv!ogl_4889ac02", string.Empty }, + { "nv!ogl_49005740", string.Empty }, + { "nv!ogl_49867584", string.Empty }, + { "nv!ogl_49960973", string.Empty }, + { "nv!ogl_4a5341", string.Empty }, + { "nv!ogl_4f4e48", string.Empty }, + { "nv!ogl_4f8a0a", string.Empty }, + { "nv!ogl_50299698", string.Empty }, + { "nv!ogl_50299699", string.Empty }, + { "nv!ogl_50361291", string.Empty }, + { "nv!ogl_5242ae", string.Empty }, + { "nv!ogl_53d30c", string.Empty }, + { "nv!ogl_56347a", string.Empty }, + { "nv!ogl_563a95f1", string.Empty }, + { "nv!ogl_573823", string.Empty }, + { "nv!ogl_58027529", string.Empty }, + { "nv!ogl_5d2d63", string.Empty }, + { "nv!ogl_5f7e3b", string.Empty }, + { "nv!ogl_60461793", string.Empty }, + { "nv!ogl_60d355", string.Empty }, + { "nv!ogl_616627aa", string.Empty }, + { "nv!ogl_62317182", string.Empty }, + { "nv!ogl_6253fa2e", string.Empty }, + { "nv!ogl_64100768", string.Empty }, + { "nv!ogl_64100769", string.Empty }, + { "nv!ogl_64100770", string.Empty }, + { "nv!ogl_647395", string.Empty }, + { "nv!ogl_66543234", string.Empty }, + { "nv!ogl_67674763", string.Empty }, + { "nv!ogl_67739784", string.Empty }, + { "nv!ogl_68fb9c", string.Empty }, + { "nv!ogl_69801276", string.Empty }, + { "nv!ogl_6af9fa2f", string.Empty }, + { "nv!ogl_6af9fa3f", string.Empty }, + { "nv!ogl_6af9fa4f", string.Empty }, + { "nv!ogl_6bd8c7", string.Empty }, + { "nv!ogl_6c7691", string.Empty }, + { "nv!ogl_6d4296ce", string.Empty }, + { "nv!ogl_6dd7e7", string.Empty }, + { "nv!ogl_6dd7e8", string.Empty }, + { "nv!ogl_6fe11ec1", string.Empty }, + { "nv!ogl_716511763", string.Empty }, + { "nv!ogl_72504593", string.Empty }, + { "nv!ogl_73304097", string.Empty }, + { "nv!ogl_73314098", string.Empty }, + { "nv!ogl_74095213", string.Empty }, + { "nv!ogl_74095213a", string.Empty }, + { "nv!ogl_74095213b", string.Empty }, + { "nv!ogl_74095214", string.Empty }, + { "nv!ogl_748f9649", string.Empty }, + { "nv!ogl_75494732", string.Empty }, + { "nv!ogl_78452832", string.Empty }, + { "nv!ogl_784561", string.Empty }, + { "nv!ogl_78e16b9c", string.Empty }, + { "nv!ogl_79251225", string.Empty }, + { "nv!ogl_7c128b", string.Empty }, + { "nv!ogl_7ccd93", string.Empty }, + { "nv!ogl_7df8d1", string.Empty }, + { "nv!ogl_800c2310", string.Empty }, + { "nv!ogl_80546710", string.Empty }, + { "nv!ogl_80772310", string.Empty }, + { "nv!ogl_808ee280", string.Empty }, + { "nv!ogl_81131154", string.Empty }, + { "nv!ogl_81274457", string.Empty }, + { "nv!ogl_8292291f", string.Empty }, + { "nv!ogl_83498426", string.Empty }, + { "nv!ogl_84993794", string.Empty }, + { "nv!ogl_84995585", string.Empty }, + { "nv!ogl_84a0a0", string.Empty }, + { "nv!ogl_852142", string.Empty }, + { "nv!ogl_85612309", string.Empty }, + { "nv!ogl_85612310", string.Empty }, + { "nv!ogl_85612311", string.Empty }, + { "nv!ogl_85612312", string.Empty }, + { "nv!ogl_8623ff27", string.Empty }, + { "nv!ogl_87364952", string.Empty }, + { "nv!ogl_87f6275666", string.Empty }, + { "nv!ogl_886748", string.Empty }, + { "nv!ogl_89894423", string.Empty }, + { "nv!ogl_8ad8a75", string.Empty }, + { "nv!ogl_8ad8ad00", string.Empty }, + { "nv!ogl_8bb815", string.Empty }, + { "nv!ogl_8bb817", string.Empty }, + { "nv!ogl_8bb818", string.Empty }, + { "nv!ogl_8bb819", string.Empty }, + { "nv!ogl_8e640cd1", string.Empty }, + { "nv!ogl_8f34971a", string.Empty }, + { "nv!ogl_8f773984", string.Empty }, + { "nv!ogl_8f7a7d", string.Empty }, + { "nv!ogl_902486209", string.Empty }, + { "nv!ogl_90482571", string.Empty }, + { "nv!ogl_91214835", string.Empty }, + { "nv!ogl_912848290", string.Empty }, + { "nv!ogl_915e56", string.Empty }, + { "nv!ogl_92179063", string.Empty }, + { "nv!ogl_92179064", string.Empty }, + { "nv!ogl_92179065", string.Empty }, + { "nv!ogl_92179066", string.Empty }, + { "nv!ogl_92350358", string.Empty }, + { "nv!ogl_92809063", string.Empty }, + { "nv!ogl_92809064", string.Empty }, + { "nv!ogl_92809065", string.Empty }, + { "nv!ogl_92809066", string.Empty }, + { "nv!ogl_92920143", string.Empty }, + { "nv!ogl_93a89b12", string.Empty }, + { "nv!ogl_93a89c0b", string.Empty }, + { "nv!ogl_94812574", string.Empty }, + { "nv!ogl_95282304", string.Empty }, + { "nv!ogl_95394027", string.Empty }, + { "nv!ogl_959b1f", string.Empty }, + { "nv!ogl_9638af", string.Empty }, + { "nv!ogl_96fd59", string.Empty }, + { "nv!ogl_97f6275666", string.Empty }, + { "nv!ogl_97f6275667", string.Empty }, + { "nv!ogl_97f6275668", string.Empty }, + { "nv!ogl_97f6275669", string.Empty }, + { "nv!ogl_97f627566a", string.Empty }, + { "nv!ogl_97f627566b", string.Empty }, + { "nv!ogl_97f627566d", string.Empty }, + { "nv!ogl_97f627566e", string.Empty }, + { "nv!ogl_97f627566f", string.Empty }, + { "nv!ogl_97f6275670", string.Empty }, + { "nv!ogl_97f6275671", string.Empty }, + { "nv!ogl_97f727566e", string.Empty }, + { "nv!ogl_98480775", string.Empty }, + { "nv!ogl_98480776", string.Empty }, + { "nv!ogl_98480777", string.Empty }, + { "nv!ogl_992431", string.Empty }, + { "nv!ogl_9aa29065", string.Empty }, + { "nv!ogl_9af32c", string.Empty }, + { "nv!ogl_9af32d", string.Empty }, + { "nv!ogl_9af32e", string.Empty }, + { "nv!ogl_9c108b71", string.Empty }, + { "nv!ogl_9f279065", string.Empty }, + { "nv!ogl_a01bc728", string.Empty }, + { "nv!ogl_a13b46c80", string.Empty }, + { "nv!ogl_a22eb0", string.Empty }, + { "nv!ogl_a2fb451e", string.Empty }, + { "nv!ogl_a3456abe", string.Empty }, + { "nv!ogl_a7044887", string.Empty }, + { "nv!ogl_a7149200", string.Empty }, + { "nv!ogl_a766215670", string.Empty }, + { "nv!ogl_aalinegamma", string.Empty }, + { "nv!ogl_aalinetweaks", string.Empty }, + { "nv!ogl_ab34ee01", string.Empty }, + { "nv!ogl_ab34ee02", string.Empty }, + { "nv!ogl_ab34ee03", string.Empty }, + { "nv!ogl_ac0274", string.Empty }, + { "nv!ogl_af73c63e", string.Empty }, + { "nv!ogl_af73c63f", string.Empty }, + { "nv!ogl_af9927", string.Empty }, + { "nv!ogl_afoverride", string.Empty }, + { "nv!ogl_allocdeviceevents", string.Empty }, + { "nv!ogl_applicationkey", string.Empty }, + { "nv!ogl_appreturnonlybasicglsltype", string.Empty }, + { "nv!ogl_app_softimage", string.Empty }, + { "nv!ogl_app_supportbits2", string.Empty }, + { "nv!ogl_assumetextureismipmappedatcreation", string.Empty }, + { "nv!ogl_b1fb0f01", string.Empty }, + { "nv!ogl_b3edd5", string.Empty }, + { "nv!ogl_b40d9e03d", string.Empty }, + { "nv!ogl_b7f6275666", string.Empty }, + { "nv!ogl_b812c1", string.Empty }, + { "nv!ogl_ba14ba1a", string.Empty }, + { "nv!ogl_ba14ba1b", string.Empty }, + { "nv!ogl_bd7559", string.Empty }, + { "nv!ogl_bd755a", string.Empty }, + { "nv!ogl_bd755c", string.Empty }, + { "nv!ogl_bd755d", string.Empty }, + { "nv!ogl_be58bb", string.Empty }, + { "nv!ogl_be92cb", string.Empty }, + { "nv!ogl_beefcba3", string.Empty }, + { "nv!ogl_beefcba4", string.Empty }, + { "nv!ogl_c023777f", string.Empty }, + { "nv!ogl_c09dc8", string.Empty }, + { "nv!ogl_c0d340", string.Empty }, + { "nv!ogl_c2ff374c", string.Empty }, + { "nv!ogl_c5e9d7a3", string.Empty }, + { "nv!ogl_c5e9d7a4", string.Empty }, + { "nv!ogl_c5e9d7b4", string.Empty }, + { "nv!ogl_c618f9", string.Empty }, + { "nv!ogl_ca345840", string.Empty }, + { "nv!ogl_cachedisable", string.Empty }, + { "nv!ogl_channelpriorityoverride", string.Empty }, + { "nv!ogl_cleardatastorevidmem", string.Empty }, + { "nv!ogl_cmdbufmemoryspaceenables", string.Empty }, + { "nv!ogl_cmdbufminwords", string.Empty }, + { "nv!ogl_cmdbufsizewords", string.Empty }, + { "nv!ogl_conformantblitframebufferscissor", string.Empty }, + { "nv!ogl_conformantincompletetextures", string.Empty }, + { "nv!ogl_copybuffermethod", string.Empty }, + { "nv!ogl_cubemapaniso", string.Empty }, + { "nv!ogl_cubemapfiltering", string.Empty }, + { "nv!ogl_d0e9a4d7", string.Empty }, + { "nv!ogl_d13733f12", string.Empty }, + { "nv!ogl_d1b399", string.Empty }, + { "nv!ogl_d2983c32", string.Empty }, + { "nv!ogl_d2983c33", string.Empty }, + { "nv!ogl_d2e71b", string.Empty }, + { "nv!ogl_d377dc", string.Empty }, + { "nv!ogl_d377dd", string.Empty }, + { "nv!ogl_d489f4", string.Empty }, + { "nv!ogl_d4bce1", string.Empty }, + { "nv!ogl_d518cb", string.Empty }, + { "nv!ogl_d518cd", string.Empty }, + { "nv!ogl_d518ce", string.Empty }, + { "nv!ogl_d518d0", string.Empty }, + { "nv!ogl_d518d1", string.Empty }, + { "nv!ogl_d518d2", string.Empty }, + { "nv!ogl_d518d3", string.Empty }, + { "nv!ogl_d518d4", string.Empty }, + { "nv!ogl_d518d5", string.Empty }, + { "nv!ogl_d59eda", string.Empty }, + { "nv!ogl_d83cbd", string.Empty }, + { "nv!ogl_d8e777", string.Empty }, + { "nv!ogl_debug_level", string.Empty }, + { "nv!ogl_debug_mask", string.Empty }, + { "nv!ogl_debug_options", string.Empty }, + { "nv!ogl_devshmpageableallocations", string.Empty }, + { "nv!ogl_df1f9812", string.Empty }, + { "nv!ogl_df783c", string.Empty }, + { "nv!ogl_diagenable", string.Empty }, + { "nv!ogl_disallowcemask", string.Empty }, + { "nv!ogl_disallowz16", string.Empty }, + { "nv!ogl_dlmemoryspaceenables", string.Empty }, + { "nv!ogl_e0bfec", string.Empty }, + { "nv!ogl_e433456d", string.Empty }, + { "nv!ogl_e435563f", string.Empty }, + { "nv!ogl_e4cd9c", string.Empty }, + { "nv!ogl_e5c972", string.Empty }, + { "nv!ogl_e639ef", string.Empty }, + { "nv!ogl_e802af", string.Empty }, + { "nv!ogl_eae964", string.Empty }, + { "nv!ogl_earlytexturehwallocation", string.Empty }, + { "nv!ogl_eb92a3", string.Empty }, + { "nv!ogl_ebca56", string.Empty }, + { "nv!ogl_expert_detail_level", string.Empty }, + { "nv!ogl_expert_output_mask", string.Empty }, + { "nv!ogl_expert_report_mask", string.Empty }, + { "nv!ogl_extensionstringnvarch", string.Empty }, + { "nv!ogl_extensionstringversion", string.Empty }, + { "nv!ogl_f00f1938", string.Empty }, + { "nv!ogl_f10736", string.Empty }, + { "nv!ogl_f1846870", string.Empty }, + { "nv!ogl_f33bc370", string.Empty }, + { "nv!ogl_f392a874", string.Empty }, + { "nv!ogl_f49ae8", string.Empty }, + { "nv!ogl_fa345cce", string.Empty }, + { "nv!ogl_fa35cc4", string.Empty }, + { "nv!ogl_faa14a", string.Empty }, + { "nv!ogl_faf8a723", string.Empty }, + { "nv!ogl_fastgs", string.Empty }, + { "nv!ogl_fbf4ac45", string.Empty }, + { "nv!ogl_fbo_blit_ignore_srgb", string.Empty }, + { "nv!ogl_fc64c7", string.Empty }, + { "nv!ogl_ff54ec97", string.Empty }, + { "nv!ogl_ff54ec98", string.Empty }, + { "nv!ogl_forceexitprocessdetach", string.Empty }, + { "nv!ogl_forcerequestedesversion", string.Empty }, + { "nv!ogl_glsynctovblank", string.Empty }, + { "nv!ogl_gvitimeoutcontrol", string.Empty }, + { "nv!ogl_hcctrl", string.Empty }, + { "nv!ogl_hwstate_per_ctx", string.Empty }, + { "nv!ogl_machinecachelimit", string.Empty }, + { "nv!ogl_maxframesallowed", string.Empty }, + { "nv!ogl_memmgrcachedalloclimit", string.Empty }, + { "nv!ogl_memmgrcachedalloclimitratio", string.Empty }, + { "nv!ogl_memmgrsysheapalloclimit", string.Empty }, + { "nv!ogl_memmgrsysheapalloclimitratio", string.Empty }, + { "nv!ogl_memmgrvidheapalloclimit", string.Empty }, + { "nv!ogl_mosaic_clip_to_subdev", string.Empty }, + { "nv!ogl_mosaic_clip_to_subdev_h_overlap", string.Empty }, + { "nv!ogl_mosaic_clip_to_subdev_v_overlap", string.Empty }, + { "nv!ogl_overlaymergeblittimerms", string.Empty }, + { "nv!ogl_perfmon_mode", string.Empty }, + { "nv!ogl_pixbar_mode", string.Empty }, + { "nv!ogl_qualityenhancements", string.Empty }, + { "nv!ogl_r27s18q28", string.Empty }, + { "nv!ogl_r2d7c1d8", string.Empty }, + { "nv!ogl_renderer", string.Empty }, + { "nv!ogl_renderqualityflags", string.Empty }, + { "nv!ogl_s3tcquality", string.Empty }, + { "nv!ogl_shaderatomics", string.Empty }, + { "nv!ogl_shadercacheinitsize", string.Empty }, + { "nv!ogl_shader_disk_cache_path", string.Empty }, + { "nv!ogl_shader_disk_cache_read_only", string.Empty }, + { "nv!ogl_shaderobjects", string.Empty }, + { "nv!ogl_shaderportabilitywarnings", string.Empty }, + { "nv!ogl_shaderwarningsaserrors", string.Empty }, + { "nv!ogl_skiptexturehostcopies", string.Empty }, + { "nv!ogl_sli_dli_control", string.Empty }, + { "nv!ogl_sparsetexture", string.Empty }, + { "nv!ogl_spinlooptimeout", string.Empty }, + { "nv!ogl_sync_to_vblank", string.Empty }, + { "nv!ogl_sysheapreuseratio", string.Empty }, + { "nv!ogl_sysmemtexturepromotion", string.Empty }, + { "nv!ogl_targetflushcount", string.Empty }, + { "nv!ogl_tearingfreeswappresent", string.Empty }, + { "nv!ogl_texclampbehavior", string.Empty }, + { "nv!ogl_texlodbias", string.Empty }, + { "nv!ogl_texmemoryspaceenables", string.Empty }, + { "nv!ogl_textureprecache", string.Empty }, + { "nv!ogl_threadcontrol", string.Empty }, + { "nv!ogl_threadcontrol2", string.Empty }, + { "nv!ogl_usegvievents", string.Empty }, + { "nv!ogl_vbomemoryspaceenables", string.Empty }, + { "nv!ogl_vertexlimit", string.Empty }, + { "nv!ogl_vidheapreuseratio", string.Empty }, + { "nv!ogl_vpipe", string.Empty }, + { "nv!ogl_vpipeformatbloatlimit", string.Empty }, + { "nv!ogl_wglmessageboxonabort", string.Empty }, + { "nv!ogl_writeinfolog", string.Empty }, + { "nv!ogl_writeprogramobjectassembly", string.Empty }, + { "nv!ogl_writeprogramobjectsource", string.Empty }, + { "nv!ogl_xnvadapterpresent", string.Empty }, + { "nv!ogl_yield", string.Empty }, + { "nv!ogl_yieldfunction", string.Empty }, + { "nv!ogl_yieldfunctionfast", string.Empty }, + { "nv!ogl_yieldfunctionslow", string.Empty }, + { "nv!ogl_yieldfunctionwaitfordcqueue", string.Empty }, + { "nv!ogl_yieldfunctionwaitforframe", string.Empty }, + { "nv!ogl_yieldfunctionwaitforgpu", string.Empty }, + { "nv!ogl_zbctableaddhysteresis", string.Empty }, + { "nv!overlaymergeblittimerms", string.Empty }, + { "nv!perfmon_mode", string.Empty }, + { "nv!persist.sys.display.resolution", string.Empty }, + { "nv!persist.tegra.composite.fallb", string.Empty }, + { "nv!persist.tegra.composite.policy", string.Empty }, + { "nv!persist.tegra.composite.range", string.Empty }, + { "nv!persist.tegra.compositor", string.Empty }, + { "nv!persist.tegra.compositor.virt", string.Empty }, + { "nv!persist.tegra.compression", string.Empty }, + { "nv!persist.tegra.cursor.enable", string.Empty }, + { "nv!persist.tegra.didim.enable", string.Empty }, + { "nv!persist.tegra.didim.normal", string.Empty }, + { "nv!persist.tegra.didim.video", string.Empty }, + { "nv!persist.tegra.disp.heads", string.Empty }, + { "nv!persist.tegra.gamma_correction", string.Empty }, + { "nv!persist.tegra.gpu_mapping_cache", string.Empty }, + { "nv!persist.tegra.grlayout", string.Empty }, + { "nv!persist.tegra.hdmi.2020.10", string.Empty }, + { "nv!persist.tegra.hdmi.2020.fake", string.Empty }, + { "nv!persist.tegra.hdmi.2020.force", string.Empty }, + { "nv!persist.tegra.hdmi.autorotate", string.Empty }, + { "nv!persist.tegra.hdmi.hdr.fake", string.Empty }, + { "nv!persist.tegra.hdmi.ignore_ratio", string.Empty }, + { "nv!persist.tegra.hdmi.limit.clock", string.Empty }, + { "nv!persist.tegra.hdmi.only_16_9", string.Empty }, + { "nv!persist.tegra.hdmi.range", string.Empty }, + { "nv!persist.tegra.hdmi.resolution", string.Empty }, + { "nv!persist.tegra.hdmi.underscan", string.Empty }, + { "nv!persist.tegra.hdmi.yuv.422", string.Empty }, + { "nv!persist.tegra.hdmi.yuv.444", string.Empty }, + { "nv!persist.tegra.hdmi.yuv.enable", string.Empty }, + { "nv!persist.tegra.hdmi.yuv.force", string.Empty }, + { "nv!persist.tegra.hwc.nvdc", string.Empty }, + { "nv!persist.tegra.idle.minimum_fps", string.Empty }, + { "nv!persist.tegra.panel.rotation", string.Empty }, + { "nv!persist.tegra.scan_props", string.Empty }, + { "nv!persist.tegra.stb.mode", string.Empty }, + { "nv!persist.tegra.zbc_override", string.Empty }, + { "nv!pixbar_mode", string.Empty }, + { "nv!qualityenhancements", string.Empty }, + { "nv!r27s18q28", string.Empty }, + { "nv!r2d7c1d8", string.Empty }, + { "nv!renderer", string.Empty }, + { "nv!renderqualityflags", string.Empty }, + { "nv!rmos_debug_mask", string.Empty }, + { "nv!rmos_set_production_mode", string.Empty }, + { "nv!s3tcquality", string.Empty }, + { "nv!shaderatomics", string.Empty }, + { "nv!shadercacheinitsize", string.Empty }, + { "nv!shader_disk_cache_path", string.Empty }, + { "nv!shader_disk_cache_read_only", string.Empty }, + { "nv!shaderobjects", string.Empty }, + { "nv!shaderportabilitywarnings", string.Empty }, + { "nv!shaderwarningsaserrors", string.Empty }, + { "nv!skiptexturehostcopies", string.Empty }, + { "nv!sli_dli_control", string.Empty }, + { "nv!sparsetexture", string.Empty }, + { "nv!spinlooptimeout", string.Empty }, + { "nv!sync_to_vblank", string.Empty }, + { "nv!sysheapreuseratio", string.Empty }, + { "nv!sysmemtexturepromotion", string.Empty }, + { "nv!targetflushcount", string.Empty }, + { "nv!tearingfreeswappresent", string.Empty }, + { "nv!tegra.refresh", string.Empty }, + { "nv!texclampbehavior", string.Empty }, + { "nv!texlodbias", string.Empty }, + { "nv!texmemoryspaceenables", string.Empty }, + { "nv!textureprecache", string.Empty }, + { "nv!threadcontrol", string.Empty }, + { "nv!threadcontrol2", string.Empty }, + { "nv!tvmr.avp.logs", string.Empty }, + { "nv!tvmr.buffer.logs", string.Empty }, + { "nv!tvmr.dec.prof", string.Empty }, + { "nv!tvmr.deint.logs", string.Empty }, + { "nv!tvmr.dfs.logs", string.Empty }, + { "nv!tvmr.ffprof.logs", string.Empty }, + { "nv!tvmr.game.stream", string.Empty }, + { "nv!tvmr.general.logs", string.Empty }, + { "nv!tvmr.input.dump", string.Empty }, + { "nv!tvmr.seeking.logs", string.Empty }, + { "nv!tvmr.ts_pulldown", string.Empty }, + { "nv!usegvievents", string.Empty }, + { "nv!vbomemoryspaceenables", string.Empty }, + { "nv!vcc_debug_ip", string.Empty }, + { "nv!vcc_verbose_level", string.Empty }, + { "nv!vertexlimit", string.Empty }, + { "nv!viccomposer.filter", string.Empty }, + { "nv!videostats-enable", string.Empty }, + { "nv!vidheapreuseratio", string.Empty }, + { "nv!vpipe", string.Empty }, + { "nv!vpipeformatbloatlimit", string.Empty }, + { "nv!wglmessageboxonabort", string.Empty }, + { "nv!writeinfolog", string.Empty }, + { "nv!writeprogramobjectassembly", string.Empty }, + { "nv!writeprogramobjectsource", string.Empty }, + { "nv!xnvadapterpresent", string.Empty }, + { "nv!yield", string.Empty }, + { "nv!yieldfunction", string.Empty }, + { "nv!yieldfunctionfast", string.Empty }, + { "nv!yieldfunctionslow", string.Empty }, + { "nv!yieldfunctionwaitfordcqueue", string.Empty }, + { "nv!yieldfunctionwaitforframe", string.Empty }, + { "nv!yieldfunctionwaitforgpu", string.Empty }, + { "nv!zbctableaddhysteresis", string.Empty }, { "pcm!enable", true }, { "pctl!intermittent_task_interval_seconds", 21600 }, { "prepo!devmenu_prepo_page_view", false }, @@ -1611,7 +1611,7 @@ namespace Ryujinx.HLE.HOS.Services.Settings { "systempowerstate!always_reboot", false }, { "systempowerstate!power_state_message_emulation_trigger_time", 0 }, { "systempowerstate!power_state_message_to_emulate", 0 }, - { "target_manager!device_name", "" }, + { "target_manager!device_name", string.Empty }, { "vulnerability!needs_update_vulnerability_policy", 0 }, { "apm!performance_mode_policy", "auto" }, { "apm!sdev_throttling_enabled", true }, diff --git a/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/IClient.cs b/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/IClient.cs index 870a6b36c..3a40a4ac5 100644 --- a/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/IClient.cs +++ b/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/IClient.cs @@ -95,7 +95,7 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd } } - ISocket newBsdSocket = new ManagedSocket(netDomain, (SocketType)type, protocol) + ISocket newBsdSocket = new ManagedSocket(netDomain, (SocketType)type, protocol, context.Device.Configuration.MultiplayerLanInterfaceId) { Blocking = !creationFlags.HasFlag(BsdSocketCreationFlags.NonBlocking), }; @@ -121,7 +121,14 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd { IPEndPoint endPoint = isRemote ? socket.RemoteEndPoint : socket.LocalEndPoint; - context.Memory.Write(bufferPosition, BsdSockAddr.FromIPEndPoint(endPoint)); + if (endPoint != null) + { + context.Memory.Write(bufferPosition, BsdSockAddr.FromIPEndPoint(endPoint)); + } + else + { + context.Memory.Write(bufferPosition, new BsdSockAddr()); + } } [CommandCmif(0)] @@ -433,8 +440,9 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd // If we are here, that mean nothing was available, sleep for 50ms context.Device.System.KernelContext.Syscall.SleepThread(50 * 1000000); + context.Thread.HandlePostSyscall(); } - while (PerformanceCounter.ElapsedMilliseconds < budgetLeftMilliseconds); + while (context.Thread.Context.Running && PerformanceCounter.ElapsedMilliseconds < budgetLeftMilliseconds); } else if (timeout == -1) { diff --git a/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/ISocket.cs b/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/ISocket.cs index fe2f8477f..cf32daf3b 100644 --- a/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/ISocket.cs +++ b/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/ISocket.cs @@ -16,7 +16,7 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd ProtocolType ProtocolType { get; } - IntPtr Handle { get; } + nint Handle { get; } LinuxError Receive(out int receiveSize, Span buffer, BsdSocketFlags flags); diff --git a/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Impl/ManagedSocket.cs b/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Impl/ManagedSocket.cs index c42b7201b..981fe0a8f 100644 --- a/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Impl/ManagedSocket.cs +++ b/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Impl/ManagedSocket.cs @@ -1,4 +1,5 @@ using Ryujinx.Common.Logging; +using Ryujinx.HLE.HOS.Services.Sockets.Bsd.Proxy; using Ryujinx.HLE.HOS.Services.Sockets.Bsd.Types; using System; using System.Collections.Generic; @@ -21,21 +22,21 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl public bool Blocking { get => Socket.Blocking; set => Socket.Blocking = value; } - public IntPtr Handle => Socket.Handle; + public nint Handle => IntPtr.Zero; public IPEndPoint RemoteEndPoint => Socket.RemoteEndPoint as IPEndPoint; public IPEndPoint LocalEndPoint => Socket.LocalEndPoint as IPEndPoint; - public Socket Socket { get; } + public ISocketImpl Socket { get; } - public ManagedSocket(AddressFamily addressFamily, SocketType socketType, ProtocolType protocolType) + public ManagedSocket(AddressFamily addressFamily, SocketType socketType, ProtocolType protocolType, string lanInterfaceId) { - Socket = new Socket(addressFamily, socketType, protocolType); + Socket = SocketHelpers.CreateSocket(addressFamily, socketType, protocolType, lanInterfaceId); Refcount = 1; } - private ManagedSocket(Socket socket) + private ManagedSocket(ISocketImpl socket) { Socket = socket; Refcount = 1; @@ -185,6 +186,8 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl } } + bool hasEmittedBlockingWarning = false; + public LinuxError Receive(out int receiveSize, Span buffer, BsdSocketFlags flags) { LinuxError result; @@ -199,6 +202,12 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl shouldBlockAfterOperation = true; } + if (Blocking && !hasEmittedBlockingWarning) + { + Logger.Warning?.PrintMsg(LogClass.ServiceBsd, "Blocking socket operations are not yet working properly. Expect network errors."); + hasEmittedBlockingWarning = true; + } + receiveSize = Socket.Receive(buffer, ConvertBsdSocketFlags(flags)); result = LinuxError.SUCCESS; @@ -236,6 +245,12 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl shouldBlockAfterOperation = true; } + if (Blocking && !hasEmittedBlockingWarning) + { + Logger.Warning?.PrintMsg(LogClass.ServiceBsd, "Blocking socket operations are not yet working properly. Expect network errors."); + hasEmittedBlockingWarning = true; + } + if (!Socket.IsBound) { receiveSize = -1; @@ -313,7 +328,7 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl Logger.Warning?.Print(LogClass.ServiceBsd, $"Unsupported GetSockOpt Option: {option} Level: {level}"); optionValue.Clear(); - return LinuxError.SUCCESS; + return LinuxError.EOPNOTSUPP; } byte[] tempOptionValue = new byte[optionValue.Length]; @@ -347,7 +362,7 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl { Logger.Warning?.Print(LogClass.ServiceBsd, $"Unsupported SetSockOpt Option: {option} Level: {level}"); - return LinuxError.SUCCESS; + return LinuxError.EOPNOTSUPP; } int value = optionValue.Length >= 4 ? MemoryMarshal.Read(optionValue) : MemoryMarshal.Read(optionValue); @@ -493,7 +508,7 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl try { - int receiveSize = Socket.Receive(ConvertMessagesToBuffer(message), ConvertBsdSocketFlags(flags), out SocketError socketError); + int receiveSize = (Socket as DefaultSocket).BaseSocket.Receive(ConvertMessagesToBuffer(message), ConvertBsdSocketFlags(flags), out SocketError socketError); if (receiveSize > 0) { @@ -531,7 +546,7 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl try { - int sendSize = Socket.Send(ConvertMessagesToBuffer(message), ConvertBsdSocketFlags(flags), out SocketError socketError); + int sendSize = (Socket as DefaultSocket).BaseSocket.Send(ConvertMessagesToBuffer(message), ConvertBsdSocketFlags(flags), out SocketError socketError); if (sendSize > 0) { diff --git a/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Impl/ManagedSocketPollManager.cs b/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Impl/ManagedSocketPollManager.cs index d0db44086..e870e8aea 100644 --- a/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Impl/ManagedSocketPollManager.cs +++ b/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Impl/ManagedSocketPollManager.cs @@ -1,4 +1,5 @@ using Ryujinx.Common.Logging; +using Ryujinx.HLE.HOS.Services.Sockets.Bsd.Proxy; using Ryujinx.HLE.HOS.Services.Sockets.Bsd.Types; using System.Collections.Generic; using System.Net.Sockets; @@ -26,45 +27,46 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl public LinuxError Poll(List events, int timeoutMilliseconds, out int updatedCount) { - List readEvents = new(); - List writeEvents = new(); - List errorEvents = new(); + List readEvents = new(); + List writeEvents = new(); + List errorEvents = new(); updatedCount = 0; foreach (PollEvent evnt in events) { - ManagedSocket socket = (ManagedSocket)evnt.FileDescriptor; - - bool isValidEvent = evnt.Data.InputEvents == 0; - - errorEvents.Add(socket.Socket); - - if ((evnt.Data.InputEvents & PollEventTypeMask.Input) != 0) + if (evnt.FileDescriptor is ManagedSocket ms) { - readEvents.Add(socket.Socket); + bool isValidEvent = evnt.Data.InputEvents == 0; - isValidEvent = true; - } + errorEvents.Add(ms.Socket); - if ((evnt.Data.InputEvents & PollEventTypeMask.UrgentInput) != 0) - { - readEvents.Add(socket.Socket); + if ((evnt.Data.InputEvents & PollEventTypeMask.Input) != 0) + { + readEvents.Add(ms.Socket); - isValidEvent = true; - } + isValidEvent = true; + } - if ((evnt.Data.InputEvents & PollEventTypeMask.Output) != 0) - { - writeEvents.Add(socket.Socket); + if ((evnt.Data.InputEvents & PollEventTypeMask.UrgentInput) != 0) + { + readEvents.Add(ms.Socket); - isValidEvent = true; - } + isValidEvent = true; + } - if (!isValidEvent) - { - Logger.Warning?.Print(LogClass.ServiceBsd, $"Unsupported Poll input event type: {evnt.Data.InputEvents}"); - return LinuxError.EINVAL; + if ((evnt.Data.InputEvents & PollEventTypeMask.Output) != 0) + { + writeEvents.Add(ms.Socket); + + isValidEvent = true; + } + + if (!isValidEvent) + { + Logger.Warning?.Print(LogClass.ServiceBsd, $"Unsupported Poll input event type: {evnt.Data.InputEvents}"); + return LinuxError.EINVAL; + } } } @@ -72,7 +74,7 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl { int actualTimeoutMicroseconds = timeoutMilliseconds == -1 ? -1 : timeoutMilliseconds * 1000; - Socket.Select(readEvents, writeEvents, errorEvents, actualTimeoutMicroseconds); + SocketHelpers.Select(readEvents, writeEvents, errorEvents, actualTimeoutMicroseconds); } catch (SocketException exception) { @@ -81,34 +83,37 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl foreach (PollEvent evnt in events) { - Socket socket = ((ManagedSocket)evnt.FileDescriptor).Socket; - - PollEventTypeMask outputEvents = evnt.Data.OutputEvents & ~evnt.Data.InputEvents; - - if (errorEvents.Contains(socket)) + if (evnt.FileDescriptor is ManagedSocket ms) { - outputEvents |= PollEventTypeMask.Error; + ISocketImpl socket = ms.Socket; - if (!socket.Connected || !socket.IsBound) + PollEventTypeMask outputEvents = evnt.Data.OutputEvents & ~evnt.Data.InputEvents; + + if (errorEvents.Contains(ms.Socket)) { - outputEvents |= PollEventTypeMask.Disconnected; - } - } + outputEvents |= PollEventTypeMask.Error; - if (readEvents.Contains(socket)) - { - if ((evnt.Data.InputEvents & PollEventTypeMask.Input) != 0) + if (!socket.Connected || !socket.IsBound) + { + outputEvents |= PollEventTypeMask.Disconnected; + } + } + + if (readEvents.Contains(ms.Socket)) { - outputEvents |= PollEventTypeMask.Input; + if ((evnt.Data.InputEvents & PollEventTypeMask.Input) != 0) + { + outputEvents |= PollEventTypeMask.Input; + } } - } - if (writeEvents.Contains(socket)) - { - outputEvents |= PollEventTypeMask.Output; - } + if (writeEvents.Contains(ms.Socket)) + { + outputEvents |= PollEventTypeMask.Output; + } - evnt.Data.OutputEvents = outputEvents; + evnt.Data.OutputEvents = outputEvents; + } } updatedCount = readEvents.Count + writeEvents.Count + errorEvents.Count; @@ -118,53 +123,55 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl public LinuxError Select(List events, int timeout, out int updatedCount) { - List readEvents = new(); - List writeEvents = new(); - List errorEvents = new(); + List readEvents = new(); + List writeEvents = new(); + List errorEvents = new(); updatedCount = 0; foreach (PollEvent pollEvent in events) { - ManagedSocket socket = (ManagedSocket)pollEvent.FileDescriptor; - - if (pollEvent.Data.InputEvents.HasFlag(PollEventTypeMask.Input)) + if (pollEvent.FileDescriptor is ManagedSocket ms) { - readEvents.Add(socket.Socket); - } + if (pollEvent.Data.InputEvents.HasFlag(PollEventTypeMask.Input)) + { + readEvents.Add(ms.Socket); + } - if (pollEvent.Data.InputEvents.HasFlag(PollEventTypeMask.Output)) - { - writeEvents.Add(socket.Socket); - } + if (pollEvent.Data.InputEvents.HasFlag(PollEventTypeMask.Output)) + { + writeEvents.Add(ms.Socket); + } - if (pollEvent.Data.InputEvents.HasFlag(PollEventTypeMask.Error)) - { - errorEvents.Add(socket.Socket); + if (pollEvent.Data.InputEvents.HasFlag(PollEventTypeMask.Error)) + { + errorEvents.Add(ms.Socket); + } } } - Socket.Select(readEvents, writeEvents, errorEvents, timeout); + SocketHelpers.Select(readEvents, writeEvents, errorEvents, timeout); updatedCount = readEvents.Count + writeEvents.Count + errorEvents.Count; foreach (PollEvent pollEvent in events) { - ManagedSocket socket = (ManagedSocket)pollEvent.FileDescriptor; - - if (readEvents.Contains(socket.Socket)) + if (pollEvent.FileDescriptor is ManagedSocket ms) { - pollEvent.Data.OutputEvents |= PollEventTypeMask.Input; - } + if (readEvents.Contains(ms.Socket)) + { + pollEvent.Data.OutputEvents |= PollEventTypeMask.Input; + } - if (writeEvents.Contains(socket.Socket)) - { - pollEvent.Data.OutputEvents |= PollEventTypeMask.Output; - } + if (writeEvents.Contains(ms.Socket)) + { + pollEvent.Data.OutputEvents |= PollEventTypeMask.Output; + } - if (errorEvents.Contains(socket.Socket)) - { - pollEvent.Data.OutputEvents |= PollEventTypeMask.Error; + if (errorEvents.Contains(ms.Socket)) + { + pollEvent.Data.OutputEvents |= PollEventTypeMask.Error; + } } } diff --git a/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Impl/WinSockHelper.cs b/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Impl/WinSockHelper.cs index b6d8be135..e2ef75f80 100644 --- a/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Impl/WinSockHelper.cs +++ b/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Impl/WinSockHelper.cs @@ -283,7 +283,7 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl public static LinuxError ConvertError(WsaError errorCode) { - if (OperatingSystem.IsMacOS() || OperatingSystem.IsIOS()) + if (OperatingSystem.IsMacOS()) { if (_errorMapMacOs.TryGetValue((int)errorCode, out LinuxError errno)) { diff --git a/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Proxy/DefaultSocket.cs b/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Proxy/DefaultSocket.cs new file mode 100644 index 000000000..f1040e799 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Proxy/DefaultSocket.cs @@ -0,0 +1,178 @@ +using Ryujinx.Common.Utilities; +using System; +using System.Net; +using System.Net.NetworkInformation; +using System.Net.Sockets; + +namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Proxy +{ + class DefaultSocket : ISocketImpl + { + public Socket BaseSocket { get; } + + public EndPoint RemoteEndPoint => BaseSocket.RemoteEndPoint; + + public EndPoint LocalEndPoint => BaseSocket.LocalEndPoint; + + public bool Connected => BaseSocket.Connected; + + public bool IsBound => BaseSocket.IsBound; + + public AddressFamily AddressFamily => BaseSocket.AddressFamily; + + public SocketType SocketType => BaseSocket.SocketType; + + public ProtocolType ProtocolType => BaseSocket.ProtocolType; + + public bool Blocking { get => BaseSocket.Blocking; set => BaseSocket.Blocking = value; } + + public int Available => BaseSocket.Available; + + private readonly string _lanInterfaceId; + + public DefaultSocket(Socket baseSocket, string lanInterfaceId) + { + _lanInterfaceId = lanInterfaceId; + + BaseSocket = baseSocket; + } + + public DefaultSocket(AddressFamily domain, SocketType type, ProtocolType protocol, string lanInterfaceId) + { + _lanInterfaceId = lanInterfaceId; + + BaseSocket = new Socket(domain, type, protocol); + } + + private void EnsureNetworkInterfaceBound() + { + if (_lanInterfaceId != "0" && !BaseSocket.IsBound) + { + (_, UnicastIPAddressInformation ipInfo) = NetworkHelpers.GetLocalInterface(_lanInterfaceId); + + BaseSocket.Bind(new IPEndPoint(ipInfo.Address, 0)); + } + } + + public ISocketImpl Accept() + { + return new DefaultSocket(BaseSocket.Accept(), _lanInterfaceId); + } + + public void Bind(EndPoint localEP) + { + // NOTE: The guest is able to receive on 0.0.0.0 without it being limited to the chosen network interface. + // This is because it must get loopback traffic as well. This could allow other network traffic to leak in. + + BaseSocket.Bind(localEP); + } + + public void Close() + { + BaseSocket.Close(); + } + + public void Connect(EndPoint remoteEP) + { + EnsureNetworkInterfaceBound(); + + BaseSocket.Connect(remoteEP); + } + + public void Disconnect(bool reuseSocket) + { + BaseSocket.Disconnect(reuseSocket); + } + + public void GetSocketOption(SocketOptionLevel optionLevel, SocketOptionName optionName, byte[] optionValue) + { + BaseSocket.GetSocketOption(optionLevel, optionName, optionValue); + } + + public void Listen(int backlog) + { + BaseSocket.Listen(backlog); + } + + public int Receive(Span buffer) + { + EnsureNetworkInterfaceBound(); + + return BaseSocket.Receive(buffer); + } + + public int Receive(Span buffer, SocketFlags flags) + { + EnsureNetworkInterfaceBound(); + + return BaseSocket.Receive(buffer, flags); + } + + public int Receive(Span buffer, SocketFlags flags, out SocketError socketError) + { + EnsureNetworkInterfaceBound(); + + return BaseSocket.Receive(buffer, flags, out socketError); + } + + public int ReceiveFrom(Span buffer, SocketFlags flags, ref EndPoint remoteEP) + { + EnsureNetworkInterfaceBound(); + + return BaseSocket.ReceiveFrom(buffer, flags, ref remoteEP); + } + + public int Send(ReadOnlySpan buffer) + { + EnsureNetworkInterfaceBound(); + + return BaseSocket.Send(buffer); + } + + public int Send(ReadOnlySpan buffer, SocketFlags flags) + { + EnsureNetworkInterfaceBound(); + + return BaseSocket.Send(buffer, flags); + } + + public int Send(ReadOnlySpan buffer, SocketFlags flags, out SocketError socketError) + { + EnsureNetworkInterfaceBound(); + + return BaseSocket.Send(buffer, flags, out socketError); + } + + public int SendTo(ReadOnlySpan buffer, SocketFlags flags, EndPoint remoteEP) + { + EnsureNetworkInterfaceBound(); + + return BaseSocket.SendTo(buffer, flags, remoteEP); + } + + public bool Poll(int microSeconds, SelectMode mode) + { + return BaseSocket.Poll(microSeconds, mode); + } + + public void SetSocketOption(SocketOptionLevel optionLevel, SocketOptionName optionName, int optionValue) + { + BaseSocket.SetSocketOption(optionLevel, optionName, optionValue); + } + + public void SetSocketOption(SocketOptionLevel optionLevel, SocketOptionName optionName, object optionValue) + { + BaseSocket.SetSocketOption(optionLevel, optionName, optionValue); + } + + public void Shutdown(SocketShutdown how) + { + BaseSocket.Shutdown(how); + } + + public void Dispose() + { + BaseSocket.Dispose(); + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Proxy/ISocket.cs b/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Proxy/ISocket.cs new file mode 100644 index 000000000..b7055f08b --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Proxy/ISocket.cs @@ -0,0 +1,47 @@ +using System; +using System.Net; +using System.Net.Sockets; + +namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Proxy +{ + interface ISocketImpl : IDisposable + { + EndPoint RemoteEndPoint { get; } + EndPoint LocalEndPoint { get; } + bool Connected { get; } + bool IsBound { get; } + + AddressFamily AddressFamily { get; } + SocketType SocketType { get; } + ProtocolType ProtocolType { get; } + + bool Blocking { get; set; } + int Available { get; } + + int Receive(Span buffer); + int Receive(Span buffer, SocketFlags flags); + int Receive(Span buffer, SocketFlags flags, out SocketError socketError); + int ReceiveFrom(Span buffer, SocketFlags flags, ref EndPoint remoteEP); + + int Send(ReadOnlySpan buffer); + int Send(ReadOnlySpan buffer, SocketFlags flags); + int Send(ReadOnlySpan buffer, SocketFlags flags, out SocketError socketError); + int SendTo(ReadOnlySpan buffer, SocketFlags flags, EndPoint remoteEP); + + bool Poll(int microSeconds, SelectMode mode); + + ISocketImpl Accept(); + + void Bind(EndPoint localEP); + void Connect(EndPoint remoteEP); + void Listen(int backlog); + + void GetSocketOption(SocketOptionLevel optionLevel, SocketOptionName optionName, byte[] optionValue); + void SetSocketOption(SocketOptionLevel optionLevel, SocketOptionName optionName, int optionValue); + void SetSocketOption(SocketOptionLevel optionLevel, SocketOptionName optionName, object optionValue); + + void Shutdown(SocketShutdown how); + void Disconnect(bool reuseSocket); + void Close(); + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Proxy/SocketHelpers.cs b/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Proxy/SocketHelpers.cs new file mode 100644 index 000000000..485a7f86b --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Proxy/SocketHelpers.cs @@ -0,0 +1,74 @@ +using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Proxy; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Sockets; + +namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Proxy +{ + static class SocketHelpers + { + private static LdnProxy _proxy; + + public static void Select(List readEvents, List writeEvents, List errorEvents, int timeout) + { + var readDefault = readEvents.Select(x => (x as DefaultSocket)?.BaseSocket).Where(x => x != null).ToList(); + var writeDefault = writeEvents.Select(x => (x as DefaultSocket)?.BaseSocket).Where(x => x != null).ToList(); + var errorDefault = errorEvents.Select(x => (x as DefaultSocket)?.BaseSocket).Where(x => x != null).ToList(); + + if (readDefault.Count != 0 || writeDefault.Count != 0 || errorDefault.Count != 0) + { + Socket.Select(readDefault, writeDefault, errorDefault, timeout); + } + + void FilterSockets(List removeFrom, List selectedSockets, Func ldnCheck) + { + removeFrom.RemoveAll(socket => + { + switch (socket) + { + case DefaultSocket dsocket: + return !selectedSockets.Contains(dsocket.BaseSocket); + case LdnProxySocket psocket: + return !ldnCheck(psocket); + default: + throw new NotImplementedException(); + } + }); + }; + + FilterSockets(readEvents, readDefault, (socket) => socket.Readable); + FilterSockets(writeEvents, writeDefault, (socket) => socket.Writable); + FilterSockets(errorEvents, errorDefault, (socket) => socket.Error); + } + + public static void RegisterProxy(LdnProxy proxy) + { + if (_proxy != null) + { + UnregisterProxy(); + } + + _proxy = proxy; + } + + public static void UnregisterProxy() + { + _proxy?.Dispose(); + _proxy = null; + } + + public static ISocketImpl CreateSocket(AddressFamily domain, SocketType type, ProtocolType protocol, string lanInterfaceId) + { + if (_proxy != null) + { + if (_proxy.Supported(domain, type, protocol)) + { + return new LdnProxySocket(domain, type, protocol, _proxy); + } + } + + return new DefaultSocket(domain, type, protocol, lanInterfaceId); + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Sockets/Nsd/Manager/FqdnResolver.cs b/src/Ryujinx.HLE/HOS/Services/Sockets/Nsd/Manager/FqdnResolver.cs index 2ec0f744e..3ff48b883 100644 --- a/src/Ryujinx.HLE/HOS/Services/Sockets/Nsd/Manager/FqdnResolver.cs +++ b/src/Ryujinx.HLE/HOS/Services/Sockets/Nsd/Manager/FqdnResolver.cs @@ -36,7 +36,7 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Nsd.Manager // TODO: Load Environment from the savedata. address = address.Replace("%", IManager.NsdSettings.Environment); - resolvedAddress = ""; + resolvedAddress = string.Empty; if (IManager.NsdSettings == null) { diff --git a/src/Ryujinx.HLE/HOS/Services/Sockets/Sfdnsres/IResolver.cs b/src/Ryujinx.HLE/HOS/Services/Sockets/Sfdnsres/IResolver.cs index 39af90383..5b2de13f0 100644 --- a/src/Ryujinx.HLE/HOS/Services/Sockets/Sfdnsres/IResolver.cs +++ b/src/Ryujinx.HLE/HOS/Services/Sockets/Sfdnsres/IResolver.cs @@ -292,7 +292,7 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Sfdnsres { string host = MemoryHelper.ReadAsciiString(context.Memory, inputBufferPosition, (int)inputBufferSize); - if (!context.Device.Configuration.EnableInternetAccess) + if (host != "localhost" && !context.Device.Configuration.EnableInternetAccess) { Logger.Info?.Print(LogClass.ServiceSfdnsres, $"Guest network access disabled, DNS Blocked: {host}"); diff --git a/src/Ryujinx.HLE/HOS/Services/Ssl/BuiltInCertificateManager.cs b/src/Ryujinx.HLE/HOS/Services/Ssl/BuiltInCertificateManager.cs index 5d2e06a4f..49becac55 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ssl/BuiltInCertificateManager.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ssl/BuiltInCertificateManager.cs @@ -23,7 +23,7 @@ namespace Ryujinx.HLE.HOS.Services.Ssl { private const long CertStoreTitleId = 0x0100000000000800; - private const string CertStoreTitleMissingErrorMessage = "CertStore system title not found! SSL CA retrieving will not work, provide the system archive to fix this error. (See https://github.com/Ryujinx/Ryujinx/wiki/Ryujinx-Setup-&-Configuration-Guide#initial-setup-continued---installation-of-firmware for more information)"; + private const string CertStoreTitleMissingErrorMessage = "CertStore system title not found! SSL CA retrieving will not work, provide the system archive to fix this error."; private static BuiltInCertificateManager _instance; diff --git a/src/Ryujinx.HLE/HOS/Services/Ssl/SslService/SslManagedSocketConnection.cs b/src/Ryujinx.HLE/HOS/Services/Ssl/SslService/SslManagedSocketConnection.cs index 4dd6367ed..dc33dd6a5 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ssl/SslService/SslManagedSocketConnection.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ssl/SslService/SslManagedSocketConnection.cs @@ -1,8 +1,10 @@ using Ryujinx.HLE.HOS.Services.Sockets.Bsd; using Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl; +using Ryujinx.HLE.HOS.Services.Sockets.Bsd.Proxy; using Ryujinx.HLE.HOS.Services.Ssl.Types; using System; using System.IO; +using System.Net; using System.Net.Security; using System.Net.Sockets; using System.Security.Authentication; @@ -83,10 +85,40 @@ namespace Ryujinx.HLE.HOS.Services.Ssl.SslService } #pragma warning restore SYSLIB0039 + /// + /// Retrieve the hostname of the current remote in case the provided hostname is null or empty. + /// + /// The current hostname + /// Either the resolved or provided hostname + /// + /// This is done to avoid getting an + /// as the remote certificate will be rejected with RemoteCertificateNameMismatch due to an empty hostname. + /// This is not what the switch does! + /// It might just skip remote hostname verification if the hostname wasn't set with before. + /// TODO: Remove this as soon as we know how the switch deals with empty hostnames + /// + private string RetrieveHostName(string hostName) + { + if (!string.IsNullOrEmpty(hostName)) + { + return hostName; + } + + try + { + return Dns.GetHostEntry(Socket.RemoteEndPoint.Address).HostName; + } + catch (SocketException) + { + return hostName; + } + } + public ResultCode Handshake(string hostName) { StartSslOperation(); - _stream = new SslStream(new NetworkStream(((ManagedSocket)Socket).Socket, false), false, null, null); + _stream = new SslStream(new NetworkStream(((DefaultSocket)((ManagedSocket)Socket).Socket).BaseSocket, false), false, null, null); + hostName = RetrieveHostName(hostName); _stream.AuthenticateAsClient(hostName, null, TranslateSslVersion(_sslVersion), false); EndSslOperation(); diff --git a/src/Ryujinx.HLE/HOS/Services/SurfaceFlinger/IBinder.cs b/src/Ryujinx.HLE/HOS/Services/SurfaceFlinger/IBinder.cs index 0fb2dfd2e..54aac48ae 100644 --- a/src/Ryujinx.HLE/HOS/Services/SurfaceFlinger/IBinder.cs +++ b/src/Ryujinx.HLE/HOS/Services/SurfaceFlinger/IBinder.cs @@ -13,10 +13,10 @@ namespace Ryujinx.HLE.HOS.Services.SurfaceFlinger ResultCode OnTransact(uint code, uint flags, ReadOnlySpan inputParcel, Span outputParcel) { - Parcel inputParcelReader = new(inputParcel.ToArray()); + using Parcel inputParcelReader = new(inputParcel); // TODO: support objects? - Parcel outputParcelWriter = new((uint)(outputParcel.Length - Unsafe.SizeOf()), 0); + using Parcel outputParcelWriter = new((uint)(outputParcel.Length - Unsafe.SizeOf()), 0); string inputInterfaceToken = inputParcelReader.ReadInterfaceToken(); diff --git a/src/Ryujinx.HLE/HOS/Services/SurfaceFlinger/IHOSBinderDriver.cs b/src/Ryujinx.HLE/HOS/Services/SurfaceFlinger/IHOSBinderDriver.cs index 7cb6763b8..2ffa961cb 100644 --- a/src/Ryujinx.HLE/HOS/Services/SurfaceFlinger/IHOSBinderDriver.cs +++ b/src/Ryujinx.HLE/HOS/Services/SurfaceFlinger/IHOSBinderDriver.cs @@ -85,9 +85,9 @@ namespace Ryujinx.HLE.HOS.Services.SurfaceFlinger ReadOnlySpan inputParcel = context.Memory.GetSpan(dataPos, (int)dataSize); - using IMemoryOwner outputParcelOwner = ByteMemoryPool.RentCleared(replySize); + using SpanOwner outputParcelOwner = SpanOwner.RentCleared(checked((int)replySize)); - Span outputParcel = outputParcelOwner.Memory.Span; + Span outputParcel = outputParcelOwner.Span; ResultCode result = OnTransact(binderId, code, flags, inputParcel, outputParcel); diff --git a/src/Ryujinx.HLE/HOS/Services/SurfaceFlinger/Parcel.cs b/src/Ryujinx.HLE/HOS/Services/SurfaceFlinger/Parcel.cs index 4ac0525ba..25c89baec 100644 --- a/src/Ryujinx.HLE/HOS/Services/SurfaceFlinger/Parcel.cs +++ b/src/Ryujinx.HLE/HOS/Services/SurfaceFlinger/Parcel.cs @@ -1,4 +1,5 @@ using Ryujinx.Common; +using Ryujinx.Common.Memory; using Ryujinx.Common.Utilities; using Ryujinx.HLE.HOS.Services.SurfaceFlinger.Types; using System; @@ -9,13 +10,13 @@ using System.Text; namespace Ryujinx.HLE.HOS.Services.SurfaceFlinger { - class Parcel + sealed class Parcel : IDisposable { - private readonly byte[] _rawData; + private readonly MemoryOwner _rawDataOwner; - private Span Raw => new(_rawData); + private Span Raw => _rawDataOwner.Span; - private ref ParcelHeader Header => ref MemoryMarshal.Cast(_rawData)[0]; + private ref ParcelHeader Header => ref MemoryMarshal.Cast(Raw)[0]; private Span Payload => Raw.Slice((int)Header.PayloadOffset, (int)Header.PayloadSize); @@ -24,9 +25,11 @@ namespace Ryujinx.HLE.HOS.Services.SurfaceFlinger private int _payloadPosition; private int _objectPosition; - public Parcel(byte[] rawData) + private bool _isDisposed; + + public Parcel(ReadOnlySpan data) { - _rawData = rawData; + _rawDataOwner = MemoryOwner.RentCopy(data); _payloadPosition = 0; _objectPosition = 0; @@ -36,7 +39,7 @@ namespace Ryujinx.HLE.HOS.Services.SurfaceFlinger { uint headerSize = (uint)Unsafe.SizeOf(); - _rawData = new byte[BitUtils.AlignUp(headerSize + payloadSize + objectsSize, 4)]; + _rawDataOwner = MemoryOwner.RentCleared(checked((int)BitUtils.AlignUp(headerSize + payloadSize + objectsSize, 4))); Header.PayloadSize = payloadSize; Header.ObjectsSize = objectsSize; @@ -60,7 +63,7 @@ namespace Ryujinx.HLE.HOS.Services.SurfaceFlinger if (size < 0) { - return ""; + return string.Empty; } ReadOnlySpan data = ReadInPlace((size + 1) * 2); @@ -132,7 +135,9 @@ namespace Ryujinx.HLE.HOS.Services.SurfaceFlinger // TODO: figure out what this value is - WriteInplaceObject(new byte[4] { 0, 0, 0, 0 }); + Span fourBytes = stackalloc byte[4]; + + WriteInplaceObject(fourBytes); } public AndroidStrongPointer ReadStrongPointer() where T : unmanaged, IFlattenable @@ -219,5 +224,15 @@ namespace Ryujinx.HLE.HOS.Services.SurfaceFlinger return Raw[..(int)(Header.PayloadSize + Header.ObjectsSize + Unsafe.SizeOf())]; } + + public void Dispose() + { + if (!_isDisposed) + { + _isDisposed = true; + + _rawDataOwner.Dispose(); + } + } } } diff --git a/src/Ryujinx.HLE/HOS/Services/SurfaceFlinger/SurfaceFlinger.cs b/src/Ryujinx.HLE/HOS/Services/SurfaceFlinger/SurfaceFlinger.cs index fd517b1ae..601e85867 100644 --- a/src/Ryujinx.HLE/HOS/Services/SurfaceFlinger/SurfaceFlinger.cs +++ b/src/Ryujinx.HLE/HOS/Services/SurfaceFlinger/SurfaceFlinger.cs @@ -10,13 +10,12 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Threading; +using VSyncMode = Ryujinx.Common.Configuration.VSyncMode; namespace Ryujinx.HLE.HOS.Services.SurfaceFlinger { class SurfaceFlinger : IConsumerListener, IDisposable { - private const int TargetFps = 60; - private readonly Switch _device; private readonly Dictionary _layers; @@ -32,6 +31,9 @@ namespace Ryujinx.HLE.HOS.Services.SurfaceFlinger private readonly long _spinTicks; private readonly long _1msTicks; + private VSyncMode _vSyncMode; + private long _targetVSyncInterval; + private int _swapInterval; private int _swapIntervalDelay; @@ -88,7 +90,8 @@ namespace Ryujinx.HLE.HOS.Services.SurfaceFlinger } else { - _ticksPerFrame = Stopwatch.Frequency / TargetFps; + _ticksPerFrame = Stopwatch.Frequency / _device.TargetVSyncInterval; + _targetVSyncInterval = _device.TargetVSyncInterval; } } @@ -370,15 +373,20 @@ namespace Ryujinx.HLE.HOS.Services.SurfaceFlinger if (acquireStatus == Status.Success) { - // If device vsync is disabled, reflect the change. - if (!_device.EnableDeviceVsync) + if (_device.VSyncMode == VSyncMode.Unbounded) { if (_swapInterval != 0) { UpdateSwapInterval(0); + _vSyncMode = _device.VSyncMode; } } - else if (item.SwapInterval != _swapInterval) + else if (_device.VSyncMode != _vSyncMode) + { + UpdateSwapInterval(_device.VSyncMode == VSyncMode.Unbounded ? 0 : item.SwapInterval); + _vSyncMode = _device.VSyncMode; + } + else if (item.SwapInterval != _swapInterval || _device.TargetVSyncInterval != _targetVSyncInterval) { UpdateSwapInterval(item.SwapInterval); } @@ -412,9 +420,9 @@ namespace Ryujinx.HLE.HOS.Services.SurfaceFlinger Format format = ConvertColorFormat(item.GraphicBuffer.Object.Buffer.Surfaces[0].ColorFormat); - int bytesPerPixel = + byte bytesPerPixel = format == Format.B5G6R5Unorm || - format == Format.R4G4B4A4Unorm ? 2 : 4; + format == Format.R4G4B4A4Unorm ? (byte)2 : (byte)4; int gobBlocksInY = 1 << item.GraphicBuffer.Object.Buffer.Surfaces[0].BlockHeightLog2; diff --git a/src/Ryujinx.HLE/HOS/Services/Time/Clock/SteadyClockCore.cs b/src/Ryujinx.HLE/HOS/Services/Time/Clock/SteadyClockCore.cs index 95cf2a899..98094d35c 100644 --- a/src/Ryujinx.HLE/HOS/Services/Time/Clock/SteadyClockCore.cs +++ b/src/Ryujinx.HLE/HOS/Services/Time/Clock/SteadyClockCore.cs @@ -12,7 +12,7 @@ namespace Ryujinx.HLE.HOS.Services.Time.Clock public SteadyClockCore() { - _clockSourceId = UInt128Utils.CreateRandom(); + _clockSourceId = Random.Shared.NextUInt128(); _isRtcResetDetected = false; _isInitialized = false; } diff --git a/src/Ryujinx.HLE/HOS/Services/Time/Clock/Types/SteadyClockTimePoint.cs b/src/Ryujinx.HLE/HOS/Services/Time/Clock/Types/SteadyClockTimePoint.cs index e4878483d..bce15c672 100644 --- a/src/Ryujinx.HLE/HOS/Services/Time/Clock/Types/SteadyClockTimePoint.cs +++ b/src/Ryujinx.HLE/HOS/Services/Time/Clock/Types/SteadyClockTimePoint.cs @@ -36,7 +36,7 @@ namespace Ryujinx.HLE.HOS.Services.Time.Clock return new SteadyClockTimePoint { TimePoint = 0, - ClockSourceId = UInt128Utils.CreateRandom(), + ClockSourceId = Random.Shared.NextUInt128(), }; } } diff --git a/src/Ryujinx.HLE/HOS/Services/Time/TimeZone/TimeZoneContentManager.cs b/src/Ryujinx.HLE/HOS/Services/Time/TimeZone/TimeZoneContentManager.cs index abf3cd7d6..222698a7f 100644 --- a/src/Ryujinx.HLE/HOS/Services/Time/TimeZone/TimeZoneContentManager.cs +++ b/src/Ryujinx.HLE/HOS/Services/Time/TimeZone/TimeZoneContentManager.cs @@ -23,7 +23,7 @@ namespace Ryujinx.HLE.HOS.Services.Time.TimeZone { private const long TimeZoneBinaryTitleId = 0x010000000000080E; - private const string TimeZoneSystemTitleMissingErrorMessage = "TimeZoneBinary system title not found! TimeZone conversions will not work, provide the system archive to fix this error. (See https://github.com/Ryujinx/Ryujinx/wiki/Ryujinx-Setup-&-Configuration-Guide#initial-setup-continued---installation-of-firmware for more information)"; + private const string TimeZoneSystemTitleMissingErrorMessage = "TimeZoneBinary system title not found! TimeZone conversions will not work, provide the system archive to fix this error."; private VirtualFileSystem _virtualFileSystem; private IntegrityCheckLevel _fsIntegrityCheckLevel; diff --git a/src/Ryujinx.HLE/HOS/Services/Vi/RootService/IApplicationDisplayService.cs b/src/Ryujinx.HLE/HOS/Services/Vi/RootService/IApplicationDisplayService.cs index 143e21661..edb441a0a 100644 --- a/src/Ryujinx.HLE/HOS/Services/Vi/RootService/IApplicationDisplayService.cs +++ b/src/Ryujinx.HLE/HOS/Services/Vi/RootService/IApplicationDisplayService.cs @@ -7,7 +7,7 @@ using Ryujinx.HLE.HOS.Services.SurfaceFlinger; using Ryujinx.HLE.HOS.Services.Vi.RootService.ApplicationDisplayService; using Ryujinx.HLE.HOS.Services.Vi.RootService.ApplicationDisplayService.Types; using Ryujinx.HLE.HOS.Services.Vi.Types; -using Ryujinx.HLE.Ui; +using Ryujinx.HLE.UI; using Ryujinx.Horizon.Common; using System; using System.Collections.Generic; @@ -166,7 +166,7 @@ namespace Ryujinx.HLE.HOS.Services.Vi.RootService private ResultCode OpenDisplayImpl(ServiceCtx context, string name) { - if (name == "") + if (name == string.Empty) { return ResultCode.InvalidValue; } @@ -250,7 +250,7 @@ namespace Ryujinx.HLE.HOS.Services.Vi.RootService context.Device.System.SurfaceFlinger.SetRenderLayer(layerId); - Parcel parcel = new(0x28, 0x4); + using Parcel parcel = new(0x28, 0x4); parcel.WriteObject(producer, "dispdrv\0"); @@ -288,7 +288,7 @@ namespace Ryujinx.HLE.HOS.Services.Vi.RootService context.Device.System.SurfaceFlinger.SetRenderLayer(layerId); - Parcel parcel = new(0x28, 0x4); + using Parcel parcel = new(0x28, 0x4); parcel.WriteObject(producer, "dispdrv\0"); diff --git a/src/Ryujinx.HLE/HOS/TamperMachine.cs b/src/Ryujinx.HLE/HOS/TamperMachine.cs index f234e540e..609221535 100644 --- a/src/Ryujinx.HLE/HOS/TamperMachine.cs +++ b/src/Ryujinx.HLE/HOS/TamperMachine.cs @@ -143,7 +143,7 @@ namespace Ryujinx.HLE.HOS try { - ControllerKeys pressedKeys = (ControllerKeys)Thread.VolatileRead(ref _pressedKeys); + ControllerKeys pressedKeys = (ControllerKeys)Volatile.Read(ref _pressedKeys); program.Process.TamperedCodeMemory = false; program.Execute(pressedKeys); @@ -175,14 +175,14 @@ namespace Ryujinx.HLE.HOS { if (input.PlayerId == PlayerIndex.Player1 || input.PlayerId == PlayerIndex.Handheld) { - Thread.VolatileWrite(ref _pressedKeys, (long)input.Buttons); + Volatile.Write(ref _pressedKeys, (long)input.Buttons); return; } } // Clear the input because player one is not conected. - Thread.VolatileWrite(ref _pressedKeys, 0); + Volatile.Write(ref _pressedKeys, 0); } } } diff --git a/src/Ryujinx.HLE/Loaders/Executables/NsoExecutable.cs b/src/Ryujinx.HLE/Loaders/Executables/NsoExecutable.cs index 83ad5d7e8..1caedb51e 100644 --- a/src/Ryujinx.HLE/Loaders/Executables/NsoExecutable.cs +++ b/src/Ryujinx.HLE/Loaders/Executables/NsoExecutable.cs @@ -102,7 +102,7 @@ namespace Ryujinx.HLE.Loaders.Executables Match fsSdkMatch = FsSdkRegex().Match(rawTextBuffer); if (fsSdkMatch.Success) { - stringBuilder.AppendLine($" FS SDK Version: {fsSdkMatch.Value.Replace("sdk_version: ", "")}"); + stringBuilder.AppendLine($" FS SDK Version: {fsSdkMatch.Value.Replace("sdk_version: ", string.Empty)}"); } MatchCollection sdkMwMatches = SdkMwRegex().Matches(rawTextBuffer); diff --git a/src/Ryujinx.HLE/Loaders/Npdm/ACI0.cs b/src/Ryujinx.HLE/Loaders/Npdm/ACI0.cs index 9a5b6b0aa..8d828e8ed 100644 --- a/src/Ryujinx.HLE/Loaders/Npdm/ACI0.cs +++ b/src/Ryujinx.HLE/Loaders/Npdm/ACI0.cs @@ -15,6 +15,12 @@ namespace Ryujinx.HLE.Loaders.Npdm public ServiceAccessControl ServiceAccessControl { get; private set; } public KernelAccessControl KernelAccessControl { get; private set; } + /// The stream doesn't contain valid ACI0 data. + /// The stream does not support reading, is , or is already closed. + /// The end of the stream is reached. + /// The stream is closed. + /// An I/O error occurred. + /// The FsAccessHeader.ContentOwnerId section is not implemented. public Aci0(Stream stream, int offset) { stream.Seek(offset, SeekOrigin.Begin); diff --git a/src/Ryujinx.HLE/Loaders/Npdm/ACID.cs b/src/Ryujinx.HLE/Loaders/Npdm/ACID.cs index ab30b40ca..57d0ee274 100644 --- a/src/Ryujinx.HLE/Loaders/Npdm/ACID.cs +++ b/src/Ryujinx.HLE/Loaders/Npdm/ACID.cs @@ -19,6 +19,11 @@ namespace Ryujinx.HLE.Loaders.Npdm public ServiceAccessControl ServiceAccessControl { get; private set; } public KernelAccessControl KernelAccessControl { get; private set; } + /// The stream doesn't contain valid ACID data. + /// The stream does not support reading, is , or is already closed. + /// The end of the stream is reached. + /// The stream is closed. + /// An I/O error occurred. public Acid(Stream stream, int offset) { stream.Seek(offset, SeekOrigin.Begin); diff --git a/src/Ryujinx.HLE/Loaders/Npdm/FsAccessControl.cs b/src/Ryujinx.HLE/Loaders/Npdm/FsAccessControl.cs index f17ca348b..a369f9f2d 100644 --- a/src/Ryujinx.HLE/Loaders/Npdm/FsAccessControl.cs +++ b/src/Ryujinx.HLE/Loaders/Npdm/FsAccessControl.cs @@ -11,6 +11,10 @@ namespace Ryujinx.HLE.Loaders.Npdm public int Unknown3 { get; private set; } public int Unknown4 { get; private set; } + /// The stream does not support reading, is , or is already closed. + /// The end of the stream is reached. + /// The stream is closed. + /// An I/O error occurred. public FsAccessControl(Stream stream, int offset, int size) { stream.Seek(offset, SeekOrigin.Begin); diff --git a/src/Ryujinx.HLE/Loaders/Npdm/FsAccessHeader.cs b/src/Ryujinx.HLE/Loaders/Npdm/FsAccessHeader.cs index 5987be0ef..249f8dd9d 100644 --- a/src/Ryujinx.HLE/Loaders/Npdm/FsAccessHeader.cs +++ b/src/Ryujinx.HLE/Loaders/Npdm/FsAccessHeader.cs @@ -9,6 +9,12 @@ namespace Ryujinx.HLE.Loaders.Npdm public int Version { get; private set; } public ulong PermissionsBitmask { get; private set; } + /// The stream contains invalid data. + /// The ContentOwnerId section is not implemented. + /// The stream does not support reading, is , or is already closed. + /// The end of the stream is reached. + /// The stream is closed. + /// An I/O error occurred. public FsAccessHeader(Stream stream, int offset, int size) { stream.Seek(offset, SeekOrigin.Begin); diff --git a/src/Ryujinx.HLE/Loaders/Npdm/KernelAccessControl.cs b/src/Ryujinx.HLE/Loaders/Npdm/KernelAccessControl.cs index 171243799..979c6f669 100644 --- a/src/Ryujinx.HLE/Loaders/Npdm/KernelAccessControl.cs +++ b/src/Ryujinx.HLE/Loaders/Npdm/KernelAccessControl.cs @@ -6,6 +6,10 @@ namespace Ryujinx.HLE.Loaders.Npdm { public int[] Capabilities { get; private set; } + /// The stream does not support reading, is , or is already closed. + /// The end of the stream is reached. + /// The stream is closed. + /// An I/O error occurred. public KernelAccessControl(Stream stream, int offset, int size) { stream.Seek(offset, SeekOrigin.Begin); diff --git a/src/Ryujinx.HLE/Loaders/Npdm/Npdm.cs b/src/Ryujinx.HLE/Loaders/Npdm/Npdm.cs index 622d7ee03..4a99de98c 100644 --- a/src/Ryujinx.HLE/Loaders/Npdm/Npdm.cs +++ b/src/Ryujinx.HLE/Loaders/Npdm/Npdm.cs @@ -24,6 +24,13 @@ namespace Ryujinx.HLE.Loaders.Npdm public Aci0 Aci0 { get; private set; } public Acid Acid { get; private set; } + /// The stream doesn't contain valid NPDM data. + /// The FsAccessHeader.ContentOwnerId section is not implemented. + /// The stream does not support reading, is , or is already closed. + /// An error occured while reading bytes from the stream. + /// The end of the stream is reached. + /// The stream is closed. + /// An I/O error occurred. public Npdm(Stream stream) { BinaryReader reader = new(stream); diff --git a/src/Ryujinx.HLE/Loaders/Npdm/ServiceAccessControl.cs b/src/Ryujinx.HLE/Loaders/Npdm/ServiceAccessControl.cs index bb6df27fa..b6bc6492d 100644 --- a/src/Ryujinx.HLE/Loaders/Npdm/ServiceAccessControl.cs +++ b/src/Ryujinx.HLE/Loaders/Npdm/ServiceAccessControl.cs @@ -9,6 +9,11 @@ namespace Ryujinx.HLE.Loaders.Npdm { public IReadOnlyDictionary Services { get; private set; } + /// The stream does not support reading, is , or is already closed. + /// An error occured while reading bytes from the stream. + /// The end of the stream is reached. + /// The stream is closed. + /// An I/O error occurred. public ServiceAccessControl(Stream stream, int offset, int size) { stream.Seek(offset, SeekOrigin.Begin); diff --git a/src/Ryujinx.HLE/Loaders/Processes/Extensions/FileSystemExtensions.cs b/src/Ryujinx.HLE/Loaders/Processes/Extensions/FileSystemExtensions.cs index 28f6fcff4..cd215781f 100644 --- a/src/Ryujinx.HLE/Loaders/Processes/Extensions/FileSystemExtensions.cs +++ b/src/Ryujinx.HLE/Loaders/Processes/Extensions/FileSystemExtensions.cs @@ -34,7 +34,7 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions return metaLoader; } - public static ProcessResult Load(this IFileSystem exeFs, Switch device, BlitStruct nacpData, MetaLoader metaLoader, bool isHomebrew = false) + public static ProcessResult Load(this IFileSystem exeFs, Switch device, BlitStruct nacpData, MetaLoader metaLoader, byte programIndex, bool isHomebrew = false) { ulong programId = metaLoader.GetProgramId(); @@ -89,7 +89,7 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions Logger.Warning?.Print(LogClass.Ptc, "Detected unsupported ExeFs modifications. PTC disabled."); } - string programName = ""; + string programName = string.Empty; if (!isHomebrew && programId > 0x010000000000FFFF) { @@ -119,6 +119,7 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions true, programName, metaLoader.GetProgramId(), + programIndex, null, nsoExecutables); diff --git a/src/Ryujinx.HLE/Loaders/Processes/Extensions/LocalFileSystemExtensions.cs b/src/Ryujinx.HLE/Loaders/Processes/Extensions/LocalFileSystemExtensions.cs index 798a9261e..e3ae9bf5f 100644 --- a/src/Ryujinx.HLE/Loaders/Processes/Extensions/LocalFileSystemExtensions.cs +++ b/src/Ryujinx.HLE/Loaders/Processes/Extensions/LocalFileSystemExtensions.cs @@ -3,7 +3,6 @@ using LibHac.FsSystem; using LibHac.Loader; using LibHac.Ncm; using LibHac.Ns; -using Ryujinx.HLE.HOS; using Ryujinx.HLE.Loaders.Processes.Extensions; namespace Ryujinx.HLE.Loaders.Processes @@ -16,17 +15,14 @@ namespace Ryujinx.HLE.Loaders.Processes var nacpData = new BlitStruct(1); ulong programId = metaLoader.GetProgramId(); - device.Configuration.VirtualFileSystem.ModLoader.CollectMods( - new[] { programId }, - ModLoader.GetModsBasePath(), - ModLoader.GetSdModsBasePath()); + device.Configuration.VirtualFileSystem.ModLoader.CollectMods([programId]); if (programId != 0) { ProcessLoaderHelper.EnsureSaveData(device, new ApplicationId(programId), nacpData); } - ProcessResult processResult = exeFs.Load(device, nacpData, metaLoader); + ProcessResult processResult = exeFs.Load(device, nacpData, metaLoader, 0); // Load RomFS. if (!string.IsNullOrEmpty(romFsPath)) diff --git a/src/Ryujinx.HLE/Loaders/Processes/Extensions/NcaExtensions.cs b/src/Ryujinx.HLE/Loaders/Processes/Extensions/NcaExtensions.cs index e369f4b04..2928ac7fe 100644 --- a/src/Ryujinx.HLE/Loaders/Processes/Extensions/NcaExtensions.cs +++ b/src/Ryujinx.HLE/Loaders/Processes/Extensions/NcaExtensions.cs @@ -7,16 +7,25 @@ using LibHac.Ncm; using LibHac.Ns; using LibHac.Tools.FsSystem; using LibHac.Tools.FsSystem.NcaUtils; +using LibHac.Tools.Ncm; +using Ryujinx.Common.Configuration; using Ryujinx.Common.Logging; +using Ryujinx.Common.Utilities; +using Ryujinx.HLE.FileSystem; using Ryujinx.HLE.HOS; +using Ryujinx.HLE.Utilities; using System.IO; using System.Linq; using ApplicationId = LibHac.Ncm.ApplicationId; +using ContentType = LibHac.Ncm.ContentType; +using Path = System.IO.Path; namespace Ryujinx.HLE.Loaders.Processes.Extensions { - static class NcaExtensions + public static class NcaExtensions { + private static readonly TitleUpdateMetadataJsonSerializerContext _applicationSerializerContext = new(JsonHelper.GetDefaultSerializerOptions()); + public static ProcessResult Load(this Nca nca, Switch device, Nca patchNca, Nca controlNca) { // Extract RomFs and ExeFs from NCA. @@ -47,7 +56,7 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions nacpData = controlNca.GetNacp(device); } - /* TODO: Rework this since it's wrong and doesn't work as it takes the DisplayVersion from a "potential" inexistant update. + /* TODO: Rework this since it's wrong and doesn't work as it takes the DisplayVersion from a "potential" non-existent update. // Load program 0 control NCA as we are going to need it for display version. (_, Nca updateProgram0ControlNca) = GetGameUpdateData(_device.Configuration.VirtualFileSystem, mainNca.Header.TitleId.ToString("x16"), 0, out _); @@ -61,7 +70,7 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions */ - ProcessResult processResult = exeFs.Load(device, nacpData, metaLoader); + ProcessResult processResult = exeFs.Load(device, nacpData, metaLoader, (byte)nca.GetProgramIndex()); // Load RomFS. if (romFs == null) @@ -86,6 +95,11 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions return processResult; } + public static ulong GetProgramIdBase(this Nca nca) + { + return nca.Header.TitleId & ~0x1FFFUL; + } + public static int GetProgramIndex(this Nca nca) { return (int)(nca.Header.TitleId & 0xF); @@ -96,6 +110,11 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions return nca.Header.ContentType == NcaContentType.Program; } + public static bool IsMain(this Nca nca) + { + return nca.IsProgram() && !nca.IsPatch(); + } + public static bool IsPatch(this Nca nca) { int dataIndex = Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program); @@ -108,6 +127,43 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions return nca.Header.ContentType == NcaContentType.Control; } + public static (Nca, Nca) GetUpdateData(this Nca mainNca, VirtualFileSystem fileSystem, IntegrityCheckLevel checkLevel, int programIndex, out string updatePath) + { + updatePath = null; + + // Load Update NCAs. + Nca updatePatchNca = null; + Nca updateControlNca = null; + + // Clear the program index part. + ulong titleIdBase = mainNca.GetProgramIdBase(); + + // Load update information if exists. + string titleUpdateMetadataPath = Path.Combine(AppDataManager.GamesDirPath, titleIdBase.ToString("x16"), "updates.json"); + if (File.Exists(titleUpdateMetadataPath)) + { + updatePath = JsonHelper.DeserializeFromFile(titleUpdateMetadataPath, _applicationSerializerContext.TitleUpdateMetadata).Selected; + if (File.Exists(updatePath)) + { + IFileSystem updatePartitionFileSystem = PartitionFileSystemUtils.OpenApplicationFileSystem(updatePath, fileSystem); + + foreach ((ulong applicationTitleId, ContentMetaData content) in updatePartitionFileSystem.GetContentData(ContentMetaType.Patch, fileSystem, checkLevel)) + { + if ((applicationTitleId & ~0x1FFFUL) != titleIdBase) + { + continue; + } + + updatePatchNca = content.GetNcaByType(fileSystem.KeySet, ContentType.Program, programIndex); + updateControlNca = content.GetNcaByType(fileSystem.KeySet, ContentType.Control, programIndex); + break; + } + } + } + + return (updatePatchNca, updateControlNca); + } + public static IFileSystem GetExeFs(this Nca nca, Switch device, Nca patchNca = null) { IFileSystem exeFs = null; @@ -172,5 +228,31 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions return nacpData; } + + public static Cnmt GetCnmt(this Nca cnmtNca, IntegrityCheckLevel checkLevel, ContentMetaType metaType) + { + string path = $"/{metaType}_{cnmtNca.Header.TitleId:x16}.cnmt"; + using var cnmtFile = new UniqueRef(); + + try + { + Result result = cnmtNca.OpenFileSystem(0, checkLevel) + .OpenFile(ref cnmtFile.Ref, path.ToU8Span(), OpenMode.Read); + + if (result.IsSuccess()) + { + return new Cnmt(cnmtFile.Release().AsStream()); + } + } + catch (HorizonResultException ex) + { + if (!ResultFs.PathNotFound.Includes(ex.ResultValue)) + { + Logger.Warning?.Print(LogClass.Application, $"Failed get CNMT for '{cnmtNca.Header.TitleId:x16}' from NCA: {ex.Message}"); + } + } + + return null; + } } } diff --git a/src/Ryujinx.HLE/Loaders/Processes/Extensions/PartitionFileSystemExtensions.cs b/src/Ryujinx.HLE/Loaders/Processes/Extensions/PartitionFileSystemExtensions.cs index 87141ab85..b3590d9bd 100644 --- a/src/Ryujinx.HLE/Loaders/Processes/Extensions/PartitionFileSystemExtensions.cs +++ b/src/Ryujinx.HLE/Loaders/Processes/Extensions/PartitionFileSystemExtensions.cs @@ -1,26 +1,58 @@ using LibHac.Common; +using LibHac.Common.Keys; using LibHac.Fs; using LibHac.Fs.Fsa; using LibHac.FsSystem; +using LibHac.Ncm; using LibHac.Tools.Fs; using LibHac.Tools.FsSystem; using LibHac.Tools.FsSystem.NcaUtils; +using LibHac.Tools.Ncm; using Ryujinx.Common.Configuration; using Ryujinx.Common.Logging; using Ryujinx.Common.Utilities; +using Ryujinx.HLE.FileSystem; using System; using System.Collections.Generic; -using System.Globalization; using System.IO; +using ContentType = LibHac.Ncm.ContentType; namespace Ryujinx.HLE.Loaders.Processes.Extensions { public static class PartitionFileSystemExtensions { private static readonly DownloadableContentJsonSerializerContext _contentSerializerContext = new(JsonHelper.GetDefaultSerializerOptions()); - private static readonly TitleUpdateMetadataJsonSerializerContext _titleSerializerContext = new(JsonHelper.GetDefaultSerializerOptions()); - internal static (bool, ProcessResult) TryLoad(this PartitionFileSystemCore partitionFileSystem, Switch device, string path, out string errorMessage) + public static Dictionary GetContentData(this IFileSystem partitionFileSystem, + ContentMetaType contentType, VirtualFileSystem fileSystem, IntegrityCheckLevel checkLevel) + { + fileSystem.ImportTickets(partitionFileSystem); + + var programs = new Dictionary(); + + foreach (DirectoryEntryEx fileEntry in partitionFileSystem.EnumerateEntries("/", "*.cnmt.nca")) + { + Cnmt cnmt = partitionFileSystem.GetNca(fileSystem.KeySet, fileEntry.FullPath).GetCnmt(checkLevel, contentType); + + if (cnmt == null) + { + continue; + } + + ContentMetaData content = new(partitionFileSystem, cnmt); + + if (content.Type != contentType) + { + continue; + } + + programs.TryAdd(content.ApplicationId, content); + } + + return programs; + } + + internal static (bool, ProcessResult) TryLoad(this PartitionFileSystemCore partitionFileSystem, Switch device, string path, ulong applicationId, out string errorMessage) where TMetaData : PartitionFileSystemMetaCore, new() where TFormat : IPartitionFileSystemFormat where THeader : unmanaged, IPartitionFileSystemHeader @@ -35,31 +67,22 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions try { - device.Configuration.VirtualFileSystem.ImportTickets(partitionFileSystem); + Dictionary applications = partitionFileSystem.GetContentData(ContentMetaType.Application, device.FileSystem, device.System.FsIntegrityCheckLevel); - // TODO: To support multi-games container, this should use CNMT NCA instead. - foreach (DirectoryEntryEx fileEntry in partitionFileSystem.EnumerateEntries("/", "*.nca")) + if (applicationId == 0) { - Nca nca = partitionFileSystem.GetNca(device, fileEntry.FullPath); - - if (nca.GetProgramIndex() != device.Configuration.UserChannelPersistence.Index) + foreach ((ulong _, ContentMetaData content) in applications) { - continue; - } - - if (nca.IsPatch()) - { - patchNca = nca; - } - else if (nca.IsProgram()) - { - mainNca = nca; - } - else if (nca.IsControl()) - { - controlNca = nca; + mainNca = content.GetNcaByType(device.FileSystem.KeySet, ContentType.Program, device.Configuration.UserChannelPersistence.Index); + controlNca = content.GetNcaByType(device.FileSystem.KeySet, ContentType.Control, device.Configuration.UserChannelPersistence.Index); + break; } } + else if (applications.TryGetValue(applicationId, out ContentMetaData content)) + { + mainNca = content.GetNcaByType(device.FileSystem.KeySet, ContentType.Program, device.Configuration.UserChannelPersistence.Index); + controlNca = content.GetNcaByType(device.FileSystem.KeySet, ContentType.Control, device.Configuration.UserChannelPersistence.Index); + } ProcessLoaderHelper.RegisterProgramMapInfo(device, partitionFileSystem).ThrowIfFailure(); } @@ -79,54 +102,7 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions return (false, ProcessResult.Failed); } - // Load Update NCAs. - Nca updatePatchNca = null; - Nca updateControlNca = null; - - if (ulong.TryParse(mainNca.Header.TitleId.ToString("x16"), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out ulong titleIdBase)) - { - // Clear the program index part. - titleIdBase &= ~0xFUL; - - // Load update information if exists. - string titleUpdateMetadataPath = System.IO.Path.Combine(AppDataManager.GamesDirPath, titleIdBase.ToString("x16"), "updates.json"); - if (File.Exists(titleUpdateMetadataPath)) - { - string updatePath = PlatformRelative(JsonHelper.DeserializeFromFile(titleUpdateMetadataPath, _titleSerializerContext.TitleUpdateMetadata).Selected); - if (File.Exists(updatePath)) - { - PartitionFileSystem updatePartitionFileSystem = new(); - updatePartitionFileSystem.Initialize(new FileStream(updatePath, FileMode.Open, FileAccess.Read).AsStorage()).ThrowIfFailure(); - - device.Configuration.VirtualFileSystem.ImportTickets(updatePartitionFileSystem); - - // TODO: This should use CNMT NCA instead. - foreach (DirectoryEntryEx fileEntry in updatePartitionFileSystem.EnumerateEntries("/", "*.nca")) - { - Nca nca = updatePartitionFileSystem.GetNca(device, fileEntry.FullPath); - - if (nca.GetProgramIndex() != device.Configuration.UserChannelPersistence.Index) - { - continue; - } - - if ($"{nca.Header.TitleId.ToString("x16")[..^3]}000" != titleIdBase.ToString("x16")) - { - break; - } - - if (nca.IsProgram()) - { - updatePatchNca = nca; - } - else if (nca.IsControl()) - { - updateControlNca = nca; - } - } - } - } - } + (Nca updatePatchNca, Nca updateControlNca) = mainNca.GetUpdateData(device.FileSystem, device.System.FsIntegrityCheckLevel, device.Configuration.UserChannelPersistence.Index, out string _); if (updatePatchNca != null) { @@ -138,13 +114,11 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions controlNca = updateControlNca; } - // Load contained DownloadableContents. // TODO: If we want to support multi-processes in future, we shouldn't clear AddOnContent data here. device.Configuration.ContentManager.ClearAocData(); - device.Configuration.ContentManager.AddAocData(partitionFileSystem, path, mainNca.Header.TitleId, device.Configuration.FsIntegrityCheckLevel); // Load DownloadableContents. - string addOnContentMetadataPath = System.IO.Path.Combine(AppDataManager.GamesDirPath, mainNca.Header.TitleId.ToString("x16"), "dlc.json"); + string addOnContentMetadataPath = System.IO.Path.Combine(AppDataManager.GamesDirPath, mainNca.GetProgramIdBase().ToString("x16"), "dlc.json"); if (File.Exists(addOnContentMetadataPath)) { List dlcContainerList = JsonHelper.DeserializeFromFile(addOnContentMetadataPath, _contentSerializerContext.ListDownloadableContentContainer); @@ -153,15 +127,16 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions { foreach (DownloadableContentNca downloadableContentNca in downloadableContentContainer.DownloadableContentNcaList) { - string dlcPath = PlatformRelative(downloadableContentContainer.ContainerPath); - - if (File.Exists(dlcPath) && downloadableContentNca.Enabled) + if (File.Exists(downloadableContentContainer.ContainerPath)) { - device.Configuration.ContentManager.AddAocItem(downloadableContentNca.TitleId, dlcPath, downloadableContentNca.FullPath); + if (downloadableContentNca.Enabled) + { + device.Configuration.ContentManager.AddAocItem(downloadableContentNca.TitleId, downloadableContentContainer.ContainerPath, downloadableContentNca.FullPath); + } } else { - Logger.Warning?.Print(LogClass.Application, $"Cannot find AddOnContent file {dlcPath}. It may have been moved or renamed."); + Logger.Warning?.Print(LogClass.Application, $"Cannot find AddOnContent file {downloadableContentContainer.ContainerPath}. It may have been moved or renamed."); } } } @@ -170,28 +145,18 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions return (true, mainNca.Load(device, patchNca, controlNca)); } - errorMessage = "Unable to load: Could not find Main NCA"; + errorMessage = $"Unable to load: Could not find Main NCA for title \"{applicationId:X16}\""; return (false, ProcessResult.Failed); } - private static string PlatformRelative(string path) - { - if (OperatingSystem.IsIOS() && !File.Exists(path)) - { - path = System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), path); - } - - return path; - } - - public static Nca GetNca(this IFileSystem fileSystem, Switch device, string path) + public static Nca GetNca(this IFileSystem fileSystem, KeySet keySet, string path) { using var ncaFile = new UniqueRef(); fileSystem.OpenFile(ref ncaFile.Ref, path.ToU8Span(), OpenMode.Read).ThrowIfFailure(); - return new Nca(device.Configuration.VirtualFileSystem.KeySet, ncaFile.Release().AsStorage()); + return new Nca(keySet, ncaFile.Release().AsStorage()); } } } diff --git a/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs b/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs index efeb9f613..a0e7e0fa1 100644 --- a/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs +++ b/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs @@ -32,7 +32,7 @@ namespace Ryujinx.HLE.Loaders.Processes _processesByPid = new ConcurrentDictionary(); } - public bool LoadXci(string path) + public bool LoadXci(string path, ulong applicationId) { FileStream stream = new(path, FileMode.Open, FileAccess.Read); Xci xci = new(_device.Configuration.VirtualFileSystem.KeySet, stream.AsStorage()); @@ -44,7 +44,7 @@ namespace Ryujinx.HLE.Loaders.Processes return false; } - (bool success, ProcessResult processResult) = xci.OpenPartition(XciPartitionType.Secure).TryLoad(_device, path, out string errorMessage); + (bool success, ProcessResult processResult) = xci.OpenPartition(XciPartitionType.Secure).TryLoad(_device, path, applicationId, out string errorMessage); if (!success) { @@ -66,18 +66,18 @@ namespace Ryujinx.HLE.Loaders.Processes return false; } - public bool LoadNsp(string path) + public bool LoadNsp(string path, ulong applicationId) { FileStream file = new(path, FileMode.Open, FileAccess.Read); PartitionFileSystem partitionFileSystem = new(); partitionFileSystem.Initialize(file.AsStorage()).ThrowIfFailure(); - (bool success, ProcessResult processResult) = partitionFileSystem.TryLoad(_device, path, out string errorMessage); + (bool success, ProcessResult processResult) = partitionFileSystem.TryLoad(_device, path, applicationId, out string errorMessage); if (processResult.ProcessId == 0) { // This is not a normal NSP, it's actually a ExeFS as a NSP - processResult = partitionFileSystem.Load(_device, new BlitStruct(1), partitionFileSystem.GetNpdm(), true); + processResult = partitionFileSystem.Load(_device, new BlitStruct(1), partitionFileSystem.GetNpdm(), 0, true); } if (processResult.ProcessId != 0 && _processesByPid.TryAdd(processResult.ProcessId, processResult)) @@ -145,7 +145,7 @@ namespace Ryujinx.HLE.Loaders.Processes IFileSystem dummyExeFs = null; Stream romfsStream = null; - string programName = ""; + string programName = string.Empty; ulong programId = 0000000000000000; // Load executable. @@ -198,7 +198,7 @@ namespace Ryujinx.HLE.Loaders.Processes } else { - programName = System.IO.Path.GetFileNameWithoutExtension(path); + programName = Path.GetFileNameWithoutExtension(path); executable = new NsoExecutable(new LocalStorage(path, FileAccess.Read), programName); } @@ -215,6 +215,7 @@ namespace Ryujinx.HLE.Loaders.Processes allowCodeMemoryForJit: true, programName, programId, + 0, null, executable); diff --git a/src/Ryujinx.HLE/Loaders/Processes/ProcessLoaderHelper.cs b/src/Ryujinx.HLE/Loaders/Processes/ProcessLoaderHelper.cs index a6a1d87e0..33aee1c4c 100644 --- a/src/Ryujinx.HLE/Loaders/Processes/ProcessLoaderHelper.cs +++ b/src/Ryujinx.HLE/Loaders/Processes/ProcessLoaderHelper.cs @@ -19,6 +19,7 @@ using Ryujinx.HLE.HOS.Kernel.Process; using Ryujinx.HLE.Loaders.Executables; using Ryujinx.HLE.Loaders.Processes.Extensions; using Ryujinx.Horizon.Common; +using Ryujinx.Horizon.Sdk.Arp; using System; using System.Linq; using System.Runtime.InteropServices; @@ -42,15 +43,14 @@ namespace Ryujinx.HLE.Loaders.Processes foreach (DirectoryEntryEx fileEntry in partitionFileSystem.EnumerateEntries("/", "*.nca")) { - Nca nca = partitionFileSystem.GetNca(device, fileEntry.FullPath); + Nca nca = partitionFileSystem.GetNca(device.FileSystem.KeySet, fileEntry.FullPath); - if (!nca.IsProgram() && nca.IsPatch()) + if (!nca.IsProgram()) { continue; } - ulong currentProgramId = nca.Header.TitleId; - ulong currentMainProgramId = currentProgramId & ~0xFFFul; + ulong currentMainProgramId = nca.GetProgramIdBase(); if (applicationId == 0 && currentMainProgramId != 0) { @@ -67,7 +67,7 @@ namespace Ryujinx.HLE.Loaders.Processes break; } - hasIndex[(int)(currentProgramId & 0xF)] = true; + hasIndex[nca.GetProgramIndex()] = true; } if (programCount == 0) @@ -229,6 +229,7 @@ namespace Ryujinx.HLE.Loaders.Processes bool allowCodeMemoryForJit, string name, ulong programId, + byte programIndex, byte[] arguments = null, params IExecutable[] executables) { @@ -254,7 +255,7 @@ namespace Ryujinx.HLE.Loaders.Processes { NsoExecutable nso => Convert.ToHexString(nso.BuildId.ItemsRo.ToArray()), NroExecutable nro => Convert.ToHexString(nro.Header.BuildId), - _ => "", + _ => string.Empty }).ToUpper()); ulong[] nsoBase = new ulong[executables.Length]; @@ -421,7 +422,7 @@ namespace Ryujinx.HLE.Loaders.Processes // Once everything is loaded, we can load cheats. device.Configuration.VirtualFileSystem.ModLoader.LoadCheats(programId, tamperInfo, device.TamperMachine); - return new ProcessResult( + ProcessResult processResult = new( metaLoader, applicationControlProperties, diskCacheEnabled, @@ -431,6 +432,25 @@ namespace Ryujinx.HLE.Loaders.Processes meta.MainThreadPriority, meta.MainThreadStackSize, device.System.State.DesiredTitleLanguage); + + // Register everything in arp service. + device.System.ServiceTable.ArpWriter.AcquireRegistrar(out IRegistrar registrar); + registrar.SetApplicationControlProperty(MemoryMarshal.Cast(applicationControlProperties.ByteSpan)[0]); + // TODO: Handle Version and StorageId when it will be needed. + registrar.SetApplicationLaunchProperty(new ApplicationLaunchProperty() + { + ApplicationId = new Horizon.Sdk.Ncm.ApplicationId(programId), + Version = 0x00, + Storage = Horizon.Sdk.Ncm.StorageId.BuiltInUser, + PatchStorage = Horizon.Sdk.Ncm.StorageId.None, + ApplicationKind = ApplicationKind.Application, + }); + + device.System.ServiceTable.ArpReader.GetApplicationInstanceId(out ulong applicationInstanceId, process.Pid); + device.System.ServiceTable.ArpWriter.AcquireApplicationProcessPropertyUpdater(out IUpdater updater, applicationInstanceId); + updater.SetApplicationProcessProperty(process.Pid, new ApplicationProcessProperty() { ProgramIndex = programIndex }); + + return processResult; } public static Result LoadIntoMemory(KProcess process, IExecutable image, ulong baseAddress) diff --git a/src/Ryujinx.HLE/Loaders/Processes/ProcessResult.cs b/src/Ryujinx.HLE/Loaders/Processes/ProcessResult.cs index 1804d045c..e187b2360 100644 --- a/src/Ryujinx.HLE/Loaders/Processes/ProcessResult.cs +++ b/src/Ryujinx.HLE/Loaders/Processes/ProcessResult.cs @@ -85,7 +85,9 @@ namespace Ryujinx.HLE.Loaders.Processes } // TODO: LibHac npdm currently doesn't support version field. - string version = ProgramId > 0x0100000000007FFF ? DisplayVersion : device.System.ContentManager.GetCurrentFirmwareVersion()?.VersionString ?? "?"; + string version = ProgramId > 0x0100000000007FFF + ? DisplayVersion + : device.System.ContentManager.GetCurrentFirmwareVersion()?.VersionString ?? "?"; Logger.Info?.Print(LogClass.Loader, $"Application Loaded: {Name} v{version} [{ProgramIdText}] [{(Is64Bit ? "64-bit" : "32-bit")}]"); diff --git a/src/Ryujinx.HLE/MemoryConfiguration.cs b/src/Ryujinx.HLE/MemoryConfiguration.cs index 45e8927db..21ecd737f 100644 --- a/src/Ryujinx.HLE/MemoryConfiguration.cs +++ b/src/Ryujinx.HLE/MemoryConfiguration.cs @@ -6,11 +6,12 @@ namespace Ryujinx.HLE public enum MemoryConfiguration { MemoryConfiguration4GiB = 0, - MemoryConfiguration4GiBAppletDev = 1, - MemoryConfiguration4GiBSystemDev = 2, - MemoryConfiguration6GiB = 3, - MemoryConfiguration6GiBAppletDev = 4, - MemoryConfiguration8GiB = 5, + MemoryConfiguration6GiB = 1, + MemoryConfiguration8GiB = 2, + MemoryConfiguration12GiB = 3, + MemoryConfiguration4GiBAppletDev = 4, + MemoryConfiguration4GiBSystemDev = 5, + MemoryConfiguration6GiBAppletDev = 6, } static class MemoryConfigurationExtensions @@ -28,6 +29,7 @@ namespace Ryujinx.HLE MemoryConfiguration.MemoryConfiguration6GiB => MemoryArrange.MemoryArrange6GiB, MemoryConfiguration.MemoryConfiguration6GiBAppletDev => MemoryArrange.MemoryArrange6GiBAppletDev, MemoryConfiguration.MemoryConfiguration8GiB => MemoryArrange.MemoryArrange8GiB, + MemoryConfiguration.MemoryConfiguration12GiB => MemoryArrange.MemoryArrange12GiB, _ => throw new AggregateException($"Invalid memory configuration \"{configuration}\"."), }; } @@ -42,6 +44,7 @@ namespace Ryujinx.HLE MemoryConfiguration.MemoryConfiguration6GiB or MemoryConfiguration.MemoryConfiguration6GiBAppletDev => MemorySize.MemorySize6GiB, MemoryConfiguration.MemoryConfiguration8GiB => MemorySize.MemorySize8GiB, + MemoryConfiguration.MemoryConfiguration12GiB => MemorySize.MemorySize12GiB, _ => throw new AggregateException($"Invalid memory configuration \"{configuration}\"."), }; } @@ -56,6 +59,7 @@ namespace Ryujinx.HLE MemoryConfiguration.MemoryConfiguration6GiB or MemoryConfiguration.MemoryConfiguration6GiBAppletDev => 6 * GiB, MemoryConfiguration.MemoryConfiguration8GiB => 8 * GiB, + MemoryConfiguration.MemoryConfiguration12GiB => 12 * GiB, _ => throw new AggregateException($"Invalid memory configuration \"{configuration}\"."), }; } diff --git a/src/Ryujinx.HLE/Ryujinx.HLE.csproj b/src/Ryujinx.HLE/Ryujinx.HLE.csproj index 59892c214..5f7f6db69 100644 --- a/src/Ryujinx.HLE/Ryujinx.HLE.csproj +++ b/src/Ryujinx.HLE/Ryujinx.HLE.csproj @@ -1,7 +1,8 @@ - + net8.0 + true @@ -11,11 +12,8 @@ - + - @@ -25,20 +23,15 @@ - + - - - + + + - - - NU1605 - - diff --git a/src/Ryujinx.HLE/Switch.cs b/src/Ryujinx.HLE/Switch.cs index 0c7112c0a..466352152 100644 --- a/src/Ryujinx.HLE/Switch.cs +++ b/src/Ryujinx.HLE/Switch.cs @@ -1,5 +1,4 @@ using Ryujinx.Audio.Backends.CompatLayer; -using Ryujinx.Audio.Backends.DelayLayer; using Ryujinx.Audio.Integration; using Ryujinx.Common.Configuration; using Ryujinx.Graphics.Gpu; @@ -8,7 +7,7 @@ using Ryujinx.HLE.HOS; using Ryujinx.HLE.HOS.Services.Apm; using Ryujinx.HLE.HOS.Services.Hid; using Ryujinx.HLE.Loaders.Processes; -using Ryujinx.HLE.Ui; +using Ryujinx.HLE.UI; using Ryujinx.Memory; using System; @@ -26,9 +25,13 @@ namespace Ryujinx.HLE public PerformanceStatistics Statistics { get; } public Hid Hid { get; } public TamperMachine TamperMachine { get; } - public IHostUiHandler UiHandler { get; } + public IHostUIHandler UIHandler { get; } - public bool EnableDeviceVsync { get; set; } = true; + public VSyncMode VSyncMode { get; set; } = VSyncMode.Switch; + public bool CustomVSyncIntervalEnabled { get; set; } = false; + public int CustomVSyncInterval { get; set; } + + public long TargetVSyncInterval { get; set; } = 60; public bool IsFrameAvailable => Gpu.Window.IsFrameAvailable; @@ -40,14 +43,14 @@ namespace Ryujinx.HLE Configuration = configuration; FileSystem = Configuration.VirtualFileSystem; - UiHandler = Configuration.HostUiHandler; + UIHandler = Configuration.HostUIHandler; MemoryAllocationFlags memoryAllocationFlags = configuration.MemoryManagerMode == MemoryManagerMode.SoftwarePageTable ? MemoryAllocationFlags.Reserve : MemoryAllocationFlags.Reserve | MemoryAllocationFlags.Mirrorable; #pragma warning disable IDE0055 // Disable formatting - AudioDeviceDriver = AddAudioCompatLayers(Configuration.AudioDeviceDriver); + AudioDeviceDriver = new CompatLayerHardwareDeviceDriver(Configuration.AudioDeviceDriver); Memory = new MemoryBlock(Configuration.MemoryConfiguration.ToDramSize(), memoryAllocationFlags); Gpu = new GpuContext(Configuration.GpuRenderer); System = new HOS.Horizon(this); @@ -56,61 +59,21 @@ namespace Ryujinx.HLE Processes = new ProcessLoader(this); TamperMachine = new TamperMachine(); + System.InitializeServices(); System.State.SetLanguage(Configuration.SystemLanguage); System.State.SetRegion(Configuration.Region); - EnableDeviceVsync = Configuration.EnableVsync; + VSyncMode = Configuration.VSyncMode; + CustomVSyncInterval = Configuration.CustomVSyncInterval; System.State.DockedMode = Configuration.EnableDockedMode; System.PerformanceState.PerformanceMode = System.State.DockedMode ? PerformanceMode.Boost : PerformanceMode.Default; System.EnablePtc = Configuration.EnablePtc; System.FsIntegrityCheckLevel = Configuration.FsIntegrityCheckLevel; System.GlobalAccessLogMode = Configuration.FsGlobalAccessLogMode; + UpdateVSyncInterval(); #pragma warning restore IDE0055 } - private IHardwareDeviceDriver AddAudioCompatLayers(IHardwareDeviceDriver driver) - { - ulong sampleDelay = OperatingSystem.IsIOS() ? 1024ul : 0; - driver = new CompatLayerHardwareDeviceDriver(driver); - - if (sampleDelay > 0) - { - driver = new DelayLayerHardwareDeviceDriver(driver, sampleDelay); - } - - return driver; - } - - public bool LoadCart(string exeFsDir, string romFsFile = null) - { - return Processes.LoadUnpackedNca(exeFsDir, romFsFile); - } - - public bool LoadXci(string xciFile) - { - return Processes.LoadXci(xciFile); - } - - public bool LoadNca(string ncaFile) - { - return Processes.LoadNca(ncaFile); - } - - public bool LoadNsp(string nspFile) - { - return Processes.LoadNsp(nspFile); - } - - public bool LoadProgram(string fileName) - { - return Processes.LoadNxo(fileName); - } - - public bool WaitFifo() - { - return Gpu.GPFifo.WaitForCommands(); - } - public void ProcessFrame() { Gpu.ProcessShaderCacheQueue(); @@ -118,40 +81,50 @@ namespace Ryujinx.HLE Gpu.GPFifo.DispatchCalls(); } - public bool ConsumeFrameAvailable() + public void IncrementCustomVSyncInterval() { - return Gpu.Window.ConsumeFrameAvailable(); + CustomVSyncInterval += 1; + UpdateVSyncInterval(); } - public void PresentFrame(Action swapBuffersCallback) + public void DecrementCustomVSyncInterval() { - Gpu.Window.Present(swapBuffersCallback); + CustomVSyncInterval -= 1; + UpdateVSyncInterval(); } - public void SetVolume(float volume) + public void UpdateVSyncInterval() { - System.SetVolume(Math.Clamp(volume, 0, 1)); + switch (VSyncMode) + { + case VSyncMode.Custom: + TargetVSyncInterval = CustomVSyncInterval; + break; + case VSyncMode.Switch: + TargetVSyncInterval = 60; + break; + case VSyncMode.Unbounded: + TargetVSyncInterval = 1; + break; + } } - public float GetVolume() - { - return System.GetVolume(); - } + public bool LoadCart(string exeFsDir, string romFsFile = null) => Processes.LoadUnpackedNca(exeFsDir, romFsFile); + public bool LoadXci(string xciFile, ulong applicationId = 0) => Processes.LoadXci(xciFile, applicationId); + public bool LoadNca(string ncaFile) => Processes.LoadNca(ncaFile); + public bool LoadNsp(string nspFile, ulong applicationId = 0) => Processes.LoadNsp(nspFile, applicationId); + public bool LoadProgram(string fileName) => Processes.LoadNxo(fileName); - public void EnableCheats() - { - ModLoader.EnableCheats(Processes.ActiveApplication.ProgramId, TamperMachine); - } + public void SetVolume(float volume) => AudioDeviceDriver.Volume = Math.Clamp(volume, 0f, 1f); + public float GetVolume() => AudioDeviceDriver.Volume; + public bool IsAudioMuted() => AudioDeviceDriver.Volume == 0; - public bool IsAudioMuted() - { - return System.GetVolume() == 0; - } + public void EnableCheats() => ModLoader.EnableCheats(Processes.ActiveApplication.ProgramId, TamperMachine); - public void DisposeGpu() - { - Gpu.Dispose(); - } + public bool WaitFifo() => Gpu.GPFifo.WaitForCommands(); + public bool ConsumeFrameAvailable() => Gpu.Window.ConsumeFrameAvailable(); + public void PresentFrame(Action swapBuffersCallback) => Gpu.Window.Present(swapBuffersCallback); + public void DisposeGpu() => Gpu.Dispose(); public void Dispose() { diff --git a/src/Ryujinx.HLE/Ui/DynamicTextChangedHandler.cs b/src/Ryujinx.HLE/Ui/DynamicTextChangedHandler.cs index cb9ca0dec..c0945259b 100644 --- a/src/Ryujinx.HLE/Ui/DynamicTextChangedHandler.cs +++ b/src/Ryujinx.HLE/Ui/DynamicTextChangedHandler.cs @@ -1,4 +1,4 @@ -namespace Ryujinx.HLE.Ui +namespace Ryujinx.HLE.UI { public delegate void DynamicTextChangedHandler(string text, int cursorBegin, int cursorEnd, bool overwriteMode); } diff --git a/src/Ryujinx.HLE/Ui/IDynamicTextInputHandler.cs b/src/Ryujinx.HLE/Ui/IDynamicTextInputHandler.cs index e530d2c4e..1ff451d10 100644 --- a/src/Ryujinx.HLE/Ui/IDynamicTextInputHandler.cs +++ b/src/Ryujinx.HLE/Ui/IDynamicTextInputHandler.cs @@ -1,6 +1,6 @@ using System; -namespace Ryujinx.HLE.Ui +namespace Ryujinx.HLE.UI { public interface IDynamicTextInputHandler : IDisposable { diff --git a/src/Ryujinx.HLE/Ui/IHostUiHandler.cs b/src/Ryujinx.HLE/Ui/IHostUiHandler.cs index 68f78f22d..8debfcca0 100644 --- a/src/Ryujinx.HLE/Ui/IHostUiHandler.cs +++ b/src/Ryujinx.HLE/Ui/IHostUiHandler.cs @@ -1,16 +1,16 @@ using Ryujinx.HLE.HOS.Applets; using Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.ApplicationProxy.Types; -namespace Ryujinx.HLE.Ui +namespace Ryujinx.HLE.UI { - public interface IHostUiHandler + public interface IHostUIHandler { /// /// Displays an Input Dialog box to the user and blocks until text is entered. /// /// Text that the user entered. Set to `null` on internal errors /// True when OK is pressed, False otherwise. Also returns True on internal errors - bool DisplayInputDialog(SoftwareKeyboardUiArgs args, out string userText); + bool DisplayInputDialog(SoftwareKeyboardUIArgs args, out string userText); /// /// Displays a Message Dialog box to the user and blocks until it is closed. @@ -22,10 +22,10 @@ namespace Ryujinx.HLE.Ui /// Displays a Message Dialog box specific to Controller Applet and blocks until it is closed. /// /// True when OK is pressed, False otherwise. - bool DisplayMessageDialog(ControllerAppletUiArgs args); + bool DisplayMessageDialog(ControllerAppletUIArgs args); /// - /// Tell the UI that we need to transisition to another program. + /// Tell the UI that we need to transition to another program. /// /// The device instance. /// The program kind. @@ -46,6 +46,6 @@ namespace Ryujinx.HLE.Ui /// /// Gets fonts and colors used by the host. /// - IHostUiTheme HostUiTheme { get; } + IHostUITheme HostUITheme { get; } } } diff --git a/src/Ryujinx.HLE/Ui/IHostUiTheme.cs b/src/Ryujinx.HLE/Ui/IHostUiTheme.cs index 11d82361a..3b0544004 100644 --- a/src/Ryujinx.HLE/Ui/IHostUiTheme.cs +++ b/src/Ryujinx.HLE/Ui/IHostUiTheme.cs @@ -1,6 +1,6 @@ -namespace Ryujinx.HLE.Ui +namespace Ryujinx.HLE.UI { - public interface IHostUiTheme + public interface IHostUITheme { string FontFamily { get; } diff --git a/src/Ryujinx.HLE/Ui/Input/NpadButtonHandler.cs b/src/Ryujinx.HLE/Ui/Input/NpadButtonHandler.cs index 2d1c1c491..73c306614 100644 --- a/src/Ryujinx.HLE/Ui/Input/NpadButtonHandler.cs +++ b/src/Ryujinx.HLE/Ui/Input/NpadButtonHandler.cs @@ -1,6 +1,6 @@ using Ryujinx.HLE.HOS.Services.Hid.Types.SharedMemory.Npad; -namespace Ryujinx.HLE.Ui.Input +namespace Ryujinx.HLE.UI.Input { delegate void NpadButtonHandler(int npadIndex, NpadButton button); } diff --git a/src/Ryujinx.HLE/Ui/Input/NpadReader.cs b/src/Ryujinx.HLE/Ui/Input/NpadReader.cs index 8fc95dc94..8276d6160 100644 --- a/src/Ryujinx.HLE/Ui/Input/NpadReader.cs +++ b/src/Ryujinx.HLE/Ui/Input/NpadReader.cs @@ -1,7 +1,7 @@ using Ryujinx.HLE.HOS.Services.Hid.Types.SharedMemory.Common; using Ryujinx.HLE.HOS.Services.Hid.Types.SharedMemory.Npad; -namespace Ryujinx.HLE.Ui.Input +namespace Ryujinx.HLE.UI.Input { /// /// Class that converts Hid entries for the Npad into pressed / released events. diff --git a/src/Ryujinx.HLE/Ui/KeyPressedHandler.cs b/src/Ryujinx.HLE/Ui/KeyPressedHandler.cs index 31e754377..6feb11bd8 100644 --- a/src/Ryujinx.HLE/Ui/KeyPressedHandler.cs +++ b/src/Ryujinx.HLE/Ui/KeyPressedHandler.cs @@ -1,6 +1,6 @@ using Ryujinx.Common.Configuration.Hid; -namespace Ryujinx.HLE.Ui +namespace Ryujinx.HLE.UI { public delegate bool KeyPressedHandler(Key key); } diff --git a/src/Ryujinx.HLE/Ui/KeyReleasedHandler.cs b/src/Ryujinx.HLE/Ui/KeyReleasedHandler.cs index d5b6d2019..3de89d0c7 100644 --- a/src/Ryujinx.HLE/Ui/KeyReleasedHandler.cs +++ b/src/Ryujinx.HLE/Ui/KeyReleasedHandler.cs @@ -1,6 +1,6 @@ using Ryujinx.Common.Configuration.Hid; -namespace Ryujinx.HLE.Ui +namespace Ryujinx.HLE.UI { public delegate bool KeyReleasedHandler(Key key); } diff --git a/src/Ryujinx.HLE/Ui/RenderingSurfaceInfo.cs b/src/Ryujinx.HLE/Ui/RenderingSurfaceInfo.cs index 0b3d0a909..af0a0d44e 100644 --- a/src/Ryujinx.HLE/Ui/RenderingSurfaceInfo.cs +++ b/src/Ryujinx.HLE/Ui/RenderingSurfaceInfo.cs @@ -1,7 +1,7 @@ using Ryujinx.HLE.HOS.Services.SurfaceFlinger; using System; -namespace Ryujinx.HLE.Ui +namespace Ryujinx.HLE.UI { /// /// Information about the indirect layer that is being drawn to. diff --git a/src/Ryujinx.HLE/Ui/ThemeColor.cs b/src/Ryujinx.HLE/Ui/ThemeColor.cs index 23657ed2b..c5cfb1474 100644 --- a/src/Ryujinx.HLE/Ui/ThemeColor.cs +++ b/src/Ryujinx.HLE/Ui/ThemeColor.cs @@ -1,4 +1,4 @@ -namespace Ryujinx.HLE.Ui +namespace Ryujinx.HLE.UI { public readonly struct ThemeColor { diff --git a/src/Ryujinx.HLE/Utilities/PartitionFileSystemUtils.cs b/src/Ryujinx.HLE/Utilities/PartitionFileSystemUtils.cs new file mode 100644 index 000000000..3c4ce0850 --- /dev/null +++ b/src/Ryujinx.HLE/Utilities/PartitionFileSystemUtils.cs @@ -0,0 +1,45 @@ +using LibHac; +using LibHac.Fs.Fsa; +using LibHac.FsSystem; +using LibHac.Tools.Fs; +using LibHac.Tools.FsSystem; +using Ryujinx.HLE.FileSystem; +using System.IO; + +namespace Ryujinx.HLE.Utilities +{ + public static class PartitionFileSystemUtils + { + public static IFileSystem OpenApplicationFileSystem(string path, VirtualFileSystem fileSystem, bool throwOnFailure = true) + { + FileStream file = File.OpenRead(path); + + IFileSystem partitionFileSystem; + + if (Path.GetExtension(path).ToLower() == ".xci") + { + partitionFileSystem = new Xci(fileSystem.KeySet, file.AsStorage()).OpenPartition(XciPartitionType.Secure); + } + else + { + var pfsTemp = new PartitionFileSystem(); + Result initResult = pfsTemp.Initialize(file.AsStorage()); + + if (throwOnFailure) + { + initResult.ThrowIfFailure(); + } + else if (initResult.IsFailure()) + { + return null; + } + + partitionFileSystem = pfsTemp; + } + + fileSystem.ImportTickets(partitionFileSystem); + + return partitionFileSystem; + } + } +} diff --git a/src/Ryujinx.Headless.SDL2/HeadlessDynamicTextInputHandler.cs b/src/Ryujinx.Headless.SDL2/HeadlessDynamicTextInputHandler.cs index aae01a0ce..40eb5ba98 100644 --- a/src/Ryujinx.Headless.SDL2/HeadlessDynamicTextInputHandler.cs +++ b/src/Ryujinx.Headless.SDL2/HeadlessDynamicTextInputHandler.cs @@ -1,4 +1,4 @@ -using Ryujinx.HLE.Ui; +using Ryujinx.HLE.UI; using System.Threading; using System.Threading.Tasks; @@ -17,10 +17,7 @@ namespace Ryujinx.Headless.SDL2 public bool TextProcessingEnabled { - get - { - return Volatile.Read(ref _canProcessInput); - } + get => Volatile.Read(ref _canProcessInput); set { diff --git a/src/Ryujinx.Headless.SDL2/HeadlessHostUiTheme.cs b/src/Ryujinx.Headless.SDL2/HeadlessHostUiTheme.cs index a2df6f3ee..78cd43ae5 100644 --- a/src/Ryujinx.Headless.SDL2/HeadlessHostUiTheme.cs +++ b/src/Ryujinx.Headless.SDL2/HeadlessHostUiTheme.cs @@ -1,8 +1,8 @@ -using Ryujinx.HLE.Ui; +using Ryujinx.HLE.UI; namespace Ryujinx.Headless.SDL2 { - internal class HeadlessHostUiTheme : IHostUiTheme + internal class HeadlessHostUiTheme : IHostUITheme { public string FontFamily => "sans-serif"; diff --git a/src/Ryujinx.Headless.SDL2/OpenGL/OpenGLWindow.cs b/src/Ryujinx.Headless.SDL2/OpenGL/OpenGLWindow.cs index 3fb93a0ec..8c4854a11 100644 --- a/src/Ryujinx.Headless.SDL2/OpenGL/OpenGLWindow.cs +++ b/src/Ryujinx.Headless.SDL2/OpenGL/OpenGLWindow.cs @@ -40,7 +40,7 @@ namespace Ryujinx.Headless.SDL2.OpenGL private class OpenToolkitBindingsContext : IBindingsContext { - public IntPtr GetProcAddress(string procName) + public nint GetProcAddress(string procName) { return SDL_GL_GetProcAddress(procName); } @@ -48,11 +48,11 @@ namespace Ryujinx.Headless.SDL2.OpenGL private class SDL2OpenGLContext : IOpenGLContext { - private readonly IntPtr _context; - private readonly IntPtr _window; + private readonly nint _context; + private readonly nint _window; private readonly bool _shouldDisposeWindow; - public SDL2OpenGLContext(IntPtr context, IntPtr window, bool shouldDisposeWindow = true) + public SDL2OpenGLContext(nint context, nint window, bool shouldDisposeWindow = true) { _context = context; _window = window; @@ -65,14 +65,14 @@ namespace Ryujinx.Headless.SDL2.OpenGL // Ensure we share our contexts. SetupOpenGLAttributes(true, GraphicsDebugLevel.None); - IntPtr windowHandle = SDL_CreateWindow("Ryujinx background context window", 0, 0, 1, 1, SDL_WindowFlags.SDL_WINDOW_OPENGL | SDL_WindowFlags.SDL_WINDOW_HIDDEN); - IntPtr context = SDL_GL_CreateContext(windowHandle); + nint windowHandle = SDL_CreateWindow("Ryujinx background context window", 0, 0, 1, 1, SDL_WindowFlags.SDL_WINDOW_OPENGL | SDL_WindowFlags.SDL_WINDOW_HIDDEN); + nint context = SDL_GL_CreateContext(windowHandle); GL.LoadBindings(new OpenToolkitBindingsContext()); CheckResult(SDL_GL_SetAttribute(SDL_GLattr.SDL_GL_SHARE_WITH_CURRENT_CONTEXT, 0)); - CheckResult(SDL_GL_MakeCurrent(windowHandle, IntPtr.Zero)); + CheckResult(SDL_GL_MakeCurrent(windowHandle, nint.Zero)); return new SDL2OpenGLContext(context, windowHandle); } @@ -96,6 +96,8 @@ namespace Ryujinx.Headless.SDL2.OpenGL } } + public bool HasContext() => SDL_GL_GetCurrentContext() != nint.Zero; + public void Dispose() { SDL_GL_DeleteContext(_context); @@ -115,8 +117,9 @@ namespace Ryujinx.Headless.SDL2.OpenGL GraphicsDebugLevel glLogLevel, AspectRatio aspectRatio, bool enableMouse, - HideCursorMode hideCursorMode) - : base(inputManager, glLogLevel, aspectRatio, enableMouse, hideCursorMode) + HideCursorMode hideCursorMode, + bool ignoreControllerApplet) + : base(inputManager, glLogLevel, aspectRatio, enableMouse, hideCursorMode, ignoreControllerApplet) { _glLogLevel = glLogLevel; } @@ -127,10 +130,10 @@ namespace Ryujinx.Headless.SDL2.OpenGL { // Ensure to not share this context with other contexts before this point. SetupOpenGLAttributes(false, _glLogLevel); - IntPtr context = SDL_GL_CreateContext(WindowHandle); + nint context = SDL_GL_CreateContext(WindowHandle); CheckResult(SDL_GL_SetSwapInterval(1)); - if (context == IntPtr.Zero) + if (context == nint.Zero) { string errorMessage = $"SDL_GL_CreateContext failed with error \"{SDL_GetError()}\""; @@ -188,7 +191,7 @@ namespace Ryujinx.Headless.SDL2.OpenGL Device.DisposeGpu(); // Unbind context and destroy everything - CheckResult(SDL_GL_MakeCurrent(WindowHandle, IntPtr.Zero)); + CheckResult(SDL_GL_MakeCurrent(WindowHandle, nint.Zero)); _openGLContext.Dispose(); } diff --git a/src/Ryujinx.Headless.SDL2/Options.cs b/src/Ryujinx.Headless.SDL2/Options.cs index 37521a57e..4e2ad5b58 100644 --- a/src/Ryujinx.Headless.SDL2/Options.cs +++ b/src/Ryujinx.Headless.SDL2/Options.cs @@ -1,5 +1,6 @@ using CommandLine; using Ryujinx.Common.Configuration; +using Ryujinx.HLE; using Ryujinx.HLE.HOS.SystemState; namespace Ryujinx.Headless.SDL2 @@ -31,9 +32,6 @@ namespace Ryujinx.Headless.SDL2 // Input - [Option("correct-ons-controller", Required = false, Default = false, HelpText = "Makes the on-screen controller (iOS) buttons correspond to what they show.")] - public bool OnScreenCorrespond { get; set; } - [Option("input-profile-1", Required = false, HelpText = "Set the input profile in use for Player 1.")] public string InputProfile1Name { get; set; } @@ -117,8 +115,11 @@ namespace Ryujinx.Headless.SDL2 [Option("fs-global-access-log-mode", Required = false, Default = 0, HelpText = "Enables FS access log output to the console.")] public int FsGlobalAccessLogMode { get; set; } - [Option("disable-vsync", Required = false, HelpText = "Disables Vertical Sync.")] - public bool DisableVSync { get; set; } + [Option("vsync-mode", Required = false, Default = VSyncMode.Switch, HelpText = "Sets the emulated VSync mode (Switch, Unbounded, or Custom).")] + public VSyncMode VSyncMode { get; set; } + + [Option("custom-refresh-rate", Required = false, Default = 90, HelpText = "Sets the custom refresh rate target value (integer).")] + public int CustomVSyncInterval { get; set; } [Option("disable-shader-cache", Required = false, HelpText = "Disables Shader cache.")] public bool DisableShaderCache { get; set; } @@ -222,11 +223,14 @@ namespace Ryujinx.Headless.SDL2 // Hacks - [Option("expand-ram", Required = false, Default = false, HelpText = "Expands the RAM amount on the emulated system from 4GiB to 6GiB.")] - public bool ExpandRAM { get; set; } + [Option("dram-size", Required = false, Default = MemoryConfiguration.MemoryConfiguration4GiB, HelpText = "Set the RAM amount on the emulated system.")] + public MemoryConfiguration DramSize { get; set; } [Option("ignore-missing-services", Required = false, Default = false, HelpText = "Enable ignoring missing services.")] public bool IgnoreMissingServices { get; set; } + + [Option("ignore-controller-applet", Required = false, Default = false, HelpText = "Enable ignoring the controller applet when your game loses connection to your controller.")] + public bool IgnoreControllerApplet { get; set; } // Values diff --git a/src/Ryujinx.Headless.SDL2/Program.cs b/src/Ryujinx.Headless.SDL2/Program.cs index 42732c6c3..ef9fbcf90 100644 --- a/src/Ryujinx.Headless.SDL2/Program.cs +++ b/src/Ryujinx.Headless.SDL2/Program.cs @@ -8,6 +8,7 @@ using Ryujinx.Common.Configuration.Hid; using Ryujinx.Common.Configuration.Hid.Controller; using Ryujinx.Common.Configuration.Hid.Controller.Motion; using Ryujinx.Common.Configuration.Hid.Keyboard; +using Ryujinx.Common.GraphicsDriver; using Ryujinx.Common.Logging; using Ryujinx.Common.Logging.Targets; using Ryujinx.Common.SystemInterop; @@ -19,6 +20,7 @@ using Ryujinx.Graphics.Gpu; using Ryujinx.Graphics.Gpu.Shader; using Ryujinx.Graphics.OpenGL; using Ryujinx.Graphics.Vulkan; +using Ryujinx.Graphics.Vulkan.MoltenVK; using Ryujinx.Headless.SDL2.OpenGL; using Ryujinx.Headless.SDL2.Vulkan; using Ryujinx.HLE; @@ -95,13 +97,14 @@ namespace Ryujinx.Headless.SDL2 static void Main(string[] args) { + Silk.NET.Core.Loader.SearchPathContainer.Platform = Silk.NET.Core.Loader.UnderlyingPlatform.MacOS; + + Version = "1"; // Make process DPI aware for proper window sizing on high-res screens. ForceDpiAware.Windows(); Silk.NET.Core.Loader.SearchPathContainer.Platform = Silk.NET.Core.Loader.UnderlyingPlatform.MacOS; - Version = ReleaseInformation.GetVersion(); - if (!OperatingSystem.IsIOS()) { Console.Title = $"Ryujinx Console {Version} (Headless SDL2)"; @@ -127,17 +130,16 @@ namespace Ryujinx.Headless.SDL2 }; } - var result = Parser.Default.ParseArguments(args) - .WithParsed(options => - { - Load(options); // Load is called with the parsed options - }) - .WithNotParsed(errors => errors.Output()); - + if (OperatingSystem.IsMacOS()) + { + MVKInitialization.InitializeResolver(); + } + Parser.Default.ParseArguments(args) + .WithParsed(Load) + .WithNotParsed(errors => errors.Output()); } - [UnmanagedCallersOnly(EntryPoint = "get_game_controllers")] public static unsafe IntPtr GetGamepadList() { @@ -170,7 +172,7 @@ namespace Ryujinx.Headless.SDL2 return ptr; } - private static InputConfig HandlePlayerConfiguration(string inputProfileName, string inputId, PlayerIndex index, Options option) + private static InputConfig HandlePlayerConfiguration(string inputProfileName, string inputId, PlayerIndex index) { if (inputId == null) { @@ -270,9 +272,8 @@ namespace Ryujinx.Headless.SDL2 }; } else - { - bool isAppleController = gamepadName.Contains("Apple") ? option.OnScreenCorrespond : false; - bool isNintendoStyle = gamepadName.Contains("Nintendo") || isAppleController; + { + bool isNintendoStyle = gamepadName.Contains("Nintendo"); config = new StandardControllerInputConfig { @@ -468,9 +469,9 @@ namespace Ryujinx.Headless.SDL2 _enableKeyboard = option.EnableKeyboard; _enableMouse = option.EnableMouse; - static void LoadPlayerConfiguration(string inputProfileName, string inputId, PlayerIndex index, Options option) + static void LoadPlayerConfiguration(string inputProfileName, string inputId, PlayerIndex index) { - InputConfig inputConfig = HandlePlayerConfiguration(inputProfileName, inputId, index, option); + InputConfig inputConfig = HandlePlayerConfiguration(inputProfileName, inputId, index); if (inputConfig != null) { @@ -478,15 +479,15 @@ namespace Ryujinx.Headless.SDL2 } } - LoadPlayerConfiguration(option.InputProfile1Name, option.InputId1, PlayerIndex.Player1, option); - LoadPlayerConfiguration(option.InputProfile2Name, option.InputId2, PlayerIndex.Player2, option); - LoadPlayerConfiguration(option.InputProfile3Name, option.InputId3, PlayerIndex.Player3, option); - LoadPlayerConfiguration(option.InputProfile4Name, option.InputId4, PlayerIndex.Player4, option); - LoadPlayerConfiguration(option.InputProfile5Name, option.InputId5, PlayerIndex.Player5, option); - LoadPlayerConfiguration(option.InputProfile6Name, option.InputId6, PlayerIndex.Player6, option); - LoadPlayerConfiguration(option.InputProfile7Name, option.InputId7, PlayerIndex.Player7, option); - LoadPlayerConfiguration(option.InputProfile8Name, option.InputId8, PlayerIndex.Player8, option); - LoadPlayerConfiguration(option.InputProfileHandheldName, option.InputIdHandheld, PlayerIndex.Handheld, option); + LoadPlayerConfiguration(option.InputProfile1Name, option.InputId1, PlayerIndex.Player1); + LoadPlayerConfiguration(option.InputProfile2Name, option.InputId2, PlayerIndex.Player2); + LoadPlayerConfiguration(option.InputProfile3Name, option.InputId3, PlayerIndex.Player3); + LoadPlayerConfiguration(option.InputProfile4Name, option.InputId4, PlayerIndex.Player4); + LoadPlayerConfiguration(option.InputProfile5Name, option.InputId5, PlayerIndex.Player5); + LoadPlayerConfiguration(option.InputProfile6Name, option.InputId6, PlayerIndex.Player6); + LoadPlayerConfiguration(option.InputProfile7Name, option.InputId7, PlayerIndex.Player7); + LoadPlayerConfiguration(option.InputProfile8Name, option.InputId8, PlayerIndex.Player8); + LoadPlayerConfiguration(option.InputProfileHandheldName, option.InputIdHandheld, PlayerIndex.Handheld); if (_inputConfiguration.Count == 0) { @@ -505,11 +506,24 @@ namespace Ryujinx.Headless.SDL2 if (!option.DisableFileLog) { - Logger.AddTarget(new AsyncLogTargetWrapper( - new FileLogTarget(ReleaseInformation.GetBaseApplicationDirectory(), "file"), - 1000, - AsyncLogTargetOverflowAction.Block - )); + string logDir = AppDataManager.LogsDirPath; + FileStream logFile = null; + + if (!string.IsNullOrEmpty(logDir)) + { + logFile = FileLogTarget.PrepareLogFile(logDir); + } + if (logFile != null) + { + Logger.AddTarget(new AsyncLogTargetWrapper( + new FileLogTarget("file", logFile), + 1000 + )); + } + else + { + Logger.Error?.Print(LogClass.Application, "No writable log directory available. Make sure either the Logs directory, Application Data, or the Ryujinx directory is writable."); + } } // Setup graphics configuration @@ -562,8 +576,8 @@ namespace Ryujinx.Headless.SDL2 private static WindowBase CreateWindow(Options options) { return options.GraphicsBackend == GraphicsBackend.Vulkan - ? new VulkanWindow(_inputManager, options.LoggingGraphicsDebugLevel, options.AspectRatio, options.EnableMouse, options.HideCursorMode) - : new OpenGLWindow(_inputManager, options.LoggingGraphicsDebugLevel, options.AspectRatio, options.EnableMouse, options.HideCursorMode); + ? new VulkanWindow(_inputManager, options.LoggingGraphicsDebugLevel, options.AspectRatio, options.EnableMouse, options.HideCursorMode, options.IgnoreControllerApplet) + : new OpenGLWindow(_inputManager, options.LoggingGraphicsDebugLevel, options.AspectRatio, options.EnableMouse, options.HideCursorMode, options.IgnoreControllerApplet); } private static IRenderer CreateRenderer(Options options, WindowBase window) @@ -616,11 +630,11 @@ namespace Ryujinx.Headless.SDL2 _userChannelPersistence, renderer, new SDL2HardwareDeviceDriver(), - options.ExpandRAM ? MemoryConfiguration.MemoryConfiguration6GiB : MemoryConfiguration.MemoryConfiguration4GiB, + options.DramSize, window, options.SystemLanguage, options.SystemRegion, - !options.DisableVSync, + options.VSyncMode, !options.DisableDockedMode, !options.DisablePTC, options.EnableInternetAccess, @@ -634,7 +648,11 @@ namespace Ryujinx.Headless.SDL2 options.AudioVolume, options.UseHypervisor ?? true, options.MultiplayerLanInterfaceId, - Common.Configuration.Multiplayer.MultiplayerMode.LdnMitm); + Common.Configuration.Multiplayer.MultiplayerMode.Disabled, + false, + "", + "", + options.CustomVSyncInterval); return new Switch(configuration); } @@ -787,9 +805,6 @@ namespace Ryujinx.Headless.SDL2 } SetupProgressHandler(); - - Translator.IsReadyForTranslation.Reset(); - ExecutionEntrypoint(); return true; diff --git a/src/Ryujinx.Headless.SDL2/Ryujinx.Headless.SDL2.csproj b/src/Ryujinx.Headless.SDL2/Ryujinx.Headless.SDL2.csproj index bb43ced2e..601227940 100644 --- a/src/Ryujinx.Headless.SDL2/Ryujinx.Headless.SDL2.csproj +++ b/src/Ryujinx.Headless.SDL2/Ryujinx.Headless.SDL2.csproj @@ -1,4 +1,4 @@ - + net8.0 diff --git a/src/Ryujinx.Headless.SDL2/StatusUpdatedEventArgs.cs b/src/Ryujinx.Headless.SDL2/StatusUpdatedEventArgs.cs index 0b199d128..c1dd3805f 100644 --- a/src/Ryujinx.Headless.SDL2/StatusUpdatedEventArgs.cs +++ b/src/Ryujinx.Headless.SDL2/StatusUpdatedEventArgs.cs @@ -2,23 +2,20 @@ using System; namespace Ryujinx.Headless.SDL2 { - class StatusUpdatedEventArgs : EventArgs + class StatusUpdatedEventArgs( + string vSyncMode, + string dockedMode, + string aspectRatio, + string gameStatus, + string fifoStatus, + string gpuName) + : EventArgs { - public bool VSyncEnabled; - public string DockedMode; - public string AspectRatio; - public string GameStatus; - public string FifoStatus; - public string GpuName; - - public StatusUpdatedEventArgs(bool vSyncEnabled, string dockedMode, string aspectRatio, string gameStatus, string fifoStatus, string gpuName) - { - VSyncEnabled = vSyncEnabled; - DockedMode = dockedMode; - AspectRatio = aspectRatio; - GameStatus = gameStatus; - FifoStatus = fifoStatus; - GpuName = gpuName; - } + public string VSyncMode = vSyncMode; + public string DockedMode = dockedMode; + public string AspectRatio = aspectRatio; + public string GameStatus = gameStatus; + public string FifoStatus = fifoStatus; + public string GpuName = gpuName; } } diff --git a/src/Ryujinx.Headless.SDL2/Vulkan/VulkanWindow.cs b/src/Ryujinx.Headless.SDL2/Vulkan/VulkanWindow.cs index e5572c936..b88e0fe83 100644 --- a/src/Ryujinx.Headless.SDL2/Vulkan/VulkanWindow.cs +++ b/src/Ryujinx.Headless.SDL2/Vulkan/VulkanWindow.cs @@ -17,8 +17,9 @@ namespace Ryujinx.Headless.SDL2.Vulkan GraphicsDebugLevel glLogLevel, AspectRatio aspectRatio, bool enableMouse, - HideCursorMode hideCursorMode) - : base(inputManager, glLogLevel, aspectRatio, enableMouse, hideCursorMode) + HideCursorMode hideCursorMode, + bool ignoreControllerApplet) + : base(inputManager, glLogLevel, aspectRatio, enableMouse, hideCursorMode, ignoreControllerApplet) { _glLogLevel = glLogLevel; } @@ -46,7 +47,7 @@ namespace Ryujinx.Headless.SDL2.Vulkan action(); } - public IntPtr CreateWindowSurface(IntPtr instance) + public nint CreateWindowSurface(nint instance) { ulong surfaceHandle = 0; @@ -71,19 +72,19 @@ namespace Ryujinx.Headless.SDL2.Vulkan CreateSurface(); } - return (IntPtr)surfaceHandle; + return (nint)surfaceHandle; } public unsafe string[] GetRequiredInstanceExtensions() { - if (SDL_Vulkan_GetInstanceExtensions(WindowHandle, out uint extensionsCount, IntPtr.Zero) == SDL_bool.SDL_TRUE) + if (SDL_Vulkan_GetInstanceExtensions(WindowHandle, out uint extensionsCount, nint.Zero) == SDL_bool.SDL_TRUE) { - IntPtr[] rawExtensions = new IntPtr[(int)extensionsCount]; + nint[] rawExtensions = new nint[(int)extensionsCount]; string[] extensions = new string[(int)extensionsCount]; - fixed (IntPtr* rawExtensionsPtr = rawExtensions) + fixed (nint* rawExtensionsPtr = rawExtensions) { - if (SDL_Vulkan_GetInstanceExtensions(WindowHandle, out extensionsCount, (IntPtr)rawExtensionsPtr) == SDL_bool.SDL_TRUE) + if (SDL_Vulkan_GetInstanceExtensions(WindowHandle, out extensionsCount, (nint)rawExtensionsPtr) == SDL_bool.SDL_TRUE) { for (int i = 0; i < extensions.Length; i++) { diff --git a/src/Ryujinx.Headless.SDL2/WindowBase.cs b/src/Ryujinx.Headless.SDL2/WindowBase.cs index 9f5b50506..2479ec127 100644 --- a/src/Ryujinx.Headless.SDL2/WindowBase.cs +++ b/src/Ryujinx.Headless.SDL2/WindowBase.cs @@ -1,4 +1,4 @@ -using ARMeilleure.Translation; +using Humanizer; using Ryujinx.Common.Configuration; using Ryujinx.Common.Configuration.Hid; using Ryujinx.Common.Logging; @@ -8,9 +8,10 @@ using Ryujinx.Graphics.Gpu; using Ryujinx.Graphics.OpenGL; using Ryujinx.HLE.HOS.Applets; using Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.ApplicationProxy.Types; -using Ryujinx.HLE.Ui; +using Ryujinx.HLE.UI; using Ryujinx.Input; using Ryujinx.Input.HLE; +using Ryujinx.Input.SDL2; using Ryujinx.SDL2.Common; using System; using System.Collections.Concurrent; @@ -26,7 +27,7 @@ using Switch = Ryujinx.HLE.Switch; namespace Ryujinx.Headless.SDL2 { - abstract partial class WindowBase : IHostUiHandler, IDisposable + abstract partial class WindowBase : IHostUIHandler, IDisposable { protected const int DefaultWidth = 1280; protected const int DefaultHeight = 720; @@ -36,9 +37,9 @@ namespace Ryujinx.Headless.SDL2 private static readonly ConcurrentQueue _mainThreadActions = new(); - [LibraryImport("SDL2.framework/SDL2")] + [LibraryImport("SDL2")] // TODO: Remove this as soon as SDL2-CS was updated to expose this method publicly - private static partial IntPtr SDL_LoadBMP_RW(IntPtr src, int freesrc); + private static partial nint SDL_LoadBMP_RW(nint src, int freesrc); public static void QueueMainThreadAction(Action action) { @@ -52,9 +53,9 @@ namespace Ryujinx.Headless.SDL2 public event EventHandler StatusUpdatedEvent; - protected IntPtr WindowHandle { get; set; } + protected nint WindowHandle { get; set; } - public IHostUiTheme HostUiTheme { get; } + public IHostUITheme HostUITheme { get; } public int Width { get; private set; } public int Height { get; private set; } public int DisplayId { get; set; } @@ -81,17 +82,19 @@ namespace Ryujinx.Headless.SDL2 private bool _isStopped; private uint _windowId; - private string _gpuVendorName; + private string _gpuDriverName; private readonly AspectRatio _aspectRatio; private readonly bool _enableMouse; + private readonly bool _ignoreControllerApplet; public WindowBase( InputManager inputManager, GraphicsDebugLevel glLogLevel, AspectRatio aspectRatio, bool enableMouse, - HideCursorMode hideCursorMode) + HideCursorMode hideCursorMode, + bool ignoreControllerApplet) { MouseDriver = new SDL2MouseDriver(hideCursorMode); _inputManager = inputManager; @@ -107,7 +110,8 @@ namespace Ryujinx.Headless.SDL2 _gpuDoneEvent = new ManualResetEvent(false); _aspectRatio = aspectRatio; _enableMouse = enableMouse; - HostUiTheme = new HeadlessHostUiTheme(); + _ignoreControllerApplet = ignoreControllerApplet; + HostUITheme = new HeadlessHostUiTheme(); SDL2Driver.Instance.Initialize(); } @@ -148,8 +152,8 @@ namespace Ryujinx.Headless.SDL2 { fixed (byte* iconPtr = iconBytes) { - IntPtr rwOpsStruct = SDL_RWFromConstMem((IntPtr)iconPtr, iconBytes.Length); - IntPtr iconHandle = SDL_LoadBMP_RW(rwOpsStruct, 1); + nint rwOpsStruct = SDL_RWFromConstMem((nint)iconPtr, iconBytes.Length); + nint iconHandle = SDL_LoadBMP_RW(rwOpsStruct, 1); SDL_SetWindowIcon(WindowHandle, iconHandle); SDL_FreeSurface(iconHandle); @@ -185,10 +189,9 @@ namespace Ryujinx.Headless.SDL2 FullscreenFlag = SDL_WindowFlags.SDL_WINDOW_FULLSCREEN_DESKTOP; } - // WindowHandle = SDL_GetWindowFromID(1); WindowHandle = SDL_CreateWindow($"Ryujinx {Program.Version}{titleNameSection}{titleVersionSection}{titleIdSection}{titleArchSection}", SDL_WINDOWPOS_CENTERED_DISPLAY(DisplayId), SDL_WINDOWPOS_CENTERED_DISPLAY(DisplayId), Width, Height, DefaultFlags | FullscreenFlag | GetWindowFlags()); - if (WindowHandle == IntPtr.Zero) + if (WindowHandle == nint.Zero) { string errorMessage = $"SDL_CreateWindow failed with error \"{SDL_GetError()}\""; @@ -243,9 +246,9 @@ namespace Ryujinx.Headless.SDL2 public abstract SDL_WindowFlags GetWindowFlags(); - private string GetGpuVendorName() + private string GetGpuDriverName() { - return Renderer.GetHardwareInfo().GpuVendor; + return Renderer.GetHardwareInfo().GpuDriver; } private void SetAntiAliasing() @@ -271,13 +274,12 @@ namespace Ryujinx.Headless.SDL2 SetScalingFilter(); - _gpuVendorName = GetGpuVendorName(); + _gpuDriverName = GetGpuDriverName(); Device.Gpu.Renderer.RunLoop(() => { Device.Gpu.SetGpuThread(); Device.Gpu.InitializeShaderCache(_gpuCancellationTokenSource.Token); - Translator.IsReadyForTranslation.Set(); while (_isActive) { @@ -312,12 +314,12 @@ namespace Ryujinx.Headless.SDL2 } StatusUpdatedEvent?.Invoke(this, new StatusUpdatedEventArgs( - Device.EnableDeviceVsync, + Device.VSyncMode.ToString(), dockedMode, Device.Configuration.AspectRatio.ToText(), $"Game: {Device.Statistics.GetGameFrameRate():00.00} FPS ({Device.Statistics.GetGameFrameTime():00.00} ms)", $"FIFO: {Device.Statistics.GetFifoPercent():0.00} %", - $"GPU: {_gpuVendorName}")); + $"GPU: {_gpuDriverName}")); _ticks = Math.Min(_ticks - _ticksPerFrame, _ticksPerFrame); } @@ -468,7 +470,7 @@ namespace Ryujinx.Headless.SDL2 Exit(); } - public bool DisplayInputDialog(SoftwareKeyboardUiArgs args, out string userText) + public bool DisplayInputDialog(SoftwareKeyboardUIArgs args, out string userText) { // SDL2 doesn't support input dialogs userText = "Ryujinx"; @@ -483,14 +485,16 @@ namespace Ryujinx.Headless.SDL2 return true; } - public bool DisplayMessageDialog(ControllerAppletUiArgs args) + public bool DisplayMessageDialog(ControllerAppletUIArgs args) { + if (_ignoreControllerApplet) return false; + string playerCount = args.PlayerCountMin == args.PlayerCountMax ? $"exactly {args.PlayerCountMin}" : $"{args.PlayerCountMin}-{args.PlayerCountMax}"; - string message = $"Application requests {playerCount} player(s) with:\n\n" + string message = $"Application requests {playerCount} {"player".ToQuantity(args.PlayerCountMin + args.PlayerCountMax, ShowQuantityAs.None)} with:\n\n" + $"TYPES: {args.SupportedStyles}\n\n" + $"PLAYERS: {string.Join(", ", args.SupportedPlayers)}\n\n" - + (args.IsDocked ? "Docked mode set. Handheld is also invalid.\n\n" : "") + + (args.IsDocked ? "Docked mode set. Handheld is also invalid.\n\n" : string.Empty) + "Please reconfigure Input now and then press OK."; return DisplayMessageDialog("Controller Applet", message); diff --git a/src/Ryujinx.Horizon.Common/IExternalEvent.cs b/src/Ryujinx.Horizon.Common/IExternalEvent.cs new file mode 100644 index 000000000..dedf4c72a --- /dev/null +++ b/src/Ryujinx.Horizon.Common/IExternalEvent.cs @@ -0,0 +1,10 @@ +using System; + +namespace Ryujinx.Horizon.Common +{ + public interface IExternalEvent + { + void Signal(); + void Clear(); + } +} diff --git a/src/Ryujinx.Horizon.Common/ISyscallApi.cs b/src/Ryujinx.Horizon.Common/ISyscallApi.cs index 20277f344..3d6da0416 100644 --- a/src/Ryujinx.Horizon.Common/ISyscallApi.cs +++ b/src/Ryujinx.Horizon.Common/ISyscallApi.cs @@ -1,3 +1,4 @@ +using Ryujinx.Memory; using System; namespace Ryujinx.Horizon.Common @@ -29,5 +30,9 @@ namespace Ryujinx.Horizon.Common Result CreatePort(out int serverPortHandle, out int clientPortHandle, int maxSessions, bool isLight, string name); Result ManageNamedPort(out int handle, string name, int maxSessions); Result ConnectToPort(out int clientSessionHandle, int clientPortHandle); + + IExternalEvent GetExternalEvent(int handle); + IVirtualMemoryManager GetMemoryManagerByProcessHandle(int handle); + ulong GetTransferMemoryAddress(int handle); } } diff --git a/src/Ryujinx.Horizon.Common/Result.cs b/src/Ryujinx.Horizon.Common/Result.cs index d313554e3..4b120b847 100644 --- a/src/Ryujinx.Horizon.Common/Result.cs +++ b/src/Ryujinx.Horizon.Common/Result.cs @@ -36,6 +36,11 @@ namespace Ryujinx.Horizon.Common ErrorCode = module | (description << ModuleBits); } + public Result(int errorCode) + { + ErrorCode = errorCode; + } + public readonly override bool Equals(object obj) { return obj is Result result && result.Equals(this); diff --git a/src/Ryujinx.Horizon.Common/ResultNames.cs b/src/Ryujinx.Horizon.Common/ResultNames.cs index 55a33d680..25d04b308 100644 --- a/src/Ryujinx.Horizon.Common/ResultNames.cs +++ b/src/Ryujinx.Horizon.Common/ResultNames.cs @@ -1235,14 +1235,14 @@ namespace Ryujinx.Horizon.Common { 0x412, "NotFound" }, { 0x612, "NotEnoughBuffer" }, { 0xCA12, "Cancelled" }, - { 0x7FE12, "" }, - { 0xFA212, "" }, + { 0x7FE12, string.Empty }, + { 0xFA212, string.Empty }, { 0xFA612, "InvalidTaskId" }, { 0xFB612, "InvalidSize" }, { 0xFCA12, "TaskCancelled" }, { 0xFCC12, "TaskNotCompleted" }, { 0xFCE12, "TaskQueueNotAvailable" }, - { 0x106A12, "" }, + { 0x106A12, string.Empty }, { 0x106C12, "OutOfRpcTask" }, { 0x109612, "InvalidCategory" }, { 0x214, "OutOfKeyResource" }, diff --git a/src/Ryujinx.Horizon.Generators/Hipc/HipcGenerator.cs b/src/Ryujinx.Horizon.Generators/Hipc/HipcGenerator.cs index a65ec3abd..d1be8298d 100644 --- a/src/Ryujinx.Horizon.Generators/Hipc/HipcGenerator.cs +++ b/src/Ryujinx.Horizon.Generators/Hipc/HipcGenerator.cs @@ -17,6 +17,8 @@ namespace Ryujinx.Horizon.Generators.Hipc private const string ResponseVariableName = "response"; private const string OutRawDataVariableName = "outRawData"; + private const string TypeSystemBuffersReadOnlySequence = "System.Buffers.ReadOnlySequence"; + private const string TypeSystemMemory = "System.Memory"; private const string TypeSystemReadOnlySpan = "System.ReadOnlySpan"; private const string TypeSystemSpan = "System.Span"; private const string TypeStructLayoutAttribute = "System.Runtime.InteropServices.StructLayoutAttribute"; @@ -74,6 +76,7 @@ namespace Ryujinx.Horizon.Generators.Hipc generator.AppendLine("using Ryujinx.Horizon.Sdk.Sf.Cmif;"); generator.AppendLine("using Ryujinx.Horizon.Sdk.Sf.Hipc;"); generator.AppendLine("using System;"); + generator.AppendLine("using System.Collections.Frozen;"); generator.AppendLine("using System.Collections.Generic;"); generator.AppendLine("using System.Runtime.CompilerServices;"); generator.AppendLine("using System.Runtime.InteropServices;"); @@ -115,67 +118,76 @@ namespace Ryujinx.Horizon.Generators.Hipc private static void GenerateMethodTable(CodeGenerator generator, Compilation compilation, CommandInterface commandInterface) { generator.EnterScope($"public IReadOnlyDictionary GetCommandHandlers()"); - generator.EnterScope($"return new Dictionary()"); - foreach (var method in commandInterface.CommandImplementations) + if (commandInterface.CommandImplementations.Count == 0) { - foreach (var commandId in GetAttributeAguments(compilation, method, TypeCommandAttribute, 0)) + generator.AppendLine("return FrozenDictionary.Empty;"); + } + else + { + generator.EnterScope($"return FrozenDictionary.ToFrozenDictionary(new []"); + + foreach (var method in commandInterface.CommandImplementations) { - string[] args = new string[method.ParameterList.Parameters.Count]; - - if (args.Length == 0) + foreach (var commandId in GetAttributeArguments(compilation, method, TypeCommandAttribute, 0)) { - generator.AppendLine($"{{ {commandId}, new CommandHandler({method.Identifier.Text}, Array.Empty()) }},"); - } - else - { - int index = 0; + string[] args = new string[method.ParameterList.Parameters.Count]; - foreach (var parameter in method.ParameterList.Parameters) + if (args.Length == 0) { - string canonicalTypeName = GetCanonicalTypeNameWithGenericArguments(compilation, parameter.Type); - CommandArgType argType = GetCommandArgType(compilation, parameter); + generator.AppendLine($"KeyValuePair.Create({commandId}, new CommandHandler({method.Identifier.Text}, Array.Empty())),"); + } + else + { + int index = 0; - string arg; - - if (argType == CommandArgType.Buffer) + foreach (var parameter in method.ParameterList.Parameters) { - string bufferFlags = GetFirstAttributeAgument(compilation, parameter, TypeBufferAttribute, 0); - string bufferFixedSize = GetFirstAttributeAgument(compilation, parameter, TypeBufferAttribute, 1); + string canonicalTypeName = GetCanonicalTypeNameWithGenericArguments(compilation, parameter.Type); + CommandArgType argType = GetCommandArgType(compilation, parameter); - if (bufferFixedSize != null) + string arg; + + if (argType == CommandArgType.Buffer) { - arg = $"new CommandArg({bufferFlags} | HipcBufferFlags.FixedSize, {bufferFixedSize})"; + string bufferFlags = GetFirstAttributeArgument(compilation, parameter, TypeBufferAttribute, 0); + string bufferFixedSize = GetFirstAttributeArgument(compilation, parameter, TypeBufferAttribute, 1); + + if (bufferFixedSize != null) + { + arg = $"new CommandArg({bufferFlags} | HipcBufferFlags.FixedSize, {bufferFixedSize})"; + } + else + { + arg = $"new CommandArg({bufferFlags})"; + } + } + else if (argType == CommandArgType.InArgument || argType == CommandArgType.OutArgument) + { + string alignment = GetTypeAlignmentExpression(compilation, parameter.Type); + + arg = $"new CommandArg(CommandArgType.{argType}, Unsafe.SizeOf<{canonicalTypeName}>(), {alignment})"; } else { - arg = $"new CommandArg({bufferFlags})"; + arg = $"new CommandArg(CommandArgType.{argType})"; } - } - else if (argType == CommandArgType.InArgument || argType == CommandArgType.OutArgument) - { - string alignment = GetTypeAlignmentExpression(compilation, parameter.Type); - arg = $"new CommandArg(CommandArgType.{argType}, Unsafe.SizeOf<{canonicalTypeName}>(), {alignment})"; - } - else - { - arg = $"new CommandArg(CommandArgType.{argType})"; + args[index++] = arg; } - args[index++] = arg; + generator.AppendLine($"KeyValuePair.Create({commandId}, new CommandHandler({method.Identifier.Text}, {string.Join(", ", args)})),"); } - - generator.AppendLine($"{{ {commandId}, new CommandHandler({method.Identifier.Text}, {string.Join(", ", args)}) }},"); } } + + generator.LeaveScope(");"); } - generator.LeaveScope(";"); generator.LeaveScope(); } - private static IEnumerable GetAttributeAguments(Compilation compilation, SyntaxNode syntaxNode, string attributeName, int argIndex) + private static IEnumerable GetAttributeArguments(Compilation compilation, SyntaxNode syntaxNode, string attributeName, int argIndex) { ISymbol symbol = compilation.GetSemanticModel(syntaxNode.SyntaxTree).GetDeclaredSymbol(syntaxNode); @@ -188,9 +200,9 @@ namespace Ryujinx.Horizon.Generators.Hipc } } - private static string GetFirstAttributeAgument(Compilation compilation, SyntaxNode syntaxNode, string attributeName, int argIndex) + private static string GetFirstAttributeArgument(Compilation compilation, SyntaxNode syntaxNode, string attributeName, int argIndex) { - return GetAttributeAguments(compilation, syntaxNode, attributeName, argIndex).FirstOrDefault(); + return GetAttributeArguments(compilation, syntaxNode, attributeName, argIndex).FirstOrDefault(); } private static void GenerateMethod(CodeGenerator generator, Compilation compilation, MethodDeclarationSyntax method) @@ -233,7 +245,7 @@ namespace Ryujinx.Horizon.Generators.Hipc if (buffersCount != 0) { - generator.AppendLine($"bool[] {IsBufferMapAliasVariableName} = new bool[{method.ParameterList.Parameters.Count}];"); + generator.AppendLine($"Span {IsBufferMapAliasVariableName} = stackalloc bool[{method.ParameterList.Parameters.Count}];"); generator.AppendLine(); generator.AppendLine($"{ResultVariableName} = processor.ProcessBuffers(ref context, {IsBufferMapAliasVariableName}, runtimeMetadata);"); @@ -286,13 +298,13 @@ namespace Ryujinx.Horizon.Generators.Hipc { if (IsNonSpanOutBuffer(compilation, parameter)) { - generator.AppendLine($"using var {argName} = CommandSerialization.GetWritableRegion(processor.GetBufferRange({outArgIndex++}));"); + generator.AppendLine($"using var {argName} = CommandSerialization.GetWritableRegion(processor.GetBufferRange({index}));"); argName = $"out {GenerateSpanCastElement0(canonicalTypeName, $"{argName}.Memory.Span")}"; } else { - outParameters.Add(new OutParameter(argName, canonicalTypeName, index, argType)); + outParameters.Add(new OutParameter(argName, canonicalTypeName, outArgIndex++, argType)); argName = $"out {canonicalTypeName} {argName}"; } @@ -319,7 +331,15 @@ namespace Ryujinx.Horizon.Generators.Hipc value = $"{InObjectsVariableName}[{inObjectIndex++}]"; break; case CommandArgType.Buffer: - if (IsReadOnlySpan(compilation, parameter)) + if (IsMemory(compilation, parameter)) + { + value = $"CommandSerialization.GetWritableRegion(processor.GetBufferRange({index}))"; + } + else if (IsReadOnlySequence(compilation, parameter)) + { + value = $"CommandSerialization.GetReadOnlySequence(processor.GetBufferRange({index}))"; + } + else if (IsReadOnlySpan(compilation, parameter)) { string spanGenericTypeName = GetCanonicalTypeNameOfGenericArgument(compilation, parameter.Type, 0); value = GenerateSpanCast(spanGenericTypeName, $"CommandSerialization.GetReadOnlySpan(processor.GetBufferRange({index}))"); @@ -336,7 +356,13 @@ namespace Ryujinx.Horizon.Generators.Hipc break; } - if (IsSpan(compilation, parameter)) + if (IsMemory(compilation, parameter)) + { + generator.AppendLine($"using var {argName} = {value};"); + + argName = $"{argName}.Memory"; + } + else if (IsSpan(compilation, parameter)) { generator.AppendLine($"using var {argName} = {value};"); @@ -627,7 +653,9 @@ namespace Ryujinx.Horizon.Generators.Hipc private static bool IsValidTypeForBuffer(Compilation compilation, ParameterSyntax parameter) { - return IsReadOnlySpan(compilation, parameter) || + return IsMemory(compilation, parameter) || + IsReadOnlySequence(compilation, parameter) || + IsReadOnlySpan(compilation, parameter) || IsSpan(compilation, parameter) || IsUnmanagedType(compilation, parameter.Type); } @@ -639,6 +667,16 @@ namespace Ryujinx.Horizon.Generators.Hipc return typeInfo.Type.IsUnmanagedType; } + private static bool IsMemory(Compilation compilation, ParameterSyntax parameter) + { + return GetCanonicalTypeName(compilation, parameter.Type) == TypeSystemMemory; + } + + private static bool IsReadOnlySequence(Compilation compilation, ParameterSyntax parameter) + { + return GetCanonicalTypeName(compilation, parameter.Type) == TypeSystemBuffersReadOnlySequence; + } + private static bool IsReadOnlySpan(Compilation compilation, ParameterSyntax parameter) { return GetCanonicalTypeName(compilation, parameter.Type) == TypeSystemReadOnlySpan; @@ -719,7 +757,9 @@ namespace Ryujinx.Horizon.Generators.Hipc private static string GenerateSpanCast(string targetType, string input) { - return $"MemoryMarshal.Cast({input})"; + return targetType == "byte" + ? input + : $"MemoryMarshal.Cast({input})"; } private static bool HasAttribute(Compilation compilation, ParameterSyntax parameterSyntax, string fullAttributeName) diff --git a/src/Ryujinx.Horizon/Arp/ArpIpcServer.cs b/src/Ryujinx.Horizon/Arp/ArpIpcServer.cs new file mode 100644 index 000000000..a6017b8a6 --- /dev/null +++ b/src/Ryujinx.Horizon/Arp/ArpIpcServer.cs @@ -0,0 +1,62 @@ +using Ryujinx.Horizon.Arp.Ipc; +using Ryujinx.Horizon.Sdk.Arp; +using Ryujinx.Horizon.Sdk.Arp.Detail; +using Ryujinx.Horizon.Sdk.Sf.Hipc; +using Ryujinx.Horizon.Sdk.Sm; + +namespace Ryujinx.Horizon.Arp +{ + class ArpIpcServer + { + private const int ArpRMaxSessionsCount = 16; + private const int ArpWMaxSessionsCount = 8; + private const int MaxSessionsCount = ArpRMaxSessionsCount + ArpWMaxSessionsCount; + + private const int PointerBufferSize = 0x1000; + private const int MaxDomains = 24; + private const int MaxDomainObjects = 32; + private const int MaxPortsCount = 2; + + private static readonly ManagerOptions _managerOptions = new(PointerBufferSize, MaxDomains, MaxDomainObjects, false); + + private SmApi _sm; + private ServerManager _serverManager; + private ApplicationInstanceManager _applicationInstanceManager; + + public IReader Reader { get; private set; } + public IWriter Writer { get; private set; } + + public void Initialize() + { + HeapAllocator allocator = new(); + + _sm = new SmApi(); + _sm.Initialize().AbortOnFailure(); + + _serverManager = new ServerManager(allocator, _sm, MaxPortsCount, _managerOptions, MaxSessionsCount); + + _applicationInstanceManager = new ApplicationInstanceManager(); + + Reader reader = new(_applicationInstanceManager); + Reader = reader; + + Writer writer = new(_applicationInstanceManager); + Writer = writer; + + _serverManager.RegisterObjectForServer(reader, ServiceName.Encode("arp:r"), ArpRMaxSessionsCount); + _serverManager.RegisterObjectForServer(writer, ServiceName.Encode("arp:w"), ArpWMaxSessionsCount); + } + + public void ServiceRequests() + { + _serverManager.ServiceRequests(); + } + + public void Shutdown() + { + _applicationInstanceManager.Dispose(); + _serverManager.Dispose(); + _sm.Dispose(); + } + } +} diff --git a/src/Ryujinx.Horizon/Arp/ArpMain.cs b/src/Ryujinx.Horizon/Arp/ArpMain.cs new file mode 100644 index 000000000..a28baa71a --- /dev/null +++ b/src/Ryujinx.Horizon/Arp/ArpMain.cs @@ -0,0 +1,20 @@ +namespace Ryujinx.Horizon.Arp +{ + class ArpMain : IService + { + public static void Main(ServiceTable serviceTable) + { + ArpIpcServer arpIpcServer = new(); + + arpIpcServer.Initialize(); + + serviceTable.ArpReader = arpIpcServer.Reader; + serviceTable.ArpWriter = arpIpcServer.Writer; + + serviceTable.SignalServiceReady(); + + arpIpcServer.ServiceRequests(); + arpIpcServer.Shutdown(); + } + } +} diff --git a/src/Ryujinx.Horizon/Arp/Ipc/Reader.cs b/src/Ryujinx.Horizon/Arp/Ipc/Reader.cs new file mode 100644 index 000000000..de99c2ade --- /dev/null +++ b/src/Ryujinx.Horizon/Arp/Ipc/Reader.cs @@ -0,0 +1,135 @@ +using Ryujinx.Horizon.Common; +using Ryujinx.Horizon.Sdk.Arp; +using Ryujinx.Horizon.Sdk.Arp.Detail; +using Ryujinx.Horizon.Sdk.Ns; +using Ryujinx.Horizon.Sdk.Sf; +using Ryujinx.Horizon.Sdk.Sf.Hipc; +using System; + +namespace Ryujinx.Horizon.Arp.Ipc +{ + partial class Reader : IReader, IServiceObject + { + private readonly ApplicationInstanceManager _applicationInstanceManager; + + public Reader(ApplicationInstanceManager applicationInstanceManager) + { + _applicationInstanceManager = applicationInstanceManager; + } + + [CmifCommand(0)] + public Result GetApplicationLaunchProperty(out ApplicationLaunchProperty applicationLaunchProperty, ulong applicationInstanceId) + { + if (_applicationInstanceManager.Entries[applicationInstanceId] == null || !_applicationInstanceManager.Entries[applicationInstanceId].LaunchProperty.HasValue) + { + applicationLaunchProperty = default; + + return ArpResult.InvalidInstanceId; + } + + applicationLaunchProperty = _applicationInstanceManager.Entries[applicationInstanceId].LaunchProperty.Value; + + return Result.Success; + } + + [CmifCommand(1)] + public Result GetApplicationControlProperty([Buffer(HipcBufferFlags.Out | HipcBufferFlags.MapAlias, 0x4000)] out ApplicationControlProperty applicationControlProperty, ulong applicationInstanceId) + { + if (_applicationInstanceManager.Entries[applicationInstanceId] == null || !_applicationInstanceManager.Entries[applicationInstanceId].ControlProperty.HasValue) + { + applicationControlProperty = default; + + return ArpResult.InvalidInstanceId; + } + + applicationControlProperty = _applicationInstanceManager.Entries[applicationInstanceId].ControlProperty.Value; + + return Result.Success; + } + + [CmifCommand(2)] + public Result GetApplicationProcessProperty(out ApplicationProcessProperty applicationProcessProperty, ulong applicationInstanceId) + { + if (_applicationInstanceManager.Entries[applicationInstanceId] == null || !_applicationInstanceManager.Entries[applicationInstanceId].ProcessProperty.HasValue) + { + applicationProcessProperty = default; + + return ArpResult.InvalidInstanceId; + } + + applicationProcessProperty = _applicationInstanceManager.Entries[applicationInstanceId].ProcessProperty.Value; + + return Result.Success; + } + + [CmifCommand(3)] + public Result GetApplicationInstanceId(out ulong applicationInstanceId, ulong pid) + { + applicationInstanceId = 0; + + if (pid == 0) + { + return ArpResult.InvalidPid; + } + + for (int i = 0; i < _applicationInstanceManager.Entries.Length; i++) + { + if (_applicationInstanceManager.Entries[i] != null && _applicationInstanceManager.Entries[i].Pid == pid) + { + applicationInstanceId = (ulong)i; + + return Result.Success; + } + } + + return ArpResult.InvalidPid; + } + + [CmifCommand(4)] + public Result GetApplicationInstanceUnregistrationNotifier(out IUnregistrationNotifier unregistrationNotifier) + { + unregistrationNotifier = new UnregistrationNotifier(_applicationInstanceManager); + + return Result.Success; + } + + [CmifCommand(5)] + public Result ListApplicationInstanceId(out int count, [Buffer(HipcBufferFlags.Out | HipcBufferFlags.MapAlias)] Span applicationInstanceIdList) + { + count = 0; + + if (_applicationInstanceManager.Entries[0] != null) + { + applicationInstanceIdList[count++] = 0; + } + + if (_applicationInstanceManager.Entries[1] != null) + { + applicationInstanceIdList[count++] = 1; + } + + return Result.Success; + } + + [CmifCommand(6)] + public Result GetMicroApplicationInstanceId(out ulong microApplicationInstanceId, [ClientProcessId] ulong pid) + { + return GetApplicationInstanceId(out microApplicationInstanceId, pid); + } + + [CmifCommand(7)] + public Result GetApplicationCertificate([Buffer(HipcBufferFlags.Out | HipcBufferFlags.MapAlias | HipcBufferFlags.FixedSize, 0x528)] out ApplicationCertificate applicationCertificate, ulong applicationInstanceId) + { + if (_applicationInstanceManager.Entries[applicationInstanceId] == null) + { + applicationCertificate = default; + + return ArpResult.InvalidInstanceId; + } + + applicationCertificate = _applicationInstanceManager.Entries[applicationInstanceId].Certificate.Value; + + return Result.Success; + } + } +} diff --git a/src/Ryujinx.Horizon/Arp/Ipc/Registrar.cs b/src/Ryujinx.Horizon/Arp/Ipc/Registrar.cs new file mode 100644 index 000000000..841ab7601 --- /dev/null +++ b/src/Ryujinx.Horizon/Arp/Ipc/Registrar.cs @@ -0,0 +1,52 @@ +using Ryujinx.Horizon.Common; +using Ryujinx.Horizon.Sdk.Arp; +using Ryujinx.Horizon.Sdk.Arp.Detail; +using Ryujinx.Horizon.Sdk.Ns; +using Ryujinx.Horizon.Sdk.Sf; +using Ryujinx.Horizon.Sdk.Sf.Hipc; +using System; + +namespace Ryujinx.Horizon.Arp.Ipc +{ + partial class Registrar : IRegistrar, IServiceObject + { + private readonly ApplicationInstance _applicationInstance; + + public Registrar(ApplicationInstance applicationInstance) + { + _applicationInstance = applicationInstance; + } + + [CmifCommand(0)] + public Result Issue(out ulong applicationInstanceId) + { + throw new NotImplementedException(); + } + + [CmifCommand(1)] + public Result SetApplicationLaunchProperty(ApplicationLaunchProperty applicationLaunchProperty) + { + if (_applicationInstance.LaunchProperty != null) + { + return ArpResult.DataAlreadyBound; + } + + _applicationInstance.LaunchProperty = applicationLaunchProperty; + + return Result.Success; + } + + [CmifCommand(2)] + public Result SetApplicationControlProperty([Buffer(HipcBufferFlags.In | HipcBufferFlags.MapAlias | HipcBufferFlags.FixedSize, 0x4000)] in ApplicationControlProperty applicationControlProperty) + { + if (_applicationInstance.ControlProperty != null) + { + return ArpResult.DataAlreadyBound; + } + + _applicationInstance.ControlProperty = applicationControlProperty; + + return Result.Success; + } + } +} diff --git a/src/Ryujinx.Horizon/Arp/Ipc/UnregistrationNotifier.cs b/src/Ryujinx.Horizon/Arp/Ipc/UnregistrationNotifier.cs new file mode 100644 index 000000000..49f2b1cce --- /dev/null +++ b/src/Ryujinx.Horizon/Arp/Ipc/UnregistrationNotifier.cs @@ -0,0 +1,25 @@ +using Ryujinx.Horizon.Common; +using Ryujinx.Horizon.Sdk.Arp; +using Ryujinx.Horizon.Sdk.Arp.Detail; +using Ryujinx.Horizon.Sdk.Sf; + +namespace Ryujinx.Horizon.Arp.Ipc +{ + partial class UnregistrationNotifier : IUnregistrationNotifier, IServiceObject + { + private readonly ApplicationInstanceManager _applicationInstanceManager; + + public UnregistrationNotifier(ApplicationInstanceManager applicationInstanceManager) + { + _applicationInstanceManager = applicationInstanceManager; + } + + [CmifCommand(0)] + public Result GetReadableHandle([CopyHandle] out int readableHandle) + { + readableHandle = _applicationInstanceManager.EventHandle; + + return Result.Success; + } + } +} diff --git a/src/Ryujinx.Horizon/Arp/Ipc/Updater.cs b/src/Ryujinx.Horizon/Arp/Ipc/Updater.cs new file mode 100644 index 000000000..f7531d712 --- /dev/null +++ b/src/Ryujinx.Horizon/Arp/Ipc/Updater.cs @@ -0,0 +1,74 @@ +using Ryujinx.Horizon.Common; +using Ryujinx.Horizon.Sdk.Arp; +using Ryujinx.Horizon.Sdk.Arp.Detail; +using Ryujinx.Horizon.Sdk.Sf; +using Ryujinx.Horizon.Sdk.Sf.Hipc; +using System; + +namespace Ryujinx.Horizon.Arp.Ipc +{ + partial class Updater : IUpdater, IServiceObject + { + private readonly ApplicationInstanceManager _applicationInstanceManager; + private readonly ulong _applicationInstanceId; + private readonly bool _forCertificate; + + public Updater(ApplicationInstanceManager applicationInstanceManager, ulong applicationInstanceId, bool forCertificate) + { + _applicationInstanceManager = applicationInstanceManager; + _applicationInstanceId = applicationInstanceId; + _forCertificate = forCertificate; + } + + [CmifCommand(0)] + public Result Issue() + { + throw new NotImplementedException(); + } + + [CmifCommand(1)] + public Result SetApplicationProcessProperty(ulong pid, ApplicationProcessProperty applicationProcessProperty) + { + if (_forCertificate) + { + return ArpResult.DataAlreadyBound; + } + + if (pid == 0) + { + return ArpResult.InvalidPid; + } + + _applicationInstanceManager.Entries[_applicationInstanceId].Pid = pid; + _applicationInstanceManager.Entries[_applicationInstanceId].ProcessProperty = applicationProcessProperty; + + return Result.Success; + } + + [CmifCommand(2)] + public Result DeleteApplicationProcessProperty() + { + if (_forCertificate) + { + return ArpResult.DataAlreadyBound; + } + + _applicationInstanceManager.Entries[_applicationInstanceId].ProcessProperty = new ApplicationProcessProperty(); + + return Result.Success; + } + + [CmifCommand(3)] + public Result SetApplicationCertificate([Buffer(HipcBufferFlags.In | HipcBufferFlags.AutoSelect)] ApplicationCertificate applicationCertificate) + { + if (!_forCertificate) + { + return ArpResult.DataAlreadyBound; + } + + _applicationInstanceManager.Entries[_applicationInstanceId].Certificate = applicationCertificate; + + return Result.Success; + } + } +} diff --git a/src/Ryujinx.Horizon/Arp/Ipc/Writer.cs b/src/Ryujinx.Horizon/Arp/Ipc/Writer.cs new file mode 100644 index 000000000..29c98b779 --- /dev/null +++ b/src/Ryujinx.Horizon/Arp/Ipc/Writer.cs @@ -0,0 +1,75 @@ +using Ryujinx.Horizon.Common; +using Ryujinx.Horizon.Sdk.Arp; +using Ryujinx.Horizon.Sdk.Arp.Detail; +using Ryujinx.Horizon.Sdk.OsTypes; +using Ryujinx.Horizon.Sdk.Sf; + +namespace Ryujinx.Horizon.Arp.Ipc +{ + partial class Writer : IWriter, IServiceObject + { + private readonly ApplicationInstanceManager _applicationInstanceManager; + + public Writer(ApplicationInstanceManager applicationInstanceManager) + { + _applicationInstanceManager = applicationInstanceManager; + } + + [CmifCommand(0)] + public Result AcquireRegistrar(out IRegistrar registrar) + { + if (_applicationInstanceManager.Entries[0] != null) + { + if (_applicationInstanceManager.Entries[1] != null) + { + registrar = null; + + return ArpResult.NoFreeInstance; + } + else + { + _applicationInstanceManager.Entries[1] = new ApplicationInstance(); + + registrar = new Registrar(_applicationInstanceManager.Entries[1]); + } + } + else + { + _applicationInstanceManager.Entries[0] = new ApplicationInstance(); + + registrar = new Registrar(_applicationInstanceManager.Entries[0]); + } + + return Result.Success; + } + + [CmifCommand(1)] + public Result UnregisterApplicationInstance(ulong applicationInstanceId) + { + if (_applicationInstanceManager.Entries[applicationInstanceId] != null) + { + _applicationInstanceManager.Entries[applicationInstanceId] = null; + } + + Os.SignalSystemEvent(ref _applicationInstanceManager.SystemEvent); + + return Result.Success; + } + + [CmifCommand(2)] + public Result AcquireApplicationProcessPropertyUpdater(out IUpdater updater, ulong applicationInstanceId) + { + updater = new Updater(_applicationInstanceManager, applicationInstanceId, false); + + return Result.Success; + } + + [CmifCommand(3)] + public Result AcquireApplicationCertificateUpdater(out IUpdater updater, ulong applicationInstanceId) + { + updater = new Updater(_applicationInstanceManager, applicationInstanceId, true); + + return Result.Success; + } + } +} diff --git a/src/Ryujinx.Horizon/Audio/AudioMain.cs b/src/Ryujinx.Horizon/Audio/AudioMain.cs new file mode 100644 index 000000000..92c9e804f --- /dev/null +++ b/src/Ryujinx.Horizon/Audio/AudioMain.cs @@ -0,0 +1,17 @@ +namespace Ryujinx.Horizon.Audio +{ + class AudioMain : IService + { + public static void Main(ServiceTable serviceTable) + { + AudioUserIpcServer ipcServer = new(); + + ipcServer.Initialize(); + + serviceTable.SignalServiceReady(); + + ipcServer.ServiceRequests(); + ipcServer.Shutdown(); + } + } +} diff --git a/src/Ryujinx.Horizon/Audio/AudioManagers.cs b/src/Ryujinx.Horizon/Audio/AudioManagers.cs new file mode 100644 index 000000000..493a6f9b5 --- /dev/null +++ b/src/Ryujinx.Horizon/Audio/AudioManagers.cs @@ -0,0 +1,78 @@ +using Ryujinx.Audio; +using Ryujinx.Audio.Input; +using Ryujinx.Audio.Integration; +using Ryujinx.Audio.Output; +using Ryujinx.Audio.Renderer.Device; +using Ryujinx.Audio.Renderer.Server; +using Ryujinx.Cpu; +using Ryujinx.Horizon.Sdk.Audio; +using System; + +namespace Ryujinx.Horizon.Audio +{ + class AudioManagers : IDisposable + { + public AudioManager AudioManager { get; } + public AudioOutputManager AudioOutputManager { get; } + public AudioInputManager AudioInputManager { get; } + public AudioRendererManager AudioRendererManager { get; } + public VirtualDeviceSessionRegistry AudioDeviceSessionRegistry { get; } + + public AudioManagers(IHardwareDeviceDriver audioDeviceDriver, ITickSource tickSource) + { + AudioManager = new AudioManager(); + AudioOutputManager = new AudioOutputManager(); + AudioInputManager = new AudioInputManager(); + AudioRendererManager = new AudioRendererManager(tickSource); + AudioDeviceSessionRegistry = new VirtualDeviceSessionRegistry(audioDeviceDriver); + + IWritableEvent[] audioOutputRegisterBufferEvents = new IWritableEvent[Constants.AudioOutSessionCountMax]; + + for (int i = 0; i < audioOutputRegisterBufferEvents.Length; i++) + { + audioOutputRegisterBufferEvents[i] = new AudioEvent(); + } + + AudioOutputManager.Initialize(audioDeviceDriver, audioOutputRegisterBufferEvents); + + IWritableEvent[] audioInputRegisterBufferEvents = new IWritableEvent[Constants.AudioInSessionCountMax]; + + for (int i = 0; i < audioInputRegisterBufferEvents.Length; i++) + { + audioInputRegisterBufferEvents[i] = new AudioEvent(); + } + + AudioInputManager.Initialize(audioDeviceDriver, audioInputRegisterBufferEvents); + + IWritableEvent[] systemEvents = new IWritableEvent[Constants.AudioRendererSessionCountMax]; + + for (int i = 0; i < systemEvents.Length; i++) + { + systemEvents[i] = new AudioEvent(); + } + + AudioManager.Initialize(audioDeviceDriver.GetUpdateRequiredEvent(), AudioOutputManager.Update, AudioInputManager.Update); + + AudioRendererManager.Initialize(systemEvents, audioDeviceDriver); + + AudioManager.Start(); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + AudioManager.Dispose(); + AudioOutputManager.Dispose(); + AudioInputManager.Dispose(); + AudioRendererManager.Dispose(); + } + } + + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } +} diff --git a/src/Ryujinx.Horizon/Audio/AudioUserIpcServer.cs b/src/Ryujinx.Horizon/Audio/AudioUserIpcServer.cs new file mode 100644 index 000000000..20c824e1e --- /dev/null +++ b/src/Ryujinx.Horizon/Audio/AudioUserIpcServer.cs @@ -0,0 +1,55 @@ +using Ryujinx.Horizon.Sdk.Audio.Detail; +using Ryujinx.Horizon.Sdk.Sf.Hipc; +using Ryujinx.Horizon.Sdk.Sm; + +namespace Ryujinx.Horizon.Audio +{ + class AudioUserIpcServer + { + private const int MaxSessionsCount = 30; + + private const int PointerBufferSize = 0xB40; + private const int MaxDomains = 0; + private const int MaxDomainObjects = 0; + private const int MaxPortsCount = 1; + + private static readonly ManagerOptions _options = new(PointerBufferSize, MaxDomains, MaxDomainObjects, false); + + private SmApi _sm; + private ServerManager _serverManager; + private AudioManagers _managers; + + public void Initialize() + { + HeapAllocator allocator = new(); + + _sm = new SmApi(); + _sm.Initialize().AbortOnFailure(); + + _serverManager = new ServerManager(allocator, _sm, MaxPortsCount, _options, MaxSessionsCount); + _managers = new AudioManagers(HorizonStatic.Options.AudioDeviceDriver, HorizonStatic.Options.TickSource); + + AudioRendererManager audioRendererManager = new(_managers.AudioRendererManager, _managers.AudioDeviceSessionRegistry); + AudioOutManager audioOutManager = new(_managers.AudioOutputManager); + AudioInManager audioInManager = new(_managers.AudioInputManager); + FinalOutputRecorderManager finalOutputRecorderManager = new(); + + _serverManager.RegisterObjectForServer(audioRendererManager, ServiceName.Encode("audren:u"), MaxSessionsCount); + _serverManager.RegisterObjectForServer(audioOutManager, ServiceName.Encode("audout:u"), MaxSessionsCount); + _serverManager.RegisterObjectForServer(audioInManager, ServiceName.Encode("audin:u"), MaxSessionsCount); + _serverManager.RegisterObjectForServer(finalOutputRecorderManager, ServiceName.Encode("audrec:u"), MaxSessionsCount); + } + + public void ServiceRequests() + { + _serverManager.ServiceRequests(); + } + + public void Shutdown() + { + _serverManager.Dispose(); + _managers.Dispose(); + _sm.Dispose(); + } + } +} diff --git a/src/Ryujinx.Horizon/Audio/HwopusIpcServer.cs b/src/Ryujinx.Horizon/Audio/HwopusIpcServer.cs new file mode 100644 index 000000000..e60e033cc --- /dev/null +++ b/src/Ryujinx.Horizon/Audio/HwopusIpcServer.cs @@ -0,0 +1,46 @@ +using Ryujinx.Horizon.Sdk.Codec.Detail; +using Ryujinx.Horizon.Sdk.Sf.Hipc; +using Ryujinx.Horizon.Sdk.Sm; + +namespace Ryujinx.Horizon.Audio +{ + class HwopusIpcServer + { + private const int MaxSessionsCount = 24; + + private const int PointerBufferSize = 0x1000; + private const int MaxDomains = 8; + private const int MaxDomainObjects = 256; + private const int MaxPortsCount = 1; + + private static readonly ManagerOptions _options = new(PointerBufferSize, MaxDomains, MaxDomainObjects, false); + + private SmApi _sm; + private ServerManager _serverManager; + + public void Initialize() + { + HeapAllocator allocator = new(); + + _sm = new SmApi(); + _sm.Initialize().AbortOnFailure(); + + _serverManager = new ServerManager(allocator, _sm, MaxPortsCount, _options, MaxSessionsCount); + + HardwareOpusDecoderManager hardwareOpusDecoderManager = new(); + + _serverManager.RegisterObjectForServer(hardwareOpusDecoderManager, ServiceName.Encode("hwopus"), MaxSessionsCount); + } + + public void ServiceRequests() + { + _serverManager.ServiceRequests(); + } + + public void Shutdown() + { + _serverManager.Dispose(); + _sm.Dispose(); + } + } +} diff --git a/src/Ryujinx.Horizon/Audio/HwopusMain.cs b/src/Ryujinx.Horizon/Audio/HwopusMain.cs new file mode 100644 index 000000000..04eee3fad --- /dev/null +++ b/src/Ryujinx.Horizon/Audio/HwopusMain.cs @@ -0,0 +1,17 @@ +namespace Ryujinx.Horizon.Audio +{ + class HwopusMain : IService + { + public static void Main(ServiceTable serviceTable) + { + HwopusIpcServer ipcServer = new(); + + ipcServer.Initialize(); + + serviceTable.SignalServiceReady(); + + ipcServer.ServiceRequests(); + ipcServer.Shutdown(); + } + } +} diff --git a/src/Ryujinx.Horizon/Bcat/BcatIpcServer.cs b/src/Ryujinx.Horizon/Bcat/BcatIpcServer.cs index dd4e5b53a..8da3971cf 100644 --- a/src/Ryujinx.Horizon/Bcat/BcatIpcServer.cs +++ b/src/Ryujinx.Horizon/Bcat/BcatIpcServer.cs @@ -44,6 +44,7 @@ namespace Ryujinx.Horizon.Bcat public void Shutdown() { _serverManager.Dispose(); + _sm.Dispose(); } } } diff --git a/src/Ryujinx.Horizon/Friends/FriendsIpcServer.cs b/src/Ryujinx.Horizon/Friends/FriendsIpcServer.cs new file mode 100644 index 000000000..a12c0cae8 --- /dev/null +++ b/src/Ryujinx.Horizon/Friends/FriendsIpcServer.cs @@ -0,0 +1,50 @@ +using Ryujinx.Horizon.Sdk.Sf.Hipc; +using Ryujinx.Horizon.Sdk.Sm; + +namespace Ryujinx.Horizon.Friends +{ + class FriendsIpcServer + { + private const int MaxSessionsCount = 8; + private const int TotalMaxSessionsCount = MaxSessionsCount * 5; + + private const int PointerBufferSize = 0xA00; + private const int MaxDomains = 64; + private const int MaxDomainObjects = 16; + private const int MaxPortsCount = 5; + + private static readonly ManagerOptions _managerOptions = new(PointerBufferSize, MaxDomains, MaxDomainObjects, false); + + private SmApi _sm; + private FriendsServerManager _serverManager; + + public void Initialize() + { + HeapAllocator allocator = new(); + + _sm = new SmApi(); + _sm.Initialize().AbortOnFailure(); + + _serverManager = new FriendsServerManager(allocator, _sm, MaxPortsCount, _managerOptions, TotalMaxSessionsCount); + +#pragma warning disable IDE0055 // Disable formatting + _serverManager.RegisterServer((int)FriendsPortIndex.Admin, ServiceName.Encode("friend:a"), MaxSessionsCount); + _serverManager.RegisterServer((int)FriendsPortIndex.User, ServiceName.Encode("friend:u"), MaxSessionsCount); + _serverManager.RegisterServer((int)FriendsPortIndex.Viewer, ServiceName.Encode("friend:v"), MaxSessionsCount); + _serverManager.RegisterServer((int)FriendsPortIndex.Manager, ServiceName.Encode("friend:m"), MaxSessionsCount); + _serverManager.RegisterServer((int)FriendsPortIndex.System, ServiceName.Encode("friend:s"), MaxSessionsCount); +#pragma warning restore IDE0055 + } + + public void ServiceRequests() + { + _serverManager.ServiceRequests(); + } + + public void Shutdown() + { + _serverManager.Dispose(); + _sm.Dispose(); + } + } +} diff --git a/src/Ryujinx.Horizon/Friends/FriendsMain.cs b/src/Ryujinx.Horizon/Friends/FriendsMain.cs new file mode 100644 index 000000000..0f119cf01 --- /dev/null +++ b/src/Ryujinx.Horizon/Friends/FriendsMain.cs @@ -0,0 +1,17 @@ +namespace Ryujinx.Horizon.Friends +{ + class FriendsMain : IService + { + public static void Main(ServiceTable serviceTable) + { + FriendsIpcServer ipcServer = new(); + + ipcServer.Initialize(); + + serviceTable.SignalServiceReady(); + + ipcServer.ServiceRequests(); + ipcServer.Shutdown(); + } + } +} diff --git a/src/Ryujinx.Horizon/Friends/FriendsPortIndex.cs b/src/Ryujinx.Horizon/Friends/FriendsPortIndex.cs new file mode 100644 index 000000000..f567db302 --- /dev/null +++ b/src/Ryujinx.Horizon/Friends/FriendsPortIndex.cs @@ -0,0 +1,11 @@ +namespace Ryujinx.Horizon.Friends +{ + enum FriendsPortIndex + { + Admin, + User, + Viewer, + Manager, + System, + } +} diff --git a/src/Ryujinx.Horizon/Friends/FriendsServerManager.cs b/src/Ryujinx.Horizon/Friends/FriendsServerManager.cs new file mode 100644 index 000000000..5026206b7 --- /dev/null +++ b/src/Ryujinx.Horizon/Friends/FriendsServerManager.cs @@ -0,0 +1,36 @@ +using Ryujinx.Horizon.Common; +using Ryujinx.Horizon.Sdk.Account; +using Ryujinx.Horizon.Sdk.Friends.Detail.Ipc; +using Ryujinx.Horizon.Sdk.Sf.Hipc; +using Ryujinx.Horizon.Sdk.Sm; +using System; + +namespace Ryujinx.Horizon.Friends +{ + class FriendsServerManager : ServerManager + { + private readonly IEmulatorAccountManager _accountManager; + private readonly NotificationEventHandler _notificationEventHandler; + + public FriendsServerManager(HeapAllocator allocator, SmApi sm, int maxPorts, ManagerOptions options, int maxSessions) : base(allocator, sm, maxPorts, options, maxSessions) + { + _accountManager = HorizonStatic.Options.AccountManager; + _notificationEventHandler = new(); + } + + protected override Result OnNeedsToAccept(int portIndex, Server server) + { + return (FriendsPortIndex)portIndex switch + { +#pragma warning disable IDE0055 // Disable formatting + FriendsPortIndex.Admin => AcceptImpl(server, new ServiceCreator(_accountManager, _notificationEventHandler, FriendsServicePermissionLevel.Admin)), + FriendsPortIndex.User => AcceptImpl(server, new ServiceCreator(_accountManager, _notificationEventHandler, FriendsServicePermissionLevel.User)), + FriendsPortIndex.Viewer => AcceptImpl(server, new ServiceCreator(_accountManager, _notificationEventHandler, FriendsServicePermissionLevel.Viewer)), + FriendsPortIndex.Manager => AcceptImpl(server, new ServiceCreator(_accountManager, _notificationEventHandler, FriendsServicePermissionLevel.Manager)), + FriendsPortIndex.System => AcceptImpl(server, new ServiceCreator(_accountManager, _notificationEventHandler, FriendsServicePermissionLevel.System)), + _ => throw new ArgumentOutOfRangeException(nameof(portIndex)), +#pragma warning restore IDE0055 + }; + } + } +} diff --git a/src/Ryujinx.Horizon/HorizonOptions.cs b/src/Ryujinx.Horizon/HorizonOptions.cs index e3c862da4..a24ce7f61 100644 --- a/src/Ryujinx.Horizon/HorizonOptions.cs +++ b/src/Ryujinx.Horizon/HorizonOptions.cs @@ -1,4 +1,7 @@ using LibHac; +using Ryujinx.Audio.Integration; +using Ryujinx.Cpu; +using Ryujinx.Horizon.Sdk.Account; using Ryujinx.Horizon.Sdk.Fs; namespace Ryujinx.Horizon @@ -10,13 +13,25 @@ namespace Ryujinx.Horizon public HorizonClient BcatClient { get; } public IFsClient FsClient { get; } + public IEmulatorAccountManager AccountManager { get; } + public IHardwareDeviceDriver AudioDeviceDriver { get; } + public ITickSource TickSource { get; } - public HorizonOptions(bool ignoreMissingServices, HorizonClient bcatClient, IFsClient fsClient) + public HorizonOptions( + bool ignoreMissingServices, + HorizonClient bcatClient, + IFsClient fsClient, + IEmulatorAccountManager accountManager, + IHardwareDeviceDriver audioDeviceDriver, + ITickSource tickSource) { IgnoreMissingServices = ignoreMissingServices; ThrowOnInvalidCommandIds = true; BcatClient = bcatClient; FsClient = fsClient; + AccountManager = accountManager; + AudioDeviceDriver = audioDeviceDriver; + TickSource = tickSource; } } } diff --git a/src/Ryujinx.Horizon/Hshl/HshlIpcServer.cs b/src/Ryujinx.Horizon/Hshl/HshlIpcServer.cs index d7d89e24b..b1cc7259d 100644 --- a/src/Ryujinx.Horizon/Hshl/HshlIpcServer.cs +++ b/src/Ryujinx.Horizon/Hshl/HshlIpcServer.cs @@ -42,6 +42,7 @@ namespace Ryujinx.Horizon.Hshl public void Shutdown() { _serverManager.Dispose(); + _sm.Dispose(); } } } diff --git a/src/Ryujinx.Horizon/Ins/InsIpcServer.cs b/src/Ryujinx.Horizon/Ins/InsIpcServer.cs index bb2749d5f..4e06dcadd 100644 --- a/src/Ryujinx.Horizon/Ins/InsIpcServer.cs +++ b/src/Ryujinx.Horizon/Ins/InsIpcServer.cs @@ -42,6 +42,7 @@ namespace Ryujinx.Horizon.Ins public void Shutdown() { _serverManager.Dispose(); + _sm.Dispose(); } } } diff --git a/src/Ryujinx.Horizon/Lbl/LblIpcServer.cs b/src/Ryujinx.Horizon/Lbl/LblIpcServer.cs index 6b5421653..f25fc54b1 100644 --- a/src/Ryujinx.Horizon/Lbl/LblIpcServer.cs +++ b/src/Ryujinx.Horizon/Lbl/LblIpcServer.cs @@ -38,6 +38,7 @@ namespace Ryujinx.Horizon.Lbl public void Shutdown() { _serverManager.Dispose(); + _sm.Dispose(); } } } diff --git a/src/Ryujinx.Horizon/LogManager/LmIpcServer.cs b/src/Ryujinx.Horizon/LogManager/LmIpcServer.cs index d023ff927..6bb4e11c7 100644 --- a/src/Ryujinx.Horizon/LogManager/LmIpcServer.cs +++ b/src/Ryujinx.Horizon/LogManager/LmIpcServer.cs @@ -38,6 +38,7 @@ namespace Ryujinx.Horizon.LogManager public void Shutdown() { _serverManager.Dispose(); + _sm.Dispose(); } } } diff --git a/src/Ryujinx.Horizon/MmNv/MmNvIpcServer.cs b/src/Ryujinx.Horizon/MmNv/MmNvIpcServer.cs index c52a294f5..b3ce81182 100644 --- a/src/Ryujinx.Horizon/MmNv/MmNvIpcServer.cs +++ b/src/Ryujinx.Horizon/MmNv/MmNvIpcServer.cs @@ -38,6 +38,7 @@ namespace Ryujinx.Horizon.MmNv public void Shutdown() { _serverManager.Dispose(); + _sm.Dispose(); } } } diff --git a/src/Ryujinx.Horizon/Ngc/Ipc/Service.cs b/src/Ryujinx.Horizon/Ngc/Ipc/Service.cs index 828c09199..740f893f8 100644 --- a/src/Ryujinx.Horizon/Ngc/Ipc/Service.cs +++ b/src/Ryujinx.Horizon/Ngc/Ipc/Service.cs @@ -26,7 +26,11 @@ namespace Ryujinx.Horizon.Ngc.Ipc } [CmifCommand(1)] - public Result Check(out uint checkMask, ReadOnlySpan text, uint regionMask, ProfanityFilterOption option) + public Result Check( + out uint checkMask, + [Buffer(HipcBufferFlags.In | HipcBufferFlags.MapAlias)] ReadOnlySpan text, + uint regionMask, + ProfanityFilterOption option) { lock (_profanityFilter) { diff --git a/src/Ryujinx.Horizon/Ngc/NgcIpcServer.cs b/src/Ryujinx.Horizon/Ngc/NgcIpcServer.cs index b2a74fb22..ec73f96ae 100644 --- a/src/Ryujinx.Horizon/Ngc/NgcIpcServer.cs +++ b/src/Ryujinx.Horizon/Ngc/NgcIpcServer.cs @@ -3,7 +3,6 @@ using Ryujinx.Horizon.Sdk.Fs; using Ryujinx.Horizon.Sdk.Ngc.Detail; using Ryujinx.Horizon.Sdk.Sf.Hipc; using Ryujinx.Horizon.Sdk.Sm; -using System; namespace Ryujinx.Horizon.Ngc { @@ -46,6 +45,7 @@ namespace Ryujinx.Horizon.Ngc { _serverManager.Dispose(); _profanityFilter.Dispose(); + _sm.Dispose(); } } } diff --git a/src/Ryujinx.Horizon/Ovln/OvlnIpcServer.cs b/src/Ryujinx.Horizon/Ovln/OvlnIpcServer.cs index c4580a861..d4257be8d 100644 --- a/src/Ryujinx.Horizon/Ovln/OvlnIpcServer.cs +++ b/src/Ryujinx.Horizon/Ovln/OvlnIpcServer.cs @@ -43,6 +43,7 @@ namespace Ryujinx.Horizon.Ovln public void Shutdown() { _serverManager.Dispose(); + _sm.Dispose(); } } } diff --git a/src/Ryujinx.Horizon/Prepo/Ipc/PrepoService.cs b/src/Ryujinx.Horizon/Prepo/Ipc/PrepoService.cs index c165f46fa..4ed7dd48e 100644 --- a/src/Ryujinx.Horizon/Prepo/Ipc/PrepoService.cs +++ b/src/Ryujinx.Horizon/Prepo/Ipc/PrepoService.cs @@ -5,6 +5,7 @@ using Ryujinx.Common.Utilities; using Ryujinx.Horizon.Common; using Ryujinx.Horizon.Prepo.Types; using Ryujinx.Horizon.Sdk.Account; +using Ryujinx.Horizon.Sdk.Arp; using Ryujinx.Horizon.Sdk.Prepo; using Ryujinx.Horizon.Sdk.Sf; using Ryujinx.Horizon.Sdk.Sf.Hipc; @@ -22,14 +23,16 @@ namespace Ryujinx.Horizon.Prepo.Ipc System, } + private readonly ArpApi _arp; private readonly PrepoServicePermissionLevel _permissionLevel; private ulong _systemSessionId; private bool _immediateTransmissionEnabled; private bool _userAgreementCheckEnabled = true; - public PrepoService(PrepoServicePermissionLevel permissionLevel) + public PrepoService(ArpApi arp, PrepoServicePermissionLevel permissionLevel) { + _arp = arp; _permissionLevel = permissionLevel; } @@ -165,7 +168,7 @@ namespace Ryujinx.Horizon.Prepo.Ipc return PrepoResult.PermissionDenied; } - private static Result ProcessPlayReport(PlayReportKind playReportKind, ReadOnlySpan gameRoomBuffer, ReadOnlySpan reportBuffer, ulong pid, Uid userId, bool withUserId = false, ApplicationId applicationId = default) + private Result ProcessPlayReport(PlayReportKind playReportKind, ReadOnlySpan gameRoomBuffer, ReadOnlySpan reportBuffer, ulong pid, Uid userId, bool withUserId = false, ApplicationId applicationId = default) { if (withUserId) { @@ -199,8 +202,8 @@ namespace Ryujinx.Horizon.Prepo.Ipc builder.AppendLine("PlayReport log:"); builder.AppendLine($" Kind: {playReportKind}"); - // NOTE: The service calls arp:r using the pid to get the application id, if it fails PrepoResult.InvalidPid is returned. - // Reports are stored internally and an event is signaled to transmit them. + // NOTE: Reports are stored internally and an event is signaled to transmit them. + if (pid != 0) { builder.AppendLine($" Pid: {pid}"); @@ -210,6 +213,16 @@ namespace Ryujinx.Horizon.Prepo.Ipc builder.AppendLine($" ApplicationId: {applicationId}"); } + Result result = _arp.GetApplicationInstanceId(out ulong applicationInstanceId, pid); + if (result.IsFailure) + { + return PrepoResult.InvalidPid; + } + + _arp.GetApplicationLaunchProperty(out ApplicationLaunchProperty applicationLaunchProperty, applicationInstanceId).AbortOnFailure(); + + builder.AppendLine($" ApplicationVersion: {applicationLaunchProperty.Version}"); + if (!userId.IsNull) { builder.AppendLine($" UserId: {userId}"); diff --git a/src/Ryujinx.Horizon/Prepo/PrepoIpcServer.cs b/src/Ryujinx.Horizon/Prepo/PrepoIpcServer.cs index fd3f86ff9..669a64594 100644 --- a/src/Ryujinx.Horizon/Prepo/PrepoIpcServer.cs +++ b/src/Ryujinx.Horizon/Prepo/PrepoIpcServer.cs @@ -1,4 +1,5 @@ using Ryujinx.Horizon.Prepo.Types; +using Ryujinx.Horizon.Sdk.Arp; using Ryujinx.Horizon.Sdk.Sf.Hipc; using Ryujinx.Horizon.Sdk.Sm; @@ -17,16 +18,19 @@ namespace Ryujinx.Horizon.Prepo private static readonly ManagerOptions _managerOptions = new(PointerBufferSize, MaxDomains, MaxDomainObjects, false); private SmApi _sm; + private ArpApi _arp; private PrepoServerManager _serverManager; public void Initialize() { HeapAllocator allocator = new(); + _arp = new ArpApi(allocator); + _sm = new SmApi(); _sm.Initialize().AbortOnFailure(); - _serverManager = new PrepoServerManager(allocator, _sm, MaxPortsCount, _managerOptions, TotalMaxSessionsCount); + _serverManager = new PrepoServerManager(allocator, _sm, _arp, MaxPortsCount, _managerOptions, TotalMaxSessionsCount); #pragma warning disable IDE0055 // Disable formatting _serverManager.RegisterServer((int)PrepoPortIndex.Admin, ServiceName.Encode("prepo:a"), MaxSessionsCount); // 1.0.0-5.1.0 @@ -45,7 +49,9 @@ namespace Ryujinx.Horizon.Prepo public void Shutdown() { + _arp.Dispose(); _serverManager.Dispose(); + _sm.Dispose(); } } } diff --git a/src/Ryujinx.Horizon/Prepo/PrepoServerManager.cs b/src/Ryujinx.Horizon/Prepo/PrepoServerManager.cs index 0c1c782f6..8cac44c8f 100644 --- a/src/Ryujinx.Horizon/Prepo/PrepoServerManager.cs +++ b/src/Ryujinx.Horizon/Prepo/PrepoServerManager.cs @@ -1,6 +1,7 @@ using Ryujinx.Horizon.Common; using Ryujinx.Horizon.Prepo.Ipc; using Ryujinx.Horizon.Prepo.Types; +using Ryujinx.Horizon.Sdk.Arp; using Ryujinx.Horizon.Sdk.Sf.Hipc; using Ryujinx.Horizon.Sdk.Sm; using System; @@ -9,8 +10,11 @@ namespace Ryujinx.Horizon.Prepo { class PrepoServerManager : ServerManager { - public PrepoServerManager(HeapAllocator allocator, SmApi sm, int maxPorts, ManagerOptions options, int maxSessions) : base(allocator, sm, maxPorts, options, maxSessions) + private readonly ArpApi _arp; + + public PrepoServerManager(HeapAllocator allocator, SmApi sm, ArpApi arp, int maxPorts, ManagerOptions options, int maxSessions) : base(allocator, sm, maxPorts, options, maxSessions) { + _arp = arp; } protected override Result OnNeedsToAccept(int portIndex, Server server) @@ -18,12 +22,12 @@ namespace Ryujinx.Horizon.Prepo return (PrepoPortIndex)portIndex switch { #pragma warning disable IDE0055 // Disable formatting - PrepoPortIndex.Admin => AcceptImpl(server, new PrepoService(PrepoServicePermissionLevel.Admin)), - PrepoPortIndex.Admin2 => AcceptImpl(server, new PrepoService(PrepoServicePermissionLevel.Admin)), - PrepoPortIndex.Manager => AcceptImpl(server, new PrepoService(PrepoServicePermissionLevel.Manager)), - PrepoPortIndex.User => AcceptImpl(server, new PrepoService(PrepoServicePermissionLevel.User)), - PrepoPortIndex.System => AcceptImpl(server, new PrepoService(PrepoServicePermissionLevel.System)), - PrepoPortIndex.Debug => AcceptImpl(server, new PrepoService(PrepoServicePermissionLevel.Debug)), + PrepoPortIndex.Admin => AcceptImpl(server, new PrepoService(_arp, PrepoServicePermissionLevel.Admin)), + PrepoPortIndex.Admin2 => AcceptImpl(server, new PrepoService(_arp, PrepoServicePermissionLevel.Admin)), + PrepoPortIndex.Manager => AcceptImpl(server, new PrepoService(_arp, PrepoServicePermissionLevel.Manager)), + PrepoPortIndex.User => AcceptImpl(server, new PrepoService(_arp, PrepoServicePermissionLevel.User)), + PrepoPortIndex.System => AcceptImpl(server, new PrepoService(_arp, PrepoServicePermissionLevel.System)), + PrepoPortIndex.Debug => AcceptImpl(server, new PrepoService(_arp, PrepoServicePermissionLevel.Debug)), _ => throw new ArgumentOutOfRangeException(nameof(portIndex)), #pragma warning restore IDE0055 }; diff --git a/src/Ryujinx.Horizon/Psc/PscIpcServer.cs b/src/Ryujinx.Horizon/Psc/PscIpcServer.cs index d6ac65685..8e574ddda 100644 --- a/src/Ryujinx.Horizon/Psc/PscIpcServer.cs +++ b/src/Ryujinx.Horizon/Psc/PscIpcServer.cs @@ -45,6 +45,7 @@ namespace Ryujinx.Horizon.Psc public void Shutdown() { _serverManager.Dispose(); + _sm.Dispose(); } } } diff --git a/src/Ryujinx.Horizon/Ptm/Ipc/MeasurementServer.cs b/src/Ryujinx.Horizon/Ptm/Ipc/MeasurementServer.cs new file mode 100644 index 000000000..ce7c0474a --- /dev/null +++ b/src/Ryujinx.Horizon/Ptm/Ipc/MeasurementServer.cs @@ -0,0 +1,63 @@ +using Ryujinx.Common.Logging; +using Ryujinx.Horizon.Common; +using Ryujinx.Horizon.Sdk.Sf; +using Ryujinx.Horizon.Sdk.Ts; +using Ryujinx.Horizon.Ts.Ipc; + +namespace Ryujinx.Horizon.Ptm.Ipc +{ + partial class MeasurementServer : IMeasurementServer + { + // NOTE: Values are randomly choosen. + public const int DefaultTemperature = 42; + public const int MinimumTemperature = 0; + public const int MaximumTemperature = 100; + + [CmifCommand(0)] // 1.0.0-16.1.0 + public Result GetTemperatureRange(out int minimumTemperature, out int maximumTemperature, Location location) + { + Logger.Stub?.PrintStub(LogClass.ServicePtm, new { location }); + + minimumTemperature = MinimumTemperature; + maximumTemperature = MaximumTemperature; + + return Result.Success; + } + + [CmifCommand(1)] // 1.0.0-16.1.0 + public Result GetTemperature(out int temperature, Location location) + { + Logger.Stub?.PrintStub(LogClass.ServicePtm, new { location }); + + temperature = DefaultTemperature; + + return Result.Success; + } + + [CmifCommand(2)] // 1.0.0-13.2.1 + public Result SetMeasurementMode(Location location, byte measurementMode) + { + Logger.Stub?.PrintStub(LogClass.ServicePtm, new { location, measurementMode }); + + return Result.Success; + } + + [CmifCommand(3)] // 1.0.0-13.2.1 + public Result GetTemperatureMilliC(out int temperatureMilliC, Location location) + { + Logger.Stub?.PrintStub(LogClass.ServicePtm, new { location }); + + temperatureMilliC = DefaultTemperature * 1000; + + return Result.Success; + } + + [CmifCommand(4)] // 8.0.0+ + public Result OpenSession(out ISession session, DeviceCode deviceCode) + { + session = new Session(deviceCode); + + return Result.Success; + } + } +} diff --git a/src/Ryujinx.Horizon/Ptm/Ipc/Session.cs b/src/Ryujinx.Horizon/Ptm/Ipc/Session.cs new file mode 100644 index 000000000..191a4b3af --- /dev/null +++ b/src/Ryujinx.Horizon/Ptm/Ipc/Session.cs @@ -0,0 +1,47 @@ +using Ryujinx.Common.Logging; +using Ryujinx.Horizon.Common; +using Ryujinx.Horizon.Ptm.Ipc; +using Ryujinx.Horizon.Sdk.Sf; +using Ryujinx.Horizon.Sdk.Ts; + +namespace Ryujinx.Horizon.Ts.Ipc +{ + partial class Session : ISession + { + private readonly DeviceCode _deviceCode; + + public Session(DeviceCode deviceCode) + { + _deviceCode = deviceCode; + } + + [CmifCommand(0)] + public Result GetTemperatureRange(out int minimumTemperature, out int maximumTemperature) + { + Logger.Stub?.PrintStub(LogClass.ServicePtm, new { _deviceCode }); + + minimumTemperature = MeasurementServer.MinimumTemperature; + maximumTemperature = MeasurementServer.MaximumTemperature; + + return Result.Success; + } + + [CmifCommand(2)] + public Result SetMeasurementMode(byte measurementMode) + { + Logger.Stub?.PrintStub(LogClass.ServicePtm, new { _deviceCode, measurementMode }); + + return Result.Success; + } + + [CmifCommand(4)] + public Result GetTemperature(out int temperature) + { + Logger.Stub?.PrintStub(LogClass.ServicePtm, new { _deviceCode }); + + temperature = MeasurementServer.DefaultTemperature; + + return Result.Success; + } + } +} diff --git a/src/Ryujinx.Horizon/Ptm/TsIpcServer.cs b/src/Ryujinx.Horizon/Ptm/TsIpcServer.cs new file mode 100644 index 000000000..db25d8e2e --- /dev/null +++ b/src/Ryujinx.Horizon/Ptm/TsIpcServer.cs @@ -0,0 +1,44 @@ +using Ryujinx.Horizon.Ptm.Ipc; +using Ryujinx.Horizon.Sdk.Sf.Hipc; +using Ryujinx.Horizon.Sdk.Sm; + +namespace Ryujinx.Horizon.Ptm +{ + class TsIpcServer + { + private const int MaxSessionsCount = 4; + + private const int PointerBufferSize = 0; + private const int MaxDomains = 0; + private const int MaxDomainObjects = 0; + private const int MaxPortsCount = 1; + + private static readonly ManagerOptions _managerOptions = new(PointerBufferSize, MaxDomains, MaxDomainObjects, false); + + private SmApi _sm; + private ServerManager _serverManager; + + public void Initialize() + { + HeapAllocator allocator = new(); + + _sm = new SmApi(); + _sm.Initialize().AbortOnFailure(); + + _serverManager = new ServerManager(allocator, _sm, MaxPortsCount, _managerOptions, MaxSessionsCount); + + _serverManager.RegisterObjectForServer(new MeasurementServer(), ServiceName.Encode("ts"), MaxSessionsCount); + } + + public void ServiceRequests() + { + _serverManager.ServiceRequests(); + } + + public void Shutdown() + { + _serverManager.Dispose(); + _sm.Dispose(); + } + } +} diff --git a/src/Ryujinx.Horizon/Ptm/TsMain.cs b/src/Ryujinx.Horizon/Ptm/TsMain.cs new file mode 100644 index 000000000..237d52cd4 --- /dev/null +++ b/src/Ryujinx.Horizon/Ptm/TsMain.cs @@ -0,0 +1,17 @@ +namespace Ryujinx.Horizon.Ptm +{ + class TsMain : IService + { + public static void Main(ServiceTable serviceTable) + { + TsIpcServer ipcServer = new(); + + ipcServer.Initialize(); + + serviceTable.SignalServiceReady(); + + ipcServer.ServiceRequests(); + ipcServer.Shutdown(); + } + } +} diff --git a/src/Ryujinx.Horizon/Ryujinx.Horizon.csproj b/src/Ryujinx.Horizon/Ryujinx.Horizon.csproj index ae40f7b5e..bf34ddd17 100644 --- a/src/Ryujinx.Horizon/Ryujinx.Horizon.csproj +++ b/src/Ryujinx.Horizon/Ryujinx.Horizon.csproj @@ -1,10 +1,11 @@ - + net8.0 + @@ -12,7 +13,7 @@ + - diff --git a/src/Ryujinx.Horizon/Sdk/Account/IEmulatorAccountManager.cs b/src/Ryujinx.Horizon/Sdk/Account/IEmulatorAccountManager.cs new file mode 100644 index 000000000..af02cc8eb --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Account/IEmulatorAccountManager.cs @@ -0,0 +1,8 @@ +namespace Ryujinx.Horizon.Sdk.Account +{ + public interface IEmulatorAccountManager + { + void OpenUserOnlinePlay(Uid userId); + void CloseUserOnlinePlay(Uid userId); + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Account/NetworkServiceAccountId.cs b/src/Ryujinx.Horizon/Sdk/Account/NetworkServiceAccountId.cs new file mode 100644 index 000000000..2512975e3 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Account/NetworkServiceAccountId.cs @@ -0,0 +1,20 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.Horizon.Sdk.Account +{ + [StructLayout(LayoutKind.Sequential, Size = 0x8, Pack = 0x8)] + readonly record struct NetworkServiceAccountId + { + public readonly ulong Id; + + public NetworkServiceAccountId(ulong id) + { + Id = id; + } + + public override readonly string ToString() + { + return Id.ToString("x16"); + } + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Account/Nickname.cs b/src/Ryujinx.Horizon/Sdk/Account/Nickname.cs new file mode 100644 index 000000000..1f351ee3a --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Account/Nickname.cs @@ -0,0 +1,29 @@ +using Ryujinx.Common.Memory; +using System; +using System.Runtime.InteropServices; +using System.Text; + +namespace Ryujinx.Horizon.Sdk.Account +{ + [StructLayout(LayoutKind.Sequential, Size = 0x21, Pack = 0x1)] + readonly struct Nickname + { + public readonly Array33 Name; + + public Nickname(in Array33 name) + { + Name = name; + } + + public override string ToString() + { + int length = ((ReadOnlySpan)Name.AsSpan()).IndexOf((byte)0); + if (length < 0) + { + length = 33; + } + + return Encoding.UTF8.GetString(Name.AsSpan()[..length]); + } + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Account/Uid.cs b/src/Ryujinx.Horizon/Sdk/Account/Uid.cs index a76e6d256..d612f4792 100644 --- a/src/Ryujinx.Horizon/Sdk/Account/Uid.cs +++ b/src/Ryujinx.Horizon/Sdk/Account/Uid.cs @@ -5,17 +5,17 @@ using System.Runtime.InteropServices; namespace Ryujinx.Horizon.Sdk.Account { - [StructLayout(LayoutKind.Sequential)] - readonly record struct Uid + [StructLayout(LayoutKind.Sequential, Size = 0x10, Pack = 0x8)] + public readonly record struct Uid { - public readonly long High; - public readonly long Low; + public readonly ulong High; + public readonly ulong Low; public bool IsNull => (Low | High) == 0; public static Uid Null => new(0, 0); - public Uid(long low, long high) + public Uid(ulong low, ulong high) { Low = low; High = high; @@ -23,8 +23,8 @@ namespace Ryujinx.Horizon.Sdk.Account public Uid(byte[] bytes) { - High = BitConverter.ToInt64(bytes, 0); - Low = BitConverter.ToInt64(bytes, 8); + High = BitConverter.ToUInt64(bytes, 0); + Low = BitConverter.ToUInt64(bytes, 8); } public Uid(string hex) @@ -34,8 +34,8 @@ namespace Ryujinx.Horizon.Sdk.Account throw new ArgumentException("Invalid Hex value!", nameof(hex)); } - Low = Convert.ToInt64(hex[16..], 16); - High = Convert.ToInt64(hex[..16], 16); + Low = Convert.ToUInt64(hex[16..], 16); + High = Convert.ToUInt64(hex[..16], 16); } public void Write(BinaryWriter binaryWriter) diff --git a/src/Ryujinx.Horizon/Sdk/Applet/AppletId.cs b/src/Ryujinx.Horizon/Sdk/Applet/AppletId.cs new file mode 100644 index 000000000..2b81fbf6f --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Applet/AppletId.cs @@ -0,0 +1,71 @@ +namespace Ryujinx.Horizon.Sdk.Applet +{ + enum AppletId : uint + { + None = 0x00, + Application = 0x01, + OverlayApplet = 0x02, + SystemAppletMenu = 0x03, + SystemApplication = 0x04, + LibraryAppletAuth = 0x0A, + LibraryAppletCabinet = 0x0B, + LibraryAppletController = 0x0C, + LibraryAppletDataErase = 0x0D, + LibraryAppletError = 0x0E, + LibraryAppletNetConnect = 0x0F, + LibraryAppletPlayerSelect = 0x10, + LibraryAppletSwkbd = 0x11, + LibraryAppletMiiEdit = 0x12, + LibraryAppletWeb = 0x13, + LibraryAppletShop = 0x14, + LibraryAppletPhotoViewer = 0x15, + LibraryAppletSet = 0x16, + LibraryAppletOfflineWeb = 0x17, + LibraryAppletLoginShare = 0x18, + LibraryAppletWifiWebAuth = 0x19, + LibraryAppletMyPage = 0x1A, + LibraryAppletGift = 0x1B, + LibraryAppletUserMigration = 0x1C, + LibraryAppletPreomiaSys = 0x1D, + LibraryAppletStory = 0x1E, + LibraryAppletPreomiaUsr = 0x1F, + LibraryAppletPreomiaUsrDummy = 0x20, + LibraryAppletSample = 0x21, + LibraryAppletPromoteQualification = 0x22, + LibraryAppletOfflineWebFw17 = 0x32, + LibraryAppletOfflineWeb2Fw17 = 0x33, + LibraryAppletLoginShareFw17 = 0x35, + LibraryAppletLoginShare2Fw17 = 0x36, + LibraryAppletLoginShare3Fw17 = 0x37, + Unknown38 = 0x38, + DevlopmentTool = 0x3E8, + CombinationLA = 0x3F1, + AeSystemApplet = 0x3F2, + AeOverlayApplet = 0x3F3, + AeStarter = 0x3F4, + AeLibraryAppletAlone = 0x3F5, + AeLibraryApplet1 = 0x3F6, + AeLibraryApplet2 = 0x3F7, + AeLibraryApplet3 = 0x3F8, + AeLibraryApplet4 = 0x3F9, + AppletISA = 0x3FA, + AppletIOA = 0x3FB, + AppletISTA = 0x3FC, + AppletILA1 = 0x3FD, + AppletILA2 = 0x3FE, + CombinationLAFw17 = 0x700000DC, + AeSystemAppletFw17 = 0x700000E6, + AeOverlayAppletFw17 = 0x700000E7, + AeStarterFw17 = 0x700000E8, + AeLibraryAppletAloneFw17 = 0x700000E9, + AeLibraryApplet1Fw17 = 0x700000EA, + AeLibraryApplet2Fw17 = 0x700000EB, + AeLibraryApplet3Fw17 = 0x700000EC, + AeLibraryApplet4Fw17 = 0x700000ED, + AppletISAFw17 = 0x700000F0, + AppletIOAFw17 = 0x700000F1, + AppletISTAFw17 = 0x700000F2, + AppletILA1Fw17 = 0x700000F3, + AppletILA2Fw17 = 0x700000F4, + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Applet/AppletResourceUserId.cs b/src/Ryujinx.Horizon/Sdk/Applet/AppletResourceUserId.cs new file mode 100644 index 000000000..00e2ad368 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Applet/AppletResourceUserId.cs @@ -0,0 +1,15 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.Horizon.Sdk.Applet +{ + [StructLayout(LayoutKind.Sequential, Size = 0x8, Pack = 0x8)] + readonly struct AppletResourceUserId + { + public readonly ulong Id; + + public AppletResourceUserId(ulong id) + { + Id = id; + } + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Arp/ApplicationCertificate.cs b/src/Ryujinx.Horizon/Sdk/Arp/ApplicationCertificate.cs new file mode 100644 index 000000000..d60d337d3 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Arp/ApplicationCertificate.cs @@ -0,0 +1,9 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.Horizon.Sdk.Arp +{ + [StructLayout(LayoutKind.Sequential, Size = 0x528)] + public struct ApplicationCertificate + { + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Arp/ApplicationKind.cs b/src/Ryujinx.Horizon/Sdk/Arp/ApplicationKind.cs new file mode 100644 index 000000000..586e6a98a --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Arp/ApplicationKind.cs @@ -0,0 +1,8 @@ +namespace Ryujinx.Horizon.Sdk.Arp +{ + public enum ApplicationKind : byte + { + Application, + MicroApplication, + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Arp/ApplicationLaunchProperty.cs b/src/Ryujinx.Horizon/Sdk/Arp/ApplicationLaunchProperty.cs new file mode 100644 index 000000000..00b818321 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Arp/ApplicationLaunchProperty.cs @@ -0,0 +1,14 @@ +using Ryujinx.Horizon.Sdk.Ncm; + +namespace Ryujinx.Horizon.Sdk.Arp +{ + public struct ApplicationLaunchProperty + { + public ApplicationId ApplicationId; + public uint Version; + public StorageId Storage; + public StorageId PatchStorage; + public ApplicationKind ApplicationKind; + public byte Padding; + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Arp/ApplicationProcessProperty.cs b/src/Ryujinx.Horizon/Sdk/Arp/ApplicationProcessProperty.cs new file mode 100644 index 000000000..13d222a12 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Arp/ApplicationProcessProperty.cs @@ -0,0 +1,10 @@ +using Ryujinx.Common.Memory; + +namespace Ryujinx.Horizon.Sdk.Arp +{ + public struct ApplicationProcessProperty + { + public byte ProgramIndex; + public Array15 Unknown; + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Arp/ArpApi.cs b/src/Ryujinx.Horizon/Sdk/Arp/ArpApi.cs new file mode 100644 index 000000000..b0acc0062 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Arp/ArpApi.cs @@ -0,0 +1,130 @@ +using Ryujinx.Common.Memory; +using Ryujinx.Horizon.Common; +using Ryujinx.Horizon.Sdk.Ns; +using Ryujinx.Horizon.Sdk.Sf.Cmif; +using Ryujinx.Horizon.Sdk.Sf.Hipc; +using Ryujinx.Horizon.Sdk.Sm; +using System; +using System.Runtime.CompilerServices; + +namespace Ryujinx.Horizon.Sdk.Arp +{ + class ArpApi : IDisposable + { + private const string ArpRName = "arp:r"; + + private readonly HeapAllocator _allocator; + private int _sessionHandle; + + public ArpApi(HeapAllocator allocator) + { + _allocator = allocator; + } + + private void InitializeArpRService() + { + if (_sessionHandle == 0) + { + using var smApi = new SmApi(); + + smApi.Initialize(); + smApi.GetServiceHandle(out _sessionHandle, ServiceName.Encode(ArpRName)).AbortOnFailure(); + } + } + + public Result GetApplicationInstanceId(out ulong applicationInstanceId, ulong applicationPid) + { + Span data = stackalloc byte[8]; + SpanWriter writer = new(data); + + writer.Write(applicationPid); + + InitializeArpRService(); + + Result result = ServiceUtil.SendRequest(out CmifResponse response, _sessionHandle, 3, sendPid: false, data); + if (result.IsFailure) + { + applicationInstanceId = 0; + + return result; + } + + SpanReader reader = new(response.Data); + + applicationInstanceId = reader.Read(); + + return Result.Success; + } + + public Result GetApplicationLaunchProperty(out ApplicationLaunchProperty applicationLaunchProperty, ulong applicationInstanceId) + { + applicationLaunchProperty = default; + + Span data = stackalloc byte[8]; + SpanWriter writer = new(data); + + writer.Write(applicationInstanceId); + + InitializeArpRService(); + + Result result = ServiceUtil.SendRequest(out CmifResponse response, _sessionHandle, 0, sendPid: false, data); + if (result.IsFailure) + { + return result; + } + + SpanReader reader = new(response.Data); + + applicationLaunchProperty = reader.Read(); + + return Result.Success; + } + + public Result GetApplicationControlProperty(out ApplicationControlProperty applicationControlProperty, ulong applicationInstanceId) + { + applicationControlProperty = default; + + Span data = stackalloc byte[8]; + SpanWriter writer = new(data); + + writer.Write(applicationInstanceId); + + ulong bufferSize = (ulong)Unsafe.SizeOf(); + ulong bufferAddress = _allocator.Allocate(bufferSize); + + InitializeArpRService(); + + Result result = ServiceUtil.SendRequest( + out CmifResponse response, + _sessionHandle, + 1, + sendPid: false, + data, + stackalloc[] { HipcBufferFlags.Out | HipcBufferFlags.MapAlias | HipcBufferFlags.FixedSize }, + stackalloc[] { new PointerAndSize(bufferAddress, bufferSize) }); + + if (result.IsFailure) + { + return result; + } + + applicationControlProperty = HorizonStatic.AddressSpace.Read(bufferAddress); + + _allocator.Free(bufferAddress, bufferSize); + + return Result.Success; + } + + public void Dispose() + { + if (_sessionHandle != 0) + { + HorizonStatic.Syscall.CloseHandle(_sessionHandle); + + _sessionHandle = 0; + } + + GC.SuppressFinalize(this); + } + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Arp/ArpResult.cs b/src/Ryujinx.Horizon/Sdk/Arp/ArpResult.cs new file mode 100644 index 000000000..5de07871d --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Arp/ArpResult.cs @@ -0,0 +1,17 @@ +using Ryujinx.Horizon.Common; + +namespace Ryujinx.Horizon.Sdk.Arp +{ + static class ArpResult + { + private const int ModuleId = 157; + + public static Result InvalidArgument => new(ModuleId, 30); + public static Result InvalidPid => new(ModuleId, 31); + public static Result InvalidPointer => new(ModuleId, 32); + public static Result DataAlreadyBound => new(ModuleId, 42); + public static Result AllocationFailed => new(ModuleId, 63); + public static Result NoFreeInstance => new(ModuleId, 101); + public static Result InvalidInstanceId => new(ModuleId, 102); + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Arp/Detail/ApplicationInstance.cs b/src/Ryujinx.Horizon/Sdk/Arp/Detail/ApplicationInstance.cs new file mode 100644 index 000000000..5eb0ab184 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Arp/Detail/ApplicationInstance.cs @@ -0,0 +1,13 @@ +using Ryujinx.Horizon.Sdk.Ns; + +namespace Ryujinx.Horizon.Sdk.Arp.Detail +{ + class ApplicationInstance + { + public ulong Pid { get; set; } + public ApplicationLaunchProperty? LaunchProperty { get; set; } + public ApplicationProcessProperty? ProcessProperty { get; set; } + public ApplicationControlProperty? ControlProperty { get; set; } + public ApplicationCertificate? Certificate { get; set; } + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Arp/Detail/ApplicationInstanceManager.cs b/src/Ryujinx.Horizon/Sdk/Arp/Detail/ApplicationInstanceManager.cs new file mode 100644 index 000000000..18c993ce4 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Arp/Detail/ApplicationInstanceManager.cs @@ -0,0 +1,31 @@ +using Ryujinx.Horizon.Sdk.OsTypes; +using System; +using System.Threading; + +namespace Ryujinx.Horizon.Sdk.Arp.Detail +{ + class ApplicationInstanceManager : IDisposable + { + private int _disposalState; + + public SystemEventType SystemEvent; + public int EventHandle; + + public readonly ApplicationInstance[] Entries = new ApplicationInstance[2]; + + public ApplicationInstanceManager() + { + Os.CreateSystemEvent(out SystemEvent, EventClearMode.ManualClear, true).AbortOnFailure(); + + EventHandle = Os.GetReadableHandleOfSystemEvent(ref SystemEvent); + } + + public void Dispose() + { + if (EventHandle != 0 && Interlocked.Exchange(ref _disposalState, 1) == 0) + { + Os.DestroySystemEvent(ref SystemEvent); + } + } + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Arp/IReader.cs b/src/Ryujinx.Horizon/Sdk/Arp/IReader.cs new file mode 100644 index 000000000..ef78f7fd6 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Arp/IReader.cs @@ -0,0 +1,18 @@ +using Ryujinx.Horizon.Common; +using Ryujinx.Horizon.Sdk.Ns; +using System; + +namespace Ryujinx.Horizon.Sdk.Arp +{ + public interface IReader + { + public Result GetApplicationLaunchProperty(out ApplicationLaunchProperty applicationLaunchProperty, ulong applicationInstanceId); + public Result GetApplicationControlProperty(out ApplicationControlProperty applicationControlProperty, ulong applicationInstanceId); + public Result GetApplicationProcessProperty(out ApplicationProcessProperty applicationControlProperty, ulong applicationInstanceId); + public Result GetApplicationInstanceId(out ulong applicationInstanceId, ulong pid); + public Result GetApplicationInstanceUnregistrationNotifier(out IUnregistrationNotifier unregistrationNotifier); + public Result ListApplicationInstanceId(out int count, Span applicationInstanceIdList); + public Result GetMicroApplicationInstanceId(out ulong MicroApplicationInstanceId, ulong pid); + public Result GetApplicationCertificate(out ApplicationCertificate applicationCertificate, ulong applicationInstanceId); + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Arp/IRegistrar.cs b/src/Ryujinx.Horizon/Sdk/Arp/IRegistrar.cs new file mode 100644 index 000000000..467f3dbd3 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Arp/IRegistrar.cs @@ -0,0 +1,12 @@ +using Ryujinx.Horizon.Common; +using Ryujinx.Horizon.Sdk.Ns; + +namespace Ryujinx.Horizon.Sdk.Arp +{ + public interface IRegistrar + { + public Result Issue(out ulong applicationInstanceId); + public Result SetApplicationLaunchProperty(ApplicationLaunchProperty applicationLaunchProperty); + public Result SetApplicationControlProperty(in ApplicationControlProperty applicationControlProperty); + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Arp/IUnregistrationNotifier.cs b/src/Ryujinx.Horizon/Sdk/Arp/IUnregistrationNotifier.cs new file mode 100644 index 000000000..24b9807d8 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Arp/IUnregistrationNotifier.cs @@ -0,0 +1,9 @@ +using Ryujinx.Horizon.Common; + +namespace Ryujinx.Horizon.Sdk.Arp +{ + public interface IUnregistrationNotifier + { + public Result GetReadableHandle(out int readableHandle); + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Arp/IUpdater.cs b/src/Ryujinx.Horizon/Sdk/Arp/IUpdater.cs new file mode 100644 index 000000000..f9beeb690 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Arp/IUpdater.cs @@ -0,0 +1,12 @@ +using Ryujinx.Horizon.Common; + +namespace Ryujinx.Horizon.Sdk.Arp +{ + public interface IUpdater + { + public Result Issue(); + public Result SetApplicationProcessProperty(ulong pid, ApplicationProcessProperty applicationProcessProperty); + public Result DeleteApplicationProcessProperty(); + public Result SetApplicationCertificate(ApplicationCertificate applicationCertificate); + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Arp/IWriter.cs b/src/Ryujinx.Horizon/Sdk/Arp/IWriter.cs new file mode 100644 index 000000000..b3e000e1e --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Arp/IWriter.cs @@ -0,0 +1,12 @@ +using Ryujinx.Horizon.Common; + +namespace Ryujinx.Horizon.Sdk.Arp +{ + public interface IWriter + { + public Result AcquireRegistrar(out IRegistrar registrar); + public Result UnregisterApplicationInstance(ulong applicationInstanceId); + public Result AcquireApplicationProcessPropertyUpdater(out IUpdater updater, ulong applicationInstanceId); + public Result AcquireApplicationCertificateUpdater(out IUpdater updater, ulong applicationInstanceId); + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Audio/AudioEvent.cs b/src/Ryujinx.Horizon/Sdk/Audio/AudioEvent.cs new file mode 100644 index 000000000..efa8d5bc1 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Audio/AudioEvent.cs @@ -0,0 +1,50 @@ +using Ryujinx.Audio.Integration; +using Ryujinx.Horizon.Common; +using Ryujinx.Horizon.Sdk.OsTypes; +using System; + +namespace Ryujinx.Horizon.Sdk.Audio +{ + class AudioEvent : IWritableEvent, IDisposable + { + private SystemEventType _systemEvent; + private readonly IExternalEvent _externalEvent; + + public AudioEvent() + { + Os.CreateSystemEvent(out _systemEvent, EventClearMode.ManualClear, interProcess: true); + + // We need to do this because the event will be signalled from a different thread. + _externalEvent = HorizonStatic.Syscall.GetExternalEvent(Os.GetWritableHandleOfSystemEvent(ref _systemEvent)); + } + + public void Signal() + { + _externalEvent.Signal(); + } + + public void Clear() + { + _externalEvent.Clear(); + } + + public int GetReadableHandle() + { + return Os.GetReadableHandleOfSystemEvent(ref _systemEvent); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + Os.DestroySystemEvent(ref _systemEvent); + } + } + + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Audio/AudioResult.cs b/src/Ryujinx.Horizon/Sdk/Audio/AudioResult.cs new file mode 100644 index 000000000..5914a747c --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Audio/AudioResult.cs @@ -0,0 +1,13 @@ +using Ryujinx.Horizon.Common; + +namespace Ryujinx.Horizon.Sdk.Audio +{ + static class AudioResult + { + private const int ModuleId = 153; + + public static Result DeviceNotFound => new(ModuleId, 1); + public static Result UnsupportedRevision => new(ModuleId, 2); + public static Result NotImplemented => new(ModuleId, 513); + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Audio/Detail/AudioDevice.cs b/src/Ryujinx.Horizon/Sdk/Audio/Detail/AudioDevice.cs new file mode 100644 index 000000000..2d3aa7ba9 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Audio/Detail/AudioDevice.cs @@ -0,0 +1,294 @@ +using Ryujinx.Audio.Renderer.Device; +using Ryujinx.Audio.Renderer.Server; +using Ryujinx.Horizon.Common; +using Ryujinx.Horizon.Sdk.Applet; +using Ryujinx.Horizon.Sdk.OsTypes; +using Ryujinx.Horizon.Sdk.Sf; +using Ryujinx.Horizon.Sdk.Sf.Hipc; +using System; + +namespace Ryujinx.Horizon.Sdk.Audio.Detail +{ + partial class AudioDevice : IAudioDevice, IDisposable + { + private readonly VirtualDeviceSessionRegistry _registry; + private readonly VirtualDeviceSession[] _sessions; + private readonly bool _isUsbDeviceSupported; + + private SystemEventType _audioEvent; + private SystemEventType _audioInputEvent; + private SystemEventType _audioOutputEvent; + + public AudioDevice(VirtualDeviceSessionRegistry registry, AppletResourceUserId appletResourceId, uint revision) + { + _registry = registry; + + BehaviourContext behaviourContext = new(); + behaviourContext.SetUserRevision((int)revision); + + _isUsbDeviceSupported = behaviourContext.IsAudioUsbDeviceOutputSupported(); + _sessions = registry.GetSessionByAppletResourceId(appletResourceId.Id); + + Os.CreateSystemEvent(out _audioEvent, EventClearMode.AutoClear, interProcess: true); + Os.CreateSystemEvent(out _audioInputEvent, EventClearMode.AutoClear, interProcess: true); + Os.CreateSystemEvent(out _audioOutputEvent, EventClearMode.AutoClear, interProcess: true); + } + + private bool TryGetDeviceByName(out VirtualDeviceSession result, string name, bool ignoreRevLimitation = false) + { + result = null; + + foreach (VirtualDeviceSession session in _sessions) + { + if (session.Device.Name.Equals(name)) + { + if (!ignoreRevLimitation && !_isUsbDeviceSupported && session.Device.IsUsbDevice()) + { + return false; + } + + result = session; + + return true; + } + } + + return false; + } + + [CmifCommand(0)] + public Result ListAudioDeviceName([Buffer(HipcBufferFlags.Out | HipcBufferFlags.MapAlias)] Span names, out int nameCount) + { + int count = 0; + + foreach (VirtualDeviceSession session in _sessions) + { + if (!_isUsbDeviceSupported && session.Device.IsUsbDevice()) + { + continue; + } + + if (count >= names.Length) + { + break; + } + + names[count] = new DeviceName(session.Device.Name); + + count++; + } + + nameCount = count; + + return Result.Success; + } + + [CmifCommand(1)] + public Result SetAudioDeviceOutputVolume([Buffer(HipcBufferFlags.In | HipcBufferFlags.MapAlias)] ReadOnlySpan name, float volume) + { + if (name.Length > 0 && TryGetDeviceByName(out VirtualDeviceSession result, name[0].ToString(), ignoreRevLimitation: true)) + { + if (!_isUsbDeviceSupported && result.Device.IsUsbDevice()) + { + result = _sessions[0]; + } + + result.Volume = volume; + } + + return Result.Success; + } + + [CmifCommand(2)] + public Result GetAudioDeviceOutputVolume([Buffer(HipcBufferFlags.In | HipcBufferFlags.MapAlias)] ReadOnlySpan name, out float volume) + { + if (name.Length > 0 && TryGetDeviceByName(out VirtualDeviceSession result, name[0].ToString())) + { + volume = result.Volume; + } + else + { + volume = 0f; + } + + return Result.Success; + } + + [CmifCommand(3)] + public Result GetActiveAudioDeviceName([Buffer(HipcBufferFlags.Out | HipcBufferFlags.MapAlias)] Span name) + { + VirtualDevice device = _registry.ActiveDevice; + + if (!_isUsbDeviceSupported && device.IsUsbDevice()) + { + device = _registry.DefaultDevice; + } + + if (name.Length > 0) + { + name[0] = new DeviceName(device.Name); + } + + return Result.Success; + } + + [CmifCommand(4)] + public Result QueryAudioDeviceSystemEvent([CopyHandle] out int eventHandle) + { + eventHandle = Os.GetReadableHandleOfSystemEvent(ref _audioEvent); + + return Result.Success; + } + + [CmifCommand(5)] + public Result GetActiveChannelCount(out int channelCount) + { + VirtualDevice device = _registry.ActiveDevice; + + if (!_isUsbDeviceSupported && device.IsUsbDevice()) + { + device = _registry.DefaultDevice; + } + + channelCount = (int)device.ChannelCount; + + return Result.Success; + } + + [CmifCommand(6)] // 3.0.0+ + public Result ListAudioDeviceNameAuto([Buffer(HipcBufferFlags.Out | HipcBufferFlags.AutoSelect)] Span names, out int nameCount) + { + return ListAudioDeviceName(names, out nameCount); + } + + [CmifCommand(7)] // 3.0.0+ + public Result SetAudioDeviceOutputVolumeAuto([Buffer(HipcBufferFlags.In | HipcBufferFlags.AutoSelect)] ReadOnlySpan name, float volume) + { + return SetAudioDeviceOutputVolume(name, volume); + } + + [CmifCommand(8)] // 3.0.0+ + public Result GetAudioDeviceOutputVolumeAuto([Buffer(HipcBufferFlags.In | HipcBufferFlags.AutoSelect)] ReadOnlySpan name, out float volume) + { + return GetAudioDeviceOutputVolume(name, out volume); + } + + [CmifCommand(10)] // 3.0.0+ + public Result GetActiveAudioDeviceNameAuto([Buffer(HipcBufferFlags.Out | HipcBufferFlags.AutoSelect)] Span name) + { + return GetActiveAudioDeviceName(name); + } + + [CmifCommand(11)] // 3.0.0+ + public Result QueryAudioDeviceInputEvent([CopyHandle] out int eventHandle) + { + eventHandle = Os.GetReadableHandleOfSystemEvent(ref _audioInputEvent); + + return Result.Success; + } + + [CmifCommand(12)] // 3.0.0+ + public Result QueryAudioDeviceOutputEvent([CopyHandle] out int eventHandle) + { + eventHandle = Os.GetReadableHandleOfSystemEvent(ref _audioOutputEvent); + + return Result.Success; + } + + [CmifCommand(13)] // 13.0.0+ + public Result GetActiveAudioOutputDeviceName([Buffer(HipcBufferFlags.Out | HipcBufferFlags.MapAlias)] Span name) + { + if (name.Length > 0) + { + name[0] = new DeviceName(_registry.ActiveDevice.GetOutputDeviceName()); + } + + return Result.Success; + } + + [CmifCommand(14)] // 13.0.0+ + public Result ListAudioOutputDeviceName([Buffer(HipcBufferFlags.Out | HipcBufferFlags.MapAlias)] Span names, out int nameCount) + { + int count = 0; + + foreach (VirtualDeviceSession session in _sessions) + { + if (!_isUsbDeviceSupported && session.Device.IsUsbDevice()) + { + continue; + } + + if (count >= names.Length) + { + break; + } + + names[count] = new DeviceName(session.Device.GetOutputDeviceName()); + + count++; + } + + nameCount = count; + + return Result.Success; + } + + [CmifCommand(15)] // 17.0.0+ + public Result AcquireAudioOutputDeviceNotification([CopyHandle] out int eventHandle, ulong deviceId) + { + eventHandle = 0; + + return AudioResult.NotImplemented; + } + + [CmifCommand(16)] // 17.0.0+ + public Result ReleaseAudioOutputDeviceNotification(ulong deviceId) + { + return AudioResult.NotImplemented; + } + + [CmifCommand(17)] // 17.0.0+ + public Result AcquireAudioInputDeviceNotification([CopyHandle] out int eventHandle, ulong deviceId) + { + eventHandle = 0; + + return AudioResult.NotImplemented; + } + + [CmifCommand(18)] // 17.0.0+ + public Result ReleaseAudioInputDeviceNotification(ulong deviceId) + { + return AudioResult.NotImplemented; + } + + [CmifCommand(19)] // 18.0.0+ + public Result SetAudioDeviceOutputVolumeAutoTuneEnabled(bool enabled) + { + return AudioResult.NotImplemented; + } + + [CmifCommand(20)] // 18.0.0+ + public Result IsAudioDeviceOutputVolumeAutoTuneEnabled(out bool enabled) + { + enabled = false; + + return AudioResult.NotImplemented; + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + Os.DestroySystemEvent(ref _audioEvent); + Os.DestroySystemEvent(ref _audioInputEvent); + Os.DestroySystemEvent(ref _audioOutputEvent); + } + } + + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Audio/Detail/AudioIn.cs b/src/Ryujinx.Horizon/Sdk/Audio/Detail/AudioIn.cs new file mode 100644 index 000000000..464ede581 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Audio/Detail/AudioIn.cs @@ -0,0 +1,171 @@ +using Ryujinx.Audio.Common; +using Ryujinx.Audio.Input; +using Ryujinx.Horizon.Common; +using Ryujinx.Horizon.Sdk.Sf; +using Ryujinx.Horizon.Sdk.Sf.Hipc; +using System; + +namespace Ryujinx.Horizon.Sdk.Audio.Detail +{ + partial class AudioIn : IAudioIn, IDisposable + { + private readonly AudioInputSystem _impl; + private int _processHandle; + + public AudioIn(AudioInputSystem impl, int processHandle) + { + _impl = impl; + _processHandle = processHandle; + } + + [CmifCommand(0)] + public Result GetAudioInState(out AudioDeviceState state) + { + state = _impl.GetState(); + + return Result.Success; + } + + [CmifCommand(1)] + public Result Start() + { + return new Result((int)_impl.Start()); + } + + [CmifCommand(2)] + public Result Stop() + { + return new Result((int)_impl.Stop()); + } + + [CmifCommand(3)] + public Result AppendAudioInBuffer(ulong bufferTag, [Buffer(HipcBufferFlags.In | HipcBufferFlags.MapAlias)] ReadOnlySpan buffer) + { + AudioUserBuffer userBuffer = default; + + if (buffer.Length > 0) + { + userBuffer = buffer[0]; + } + + return new Result((int)_impl.AppendBuffer(bufferTag, ref userBuffer)); + } + + [CmifCommand(4)] + public Result RegisterBufferEvent([CopyHandle] out int eventHandle) + { + eventHandle = 0; + + if (_impl.RegisterBufferEvent() is AudioEvent audioEvent) + { + eventHandle = audioEvent.GetReadableHandle(); + } + + return Result.Success; + } + + [CmifCommand(5)] + public Result GetReleasedAudioInBuffers(out uint count, [Buffer(HipcBufferFlags.Out | HipcBufferFlags.MapAlias)] Span bufferTags) + { + return new Result((int)_impl.GetReleasedBuffers(bufferTags, out count)); + } + + [CmifCommand(6)] + public Result ContainsAudioInBuffer(out bool contains, ulong bufferTag) + { + contains = _impl.ContainsBuffer(bufferTag); + + return Result.Success; + } + + [CmifCommand(7)] // 3.0.0+ + public Result AppendUacInBuffer( + ulong bufferTag, + [Buffer(HipcBufferFlags.In | HipcBufferFlags.MapAlias)] ReadOnlySpan buffer, + [CopyHandle] int eventHandle) + { + AudioUserBuffer userBuffer = default; + + if (buffer.Length > 0) + { + userBuffer = buffer[0]; + } + + return new Result((int)_impl.AppendUacBuffer(bufferTag, ref userBuffer, (uint)eventHandle)); + } + + [CmifCommand(8)] // 3.0.0+ + public Result AppendAudioInBufferAuto(ulong bufferTag, [Buffer(HipcBufferFlags.In | HipcBufferFlags.AutoSelect)] ReadOnlySpan buffer) + { + return AppendAudioInBuffer(bufferTag, buffer); + } + + [CmifCommand(9)] // 3.0.0+ + public Result GetReleasedAudioInBuffersAuto(out uint count, [Buffer(HipcBufferFlags.Out | HipcBufferFlags.AutoSelect)] Span bufferTags) + { + return GetReleasedAudioInBuffers(out count, bufferTags); + } + + [CmifCommand(10)] // 3.0.0+ + public Result AppendUacInBufferAuto( + ulong bufferTag, + [Buffer(HipcBufferFlags.In | HipcBufferFlags.AutoSelect)] ReadOnlySpan buffer, + [CopyHandle] int eventHandle) + { + return AppendUacInBuffer(bufferTag, buffer, eventHandle); + } + + [CmifCommand(11)] // 4.0.0+ + public Result GetAudioInBufferCount(out uint bufferCount) + { + bufferCount = _impl.GetBufferCount(); + + return Result.Success; + } + + [CmifCommand(12)] // 4.0.0+ + public Result SetDeviceGain(float gain) + { + _impl.SetVolume(gain); + + return Result.Success; + } + + [CmifCommand(13)] // 4.0.0+ + public Result GetDeviceGain(out float gain) + { + gain = _impl.GetVolume(); + + return Result.Success; + } + + [CmifCommand(14)] // 6.0.0+ + public Result FlushAudioInBuffers(out bool pending) + { + pending = _impl.FlushBuffers(); + + return Result.Success; + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + _impl.Dispose(); + + if (_processHandle != 0) + { + HorizonStatic.Syscall.CloseHandle(_processHandle); + + _processHandle = 0; + } + } + } + + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Audio/Detail/AudioInManager.cs b/src/Ryujinx.Horizon/Sdk/Audio/Detail/AudioInManager.cs new file mode 100644 index 000000000..d5d047201 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Audio/Detail/AudioInManager.cs @@ -0,0 +1,130 @@ +using Ryujinx.Audio; +using Ryujinx.Audio.Common; +using Ryujinx.Audio.Input; +using Ryujinx.Horizon.Common; +using Ryujinx.Horizon.Sdk.Applet; +using Ryujinx.Horizon.Sdk.Sf; +using Ryujinx.Horizon.Sdk.Sf.Hipc; +using System; + +namespace Ryujinx.Horizon.Sdk.Audio.Detail +{ + partial class AudioInManager : IAudioInManager + { + private readonly AudioInputManager _impl; + + public AudioInManager(AudioInputManager impl) + { + _impl = impl; + } + + [CmifCommand(0)] + public Result ListAudioIns(out int count, [Buffer(HipcBufferFlags.Out | HipcBufferFlags.MapAlias)] Span names) + { + string[] deviceNames = _impl.ListAudioIns(filtered: false); + + count = 0; + + foreach (string deviceName in deviceNames) + { + if (count >= names.Length) + { + break; + } + + names[count++] = new DeviceName(deviceName); + } + + return Result.Success; + } + + [CmifCommand(1)] + public Result OpenAudioIn( + out AudioOutputConfiguration outputConfiguration, + out IAudioIn audioIn, + [Buffer(HipcBufferFlags.Out | HipcBufferFlags.MapAlias)] Span outName, + AudioInputConfiguration parameter, + AppletResourceUserId appletResourceId, + [CopyHandle] int processHandle, + [Buffer(HipcBufferFlags.In | HipcBufferFlags.MapAlias)] ReadOnlySpan name, + [ClientProcessId] ulong pid) + { + var clientMemoryManager = HorizonStatic.Syscall.GetMemoryManagerByProcessHandle(processHandle); + + ResultCode rc = _impl.OpenAudioIn( + out string outputDeviceName, + out outputConfiguration, + out AudioInputSystem inSystem, + clientMemoryManager, + name.Length > 0 ? name[0].ToString() : string.Empty, + SampleFormat.PcmInt16, + ref parameter); + + if (rc == ResultCode.Success && outName.Length > 0) + { + outName[0] = new DeviceName(outputDeviceName); + } + + audioIn = new AudioIn(inSystem, processHandle); + + return new Result((int)rc); + } + + [CmifCommand(2)] // 3.0.0+ + public Result ListAudioInsAuto(out int count, [Buffer(HipcBufferFlags.Out | HipcBufferFlags.AutoSelect)] Span names) + { + return ListAudioIns(out count, names); + } + + [CmifCommand(3)] // 3.0.0+ + public Result OpenAudioInAuto( + out AudioOutputConfiguration outputConfig, + out IAudioIn audioIn, + [Buffer(HipcBufferFlags.Out | HipcBufferFlags.AutoSelect)] Span outName, + AudioInputConfiguration parameter, + AppletResourceUserId appletResourceId, + [CopyHandle] int processHandle, + [Buffer(HipcBufferFlags.In | HipcBufferFlags.AutoSelect)] ReadOnlySpan name, + [ClientProcessId] ulong pid) + { + return OpenAudioIn(out outputConfig, out audioIn, outName, parameter, appletResourceId, processHandle, name, pid); + } + + [CmifCommand(4)] // 3.0.0+ + public Result ListAudioInsAutoFiltered(out int count, [Buffer(HipcBufferFlags.Out | HipcBufferFlags.AutoSelect)] Span names) + { + string[] deviceNames = _impl.ListAudioIns(filtered: true); + + count = 0; + + foreach (string deviceName in deviceNames) + { + if (count >= names.Length) + { + break; + } + + names[count++] = new DeviceName(deviceName); + } + + return Result.Success; + } + + [CmifCommand(5)] // 5.0.0+ + public Result OpenAudioInProtocolSpecified( + out AudioOutputConfiguration outputConfig, + out IAudioIn audioIn, + [Buffer(HipcBufferFlags.Out | HipcBufferFlags.MapAlias)] Span outName, + AudioInProtocol protocol, + AudioInputConfiguration parameter, + AppletResourceUserId appletResourceId, + [CopyHandle] int processHandle, + [Buffer(HipcBufferFlags.In | HipcBufferFlags.MapAlias)] ReadOnlySpan name, + [ClientProcessId] ulong pid) + { + // NOTE: We always assume that only the default device will be plugged (we never report any USB Audio Class type devices). + + return OpenAudioIn(out outputConfig, out audioIn, outName, parameter, appletResourceId, processHandle, name, pid); + } + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Audio/Detail/AudioInProtocol.cs b/src/Ryujinx.Horizon/Sdk/Audio/Detail/AudioInProtocol.cs new file mode 100644 index 000000000..48785f1c0 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Audio/Detail/AudioInProtocol.cs @@ -0,0 +1,23 @@ +using Ryujinx.Common.Memory; +using System.Runtime.InteropServices; + +namespace Ryujinx.Horizon.Sdk.Audio.Detail +{ + [StructLayout(LayoutKind.Sequential, Size = 0x8, Pack = 0x1)] + struct AudioInProtocol + { + public AudioInProtocolName Name; + public Array7 Padding; + + public AudioInProtocol(AudioInProtocolName name) + { + Name = name; + Padding = new(); + } + + public override readonly string ToString() + { + return Name.ToString(); + } + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Audio/Detail/AudioInProtocolName.cs b/src/Ryujinx.Horizon/Sdk/Audio/Detail/AudioInProtocolName.cs new file mode 100644 index 000000000..68d283cc5 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Audio/Detail/AudioInProtocolName.cs @@ -0,0 +1,8 @@ +namespace Ryujinx.Horizon.Sdk.Audio.Detail +{ + enum AudioInProtocolName : byte + { + DeviceIn = 0, + UacIn = 1, + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Audio/Detail/AudioOut.cs b/src/Ryujinx.Horizon/Sdk/Audio/Detail/AudioOut.cs new file mode 100644 index 000000000..7607e2643 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Audio/Detail/AudioOut.cs @@ -0,0 +1,154 @@ +using Ryujinx.Audio.Common; +using Ryujinx.Audio.Output; +using Ryujinx.Horizon.Common; +using Ryujinx.Horizon.Sdk.Sf; +using Ryujinx.Horizon.Sdk.Sf.Hipc; +using System; + +namespace Ryujinx.Horizon.Sdk.Audio.Detail +{ + partial class AudioOut : IAudioOut, IDisposable + { + private readonly AudioOutputSystem _impl; + private int _processHandle; + + public AudioOut(AudioOutputSystem impl, int processHandle) + { + _impl = impl; + _processHandle = processHandle; + } + + [CmifCommand(0)] + public Result GetAudioOutState(out AudioDeviceState state) + { + state = _impl.GetState(); + + return Result.Success; + } + + [CmifCommand(1)] + public Result Start() + { + return new Result((int)_impl.Start()); + } + + [CmifCommand(2)] + public Result Stop() + { + return new Result((int)_impl.Stop()); + } + + [CmifCommand(3)] + public Result AppendAudioOutBuffer(ulong bufferTag, [Buffer(HipcBufferFlags.In | HipcBufferFlags.MapAlias)] ReadOnlySpan buffer) + { + AudioUserBuffer userBuffer = default; + + if (buffer.Length > 0) + { + userBuffer = buffer[0]; + } + + return new Result((int)_impl.AppendBuffer(bufferTag, ref userBuffer)); + } + + [CmifCommand(4)] + public Result RegisterBufferEvent([CopyHandle] out int eventHandle) + { + eventHandle = 0; + + if (_impl.RegisterBufferEvent() is AudioEvent audioEvent) + { + eventHandle = audioEvent.GetReadableHandle(); + } + + return Result.Success; + } + + [CmifCommand(5)] + public Result GetReleasedAudioOutBuffers(out uint count, [Buffer(HipcBufferFlags.Out | HipcBufferFlags.MapAlias)] Span bufferTags) + { + return new Result((int)_impl.GetReleasedBuffer(bufferTags, out count)); + } + + [CmifCommand(6)] + public Result ContainsAudioOutBuffer(out bool contains, ulong bufferTag) + { + contains = _impl.ContainsBuffer(bufferTag); + + return Result.Success; + } + + [CmifCommand(7)] // 3.0.0+ + public Result AppendAudioOutBufferAuto(ulong bufferTag, [Buffer(HipcBufferFlags.In | HipcBufferFlags.AutoSelect)] ReadOnlySpan buffer) + { + return AppendAudioOutBuffer(bufferTag, buffer); + } + + [CmifCommand(8)] // 3.0.0+ + public Result GetReleasedAudioOutBuffersAuto(out uint count, [Buffer(HipcBufferFlags.Out | HipcBufferFlags.AutoSelect)] Span bufferTags) + { + return GetReleasedAudioOutBuffers(out count, bufferTags); + } + + [CmifCommand(9)] // 4.0.0+ + public Result GetAudioOutBufferCount(out uint bufferCount) + { + bufferCount = _impl.GetBufferCount(); + + return Result.Success; + } + + [CmifCommand(10)] // 4.0.0+ + public Result GetAudioOutPlayedSampleCount(out ulong sampleCount) + { + sampleCount = _impl.GetPlayedSampleCount(); + + return Result.Success; + } + + [CmifCommand(11)] // 4.0.0+ + public Result FlushAudioOutBuffers(out bool pending) + { + pending = _impl.FlushBuffers(); + + return Result.Success; + } + + [CmifCommand(12)] // 6.0.0+ + public Result SetAudioOutVolume(float volume) + { + _impl.SetVolume(volume); + + return Result.Success; + } + + [CmifCommand(13)] // 6.0.0+ + public Result GetAudioOutVolume(out float volume) + { + volume = _impl.GetVolume(); + + return Result.Success; + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + _impl.Dispose(); + + if (_processHandle != 0) + { + HorizonStatic.Syscall.CloseHandle(_processHandle); + + _processHandle = 0; + } + } + } + + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Audio/Detail/AudioOutManager.cs b/src/Ryujinx.Horizon/Sdk/Audio/Detail/AudioOutManager.cs new file mode 100644 index 000000000..3d129470c --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Audio/Detail/AudioOutManager.cs @@ -0,0 +1,93 @@ +using Ryujinx.Audio; +using Ryujinx.Audio.Common; +using Ryujinx.Audio.Output; +using Ryujinx.Horizon.Common; +using Ryujinx.Horizon.Sdk.Applet; +using Ryujinx.Horizon.Sdk.Sf; +using Ryujinx.Horizon.Sdk.Sf.Hipc; +using System; + +namespace Ryujinx.Horizon.Sdk.Audio.Detail +{ + partial class AudioOutManager : IAudioOutManager + { + private readonly AudioOutputManager _impl; + + public AudioOutManager(AudioOutputManager impl) + { + _impl = impl; + } + + [CmifCommand(0)] + public Result ListAudioOuts(out int count, [Buffer(HipcBufferFlags.Out | HipcBufferFlags.MapAlias)] Span names) + { + string[] deviceNames = _impl.ListAudioOuts(); + + count = 0; + + foreach (string deviceName in deviceNames) + { + if (count >= names.Length) + { + break; + } + + names[count++] = new DeviceName(deviceName); + } + + return Result.Success; + } + + [CmifCommand(1)] + public Result OpenAudioOut( + out AudioOutputConfiguration outputConfig, + out IAudioOut audioOut, + [Buffer(HipcBufferFlags.Out | HipcBufferFlags.MapAlias)] Span outName, + AudioInputConfiguration parameter, + AppletResourceUserId appletResourceId, + [CopyHandle] int processHandle, + [Buffer(HipcBufferFlags.In | HipcBufferFlags.MapAlias)] ReadOnlySpan name, + [ClientProcessId] ulong pid) + { + var clientMemoryManager = HorizonStatic.Syscall.GetMemoryManagerByProcessHandle(processHandle); + + ResultCode rc = _impl.OpenAudioOut( + out string outputDeviceName, + out outputConfig, + out AudioOutputSystem outSystem, + clientMemoryManager, + name.Length > 0 ? name[0].ToString() : string.Empty, + SampleFormat.PcmInt16, + ref parameter); + + if (rc == ResultCode.Success && outName.Length > 0) + { + outName[0] = new DeviceName(outputDeviceName); + } + + audioOut = new AudioOut(outSystem, processHandle); + + return new Result((int)rc); + } + + [CmifCommand(2)] // 3.0.0+ + public Result ListAudioOutsAuto(out int count, [Buffer(HipcBufferFlags.Out | HipcBufferFlags.AutoSelect)] Span names) + { + return ListAudioOuts(out count, names); + } + + [CmifCommand(3)] // 3.0.0+ + public Result OpenAudioOutAuto( + out AudioOutputConfiguration outputConfig, + out IAudioOut audioOut, + [Buffer(HipcBufferFlags.Out | HipcBufferFlags.AutoSelect)] Span outName, + AudioInputConfiguration parameter, + AppletResourceUserId appletResourceId, + [CopyHandle] int processHandle, + [Buffer(HipcBufferFlags.In | HipcBufferFlags.AutoSelect)] ReadOnlySpan name, + [ClientProcessId] ulong pid) + { + return OpenAudioOut(out outputConfig, out audioOut, outName, parameter, appletResourceId, processHandle, name, pid); + } + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Audio/Detail/AudioRenderer.cs b/src/Ryujinx.Horizon/Sdk/Audio/Detail/AudioRenderer.cs new file mode 100644 index 000000000..4d446bba7 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Audio/Detail/AudioRenderer.cs @@ -0,0 +1,178 @@ +using Ryujinx.Audio; +using Ryujinx.Audio.Integration; +using Ryujinx.Audio.Renderer.Server; +using Ryujinx.Common.Memory; +using Ryujinx.Horizon.Common; +using Ryujinx.Horizon.Sdk.Sf; +using Ryujinx.Horizon.Sdk.Sf.Hipc; +using System; +using System.Buffers; + +namespace Ryujinx.Horizon.Sdk.Audio.Detail +{ + partial class AudioRenderer : IAudioRenderer, IDisposable + { + private readonly AudioRenderSystem _renderSystem; + private int _workBufferHandle; + private int _processHandle; + + public AudioRenderer(AudioRenderSystem renderSystem, int workBufferHandle, int processHandle) + { + _renderSystem = renderSystem; + _workBufferHandle = workBufferHandle; + _processHandle = processHandle; + } + + [CmifCommand(0)] + public Result GetSampleRate(out int sampleRate) + { + sampleRate = (int)_renderSystem.GetSampleRate(); + + return Result.Success; + } + + [CmifCommand(1)] + public Result GetSampleCount(out int sampleCount) + { + sampleCount = (int)_renderSystem.GetSampleCount(); + + return Result.Success; + } + + [CmifCommand(2)] + public Result GetMixBufferCount(out int mixBufferCount) + { + mixBufferCount = (int)_renderSystem.GetMixBufferCount(); + + return Result.Success; + } + + [CmifCommand(3)] + public Result GetState(out int state) + { + state = _renderSystem.IsActive() ? 0 : 1; + + return Result.Success; + } + + [CmifCommand(4)] + public Result RequestUpdate( + [Buffer(HipcBufferFlags.Out | HipcBufferFlags.MapAlias)] Memory output, + [Buffer(HipcBufferFlags.Out | HipcBufferFlags.MapAlias)] Memory performanceOutput, + [Buffer(HipcBufferFlags.In | HipcBufferFlags.MapAlias)] ReadOnlySequence input) + { + using MemoryHandle outputHandle = output.Pin(); + using MemoryHandle performanceOutputHandle = performanceOutput.Pin(); + + Result result = new Result((int)_renderSystem.Update(output, performanceOutput, input)); + + return result; + } + + [CmifCommand(5)] + public Result Start() + { + _renderSystem.Start(); + + return Result.Success; + } + + [CmifCommand(6)] + public Result Stop() + { + _renderSystem.Stop(); + + return Result.Success; + } + + [CmifCommand(7)] + public Result QuerySystemEvent([CopyHandle] out int eventHandle) + { + ResultCode rc = _renderSystem.QuerySystemEvent(out IWritableEvent systemEvent); + + eventHandle = 0; + + if (rc == ResultCode.Success && systemEvent is AudioEvent audioEvent) + { + eventHandle = audioEvent.GetReadableHandle(); + } + + return new Result((int)rc); + } + + [CmifCommand(8)] + public Result SetRenderingTimeLimit(int percent) + { + _renderSystem.SetRenderingTimeLimitPercent((uint)percent); + + return Result.Success; + } + + [CmifCommand(9)] + public Result GetRenderingTimeLimit(out int percent) + { + percent = (int)_renderSystem.GetRenderingTimeLimit(); + + return Result.Success; + } + + [CmifCommand(10)] // 3.0.0+ + public Result RequestUpdateAuto( + [Buffer(HipcBufferFlags.Out | HipcBufferFlags.AutoSelect)] Memory output, + [Buffer(HipcBufferFlags.Out | HipcBufferFlags.AutoSelect)] Memory performanceOutput, + [Buffer(HipcBufferFlags.In | HipcBufferFlags.AutoSelect)] ReadOnlySequence input) + { + return RequestUpdate(output, performanceOutput, input); + } + + [CmifCommand(11)] // 3.0.0+ + public Result ExecuteAudioRendererRendering() + { + return new Result((int)_renderSystem.ExecuteAudioRendererRendering()); + } + + [CmifCommand(12)] // 15.0.0+ + public Result SetVoiceDropParameter(float voiceDropParameter) + { + _renderSystem.SetVoiceDropParameter(voiceDropParameter); + + return Result.Success; + } + + [CmifCommand(13)] // 15.0.0+ + public Result GetVoiceDropParameter(out float voiceDropParameter) + { + voiceDropParameter = _renderSystem.GetVoiceDropParameter(); + + return Result.Success; + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + _renderSystem.Dispose(); + + if (_workBufferHandle != 0) + { + HorizonStatic.Syscall.CloseHandle(_workBufferHandle); + + _workBufferHandle = 0; + } + + if (_processHandle != 0) + { + HorizonStatic.Syscall.CloseHandle(_processHandle); + + _processHandle = 0; + } + } + } + + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Audio/Detail/AudioRendererManager.cs b/src/Ryujinx.Horizon/Sdk/Audio/Detail/AudioRendererManager.cs new file mode 100644 index 000000000..7138d27ce --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Audio/Detail/AudioRendererManager.cs @@ -0,0 +1,132 @@ +using Ryujinx.Audio.Renderer.Device; +using Ryujinx.Audio.Renderer.Server; +using Ryujinx.Common.Logging; +using Ryujinx.Horizon.Common; +using Ryujinx.Horizon.Sdk.Applet; +using Ryujinx.Horizon.Sdk.Sf; + +namespace Ryujinx.Horizon.Sdk.Audio.Detail +{ + partial class AudioRendererManager : IAudioRendererManager + { + private const uint InitialRevision = ('R' << 0) | ('E' << 8) | ('V' << 16) | ('1' << 24); + + private readonly Ryujinx.Audio.Renderer.Server.AudioRendererManager _impl; + private readonly VirtualDeviceSessionRegistry _registry; + + public AudioRendererManager(Ryujinx.Audio.Renderer.Server.AudioRendererManager impl, VirtualDeviceSessionRegistry registry) + { + _impl = impl; + _registry = registry; + } + + [CmifCommand(0)] + public Result OpenAudioRenderer( + out IAudioRenderer renderer, + AudioRendererParameterInternal parameter, + [CopyHandle] int workBufferHandle, + [CopyHandle] int processHandle, + ulong workBufferSize, + AppletResourceUserId appletResourceId, + [ClientProcessId] ulong pid) + { + var clientMemoryManager = HorizonStatic.Syscall.GetMemoryManagerByProcessHandle(processHandle); + ulong workBufferAddress = HorizonStatic.Syscall.GetTransferMemoryAddress(workBufferHandle); + + Result result = new Result((int)_impl.OpenAudioRenderer( + out var renderSystem, + clientMemoryManager, + ref parameter.Configuration, + appletResourceId.Id, + workBufferAddress, + workBufferSize, + (uint)processHandle)); + + if (result.IsSuccess) + { + renderer = new AudioRenderer(renderSystem, workBufferHandle, processHandle); + } + else + { + renderer = null; + + HorizonStatic.Syscall.CloseHandle(workBufferHandle); + HorizonStatic.Syscall.CloseHandle(processHandle); + } + + return result; + } + + [CmifCommand(1)] + public Result GetWorkBufferSize(out long workBufferSize, AudioRendererParameterInternal parameter) + { + if (BehaviourContext.CheckValidRevision(parameter.Configuration.Revision)) + { + workBufferSize = (long)Ryujinx.Audio.Renderer.Server.AudioRendererManager.GetWorkBufferSize(ref parameter.Configuration); + + Logger.Debug?.Print(LogClass.ServiceAudio, $"WorkBufferSize is 0x{workBufferSize:x16}."); + + return Result.Success; + } + else + { + workBufferSize = 0; + + Logger.Warning?.Print(LogClass.ServiceAudio, $"Library Revision REV{BehaviourContext.GetRevisionNumber(parameter.Configuration.Revision)} is not supported!"); + + return AudioResult.UnsupportedRevision; + } + } + + [CmifCommand(2)] + public Result GetAudioDeviceService(out IAudioDevice audioDevice, AppletResourceUserId appletResourceId) + { + audioDevice = new AudioDevice(_registry, appletResourceId, InitialRevision); + + return Result.Success; + } + + [CmifCommand(3)] // 3.0.0+ + public Result OpenAudioRendererForManualExecution( + out IAudioRenderer renderer, + AudioRendererParameterInternal parameter, + ulong workBufferAddress, + [CopyHandle] int processHandle, + ulong workBufferSize, + AppletResourceUserId appletResourceId, + [ClientProcessId] ulong pid) + { + var clientMemoryManager = HorizonStatic.Syscall.GetMemoryManagerByProcessHandle(processHandle); + + Result result = new Result((int)_impl.OpenAudioRenderer( + out var renderSystem, + clientMemoryManager, + ref parameter.Configuration, + appletResourceId.Id, + workBufferAddress, + workBufferSize, + (uint)processHandle)); + + if (result.IsSuccess) + { + renderer = new AudioRenderer(renderSystem, 0, processHandle); + } + else + { + renderer = null; + + HorizonStatic.Syscall.CloseHandle(processHandle); + } + + return result; + } + + [CmifCommand(4)] // 4.0.0+ + public Result GetAudioDeviceServiceWithRevisionInfo(out IAudioDevice audioDevice, AppletResourceUserId appletResourceId, uint revision) + { + audioDevice = new AudioDevice(_registry, appletResourceId, revision); + + return Result.Success; + } + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Audio/Detail/AudioRendererParameterInternal.cs b/src/Ryujinx.Horizon/Sdk/Audio/Detail/AudioRendererParameterInternal.cs new file mode 100644 index 000000000..e5fcf7b3b --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Audio/Detail/AudioRendererParameterInternal.cs @@ -0,0 +1,14 @@ +using Ryujinx.Audio.Renderer.Parameter; + +namespace Ryujinx.Horizon.Sdk.Audio.Detail +{ + struct AudioRendererParameterInternal + { + public AudioRendererConfiguration Configuration; + + public AudioRendererParameterInternal(AudioRendererConfiguration configuration) + { + Configuration = configuration; + } + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Audio/Detail/AudioSnoopManager.cs b/src/Ryujinx.Horizon/Sdk/Audio/Detail/AudioSnoopManager.cs new file mode 100644 index 000000000..cf1fe3d1d --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Audio/Detail/AudioSnoopManager.cs @@ -0,0 +1,30 @@ +using Ryujinx.Horizon.Common; +using Ryujinx.Horizon.Sdk.Sf; + +namespace Ryujinx.Horizon.Sdk.Audio.Detail +{ + partial class AudioSnoopManager : IAudioSnoopManager + { + // Note: The interface changed completely on firmware 17.0.0, this implementation is for older firmware. + + [CmifCommand(0)] + public Result EnableDspUsageMeasurement() + { + return Result.Success; + } + + [CmifCommand(1)] + public Result DisableDspUsageMeasurement() + { + return Result.Success; + } + + [CmifCommand(6)] + public Result GetDspUsage(out uint usage) + { + usage = 0; + + return Result.Success; + } + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Audio/Detail/DeviceName.cs b/src/Ryujinx.Horizon/Sdk/Audio/Detail/DeviceName.cs new file mode 100644 index 000000000..b77e2f402 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Audio/Detail/DeviceName.cs @@ -0,0 +1,30 @@ +using Ryujinx.Common.Memory; +using System; +using System.Runtime.InteropServices; +using System.Text; + +namespace Ryujinx.Horizon.Sdk.Audio.Detail +{ + [StructLayout(LayoutKind.Sequential, Size = 0x100, Pack = 1)] + struct DeviceName + { + public Array256 Name; + + public DeviceName(string name) + { + Name = new(); + Encoding.ASCII.GetBytes(name, Name.AsSpan()); + } + + public override string ToString() + { + int length = Name.AsSpan().IndexOf((byte)0); + if (length < 0) + { + length = 0x100; + } + + return Encoding.ASCII.GetString(Name.AsSpan()[..length]); + } + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Audio/Detail/FinalOutputRecorder.cs b/src/Ryujinx.Horizon/Sdk/Audio/Detail/FinalOutputRecorder.cs new file mode 100644 index 000000000..393914371 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Audio/Detail/FinalOutputRecorder.cs @@ -0,0 +1,147 @@ +using Ryujinx.Common.Logging; +using Ryujinx.Horizon.Common; +using Ryujinx.Horizon.Sdk.OsTypes; +using Ryujinx.Horizon.Sdk.Sf; +using Ryujinx.Horizon.Sdk.Sf.Hipc; +using System; + +namespace Ryujinx.Horizon.Sdk.Audio.Detail +{ + partial class FinalOutputRecorder : IFinalOutputRecorder, IDisposable + { + private int _processHandle; + private SystemEventType _event; + + public FinalOutputRecorder(int processHandle) + { + _processHandle = processHandle; + Os.CreateSystemEvent(out _event, EventClearMode.ManualClear, interProcess: true); + } + + [CmifCommand(0)] + public Result GetFinalOutputRecorderState(out uint state) + { + state = 0; + + Logger.Stub?.PrintStub(LogClass.ServiceAudio); + + return Result.Success; + } + + [CmifCommand(1)] + public Result Start() + { + Logger.Stub?.PrintStub(LogClass.ServiceAudio); + + return Result.Success; + } + + [CmifCommand(2)] + public Result Stop() + { + Logger.Stub?.PrintStub(LogClass.ServiceAudio); + + return Result.Success; + } + + [CmifCommand(3)] + public Result AppendFinalOutputRecorderBuffer([Buffer(HipcBufferFlags.In | HipcBufferFlags.MapAlias)] ReadOnlySpan buffer, ulong bufferClientPtr) + { + Logger.Stub?.PrintStub(LogClass.ServiceAudio, new { bufferClientPtr }); + + return Result.Success; + } + + [CmifCommand(4)] + public Result RegisterBufferEvent([CopyHandle] out int eventHandle) + { + eventHandle = Os.GetReadableHandleOfSystemEvent(ref _event); + + Logger.Stub?.PrintStub(LogClass.ServiceAudio); + + return Result.Success; + } + + [CmifCommand(5)] + public Result GetReleasedFinalOutputRecorderBuffers([Buffer(HipcBufferFlags.Out | HipcBufferFlags.MapAlias)] Span buffer, out uint count, out ulong released) + { + count = 0; + released = 0; + + Logger.Stub?.PrintStub(LogClass.ServiceAudio); + + return Result.Success; + } + + [CmifCommand(6)] + public Result ContainsFinalOutputRecorderBuffer(ulong bufferPointer, out bool contains) + { + contains = false; + + Logger.Stub?.PrintStub(LogClass.ServiceAudio, new { bufferPointer }); + + return Result.Success; + } + + [CmifCommand(7)] + public Result GetFinalOutputRecorderBufferEndTime(ulong bufferPointer, out ulong released) + { + released = 0; + + Logger.Stub?.PrintStub(LogClass.ServiceAudio, new { bufferPointer }); + + return Result.Success; + } + + [CmifCommand(8)] // 3.0.0+ + public Result AppendFinalOutputRecorderBufferAuto([Buffer(HipcBufferFlags.In | HipcBufferFlags.AutoSelect)] ReadOnlySpan buffer, ulong bufferClientPtr) + { + return AppendFinalOutputRecorderBuffer(buffer, bufferClientPtr); + } + + [CmifCommand(9)] // 3.0.0+ + public Result GetReleasedFinalOutputRecorderBuffersAuto([Buffer(HipcBufferFlags.Out | HipcBufferFlags.AutoSelect)] Span buffer, out uint count, out ulong released) + { + return GetReleasedFinalOutputRecorderBuffers(buffer, out count, out released); + } + + [CmifCommand(10)] // 6.0.0+ + public Result FlushFinalOutputRecorderBuffers(out bool pending) + { + pending = false; + + Logger.Stub?.PrintStub(LogClass.ServiceAudio); + + return Result.Success; + } + + [CmifCommand(11)] // 9.0.0+ + public Result AttachWorkBuffer(FinalOutputRecorderParameterInternal parameter) + { + Logger.Stub?.PrintStub(LogClass.ServiceAudio, new { parameter }); + + return Result.Success; + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + Os.DestroySystemEvent(ref _event); + + if (_processHandle != 0) + { + HorizonStatic.Syscall.CloseHandle(_processHandle); + + _processHandle = 0; + } + } + } + + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Audio/Detail/FinalOutputRecorderManager.cs b/src/Ryujinx.Horizon/Sdk/Audio/Detail/FinalOutputRecorderManager.cs new file mode 100644 index 000000000..76491bb79 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Audio/Detail/FinalOutputRecorderManager.cs @@ -0,0 +1,23 @@ +using Ryujinx.Horizon.Common; +using Ryujinx.Horizon.Sdk.Applet; +using Ryujinx.Horizon.Sdk.Sf; + +namespace Ryujinx.Horizon.Sdk.Audio.Detail +{ + partial class FinalOutputRecorderManager : IFinalOutputRecorderManager + { + [CmifCommand(0)] + public Result OpenFinalOutputRecorder( + out IFinalOutputRecorder recorder, + FinalOutputRecorderParameter parameter, + [CopyHandle] int processHandle, + out FinalOutputRecorderParameterInternal outParameter, + AppletResourceUserId appletResourceId) + { + recorder = new FinalOutputRecorder(processHandle); + outParameter = new(parameter.SampleRate, 2, 0); + + return Result.Success; + } + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Audio/Detail/FinalOutputRecorderParameter.cs b/src/Ryujinx.Horizon/Sdk/Audio/Detail/FinalOutputRecorderParameter.cs new file mode 100644 index 000000000..afa060fca --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Audio/Detail/FinalOutputRecorderParameter.cs @@ -0,0 +1,17 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.Horizon.Sdk.Audio.Detail +{ + [StructLayout(LayoutKind.Sequential, Size = 0x8, Pack = 0x4)] + readonly struct FinalOutputRecorderParameter + { + public readonly uint SampleRate; + public readonly uint Padding; + + public FinalOutputRecorderParameter(uint sampleRate) + { + SampleRate = sampleRate; + Padding = 0; + } + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Audio/Detail/FinalOutputRecorderParameterInternal.cs b/src/Ryujinx.Horizon/Sdk/Audio/Detail/FinalOutputRecorderParameterInternal.cs new file mode 100644 index 000000000..e88398eba --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Audio/Detail/FinalOutputRecorderParameterInternal.cs @@ -0,0 +1,21 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.Horizon.Sdk.Audio.Detail +{ + [StructLayout(LayoutKind.Sequential, Size = 0x10, Pack = 0x4)] + readonly struct FinalOutputRecorderParameterInternal + { + public readonly uint SampleRate; + public readonly uint ChannelCount; + public readonly uint UseLargeFrameSize; + public readonly uint Padding; + + public FinalOutputRecorderParameterInternal(uint sampleRate, uint channelCount, uint useLargeFrameSize) + { + SampleRate = sampleRate; + ChannelCount = channelCount; + UseLargeFrameSize = useLargeFrameSize; + Padding = 0; + } + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Audio/Detail/IAudioDevice.cs b/src/Ryujinx.Horizon/Sdk/Audio/Detail/IAudioDevice.cs new file mode 100644 index 000000000..3df1fe227 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Audio/Detail/IAudioDevice.cs @@ -0,0 +1,24 @@ +using Ryujinx.Horizon.Common; +using Ryujinx.Horizon.Sdk.Sf; +using System; + +namespace Ryujinx.Horizon.Sdk.Audio.Detail +{ + interface IAudioDevice : IServiceObject + { + Result ListAudioDeviceName(Span names, out int nameCount); + Result SetAudioDeviceOutputVolume(ReadOnlySpan name, float volume); + Result GetAudioDeviceOutputVolume(ReadOnlySpan name, out float volume); + Result GetActiveAudioDeviceName(Span name); + Result QueryAudioDeviceSystemEvent(out int eventHandle); + Result GetActiveChannelCount(out int channelCount); + Result ListAudioDeviceNameAuto(Span names, out int nameCount); + Result SetAudioDeviceOutputVolumeAuto(ReadOnlySpan name, float volume); + Result GetAudioDeviceOutputVolumeAuto(ReadOnlySpan name, out float volume); + Result GetActiveAudioDeviceNameAuto(Span name); + Result QueryAudioDeviceInputEvent(out int eventHandle); + Result QueryAudioDeviceOutputEvent(out int eventHandle); + Result GetActiveAudioOutputDeviceName(Span name); + Result ListAudioOutputDeviceName(Span names, out int nameCount); + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Audio/Detail/IAudioIn.cs b/src/Ryujinx.Horizon/Sdk/Audio/Detail/IAudioIn.cs new file mode 100644 index 000000000..bdc3bcf62 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Audio/Detail/IAudioIn.cs @@ -0,0 +1,26 @@ +using Ryujinx.Audio.Common; +using Ryujinx.Horizon.Common; +using Ryujinx.Horizon.Sdk.Sf; +using System; + +namespace Ryujinx.Horizon.Sdk.Audio.Detail +{ + interface IAudioIn : IServiceObject + { + Result GetAudioInState(out AudioDeviceState state); + Result Start(); + Result Stop(); + Result AppendAudioInBuffer(ulong bufferTag, ReadOnlySpan buffer); + Result RegisterBufferEvent(out int eventHandle); + Result GetReleasedAudioInBuffers(out uint count, Span bufferTags); + Result ContainsAudioInBuffer(out bool contains, ulong bufferTag); + Result AppendUacInBuffer(ulong bufferTag, ReadOnlySpan buffer, int eventHandle); + Result AppendAudioInBufferAuto(ulong bufferTag, ReadOnlySpan buffer); + Result GetReleasedAudioInBuffersAuto(out uint count, Span bufferTags); + Result AppendUacInBufferAuto(ulong bufferTag, ReadOnlySpan buffer, int eventHandle); + Result GetAudioInBufferCount(out uint bufferCount); + Result SetDeviceGain(float gain); + Result GetDeviceGain(out float gain); + Result FlushAudioInBuffers(out bool pending); + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Audio/Detail/IAudioInManager.cs b/src/Ryujinx.Horizon/Sdk/Audio/Detail/IAudioInManager.cs new file mode 100644 index 000000000..e7f32fbd2 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Audio/Detail/IAudioInManager.cs @@ -0,0 +1,43 @@ +using Ryujinx.Audio.Common; +using Ryujinx.Horizon.Common; +using Ryujinx.Horizon.Sdk.Applet; +using Ryujinx.Horizon.Sdk.Sf; +using System; + +namespace Ryujinx.Horizon.Sdk.Audio.Detail +{ + interface IAudioInManager : IServiceObject + { + Result ListAudioIns(out int count, Span names); + Result OpenAudioIn( + out AudioOutputConfiguration outputConfig, + out IAudioIn audioIn, + Span outName, + AudioInputConfiguration parameter, + AppletResourceUserId appletResourceId, + int processHandle, + ReadOnlySpan name, + ulong pid); + Result ListAudioInsAuto(out int count, Span names); + Result OpenAudioInAuto( + out AudioOutputConfiguration outputConfig, + out IAudioIn audioIn, + Span outName, + AudioInputConfiguration parameter, + AppletResourceUserId appletResourceId, + int processHandle, + ReadOnlySpan name, + ulong pid); + Result ListAudioInsAutoFiltered(out int count, Span names); + Result OpenAudioInProtocolSpecified( + out AudioOutputConfiguration outputConfig, + out IAudioIn audioIn, + Span outName, + AudioInProtocol protocol, + AudioInputConfiguration parameter, + AppletResourceUserId appletResourceId, + int processHandle, + ReadOnlySpan name, + ulong pid); + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Audio/Detail/IAudioOut.cs b/src/Ryujinx.Horizon/Sdk/Audio/Detail/IAudioOut.cs new file mode 100644 index 000000000..1b2009260 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Audio/Detail/IAudioOut.cs @@ -0,0 +1,25 @@ +using Ryujinx.Audio.Common; +using Ryujinx.Horizon.Common; +using Ryujinx.Horizon.Sdk.Sf; +using System; + +namespace Ryujinx.Horizon.Sdk.Audio.Detail +{ + interface IAudioOut : IServiceObject + { + Result GetAudioOutState(out AudioDeviceState state); + Result Start(); + Result Stop(); + Result AppendAudioOutBuffer(ulong bufferTag, ReadOnlySpan buffer); + Result RegisterBufferEvent(out int eventHandle); + Result GetReleasedAudioOutBuffers(out uint count, Span bufferTags); + Result ContainsAudioOutBuffer(out bool contains, ulong bufferTag); + Result AppendAudioOutBufferAuto(ulong bufferTag, ReadOnlySpan buffer); + Result GetReleasedAudioOutBuffersAuto(out uint count, Span bufferTags); + Result GetAudioOutBufferCount(out uint bufferCount); + Result GetAudioOutPlayedSampleCount(out ulong sampleCount); + Result FlushAudioOutBuffers(out bool pending); + Result SetAudioOutVolume(float volume); + Result GetAudioOutVolume(out float volume); + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Audio/Detail/IAudioOutManager.cs b/src/Ryujinx.Horizon/Sdk/Audio/Detail/IAudioOutManager.cs new file mode 100644 index 000000000..40d62836b --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Audio/Detail/IAudioOutManager.cs @@ -0,0 +1,32 @@ +using Ryujinx.Audio.Common; +using Ryujinx.Horizon.Common; +using Ryujinx.Horizon.Sdk.Applet; +using Ryujinx.Horizon.Sdk.Sf; +using System; + +namespace Ryujinx.Horizon.Sdk.Audio.Detail +{ + interface IAudioOutManager : IServiceObject + { + Result ListAudioOuts(out int count, Span names); + Result OpenAudioOut( + out AudioOutputConfiguration outputConfig, + out IAudioOut audioOut, + Span outName, + AudioInputConfiguration inputConfig, + AppletResourceUserId appletResourceId, + int processHandle, + ReadOnlySpan name, + ulong pid); + Result ListAudioOutsAuto(out int count, Span names); + Result OpenAudioOutAuto( + out AudioOutputConfiguration outputConfig, + out IAudioOut audioOut, + Span outName, + AudioInputConfiguration inputConfig, + AppletResourceUserId appletResourceId, + int processHandle, + ReadOnlySpan name, + ulong pid); + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Audio/Detail/IAudioRenderer.cs b/src/Ryujinx.Horizon/Sdk/Audio/Detail/IAudioRenderer.cs new file mode 100644 index 000000000..b766bd73c --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Audio/Detail/IAudioRenderer.cs @@ -0,0 +1,25 @@ +using Ryujinx.Horizon.Common; +using Ryujinx.Horizon.Sdk.Sf; +using System; +using System.Buffers; + +namespace Ryujinx.Horizon.Sdk.Audio.Detail +{ + interface IAudioRenderer : IServiceObject + { + Result GetSampleRate(out int sampleRate); + Result GetSampleCount(out int sampleCount); + Result GetMixBufferCount(out int mixBufferCount); + Result GetState(out int state); + Result RequestUpdate(Memory output, Memory performanceOutput, ReadOnlySequence input); + Result Start(); + Result Stop(); + Result QuerySystemEvent(out int eventHandle); + Result SetRenderingTimeLimit(int percent); + Result GetRenderingTimeLimit(out int percent); + Result RequestUpdateAuto(Memory output, Memory performanceOutput, ReadOnlySequence input); + Result ExecuteAudioRendererRendering(); + Result SetVoiceDropParameter(float voiceDropParameter); + Result GetVoiceDropParameter(out float voiceDropParameter); + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Audio/Detail/IAudioRendererManager.cs b/src/Ryujinx.Horizon/Sdk/Audio/Detail/IAudioRendererManager.cs new file mode 100644 index 000000000..fe95a2084 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Audio/Detail/IAudioRendererManager.cs @@ -0,0 +1,29 @@ +using Ryujinx.Horizon.Common; +using Ryujinx.Horizon.Sdk.Applet; +using Ryujinx.Horizon.Sdk.Sf; + +namespace Ryujinx.Horizon.Sdk.Audio.Detail +{ + interface IAudioRendererManager : IServiceObject + { + Result OpenAudioRenderer( + out IAudioRenderer renderer, + AudioRendererParameterInternal parameter, + int processHandle, + int workBufferHandle, + ulong workBufferSize, + AppletResourceUserId appletUserId, + ulong pid); + Result GetWorkBufferSize(out long workBufferSize, AudioRendererParameterInternal parameter); + Result GetAudioDeviceService(out IAudioDevice audioDevice, AppletResourceUserId appletUserId); + Result OpenAudioRendererForManualExecution( + out IAudioRenderer renderer, + AudioRendererParameterInternal parameter, + ulong workBufferAddress, + int processHandle, + ulong workBufferSize, + AppletResourceUserId appletUserId, + ulong pid); + Result GetAudioDeviceServiceWithRevisionInfo(out IAudioDevice audioDevice, AppletResourceUserId appletUserId, uint revision); + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Audio/Detail/IAudioSnoopManager.cs b/src/Ryujinx.Horizon/Sdk/Audio/Detail/IAudioSnoopManager.cs new file mode 100644 index 000000000..72853886a --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Audio/Detail/IAudioSnoopManager.cs @@ -0,0 +1,12 @@ +using Ryujinx.Horizon.Common; +using Ryujinx.Horizon.Sdk.Sf; + +namespace Ryujinx.Horizon.Sdk.Audio.Detail +{ + interface IAudioSnoopManager : IServiceObject + { + Result EnableDspUsageMeasurement(); + Result DisableDspUsageMeasurement(); + Result GetDspUsage(out uint usage); + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Audio/Detail/IFinalOutputRecorder.cs b/src/Ryujinx.Horizon/Sdk/Audio/Detail/IFinalOutputRecorder.cs new file mode 100644 index 000000000..be21c38b7 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Audio/Detail/IFinalOutputRecorder.cs @@ -0,0 +1,22 @@ +using Ryujinx.Horizon.Common; +using Ryujinx.Horizon.Sdk.Sf; +using System; + +namespace Ryujinx.Horizon.Sdk.Audio.Detail +{ + interface IFinalOutputRecorder : IServiceObject + { + Result GetFinalOutputRecorderState(out uint state); + Result Start(); + Result Stop(); + Result AppendFinalOutputRecorderBuffer(ReadOnlySpan buffer, ulong bufferClientPtr); + Result RegisterBufferEvent(out int eventHandle); + Result GetReleasedFinalOutputRecorderBuffers(Span buffer, out uint count, out ulong released); + Result ContainsFinalOutputRecorderBuffer(ulong bufferPointer, out bool contains); + Result GetFinalOutputRecorderBufferEndTime(ulong bufferPointer, out ulong released); + Result AppendFinalOutputRecorderBufferAuto(ReadOnlySpan buffer, ulong bufferClientPtr); + Result GetReleasedFinalOutputRecorderBuffersAuto(Span buffer, out uint count, out ulong released); + Result FlushFinalOutputRecorderBuffers(out bool pending); + Result AttachWorkBuffer(FinalOutputRecorderParameterInternal parameter); + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Audio/Detail/IFinalOutputRecorderManager.cs b/src/Ryujinx.Horizon/Sdk/Audio/Detail/IFinalOutputRecorderManager.cs new file mode 100644 index 000000000..bac41ca91 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Audio/Detail/IFinalOutputRecorderManager.cs @@ -0,0 +1,16 @@ +using Ryujinx.Horizon.Common; +using Ryujinx.Horizon.Sdk.Applet; +using Ryujinx.Horizon.Sdk.Sf; + +namespace Ryujinx.Horizon.Sdk.Audio.Detail +{ + interface IFinalOutputRecorderManager : IServiceObject + { + Result OpenFinalOutputRecorder( + out IFinalOutputRecorder recorder, + FinalOutputRecorderParameter parameter, + int processHandle, + out FinalOutputRecorderParameterInternal outParameter, + AppletResourceUserId appletResourceId); + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Codec/CodecResult.cs b/src/Ryujinx.Horizon/Sdk/Codec/CodecResult.cs new file mode 100644 index 000000000..21508b7f1 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Codec/CodecResult.cs @@ -0,0 +1,16 @@ +using Ryujinx.Horizon.Common; + +namespace Ryujinx.Horizon.Sdk.Codec +{ + static class CodecResult + { + private const int ModuleId = 111; + + public static Result InvalidLength => new(ModuleId, 3); + public static Result OpusBadArg => new(ModuleId, 130); + public static Result OpusInvalidPacket => new(ModuleId, 133); + public static Result InvalidNumberOfStreams => new(ModuleId, 1000); + public static Result InvalidSampleRate => new(ModuleId, 1001); + public static Result InvalidChannelCount => new(ModuleId, 1002); + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Codec/Detail/HardwareOpusDecoder.cs b/src/Ryujinx.Horizon/Sdk/Codec/Detail/HardwareOpusDecoder.cs new file mode 100644 index 000000000..2146362df --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Codec/Detail/HardwareOpusDecoder.cs @@ -0,0 +1,375 @@ +using Concentus; +using Concentus.Enums; +using Concentus.Structs; +using Ryujinx.Common.Logging; +using Ryujinx.Horizon.Common; +using Ryujinx.Horizon.Sdk.Sf; +using Ryujinx.Horizon.Sdk.Sf.Hipc; +using System; +using System.Buffers.Binary; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace Ryujinx.Horizon.Sdk.Codec.Detail +{ + partial class HardwareOpusDecoder : IHardwareOpusDecoder, IDisposable + { + static HardwareOpusDecoder() + { + OpusCodecFactory.AttemptToUseNativeLibrary = false; + } + + [StructLayout(LayoutKind.Sequential)] + private struct OpusPacketHeader + { + public uint Length; + public uint FinalRange; + + public static OpusPacketHeader FromSpan(ReadOnlySpan data) + { + return new() + { + Length = BinaryPrimitives.ReadUInt32BigEndian(data), + FinalRange = BinaryPrimitives.ReadUInt32BigEndian(data[sizeof(uint)..]), + }; + } + } + + private interface IDecoder : IDisposable + { + int SampleRate { get; } + int ChannelsCount { get; } + + int Decode(ReadOnlySpan inData, Span outPcm, int frameSize); + void ResetState(); + } + + private class Decoder : IDecoder + { + private readonly IOpusDecoder _decoder; + + public int SampleRate => _decoder.SampleRate; + public int ChannelsCount => _decoder.NumChannels; + + public Decoder(int sampleRate, int channelsCount) + { + _decoder = OpusCodecFactory.CreateDecoder(sampleRate, channelsCount); + } + + public int Decode(ReadOnlySpan inData, Span outPcm, int frameSize) + { + return _decoder.Decode(inData, outPcm, frameSize); + } + + public void ResetState() + { + _decoder.ResetState(); + } + + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + _decoder?.Dispose(); + } + } + } + + private class MultiSampleDecoder : IDecoder + { + private readonly IOpusMultiStreamDecoder _decoder; + + public int SampleRate => _decoder.SampleRate; + public int ChannelsCount => _decoder.NumChannels; + + public MultiSampleDecoder(int sampleRate, int channelsCount, int streams, int coupledStreams, byte[] mapping) + { + _decoder = OpusCodecFactory.CreateMultiStreamDecoder(sampleRate, channelsCount, streams, coupledStreams, mapping); + } + + public int Decode(ReadOnlySpan inData, Span outPcm, int frameSize) + { + return _decoder.DecodeMultistream(inData, outPcm, frameSize, false); + } + + public void ResetState() + { + _decoder.ResetState(); + } + + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + _decoder?.Dispose(); + } + } + } + + private readonly IDecoder _decoder; + private int _workBufferHandle; + + private HardwareOpusDecoder(int workBufferHandle) + { + _workBufferHandle = workBufferHandle; + } + + public HardwareOpusDecoder(int sampleRate, int channelsCount, int workBufferHandle) : this(workBufferHandle) + { + _decoder = new Decoder(sampleRate, channelsCount); + } + + public HardwareOpusDecoder(int sampleRate, int channelsCount, int streams, int coupledStreams, byte[] mapping, int workBufferHandle) : this(workBufferHandle) + { + _decoder = new MultiSampleDecoder(sampleRate, channelsCount, streams, coupledStreams, mapping); + } + + [CmifCommand(0)] + public Result DecodeInterleavedOld( + out int outConsumed, + out int outSamples, + [Buffer(HipcBufferFlags.Out | HipcBufferFlags.MapAlias)] Span output, + [Buffer(HipcBufferFlags.In | HipcBufferFlags.MapAlias)] ReadOnlySpan input) + { + return DecodeInterleavedInternal(out outConsumed, out outSamples, out _, output, input, reset: false, withPerf: false); + } + + [CmifCommand(1)] + public Result SetContext(ReadOnlySpan context) + { + Logger.Stub?.PrintStub(LogClass.ServiceAudio); + + return Result.Success; + } + + [CmifCommand(2)] // 3.0.0+ + public Result DecodeInterleavedForMultiStreamOld( + out int outConsumed, + out int outSamples, + [Buffer(HipcBufferFlags.Out | HipcBufferFlags.MapAlias)] Span output, + [Buffer(HipcBufferFlags.In | HipcBufferFlags.MapAlias)] ReadOnlySpan input) + { + return DecodeInterleavedInternal(out outConsumed, out outSamples, out _, output, input, reset: false, withPerf: false); + } + + [CmifCommand(3)] // 3.0.0+ + public Result SetContextForMultiStream(ReadOnlySpan arg0) + { + Logger.Stub?.PrintStub(LogClass.ServiceAudio); + + return Result.Success; + } + + [CmifCommand(4)] // 4.0.0+ + public Result DecodeInterleavedWithPerfOld( + out int outConsumed, + out long timeTaken, + out int outSamples, + [Buffer(HipcBufferFlags.Out | HipcBufferFlags.MapAlias | HipcBufferFlags.MapTransferAllowsNonSecure)] Span output, + [Buffer(HipcBufferFlags.In | HipcBufferFlags.MapAlias)] ReadOnlySpan input) + { + return DecodeInterleavedInternal(out outConsumed, out outSamples, out timeTaken, output, input, reset: false, withPerf: true); + } + + [CmifCommand(5)] // 4.0.0+ + public Result DecodeInterleavedForMultiStreamWithPerfOld( + out int outConsumed, + out long timeTaken, + out int outSamples, + [Buffer(HipcBufferFlags.Out | HipcBufferFlags.MapAlias | HipcBufferFlags.MapTransferAllowsNonSecure)] Span output, + [Buffer(HipcBufferFlags.In | HipcBufferFlags.MapAlias)] ReadOnlySpan input) + { + return DecodeInterleavedInternal(out outConsumed, out outSamples, out timeTaken, output, input, reset: false, withPerf: true); + } + + [CmifCommand(6)] // 6.0.0+ + public Result DecodeInterleavedWithPerfAndResetOld( + out int outConsumed, + out long timeTaken, + out int outSamples, + [Buffer(HipcBufferFlags.Out | HipcBufferFlags.MapAlias | HipcBufferFlags.MapTransferAllowsNonSecure)] Span output, + [Buffer(HipcBufferFlags.In | HipcBufferFlags.MapAlias)] ReadOnlySpan input, + bool reset) + { + return DecodeInterleavedInternal(out outConsumed, out outSamples, out timeTaken, output, input, reset, withPerf: true); + } + + [CmifCommand(7)] // 6.0.0+ + public Result DecodeInterleavedForMultiStreamWithPerfAndResetOld( + out int outConsumed, + out long timeTaken, + out int outSamples, + [Buffer(HipcBufferFlags.Out | HipcBufferFlags.MapAlias | HipcBufferFlags.MapTransferAllowsNonSecure)] Span output, + [Buffer(HipcBufferFlags.In | HipcBufferFlags.MapAlias)] ReadOnlySpan input, + bool reset) + { + return DecodeInterleavedInternal(out outConsumed, out outSamples, out timeTaken, output, input, reset, withPerf: true); + } + + [CmifCommand(8)] // 7.0.0+ + public Result DecodeInterleaved( + out int outConsumed, + out long timeTaken, + out int outSamples, + [Buffer(HipcBufferFlags.Out | HipcBufferFlags.MapAlias | HipcBufferFlags.MapTransferAllowsNonSecure)] Span output, + [Buffer(HipcBufferFlags.In | HipcBufferFlags.MapAlias | HipcBufferFlags.MapTransferAllowsNonSecure)] ReadOnlySpan input, + bool reset) + { + return DecodeInterleavedInternal(out outConsumed, out outSamples, out timeTaken, output, input, reset, withPerf: true); + } + + [CmifCommand(9)] // 7.0.0+ + public Result DecodeInterleavedForMultiStream( + out int outConsumed, + out long timeTaken, + out int outSamples, + [Buffer(HipcBufferFlags.Out | HipcBufferFlags.MapAlias | HipcBufferFlags.MapTransferAllowsNonSecure)] Span output, + [Buffer(HipcBufferFlags.In | HipcBufferFlags.MapAlias | HipcBufferFlags.MapTransferAllowsNonSecure)] ReadOnlySpan input, + bool reset) + { + return DecodeInterleavedInternal(out outConsumed, out outSamples, out timeTaken, output, input, reset, withPerf: true); + } + + private Result DecodeInterleavedInternal( + out int outConsumed, + out int outSamples, + out long timeTaken, + Span output, + ReadOnlySpan input, + bool reset, + bool withPerf) + { + timeTaken = 0; + + Span outPcmSpace = MemoryMarshal.Cast(output); + Result result = DecodeInterleaved(_decoder, reset, input, outPcmSpace, output.Length, out outConsumed, out outSamples); + + if (withPerf) + { + // This is the time the DSP took to process the request, TODO: fill this. + timeTaken = 0; + } + + return result; + } + + private static Result GetPacketNumSamples(IDecoder decoder, out int numSamples, ReadOnlySpan packet) + { + int result = OpusPacketInfo.GetNumSamples(packet, decoder.SampleRate); + + numSamples = result; + + if (result == OpusError.OPUS_INVALID_PACKET) + { + return CodecResult.OpusInvalidPacket; + } + else if (result == OpusError.OPUS_BAD_ARG) + { + return CodecResult.OpusBadArg; + } + + return Result.Success; + } + + private static Result DecodeInterleaved( + IDecoder decoder, + bool reset, + ReadOnlySpan input, + Span outPcmData, + int outputSize, + out int outConsumed, + out int outSamples) + { + outConsumed = 0; + outSamples = 0; + + int streamSize = input.Length; + + if (streamSize < Unsafe.SizeOf()) + { + return CodecResult.InvalidLength; + } + + OpusPacketHeader header = OpusPacketHeader.FromSpan(input); + int headerSize = Unsafe.SizeOf(); + uint totalSize = header.Length + (uint)headerSize; + + if (totalSize > streamSize) + { + return CodecResult.InvalidLength; + } + + ReadOnlySpan opusData = input.Slice(headerSize, (int)header.Length); + + Result result = GetPacketNumSamples(decoder, out int numSamples, opusData); + + if (result.IsSuccess) + { + if ((uint)numSamples * (uint)decoder.ChannelsCount * sizeof(short) > outputSize) + { + return CodecResult.InvalidLength; + } + + if (reset) + { + decoder.ResetState(); + } + + try + { + outSamples = decoder.Decode(opusData, outPcmData, numSamples); + outConsumed = (int)totalSize; + } + catch (OpusException e) + { + switch (e.OpusErrorCode) + { + case OpusError.OPUS_BUFFER_TOO_SMALL: + return CodecResult.InvalidLength; + case OpusError.OPUS_BAD_ARG: + return CodecResult.OpusBadArg; + case OpusError.OPUS_INVALID_PACKET: + return CodecResult.OpusInvalidPacket; + default: + return CodecResult.InvalidLength; + } + } + } + + return Result.Success; + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + if (_workBufferHandle != 0) + { + HorizonStatic.Syscall.CloseHandle(_workBufferHandle); + + _workBufferHandle = 0; + } + + _decoder?.Dispose(); + } + } + + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Codec/Detail/HardwareOpusDecoderManager.cs b/src/Ryujinx.Horizon/Sdk/Codec/Detail/HardwareOpusDecoderManager.cs new file mode 100644 index 000000000..acec66e82 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Codec/Detail/HardwareOpusDecoderManager.cs @@ -0,0 +1,386 @@ +using Ryujinx.Common; +using Ryujinx.Horizon.Common; +using Ryujinx.Horizon.Sdk.Sf; +using Ryujinx.Horizon.Sdk.Sf.Hipc; +using System; + +namespace Ryujinx.Horizon.Sdk.Codec.Detail +{ + partial class HardwareOpusDecoderManager : IHardwareOpusDecoderManager + { + [CmifCommand(0)] + public Result OpenHardwareOpusDecoder( + out IHardwareOpusDecoder decoder, + HardwareOpusDecoderParameterInternal parameter, + [CopyHandle] int workBufferHandle, + int workBufferSize) + { + decoder = null; + + if (!IsValidSampleRate(parameter.SampleRate)) + { + HorizonStatic.Syscall.CloseHandle(workBufferHandle); + + return CodecResult.InvalidSampleRate; + } + + if (!IsValidChannelCount(parameter.ChannelsCount)) + { + HorizonStatic.Syscall.CloseHandle(workBufferHandle); + + return CodecResult.InvalidChannelCount; + } + + decoder = new HardwareOpusDecoder(parameter.SampleRate, parameter.ChannelsCount, workBufferHandle); + + return Result.Success; + } + + [CmifCommand(1)] + public Result GetWorkBufferSize(out int size, HardwareOpusDecoderParameterInternal parameter) + { + size = 0; + + if (!IsValidChannelCount(parameter.ChannelsCount)) + { + return CodecResult.InvalidChannelCount; + } + + if (!IsValidSampleRate(parameter.SampleRate)) + { + return CodecResult.InvalidSampleRate; + } + + int opusDecoderSize = GetOpusDecoderSize(parameter.ChannelsCount); + + int sampleRateRatio = parameter.SampleRate != 0 ? 48000 / parameter.SampleRate : 0; + int frameSize = BitUtils.AlignUp(sampleRateRatio != 0 ? parameter.ChannelsCount * 1920 / sampleRateRatio : 0, 64); + size = opusDecoderSize + 1536 + frameSize; + + return Result.Success; + } + + [CmifCommand(2)] // 3.0.0+ + public Result OpenHardwareOpusDecoderForMultiStream( + out IHardwareOpusDecoder decoder, + [Buffer(HipcBufferFlags.In | HipcBufferFlags.Pointer, 0x110)] in HardwareOpusMultiStreamDecoderParameterInternal parameter, + [CopyHandle] int workBufferHandle, + int workBufferSize) + { + decoder = null; + + if (!IsValidSampleRate(parameter.SampleRate)) + { + HorizonStatic.Syscall.CloseHandle(workBufferHandle); + + return CodecResult.InvalidSampleRate; + } + + if (!IsValidMultiChannelCount(parameter.ChannelsCount)) + { + HorizonStatic.Syscall.CloseHandle(workBufferHandle); + + return CodecResult.InvalidChannelCount; + } + + if (!IsValidNumberOfStreams(parameter.NumberOfStreams, parameter.NumberOfStereoStreams, parameter.ChannelsCount)) + { + HorizonStatic.Syscall.CloseHandle(workBufferHandle); + + return CodecResult.InvalidNumberOfStreams; + } + + decoder = new HardwareOpusDecoder( + parameter.SampleRate, + parameter.ChannelsCount, + parameter.NumberOfStreams, + parameter.NumberOfStereoStreams, + parameter.ChannelMappings.AsSpan().ToArray(), + workBufferHandle); + + return Result.Success; + } + + [CmifCommand(3)] // 3.0.0+ + public Result GetWorkBufferSizeForMultiStream( + out int size, + [Buffer(HipcBufferFlags.In | HipcBufferFlags.Pointer, 0x110)] in HardwareOpusMultiStreamDecoderParameterInternal parameter) + { + size = 0; + + if (!IsValidMultiChannelCount(parameter.ChannelsCount)) + { + return CodecResult.InvalidChannelCount; + } + + if (!IsValidSampleRate(parameter.SampleRate)) + { + return CodecResult.InvalidSampleRate; + } + + if (!IsValidNumberOfStreams(parameter.NumberOfStreams, parameter.NumberOfStereoStreams, parameter.ChannelsCount)) + { + return CodecResult.InvalidSampleRate; + } + + int opusDecoderSize = GetOpusMultistreamDecoderSize(parameter.NumberOfStreams, parameter.NumberOfStereoStreams); + + int streamSize = BitUtils.AlignUp(parameter.NumberOfStreams * 1500, 64); + int sampleRateRatio = parameter.SampleRate != 0 ? 48000 / parameter.SampleRate : 0; + int frameSize = BitUtils.AlignUp(sampleRateRatio != 0 ? parameter.ChannelsCount * 1920 / sampleRateRatio : 0, 64); + size = opusDecoderSize + streamSize + frameSize; + + return Result.Success; + } + + [CmifCommand(4)] // 12.0.0+ + public Result OpenHardwareOpusDecoderEx( + out IHardwareOpusDecoder decoder, + HardwareOpusDecoderParameterInternalEx parameter, + [CopyHandle] int workBufferHandle, + int workBufferSize) + { + decoder = null; + + if (!IsValidChannelCount(parameter.ChannelsCount)) + { + HorizonStatic.Syscall.CloseHandle(workBufferHandle); + + return CodecResult.InvalidChannelCount; + } + + if (!IsValidSampleRate(parameter.SampleRate)) + { + HorizonStatic.Syscall.CloseHandle(workBufferHandle); + + return CodecResult.InvalidSampleRate; + } + + decoder = new HardwareOpusDecoder(parameter.SampleRate, parameter.ChannelsCount, workBufferHandle); + + return Result.Success; + } + + [CmifCommand(5)] // 12.0.0+ + public Result GetWorkBufferSizeEx(out int size, HardwareOpusDecoderParameterInternalEx parameter) + { + return GetWorkBufferSizeExImpl(out size, in parameter, fromDsp: false); + } + + [CmifCommand(6)] // 12.0.0+ + public Result OpenHardwareOpusDecoderForMultiStreamEx( + out IHardwareOpusDecoder decoder, + [Buffer(HipcBufferFlags.In | HipcBufferFlags.Pointer, 0x118)] in HardwareOpusMultiStreamDecoderParameterInternalEx parameter, + [CopyHandle] int workBufferHandle, + int workBufferSize) + { + decoder = null; + + if (!IsValidSampleRate(parameter.SampleRate)) + { + HorizonStatic.Syscall.CloseHandle(workBufferHandle); + + return CodecResult.InvalidSampleRate; + } + + if (!IsValidMultiChannelCount(parameter.ChannelsCount)) + { + HorizonStatic.Syscall.CloseHandle(workBufferHandle); + + return CodecResult.InvalidChannelCount; + } + + if (!IsValidNumberOfStreams(parameter.NumberOfStreams, parameter.NumberOfStereoStreams, parameter.ChannelsCount)) + { + HorizonStatic.Syscall.CloseHandle(workBufferHandle); + + return CodecResult.InvalidNumberOfStreams; + } + + decoder = new HardwareOpusDecoder( + parameter.SampleRate, + parameter.ChannelsCount, + parameter.NumberOfStreams, + parameter.NumberOfStereoStreams, + parameter.ChannelMappings.AsSpan().ToArray(), + workBufferHandle); + + return Result.Success; + } + + [CmifCommand(7)] // 12.0.0+ + public Result GetWorkBufferSizeForMultiStreamEx( + out int size, + [Buffer(HipcBufferFlags.In | HipcBufferFlags.Pointer, 0x118)] in HardwareOpusMultiStreamDecoderParameterInternalEx parameter) + { + return GetWorkBufferSizeForMultiStreamExImpl(out size, in parameter, fromDsp: false); + } + + [CmifCommand(8)] // 16.0.0+ + public Result GetWorkBufferSizeExEx(out int size, HardwareOpusDecoderParameterInternalEx parameter) + { + return GetWorkBufferSizeExImpl(out size, in parameter, fromDsp: true); + } + + [CmifCommand(9)] // 16.0.0+ + public Result GetWorkBufferSizeForMultiStreamExEx( + out int size, + [Buffer(HipcBufferFlags.In | HipcBufferFlags.Pointer, 0x118)] in HardwareOpusMultiStreamDecoderParameterInternalEx parameter) + { + return GetWorkBufferSizeForMultiStreamExImpl(out size, in parameter, fromDsp: true); + } + + private Result GetWorkBufferSizeExImpl(out int size, in HardwareOpusDecoderParameterInternalEx parameter, bool fromDsp) + { + size = 0; + + if (!IsValidChannelCount(parameter.ChannelsCount)) + { + return CodecResult.InvalidChannelCount; + } + + if (!IsValidSampleRate(parameter.SampleRate)) + { + return CodecResult.InvalidSampleRate; + } + + int opusDecoderSize = fromDsp ? GetDspOpusDecoderSize(parameter.ChannelsCount) : GetOpusDecoderSize(parameter.ChannelsCount); + + int frameSizeMono48KHz = parameter.Flags.HasFlag(OpusDecoderFlags.LargeFrameSize) ? 5760 : 1920; + int sampleRateRatio = parameter.SampleRate != 0 ? 48000 / parameter.SampleRate : 0; + int frameSize = BitUtils.AlignUp(sampleRateRatio != 0 ? parameter.ChannelsCount * frameSizeMono48KHz / sampleRateRatio : 0, 64); + size = opusDecoderSize + 1536 + frameSize; + + return Result.Success; + } + + private Result GetWorkBufferSizeForMultiStreamExImpl(out int size, in HardwareOpusMultiStreamDecoderParameterInternalEx parameter, bool fromDsp) + { + size = 0; + + if (!IsValidMultiChannelCount(parameter.ChannelsCount)) + { + return CodecResult.InvalidChannelCount; + } + + if (!IsValidSampleRate(parameter.SampleRate)) + { + return CodecResult.InvalidSampleRate; + } + + if (!IsValidNumberOfStreams(parameter.NumberOfStreams, parameter.NumberOfStereoStreams, parameter.ChannelsCount)) + { + return CodecResult.InvalidSampleRate; + } + + int opusDecoderSize = fromDsp + ? GetDspOpusMultistreamDecoderSize(parameter.NumberOfStreams, parameter.NumberOfStereoStreams) + : GetOpusMultistreamDecoderSize(parameter.NumberOfStreams, parameter.NumberOfStereoStreams); + + int frameSizeMono48KHz = parameter.Flags.HasFlag(OpusDecoderFlags.LargeFrameSize) ? 5760 : 1920; + int streamSize = BitUtils.AlignUp(parameter.NumberOfStreams * 1500, 64); + int sampleRateRatio = parameter.SampleRate != 0 ? 48000 / parameter.SampleRate : 0; + int frameSize = BitUtils.AlignUp(sampleRateRatio != 0 ? parameter.ChannelsCount * frameSizeMono48KHz / sampleRateRatio : 0, 64); + size = opusDecoderSize + streamSize + frameSize; + + return Result.Success; + } + + private static int GetDspOpusDecoderSize(int channelsCount) + { + // TODO: Figure out the size returned here. + // Not really important because we don't use the work buffer, and the size being lower is fine. + + return 0; + } + + private static int GetDspOpusMultistreamDecoderSize(int streams, int coupledStreams) + { + // TODO: Figure out the size returned here. + // Not really important because we don't use the work buffer, and the size being lower is fine. + + return 0; + } + + private static int GetOpusDecoderSize(int channelsCount) + { + const int SilkDecoderSize = 0x2160; + + if (channelsCount < 1 || channelsCount > 2) + { + return 0; + } + + int celtDecoderSize = GetCeltDecoderSize(channelsCount); + int opusDecoderSize = GetOpusDecoderAllocSize(channelsCount) | 0x50; + + return opusDecoderSize + SilkDecoderSize + celtDecoderSize; + } + + private static int GetOpusMultistreamDecoderSize(int streams, int coupledStreams) + { + if (streams < 1 || coupledStreams > streams || coupledStreams < 0) + { + return 0; + } + + int coupledSize = GetOpusDecoderSize(2); + int monoSize = GetOpusDecoderSize(1); + + return Align4(monoSize - GetOpusDecoderAllocSize(1)) * (streams - coupledStreams) + + Align4(coupledSize - GetOpusDecoderAllocSize(2)) * coupledStreams + 0xb920; + } + + private static int Align4(int value) + { + return BitUtils.AlignUp(value, 4); + } + + private static int GetOpusDecoderAllocSize(int channelsCount) + { + return channelsCount * 0x800 + 0x4800; + } + + private static int GetCeltDecoderSize(int channelsCount) + { + const int DecodeBufferSize = 0x2030; + const int Overlap = 120; + const int EBandsCount = 21; + + return (DecodeBufferSize + Overlap * 4) * channelsCount + EBandsCount * 16 + 0x54; + } + + private static bool IsValidChannelCount(int channelsCount) + { + return channelsCount > 0 && channelsCount <= 2; + } + + private static bool IsValidMultiChannelCount(int channelsCount) + { + return channelsCount > 0 && channelsCount <= 255; + } + + private static bool IsValidSampleRate(int sampleRate) + { + switch (sampleRate) + { + case 8000: + case 12000: + case 16000: + case 24000: + case 48000: + return true; + } + + return false; + } + + private static bool IsValidNumberOfStreams(int numberOfStreams, int numberOfStereoStreams, int channelsCount) + { + return numberOfStreams > 0 && + numberOfStreams + numberOfStereoStreams <= channelsCount && + numberOfStereoStreams >= 0 && + numberOfStereoStreams <= numberOfStreams; + } + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Codec/Detail/HardwareOpusDecoderParameterInternal.cs b/src/Ryujinx.Horizon/Sdk/Codec/Detail/HardwareOpusDecoderParameterInternal.cs new file mode 100644 index 000000000..271a592c1 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Codec/Detail/HardwareOpusDecoderParameterInternal.cs @@ -0,0 +1,11 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.Horizon.Sdk.Codec.Detail +{ + [StructLayout(LayoutKind.Sequential, Size = 0x8, Pack = 0x4)] + struct HardwareOpusDecoderParameterInternal + { + public int SampleRate; + public int ChannelsCount; + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Codec/Detail/HardwareOpusDecoderParameterInternalEx.cs b/src/Ryujinx.Horizon/Sdk/Codec/Detail/HardwareOpusDecoderParameterInternalEx.cs new file mode 100644 index 000000000..e2b81c771 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Codec/Detail/HardwareOpusDecoderParameterInternalEx.cs @@ -0,0 +1,13 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.Horizon.Sdk.Codec.Detail +{ + [StructLayout(LayoutKind.Sequential, Size = 0x10, Pack = 0x4)] + struct HardwareOpusDecoderParameterInternalEx + { + public int SampleRate; + public int ChannelsCount; + public OpusDecoderFlags Flags; + public uint Reserved; + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Audio/Types/OpusMultiStreamParameters.cs b/src/Ryujinx.Horizon/Sdk/Codec/Detail/HardwareOpusMultiStreamDecoderParameterInternal.cs similarity index 65% rename from src/Ryujinx.HLE/HOS/Services/Audio/Types/OpusMultiStreamParameters.cs rename to src/Ryujinx.Horizon/Sdk/Codec/Detail/HardwareOpusMultiStreamDecoderParameterInternal.cs index fd63a4f79..98536a4f8 100644 --- a/src/Ryujinx.HLE/HOS/Services/Audio/Types/OpusMultiStreamParameters.cs +++ b/src/Ryujinx.Horizon/Sdk/Codec/Detail/HardwareOpusMultiStreamDecoderParameterInternal.cs @@ -1,15 +1,15 @@ using Ryujinx.Common.Memory; using System.Runtime.InteropServices; -namespace Ryujinx.HLE.HOS.Services.Audio.Types +namespace Ryujinx.Horizon.Sdk.Codec.Detail { [StructLayout(LayoutKind.Sequential, Size = 0x110)] - struct OpusMultiStreamParameters + struct HardwareOpusMultiStreamDecoderParameterInternal { public int SampleRate; public int ChannelsCount; public int NumberOfStreams; public int NumberOfStereoStreams; - public Array64 ChannelMappings; + public Array256 ChannelMappings; } } diff --git a/src/Ryujinx.HLE/HOS/Services/Audio/Types/OpusMultiStreamParametersEx.cs b/src/Ryujinx.Horizon/Sdk/Codec/Detail/HardwareOpusMultiStreamDecoderParameterInternalEx.cs similarity index 64% rename from src/Ryujinx.HLE/HOS/Services/Audio/Types/OpusMultiStreamParametersEx.cs rename to src/Ryujinx.Horizon/Sdk/Codec/Detail/HardwareOpusMultiStreamDecoderParameterInternalEx.cs index 1315c734e..8f8615dff 100644 --- a/src/Ryujinx.HLE/HOS/Services/Audio/Types/OpusMultiStreamParametersEx.cs +++ b/src/Ryujinx.Horizon/Sdk/Codec/Detail/HardwareOpusMultiStreamDecoderParameterInternalEx.cs @@ -1,19 +1,17 @@ using Ryujinx.Common.Memory; using System.Runtime.InteropServices; -namespace Ryujinx.HLE.HOS.Services.Audio.Types +namespace Ryujinx.Horizon.Sdk.Codec.Detail { [StructLayout(LayoutKind.Sequential, Size = 0x118)] - struct OpusMultiStreamParametersEx + struct HardwareOpusMultiStreamDecoderParameterInternalEx { public int SampleRate; public int ChannelsCount; public int NumberOfStreams; public int NumberOfStereoStreams; public OpusDecoderFlags Flags; - - Array4 Padding1; - - public Array64 ChannelMappings; + public uint Reserved; + public Array256 ChannelMappings; } } diff --git a/src/Ryujinx.Horizon/Sdk/Codec/Detail/IHardwareOpusDecoder.cs b/src/Ryujinx.Horizon/Sdk/Codec/Detail/IHardwareOpusDecoder.cs new file mode 100644 index 000000000..ae09ad15a --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Codec/Detail/IHardwareOpusDecoder.cs @@ -0,0 +1,20 @@ +using Ryujinx.Horizon.Common; +using Ryujinx.Horizon.Sdk.Sf; +using System; + +namespace Ryujinx.Horizon.Sdk.Codec.Detail +{ + interface IHardwareOpusDecoder : IServiceObject + { + Result DecodeInterleavedOld(out int outConsumed, out int outSamples, Span output, ReadOnlySpan input); + Result SetContext(ReadOnlySpan context); + Result DecodeInterleavedForMultiStreamOld(out int outConsumed, out int outSamples, Span output, ReadOnlySpan input); + Result SetContextForMultiStream(ReadOnlySpan context); + Result DecodeInterleavedWithPerfOld(out int outConsumed, out long timeTaken, out int outSamples, Span output, ReadOnlySpan input); + Result DecodeInterleavedForMultiStreamWithPerfOld(out int outConsumed, out long timeTaken, out int outSamples, Span output, ReadOnlySpan input); + Result DecodeInterleavedWithPerfAndResetOld(out int outConsumed, out long timeTaken, out int outSamples, Span output, ReadOnlySpan input, bool reset); + Result DecodeInterleavedForMultiStreamWithPerfAndResetOld(out int outConsumed, out long timeTaken, out int outSamples, Span output, ReadOnlySpan input, bool reset); + Result DecodeInterleaved(out int outConsumed, out long timeTaken, out int outSamples, Span output, ReadOnlySpan input, bool reset); + Result DecodeInterleavedForMultiStream(out int outConsumed, out long timeTaken, out int outSamples, Span output, ReadOnlySpan input, bool reset); + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Codec/Detail/IHardwareOpusDecoderManager.cs b/src/Ryujinx.Horizon/Sdk/Codec/Detail/IHardwareOpusDecoderManager.cs new file mode 100644 index 000000000..fb6c787b6 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Codec/Detail/IHardwareOpusDecoderManager.cs @@ -0,0 +1,19 @@ +using Ryujinx.Horizon.Common; +using Ryujinx.Horizon.Sdk.Sf; + +namespace Ryujinx.Horizon.Sdk.Codec.Detail +{ + interface IHardwareOpusDecoderManager : IServiceObject + { + Result OpenHardwareOpusDecoder(out IHardwareOpusDecoder decoder, HardwareOpusDecoderParameterInternal parameter, int workBufferHandle, int workBufferSize); + Result GetWorkBufferSize(out int size, HardwareOpusDecoderParameterInternal parameter); + Result OpenHardwareOpusDecoderForMultiStream(out IHardwareOpusDecoder decoder, in HardwareOpusMultiStreamDecoderParameterInternal parameter, int workBufferHandle, int workBufferSize); + Result GetWorkBufferSizeForMultiStream(out int size, in HardwareOpusMultiStreamDecoderParameterInternal parameter); + Result OpenHardwareOpusDecoderEx(out IHardwareOpusDecoder decoder, HardwareOpusDecoderParameterInternalEx parameter, int workBufferHandle, int workBufferSize); + Result GetWorkBufferSizeEx(out int size, HardwareOpusDecoderParameterInternalEx parameter); + Result OpenHardwareOpusDecoderForMultiStreamEx(out IHardwareOpusDecoder decoder, in HardwareOpusMultiStreamDecoderParameterInternalEx parameter, int workBufferHandle, int workBufferSize); + Result GetWorkBufferSizeForMultiStreamEx(out int size, in HardwareOpusMultiStreamDecoderParameterInternalEx parameter); + Result GetWorkBufferSizeExEx(out int size, HardwareOpusDecoderParameterInternalEx parameter); + Result GetWorkBufferSizeForMultiStreamExEx(out int size, in HardwareOpusMultiStreamDecoderParameterInternalEx parameter); + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Audio/Types/OpusDecoderFlags.cs b/src/Ryujinx.Horizon/Sdk/Codec/Detail/OpusDecoderFlags.cs similarity index 72% rename from src/Ryujinx.HLE/HOS/Services/Audio/Types/OpusDecoderFlags.cs rename to src/Ryujinx.Horizon/Sdk/Codec/Detail/OpusDecoderFlags.cs index 572535a92..d630b10f4 100644 --- a/src/Ryujinx.HLE/HOS/Services/Audio/Types/OpusDecoderFlags.cs +++ b/src/Ryujinx.Horizon/Sdk/Codec/Detail/OpusDecoderFlags.cs @@ -1,6 +1,6 @@ using System; -namespace Ryujinx.HLE.HOS.Services.Audio.Types +namespace Ryujinx.Horizon.Sdk.Codec.Detail { [Flags] enum OpusDecoderFlags : uint diff --git a/src/Ryujinx.Horizon/Sdk/Friends/ApplicationInfo.cs b/src/Ryujinx.Horizon/Sdk/Friends/ApplicationInfo.cs new file mode 100644 index 000000000..23bad3d13 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Friends/ApplicationInfo.cs @@ -0,0 +1,12 @@ +using Ryujinx.Horizon.Sdk.Ncm; +using System.Runtime.InteropServices; + +namespace Ryujinx.Horizon.Sdk.Friends +{ + [StructLayout(LayoutKind.Sequential, Size = 0x10, Pack = 0x8)] + struct ApplicationInfo + { + public ApplicationId ApplicationId; + public ulong PresenceGroupId; + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Friends/Detail/BlockedUserImpl.cs b/src/Ryujinx.Horizon/Sdk/Friends/Detail/BlockedUserImpl.cs new file mode 100644 index 000000000..d5f8a0313 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Friends/Detail/BlockedUserImpl.cs @@ -0,0 +1,8 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.Horizon.Sdk.Friends.Detail +{ + struct BlockedUserImpl + { + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Friends/Detail/FriendCandidateImpl.cs b/src/Ryujinx.Horizon/Sdk/Friends/Detail/FriendCandidateImpl.cs new file mode 100644 index 000000000..21e99c754 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Friends/Detail/FriendCandidateImpl.cs @@ -0,0 +1,8 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.Horizon.Sdk.Friends.Detail +{ + struct FriendCandidateImpl + { + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Friends/Detail/FriendDetailedInfoImpl.cs b/src/Ryujinx.Horizon/Sdk/Friends/Detail/FriendDetailedInfoImpl.cs new file mode 100644 index 000000000..1b46dccd5 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Friends/Detail/FriendDetailedInfoImpl.cs @@ -0,0 +1,9 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.Horizon.Sdk.Friends.Detail +{ + [StructLayout(LayoutKind.Sequential, Size = 0x800)] + struct FriendDetailedInfoImpl + { + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Friends/Detail/FriendImpl.cs b/src/Ryujinx.Horizon/Sdk/Friends/Detail/FriendImpl.cs new file mode 100644 index 000000000..d22ca4b90 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Friends/Detail/FriendImpl.cs @@ -0,0 +1,19 @@ +using Ryujinx.Common.Memory; +using Ryujinx.Horizon.Sdk.Account; +using System.Runtime.InteropServices; + +namespace Ryujinx.Horizon.Sdk.Friends.Detail +{ + [StructLayout(LayoutKind.Sequential, Size = 0x200, Pack = 0x8)] + struct FriendImpl + { + public Uid UserId; + public NetworkServiceAccountId NetworkUserId; + public Nickname Nickname; + public UserPresenceImpl Presence; + public bool IsFavourite; + public bool IsNew; + public Array6 Unknown; + public bool IsValid; + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Friends/Detail/FriendInvitationForViewerImpl.cs b/src/Ryujinx.Horizon/Sdk/Friends/Detail/FriendInvitationForViewerImpl.cs new file mode 100644 index 000000000..416ba3655 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Friends/Detail/FriendInvitationForViewerImpl.cs @@ -0,0 +1,8 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.Horizon.Sdk.Friends.Detail +{ + struct FriendInvitationForViewerImpl + { + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Friends/Detail/FriendInvitationGroupImpl.cs b/src/Ryujinx.Horizon/Sdk/Friends/Detail/FriendInvitationGroupImpl.cs new file mode 100644 index 000000000..ef9238347 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Friends/Detail/FriendInvitationGroupImpl.cs @@ -0,0 +1,9 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.Horizon.Sdk.Friends.Detail +{ + [StructLayout(LayoutKind.Sequential, Size = 0x1400)] + struct FriendInvitationGroupImpl + { + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Friends/Detail/FriendRequestImpl.cs b/src/Ryujinx.Horizon/Sdk/Friends/Detail/FriendRequestImpl.cs new file mode 100644 index 000000000..ba5671692 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Friends/Detail/FriendRequestImpl.cs @@ -0,0 +1,8 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.Horizon.Sdk.Friends.Detail +{ + struct FriendRequestImpl + { + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Friends/Detail/FriendSettingImpl.cs b/src/Ryujinx.Horizon/Sdk/Friends/Detail/FriendSettingImpl.cs new file mode 100644 index 000000000..f711d31fd --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Friends/Detail/FriendSettingImpl.cs @@ -0,0 +1,9 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.Horizon.Sdk.Friends.Detail +{ + [StructLayout(LayoutKind.Sequential, Size = 0x40)] + struct FriendSettingImpl + { + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/DaemonSuspendSessionService.cs b/src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/DaemonSuspendSessionService.cs new file mode 100644 index 000000000..aaf88ed03 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/DaemonSuspendSessionService.cs @@ -0,0 +1,7 @@ +namespace Ryujinx.Horizon.Sdk.Friends.Detail.Ipc +{ + partial class DaemonSuspendSessionService : IDaemonSuspendSessionService + { + // NOTE: This service has no commands. + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/FriendService.cs b/src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/FriendService.cs new file mode 100644 index 000000000..1b4c8c309 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/FriendService.cs @@ -0,0 +1,1015 @@ +using Ryujinx.Common.Logging; +using Ryujinx.Horizon.Common; +using Ryujinx.Horizon.Sdk.Account; +using Ryujinx.Horizon.Sdk.OsTypes; +using Ryujinx.Horizon.Sdk.Settings; +using Ryujinx.Horizon.Sdk.Sf; +using Ryujinx.Horizon.Sdk.Sf.Hipc; +using System; +using System.Runtime.InteropServices; +using System.Text; + +namespace Ryujinx.Horizon.Sdk.Friends.Detail.Ipc +{ + partial class FriendService : IFriendService, IDisposable + { + private readonly IEmulatorAccountManager _accountManager; + private SystemEventType _completionEvent; + + public FriendService(IEmulatorAccountManager accountManager, FriendsServicePermissionLevel permissionLevel) + { + _accountManager = accountManager; + + Os.CreateSystemEvent(out _completionEvent, EventClearMode.ManualClear, interProcess: true).AbortOnFailure(); + Os.SignalSystemEvent(ref _completionEvent); // TODO: Figure out where we are supposed to signal this. + } + + [CmifCommand(0)] + public Result GetCompletionEvent([CopyHandle] out int completionEventHandle) + { + completionEventHandle = Os.GetReadableHandleOfSystemEvent(ref _completionEvent); + + return Result.Success; + } + + [CmifCommand(1)] + public Result Cancel() + { + Logger.Stub?.PrintStub(LogClass.ServiceFriend); + + return Result.Success; + } + + [CmifCommand(10100)] + public Result GetFriendListIds( + out int count, + [Buffer(HipcBufferFlags.Out | HipcBufferFlags.Pointer)] Span friendIds, + Uid userId, + int offset, + SizedFriendFilter filter, + ulong pidPlaceholder, + [ClientProcessId] ulong pid) + { + count = 0; + + Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId, offset, filter, pidPlaceholder, pid }); + + if (userId.IsNull) + { + return FriendResult.InvalidArgument; + } + + return Result.Success; + } + + [CmifCommand(10101)] + public Result GetFriendList( + out int count, + [Buffer(HipcBufferFlags.Out | HipcBufferFlags.MapAlias)] Span friendList, + Uid userId, + int offset, + SizedFriendFilter filter, + ulong pidPlaceholder, + [ClientProcessId] ulong pid) + { + count = 0; + + Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId, offset, filter, pidPlaceholder, pid }); + + if (userId.IsNull) + { + return FriendResult.InvalidArgument; + } + + return Result.Success; + } + + [CmifCommand(10102)] + public Result UpdateFriendInfo( + [Buffer(HipcBufferFlags.Out | HipcBufferFlags.MapAlias)] Span info, + Uid userId, + [Buffer(HipcBufferFlags.In | HipcBufferFlags.Pointer)] ReadOnlySpan friendIds, + ulong pidPlaceholder, + [ClientProcessId] ulong pid) + { + string friendIdList = string.Join(", ", friendIds.ToArray()); + + Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId, friendIdList, pidPlaceholder, pid }); + + return Result.Success; + } + + [CmifCommand(10110)] + public Result GetFriendProfileImage( + out int size, + Uid userId, + NetworkServiceAccountId friendId, + [Buffer(HipcBufferFlags.Out | HipcBufferFlags.MapAlias)] Span profileImage) + { + size = 0; + + Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId, friendId }); + + return Result.Success; + } + + [CmifCommand(10120)] + public Result CheckFriendListAvailability(out bool listAvailable, Uid userId) + { + listAvailable = true; + + Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId }); + + return Result.Success; + } + + [CmifCommand(10121)] + public Result EnsureFriendListAvailable(Uid userId) + { + Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId }); + + return Result.Success; + } + + [CmifCommand(10200)] + public Result SendFriendRequestForApplication( + Uid userId, + NetworkServiceAccountId friendId, + [Buffer(HipcBufferFlags.In | HipcBufferFlags.Pointer, 0x48)] in InAppScreenName arg2, + [Buffer(HipcBufferFlags.In | HipcBufferFlags.Pointer, 0x48)] in InAppScreenName arg3, + ulong pidPlaceholder, + [ClientProcessId] ulong pid) + { + Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId, friendId, arg2, arg3, pidPlaceholder, pid }); + + return Result.Success; + } + + [CmifCommand(10211)] + public Result AddFacedFriendRequestForApplication( + Uid userId, + FacedFriendRequestRegistrationKey key, + Nickname nickname, + [Buffer(HipcBufferFlags.In | HipcBufferFlags.MapAlias)] ReadOnlySpan arg3, + [Buffer(HipcBufferFlags.In | HipcBufferFlags.Pointer, 0x48)] in InAppScreenName arg4, + [Buffer(HipcBufferFlags.In | HipcBufferFlags.Pointer, 0x48)] in InAppScreenName arg5, + ulong pidPlaceholder, + [ClientProcessId] ulong pid) + { + Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId, key, nickname, arg4, arg5, pidPlaceholder, pid }); + + return Result.Success; + } + + [CmifCommand(10400)] + public Result GetBlockedUserListIds( + out int count, + [Buffer(HipcBufferFlags.Out | HipcBufferFlags.Pointer)] Span blockedIds, + Uid userId, + int offset) + { + count = 0; + + Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId, offset }); + + return Result.Success; + } + + [CmifCommand(10420)] + public Result CheckBlockedUserListAvailability(out bool listAvailable, Uid userId) + { + listAvailable = true; + + Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId }); + + return Result.Success; + } + + [CmifCommand(10421)] + public Result EnsureBlockedUserListAvailable(Uid userId) + { + Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId }); + + return Result.Success; + } + + [CmifCommand(10500)] + public Result GetProfileList( + [Buffer(HipcBufferFlags.Out | HipcBufferFlags.MapAlias)] Span profileList, + Uid userId, + [Buffer(HipcBufferFlags.In | HipcBufferFlags.Pointer)] ReadOnlySpan friendIds) + { + string friendIdList = string.Join(", ", friendIds.ToArray()); + + Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId, friendIdList }); + + return Result.Success; + } + + [CmifCommand(10600)] + public Result DeclareOpenOnlinePlaySession(Uid userId) + { + Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId }); + + if (userId.IsNull) + { + return FriendResult.InvalidArgument; + } + + _accountManager.OpenUserOnlinePlay(userId); + + return Result.Success; + } + + [CmifCommand(10601)] + public Result DeclareCloseOnlinePlaySession(Uid userId) + { + Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId }); + + if (userId.IsNull) + { + return FriendResult.InvalidArgument; + } + + _accountManager.CloseUserOnlinePlay(userId); + + return Result.Success; + } + + [CmifCommand(10610)] + public Result UpdateUserPresence( + Uid userId, + [Buffer(HipcBufferFlags.In | HipcBufferFlags.Pointer, 0xE0)] in UserPresenceImpl userPresence, + ulong pidPlaceholder, + [ClientProcessId] ulong pid) + { + Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId, userPresence, pidPlaceholder, pid }); + + return Result.Success; + } + + [CmifCommand(10700)] + public Result GetPlayHistoryRegistrationKey( + [Buffer(HipcBufferFlags.Out | HipcBufferFlags.Pointer, 0x40)] out PlayHistoryRegistrationKey registrationKey, + Uid userId, + bool arg2) + { + if (userId.IsNull) + { + registrationKey = default; + + return FriendResult.InvalidArgument; + } + + // NOTE: Calls nn::friends::detail::service::core::PlayHistoryManager::GetInstance and stores the instance. + + // NOTE: Calls nn::friends::detail::service::core::UuidManager::GetInstance and stores the instance. + // Then calls nn::friends::detail::service::core::AccountStorageManager::GetInstance and stores the instance. + // Then it checks if an Uuid is already stored for the UserId, if not it generates a random Uuid, + // and stores it in the savedata 8000000000000080 in the friends:/uid.bin file. + + /* + + NOTE: The service uses the KeyIndex to get a random key from a keys buffer (since the key index is stored in the returned buffer). + We currently don't support play history and online services so we can use a blank key for now. + Code for reference: + + byte[] hmacKey = new byte[0x20]; + + HMACSHA256 hmacSha256 = new HMACSHA256(hmacKey); + byte[] hmacHash = hmacSha256.ComputeHash(playHistoryRegistrationKeyBuffer); + + */ + + Uid randomGuid = new(); + + Guid.NewGuid().TryWriteBytes(MemoryMarshal.AsBytes(MemoryMarshal.CreateSpan(ref randomGuid, 1))); + + registrationKey = new() + { + Type = 0x101, + KeyIndex = (byte)(Random.Shared.Next() & 7), + UserIdBool = 0, // TODO: Find it. + UnknownBool = (byte)(arg2 ? 1 : 0), // TODO: Find it. + Reserved = new(), + Uuid = randomGuid, + HmacHash = new(), + }; + + return Result.Success; + } + + [CmifCommand(10701)] + public Result GetPlayHistoryRegistrationKeyWithNetworkServiceAccountId( + [Buffer(HipcBufferFlags.Out | HipcBufferFlags.Pointer, 0x40)] out PlayHistoryRegistrationKey registrationKey, + NetworkServiceAccountId friendId, + bool arg2) + { + registrationKey = default; + + Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { friendId, arg2 }); + + return Result.Success; + } + + [CmifCommand(10702)] + public Result AddPlayHistory( + Uid userId, + [Buffer(HipcBufferFlags.In | HipcBufferFlags.Pointer, 0x40)] in PlayHistoryRegistrationKey registrationKey, + [Buffer(HipcBufferFlags.In | HipcBufferFlags.Pointer, 0x48)] in InAppScreenName arg2, + [Buffer(HipcBufferFlags.In | HipcBufferFlags.Pointer, 0x48)] in InAppScreenName arg3, + ulong pidPlaceholder, + [ClientProcessId] ulong pid) + { + Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId, registrationKey, arg2, arg3, pidPlaceholder, pid }); + + return Result.Success; + } + + [CmifCommand(11000)] + public Result GetProfileImageUrl(out Url imageUrl, Url url, int arg2) + { + imageUrl = default; + + Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { url, arg2 }); + + return Result.Success; + } + + [CmifCommand(20100)] + public Result GetFriendCount(out int count, Uid userId, SizedFriendFilter filter, ulong pidPlaceholder, [ClientProcessId] ulong pid) + { + count = 0; + + Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId, filter, pidPlaceholder, pid }); + + return Result.Success; + } + + [CmifCommand(20101)] + public Result GetNewlyFriendCount(out int count, Uid userId) + { + count = 0; + + Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId }); + + return Result.Success; + } + + [CmifCommand(20102)] + public Result GetFriendDetailedInfo( + [Buffer(HipcBufferFlags.Out | HipcBufferFlags.Pointer, 0x800)] out FriendDetailedInfoImpl detailedInfo, + Uid userId, + NetworkServiceAccountId friendId) + { + detailedInfo = default; + + Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId, friendId }); + + return Result.Success; + } + + [CmifCommand(20103)] + public Result SyncFriendList(Uid userId) + { + Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId }); + + return Result.Success; + } + + [CmifCommand(20104)] + public Result RequestSyncFriendList(Uid userId) + { + Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId }); + + return Result.Success; + } + + [CmifCommand(20110)] + public Result LoadFriendSetting( + [Buffer(HipcBufferFlags.Out | HipcBufferFlags.Pointer, 0x40)] out FriendSettingImpl friendSetting, + Uid userId, + NetworkServiceAccountId friendId) + { + friendSetting = default; + + Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId, friendId }); + + return Result.Success; + } + + [CmifCommand(20200)] + public Result GetReceivedFriendRequestCount(out int count, out int count2, Uid userId) + { + count = 0; + count2 = 0; + + Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId }); + + return Result.Success; + } + + [CmifCommand(20201)] + public Result GetFriendRequestList( + out int count, + [Buffer(HipcBufferFlags.Out | HipcBufferFlags.MapAlias)] Span requestList, + Uid userId, + int arg3, + int arg4) + { + count = 0; + + Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId, arg3, arg4 }); + + return Result.Success; + } + + [CmifCommand(20300)] + public Result GetFriendCandidateList( + out int count, + [Buffer(HipcBufferFlags.Out | HipcBufferFlags.MapAlias)] Span candidateList, + Uid userId, + int arg3) + { + count = 0; + + Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId, arg3 }); + + return Result.Success; + } + + [CmifCommand(20301)] + public Result GetNintendoNetworkIdInfo( + [Buffer(HipcBufferFlags.Out | HipcBufferFlags.Pointer, 0x38)] out NintendoNetworkIdUserInfo networkIdInfo, + out int arg1, + [Buffer(HipcBufferFlags.Out | HipcBufferFlags.MapAlias)] Span friendInfo, + Uid userId, + int arg4) + { + networkIdInfo = default; + arg1 = 0; + + Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId, arg4 }); + + return Result.Success; + } + + [CmifCommand(20302)] + public Result GetSnsAccountLinkage(out SnsAccountLinkage accountLinkage, Uid userId) + { + accountLinkage = default; + + Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId }); + + return Result.Success; + } + + [CmifCommand(20303)] + public Result GetSnsAccountProfile( + [Buffer(HipcBufferFlags.Out | HipcBufferFlags.Pointer, 0x380)] out SnsAccountProfile accountProfile, + Uid userId, + NetworkServiceAccountId friendId, + int arg3) + { + accountProfile = default; + + Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId, friendId, arg3 }); + + return Result.Success; + } + + [CmifCommand(20304)] + public Result GetSnsAccountFriendList( + out int count, + [Buffer(HipcBufferFlags.Out | HipcBufferFlags.MapAlias)] Span friendList, + Uid userId, + int arg3) + { + count = 0; + + Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId, arg3 }); + + return Result.Success; + } + + [CmifCommand(20400)] + public Result GetBlockedUserList( + out int count, + [Buffer(HipcBufferFlags.Out | HipcBufferFlags.MapAlias)] Span blockedUsers, + Uid userId, + int arg3) + { + count = 0; + + Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId, arg3 }); + + return Result.Success; + } + + [CmifCommand(20401)] + public Result SyncBlockedUserList(Uid userId) + { + Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId }); + + return Result.Success; + } + + [CmifCommand(20500)] + public Result GetProfileExtraList( + [Buffer(HipcBufferFlags.Out | HipcBufferFlags.MapAlias)] Span extraList, + Uid userId, + [Buffer(HipcBufferFlags.In | HipcBufferFlags.Pointer)] ReadOnlySpan friendIds) + { + string friendIdList = string.Join(", ", friendIds.ToArray()); + + Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId, friendIdList }); + + return Result.Success; + } + + [CmifCommand(20501)] + public Result GetRelationship(out Relationship relationship, Uid userId, NetworkServiceAccountId friendId) + { + relationship = default; + + Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId, friendId }); + + return Result.Success; + } + + [CmifCommand(20600)] + public Result GetUserPresenceView([Buffer(HipcBufferFlags.Out | HipcBufferFlags.Pointer, 0xE0)] out UserPresenceViewImpl userPresenceView, Uid userId) + { + userPresenceView = default; + + Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId }); + + return Result.Success; + } + + [CmifCommand(20700)] + public Result GetPlayHistoryList(out int count, [Buffer(HipcBufferFlags.Out | HipcBufferFlags.MapAlias)] Span playHistoryList, Uid userId, int arg3) + { + count = 0; + + Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId, arg3 }); + + return Result.Success; + } + + [CmifCommand(20701)] + public Result GetPlayHistoryStatistics(out PlayHistoryStatistics statistics, Uid userId) + { + statistics = default; + + Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId }); + + return Result.Success; + } + + [CmifCommand(20800)] + public Result LoadUserSetting([Buffer(HipcBufferFlags.Out | HipcBufferFlags.Pointer, 0x800)] out UserSettingImpl userSetting, Uid userId) + { + userSetting = default; + + Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId }); + + return Result.Success; + } + + [CmifCommand(20801)] + public Result SyncUserSetting(Uid userId) + { + Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId }); + + return Result.Success; + } + + [CmifCommand(20900)] + public Result RequestListSummaryOverlayNotification() + { + Logger.Stub?.PrintStub(LogClass.ServiceFriend); + + return Result.Success; + } + + [CmifCommand(21000)] + public Result GetExternalApplicationCatalog( + [Buffer(HipcBufferFlags.Out | HipcBufferFlags.Pointer, 0x4B8)] out ExternalApplicationCatalog catalog, + ExternalApplicationCatalogId catalogId, + LanguageCode language) + { + catalog = default; + + Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { catalogId, language }); + + return Result.Success; + } + + [CmifCommand(22000)] + public Result GetReceivedFriendInvitationList( + out int count, + [Buffer(HipcBufferFlags.Out | HipcBufferFlags.MapAlias)] Span invitationList, + Uid userId) + { + count = 0; + + Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId }); + + return Result.Success; + } + + [CmifCommand(22001)] + public Result GetReceivedFriendInvitationDetailedInfo( + [Buffer(HipcBufferFlags.Out | HipcBufferFlags.MapAlias, 0x1400)] out FriendInvitationGroupImpl invicationGroup, + Uid userId, + FriendInvitationGroupId groupId) + { + invicationGroup = default; + + Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId, groupId }); + + return Result.Success; + } + + [CmifCommand(22010)] + public Result GetReceivedFriendInvitationCountCache(out int count, Uid userId) + { + count = 0; + + Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId }); + + return Result.Success; + } + + [CmifCommand(30100)] + public Result DropFriendNewlyFlags(Uid userId) + { + Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId }); + + return Result.Success; + } + + [CmifCommand(30101)] + public Result DeleteFriend(Uid userId, NetworkServiceAccountId friendId) + { + Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId, friendId }); + + return Result.Success; + } + + [CmifCommand(30110)] + public Result DropFriendNewlyFlag(Uid userId, NetworkServiceAccountId friendId) + { + Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId, friendId }); + + return Result.Success; + } + + [CmifCommand(30120)] + public Result ChangeFriendFavoriteFlag(Uid userId, NetworkServiceAccountId friendId, bool favoriteFlag) + { + Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId, friendId, favoriteFlag }); + + return Result.Success; + } + + [CmifCommand(30121)] + public Result ChangeFriendOnlineNotificationFlag(Uid userId, NetworkServiceAccountId friendId, bool onlineNotificationFlag) + { + Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId, friendId, onlineNotificationFlag }); + + return Result.Success; + } + + [CmifCommand(30200)] + public Result SendFriendRequest(Uid userId, NetworkServiceAccountId friendId, int arg2) + { + Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId, friendId, arg2 }); + + return Result.Success; + } + + [CmifCommand(30201)] + public Result SendFriendRequestWithApplicationInfo( + Uid userId, + NetworkServiceAccountId friendId, + int arg2, + ApplicationInfo applicationInfo, + [Buffer(HipcBufferFlags.In | HipcBufferFlags.Pointer, 0x48)] in InAppScreenName arg4, + [Buffer(HipcBufferFlags.In | HipcBufferFlags.Pointer, 0x48)] in InAppScreenName arg5) + { + Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId, friendId, arg2, applicationInfo, arg4, arg5 }); + + return Result.Success; + } + + [CmifCommand(30202)] + public Result CancelFriendRequest(Uid userId, RequestId requestId) + { + Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId, requestId }); + + return Result.Success; + } + + [CmifCommand(30203)] + public Result AcceptFriendRequest(Uid userId, RequestId requestId) + { + Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId, requestId }); + + return Result.Success; + } + + [CmifCommand(30204)] + public Result RejectFriendRequest(Uid userId, RequestId requestId) + { + Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId, requestId }); + + return Result.Success; + } + + [CmifCommand(30205)] + public Result ReadFriendRequest(Uid userId, RequestId requestId) + { + Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId, requestId }); + + return Result.Success; + } + + [CmifCommand(30210)] + public Result GetFacedFriendRequestRegistrationKey(out FacedFriendRequestRegistrationKey registrationKey, Uid userId) + { + registrationKey = default; + + Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId }); + + return Result.Success; + } + + [CmifCommand(30211)] + public Result AddFacedFriendRequest( + Uid userId, + FacedFriendRequestRegistrationKey registrationKey, + Nickname nickname, + [Buffer(HipcBufferFlags.In | HipcBufferFlags.MapAlias)] ReadOnlySpan arg3) + { + Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId, registrationKey, nickname }); + + return Result.Success; + } + + [CmifCommand(30212)] + public Result CancelFacedFriendRequest(Uid userId, NetworkServiceAccountId friendId) + { + Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId, friendId }); + + return Result.Success; + } + + [CmifCommand(30213)] + public Result GetFacedFriendRequestProfileImage( + out int size, + Uid userId, + NetworkServiceAccountId friendId, + [Buffer(HipcBufferFlags.Out | HipcBufferFlags.MapAlias)] Span profileImage) + { + size = 0; + + Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId, friendId }); + + return Result.Success; + } + + [CmifCommand(30214)] + public Result GetFacedFriendRequestProfileImageFromPath( + out int size, + [Buffer(HipcBufferFlags.In | HipcBufferFlags.Pointer)] ReadOnlySpan path, + [Buffer(HipcBufferFlags.Out | HipcBufferFlags.MapAlias)] Span profileImage) + { + size = 0; + + string pathString = Encoding.UTF8.GetString(path); + + Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { pathString }); + + return Result.Success; + } + + [CmifCommand(30215)] + public Result SendFriendRequestWithExternalApplicationCatalogId( + Uid userId, + NetworkServiceAccountId friendId, + int arg2, + ExternalApplicationCatalogId catalogId, + [Buffer(HipcBufferFlags.In | HipcBufferFlags.Pointer, 0x48)] in InAppScreenName arg4, + [Buffer(HipcBufferFlags.In | HipcBufferFlags.Pointer, 0x48)] in InAppScreenName arg5) + { + Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId, friendId, arg2, catalogId, arg4, arg5 }); + + return Result.Success; + } + + [CmifCommand(30216)] + public Result ResendFacedFriendRequest(Uid userId, NetworkServiceAccountId friendId) + { + Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId, friendId }); + + return Result.Success; + } + + [CmifCommand(30217)] + public Result SendFriendRequestWithNintendoNetworkIdInfo( + Uid userId, + NetworkServiceAccountId friendId, + int arg2, + MiiName arg3, + MiiImageUrlParam arg4, + MiiName arg5, + MiiImageUrlParam arg6) + { + Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId, friendId, arg2, arg3, arg4, arg5, arg6 }); + + return Result.Success; + } + + [CmifCommand(30300)] + public Result GetSnsAccountLinkPageUrl([Buffer(HipcBufferFlags.Out | HipcBufferFlags.MapAlias, 0x1000)] out WebPageUrl url, Uid userId, int arg2) + { + url = default; + + Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId, arg2 }); + + return Result.Success; + } + + [CmifCommand(30301)] + public Result UnlinkSnsAccount(Uid userId, int arg1) + { + Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId, arg1 }); + + return Result.Success; + } + + [CmifCommand(30400)] + public Result BlockUser(Uid userId, NetworkServiceAccountId friendId, int arg2) + { + Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId, friendId, arg2 }); + + return Result.Success; + } + + [CmifCommand(30401)] + public Result BlockUserWithApplicationInfo( + Uid userId, + NetworkServiceAccountId friendId, + int arg2, + ApplicationInfo applicationInfo, + [Buffer(HipcBufferFlags.In | HipcBufferFlags.Pointer, 0x48)] in InAppScreenName arg4) + { + Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId, friendId, arg2, applicationInfo, arg4 }); + + return Result.Success; + } + + [CmifCommand(30402)] + public Result UnblockUser(Uid userId, NetworkServiceAccountId friendId) + { + Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId, friendId }); + + return Result.Success; + } + + [CmifCommand(30500)] + public Result GetProfileExtraFromFriendCode( + [Buffer(HipcBufferFlags.Out | HipcBufferFlags.Pointer, 0x400)] out ProfileExtraImpl profileExtra, + Uid userId, + FriendCode friendCode) + { + profileExtra = default; + + Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId, friendCode }); + + return Result.Success; + } + + [CmifCommand(30700)] + public Result DeletePlayHistory(Uid userId) + { + Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId }); + + return Result.Success; + } + + [CmifCommand(30810)] + public Result ChangePresencePermission(Uid userId, int permission) + { + Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId, permission }); + + return Result.Success; + } + + [CmifCommand(30811)] + public Result ChangeFriendRequestReception(Uid userId, bool reception) + { + Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId, reception }); + + return Result.Success; + } + + [CmifCommand(30812)] + public Result ChangePlayLogPermission(Uid userId, int permission) + { + Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId, permission }); + + return Result.Success; + } + + [CmifCommand(30820)] + public Result IssueFriendCode(Uid userId) + { + Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId }); + + return Result.Success; + } + + [CmifCommand(30830)] + public Result ClearPlayLog(Uid userId) + { + Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId }); + + return Result.Success; + } + + [CmifCommand(30900)] + public Result SendFriendInvitation( + Uid userId, + [Buffer(HipcBufferFlags.In | HipcBufferFlags.Pointer)] ReadOnlySpan friendIds, + [Buffer(HipcBufferFlags.In | HipcBufferFlags.MapAlias, 0xC00)] in FriendInvitationGameModeDescription description, + ApplicationInfo applicationInfo, + [Buffer(HipcBufferFlags.In | HipcBufferFlags.MapAlias)] ReadOnlySpan arg4, + bool arg5) + { + string friendIdList = string.Join(", ", friendIds.ToArray()); + + Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId, friendIdList, description, applicationInfo, arg5 }); + + return Result.Success; + } + + [CmifCommand(30910)] + public Result ReadFriendInvitation(Uid userId, [Buffer(HipcBufferFlags.In | HipcBufferFlags.Pointer)] ReadOnlySpan invitationIds) + { + string invitationIdList = string.Join(", ", invitationIds.ToArray()); + + Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId, invitationIdList }); + + return Result.Success; + } + + [CmifCommand(30911)] + public Result ReadAllFriendInvitations(Uid userId) + { + Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId }); + + return Result.Success; + } + + [CmifCommand(40100)] + public Result DeleteFriendListCache(Uid userId) + { + Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId }); + + return Result.Success; + } + + [CmifCommand(40400)] + public Result DeleteBlockedUserListCache(Uid userId) + { + Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId }); + + return Result.Success; + } + + [CmifCommand(49900)] + public Result DeleteNetworkServiceAccountCache(Uid userId) + { + Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { userId }); + + return Result.Success; + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + Os.DestroySystemEvent(ref _completionEvent); + } + } + + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/Types/FriendServicePermissionLevel.cs b/src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/FriendsServicePermissionLevel.cs similarity index 64% rename from src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/Types/FriendServicePermissionLevel.cs rename to src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/FriendsServicePermissionLevel.cs index 7902d9c53..f4bbe100f 100644 --- a/src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/Types/FriendServicePermissionLevel.cs +++ b/src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/FriendsServicePermissionLevel.cs @@ -1,16 +1,13 @@ -using System; - -namespace Ryujinx.HLE.HOS.Services.Friend.ServiceCreator +namespace Ryujinx.Horizon.Sdk.Friends.Detail.Ipc { - [Flags] - enum FriendServicePermissionLevel + enum FriendsServicePermissionLevel { UserMask = 1, ViewerMask = 2, ManagerMask = 4, SystemMask = 8, - Administrator = -1, + Admin = -1, User = UserMask, Viewer = UserMask | ViewerMask, Manager = UserMask | ViewerMask | ManagerMask, diff --git a/src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/IDaemonSuspendSessionService.cs b/src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/IDaemonSuspendSessionService.cs new file mode 100644 index 000000000..2bb0434e8 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/IDaemonSuspendSessionService.cs @@ -0,0 +1,9 @@ +using Ryujinx.Horizon.Sdk.Sf; + +namespace Ryujinx.Horizon.Sdk.Friends.Detail.Ipc +{ + interface IDaemonSuspendSessionService : IServiceObject + { + // NOTE: This service has no commands. + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/IFriendService.cs b/src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/IFriendService.cs new file mode 100644 index 000000000..c19d0b788 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/IFriendService.cs @@ -0,0 +1,97 @@ +using Ryujinx.Horizon.Common; +using Ryujinx.Horizon.Sdk.Account; +using Ryujinx.Horizon.Sdk.Settings; +using Ryujinx.Horizon.Sdk.Sf; +using System; + +namespace Ryujinx.Horizon.Sdk.Friends.Detail.Ipc +{ + interface IFriendService : IServiceObject + { + Result GetCompletionEvent(out int completionEventHandle); + Result Cancel(); + Result GetFriendListIds(out int count, Span friendIds, Uid userId, int offset, SizedFriendFilter filter, ulong pidPlaceholder, ulong pid); + Result GetFriendList(out int count, Span friendList, Uid userId, int offset, SizedFriendFilter filter, ulong pidPlaceholder, ulong pid); + Result UpdateFriendInfo(Span info, Uid userId, ReadOnlySpan friendIds, ulong pidPlaceholder, ulong pid); + Result GetFriendProfileImage(out int size, Uid userId, NetworkServiceAccountId friendId, Span profileImage); + Result CheckFriendListAvailability(out bool listAvailable, Uid userId); + Result EnsureFriendListAvailable(Uid userId); + Result SendFriendRequestForApplication(Uid userId, NetworkServiceAccountId friendId, in InAppScreenName arg2, in InAppScreenName arg3, ulong pidPlaceholder, ulong pid); + Result AddFacedFriendRequestForApplication(Uid userId, FacedFriendRequestRegistrationKey key, Nickname nickname, ReadOnlySpan arg3, in InAppScreenName arg4, in InAppScreenName arg5, ulong pidPlaceholder, ulong pid); + Result GetBlockedUserListIds(out int count, Span blockedIds, Uid userId, int offset); + Result CheckBlockedUserListAvailability(out bool listAvailable, Uid userId); + Result EnsureBlockedUserListAvailable(Uid userId); + Result GetProfileList(Span profileList, Uid userId, ReadOnlySpan friendIds); + Result DeclareOpenOnlinePlaySession(Uid userId); + Result DeclareCloseOnlinePlaySession(Uid userId); + Result UpdateUserPresence(Uid userId, in UserPresenceImpl userPresence, ulong pidPlaceholder, ulong pid); + Result GetPlayHistoryRegistrationKey(out PlayHistoryRegistrationKey registrationKey, Uid userId, bool arg2); + Result GetPlayHistoryRegistrationKeyWithNetworkServiceAccountId(out PlayHistoryRegistrationKey registrationKey, NetworkServiceAccountId friendId, bool arg2); + Result AddPlayHistory(Uid userId, in PlayHistoryRegistrationKey registrationKey, in InAppScreenName arg2, in InAppScreenName arg3, ulong pidPlaceholder, ulong pid); + Result GetProfileImageUrl(out Url imageUrl, Url url, int arg2); + Result GetFriendCount(out int count, Uid userId, SizedFriendFilter filter, ulong pidPlaceholder, ulong pid); + Result GetNewlyFriendCount(out int count, Uid userId); + Result GetFriendDetailedInfo(out FriendDetailedInfoImpl detailedInfo, Uid userId, NetworkServiceAccountId friendId); + Result SyncFriendList(Uid userId); + Result RequestSyncFriendList(Uid userId); + Result LoadFriendSetting(out FriendSettingImpl friendSetting, Uid userId, NetworkServiceAccountId friendId); + Result GetReceivedFriendRequestCount(out int count, out int count2, Uid userId); + Result GetFriendRequestList(out int count, Span requestList, Uid userId, int arg3, int arg4); + Result GetFriendCandidateList(out int count, Span candidateList, Uid userId, int arg3); + Result GetNintendoNetworkIdInfo(out NintendoNetworkIdUserInfo networkIdInfo, out int arg1, Span friendInfo, Uid userId, int arg4); + Result GetSnsAccountLinkage(out SnsAccountLinkage accountLinkage, Uid userId); + Result GetSnsAccountProfile(out SnsAccountProfile accountProfile, Uid userId, NetworkServiceAccountId friendId, int arg3); + Result GetSnsAccountFriendList(out int count, Span friendList, Uid userId, int arg3); + Result GetBlockedUserList(out int count, Span blockedUsers, Uid userId, int arg3); + Result SyncBlockedUserList(Uid userId); + Result GetProfileExtraList(Span extraList, Uid userId, ReadOnlySpan friendIds); + Result GetRelationship(out Relationship relationship, Uid userId, NetworkServiceAccountId friendId); + Result GetUserPresenceView(out UserPresenceViewImpl userPresenceView, Uid userId); + Result GetPlayHistoryList(out int count, Span playHistoryList, Uid userId, int arg3); + Result GetPlayHistoryStatistics(out PlayHistoryStatistics statistics, Uid userId); + Result LoadUserSetting(out UserSettingImpl userSetting, Uid userId); + Result SyncUserSetting(Uid userId); + Result RequestListSummaryOverlayNotification(); + Result GetExternalApplicationCatalog(out ExternalApplicationCatalog catalog, ExternalApplicationCatalogId catalogId, LanguageCode language); + Result GetReceivedFriendInvitationList(out int count, Span invitationList, Uid userId); + Result GetReceivedFriendInvitationDetailedInfo(out FriendInvitationGroupImpl invicationGroup, Uid userId, FriendInvitationGroupId groupId); + Result GetReceivedFriendInvitationCountCache(out int count, Uid userId); + Result DropFriendNewlyFlags(Uid userId); + Result DeleteFriend(Uid userId, NetworkServiceAccountId friendId); + Result DropFriendNewlyFlag(Uid userId, NetworkServiceAccountId friendId); + Result ChangeFriendFavoriteFlag(Uid userId, NetworkServiceAccountId friendId, bool favoriteFlag); + Result ChangeFriendOnlineNotificationFlag(Uid userId, NetworkServiceAccountId friendId, bool onlineNotificationFlag); + Result SendFriendRequest(Uid userId, NetworkServiceAccountId friendId, int arg2); + Result SendFriendRequestWithApplicationInfo(Uid userId, NetworkServiceAccountId friendId, int arg2, ApplicationInfo applicationInfo, in InAppScreenName arg4, in InAppScreenName arg5); + Result CancelFriendRequest(Uid userId, RequestId requestId); + Result AcceptFriendRequest(Uid userId, RequestId requestId); + Result RejectFriendRequest(Uid userId, RequestId requestId); + Result ReadFriendRequest(Uid userId, RequestId requestId); + Result GetFacedFriendRequestRegistrationKey(out FacedFriendRequestRegistrationKey registrationKey, Uid userId); + Result AddFacedFriendRequest(Uid userId, FacedFriendRequestRegistrationKey registrationKey, Nickname nickname, ReadOnlySpan arg3); + Result CancelFacedFriendRequest(Uid userId, NetworkServiceAccountId friendId); + Result GetFacedFriendRequestProfileImage(out int size, Uid userId, NetworkServiceAccountId friendId, Span profileImage); + Result GetFacedFriendRequestProfileImageFromPath(out int size, ReadOnlySpan path, Span profileImage); + Result SendFriendRequestWithExternalApplicationCatalogId(Uid userId, NetworkServiceAccountId friendId, int arg2, ExternalApplicationCatalogId catalogId, in InAppScreenName arg4, in InAppScreenName arg5); + Result ResendFacedFriendRequest(Uid userId, NetworkServiceAccountId friendId); + Result SendFriendRequestWithNintendoNetworkIdInfo(Uid userId, NetworkServiceAccountId friendId, int arg2, MiiName arg3, MiiImageUrlParam arg4, MiiName arg5, MiiImageUrlParam arg6); + Result GetSnsAccountLinkPageUrl(out WebPageUrl url, Uid userId, int arg2); + Result UnlinkSnsAccount(Uid userId, int arg1); + Result BlockUser(Uid userId, NetworkServiceAccountId friendId, int arg2); + Result BlockUserWithApplicationInfo(Uid userId, NetworkServiceAccountId friendId, int arg2, ApplicationInfo applicationInfo, in InAppScreenName arg4); + Result UnblockUser(Uid userId, NetworkServiceAccountId friendId); + Result GetProfileExtraFromFriendCode(out ProfileExtraImpl profileExtra, Uid userId, FriendCode friendCode); + Result DeletePlayHistory(Uid userId); + Result ChangePresencePermission(Uid userId, int permission); + Result ChangeFriendRequestReception(Uid userId, bool reception); + Result ChangePlayLogPermission(Uid userId, int permission); + Result IssueFriendCode(Uid userId); + Result ClearPlayLog(Uid userId); + Result SendFriendInvitation(Uid userId, ReadOnlySpan friendIds, in FriendInvitationGameModeDescription description, ApplicationInfo applicationInfo, ReadOnlySpan arg4, bool arg5); + Result ReadFriendInvitation(Uid userId, ReadOnlySpan invitationIds); + Result ReadAllFriendInvitations(Uid userId); + Result DeleteFriendListCache(Uid userId); + Result DeleteBlockedUserListCache(Uid userId); + Result DeleteNetworkServiceAccountCache(Uid userId); + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/INotificationService.cs b/src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/INotificationService.cs new file mode 100644 index 000000000..a3a28e8ce --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/INotificationService.cs @@ -0,0 +1,12 @@ +using Ryujinx.Horizon.Common; +using Ryujinx.Horizon.Sdk.Sf; + +namespace Ryujinx.Horizon.Sdk.Friends.Detail.Ipc +{ + interface INotificationService : IServiceObject + { + Result GetEvent(out int eventHandle); + Result Clear(); + Result Pop(out SizedNotificationInfo sizedNotificationInfo); + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/IServiceCreator.cs b/src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/IServiceCreator.cs new file mode 100644 index 000000000..58e2569bb --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/IServiceCreator.cs @@ -0,0 +1,13 @@ +using Ryujinx.Horizon.Common; +using Ryujinx.Horizon.Sdk.Account; +using Ryujinx.Horizon.Sdk.Sf; + +namespace Ryujinx.Horizon.Sdk.Friends.Detail.Ipc +{ + interface IServiceCreator : IServiceObject + { + Result CreateFriendService(out IFriendService friendService); + Result CreateNotificationService(out INotificationService notificationService, Uid userId); + Result CreateDaemonSuspendSessionService(out IDaemonSuspendSessionService daemonSuspendSessionService); + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/NotificationEventHandler.cs b/src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/NotificationEventHandler.cs new file mode 100644 index 000000000..61c692a6e --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/NotificationEventHandler.cs @@ -0,0 +1,58 @@ +using Ryujinx.Horizon.Sdk.Account; + +namespace Ryujinx.Horizon.Sdk.Friends.Detail.Ipc +{ + sealed class NotificationEventHandler + { + private readonly NotificationService[] _registry; + + public NotificationEventHandler() + { + _registry = new NotificationService[0x20]; + } + + public void RegisterNotificationService(NotificationService service) + { + // NOTE: When there's no enough space in the registry array, Nintendo doesn't return any errors. + for (int i = 0; i < _registry.Length; i++) + { + if (_registry[i] == null) + { + _registry[i] = service; + break; + } + } + } + + public void UnregisterNotificationService(NotificationService service) + { + // NOTE: When there's no enough space in the registry array, Nintendo doesn't return any errors. + for (int i = 0; i < _registry.Length; i++) + { + if (_registry[i] == service) + { + _registry[i] = null; + break; + } + } + } + + // TODO: Use this when we have enough things to go online. + public void SignalFriendListUpdate(Uid targetId) + { + for (int i = 0; i < _registry.Length; i++) + { + _registry[i]?.SignalFriendListUpdate(targetId); + } + } + + // TODO: Use this when we have enough things to go online. + public void SignalNewFriendRequest(Uid targetId) + { + for (int i = 0; i < _registry.Length; i++) + { + _registry[i]?.SignalNewFriendRequest(targetId); + } + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/NotificationService/Types/NotificationEventType.cs b/src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/NotificationEventType.cs similarity index 64% rename from src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/NotificationService/Types/NotificationEventType.cs rename to src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/NotificationEventType.cs index 363e03eaf..e46fc9b7a 100644 --- a/src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/NotificationService/Types/NotificationEventType.cs +++ b/src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/NotificationEventType.cs @@ -1,4 +1,4 @@ -namespace Ryujinx.HLE.HOS.Services.Friend.ServiceCreator.NotificationService +namespace Ryujinx.Horizon.Sdk.Friends.Detail.Ipc { enum NotificationEventType : uint { diff --git a/src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/NotificationService.cs b/src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/NotificationService.cs new file mode 100644 index 000000000..534bf63ed --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/NotificationService.cs @@ -0,0 +1,172 @@ +using Ryujinx.Horizon.Common; +using Ryujinx.Horizon.Sdk.Account; +using Ryujinx.Horizon.Sdk.OsTypes; +using Ryujinx.Horizon.Sdk.Sf; +using System; +using System.Collections.Generic; + +namespace Ryujinx.Horizon.Sdk.Friends.Detail.Ipc +{ + partial class NotificationService : INotificationService, IDisposable + { + private readonly NotificationEventHandler _notificationEventHandler; + private readonly Uid _userId; + private readonly FriendsServicePermissionLevel _permissionLevel; + + private readonly object _lock = new(); + + private SystemEventType _notificationEvent; + + private readonly LinkedList _notifications; + + private bool _hasNewFriendRequest; + private bool _hasFriendListUpdate; + + public NotificationService(NotificationEventHandler notificationEventHandler, Uid userId, FriendsServicePermissionLevel permissionLevel) + { + _notificationEventHandler = notificationEventHandler; + _userId = userId; + _permissionLevel = permissionLevel; + _notifications = new LinkedList(); + Os.CreateSystemEvent(out _notificationEvent, EventClearMode.AutoClear, interProcess: true).AbortOnFailure(); + + _hasNewFriendRequest = false; + _hasFriendListUpdate = false; + + notificationEventHandler.RegisterNotificationService(this); + } + + [CmifCommand(0)] + public Result GetEvent([CopyHandle] out int eventHandle) + { + eventHandle = Os.GetReadableHandleOfSystemEvent(ref _notificationEvent); + + return Result.Success; + } + + [CmifCommand(1)] + public Result Clear() + { + lock (_lock) + { + _hasNewFriendRequest = false; + _hasFriendListUpdate = false; + + _notifications.Clear(); + } + + return Result.Success; + } + + [CmifCommand(2)] + public Result Pop(out SizedNotificationInfo sizedNotificationInfo) + { + lock (_lock) + { + if (_notifications.Count >= 1) + { + sizedNotificationInfo = _notifications.First.Value; + _notifications.RemoveFirst(); + + if (sizedNotificationInfo.Type == NotificationEventType.FriendListUpdate) + { + _hasFriendListUpdate = false; + } + else if (sizedNotificationInfo.Type == NotificationEventType.NewFriendRequest) + { + _hasNewFriendRequest = false; + } + + return Result.Success; + } + } + + sizedNotificationInfo = default; + + return FriendResult.NotificationQueueEmpty; + } + + public void SignalFriendListUpdate(Uid targetId) + { + lock (_lock) + { + if (_userId == targetId) + { + if (!_hasFriendListUpdate) + { + SizedNotificationInfo friendListNotification = new(); + + if (_notifications.Count != 0) + { + friendListNotification = _notifications.First.Value; + _notifications.RemoveFirst(); + } + + friendListNotification.Type = NotificationEventType.FriendListUpdate; + _hasFriendListUpdate = true; + + if (_hasNewFriendRequest) + { + SizedNotificationInfo newFriendRequestNotification = new(); + + if (_notifications.Count != 0) + { + newFriendRequestNotification = _notifications.First.Value; + _notifications.RemoveFirst(); + } + + newFriendRequestNotification.Type = NotificationEventType.NewFriendRequest; + _notifications.AddFirst(newFriendRequestNotification); + } + + // We defer this to make sure we are on top of the queue. + _notifications.AddFirst(friendListNotification); + } + + Os.SignalSystemEvent(ref _notificationEvent); + } + } + } + + public void SignalNewFriendRequest(Uid targetId) + { + lock (_lock) + { + if (_permissionLevel.HasFlag(FriendsServicePermissionLevel.ViewerMask) && _userId == targetId) + { + if (!_hasNewFriendRequest) + { + if (_notifications.Count == 100) + { + SignalFriendListUpdate(targetId); + } + + SizedNotificationInfo newFriendRequestNotification = new() + { + Type = NotificationEventType.NewFriendRequest, + }; + + _notifications.AddLast(newFriendRequestNotification); + _hasNewFriendRequest = true; + } + + Os.SignalSystemEvent(ref _notificationEvent); + } + } + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + _notificationEventHandler.UnregisterNotificationService(this); + } + } + + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/FriendService/Types/PresenceStatusFilter.cs b/src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/PresenceStatusFilter.cs similarity index 64% rename from src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/FriendService/Types/PresenceStatusFilter.cs rename to src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/PresenceStatusFilter.cs index c9a54250f..3ea105872 100644 --- a/src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/FriendService/Types/PresenceStatusFilter.cs +++ b/src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/PresenceStatusFilter.cs @@ -1,4 +1,4 @@ -namespace Ryujinx.HLE.HOS.Services.Friend.ServiceCreator.FriendService +namespace Ryujinx.Horizon.Sdk.Friends.Detail.Ipc { enum PresenceStatusFilter : uint { diff --git a/src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/ServiceCreator.cs b/src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/ServiceCreator.cs new file mode 100644 index 000000000..1be804dfd --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/ServiceCreator.cs @@ -0,0 +1,51 @@ +using Ryujinx.Horizon.Common; +using Ryujinx.Horizon.Sdk.Account; +using Ryujinx.Horizon.Sdk.Sf; + +namespace Ryujinx.Horizon.Sdk.Friends.Detail.Ipc +{ + partial class ServiceCreator : IServiceCreator + { + private readonly IEmulatorAccountManager _accountManager; + private readonly NotificationEventHandler _notificationEventHandler; + private readonly FriendsServicePermissionLevel _permissionLevel; + + public ServiceCreator(IEmulatorAccountManager accountManager, NotificationEventHandler notificationEventHandler, FriendsServicePermissionLevel permissionLevel) + { + _accountManager = accountManager; + _notificationEventHandler = notificationEventHandler; + _permissionLevel = permissionLevel; + } + + [CmifCommand(0)] + public Result CreateFriendService(out IFriendService friendService) + { + friendService = new FriendService(_accountManager, _permissionLevel); + + return Result.Success; + } + + [CmifCommand(1)] // 2.0.0+ + public Result CreateNotificationService(out INotificationService notificationService, Uid userId) + { + if (userId.IsNull) + { + notificationService = null; + + return FriendResult.InvalidArgument; + } + + notificationService = new NotificationService(_notificationEventHandler, userId, _permissionLevel); + + return Result.Success; + } + + [CmifCommand(2)] // 4.0.0+ + public Result CreateDaemonSuspendSessionService(out IDaemonSuspendSessionService daemonSuspendSessionService) + { + daemonSuspendSessionService = new DaemonSuspendSessionService(); + + return Result.Success; + } + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/SizedFriendFilter.cs b/src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/SizedFriendFilter.cs new file mode 100644 index 000000000..d93a2ae29 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/SizedFriendFilter.cs @@ -0,0 +1,25 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.Horizon.Sdk.Friends.Detail.Ipc +{ + [StructLayout(LayoutKind.Sequential, Size = 0x10, Pack = 0x8)] + struct SizedFriendFilter + { + public PresenceStatusFilter PresenceStatus; + public bool IsFavoriteOnly; + public bool IsSameAppPresenceOnly; + public bool IsSameAppPlayedOnly; + public bool IsArbitraryAppPlayedOnly; + public ulong PresenceGroupId; + + public readonly override string ToString() + { + return $"{{ PresenceStatus: {PresenceStatus}, " + + $"IsFavoriteOnly: {IsFavoriteOnly}, " + + $"IsSameAppPresenceOnly: {IsSameAppPresenceOnly}, " + + $"IsSameAppPlayedOnly: {IsSameAppPlayedOnly}, " + + $"IsArbitraryAppPlayedOnly: {IsArbitraryAppPlayedOnly}, " + + $"PresenceGroupId: {PresenceGroupId} }}"; + } + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/SizedNotificationInfo.cs b/src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/SizedNotificationInfo.cs new file mode 100644 index 000000000..0da26a1ae --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Friends/Detail/Ipc/SizedNotificationInfo.cs @@ -0,0 +1,13 @@ +using Ryujinx.Horizon.Sdk.Account; +using System.Runtime.InteropServices; + +namespace Ryujinx.Horizon.Sdk.Friends.Detail.Ipc +{ + [StructLayout(LayoutKind.Sequential, Size = 0x10, Pack = 0x8)] + struct SizedNotificationInfo + { + public NotificationEventType Type; + public uint Padding; + public NetworkServiceAccountId NetworkUserIdPlaceholder; + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Friends/Detail/NintendoNetworkIdFriendImpl.cs b/src/Ryujinx.Horizon/Sdk/Friends/Detail/NintendoNetworkIdFriendImpl.cs new file mode 100644 index 000000000..66d61e4c1 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Friends/Detail/NintendoNetworkIdFriendImpl.cs @@ -0,0 +1,8 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.Horizon.Sdk.Friends.Detail +{ + struct NintendoNetworkIdFriendImpl + { + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Friends/Detail/PlayHistoryImpl.cs b/src/Ryujinx.Horizon/Sdk/Friends/Detail/PlayHistoryImpl.cs new file mode 100644 index 000000000..9f90f0c8f --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Friends/Detail/PlayHistoryImpl.cs @@ -0,0 +1,8 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.Horizon.Sdk.Friends.Detail +{ + struct PlayHistoryImpl + { + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/FriendService/Types/PresenceStatus.cs b/src/Ryujinx.Horizon/Sdk/Friends/Detail/PresenceStatus.cs similarity index 58% rename from src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/FriendService/Types/PresenceStatus.cs rename to src/Ryujinx.Horizon/Sdk/Friends/Detail/PresenceStatus.cs index 7930aff0b..5ddbe14ea 100644 --- a/src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/FriendService/Types/PresenceStatus.cs +++ b/src/Ryujinx.Horizon/Sdk/Friends/Detail/PresenceStatus.cs @@ -1,4 +1,4 @@ -namespace Ryujinx.HLE.HOS.Services.Friend.ServiceCreator.FriendService +namespace Ryujinx.Horizon.Sdk.Friends.Detail { enum PresenceStatus : uint { diff --git a/src/Ryujinx.Horizon/Sdk/Friends/Detail/ProfileExtraImpl.cs b/src/Ryujinx.Horizon/Sdk/Friends/Detail/ProfileExtraImpl.cs new file mode 100644 index 000000000..1548d725f --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Friends/Detail/ProfileExtraImpl.cs @@ -0,0 +1,9 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.Horizon.Sdk.Friends.Detail +{ + [StructLayout(LayoutKind.Sequential, Size = 0x400)] + struct ProfileExtraImpl + { + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Friends/Detail/ProfileImpl.cs b/src/Ryujinx.Horizon/Sdk/Friends/Detail/ProfileImpl.cs new file mode 100644 index 000000000..f779d93cf --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Friends/Detail/ProfileImpl.cs @@ -0,0 +1,8 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.Horizon.Sdk.Friends.Detail +{ + struct ProfileImpl + { + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Friends/Detail/SnsAccountFriendImpl.cs b/src/Ryujinx.Horizon/Sdk/Friends/Detail/SnsAccountFriendImpl.cs new file mode 100644 index 000000000..dc6adf03a --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Friends/Detail/SnsAccountFriendImpl.cs @@ -0,0 +1,8 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.Horizon.Sdk.Friends.Detail +{ + struct SnsAccountFriendImpl + { + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Friends/Detail/UserPresenceImpl.cs b/src/Ryujinx.Horizon/Sdk/Friends/Detail/UserPresenceImpl.cs new file mode 100644 index 000000000..cf4520cf4 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Friends/Detail/UserPresenceImpl.cs @@ -0,0 +1,29 @@ +using Ryujinx.Common.Memory; +using Ryujinx.Horizon.Sdk.Account; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace Ryujinx.Horizon.Sdk.Friends.Detail +{ + [StructLayout(LayoutKind.Sequential, Size = 0xE0)] + struct UserPresenceImpl + { + public Uid UserId; + public long LastTimeOnlineTimestamp; + public PresenceStatus Status; + public bool SamePresenceGroupApplication; + public Array3 Unknown; + public AppKeyValueStorageHolder AppKeyValueStorage; + + [InlineArray(0xC0)] + public struct AppKeyValueStorageHolder + { + public byte Value; + } + + public readonly override string ToString() + { + return $"{{ UserId: {UserId}, LastTimeOnlineTimestamp: {LastTimeOnlineTimestamp}, Status: {Status} }}"; + } + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Friends/Detail/UserPresenceViewImpl.cs b/src/Ryujinx.Horizon/Sdk/Friends/Detail/UserPresenceViewImpl.cs new file mode 100644 index 000000000..04c092600 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Friends/Detail/UserPresenceViewImpl.cs @@ -0,0 +1,9 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.Horizon.Sdk.Friends.Detail +{ + [StructLayout(LayoutKind.Sequential, Size = 0xE0)] + struct UserPresenceViewImpl + { + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Friends/Detail/UserSettingImpl.cs b/src/Ryujinx.Horizon/Sdk/Friends/Detail/UserSettingImpl.cs new file mode 100644 index 000000000..9d057fb1e --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Friends/Detail/UserSettingImpl.cs @@ -0,0 +1,9 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.Horizon.Sdk.Friends.Detail +{ + [StructLayout(LayoutKind.Sequential, Size = 0x800)] + struct UserSettingImpl + { + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Friends/ExternalApplicationCatalog.cs b/src/Ryujinx.Horizon/Sdk/Friends/ExternalApplicationCatalog.cs new file mode 100644 index 000000000..0d9c157d3 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Friends/ExternalApplicationCatalog.cs @@ -0,0 +1,9 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.Horizon.Sdk.Friends +{ + [StructLayout(LayoutKind.Sequential, Size = 0x4B8)] + struct ExternalApplicationCatalog + { + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Friends/ExternalApplicationCatalogId.cs b/src/Ryujinx.Horizon/Sdk/Friends/ExternalApplicationCatalogId.cs new file mode 100644 index 000000000..7ed36cd9d --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Friends/ExternalApplicationCatalogId.cs @@ -0,0 +1,9 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.Horizon.Sdk.Friends +{ + [StructLayout(LayoutKind.Sequential, Size = 0x10, Pack = 0x8)] + struct ExternalApplicationCatalogId + { + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Friends/FacedFriendRequestRegistrationKey.cs b/src/Ryujinx.Horizon/Sdk/Friends/FacedFriendRequestRegistrationKey.cs new file mode 100644 index 000000000..6b5812f64 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Friends/FacedFriendRequestRegistrationKey.cs @@ -0,0 +1,9 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.Horizon.Sdk.Friends +{ + [StructLayout(LayoutKind.Sequential, Size = 0x40, Pack = 0x1)] + struct FacedFriendRequestRegistrationKey + { + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Friends/FriendCode.cs b/src/Ryujinx.Horizon/Sdk/Friends/FriendCode.cs new file mode 100644 index 000000000..d78497a18 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Friends/FriendCode.cs @@ -0,0 +1,9 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.Horizon.Sdk.Friends +{ + [StructLayout(LayoutKind.Sequential, Size = 0x20, Pack = 0x1)] + struct FriendCode + { + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Friends/FriendInvitationGameModeDescription.cs b/src/Ryujinx.Horizon/Sdk/Friends/FriendInvitationGameModeDescription.cs new file mode 100644 index 000000000..29b4a0974 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Friends/FriendInvitationGameModeDescription.cs @@ -0,0 +1,9 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.Horizon.Sdk.Friends +{ + [StructLayout(LayoutKind.Sequential, Size = 0xC00)] + struct FriendInvitationGameModeDescription + { + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Friends/FriendInvitationGroupId.cs b/src/Ryujinx.Horizon/Sdk/Friends/FriendInvitationGroupId.cs new file mode 100644 index 000000000..ef53882b3 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Friends/FriendInvitationGroupId.cs @@ -0,0 +1,9 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.Horizon.Sdk.Friends +{ + [StructLayout(LayoutKind.Sequential, Size = 0x8, Pack = 0x8)] + struct FriendInvitationGroupId + { + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Friends/FriendInvitationId.cs b/src/Ryujinx.Horizon/Sdk/Friends/FriendInvitationId.cs new file mode 100644 index 000000000..7be19d574 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Friends/FriendInvitationId.cs @@ -0,0 +1,8 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.Horizon.Sdk.Friends +{ + struct FriendInvitationId + { + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Friends/FriendResult.cs b/src/Ryujinx.Horizon/Sdk/Friends/FriendResult.cs new file mode 100644 index 000000000..5965d508d --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Friends/FriendResult.cs @@ -0,0 +1,13 @@ +using Ryujinx.Horizon.Common; + +namespace Ryujinx.Horizon.Sdk.Friends +{ + static class FriendResult + { + private const int ModuleId = 121; + + public static Result InvalidArgument => new(ModuleId, 2); + public static Result InternetRequestDenied => new(ModuleId, 6); + public static Result NotificationQueueEmpty => new(ModuleId, 15); + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Friends/InAppScreenName.cs b/src/Ryujinx.Horizon/Sdk/Friends/InAppScreenName.cs new file mode 100644 index 000000000..22574a5cc --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Friends/InAppScreenName.cs @@ -0,0 +1,26 @@ +using Ryujinx.Common.Memory; +using Ryujinx.Horizon.Sdk.Settings; +using System; +using System.Runtime.InteropServices; +using System.Text; + +namespace Ryujinx.Horizon.Sdk.Friends +{ + [StructLayout(LayoutKind.Sequential, Size = 0x48)] + struct InAppScreenName + { + public Array64 Name; + public LanguageCode LanguageCode; + + public override readonly string ToString() + { + int length = Name.AsSpan().IndexOf((byte)0); + if (length < 0) + { + length = 64; + } + + return Encoding.UTF8.GetString(Name.AsSpan()[..length]); + } + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Friends/MiiImageUrlParam.cs b/src/Ryujinx.Horizon/Sdk/Friends/MiiImageUrlParam.cs new file mode 100644 index 000000000..8790bb931 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Friends/MiiImageUrlParam.cs @@ -0,0 +1,9 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.Horizon.Sdk.Friends +{ + [StructLayout(LayoutKind.Sequential, Size = 0x10, Pack = 0x1)] + struct MiiImageUrlParam + { + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Friends/MiiName.cs b/src/Ryujinx.Horizon/Sdk/Friends/MiiName.cs new file mode 100644 index 000000000..e73c0d833 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Friends/MiiName.cs @@ -0,0 +1,9 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.Horizon.Sdk.Friends +{ + [StructLayout(LayoutKind.Sequential, Size = 0x20, Pack = 0x1)] + struct MiiName + { + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Friends/NintendoNetworkIdUserInfo.cs b/src/Ryujinx.Horizon/Sdk/Friends/NintendoNetworkIdUserInfo.cs new file mode 100644 index 000000000..a2a9e046f --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Friends/NintendoNetworkIdUserInfo.cs @@ -0,0 +1,9 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.Horizon.Sdk.Friends +{ + [StructLayout(LayoutKind.Sequential, Size = 0x38)] + struct NintendoNetworkIdUserInfo + { + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/Types/PlayHistoryRegistrationKey.cs b/src/Ryujinx.Horizon/Sdk/Friends/PlayHistoryRegistrationKey.cs similarity index 59% rename from src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/Types/PlayHistoryRegistrationKey.cs rename to src/Ryujinx.Horizon/Sdk/Friends/PlayHistoryRegistrationKey.cs index 9687c5478..bb672a795 100644 --- a/src/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/Types/PlayHistoryRegistrationKey.cs +++ b/src/Ryujinx.Horizon/Sdk/Friends/PlayHistoryRegistrationKey.cs @@ -1,9 +1,10 @@ using Ryujinx.Common.Memory; +using Ryujinx.Horizon.Sdk.Account; using System.Runtime.InteropServices; -namespace Ryujinx.HLE.HOS.Services.Friend.ServiceCreator +namespace Ryujinx.Horizon.Sdk.Friends { - [StructLayout(LayoutKind.Sequential, Size = 0x20)] + [StructLayout(LayoutKind.Sequential, Size = 0x40)] struct PlayHistoryRegistrationKey { public ushort Type; @@ -11,6 +12,7 @@ namespace Ryujinx.HLE.HOS.Services.Friend.ServiceCreator public byte UserIdBool; public byte UnknownBool; public Array11 Reserved; - public Array16 Uuid; + public Uid Uuid; + public Array32 HmacHash; } } diff --git a/src/Ryujinx.Horizon/Sdk/Friends/PlayHistoryStatistics.cs b/src/Ryujinx.Horizon/Sdk/Friends/PlayHistoryStatistics.cs new file mode 100644 index 000000000..ea3e3d997 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Friends/PlayHistoryStatistics.cs @@ -0,0 +1,9 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.Horizon.Sdk.Friends +{ + [StructLayout(LayoutKind.Sequential, Size = 0x10, Pack = 0x8)] + struct PlayHistoryStatistics + { + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Friends/Relationship.cs b/src/Ryujinx.Horizon/Sdk/Friends/Relationship.cs new file mode 100644 index 000000000..efba09a8f --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Friends/Relationship.cs @@ -0,0 +1,9 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.Horizon.Sdk.Friends +{ + [StructLayout(LayoutKind.Sequential, Size = 0x8, Pack = 0x1)] + struct Relationship + { + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Friends/RequestId.cs b/src/Ryujinx.Horizon/Sdk/Friends/RequestId.cs new file mode 100644 index 000000000..3236a1d7d --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Friends/RequestId.cs @@ -0,0 +1,9 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.Horizon.Sdk.Friends +{ + [StructLayout(LayoutKind.Sequential, Size = 0x8, Pack = 0x8)] + struct RequestId + { + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Friends/SnsAccountLinkage.cs b/src/Ryujinx.Horizon/Sdk/Friends/SnsAccountLinkage.cs new file mode 100644 index 000000000..b4660d9e7 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Friends/SnsAccountLinkage.cs @@ -0,0 +1,9 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.Horizon.Sdk.Friends +{ + [StructLayout(LayoutKind.Sequential, Size = 0x8, Pack = 0x1)] + struct SnsAccountLinkage + { + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Friends/SnsAccountProfile.cs b/src/Ryujinx.Horizon/Sdk/Friends/SnsAccountProfile.cs new file mode 100644 index 000000000..d872b3dac --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Friends/SnsAccountProfile.cs @@ -0,0 +1,9 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.Horizon.Sdk.Friends +{ + [StructLayout(LayoutKind.Sequential, Size = 0x380)] + struct SnsAccountProfile + { + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Friends/Url.cs b/src/Ryujinx.Horizon/Sdk/Friends/Url.cs new file mode 100644 index 000000000..833ee1230 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Friends/Url.cs @@ -0,0 +1,30 @@ +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Text; + +namespace Ryujinx.Horizon.Sdk.Friends +{ + [StructLayout(LayoutKind.Sequential, Size = 0xA0, Pack = 0x1)] + struct Url + { + public UrlStorage Path; + + [InlineArray(0xA0)] + public struct UrlStorage + { + public byte Value; + } + + public override readonly string ToString() + { + int length = ((ReadOnlySpan)Path).IndexOf((byte)0); + if (length < 0) + { + length = 33; + } + + return Encoding.UTF8.GetString(((ReadOnlySpan)Path)[..length]); + } + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Friends/WebPageUrl.cs b/src/Ryujinx.Horizon/Sdk/Friends/WebPageUrl.cs new file mode 100644 index 000000000..85488af61 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Friends/WebPageUrl.cs @@ -0,0 +1,9 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.Horizon.Sdk.Friends +{ + [StructLayout(LayoutKind.Sequential, Size = 0x1000)] + struct WebPageUrl + { + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Ncm/ApplicationId.cs b/src/Ryujinx.Horizon/Sdk/Ncm/ApplicationId.cs index 4c5e76e6f..24b7d9cab 100644 --- a/src/Ryujinx.Horizon/Sdk/Ncm/ApplicationId.cs +++ b/src/Ryujinx.Horizon/Sdk/Ncm/ApplicationId.cs @@ -1,6 +1,6 @@ namespace Ryujinx.Horizon.Sdk.Ncm { - readonly struct ApplicationId + public readonly struct ApplicationId { public readonly ulong Id; diff --git a/src/Ryujinx.Horizon/Sdk/Ncm/StorageId.cs b/src/Ryujinx.Horizon/Sdk/Ncm/StorageId.cs new file mode 100644 index 000000000..e2fb32505 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Ncm/StorageId.cs @@ -0,0 +1,13 @@ +namespace Ryujinx.Horizon.Sdk.Ncm +{ + public enum StorageId : byte + { + None, + Host, + GameCard, + BuiltInSystem, + BuiltInUser, + SdCard, + Any, + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Ngc/Detail/AhoCorasick.cs b/src/Ryujinx.Horizon/Sdk/Ngc/Detail/AhoCorasick.cs index 6acb9be97..03f61c218 100644 --- a/src/Ryujinx.Horizon/Sdk/Ngc/Detail/AhoCorasick.cs +++ b/src/Ryujinx.Horizon/Sdk/Ngc/Detail/AhoCorasick.cs @@ -221,7 +221,7 @@ namespace Ryujinx.Horizon.Sdk.Ngc.Detail if (includeMultiWord) { int lastMultiWordIndex = 0; - string multiWord = ""; + string multiWord = string.Empty; while (_multiWordMap.Has(nodePlainIndex)) { diff --git a/src/Ryujinx.Horizon/Sdk/Ns/ApplicationControlProperty.cs b/src/Ryujinx.Horizon/Sdk/Ns/ApplicationControlProperty.cs new file mode 100644 index 000000000..12c19168d --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Ns/ApplicationControlProperty.cs @@ -0,0 +1,309 @@ +using Ryujinx.Common.Memory; +using Ryujinx.Horizon.Sdk.Arp.Detail; +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Text; + +namespace Ryujinx.Horizon.Sdk.Ns +{ + public struct ApplicationControlProperty + { + public Array16 Title; + public Array37 Isbn; + public StartupUserAccountValue StartupUserAccount; + public UserAccountSwitchLockValue UserAccountSwitchLock; + public AddOnContentRegistrationTypeValue AddOnContentRegistrationType; + public AttributeFlagValue AttributeFlag; + public uint SupportedLanguageFlag; + public ParentalControlFlagValue ParentalControlFlag; + public ScreenshotValue Screenshot; + public VideoCaptureValue VideoCapture; + public DataLossConfirmationValue DataLossConfirmation; + public PlayLogPolicyValue PlayLogPolicy; + public ulong PresenceGroupId; + public Array32 RatingAge; + public Array16 DisplayVersion; + public ulong AddOnContentBaseId; + public ulong SaveDataOwnerId; + public long UserAccountSaveDataSize; + public long UserAccountSaveDataJournalSize; + public long DeviceSaveDataSize; + public long DeviceSaveDataJournalSize; + public long BcatDeliveryCacheStorageSize; + public Array8 ApplicationErrorCodeCategory; + public Array8 LocalCommunicationId; + public LogoTypeValue LogoType; + public LogoHandlingValue LogoHandling; + public RuntimeAddOnContentInstallValue RuntimeAddOnContentInstall; + public RuntimeParameterDeliveryValue RuntimeParameterDelivery; + public Array2 Reserved30F4; + public CrashReportValue CrashReport; + public HdcpValue Hdcp; + public ulong SeedForPseudoDeviceId; + public Array65 BcatPassphrase; + public StartupUserAccountOptionFlagValue StartupUserAccountOption; + public Array6 ReservedForUserAccountSaveDataOperation; + public long UserAccountSaveDataSizeMax; + public long UserAccountSaveDataJournalSizeMax; + public long DeviceSaveDataSizeMax; + public long DeviceSaveDataJournalSizeMax; + public long TemporaryStorageSize; + public long CacheStorageSize; + public long CacheStorageJournalSize; + public long CacheStorageDataAndJournalSizeMax; + public ushort CacheStorageIndexMax; + public byte Reserved318A; + public byte RuntimeUpgrade; + public uint SupportingLimitedLicenses; + public Array16 PlayLogQueryableApplicationId; + public PlayLogQueryCapabilityValue PlayLogQueryCapability; + public RepairFlagValue RepairFlag; + public byte ProgramIndex; + public RequiredNetworkServiceLicenseOnLaunchValue RequiredNetworkServiceLicenseOnLaunchFlag; + public Array4 Reserved3214; + public ApplicationNeighborDetectionClientConfiguration NeighborDetectionClientConfiguration; + public ApplicationJitConfiguration JitConfiguration; + public RequiredAddOnContentsSetBinaryDescriptor RequiredAddOnContentsSetBinaryDescriptors; + public PlayReportPermissionValue PlayReportPermission; + public CrashScreenshotForProdValue CrashScreenshotForProd; + public CrashScreenshotForDevValue CrashScreenshotForDev; + public byte ContentsAvailabilityTransitionPolicy; + public Array4 Reserved3404; + public AccessibleLaunchRequiredVersionValue AccessibleLaunchRequiredVersion; + public ByteArray3000 Reserved3448; + + public readonly string IsbnString => Encoding.UTF8.GetString(Isbn.AsSpan()).TrimEnd('\0'); + public readonly string DisplayVersionString => Encoding.UTF8.GetString(DisplayVersion.AsSpan()).TrimEnd('\0'); + public readonly string ApplicationErrorCodeCategoryString => Encoding.UTF8.GetString(ApplicationErrorCodeCategory.AsSpan()).TrimEnd('\0'); + public readonly string BcatPassphraseString => Encoding.UTF8.GetString(BcatPassphrase.AsSpan()).TrimEnd('\0'); + + public struct ApplicationTitle + { + public ByteArray512 Name; + public Array256 Publisher; + + public readonly string NameString => Encoding.UTF8.GetString(Name.AsSpan()).TrimEnd('\0'); + public readonly string PublisherString => Encoding.UTF8.GetString(Publisher.AsSpan()).TrimEnd('\0'); + } + + public struct ApplicationNeighborDetectionClientConfiguration + { + public ApplicationNeighborDetectionGroupConfiguration SendGroupConfiguration; + public Array16 ReceivableGroupConfigurations; + } + + public struct ApplicationNeighborDetectionGroupConfiguration + { + public ulong GroupId; + public Array16 Key; + } + + public struct ApplicationJitConfiguration + { + public JitConfigurationFlag Flags; + public long MemorySize; + } + + public struct RequiredAddOnContentsSetBinaryDescriptor + { + public Array32 Descriptors; + } + + public struct AccessibleLaunchRequiredVersionValue + { + public Array8 ApplicationId; + } + + public enum Language + { + AmericanEnglish = 0, + BritishEnglish = 1, + Japanese = 2, + French = 3, + German = 4, + LatinAmericanSpanish = 5, + Spanish = 6, + Italian = 7, + Dutch = 8, + CanadianFrench = 9, + Portuguese = 10, + Russian = 11, + Korean = 12, + TraditionalChinese = 13, + SimplifiedChinese = 14, + BrazilianPortuguese = 15, + } + + public enum Organization + { + CERO = 0, + GRACGCRB = 1, + GSRMR = 2, + ESRB = 3, + ClassInd = 4, + USK = 5, + PEGI = 6, + PEGIPortugal = 7, + PEGIBBFC = 8, + Russian = 9, + ACB = 10, + OFLC = 11, + IARCGeneric = 12, + } + + public enum StartupUserAccountValue : byte + { + None = 0, + Required = 1, + RequiredWithNetworkServiceAccountAvailable = 2, + } + + public enum UserAccountSwitchLockValue : byte + { + Disable = 0, + Enable = 1, + } + + public enum AddOnContentRegistrationTypeValue : byte + { + AllOnLaunch = 0, + OnDemand = 1, + } + + [Flags] + public enum AttributeFlagValue + { + None = 0, + Demo = 1 << 0, + RetailInteractiveDisplay = 1 << 1, + } + + public enum ParentalControlFlagValue + { + None = 0, + FreeCommunication = 1, + } + + public enum ScreenshotValue : byte + { + Allow = 0, + Deny = 1, + } + + public enum VideoCaptureValue : byte + { + Disable = 0, + Manual = 1, + Enable = 2, + } + + public enum DataLossConfirmationValue : byte + { + None = 0, + Required = 1, + } + + public enum PlayLogPolicyValue : byte + { + Open = 0, + LogOnly = 1, + None = 2, + Closed = 3, + All = Open, + } + + public enum LogoTypeValue : byte + { + LicensedByNintendo = 0, + DistributedByNintendo = 1, + Nintendo = 2, + } + + public enum LogoHandlingValue : byte + { + Auto = 0, + Manual = 1, + } + + public enum RuntimeAddOnContentInstallValue : byte + { + Deny = 0, + AllowAppend = 1, + AllowAppendButDontDownloadWhenUsingNetwork = 2, + } + + public enum RuntimeParameterDeliveryValue : byte + { + Always = 0, + AlwaysIfUserStateMatched = 1, + OnRestart = 2, + } + + public enum CrashReportValue : byte + { + Deny = 0, + Allow = 1, + } + + public enum HdcpValue : byte + { + None = 0, + Required = 1, + } + + [Flags] + public enum StartupUserAccountOptionFlagValue : byte + { + None = 0, + IsOptional = 1 << 0, + } + + public enum PlayLogQueryCapabilityValue : byte + { + None = 0, + WhiteList = 1, + All = 2, + } + + [Flags] + public enum RepairFlagValue : byte + { + None = 0, + SuppressGameCardAccess = 1 << 0, + } + + [Flags] + public enum RequiredNetworkServiceLicenseOnLaunchValue : byte + { + None = 0, + Common = 1 << 0, + } + + [Flags] + public enum JitConfigurationFlag : ulong + { + None = 0, + Enabled = 1 << 0, + } + + [Flags] + public enum PlayReportPermissionValue : byte + { + None = 0, + TargetMarketing = 1 << 0, + } + + public enum CrashScreenshotForProdValue : byte + { + Deny = 0, + Allow = 1, + } + + public enum CrashScreenshotForDevValue : byte + { + Deny = 0, + Allow = 1, + } + } +} diff --git a/src/Ryujinx.Horizon/Sdk/OsTypes/Impl/MultiWaitImpl.cs b/src/Ryujinx.Horizon/Sdk/OsTypes/Impl/MultiWaitImpl.cs index 2aefb0db5..406352003 100644 --- a/src/Ryujinx.Horizon/Sdk/OsTypes/Impl/MultiWaitImpl.cs +++ b/src/Ryujinx.Horizon/Sdk/OsTypes/Impl/MultiWaitImpl.cs @@ -21,6 +21,8 @@ namespace Ryujinx.Horizon.Sdk.OsTypes.Impl public long CurrentTime { get; private set; } + public IEnumerable MultiWaits => _multiWaits; + public MultiWaitImpl() { _multiWaits = new List(); diff --git a/src/Ryujinx.Horizon/Sdk/OsTypes/MultiWait.cs b/src/Ryujinx.Horizon/Sdk/OsTypes/MultiWait.cs index 0e73e3f88..41d17802a 100644 --- a/src/Ryujinx.Horizon/Sdk/OsTypes/MultiWait.cs +++ b/src/Ryujinx.Horizon/Sdk/OsTypes/MultiWait.cs @@ -1,4 +1,5 @@ using Ryujinx.Horizon.Sdk.OsTypes.Impl; +using System.Collections.Generic; namespace Ryujinx.Horizon.Sdk.OsTypes { @@ -6,6 +7,8 @@ namespace Ryujinx.Horizon.Sdk.OsTypes { private readonly MultiWaitImpl _impl; + public IEnumerable MultiWaits => _impl.MultiWaits; + public MultiWait() { _impl = new MultiWaitImpl(); diff --git a/src/Ryujinx.Horizon/Sdk/ServiceUtil.cs b/src/Ryujinx.Horizon/Sdk/ServiceUtil.cs index ccd6c93a6..5527c1e35 100644 --- a/src/Ryujinx.Horizon/Sdk/ServiceUtil.cs +++ b/src/Ryujinx.Horizon/Sdk/ServiceUtil.cs @@ -35,5 +35,254 @@ namespace Ryujinx.Horizon.Sdk return CmifMessage.ParseResponse(out response, HorizonStatic.AddressSpace.GetWritableRegion(tlsAddress, tlsSize).Memory.Span, false, 0); } + + public static Result SendRequest( + out CmifResponse response, + int sessionHandle, + uint requestId, + bool sendPid, + scoped ReadOnlySpan data, + ReadOnlySpan bufferFlags, + ReadOnlySpan buffers) + { + ulong tlsAddress = HorizonStatic.ThreadContext.TlsAddress; + int tlsSize = Api.TlsMessageBufferSize; + + using (var tlsRegion = HorizonStatic.AddressSpace.GetWritableRegion(tlsAddress, tlsSize)) + { + CmifRequestFormat format = new() + { + DataSize = data.Length, + RequestId = requestId, + SendPid = sendPid, + }; + + for (int index = 0; index < bufferFlags.Length; index++) + { + FormatProcessBuffer(ref format, bufferFlags[index]); + } + + CmifRequest request = CmifMessage.CreateRequest(tlsRegion.Memory.Span, format); + + for (int index = 0; index < buffers.Length; index++) + { + RequestProcessBuffer(ref request, buffers[index], bufferFlags[index]); + } + + data.CopyTo(request.Data); + } + + Result result = HorizonStatic.Syscall.SendSyncRequest(sessionHandle); + + if (result.IsFailure) + { + response = default; + + return result; + } + + return CmifMessage.ParseResponse(out response, HorizonStatic.AddressSpace.GetWritableRegion(tlsAddress, tlsSize).Memory.Span, false, 0); + } + + private static void FormatProcessBuffer(ref CmifRequestFormat format, HipcBufferFlags flags) + { + if (flags == 0) + { + return; + } + + bool isIn = flags.HasFlag(HipcBufferFlags.In); + bool isOut = flags.HasFlag(HipcBufferFlags.Out); + + if (flags.HasFlag(HipcBufferFlags.AutoSelect)) + { + if (isIn) + { + format.InAutoBuffersCount++; + } + + if (isOut) + { + format.OutAutoBuffersCount++; + } + } + else if (flags.HasFlag(HipcBufferFlags.Pointer)) + { + if (isIn) + { + format.InPointersCount++; + } + + if (isOut) + { + if (flags.HasFlag(HipcBufferFlags.FixedSize)) + { + format.OutFixedPointersCount++; + } + else + { + format.OutPointersCount++; + } + } + } + else if (flags.HasFlag(HipcBufferFlags.MapAlias)) + { + if (isIn && isOut) + { + format.InOutBuffersCount++; + } + else if (isIn) + { + format.InBuffersCount++; + } + else + { + format.OutBuffersCount++; + } + } + } + + private static void RequestProcessBuffer(ref CmifRequest request, PointerAndSize buffer, HipcBufferFlags flags) + { + if (flags == 0) + { + return; + } + + bool isIn = flags.HasFlag(HipcBufferFlags.In); + bool isOut = flags.HasFlag(HipcBufferFlags.Out); + + if (flags.HasFlag(HipcBufferFlags.AutoSelect)) + { + HipcBufferMode mode = HipcBufferMode.Normal; + + if (flags.HasFlag(HipcBufferFlags.MapTransferAllowsNonSecure)) + { + mode = HipcBufferMode.NonSecure; + } + + if (flags.HasFlag(HipcBufferFlags.MapTransferAllowsNonDevice)) + { + mode = HipcBufferMode.NonDevice; + } + + if (isIn) + { + RequestInAutoBuffer(ref request, buffer.Address, buffer.Size, mode); + } + + if (isOut) + { + RequestOutAutoBuffer(ref request, buffer.Address, buffer.Size, mode); + } + } + else if (flags.HasFlag(HipcBufferFlags.Pointer)) + { + if (isIn) + { + RequestInPointer(ref request, buffer.Address, buffer.Size); + } + + if (isOut) + { + if (flags.HasFlag(HipcBufferFlags.FixedSize)) + { + RequestOutFixedPointer(ref request, buffer.Address, buffer.Size); + } + else + { + RequestOutPointer(ref request, buffer.Address, buffer.Size); + } + } + } + else if (flags.HasFlag(HipcBufferFlags.MapAlias)) + { + HipcBufferMode mode = HipcBufferMode.Normal; + + if (flags.HasFlag(HipcBufferFlags.MapTransferAllowsNonSecure)) + { + mode = HipcBufferMode.NonSecure; + } + + if (flags.HasFlag(HipcBufferFlags.MapTransferAllowsNonDevice)) + { + mode = HipcBufferMode.NonDevice; + } + + if (isIn && isOut) + { + RequestInOutBuffer(ref request, buffer.Address, buffer.Size, mode); + } + else if (isIn) + { + RequestInBuffer(ref request, buffer.Address, buffer.Size, mode); + } + else + { + RequestOutBuffer(ref request, buffer.Address, buffer.Size, mode); + } + } + } + + private static void RequestInAutoBuffer(ref CmifRequest request, ulong bufferAddress, ulong bufferSize, HipcBufferMode mode) + { + if (request.ServerPointerSize != 0 && bufferSize <= (ulong)request.ServerPointerSize) + { + RequestInPointer(ref request, bufferAddress, bufferSize); + RequestInBuffer(ref request, 0UL, 0UL, mode); + } + else + { + RequestInPointer(ref request, 0UL, 0UL); + RequestInBuffer(ref request, bufferAddress, bufferSize, mode); + } + } + + private static void RequestOutAutoBuffer(ref CmifRequest request, ulong bufferAddress, ulong bufferSize, HipcBufferMode mode) + { + if (request.ServerPointerSize != 0 && bufferSize <= (ulong)request.ServerPointerSize) + { + RequestOutPointer(ref request, bufferAddress, bufferSize); + RequestOutBuffer(ref request, 0UL, 0UL, mode); + } + else + { + RequestOutPointer(ref request, 0UL, 0UL); + RequestOutBuffer(ref request, bufferAddress, bufferSize, mode); + } + } + + private static void RequestInBuffer(ref CmifRequest request, ulong bufferAddress, ulong bufferSize, HipcBufferMode mode) + { + request.Hipc.SendBuffers[request.SendBufferIndex++] = new HipcBufferDescriptor(bufferAddress, bufferSize, mode); + } + + private static void RequestOutBuffer(ref CmifRequest request, ulong bufferAddress, ulong bufferSize, HipcBufferMode mode) + { + request.Hipc.ReceiveBuffers[request.RecvBufferIndex++] = new HipcBufferDescriptor(bufferAddress, bufferSize, mode); + } + + private static void RequestInOutBuffer(ref CmifRequest request, ulong bufferAddress, ulong bufferSize, HipcBufferMode mode) + { + request.Hipc.ExchangeBuffers[request.ExchBufferIndex++] = new HipcBufferDescriptor(bufferAddress, bufferSize, mode); + } + + private static void RequestInPointer(ref CmifRequest request, ulong bufferAddress, ulong bufferSize) + { + request.Hipc.SendStatics[request.SendStaticIndex++] = new HipcStaticDescriptor(bufferAddress, (ushort)bufferSize, request.CurrentInPointerId++); + request.ServerPointerSize -= (int)bufferSize; + } + + private static void RequestOutFixedPointer(ref CmifRequest request, ulong bufferAddress, ulong bufferSize) + { + request.Hipc.ReceiveList[request.RecvListIndex++] = new HipcReceiveListEntry(bufferAddress, (ushort)bufferSize); + request.ServerPointerSize -= (int)bufferSize; + } + + private static void RequestOutPointer(ref CmifRequest request, ulong bufferAddress, ulong bufferSize) + { + RequestOutFixedPointer(ref request, bufferAddress, bufferSize); + request.OutPointerSizes[request.OutPointerSizeIndex++] = (ushort)bufferSize; + } } } diff --git a/src/Ryujinx.Horizon/Sdk/Settings/BatteryLot.cs b/src/Ryujinx.Horizon/Sdk/Settings/BatteryLot.cs new file mode 100644 index 000000000..71185fcd0 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Settings/BatteryLot.cs @@ -0,0 +1,9 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.Horizon.Sdk.Settings +{ + [StructLayout(LayoutKind.Sequential, Size = 0x18, Pack = 0x1)] + struct BatteryLot + { + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Settings/Factory/AccelerometerOffset.cs b/src/Ryujinx.Horizon/Sdk/Settings/Factory/AccelerometerOffset.cs new file mode 100644 index 000000000..292a368f1 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Settings/Factory/AccelerometerOffset.cs @@ -0,0 +1,12 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.Horizon.Sdk.Settings.Factory +{ + [StructLayout(LayoutKind.Sequential, Size = 0x6, Pack = 0x2)] + struct AccelerometerOffset + { + public ushort X; + public ushort Y; + public ushort Z; + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Settings/Factory/AccelerometerScale.cs b/src/Ryujinx.Horizon/Sdk/Settings/Factory/AccelerometerScale.cs new file mode 100644 index 000000000..ef9d17ef9 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Settings/Factory/AccelerometerScale.cs @@ -0,0 +1,12 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.Horizon.Sdk.Settings.Factory +{ + [StructLayout(LayoutKind.Sequential, Size = 0x6, Pack = 0x2)] + struct AccelerometerScale + { + public ushort X; + public ushort Y; + public ushort Z; + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Settings/Factory/AmiiboEcdsaCertificate.cs b/src/Ryujinx.Horizon/Sdk/Settings/Factory/AmiiboEcdsaCertificate.cs new file mode 100644 index 000000000..7cbab2f09 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Settings/Factory/AmiiboEcdsaCertificate.cs @@ -0,0 +1,9 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.Horizon.Sdk.Settings.Factory +{ + [StructLayout(LayoutKind.Sequential, Size = 0x74, Pack = 0x4)] + struct AmiiboEcdsaCertificate + { + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Settings/Factory/AmiiboEcqvBlsCertificate.cs b/src/Ryujinx.Horizon/Sdk/Settings/Factory/AmiiboEcqvBlsCertificate.cs new file mode 100644 index 000000000..8d16b51b1 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Settings/Factory/AmiiboEcqvBlsCertificate.cs @@ -0,0 +1,9 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.Horizon.Sdk.Settings.Factory +{ + [StructLayout(LayoutKind.Sequential, Size = 0x24, Pack = 0x4)] + struct AmiiboEcqvBlsCertificate + { + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Settings/Factory/AmiiboEcqvBlsKey.cs b/src/Ryujinx.Horizon/Sdk/Settings/Factory/AmiiboEcqvBlsKey.cs new file mode 100644 index 000000000..da6ca53bc --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Settings/Factory/AmiiboEcqvBlsKey.cs @@ -0,0 +1,9 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.Horizon.Sdk.Settings.Factory +{ + [StructLayout(LayoutKind.Sequential, Size = 0x48, Pack = 0x4)] + struct AmiiboEcqvBlsKey + { + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Settings/Factory/AmiiboEcqvBlsRootCertificate.cs b/src/Ryujinx.Horizon/Sdk/Settings/Factory/AmiiboEcqvBlsRootCertificate.cs new file mode 100644 index 000000000..e69e38a1a --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Settings/Factory/AmiiboEcqvBlsRootCertificate.cs @@ -0,0 +1,9 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.Horizon.Sdk.Settings.Factory +{ + [StructLayout(LayoutKind.Sequential, Size = 0x94, Pack = 0x4)] + struct AmiiboEcqvBlsRootCertificate + { + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Settings/Factory/AmiiboEcqvCertificate.cs b/src/Ryujinx.Horizon/Sdk/Settings/Factory/AmiiboEcqvCertificate.cs new file mode 100644 index 000000000..43742fbb7 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Settings/Factory/AmiiboEcqvCertificate.cs @@ -0,0 +1,9 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.Horizon.Sdk.Settings.Factory +{ + [StructLayout(LayoutKind.Sequential, Size = 0x18, Pack = 0x4)] + struct AmiiboEcqvCertificate + { + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Settings/Factory/AmiiboKey.cs b/src/Ryujinx.Horizon/Sdk/Settings/Factory/AmiiboKey.cs new file mode 100644 index 000000000..43ffccb00 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Settings/Factory/AmiiboKey.cs @@ -0,0 +1,9 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.Horizon.Sdk.Settings.Factory +{ + [StructLayout(LayoutKind.Sequential, Size = 0x58, Pack = 0x4)] + struct AmiiboKey + { + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Settings/Factory/AnalogStickFactoryCalibration.cs b/src/Ryujinx.Horizon/Sdk/Settings/Factory/AnalogStickFactoryCalibration.cs new file mode 100644 index 000000000..3fe6f3223 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Settings/Factory/AnalogStickFactoryCalibration.cs @@ -0,0 +1,9 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.Horizon.Sdk.Settings.Factory +{ + [StructLayout(LayoutKind.Sequential, Size = 0x9, Pack = 0x1)] + struct AnalogStickFactoryCalibration + { + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Settings/Factory/AnalogStickModelParameter.cs b/src/Ryujinx.Horizon/Sdk/Settings/Factory/AnalogStickModelParameter.cs new file mode 100644 index 000000000..a442032c7 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Settings/Factory/AnalogStickModelParameter.cs @@ -0,0 +1,9 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.Horizon.Sdk.Settings.Factory +{ + [StructLayout(LayoutKind.Sequential, Size = 0x12, Pack = 0x1)] + struct AnalogStickModelParameter + { + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Settings/Factory/BdAddress.cs b/src/Ryujinx.Horizon/Sdk/Settings/Factory/BdAddress.cs new file mode 100644 index 000000000..519d72e8f --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Settings/Factory/BdAddress.cs @@ -0,0 +1,9 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.Horizon.Sdk.Settings.Factory +{ + [StructLayout(LayoutKind.Sequential, Size = 0x6, Pack = 0x1)] + struct BdAddress + { + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Settings/Factory/ConfigurationId1.cs b/src/Ryujinx.Horizon/Sdk/Settings/Factory/ConfigurationId1.cs new file mode 100644 index 000000000..40565805f --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Settings/Factory/ConfigurationId1.cs @@ -0,0 +1,9 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.Horizon.Sdk.Settings.Factory +{ + [StructLayout(LayoutKind.Sequential, Size = 0x1E, Pack = 0x1)] + struct ConfigurationId1 + { + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Settings/Factory/ConsoleSixAxisSensorHorizontalOffset.cs b/src/Ryujinx.Horizon/Sdk/Settings/Factory/ConsoleSixAxisSensorHorizontalOffset.cs new file mode 100644 index 000000000..c5503edc5 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Settings/Factory/ConsoleSixAxisSensorHorizontalOffset.cs @@ -0,0 +1,12 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.Horizon.Sdk.Settings.Factory +{ + [StructLayout(LayoutKind.Sequential, Size = 0x6, Pack = 0x2)] + struct ConsoleSixAxisSensorHorizontalOffset + { + public ushort X; + public ushort Y; + public ushort Z; + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Settings/Factory/CountryCode.cs b/src/Ryujinx.Horizon/Sdk/Settings/Factory/CountryCode.cs new file mode 100644 index 000000000..daf2ba3b8 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Settings/Factory/CountryCode.cs @@ -0,0 +1,8 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.Horizon.Sdk.Settings.Factory +{ + struct CountryCode + { + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Settings/Factory/EccB233DeviceCertificate.cs b/src/Ryujinx.Horizon/Sdk/Settings/Factory/EccB233DeviceCertificate.cs new file mode 100644 index 000000000..727408ed5 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Settings/Factory/EccB233DeviceCertificate.cs @@ -0,0 +1,9 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.Horizon.Sdk.Settings.Factory +{ + [StructLayout(LayoutKind.Sequential, Size = 0x180)] + struct EccB233DeviceCertificate + { + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Settings/Factory/EccB233DeviceKey.cs b/src/Ryujinx.Horizon/Sdk/Settings/Factory/EccB233DeviceKey.cs new file mode 100644 index 000000000..a0481f4dc --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Settings/Factory/EccB233DeviceKey.cs @@ -0,0 +1,9 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.Horizon.Sdk.Settings.Factory +{ + [StructLayout(LayoutKind.Sequential, Size = 0x58, Pack = 0x4)] + struct EccB233DeviceKey + { + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Settings/Factory/GameCardCertificate.cs b/src/Ryujinx.Horizon/Sdk/Settings/Factory/GameCardCertificate.cs new file mode 100644 index 000000000..ce3908afe --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Settings/Factory/GameCardCertificate.cs @@ -0,0 +1,9 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.Horizon.Sdk.Settings.Factory +{ + [StructLayout(LayoutKind.Sequential, Size = 0x400)] + struct GameCardCertificate + { + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Settings/Factory/GameCardKey.cs b/src/Ryujinx.Horizon/Sdk/Settings/Factory/GameCardKey.cs new file mode 100644 index 000000000..81144ac48 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Settings/Factory/GameCardKey.cs @@ -0,0 +1,9 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.Horizon.Sdk.Settings.Factory +{ + [StructLayout(LayoutKind.Sequential, Size = 0x138)] + struct GameCardKey + { + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Settings/Factory/GyroscopeOffset.cs b/src/Ryujinx.Horizon/Sdk/Settings/Factory/GyroscopeOffset.cs new file mode 100644 index 000000000..801d117cb --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Settings/Factory/GyroscopeOffset.cs @@ -0,0 +1,12 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.Horizon.Sdk.Settings.Factory +{ + [StructLayout(LayoutKind.Sequential, Size = 0x6, Pack = 0x2)] + struct GyroscopeOffset + { + public ushort X; + public ushort Y; + public ushort Z; + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Settings/Factory/GyroscopeScale.cs b/src/Ryujinx.Horizon/Sdk/Settings/Factory/GyroscopeScale.cs new file mode 100644 index 000000000..7812281f8 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Settings/Factory/GyroscopeScale.cs @@ -0,0 +1,12 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.Horizon.Sdk.Settings.Factory +{ + [StructLayout(LayoutKind.Sequential, Size = 0x6, Pack = 0x2)] + struct GyroscopeScale + { + public ushort X; + public ushort Y; + public ushort Z; + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Settings/Factory/MacAddress.cs b/src/Ryujinx.Horizon/Sdk/Settings/Factory/MacAddress.cs new file mode 100644 index 000000000..65e222ee5 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Settings/Factory/MacAddress.cs @@ -0,0 +1,9 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.Horizon.Sdk.Settings.Factory +{ + [StructLayout(LayoutKind.Sequential, Size = 0x6, Pack = 0x1)] + struct MacAddress + { + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Settings/Factory/Rsa2048DeviceCertificate.cs b/src/Ryujinx.Horizon/Sdk/Settings/Factory/Rsa2048DeviceCertificate.cs new file mode 100644 index 000000000..57217059f --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Settings/Factory/Rsa2048DeviceCertificate.cs @@ -0,0 +1,9 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.Horizon.Sdk.Settings.Factory +{ + [StructLayout(LayoutKind.Sequential, Size = 0x240)] + struct Rsa2048DeviceCertificate + { + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Settings/Factory/Rsa2048DeviceKey.cs b/src/Ryujinx.Horizon/Sdk/Settings/Factory/Rsa2048DeviceKey.cs new file mode 100644 index 000000000..d2fd51cf7 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Settings/Factory/Rsa2048DeviceKey.cs @@ -0,0 +1,9 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.Horizon.Sdk.Settings.Factory +{ + [StructLayout(LayoutKind.Sequential, Size = 0x248)] + struct Rsa2048DeviceKey + { + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Settings/Factory/SerialNumber.cs b/src/Ryujinx.Horizon/Sdk/Settings/Factory/SerialNumber.cs new file mode 100644 index 000000000..af664cdc5 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Settings/Factory/SerialNumber.cs @@ -0,0 +1,9 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.Horizon.Sdk.Settings.Factory +{ + [StructLayout(LayoutKind.Sequential, Size = 0x18, Pack = 0x1)] + struct SerialNumber + { + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Settings/Factory/SpeakerParameter.cs b/src/Ryujinx.Horizon/Sdk/Settings/Factory/SpeakerParameter.cs new file mode 100644 index 000000000..f147f66ff --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Settings/Factory/SpeakerParameter.cs @@ -0,0 +1,32 @@ +using Ryujinx.Common.Memory; +using System.Runtime.InteropServices; + +namespace Ryujinx.Horizon.Sdk.Settings.Factory +{ + [StructLayout(LayoutKind.Sequential, Size = 0x5A, Pack = 0x2)] + struct SpeakerParameter + { + public ushort Version; + public Array34 Reserved; + public ushort SpeakerHpf2A1; + public ushort SpeakerHpf2A2; + public ushort SpeakerHpf2H0; + public ushort SpeakerEqInputVolume; + public ushort SpeakerEqOutputVolume; + public ushort SpeakerEqCtrl1; + public ushort SpeakerEqCtrl2; + public ushort SpeakerDrcAgcCtrl2; + public ushort SpeakerDrcAgcCtrl3; + public ushort SpeakerDrcAgcCtrl1; + public ushort SpeakerAnalogVolume; + public ushort HeadphoneAnalogVolume; + public ushort SpeakerDigitalVolumeMin; + public ushort SpeakerDigitalVolumeMax; + public ushort HeadphoneDigitalVolumeMin; + public ushort HeadphoneDigitalVolumeMax; + public ushort MicFixedGain; + public ushort MicVariableVolumeMin; + public ushort MicVariableVolumeMax; + public Array16 Reserved2; + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Settings/Factory/SslCertificate.cs b/src/Ryujinx.Horizon/Sdk/Settings/Factory/SslCertificate.cs new file mode 100644 index 000000000..5d8252164 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Settings/Factory/SslCertificate.cs @@ -0,0 +1,9 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.Horizon.Sdk.Settings.Factory +{ + [StructLayout(LayoutKind.Sequential, Size = 0x804)] + struct SslCertificate + { + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Settings/Factory/SslKey.cs b/src/Ryujinx.Horizon/Sdk/Settings/Factory/SslKey.cs new file mode 100644 index 000000000..7d4b41369 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Settings/Factory/SslKey.cs @@ -0,0 +1,9 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.Horizon.Sdk.Settings.Factory +{ + [StructLayout(LayoutKind.Sequential, Size = 0x138)] + struct SslKey + { + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Settings/Language.cs b/src/Ryujinx.Horizon/Sdk/Settings/Language.cs new file mode 100644 index 000000000..4ffc66fec --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Settings/Language.cs @@ -0,0 +1,24 @@ +namespace Ryujinx.Horizon.Sdk.Settings +{ + enum Language : uint + { + Japanese, + AmericanEnglish, + French, + German, + Italian, + Spanish, + Chinese, + Korean, + Dutch, + Portuguese, + Russian, + Taiwanese, + BritishEnglish, + CanadianFrench, + LatinAmericanSpanish, + SimplifiedChinese, + TraditionalChinese, + BrazilianPortuguese, + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Settings/LanguageCode.cs b/src/Ryujinx.Horizon/Sdk/Settings/LanguageCode.cs new file mode 100644 index 000000000..dc9712692 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Settings/LanguageCode.cs @@ -0,0 +1,63 @@ +using Ryujinx.Common.Memory; +using System; +using System.Runtime.InteropServices; +using System.Text; + +namespace Ryujinx.Horizon.Sdk.Settings +{ + [StructLayout(LayoutKind.Sequential, Size = 0x8, Pack = 0x1)] + struct LanguageCode + { + private static readonly string[] _languageCodes = new string[] + { + "ja", + "en-US", + "fr", + "de", + "it", + "es", + "zh-CN", + "ko", + "nl", + "pt", + "ru", + "zh-TW", + "en-GB", + "fr-CA", + "es-419", + "zh-Hans", + "zh-Hant", + "pt-BR" + }; + + public Array8 Value; + + public bool IsValid() + { + int length = Value.AsSpan().IndexOf((byte)0); + if (length < 0) + { + return false; + } + + string str = Encoding.ASCII.GetString(Value.AsSpan()[..length]); + + return _languageCodes.AsSpan().Contains(str); + } + + public LanguageCode(Language language) + { + if ((uint)language >= _languageCodes.Length) + { + throw new ArgumentOutOfRangeException(nameof(language)); + } + + Value = new LanguageCode(_languageCodes[(int)language]).Value; + } + + public LanguageCode(string strCode) + { + Encoding.ASCII.GetBytes(strCode, Value.AsSpan()); + } + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Settings/SettingsItemKey.cs b/src/Ryujinx.Horizon/Sdk/Settings/SettingsItemKey.cs new file mode 100644 index 000000000..661184103 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Settings/SettingsItemKey.cs @@ -0,0 +1,9 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.Horizon.Sdk.Settings +{ + [StructLayout(LayoutKind.Sequential, Size = 0x48)] + struct SettingsItemKey + { + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Settings/SettingsName.cs b/src/Ryujinx.Horizon/Sdk/Settings/SettingsName.cs new file mode 100644 index 000000000..6864b8cd6 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Settings/SettingsName.cs @@ -0,0 +1,9 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.Horizon.Sdk.Settings +{ + [StructLayout(LayoutKind.Sequential, Size = 0x48)] + struct SettingsName + { + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Settings/System/AccountNotificationSettings.cs b/src/Ryujinx.Horizon/Sdk/Settings/System/AccountNotificationSettings.cs new file mode 100644 index 000000000..a2cbad6a6 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Settings/System/AccountNotificationSettings.cs @@ -0,0 +1,15 @@ +using Ryujinx.Horizon.Sdk.Account; + +namespace Ryujinx.Horizon.Sdk.Settings.System +{ + struct AccountNotificationSettings + { +#pragma warning disable CS0649 // Field is never assigned to + public Uid UserId; + public uint Flags; + public byte FriendPresenceOverlayPermission; + public byte FriendInvitationOverlayPermission; + public ushort Reserved; +#pragma warning restore CS0649 + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Settings/System/AccountOnlineStorageSettings.cs b/src/Ryujinx.Horizon/Sdk/Settings/System/AccountOnlineStorageSettings.cs new file mode 100644 index 000000000..3ed77e52b --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Settings/System/AccountOnlineStorageSettings.cs @@ -0,0 +1,6 @@ +namespace Ryujinx.Horizon.Sdk.Settings.System +{ + struct AccountOnlineStorageSettings + { + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Settings/System/AccountSettings.cs b/src/Ryujinx.Horizon/Sdk/Settings/System/AccountSettings.cs new file mode 100644 index 000000000..bd27ea0bf --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Settings/System/AccountSettings.cs @@ -0,0 +1,9 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.Horizon.Sdk.Settings.System +{ + [StructLayout(LayoutKind.Sequential, Size = 0x4, Pack = 0x4)] + struct AccountSettings + { + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Settings/System/AllowedSslHost.cs b/src/Ryujinx.Horizon/Sdk/Settings/System/AllowedSslHost.cs new file mode 100644 index 000000000..cb90daf18 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Settings/System/AllowedSslHost.cs @@ -0,0 +1,9 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.Horizon.Sdk.Settings.System +{ + [StructLayout(LayoutKind.Sequential, Size = 0x100)] + struct AllowedSslHost + { + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Settings/System/AnalogStickUserCalibration.cs b/src/Ryujinx.Horizon/Sdk/Settings/System/AnalogStickUserCalibration.cs new file mode 100644 index 000000000..36023da9c --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Settings/System/AnalogStickUserCalibration.cs @@ -0,0 +1,9 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.Horizon.Sdk.Settings.System +{ + [StructLayout(LayoutKind.Sequential, Size = 0x10, Pack = 0x4)] + struct AnalogStickUserCalibration + { + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Settings/System/AppletLaunchFlag.cs b/src/Ryujinx.Horizon/Sdk/Settings/System/AppletLaunchFlag.cs new file mode 100644 index 000000000..00d6f4d06 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Settings/System/AppletLaunchFlag.cs @@ -0,0 +1,9 @@ +using System; + +namespace Ryujinx.Horizon.Sdk.Settings.System +{ + [Flags] + enum AppletLaunchFlag : uint + { + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Settings/System/AudioVolume.cs b/src/Ryujinx.Horizon/Sdk/Settings/System/AudioVolume.cs new file mode 100644 index 000000000..d246bc2b9 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Settings/System/AudioVolume.cs @@ -0,0 +1,9 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.Horizon.Sdk.Settings.System +{ + [StructLayout(LayoutKind.Sequential, Size = 0x8, Pack = 0x4)] + struct AudioVolume + { + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Settings/System/BacklightSettings.cs b/src/Ryujinx.Horizon/Sdk/Settings/System/BacklightSettings.cs new file mode 100644 index 000000000..00de6869c --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Settings/System/BacklightSettings.cs @@ -0,0 +1,22 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.Horizon.Sdk.Settings.System +{ + [StructLayout(LayoutKind.Sequential, Size = 0x28, Pack = 0x4)] + struct BacklightSettings + { + // TODO: Determine field names. + public uint Unknown0x00; + public float Unknown0x04; + // 1st group + public float Unknown0x08; + public float Unknown0x0C; + public float Unknown0x10; + // 2nd group + public float Unknown0x14; + public float Unknown0x18; + public float Unknown0x1C; + public float Unknown0x20; + public float Unknown0x24; + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Settings/System/BacklightSettingsEx.cs b/src/Ryujinx.Horizon/Sdk/Settings/System/BacklightSettingsEx.cs new file mode 100644 index 000000000..347afdfe3 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Settings/System/BacklightSettingsEx.cs @@ -0,0 +1,9 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.Horizon.Sdk.Settings.System +{ + [StructLayout(LayoutKind.Sequential, Size = 0x2C, Pack = 0x4)] + struct BacklightSettingsEx + { + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Settings/System/BlePairingSettings.cs b/src/Ryujinx.Horizon/Sdk/Settings/System/BlePairingSettings.cs new file mode 100644 index 000000000..d9b01f9ff --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Settings/System/BlePairingSettings.cs @@ -0,0 +1,6 @@ +namespace Ryujinx.Horizon.Sdk.Settings.System +{ + struct BlePairingSettings + { + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Settings/System/BluetoothDevicesSettings.cs b/src/Ryujinx.Horizon/Sdk/Settings/System/BluetoothDevicesSettings.cs new file mode 100644 index 000000000..ec5c97c5a --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Settings/System/BluetoothDevicesSettings.cs @@ -0,0 +1,29 @@ +using Ryujinx.Common.Memory; + +namespace Ryujinx.Horizon.Sdk.Settings.System +{ + struct BluetoothDevicesSettings + { +#pragma warning disable CS0649 // Field is never assigned to + public Array6 BdAddr; + public Array32 DeviceName; + public Array3 ClassOfDevice; + public Array16 LinkKey; + public bool LinkKeyPresent; + public ushort Version; + public uint TrustedServices; + public ushort Vid; + public ushort Pid; + public byte SubClass; + public byte AttributeMask; + public ushort DescriptorLength; + public Array128 Descriptor; + public byte KeyType; + public byte DeviceType; + public ushort BrrSize; + public Array9 Brr; + public Array256 Reserved; + public Array43 Reserved2; +#pragma warning restore CS0649 + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Settings/System/ButtonConfigRegisteredSettings.cs b/src/Ryujinx.Horizon/Sdk/Settings/System/ButtonConfigRegisteredSettings.cs new file mode 100644 index 000000000..8bd4924e9 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Settings/System/ButtonConfigRegisteredSettings.cs @@ -0,0 +1,9 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.Horizon.Sdk.Settings.System +{ + [StructLayout(LayoutKind.Sequential, Size = 0x5C8)] + struct ButtonConfigRegisteredSettings + { + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Settings/System/ButtonConfigSettings.cs b/src/Ryujinx.Horizon/Sdk/Settings/System/ButtonConfigSettings.cs new file mode 100644 index 000000000..2f06e32e1 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Settings/System/ButtonConfigSettings.cs @@ -0,0 +1,9 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.Horizon.Sdk.Settings.System +{ + [StructLayout(LayoutKind.Sequential, Size = 0x5A8)] + struct ButtonConfigSettings + { + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Settings/System/ConsoleSixAxisSensorAccelerationBias.cs b/src/Ryujinx.Horizon/Sdk/Settings/System/ConsoleSixAxisSensorAccelerationBias.cs new file mode 100644 index 000000000..c70d4ff28 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Settings/System/ConsoleSixAxisSensorAccelerationBias.cs @@ -0,0 +1,9 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.Horizon.Sdk.Settings.System +{ + [StructLayout(LayoutKind.Sequential, Size = 0xC, Pack = 0x4)] + struct ConsoleSixAxisSensorAccelerationBias + { + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Settings/System/ConsoleSixAxisSensorAccelerationGain.cs b/src/Ryujinx.Horizon/Sdk/Settings/System/ConsoleSixAxisSensorAccelerationGain.cs new file mode 100644 index 000000000..0803beb87 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Settings/System/ConsoleSixAxisSensorAccelerationGain.cs @@ -0,0 +1,9 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.Horizon.Sdk.Settings.System +{ + [StructLayout(LayoutKind.Sequential, Size = 0x24, Pack = 0x4)] + struct ConsoleSixAxisSensorAccelerationGain + { + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Settings/System/ConsoleSixAxisSensorAngularAcceleration.cs b/src/Ryujinx.Horizon/Sdk/Settings/System/ConsoleSixAxisSensorAngularAcceleration.cs new file mode 100644 index 000000000..831e44bd5 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Settings/System/ConsoleSixAxisSensorAngularAcceleration.cs @@ -0,0 +1,9 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.Horizon.Sdk.Settings.System +{ + [StructLayout(LayoutKind.Sequential, Size = 0x24, Pack = 0x4)] + struct ConsoleSixAxisSensorAngularAcceleration + { + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Settings/System/ConsoleSixAxisSensorAngularVelocityBias.cs b/src/Ryujinx.Horizon/Sdk/Settings/System/ConsoleSixAxisSensorAngularVelocityBias.cs new file mode 100644 index 000000000..83d1faa8d --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Settings/System/ConsoleSixAxisSensorAngularVelocityBias.cs @@ -0,0 +1,9 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.Horizon.Sdk.Settings.System +{ + [StructLayout(LayoutKind.Sequential, Size = 0xC, Pack = 0x4)] + struct ConsoleSixAxisSensorAngularVelocityBias + { + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Settings/System/ConsoleSixAxisSensorAngularVelocityGain.cs b/src/Ryujinx.Horizon/Sdk/Settings/System/ConsoleSixAxisSensorAngularVelocityGain.cs new file mode 100644 index 000000000..68e0c614a --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Settings/System/ConsoleSixAxisSensorAngularVelocityGain.cs @@ -0,0 +1,9 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.Horizon.Sdk.Settings.System +{ + [StructLayout(LayoutKind.Sequential, Size = 0x24, Pack = 0x4)] + struct ConsoleSixAxisSensorAngularVelocityGain + { + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Settings/System/ConsoleSixAxisSensorAngularVelocityTimeBias.cs b/src/Ryujinx.Horizon/Sdk/Settings/System/ConsoleSixAxisSensorAngularVelocityTimeBias.cs new file mode 100644 index 000000000..47f3d951c --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Settings/System/ConsoleSixAxisSensorAngularVelocityTimeBias.cs @@ -0,0 +1,9 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.Horizon.Sdk.Settings.System +{ + [StructLayout(LayoutKind.Sequential, Size = 0xC, Pack = 0x4)] + struct ConsoleSixAxisSensorAngularVelocityTimeBias + { + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Settings/System/DataDeletionSettings.cs b/src/Ryujinx.Horizon/Sdk/Settings/System/DataDeletionSettings.cs new file mode 100644 index 000000000..a10a265d1 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Settings/System/DataDeletionSettings.cs @@ -0,0 +1,18 @@ +using System; +using System.Runtime.InteropServices; + +namespace Ryujinx.Horizon.Sdk.Settings.System +{ + [Flags] + enum DataDeletionFlag : uint + { + AutomaticDeletionFlag = 1 << 0, + } + + [StructLayout(LayoutKind.Sequential, Size = 0x8, Pack = 0x4)] + struct DataDeletionSettings + { + public DataDeletionFlag Flags; + public uint UseCount; + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Settings/System/DeviceNickName.cs b/src/Ryujinx.Horizon/Sdk/Settings/System/DeviceNickName.cs new file mode 100644 index 000000000..99c9f9817 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Settings/System/DeviceNickName.cs @@ -0,0 +1,25 @@ +using Ryujinx.Common.Memory; +using System.Runtime.InteropServices; +using System.Text; + +namespace Ryujinx.Horizon.Sdk.Settings.System +{ + [StructLayout(LayoutKind.Sequential, Size = 0x80)] + struct DeviceNickName + { + public Array128 Value; + + public DeviceNickName(string value) + { + int bytesWritten = Encoding.ASCII.GetBytes(value, Value.AsSpan()); + if (bytesWritten < 128) + { + Value[bytesWritten] = 0; + } + else + { + Value[127] = 0; + } + } + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Settings/System/Edid.cs b/src/Ryujinx.Horizon/Sdk/Settings/System/Edid.cs new file mode 100644 index 000000000..3ff566854 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Settings/System/Edid.cs @@ -0,0 +1,9 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.Horizon.Sdk.Settings.System +{ + [StructLayout(LayoutKind.Sequential, Size = 0x200)] + struct Edid + { + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Settings/System/EulaVersion.cs b/src/Ryujinx.Horizon/Sdk/Settings/System/EulaVersion.cs new file mode 100644 index 000000000..65905b1ba --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Settings/System/EulaVersion.cs @@ -0,0 +1,6 @@ +namespace Ryujinx.Horizon.Sdk.Settings.System +{ + struct EulaVersion + { + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Settings/System/FatalDirtyFlag.cs b/src/Ryujinx.Horizon/Sdk/Settings/System/FatalDirtyFlag.cs new file mode 100644 index 000000000..6be941151 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Settings/System/FatalDirtyFlag.cs @@ -0,0 +1,9 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.Horizon.Sdk.Settings.System +{ + [StructLayout(LayoutKind.Sequential, Size = 0x10, Pack = 0x8)] + struct FatalDirtyFlag + { + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Settings/System/FirmwareVersion.cs b/src/Ryujinx.Horizon/Sdk/Settings/System/FirmwareVersion.cs new file mode 100644 index 000000000..39825e010 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Settings/System/FirmwareVersion.cs @@ -0,0 +1,9 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.Horizon.Sdk.Settings.System +{ + [StructLayout(LayoutKind.Sequential, Size = 0x100)] + struct FirmwareVersion + { + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Settings/System/FirmwareVersionDigest.cs b/src/Ryujinx.Horizon/Sdk/Settings/System/FirmwareVersionDigest.cs new file mode 100644 index 000000000..0027d7ef1 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Settings/System/FirmwareVersionDigest.cs @@ -0,0 +1,9 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.Horizon.Sdk.Settings.System +{ + [StructLayout(LayoutKind.Sequential, Size = 0x40, Pack = 0x1)] + struct FirmwareVersionDigest + { + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Settings/System/HomeMenuScheme.cs b/src/Ryujinx.Horizon/Sdk/Settings/System/HomeMenuScheme.cs new file mode 100644 index 000000000..cc7b317be --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Settings/System/HomeMenuScheme.cs @@ -0,0 +1,14 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.Horizon.Sdk.Settings.System +{ + [StructLayout(LayoutKind.Sequential, Size = 0x14, Pack = 0x1)] + struct HomeMenuScheme + { + public uint Main; + public uint Back; + public uint Sub; + public uint Bezel; + public uint Extra; + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Settings/System/HostFsMountPoint.cs b/src/Ryujinx.Horizon/Sdk/Settings/System/HostFsMountPoint.cs new file mode 100644 index 000000000..1a66abac9 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Settings/System/HostFsMountPoint.cs @@ -0,0 +1,9 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.Horizon.Sdk.Settings.System +{ + [StructLayout(LayoutKind.Sequential, Size = 0x100)] + struct HostFsMountPoint + { + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Settings/System/InitialLaunchSettings.cs b/src/Ryujinx.Horizon/Sdk/Settings/System/InitialLaunchSettings.cs new file mode 100644 index 000000000..b3989de75 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Settings/System/InitialLaunchSettings.cs @@ -0,0 +1,14 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.Horizon.Sdk.Settings.System +{ + [StructLayout(LayoutKind.Sequential, Size = 0x20, Pack = 0x8)] + struct InitialLaunchSettings + { + public uint Flags; + public uint Reserved; + public ulong TimeStamp1; + public ulong TimeStamp2; + public ulong TimeStamp3; + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Settings/System/NetworkSettings.cs b/src/Ryujinx.Horizon/Sdk/Settings/System/NetworkSettings.cs new file mode 100644 index 000000000..a0101b626 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Settings/System/NetworkSettings.cs @@ -0,0 +1,6 @@ +namespace Ryujinx.Horizon.Sdk.Settings.System +{ + struct NetworkSettings + { + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Settings/System/NotificationSettings.cs b/src/Ryujinx.Horizon/Sdk/Settings/System/NotificationSettings.cs new file mode 100644 index 000000000..2ce56c4df --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Settings/System/NotificationSettings.cs @@ -0,0 +1,38 @@ +using System; +using System.Runtime.InteropServices; + +namespace Ryujinx.Horizon.Sdk.Settings.System +{ + [Flags] + enum NotificationFlag : uint + { + RingtoneFlag = 1 << 0, + DownloadCompletionFlag = 1 << 1, + EnablesNews = 1 << 8, + IncomingLampFlag = 1 << 9, + } + + enum NotificationVolume : uint + { + Mute, + Low, + High, + } + + struct NotificationTime + { +#pragma warning disable CS0649 // Field is never assigned to + public uint Hour; + public uint Minute; +#pragma warning restore CS0649 + } + + [StructLayout(LayoutKind.Sequential, Size = 0x18, Pack = 0x4)] + struct NotificationSettings + { + public NotificationFlag Flag; + public NotificationVolume Volume; + public NotificationTime HeadTime; + public NotificationTime TailTime; + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Settings/System/NxControllerLegacySettings.cs b/src/Ryujinx.Horizon/Sdk/Settings/System/NxControllerLegacySettings.cs new file mode 100644 index 000000000..845715df2 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Settings/System/NxControllerLegacySettings.cs @@ -0,0 +1,9 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.Horizon.Sdk.Settings.System +{ + [StructLayout(LayoutKind.Sequential, Size = 0x29)] + struct NxControllerLegacySettings + { + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Settings/System/NxControllerSettings.cs b/src/Ryujinx.Horizon/Sdk/Settings/System/NxControllerSettings.cs new file mode 100644 index 000000000..c8f81cecb --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Settings/System/NxControllerSettings.cs @@ -0,0 +1,9 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.Horizon.Sdk.Settings.System +{ + [StructLayout(LayoutKind.Sequential, Size = 0x42C)] + struct NxControllerSettings + { + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Settings/System/PtmFuelGaugeParameter.cs b/src/Ryujinx.Horizon/Sdk/Settings/System/PtmFuelGaugeParameter.cs new file mode 100644 index 000000000..b843bcd64 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Settings/System/PtmFuelGaugeParameter.cs @@ -0,0 +1,20 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.Horizon.Sdk.Settings.System +{ + [StructLayout(LayoutKind.Sequential, Size = 0x18, Pack = 0x4)] + struct PtmFuelGaugeParameter + { + public ushort Rcomp0; + public ushort TempCo; + public ushort FullCap; + public ushort FullCapNom; + public ushort IavgEmpty; + public ushort QrTable00; + public ushort QrTable10; + public ushort QrTable20; + public ushort QrTable30; + public ushort Reserved; + public uint Cycles; + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Settings/System/RebootlessSystemUpdateVersion.cs b/src/Ryujinx.Horizon/Sdk/Settings/System/RebootlessSystemUpdateVersion.cs new file mode 100644 index 000000000..b4e9b8b28 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Settings/System/RebootlessSystemUpdateVersion.cs @@ -0,0 +1,9 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.Horizon.Sdk.Settings.System +{ + [StructLayout(LayoutKind.Sequential, Size = 0x40, Pack = 0x4)] + struct RebootlessSystemUpdateVersion + { + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Settings/System/SerialNumber.cs b/src/Ryujinx.Horizon/Sdk/Settings/System/SerialNumber.cs new file mode 100644 index 000000000..22ddb85ce --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Settings/System/SerialNumber.cs @@ -0,0 +1,9 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.Horizon.Sdk.Settings.System +{ + [StructLayout(LayoutKind.Sequential, Size = 0x18, Pack = 0x1)] + struct SerialNumber + { + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Settings/System/ServiceDiscoveryControlSettings.cs b/src/Ryujinx.Horizon/Sdk/Settings/System/ServiceDiscoveryControlSettings.cs new file mode 100644 index 000000000..7c7b625a2 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Settings/System/ServiceDiscoveryControlSettings.cs @@ -0,0 +1,10 @@ +using System; + +namespace Ryujinx.Horizon.Sdk.Settings.System +{ + [Flags] + enum ServiceDiscoveryControlSettings : uint + { + IsChangeEnvironmentIdentifierDisabled = 1 << 0, + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Settings/System/SleepSettings.cs b/src/Ryujinx.Horizon/Sdk/Settings/System/SleepSettings.cs new file mode 100644 index 000000000..7493c677c --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Settings/System/SleepSettings.cs @@ -0,0 +1,40 @@ +using System; +using System.Runtime.InteropServices; + +namespace Ryujinx.Horizon.Sdk.Settings.System +{ + [Flags] + enum SleepFlag : uint + { + SleepsWhilePlayingMedia = 1 << 0, + WakesAtPowerStateChange = 1 << 1, + } + + enum HandheldSleepPlan : uint + { + At1Min, + At3Min, + At5Min, + At10Min, + At30Min, + Never, + } + + enum ConsoleSleepPlan : uint + { + At1Hour, + At2Hour, + At3Hour, + At6Hour, + At12Hour, + Never, + } + + [StructLayout(LayoutKind.Sequential, Size = 0xC, Pack = 0x4)] + struct SleepSettings + { + public SleepFlag Flags; + public HandheldSleepPlan HandheldSleepPlan; + public ConsoleSleepPlan ConsoleSleepPlan; + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Settings/System/TelemetryDirtyFlag.cs b/src/Ryujinx.Horizon/Sdk/Settings/System/TelemetryDirtyFlag.cs new file mode 100644 index 000000000..46ec2d767 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Settings/System/TelemetryDirtyFlag.cs @@ -0,0 +1,9 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.Horizon.Sdk.Settings.System +{ + [StructLayout(LayoutKind.Sequential, Size = 0x10, Pack = 0x8)] + struct TelemetryDirtyFlag + { + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Settings/System/ThemeId.cs b/src/Ryujinx.Horizon/Sdk/Settings/System/ThemeId.cs new file mode 100644 index 000000000..886ec8721 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Settings/System/ThemeId.cs @@ -0,0 +1,9 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.Horizon.Sdk.Settings.System +{ + [StructLayout(LayoutKind.Sequential, Size = 0x80, Pack = 0x8)] + struct ThemeId + { + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Settings/System/ThemeSettings.cs b/src/Ryujinx.Horizon/Sdk/Settings/System/ThemeSettings.cs new file mode 100644 index 000000000..ac36bcd80 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Settings/System/ThemeSettings.cs @@ -0,0 +1,9 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.Horizon.Sdk.Settings.System +{ + [StructLayout(LayoutKind.Sequential, Size = 0x8, Pack = 0x8)] + struct ThemeSettings + { + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Settings/System/TvSettings.cs b/src/Ryujinx.Horizon/Sdk/Settings/System/TvSettings.cs new file mode 100644 index 000000000..5ee0b85d9 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Settings/System/TvSettings.cs @@ -0,0 +1,59 @@ +using System; +using System.Runtime.InteropServices; + +namespace Ryujinx.Horizon.Sdk.Settings.System +{ + [Flags] + enum TvFlag : uint + { + Allows4k = 1 << 0, + Allows3d = 1 << 1, + AllowsCec = 1 << 2, + PreventsScreenBurnIn = 1 << 3, + } + + enum TvResolution : uint + { + Auto, + At1080p, + At720p, + At480p, + } + + enum HdmiContentType : uint + { + None, + Graphics, + Cinema, + Photo, + Game, + } + + enum RgbRange : uint + { + Auto, + Full, + Limited, + } + + enum CmuMode : uint + { + None, + ColorInvert, + HighContrast, + GrayScale, + } + + [StructLayout(LayoutKind.Sequential, Size = 0x20, Pack = 0x4)] + struct TvSettings + { + public TvFlag Flags; + public TvResolution TvResolution; + public HdmiContentType HdmiContentType; + public RgbRange RgbRange; + public CmuMode CmuMode; + public float TvUnderscan; + public float TvGamma; + public float ContrastRatio; + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Sf/Cmif/CmifRequest.cs b/src/Ryujinx.Horizon/Sdk/Sf/Cmif/CmifRequest.cs index d409be5b3..62c15baa6 100644 --- a/src/Ryujinx.Horizon/Sdk/Sf/Cmif/CmifRequest.cs +++ b/src/Ryujinx.Horizon/Sdk/Sf/Cmif/CmifRequest.cs @@ -10,5 +10,12 @@ namespace Ryujinx.Horizon.Sdk.Sf.Cmif public Span OutPointerSizes; public Span Objects; public int ServerPointerSize; + public int CurrentInPointerId; + public int SendBufferIndex; + public int RecvBufferIndex; + public int ExchBufferIndex; + public int SendStaticIndex; + public int RecvListIndex; + public int OutPointerSizeIndex; } } diff --git a/src/Ryujinx.Horizon/Sdk/Sf/CommandSerialization.cs b/src/Ryujinx.Horizon/Sdk/Sf/CommandSerialization.cs index 038135ac8..7f5284648 100644 --- a/src/Ryujinx.Horizon/Sdk/Sf/CommandSerialization.cs +++ b/src/Ryujinx.Horizon/Sdk/Sf/CommandSerialization.cs @@ -2,6 +2,7 @@ using Ryujinx.Horizon.Sdk.Sf.Cmif; using Ryujinx.Horizon.Sdk.Sf.Hipc; using Ryujinx.Memory; using System; +using System.Buffers; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -9,6 +10,11 @@ namespace Ryujinx.Horizon.Sdk.Sf { static class CommandSerialization { + public static ReadOnlySequence GetReadOnlySequence(PointerAndSize bufferRange) + { + return HorizonStatic.AddressSpace.GetReadOnlySequence(bufferRange.Address, checked((int)bufferRange.Size)); + } + public static ReadOnlySpan GetReadOnlySpan(PointerAndSize bufferRange) { return HorizonStatic.AddressSpace.GetSpan(bufferRange.Address, checked((int)bufferRange.Size)); diff --git a/src/Ryujinx.Horizon/Sdk/Sf/Hipc/HipcBufferDescriptor.cs b/src/Ryujinx.Horizon/Sdk/Sf/Hipc/HipcBufferDescriptor.cs index 03ef6d3fc..4e9628947 100644 --- a/src/Ryujinx.Horizon/Sdk/Sf/Hipc/HipcBufferDescriptor.cs +++ b/src/Ryujinx.Horizon/Sdk/Sf/Hipc/HipcBufferDescriptor.cs @@ -11,5 +11,12 @@ namespace Ryujinx.Horizon.Sdk.Sf.Hipc public ulong Address => _addressLow | (((ulong)_word2 << 4) & 0xf00000000UL) | (((ulong)_word2 << 34) & 0x7000000000UL); public ulong Size => _sizeLow | ((ulong)_word2 << 8) & 0xf00000000UL; public HipcBufferMode Mode => (HipcBufferMode)(_word2 & 3); + + public HipcBufferDescriptor(ulong address, ulong size, HipcBufferMode mode) + { + _sizeLow = (uint)size; + _addressLow = (uint)address; + _word2 = (uint)mode | ((uint)(address >> 34) & 0x1c) | ((uint)(size >> 32) << 24) | ((uint)(address >> 4) & 0xf0000000); + } } } diff --git a/src/Ryujinx.Horizon/Sdk/Sf/Hipc/HipcMessage.cs b/src/Ryujinx.Horizon/Sdk/Sf/Hipc/HipcMessage.cs index 887c82eb8..73321a891 100644 --- a/src/Ryujinx.Horizon/Sdk/Sf/Hipc/HipcMessage.cs +++ b/src/Ryujinx.Horizon/Sdk/Sf/Hipc/HipcMessage.cs @@ -181,6 +181,7 @@ namespace Ryujinx.Horizon.Sdk.Sf.Hipc } Span dataWords = Span.Empty; + Span dataWordsPadded = Span.Empty; if (meta.DataWordsCount != 0) { @@ -189,6 +190,7 @@ namespace Ryujinx.Horizon.Sdk.Sf.Hipc int padding = (dataOffsetAligned - dataOffset) / sizeof(uint); dataWords = MemoryMarshal.Cast(data)[padding..meta.DataWordsCount]; + dataWordsPadded = MemoryMarshal.Cast(data)[..meta.DataWordsCount]; data = data[(meta.DataWordsCount * sizeof(uint))..]; } @@ -209,6 +211,7 @@ namespace Ryujinx.Horizon.Sdk.Sf.Hipc ReceiveBuffers = receiveBuffers, ExchangeBuffers = exchangeBuffers, DataWords = dataWords, + DataWordsPadded = dataWordsPadded, ReceiveList = receiveList, CopyHandles = copyHandles, MoveHandles = moveHandles, diff --git a/src/Ryujinx.Horizon/Sdk/Sf/Hipc/HipcMessageData.cs b/src/Ryujinx.Horizon/Sdk/Sf/Hipc/HipcMessageData.cs index 548f12e8b..0d45d756f 100644 --- a/src/Ryujinx.Horizon/Sdk/Sf/Hipc/HipcMessageData.cs +++ b/src/Ryujinx.Horizon/Sdk/Sf/Hipc/HipcMessageData.cs @@ -9,6 +9,7 @@ namespace Ryujinx.Horizon.Sdk.Sf.Hipc public Span ReceiveBuffers; public Span ExchangeBuffers; public Span DataWords; + public Span DataWordsPadded; public Span ReceiveList; public Span CopyHandles; public Span MoveHandles; diff --git a/src/Ryujinx.Horizon/Sdk/Sf/Hipc/ServerManagerBase.cs b/src/Ryujinx.Horizon/Sdk/Sf/Hipc/ServerManagerBase.cs index 9886e1cbf..570e3c802 100644 --- a/src/Ryujinx.Horizon/Sdk/Sf/Hipc/ServerManagerBase.cs +++ b/src/Ryujinx.Horizon/Sdk/Sf/Hipc/ServerManagerBase.cs @@ -3,6 +3,7 @@ using Ryujinx.Horizon.Sdk.OsTypes; using Ryujinx.Horizon.Sdk.Sf.Cmif; using Ryujinx.Horizon.Sdk.Sm; using System; +using System.Linq; namespace Ryujinx.Horizon.Sdk.Sf.Hipc { @@ -116,6 +117,18 @@ namespace Ryujinx.Horizon.Sdk.Sf.Hipc while (WaitAndProcessRequestsImpl()) { } + + // Unlink pending sessions, dispose expects them to be already unlinked. + + ServerSession[] serverSessions = Enumerable.OfType(_multiWait.MultiWaits).ToArray(); + + foreach (ServerSession serverSession in serverSessions) + { + if (serverSession.IsLinked) + { + serverSession.UnlinkFromMultiWaitHolder(); + } + } } public void WaitAndProcessRequests() diff --git a/src/Ryujinx.Horizon/Sdk/Sf/HipcCommandProcessor.cs b/src/Ryujinx.Horizon/Sdk/Sf/HipcCommandProcessor.cs index bb9b37e28..dc34f791a 100644 --- a/src/Ryujinx.Horizon/Sdk/Sf/HipcCommandProcessor.cs +++ b/src/Ryujinx.Horizon/Sdk/Sf/HipcCommandProcessor.cs @@ -127,7 +127,7 @@ namespace Ryujinx.Horizon.Sdk.Sf return _bufferRanges[argIndex]; } - public Result ProcessBuffers(ref ServiceDispatchContext context, bool[] isBufferMapAlias, ServerMessageRuntimeMetadata runtimeMetadata) + public Result ProcessBuffers(ref ServiceDispatchContext context, scoped Span isBufferMapAlias, ServerMessageRuntimeMetadata runtimeMetadata) { bool mapAliasBuffersValid = true; @@ -206,7 +206,7 @@ namespace Ryujinx.Horizon.Sdk.Sf } else { - var data = MemoryMarshal.Cast(context.Request.Data.DataWords); + var data = MemoryMarshal.Cast(context.Request.Data.DataWordsPadded); var recvPointerSizes = MemoryMarshal.Cast(data[runtimeMetadata.UnfixedOutPointerSizeOffset..]); size = recvPointerSizes[unfixedRecvPointerIndex++]; @@ -246,7 +246,7 @@ namespace Ryujinx.Horizon.Sdk.Sf return mode == HipcBufferMode.Normal; } - public void SetOutBuffers(HipcMessageData response, bool[] isBufferMapAlias) + public void SetOutBuffers(HipcMessageData response, ReadOnlySpan isBufferMapAlias) { int recvPointerIndex = 0; diff --git a/src/Ryujinx.Horizon/Sdk/Ts/DeviceCode.cs b/src/Ryujinx.Horizon/Sdk/Ts/DeviceCode.cs new file mode 100644 index 000000000..4fce4238e --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Ts/DeviceCode.cs @@ -0,0 +1,8 @@ +namespace Ryujinx.Horizon.Sdk.Ts +{ + enum DeviceCode : uint + { + Internal = 0x41000001, + External = 0x41000002, + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Ts/IMeasurementServer.cs b/src/Ryujinx.Horizon/Sdk/Ts/IMeasurementServer.cs new file mode 100644 index 000000000..ba9c2a748 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Ts/IMeasurementServer.cs @@ -0,0 +1,14 @@ +using Ryujinx.Horizon.Common; +using Ryujinx.Horizon.Sdk.Sf; + +namespace Ryujinx.Horizon.Sdk.Ts +{ + interface IMeasurementServer : IServiceObject + { + Result GetTemperatureRange(out int minimumTemperature, out int maximumTemperature, Location location); + Result GetTemperature(out int temperature, Location location); + Result SetMeasurementMode(Location location, byte measurementMode); + Result GetTemperatureMilliC(out int temperatureMilliC, Location location); + Result OpenSession(out ISession session, DeviceCode deviceCode); + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Ts/ISession.cs b/src/Ryujinx.Horizon/Sdk/Ts/ISession.cs new file mode 100644 index 000000000..23c0d94f6 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Ts/ISession.cs @@ -0,0 +1,12 @@ +using Ryujinx.Horizon.Common; +using Ryujinx.Horizon.Sdk.Sf; + +namespace Ryujinx.Horizon.Sdk.Ts +{ + interface ISession : IServiceObject + { + Result GetTemperatureRange(out int minimumTemperature, out int maximumTemperature); + Result GetTemperature(out int temperature); + Result SetMeasurementMode(byte measurementMode); + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ptm/Ts/Types/Location.cs b/src/Ryujinx.Horizon/Sdk/Ts/Location.cs similarity index 61% rename from src/Ryujinx.HLE/HOS/Services/Ptm/Ts/Types/Location.cs rename to src/Ryujinx.Horizon/Sdk/Ts/Location.cs index 409188a97..177b0ee88 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ptm/Ts/Types/Location.cs +++ b/src/Ryujinx.Horizon/Sdk/Ts/Location.cs @@ -1,4 +1,4 @@ -namespace Ryujinx.HLE.HOS.Services.Ptm.Ts.Types +namespace Ryujinx.Horizon.Sdk.Ts { enum Location : byte { diff --git a/src/Ryujinx.Horizon/ServiceTable.cs b/src/Ryujinx.Horizon/ServiceTable.cs index c79328a96..28c43a716 100644 --- a/src/Ryujinx.Horizon/ServiceTable.cs +++ b/src/Ryujinx.Horizon/ServiceTable.cs @@ -1,4 +1,7 @@ +using Ryujinx.Horizon.Arp; +using Ryujinx.Horizon.Audio; using Ryujinx.Horizon.Bcat; +using Ryujinx.Horizon.Friends; using Ryujinx.Horizon.Hshl; using Ryujinx.Horizon.Ins; using Ryujinx.Horizon.Lbl; @@ -8,6 +11,8 @@ using Ryujinx.Horizon.Ngc; using Ryujinx.Horizon.Ovln; using Ryujinx.Horizon.Prepo; using Ryujinx.Horizon.Psc; +using Ryujinx.Horizon.Ptm; +using Ryujinx.Horizon.Sdk.Arp; using Ryujinx.Horizon.Srepo; using Ryujinx.Horizon.Usb; using Ryujinx.Horizon.Wlan; @@ -23,6 +28,9 @@ namespace Ryujinx.Horizon private readonly ManualResetEvent _servicesReadyEvent = new(false); + public IReader ArpReader { get; internal set; } + public IWriter ArpWriter { get; internal set; } + public IEnumerable GetServices(HorizonOptions options) { List entries = new(); @@ -32,8 +40,12 @@ namespace Ryujinx.Horizon entries.Add(new ServiceEntry(T.Main, this, options)); } + RegisterService(); + RegisterService(); RegisterService(); + RegisterService(); RegisterService(); + RegisterService(); // TODO: Merge with audio once we can start multiple threads. RegisterService(); RegisterService(); RegisterService(); @@ -43,6 +55,7 @@ namespace Ryujinx.Horizon RegisterService(); RegisterService(); RegisterService(); + RegisterService(); RegisterService(); RegisterService(); diff --git a/src/Ryujinx.Horizon/Srepo/SrepoIpcServer.cs b/src/Ryujinx.Horizon/Srepo/SrepoIpcServer.cs index 2060782cc..44d008224 100644 --- a/src/Ryujinx.Horizon/Srepo/SrepoIpcServer.cs +++ b/src/Ryujinx.Horizon/Srepo/SrepoIpcServer.cs @@ -41,6 +41,7 @@ namespace Ryujinx.Horizon.Srepo public void Shutdown() { _serverManager.Dispose(); + _sm.Dispose(); } } } diff --git a/src/Ryujinx.Horizon/Usb/UsbIpcServer.cs b/src/Ryujinx.Horizon/Usb/UsbIpcServer.cs index 38eeed496..a04b81f97 100644 --- a/src/Ryujinx.Horizon/Usb/UsbIpcServer.cs +++ b/src/Ryujinx.Horizon/Usb/UsbIpcServer.cs @@ -66,6 +66,7 @@ namespace Ryujinx.Horizon.Usb public void Shutdown() { _serverManager.Dispose(); + _sm.Dispose(); } } } diff --git a/src/Ryujinx.Horizon/Wlan/WlanIpcServer.cs b/src/Ryujinx.Horizon/Wlan/WlanIpcServer.cs index c7b336231..776b9a7cb 100644 --- a/src/Ryujinx.Horizon/Wlan/WlanIpcServer.cs +++ b/src/Ryujinx.Horizon/Wlan/WlanIpcServer.cs @@ -54,6 +54,7 @@ namespace Ryujinx.Horizon.Wlan public void Shutdown() { _serverManager.Dispose(); + _sm.Dispose(); } } } diff --git a/src/Ryujinx.Input.SDL2/SDL2Gamepad.cs b/src/Ryujinx.Input.SDL2/SDL2Gamepad.cs index 187ca48dd..af6f4c625 100644 --- a/src/Ryujinx.Input.SDL2/SDL2Gamepad.cs +++ b/src/Ryujinx.Input.SDL2/SDL2Gamepad.cs @@ -12,7 +12,10 @@ namespace Ryujinx.Input.SDL2 { private bool HasConfiguration => _configuration != null; - private record struct ButtonMappingEntry(GamepadButtonInputId To, GamepadButtonInputId From); + private readonly record struct ButtonMappingEntry(GamepadButtonInputId To, GamepadButtonInputId From) + { + public bool IsValid => To is not GamepadButtonInputId.Unbound && From is not GamepadButtonInputId.Unbound; + } private StandardControllerInputConfig _configuration; @@ -68,11 +71,11 @@ namespace Ryujinx.Input.SDL2 public GamepadFeaturesFlag Features { get; } - private IntPtr _gamepadHandle; + private nint _gamepadHandle; private float _triggerThreshold; - public SDL2Gamepad(IntPtr gamepadHandle, string driverId) + public SDL2Gamepad(nint gamepadHandle, string driverId) { _gamepadHandle = gamepadHandle; _buttonsUserMapping = new List(20); @@ -124,11 +127,11 @@ namespace Ryujinx.Input.SDL2 protected virtual void Dispose(bool disposing) { - if (disposing && _gamepadHandle != IntPtr.Zero) + if (disposing && _gamepadHandle != nint.Zero) { SDL_GameControllerClose(_gamepadHandle); - _gamepadHandle = IntPtr.Zero; + _gamepadHandle = nint.Zero; } } @@ -144,86 +147,65 @@ namespace Ryujinx.Input.SDL2 public void Rumble(float lowFrequency, float highFrequency, uint durationMs) { - if (Features.HasFlag(GamepadFeaturesFlag.Rumble)) - { - ushort lowFrequencyRaw = (ushort)(lowFrequency * ushort.MaxValue); - ushort highFrequencyRaw = (ushort)(highFrequency * ushort.MaxValue); + if (!Features.HasFlag(GamepadFeaturesFlag.Rumble)) + return; - if (durationMs == uint.MaxValue) - { - if (SDL_GameControllerRumble(_gamepadHandle, lowFrequencyRaw, highFrequencyRaw, SDL_HAPTIC_INFINITY) != 0) - { - Logger.Error?.Print(LogClass.Hid, "Rumble is not supported on this game controller."); - } - } - else if (durationMs > SDL_HAPTIC_INFINITY) - { - Logger.Error?.Print(LogClass.Hid, $"Unsupported rumble duration {durationMs}"); - } - else - { - if (SDL_GameControllerRumble(_gamepadHandle, lowFrequencyRaw, highFrequencyRaw, durationMs) != 0) - { - Logger.Error?.Print(LogClass.Hid, "Rumble is not supported on this game controller."); - } - } + ushort lowFrequencyRaw = (ushort)(lowFrequency * ushort.MaxValue); + ushort highFrequencyRaw = (ushort)(highFrequency * ushort.MaxValue); + + if (durationMs == uint.MaxValue) + { + if (SDL_GameControllerRumble(_gamepadHandle, lowFrequencyRaw, highFrequencyRaw, SDL_HAPTIC_INFINITY) != 0) + Logger.Error?.Print(LogClass.Hid, "Rumble is not supported on this game controller."); + } + else if (durationMs > SDL_HAPTIC_INFINITY) + { + Logger.Error?.Print(LogClass.Hid, $"Unsupported rumble duration {durationMs}"); + } + else + { + if (SDL_GameControllerRumble(_gamepadHandle, lowFrequencyRaw, highFrequencyRaw, durationMs) != 0) + Logger.Error?.Print(LogClass.Hid, "Rumble is not supported on this game controller."); } } public Vector3 GetMotionData(MotionInputId inputId) { - SDL_SensorType sensorType = SDL_SensorType.SDL_SENSOR_INVALID; - - if (inputId == MotionInputId.Accelerometer) + SDL_SensorType sensorType = inputId switch { - sensorType = SDL_SensorType.SDL_SENSOR_ACCEL; - } - else if (inputId == MotionInputId.Gyroscope) - { - sensorType = SDL_SensorType.SDL_SENSOR_GYRO; - } + MotionInputId.Accelerometer => SDL_SensorType.SDL_SENSOR_ACCEL, + MotionInputId.Gyroscope => SDL_SensorType.SDL_SENSOR_GYRO, + _ => SDL_SensorType.SDL_SENSOR_INVALID + }; - if (Features.HasFlag(GamepadFeaturesFlag.Motion) && sensorType != SDL_SensorType.SDL_SENSOR_INVALID) - { - const int ElementCount = 3; + if (!Features.HasFlag(GamepadFeaturesFlag.Motion) || sensorType is SDL_SensorType.SDL_SENSOR_INVALID) + return Vector3.Zero; - unsafe + const int ElementCount = 3; + + unsafe + { + float* values = stackalloc float[ElementCount]; + + int result = SDL_GameControllerGetSensorData(_gamepadHandle, sensorType, (nint)values, ElementCount); + + if (result != 0) + return Vector3.Zero; + + Vector3 value = new(values[0], values[1], values[2]); + + return inputId switch { - float* values = stackalloc float[ElementCount]; - - int result = SDL_GameControllerGetSensorData(_gamepadHandle, sensorType, (IntPtr)values, ElementCount); - - if (result == 0) - { - Vector3 value = new(values[0], values[1], values[2]); - - if (inputId == MotionInputId.Gyroscope) - { - return RadToDegree(value); - } - - if (inputId == MotionInputId.Accelerometer) - { - return GsToMs2(value); - } - - return value; - } - } + MotionInputId.Gyroscope => RadToDegree(value), + MotionInputId.Accelerometer => GsToMs2(value), + _ => value + }; } - - return Vector3.Zero; } - private static Vector3 RadToDegree(Vector3 rad) - { - return rad * (180 / MathF.PI); - } + private static Vector3 RadToDegree(Vector3 rad) => rad * (180 / MathF.PI); - private static Vector3 GsToMs2(Vector3 gs) - { - return gs / SDL_STANDARD_GRAVITY; - } + private static Vector3 GsToMs2(Vector3 gs) => gs / SDL_STANDARD_GRAVITY; public void SetConfiguration(InputConfig configuration) { @@ -278,16 +260,14 @@ namespace Ryujinx.Input.SDL2 lock (_userMappingLock) { if (_buttonsUserMapping.Count == 0) - { return rawState; - } + + // ReSharper disable once ForeachCanBePartlyConvertedToQueryUsingAnotherGetEnumerator foreach (ButtonMappingEntry entry in _buttonsUserMapping) { - if (entry.From == GamepadButtonInputId.Unbound || entry.To == GamepadButtonInputId.Unbound) - { + if (!entry.IsValid) continue; - } // Do not touch state of button already pressed if (!result.IsPressed(entry.To)) @@ -313,70 +293,79 @@ namespace Ryujinx.Input.SDL2 return value * ConvertRate; } + private JoyconConfigControllerStick GetLogicalJoyStickConfig(StickInputId inputId) + { + switch (inputId) + { + case StickInputId.Left: + if (_configuration.RightJoyconStick.Joystick == Common.Configuration.Hid.Controller.StickInputId.Left) + return _configuration.RightJoyconStick; + else + return _configuration.LeftJoyconStick; + case StickInputId.Right: + if (_configuration.LeftJoyconStick.Joystick == Common.Configuration.Hid.Controller.StickInputId.Right) + return _configuration.LeftJoyconStick; + else + return _configuration.RightJoyconStick; + } + return null; + } + public (float, float) GetStick(StickInputId inputId) { if (inputId == StickInputId.Unbound) - { return (0.0f, 0.0f); - } - short stickX; - short stickY; - - if (inputId == StickInputId.Left) - { - stickX = SDL_GameControllerGetAxis(_gamepadHandle, SDL_GameControllerAxis.SDL_CONTROLLER_AXIS_LEFTX); - stickY = SDL_GameControllerGetAxis(_gamepadHandle, SDL_GameControllerAxis.SDL_CONTROLLER_AXIS_LEFTY); - } - else if (inputId == StickInputId.Right) - { - stickX = SDL_GameControllerGetAxis(_gamepadHandle, SDL_GameControllerAxis.SDL_CONTROLLER_AXIS_RIGHTX); - stickY = SDL_GameControllerGetAxis(_gamepadHandle, SDL_GameControllerAxis.SDL_CONTROLLER_AXIS_RIGHTY); - } - else - { - throw new NotSupportedException($"Unsupported stick {inputId}"); - } + (short stickX, short stickY) = GetStickXY(inputId); float resultX = ConvertRawStickValue(stickX); float resultY = -ConvertRawStickValue(stickY); if (HasConfiguration) { - if ((inputId == StickInputId.Left && _configuration.LeftJoyconStick.InvertStickX) || - (inputId == StickInputId.Right && _configuration.RightJoyconStick.InvertStickX)) - { - resultX = -resultX; - } + var joyconStickConfig = GetLogicalJoyStickConfig(inputId); - if ((inputId == StickInputId.Left && _configuration.LeftJoyconStick.InvertStickY) || - (inputId == StickInputId.Right && _configuration.RightJoyconStick.InvertStickY)) + if (joyconStickConfig != null) { - resultY = -resultY; - } + if (joyconStickConfig.InvertStickX) + resultX = -resultX; - if ((inputId == StickInputId.Left && _configuration.LeftJoyconStick.Rotate90CW) || - (inputId == StickInputId.Right && _configuration.RightJoyconStick.Rotate90CW)) - { - float temp = resultX; - resultX = resultY; - resultY = -temp; + if (joyconStickConfig.InvertStickY) + resultY = -resultY; + + if (joyconStickConfig.Rotate90CW) + { + float temp = resultX; + resultX = resultY; + resultY = -temp; + } } } return (resultX, resultY); } + // ReSharper disable once InconsistentNaming + private (short, short) GetStickXY(StickInputId inputId) => + inputId switch + { + StickInputId.Left => ( + SDL_GameControllerGetAxis(_gamepadHandle, SDL_GameControllerAxis.SDL_CONTROLLER_AXIS_LEFTX), + SDL_GameControllerGetAxis(_gamepadHandle, SDL_GameControllerAxis.SDL_CONTROLLER_AXIS_LEFTY)), + StickInputId.Right => ( + SDL_GameControllerGetAxis(_gamepadHandle, SDL_GameControllerAxis.SDL_CONTROLLER_AXIS_RIGHTX), + SDL_GameControllerGetAxis(_gamepadHandle, SDL_GameControllerAxis.SDL_CONTROLLER_AXIS_RIGHTY)), + _ => throw new NotSupportedException($"Unsupported stick {inputId}") + }; + public bool IsPressed(GamepadButtonInputId inputId) { - if (inputId == GamepadButtonInputId.LeftTrigger) + switch (inputId) { - return ConvertRawStickValue(SDL_GameControllerGetAxis(_gamepadHandle, SDL_GameControllerAxis.SDL_CONTROLLER_AXIS_TRIGGERLEFT)) > _triggerThreshold; - } - - if (inputId == GamepadButtonInputId.RightTrigger) - { - return ConvertRawStickValue(SDL_GameControllerGetAxis(_gamepadHandle, SDL_GameControllerAxis.SDL_CONTROLLER_AXIS_TRIGGERRIGHT)) > _triggerThreshold; + case GamepadButtonInputId.LeftTrigger: + return ConvertRawStickValue(SDL_GameControllerGetAxis(_gamepadHandle, SDL_GameControllerAxis.SDL_CONTROLLER_AXIS_TRIGGERLEFT)) > _triggerThreshold; + case GamepadButtonInputId.RightTrigger: + return ConvertRawStickValue(SDL_GameControllerGetAxis(_gamepadHandle, SDL_GameControllerAxis.SDL_CONTROLLER_AXIS_TRIGGERRIGHT)) > _triggerThreshold; } if (_buttonsDriverMapping[(int)inputId] == SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_INVALID) diff --git a/src/Ryujinx.Input.SDL2/SDL2GamepadDriver.cs b/src/Ryujinx.Input.SDL2/SDL2GamepadDriver.cs index 0e3a13011..fd34fe219 100644 --- a/src/Ryujinx.Input.SDL2/SDL2GamepadDriver.cs +++ b/src/Ryujinx.Input.SDL2/SDL2GamepadDriver.cs @@ -9,8 +9,18 @@ namespace Ryujinx.Input.SDL2 { private readonly Dictionary _gamepadsInstanceIdsMapping; private readonly List _gamepadsIds; + private readonly object _lock = new(); - public ReadOnlySpan GamepadsIds => _gamepadsIds.ToArray(); + public ReadOnlySpan GamepadsIds + { + get + { + lock (_lock) + { + return _gamepadsIds.ToArray(); + } + } + } public string DriverName => "SDL2"; @@ -35,45 +45,65 @@ namespace Ryujinx.Input.SDL2 } } - private static string GenerateGamepadId(int joystickIndex) + private string GenerateGamepadId(int joystickIndex) { Guid guid = SDL_JoystickGetDeviceGUID(joystickIndex); + // Add a unique identifier to the start of the GUID in case of duplicates. + if (guid == Guid.Empty) { return null; } - return joystickIndex + "-" + guid; - } + string id; - private static int GetJoystickIndexByGamepadId(string id) - { - string[] data = id.Split("-"); - - if (data.Length != 6 || !int.TryParse(data[0], out int joystickIndex)) + lock (_lock) { - return -1; + int guidIndex = 0; + id = guidIndex + "-" + guid; + + while (_gamepadsIds.Contains(id)) + { + id = (++guidIndex) + "-" + guid; + } } - return joystickIndex; + return id; + } + + private int GetJoystickIndexByGamepadId(string id) + { + lock (_lock) + { + return _gamepadsIds.IndexOf(id); + } } private void HandleJoyStickDisconnected(int joystickInstanceId) { - if (_gamepadsInstanceIdsMapping.TryGetValue(joystickInstanceId, out string id)) - { - _gamepadsInstanceIdsMapping.Remove(joystickInstanceId); - _gamepadsIds.Remove(id); + if (!_gamepadsInstanceIdsMapping.Remove(joystickInstanceId, out string id)) + return; - OnGamepadDisconnected?.Invoke(id); + lock (_lock) + { + _gamepadsIds.Remove(id); } + + OnGamepadDisconnected?.Invoke(id); } private void HandleJoyStickConnected(int joystickDeviceId, int joystickInstanceId) { if (SDL_IsGameController(joystickDeviceId) == SDL_bool.SDL_TRUE) { + if (_gamepadsInstanceIdsMapping.ContainsKey(joystickInstanceId)) + { + // Sometimes a JoyStick connected event fires after the app starts even though it was connected before + // so it is rejected to avoid doubling the entries. + return; + } + string id = GenerateGamepadId(joystickDeviceId); if (id == null) @@ -81,16 +111,15 @@ namespace Ryujinx.Input.SDL2 return; } - // Sometimes a JoyStick connected event fires after the app starts even though it was connected before - // so it is rejected to avoid doubling the entries. - if (_gamepadsIds.Contains(id)) - { - return; - } - if (_gamepadsInstanceIdsMapping.TryAdd(joystickInstanceId, id)) { - _gamepadsIds.Add(id); + lock (_lock) + { + if (joystickDeviceId <= _gamepadsIds.FindLastIndex(_ => true)) + _gamepadsIds.Insert(joystickDeviceId, id); + else + _gamepadsIds.Add(id); + } OnGamepadConnected?.Invoke(id); } @@ -110,7 +139,10 @@ namespace Ryujinx.Input.SDL2 OnGamepadDisconnected?.Invoke(id); } - _gamepadsIds.Clear(); + lock (_lock) + { + _gamepadsIds.Clear(); + } SDL2Driver.Instance.Dispose(); } @@ -131,14 +163,9 @@ namespace Ryujinx.Input.SDL2 return null; } - if (id != GenerateGamepadId(joystickIndex)) - { - return null; - } + nint gamepadHandle = SDL_GameControllerOpen(joystickIndex); - IntPtr gamepadHandle = SDL_GameControllerOpen(joystickIndex); - - if (gamepadHandle == IntPtr.Zero) + if (gamepadHandle == nint.Zero) { return null; } diff --git a/src/Ryujinx.Input.SDL2/SDL2Keyboard.cs b/src/Ryujinx.Input.SDL2/SDL2Keyboard.cs index bc0a7e660..a0dd8ab95 100644 --- a/src/Ryujinx.Input.SDL2/SDL2Keyboard.cs +++ b/src/Ryujinx.Input.SDL2/SDL2Keyboard.cs @@ -12,16 +12,9 @@ namespace Ryujinx.Input.SDL2 { class SDL2Keyboard : IKeyboard { - private class ButtonMappingEntry + private readonly record struct ButtonMappingEntry(GamepadButtonInputId To, Key From) { - public readonly GamepadButtonInputId To; - public readonly Key From; - - public ButtonMappingEntry(GamepadButtonInputId to, Key from) - { - To = to; - From = from; - } + public bool IsValid => To is not GamepadButtonInputId.Unbound && From is not Key.Unbound; } private readonly object _userMappingLock = new(); @@ -232,7 +225,7 @@ namespace Ryujinx.Input.SDL2 unsafe { - IntPtr statePtr = SDL_GetKeyboardState(out int numKeys); + nint statePtr = SDL_GetKeyboardState(out int numKeys); rawKeyboardState = new ReadOnlySpan((byte*)statePtr, numKeys); } diff --git a/src/Ryujinx.Headless.SDL2/SDL2Mouse.cs b/src/Ryujinx.Input.SDL2/SDL2Mouse.cs similarity index 95% rename from src/Ryujinx.Headless.SDL2/SDL2Mouse.cs rename to src/Ryujinx.Input.SDL2/SDL2Mouse.cs index de64b4f8f..37b356b76 100644 --- a/src/Ryujinx.Headless.SDL2/SDL2Mouse.cs +++ b/src/Ryujinx.Input.SDL2/SDL2Mouse.cs @@ -1,12 +1,11 @@ using Ryujinx.Common.Configuration.Hid; -using Ryujinx.Input; using System; using System.Drawing; using System.Numerics; -namespace Ryujinx.Headless.SDL2 +namespace Ryujinx.Input.SDL2 { - class SDL2Mouse : IMouse + public class SDL2Mouse : IMouse { private SDL2MouseDriver _driver; @@ -84,6 +83,7 @@ namespace Ryujinx.Headless.SDL2 public void Dispose() { + GC.SuppressFinalize(this); _driver = null; } } diff --git a/src/Ryujinx.Headless.SDL2/SDL2MouseDriver.cs b/src/Ryujinx.Input.SDL2/SDL2MouseDriver.cs similarity index 96% rename from src/Ryujinx.Headless.SDL2/SDL2MouseDriver.cs rename to src/Ryujinx.Input.SDL2/SDL2MouseDriver.cs index 8983091f5..768ea8c62 100644 --- a/src/Ryujinx.Headless.SDL2/SDL2MouseDriver.cs +++ b/src/Ryujinx.Input.SDL2/SDL2MouseDriver.cs @@ -1,6 +1,5 @@ using Ryujinx.Common.Configuration; using Ryujinx.Common.Logging; -using Ryujinx.Input; using System; using System.Diagnostics; using System.Drawing; @@ -8,9 +7,9 @@ using System.Numerics; using System.Runtime.CompilerServices; using static SDL2.SDL; -namespace Ryujinx.Headless.SDL2 +namespace Ryujinx.Input.SDL2 { - class SDL2MouseDriver : IGamepadDriver + public class SDL2MouseDriver : IGamepadDriver { private const int CursorHideIdleTime = 5; // seconds @@ -44,7 +43,7 @@ namespace Ryujinx.Headless.SDL2 [MethodImpl(MethodImplOptions.AggressiveInlining)] private static MouseButton DriverButtonToMouseButton(uint rawButton) { - Debug.Assert(rawButton > 0 && rawButton <= (int)MouseButton.Count); + Debug.Assert(rawButton is > 0 and <= (int)MouseButton.Count); return (MouseButton)(rawButton - 1); } @@ -172,6 +171,7 @@ namespace Ryujinx.Headless.SDL2 return; } + GC.SuppressFinalize(this); _isDisposed = true; } } diff --git a/src/Ryujinx.Input/Assigner/GamepadButtonAssigner.cs b/src/Ryujinx.Input/Assigner/GamepadButtonAssigner.cs index 388ebcc07..80fed2b82 100644 --- a/src/Ryujinx.Input/Assigner/GamepadButtonAssigner.cs +++ b/src/Ryujinx.Input/Assigner/GamepadButtonAssigner.cs @@ -49,9 +49,9 @@ namespace Ryujinx.Input.Assigner CollectButtonStats(); } - public bool HasAnyButtonPressed() + public bool IsAnyButtonPressed() { - return _detector.HasAnyButtonPressed(); + return _detector.IsAnyButtonPressed(); } public bool ShouldCancel() @@ -59,16 +59,11 @@ namespace Ryujinx.Input.Assigner return _gamepad == null || !_gamepad.IsConnected; } - public string GetPressedButton() + public Button? GetPressedButton() { IEnumerable pressedButtons = _detector.GetPressedButtons(); - if (pressedButtons.Any()) - { - return !_forStick ? pressedButtons.First().ToString() : ((StickInputId)pressedButtons.First()).ToString(); - } - - return ""; + return !_forStick ? new(pressedButtons.FirstOrDefault()) : new((StickInputId)pressedButtons.FirstOrDefault()); } private void CollectButtonStats() @@ -123,7 +118,7 @@ namespace Ryujinx.Input.Assigner _stats = new Dictionary(); } - public bool HasAnyButtonPressed() + public bool IsAnyButtonPressed() { return _stats.Values.Any(CheckButtonPressed); } diff --git a/src/Ryujinx.Input/Assigner/IButtonAssigner.cs b/src/Ryujinx.Input/Assigner/IButtonAssigner.cs index 76a9fece4..688fbddb4 100644 --- a/src/Ryujinx.Input/Assigner/IButtonAssigner.cs +++ b/src/Ryujinx.Input/Assigner/IButtonAssigner.cs @@ -19,7 +19,7 @@ namespace Ryujinx.Input.Assigner /// Check if a button was pressed. /// /// True if a button was pressed - bool HasAnyButtonPressed(); + bool IsAnyButtonPressed(); /// /// Indicate if the user of this API should cancel operations. This is triggered for example when a gamepad get disconnected or when a user cancel assignation operations. @@ -31,6 +31,6 @@ namespace Ryujinx.Input.Assigner /// Get the pressed button that was read in by the button assigner. /// /// The pressed button that was read - string GetPressedButton(); + Button? GetPressedButton(); } } diff --git a/src/Ryujinx.Input/Assigner/KeyboardKeyAssigner.cs b/src/Ryujinx.Input/Assigner/KeyboardKeyAssigner.cs index e52ef4a2c..3c011a63b 100644 --- a/src/Ryujinx.Input/Assigner/KeyboardKeyAssigner.cs +++ b/src/Ryujinx.Input/Assigner/KeyboardKeyAssigner.cs @@ -21,9 +21,9 @@ namespace Ryujinx.Input.Assigner _keyboardState = _keyboard.GetKeyboardStateSnapshot(); } - public bool HasAnyButtonPressed() + public bool IsAnyButtonPressed() { - return GetPressedButton().Length != 0; + return GetPressedButton() is not null; } public bool ShouldCancel() @@ -31,20 +31,20 @@ namespace Ryujinx.Input.Assigner return _keyboardState.IsPressed(Key.Escape); } - public string GetPressedButton() + public Button? GetPressedButton() { - string keyPressed = ""; + Button? keyPressed = null; for (Key key = Key.Unknown; key < Key.Count; key++) { if (_keyboardState.IsPressed(key)) { - keyPressed = key.ToString(); + keyPressed = new(key); break; } } - return !ShouldCancel() ? keyPressed : ""; + return !ShouldCancel() ? keyPressed : null; } } } diff --git a/src/Ryujinx.Input/Button.cs b/src/Ryujinx.Input/Button.cs new file mode 100644 index 000000000..4289901ce --- /dev/null +++ b/src/Ryujinx.Input/Button.cs @@ -0,0 +1,33 @@ +using System; + +namespace Ryujinx.Input +{ + public readonly struct Button + { + public readonly ButtonType Type; + private readonly uint _rawValue; + + public Button(Key key) + { + Type = ButtonType.Key; + _rawValue = (uint)key; + } + + public Button(GamepadButtonInputId gamepad) + { + Type = ButtonType.GamepadButtonInputId; + _rawValue = (uint)gamepad; + } + + public Button(StickInputId stick) + { + Type = ButtonType.StickId; + _rawValue = (uint)stick; + } + + public T AsHidType() where T : Enum + { + return (T)Enum.ToObject(typeof(T), _rawValue); + } + } +} diff --git a/src/Ryujinx.Input/ButtonType.cs b/src/Ryujinx.Input/ButtonType.cs new file mode 100644 index 000000000..25ef5eea8 --- /dev/null +++ b/src/Ryujinx.Input/ButtonType.cs @@ -0,0 +1,9 @@ +namespace Ryujinx.Input +{ + public enum ButtonType + { + Key, + GamepadButtonInputId, + StickId, + } +} diff --git a/src/Ryujinx.Input/HLE/InputManager.cs b/src/Ryujinx.Input/HLE/InputManager.cs index 7111f5502..2825542a0 100644 --- a/src/Ryujinx.Input/HLE/InputManager.cs +++ b/src/Ryujinx.Input/HLE/InputManager.cs @@ -2,18 +2,13 @@ using System; namespace Ryujinx.Input.HLE { - public class InputManager : IDisposable + public class InputManager(IGamepadDriver keyboardDriver, IGamepadDriver gamepadDriver) + : IDisposable { - public IGamepadDriver KeyboardDriver { get; private set; } - public IGamepadDriver GamepadDriver { get; private set; } + public IGamepadDriver KeyboardDriver { get; } = keyboardDriver; + public IGamepadDriver GamepadDriver { get; } = gamepadDriver; public IGamepadDriver MouseDriver { get; private set; } - public InputManager(IGamepadDriver keyboardDriver, IGamepadDriver gamepadDriver) - { - KeyboardDriver = keyboardDriver; - GamepadDriver = gamepadDriver; - } - public void SetMouseDriver(IGamepadDriver mouseDriver) { MouseDriver?.Dispose(); diff --git a/src/Ryujinx.Input/HLE/NpadController.cs b/src/Ryujinx.Input/HLE/NpadController.cs index f00db94e2..380745283 100644 --- a/src/Ryujinx.Input/HLE/NpadController.cs +++ b/src/Ryujinx.Input/HLE/NpadController.cs @@ -203,8 +203,6 @@ namespace Ryujinx.Input.HLE new(Key.NumLock, 10), }; - private bool _isValid; - private MotionInput _leftMotionInput; private MotionInput _rightMotionInput; @@ -222,7 +220,6 @@ namespace Ryujinx.Input.HLE { State = default; Id = null; - _isValid = false; _cemuHookClient = cemuHookClient; } @@ -234,20 +231,19 @@ namespace Ryujinx.Input.HLE Id = config.Id; _gamepad = GamepadDriver.GetGamepad(Id); - _isValid = _gamepad != null; UpdateUserConfiguration(config); - return _isValid; + return _gamepad != null; } public void UpdateUserConfiguration(InputConfig config) { if (config is StandardControllerInputConfig controllerConfig) { - bool needsMotionInputUpdate = _config == null || (_config is StandardControllerInputConfig oldControllerConfig && - (oldControllerConfig.Motion.EnableMotion != controllerConfig.Motion.EnableMotion) && - (oldControllerConfig.Motion.MotionBackend != controllerConfig.Motion.MotionBackend)); + bool needsMotionInputUpdate = _config is not StandardControllerInputConfig oldControllerConfig || + ((oldControllerConfig.Motion.EnableMotion != controllerConfig.Motion.EnableMotion) && + (oldControllerConfig.Motion.MotionBackend != controllerConfig.Motion.MotionBackend)); if (needsMotionInputUpdate) { @@ -262,10 +258,7 @@ namespace Ryujinx.Input.HLE _config = config; - if (_isValid) - { - _gamepad.SetConfiguration(config); - } + _gamepad?.SetConfiguration(config); } private void UpdateMotionInput(MotionConfigController motionConfig) @@ -282,18 +275,21 @@ namespace Ryujinx.Input.HLE public void Update() { - if (_isValid && GamepadDriver != null) + // _gamepad may be altered by other threads + var gamepad = _gamepad; + + if (gamepad != null && GamepadDriver != null) { - State = _gamepad.GetMappedStateSnapshot(); + State = gamepad.GetMappedStateSnapshot(); if (_config is StandardControllerInputConfig controllerConfig && controllerConfig.Motion.EnableMotion) { if (controllerConfig.Motion.MotionBackend == MotionInputBackendType.GamepadDriver) { - if (_gamepad.Features.HasFlag(GamepadFeaturesFlag.Motion)) + if (gamepad.Features.HasFlag(GamepadFeaturesFlag.Motion)) { - Vector3 accelerometer = _gamepad.GetMotionData(MotionInputId.Accelerometer); - Vector3 gyroscope = _gamepad.GetMotionData(MotionInputId.Gyroscope); + Vector3 accelerometer = gamepad.GetMotionData(MotionInputId.Accelerometer); + Vector3 gyroscope = gamepad.GetMotionData(MotionInputId.Gyroscope); accelerometer = new Vector3(accelerometer.X, -accelerometer.Z, accelerometer.Y); gyroscope = new Vector3(gyroscope.X, -gyroscope.Z, gyroscope.Y); @@ -491,38 +487,35 @@ namespace Ryujinx.Input.HLE return value; } - public KeyboardInput? GetHLEKeyboardInput() + public static KeyboardInput GetHLEKeyboardInput(IGamepadDriver KeyboardDriver) { - if (_gamepad is IKeyboard keyboard) + var keyboard = KeyboardDriver.GetGamepad("0") as IKeyboard; + + KeyboardStateSnapshot keyboardState = keyboard.GetKeyboardStateSnapshot(); + + KeyboardInput hidKeyboard = new() { - KeyboardStateSnapshot keyboardState = keyboard.GetKeyboardStateSnapshot(); + Modifier = 0, + Keys = new ulong[0x4], + }; - KeyboardInput hidKeyboard = new() - { - Modifier = 0, - Keys = new ulong[0x4], - }; + foreach (HLEKeyboardMappingEntry entry in _keyMapping) + { + ulong value = keyboardState.IsPressed(entry.TargetKey) ? 1UL : 0UL; - foreach (HLEKeyboardMappingEntry entry in _keyMapping) - { - ulong value = keyboardState.IsPressed(entry.TargetKey) ? 1UL : 0UL; - - hidKeyboard.Keys[entry.Target / 0x40] |= (value << (entry.Target % 0x40)); - } - - foreach (HLEKeyboardMappingEntry entry in _keyModifierMapping) - { - int value = keyboardState.IsPressed(entry.TargetKey) ? 1 : 0; - - hidKeyboard.Modifier |= value << entry.Target; - } - - return hidKeyboard; + hidKeyboard.Keys[entry.Target / 0x40] |= (value << (entry.Target % 0x40)); } - return null; - } + foreach (HLEKeyboardMappingEntry entry in _keyModifierMapping) + { + int value = keyboardState.IsPressed(entry.TargetKey) ? 1 : 0; + hidKeyboard.Modifier |= value << entry.Target; + } + + return hidKeyboard; + + } protected virtual void Dispose(bool disposing) { diff --git a/src/Ryujinx.Input/HLE/NpadManager.cs b/src/Ryujinx.Input/HLE/NpadManager.cs index 25887748f..1dc87358d 100644 --- a/src/Ryujinx.Input/HLE/NpadManager.cs +++ b/src/Ryujinx.Input/HLE/NpadManager.cs @@ -5,6 +5,7 @@ using Ryujinx.HLE.HOS.Services.Hid; using System; using System.Collections.Generic; using System.Diagnostics; +using System.Linq; using System.Runtime.CompilerServices; using CemuHookClient = Ryujinx.Input.Motion.CemuHook.Client; using ControllerType = Ryujinx.Common.Configuration.Hid.ControllerType; @@ -69,7 +70,20 @@ namespace Ryujinx.Input.HLE private void HandleOnGamepadDisconnected(string obj) { // Force input reload - ReloadConfiguration(_inputConfig, _enableKeyboard, _enableMouse); + lock (_lock) + { + // Forcibly disconnect any controllers with this ID. + for (int i = 0; i < _controllers.Length; i++) + { + if (_controllers[i]?.Id == obj) + { + _controllers[i]?.Dispose(); + _controllers[i] = null; + } + } + + ReloadConfiguration(_inputConfig, _enableKeyboard, _enableMouse); + } } private void HandleOnGamepadConnected(string id) @@ -106,31 +120,48 @@ namespace Ryujinx.Input.HLE { lock (_lock) { - for (int i = 0; i < _controllers.Length; i++) - { - _controllers[i]?.Dispose(); - _controllers[i] = null; - } + NpadController[] oldControllers = _controllers.ToArray(); List validInputs = new(); foreach (InputConfig inputConfigEntry in inputConfig) { - NpadController controller = new(_cemuHookClient); + NpadController controller; + int index = (int)inputConfigEntry.PlayerIndex; + + if (oldControllers[index] != null) + { + // Try reuse the existing controller. + controller = oldControllers[index]; + oldControllers[index] = null; + } + else + { + controller = new(_cemuHookClient); + } bool isValid = DriverConfigurationUpdate(ref controller, inputConfigEntry); if (!isValid) { + _controllers[index] = null; controller.Dispose(); } else { - _controllers[(int)inputConfigEntry.PlayerIndex] = controller; + _controllers[index] = controller; validInputs.Add(inputConfigEntry); } } + for (int i = 0; i < oldControllers.Length; i++) + { + // Disconnect any controllers that weren't reused by the new configuration. + + oldControllers[i]?.Dispose(); + oldControllers[i] = null; + } + _inputConfig = inputConfig; _enableKeyboard = enableKeyboard; _enableMouse = enableMouse; @@ -143,6 +174,11 @@ namespace Ryujinx.Input.HLE { lock (_lock) { + foreach (InputConfig inputConfig in _inputConfig) + { + _controllers[(int)inputConfig.PlayerIndex]?.GamepadDriver?.Clear(); + } + _blockInputUpdates = false; } } @@ -200,11 +236,6 @@ namespace Ryujinx.Input.HLE var altMotionState = isJoyconPair ? controller.GetHLEMotionState(true) : default; motionState = (controller.GetHLEMotionState(), altMotionState); - - if (_enableKeyboard) - { - hleKeyboardInput = controller.GetHLEKeyboardInput(); - } } else { @@ -226,6 +257,11 @@ namespace Ryujinx.Input.HLE } } + if (!_blockInputUpdates && _enableKeyboard) + { + hleKeyboardInput = NpadController.GetHLEKeyboardInput(_keyboardDriver); + } + _device.Hid.Npads.Update(hleInputStates); _device.Hid.Npads.UpdateSixAxis(hleMotionStates); diff --git a/src/Ryujinx.Input/IGamepadDriver.cs b/src/Ryujinx.Input/IGamepadDriver.cs index 67b01c26c..625c3e694 100644 --- a/src/Ryujinx.Input/IGamepadDriver.cs +++ b/src/Ryujinx.Input/IGamepadDriver.cs @@ -33,5 +33,11 @@ namespace Ryujinx.Input /// The unique id of the gamepad /// An instance of associated to the gamepad id given or null if not found IGamepad GetGamepad(string id); + + /// + /// Clear the internal state of the driver. + /// + /// Does nothing by default. + void Clear() { } } } diff --git a/src/Ryujinx.Memory/AddressSpaceManager.cs b/src/Ryujinx.Memory/AddressSpaceManager.cs index 05447ae39..807c5c0f4 100644 --- a/src/Ryujinx.Memory/AddressSpaceManager.cs +++ b/src/Ryujinx.Memory/AddressSpaceManager.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; using System.Linq; using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; namespace Ryujinx.Memory { @@ -11,25 +10,21 @@ namespace Ryujinx.Memory /// Represents a address space manager. /// Supports virtual memory region mapping, address translation and read/write access to mapped regions. /// - public sealed class AddressSpaceManager : IVirtualMemoryManager, IWritableBlock + public sealed class AddressSpaceManager : VirtualMemoryManagerBase, IVirtualMemoryManager { - public const int PageBits = PageTable.PageBits; - public const int PageSize = PageTable.PageSize; - public const int PageMask = PageTable.PageMask; - /// - public bool Supports4KBPages => true; + public bool UsesPrivateAllocations => false; /// /// Address space width in bits. /// public int AddressSpaceBits { get; } - private readonly ulong _addressSpaceSize; - private readonly MemoryBlock _backingMemory; private readonly PageTable _pageTable; + protected override ulong AddressSpaceSize { get; } + /// /// Creates a new instance of the memory manager. /// @@ -47,7 +42,7 @@ namespace Ryujinx.Memory } AddressSpaceBits = asBits; - _addressSpaceSize = asSize; + AddressSpaceSize = asSize; _backingMemory = backingMemory; _pageTable = new PageTable(); } @@ -67,8 +62,7 @@ namespace Ryujinx.Memory } } - /// - public void MapForeign(ulong va, nuint hostPointer, ulong size) + public override void MapForeign(ulong va, nuint hostPointer, ulong size) { AssertValidAddressAndSize(va, size); @@ -96,112 +90,6 @@ namespace Ryujinx.Memory } } - /// - public T Read(ulong va) where T : unmanaged - { - return MemoryMarshal.Cast(GetSpan(va, Unsafe.SizeOf()))[0]; - } - - /// - public void Read(ulong va, Span data) - { - ReadImpl(va, data); - } - - /// - public void Write(ulong va, T value) where T : unmanaged - { - Write(va, MemoryMarshal.Cast(MemoryMarshal.CreateSpan(ref value, 1))); - } - - /// - public void Write(ulong va, ReadOnlySpan data) - { - if (data.Length == 0) - { - return; - } - - AssertValidAddressAndSize(va, (ulong)data.Length); - - if (IsContiguousAndMapped(va, data.Length)) - { - data.CopyTo(GetHostSpanContiguous(va, data.Length)); - } - else - { - int offset = 0, size; - - if ((va & PageMask) != 0) - { - size = Math.Min(data.Length, PageSize - (int)(va & PageMask)); - - data[..size].CopyTo(GetHostSpanContiguous(va, size)); - - offset += size; - } - - for (; offset < data.Length; offset += size) - { - size = Math.Min(data.Length - offset, PageSize); - - data.Slice(offset, size).CopyTo(GetHostSpanContiguous(va + (ulong)offset, size)); - } - } - } - - /// - public bool WriteWithRedundancyCheck(ulong va, ReadOnlySpan data) - { - Write(va, data); - - return true; - } - - /// - public ReadOnlySpan GetSpan(ulong va, int size, bool tracked = false) - { - if (size == 0) - { - return ReadOnlySpan.Empty; - } - - if (IsContiguousAndMapped(va, size)) - { - return GetHostSpanContiguous(va, size); - } - else - { - Span data = new byte[size]; - - ReadImpl(va, data); - - return data; - } - } - - /// - public unsafe WritableRegion GetWritableRegion(ulong va, int size, bool tracked = false) - { - if (size == 0) - { - return new WritableRegion(null, va, Memory.Empty); - } - - if (IsContiguousAndMapped(va, size)) - { - return new WritableRegion(null, va, new NativeMemoryManager((byte*)GetHostAddress(va), size).Memory); - } - else - { - Memory memory = new byte[size]; - - GetSpan(va, size).CopyTo(memory.Span); - - return new WritableRegion(this, va, memory); - } - } - /// public unsafe ref T GetRef(ulong va) where T : unmanaged { @@ -213,50 +101,6 @@ namespace Ryujinx.Memory return ref *(T*)GetHostAddress(va); } - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static int GetPagesCount(ulong va, uint size, out ulong startVa) - { - // WARNING: Always check if ulong does not overflow during the operations. - startVa = va & ~(ulong)PageMask; - ulong vaSpan = (va - startVa + size + PageMask) & ~(ulong)PageMask; - - return (int)(vaSpan / PageSize); - } - - private static void ThrowMemoryNotContiguous() => throw new MemoryNotContiguousException(); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private bool IsContiguousAndMapped(ulong va, int size) => IsContiguous(va, size) && IsMapped(va); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private bool IsContiguous(ulong va, int size) - { - if (!ValidateAddress(va) || !ValidateAddressAndSize(va, (ulong)size)) - { - return false; - } - - int pages = GetPagesCount(va, (uint)size, out va); - - for (int page = 0; page < pages - 1; page++) - { - if (!ValidateAddress(va + PageSize)) - { - return false; - } - - if (GetHostAddress(va) + PageSize != GetHostAddress(va + PageSize)) - { - return false; - } - - va += PageSize; - } - - return true; - } - /// public IEnumerable GetHostRegions(ulong va, ulong size) { @@ -314,7 +158,7 @@ namespace Ryujinx.Memory return null; } - int pages = GetPagesCount(va, (uint)size, out va); + int pages = GetPagesCount(va, size, out va); var regions = new List(); @@ -346,37 +190,8 @@ namespace Ryujinx.Memory return regions; } - private void ReadImpl(ulong va, Span data) - { - if (data.Length == 0) - { - return; - } - - AssertValidAddressAndSize(va, (ulong)data.Length); - - int offset = 0, size; - - if ((va & PageMask) != 0) - { - size = Math.Min(data.Length, PageSize - (int)(va & PageMask)); - - GetHostSpanContiguous(va, size).CopyTo(data[..size]); - - offset += size; - } - - for (; offset < data.Length; offset += size) - { - size = Math.Min(data.Length - offset, PageSize); - - GetHostSpanContiguous(va + (ulong)offset, size).CopyTo(data.Slice(offset, size)); - } - } - - /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool IsMapped(ulong va) + public override bool IsMapped(ulong va) { if (!ValidateAddress(va)) { @@ -389,7 +204,7 @@ namespace Ryujinx.Memory /// public bool IsRangeMapped(ulong va, ulong size) { - if (size == 0UL) + if (size == 0) { return true; } @@ -414,42 +229,6 @@ namespace Ryujinx.Memory return true; } - private bool ValidateAddress(ulong va) - { - return va < _addressSpaceSize; - } - - /// - /// Checks if the combination of virtual address and size is part of the addressable space. - /// - /// Virtual address of the range - /// Size of the range in bytes - /// True if the combination of virtual address and size is part of the addressable space - private bool ValidateAddressAndSize(ulong va, ulong size) - { - ulong endVa = va + size; - return endVa >= va && endVa >= size && endVa <= _addressSpaceSize; - } - - /// - /// Ensures the combination of virtual address and size is part of the addressable space. - /// - /// Virtual address of the range - /// Size of the range in bytes - /// Throw when the memory region specified outside the addressable space - private void AssertValidAddressAndSize(ulong va, ulong size) - { - if (!ValidateAddressAndSize(va, size)) - { - throw new InvalidMemoryRegionException($"va=0x{va:X16}, size=0x{size:X16}"); - } - } - - private unsafe Span GetHostSpanContiguous(ulong va, int size) - { - return new Span((void*)GetHostAddress(va), size); - } - private nuint GetHostAddress(ulong va) { return _pageTable.Read(va) + (nuint)(va & PageMask); @@ -461,15 +240,21 @@ namespace Ryujinx.Memory } /// - public void TrackingReprotect(ulong va, ulong size, MemoryPermission protection) + public void TrackingReprotect(ulong va, ulong size, MemoryPermission protection, bool guest = false) { throw new NotImplementedException(); } - /// - public void SignalMemoryTracking(ulong va, ulong size, bool write, bool precise = false, int? exemptId = null) - { - // Only the ARM Memory Manager has tracking for now. - } + protected unsafe override Memory GetPhysicalAddressMemory(nuint pa, int size) + => new NativeMemoryManager((byte*)pa, size).Memory; + + protected override unsafe Span GetPhysicalAddressSpan(nuint pa, int size) + => new Span((void*)pa, size); + + protected override nuint TranslateVirtualAddressChecked(ulong va) + => GetHostAddress(va); + + protected override nuint TranslateVirtualAddressUnchecked(ulong va) + => GetHostAddress(va); } } diff --git a/src/Ryujinx.Memory/BytesReadOnlySequenceSegment.cs b/src/Ryujinx.Memory/BytesReadOnlySequenceSegment.cs new file mode 100644 index 000000000..5fe8d936c --- /dev/null +++ b/src/Ryujinx.Memory/BytesReadOnlySequenceSegment.cs @@ -0,0 +1,60 @@ +using System; +using System.Buffers; +using System.Runtime.InteropServices; + +namespace Ryujinx.Memory +{ + /// + /// A concrete implementation of , + /// with methods to help build a full sequence. + /// + public sealed class BytesReadOnlySequenceSegment : ReadOnlySequenceSegment + { + public BytesReadOnlySequenceSegment(Memory memory) => Memory = memory; + + public BytesReadOnlySequenceSegment Append(Memory memory) + { + var nextSegment = new BytesReadOnlySequenceSegment(memory) + { + RunningIndex = RunningIndex + Memory.Length + }; + + Next = nextSegment; + + return nextSegment; + } + + /// + /// Attempts to determine if the current and are contiguous. + /// Only works if both were created by a . + /// + /// The segment to check if continuous with the current one + /// The starting address of the contiguous segment + /// The size of the contiguous segment + /// True if the segments are contiguous, otherwise false + public unsafe bool IsContiguousWith(Memory other, out nuint contiguousStart, out int contiguousSize) + { + if (MemoryMarshal.TryGetMemoryManager>(Memory, out var thisMemoryManager) && + MemoryMarshal.TryGetMemoryManager>(other, out var otherMemoryManager) && + thisMemoryManager.Pointer + thisMemoryManager.Length == otherMemoryManager.Pointer) + { + contiguousStart = (nuint)thisMemoryManager.Pointer; + contiguousSize = thisMemoryManager.Length + otherMemoryManager.Length; + return true; + } + else + { + contiguousStart = 0; + contiguousSize = 0; + return false; + } + } + + /// + /// Replaces the current value with the one provided. + /// + /// The new segment to hold in this + public void Replace(Memory memory) + => Memory = memory; + } +} diff --git a/src/Ryujinx.Memory/IVirtualMemoryManager.cs b/src/Ryujinx.Memory/IVirtualMemoryManager.cs index 9cf3663cf..102cedc94 100644 --- a/src/Ryujinx.Memory/IVirtualMemoryManager.cs +++ b/src/Ryujinx.Memory/IVirtualMemoryManager.cs @@ -8,10 +8,10 @@ namespace Ryujinx.Memory public interface IVirtualMemoryManager { /// - /// Indicates whenever the memory manager supports aliasing pages at 4KB granularity. + /// Indicates whether the memory manager creates private allocations when the flag is set on map. /// - /// True if 4KB pages are supported by the memory manager, false otherwise - bool Supports4KBPages { get; } + /// True if private mappings might be used, false otherwise + bool UsesPrivateAllocations { get; } /// /// Maps a virtual memory range into a physical memory range. @@ -124,6 +124,16 @@ namespace Ryujinx.Memory } } + /// + /// Gets a read-only sequence of read-only memory blocks from CPU mapped memory. + /// + /// Virtual address of the data + /// Size of the data + /// True if read tracking is triggered on the memory + /// A read-only sequence of read-only memory of the data + /// Throw for unhandled invalid or unmapped memory accesses + ReadOnlySequence GetReadOnlySequence(ulong va, int size, bool tracked = false); + /// /// Gets a read-only span of data from CPU mapped memory. /// @@ -214,6 +224,7 @@ namespace Ryujinx.Memory /// Virtual address base /// Size of the region to protect /// Memory protection to set - void TrackingReprotect(ulong va, ulong size, MemoryPermission protection); + /// True if the protection is for guest access, false otherwise + void TrackingReprotect(ulong va, ulong size, MemoryPermission protection, bool guest); } } diff --git a/src/Ryujinx.Memory/IWritableBlock.cs b/src/Ryujinx.Memory/IWritableBlock.cs index 0858e0c96..78ae2479d 100644 --- a/src/Ryujinx.Memory/IWritableBlock.cs +++ b/src/Ryujinx.Memory/IWritableBlock.cs @@ -1,9 +1,25 @@ using System; +using System.Buffers; namespace Ryujinx.Memory { public interface IWritableBlock { + /// + /// Writes data to CPU mapped memory, with write tracking. + /// + /// Virtual address to write the data into + /// Data to be written + /// Throw for unhandled invalid or unmapped memory accesses + void Write(ulong va, ReadOnlySequence data) + { + foreach (ReadOnlyMemory segment in data) + { + Write(va, segment.Span); + va += (ulong)segment.Length; + } + } + void Write(ulong va, ReadOnlySpan data); void WriteUntracked(ulong va, ReadOnlySpan data) => Write(va, data); diff --git a/src/Ryujinx.Memory/MachJitWorkaround.cs b/src/Ryujinx.Memory/MachJitWorkaround.cs deleted file mode 100644 index cfb1e419c..000000000 --- a/src/Ryujinx.Memory/MachJitWorkaround.cs +++ /dev/null @@ -1,168 +0,0 @@ -using System; -using System.Runtime.InteropServices; -using System.Runtime.Versioning; - -namespace Ryujinx.Memory -{ - [SupportedOSPlatform("ios")] - static unsafe partial class MachJitWorkaround - { - [LibraryImport("libc")] - public static partial int mach_task_self(); - - [LibraryImport("libc")] - public static partial int mach_make_memory_entry_64(IntPtr target_task, IntPtr* size, IntPtr offset, int permission, IntPtr* object_handle, IntPtr parent_entry); - - [LibraryImport("libc")] - public static partial int mach_memory_entry_ownership(IntPtr mem_entry, IntPtr owner, int ledger_tag, int ledger_flags); - - [LibraryImport("libc")] - public static partial int vm_map(IntPtr target_task, IntPtr* address, IntPtr size, IntPtr mask, int flags, IntPtr obj, IntPtr offset, int copy, int cur_protection, int max_protection, int inheritance); - - [LibraryImport("libc")] - public static partial int vm_allocate(IntPtr target_task, IntPtr* address, IntPtr size, int flags); - - [LibraryImport("libc")] - public static partial int vm_deallocate(IntPtr target_task, IntPtr address, IntPtr size); - - [LibraryImport("libc")] - public static partial int vm_remap(IntPtr target_task, IntPtr* target_address, IntPtr size, IntPtr mask, int flags, IntPtr src_task, IntPtr src_address, int copy, int* cur_protection, int* max_protection, int inheritance); - - const int MAP_MEM_LEDGER_TAGGED = 0x002000; - const int MAP_MEM_NAMED_CREATE = 0x020000; - - const int VM_PROT_READ = 0x01; - const int VM_PROT_WRITE = 0x02; - const int VM_PROT_EXECUTE = 0x04; - - const int VM_LEDGER_TAG_DEFAULT = 0x00000001; - const int VM_LEDGER_FLAG_NO_FOOTPRINT = 0x00000001; - - const int VM_INHERIT_COPY = 1; - const int VM_INHERIT_DEFAULT = VM_INHERIT_COPY; - - const int VM_FLAGS_FIXED = 0x0000; - const int VM_FLAGS_ANYWHERE = 0x0001; - const int VM_FLAGS_OVERWRITE = 0x4000; - - const IntPtr TASK_NULL = 0; - - public static void ReallocateBlock(IntPtr address, int size) - { - IntPtr selfTask = mach_task_self(); - IntPtr memorySize = (IntPtr)size; - IntPtr memoryObjectPort = IntPtr.Zero; - - int err = mach_make_memory_entry_64(selfTask, &memorySize, 0, MAP_MEM_NAMED_CREATE | MAP_MEM_LEDGER_TAGGED | VM_PROT_READ | VM_PROT_WRITE | VM_PROT_EXECUTE, &memoryObjectPort, 0); - - if (err != 0) - { - throw new InvalidOperationException($"Make memory entry failed: {err}"); - } - - try - { - if (memorySize != (IntPtr)size) - { - throw new InvalidOperationException($"Created with size {memorySize} instead of {size}."); - } - - err = mach_memory_entry_ownership(memoryObjectPort, TASK_NULL, VM_LEDGER_TAG_DEFAULT, VM_LEDGER_FLAG_NO_FOOTPRINT); - - if (err != 0) - { - throw new InvalidOperationException($"Failed to set ownership: {err}"); - } - - IntPtr mapAddress = address; - - err = vm_map( - selfTask, - &mapAddress, - memorySize, - /*mask=*/ 0, - /*flags=*/ VM_FLAGS_OVERWRITE, - memoryObjectPort, - /*offset=*/ 0, - /*copy=*/ 0, - VM_PROT_READ | VM_PROT_WRITE, - VM_PROT_READ | VM_PROT_WRITE | VM_PROT_EXECUTE, - VM_INHERIT_COPY); - - if (err != 0) - { - throw new InvalidOperationException($"Failed to map: {err}"); - } - - if (address != mapAddress) - { - throw new InvalidOperationException($"Remap changed address"); - } - } - finally - { - //mach_port_deallocate(selfTask, memoryObjectPort); - } - - Console.WriteLine($"Reallocated an area... {address:x16}"); - } - - public static void ReallocateAreaWithOwnership(IntPtr address, int size) - { - int mapChunkSize = 128 * 1024 * 1024; - IntPtr endAddress = address + size; - IntPtr blockAddress = address; - while (blockAddress < endAddress) - { - int blockSize = Math.Min(mapChunkSize, (int)(endAddress - blockAddress)); - - ReallocateBlock(blockAddress, blockSize); - - blockAddress += blockSize; - } - } - - public static IntPtr AllocateSharedMemory(ulong size, bool reserve) - { - IntPtr address = 0; - - int err = vm_allocate(mach_task_self(), &address, (IntPtr)size, VM_FLAGS_ANYWHERE); - - if (err != 0) - { - throw new InvalidOperationException($"Failed to allocate shared memory: {err}"); - } - - return address; - } - - public static void DestroySharedMemory(IntPtr handle, ulong size) - { - vm_deallocate(mach_task_self(), handle, (IntPtr)size); - } - - public static IntPtr MapView(IntPtr sharedMemory, ulong srcOffset, IntPtr location, ulong size) - { - IntPtr taskSelf = mach_task_self(); - IntPtr srcAddress = (IntPtr)((ulong)sharedMemory + srcOffset); - IntPtr dstAddress = location; - - int cur_protection = 0; - int max_protection = 0; - - int err = vm_remap(taskSelf, &dstAddress, (IntPtr)size, 0, VM_FLAGS_OVERWRITE, taskSelf, srcAddress, 0, &cur_protection, &max_protection, VM_INHERIT_DEFAULT); - - if (err != 0) - { - throw new InvalidOperationException($"Failed to allocate remap memory: {err}"); - } - - return dstAddress; - } - - public static void UnmapView(IntPtr location, ulong size) - { - vm_deallocate(mach_task_self(), location, (IntPtr)size); - } - } -} \ No newline at end of file diff --git a/src/Ryujinx.Memory/MemoryBlock.cs b/src/Ryujinx.Memory/MemoryBlock.cs index 477be893a..c2ba51f82 100644 --- a/src/Ryujinx.Memory/MemoryBlock.cs +++ b/src/Ryujinx.Memory/MemoryBlock.cs @@ -13,13 +13,13 @@ namespace Ryujinx.Memory private readonly bool _isMirror; private readonly bool _viewCompatible; private readonly bool _forJit; - private IntPtr _sharedMemory; - private IntPtr _pointer; + private nint _sharedMemory; + private nint _pointer; /// /// Pointer to the memory block data. /// - public IntPtr Pointer => _pointer; + public nint Pointer => _pointer; /// /// Size of the memory block. @@ -68,7 +68,7 @@ namespace Ryujinx.Memory /// Shared memory to use as backing storage for this block /// Throw when there's an error while mapping the shared memory /// Throw when the current platform is not supported - private MemoryBlock(ulong size, IntPtr sharedMemory) + private MemoryBlock(ulong size, nint sharedMemory) { _pointer = MemoryManagement.MapSharedMemory(sharedMemory, size); Size = size; @@ -86,7 +86,7 @@ namespace Ryujinx.Memory /// Throw when the current platform is not supported public MemoryBlock CreateMirror() { - if (_sharedMemory == IntPtr.Zero) + if (_sharedMemory == nint.Zero) { throw new NotSupportedException("Mirroring is not supported on the memory block because the Mirrorable flag was not set."); } @@ -134,7 +134,7 @@ namespace Ryujinx.Memory /// Throw when either or are out of range public void MapView(MemoryBlock srcBlock, ulong srcOffset, ulong dstOffset, ulong size) { - if (srcBlock._sharedMemory == IntPtr.Zero) + if (srcBlock._sharedMemory == nint.Zero) { throw new ArgumentException("The source memory block is not mirrorable, and thus cannot be mapped on the current block."); } @@ -174,7 +174,7 @@ namespace Ryujinx.Memory /// Starting offset of the range being read /// Span where the bytes being read will be copied to /// Throw when the memory block has already been disposed - /// Throw when the memory region specified for the the data is out of range + /// Throw when the memory region specified for the data is out of range [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Read(ulong offset, Span data) { @@ -188,7 +188,7 @@ namespace Ryujinx.Memory /// Offset where the data is located /// Data at the specified address /// Throw when the memory block has already been disposed - /// Throw when the memory region specified for the the data is out of range + /// Throw when the memory region specified for the data is out of range [MethodImpl(MethodImplOptions.AggressiveInlining)] public T Read(ulong offset) where T : unmanaged { @@ -201,7 +201,7 @@ namespace Ryujinx.Memory /// Starting offset of the range being written /// Span where the bytes being written will be copied from /// Throw when the memory block has already been disposed - /// Throw when the memory region specified for the the data is out of range + /// Throw when the memory region specified for the data is out of range [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Write(ulong offset, ReadOnlySpan data) { @@ -215,7 +215,7 @@ namespace Ryujinx.Memory /// Offset to write the data into /// Data to be written /// Throw when the memory block has already been disposed - /// Throw when the memory region specified for the the data is out of range + /// Throw when the memory region specified for the data is out of range [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Write(ulong offset, T data) where T : unmanaged { @@ -273,9 +273,9 @@ namespace Ryujinx.Memory [MethodImpl(MethodImplOptions.AggressiveInlining)] public unsafe ref T GetRef(ulong offset) where T : unmanaged { - IntPtr ptr = _pointer; + nint ptr = _pointer; - ObjectDisposedException.ThrowIf(ptr == IntPtr.Zero, this); + ObjectDisposedException.ThrowIf(ptr == nint.Zero, this); int size = Unsafe.SizeOf(); @@ -298,14 +298,14 @@ namespace Ryujinx.Memory /// Throw when the memory block has already been disposed /// Throw when either or are out of range [MethodImpl(MethodImplOptions.AggressiveInlining)] - public IntPtr GetPointer(ulong offset, ulong size) => GetPointerInternal(offset, size); + public nint GetPointer(ulong offset, ulong size) => GetPointerInternal(offset, size); [MethodImpl(MethodImplOptions.AggressiveInlining)] - private IntPtr GetPointerInternal(ulong offset, ulong size) + private nint GetPointerInternal(ulong offset, ulong size) { - IntPtr ptr = _pointer; + nint ptr = _pointer; - ObjectDisposedException.ThrowIf(ptr == IntPtr.Zero, this); + ObjectDisposedException.ThrowIf(ptr == nint.Zero, this); ulong endOffset = offset + size; @@ -364,9 +364,9 @@ namespace Ryujinx.Memory /// Native pointer /// Offset to add /// Native pointer with the added offset - private static IntPtr PtrAddr(IntPtr pointer, ulong offset) + private static nint PtrAddr(nint pointer, ulong offset) { - return new IntPtr(pointer.ToInt64() + (long)offset); + return new nint(pointer.ToInt64() + (long)offset); } /// @@ -386,10 +386,10 @@ namespace Ryujinx.Memory private void FreeMemory() { - IntPtr ptr = Interlocked.Exchange(ref _pointer, IntPtr.Zero); + nint ptr = Interlocked.Exchange(ref _pointer, nint.Zero); // If pointer is null, the memory was already freed or never allocated. - if (ptr != IntPtr.Zero) + if (ptr != nint.Zero) { if (_usesSharedMemory) { @@ -403,9 +403,9 @@ namespace Ryujinx.Memory if (!_isMirror) { - IntPtr sharedMemory = Interlocked.Exchange(ref _sharedMemory, IntPtr.Zero); + nint sharedMemory = Interlocked.Exchange(ref _sharedMemory, nint.Zero); - if (sharedMemory != IntPtr.Zero) + if (sharedMemory != nint.Zero) { MemoryManagement.DestroySharedMemory(sharedMemory); } @@ -426,7 +426,7 @@ namespace Ryujinx.Memory return OperatingSystem.IsWindowsVersionAtLeast(10, 0, 17134); } - return OperatingSystem.IsLinux() || OperatingSystem.IsMacOS() || OperatingSystem.IsIOS(); + return OperatingSystem.IsLinux() || OperatingSystem.IsMacOS(); } return true; diff --git a/src/Ryujinx.Memory/MemoryManagement.cs b/src/Ryujinx.Memory/MemoryManagement.cs index a23fafb57..276cc2a4c 100644 --- a/src/Ryujinx.Memory/MemoryManagement.cs +++ b/src/Ryujinx.Memory/MemoryManagement.cs @@ -4,13 +4,13 @@ namespace Ryujinx.Memory { public static class MemoryManagement { - public static IntPtr Allocate(ulong size, bool forJit) + public static nint Allocate(ulong size, bool forJit) { if (OperatingSystem.IsWindows()) { - return MemoryManagementWindows.Allocate((IntPtr)size); + return MemoryManagementWindows.Allocate((nint)size); } - else if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS() || OperatingSystem.IsIOS()) + else if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS()) { return MemoryManagementUnix.Allocate(size, forJit); } @@ -20,13 +20,13 @@ namespace Ryujinx.Memory } } - public static IntPtr Reserve(ulong size, bool forJit, bool viewCompatible) + public static nint Reserve(ulong size, bool forJit, bool viewCompatible) { if (OperatingSystem.IsWindows()) { - return MemoryManagementWindows.Reserve((IntPtr)size, viewCompatible); + return MemoryManagementWindows.Reserve((nint)size, viewCompatible); } - else if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS() || OperatingSystem.IsIOS()) + else if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS()) { return MemoryManagementUnix.Reserve(size, forJit); } @@ -36,13 +36,13 @@ namespace Ryujinx.Memory } } - public static void Commit(IntPtr address, ulong size, bool forJit) + public static void Commit(nint address, ulong size, bool forJit) { if (OperatingSystem.IsWindows()) { - MemoryManagementWindows.Commit(address, (IntPtr)size); + MemoryManagementWindows.Commit(address, (nint)size); } - else if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS() || OperatingSystem.IsIOS()) + else if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS()) { MemoryManagementUnix.Commit(address, size, forJit); } @@ -52,13 +52,13 @@ namespace Ryujinx.Memory } } - public static void Decommit(IntPtr address, ulong size) + public static void Decommit(nint address, ulong size) { if (OperatingSystem.IsWindows()) { - MemoryManagementWindows.Decommit(address, (IntPtr)size); + MemoryManagementWindows.Decommit(address, (nint)size); } - else if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS() || OperatingSystem.IsIOS()) + else if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS()) { MemoryManagementUnix.Decommit(address, size); } @@ -68,13 +68,13 @@ namespace Ryujinx.Memory } } - public static void MapView(IntPtr sharedMemory, ulong srcOffset, IntPtr address, ulong size, MemoryBlock owner) + public static void MapView(nint sharedMemory, ulong srcOffset, nint address, ulong size, MemoryBlock owner) { if (OperatingSystem.IsWindows()) { - MemoryManagementWindows.MapView(sharedMemory, srcOffset, address, (IntPtr)size, owner); + MemoryManagementWindows.MapView(sharedMemory, srcOffset, address, (nint)size, owner); } - else if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS() || OperatingSystem.IsIOS()) + else if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS()) { MemoryManagementUnix.MapView(sharedMemory, srcOffset, address, size); } @@ -84,13 +84,13 @@ namespace Ryujinx.Memory } } - public static void UnmapView(IntPtr sharedMemory, IntPtr address, ulong size, MemoryBlock owner) + public static void UnmapView(nint sharedMemory, nint address, ulong size, MemoryBlock owner) { if (OperatingSystem.IsWindows()) { - MemoryManagementWindows.UnmapView(sharedMemory, address, (IntPtr)size, owner); + MemoryManagementWindows.UnmapView(sharedMemory, address, (nint)size, owner); } - else if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS() || OperatingSystem.IsIOS()) + else if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS()) { MemoryManagementUnix.UnmapView(address, size); } @@ -100,15 +100,15 @@ namespace Ryujinx.Memory } } - public static void Reprotect(IntPtr address, ulong size, MemoryPermission permission, bool forView, bool throwOnFail) + public static void Reprotect(nint address, ulong size, MemoryPermission permission, bool forView, bool throwOnFail) { bool result; if (OperatingSystem.IsWindows()) { - result = MemoryManagementWindows.Reprotect(address, (IntPtr)size, permission, forView); + result = MemoryManagementWindows.Reprotect(address, (nint)size, permission, forView); } - else if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS() || OperatingSystem.IsIOS()) + else if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS()) { result = MemoryManagementUnix.Reprotect(address, size, permission); } @@ -123,13 +123,13 @@ namespace Ryujinx.Memory } } - public static bool Free(IntPtr address, ulong size) + public static bool Free(nint address, ulong size) { if (OperatingSystem.IsWindows()) { - return MemoryManagementWindows.Free(address, (IntPtr)size); + return MemoryManagementWindows.Free(address, (nint)size); } - else if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS() || OperatingSystem.IsIOS()) + else if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS()) { return MemoryManagementUnix.Free(address); } @@ -139,13 +139,13 @@ namespace Ryujinx.Memory } } - public static IntPtr CreateSharedMemory(ulong size, bool reserve) + public static nint CreateSharedMemory(ulong size, bool reserve) { if (OperatingSystem.IsWindows()) { - return MemoryManagementWindows.CreateSharedMemory((IntPtr)size, reserve); + return MemoryManagementWindows.CreateSharedMemory((nint)size, reserve); } - else if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS() || OperatingSystem.IsIOS()) + else if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS()) { return MemoryManagementUnix.CreateSharedMemory(size, reserve); } @@ -155,13 +155,13 @@ namespace Ryujinx.Memory } } - public static void DestroySharedMemory(IntPtr handle) + public static void DestroySharedMemory(nint handle) { if (OperatingSystem.IsWindows()) { MemoryManagementWindows.DestroySharedMemory(handle); } - else if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS() || OperatingSystem.IsIOS()) + else if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS()) { MemoryManagementUnix.DestroySharedMemory(handle); } @@ -171,13 +171,13 @@ namespace Ryujinx.Memory } } - public static IntPtr MapSharedMemory(IntPtr handle, ulong size) + public static nint MapSharedMemory(nint handle, ulong size) { if (OperatingSystem.IsWindows()) { return MemoryManagementWindows.MapSharedMemory(handle); } - else if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS() || OperatingSystem.IsIOS()) + else if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS()) { return MemoryManagementUnix.MapSharedMemory(handle, size); } @@ -187,13 +187,13 @@ namespace Ryujinx.Memory } } - public static void UnmapSharedMemory(IntPtr address, ulong size) + public static void UnmapSharedMemory(nint address, ulong size) { if (OperatingSystem.IsWindows()) { MemoryManagementWindows.UnmapSharedMemory(address); } - else if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS() || OperatingSystem.IsIOS()) + else if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS()) { MemoryManagementUnix.UnmapSharedMemory(address, size); } diff --git a/src/Ryujinx.Memory/MemoryManagementUnix.cs b/src/Ryujinx.Memory/MemoryManagementUnix.cs index 948bf8023..76a63a466 100644 --- a/src/Ryujinx.Memory/MemoryManagementUnix.cs +++ b/src/Ryujinx.Memory/MemoryManagementUnix.cs @@ -1,6 +1,5 @@ -using System; +using System; using System.Collections.Concurrent; -using System.Collections.Generic; using System.Runtime.InteropServices; using System.Runtime.Versioning; using static Ryujinx.Memory.MemoryManagerUnixHelper; @@ -9,22 +8,21 @@ namespace Ryujinx.Memory { [SupportedOSPlatform("linux")] [SupportedOSPlatform("macos")] - [SupportedOSPlatform("ios")] static class MemoryManagementUnix { - private static readonly ConcurrentDictionary _allocations = new(); + private static readonly ConcurrentDictionary _allocations = new(); - public static IntPtr Allocate(ulong size, bool forJit) + public static nint Allocate(ulong size, bool forJit) { return AllocateInternal(size, MmapProts.PROT_READ | MmapProts.PROT_WRITE, forJit); } - public static IntPtr Reserve(ulong size, bool forJit) + public static nint Reserve(ulong size, bool forJit) { return AllocateInternal(size, MmapProts.PROT_NONE, forJit); } - private static IntPtr AllocateInternal(ulong size, MmapProts prot, bool forJit, bool shared = false) + private static nint AllocateInternal(ulong size, MmapProts prot, bool forJit, bool shared = false) { MmapFlags flags = MmapFlags.MAP_ANONYMOUS; @@ -42,7 +40,7 @@ namespace Ryujinx.Memory flags |= MmapFlags.MAP_NORESERVE; } - if (OperatingSystem.IsMacOS() && OperatingSystem.IsMacOSVersionAtLeast(10, 14) && forJit) + if (OperatingSystem.IsMacOSVersionAtLeast(10, 14) && forJit) { flags |= MmapFlags.MAP_JIT_DARWIN; @@ -52,18 +50,13 @@ namespace Ryujinx.Memory } } - IntPtr ptr = Mmap(IntPtr.Zero, size, prot, flags, -1, 0); + nint ptr = Mmap(nint.Zero, size, prot, flags, -1, 0); if (ptr == MAP_FAILED) { throw new SystemException(Marshal.GetLastPInvokeErrorMessage()); } - if (OperatingSystem.IsIOS() && forJit) - { - MachJitWorkaround.ReallocateAreaWithOwnership(ptr, (int)size); - } - if (!_allocations.TryAdd(ptr, size)) { // This should be impossible, kernel shouldn't return an already mapped address. @@ -73,11 +66,11 @@ namespace Ryujinx.Memory return ptr; } - public static void Commit(IntPtr address, ulong size, bool forJit) + public static void Commit(nint address, ulong size, bool forJit) { MmapProts prot = MmapProts.PROT_READ | MmapProts.PROT_WRITE; - if ((OperatingSystem.IsIOS() || OperatingSystem.IsMacOSVersionAtLeast(10, 14)) && forJit) + if (OperatingSystem.IsMacOSVersionAtLeast(10, 14) && forJit) { prot |= MmapProts.PROT_EXEC; } @@ -88,7 +81,7 @@ namespace Ryujinx.Memory } } - public static void Decommit(IntPtr address, ulong size) + public static void Decommit(nint address, ulong size) { // Must be writable for madvise to work properly. if (mprotect(address, size, MmapProts.PROT_READ | MmapProts.PROT_WRITE) != 0) @@ -107,7 +100,7 @@ namespace Ryujinx.Memory } } - public static bool Reprotect(IntPtr address, ulong size, MemoryPermission permission) + public static bool Reprotect(nint address, ulong size, MemoryPermission permission) { return mprotect(address, size, GetProtection(permission)) == 0; } @@ -126,7 +119,7 @@ namespace Ryujinx.Memory }; } - public static bool Free(IntPtr address) + public static bool Free(nint address) { if (_allocations.TryRemove(address, out ulong size)) { @@ -136,38 +129,28 @@ namespace Ryujinx.Memory return false; } - public static bool Unmap(IntPtr address, ulong size) + public static bool Unmap(nint address, ulong size) { return munmap(address, size) == 0; } - private static ConcurrentDictionary _sharedMemorySizes = new ConcurrentDictionary(); - - public unsafe static IntPtr CreateSharedMemory(ulong size, bool reserve) + public unsafe static nint CreateSharedMemory(ulong size, bool reserve) { int fd; - if (OperatingSystem.IsIOS()) - { - IntPtr baseAddress = MachJitWorkaround.AllocateSharedMemory(size, reserve); - - _sharedMemorySizes.TryAdd(baseAddress, size); - - return baseAddress; - } - else if (OperatingSystem.IsMacOS()) + if (OperatingSystem.IsMacOS()) { byte[] memName = "Ryujinx-XXXXXX"u8.ToArray(); fixed (byte* pMemName = memName) { - fd = shm_open((IntPtr)pMemName, 0x2 | 0x200 | 0x800 | 0x400, 384); // O_RDWR | O_CREAT | O_EXCL | O_TRUNC, 0600 + fd = shm_open((nint)pMemName, 0x2 | 0x200 | 0x800 | 0x400, 384); // O_RDWR | O_CREAT | O_EXCL | O_TRUNC, 0600 if (fd == -1) { throw new SystemException(Marshal.GetLastPInvokeErrorMessage()); } - if (shm_unlink((IntPtr)pMemName) != 0) + if (shm_unlink((nint)pMemName) != 0) { throw new SystemException(Marshal.GetLastPInvokeErrorMessage()); } @@ -179,20 +162,20 @@ namespace Ryujinx.Memory fixed (byte* pFileName = fileName) { - fd = mkstemp((IntPtr)pFileName); + fd = mkstemp((nint)pFileName); if (fd == -1) { throw new SystemException(Marshal.GetLastPInvokeErrorMessage()); } - if (unlink((IntPtr)pFileName) != 0) + if (unlink((nint)pFileName) != 0) { throw new SystemException(Marshal.GetLastPInvokeErrorMessage()); } } } - if (ftruncate(fd, (IntPtr)size) != 0) + if (ftruncate(fd, (nint)size) != 0) { throw new SystemException(Marshal.GetLastPInvokeErrorMessage()); } @@ -200,58 +183,27 @@ namespace Ryujinx.Memory return fd; } - public static void DestroySharedMemory(IntPtr handle) + public static void DestroySharedMemory(nint handle) { - if (OperatingSystem.IsIOS()) - { - if (_sharedMemorySizes.TryGetValue(handle, out ulong size)) - { - _sharedMemorySizes.Remove(handle, out _); - MachJitWorkaround.DestroySharedMemory(handle, size); - } - } - else - { - close(handle.ToInt32()); - } + close(handle.ToInt32()); } - public static IntPtr MapSharedMemory(IntPtr handle, ulong size) + public static nint MapSharedMemory(nint handle, ulong size) { - if (OperatingSystem.IsIOS()) - { - // The base of the shared memory is already mapped - it's the handle. - // Views are remapped from it. - - return handle; - } - else - { - return Mmap(IntPtr.Zero, size, MmapProts.PROT_READ | MmapProts.PROT_WRITE, MmapFlags.MAP_SHARED, handle.ToInt32(), 0); - } + return Mmap(nint.Zero, size, MmapProts.PROT_READ | MmapProts.PROT_WRITE, MmapFlags.MAP_SHARED, handle.ToInt32(), 0); } - public static void UnmapSharedMemory(IntPtr address, ulong size) + public static void UnmapSharedMemory(nint address, ulong size) { - if (!OperatingSystem.IsIOS()) - { - munmap(address, size); - } + munmap(address, size); } - public static void MapView(IntPtr sharedMemory, ulong srcOffset, IntPtr location, ulong size) + public static void MapView(nint sharedMemory, ulong srcOffset, nint location, ulong size) { - if (OperatingSystem.IsIOS()) - { - MachJitWorkaround.MapView(sharedMemory, srcOffset, location, size); - } - else - { - Mmap(location, size, MmapProts.PROT_READ | MmapProts.PROT_WRITE, MmapFlags.MAP_FIXED | MmapFlags.MAP_SHARED, sharedMemory.ToInt32(), (long)srcOffset); - } + Mmap(location, size, MmapProts.PROT_READ | MmapProts.PROT_WRITE, MmapFlags.MAP_FIXED | MmapFlags.MAP_SHARED, sharedMemory.ToInt32(), (long)srcOffset); } - public static void UnmapView(IntPtr location, ulong size) + public static void UnmapView(nint location, ulong size) { Mmap(location, size, MmapProts.PROT_NONE, MmapFlags.MAP_FIXED | MmapFlags.MAP_PRIVATE | MmapFlags.MAP_ANONYMOUS | MmapFlags.MAP_NORESERVE, -1, 0); } diff --git a/src/Ryujinx.Memory/MemoryManagementWindows.cs b/src/Ryujinx.Memory/MemoryManagementWindows.cs index 742ef6c96..468355dd0 100644 --- a/src/Ryujinx.Memory/MemoryManagementWindows.cs +++ b/src/Ryujinx.Memory/MemoryManagementWindows.cs @@ -12,16 +12,16 @@ namespace Ryujinx.Memory private static readonly PlaceholderManager _placeholders = new(); - public static IntPtr Allocate(IntPtr size) + public static nint Allocate(nint size) { return AllocateInternal(size, AllocationType.Reserve | AllocationType.Commit); } - public static IntPtr Reserve(IntPtr size, bool viewCompatible) + public static nint Reserve(nint size, bool viewCompatible) { if (viewCompatible) { - IntPtr baseAddress = AllocateInternal2(size, AllocationType.Reserve | AllocationType.ReservePlaceholder); + nint baseAddress = AllocateInternal2(size, AllocationType.Reserve | AllocationType.ReservePlaceholder); _placeholders.ReserveRange((ulong)baseAddress, (ulong)size); @@ -31,11 +31,11 @@ namespace Ryujinx.Memory return AllocateInternal(size, AllocationType.Reserve); } - private static IntPtr AllocateInternal(IntPtr size, AllocationType flags = 0) + private static nint AllocateInternal(nint size, AllocationType flags = 0) { - IntPtr ptr = WindowsApi.VirtualAlloc(IntPtr.Zero, size, flags, MemoryProtection.ReadWrite); + nint ptr = WindowsApi.VirtualAlloc(nint.Zero, size, flags, MemoryProtection.ReadWrite); - if (ptr == IntPtr.Zero) + if (ptr == nint.Zero) { throw new SystemException(Marshal.GetLastPInvokeErrorMessage()); } @@ -43,11 +43,11 @@ namespace Ryujinx.Memory return ptr; } - private static IntPtr AllocateInternal2(IntPtr size, AllocationType flags = 0) + private static nint AllocateInternal2(nint size, AllocationType flags = 0) { - IntPtr ptr = WindowsApi.VirtualAlloc2(WindowsApi.CurrentProcessHandle, IntPtr.Zero, size, flags, MemoryProtection.NoAccess, IntPtr.Zero, 0); + nint ptr = WindowsApi.VirtualAlloc2(WindowsApi.CurrentProcessHandle, nint.Zero, size, flags, MemoryProtection.NoAccess, nint.Zero, 0); - if (ptr == IntPtr.Zero) + if (ptr == nint.Zero) { throw new SystemException(Marshal.GetLastPInvokeErrorMessage()); } @@ -55,15 +55,15 @@ namespace Ryujinx.Memory return ptr; } - public static void Commit(IntPtr location, IntPtr size) + public static void Commit(nint location, nint size) { - if (WindowsApi.VirtualAlloc(location, size, AllocationType.Commit, MemoryProtection.ReadWrite) == IntPtr.Zero) + if (WindowsApi.VirtualAlloc(location, size, AllocationType.Commit, MemoryProtection.ReadWrite) == nint.Zero) { throw new SystemException(Marshal.GetLastPInvokeErrorMessage()); } } - public static void Decommit(IntPtr location, IntPtr size) + public static void Decommit(nint location, nint size) { if (!WindowsApi.VirtualFree(location, size, AllocationType.Decommit)) { @@ -71,17 +71,17 @@ namespace Ryujinx.Memory } } - public static void MapView(IntPtr sharedMemory, ulong srcOffset, IntPtr location, IntPtr size, MemoryBlock owner) + public static void MapView(nint sharedMemory, ulong srcOffset, nint location, nint size, MemoryBlock owner) { _placeholders.MapView(sharedMemory, srcOffset, location, size, owner); } - public static void UnmapView(IntPtr sharedMemory, IntPtr location, IntPtr size, MemoryBlock owner) + public static void UnmapView(nint sharedMemory, nint location, nint size, MemoryBlock owner) { _placeholders.UnmapView(sharedMemory, location, size, owner); } - public static bool Reprotect(IntPtr address, IntPtr size, MemoryPermission permission, bool forView) + public static bool Reprotect(nint address, nint size, MemoryPermission permission, bool forView) { if (forView) { @@ -93,26 +93,26 @@ namespace Ryujinx.Memory } } - public static bool Free(IntPtr address, IntPtr size) + public static bool Free(nint address, nint size) { _placeholders.UnreserveRange((ulong)address, (ulong)size); - return WindowsApi.VirtualFree(address, IntPtr.Zero, AllocationType.Release); + return WindowsApi.VirtualFree(address, nint.Zero, AllocationType.Release); } - public static IntPtr CreateSharedMemory(IntPtr size, bool reserve) + public static nint CreateSharedMemory(nint size, bool reserve) { var prot = reserve ? FileMapProtection.SectionReserve : FileMapProtection.SectionCommit; - IntPtr handle = WindowsApi.CreateFileMapping( + nint handle = WindowsApi.CreateFileMapping( WindowsApi.InvalidHandleValue, - IntPtr.Zero, + nint.Zero, FileMapProtection.PageReadWrite | prot, (uint)(size.ToInt64() >> 32), (uint)size.ToInt64(), null); - if (handle == IntPtr.Zero) + if (handle == nint.Zero) { throw new SystemException(Marshal.GetLastPInvokeErrorMessage()); } @@ -120,7 +120,7 @@ namespace Ryujinx.Memory return handle; } - public static void DestroySharedMemory(IntPtr handle) + public static void DestroySharedMemory(nint handle) { if (!WindowsApi.CloseHandle(handle)) { @@ -128,11 +128,11 @@ namespace Ryujinx.Memory } } - public static IntPtr MapSharedMemory(IntPtr handle) + public static nint MapSharedMemory(nint handle) { - IntPtr ptr = WindowsApi.MapViewOfFile(handle, 4 | 2, 0, 0, IntPtr.Zero); + nint ptr = WindowsApi.MapViewOfFile(handle, 4 | 2, 0, 0, nint.Zero); - if (ptr == IntPtr.Zero) + if (ptr == nint.Zero) { throw new SystemException(Marshal.GetLastPInvokeErrorMessage()); } @@ -140,7 +140,7 @@ namespace Ryujinx.Memory return ptr; } - public static void UnmapSharedMemory(IntPtr address) + public static void UnmapSharedMemory(nint address) { if (!WindowsApi.UnmapViewOfFile(address)) { diff --git a/src/Ryujinx.Memory/MemoryManagerUnixHelper.cs b/src/Ryujinx.Memory/MemoryManagerUnixHelper.cs index 6ed4d2387..e5a0b7a4d 100644 --- a/src/Ryujinx.Memory/MemoryManagerUnixHelper.cs +++ b/src/Ryujinx.Memory/MemoryManagerUnixHelper.cs @@ -6,7 +6,6 @@ namespace Ryujinx.Memory { [SupportedOSPlatform("linux")] [SupportedOSPlatform("macos")] - [SupportedOSPlatform("ios")] public static partial class MemoryManagerUnixHelper { [Flags] @@ -45,7 +44,7 @@ namespace Ryujinx.Memory O_SYNC = 256, } - public const IntPtr MAP_FAILED = -1; + public const nint MAP_FAILED = -1; private const int MAP_ANONYMOUS_LINUX_GENERIC = 0x20; private const int MAP_NORESERVE_LINUX_GENERIC = 0x4000; @@ -58,37 +57,37 @@ namespace Ryujinx.Memory public const int MADV_REMOVE = 9; [LibraryImport("libc", EntryPoint = "mmap", SetLastError = true)] - private static partial IntPtr Internal_mmap(IntPtr address, ulong length, MmapProts prot, int flags, int fd, long offset); + private static partial nint Internal_mmap(nint address, ulong length, MmapProts prot, int flags, int fd, long offset); [LibraryImport("libc", SetLastError = true)] - public static partial int mprotect(IntPtr address, ulong length, MmapProts prot); + public static partial int mprotect(nint address, ulong length, MmapProts prot); [LibraryImport("libc", SetLastError = true)] - public static partial int munmap(IntPtr address, ulong length); + public static partial int munmap(nint address, ulong length); [LibraryImport("libc", SetLastError = true)] - public static partial IntPtr mremap(IntPtr old_address, ulong old_size, ulong new_size, int flags, IntPtr new_address); + public static partial nint mremap(nint old_address, ulong old_size, ulong new_size, int flags, nint new_address); [LibraryImport("libc", SetLastError = true)] - public static partial int madvise(IntPtr address, ulong size, int advice); + public static partial int madvise(nint address, ulong size, int advice); [LibraryImport("libc", SetLastError = true)] - public static partial int mkstemp(IntPtr template); + public static partial int mkstemp(nint template); [LibraryImport("libc", SetLastError = true)] - public static partial int unlink(IntPtr pathname); + public static partial int unlink(nint pathname); [LibraryImport("libc", SetLastError = true)] - public static partial int ftruncate(int fildes, IntPtr length); + public static partial int ftruncate(int fildes, nint length); [LibraryImport("libc", SetLastError = true)] public static partial int close(int fd); [LibraryImport("libc", SetLastError = true)] - public static partial int shm_open(IntPtr name, int oflag, uint mode); + public static partial int shm_open(nint name, int oflag, uint mode); [LibraryImport("libc", SetLastError = true)] - public static partial int shm_unlink(IntPtr name); + public static partial int shm_unlink(nint name); private static int MmapFlagsToSystemFlags(MmapFlags flags) { @@ -115,7 +114,7 @@ namespace Ryujinx.Memory { result |= MAP_ANONYMOUS_LINUX_GENERIC; } - else if (OperatingSystem.IsMacOS() || OperatingSystem.IsIOS()) + else if (OperatingSystem.IsMacOS()) { result |= MAP_ANONYMOUS_DARWIN; } @@ -131,7 +130,7 @@ namespace Ryujinx.Memory { result |= MAP_NORESERVE_LINUX_GENERIC; } - else if (OperatingSystem.IsMacOS() || OperatingSystem.IsIOS()) + else if (OperatingSystem.IsMacOS()) { result |= MAP_NORESERVE_DARWIN; } @@ -147,7 +146,7 @@ namespace Ryujinx.Memory { result |= MAP_UNLOCKED_LINUX_GENERIC; } - else if (OperatingSystem.IsMacOS() || OperatingSystem.IsIOS()) + else if (OperatingSystem.IsMacOS()) { // FIXME: Doesn't exist on Darwin } @@ -157,7 +156,7 @@ namespace Ryujinx.Memory } } - if (flags.HasFlag(MmapFlags.MAP_JIT_DARWIN) && (OperatingSystem.IsIOS() || OperatingSystem.IsMacOSVersionAtLeast(10, 14))) + if (flags.HasFlag(MmapFlags.MAP_JIT_DARWIN) && OperatingSystem.IsMacOSVersionAtLeast(10, 14)) { result |= (int)MmapFlags.MAP_JIT_DARWIN; } @@ -165,7 +164,7 @@ namespace Ryujinx.Memory return result; } - public static IntPtr Mmap(IntPtr address, ulong length, MmapProts prot, MmapFlags flags, int fd, long offset) + public static nint Mmap(nint address, ulong length, MmapProts prot, MmapFlags flags, int fd, long offset) { return Internal_mmap(address, length, prot, MmapFlagsToSystemFlags(flags), fd, offset); } diff --git a/src/Ryujinx.Memory/NativeMemoryManager.cs b/src/Ryujinx.Memory/NativeMemoryManager.cs index fe718bda8..cb8d5c243 100644 --- a/src/Ryujinx.Memory/NativeMemoryManager.cs +++ b/src/Ryujinx.Memory/NativeMemoryManager.cs @@ -8,12 +8,21 @@ namespace Ryujinx.Memory private readonly T* _pointer; private readonly int _length; + public NativeMemoryManager(nuint pointer, int length) + : this((T*)pointer, length) + { + } + public NativeMemoryManager(T* pointer, int length) { _pointer = pointer; _length = length; } + public unsafe T* Pointer => _pointer; + + public int Length => _length; + public override Span GetSpan() { return new Span((void*)_pointer, _length); diff --git a/src/Ryujinx.Memory/Range/IMultiRangeItem.cs b/src/Ryujinx.Memory/Range/IMultiRangeItem.cs index 87fde2465..5f9611c75 100644 --- a/src/Ryujinx.Memory/Range/IMultiRangeItem.cs +++ b/src/Ryujinx.Memory/Range/IMultiRangeItem.cs @@ -4,6 +4,22 @@ namespace Ryujinx.Memory.Range { MultiRange Range { get; } - ulong BaseAddress => Range.GetSubRange(0).Address; + ulong BaseAddress + { + get + { + for (int index = 0; index < Range.Count; index++) + { + MemoryRange subRange = Range.GetSubRange(index); + + if (!MemoryRange.IsInvalid(ref subRange)) + { + return subRange.Address; + } + } + + return MemoryRange.InvalidAddress; + } + } } } diff --git a/src/Ryujinx.Memory/Range/MemoryRange.cs b/src/Ryujinx.Memory/Range/MemoryRange.cs index 46aca9ba0..20e9d00bb 100644 --- a/src/Ryujinx.Memory/Range/MemoryRange.cs +++ b/src/Ryujinx.Memory/Range/MemoryRange.cs @@ -5,6 +5,11 @@ namespace Ryujinx.Memory.Range /// public readonly record struct MemoryRange { + /// + /// Special address value used to indicate than an address is invalid. + /// + internal const ulong InvalidAddress = ulong.MaxValue; + /// /// An empty memory range, with a null address and zero size. /// @@ -58,13 +63,24 @@ namespace Ryujinx.Memory.Range return thisAddress < otherEndAddress && otherAddress < thisEndAddress; } + /// + /// Checks if a given sub-range of memory is invalid. + /// Those are used to represent unmapped memory regions (holes in the region mapping). + /// + /// Memory range to check + /// True if the memory range is considered invalid, false otherwise + internal static bool IsInvalid(ref MemoryRange subRange) + { + return subRange.Address == InvalidAddress; + } + /// /// Returns a string summary of the memory range. /// /// A string summary of the memory range public override string ToString() { - if (Address == ulong.MaxValue) + if (Address == InvalidAddress) { return $"[Unmapped 0x{Size:X}]"; } diff --git a/src/Ryujinx.Memory/Range/MultiRangeList.cs b/src/Ryujinx.Memory/Range/MultiRangeList.cs index 1804ff5c8..c3c6ae797 100644 --- a/src/Ryujinx.Memory/Range/MultiRangeList.cs +++ b/src/Ryujinx.Memory/Range/MultiRangeList.cs @@ -30,7 +30,7 @@ namespace Ryujinx.Memory.Range { var subrange = range.GetSubRange(i); - if (IsInvalid(ref subrange)) + if (MemoryRange.IsInvalid(ref subrange)) { continue; } @@ -56,7 +56,7 @@ namespace Ryujinx.Memory.Range { var subrange = range.GetSubRange(i); - if (IsInvalid(ref subrange)) + if (MemoryRange.IsInvalid(ref subrange)) { continue; } @@ -99,7 +99,7 @@ namespace Ryujinx.Memory.Range { var subrange = range.GetSubRange(i); - if (IsInvalid(ref subrange)) + if (MemoryRange.IsInvalid(ref subrange)) { continue; } @@ -142,17 +142,6 @@ namespace Ryujinx.Memory.Range return overlapCount; } - /// - /// Checks if a given sub-range of memory is invalid. - /// Those are used to represent unmapped memory regions (holes in the region mapping). - /// - /// Memory range to checl - /// True if the memory range is considered invalid, false otherwise - private static bool IsInvalid(ref MemoryRange subRange) - { - return subRange.Address == ulong.MaxValue; - } - /// /// Gets all items on the list starting at the specified memory address. /// diff --git a/src/Ryujinx.Memory/SparseMemoryBlock.cs b/src/Ryujinx.Memory/SparseMemoryBlock.cs new file mode 100644 index 000000000..523685de1 --- /dev/null +++ b/src/Ryujinx.Memory/SparseMemoryBlock.cs @@ -0,0 +1,125 @@ +using Ryujinx.Common; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; + +namespace Ryujinx.Memory +{ + public delegate void PageInitDelegate(Span page); + + public class SparseMemoryBlock : IDisposable + { + private const ulong MapGranularity = 1UL << 17; + + private readonly PageInitDelegate _pageInit; + + private readonly object _lock = new object(); + private readonly ulong _pageSize; + private readonly MemoryBlock _reservedBlock; + private readonly List _mappedBlocks; + private ulong _mappedBlockUsage; + + private readonly ulong[] _mappedPageBitmap; + + public MemoryBlock Block => _reservedBlock; + + public SparseMemoryBlock(ulong size, PageInitDelegate pageInit, MemoryBlock fill) + { + _pageSize = MemoryBlock.GetPageSize(); + _reservedBlock = new MemoryBlock(size, MemoryAllocationFlags.Reserve | MemoryAllocationFlags.ViewCompatible); + _mappedBlocks = new List(); + _pageInit = pageInit; + + int pages = (int)BitUtils.DivRoundUp(size, _pageSize); + int bitmapEntries = BitUtils.DivRoundUp(pages, 64); + _mappedPageBitmap = new ulong[bitmapEntries]; + + if (fill != null) + { + // Fill the block with mappings from the fill block. + + if (fill.Size % _pageSize != 0) + { + throw new ArgumentException("Fill memory block should be page aligned.", nameof(fill)); + } + + int repeats = (int)BitUtils.DivRoundUp(size, fill.Size); + + ulong offset = 0; + for (int i = 0; i < repeats; i++) + { + _reservedBlock.MapView(fill, 0, offset, Math.Min(fill.Size, size - offset)); + offset += fill.Size; + } + } + + // If a fill block isn't provided, the pages that aren't EnsureMapped are unmapped. + // The caller can rely on signal handler to fill empty pages instead. + } + + private void MapPage(ulong pageOffset) + { + // Take a page from the latest mapped block. + MemoryBlock block = _mappedBlocks.LastOrDefault(); + + if (block == null || _mappedBlockUsage == MapGranularity) + { + // Need to map some more memory. + + block = new MemoryBlock(MapGranularity, MemoryAllocationFlags.Mirrorable); + + _mappedBlocks.Add(block); + + _mappedBlockUsage = 0; + } + + _pageInit(block.GetSpan(_mappedBlockUsage, (int)_pageSize)); + _reservedBlock.MapView(block, _mappedBlockUsage, pageOffset, _pageSize); + + _mappedBlockUsage += _pageSize; + } + + public void EnsureMapped(ulong offset) + { + int pageIndex = (int)(offset / _pageSize); + int bitmapIndex = pageIndex >> 6; + + ref ulong entry = ref _mappedPageBitmap[bitmapIndex]; + ulong bit = 1UL << (pageIndex & 63); + + if ((Volatile.Read(ref entry) & bit) == 0) + { + // Not mapped. + + lock (_lock) + { + // Check the bit while locked to make sure that this only happens once. + + ulong lockedEntry = Volatile.Read(ref entry); + + if ((lockedEntry & bit) == 0) + { + MapPage(offset & ~(_pageSize - 1)); + + lockedEntry |= bit; + + Interlocked.Exchange(ref entry, lockedEntry); + } + } + } + } + + public void Dispose() + { + _reservedBlock.Dispose(); + + foreach (MemoryBlock block in _mappedBlocks) + { + block.Dispose(); + } + + GC.SuppressFinalize(this); + } + } +} diff --git a/src/Ryujinx.Memory/Tracking/MemoryTracking.cs b/src/Ryujinx.Memory/Tracking/MemoryTracking.cs index 6febcbbb3..96cb2c5f5 100644 --- a/src/Ryujinx.Memory/Tracking/MemoryTracking.cs +++ b/src/Ryujinx.Memory/Tracking/MemoryTracking.cs @@ -14,9 +14,14 @@ namespace Ryujinx.Memory.Tracking // Only use these from within the lock. private readonly NonOverlappingRangeList _virtualRegions; + // Guest virtual regions are a subset of the normal virtual regions, with potentially different protection + // and expanded area of effect on platforms that don't support misaligned page protection. + private readonly NonOverlappingRangeList _guestVirtualRegions; private readonly int _pageSize; + private readonly bool _singleByteGuestTracking; + /// /// This lock must be obtained when traversing or updating the region-handle hierarchy. /// It is not required when reading dirty flags. @@ -27,16 +32,27 @@ namespace Ryujinx.Memory.Tracking /// Create a new tracking structure for the given "physical" memory block, /// with a given "virtual" memory manager that will provide mappings and virtual memory protection. /// + /// + /// If is true, the memory manager must also support protection on partially + /// unmapped regions without throwing exceptions or dropping protection on the mapped portion. + /// /// Virtual memory manager - /// Physical memory block /// Page size of the virtual memory space - public MemoryTracking(IVirtualMemoryManager memoryManager, int pageSize, InvalidAccessHandler invalidAccessHandler = null) + /// Method to call for invalid memory accesses + /// True if the guest only signals writes for the first byte + public MemoryTracking( + IVirtualMemoryManager memoryManager, + int pageSize, + InvalidAccessHandler invalidAccessHandler = null, + bool singleByteGuestTracking = false) { _memoryManager = memoryManager; _pageSize = pageSize; _invalidAccessHandler = invalidAccessHandler; + _singleByteGuestTracking = singleByteGuestTracking; _virtualRegions = new NonOverlappingRangeList(); + _guestVirtualRegions = new NonOverlappingRangeList(); } private (ulong address, ulong size) PageAlign(ulong address, ulong size) @@ -62,20 +78,25 @@ namespace Ryujinx.Memory.Tracking { ref var overlaps = ref ThreadStaticArray.Get(); - int count = _virtualRegions.FindOverlapsNonOverlapping(va, size, ref overlaps); - - for (int i = 0; i < count; i++) + for (int type = 0; type < 2; type++) { - VirtualRegion region = overlaps[i]; + NonOverlappingRangeList regions = type == 0 ? _virtualRegions : _guestVirtualRegions; - // If the region has been fully remapped, signal that it has been mapped again. - bool remapped = _memoryManager.IsRangeMapped(region.Address, region.Size); - if (remapped) + int count = regions.FindOverlapsNonOverlapping(va, size, ref overlaps); + + for (int i = 0; i < count; i++) { - region.SignalMappingChanged(true); - } + VirtualRegion region = overlaps[i]; - region.UpdateProtection(); + // If the region has been fully remapped, signal that it has been mapped again. + bool remapped = _memoryManager.IsRangeMapped(region.Address, region.Size); + if (remapped) + { + region.SignalMappingChanged(true); + } + + region.UpdateProtection(); + } } } } @@ -95,27 +116,58 @@ namespace Ryujinx.Memory.Tracking { ref var overlaps = ref ThreadStaticArray.Get(); - int count = _virtualRegions.FindOverlapsNonOverlapping(va, size, ref overlaps); - - for (int i = 0; i < count; i++) + for (int type = 0; type < 2; type++) { - VirtualRegion region = overlaps[i]; + NonOverlappingRangeList regions = type == 0 ? _virtualRegions : _guestVirtualRegions; - region.SignalMappingChanged(false); + int count = regions.FindOverlapsNonOverlapping(va, size, ref overlaps); + + for (int i = 0; i < count; i++) + { + VirtualRegion region = overlaps[i]; + + region.SignalMappingChanged(false); + } } } } + /// + /// Alter a tracked memory region to properly capture unaligned accesses. + /// For most memory manager modes, this does nothing. + /// + /// Original region address + /// Original region size + /// A new address and size for tracking unaligned accesses + internal (ulong newAddress, ulong newSize) GetUnalignedSafeRegion(ulong address, ulong size) + { + if (_singleByteGuestTracking) + { + // The guest only signals the first byte of each memory access with the current memory manager. + // To catch unaligned access properly, we need to also protect the page before the address. + + // Assume that the address and size are already aligned. + + return (address - (ulong)_pageSize, size + (ulong)_pageSize); + } + else + { + return (address, size); + } + } + /// /// Get a list of virtual regions that a handle covers. /// /// Starting virtual memory address of the handle /// Size of the handle's memory region + /// True if getting handles for guest protection, false otherwise /// A list of virtual regions within the given range - internal List GetVirtualRegionsForHandle(ulong va, ulong size) + internal List GetVirtualRegionsForHandle(ulong va, ulong size, bool guest) { List result = new(); - _virtualRegions.GetOrAddRegions(result, va, size, (va, size) => new VirtualRegion(this, va, size)); + NonOverlappingRangeList regions = guest ? _guestVirtualRegions : _virtualRegions; + regions.GetOrAddRegions(result, va, size, (va, size) => new VirtualRegion(this, va, size, guest)); return result; } @@ -126,7 +178,14 @@ namespace Ryujinx.Memory.Tracking /// Region to remove internal void RemoveVirtual(VirtualRegion region) { - _virtualRegions.Remove(region); + if (region.Guest) + { + _guestVirtualRegions.Remove(region); + } + else + { + _virtualRegions.Remove(region); + } } /// @@ -137,10 +196,11 @@ namespace Ryujinx.Memory.Tracking /// Handles to inherit state from or reuse. When none are present, provide null /// Desired granularity of write tracking /// Handle ID + /// Region flags /// The memory tracking handle - public MultiRegionHandle BeginGranularTracking(ulong address, ulong size, IEnumerable handles, ulong granularity, int id) + public MultiRegionHandle BeginGranularTracking(ulong address, ulong size, IEnumerable handles, ulong granularity, int id, RegionFlags flags = RegionFlags.None) { - return new MultiRegionHandle(this, address, size, handles, granularity, id); + return new MultiRegionHandle(this, address, size, handles, granularity, id, flags); } /// @@ -164,15 +224,16 @@ namespace Ryujinx.Memory.Tracking /// CPU virtual address of the region /// Size of the region /// Handle ID + /// Region flags /// The memory tracking handle - public RegionHandle BeginTracking(ulong address, ulong size, int id) + public RegionHandle BeginTracking(ulong address, ulong size, int id, RegionFlags flags = RegionFlags.None) { var (paAddress, paSize) = PageAlign(address, size); lock (TrackingLock) { bool mapped = _memoryManager.IsRangeMapped(address, size); - RegionHandle handle = new(this, paAddress, paSize, address, size, id, mapped); + RegionHandle handle = new(this, paAddress, paSize, address, size, id, flags, mapped); return handle; } @@ -186,15 +247,16 @@ namespace Ryujinx.Memory.Tracking /// The bitmap owning the dirty flag for this handle /// The bit of this handle within the dirty flag /// Handle ID + /// Region flags /// The memory tracking handle - internal RegionHandle BeginTrackingBitmap(ulong address, ulong size, ConcurrentBitmap bitmap, int bit, int id) + internal RegionHandle BeginTrackingBitmap(ulong address, ulong size, ConcurrentBitmap bitmap, int bit, int id, RegionFlags flags = RegionFlags.None) { var (paAddress, paSize) = PageAlign(address, size); lock (TrackingLock) { bool mapped = _memoryManager.IsRangeMapped(address, size); - RegionHandle handle = new(this, paAddress, paSize, address, size, bitmap, bit, id, mapped); + RegionHandle handle = new(this, paAddress, paSize, address, size, bitmap, bit, id, flags, mapped); return handle; } @@ -202,6 +264,7 @@ namespace Ryujinx.Memory.Tracking /// /// Signal that a virtual memory event happened at the given location. + /// The memory event is assumed to be triggered by guest code. /// /// Virtual address accessed /// Size of the region affected in bytes @@ -209,7 +272,7 @@ namespace Ryujinx.Memory.Tracking /// True if the event triggered any tracking regions, false otherwise public bool VirtualMemoryEvent(ulong address, ulong size, bool write) { - return VirtualMemoryEvent(address, size, write, precise: false, null); + return VirtualMemoryEvent(address, size, write, precise: false, exemptId: null, guest: true); } /// @@ -222,8 +285,9 @@ namespace Ryujinx.Memory.Tracking /// Whether the region was written to or read /// True if the access is precise, false otherwise /// Optional ID that of the handles that should not be signalled + /// True if the access is from the guest, false otherwise /// True if the event triggered any tracking regions, false otherwise - public bool VirtualMemoryEvent(ulong address, ulong size, bool write, bool precise, int? exemptId = null) + public bool VirtualMemoryEvent(ulong address, ulong size, bool write, bool precise, int? exemptId = null, bool guest = false) { // Look up the virtual region using the region list. // Signal up the chain to relevant handles. @@ -234,7 +298,9 @@ namespace Ryujinx.Memory.Tracking { ref var overlaps = ref ThreadStaticArray.Get(); - int count = _virtualRegions.FindOverlapsNonOverlapping(address, size, ref overlaps); + NonOverlappingRangeList regions = guest ? _guestVirtualRegions : _virtualRegions; + + int count = regions.FindOverlapsNonOverlapping(address, size, ref overlaps); if (count == 0 && !precise) { @@ -242,7 +308,7 @@ namespace Ryujinx.Memory.Tracking { // TODO: There is currently the possibility that a page can be protected after its virtual region is removed. // This code handles that case when it happens, but it would be better to find out how this happens. - _memoryManager.TrackingReprotect(address & ~(ulong)(_pageSize - 1), (ulong)_pageSize, MemoryPermission.ReadAndWrite); + _memoryManager.TrackingReprotect(address & ~(ulong)(_pageSize - 1), (ulong)_pageSize, MemoryPermission.ReadAndWrite, guest); return true; // This memory _should_ be mapped, so we need to try again. } else @@ -252,6 +318,12 @@ namespace Ryujinx.Memory.Tracking } else { + if (guest && _singleByteGuestTracking) + { + // Increase the access size to trigger handles with misaligned accesses. + size += (ulong)_pageSize; + } + for (int i = 0; i < count; i++) { VirtualRegion region = overlaps[i]; @@ -285,9 +357,10 @@ namespace Ryujinx.Memory.Tracking /// /// Region to reprotect /// Memory permission to protect with - internal void ProtectVirtualRegion(VirtualRegion region, MemoryPermission permission) + /// True if the protection is for guest access, false otherwise + internal void ProtectVirtualRegion(VirtualRegion region, MemoryPermission permission, bool guest) { - _memoryManager.TrackingReprotect(region.Address, region.Size, permission); + _memoryManager.TrackingReprotect(region.Address, region.Size, permission, guest); } /// diff --git a/src/Ryujinx.Memory/Tracking/MultiRegionHandle.cs b/src/Ryujinx.Memory/Tracking/MultiRegionHandle.cs index 1c1b48b3c..6fdca69f5 100644 --- a/src/Ryujinx.Memory/Tracking/MultiRegionHandle.cs +++ b/src/Ryujinx.Memory/Tracking/MultiRegionHandle.cs @@ -37,7 +37,8 @@ namespace Ryujinx.Memory.Tracking ulong size, IEnumerable handles, ulong granularity, - int id) + int id, + RegionFlags flags) { _handles = new RegionHandle[(size + granularity - 1) / granularity]; Granularity = granularity; @@ -62,7 +63,7 @@ namespace Ryujinx.Memory.Tracking // Fill any gap left before this handle. while (i < startIndex) { - RegionHandle fillHandle = tracking.BeginTrackingBitmap(address + (ulong)i * granularity, granularity, _dirtyBitmap, i, id); + RegionHandle fillHandle = tracking.BeginTrackingBitmap(address + (ulong)i * granularity, granularity, _dirtyBitmap, i, id, flags); fillHandle.Parent = this; _handles[i++] = fillHandle; } @@ -83,7 +84,7 @@ namespace Ryujinx.Memory.Tracking while (i < endIndex) { - RegionHandle splitHandle = tracking.BeginTrackingBitmap(address + (ulong)i * granularity, granularity, _dirtyBitmap, i, id); + RegionHandle splitHandle = tracking.BeginTrackingBitmap(address + (ulong)i * granularity, granularity, _dirtyBitmap, i, id, flags); splitHandle.Parent = this; splitHandle.Reprotect(handle.Dirty); @@ -106,7 +107,7 @@ namespace Ryujinx.Memory.Tracking // Fill any remaining space with new handles. while (i < _handles.Length) { - RegionHandle handle = tracking.BeginTrackingBitmap(address + (ulong)i * granularity, granularity, _dirtyBitmap, i, id); + RegionHandle handle = tracking.BeginTrackingBitmap(address + (ulong)i * granularity, granularity, _dirtyBitmap, i, id, flags); handle.Parent = this; _handles[i++] = handle; } diff --git a/src/Ryujinx.Memory/Tracking/RegionFlags.cs b/src/Ryujinx.Memory/Tracking/RegionFlags.cs new file mode 100644 index 000000000..ceb8e56a6 --- /dev/null +++ b/src/Ryujinx.Memory/Tracking/RegionFlags.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Ryujinx.Memory.Tracking +{ + [Flags] + public enum RegionFlags + { + None = 0, + + /// + /// Access to the resource is expected to occasionally be unaligned. + /// With some memory managers, guest protection must extend into the previous page to cover unaligned access. + /// If this is not expected, protection is not altered, which can avoid unintended resource dirty/flush. + /// + UnalignedAccess = 1, + } +} diff --git a/src/Ryujinx.Memory/Tracking/RegionHandle.cs b/src/Ryujinx.Memory/Tracking/RegionHandle.cs index df3d9c311..a94ffa43c 100644 --- a/src/Ryujinx.Memory/Tracking/RegionHandle.cs +++ b/src/Ryujinx.Memory/Tracking/RegionHandle.cs @@ -55,6 +55,8 @@ namespace Ryujinx.Memory.Tracking private RegionSignal _preAction; // Action to perform before a read or write. This will block the memory access. private PreciseRegionSignal _preciseAction; // Action to perform on a precise read or write. private readonly List _regions; + private readonly List _guestRegions; + private readonly List _allRegions; private readonly MemoryTracking _tracking; private bool _disposed; @@ -99,6 +101,7 @@ namespace Ryujinx.Memory.Tracking /// The bitmap the dirty flag for this handle is stored in /// The bit index representing the dirty flag for this handle /// Handle ID + /// Region flags /// True if the region handle starts mapped internal RegionHandle( MemoryTracking tracking, @@ -109,6 +112,7 @@ namespace Ryujinx.Memory.Tracking ConcurrentBitmap bitmap, int bit, int id, + RegionFlags flags, bool mapped = true) { Bitmap = bitmap; @@ -128,11 +132,12 @@ namespace Ryujinx.Memory.Tracking RealEndAddress = realAddress + realSize; _tracking = tracking; - _regions = tracking.GetVirtualRegionsForHandle(address, size); - foreach (var region in _regions) - { - region.Handles.Add(this); - } + + _regions = tracking.GetVirtualRegionsForHandle(address, size, false); + _guestRegions = GetGuestRegions(tracking, address, size, flags); + _allRegions = new List(_regions.Count + _guestRegions.Count); + + InitializeRegions(); } /// @@ -145,8 +150,9 @@ namespace Ryujinx.Memory.Tracking /// The real, unaligned address of the handle /// The real, unaligned size of the handle /// Handle ID + /// Region flags /// True if the region handle starts mapped - internal RegionHandle(MemoryTracking tracking, ulong address, ulong size, ulong realAddress, ulong realSize, int id, bool mapped = true) + internal RegionHandle(MemoryTracking tracking, ulong address, ulong size, ulong realAddress, ulong realSize, int id, RegionFlags flags, bool mapped = true) { Bitmap = new ConcurrentBitmap(1, mapped); @@ -163,8 +169,37 @@ namespace Ryujinx.Memory.Tracking RealEndAddress = realAddress + realSize; _tracking = tracking; - _regions = tracking.GetVirtualRegionsForHandle(address, size); - foreach (var region in _regions) + + _regions = tracking.GetVirtualRegionsForHandle(address, size, false); + _guestRegions = GetGuestRegions(tracking, address, size, flags); + _allRegions = new List(_regions.Count + _guestRegions.Count); + + InitializeRegions(); + } + + private List GetGuestRegions(MemoryTracking tracking, ulong address, ulong size, RegionFlags flags) + { + ulong guestAddress; + ulong guestSize; + + if (flags.HasFlag(RegionFlags.UnalignedAccess)) + { + (guestAddress, guestSize) = tracking.GetUnalignedSafeRegion(address, size); + } + else + { + (guestAddress, guestSize) = (address, size); + } + + return tracking.GetVirtualRegionsForHandle(guestAddress, guestSize, true); + } + + private void InitializeRegions() + { + _allRegions.AddRange(_regions); + _allRegions.AddRange(_guestRegions); + + foreach (var region in _allRegions) { region.Handles.Add(this); } @@ -321,7 +356,7 @@ namespace Ryujinx.Memory.Tracking lock (_tracking.TrackingLock) { - foreach (VirtualRegion region in _regions) + foreach (VirtualRegion region in _allRegions) { protectionChanged |= region.UpdateProtection(); } @@ -379,7 +414,7 @@ namespace Ryujinx.Memory.Tracking { lock (_tracking.TrackingLock) { - foreach (VirtualRegion region in _regions) + foreach (VirtualRegion region in _allRegions) { region.UpdateProtection(); } @@ -414,7 +449,16 @@ namespace Ryujinx.Memory.Tracking /// Virtual region to add as a child internal void AddChild(VirtualRegion region) { - _regions.Add(region); + if (region.Guest) + { + _guestRegions.Add(region); + } + else + { + _regions.Add(region); + } + + _allRegions.Add(region); } /// @@ -469,7 +513,7 @@ namespace Ryujinx.Memory.Tracking lock (_tracking.TrackingLock) { - foreach (VirtualRegion region in _regions) + foreach (VirtualRegion region in _allRegions) { region.RemoveHandle(this); } diff --git a/src/Ryujinx.Memory/Tracking/VirtualRegion.cs b/src/Ryujinx.Memory/Tracking/VirtualRegion.cs index 538e94fef..35e9c2d9b 100644 --- a/src/Ryujinx.Memory/Tracking/VirtualRegion.cs +++ b/src/Ryujinx.Memory/Tracking/VirtualRegion.cs @@ -13,10 +13,14 @@ namespace Ryujinx.Memory.Tracking private readonly MemoryTracking _tracking; private MemoryPermission _lastPermission; - public VirtualRegion(MemoryTracking tracking, ulong address, ulong size, MemoryPermission lastPermission = MemoryPermission.Invalid) : base(address, size) + public bool Guest { get; } + + public VirtualRegion(MemoryTracking tracking, ulong address, ulong size, bool guest, MemoryPermission lastPermission = MemoryPermission.Invalid) : base(address, size) { _lastPermission = lastPermission; _tracking = tracking; + + Guest = guest; } /// @@ -66,9 +70,12 @@ namespace Ryujinx.Memory.Tracking { _lastPermission = MemoryPermission.Invalid; - foreach (RegionHandle handle in Handles) + if (!Guest) { - handle.SignalMappingChanged(mapped); + foreach (RegionHandle handle in Handles) + { + handle.SignalMappingChanged(mapped); + } } } @@ -103,7 +110,7 @@ namespace Ryujinx.Memory.Tracking if (_lastPermission != permission) { - _tracking.ProtectVirtualRegion(this, permission); + _tracking.ProtectVirtualRegion(this, permission, Guest); _lastPermission = permission; return true; @@ -131,7 +138,7 @@ namespace Ryujinx.Memory.Tracking public override INonOverlappingRange Split(ulong splitAddress) { - VirtualRegion newRegion = new(_tracking, splitAddress, EndAddress - splitAddress, _lastPermission); + VirtualRegion newRegion = new(_tracking, splitAddress, EndAddress - splitAddress, Guest, _lastPermission); Size = splitAddress - Address; // The new region inherits all of our parents. diff --git a/src/Ryujinx.Memory/VirtualMemoryManagerBase.cs b/src/Ryujinx.Memory/VirtualMemoryManagerBase.cs new file mode 100644 index 000000000..f41072244 --- /dev/null +++ b/src/Ryujinx.Memory/VirtualMemoryManagerBase.cs @@ -0,0 +1,405 @@ +using Ryujinx.Common.Memory; +using System; +using System.Buffers; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace Ryujinx.Memory +{ + public abstract class VirtualMemoryManagerBase : IWritableBlock + { + public const int PageBits = 12; + public const int PageSize = 1 << PageBits; + public const int PageMask = PageSize - 1; + + protected abstract ulong AddressSpaceSize { get; } + + public virtual ReadOnlySequence GetReadOnlySequence(ulong va, int size, bool tracked = false) + { + if (size == 0) + { + return ReadOnlySequence.Empty; + } + + if (tracked) + { + SignalMemoryTracking(va, (ulong)size, false); + } + + if (IsContiguousAndMapped(va, size)) + { + nuint pa = TranslateVirtualAddressUnchecked(va); + + return new ReadOnlySequence(GetPhysicalAddressMemory(pa, size)); + } + else + { + AssertValidAddressAndSize(va, size); + + int offset = 0, segmentSize; + + BytesReadOnlySequenceSegment first = null, last = null; + + if ((va & PageMask) != 0) + { + nuint pa = TranslateVirtualAddressChecked(va); + + segmentSize = Math.Min(size, PageSize - (int)(va & PageMask)); + + Memory memory = GetPhysicalAddressMemory(pa, segmentSize); + + first = last = new BytesReadOnlySequenceSegment(memory); + + offset += segmentSize; + } + + for (; offset < size; offset += segmentSize) + { + nuint pa = TranslateVirtualAddressChecked(va + (ulong)offset); + + segmentSize = Math.Min(size - offset, PageSize); + + Memory memory = GetPhysicalAddressMemory(pa, segmentSize); + + if (first is null) + { + first = last = new BytesReadOnlySequenceSegment(memory); + } + else + { + if (last.IsContiguousWith(memory, out nuint contiguousStart, out int contiguousSize)) + { + last.Replace(GetPhysicalAddressMemory(contiguousStart, contiguousSize)); + } + else + { + last = last.Append(memory); + } + } + } + + return new ReadOnlySequence(first, 0, last, (int)(size - last.RunningIndex)); + } + } + + public virtual ReadOnlySpan GetSpan(ulong va, int size, bool tracked = false) + { + if (size == 0) + { + return ReadOnlySpan.Empty; + } + + if (tracked) + { + SignalMemoryTracking(va, (ulong)size, false); + } + + if (IsContiguousAndMapped(va, size)) + { + nuint pa = TranslateVirtualAddressUnchecked(va); + + return GetPhysicalAddressSpan(pa, size); + } + else + { + Span data = new byte[size]; + + Read(va, data); + + return data; + } + } + + public virtual WritableRegion GetWritableRegion(ulong va, int size, bool tracked = false) + { + if (size == 0) + { + return new WritableRegion(null, va, Memory.Empty); + } + + if (tracked) + { + SignalMemoryTracking(va, (ulong)size, true); + } + + if (IsContiguousAndMapped(va, size)) + { + nuint pa = TranslateVirtualAddressUnchecked(va); + + return new WritableRegion(null, va, GetPhysicalAddressMemory(pa, size)); + } + else + { + MemoryOwner memoryOwner = MemoryOwner.Rent(size); + + Read(va, memoryOwner.Span); + + return new WritableRegion(this, va, memoryOwner); + } + } + + public abstract bool IsMapped(ulong va); + + public virtual void MapForeign(ulong va, nuint hostPointer, ulong size) + { + throw new NotSupportedException(); + } + + public virtual T Read(ulong va) where T : unmanaged + { + return MemoryMarshal.Cast(GetSpan(va, Unsafe.SizeOf()))[0]; + } + + public virtual void Read(ulong va, Span data) + { + if (data.Length == 0) + { + return; + } + + AssertValidAddressAndSize(va, data.Length); + + int offset = 0, size; + + if ((va & PageMask) != 0) + { + nuint pa = TranslateVirtualAddressChecked(va); + + size = Math.Min(data.Length, PageSize - (int)(va & PageMask)); + + GetPhysicalAddressSpan(pa, size).CopyTo(data[..size]); + + offset += size; + } + + for (; offset < data.Length; offset += size) + { + nuint pa = TranslateVirtualAddressChecked(va + (ulong)offset); + + size = Math.Min(data.Length - offset, PageSize); + + GetPhysicalAddressSpan(pa, size).CopyTo(data.Slice(offset, size)); + } + } + + public virtual T ReadTracked(ulong va) where T : unmanaged + { + SignalMemoryTracking(va, (ulong)Unsafe.SizeOf(), false); + + return Read(va); + } + + public virtual void SignalMemoryTracking(ulong va, ulong size, bool write, bool precise = false, int? exemptId = null) + { + // No default implementation + } + + public virtual void Write(ulong va, ReadOnlySpan data) + { + if (data.Length == 0) + { + return; + } + + SignalMemoryTracking(va, (ulong)data.Length, true); + + WriteImpl(va, data); + } + + public virtual void Write(ulong va, T value) where T : unmanaged + { + Write(va, MemoryMarshal.Cast(MemoryMarshal.CreateSpan(ref value, 1))); + } + + public virtual void WriteUntracked(ulong va, ReadOnlySpan data) + { + if (data.Length == 0) + { + return; + } + + WriteImpl(va, data); + } + + public virtual bool WriteWithRedundancyCheck(ulong va, ReadOnlySpan data) + { + if (data.Length == 0) + { + return false; + } + + if (IsContiguousAndMapped(va, data.Length)) + { + SignalMemoryTracking(va, (ulong)data.Length, false); + + nuint pa = TranslateVirtualAddressChecked(va); + + var target = GetPhysicalAddressSpan(pa, data.Length); + + bool changed = !data.SequenceEqual(target); + + if (changed) + { + data.CopyTo(target); + } + + return changed; + } + else + { + Write(va, data); + + return true; + } + } + + /// + /// Ensures the combination of virtual address and size is part of the addressable space. + /// + /// Virtual address of the range + /// Size of the range in bytes + /// Throw when the memory region specified outside the addressable space + protected void AssertValidAddressAndSize(ulong va, ulong size) + { + if (!ValidateAddressAndSize(va, size)) + { + throw new InvalidMemoryRegionException($"va=0x{va:X16}, size=0x{size:X16}"); + } + } + + /// + /// Ensures the combination of virtual address and size is part of the addressable space. + /// + /// Virtual address of the range + /// Size of the range in bytes + /// Throw when the memory region specified outside the addressable space + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected void AssertValidAddressAndSize(ulong va, int size) + => AssertValidAddressAndSize(va, (ulong)size); + + /// + /// Computes the number of pages in a virtual address range. + /// + /// Virtual address of the range + /// Size of the range + /// The virtual address of the beginning of the first page + /// This function does not differentiate between allocated and unallocated pages. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected static int GetPagesCount(ulong va, ulong size, out ulong startVa) + { + // WARNING: Always check if ulong does not overflow during the operations. + startVa = va & ~(ulong)PageMask; + ulong vaSpan = (va - startVa + size + PageMask) & ~(ulong)PageMask; + + return (int)(vaSpan / PageSize); + } + + protected abstract Memory GetPhysicalAddressMemory(nuint pa, int size); + + protected abstract Span GetPhysicalAddressSpan(nuint pa, int size); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected bool IsContiguous(ulong va, int size) => IsContiguous(va, (ulong)size); + + protected virtual bool IsContiguous(ulong va, ulong size) + { + if (!ValidateAddress(va) || !ValidateAddressAndSize(va, size)) + { + return false; + } + + int pages = GetPagesCount(va, size, out va); + + for (int page = 0; page < pages - 1; page++) + { + if (!ValidateAddress(va + PageSize)) + { + return false; + } + + if (TranslateVirtualAddressUnchecked(va) + PageSize != TranslateVirtualAddressUnchecked(va + PageSize)) + { + return false; + } + + va += PageSize; + } + + return true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected bool IsContiguousAndMapped(ulong va, int size) + => IsContiguous(va, size) && IsMapped(va); + + protected abstract nuint TranslateVirtualAddressChecked(ulong va); + + protected abstract nuint TranslateVirtualAddressUnchecked(ulong va); + + /// + /// Checks if the virtual address is part of the addressable space. + /// + /// Virtual address + /// True if the virtual address is part of the addressable space + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected bool ValidateAddress(ulong va) + { + return va < AddressSpaceSize; + } + + /// + /// Checks if the combination of virtual address and size is part of the addressable space. + /// + /// Virtual address of the range + /// Size of the range in bytes + /// True if the combination of virtual address and size is part of the addressable space + protected bool ValidateAddressAndSize(ulong va, ulong size) + { + ulong endVa = va + size; + return endVa >= va && endVa >= size && endVa <= AddressSpaceSize; + } + + protected static void ThrowInvalidMemoryRegionException(string message) + => throw new InvalidMemoryRegionException(message); + + protected static void ThrowMemoryNotContiguous() + => throw new MemoryNotContiguousException(); + + protected virtual void WriteImpl(ulong va, ReadOnlySpan data) + { + AssertValidAddressAndSize(va, data.Length); + + if (IsContiguousAndMapped(va, data.Length)) + { + nuint pa = TranslateVirtualAddressUnchecked(va); + + data.CopyTo(GetPhysicalAddressSpan(pa, data.Length)); + } + else + { + int offset = 0, size; + + if ((va & PageMask) != 0) + { + nuint pa = TranslateVirtualAddressChecked(va); + + size = Math.Min(data.Length, PageSize - (int)(va & PageMask)); + + data[..size].CopyTo(GetPhysicalAddressSpan(pa, size)); + + offset += size; + } + + for (; offset < data.Length; offset += size) + { + nuint pa = TranslateVirtualAddressChecked(va + (ulong)offset); + + size = Math.Min(data.Length - offset, PageSize); + + data.Slice(offset, size).CopyTo(GetPhysicalAddressSpan(pa, size)); + } + } + } + + } +} diff --git a/src/Ryujinx.Memory/WindowsShared/PlaceholderManager.cs b/src/Ryujinx.Memory/WindowsShared/PlaceholderManager.cs index b68a076c4..2a294bba9 100644 --- a/src/Ryujinx.Memory/WindowsShared/PlaceholderManager.cs +++ b/src/Ryujinx.Memory/WindowsShared/PlaceholderManager.cs @@ -18,7 +18,7 @@ namespace Ryujinx.Memory.WindowsShared private readonly MappingTree _mappings; private readonly MappingTree _protections; - private readonly IntPtr _partialUnmapStatePtr; + private readonly nint _partialUnmapStatePtr; private readonly Thread _partialUnmapTrimThread; /// @@ -100,7 +100,7 @@ namespace Ryujinx.Memory.WindowsShared if (IsMapped(node.Value)) { - if (!WindowsApi.UnmapViewOfFile2(WindowsApi.CurrentProcessHandle, (IntPtr)node.Start, 2)) + if (!WindowsApi.UnmapViewOfFile2(WindowsApi.CurrentProcessHandle, (nint)node.Start, 2)) { throw new WindowsApiException("UnmapViewOfFile2"); } @@ -126,7 +126,7 @@ namespace Ryujinx.Memory.WindowsShared /// Address to map the view into /// Size of the view in bytes /// Memory block that owns the mapping - public void MapView(IntPtr sharedMemory, ulong srcOffset, IntPtr location, IntPtr size, MemoryBlock owner) + public void MapView(nint sharedMemory, ulong srcOffset, nint location, nint size, MemoryBlock owner) { ref var partialUnmapLock = ref GetPartialUnmapState().PartialUnmapLock; partialUnmapLock.AcquireReaderLock(); @@ -151,7 +151,7 @@ namespace Ryujinx.Memory.WindowsShared /// Size of the view in bytes /// Indicates if the memory protections should be updated after the map /// Thrown when the Windows API returns an error mapping the memory - private void MapViewInternal(IntPtr sharedMemory, ulong srcOffset, IntPtr location, IntPtr size, bool updateProtection) + private void MapViewInternal(nint sharedMemory, ulong srcOffset, nint location, nint size, bool updateProtection) { SplitForMap((ulong)location, (ulong)size, srcOffset); @@ -163,10 +163,10 @@ namespace Ryujinx.Memory.WindowsShared size, 0x4000, MemoryProtection.ReadWrite, - IntPtr.Zero, + nint.Zero, 0); - if (ptr == IntPtr.Zero) + if (ptr == nint.Zero) { throw new WindowsApiException("MapViewOfFile3"); } @@ -210,8 +210,8 @@ namespace Ryujinx.Memory.WindowsShared if (overlapStartsBefore && overlapEndsAfter) { CheckFreeResult(WindowsApi.VirtualFree( - (IntPtr)address, - (IntPtr)size, + (nint)address, + (nint)size, AllocationType.Release | AllocationType.PreservePlaceholder)); _mappings.Add(new RangeNode(overlapStart, address, overlapValue)); @@ -222,8 +222,8 @@ namespace Ryujinx.Memory.WindowsShared ulong overlappedSize = overlapEnd - address; CheckFreeResult(WindowsApi.VirtualFree( - (IntPtr)address, - (IntPtr)overlappedSize, + (nint)address, + (nint)overlappedSize, AllocationType.Release | AllocationType.PreservePlaceholder)); _mappings.Add(new RangeNode(overlapStart, address, overlapValue)); @@ -233,8 +233,8 @@ namespace Ryujinx.Memory.WindowsShared ulong overlappedSize = endAddress - overlapStart; CheckFreeResult(WindowsApi.VirtualFree( - (IntPtr)overlapStart, - (IntPtr)overlappedSize, + (nint)overlapStart, + (nint)overlappedSize, AllocationType.Release | AllocationType.PreservePlaceholder)); _mappings.Add(new RangeNode(endAddress, overlapEnd, AddBackingOffset(overlapValue, overlappedSize))); @@ -255,7 +255,7 @@ namespace Ryujinx.Memory.WindowsShared /// Address to unmap /// Size of the region to unmap in bytes /// Memory block that owns the mapping - public void UnmapView(IntPtr sharedMemory, IntPtr location, IntPtr size, MemoryBlock owner) + public void UnmapView(nint sharedMemory, nint location, nint size, MemoryBlock owner) { ref var partialUnmapLock = ref GetPartialUnmapState().PartialUnmapLock; partialUnmapLock.AcquireReaderLock(); @@ -283,7 +283,7 @@ namespace Ryujinx.Memory.WindowsShared /// Memory block that owns the mapping /// Indicates if the memory protections should be updated after the unmap /// Thrown when the Windows API returns an error unmapping or remapping the memory - private void UnmapViewInternal(IntPtr sharedMemory, IntPtr location, IntPtr size, MemoryBlock owner, bool updateProtection) + private void UnmapViewInternal(nint sharedMemory, nint location, nint size, MemoryBlock owner, bool updateProtection) { ulong startAddress = (ulong)location; ulong unmapSize = (ulong)size; @@ -327,7 +327,7 @@ namespace Ryujinx.Memory.WindowsShared { partialUnmapState.PartialUnmapsCount++; - if (!WindowsApi.UnmapViewOfFile2(WindowsApi.CurrentProcessHandle, (IntPtr)overlap.Start, 2)) + if (!WindowsApi.UnmapViewOfFile2(WindowsApi.CurrentProcessHandle, (nint)overlap.Start, 2)) { throw new WindowsApiException("UnmapViewOfFile2"); } @@ -336,7 +336,7 @@ namespace Ryujinx.Memory.WindowsShared { ulong remapSize = startAddress - overlap.Start; - MapViewInternal(sharedMemory, overlap.Value, (IntPtr)overlap.Start, (IntPtr)remapSize, updateProtection: false); + MapViewInternal(sharedMemory, overlap.Value, (nint)overlap.Start, (nint)remapSize, updateProtection: false); RestoreRangeProtection(overlap.Start, remapSize); } @@ -347,7 +347,7 @@ namespace Ryujinx.Memory.WindowsShared ulong remapAddress = overlap.Start + overlappedSize; ulong remapSize = overlap.End - endAddress; - MapViewInternal(sharedMemory, remapBackingOffset, (IntPtr)remapAddress, (IntPtr)remapSize, updateProtection: false); + MapViewInternal(sharedMemory, remapBackingOffset, (nint)remapAddress, (nint)remapSize, updateProtection: false); RestoreRangeProtection(remapAddress, remapSize); } } @@ -356,7 +356,7 @@ namespace Ryujinx.Memory.WindowsShared partialUnmapLock.DowngradeFromWriterLock(); } } - else if (!WindowsApi.UnmapViewOfFile2(WindowsApi.CurrentProcessHandle, (IntPtr)overlap.Start, 2)) + else if (!WindowsApi.UnmapViewOfFile2(WindowsApi.CurrentProcessHandle, (nint)overlap.Start, 2)) { throw new WindowsApiException("UnmapViewOfFile2"); } @@ -441,8 +441,8 @@ namespace Ryujinx.Memory.WindowsShared size = endAddress - address; CheckFreeResult(WindowsApi.VirtualFree( - (IntPtr)address, - (IntPtr)size, + (nint)address, + (nint)size, AllocationType.Release | AllocationType.CoalescePlaceholders)); } } @@ -454,7 +454,7 @@ namespace Ryujinx.Memory.WindowsShared /// Size of the region to reprotect in bytes /// New permissions /// True if the reprotection was successful, false otherwise - public bool ReprotectView(IntPtr address, IntPtr size, MemoryPermission permission) + public bool ReprotectView(nint address, nint size, MemoryPermission permission) { ref var partialUnmapLock = ref GetPartialUnmapState().PartialUnmapLock; partialUnmapLock.AcquireReaderLock(); @@ -478,7 +478,7 @@ namespace Ryujinx.Memory.WindowsShared /// Throw an exception instead of returning an error if the operation fails /// True if the reprotection was successful or if is true, false otherwise /// If is true, it is thrown when the Windows API returns an error reprotecting the memory - private bool ReprotectViewInternal(IntPtr address, IntPtr size, MemoryPermission permission, bool throwOnError) + private bool ReprotectViewInternal(nint address, nint size, MemoryPermission permission, bool throwOnError) { ulong reprotectAddress = (ulong)address; ulong reprotectSize = (ulong)size; @@ -514,7 +514,7 @@ namespace Ryujinx.Memory.WindowsShared mappedSize -= delta; } - if (!WindowsApi.VirtualProtect((IntPtr)mappedAddress, (IntPtr)mappedSize, WindowsApi.GetProtection(permission), out _)) + if (!WindowsApi.VirtualProtect((nint)mappedAddress, (nint)mappedSize, WindowsApi.GetProtection(permission), out _)) { if (throwOnError) { @@ -729,7 +729,7 @@ namespace Ryujinx.Memory.WindowsShared protEndAddress = endAddress; } - ReprotectViewInternal((IntPtr)protAddress, (IntPtr)(protEndAddress - protAddress), protection.Value, true); + ReprotectViewInternal((nint)protAddress, (nint)(protEndAddress - protAddress), protection.Value, true); } } } diff --git a/src/Ryujinx.Memory/WindowsShared/WindowsApi.cs b/src/Ryujinx.Memory/WindowsShared/WindowsApi.cs index 82903c05f..0d002dada 100644 --- a/src/Ryujinx.Memory/WindowsShared/WindowsApi.cs +++ b/src/Ryujinx.Memory/WindowsShared/WindowsApi.cs @@ -7,42 +7,42 @@ namespace Ryujinx.Memory.WindowsShared [SupportedOSPlatform("windows")] static partial class WindowsApi { - public static readonly IntPtr InvalidHandleValue = new(-1); - public static readonly IntPtr CurrentProcessHandle = new(-1); + public static readonly nint InvalidHandleValue = new(-1); + public static readonly nint CurrentProcessHandle = new(-1); [LibraryImport("kernel32.dll", SetLastError = true)] - public static partial IntPtr VirtualAlloc( - IntPtr lpAddress, - IntPtr dwSize, + public static partial nint VirtualAlloc( + nint lpAddress, + nint dwSize, AllocationType flAllocationType, MemoryProtection flProtect); [LibraryImport("KernelBase.dll", SetLastError = true)] - public static partial IntPtr VirtualAlloc2( - IntPtr process, - IntPtr lpAddress, - IntPtr dwSize, + public static partial nint VirtualAlloc2( + nint process, + nint lpAddress, + nint dwSize, AllocationType flAllocationType, MemoryProtection flProtect, - IntPtr extendedParameters, + nint extendedParameters, ulong parameterCount); [LibraryImport("kernel32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] public static partial bool VirtualProtect( - IntPtr lpAddress, - IntPtr dwSize, + nint lpAddress, + nint dwSize, MemoryProtection flNewProtect, out MemoryProtection lpflOldProtect); [LibraryImport("kernel32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] - public static partial bool VirtualFree(IntPtr lpAddress, IntPtr dwSize, AllocationType dwFreeType); + public static partial bool VirtualFree(nint lpAddress, nint dwSize, AllocationType dwFreeType); [LibraryImport("kernel32.dll", SetLastError = true, EntryPoint = "CreateFileMappingW")] - public static partial IntPtr CreateFileMapping( - IntPtr hFile, - IntPtr lpFileMappingAttributes, + public static partial nint CreateFileMapping( + nint hFile, + nint lpFileMappingAttributes, FileMapProtection flProtect, uint dwMaximumSizeHigh, uint dwMaximumSizeLow, @@ -50,35 +50,35 @@ namespace Ryujinx.Memory.WindowsShared [LibraryImport("kernel32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] - public static partial bool CloseHandle(IntPtr hObject); + public static partial bool CloseHandle(nint hObject); [LibraryImport("kernel32.dll", SetLastError = true)] - public static partial IntPtr MapViewOfFile( - IntPtr hFileMappingObject, + public static partial nint MapViewOfFile( + nint hFileMappingObject, uint dwDesiredAccess, uint dwFileOffsetHigh, uint dwFileOffsetLow, - IntPtr dwNumberOfBytesToMap); + nint dwNumberOfBytesToMap); [LibraryImport("KernelBase.dll", SetLastError = true)] - public static partial IntPtr MapViewOfFile3( - IntPtr hFileMappingObject, - IntPtr process, - IntPtr baseAddress, + public static partial nint MapViewOfFile3( + nint hFileMappingObject, + nint process, + nint baseAddress, ulong offset, - IntPtr dwNumberOfBytesToMap, + nint dwNumberOfBytesToMap, ulong allocationType, MemoryProtection dwDesiredAccess, - IntPtr extendedParameters, + nint extendedParameters, ulong parameterCount); [LibraryImport("kernel32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] - public static partial bool UnmapViewOfFile(IntPtr lpBaseAddress); + public static partial bool UnmapViewOfFile(nint lpBaseAddress); [LibraryImport("KernelBase.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] - public static partial bool UnmapViewOfFile2(IntPtr process, IntPtr lpBaseAddress, ulong unmapFlags); + public static partial bool UnmapViewOfFile2(nint process, nint lpBaseAddress, ulong unmapFlags); [LibraryImport("kernel32.dll")] public static partial uint GetLastError(); diff --git a/src/Ryujinx.Memory/WritableRegion.cs b/src/Ryujinx.Memory/WritableRegion.cs index 666c8a99b..54facb508 100644 --- a/src/Ryujinx.Memory/WritableRegion.cs +++ b/src/Ryujinx.Memory/WritableRegion.cs @@ -1,3 +1,4 @@ +using Ryujinx.Common.Memory; using System; namespace Ryujinx.Memory @@ -6,6 +7,7 @@ namespace Ryujinx.Memory { private readonly IWritableBlock _block; private readonly ulong _va; + private readonly MemoryOwner _memoryOwner; private readonly bool _tracked; private bool NeedsWriteback => _block != null; @@ -20,6 +22,12 @@ namespace Ryujinx.Memory Memory = memory; } + public WritableRegion(IWritableBlock block, ulong va, MemoryOwner memoryOwner, bool tracked = false) + : this(block, va, memoryOwner.Memory, tracked) + { + _memoryOwner = memoryOwner; + } + public void Dispose() { if (NeedsWriteback) @@ -33,6 +41,8 @@ namespace Ryujinx.Memory _block.WriteUntracked(_va, Memory.Span); } } + + _memoryOwner?.Dispose(); } } } diff --git a/src/Ryujinx.SDL2.Common/SDL2Driver.cs b/src/Ryujinx.SDL2.Common/SDL2Driver.cs index 49e7dd147..4d8961335 100644 --- a/src/Ryujinx.SDL2.Common/SDL2Driver.cs +++ b/src/Ryujinx.SDL2.Common/SDL2Driver.cs @@ -1,4 +1,4 @@ -using Ryujinx.Common; +using Ryujinx.Common.Configuration; using Ryujinx.Common.Logging; using System; using System.Collections.Concurrent; @@ -13,8 +13,6 @@ namespace Ryujinx.SDL2.Common { private static SDL2Driver _instance; - public static bool IsInitialized => _instance != null; - public static SDL2Driver Instance { get @@ -55,6 +53,7 @@ namespace Ryujinx.SDL2.Common return; } + SDL_SetHint(SDL_HINT_APP_NAME, "Ryujinx"); SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS4_RUMBLE, "1"); SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS5_RUMBLE, "1"); SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1"); @@ -96,12 +95,12 @@ namespace Ryujinx.SDL2.Common SDL_EventState(SDL_EventType.SDL_CONTROLLERSENSORUPDATE, SDL_DISABLE); - // string gamepadDbPath = Path.Combine(ReleaseInformation.GetBaseApplicationDirectory(), "SDL_GameControllerDB.txt"); + string gamepadDbPath = Path.Combine(AppDataManager.BaseDirPath, "SDL_GameControllerDB.txt"); - // if (File.Exists(gamepadDbPath)) - // { - // SDL_GameControllerAddMappingsFromFile(gamepadDbPath); - // } + if (File.Exists(gamepadDbPath)) + { + SDL_GameControllerAddMappingsFromFile(gamepadDbPath); + } _registeredWindowHandlers = new ConcurrentDictionary>(); _worker = new Thread(EventWorker); @@ -144,7 +143,7 @@ namespace Ryujinx.SDL2.Common OnJoystickDisconnected?.Invoke(evnt.cbutton.which); } - else if (evnt.type == SDL_EventType.SDL_WINDOWEVENT || evnt.type == SDL_EventType.SDL_MOUSEBUTTONDOWN || evnt.type == SDL_EventType.SDL_MOUSEBUTTONUP) + else if (evnt.type is SDL_EventType.SDL_WINDOWEVENT or SDL_EventType.SDL_MOUSEBUTTONDOWN or SDL_EventType.SDL_MOUSEBUTTONUP) { if (_registeredWindowHandlers.TryGetValue(evnt.window.windowID, out Action handler)) { diff --git a/src/Ryujinx.ShaderTools/Program.cs b/src/Ryujinx.ShaderTools/Program.cs index 04453912b..a84d7b466 100644 --- a/src/Ryujinx.ShaderTools/Program.cs +++ b/src/Ryujinx.ShaderTools/Program.cs @@ -11,17 +11,67 @@ namespace Ryujinx.ShaderTools { private class GpuAccessor : IGpuAccessor { + private const int DefaultArrayLength = 32; + private readonly byte[] _data; + private int _texturesCount; + private int _imagesCount; + public GpuAccessor(byte[] data) { _data = data; + _texturesCount = 0; + _imagesCount = 0; + } + + public SetBindingPair CreateConstantBufferBinding(int index) + { + return new SetBindingPair(0, index + 1); + } + + public SetBindingPair CreateImageBinding(int count, bool isBuffer) + { + int binding = _imagesCount; + + _imagesCount += count; + + return new SetBindingPair(3, binding); + } + + public SetBindingPair CreateStorageBufferBinding(int index) + { + return new SetBindingPair(1, index); + } + + public SetBindingPair CreateTextureBinding(int count, bool isBuffer) + { + int binding = _texturesCount; + + _texturesCount += count; + + return new SetBindingPair(2, binding); } public ReadOnlySpan GetCode(ulong address, int minimumSize) { return MemoryMarshal.Cast(new ReadOnlySpan(_data)[(int)address..]); } + + public int QuerySamplerArrayLengthFromPool() + { + return DefaultArrayLength; + } + + public int QueryTextureArrayLengthFromBuffer(int slot) + { + return DefaultArrayLength; + } + + public int QueryTextureArrayLengthFromPool() + { + return DefaultArrayLength; + } } private class Options diff --git a/src/Ryujinx.Tests.Memory/MockVirtualMemoryManager.cs b/src/Ryujinx.Tests.Memory/MockVirtualMemoryManager.cs index 5c8396e6d..3fe44db21 100644 --- a/src/Ryujinx.Tests.Memory/MockVirtualMemoryManager.cs +++ b/src/Ryujinx.Tests.Memory/MockVirtualMemoryManager.cs @@ -1,13 +1,14 @@ using Ryujinx.Memory; using Ryujinx.Memory.Range; using System; +using System.Buffers; using System.Collections.Generic; namespace Ryujinx.Tests.Memory { public class MockVirtualMemoryManager : IVirtualMemoryManager { - public bool Supports4KBPages => true; + public bool UsesPrivateAllocations => false; public bool NoMappings = false; @@ -57,6 +58,11 @@ namespace Ryujinx.Tests.Memory throw new NotImplementedException(); } + public ReadOnlySequence GetReadOnlySequence(ulong va, int size, bool tracked = false) + { + throw new NotImplementedException(); + } + public ReadOnlySpan GetSpan(ulong va, int size, bool tracked = false) { throw new NotImplementedException(); @@ -107,7 +113,7 @@ namespace Ryujinx.Tests.Memory throw new NotImplementedException(); } - public void TrackingReprotect(ulong va, ulong size, MemoryPermission protection) + public void TrackingReprotect(ulong va, ulong size, MemoryPermission protection, bool guest) { OnProtect?.Invoke(va, size, protection); } diff --git a/src/Ryujinx.Tests/Audio/Renderer/Server/BehaviourContextTests.cs b/src/Ryujinx.Tests/Audio/Renderer/Server/BehaviourContextTests.cs index 557581881..0b0ed7a54 100644 --- a/src/Ryujinx.Tests/Audio/Renderer/Server/BehaviourContextTests.cs +++ b/src/Ryujinx.Tests/Audio/Renderer/Server/BehaviourContextTests.cs @@ -52,7 +52,10 @@ namespace Ryujinx.Tests.Audio.Renderer.Server Assert.IsFalse(behaviourContext.IsMixInParameterDirtyOnlyUpdateSupported()); Assert.IsFalse(behaviourContext.IsWaveBufferVersion2Supported()); Assert.IsFalse(behaviourContext.IsEffectInfoVersion2Supported()); - Assert.IsFalse(behaviourContext.IsBiquadFilterGroupedOptimizationSupported()); + Assert.IsFalse(behaviourContext.UseMultiTapBiquadFilterProcessing()); + Assert.IsFalse(behaviourContext.IsNewEffectChannelMappingSupported()); + Assert.IsFalse(behaviourContext.IsBiquadFilterParameterForSplitterEnabled()); + Assert.IsFalse(behaviourContext.IsSplitterPrevVolumeResetSupported()); Assert.AreEqual(0.70f, behaviourContext.GetAudioRendererProcessingTimeLimit()); Assert.AreEqual(1, behaviourContext.GetCommandProcessingTimeEstimatorVersion()); @@ -78,7 +81,10 @@ namespace Ryujinx.Tests.Audio.Renderer.Server Assert.IsFalse(behaviourContext.IsMixInParameterDirtyOnlyUpdateSupported()); Assert.IsFalse(behaviourContext.IsWaveBufferVersion2Supported()); Assert.IsFalse(behaviourContext.IsEffectInfoVersion2Supported()); - Assert.IsFalse(behaviourContext.IsBiquadFilterGroupedOptimizationSupported()); + Assert.IsFalse(behaviourContext.UseMultiTapBiquadFilterProcessing()); + Assert.IsFalse(behaviourContext.IsNewEffectChannelMappingSupported()); + Assert.IsFalse(behaviourContext.IsBiquadFilterParameterForSplitterEnabled()); + Assert.IsFalse(behaviourContext.IsSplitterPrevVolumeResetSupported()); Assert.AreEqual(0.70f, behaviourContext.GetAudioRendererProcessingTimeLimit()); Assert.AreEqual(1, behaviourContext.GetCommandProcessingTimeEstimatorVersion()); @@ -104,7 +110,10 @@ namespace Ryujinx.Tests.Audio.Renderer.Server Assert.IsFalse(behaviourContext.IsMixInParameterDirtyOnlyUpdateSupported()); Assert.IsFalse(behaviourContext.IsWaveBufferVersion2Supported()); Assert.IsFalse(behaviourContext.IsEffectInfoVersion2Supported()); - Assert.IsFalse(behaviourContext.IsBiquadFilterGroupedOptimizationSupported()); + Assert.IsFalse(behaviourContext.UseMultiTapBiquadFilterProcessing()); + Assert.IsFalse(behaviourContext.IsNewEffectChannelMappingSupported()); + Assert.IsFalse(behaviourContext.IsBiquadFilterParameterForSplitterEnabled()); + Assert.IsFalse(behaviourContext.IsSplitterPrevVolumeResetSupported()); Assert.AreEqual(0.70f, behaviourContext.GetAudioRendererProcessingTimeLimit()); Assert.AreEqual(1, behaviourContext.GetCommandProcessingTimeEstimatorVersion()); @@ -130,7 +139,10 @@ namespace Ryujinx.Tests.Audio.Renderer.Server Assert.IsFalse(behaviourContext.IsMixInParameterDirtyOnlyUpdateSupported()); Assert.IsFalse(behaviourContext.IsWaveBufferVersion2Supported()); Assert.IsFalse(behaviourContext.IsEffectInfoVersion2Supported()); - Assert.IsFalse(behaviourContext.IsBiquadFilterGroupedOptimizationSupported()); + Assert.IsFalse(behaviourContext.UseMultiTapBiquadFilterProcessing()); + Assert.IsFalse(behaviourContext.IsNewEffectChannelMappingSupported()); + Assert.IsFalse(behaviourContext.IsBiquadFilterParameterForSplitterEnabled()); + Assert.IsFalse(behaviourContext.IsSplitterPrevVolumeResetSupported()); Assert.AreEqual(0.75f, behaviourContext.GetAudioRendererProcessingTimeLimit()); Assert.AreEqual(1, behaviourContext.GetCommandProcessingTimeEstimatorVersion()); @@ -156,7 +168,10 @@ namespace Ryujinx.Tests.Audio.Renderer.Server Assert.IsFalse(behaviourContext.IsMixInParameterDirtyOnlyUpdateSupported()); Assert.IsFalse(behaviourContext.IsWaveBufferVersion2Supported()); Assert.IsFalse(behaviourContext.IsEffectInfoVersion2Supported()); - Assert.IsFalse(behaviourContext.IsBiquadFilterGroupedOptimizationSupported()); + Assert.IsFalse(behaviourContext.UseMultiTapBiquadFilterProcessing()); + Assert.IsFalse(behaviourContext.IsNewEffectChannelMappingSupported()); + Assert.IsFalse(behaviourContext.IsBiquadFilterParameterForSplitterEnabled()); + Assert.IsFalse(behaviourContext.IsSplitterPrevVolumeResetSupported()); Assert.AreEqual(0.80f, behaviourContext.GetAudioRendererProcessingTimeLimit()); Assert.AreEqual(2, behaviourContext.GetCommandProcessingTimeEstimatorVersion()); @@ -182,7 +197,10 @@ namespace Ryujinx.Tests.Audio.Renderer.Server Assert.IsFalse(behaviourContext.IsMixInParameterDirtyOnlyUpdateSupported()); Assert.IsFalse(behaviourContext.IsWaveBufferVersion2Supported()); Assert.IsFalse(behaviourContext.IsEffectInfoVersion2Supported()); - Assert.IsFalse(behaviourContext.IsBiquadFilterGroupedOptimizationSupported()); + Assert.IsFalse(behaviourContext.UseMultiTapBiquadFilterProcessing()); + Assert.IsFalse(behaviourContext.IsNewEffectChannelMappingSupported()); + Assert.IsFalse(behaviourContext.IsBiquadFilterParameterForSplitterEnabled()); + Assert.IsFalse(behaviourContext.IsSplitterPrevVolumeResetSupported()); Assert.AreEqual(0.80f, behaviourContext.GetAudioRendererProcessingTimeLimit()); Assert.AreEqual(2, behaviourContext.GetCommandProcessingTimeEstimatorVersion()); @@ -208,7 +226,10 @@ namespace Ryujinx.Tests.Audio.Renderer.Server Assert.IsTrue(behaviourContext.IsMixInParameterDirtyOnlyUpdateSupported()); Assert.IsFalse(behaviourContext.IsWaveBufferVersion2Supported()); Assert.IsFalse(behaviourContext.IsEffectInfoVersion2Supported()); - Assert.IsFalse(behaviourContext.IsBiquadFilterGroupedOptimizationSupported()); + Assert.IsFalse(behaviourContext.UseMultiTapBiquadFilterProcessing()); + Assert.IsFalse(behaviourContext.IsNewEffectChannelMappingSupported()); + Assert.IsFalse(behaviourContext.IsBiquadFilterParameterForSplitterEnabled()); + Assert.IsFalse(behaviourContext.IsSplitterPrevVolumeResetSupported()); Assert.AreEqual(0.80f, behaviourContext.GetAudioRendererProcessingTimeLimit()); Assert.AreEqual(2, behaviourContext.GetCommandProcessingTimeEstimatorVersion()); @@ -234,7 +255,10 @@ namespace Ryujinx.Tests.Audio.Renderer.Server Assert.IsTrue(behaviourContext.IsMixInParameterDirtyOnlyUpdateSupported()); Assert.IsTrue(behaviourContext.IsWaveBufferVersion2Supported()); Assert.IsFalse(behaviourContext.IsEffectInfoVersion2Supported()); - Assert.IsFalse(behaviourContext.IsBiquadFilterGroupedOptimizationSupported()); + Assert.IsFalse(behaviourContext.UseMultiTapBiquadFilterProcessing()); + Assert.IsFalse(behaviourContext.IsNewEffectChannelMappingSupported()); + Assert.IsFalse(behaviourContext.IsBiquadFilterParameterForSplitterEnabled()); + Assert.IsFalse(behaviourContext.IsSplitterPrevVolumeResetSupported()); Assert.AreEqual(0.80f, behaviourContext.GetAudioRendererProcessingTimeLimit()); Assert.AreEqual(3, behaviourContext.GetCommandProcessingTimeEstimatorVersion()); @@ -260,7 +284,10 @@ namespace Ryujinx.Tests.Audio.Renderer.Server Assert.IsTrue(behaviourContext.IsMixInParameterDirtyOnlyUpdateSupported()); Assert.IsTrue(behaviourContext.IsWaveBufferVersion2Supported()); Assert.IsTrue(behaviourContext.IsEffectInfoVersion2Supported()); - Assert.IsFalse(behaviourContext.IsBiquadFilterGroupedOptimizationSupported()); + Assert.IsFalse(behaviourContext.UseMultiTapBiquadFilterProcessing()); + Assert.IsFalse(behaviourContext.IsNewEffectChannelMappingSupported()); + Assert.IsFalse(behaviourContext.IsBiquadFilterParameterForSplitterEnabled()); + Assert.IsFalse(behaviourContext.IsSplitterPrevVolumeResetSupported()); Assert.AreEqual(0.80f, behaviourContext.GetAudioRendererProcessingTimeLimit()); Assert.AreEqual(3, behaviourContext.GetCommandProcessingTimeEstimatorVersion()); @@ -286,11 +313,101 @@ namespace Ryujinx.Tests.Audio.Renderer.Server Assert.IsTrue(behaviourContext.IsMixInParameterDirtyOnlyUpdateSupported()); Assert.IsTrue(behaviourContext.IsWaveBufferVersion2Supported()); Assert.IsTrue(behaviourContext.IsEffectInfoVersion2Supported()); - Assert.IsTrue(behaviourContext.IsBiquadFilterGroupedOptimizationSupported()); + Assert.IsTrue(behaviourContext.UseMultiTapBiquadFilterProcessing()); + Assert.IsFalse(behaviourContext.IsNewEffectChannelMappingSupported()); + Assert.IsFalse(behaviourContext.IsBiquadFilterParameterForSplitterEnabled()); + Assert.IsFalse(behaviourContext.IsSplitterPrevVolumeResetSupported()); Assert.AreEqual(0.80f, behaviourContext.GetAudioRendererProcessingTimeLimit()); Assert.AreEqual(4, behaviourContext.GetCommandProcessingTimeEstimatorVersion()); Assert.AreEqual(2, behaviourContext.GetPerformanceMetricsDataFormat()); } + + [Test] + public void TestRevision11() + { + BehaviourContext behaviourContext = new(); + + behaviourContext.SetUserRevision(BehaviourContext.BaseRevisionMagic + BehaviourContext.Revision11); + + Assert.IsTrue(behaviourContext.IsAdpcmLoopContextBugFixed()); + Assert.IsTrue(behaviourContext.IsSplitterSupported()); + Assert.IsTrue(behaviourContext.IsLongSizePreDelaySupported()); + Assert.IsTrue(behaviourContext.IsAudioUsbDeviceOutputSupported()); + Assert.IsTrue(behaviourContext.IsFlushVoiceWaveBuffersSupported()); + Assert.IsTrue(behaviourContext.IsSplitterBugFixed()); + Assert.IsTrue(behaviourContext.IsElapsedFrameCountSupported()); + Assert.IsTrue(behaviourContext.IsDecodingBehaviourFlagSupported()); + Assert.IsTrue(behaviourContext.IsBiquadFilterEffectStateClearBugFixed()); + Assert.IsTrue(behaviourContext.IsMixInParameterDirtyOnlyUpdateSupported()); + Assert.IsTrue(behaviourContext.IsWaveBufferVersion2Supported()); + Assert.IsTrue(behaviourContext.IsEffectInfoVersion2Supported()); + Assert.IsTrue(behaviourContext.UseMultiTapBiquadFilterProcessing()); + Assert.IsTrue(behaviourContext.IsNewEffectChannelMappingSupported()); + Assert.IsFalse(behaviourContext.IsBiquadFilterParameterForSplitterEnabled()); + Assert.IsFalse(behaviourContext.IsSplitterPrevVolumeResetSupported()); + + Assert.AreEqual(0.80f, behaviourContext.GetAudioRendererProcessingTimeLimit()); + Assert.AreEqual(5, behaviourContext.GetCommandProcessingTimeEstimatorVersion()); + Assert.AreEqual(2, behaviourContext.GetPerformanceMetricsDataFormat()); + } + + [Test] + public void TestRevision12() + { + BehaviourContext behaviourContext = new(); + + behaviourContext.SetUserRevision(BehaviourContext.BaseRevisionMagic + BehaviourContext.Revision12); + + Assert.IsTrue(behaviourContext.IsAdpcmLoopContextBugFixed()); + Assert.IsTrue(behaviourContext.IsSplitterSupported()); + Assert.IsTrue(behaviourContext.IsLongSizePreDelaySupported()); + Assert.IsTrue(behaviourContext.IsAudioUsbDeviceOutputSupported()); + Assert.IsTrue(behaviourContext.IsFlushVoiceWaveBuffersSupported()); + Assert.IsTrue(behaviourContext.IsSplitterBugFixed()); + Assert.IsTrue(behaviourContext.IsElapsedFrameCountSupported()); + Assert.IsTrue(behaviourContext.IsDecodingBehaviourFlagSupported()); + Assert.IsTrue(behaviourContext.IsBiquadFilterEffectStateClearBugFixed()); + Assert.IsTrue(behaviourContext.IsMixInParameterDirtyOnlyUpdateSupported()); + Assert.IsTrue(behaviourContext.IsWaveBufferVersion2Supported()); + Assert.IsTrue(behaviourContext.IsEffectInfoVersion2Supported()); + Assert.IsTrue(behaviourContext.UseMultiTapBiquadFilterProcessing()); + Assert.IsTrue(behaviourContext.IsNewEffectChannelMappingSupported()); + Assert.IsTrue(behaviourContext.IsBiquadFilterParameterForSplitterEnabled()); + Assert.IsFalse(behaviourContext.IsSplitterPrevVolumeResetSupported()); + + Assert.AreEqual(0.80f, behaviourContext.GetAudioRendererProcessingTimeLimit()); + Assert.AreEqual(5, behaviourContext.GetCommandProcessingTimeEstimatorVersion()); + Assert.AreEqual(2, behaviourContext.GetPerformanceMetricsDataFormat()); + } + + [Test] + public void TestRevision13() + { + BehaviourContext behaviourContext = new(); + + behaviourContext.SetUserRevision(BehaviourContext.BaseRevisionMagic + BehaviourContext.Revision13); + + Assert.IsTrue(behaviourContext.IsAdpcmLoopContextBugFixed()); + Assert.IsTrue(behaviourContext.IsSplitterSupported()); + Assert.IsTrue(behaviourContext.IsLongSizePreDelaySupported()); + Assert.IsTrue(behaviourContext.IsAudioUsbDeviceOutputSupported()); + Assert.IsTrue(behaviourContext.IsFlushVoiceWaveBuffersSupported()); + Assert.IsTrue(behaviourContext.IsSplitterBugFixed()); + Assert.IsTrue(behaviourContext.IsElapsedFrameCountSupported()); + Assert.IsTrue(behaviourContext.IsDecodingBehaviourFlagSupported()); + Assert.IsTrue(behaviourContext.IsBiquadFilterEffectStateClearBugFixed()); + Assert.IsTrue(behaviourContext.IsMixInParameterDirtyOnlyUpdateSupported()); + Assert.IsTrue(behaviourContext.IsWaveBufferVersion2Supported()); + Assert.IsTrue(behaviourContext.IsEffectInfoVersion2Supported()); + Assert.IsTrue(behaviourContext.UseMultiTapBiquadFilterProcessing()); + Assert.IsTrue(behaviourContext.IsNewEffectChannelMappingSupported()); + Assert.IsTrue(behaviourContext.IsBiquadFilterParameterForSplitterEnabled()); + Assert.IsTrue(behaviourContext.IsSplitterPrevVolumeResetSupported()); + + Assert.AreEqual(0.80f, behaviourContext.GetAudioRendererProcessingTimeLimit()); + Assert.AreEqual(5, behaviourContext.GetCommandProcessingTimeEstimatorVersion()); + Assert.AreEqual(2, behaviourContext.GetPerformanceMetricsDataFormat()); + } } } diff --git a/src/Ryujinx.Tests/Audio/Renderer/Server/SplitterDestinationTests.cs b/src/Ryujinx.Tests/Audio/Renderer/Server/SplitterDestinationTests.cs index ad974aab1..80b801336 100644 --- a/src/Ryujinx.Tests/Audio/Renderer/Server/SplitterDestinationTests.cs +++ b/src/Ryujinx.Tests/Audio/Renderer/Server/SplitterDestinationTests.cs @@ -9,7 +9,8 @@ namespace Ryujinx.Tests.Audio.Renderer.Server [Test] public void EnsureTypeSize() { - Assert.AreEqual(0xE0, Unsafe.SizeOf()); + Assert.AreEqual(0xE0, Unsafe.SizeOf()); + Assert.AreEqual(0x110, Unsafe.SizeOf()); } } } diff --git a/src/Ryujinx.Tests/Common/Extensions/SequenceReaderExtensionsTests.cs b/src/Ryujinx.Tests/Common/Extensions/SequenceReaderExtensionsTests.cs new file mode 100644 index 000000000..c0127530a --- /dev/null +++ b/src/Ryujinx.Tests/Common/Extensions/SequenceReaderExtensionsTests.cs @@ -0,0 +1,359 @@ +using NUnit.Framework; +using Ryujinx.Common.Extensions; +using Ryujinx.Memory; +using System; +using System.Buffers; +using System.Buffers.Binary; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace Ryujinx.Tests.Common.Extensions +{ + public class SequenceReaderExtensionsTests + { + [TestCase(null)] + [TestCase(sizeof(int) + 1)] + public void GetRefOrRefToCopy_ReadsMultiSegmentedSequenceSuccessfully(int? maxSegmentSize) + { + // Arrange + MyUnmanagedStruct[] originalStructs = EnumerateNewUnmanagedStructs().Take(3).ToArray(); + + ReadOnlySequence sequence = + CreateSegmentedByteSequence(originalStructs, maxSegmentSize ?? Unsafe.SizeOf()); + + var sequenceReader = new SequenceReader(sequence); + + foreach (var original in originalStructs) + { + // Act + ref readonly MyUnmanagedStruct read = ref sequenceReader.GetRefOrRefToCopy(out _); + + // Assert + MyUnmanagedStruct.Assert(Assert.AreEqual, original, read); + } + } + + [Test] + public void GetRefOrRefToCopy_FragmentedSequenceReturnsRefToCopy() + { + // Arrange + MyUnmanagedStruct[] originalStructs = EnumerateNewUnmanagedStructs().Take(1).ToArray(); + + ReadOnlySequence sequence = CreateSegmentedByteSequence(originalStructs, 3); + + var sequenceReader = new SequenceReader(sequence); + + foreach (var original in originalStructs) + { + // Act + ref readonly MyUnmanagedStruct read = ref sequenceReader.GetRefOrRefToCopy(out var copy); + + // Assert + MyUnmanagedStruct.Assert(Assert.AreEqual, original, read); + MyUnmanagedStruct.Assert(Assert.AreEqual, read, copy); + } + } + + [Test] + public void GetRefOrRefToCopy_ContiguousSequenceReturnsRefToBuffer() + { + // Arrange + MyUnmanagedStruct[] originalStructs = EnumerateNewUnmanagedStructs().Take(1).ToArray(); + + ReadOnlySequence sequence = CreateSegmentedByteSequence(originalStructs, int.MaxValue); + + var sequenceReader = new SequenceReader(sequence); + + foreach (var original in originalStructs) + { + // Act + ref readonly MyUnmanagedStruct read = ref sequenceReader.GetRefOrRefToCopy(out var copy); + + // Assert + MyUnmanagedStruct.Assert(Assert.AreEqual, original, read); + MyUnmanagedStruct.Assert(Assert.AreNotEqual, read, copy); + } + } + + [Test] + public void GetRefOrRefToCopy_ThrowsWhenNotEnoughData() + { + // Arrange + MyUnmanagedStruct[] originalStructs = EnumerateNewUnmanagedStructs().Take(1).ToArray(); + + ReadOnlySequence sequence = CreateSegmentedByteSequence(originalStructs, int.MaxValue); + + // Act/Assert + Assert.Throws(() => + { + var sequenceReader = new SequenceReader(sequence); + + sequenceReader.Advance(1); + + ref readonly MyUnmanagedStruct result = ref sequenceReader.GetRefOrRefToCopy(out _); + }); + } + + [Test] + public void ReadLittleEndian_Int32_RoundTripsSuccessfully() + { + // Arrange + const int TestValue = 0x1234abcd; + + byte[] buffer = new byte[sizeof(int)]; + + BinaryPrimitives.WriteInt32LittleEndian(buffer.AsSpan(), TestValue); + + var sequenceReader = new SequenceReader(new ReadOnlySequence(buffer)); + + // Act + sequenceReader.ReadLittleEndian(out int roundTrippedValue); + + // Assert + Assert.AreEqual(TestValue, roundTrippedValue); + } + + [Test] + public void ReadLittleEndian_Int32_ResultIsNotBigEndian() + { + // Arrange + const int TestValue = 0x1234abcd; + + byte[] buffer = new byte[sizeof(int)]; + + BinaryPrimitives.WriteInt32BigEndian(buffer.AsSpan(), TestValue); + + var sequenceReader = new SequenceReader(new ReadOnlySequence(buffer)); + + // Act + sequenceReader.ReadLittleEndian(out int roundTrippedValue); + + // Assert + Assert.AreNotEqual(TestValue, roundTrippedValue); + } + + [Test] + public void ReadLittleEndian_Int32_ThrowsWhenNotEnoughData() + { + // Arrange + const int TestValue = 0x1234abcd; + + byte[] buffer = new byte[sizeof(int)]; + + BinaryPrimitives.WriteInt32BigEndian(buffer.AsSpan(), TestValue); + + // Act/Assert + Assert.Throws(() => + { + var sequenceReader = new SequenceReader(new ReadOnlySequence(buffer)); + sequenceReader.Advance(1); + + sequenceReader.ReadLittleEndian(out int roundTrippedValue); + }); + } + + [Test] + public void ReadUnmanaged_ContiguousSequence_Succeeds() + => ReadUnmanaged_Succeeds(int.MaxValue); + + [Test] + public void ReadUnmanaged_FragmentedSequence_Succeeds() + => ReadUnmanaged_Succeeds(sizeof(int) + 1); + + [Test] + public void ReadUnmanaged_ThrowsWhenNotEnoughData() + { + // Arrange + MyUnmanagedStruct[] originalStructs = EnumerateNewUnmanagedStructs().Take(1).ToArray(); + + ReadOnlySequence sequence = CreateSegmentedByteSequence(originalStructs, int.MaxValue); + + // Act/Assert + Assert.Throws(() => + { + var sequenceReader = new SequenceReader(sequence); + + sequenceReader.Advance(1); + + sequenceReader.ReadUnmanaged(out MyUnmanagedStruct read); + }); + } + + [Test] + public void SetConsumed_ContiguousSequence_SucceedsWhenValid() + => SetConsumed_SucceedsWhenValid(int.MaxValue); + + [Test] + public void SetConsumed_FragmentedSequence_SucceedsWhenValid() + => SetConsumed_SucceedsWhenValid(sizeof(int) + 1); + + [Test] + public void SetConsumed_ThrowsWhenBeyondActualLength() + { + const int StructCount = 2; + // Arrange + MyUnmanagedStruct[] originalStructs = EnumerateNewUnmanagedStructs().Take(StructCount).ToArray(); + + ReadOnlySequence sequence = CreateSegmentedByteSequence(originalStructs, MyUnmanagedStruct.SizeOf); + + Assert.Throws(() => + { + var sequenceReader = new SequenceReader(sequence); + + sequenceReader.SetConsumed(MyUnmanagedStruct.SizeOf * StructCount + 1); + }); + } + + private static void ReadUnmanaged_Succeeds(int maxSegmentLength) + { + // Arrange + MyUnmanagedStruct[] originalStructs = EnumerateNewUnmanagedStructs().Take(3).ToArray(); + + ReadOnlySequence sequence = CreateSegmentedByteSequence(originalStructs, maxSegmentLength); + + var sequenceReader = new SequenceReader(sequence); + + foreach (var original in originalStructs) + { + // Act + sequenceReader.ReadUnmanaged(out MyUnmanagedStruct read); + + // Assert + MyUnmanagedStruct.Assert(Assert.AreEqual, original, read); + } + } + + private static void SetConsumed_SucceedsWhenValid(int maxSegmentLength) + { + // Arrange + MyUnmanagedStruct[] originalStructs = EnumerateNewUnmanagedStructs().Take(2).ToArray(); + + ReadOnlySequence sequence = CreateSegmentedByteSequence(originalStructs, maxSegmentLength); + + var sequenceReader = new SequenceReader(sequence); + + static void SetConsumedAndAssert(scoped ref SequenceReader sequenceReader, long consumed) + { + sequenceReader.SetConsumed(consumed); + Assert.AreEqual(consumed, sequenceReader.Consumed); + } + + // Act/Assert + ref readonly MyUnmanagedStruct struct0A = ref sequenceReader.GetRefOrRefToCopy(out _); + + Assert.AreEqual(sequenceReader.Consumed, MyUnmanagedStruct.SizeOf); + + SetConsumedAndAssert(ref sequenceReader, 0); + + ref readonly MyUnmanagedStruct struct0B = ref sequenceReader.GetRefOrRefToCopy(out _); + + MyUnmanagedStruct.Assert(Assert.AreEqual, struct0A, struct0B); + + SetConsumedAndAssert(ref sequenceReader, 1); + + SetConsumedAndAssert(ref sequenceReader, MyUnmanagedStruct.SizeOf); + + ref readonly MyUnmanagedStruct struct1A = ref sequenceReader.GetRefOrRefToCopy(out _); + + SetConsumedAndAssert(ref sequenceReader, MyUnmanagedStruct.SizeOf); + + ref readonly MyUnmanagedStruct struct1B = ref sequenceReader.GetRefOrRefToCopy(out _); + + MyUnmanagedStruct.Assert(Assert.AreEqual, struct1A, struct1B); + } + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + private struct MyUnmanagedStruct + { + public int BehaviourSize; + public int MemoryPoolsSize; + public short VoicesSize; + public int VoiceResourcesSize; + public short EffectsSize; + public int RenderInfoSize; + + public unsafe fixed byte Reserved[16]; + + public static readonly int SizeOf = Unsafe.SizeOf(); + + public static unsafe MyUnmanagedStruct Generate(Random rng) + { + const int BaseInt32Value = 0x1234abcd; + const short BaseInt16Value = 0x5678; + + var result = new MyUnmanagedStruct + { + BehaviourSize = BaseInt32Value ^ rng.Next(), + MemoryPoolsSize = BaseInt32Value ^ rng.Next(), + VoicesSize = (short)(BaseInt16Value ^ rng.Next()), + VoiceResourcesSize = BaseInt32Value ^ rng.Next(), + EffectsSize = (short)(BaseInt16Value ^ rng.Next()), + RenderInfoSize = BaseInt32Value ^ rng.Next(), + }; + + Unsafe.Write(result.Reserved, rng.NextInt64()); + + return result; + } + + public static unsafe void Assert(Action assert, in MyUnmanagedStruct expected, in MyUnmanagedStruct actual) + { + assert(expected.BehaviourSize, actual.BehaviourSize); + assert(expected.MemoryPoolsSize, actual.MemoryPoolsSize); + assert(expected.VoicesSize, actual.VoicesSize); + assert(expected.VoiceResourcesSize, actual.VoiceResourcesSize); + assert(expected.EffectsSize, actual.EffectsSize); + assert(expected.RenderInfoSize, actual.RenderInfoSize); + + fixed (void* expectedReservedPtr = expected.Reserved) + fixed (void* actualReservedPtr = actual.Reserved) + { + long expectedReservedLong = Unsafe.Read(expectedReservedPtr); + long actualReservedLong = Unsafe.Read(actualReservedPtr); + + assert(expectedReservedLong, actualReservedLong); + } + } + } + + private static IEnumerable EnumerateNewUnmanagedStructs() + { + var rng = new Random(0); + + while (true) + { + yield return MyUnmanagedStruct.Generate(rng); + } + } + + private static ReadOnlySequence CreateSegmentedByteSequence(T[] array, int maxSegmentLength) where T : unmanaged + { + byte[] arrayBytes = MemoryMarshal.AsBytes(array.AsSpan()).ToArray(); + var memory = new Memory(arrayBytes); + int index = 0; + + BytesReadOnlySequenceSegment first = null, last = null; + + while (index < memory.Length) + { + int nextSegmentLength = Math.Min(maxSegmentLength, memory.Length - index); + var nextSegment = memory.Slice(index, nextSegmentLength); + + if (first == null) + { + first = last = new BytesReadOnlySequenceSegment(nextSegment); + } + else + { + last = last.Append(nextSegment); + } + + index += nextSegmentLength; + } + + return new ReadOnlySequence(first, 0, last, (int)(memory.Length - last.RunningIndex)); + } + } +} diff --git a/src/Ryujinx.Tests/Cpu/CpuContext.cs b/src/Ryujinx.Tests/Cpu/CpuContext.cs index 96b4965a2..81e8ba8c9 100644 --- a/src/Ryujinx.Tests/Cpu/CpuContext.cs +++ b/src/Ryujinx.Tests/Cpu/CpuContext.cs @@ -1,3 +1,4 @@ +using ARMeilleure.Common; using ARMeilleure.Memory; using ARMeilleure.State; using ARMeilleure.Translation; @@ -12,7 +13,7 @@ namespace Ryujinx.Tests.Cpu public CpuContext(IMemoryManager memory, bool for64Bit) { - _translator = new Translator(new JitMemoryAllocator(), memory, for64Bit); + _translator = new Translator(new JitMemoryAllocator(), memory, AddressTable.CreateForArm(for64Bit, memory.Type)); memory.UnmapEvent += UnmapHandler; } diff --git a/src/Ryujinx.Tests/Cpu/CpuTest.cs b/src/Ryujinx.Tests/Cpu/CpuTest.cs index 35158c0b4..da0f03e6b 100644 --- a/src/Ryujinx.Tests/Cpu/CpuTest.cs +++ b/src/Ryujinx.Tests/Cpu/CpuTest.cs @@ -61,7 +61,6 @@ namespace Ryujinx.Tests.Cpu _memory.Map(DataBaseAddress, Size, Size, MemoryMapFlags.Private); _context = CpuContext.CreateExecutionContext(); - Translator.IsReadyForTranslation.Set(); _cpuContext = new CpuContext(_memory, for64Bit: true); diff --git a/src/Ryujinx.Tests/Cpu/CpuTest32.cs b/src/Ryujinx.Tests/Cpu/CpuTest32.cs index f5eb94fa9..6a690834f 100644 --- a/src/Ryujinx.Tests/Cpu/CpuTest32.cs +++ b/src/Ryujinx.Tests/Cpu/CpuTest32.cs @@ -56,7 +56,6 @@ namespace Ryujinx.Tests.Cpu _context = CpuContext.CreateExecutionContext(); _context.IsAarch32 = true; - Translator.IsReadyForTranslation.Set(); _cpuContext = new CpuContext(_memory, for64Bit: false); diff --git a/src/Ryujinx.Tests/Cpu/CpuTestAlu32.cs b/src/Ryujinx.Tests/Cpu/CpuTestAlu32.cs index 41365c624..1e66d8112 100644 --- a/src/Ryujinx.Tests/Cpu/CpuTestAlu32.cs +++ b/src/Ryujinx.Tests/Cpu/CpuTestAlu32.cs @@ -25,6 +25,25 @@ namespace Ryujinx.Tests.Cpu }; } + private static uint[] UQAddSub16() + { + return new[] + { + 0xe6200f10u, // QADD16 R0, R0, R0 + 0xe6600f10u, // UQADD16 R0, R0, R0 + 0xe6600f70u, // UQSUB16 R0, R0, R0 + }; + } + + private static uint[] UQAddSub8() + { + return new[] + { + 0xe6600f90u, // UQADD8 R0, R0, R0 + 0xe6600ff0u, // UQSUB8 R0, R0, R0 + }; + } + private static uint[] SsatUsat() { return new[] @@ -182,6 +201,42 @@ namespace Ryujinx.Tests.Cpu CompareAgainstUnicorn(); } + [Test, Pairwise] + public void U_Q_AddSub_16([ValueSource(nameof(UQAddSub16))] uint opcode, + [Values(0u, 0xdu)] uint rd, + [Values(1u)] uint rm, + [Values(2u)] uint rn, + [Random(RndCnt)] uint w0, + [Random(RndCnt)] uint w1, + [Random(RndCnt)] uint w2) + { + opcode |= ((rm & 15) << 0) | ((rd & 15) << 12) | ((rn & 15) << 16); + + uint sp = TestContext.CurrentContext.Random.NextUInt(); + + SingleOpcode(opcode, r0: w0, r1: w1, r2: w2, sp: sp); + + CompareAgainstUnicorn(); + } + + [Test, Pairwise] + public void U_Q_AddSub_8([ValueSource(nameof(UQAddSub8))] uint opcode, + [Values(0u, 0xdu)] uint rd, + [Values(1u)] uint rm, + [Values(2u)] uint rn, + [Random(RndCnt)] uint w0, + [Random(RndCnt)] uint w1, + [Random(RndCnt)] uint w2) + { + opcode |= ((rm & 15) << 0) | ((rd & 15) << 12) | ((rn & 15) << 16); + + uint sp = TestContext.CurrentContext.Random.NextUInt(); + + SingleOpcode(opcode, r0: w0, r1: w1, r2: w2, sp: sp); + + CompareAgainstUnicorn(); + } + [Test, Pairwise] public void Uadd8_Sel([Values(0u)] uint rd, [Values(1u)] uint rm, diff --git a/src/Ryujinx.Tests/Cpu/CpuTestSimd32.cs b/src/Ryujinx.Tests/Cpu/CpuTestSimd32.cs index 6087a6834..08202c9e1 100644 --- a/src/Ryujinx.Tests/Cpu/CpuTestSimd32.cs +++ b/src/Ryujinx.Tests/Cpu/CpuTestSimd32.cs @@ -327,6 +327,55 @@ namespace Ryujinx.Tests.Cpu CompareAgainstUnicorn(); } + + [Test, Pairwise, Description("VSHLL. {}, , #")] + public void Vshll([Values(0u, 2u)] uint rd, + [Values(1u, 0u)] uint rm, + [Values(0u, 1u, 2u)] uint size, + [Random(RndCnt)] ulong z, + [Random(RndCnt)] ulong a, + [Random(RndCnt)] ulong b) + { + uint opcode = 0xf3b20300u; // VSHLL.I8 Q0, D0, #8 + + opcode |= ((rm & 0xf) << 0) | ((rm & 0x10) << 1); + opcode |= ((rd & 0xf) << 12) | ((rd & 0x10) << 18); + opcode |= size << 18; + + V128 v0 = MakeVectorE0E1(z, z); + V128 v1 = MakeVectorE0E1(a, z); + V128 v2 = MakeVectorE0E1(b, z); + + SingleOpcode(opcode, v0: v0, v1: v1, v2: v2); + + CompareAgainstUnicorn(); + } + + [Test, Pairwise, Description("VSWP D0, D0")] + public void Vswp([Values(0u, 1u)] uint rd, + [Values(0u, 1u)] uint rm, + [Values] bool q) + { + uint opcode = 0xf3b20000u; // VSWP D0, D0 + + if (q) + { + opcode |= 1u << 6; + + rd &= ~1u; + rm &= ~1u; + } + + opcode |= ((rd & 0xf) << 12) | ((rd & 0x10) << 18); + opcode |= ((rm & 0xf) << 0) | ((rm & 0x10) << 1); + + V128 v0 = new(TestContext.CurrentContext.Random.NextULong(), TestContext.CurrentContext.Random.NextULong()); + V128 v1 = new(TestContext.CurrentContext.Random.NextULong(), TestContext.CurrentContext.Random.NextULong()); + + SingleOpcode(opcode, v0: v0, v1: v1); + + CompareAgainstUnicorn(); + } #endif } } diff --git a/src/Ryujinx.Tests/Cpu/CpuTestSimdCvt32.cs b/src/Ryujinx.Tests/Cpu/CpuTestSimdCvt32.cs index 5b24432bb..ba201a480 100644 --- a/src/Ryujinx.Tests/Cpu/CpuTestSimdCvt32.cs +++ b/src/Ryujinx.Tests/Cpu/CpuTestSimdCvt32.cs @@ -511,6 +511,45 @@ namespace Ryujinx.Tests.Cpu CompareAgainstUnicorn(); } + + [Test, Pairwise, Description("VRINTR.F , ")] + [Platform(Exclude = "Linux,MacOsX")] // Instruction isn't testable due to Unicorn. + public void Vrintr([Values(0u, 1u)] uint rd, + [Values(0u, 1u)] uint rm, + [Values(2u, 3u)] uint size, + [ValueSource(nameof(_1D_F_))] ulong s0, + [ValueSource(nameof(_1D_F_))] ulong s1, + [ValueSource(nameof(_1D_F_))] ulong s2, + [Values(RMode.Rn, RMode.Rm, RMode.Rp)] RMode rMode) + { + uint opcode = 0xEEB60A40; + + V128 v0, v1, v2; + + if (size == 2) + { + opcode |= ((rm & 0x1e) >> 1) | ((rm & 0x1) << 5); + opcode |= ((rd & 0x1e) << 11) | ((rd & 0x1) << 22); + v0 = MakeVectorE0E1((uint)BitConverter.SingleToInt32Bits(s0), (uint)BitConverter.SingleToInt32Bits(s0)); + v1 = MakeVectorE0E1((uint)BitConverter.SingleToInt32Bits(s1), (uint)BitConverter.SingleToInt32Bits(s0)); + v2 = MakeVectorE0E1((uint)BitConverter.SingleToInt32Bits(s2), (uint)BitConverter.SingleToInt32Bits(s1)); + } + else + { + opcode |= ((rm & 0xf) << 0) | ((rm & 0x10) << 1); + opcode |= ((rd & 0xf) << 12) | ((rd & 0x10) << 18); + v0 = MakeVectorE0E1((uint)BitConverter.DoubleToInt64Bits(s0), (uint)BitConverter.DoubleToInt64Bits(s0)); + v1 = MakeVectorE0E1((uint)BitConverter.DoubleToInt64Bits(s1), (uint)BitConverter.DoubleToInt64Bits(s0)); + v2 = MakeVectorE0E1((uint)BitConverter.DoubleToInt64Bits(s2), (uint)BitConverter.DoubleToInt64Bits(s1)); + } + + opcode |= ((size & 3) << 8); + + int fpscr = (int)rMode << (int)Fpcr.RMode; + SingleOpcode(opcode, v0: v0, v1: v1, v2: v2, fpscr: fpscr); + + CompareAgainstUnicorn(); + } #endif } } diff --git a/src/Ryujinx.Tests/Cpu/CpuTestSimdReg32.cs b/src/Ryujinx.Tests/Cpu/CpuTestSimdReg32.cs index 9d9606bba..843273dc2 100644 --- a/src/Ryujinx.Tests/Cpu/CpuTestSimdReg32.cs +++ b/src/Ryujinx.Tests/Cpu/CpuTestSimdReg32.cs @@ -908,6 +908,77 @@ namespace Ryujinx.Tests.Cpu CompareAgainstUnicorn(); } + + [Test, Pairwise, Description("VQRDMULH. , , ")] + public void Vqrdmulh_I([Range(0u, 5u)] uint rd, + [Range(0u, 5u)] uint rn, + [Range(0u, 5u)] uint rm, + [ValueSource(nameof(_8B4H2S1D_))] ulong z, + [ValueSource(nameof(_8B4H2S1D_))] ulong a, + [ValueSource(nameof(_8B4H2S1D_))] ulong b, + [Values(1u, 2u)] uint size) // + { + rd >>= 1; + rd <<= 1; + rn >>= 1; + rn <<= 1; + rm >>= 1; + rm <<= 1; + + uint opcode = 0xf3100b40u & ~(3u << 20); // VQRDMULH.S16 Q0, Q0, Q0 + + opcode |= ((rd & 0xf) << 12) | ((rd & 0x10) << 18); + opcode |= ((rn & 0xf) << 16) | ((rn & 0x10) << 3); + opcode |= ((rm & 0xf) << 0) | ((rm & 0x10) << 1); + + opcode |= (size & 0x3) << 20; + + V128 v0 = MakeVectorE0E1(z, ~z); + V128 v1 = MakeVectorE0E1(a, ~a); + V128 v2 = MakeVectorE0E1(b, ~b); + + SingleOpcode(opcode, v0: v0, v1: v1, v2: v2); + + CompareAgainstUnicorn(); + } + + [Test, Pairwise] + public void Vp_Add_Long_Accumulate([Values(0u, 2u, 4u, 8u)] uint rd, + [Values(0u, 2u, 4u, 8u)] uint rm, + [Values(0u, 1u, 2u)] uint size, + [Random(RndCnt)] ulong z, + [Random(RndCnt)] ulong a, + [Random(RndCnt)] ulong b, + [Values] bool q, + [Values] bool unsigned) + { + uint opcode = 0xF3B00600; // VPADAL.S8 D0, Q0 + + if (q) + { + opcode |= 1 << 6; + rm <<= 1; + rd <<= 1; + } + + if (unsigned) + { + opcode |= 1 << 7; + } + + opcode |= ((rm & 0xf) << 0) | ((rm & 0x10) << 1); + opcode |= ((rd & 0xf) << 12) | ((rd & 0x10) << 18); + + opcode |= size << 18; + + V128 v0 = MakeVectorE0E1(z, z); + V128 v1 = MakeVectorE0E1(a, z); + V128 v2 = MakeVectorE0E1(b, z); + + SingleOpcode(opcode, v0: v0, v1: v1, v2: v2); + + CompareAgainstUnicorn(); + } #endif } } diff --git a/src/Ryujinx.Tests/Cpu/CpuTestSimdShImm.cs b/src/Ryujinx.Tests/Cpu/CpuTestSimdShImm.cs index fbac54c8c..9816bc2cc 100644 --- a/src/Ryujinx.Tests/Cpu/CpuTestSimdShImm.cs +++ b/src/Ryujinx.Tests/Cpu/CpuTestSimdShImm.cs @@ -311,6 +311,46 @@ namespace Ryujinx.Tests.Cpu }; } + private static uint[] _ShlImm_S_D_() + { + return new[] + { + 0x5F407400u, // SQSHL D0, D0, #0 + }; + } + + private static uint[] _ShlImm_V_8B_16B_() + { + return new[] + { + 0x0F087400u, // SQSHL V0.8B, V0.8B, #0 + }; + } + + private static uint[] _ShlImm_V_4H_8H_() + { + return new[] + { + 0x0F107400u, // SQSHL V0.4H, V0.4H, #0 + }; + } + + private static uint[] _ShlImm_V_2S_4S_() + { + return new[] + { + 0x0F207400u, // SQSHL V0.2S, V0.2S, #0 + }; + } + + private static uint[] _ShlImm_V_2D_() + { + return new[] + { + 0x4F407400u, // SQSHL V0.2D, V0.2D, #0 + }; + } + private static uint[] _ShrImm_Sri_S_D_() { return new[] @@ -813,6 +853,117 @@ namespace Ryujinx.Tests.Cpu CompareAgainstUnicorn(); } + [Test, Pairwise] + public void ShlImm_S_D([ValueSource(nameof(_ShlImm_S_D_))] uint opcodes, + [Values(0u)] uint rd, + [Values(1u, 0u)] uint rn, + [ValueSource(nameof(_1D_))] ulong z, + [ValueSource(nameof(_1D_))] ulong a, + [Values(1u, 64u)] uint shift) + { + uint immHb = (64 + shift) & 0x7F; + + opcodes |= ((rn & 31) << 5) | ((rd & 31) << 0); + opcodes |= (immHb << 16); + + V128 v0 = MakeVectorE0E1(z, z); + V128 v1 = MakeVectorE0(a); + + SingleOpcode(opcodes, v0: v0, v1: v1); + + CompareAgainstUnicorn(); + } + + [Test, Pairwise] + public void ShlImm_V_8B_16B([ValueSource(nameof(_ShlImm_V_8B_16B_))] uint opcodes, + [Values(0u)] uint rd, + [Values(1u, 0u)] uint rn, + [ValueSource(nameof(_8B_))] ulong z, + [ValueSource(nameof(_8B_))] ulong a, + [Values(1u, 8u)] uint shift, + [Values(0b0u, 0b1u)] uint q) // <8B, 16B> + { + uint immHb = (8 + shift) & 0x7F; + + opcodes |= ((rn & 31) << 5) | ((rd & 31) << 0); + opcodes |= (immHb << 16); + opcodes |= ((q & 1) << 30); + + V128 v0 = MakeVectorE0E1(z, z); + V128 v1 = MakeVectorE0E1(a, a * q); + + SingleOpcode(opcodes, v0: v0, v1: v1); + + CompareAgainstUnicorn(); + } + + [Test, Pairwise] + public void ShlImm_V_4H_8H([ValueSource(nameof(_ShlImm_V_4H_8H_))] uint opcodes, + [Values(0u)] uint rd, + [Values(1u, 0u)] uint rn, + [ValueSource(nameof(_4H_))] ulong z, + [ValueSource(nameof(_4H_))] ulong a, + [Values(1u, 16u)] uint shift, + [Values(0b0u, 0b1u)] uint q) // <4H, 8H> + { + uint immHb = (16 + shift) & 0x7F; + + opcodes |= ((rn & 31) << 5) | ((rd & 31) << 0); + opcodes |= (immHb << 16); + opcodes |= ((q & 1) << 30); + + V128 v0 = MakeVectorE0E1(z, z); + V128 v1 = MakeVectorE0E1(a, a * q); + + SingleOpcode(opcodes, v0: v0, v1: v1); + + CompareAgainstUnicorn(); + } + + [Test, Pairwise] + public void ShlImm_V_2S_4S([ValueSource(nameof(_ShlImm_V_2S_4S_))] uint opcodes, + [Values(0u)] uint rd, + [Values(1u, 0u)] uint rn, + [ValueSource(nameof(_2S_))] ulong z, + [ValueSource(nameof(_2S_))] ulong a, + [Values(1u, 32u)] uint shift, + [Values(0b0u, 0b1u)] uint q) // <2S, 4S> + { + uint immHb = (32 + shift) & 0x7F; + + opcodes |= ((rn & 31) << 5) | ((rd & 31) << 0); + opcodes |= (immHb << 16); + opcodes |= (((q | (immHb >> 6)) & 1) << 30); + + V128 v0 = MakeVectorE0E1(z, z); + V128 v1 = MakeVectorE0E1(a, a * q); + + SingleOpcode(opcodes, v0: v0, v1: v1); + + CompareAgainstUnicorn(); + } + + [Test, Pairwise] + public void ShlImm_V_2D([ValueSource(nameof(_ShlImm_V_2D_))] uint opcodes, + [Values(0u)] uint rd, + [Values(1u, 0u)] uint rn, + [ValueSource(nameof(_1D_))] ulong z, + [ValueSource(nameof(_1D_))] ulong a, + [Values(1u, 64u)] uint shift) + { + uint immHb = (64 + shift) & 0x7F; + + opcodes |= ((rn & 31) << 5) | ((rd & 31) << 0); + opcodes |= (immHb << 16); + + V128 v0 = MakeVectorE0E1(z, z); + V128 v1 = MakeVectorE0E1(a, a); + + SingleOpcode(opcodes, v0: v0, v1: v1); + + CompareAgainstUnicorn(); + } + [Test, Pairwise] public void ShrImm_Sri_S_D([ValueSource(nameof(_ShrImm_Sri_S_D_))] uint opcodes, [Values(0u)] uint rd, diff --git a/src/Ryujinx.Tests/Cpu/CpuTestSimdShImm32.cs b/src/Ryujinx.Tests/Cpu/CpuTestSimdShImm32.cs index 39b50867f..7375f4d55 100644 --- a/src/Ryujinx.Tests/Cpu/CpuTestSimdShImm32.cs +++ b/src/Ryujinx.Tests/Cpu/CpuTestSimdShImm32.cs @@ -202,7 +202,7 @@ namespace Ryujinx.Tests.Cpu } [Test, Pairwise, Description("VSHL. {}, , #")] - public void Vshl_Imm([Values(0u)] uint rd, + public void Vshl_Imm([Values(0u, 1u)] uint rd, [Values(2u, 0u)] uint rm, [Values(0u, 1u, 2u, 3u)] uint size, [Random(RndCntShiftImm)] uint shiftImm, @@ -262,6 +262,40 @@ namespace Ryujinx.Tests.Cpu CompareAgainstUnicorn(); } + [Test, Pairwise, Description("VSLI. {}, , #")] + public void Vsli([Values(0u, 1u)] uint rd, + [Values(2u, 0u)] uint rm, + [Values(0u, 1u, 2u, 3u)] uint size, + [Random(RndCntShiftImm)] uint shiftImm, + [Random(RndCnt)] ulong z, + [Random(RndCnt)] ulong a, + [Random(RndCnt)] ulong b, + [Values] bool q) + { + uint opcode = 0xf3800510u; // VORR.I32 D0, #0x800000 (immediate value changes it into SLI) + if (q) + { + opcode |= 1 << 6; + rm <<= 1; + rd <<= 1; + } + + uint imm = 1u << ((int)size + 3); + imm |= shiftImm & (imm - 1); + + opcode |= ((rm & 0xf) << 0) | ((rm & 0x10) << 1); + opcode |= ((rd & 0xf) << 12) | ((rd & 0x10) << 18); + opcode |= ((imm & 0x3f) << 16) | ((imm & 0x40) << 1); + + V128 v0 = MakeVectorE0E1(z, z); + V128 v1 = MakeVectorE0E1(a, z); + V128 v2 = MakeVectorE0E1(b, z); + + SingleOpcode(opcode, v0: v0, v1: v1, v2: v2); + + CompareAgainstUnicorn(); + } + [Test, Pairwise] public void Vqshrn_Vqrshrn_Vrshrn_Imm([ValueSource(nameof(_Vqshrn_Vqrshrn_Vrshrn_Imm_))] uint opcode, [Values(0u, 1u)] uint rd, diff --git a/src/Ryujinx.Tests/Cpu/EnvironmentTests.cs b/src/Ryujinx.Tests/Cpu/EnvironmentTests.cs index 2a4775a31..43c84c193 100644 --- a/src/Ryujinx.Tests/Cpu/EnvironmentTests.cs +++ b/src/Ryujinx.Tests/Cpu/EnvironmentTests.cs @@ -1,3 +1,5 @@ +using ARMeilleure.Common; +using ARMeilleure.Memory; using ARMeilleure.Translation; using NUnit.Framework; using Ryujinx.Cpu.Jit; @@ -17,7 +19,10 @@ namespace Ryujinx.Tests.Cpu private static void EnsureTranslator() { // Create a translator, as one is needed to register the signal handler or emit methods. - _translator ??= new Translator(new JitMemoryAllocator(), new MockMemoryManager(), true); + _translator ??= new Translator( + new JitMemoryAllocator(), + new MockMemoryManager(), + AddressTable.CreateForArm(true, MemoryManagerType.SoftwarePageTable)); } [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] diff --git a/src/Ryujinx.Tests/Memory/MockMemoryManager.cs b/src/Ryujinx.Tests/Memory/MockMemoryManager.cs index 20c318de6..207d28f50 100644 --- a/src/Ryujinx.Tests/Memory/MockMemoryManager.cs +++ b/src/Ryujinx.Tests/Memory/MockMemoryManager.cs @@ -7,7 +7,7 @@ namespace Ryujinx.Tests.Memory { public int AddressSpaceBits => throw new NotImplementedException(); - public IntPtr PageTablePointer => throw new NotImplementedException(); + public nint PageTablePointer => throw new NotImplementedException(); public MemoryManagerType Type => MemoryManagerType.HostMappedUnsafe; diff --git a/src/Ryujinx.Tests/Memory/PartialUnmaps.cs b/src/Ryujinx.Tests/Memory/PartialUnmaps.cs index 04f7f40e6..3e5b47423 100644 --- a/src/Ryujinx.Tests/Memory/PartialUnmaps.cs +++ b/src/Ryujinx.Tests/Memory/PartialUnmaps.cs @@ -1,3 +1,5 @@ +using ARMeilleure.Common; +using ARMeilleure.Memory; using ARMeilleure.Signal; using ARMeilleure.Translation; using NUnit.Framework; @@ -53,7 +55,10 @@ namespace Ryujinx.Tests.Memory private static void EnsureTranslator() { // Create a translator, as one is needed to register the signal handler or emit methods. - _translator ??= new Translator(new JitMemoryAllocator(), new MockMemoryManager(), true); + _translator ??= new Translator( + new JitMemoryAllocator(), + new MockMemoryManager(), + AddressTable.CreateForArm(true, MemoryManagerType.SoftwarePageTable)); } [Test] @@ -237,7 +242,7 @@ namespace Ryujinx.Tests.Memory mainMemory.MapView(backing, 0, 0, vaSize); var writeFunc = TestMethods.GenerateDebugNativeWriteLoop(); - IntPtr writePtr = mainMemory.GetPointer(vaSize - 0x1000, 4); + nint writePtr = mainMemory.GetPointer(vaSize - 0x1000, 4); Thread testThread = new(() => { @@ -330,7 +335,7 @@ namespace Ryujinx.Tests.Memory fixed (void* localMap = &state.LocalCounts) { - var getOrReserve = TestMethods.GenerateDebugThreadLocalMapGetOrReserve((IntPtr)localMap); + var getOrReserve = TestMethods.GenerateDebugThreadLocalMapGetOrReserve((nint)localMap); for (int i = 0; i < ThreadLocalMap.MapSize; i++) { @@ -388,14 +393,14 @@ namespace Ryujinx.Tests.Memory { rwLock.AcquireReaderLock(); - int originalValue = Thread.VolatileRead(ref value); + int originalValue = Volatile.Read(ref value); count++; // Spin a bit. for (int i = 0; i < 100; i++) { - if (Thread.VolatileRead(ref readersAllowed) == 0) + if (Volatile.Read(ref readersAllowed) == 0) { error = true; running = false; @@ -403,7 +408,7 @@ namespace Ryujinx.Tests.Memory } // Should not change while the lock is held. - if (Thread.VolatileRead(ref value) != originalValue) + if (Volatile.Read(ref value) != originalValue) { error = true; running = false; diff --git a/src/Ryujinx.UI.Common/App/LdnGameData.cs b/src/Ryujinx.UI.Common/App/LdnGameData.cs new file mode 100644 index 000000000..6c784c991 --- /dev/null +++ b/src/Ryujinx.UI.Common/App/LdnGameData.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; + +namespace Ryujinx.UI.App.Common +{ + public struct LdnGameData + { + public string Id { get; set; } + public int PlayerCount { get; set; } + public int MaxPlayerCount { get; set; } + public string GameName { get; set; } + public string TitleId { get; set; } + public string Mode { get; set; } + public string Status { get; set; } + public IEnumerable Players { get; set; } + } +} diff --git a/src/Ryujinx.UI.Common/App/LdnGameDataReceivedEventArgs.cs b/src/Ryujinx.UI.Common/App/LdnGameDataReceivedEventArgs.cs new file mode 100644 index 000000000..7c7454411 --- /dev/null +++ b/src/Ryujinx.UI.Common/App/LdnGameDataReceivedEventArgs.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; + +namespace Ryujinx.UI.App.Common +{ + public class LdnGameDataReceivedEventArgs : EventArgs + { + public IEnumerable LdnData { get; set; } + } +} diff --git a/src/Ryujinx.UI.Common/App/LdnGameDataSerializerContext.cs b/src/Ryujinx.UI.Common/App/LdnGameDataSerializerContext.cs new file mode 100644 index 000000000..ce8edcdb6 --- /dev/null +++ b/src/Ryujinx.UI.Common/App/LdnGameDataSerializerContext.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace Ryujinx.UI.App.Common +{ + [JsonSerializable(typeof(IEnumerable))] + internal partial class LdnGameDataSerializerContext : JsonSerializerContext + { + + } +} diff --git a/src/Ryujinx.UI.Common/Configuration/ConfigurationState.Migration.cs b/src/Ryujinx.UI.Common/Configuration/ConfigurationState.Migration.cs new file mode 100644 index 000000000..a41ea2cd7 --- /dev/null +++ b/src/Ryujinx.UI.Common/Configuration/ConfigurationState.Migration.cs @@ -0,0 +1,747 @@ +using Ryujinx.Common.Configuration; +using Ryujinx.Common.Configuration.Hid; +using Ryujinx.Common.Configuration.Hid.Controller; +using Ryujinx.Common.Configuration.Hid.Keyboard; +using Ryujinx.Common.Configuration.Multiplayer; +using Ryujinx.Common.Logging; +using Ryujinx.HLE; +using Ryujinx.UI.Common.Configuration.System; +using Ryujinx.UI.Common.Configuration.UI; +using System; +using System.Collections.Generic; + +namespace Ryujinx.UI.Common.Configuration +{ + public partial class ConfigurationState + { + public void Load(ConfigurationFileFormat configurationFileFormat, string configurationFilePath) + { + bool configurationFileUpdated = false; + + if (configurationFileFormat.Version is < 0 or > ConfigurationFileFormat.CurrentVersion) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Unsupported configuration version {configurationFileFormat.Version}, loading default."); + + LoadDefault(); + } + + if (configurationFileFormat.Version < 2) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 2."); + + configurationFileFormat.SystemRegion = Region.USA; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 3) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 3."); + + configurationFileFormat.SystemTimeZone = "UTC"; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 4) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 4."); + + configurationFileFormat.MaxAnisotropy = -1; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 5) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 5."); + + configurationFileFormat.SystemTimeOffset = 0; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 8) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 8."); + + configurationFileFormat.EnablePtc = true; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 9) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 9."); + + configurationFileFormat.ColumnSort = new ColumnSort + { + SortColumnId = 0, + SortAscending = false, + }; + + configurationFileFormat.Hotkeys = new KeyboardHotkeys + { + ToggleVSyncMode = Key.F1, + }; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 10) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 10."); + + configurationFileFormat.AudioBackend = AudioBackend.OpenAl; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 11) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 11."); + + configurationFileFormat.ResScale = 1; + configurationFileFormat.ResScaleCustom = 1.0f; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 12) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 12."); + + configurationFileFormat.LoggingGraphicsDebugLevel = GraphicsDebugLevel.None; + + configurationFileUpdated = true; + } + + // configurationFileFormat.Version == 13 -> LDN1 + + if (configurationFileFormat.Version < 14) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 14."); + + configurationFileFormat.CheckUpdatesOnStart = true; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 16) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 16."); + + configurationFileFormat.EnableShaderCache = true; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 17) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 17."); + + configurationFileFormat.StartFullscreen = false; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 18) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 18."); + + configurationFileFormat.AspectRatio = AspectRatio.Fixed16x9; + + configurationFileUpdated = true; + } + + // configurationFileFormat.Version == 19 -> LDN2 + + if (configurationFileFormat.Version < 20) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 20."); + + configurationFileFormat.ShowConfirmExit = true; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 21) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 21."); + + // Initialize network config. + + configurationFileFormat.MultiplayerMode = MultiplayerMode.Disabled; + configurationFileFormat.MultiplayerLanInterfaceId = "0"; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 22) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 22."); + + configurationFileFormat.HideCursor = HideCursorMode.Never; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 24) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 24."); + + configurationFileFormat.InputConfig = new List + { + new StandardKeyboardInputConfig + { + Version = InputConfig.CurrentVersion, + Backend = InputBackendType.WindowKeyboard, + Id = "0", + PlayerIndex = PlayerIndex.Player1, + ControllerType = ControllerType.ProController, + LeftJoycon = new LeftJoyconCommonConfig + { + DpadUp = Key.Up, + DpadDown = Key.Down, + DpadLeft = Key.Left, + DpadRight = Key.Right, + ButtonMinus = Key.Minus, + ButtonL = Key.E, + ButtonZl = Key.Q, + ButtonSl = Key.Unbound, + ButtonSr = Key.Unbound, + }, + LeftJoyconStick = new JoyconConfigKeyboardStick + { + StickUp = Key.W, + StickDown = Key.S, + StickLeft = Key.A, + StickRight = Key.D, + StickButton = Key.F, + }, + RightJoycon = new RightJoyconCommonConfig + { + ButtonA = Key.Z, + ButtonB = Key.X, + ButtonX = Key.C, + ButtonY = Key.V, + ButtonPlus = Key.Plus, + ButtonR = Key.U, + ButtonZr = Key.O, + ButtonSl = Key.Unbound, + ButtonSr = Key.Unbound, + }, + RightJoyconStick = new JoyconConfigKeyboardStick + { + StickUp = Key.I, + StickDown = Key.K, + StickLeft = Key.J, + StickRight = Key.L, + StickButton = Key.H, + }, + }, + }; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 25) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 25."); + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 26) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 26."); + + configurationFileFormat.MemoryManagerMode = MemoryManagerMode.HostMappedUnsafe; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 27) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 27."); + + configurationFileFormat.EnableMouse = false; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 28) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 28."); + + configurationFileFormat.Hotkeys = new KeyboardHotkeys + { + ToggleVSyncMode = Key.F1, + Screenshot = Key.F8, + }; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 29) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 29."); + + configurationFileFormat.Hotkeys = new KeyboardHotkeys + { + ToggleVSyncMode = Key.F1, + Screenshot = Key.F8, + ShowUI = Key.F4, + }; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 30) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 30."); + + foreach (InputConfig config in configurationFileFormat.InputConfig) + { + if (config is StandardControllerInputConfig controllerConfig) + { + controllerConfig.Rumble = new RumbleConfigController + { + EnableRumble = false, + StrongRumble = 1f, + WeakRumble = 1f, + }; + } + } + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 31) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 31."); + + configurationFileFormat.BackendThreading = BackendThreading.Auto; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 32) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 32."); + + configurationFileFormat.Hotkeys = new KeyboardHotkeys + { + ToggleVSyncMode = configurationFileFormat.Hotkeys.ToggleVSyncMode, + Screenshot = configurationFileFormat.Hotkeys.Screenshot, + ShowUI = configurationFileFormat.Hotkeys.ShowUI, + Pause = Key.F5, + }; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 33) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 33."); + + configurationFileFormat.Hotkeys = new KeyboardHotkeys + { + ToggleVSyncMode = configurationFileFormat.Hotkeys.ToggleVSyncMode, + Screenshot = configurationFileFormat.Hotkeys.Screenshot, + ShowUI = configurationFileFormat.Hotkeys.ShowUI, + Pause = configurationFileFormat.Hotkeys.Pause, + ToggleMute = Key.F2, + }; + + configurationFileFormat.AudioVolume = 1; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 34) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 34."); + + configurationFileFormat.EnableInternetAccess = false; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 35) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 35."); + + foreach (InputConfig config in configurationFileFormat.InputConfig) + { + if (config is StandardControllerInputConfig controllerConfig) + { + controllerConfig.RangeLeft = 1.0f; + controllerConfig.RangeRight = 1.0f; + } + } + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 36) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 36."); + + configurationFileFormat.LoggingEnableTrace = false; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 37) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 37."); + + configurationFileFormat.ShowConsole = true; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 38) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 38."); + + configurationFileFormat.BaseStyle = "Dark"; + configurationFileFormat.GameListViewMode = 0; + configurationFileFormat.ShowNames = true; + configurationFileFormat.GridSize = 2; + configurationFileFormat.LanguageCode = "en_US"; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 39) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 39."); + + configurationFileFormat.Hotkeys = new KeyboardHotkeys + { + ToggleVSyncMode = configurationFileFormat.Hotkeys.ToggleVSyncMode, + Screenshot = configurationFileFormat.Hotkeys.Screenshot, + ShowUI = configurationFileFormat.Hotkeys.ShowUI, + Pause = configurationFileFormat.Hotkeys.Pause, + ToggleMute = configurationFileFormat.Hotkeys.ToggleMute, + ResScaleUp = Key.Unbound, + ResScaleDown = Key.Unbound, + }; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 40) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 40."); + + configurationFileFormat.GraphicsBackend = GraphicsBackend.OpenGl; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 41) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 41."); + + configurationFileFormat.Hotkeys = new KeyboardHotkeys + { + ToggleVSyncMode = configurationFileFormat.Hotkeys.ToggleVSyncMode, + Screenshot = configurationFileFormat.Hotkeys.Screenshot, + ShowUI = configurationFileFormat.Hotkeys.ShowUI, + Pause = configurationFileFormat.Hotkeys.Pause, + ToggleMute = configurationFileFormat.Hotkeys.ToggleMute, + ResScaleUp = configurationFileFormat.Hotkeys.ResScaleUp, + ResScaleDown = configurationFileFormat.Hotkeys.ResScaleDown, + VolumeUp = Key.Unbound, + VolumeDown = Key.Unbound, + }; + } + + if (configurationFileFormat.Version < 42) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 42."); + + configurationFileFormat.EnableMacroHLE = true; + } + + if (configurationFileFormat.Version < 43) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 43."); + + configurationFileFormat.UseHypervisor = true; + } + + if (configurationFileFormat.Version < 44) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 44."); + + configurationFileFormat.AntiAliasing = AntiAliasing.None; + configurationFileFormat.ScalingFilter = ScalingFilter.Bilinear; + configurationFileFormat.ScalingFilterLevel = 80; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 45) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 45."); + + configurationFileFormat.ShownFileTypes = new ShownFileTypes + { + NSP = true, + PFS0 = true, + XCI = true, + NCA = true, + NRO = true, + NSO = true, + }; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 46) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 46."); + + configurationFileFormat.MultiplayerLanInterfaceId = "0"; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 47) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 47."); + + configurationFileFormat.WindowStartup = new WindowStartup + { + WindowPositionX = 0, + WindowPositionY = 0, + WindowSizeHeight = 760, + WindowSizeWidth = 1280, + WindowMaximized = false, + }; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 48) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 48."); + + configurationFileFormat.EnableColorSpacePassthrough = false; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 49) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 49."); + + if (OperatingSystem.IsMacOS()) + { + AppDataManager.FixMacOSConfigurationFolders(); + } + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 50) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 50."); + + configurationFileFormat.EnableHardwareAcceleration = true; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 51) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 51."); + + configurationFileFormat.RememberWindowState = true; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 52) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 52."); + + configurationFileFormat.AutoloadDirs = []; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 53) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 53."); + + configurationFileFormat.EnableLowPowerPtc = false; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 54) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 54."); + + configurationFileFormat.DramSize = MemoryConfiguration.MemoryConfiguration4GiB; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 55) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 55."); + + configurationFileFormat.IgnoreApplet = false; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 56) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 56."); + + configurationFileFormat.ShowTitleBar = !OperatingSystem.IsWindows(); + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 57) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 57."); + + configurationFileFormat.VSyncMode = VSyncMode.Switch; + configurationFileFormat.EnableCustomVSyncInterval = false; + + configurationFileFormat.Hotkeys = new KeyboardHotkeys + { + ToggleVSyncMode = Key.F1, + Screenshot = configurationFileFormat.Hotkeys.Screenshot, + ShowUI = configurationFileFormat.Hotkeys.ShowUI, + Pause = configurationFileFormat.Hotkeys.Pause, + ToggleMute = configurationFileFormat.Hotkeys.ToggleMute, + ResScaleUp = configurationFileFormat.Hotkeys.ResScaleUp, + ResScaleDown = configurationFileFormat.Hotkeys.ResScaleDown, + VolumeUp = configurationFileFormat.Hotkeys.VolumeUp, + VolumeDown = configurationFileFormat.Hotkeys.VolumeDown, + CustomVSyncIntervalIncrement = Key.Unbound, + CustomVSyncIntervalDecrement = Key.Unbound, + }; + + configurationFileFormat.CustomVSyncInterval = 120; + + configurationFileUpdated = true; + } + + Logger.EnableFileLog.Value = configurationFileFormat.EnableFileLog; + Graphics.ResScale.Value = configurationFileFormat.ResScale; + Graphics.ResScaleCustom.Value = configurationFileFormat.ResScaleCustom; + Graphics.MaxAnisotropy.Value = configurationFileFormat.MaxAnisotropy; + Graphics.AspectRatio.Value = configurationFileFormat.AspectRatio; + Graphics.ShadersDumpPath.Value = configurationFileFormat.GraphicsShadersDumpPath; + Graphics.BackendThreading.Value = configurationFileFormat.BackendThreading; + Graphics.GraphicsBackend.Value = configurationFileFormat.GraphicsBackend; + Graphics.PreferredGpu.Value = configurationFileFormat.PreferredGpu; + Graphics.AntiAliasing.Value = configurationFileFormat.AntiAliasing; + Graphics.ScalingFilter.Value = configurationFileFormat.ScalingFilter; + Graphics.ScalingFilterLevel.Value = configurationFileFormat.ScalingFilterLevel; + Logger.EnableDebug.Value = configurationFileFormat.LoggingEnableDebug; + Logger.EnableStub.Value = configurationFileFormat.LoggingEnableStub; + Logger.EnableInfo.Value = configurationFileFormat.LoggingEnableInfo; + Logger.EnableWarn.Value = configurationFileFormat.LoggingEnableWarn; + Logger.EnableError.Value = configurationFileFormat.LoggingEnableError; + Logger.EnableTrace.Value = configurationFileFormat.LoggingEnableTrace; + Logger.EnableGuest.Value = configurationFileFormat.LoggingEnableGuest; + Logger.EnableFsAccessLog.Value = configurationFileFormat.LoggingEnableFsAccessLog; + Logger.FilteredClasses.Value = configurationFileFormat.LoggingFilteredClasses; + Logger.GraphicsDebugLevel.Value = configurationFileFormat.LoggingGraphicsDebugLevel; + System.Language.Value = configurationFileFormat.SystemLanguage; + System.Region.Value = configurationFileFormat.SystemRegion; + System.TimeZone.Value = configurationFileFormat.SystemTimeZone; + System.SystemTimeOffset.Value = configurationFileFormat.SystemTimeOffset; + System.EnableDockedMode.Value = configurationFileFormat.DockedMode; + EnableDiscordIntegration.Value = configurationFileFormat.EnableDiscordIntegration; + CheckUpdatesOnStart.Value = configurationFileFormat.CheckUpdatesOnStart; + ShowConfirmExit.Value = configurationFileFormat.ShowConfirmExit; + IgnoreApplet.Value = configurationFileFormat.IgnoreApplet; + RememberWindowState.Value = configurationFileFormat.RememberWindowState; + ShowTitleBar.Value = configurationFileFormat.ShowTitleBar; + EnableHardwareAcceleration.Value = configurationFileFormat.EnableHardwareAcceleration; + HideCursor.Value = configurationFileFormat.HideCursor; + Graphics.VSyncMode.Value = configurationFileFormat.VSyncMode; + Graphics.EnableCustomVSyncInterval.Value = configurationFileFormat.EnableCustomVSyncInterval; + Graphics.CustomVSyncInterval.Value = configurationFileFormat.CustomVSyncInterval; + Graphics.EnableShaderCache.Value = configurationFileFormat.EnableShaderCache; + Graphics.EnableTextureRecompression.Value = configurationFileFormat.EnableTextureRecompression; + Graphics.EnableMacroHLE.Value = configurationFileFormat.EnableMacroHLE; + Graphics.EnableColorSpacePassthrough.Value = configurationFileFormat.EnableColorSpacePassthrough; + System.EnablePtc.Value = configurationFileFormat.EnablePtc; + System.EnableLowPowerPtc.Value = configurationFileFormat.EnableLowPowerPtc; + System.EnableInternetAccess.Value = configurationFileFormat.EnableInternetAccess; + System.EnableFsIntegrityChecks.Value = configurationFileFormat.EnableFsIntegrityChecks; + System.FsGlobalAccessLogMode.Value = configurationFileFormat.FsGlobalAccessLogMode; + System.AudioBackend.Value = configurationFileFormat.AudioBackend; + System.AudioVolume.Value = configurationFileFormat.AudioVolume; + System.MemoryManagerMode.Value = configurationFileFormat.MemoryManagerMode; + System.DramSize.Value = configurationFileFormat.DramSize; + System.IgnoreMissingServices.Value = configurationFileFormat.IgnoreMissingServices; + System.UseHypervisor.Value = configurationFileFormat.UseHypervisor; + UI.GuiColumns.FavColumn.Value = configurationFileFormat.GuiColumns.FavColumn; + UI.GuiColumns.IconColumn.Value = configurationFileFormat.GuiColumns.IconColumn; + UI.GuiColumns.AppColumn.Value = configurationFileFormat.GuiColumns.AppColumn; + UI.GuiColumns.DevColumn.Value = configurationFileFormat.GuiColumns.DevColumn; + UI.GuiColumns.VersionColumn.Value = configurationFileFormat.GuiColumns.VersionColumn; + UI.GuiColumns.TimePlayedColumn.Value = configurationFileFormat.GuiColumns.TimePlayedColumn; + UI.GuiColumns.LastPlayedColumn.Value = configurationFileFormat.GuiColumns.LastPlayedColumn; + UI.GuiColumns.FileExtColumn.Value = configurationFileFormat.GuiColumns.FileExtColumn; + UI.GuiColumns.FileSizeColumn.Value = configurationFileFormat.GuiColumns.FileSizeColumn; + UI.GuiColumns.PathColumn.Value = configurationFileFormat.GuiColumns.PathColumn; + UI.ColumnSort.SortColumnId.Value = configurationFileFormat.ColumnSort.SortColumnId; + UI.ColumnSort.SortAscending.Value = configurationFileFormat.ColumnSort.SortAscending; + UI.GameDirs.Value = configurationFileFormat.GameDirs; + UI.AutoloadDirs.Value = configurationFileFormat.AutoloadDirs ?? []; + UI.ShownFileTypes.NSP.Value = configurationFileFormat.ShownFileTypes.NSP; + UI.ShownFileTypes.PFS0.Value = configurationFileFormat.ShownFileTypes.PFS0; + UI.ShownFileTypes.XCI.Value = configurationFileFormat.ShownFileTypes.XCI; + UI.ShownFileTypes.NCA.Value = configurationFileFormat.ShownFileTypes.NCA; + UI.ShownFileTypes.NRO.Value = configurationFileFormat.ShownFileTypes.NRO; + UI.ShownFileTypes.NSO.Value = configurationFileFormat.ShownFileTypes.NSO; + UI.LanguageCode.Value = configurationFileFormat.LanguageCode; + UI.BaseStyle.Value = configurationFileFormat.BaseStyle; + UI.GameListViewMode.Value = configurationFileFormat.GameListViewMode; + UI.ShowNames.Value = configurationFileFormat.ShowNames; + UI.IsAscendingOrder.Value = configurationFileFormat.IsAscendingOrder; + UI.GridSize.Value = configurationFileFormat.GridSize; + UI.ApplicationSort.Value = configurationFileFormat.ApplicationSort; + UI.StartFullscreen.Value = configurationFileFormat.StartFullscreen; + UI.ShowConsole.Value = configurationFileFormat.ShowConsole; + UI.WindowStartup.WindowSizeWidth.Value = configurationFileFormat.WindowStartup.WindowSizeWidth; + UI.WindowStartup.WindowSizeHeight.Value = configurationFileFormat.WindowStartup.WindowSizeHeight; + UI.WindowStartup.WindowPositionX.Value = configurationFileFormat.WindowStartup.WindowPositionX; + UI.WindowStartup.WindowPositionY.Value = configurationFileFormat.WindowStartup.WindowPositionY; + UI.WindowStartup.WindowMaximized.Value = configurationFileFormat.WindowStartup.WindowMaximized; + Hid.EnableKeyboard.Value = configurationFileFormat.EnableKeyboard; + Hid.EnableMouse.Value = configurationFileFormat.EnableMouse; + Hid.Hotkeys.Value = configurationFileFormat.Hotkeys; + Hid.InputConfig.Value = configurationFileFormat.InputConfig ?? []; + + Multiplayer.LanInterfaceId.Value = configurationFileFormat.MultiplayerLanInterfaceId; + Multiplayer.Mode.Value = configurationFileFormat.MultiplayerMode; + Multiplayer.DisableP2p.Value = configurationFileFormat.MultiplayerDisableP2p; + Multiplayer.LdnPassphrase.Value = configurationFileFormat.MultiplayerLdnPassphrase; + Multiplayer.LdnServer.Value = configurationFileFormat.LdnServer; + + if (configurationFileUpdated) + { + ToFileFormat().SaveConfig(configurationFilePath); + + Ryujinx.Common.Logging.Logger.Notice.Print(LogClass.Application, $"Configuration file updated to version {ConfigurationFileFormat.CurrentVersion}"); + } + } + } +} diff --git a/src/Ryujinx.UI.Common/Configuration/ConfigurationState.Model.cs b/src/Ryujinx.UI.Common/Configuration/ConfigurationState.Model.cs new file mode 100644 index 000000000..f28ce0348 --- /dev/null +++ b/src/Ryujinx.UI.Common/Configuration/ConfigurationState.Model.cs @@ -0,0 +1,714 @@ +using ARMeilleure; +using Ryujinx.Common; +using Ryujinx.Common.Configuration; +using Ryujinx.Common.Configuration.Hid; +using Ryujinx.Common.Configuration.Multiplayer; +using Ryujinx.Common.Logging; +using Ryujinx.HLE; +using Ryujinx.UI.Common.Configuration.System; +using Ryujinx.UI.Common.Helper; +using System.Collections.Generic; + +namespace Ryujinx.UI.Common.Configuration +{ + public partial class ConfigurationState + { + /// + /// UI configuration section + /// + public class UISection + { + public class Columns + { + public ReactiveObject FavColumn { get; private set; } + public ReactiveObject IconColumn { get; private set; } + public ReactiveObject AppColumn { get; private set; } + public ReactiveObject DevColumn { get; private set; } + public ReactiveObject VersionColumn { get; private set; } + public ReactiveObject LdnInfoColumn { get; private set; } + public ReactiveObject TimePlayedColumn { get; private set; } + public ReactiveObject LastPlayedColumn { get; private set; } + public ReactiveObject FileExtColumn { get; private set; } + public ReactiveObject FileSizeColumn { get; private set; } + public ReactiveObject PathColumn { get; private set; } + + public Columns() + { + FavColumn = new ReactiveObject(); + IconColumn = new ReactiveObject(); + AppColumn = new ReactiveObject(); + DevColumn = new ReactiveObject(); + VersionColumn = new ReactiveObject(); + LdnInfoColumn = new ReactiveObject(); + TimePlayedColumn = new ReactiveObject(); + LastPlayedColumn = new ReactiveObject(); + FileExtColumn = new ReactiveObject(); + FileSizeColumn = new ReactiveObject(); + PathColumn = new ReactiveObject(); + } + } + + public class ColumnSortSettings + { + public ReactiveObject SortColumnId { get; private set; } + public ReactiveObject SortAscending { get; private set; } + + public ColumnSortSettings() + { + SortColumnId = new ReactiveObject(); + SortAscending = new ReactiveObject(); + } + } + + /// + /// Used to toggle which file types are shown in the UI + /// + public class ShownFileTypeSettings + { + public ReactiveObject NSP { get; private set; } + public ReactiveObject PFS0 { get; private set; } + public ReactiveObject XCI { get; private set; } + public ReactiveObject NCA { get; private set; } + public ReactiveObject NRO { get; private set; } + public ReactiveObject NSO { get; private set; } + + public ShownFileTypeSettings() + { + NSP = new ReactiveObject(); + PFS0 = new ReactiveObject(); + XCI = new ReactiveObject(); + NCA = new ReactiveObject(); + NRO = new ReactiveObject(); + NSO = new ReactiveObject(); + } + } + + // + /// Determines main window start-up position, size and state + /// + public class WindowStartupSettings + { + public ReactiveObject WindowSizeWidth { get; private set; } + public ReactiveObject WindowSizeHeight { get; private set; } + public ReactiveObject WindowPositionX { get; private set; } + public ReactiveObject WindowPositionY { get; private set; } + public ReactiveObject WindowMaximized { get; private set; } + + public WindowStartupSettings() + { + WindowSizeWidth = new ReactiveObject(); + WindowSizeHeight = new ReactiveObject(); + WindowPositionX = new ReactiveObject(); + WindowPositionY = new ReactiveObject(); + WindowMaximized = new ReactiveObject(); + } + } + + /// + /// Used to toggle columns in the GUI + /// + public Columns GuiColumns { get; private set; } + + /// + /// Used to configure column sort settings in the GUI + /// + public ColumnSortSettings ColumnSort { get; private set; } + + /// + /// A list of directories containing games to be used to load games into the games list + /// + public ReactiveObject> GameDirs { get; private set; } + + /// + /// A list of directories containing DLC/updates the user wants to autoload during library refreshes + /// + public ReactiveObject> AutoloadDirs { get; private set; } + + /// + /// A list of file types to be hidden in the games List + /// + public ShownFileTypeSettings ShownFileTypes { get; private set; } + + /// + /// Determines main window start-up position, size and state + /// + public WindowStartupSettings WindowStartup { get; private set; } + + /// + /// Language Code for the UI + /// + public ReactiveObject LanguageCode { get; private set; } + + /// + /// Selects the base style + /// + public ReactiveObject BaseStyle { get; private set; } + + /// + /// Start games in fullscreen mode + /// + public ReactiveObject StartFullscreen { get; private set; } + + /// + /// Hide / Show Console Window + /// + public ReactiveObject ShowConsole { get; private set; } + + /// + /// View Mode of the Game list + /// + public ReactiveObject GameListViewMode { get; private set; } + + /// + /// Show application name in Grid Mode + /// + public ReactiveObject ShowNames { get; private set; } + + /// + /// Sets App Icon Size in Grid Mode + /// + public ReactiveObject GridSize { get; private set; } + + /// + /// Sorts Apps in Grid Mode + /// + public ReactiveObject ApplicationSort { get; private set; } + + /// + /// Sets if Grid is ordered in Ascending Order + /// + public ReactiveObject IsAscendingOrder { get; private set; } + + public UISection() + { + GuiColumns = new Columns(); + ColumnSort = new ColumnSortSettings(); + GameDirs = new ReactiveObject>(); + AutoloadDirs = new ReactiveObject>(); + ShownFileTypes = new ShownFileTypeSettings(); + WindowStartup = new WindowStartupSettings(); + BaseStyle = new ReactiveObject(); + StartFullscreen = new ReactiveObject(); + GameListViewMode = new ReactiveObject(); + ShowNames = new ReactiveObject(); + GridSize = new ReactiveObject(); + ApplicationSort = new ReactiveObject(); + IsAscendingOrder = new ReactiveObject(); + LanguageCode = new ReactiveObject(); + ShowConsole = new ReactiveObject(); + ShowConsole.Event += static (_, e) => ConsoleHelper.SetConsoleWindowState(e.NewValue); + } + } + + /// + /// Logger configuration section + /// + public class LoggerSection + { + /// + /// Enables printing debug log messages + /// + public ReactiveObject EnableDebug { get; private set; } + + /// + /// Enables printing stub log messages + /// + public ReactiveObject EnableStub { get; private set; } + + /// + /// Enables printing info log messages + /// + public ReactiveObject EnableInfo { get; private set; } + + /// + /// Enables printing warning log messages + /// + public ReactiveObject EnableWarn { get; private set; } + + /// + /// Enables printing error log messages + /// + public ReactiveObject EnableError { get; private set; } + + /// + /// Enables printing trace log messages + /// + public ReactiveObject EnableTrace { get; private set; } + + /// + /// Enables printing guest log messages + /// + public ReactiveObject EnableGuest { get; private set; } + + /// + /// Enables printing FS access log messages + /// + public ReactiveObject EnableFsAccessLog { get; private set; } + + /// + /// Controls which log messages are written to the log targets + /// + public ReactiveObject FilteredClasses { get; private set; } + + /// + /// Enables or disables logging to a file on disk + /// + public ReactiveObject EnableFileLog { get; private set; } + + /// + /// Controls which OpenGL log messages are recorded in the log + /// + public ReactiveObject GraphicsDebugLevel { get; private set; } + + public LoggerSection() + { + EnableDebug = new ReactiveObject(); + EnableDebug.LogChangesToValue(nameof(EnableDebug)); + EnableStub = new ReactiveObject(); + EnableInfo = new ReactiveObject(); + EnableWarn = new ReactiveObject(); + EnableError = new ReactiveObject(); + EnableTrace = new ReactiveObject(); + EnableGuest = new ReactiveObject(); + EnableFsAccessLog = new ReactiveObject(); + FilteredClasses = new ReactiveObject(); + EnableFileLog = new ReactiveObject(); + EnableFileLog.LogChangesToValue(nameof(EnableFileLog)); + GraphicsDebugLevel = new ReactiveObject(); + } + } + + /// + /// System configuration section + /// + public class SystemSection + { + /// + /// Change System Language + /// + public ReactiveObject Language { get; private set; } + + /// + /// Change System Region + /// + public ReactiveObject Region { get; private set; } + + /// + /// Change System TimeZone + /// + public ReactiveObject TimeZone { get; private set; } + + /// + /// System Time Offset in Seconds + /// + public ReactiveObject SystemTimeOffset { get; private set; } + + /// + /// Enables or disables Docked Mode + /// + public ReactiveObject EnableDockedMode { get; private set; } + + /// + /// Enables or disables persistent profiled translation cache + /// + public ReactiveObject EnablePtc { get; private set; } + + /// + /// Enables or disables low-power persistent profiled translation cache loading + /// + public ReactiveObject EnableLowPowerPtc { get; private set; } + + /// + /// Enables or disables guest Internet access + /// + public ReactiveObject EnableInternetAccess { get; private set; } + + /// + /// Enables integrity checks on Game content files + /// + public ReactiveObject EnableFsIntegrityChecks { get; private set; } + + /// + /// Enables FS access log output to the console. Possible modes are 0-3 + /// + public ReactiveObject FsGlobalAccessLogMode { get; private set; } + + /// + /// The selected audio backend + /// + public ReactiveObject AudioBackend { get; private set; } + + /// + /// The audio backend volume + /// + public ReactiveObject AudioVolume { get; private set; } + + /// + /// The selected memory manager mode + /// + public ReactiveObject MemoryManagerMode { get; private set; } + + /// + /// Defines the amount of RAM available on the emulated system, and how it is distributed + /// + public ReactiveObject DramSize { get; private set; } + + /// + /// Enable or disable ignoring missing services + /// + public ReactiveObject IgnoreMissingServices { get; private set; } + + /// + /// Uses Hypervisor over JIT if available + /// + public ReactiveObject UseHypervisor { get; private set; } + + public SystemSection() + { + Language = new ReactiveObject(); + Language.LogChangesToValue(nameof(Language)); + Region = new ReactiveObject(); + Region.LogChangesToValue(nameof(Region)); + TimeZone = new ReactiveObject(); + TimeZone.LogChangesToValue(nameof(TimeZone)); + SystemTimeOffset = new ReactiveObject(); + SystemTimeOffset.LogChangesToValue(nameof(SystemTimeOffset)); + EnableDockedMode = new ReactiveObject(); + EnableDockedMode.LogChangesToValue(nameof(EnableDockedMode)); + EnablePtc = new ReactiveObject(); + EnablePtc.LogChangesToValue(nameof(EnablePtc)); + EnableLowPowerPtc = new ReactiveObject(); + EnableLowPowerPtc.LogChangesToValue(nameof(EnableLowPowerPtc)); + EnableLowPowerPtc.Event += (_, evnt) + => Optimizations.LowPower = evnt.NewValue; + EnableInternetAccess = new ReactiveObject(); + EnableInternetAccess.LogChangesToValue(nameof(EnableInternetAccess)); + EnableFsIntegrityChecks = new ReactiveObject(); + EnableFsIntegrityChecks.LogChangesToValue(nameof(EnableFsIntegrityChecks)); + FsGlobalAccessLogMode = new ReactiveObject(); + FsGlobalAccessLogMode.LogChangesToValue(nameof(FsGlobalAccessLogMode)); + AudioBackend = new ReactiveObject(); + AudioBackend.LogChangesToValue(nameof(AudioBackend)); + MemoryManagerMode = new ReactiveObject(); + MemoryManagerMode.LogChangesToValue(nameof(MemoryManagerMode)); + DramSize = new ReactiveObject(); + DramSize.LogChangesToValue(nameof(DramSize)); + IgnoreMissingServices = new ReactiveObject(); + IgnoreMissingServices.LogChangesToValue(nameof(IgnoreMissingServices)); + AudioVolume = new ReactiveObject(); + AudioVolume.LogChangesToValue(nameof(AudioVolume)); + UseHypervisor = new ReactiveObject(); + UseHypervisor.LogChangesToValue(nameof(UseHypervisor)); + } + } + + /// + /// Hid configuration section + /// + public class HidSection + { + /// + /// Enable or disable keyboard support (Independent from controllers binding) + /// + public ReactiveObject EnableKeyboard { get; private set; } + + /// + /// Enable or disable mouse support (Independent from controllers binding) + /// + public ReactiveObject EnableMouse { get; private set; } + + /// + /// Hotkey Keyboard Bindings + /// + public ReactiveObject Hotkeys { get; private set; } + + /// + /// Input device configuration. + /// NOTE: This ReactiveObject won't issue an event when the List has elements added or removed. + /// TODO: Implement a ReactiveList class. + /// + public ReactiveObject> InputConfig { get; private set; } + + public HidSection() + { + EnableKeyboard = new ReactiveObject(); + EnableMouse = new ReactiveObject(); + Hotkeys = new ReactiveObject(); + InputConfig = new ReactiveObject>(); + } + } + + /// + /// Graphics configuration section + /// + public class GraphicsSection + { + /// + /// Whether or not backend threading is enabled. The "Auto" setting will determine whether threading should be enabled at runtime. + /// + public ReactiveObject BackendThreading { get; private set; } + + /// + /// Max Anisotropy. Values range from 0 - 16. Set to -1 to let the game decide. + /// + public ReactiveObject MaxAnisotropy { get; private set; } + + /// + /// Aspect Ratio applied to the renderer window. + /// + public ReactiveObject AspectRatio { get; private set; } + + /// + /// Resolution Scale. An integer scale applied to applicable render targets. Values 1-4, or -1 to use a custom floating point scale instead. + /// + public ReactiveObject ResScale { get; private set; } + + /// + /// Custom Resolution Scale. A custom floating point scale applied to applicable render targets. Only active when Resolution Scale is -1. + /// + public ReactiveObject ResScaleCustom { get; private set; } + + /// + /// Dumps shaders in this local directory + /// + public ReactiveObject ShadersDumpPath { get; private set; } + + /// + /// Toggles the present interval mode. Options are Switch (60Hz), Unbounded (previously Vsync off), and Custom, if enabled. + /// + public ReactiveObject VSyncMode { get; private set; } + + /// + /// Enables or disables the custom present interval mode. + /// + public ReactiveObject EnableCustomVSyncInterval { get; private set; } + + /// + /// Changes the custom present interval. + /// + public ReactiveObject CustomVSyncInterval { get; private set; } + + /// + /// Enables or disables Shader cache + /// + public ReactiveObject EnableShaderCache { get; private set; } + + /// + /// Enables or disables texture recompression + /// + public ReactiveObject EnableTextureRecompression { get; private set; } + + /// + /// Enables or disables Macro high-level emulation + /// + public ReactiveObject EnableMacroHLE { get; private set; } + + /// + /// Enables or disables color space passthrough, if available. + /// + public ReactiveObject EnableColorSpacePassthrough { get; private set; } + + /// + /// Graphics backend + /// + public ReactiveObject GraphicsBackend { get; private set; } + + /// + /// Applies anti-aliasing to the renderer. + /// + public ReactiveObject AntiAliasing { get; private set; } + + /// + /// Sets the framebuffer upscaling type. + /// + public ReactiveObject ScalingFilter { get; private set; } + + /// + /// Sets the framebuffer upscaling level. + /// + public ReactiveObject ScalingFilterLevel { get; private set; } + + /// + /// Preferred GPU + /// + public ReactiveObject PreferredGpu { get; private set; } + + public GraphicsSection() + { + BackendThreading = new ReactiveObject(); + BackendThreading.LogChangesToValue(nameof(BackendThreading)); + ResScale = new ReactiveObject(); + ResScale.LogChangesToValue(nameof(ResScale)); + ResScaleCustom = new ReactiveObject(); + ResScaleCustom.LogChangesToValue(nameof(ResScaleCustom)); + MaxAnisotropy = new ReactiveObject(); + MaxAnisotropy.LogChangesToValue(nameof(MaxAnisotropy)); + AspectRatio = new ReactiveObject(); + AspectRatio.LogChangesToValue(nameof(AspectRatio)); + ShadersDumpPath = new ReactiveObject(); + VSyncMode = new ReactiveObject(); + VSyncMode.LogChangesToValue(nameof(VSyncMode)); + EnableCustomVSyncInterval = new ReactiveObject(); + EnableCustomVSyncInterval.LogChangesToValue(nameof(EnableCustomVSyncInterval)); + CustomVSyncInterval = new ReactiveObject(); + CustomVSyncInterval.LogChangesToValue(nameof(CustomVSyncInterval)); + EnableShaderCache = new ReactiveObject(); + EnableShaderCache.LogChangesToValue(nameof(EnableShaderCache)); + EnableTextureRecompression = new ReactiveObject(); + EnableTextureRecompression.LogChangesToValue(nameof(EnableTextureRecompression)); + GraphicsBackend = new ReactiveObject(); + GraphicsBackend.LogChangesToValue(nameof(GraphicsBackend)); + PreferredGpu = new ReactiveObject(); + PreferredGpu.LogChangesToValue(nameof(PreferredGpu)); + EnableMacroHLE = new ReactiveObject(); + EnableMacroHLE.LogChangesToValue(nameof(EnableMacroHLE)); + EnableColorSpacePassthrough = new ReactiveObject(); + EnableColorSpacePassthrough.LogChangesToValue(nameof(EnableColorSpacePassthrough)); + AntiAliasing = new ReactiveObject(); + AntiAliasing.LogChangesToValue(nameof(AntiAliasing)); + ScalingFilter = new ReactiveObject(); + ScalingFilter.LogChangesToValue(nameof(ScalingFilter)); + ScalingFilterLevel = new ReactiveObject(); + ScalingFilterLevel.LogChangesToValue(nameof(ScalingFilterLevel)); + } + } + + /// + /// Multiplayer configuration section + /// + public class MultiplayerSection + { + /// + /// GUID for the network interface used by LAN (or 0 for default) + /// + public ReactiveObject LanInterfaceId { get; private set; } + + /// + /// Multiplayer Mode + /// + public ReactiveObject Mode { get; private set; } + + /// + /// Disable P2P + /// + public ReactiveObject DisableP2p { get; private set; } + + /// + /// LDN PassPhrase + /// + public ReactiveObject LdnPassphrase { get; private set; } + + /// + /// LDN Server + /// + public ReactiveObject LdnServer { get; private set; } + + public MultiplayerSection() + { + LanInterfaceId = new ReactiveObject(); + Mode = new ReactiveObject(); + Mode.LogChangesToValue(nameof(MultiplayerMode)); + DisableP2p = new ReactiveObject(); + DisableP2p.LogChangesToValue(nameof(DisableP2p)); + LdnPassphrase = new ReactiveObject(); + LdnPassphrase.LogChangesToValue(nameof(LdnPassphrase)); + LdnServer = new ReactiveObject(); + LdnServer.LogChangesToValue(nameof(LdnServer)); + } + } + + /// + /// The default configuration instance + /// + public static ConfigurationState Instance { get; private set; } + + /// + /// The UI section + /// + public UISection UI { get; private set; } + + /// + /// The Logger section + /// + public LoggerSection Logger { get; private set; } + + /// + /// The System section + /// + public SystemSection System { get; private set; } + + /// + /// The Graphics section + /// + public GraphicsSection Graphics { get; private set; } + + /// + /// The Hid section + /// + public HidSection Hid { get; private set; } + + /// + /// The Multiplayer section + /// + public MultiplayerSection Multiplayer { get; private set; } + + /// + /// Enables or disables Discord Rich Presence + /// + public ReactiveObject EnableDiscordIntegration { get; private set; } + + /// + /// Checks for updates when Ryujinx starts when enabled + /// + public ReactiveObject CheckUpdatesOnStart { get; private set; } + + /// + /// Show "Confirm Exit" Dialog + /// + public ReactiveObject ShowConfirmExit { get; private set; } + + /// + /// Ignore Applet + /// + public ReactiveObject IgnoreApplet { get; private set; } + + /// + /// Enables or disables save window size, position and state on close. + /// + public ReactiveObject RememberWindowState { get; private set; } + + /// + /// Enables or disables the redesigned title bar + /// + public ReactiveObject ShowTitleBar { get; private set; } + + /// + /// Enables hardware-accelerated rendering for Avalonia + /// + public ReactiveObject EnableHardwareAcceleration { get; private set; } + + /// + /// Hide Cursor on Idle + /// + public ReactiveObject HideCursor { get; private set; } + + private ConfigurationState() + { + UI = new UISection(); + Logger = new LoggerSection(); + System = new SystemSection(); + Graphics = new GraphicsSection(); + Hid = new HidSection(); + Multiplayer = new MultiplayerSection(); + EnableDiscordIntegration = new ReactiveObject(); + CheckUpdatesOnStart = new ReactiveObject(); + ShowConfirmExit = new ReactiveObject(); + IgnoreApplet = new ReactiveObject(); + IgnoreApplet.LogChangesToValue(nameof(IgnoreApplet)); + RememberWindowState = new ReactiveObject(); + ShowTitleBar = new ReactiveObject(); + EnableHardwareAcceleration = new ReactiveObject(); + HideCursor = new ReactiveObject(); + } + } +} diff --git a/src/Ryujinx.UI.Common/Helper/DownloadableContentsHelper.cs b/src/Ryujinx.UI.Common/Helper/DownloadableContentsHelper.cs new file mode 100644 index 000000000..3695c5c5c --- /dev/null +++ b/src/Ryujinx.UI.Common/Helper/DownloadableContentsHelper.cs @@ -0,0 +1,135 @@ +using LibHac.Common; +using LibHac.Fs; +using LibHac.Fs.Fsa; +using LibHac.Tools.FsSystem; +using LibHac.Tools.FsSystem.NcaUtils; +using Ryujinx.Common.Configuration; +using Ryujinx.Common.Logging; +using Ryujinx.Common.Utilities; +using Ryujinx.HLE.FileSystem; +using Ryujinx.HLE.Utilities; +using Ryujinx.UI.Common.Models; +using System; +using System.Collections.Generic; +using System.IO; +using Path = System.IO.Path; + +namespace Ryujinx.UI.Common.Helper +{ + public static class DownloadableContentsHelper + { + private static readonly DownloadableContentJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions()); + + public static List<(DownloadableContentModel, bool IsEnabled)> LoadDownloadableContentsJson(VirtualFileSystem vfs, ulong applicationIdBase) + { + var downloadableContentJsonPath = PathToGameDLCJson(applicationIdBase); + + if (!File.Exists(downloadableContentJsonPath)) + { + return []; + } + + try + { + var downloadableContentContainerList = JsonHelper.DeserializeFromFile(downloadableContentJsonPath, + _serializerContext.ListDownloadableContentContainer); + return LoadDownloadableContents(vfs, downloadableContentContainerList); + } + catch + { + Logger.Error?.Print(LogClass.Configuration, "Downloadable Content JSON failed to deserialize."); + return []; + } + } + + public static void SaveDownloadableContentsJson(VirtualFileSystem vfs, ulong applicationIdBase, List<(DownloadableContentModel, bool IsEnabled)> dlcs) + { + DownloadableContentContainer container = default; + List downloadableContentContainerList = new(); + + foreach ((DownloadableContentModel dlc, bool isEnabled) in dlcs) + { + if (container.ContainerPath != dlc.ContainerPath) + { + if (!string.IsNullOrWhiteSpace(container.ContainerPath)) + { + downloadableContentContainerList.Add(container); + } + + container = new DownloadableContentContainer + { + ContainerPath = dlc.ContainerPath, + DownloadableContentNcaList = [], + }; + } + + container.DownloadableContentNcaList.Add(new DownloadableContentNca + { + Enabled = isEnabled, + TitleId = dlc.TitleId, + FullPath = dlc.FullPath, + }); + } + + if (!string.IsNullOrWhiteSpace(container.ContainerPath)) + { + downloadableContentContainerList.Add(container); + } + + var downloadableContentJsonPath = PathToGameDLCJson(applicationIdBase); + JsonHelper.SerializeToFile(downloadableContentJsonPath, downloadableContentContainerList, _serializerContext.ListDownloadableContentContainer); + } + + private static List<(DownloadableContentModel, bool IsEnabled)> LoadDownloadableContents(VirtualFileSystem vfs, List downloadableContentContainers) + { + var result = new List<(DownloadableContentModel, bool IsEnabled)>(); + + foreach (DownloadableContentContainer downloadableContentContainer in downloadableContentContainers) + { + if (!File.Exists(downloadableContentContainer.ContainerPath)) + { + continue; + } + + using IFileSystem partitionFileSystem = PartitionFileSystemUtils.OpenApplicationFileSystem(downloadableContentContainer.ContainerPath, vfs); + + foreach (DownloadableContentNca downloadableContentNca in downloadableContentContainer.DownloadableContentNcaList) + { + using UniqueRef ncaFile = new(); + + partitionFileSystem.OpenFile(ref ncaFile.Ref, downloadableContentNca.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); + + Nca nca = TryOpenNca(vfs, ncaFile.Get.AsStorage()); + if (nca == null) + { + continue; + } + + var content = new DownloadableContentModel(nca.Header.TitleId, + downloadableContentContainer.ContainerPath, + downloadableContentNca.FullPath); + + result.Add((content, downloadableContentNca.Enabled)); + } + } + + return result; + } + + private static Nca TryOpenNca(VirtualFileSystem vfs, IStorage ncaStorage) + { + try + { + return new Nca(vfs.KeySet, ncaStorage); + } + catch (Exception) { } + + return null; + } + + private static string PathToGameDLCJson(ulong applicationIdBase) + { + return Path.Combine(AppDataManager.GamesDirPath, applicationIdBase.ToString("x16"), "dlc.json"); + } + } +} diff --git a/src/Ryujinx.UI.Common/Helper/TitleUpdatesHelper.cs b/src/Ryujinx.UI.Common/Helper/TitleUpdatesHelper.cs new file mode 100644 index 000000000..18fbabd6d --- /dev/null +++ b/src/Ryujinx.UI.Common/Helper/TitleUpdatesHelper.cs @@ -0,0 +1,151 @@ +using LibHac.Common; +using LibHac.Common.Keys; +using LibHac.Fs; +using LibHac.Fs.Fsa; +using LibHac.Ncm; +using LibHac.Ns; +using LibHac.Tools.FsSystem; +using LibHac.Tools.FsSystem.NcaUtils; +using Ryujinx.Common.Configuration; +using Ryujinx.Common.Logging; +using Ryujinx.Common.Utilities; +using Ryujinx.HLE.FileSystem; +using Ryujinx.HLE.Loaders.Processes.Extensions; +using Ryujinx.HLE.Utilities; +using Ryujinx.UI.Common.Configuration; +using Ryujinx.UI.Common.Models; +using System; +using System.Collections.Generic; +using System.IO; +using ContentType = LibHac.Ncm.ContentType; +using Path = System.IO.Path; +using SpanHelpers = LibHac.Common.SpanHelpers; +using TitleUpdateMetadata = Ryujinx.Common.Configuration.TitleUpdateMetadata; + +namespace Ryujinx.UI.Common.Helper +{ + public static class TitleUpdatesHelper + { + private static readonly TitleUpdateMetadataJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions()); + + public static List<(TitleUpdateModel, bool IsSelected)> LoadTitleUpdatesJson(VirtualFileSystem vfs, ulong applicationIdBase) + { + var titleUpdatesJsonPath = PathToGameUpdatesJson(applicationIdBase); + + if (!File.Exists(titleUpdatesJsonPath)) + { + return []; + } + + try + { + var titleUpdateWindowData = JsonHelper.DeserializeFromFile(titleUpdatesJsonPath, _serializerContext.TitleUpdateMetadata); + return LoadTitleUpdates(vfs, titleUpdateWindowData, applicationIdBase); + } + catch + { + Logger.Warning?.Print(LogClass.Application, $"Failed to deserialize title update data for {applicationIdBase:x16} at {titleUpdatesJsonPath}"); + return []; + } + } + + public static void SaveTitleUpdatesJson(VirtualFileSystem vfs, ulong applicationIdBase, List<(TitleUpdateModel, bool IsSelected)> updates) + { + var titleUpdateWindowData = new TitleUpdateMetadata + { + Selected = string.Empty, + Paths = [], + }; + + foreach ((TitleUpdateModel update, bool isSelected) in updates) + { + titleUpdateWindowData.Paths.Add(update.Path); + if (isSelected) + { + if (!string.IsNullOrEmpty(titleUpdateWindowData.Selected)) + { + Logger.Error?.Print(LogClass.Application, + $"Tried to save two updates as 'IsSelected' for {applicationIdBase:x16}"); + return; + } + + titleUpdateWindowData.Selected = update.Path; + } + } + + var titleUpdatesJsonPath = PathToGameUpdatesJson(applicationIdBase); + JsonHelper.SerializeToFile(titleUpdatesJsonPath, titleUpdateWindowData, _serializerContext.TitleUpdateMetadata); + } + + private static List<(TitleUpdateModel, bool IsSelected)> LoadTitleUpdates(VirtualFileSystem vfs, TitleUpdateMetadata titleUpdateMetadata, ulong applicationIdBase) + { + var result = new List<(TitleUpdateModel, bool IsSelected)>(); + + IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks + ? IntegrityCheckLevel.ErrorOnInvalid + : IntegrityCheckLevel.None; + + foreach (string path in titleUpdateMetadata.Paths) + { + if (!File.Exists(path)) + continue; + + try + { + using IFileSystem pfs = PartitionFileSystemUtils.OpenApplicationFileSystem(path, vfs); + + Dictionary updates = + pfs.GetContentData(ContentMetaType.Patch, vfs, checkLevel); + + if (!updates.TryGetValue(applicationIdBase, out ContentMetaData content)) + continue; + + Nca patchNca = content.GetNcaByType(vfs.KeySet, ContentType.Program); + Nca controlNca = content.GetNcaByType(vfs.KeySet, ContentType.Control); + + if (controlNca is null || patchNca is null) + continue; + + ApplicationControlProperty controlData = new(); + + using UniqueRef nacpFile = new(); + + controlNca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None) + .OpenFile(ref nacpFile.Ref, "/control.nacp".ToU8Span(), OpenMode.Read).ThrowIfFailure(); + nacpFile.Get.Read(out _, 0, SpanHelpers.AsByteSpan(ref controlData), ReadOption.None) + .ThrowIfFailure(); + + var displayVersion = controlData.DisplayVersionString.ToString(); + var update = new TitleUpdateModel(content.ApplicationId, content.Version.Version, + displayVersion, path); + + result.Add((update, path == titleUpdateMetadata.Selected)); + } + catch (MissingKeyException exception) + { + Logger.Warning?.Print(LogClass.Application, + $"Your key set is missing a key with the name: {exception.Name}"); + } + catch (InvalidDataException) + { + Logger.Warning?.Print(LogClass.Application, + $"The header key is incorrect or missing and therefore the NCA header content type check has failed. Malformed File: {path}"); + } + catch (IOException exception) + { + Logger.Warning?.Print(LogClass.Application, exception.Message); + } + catch (Exception exception) + { + Logger.Warning?.Print(LogClass.Application, + $"The file encountered was not of a valid type. File: '{path}' Error: {exception}"); + } + } + + return result; + } + + private static string PathToGameUpdatesJson(ulong applicationIdBase) + => Path.Combine(AppDataManager.GamesDirPath, applicationIdBase.ToString("x16"), "updates.json"); + } +} diff --git a/src/Ryujinx.UI.Common/Models/DownloadableContentModel.cs b/src/Ryujinx.UI.Common/Models/DownloadableContentModel.cs new file mode 100644 index 000000000..95c64f078 --- /dev/null +++ b/src/Ryujinx.UI.Common/Models/DownloadableContentModel.cs @@ -0,0 +1,12 @@ +namespace Ryujinx.UI.Common.Models +{ + // NOTE: most consuming code relies on this model being value-comparable + public record DownloadableContentModel(ulong TitleId, string ContainerPath, string FullPath) + { + public bool IsBundled { get; } = System.IO.Path.GetExtension(ContainerPath)?.ToLower() == ".xci"; + + public string FileName => System.IO.Path.GetFileName(ContainerPath); + public string TitleIdStr => TitleId.ToString("x16"); + public ulong TitleIdBase => TitleId & ~0x1FFFUL; + } +} diff --git a/src/Ryujinx.UI.Common/Models/TitleUpdateModel.cs b/src/Ryujinx.UI.Common/Models/TitleUpdateModel.cs new file mode 100644 index 000000000..5422e1303 --- /dev/null +++ b/src/Ryujinx.UI.Common/Models/TitleUpdateModel.cs @@ -0,0 +1,11 @@ +namespace Ryujinx.UI.Common.Models +{ + // NOTE: most consuming code relies on this model being value-comparable + public record TitleUpdateModel(ulong TitleId, ulong Version, string DisplayVersion, string Path) + { + public bool IsBundled { get; } = System.IO.Path.GetExtension(Path)?.ToLower() == ".xci"; + + public string TitleIdStr => TitleId.ToString("x16"); + public ulong TitleIdBase => TitleId & ~0x1FFFUL; + } +} diff --git a/src/Ryujinx.UI.Common/Models/XCITrimmerFileModel.cs b/src/Ryujinx.UI.Common/Models/XCITrimmerFileModel.cs new file mode 100644 index 000000000..95fb3985b --- /dev/null +++ b/src/Ryujinx.UI.Common/Models/XCITrimmerFileModel.cs @@ -0,0 +1,55 @@ +using Ryujinx.Common.Logging; +using Ryujinx.Common.Utilities; +using Ryujinx.UI.App.Common; + +namespace Ryujinx.UI.Common.Models +{ + public record XCITrimmerFileModel( + string Name, + string Path, + bool Trimmable, + bool Untrimmable, + long PotentialSavingsB, + long CurrentSavingsB, + int? PercentageProgress, + XCIFileTrimmer.OperationOutcome ProcessingOutcome) + { + public static XCITrimmerFileModel FromApplicationData(ApplicationData applicationData, XCIFileTrimmerLog logger) + { + var trimmer = new XCIFileTrimmer(applicationData.Path, logger); + + return new XCITrimmerFileModel( + applicationData.Name, + applicationData.Path, + trimmer.CanBeTrimmed, + trimmer.CanBeUntrimmed, + trimmer.DiskSpaceSavingsB, + trimmer.DiskSpaceSavedB, + null, + XCIFileTrimmer.OperationOutcome.Undetermined + ); + } + + public bool IsFailed + { + get + { + return ProcessingOutcome != XCIFileTrimmer.OperationOutcome.Undetermined && + ProcessingOutcome != XCIFileTrimmer.OperationOutcome.Successful; + } + } + + public virtual bool Equals(XCITrimmerFileModel obj) + { + if (obj == null) + return false; + + return this.Path == obj.Path; + } + + public override int GetHashCode() + { + return this.Path.GetHashCode(); + } + } +} diff --git a/src/Ryujinx.UI.Common/Resources/Icon_Blank.png b/src/Ryujinx.UI.Common/Resources/Icon_Blank.png new file mode 100644 index 000000000..d2bba8a92 Binary files /dev/null and b/src/Ryujinx.UI.Common/Resources/Icon_Blank.png differ diff --git a/src/Ryujinx.Ui.Common/App/ApplicationAddedEventArgs.cs b/src/Ryujinx.Ui.Common/App/ApplicationAddedEventArgs.cs deleted file mode 100644 index 01e20276e..000000000 --- a/src/Ryujinx.Ui.Common/App/ApplicationAddedEventArgs.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System; - -namespace Ryujinx.Ui.App.Common -{ - public class ApplicationAddedEventArgs : EventArgs - { - public ApplicationData AppData { get; set; } - } -} diff --git a/src/Ryujinx.Ui.Common/App/ApplicationCountUpdatedEventArgs.cs b/src/Ryujinx.Ui.Common/App/ApplicationCountUpdatedEventArgs.cs index ca54ddf7a..5ed7baf19 100644 --- a/src/Ryujinx.Ui.Common/App/ApplicationCountUpdatedEventArgs.cs +++ b/src/Ryujinx.Ui.Common/App/ApplicationCountUpdatedEventArgs.cs @@ -1,6 +1,6 @@ using System; -namespace Ryujinx.Ui.App.Common +namespace Ryujinx.UI.App.Common { public class ApplicationCountUpdatedEventArgs : EventArgs { diff --git a/src/Ryujinx.Ui.Common/App/ApplicationData.cs b/src/Ryujinx.Ui.Common/App/ApplicationData.cs index bd844805b..7aa0dccaa 100644 --- a/src/Ryujinx.Ui.Common/App/ApplicationData.cs +++ b/src/Ryujinx.Ui.Common/App/ApplicationData.cs @@ -9,20 +9,26 @@ using LibHac.Tools.FsSystem; using LibHac.Tools.FsSystem.NcaUtils; using Ryujinx.Common.Logging; using Ryujinx.HLE.FileSystem; -using Ryujinx.Ui.Common.Helper; +using Ryujinx.HLE.Loaders.Processes.Extensions; +using Ryujinx.UI.Common.Helper; using System; using System.IO; +using System.Text.Json.Serialization; -namespace Ryujinx.Ui.App.Common +namespace Ryujinx.UI.App.Common { public class ApplicationData { + public static Func LocalizedNever { get; set; } = () => "Never"; + public bool Favorite { get; set; } public byte[] Icon { get; set; } - public string TitleName { get; set; } - public string TitleId { get; set; } - public string Developer { get; set; } - public string Version { get; set; } + public string Name { get; set; } = "Unknown"; + public ulong Id { get; set; } + public string Developer { get; set; } = "Unknown"; + public string Version { get; set; } = "0"; + public int PlayerCount { get; set; } + public int GameCount { get; set; } public TimeSpan TimePlayed { get; set; } public DateTime? LastPlayed { get; set; } public string FileExtension { get; set; } @@ -32,11 +38,17 @@ namespace Ryujinx.Ui.App.Common public string TimePlayedString => ValueFormatUtils.FormatTimeSpan(TimePlayed); - public string LastPlayedString => ValueFormatUtils.FormatDateTime(LastPlayed); + public string LastPlayedString => ValueFormatUtils.FormatDateTime(LastPlayed) ?? LocalizedNever(); public string FileSizeString => ValueFormatUtils.FormatFileSize(FileSize); - public static string GetApplicationBuildId(VirtualFileSystem virtualFileSystem, string titleFilePath) + [JsonIgnore] public string IdString => Id.ToString("x16"); + + [JsonIgnore] public ulong IdBase => Id & ~0x1FFFUL; + + [JsonIgnore] public string IdBaseString => IdBase.ToString("x16"); + + public static string GetBuildId(VirtualFileSystem virtualFileSystem, IntegrityCheckLevel checkLevel, string titleFilePath) { using FileStream file = new(titleFilePath, FileMode.Open, FileAccess.Read); @@ -45,7 +57,7 @@ namespace Ryujinx.Ui.App.Common if (!System.IO.Path.Exists(titleFilePath)) { - Logger.Error?.Print(LogClass.Application, $"File does not exists. {titleFilePath}"); + Logger.Error?.Print(LogClass.Application, $"File \"{titleFilePath}\" does not exist."); return string.Empty; } @@ -105,7 +117,7 @@ namespace Ryujinx.Ui.App.Common return string.Empty; } - (Nca updatePatchNca, _) = ApplicationLibrary.GetGameUpdateData(virtualFileSystem, mainNca.Header.TitleId.ToString("x16"), 0, out _); + (Nca updatePatchNca, _) = mainNca.GetUpdateData(virtualFileSystem, checkLevel, 0, out string _); if (updatePatchNca != null) { @@ -152,7 +164,7 @@ namespace Ryujinx.Ui.App.Common NsoReader reader = new(); reader.Initialize(nsoFile.Release().AsStorage().AsFile(OpenMode.Read)).ThrowIfFailure(); - return BitConverter.ToString(reader.Header.ModuleId.ItemsRo.ToArray()).Replace("-", "").ToUpper()[..16]; + return BitConverter.ToString(reader.Header.ModuleId.ItemsRo.ToArray()).Replace("-", string.Empty).ToUpper()[..16]; } } } diff --git a/src/Ryujinx.Ui.Common/App/ApplicationJsonSerializerContext.cs b/src/Ryujinx.Ui.Common/App/ApplicationJsonSerializerContext.cs index 9a7b3eddf..ada7cc346 100644 --- a/src/Ryujinx.Ui.Common/App/ApplicationJsonSerializerContext.cs +++ b/src/Ryujinx.Ui.Common/App/ApplicationJsonSerializerContext.cs @@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace Ryujinx.Ui.App.Common +namespace Ryujinx.UI.App.Common { [JsonSourceGenerationOptions(WriteIndented = true)] [JsonSerializable(typeof(ApplicationMetadata))] diff --git a/src/Ryujinx.Ui.Common/App/ApplicationLibrary.cs b/src/Ryujinx.Ui.Common/App/ApplicationLibrary.cs index 4d0c4e8b9..174db51ad 100644 --- a/src/Ryujinx.Ui.Common/App/ApplicationLibrary.cs +++ b/src/Ryujinx.Ui.Common/App/ApplicationLibrary.cs @@ -1,38 +1,57 @@ +using DynamicData; +using DynamicData.Kernel; +using Gommon; using LibHac; using LibHac.Common; -using LibHac.Common.Keys; using LibHac.Fs; using LibHac.Fs.Fsa; using LibHac.FsSystem; +using LibHac.Ncm; using LibHac.Ns; using LibHac.Tools.Fs; using LibHac.Tools.FsSystem; using LibHac.Tools.FsSystem.NcaUtils; using Ryujinx.Common.Configuration; +using Ryujinx.Common.Configuration.Multiplayer; using Ryujinx.Common.Logging; using Ryujinx.Common.Utilities; using Ryujinx.HLE.FileSystem; using Ryujinx.HLE.HOS.SystemState; using Ryujinx.HLE.Loaders.Npdm; -using Ryujinx.Ui.Common.Configuration; -using Ryujinx.Ui.Common.Configuration.System; +using Ryujinx.HLE.Loaders.Processes.Extensions; +using Ryujinx.HLE.Utilities; +using Ryujinx.UI.Common.Configuration; +using Ryujinx.UI.Common.Configuration.System; +using Ryujinx.UI.Common.Helper; +using Ryujinx.UI.Common.Models; using System; using System.Collections.Generic; -using System.Globalization; using System.IO; using System.Linq; +using System.Net.Http; +using System.Reflection; using System.Text; using System.Text.Json; using System.Threading; +using System.Threading.Tasks; +using ContentType = LibHac.Ncm.ContentType; +using MissingKeyException = LibHac.Common.Keys.MissingKeyException; using Path = System.IO.Path; +using SpanHelpers = LibHac.Common.SpanHelpers; using TimeSpan = System.TimeSpan; -namespace Ryujinx.Ui.App.Common +namespace Ryujinx.UI.App.Common { public class ApplicationLibrary { - public event EventHandler ApplicationAdded; + public static string DefaultLanPlayWebHost = "ryuldnweb.vudjun.com"; + public Language DesiredLanguage { get; set; } public event EventHandler ApplicationCountUpdated; + public event EventHandler LdnGameDataReceived; + + public readonly IObservableCache Applications; + public readonly IObservableCache<(TitleUpdateModel TitleUpdate, bool IsSelected), TitleUpdateModel> TitleUpdates; + public readonly IObservableCache<(DownloadableContentModel Dlc, bool IsEnabled), DownloadableContentModel> DownloadableContents; private readonly byte[] _nspIcon; private readonly byte[] _xciIcon; @@ -41,292 +60,332 @@ namespace Ryujinx.Ui.App.Common private readonly byte[] _nsoIcon; private readonly VirtualFileSystem _virtualFileSystem; - private Language _desiredTitleLanguage; + private readonly IntegrityCheckLevel _checkLevel; private CancellationTokenSource _cancellationToken; + private readonly SourceCache _applications = new(it => it.Id); + private readonly SourceCache<(TitleUpdateModel TitleUpdate, bool IsSelected), TitleUpdateModel> _titleUpdates = new(it => it.TitleUpdate); + private readonly SourceCache<(DownloadableContentModel Dlc, bool IsEnabled), DownloadableContentModel> _downloadableContents = new(it => it.Dlc); private static readonly ApplicationJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions()); - private static readonly TitleUpdateMetadataJsonSerializerContext _titleSerializerContext = new(JsonHelper.GetDefaultSerializerOptions()); + private static readonly LdnGameDataSerializerContext _ldnDataSerializerContext = new(JsonHelper.GetDefaultSerializerOptions()); - public ApplicationLibrary(VirtualFileSystem virtualFileSystem) + public ApplicationLibrary(VirtualFileSystem virtualFileSystem, IntegrityCheckLevel checkLevel) { _virtualFileSystem = virtualFileSystem; + _checkLevel = checkLevel; - _nspIcon = GetResourceBytes("Ryujinx.Ui.Common.Resources.Icon_NSP.png"); - _xciIcon = GetResourceBytes("Ryujinx.Ui.Common.Resources.Icon_XCI.png"); - _ncaIcon = GetResourceBytes("Ryujinx.Ui.Common.Resources.Icon_NCA.png"); - _nroIcon = GetResourceBytes("Ryujinx.Ui.Common.Resources.Icon_NRO.png"); - _nsoIcon = GetResourceBytes("Ryujinx.Ui.Common.Resources.Icon_NSO.png"); + Applications = _applications.AsObservableCache(); + TitleUpdates = _titleUpdates.AsObservableCache(); + DownloadableContents = _downloadableContents.AsObservableCache(); + + _nspIcon = GetResourceBytes("Ryujinx.UI.Common.Resources.Icon_NSP.png"); + _xciIcon = GetResourceBytes("Ryujinx.UI.Common.Resources.Icon_XCI.png"); + _ncaIcon = GetResourceBytes("Ryujinx.UI.Common.Resources.Icon_NCA.png"); + _nroIcon = GetResourceBytes("Ryujinx.UI.Common.Resources.Icon_NRO.png"); + _nsoIcon = GetResourceBytes("Ryujinx.UI.Common.Resources.Icon_NSO.png"); } private static byte[] GetResourceBytes(string resourceName) { - Stream resourceStream = typeof(ApplicationLibrary).Assembly.GetManifestResourceStream(resourceName); + Stream resourceStream = Assembly.GetCallingAssembly().GetManifestResourceStream(resourceName)!; byte[] resourceByteArray = new byte[resourceStream.Length]; - resourceStream.Read(resourceByteArray); + resourceStream.ReadExactly(resourceByteArray); return resourceByteArray; } - public void CancelLoading() + /// The npdm file doesn't contain valid data. + /// The FsAccessHeader.ContentOwnerId section is not implemented. + /// An error occured while reading bytes from the stream. + /// The end of the stream is reached. + /// An I/O error occurred. + private ApplicationData GetApplicationFromExeFs(PartitionFileSystem pfs, string filePath) { - _cancellationToken?.Cancel(); + ApplicationData data = new() + { + Icon = _nspIcon, + Path = filePath, + }; + + using UniqueRef npdmFile = new(); + + Result result = pfs.OpenFile(ref npdmFile.Ref, "/main.npdm".ToU8Span(), OpenMode.Read); + + if (ResultFs.PathNotFound.Includes(result)) + { + Npdm npdm = new(npdmFile.Get.AsStream()); + + data.Name = npdm.TitleName; + data.Id = npdm.Aci0.TitleId; + } + + return data; } - public static void ReadControlData(IFileSystem controlFs, Span outProperty) + /// The configured key set is missing a key. + /// The NCA header could not be decrypted. + /// The NCA version is not supported. + /// An error occured while reading PFS data. + /// The npdm file doesn't contain valid data. + /// The FsAccessHeader.ContentOwnerId section is not implemented. + /// An error occured while reading bytes from the stream. + /// The end of the stream is reached. + /// An I/O error occurred. + private ApplicationData GetApplicationFromNsp(PartitionFileSystem pfs, string filePath) { - using UniqueRef controlFile = new(); + bool isExeFs = false; - controlFs.OpenFile(ref controlFile.Ref, "/control.nacp".ToU8Span(), OpenMode.Read).ThrowIfFailure(); - controlFile.Get.Read(out _, 0, outProperty, ReadOption.None).ThrowIfFailure(); + // If the NSP doesn't have a main NCA, decrement the number of applications found and then continue to the next application. + bool hasMainNca = false; + + foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*")) + { + if (Path.GetExtension(fileEntry.FullPath)?.ToLower() == ".nca") + { + using UniqueRef ncaFile = new(); + + try + { + pfs.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); + + Nca nca = new(_virtualFileSystem.KeySet, ncaFile.Get.AsStorage()); + int dataIndex = Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program); + + // Some main NCAs don't have a data partition, so check if the partition exists before opening it + if (nca.Header.ContentType == NcaContentType.Program && + !(nca.SectionExists(NcaSectionType.Data) && + nca.Header.GetFsHeader(dataIndex).IsPatchSection())) + { + hasMainNca = true; + + break; + } + } + catch (Exception exception) + { + Logger.Warning?.Print(LogClass.Application, $"Encountered an error while trying to load applications from file '{filePath}': {exception}"); + + return null; + } + } + else if (Path.GetFileNameWithoutExtension(fileEntry.FullPath) == "main") + { + isExeFs = true; + } + } + + if (hasMainNca) + { + List applications = GetApplicationsFromPfs(pfs, filePath); + + switch (applications.Count) + { + case 1: + return applications[0]; + case >= 1: + Logger.Warning?.Print(LogClass.Application, $"File '{filePath}' contains more applications than expected: {applications.Count}"); + return applications[0]; + default: + return null; + } + } + + if (isExeFs) + { + return GetApplicationFromExeFs(pfs, filePath); + } + + return null; } - public void LoadApplications(List appDirs, Language desiredTitleLanguage) + /// The configured key set is missing a key. + /// The NCA header could not be decrypted. + /// The NCA version is not supported. + /// An error occured while reading PFS data. + private List GetApplicationsFromPfs(IFileSystem pfs, string filePath) { - int numApplicationsFound = 0; - int numApplicationsLoaded = 0; + var applications = new List(); + string extension = Path.GetExtension(filePath).ToLower(); - _desiredTitleLanguage = desiredTitleLanguage; + foreach ((ulong titleId, ContentMetaData content) in pfs.GetContentData(ContentMetaType.Application, _virtualFileSystem, _checkLevel)) + { + ApplicationData applicationData = new() + { + Id = titleId, + Path = filePath, + }; - _cancellationToken = new CancellationTokenSource(); + Nca mainNca = content.GetNcaByType(_virtualFileSystem.KeySet, ContentType.Program); + Nca controlNca = content.GetNcaByType(_virtualFileSystem.KeySet, ContentType.Control); - // Builds the applications list with paths to found applications - List applications = new(); + BlitStruct controlHolder = new(1); + + IFileSystem controlFs = controlNca?.OpenFileSystem(NcaSectionType.Data, _checkLevel); + + // Check if there is an update available. + if (IsUpdateApplied(mainNca, out IFileSystem updatedControlFs)) + { + // Replace the original ControlFs by the updated one. + controlFs = updatedControlFs; + } + + if (controlFs == null) + { + continue; + } + + ReadControlData(controlFs, controlHolder.ByteSpan); + + GetApplicationInformation(ref controlHolder.Value, ref applicationData); + + // Read the icon from the ControlFS and store it as a byte array + try + { + using UniqueRef icon = new(); + + controlFs.OpenFile(ref icon.Ref, $"/icon_{DesiredLanguage}.dat".ToU8Span(), OpenMode.Read).ThrowIfFailure(); + + using MemoryStream stream = new(); + + icon.Get.AsStream().CopyTo(stream); + applicationData.Icon = stream.ToArray(); + } + catch (HorizonResultException) + { + foreach (DirectoryEntryEx entry in controlFs.EnumerateEntries("/", "*")) + { + if (entry.Name == "control.nacp") + { + continue; + } + + using var icon = new UniqueRef(); + + controlFs.OpenFile(ref icon.Ref, entry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); + + using MemoryStream stream = new(); + + icon.Get.AsStream().CopyTo(stream); + applicationData.Icon = stream.ToArray(); + + if (applicationData.Icon != null) + { + break; + } + } + + applicationData.Icon ??= extension == ".xci" ? _xciIcon : _nspIcon; + } + + applicationData.ControlHolder = controlHolder; + + applications.Add(applicationData); + } + + return applications; + } + + public bool TryGetApplicationsFromFile(string applicationPath, out List applications) + { + applications = []; + long fileSize; try { - foreach (string appDir in appDirs) + fileSize = new FileInfo(applicationPath).Length; + } + catch (FileNotFoundException) + { + Logger.Warning?.Print(LogClass.Application, $"The file was not found: '{applicationPath}'"); + + return false; + } + + BlitStruct controlHolder = new(1); + + try + { + string extension = Path.GetExtension(applicationPath).ToLower(); + + using FileStream file = new(applicationPath, FileMode.Open, FileAccess.Read); + + switch (extension) { - if (_cancellationToken.Token.IsCancellationRequested) - { - return; - } - - if (!Directory.Exists(appDir)) - { - Logger.Warning?.Print(LogClass.Application, $"The \"game_dirs\" section in \"Config.json\" contains an invalid directory: \"{appDir}\""); - - continue; - } - - try - { - IEnumerable files = Directory.EnumerateFiles(appDir, "*", SearchOption.AllDirectories).Where(file => + case ".xci": { - return - (Path.GetExtension(file).ToLower() is ".nsp" && ConfigurationState.Instance.Ui.ShownFileTypes.NSP.Value) || - (Path.GetExtension(file).ToLower() is ".pfs0" && ConfigurationState.Instance.Ui.ShownFileTypes.PFS0.Value) || - (Path.GetExtension(file).ToLower() is ".xci" && ConfigurationState.Instance.Ui.ShownFileTypes.XCI.Value) || - (Path.GetExtension(file).ToLower() is ".nca" && ConfigurationState.Instance.Ui.ShownFileTypes.NCA.Value) || - (Path.GetExtension(file).ToLower() is ".nro" && ConfigurationState.Instance.Ui.ShownFileTypes.NRO.Value) || - (Path.GetExtension(file).ToLower() is ".nso" && ConfigurationState.Instance.Ui.ShownFileTypes.NSO.Value); - }); + Xci xci = new(_virtualFileSystem.KeySet, file.AsStorage()); - foreach (string app in files) - { - if (_cancellationToken.Token.IsCancellationRequested) + applications = GetApplicationsFromPfs(xci.OpenPartition(XciPartitionType.Secure), applicationPath); + + if (applications.Count == 0) { - return; + return false; } - var fileInfo = new FileInfo(app); - string extension = fileInfo.Extension.ToLower(); - - if (!fileInfo.Attributes.HasFlag(FileAttributes.Hidden) && extension is ".nsp" or ".pfs0" or ".xci" or ".nca" or ".nro" or ".nso") - { - var fullPath = fileInfo.ResolveLinkTarget(true)?.FullName ?? fileInfo.FullName; - - if (!File.Exists(fullPath)) - { - Logger.Warning?.Print(LogClass.Application, $"Skipping invalid symlink: {fileInfo.FullName}"); - continue; - } - - applications.Add(fullPath); - numApplicationsFound++; - } + break; } - } - catch (UnauthorizedAccessException) - { - Logger.Warning?.Print(LogClass.Application, $"Failed to get access to directory: \"{appDir}\""); - } - } - - // Loops through applications list, creating a struct and then firing an event containing the struct for each application - foreach (string applicationPath in applications) - { - if (_cancellationToken.Token.IsCancellationRequested) - { - return; - } - - long fileSize = new FileInfo(applicationPath).Length; - string titleName = "Unknown"; - string titleId = "0000000000000000"; - string developer = "Unknown"; - string version = "0"; - byte[] applicationIcon = null; - - BlitStruct controlHolder = new(1); - - try - { - string extension = Path.GetExtension(applicationPath).ToLower(); - - using FileStream file = new(applicationPath, FileMode.Open, FileAccess.Read); - - if (extension == ".nsp" || extension == ".pfs0" || extension == ".xci") + case ".nsp": + case ".pfs0": { - try + var pfs = new PartitionFileSystem(); + pfs.Initialize(file.AsStorage()).ThrowIfFailure(); + + ApplicationData result = GetApplicationFromNsp(pfs, applicationPath); + + if (result == null) { - IFileSystem pfs; - - bool isExeFs = false; - - if (extension == ".xci") - { - Xci xci = new(_virtualFileSystem.KeySet, file.AsStorage()); - - pfs = xci.OpenPartition(XciPartitionType.Secure); - } - else - { - var pfsTemp = new PartitionFileSystem(); - pfsTemp.Initialize(file.AsStorage()).ThrowIfFailure(); - pfs = pfsTemp; - - // If the NSP doesn't have a main NCA, decrement the number of applications found and then continue to the next application. - bool hasMainNca = false; - - foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*")) - { - if (Path.GetExtension(fileEntry.FullPath).ToLower() == ".nca") - { - using UniqueRef ncaFile = new(); - - pfs.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); - - Nca nca = new(_virtualFileSystem.KeySet, ncaFile.Get.AsStorage()); - int dataIndex = Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program); - - // Some main NCAs don't have a data partition, so check if the partition exists before opening it - if (nca.Header.ContentType == NcaContentType.Program && !(nca.SectionExists(NcaSectionType.Data) && nca.Header.GetFsHeader(dataIndex).IsPatchSection())) - { - hasMainNca = true; - - break; - } - } - else if (Path.GetFileNameWithoutExtension(fileEntry.FullPath) == "main") - { - isExeFs = true; - } - } - - if (!hasMainNca && !isExeFs) - { - numApplicationsFound--; - - continue; - } - } - - if (isExeFs) - { - applicationIcon = _nspIcon; - - using UniqueRef npdmFile = new(); - - Result result = pfs.OpenFile(ref npdmFile.Ref, "/main.npdm".ToU8Span(), OpenMode.Read); - - if (ResultFs.PathNotFound.Includes(result)) - { - Npdm npdm = new(npdmFile.Get.AsStream()); - - titleName = npdm.TitleName; - titleId = npdm.Aci0.TitleId.ToString("x16"); - } - } - else - { - GetControlFsAndTitleId(pfs, out IFileSystem controlFs, out titleId); - - // Check if there is an update available. - if (IsUpdateApplied(titleId, out IFileSystem updatedControlFs)) - { - // Replace the original ControlFs by the updated one. - controlFs = updatedControlFs; - } - - ReadControlData(controlFs, controlHolder.ByteSpan); - - GetGameInformation(ref controlHolder.Value, out titleName, out _, out developer, out version); - - // Read the icon from the ControlFS and store it as a byte array - try - { - using UniqueRef icon = new(); - - controlFs.OpenFile(ref icon.Ref, $"/icon_{_desiredTitleLanguage}.dat".ToU8Span(), OpenMode.Read).ThrowIfFailure(); - - using MemoryStream stream = new(); - - icon.Get.AsStream().CopyTo(stream); - applicationIcon = stream.ToArray(); - } - catch (HorizonResultException) - { - foreach (DirectoryEntryEx entry in controlFs.EnumerateEntries("/", "*")) - { - if (entry.Name == "control.nacp") - { - continue; - } - - using var icon = new UniqueRef(); - - controlFs.OpenFile(ref icon.Ref, entry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); - - using MemoryStream stream = new(); - - icon.Get.AsStream().CopyTo(stream); - applicationIcon = stream.ToArray(); - - if (applicationIcon != null) - { - break; - } - } - - applicationIcon ??= extension == ".xci" ? _xciIcon : _nspIcon; - } - } + return false; } - catch (MissingKeyException exception) - { - applicationIcon = extension == ".xci" ? _xciIcon : _nspIcon; - Logger.Warning?.Print(LogClass.Application, $"Your key set is missing a key with the name: {exception.Name}"); - } - catch (InvalidDataException) - { - applicationIcon = extension == ".xci" ? _xciIcon : _nspIcon; + applications.Add(result); - Logger.Warning?.Print(LogClass.Application, $"The header key is incorrect or missing and therefore the NCA header content type check has failed. Errored File: {applicationPath}"); - } - catch (Exception exception) - { - Logger.Warning?.Print(LogClass.Application, $"The file encountered was not of a valid type. File: '{applicationPath}' Error: {exception}"); - - numApplicationsFound--; - - continue; - } + break; } - else if (extension == ".nro") + case ".nro": { BinaryReader reader = new(file); + ApplicationData application = new(); + + file.Seek(24, SeekOrigin.Begin); + + int assetOffset = reader.ReadInt32(); + + if (Encoding.ASCII.GetString(Read(assetOffset, 4)) == "ASET") + { + byte[] iconSectionInfo = Read(assetOffset + 8, 0x10); + + long iconOffset = BitConverter.ToInt64(iconSectionInfo, 0); + long iconSize = BitConverter.ToInt64(iconSectionInfo, 8); + + ulong nacpOffset = reader.ReadUInt64(); + ulong nacpSize = reader.ReadUInt64(); + + // Reads and stores game icon as byte array + if (iconSize > 0) + { + application.Icon = Read(assetOffset + iconOffset, (int)iconSize); + } + else + { + application.Icon = _nroIcon; + } + + // Read the NACP data + Read(assetOffset + (int)nacpOffset, (int)nacpSize).AsSpan().CopyTo(controlHolder.ByteSpan); + + GetApplicationInformation(ref controlHolder.Value, ref application); + } + else + { + application.Icon = _nroIcon; + application.Name = Path.GetFileNameWithoutExtension(applicationPath); + } + + application.ControlHolder = controlHolder; + applications.Add(application); + + break; byte[] Read(long position, int size) { @@ -334,102 +393,74 @@ namespace Ryujinx.Ui.App.Common return reader.ReadBytes(size); } - - try - { - file.Seek(24, SeekOrigin.Begin); - - int assetOffset = reader.ReadInt32(); - - if (Encoding.ASCII.GetString(Read(assetOffset, 4)) == "ASET") - { - byte[] iconSectionInfo = Read(assetOffset + 8, 0x10); - - long iconOffset = BitConverter.ToInt64(iconSectionInfo, 0); - long iconSize = BitConverter.ToInt64(iconSectionInfo, 8); - - ulong nacpOffset = reader.ReadUInt64(); - ulong nacpSize = reader.ReadUInt64(); - - // Reads and stores game icon as byte array - if (iconSize > 0) - { - applicationIcon = Read(assetOffset + iconOffset, (int)iconSize); - } - else - { - applicationIcon = _nroIcon; - } - - // Read the NACP data - Read(assetOffset + (int)nacpOffset, (int)nacpSize).AsSpan().CopyTo(controlHolder.ByteSpan); - - GetGameInformation(ref controlHolder.Value, out titleName, out titleId, out developer, out version); - } - else - { - applicationIcon = _nroIcon; - titleName = Path.GetFileNameWithoutExtension(applicationPath); - } - } - catch - { - Logger.Warning?.Print(LogClass.Application, $"The file encountered was not of a valid type. Errored File: {applicationPath}"); - - numApplicationsFound--; - - continue; - } } - else if (extension == ".nca") + case ".nca": { - try + ApplicationData application = new(); + + Nca nca = new(_virtualFileSystem.KeySet, new FileStream(applicationPath, FileMode.Open, FileAccess.Read).AsStorage()); + + if (!nca.IsProgram() || nca.IsPatch()) { - Nca nca = new(_virtualFileSystem.KeySet, new FileStream(applicationPath, FileMode.Open, FileAccess.Read).AsStorage()); - int dataIndex = Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program); - - if (nca.Header.ContentType != NcaContentType.Program || (nca.SectionExists(NcaSectionType.Data) && nca.Header.GetFsHeader(dataIndex).IsPatchSection())) - { - numApplicationsFound--; - - continue; - } - } - catch (InvalidDataException) - { - Logger.Warning?.Print(LogClass.Application, $"The NCA header content type check has failed. This is usually because the header key is incorrect or missing. Errored File: {applicationPath}"); - } - catch - { - Logger.Warning?.Print(LogClass.Application, $"The file encountered was not of a valid type. Errored File: {applicationPath}"); - - numApplicationsFound--; - - continue; + return false; } - applicationIcon = _ncaIcon; - titleName = Path.GetFileNameWithoutExtension(applicationPath); + application.Icon = _ncaIcon; + application.Name = Path.GetFileNameWithoutExtension(applicationPath); + application.ControlHolder = controlHolder; + + applications.Add(application); + + break; } - // If its an NSO we just set defaults - else if (extension == ".nso") + // If its an NSO we just set defaults + case ".nso": { - applicationIcon = _nsoIcon; - titleName = Path.GetFileNameWithoutExtension(applicationPath); + ApplicationData application = new() + { + Icon = _nsoIcon, + Name = Path.GetFileNameWithoutExtension(applicationPath), + }; + + applications.Add(application); + + break; } - } - catch (IOException exception) + } + } + catch (MissingKeyException exception) + { + Logger.Warning?.Print(LogClass.Application, $"Your key set is missing a key with the name: {exception.Name}"); + + return false; + } + catch (InvalidDataException) + { + Logger.Warning?.Print(LogClass.Application, $"The header key is incorrect or missing and therefore the NCA header content type check has failed. Errored File: {applicationPath}"); + + return false; + } + catch (IOException exception) + { + Logger.Warning?.Print(LogClass.Application, exception.Message); + + return false; + } + catch (Exception exception) + { + Logger.Warning?.Print(LogClass.Application, $"The file encountered was not of a valid type. File: '{applicationPath}' Error: {exception}"); + + return false; + } + + foreach (var data in applications) + { + // Only load metadata for applications with an ID + if (data.Id != 0) + { + ApplicationMetadata appMetadata = LoadAndSaveMetaData(data.IdString, appMetadata => { - Logger.Warning?.Print(LogClass.Application, exception.Message); - - numApplicationsFound--; - - continue; - } - - ApplicationMetadata appMetadata = LoadAndSaveMetaData(titleId, appMetadata => - { - appMetadata.Title = titleName; + appMetadata.Title = data.Name; // Only do the migration if time_played has a value and timespan_played hasn't been updated yet. if (appMetadata.TimePlayedOld != default && appMetadata.TimePlayed == TimeSpan.Zero) @@ -453,28 +484,283 @@ namespace Ryujinx.Ui.App.Common } }); - ApplicationData data = new() - { - Favorite = appMetadata.Favorite, - Icon = applicationIcon, - TitleName = titleName, - TitleId = titleId, - Developer = developer, - Version = version, - TimePlayed = appMetadata.TimePlayed, - LastPlayed = appMetadata.LastPlayed, - FileExtension = Path.GetExtension(applicationPath).TrimStart('.').ToUpper(), - FileSize = fileSize, - Path = applicationPath, - ControlHolder = controlHolder, - }; + data.Favorite = appMetadata.Favorite; + data.TimePlayed = appMetadata.TimePlayed; + data.LastPlayed = appMetadata.LastPlayed; + } - numApplicationsLoaded++; + data.FileExtension = Path.GetExtension(applicationPath).TrimStart('.').ToUpper(); + data.FileSize = fileSize; + data.Path = applicationPath; + } - OnApplicationAdded(new ApplicationAddedEventArgs + return true; + } + + public bool TryGetDownloadableContentFromFile(string filePath, out List titleUpdates) + { + titleUpdates = []; + + try + { + string extension = Path.GetExtension(filePath).ToLower(); + + using FileStream file = new(filePath, FileMode.Open, FileAccess.Read); + + switch (extension) + { + case ".xci": + case ".nsp": + { + IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks + ? IntegrityCheckLevel.ErrorOnInvalid + : IntegrityCheckLevel.None; + + using IFileSystem pfs = PartitionFileSystemUtils.OpenApplicationFileSystem(filePath, _virtualFileSystem); + + foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca")) + { + using var ncaFile = new UniqueRef(); + + pfs.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); + + Nca nca = TryOpenNca(ncaFile.Get.AsStorage()); + if (nca == null) + { + continue; + } + + if (nca.Header.ContentType == NcaContentType.PublicData) + { + titleUpdates.Add(new DownloadableContentModel(nca.Header.TitleId, filePath, fileEntry.FullPath)); + } + } + + return titleUpdates.Count != 0; + } + } + } + catch (MissingKeyException exception) + { + Logger.Warning?.Print(LogClass.Application, $"Your key set is missing a key with the name: {exception.Name}"); + } + catch (InvalidDataException) + { + Logger.Warning?.Print(LogClass.Application, $"The header key is incorrect or missing and therefore the NCA header content type check has failed. Errored File: {filePath}"); + } + catch (IOException exception) + { + Logger.Warning?.Print(LogClass.Application, exception.Message); + } + catch (Exception exception) + { + Logger.Warning?.Print(LogClass.Application, $"The file encountered was not of a valid type. File: '{filePath}' Error: {exception}"); + } + + return false; + } + + public bool TryGetTitleUpdatesFromFile(string filePath, out List titleUpdates) + { + titleUpdates = []; + + try + { + string extension = Path.GetExtension(filePath).ToLower(); + + using FileStream file = new(filePath, FileMode.Open, FileAccess.Read); + + switch (extension) + { + case ".xci": + case ".nsp": + { + IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks + ? IntegrityCheckLevel.ErrorOnInvalid + : IntegrityCheckLevel.None; + + using IFileSystem pfs = + PartitionFileSystemUtils.OpenApplicationFileSystem(filePath, _virtualFileSystem); + + Dictionary updates = + pfs.GetContentData(ContentMetaType.Patch, _virtualFileSystem, checkLevel); + + if (updates.Count == 0) + { + return false; + } + + foreach ((_, ContentMetaData content) in updates) + { + Nca patchNca = content.GetNcaByType(_virtualFileSystem.KeySet, ContentType.Program); + Nca controlNca = content.GetNcaByType(_virtualFileSystem.KeySet, ContentType.Control); + + if (controlNca != null && patchNca != null) + { + ApplicationControlProperty controlData = new(); + + using UniqueRef nacpFile = new(); + + controlNca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None) + .OpenFile(ref nacpFile.Ref, "/control.nacp".ToU8Span(), OpenMode.Read) + .ThrowIfFailure(); + nacpFile.Get.Read(out _, 0, SpanHelpers.AsByteSpan(ref controlData), + ReadOption.None).ThrowIfFailure(); + + var displayVersion = controlData.DisplayVersionString.ToString(); + var update = new TitleUpdateModel(content.ApplicationId, content.Version.Version, + displayVersion, filePath); + + titleUpdates.Add(update); + } + } + + return true; + } + } + } + catch (MissingKeyException exception) + { + Logger.Warning?.Print(LogClass.Application, $"Your key set is missing a key with the name: {exception.Name}"); + } + catch (InvalidDataException) + { + Logger.Warning?.Print(LogClass.Application, $"The header key is incorrect or missing and therefore the NCA header content type check has failed. Errored File: {filePath}"); + } + catch (IOException exception) + { + Logger.Warning?.Print(LogClass.Application, exception.Message); + } + catch (Exception exception) + { + Logger.Warning?.Print(LogClass.Application, $"The file encountered was not of a valid type. File: '{filePath}' Error: {exception}"); + } + + return false; + } + + public void CancelLoading() + { + _cancellationToken?.Cancel(); + } + + public static void ReadControlData(IFileSystem controlFs, Span outProperty) + { + using UniqueRef controlFile = new(); + + controlFs.OpenFile(ref controlFile.Ref, "/control.nacp".ToU8Span(), OpenMode.Read).ThrowIfFailure(); + controlFile.Get.Read(out _, 0, outProperty, ReadOption.None).ThrowIfFailure(); + } + + public void LoadApplications(List appDirs) + { + int numApplicationsFound = 0; + int numApplicationsLoaded = 0; + + _cancellationToken = new CancellationTokenSource(); + _applications.Clear(); + + // Builds the applications list with paths to found applications + List applicationPaths = new(); + + try + { + foreach (string appDir in appDirs) + { + if (_cancellationToken.Token.IsCancellationRequested) { - AppData = data, - }); + return; + } + + if (!Directory.Exists(appDir)) + { + Logger.Warning?.Print(LogClass.Application, $"The specified game directory \"{appDir}\" does not exist."); + + continue; + } + + try + { + EnumerationOptions options = new() + { + RecurseSubdirectories = true, + IgnoreInaccessible = false + }; + + IEnumerable files = Directory.EnumerateFiles(appDir, "*", options) + .Where(file => + (Path.GetExtension(file).ToLower() is ".nsp" && ConfigurationState.Instance.UI.ShownFileTypes.NSP) || + (Path.GetExtension(file).ToLower() is ".pfs0" && ConfigurationState.Instance.UI.ShownFileTypes.PFS0) || + (Path.GetExtension(file).ToLower() is ".xci" && ConfigurationState.Instance.UI.ShownFileTypes.XCI) || + (Path.GetExtension(file).ToLower() is ".nca" && ConfigurationState.Instance.UI.ShownFileTypes.NCA) || + (Path.GetExtension(file).ToLower() is ".nro" && ConfigurationState.Instance.UI.ShownFileTypes.NRO) || + (Path.GetExtension(file).ToLower() is ".nso" && ConfigurationState.Instance.UI.ShownFileTypes.NSO) + ); + + foreach (string app in files) + { + if (_cancellationToken.Token.IsCancellationRequested) + { + return; + } + + var fileInfo = new FileInfo(app); + + try + { + var fullPath = fileInfo.ResolveLinkTarget(true)?.FullName ?? fileInfo.FullName; + + applicationPaths.Add(fullPath); + numApplicationsFound++; + } + catch (IOException exception) + { + Logger.Warning?.Print(LogClass.Application, $"Failed to resolve the full path to file: \"{app}\" Error: {exception}"); + } + } + } + catch (UnauthorizedAccessException) + { + Logger.Warning?.Print(LogClass.Application, $"Failed to get access to directory: \"{appDir}\""); + } + } + + + // Loops through applications list, creating a struct and then firing an event containing the struct for each application + foreach (string applicationPath in applicationPaths) + { + if (_cancellationToken.Token.IsCancellationRequested) + { + return; + } + + if (TryGetApplicationsFromFile(applicationPath, out List applications)) + { + _applications.Edit(it => + { + foreach (var application in applications) + { + it.AddOrUpdate(application); + LoadDlcForApplication(application); + if (LoadTitleUpdatesForApplication(application)) + { + // Trigger a reload of the version data + RefreshApplicationInfo(application.IdBase); + } + } + }); + + if (applications.Count > 1) + { + numApplicationsFound += applications.Count - 1; + } + + numApplicationsLoaded += applications.Count; + } + else + { + numApplicationsFound--; + } OnApplicationCountUpdated(new ApplicationCountUpdatedEventArgs { @@ -496,9 +782,326 @@ namespace Ryujinx.Ui.App.Common } } - protected void OnApplicationAdded(ApplicationAddedEventArgs e) + public async Task RefreshLdn() { - ApplicationAdded?.Invoke(null, e); + + if (ConfigurationState.Instance.Multiplayer.Mode == MultiplayerMode.LdnRyu) + { + try + { + string ldnWebHost = ConfigurationState.Instance.Multiplayer.LdnServer; + if (string.IsNullOrEmpty(ldnWebHost)) + { + ldnWebHost = DefaultLanPlayWebHost; + } + IEnumerable ldnGameDataArray = Array.Empty(); + using HttpClient httpClient = new HttpClient(); + string ldnGameDataArrayString = await httpClient.GetStringAsync($"https://{ldnWebHost}/api/public_games"); + ldnGameDataArray = JsonHelper.Deserialize(ldnGameDataArrayString, _ldnDataSerializerContext.IEnumerableLdnGameData); + var evt = new LdnGameDataReceivedEventArgs + { + LdnData = ldnGameDataArray + }; + LdnGameDataReceived?.Invoke(null, evt); + } + catch (Exception ex) + { + Logger.Warning?.Print(LogClass.Application, $"Failed to fetch the public games JSON from the API. Player and game count in the game list will be unavailable.\n{ex.Message}"); + LdnGameDataReceived?.Invoke(null, new LdnGameDataReceivedEventArgs() + { + LdnData = Array.Empty() + }); + } + } + else + { + LdnGameDataReceived?.Invoke(null, new LdnGameDataReceivedEventArgs() + { + LdnData = Array.Empty() + }); + } + } + + // Replace the currently stored DLC state for the game with the provided DLC state. + public void SaveDownloadableContentsForGame(ApplicationData application, List<(DownloadableContentModel, bool IsEnabled)> dlcs) + { + _downloadableContents.Edit(it => + { + DownloadableContentsHelper.SaveDownloadableContentsJson(_virtualFileSystem, application.IdBase, dlcs); + + it.Remove(it.Items.Where(item => item.Dlc.TitleIdBase == application.IdBase)); + it.AddOrUpdate(dlcs); + }); + } + + // Replace the currently stored update state for the game with the provided update state. + public void SaveTitleUpdatesForGame(ApplicationData application, List<(TitleUpdateModel, bool IsSelected)> updates) + { + _titleUpdates.Edit(it => + { + TitleUpdatesHelper.SaveTitleUpdatesJson(_virtualFileSystem, application.IdBase, updates); + + it.Remove(it.Items.Where(item => item.TitleUpdate.TitleIdBase == application.IdBase)); + it.AddOrUpdate(updates); + RefreshApplicationInfo(application.IdBase); + }); + } + + // Searches the provided directories for DLC NSP files that are _valid for the currently detected games in the + // library_, and then enables those DLC. + public int AutoLoadDownloadableContents(List appDirs, out int numDlcRemoved) + { + _cancellationToken = new CancellationTokenSource(); + + List dlcPaths = new(); + int newDlcLoaded = 0; + numDlcRemoved = 0; + + try + { + // Remove any downloadable content which can no longer be located on disk + Logger.Notice.Print(LogClass.Application, $"Removing non-existing Title DLCs"); + var dlcToRemove = _downloadableContents.Items + .Where(dlc => !File.Exists(dlc.Dlc.ContainerPath)) + .ToList(); + dlcToRemove.ForEach(dlc => + Logger.Warning?.Print(LogClass.Application, $"Title DLC removed: {dlc.Dlc.ContainerPath}") + ); + numDlcRemoved += dlcToRemove.Distinct().Count(); + _downloadableContents.RemoveKeys(dlcToRemove.Select(dlc => dlc.Dlc)); + + foreach (string appDir in appDirs) + { + Logger.Notice.Print(LogClass.Application, $"Auto loading DLC from: {appDir}"); + + if (_cancellationToken.Token.IsCancellationRequested) + { + return newDlcLoaded; + } + + if (!Directory.Exists(appDir)) + { + Logger.Warning?.Print(LogClass.Application, + $"The specified autoload directory \"{appDir}\" does not exist."); + + continue; + } + + try + { + EnumerationOptions options = new() + { + RecurseSubdirectories = true, + IgnoreInaccessible = false, + }; + + IEnumerable files = Directory.EnumerateFiles(appDir, "*", options).Where( + file => Path.GetExtension(file).ToLower() is ".nsp"); + + foreach (string app in files) + { + if (_cancellationToken.Token.IsCancellationRequested) + { + return newDlcLoaded; + } + + var fileInfo = new FileInfo(app); + + try + { + var fullPath = fileInfo.ResolveLinkTarget(true)?.FullName ?? fileInfo.FullName; + + dlcPaths.Add(fullPath); + } + catch (IOException exception) + { + Logger.Warning?.Print(LogClass.Application, + $"Failed to resolve the full path to file: \"{app}\" Error: {exception}"); + } + } + } + catch (UnauthorizedAccessException) + { + Logger.Warning?.Print(LogClass.Application, + $"Failed to get access to directory: \"{appDir}\""); + } + } + + var appIdLookup = Applications.Items.Select(it => it.IdBase).ToHashSet(); + + foreach (string dlcPath in dlcPaths) + { + if (_cancellationToken.Token.IsCancellationRequested) + { + return newDlcLoaded; + } + + if (TryGetDownloadableContentFromFile(dlcPath, out var foundDlcs)) + { + foreach (var dlc in foundDlcs.Where(it => appIdLookup.Contains(it.TitleIdBase))) + { + if (!_downloadableContents.Lookup(dlc).HasValue) + { + _downloadableContents.AddOrUpdate((dlc, true)); + SaveDownloadableContentsForGame(dlc.TitleIdBase); + newDlcLoaded++; + } + } + } + } + } + finally + { + _cancellationToken.Dispose(); + _cancellationToken = null; + } + + return newDlcLoaded; + } + + // Searches the provided directories for update NSP files that are _valid for the currently detected games in the + // library_, and then applies those updates. If a newly-detected update is a newer version than the currently + // selected update (or if no update is currently selected), then that update will be selected. + public int AutoLoadTitleUpdates(List appDirs, out int numUpdatesRemoved) + { + _cancellationToken = new CancellationTokenSource(); + + List updatePaths = new(); + int numUpdatesLoaded = 0; + numUpdatesRemoved = 0; + + try + { + var titleIdsToSave = new HashSet(); + var titleIdsToRefresh = new HashSet(); + + // Remove any updates which can no longer be located on disk + Logger.Notice.Print(LogClass.Application, $"Removing non-existing Title Updates"); + var updatesToRemove = _titleUpdates.Items + .Where(it => !File.Exists(it.TitleUpdate.Path)) + .ToList(); + + numUpdatesRemoved += updatesToRemove.Select(it => it.TitleUpdate).Distinct().Count(); + updatesToRemove.ForEach(ti => + Logger.Warning?.Print(LogClass.Application, $"Title update removed: {ti.TitleUpdate.Path}") + ); + _titleUpdates.RemoveKeys(updatesToRemove.Select(it => it.TitleUpdate)); + titleIdsToSave.UnionWith(updatesToRemove.Select(it => it.TitleUpdate.TitleIdBase)); + titleIdsToRefresh.UnionWith(updatesToRemove.Where(it => it.IsSelected).Select(update => update.TitleUpdate.TitleIdBase)); + + foreach (string appDir in appDirs) + { + Logger.Notice.Print(LogClass.Application, $"Auto loading updates from: {appDir}"); + + if (_cancellationToken.Token.IsCancellationRequested) + { + return numUpdatesLoaded; + } + + if (!Directory.Exists(appDir)) + { + Logger.Warning?.Print(LogClass.Application, + $"The specified autoload directory \"{appDir}\" does not exist."); + + continue; + } + + try + { + EnumerationOptions options = new() + { + RecurseSubdirectories = true, + IgnoreInaccessible = false, + }; + + IEnumerable files = Directory.EnumerateFiles(appDir, "*", options).Where( + file => Path.GetExtension(file).ToLower() is ".nsp"); + + foreach (string app in files) + { + if (_cancellationToken.Token.IsCancellationRequested) + { + return numUpdatesLoaded; + } + + var fileInfo = new FileInfo(app); + + try + { + var fullPath = fileInfo.ResolveLinkTarget(true)?.FullName ?? fileInfo.FullName; + + updatePaths.Add(fullPath); + } + catch (IOException exception) + { + Logger.Warning?.Print(LogClass.Application, + $"Failed to resolve the full path to file: \"{app}\" Error: {exception}"); + } + } + } + catch (UnauthorizedAccessException) + { + Logger.Warning?.Print(LogClass.Application, + $"Failed to get access to directory: \"{appDir}\""); + } + } + + var appIdLookup = Applications.Items.Select(it => it.IdBase).ToHashSet(); + + foreach (string updatePath in updatePaths) + { + if (_cancellationToken.Token.IsCancellationRequested) + { + return numUpdatesLoaded; + } + + if (TryGetTitleUpdatesFromFile(updatePath, out var foundUpdates)) + { + foreach (var update in foundUpdates.Where(it => appIdLookup.Contains(it.TitleIdBase))) + { + if (!_titleUpdates.Lookup(update).HasValue) + { + bool shouldSelect = AddAndAutoSelectUpdate(update); + titleIdsToSave.Add(update.TitleIdBase); + numUpdatesLoaded++; + + if (shouldSelect) + { + titleIdsToRefresh.Add(update.TitleIdBase); + } + } + } + } + } + + titleIdsToSave.ForEach(titleId => SaveTitleUpdatesForGame(titleId)); + titleIdsToRefresh.ForEach(titleId => RefreshApplicationInfo(titleId)); + } + finally + { + _cancellationToken.Dispose(); + _cancellationToken = null; + } + + return numUpdatesLoaded; + } + + private bool AddAndAutoSelectUpdate(TitleUpdateModel update) + { + var currentlySelected = TitleUpdates.Items.FirstOrOptional(it => + it.TitleUpdate.TitleIdBase == update.TitleIdBase && it.IsSelected); + + var shouldSelect = !currentlySelected.HasValue || + currentlySelected.Value.TitleUpdate.Version < update.Version; + + _titleUpdates.AddOrUpdate((update, shouldSelect)); + + if (currentlySelected.HasValue && shouldSelect) + { + _titleUpdates.AddOrUpdate((currentlySelected.Value.TitleUpdate, false)); + } + + return shouldSelect; } protected void OnApplicationCountUpdated(ApplicationCountUpdatedEventArgs e) @@ -506,15 +1109,6 @@ namespace Ryujinx.Ui.App.Common ApplicationCountUpdated?.Invoke(null, e); } - private void GetControlFsAndTitleId(IFileSystem pfs, out IFileSystem controlFs, out string titleId) - { - (_, _, Nca controlNca) = GetGameData(_virtualFileSystem, pfs, 0); - - // Return the ControlFS - controlFs = controlNca?.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None); - titleId = controlNca?.Header.TitleId.ToString("x16"); - } - public static ApplicationMetadata LoadAndSaveMetaData(string titleId, Action modifyFunction = null) { string metadataFolder = Path.Combine(AppDataManager.GamesDirPath, titleId, "gui"); @@ -552,10 +1146,29 @@ namespace Ryujinx.Ui.App.Common return appMetadata; } - public byte[] GetApplicationIcon(string applicationPath, Language desiredTitleLanguage) + public byte[] GetApplicationIcon(string applicationPath, Language desiredTitleLanguage, ulong applicationId) { byte[] applicationIcon = null; + if (applicationId == 0) + { + if (Directory.Exists(applicationPath)) + { + return _ncaIcon; + } + + return Path.GetExtension(applicationPath).ToLower() switch + { + ".nsp" => _nspIcon, + ".pfs0" => _nspIcon, + ".xci" => _xciIcon, + ".nso" => _nsoIcon, + ".nro" => _nroIcon, + ".nca" => _ncaIcon, + _ => _ncaIcon, + }; + } + try { // Look for icon only if applicationPath is not a directory @@ -565,7 +1178,7 @@ namespace Ryujinx.Ui.App.Common using FileStream file = new(applicationPath, FileMode.Open, FileAccess.Read); - if (extension == ".nsp" || extension == ".pfs0" || extension == ".xci") + if (extension is ".nsp" or ".pfs0" or ".xci") { try { @@ -601,7 +1214,16 @@ namespace Ryujinx.Ui.App.Common else { // Store the ControlFS in variable called controlFs - GetControlFsAndTitleId(pfs, out IFileSystem controlFs, out _); + Dictionary programs = pfs.GetContentData(ContentMetaType.Application, _virtualFileSystem, _checkLevel); + IFileSystem controlFs = null; + + if (programs.TryGetValue(applicationId, out ContentMetaData value)) + { + if (value.GetNcaByType(_virtualFileSystem.KeySet, ContentType.Control) is { } controlNca) + { + controlFs = controlNca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None); + } + } // Read the icon from the ControlFS and store it as a byte array try @@ -628,16 +1250,11 @@ namespace Ryujinx.Ui.App.Common controlFs.OpenFile(ref icon.Ref, entry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); - using (MemoryStream stream = new()) - { - icon.Get.AsStream().CopyTo(stream); - applicationIcon = stream.ToArray(); - } + using MemoryStream stream = new(); + icon.Get.AsStream().CopyTo(stream); + applicationIcon = stream.ToArray(); - if (applicationIcon != null) - { - break; - } + break; } applicationIcon ??= extension == ".xci" ? _xciIcon : _nspIcon; @@ -720,80 +1337,79 @@ namespace Ryujinx.Ui.App.Common return applicationIcon ?? _ncaIcon; } - private void GetGameInformation(ref ApplicationControlProperty controlData, out string titleName, out string titleId, out string publisher, out string version) + private void GetApplicationInformation(ref ApplicationControlProperty controlData, ref ApplicationData data) { - _ = Enum.TryParse(_desiredTitleLanguage.ToString(), out TitleLanguage desiredTitleLanguage); + _ = Enum.TryParse(DesiredLanguage.ToString(), out TitleLanguage desiredTitleLanguage); if (controlData.Title.ItemsRo.Length > (int)desiredTitleLanguage) { - titleName = controlData.Title[(int)desiredTitleLanguage].NameString.ToString(); - publisher = controlData.Title[(int)desiredTitleLanguage].PublisherString.ToString(); + data.Name = controlData.Title[(int)desiredTitleLanguage].NameString.ToString(); + data.Developer = controlData.Title[(int)desiredTitleLanguage].PublisherString.ToString(); } else { - titleName = null; - publisher = null; + data.Name = null; + data.Developer = null; } - if (string.IsNullOrWhiteSpace(titleName)) + if (string.IsNullOrWhiteSpace(data.Name)) { foreach (ref readonly var controlTitle in controlData.Title.ItemsRo) { if (!controlTitle.NameString.IsEmpty()) { - titleName = controlTitle.NameString.ToString(); + data.Name = controlTitle.NameString.ToString(); break; } } } - if (string.IsNullOrWhiteSpace(publisher)) + if (string.IsNullOrWhiteSpace(data.Developer)) { foreach (ref readonly var controlTitle in controlData.Title.ItemsRo) { if (!controlTitle.PublisherString.IsEmpty()) { - publisher = controlTitle.PublisherString.ToString(); + data.Developer = controlTitle.PublisherString.ToString(); break; } } } - if (controlData.PresenceGroupId != 0) + if (data.Id == 0) { - titleId = controlData.PresenceGroupId.ToString("x16"); - } - else if (controlData.SaveDataOwnerId != 0) - { - titleId = controlData.SaveDataOwnerId.ToString(); - } - else if (controlData.AddOnContentBaseId != 0) - { - titleId = (controlData.AddOnContentBaseId - 0x1000).ToString("x16"); - } - else - { - titleId = "0000000000000000"; + if (controlData.SaveDataOwnerId != 0) + { + data.Id = controlData.SaveDataOwnerId; + } + else if (controlData.PresenceGroupId != 0) + { + data.Id = controlData.PresenceGroupId; + } + else if (controlData.AddOnContentBaseId != 0) + { + data.Id = (controlData.AddOnContentBaseId - 0x1000); + } } - version = controlData.DisplayVersionString.ToString(); + data.Version = controlData.DisplayVersionString.ToString(); } - private bool IsUpdateApplied(string titleId, out IFileSystem updatedControlFs) + private bool IsUpdateApplied(Nca mainNca, out IFileSystem updatedControlFs) { updatedControlFs = null; - string updatePath = "(unknown)"; + string updatePath = null; try { - (Nca patchNca, Nca controlNca) = GetGameUpdateData(_virtualFileSystem, titleId, 0, out updatePath); + (Nca patchNca, Nca controlNca) = mainNca.GetUpdateData(_virtualFileSystem, _checkLevel, 0, out updatePath); if (patchNca != null && controlNca != null) { - updatedControlFs = controlNca?.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None); + updatedControlFs = controlNca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None); return true; } @@ -810,119 +1426,129 @@ namespace Ryujinx.Ui.App.Common return false; } - public static (Nca main, Nca patch, Nca control) GetGameData(VirtualFileSystem fileSystem, IFileSystem pfs, int programIndex) + private Nca TryOpenNca(IStorage ncaStorage) { - Nca mainNca = null; - Nca patchNca = null; - Nca controlNca = null; - - fileSystem.ImportTickets(pfs); - - foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca")) + try { - using var ncaFile = new UniqueRef(); - - pfs.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); - - Nca nca = new(fileSystem.KeySet, ncaFile.Release().AsStorage()); - - int ncaProgramIndex = (int)(nca.Header.TitleId & 0xF); - - if (ncaProgramIndex != programIndex) - { - continue; - } - - if (nca.Header.ContentType == NcaContentType.Program) - { - int dataIndex = Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program); - - if (nca.SectionExists(NcaSectionType.Data) && nca.Header.GetFsHeader(dataIndex).IsPatchSection()) - { - patchNca = nca; - } - else - { - mainNca = nca; - } - } - else if (nca.Header.ContentType == NcaContentType.Control) - { - controlNca = nca; - } + return new Nca(_virtualFileSystem.KeySet, ncaStorage); } + catch (Exception) { } - return (mainNca, patchNca, controlNca); + return null; } - public static (Nca patch, Nca control) GetGameUpdateDataFromPartition(VirtualFileSystem fileSystem, PartitionFileSystem pfs, string titleId, int programIndex) + // Does a two-phase load of DLC. First reading the metadata on disk, then loading anything bundled in the game + // file itself + private void LoadDlcForApplication(ApplicationData application) { - Nca patchNca = null; - Nca controlNca = null; - - fileSystem.ImportTickets(pfs); - - foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca")) + _downloadableContents.Edit(it => { - using var ncaFile = new UniqueRef(); + var savedDlc = + DownloadableContentsHelper.LoadDownloadableContentsJson(_virtualFileSystem, application.IdBase); + it.AddOrUpdate(savedDlc); - pfs.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); - - Nca nca = new(fileSystem.KeySet, ncaFile.Release().AsStorage()); - - int ncaProgramIndex = (int)(nca.Header.TitleId & 0xF); - - if (ncaProgramIndex != programIndex) + if (TryGetDownloadableContentFromFile(application.Path, out var bundledDlc)) { - continue; - } + var savedDlcLookup = savedDlc.Select(dlc => dlc.Item1).ToHashSet(); - if ($"{nca.Header.TitleId.ToString("x16")[..^3]}000" != titleId) - { - break; - } - - if (nca.Header.ContentType == NcaContentType.Program) - { - patchNca = nca; - } - else if (nca.Header.ContentType == NcaContentType.Control) - { - controlNca = nca; - } - } - - return (patchNca, controlNca); - } - - public static (Nca patch, Nca control) GetGameUpdateData(VirtualFileSystem fileSystem, string titleId, int programIndex, out string updatePath) - { - updatePath = null; - - if (ulong.TryParse(titleId, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out ulong titleIdBase)) - { - // Clear the program index part. - titleIdBase &= ~0xFUL; - - // Load update information if exists. - string titleUpdateMetadataPath = Path.Combine(AppDataManager.GamesDirPath, titleIdBase.ToString("x16"), "updates.json"); - - if (File.Exists(titleUpdateMetadataPath)) - { - updatePath = JsonHelper.DeserializeFromFile(titleUpdateMetadataPath, _titleSerializerContext.TitleUpdateMetadata).Selected; - - if (File.Exists(updatePath)) + bool addedNewDlc = false; + foreach (var dlc in bundledDlc) { - FileStream file = new(updatePath, FileMode.Open, FileAccess.Read); - PartitionFileSystem nsp = new(); - nsp.Initialize(file.AsStorage()).ThrowIfFailure(); + if (!savedDlcLookup.Contains(dlc)) + { + addedNewDlc = true; + it.AddOrUpdate((dlc, true)); + } + } - return GetGameUpdateDataFromPartition(fileSystem, nsp, titleIdBase.ToString("x16"), programIndex); + if (addedNewDlc) + { + var gameDlcs = it.Items.Where(dlc => dlc.Dlc.TitleIdBase == application.IdBase).ToList(); + DownloadableContentsHelper.SaveDownloadableContentsJson(_virtualFileSystem, application.IdBase, + gameDlcs); } } - } + }); + } - return (null, null); + // Does a two-phase load of updates. First reading the metadata on disk, then loading anything bundled in the game + // file itself + private bool LoadTitleUpdatesForApplication(ApplicationData application) + { + var modifiedVersion = false; + + _titleUpdates.Edit(it => + { + var savedUpdates = + TitleUpdatesHelper.LoadTitleUpdatesJson(_virtualFileSystem, application.IdBase); + it.AddOrUpdate(savedUpdates); + + var selectedUpdate = savedUpdates.FirstOrOptional(update => update.IsSelected); + + if (TryGetTitleUpdatesFromFile(application.Path, out var bundledUpdates)) + { + var savedUpdateLookup = savedUpdates.Select(update => update.Item1).ToHashSet(); + bool updatesChanged = false; + + foreach (var update in bundledUpdates.OrderByDescending(bundled => bundled.Version)) + { + if (!savedUpdateLookup.Contains(update)) + { + bool shouldSelect = false; + if (!selectedUpdate.HasValue || selectedUpdate.Value.Item1.Version < update.Version) + { + shouldSelect = true; + if (selectedUpdate.HasValue) + _titleUpdates.AddOrUpdate((selectedUpdate.Value.Item1, false)); + selectedUpdate = DynamicData.Kernel.Optional<(TitleUpdateModel, bool IsSelected)>.Create((update, true)); + } + + modifiedVersion = modifiedVersion || shouldSelect; + it.AddOrUpdate((update, shouldSelect)); + + updatesChanged = true; + } + } + + if (updatesChanged) + { + var gameUpdates = it.Items.Where(update => update.TitleUpdate.TitleIdBase == application.IdBase).ToList(); + TitleUpdatesHelper.SaveTitleUpdatesJson(_virtualFileSystem, application.IdBase, gameUpdates); + } + } + }); + + return modifiedVersion; + } + + // Save the _currently tracked_ DLC state for the game + private void SaveDownloadableContentsForGame(ulong titleIdBase) + { + var dlcs = DownloadableContents.Items.Where(dlc => dlc.Dlc.TitleIdBase == titleIdBase).ToList(); + DownloadableContentsHelper.SaveDownloadableContentsJson(_virtualFileSystem, titleIdBase, dlcs); + } + + // Save the _currently tracked_ update state for the game + private void SaveTitleUpdatesForGame(ulong titleIdBase) + { + var updates = TitleUpdates.Items.Where(update => update.TitleUpdate.TitleIdBase == titleIdBase).ToList(); + TitleUpdatesHelper.SaveTitleUpdatesJson(_virtualFileSystem, titleIdBase, updates); + } + + // ApplicationData isnt live-updating (e.g. when an update gets applied) and so this is meant to trigger a refresh + // of its state + private void RefreshApplicationInfo(ulong appIdBase) + { + var application = _applications.Lookup(appIdBase); + + if (!application.HasValue) + return; + + if (!TryGetApplicationsFromFile(application.Value.Path, out List newApplications)) + return; + + var newApplication = newApplications.First(it => it.IdBase == appIdBase); + _applications.AddOrUpdate(newApplication); } } } diff --git a/src/Ryujinx.Ui.Common/App/ApplicationMetadata.cs b/src/Ryujinx.Ui.Common/App/ApplicationMetadata.cs index 43647feef..81193c5b3 100644 --- a/src/Ryujinx.Ui.Common/App/ApplicationMetadata.cs +++ b/src/Ryujinx.Ui.Common/App/ApplicationMetadata.cs @@ -1,7 +1,7 @@ using System; using System.Text.Json.Serialization; -namespace Ryujinx.Ui.App.Common +namespace Ryujinx.UI.App.Common { public class ApplicationMetadata { diff --git a/src/Ryujinx.Ui.Common/Configuration/AudioBackend.cs b/src/Ryujinx.Ui.Common/Configuration/AudioBackend.cs index dc0a5ac61..a952e7ac0 100644 --- a/src/Ryujinx.Ui.Common/Configuration/AudioBackend.cs +++ b/src/Ryujinx.Ui.Common/Configuration/AudioBackend.cs @@ -1,7 +1,7 @@ using Ryujinx.Common.Utilities; using System.Text.Json.Serialization; -namespace Ryujinx.Ui.Common.Configuration +namespace Ryujinx.UI.Common.Configuration { [JsonConverter(typeof(TypedStringEnumConverter))] public enum AudioBackend diff --git a/src/Ryujinx.Ui.Common/Configuration/ConfigurationFileFormat.cs b/src/Ryujinx.Ui.Common/Configuration/ConfigurationFileFormat.cs index 8a4db1fe7..027e1052b 100644 --- a/src/Ryujinx.Ui.Common/Configuration/ConfigurationFileFormat.cs +++ b/src/Ryujinx.Ui.Common/Configuration/ConfigurationFileFormat.cs @@ -1,21 +1,23 @@ +using Ryujinx.Common; using Ryujinx.Common.Configuration; using Ryujinx.Common.Configuration.Hid; using Ryujinx.Common.Configuration.Multiplayer; using Ryujinx.Common.Logging; using Ryujinx.Common.Utilities; -using Ryujinx.Ui.Common.Configuration.System; -using Ryujinx.Ui.Common.Configuration.Ui; +using Ryujinx.HLE; +using Ryujinx.UI.Common.Configuration.System; +using Ryujinx.UI.Common.Configuration.UI; using System.Collections.Generic; using System.Text.Json.Nodes; -namespace Ryujinx.Ui.Common.Configuration +namespace Ryujinx.UI.Common.Configuration { public class ConfigurationFileFormat { /// /// The current version of the file format /// - public const int CurrentVersion = 48; + public const int CurrentVersion = 57; /// /// Version of the configuration file format @@ -162,6 +164,26 @@ namespace Ryujinx.Ui.Common.Configuration /// public bool ShowConfirmExit { get; set; } + /// + /// ignore "Applet" dialog + /// + public bool IgnoreApplet { get; set; } + + /// + /// Enables or disables save window size, position and state on close. + /// + public bool RememberWindowState { get; set; } + + /// + /// Enables or disables the redesigned title bar + /// + public bool ShowTitleBar { get; set; } + + /// + /// Enables hardware-accelerated rendering for Avalonia + /// + public bool EnableHardwareAcceleration { get; set; } + /// /// Whether to hide cursor on idle, always or never /// @@ -170,8 +192,25 @@ namespace Ryujinx.Ui.Common.Configuration /// /// Enables or disables Vertical Sync /// + /// Kept for file format compatibility (to avoid possible failure when parsing configuration on old versions) + /// TODO: Remove this when those older versions aren't in use anymore. public bool EnableVsync { get; set; } + /// + /// Current VSync mode; 60 (Switch), unbounded ("Vsync off"), or custom + /// + public VSyncMode VSyncMode { get; set; } + + /// + /// Enables or disables the custom present interval + /// + public bool EnableCustomVSyncInterval { get; set; } + + /// + /// The custom present interval value + /// + public int CustomVSyncInterval { get; set; } + /// /// Enables or disables Shader cache /// @@ -197,6 +236,11 @@ namespace Ryujinx.Ui.Common.Configuration /// public bool EnablePtc { get; set; } + /// + /// Enables or disables low-power profiled translation cache persistency loading + /// + public bool EnableLowPowerPtc { get; set; } + /// /// Enables or disables guest Internet access /// @@ -228,9 +272,9 @@ namespace Ryujinx.Ui.Common.Configuration public MemoryManagerMode MemoryManagerMode { get; set; } /// - /// Expands the RAM amount on the emulated system from 4GiB to 6GiB + /// Expands the RAM amount on the emulated system from 4GiB to 8GiB /// - public bool ExpandRam { get; set; } + public MemoryConfiguration DramSize { get; set; } /// /// Enable or disable ignoring missing services @@ -252,6 +296,11 @@ namespace Ryujinx.Ui.Common.Configuration /// public List GameDirs { get; set; } + /// + /// A list of directories containing DLC/updates the user wants to autoload during library refreshes + /// + public List AutoloadDirs { get; set; } + /// /// A list of file types to be hidden in the games List /// @@ -267,16 +316,6 @@ namespace Ryujinx.Ui.Common.Configuration /// public string LanguageCode { get; set; } - /// - /// Enable or disable custom themes in the GUI - /// - public bool EnableCustomTheme { get; set; } - - /// - /// Path to custom GUI theme - /// - public string CustomThemePath { get; set; } - /// /// Chooses the base style // Not Used /// @@ -371,6 +410,21 @@ namespace Ryujinx.Ui.Common.Configuration /// public string MultiplayerLanInterfaceId { get; set; } + /// + /// Disable P2p Toggle + /// + public bool MultiplayerDisableP2p { get; set; } + + /// + /// Local network passphrase, for private networks. + /// + public string MultiplayerLdnPassphrase { get; set; } + + /// + /// Custom LDN Server + /// + public string LdnServer { get; set; } + /// /// Uses Hypervisor over JIT if available /// diff --git a/src/Ryujinx.Ui.Common/Configuration/ConfigurationFileFormatSettings.cs b/src/Ryujinx.Ui.Common/Configuration/ConfigurationFileFormatSettings.cs index 9a1841fc5..9861ebf1f 100644 --- a/src/Ryujinx.Ui.Common/Configuration/ConfigurationFileFormatSettings.cs +++ b/src/Ryujinx.Ui.Common/Configuration/ConfigurationFileFormatSettings.cs @@ -1,6 +1,6 @@ using Ryujinx.Common.Utilities; -namespace Ryujinx.Ui.Common.Configuration +namespace Ryujinx.UI.Common.Configuration { internal static class ConfigurationFileFormatSettings { diff --git a/src/Ryujinx.Ui.Common/Configuration/ConfigurationJsonSerializerContext.cs b/src/Ryujinx.Ui.Common/Configuration/ConfigurationJsonSerializerContext.cs index 03989edec..3c3e3f20d 100644 --- a/src/Ryujinx.Ui.Common/Configuration/ConfigurationJsonSerializerContext.cs +++ b/src/Ryujinx.Ui.Common/Configuration/ConfigurationJsonSerializerContext.cs @@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace Ryujinx.Ui.Common.Configuration +namespace Ryujinx.UI.Common.Configuration { [JsonSourceGenerationOptions(WriteIndented = true)] [JsonSerializable(typeof(ConfigurationFileFormat))] diff --git a/src/Ryujinx.Ui.Common/Configuration/ConfigurationState.cs b/src/Ryujinx.Ui.Common/Configuration/ConfigurationState.cs index 52575a7e3..badb047df 100644 --- a/src/Ryujinx.Ui.Common/Configuration/ConfigurationState.cs +++ b/src/Ryujinx.Ui.Common/Configuration/ConfigurationState.cs @@ -1,4 +1,4 @@ -using Ryujinx.Common; +using ARMeilleure; using Ryujinx.Common.Configuration; using Ryujinx.Common.Configuration.Hid; using Ryujinx.Common.Configuration.Hid.Controller; @@ -6,644 +6,25 @@ using Ryujinx.Common.Configuration.Hid.Keyboard; using Ryujinx.Common.Configuration.Multiplayer; using Ryujinx.Common.Logging; using Ryujinx.Graphics.Vulkan; -using Ryujinx.Ui.Common.Configuration.System; -using Ryujinx.Ui.Common.Configuration.Ui; -using Ryujinx.Ui.Common.Helper; +using Ryujinx.HLE; +using Ryujinx.UI.Common.Configuration.System; +using Ryujinx.UI.Common.Configuration.UI; using System; using System.Collections.Generic; -using System.Text.Json.Nodes; -namespace Ryujinx.Ui.Common.Configuration +namespace Ryujinx.UI.Common.Configuration { - public class ConfigurationState + public partial class ConfigurationState { - /// - /// UI configuration section - /// - public class UiSection + public static void Initialize() { - public class Columns + if (Instance != null) { - public ReactiveObject FavColumn { get; private set; } - public ReactiveObject IconColumn { get; private set; } - public ReactiveObject AppColumn { get; private set; } - public ReactiveObject DevColumn { get; private set; } - public ReactiveObject VersionColumn { get; private set; } - public ReactiveObject TimePlayedColumn { get; private set; } - public ReactiveObject LastPlayedColumn { get; private set; } - public ReactiveObject FileExtColumn { get; private set; } - public ReactiveObject FileSizeColumn { get; private set; } - public ReactiveObject PathColumn { get; private set; } - - public Columns() - { - FavColumn = new ReactiveObject(); - IconColumn = new ReactiveObject(); - AppColumn = new ReactiveObject(); - DevColumn = new ReactiveObject(); - VersionColumn = new ReactiveObject(); - TimePlayedColumn = new ReactiveObject(); - LastPlayedColumn = new ReactiveObject(); - FileExtColumn = new ReactiveObject(); - FileSizeColumn = new ReactiveObject(); - PathColumn = new ReactiveObject(); + throw new InvalidOperationException("Configuration is already initialized"); } - } - public class ColumnSortSettings - { - public ReactiveObject SortColumnId { get; private set; } - public ReactiveObject SortAscending { get; private set; } - - public ColumnSortSettings() - { - SortColumnId = new ReactiveObject(); - SortAscending = new ReactiveObject(); + Instance = new ConfigurationState(); } - } - - /// - /// Used to toggle which file types are shown in the UI - /// - public class ShownFileTypeSettings - { - public ReactiveObject NSP { get; private set; } - public ReactiveObject PFS0 { get; private set; } - public ReactiveObject XCI { get; private set; } - public ReactiveObject NCA { get; private set; } - public ReactiveObject NRO { get; private set; } - public ReactiveObject NSO { get; private set; } - - public ShownFileTypeSettings() - { - NSP = new ReactiveObject(); - PFS0 = new ReactiveObject(); - XCI = new ReactiveObject(); - NCA = new ReactiveObject(); - NRO = new ReactiveObject(); - NSO = new ReactiveObject(); - } - } - - // - /// Determines main window start-up position, size and state - /// - public class WindowStartupSettings - { - public ReactiveObject WindowSizeWidth { get; private set; } - public ReactiveObject WindowSizeHeight { get; private set; } - public ReactiveObject WindowPositionX { get; private set; } - public ReactiveObject WindowPositionY { get; private set; } - public ReactiveObject WindowMaximized { get; private set; } - - public WindowStartupSettings() - { - WindowSizeWidth = new ReactiveObject(); - WindowSizeHeight = new ReactiveObject(); - WindowPositionX = new ReactiveObject(); - WindowPositionY = new ReactiveObject(); - WindowMaximized = new ReactiveObject(); - } - } - - /// - /// Used to toggle columns in the GUI - /// - public Columns GuiColumns { get; private set; } - - /// - /// Used to configure column sort settings in the GUI - /// - public ColumnSortSettings ColumnSort { get; private set; } - - /// - /// A list of directories containing games to be used to load games into the games list - /// - public ReactiveObject> GameDirs { get; private set; } - - /// - /// A list of file types to be hidden in the games List - /// - public ShownFileTypeSettings ShownFileTypes { get; private set; } - - /// - /// Determines main window start-up position, size and state - /// - public WindowStartupSettings WindowStartup { get; private set; } - - /// - /// Language Code for the UI - /// - public ReactiveObject LanguageCode { get; private set; } - - /// - /// Enable or disable custom themes in the GUI - /// - public ReactiveObject EnableCustomTheme { get; private set; } - - /// - /// Path to custom GUI theme - /// - public ReactiveObject CustomThemePath { get; private set; } - - /// - /// Selects the base style - /// - public ReactiveObject BaseStyle { get; private set; } - - /// - /// Start games in fullscreen mode - /// - public ReactiveObject StartFullscreen { get; private set; } - - /// - /// Hide / Show Console Window - /// - public ReactiveObject ShowConsole { get; private set; } - - /// - /// View Mode of the Game list - /// - public ReactiveObject GameListViewMode { get; private set; } - - /// - /// Show application name in Grid Mode - /// - public ReactiveObject ShowNames { get; private set; } - - /// - /// Sets App Icon Size in Grid Mode - /// - public ReactiveObject GridSize { get; private set; } - - /// - /// Sorts Apps in Grid Mode - /// - public ReactiveObject ApplicationSort { get; private set; } - - /// - /// Sets if Grid is ordered in Ascending Order - /// - public ReactiveObject IsAscendingOrder { get; private set; } - - public UiSection() - { - GuiColumns = new Columns(); - ColumnSort = new ColumnSortSettings(); - GameDirs = new ReactiveObject>(); - ShownFileTypes = new ShownFileTypeSettings(); - WindowStartup = new WindowStartupSettings(); - EnableCustomTheme = new ReactiveObject(); - CustomThemePath = new ReactiveObject(); - BaseStyle = new ReactiveObject(); - StartFullscreen = new ReactiveObject(); - GameListViewMode = new ReactiveObject(); - ShowNames = new ReactiveObject(); - GridSize = new ReactiveObject(); - ApplicationSort = new ReactiveObject(); - IsAscendingOrder = new ReactiveObject(); - LanguageCode = new ReactiveObject(); - ShowConsole = new ReactiveObject(); - ShowConsole.Event += static (s, e) => { ConsoleHelper.SetConsoleWindowState(e.NewValue); }; - } - } - - /// - /// Logger configuration section - /// - public class LoggerSection - { - /// - /// Enables printing debug log messages - /// - public ReactiveObject EnableDebug { get; private set; } - - /// - /// Enables printing stub log messages - /// - public ReactiveObject EnableStub { get; private set; } - - /// - /// Enables printing info log messages - /// - public ReactiveObject EnableInfo { get; private set; } - - /// - /// Enables printing warning log messages - /// - public ReactiveObject EnableWarn { get; private set; } - - /// - /// Enables printing error log messages - /// - public ReactiveObject EnableError { get; private set; } - - /// - /// Enables printing trace log messages - /// - public ReactiveObject EnableTrace { get; private set; } - - /// - /// Enables printing guest log messages - /// - public ReactiveObject EnableGuest { get; private set; } - - /// - /// Enables printing FS access log messages - /// - public ReactiveObject EnableFsAccessLog { get; private set; } - - /// - /// Controls which log messages are written to the log targets - /// - public ReactiveObject FilteredClasses { get; private set; } - - /// - /// Enables or disables logging to a file on disk - /// - public ReactiveObject EnableFileLog { get; private set; } - - /// - /// Controls which OpenGL log messages are recorded in the log - /// - public ReactiveObject GraphicsDebugLevel { get; private set; } - - public LoggerSection() - { - EnableDebug = new ReactiveObject(); - EnableStub = new ReactiveObject(); - EnableInfo = new ReactiveObject(); - EnableWarn = new ReactiveObject(); - EnableError = new ReactiveObject(); - EnableTrace = new ReactiveObject(); - EnableGuest = new ReactiveObject(); - EnableFsAccessLog = new ReactiveObject(); - FilteredClasses = new ReactiveObject(); - EnableFileLog = new ReactiveObject(); - EnableFileLog.Event += static (sender, e) => LogValueChange(e, nameof(EnableFileLog)); - GraphicsDebugLevel = new ReactiveObject(); - } - } - - /// - /// System configuration section - /// - public class SystemSection - { - /// - /// Change System Language - /// - public ReactiveObject Language { get; private set; } - - /// - /// Change System Region - /// - public ReactiveObject Region { get; private set; } - - /// - /// Change System TimeZone - /// - public ReactiveObject TimeZone { get; private set; } - - /// - /// System Time Offset in Seconds - /// - public ReactiveObject SystemTimeOffset { get; private set; } - - /// - /// Enables or disables Docked Mode - /// - public ReactiveObject EnableDockedMode { get; private set; } - - /// - /// Enables or disables profiled translation cache persistency - /// - public ReactiveObject EnablePtc { get; private set; } - - /// - /// Enables or disables guest Internet access - /// - public ReactiveObject EnableInternetAccess { get; private set; } - - /// - /// Enables integrity checks on Game content files - /// - public ReactiveObject EnableFsIntegrityChecks { get; private set; } - - /// - /// Enables FS access log output to the console. Possible modes are 0-3 - /// - public ReactiveObject FsGlobalAccessLogMode { get; private set; } - - /// - /// The selected audio backend - /// - public ReactiveObject AudioBackend { get; private set; } - - /// - /// The audio backend volume - /// - public ReactiveObject AudioVolume { get; private set; } - - /// - /// The selected memory manager mode - /// - public ReactiveObject MemoryManagerMode { get; private set; } - - /// - /// Defines the amount of RAM available on the emulated system, and how it is distributed - /// - public ReactiveObject ExpandRam { get; private set; } - - /// - /// Enable or disable ignoring missing services - /// - public ReactiveObject IgnoreMissingServices { get; private set; } - - /// - /// Uses Hypervisor over JIT if available - /// - public ReactiveObject UseHypervisor { get; private set; } - - public SystemSection() - { - Language = new ReactiveObject(); - Region = new ReactiveObject(); - TimeZone = new ReactiveObject(); - SystemTimeOffset = new ReactiveObject(); - EnableDockedMode = new ReactiveObject(); - EnableDockedMode.Event += static (sender, e) => LogValueChange(e, nameof(EnableDockedMode)); - EnablePtc = new ReactiveObject(); - EnablePtc.Event += static (sender, e) => LogValueChange(e, nameof(EnablePtc)); - EnableInternetAccess = new ReactiveObject(); - EnableInternetAccess.Event += static (sender, e) => LogValueChange(e, nameof(EnableInternetAccess)); - EnableFsIntegrityChecks = new ReactiveObject(); - EnableFsIntegrityChecks.Event += static (sender, e) => LogValueChange(e, nameof(EnableFsIntegrityChecks)); - FsGlobalAccessLogMode = new ReactiveObject(); - FsGlobalAccessLogMode.Event += static (sender, e) => LogValueChange(e, nameof(FsGlobalAccessLogMode)); - AudioBackend = new ReactiveObject(); - AudioBackend.Event += static (sender, e) => LogValueChange(e, nameof(AudioBackend)); - MemoryManagerMode = new ReactiveObject(); - MemoryManagerMode.Event += static (sender, e) => LogValueChange(e, nameof(MemoryManagerMode)); - ExpandRam = new ReactiveObject(); - ExpandRam.Event += static (sender, e) => LogValueChange(e, nameof(ExpandRam)); - IgnoreMissingServices = new ReactiveObject(); - IgnoreMissingServices.Event += static (sender, e) => LogValueChange(e, nameof(IgnoreMissingServices)); - AudioVolume = new ReactiveObject(); - AudioVolume.Event += static (sender, e) => LogValueChange(e, nameof(AudioVolume)); - UseHypervisor = new ReactiveObject(); - UseHypervisor.Event += static (sender, e) => LogValueChange(e, nameof(UseHypervisor)); - } - } - - /// - /// Hid configuration section - /// - public class HidSection - { - /// - /// Enable or disable keyboard support (Independent from controllers binding) - /// - public ReactiveObject EnableKeyboard { get; private set; } - - /// - /// Enable or disable mouse support (Independent from controllers binding) - /// - public ReactiveObject EnableMouse { get; private set; } - - /// - /// Hotkey Keyboard Bindings - /// - public ReactiveObject Hotkeys { get; private set; } - - /// - /// Input device configuration. - /// NOTE: This ReactiveObject won't issue an event when the List has elements added or removed. - /// TODO: Implement a ReactiveList class. - /// - public ReactiveObject> InputConfig { get; private set; } - - public HidSection() - { - EnableKeyboard = new ReactiveObject(); - EnableMouse = new ReactiveObject(); - Hotkeys = new ReactiveObject(); - InputConfig = new ReactiveObject>(); - } - } - - /// - /// Graphics configuration section - /// - public class GraphicsSection - { - /// - /// Whether or not backend threading is enabled. The "Auto" setting will determine whether threading should be enabled at runtime. - /// - public ReactiveObject BackendThreading { get; private set; } - - /// - /// Max Anisotropy. Values range from 0 - 16. Set to -1 to let the game decide. - /// - public ReactiveObject MaxAnisotropy { get; private set; } - - /// - /// Aspect Ratio applied to the renderer window. - /// - public ReactiveObject AspectRatio { get; private set; } - - /// - /// Resolution Scale. An integer scale applied to applicable render targets. Values 1-4, or -1 to use a custom floating point scale instead. - /// - public ReactiveObject ResScale { get; private set; } - - /// - /// Custom Resolution Scale. A custom floating point scale applied to applicable render targets. Only active when Resolution Scale is -1. - /// - public ReactiveObject ResScaleCustom { get; private set; } - - /// - /// Dumps shaders in this local directory - /// - public ReactiveObject ShadersDumpPath { get; private set; } - - /// - /// Enables or disables Vertical Sync - /// - public ReactiveObject EnableVsync { get; private set; } - - /// - /// Enables or disables Shader cache - /// - public ReactiveObject EnableShaderCache { get; private set; } - - /// - /// Enables or disables texture recompression - /// - public ReactiveObject EnableTextureRecompression { get; private set; } - - /// - /// Enables or disables Macro high-level emulation - /// - public ReactiveObject EnableMacroHLE { get; private set; } - - /// - /// Enables or disables color space passthrough, if available. - /// - public ReactiveObject EnableColorSpacePassthrough { get; private set; } - - /// - /// Graphics backend - /// - public ReactiveObject GraphicsBackend { get; private set; } - - /// - /// Applies anti-aliasing to the renderer. - /// - public ReactiveObject AntiAliasing { get; private set; } - - /// - /// Sets the framebuffer upscaling type. - /// - public ReactiveObject ScalingFilter { get; private set; } - - /// - /// Sets the framebuffer upscaling level. - /// - public ReactiveObject ScalingFilterLevel { get; private set; } - - /// - /// Preferred GPU - /// - public ReactiveObject PreferredGpu { get; private set; } - - public GraphicsSection() - { - BackendThreading = new ReactiveObject(); - BackendThreading.Event += static (sender, e) => LogValueChange(e, nameof(BackendThreading)); - ResScale = new ReactiveObject(); - ResScale.Event += static (sender, e) => LogValueChange(e, nameof(ResScale)); - ResScaleCustom = new ReactiveObject(); - ResScaleCustom.Event += static (sender, e) => LogValueChange(e, nameof(ResScaleCustom)); - MaxAnisotropy = new ReactiveObject(); - MaxAnisotropy.Event += static (sender, e) => LogValueChange(e, nameof(MaxAnisotropy)); - AspectRatio = new ReactiveObject(); - AspectRatio.Event += static (sender, e) => LogValueChange(e, nameof(AspectRatio)); - ShadersDumpPath = new ReactiveObject(); - EnableVsync = new ReactiveObject(); - EnableVsync.Event += static (sender, e) => LogValueChange(e, nameof(EnableVsync)); - EnableShaderCache = new ReactiveObject(); - EnableShaderCache.Event += static (sender, e) => LogValueChange(e, nameof(EnableShaderCache)); - EnableTextureRecompression = new ReactiveObject(); - EnableTextureRecompression.Event += static (sender, e) => LogValueChange(e, nameof(EnableTextureRecompression)); - GraphicsBackend = new ReactiveObject(); - GraphicsBackend.Event += static (sender, e) => LogValueChange(e, nameof(GraphicsBackend)); - PreferredGpu = new ReactiveObject(); - PreferredGpu.Event += static (sender, e) => LogValueChange(e, nameof(PreferredGpu)); - EnableMacroHLE = new ReactiveObject(); - EnableMacroHLE.Event += static (sender, e) => LogValueChange(e, nameof(EnableMacroHLE)); - EnableColorSpacePassthrough = new ReactiveObject(); - EnableColorSpacePassthrough.Event += static (sender, e) => LogValueChange(e, nameof(EnableColorSpacePassthrough)); - AntiAliasing = new ReactiveObject(); - AntiAliasing.Event += static (sender, e) => LogValueChange(e, nameof(AntiAliasing)); - ScalingFilter = new ReactiveObject(); - ScalingFilter.Event += static (sender, e) => LogValueChange(e, nameof(ScalingFilter)); - ScalingFilterLevel = new ReactiveObject(); - ScalingFilterLevel.Event += static (sender, e) => LogValueChange(e, nameof(ScalingFilterLevel)); - } - } - - /// - /// Multiplayer configuration section - /// - public class MultiplayerSection - { - /// - /// GUID for the network interface used by LAN (or 0 for default) - /// - public ReactiveObject LanInterfaceId { get; private set; } - - /// - /// Multiplayer Mode - /// - public ReactiveObject Mode { get; private set; } - - public MultiplayerSection() - { - LanInterfaceId = new ReactiveObject(); - Mode = new ReactiveObject(); - Mode.Event += static (_, e) => LogValueChange(e, nameof(MultiplayerMode)); - } - } - - /// - /// The default configuration instance - /// - public static ConfigurationState Instance { get; private set; } - - /// - /// The Ui section - /// - public UiSection Ui { get; private set; } - - /// - /// The Logger section - /// - public LoggerSection Logger { get; private set; } - - /// - /// The System section - /// - public SystemSection System { get; private set; } - - /// - /// The Graphics section - /// - public GraphicsSection Graphics { get; private set; } - - /// - /// The Hid section - /// - public HidSection Hid { get; private set; } - - /// - /// The Multiplayer section - /// - public MultiplayerSection Multiplayer { get; private set; } - - /// - /// Enables or disables Discord Rich Presence - /// - public ReactiveObject EnableDiscordIntegration { get; private set; } - - /// - /// Checks for updates when Ryujinx starts when enabled - /// - public ReactiveObject CheckUpdatesOnStart { get; private set; } - - /// - /// Show "Confirm Exit" Dialog - /// - public ReactiveObject ShowConfirmExit { get; private set; } - - /// - /// Hide Cursor on Idle - /// - public ReactiveObject HideCursor { get; private set; } - - private ConfigurationState() - { - Ui = new UiSection(); - Logger = new LoggerSection(); - System = new SystemSection(); - Graphics = new GraphicsSection(); - Hid = new HidSection(); - Multiplayer = new MultiplayerSection(); - EnableDiscordIntegration = new ReactiveObject(); - CheckUpdatesOnStart = new ReactiveObject(); - ShowConfirmExit = new ReactiveObject(); - HideCursor = new ReactiveObject(); - } public ConfigurationFileFormat ToFileFormat() { @@ -678,79 +59,89 @@ namespace Ryujinx.Ui.Common.Configuration EnableDiscordIntegration = EnableDiscordIntegration, CheckUpdatesOnStart = CheckUpdatesOnStart, ShowConfirmExit = ShowConfirmExit, + IgnoreApplet = IgnoreApplet, + RememberWindowState = RememberWindowState, + ShowTitleBar = ShowTitleBar, + EnableHardwareAcceleration = EnableHardwareAcceleration, HideCursor = HideCursor, - EnableVsync = Graphics.EnableVsync, + VSyncMode = Graphics.VSyncMode, + EnableCustomVSyncInterval = Graphics.EnableCustomVSyncInterval, + CustomVSyncInterval = Graphics.CustomVSyncInterval, EnableShaderCache = Graphics.EnableShaderCache, EnableTextureRecompression = Graphics.EnableTextureRecompression, EnableMacroHLE = Graphics.EnableMacroHLE, EnableColorSpacePassthrough = Graphics.EnableColorSpacePassthrough, EnablePtc = System.EnablePtc, + EnableLowPowerPtc = System.EnableLowPowerPtc, EnableInternetAccess = System.EnableInternetAccess, EnableFsIntegrityChecks = System.EnableFsIntegrityChecks, FsGlobalAccessLogMode = System.FsGlobalAccessLogMode, AudioBackend = System.AudioBackend, AudioVolume = System.AudioVolume, MemoryManagerMode = System.MemoryManagerMode, - ExpandRam = System.ExpandRam, + DramSize = System.DramSize, IgnoreMissingServices = System.IgnoreMissingServices, UseHypervisor = System.UseHypervisor, GuiColumns = new GuiColumns { - FavColumn = Ui.GuiColumns.FavColumn, - IconColumn = Ui.GuiColumns.IconColumn, - AppColumn = Ui.GuiColumns.AppColumn, - DevColumn = Ui.GuiColumns.DevColumn, - VersionColumn = Ui.GuiColumns.VersionColumn, - TimePlayedColumn = Ui.GuiColumns.TimePlayedColumn, - LastPlayedColumn = Ui.GuiColumns.LastPlayedColumn, - FileExtColumn = Ui.GuiColumns.FileExtColumn, - FileSizeColumn = Ui.GuiColumns.FileSizeColumn, - PathColumn = Ui.GuiColumns.PathColumn, + FavColumn = UI.GuiColumns.FavColumn, + IconColumn = UI.GuiColumns.IconColumn, + AppColumn = UI.GuiColumns.AppColumn, + DevColumn = UI.GuiColumns.DevColumn, + VersionColumn = UI.GuiColumns.VersionColumn, + LdnInfoColumn = UI.GuiColumns.LdnInfoColumn, + TimePlayedColumn = UI.GuiColumns.TimePlayedColumn, + LastPlayedColumn = UI.GuiColumns.LastPlayedColumn, + FileExtColumn = UI.GuiColumns.FileExtColumn, + FileSizeColumn = UI.GuiColumns.FileSizeColumn, + PathColumn = UI.GuiColumns.PathColumn, }, ColumnSort = new ColumnSort { - SortColumnId = Ui.ColumnSort.SortColumnId, - SortAscending = Ui.ColumnSort.SortAscending, + SortColumnId = UI.ColumnSort.SortColumnId, + SortAscending = UI.ColumnSort.SortAscending, }, - GameDirs = Ui.GameDirs, + GameDirs = UI.GameDirs, + AutoloadDirs = UI.AutoloadDirs, ShownFileTypes = new ShownFileTypes { - NSP = Ui.ShownFileTypes.NSP, - PFS0 = Ui.ShownFileTypes.PFS0, - XCI = Ui.ShownFileTypes.XCI, - NCA = Ui.ShownFileTypes.NCA, - NRO = Ui.ShownFileTypes.NRO, - NSO = Ui.ShownFileTypes.NSO, + NSP = UI.ShownFileTypes.NSP, + PFS0 = UI.ShownFileTypes.PFS0, + XCI = UI.ShownFileTypes.XCI, + NCA = UI.ShownFileTypes.NCA, + NRO = UI.ShownFileTypes.NRO, + NSO = UI.ShownFileTypes.NSO, }, WindowStartup = new WindowStartup { - WindowSizeWidth = Ui.WindowStartup.WindowSizeWidth, - WindowSizeHeight = Ui.WindowStartup.WindowSizeHeight, - WindowPositionX = Ui.WindowStartup.WindowPositionX, - WindowPositionY = Ui.WindowStartup.WindowPositionY, - WindowMaximized = Ui.WindowStartup.WindowMaximized, + WindowSizeWidth = UI.WindowStartup.WindowSizeWidth, + WindowSizeHeight = UI.WindowStartup.WindowSizeHeight, + WindowPositionX = UI.WindowStartup.WindowPositionX, + WindowPositionY = UI.WindowStartup.WindowPositionY, + WindowMaximized = UI.WindowStartup.WindowMaximized, }, - LanguageCode = Ui.LanguageCode, - EnableCustomTheme = Ui.EnableCustomTheme, - CustomThemePath = Ui.CustomThemePath, - BaseStyle = Ui.BaseStyle, - GameListViewMode = Ui.GameListViewMode, - ShowNames = Ui.ShowNames, - GridSize = Ui.GridSize, - ApplicationSort = Ui.ApplicationSort, - IsAscendingOrder = Ui.IsAscendingOrder, - StartFullscreen = Ui.StartFullscreen, - ShowConsole = Ui.ShowConsole, + LanguageCode = UI.LanguageCode, + BaseStyle = UI.BaseStyle, + GameListViewMode = UI.GameListViewMode, + ShowNames = UI.ShowNames, + GridSize = UI.GridSize, + ApplicationSort = UI.ApplicationSort, + IsAscendingOrder = UI.IsAscendingOrder, + StartFullscreen = UI.StartFullscreen, + ShowConsole = UI.ShowConsole, EnableKeyboard = Hid.EnableKeyboard, EnableMouse = Hid.EnableMouse, Hotkeys = Hid.Hotkeys, - KeyboardConfig = new List(), - ControllerConfig = new List(), + KeyboardConfig = [], + ControllerConfig = [], InputConfig = Hid.InputConfig, GraphicsBackend = Graphics.GraphicsBackend, PreferredGpu = Graphics.PreferredGpu, MultiplayerLanInterfaceId = Multiplayer.LanInterfaceId, MultiplayerMode = Multiplayer.Mode, + MultiplayerDisableP2p = Multiplayer.DisableP2p, + MultiplayerLdnPassphrase = Multiplayer.LdnPassphrase, + LdnServer = Multiplayer.LdnServer, }; return configurationFile; @@ -765,8 +156,8 @@ namespace Ryujinx.Ui.Common.Configuration Graphics.MaxAnisotropy.Value = -1.0f; Graphics.AspectRatio.Value = AspectRatio.Fixed16x9; Graphics.GraphicsBackend.Value = DefaultGraphicsBackend(); - Graphics.PreferredGpu.Value = ""; - Graphics.ShadersDumpPath.Value = ""; + Graphics.PreferredGpu.Value = string.Empty; + Graphics.ShadersDumpPath.Value = string.Empty; Logger.EnableDebug.Value = false; Logger.EnableStub.Value = true; Logger.EnableInfo.Value = true; @@ -775,7 +166,7 @@ namespace Ryujinx.Ui.Common.Configuration Logger.EnableTrace.Value = false; Logger.EnableGuest.Value = true; Logger.EnableFsAccessLog.Value = false; - Logger.FilteredClasses.Value = Array.Empty(); + Logger.FilteredClasses.Value = []; Logger.GraphicsDebugLevel.Value = GraphicsDebugLevel.None; System.Language.Value = Language.AmericanEnglish; System.Region.Value = Region.USA; @@ -785,8 +176,14 @@ namespace Ryujinx.Ui.Common.Configuration EnableDiscordIntegration.Value = true; CheckUpdatesOnStart.Value = true; ShowConfirmExit.Value = true; + IgnoreApplet.Value = false; + RememberWindowState.Value = true; + ShowTitleBar.Value = !OperatingSystem.IsWindows(); + EnableHardwareAcceleration.Value = true; HideCursor.Value = HideCursorMode.OnIdle; - Graphics.EnableVsync.Value = true; + Graphics.VSyncMode.Value = VSyncMode.Switch; + Graphics.CustomVSyncInterval.Value = 120; + Graphics.EnableCustomVSyncInterval.Value = false; Graphics.EnableShaderCache.Value = true; Graphics.EnableTextureRecompression.Value = false; Graphics.EnableMacroHLE.Value = true; @@ -801,69 +198,71 @@ namespace Ryujinx.Ui.Common.Configuration System.AudioBackend.Value = AudioBackend.SDL2; System.AudioVolume.Value = 1; System.MemoryManagerMode.Value = MemoryManagerMode.HostMappedUnsafe; - System.ExpandRam.Value = false; + System.DramSize.Value = MemoryConfiguration.MemoryConfiguration4GiB; System.IgnoreMissingServices.Value = false; System.UseHypervisor.Value = true; Multiplayer.LanInterfaceId.Value = "0"; Multiplayer.Mode.Value = MultiplayerMode.Disabled; - Ui.GuiColumns.FavColumn.Value = true; - Ui.GuiColumns.IconColumn.Value = true; - Ui.GuiColumns.AppColumn.Value = true; - Ui.GuiColumns.DevColumn.Value = true; - Ui.GuiColumns.VersionColumn.Value = true; - Ui.GuiColumns.TimePlayedColumn.Value = true; - Ui.GuiColumns.LastPlayedColumn.Value = true; - Ui.GuiColumns.FileExtColumn.Value = true; - Ui.GuiColumns.FileSizeColumn.Value = true; - Ui.GuiColumns.PathColumn.Value = true; - Ui.ColumnSort.SortColumnId.Value = 0; - Ui.ColumnSort.SortAscending.Value = false; - Ui.GameDirs.Value = new List(); - Ui.ShownFileTypes.NSP.Value = true; - Ui.ShownFileTypes.PFS0.Value = true; - Ui.ShownFileTypes.XCI.Value = true; - Ui.ShownFileTypes.NCA.Value = true; - Ui.ShownFileTypes.NRO.Value = true; - Ui.ShownFileTypes.NSO.Value = true; - Ui.EnableCustomTheme.Value = true; - Ui.LanguageCode.Value = "en_US"; - Ui.CustomThemePath.Value = ""; - Ui.BaseStyle.Value = "Dark"; - Ui.GameListViewMode.Value = 0; - Ui.ShowNames.Value = true; - Ui.GridSize.Value = 2; - Ui.ApplicationSort.Value = 0; - Ui.IsAscendingOrder.Value = true; - Ui.StartFullscreen.Value = false; - Ui.ShowConsole.Value = true; - Ui.WindowStartup.WindowSizeWidth.Value = 1280; - Ui.WindowStartup.WindowSizeHeight.Value = 760; - Ui.WindowStartup.WindowPositionX.Value = 0; - Ui.WindowStartup.WindowPositionY.Value = 0; - Ui.WindowStartup.WindowMaximized.Value = false; + Multiplayer.DisableP2p.Value = false; + Multiplayer.LdnPassphrase.Value = ""; + Multiplayer.LdnServer.Value = ""; + UI.GuiColumns.FavColumn.Value = true; + UI.GuiColumns.IconColumn.Value = true; + UI.GuiColumns.AppColumn.Value = true; + UI.GuiColumns.DevColumn.Value = true; + UI.GuiColumns.VersionColumn.Value = true; + UI.GuiColumns.TimePlayedColumn.Value = true; + UI.GuiColumns.LastPlayedColumn.Value = true; + UI.GuiColumns.FileExtColumn.Value = true; + UI.GuiColumns.FileSizeColumn.Value = true; + UI.GuiColumns.PathColumn.Value = true; + UI.ColumnSort.SortColumnId.Value = 0; + UI.ColumnSort.SortAscending.Value = false; + UI.GameDirs.Value = []; + UI.AutoloadDirs.Value = []; + UI.ShownFileTypes.NSP.Value = true; + UI.ShownFileTypes.PFS0.Value = true; + UI.ShownFileTypes.XCI.Value = true; + UI.ShownFileTypes.NCA.Value = true; + UI.ShownFileTypes.NRO.Value = true; + UI.ShownFileTypes.NSO.Value = true; + UI.LanguageCode.Value = "en_US"; + UI.BaseStyle.Value = "Dark"; + UI.GameListViewMode.Value = 0; + UI.ShowNames.Value = true; + UI.GridSize.Value = 2; + UI.ApplicationSort.Value = 0; + UI.IsAscendingOrder.Value = true; + UI.StartFullscreen.Value = false; + UI.ShowConsole.Value = true; + UI.WindowStartup.WindowSizeWidth.Value = 1280; + UI.WindowStartup.WindowSizeHeight.Value = 760; + UI.WindowStartup.WindowPositionX.Value = 0; + UI.WindowStartup.WindowPositionY.Value = 0; + UI.WindowStartup.WindowMaximized.Value = false; Hid.EnableKeyboard.Value = false; Hid.EnableMouse.Value = false; Hid.Hotkeys.Value = new KeyboardHotkeys { - ToggleVsync = Key.F1, + ToggleVSyncMode = Key.F1, ToggleMute = Key.F2, Screenshot = Key.F8, - ShowUi = Key.F4, + ShowUI = Key.F4, Pause = Key.F5, ResScaleUp = Key.Unbound, ResScaleDown = Key.Unbound, VolumeUp = Key.Unbound, VolumeDown = Key.Unbound, }; - Hid.InputConfig.Value = new List - { + Hid.InputConfig.Value = + [ new StandardKeyboardInputConfig { Version = InputConfig.CurrentVersion, Backend = InputBackendType.WindowKeyboard, Id = "0", PlayerIndex = PlayerIndex.Player1, - ControllerType = ControllerType.JoyconPair, + ControllerType = ControllerType.ProController, LeftJoycon = new LeftJoyconCommonConfig { DpadUp = Key.Up, @@ -904,659 +303,20 @@ namespace Ryujinx.Ui.Common.Configuration StickRight = Key.L, StickButton = Key.H, }, - }, - }; - } - - public void Load(ConfigurationFileFormat configurationFileFormat, string configurationFilePath) - { - bool configurationFileUpdated = false; - - if (configurationFileFormat.Version < 0 || configurationFileFormat.Version > ConfigurationFileFormat.CurrentVersion) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Unsupported configuration version {configurationFileFormat.Version}, loading default."); - - LoadDefault(); - } - - if (configurationFileFormat.Version < 2) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 2."); - - configurationFileFormat.SystemRegion = Region.USA; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 3) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 3."); - - configurationFileFormat.SystemTimeZone = "UTC"; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 4) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 4."); - - configurationFileFormat.MaxAnisotropy = -1; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 5) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 5."); - - configurationFileFormat.SystemTimeOffset = 0; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 8) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 8."); - - configurationFileFormat.EnablePtc = true; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 9) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 9."); - - configurationFileFormat.ColumnSort = new ColumnSort - { - SortColumnId = 0, - SortAscending = false, - }; - - configurationFileFormat.Hotkeys = new KeyboardHotkeys - { - ToggleVsync = Key.F1, - }; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 10) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 10."); - - configurationFileFormat.AudioBackend = AudioBackend.OpenAl; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 11) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 11."); - - configurationFileFormat.ResScale = 1; - configurationFileFormat.ResScaleCustom = 1.0f; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 12) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 12."); - - configurationFileFormat.LoggingGraphicsDebugLevel = GraphicsDebugLevel.None; - - configurationFileUpdated = true; - } - - // configurationFileFormat.Version == 13 -> LDN1 - - if (configurationFileFormat.Version < 14) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 14."); - - configurationFileFormat.CheckUpdatesOnStart = true; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 16) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 16."); - - configurationFileFormat.EnableShaderCache = true; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 17) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 17."); - - configurationFileFormat.StartFullscreen = false; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 18) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 18."); - - configurationFileFormat.AspectRatio = AspectRatio.Fixed16x9; - - configurationFileUpdated = true; - } - - // configurationFileFormat.Version == 19 -> LDN2 - - if (configurationFileFormat.Version < 20) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 20."); - - configurationFileFormat.ShowConfirmExit = true; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 21) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 21."); - - // Initialize network config. - - configurationFileFormat.MultiplayerMode = MultiplayerMode.Disabled; - configurationFileFormat.MultiplayerLanInterfaceId = "0"; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 22) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 22."); - - configurationFileFormat.HideCursor = HideCursorMode.Never; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 24) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 24."); - - configurationFileFormat.InputConfig = new List - { - new StandardKeyboardInputConfig - { - Version = InputConfig.CurrentVersion, - Backend = InputBackendType.WindowKeyboard, - Id = "0", - PlayerIndex = PlayerIndex.Player1, - ControllerType = ControllerType.JoyconPair, - LeftJoycon = new LeftJoyconCommonConfig - { - DpadUp = Key.Up, - DpadDown = Key.Down, - DpadLeft = Key.Left, - DpadRight = Key.Right, - ButtonMinus = Key.Minus, - ButtonL = Key.E, - ButtonZl = Key.Q, - ButtonSl = Key.Unbound, - ButtonSr = Key.Unbound, - }, - LeftJoyconStick = new JoyconConfigKeyboardStick - { - StickUp = Key.W, - StickDown = Key.S, - StickLeft = Key.A, - StickRight = Key.D, - StickButton = Key.F, - }, - RightJoycon = new RightJoyconCommonConfig - { - ButtonA = Key.Z, - ButtonB = Key.X, - ButtonX = Key.C, - ButtonY = Key.V, - ButtonPlus = Key.Plus, - ButtonR = Key.U, - ButtonZr = Key.O, - ButtonSl = Key.Unbound, - ButtonSr = Key.Unbound, - }, - RightJoyconStick = new JoyconConfigKeyboardStick - { - StickUp = Key.I, - StickDown = Key.K, - StickLeft = Key.J, - StickRight = Key.L, - StickButton = Key.H, - }, - }, - }; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 25) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 25."); - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 26) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 26."); - - configurationFileFormat.MemoryManagerMode = MemoryManagerMode.HostMappedUnsafe; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 27) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 27."); - - configurationFileFormat.EnableMouse = false; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 28) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 28."); - - configurationFileFormat.Hotkeys = new KeyboardHotkeys - { - ToggleVsync = Key.F1, - Screenshot = Key.F8, - }; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 29) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 29."); - - configurationFileFormat.Hotkeys = new KeyboardHotkeys - { - ToggleVsync = Key.F1, - Screenshot = Key.F8, - ShowUi = Key.F4, - }; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 30) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 30."); - - foreach (InputConfig config in configurationFileFormat.InputConfig) - { - if (config is StandardControllerInputConfig controllerConfig) - { - controllerConfig.Rumble = new RumbleConfigController - { - EnableRumble = false, - StrongRumble = 1f, - WeakRumble = 1f, - }; - } } - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 31) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 31."); - - configurationFileFormat.BackendThreading = BackendThreading.Auto; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 32) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 32."); - - configurationFileFormat.Hotkeys = new KeyboardHotkeys - { - ToggleVsync = configurationFileFormat.Hotkeys.ToggleVsync, - Screenshot = configurationFileFormat.Hotkeys.Screenshot, - ShowUi = configurationFileFormat.Hotkeys.ShowUi, - Pause = Key.F5, - }; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 33) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 33."); - - configurationFileFormat.Hotkeys = new KeyboardHotkeys - { - ToggleVsync = configurationFileFormat.Hotkeys.ToggleVsync, - Screenshot = configurationFileFormat.Hotkeys.Screenshot, - ShowUi = configurationFileFormat.Hotkeys.ShowUi, - Pause = configurationFileFormat.Hotkeys.Pause, - ToggleMute = Key.F2, - }; - - configurationFileFormat.AudioVolume = 1; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 34) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 34."); - - configurationFileFormat.EnableInternetAccess = false; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 35) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 35."); - - foreach (InputConfig config in configurationFileFormat.InputConfig) - { - if (config is StandardControllerInputConfig controllerConfig) - { - controllerConfig.RangeLeft = 1.0f; - controllerConfig.RangeRight = 1.0f; - } - } - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 36) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 36."); - - configurationFileFormat.LoggingEnableTrace = false; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 37) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 37."); - - configurationFileFormat.ShowConsole = true; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 38) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 38."); - - configurationFileFormat.BaseStyle = "Dark"; - configurationFileFormat.GameListViewMode = 0; - configurationFileFormat.ShowNames = true; - configurationFileFormat.GridSize = 2; - configurationFileFormat.LanguageCode = "en_US"; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 39) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 39."); - - configurationFileFormat.Hotkeys = new KeyboardHotkeys - { - ToggleVsync = configurationFileFormat.Hotkeys.ToggleVsync, - Screenshot = configurationFileFormat.Hotkeys.Screenshot, - ShowUi = configurationFileFormat.Hotkeys.ShowUi, - Pause = configurationFileFormat.Hotkeys.Pause, - ToggleMute = configurationFileFormat.Hotkeys.ToggleMute, - ResScaleUp = Key.Unbound, - ResScaleDown = Key.Unbound, - }; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 40) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 40."); - - configurationFileFormat.GraphicsBackend = GraphicsBackend.OpenGl; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 41) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 41."); - - configurationFileFormat.Hotkeys = new KeyboardHotkeys - { - ToggleVsync = configurationFileFormat.Hotkeys.ToggleVsync, - Screenshot = configurationFileFormat.Hotkeys.Screenshot, - ShowUi = configurationFileFormat.Hotkeys.ShowUi, - Pause = configurationFileFormat.Hotkeys.Pause, - ToggleMute = configurationFileFormat.Hotkeys.ToggleMute, - ResScaleUp = configurationFileFormat.Hotkeys.ResScaleUp, - ResScaleDown = configurationFileFormat.Hotkeys.ResScaleDown, - VolumeUp = Key.Unbound, - VolumeDown = Key.Unbound, - }; - } - - if (configurationFileFormat.Version < 42) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 42."); - - configurationFileFormat.EnableMacroHLE = true; - } - - if (configurationFileFormat.Version < 43) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 43."); - - configurationFileFormat.UseHypervisor = true; - } - - if (configurationFileFormat.Version < 44) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 44."); - - configurationFileFormat.AntiAliasing = AntiAliasing.None; - configurationFileFormat.ScalingFilter = ScalingFilter.Bilinear; - configurationFileFormat.ScalingFilterLevel = 80; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 45) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 45."); - - configurationFileFormat.ShownFileTypes = new ShownFileTypes - { - NSP = true, - PFS0 = true, - XCI = true, - NCA = true, - NRO = true, - NSO = true, - }; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 46) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 46."); - - configurationFileFormat.MultiplayerLanInterfaceId = "0"; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 47) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 47."); - - configurationFileFormat.WindowStartup = new WindowStartup - { - WindowPositionX = 0, - WindowPositionY = 0, - WindowSizeHeight = 760, - WindowSizeWidth = 1280, - WindowMaximized = false, - }; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 48) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 48."); - - configurationFileFormat.EnableColorSpacePassthrough = false; - - configurationFileUpdated = true; - } - - Logger.EnableFileLog.Value = configurationFileFormat.EnableFileLog; - Graphics.ResScale.Value = configurationFileFormat.ResScale; - Graphics.ResScaleCustom.Value = configurationFileFormat.ResScaleCustom; - Graphics.MaxAnisotropy.Value = configurationFileFormat.MaxAnisotropy; - Graphics.AspectRatio.Value = configurationFileFormat.AspectRatio; - Graphics.ShadersDumpPath.Value = configurationFileFormat.GraphicsShadersDumpPath; - Graphics.BackendThreading.Value = configurationFileFormat.BackendThreading; - Graphics.GraphicsBackend.Value = configurationFileFormat.GraphicsBackend; - Graphics.PreferredGpu.Value = configurationFileFormat.PreferredGpu; - Graphics.AntiAliasing.Value = configurationFileFormat.AntiAliasing; - Graphics.ScalingFilter.Value = configurationFileFormat.ScalingFilter; - Graphics.ScalingFilterLevel.Value = configurationFileFormat.ScalingFilterLevel; - Logger.EnableDebug.Value = configurationFileFormat.LoggingEnableDebug; - Logger.EnableStub.Value = configurationFileFormat.LoggingEnableStub; - Logger.EnableInfo.Value = configurationFileFormat.LoggingEnableInfo; - Logger.EnableWarn.Value = configurationFileFormat.LoggingEnableWarn; - Logger.EnableError.Value = configurationFileFormat.LoggingEnableError; - Logger.EnableTrace.Value = configurationFileFormat.LoggingEnableTrace; - Logger.EnableGuest.Value = configurationFileFormat.LoggingEnableGuest; - Logger.EnableFsAccessLog.Value = configurationFileFormat.LoggingEnableFsAccessLog; - Logger.FilteredClasses.Value = configurationFileFormat.LoggingFilteredClasses; - Logger.GraphicsDebugLevel.Value = configurationFileFormat.LoggingGraphicsDebugLevel; - System.Language.Value = configurationFileFormat.SystemLanguage; - System.Region.Value = configurationFileFormat.SystemRegion; - System.TimeZone.Value = configurationFileFormat.SystemTimeZone; - System.SystemTimeOffset.Value = configurationFileFormat.SystemTimeOffset; - System.EnableDockedMode.Value = configurationFileFormat.DockedMode; - EnableDiscordIntegration.Value = configurationFileFormat.EnableDiscordIntegration; - CheckUpdatesOnStart.Value = configurationFileFormat.CheckUpdatesOnStart; - ShowConfirmExit.Value = configurationFileFormat.ShowConfirmExit; - HideCursor.Value = configurationFileFormat.HideCursor; - Graphics.EnableVsync.Value = configurationFileFormat.EnableVsync; - Graphics.EnableShaderCache.Value = configurationFileFormat.EnableShaderCache; - Graphics.EnableTextureRecompression.Value = configurationFileFormat.EnableTextureRecompression; - Graphics.EnableMacroHLE.Value = configurationFileFormat.EnableMacroHLE; - Graphics.EnableColorSpacePassthrough.Value = configurationFileFormat.EnableColorSpacePassthrough; - System.EnablePtc.Value = configurationFileFormat.EnablePtc; - System.EnableInternetAccess.Value = configurationFileFormat.EnableInternetAccess; - System.EnableFsIntegrityChecks.Value = configurationFileFormat.EnableFsIntegrityChecks; - System.FsGlobalAccessLogMode.Value = configurationFileFormat.FsGlobalAccessLogMode; - System.AudioBackend.Value = configurationFileFormat.AudioBackend; - System.AudioVolume.Value = configurationFileFormat.AudioVolume; - System.MemoryManagerMode.Value = configurationFileFormat.MemoryManagerMode; - System.ExpandRam.Value = configurationFileFormat.ExpandRam; - System.IgnoreMissingServices.Value = configurationFileFormat.IgnoreMissingServices; - System.UseHypervisor.Value = configurationFileFormat.UseHypervisor; - Ui.GuiColumns.FavColumn.Value = configurationFileFormat.GuiColumns.FavColumn; - Ui.GuiColumns.IconColumn.Value = configurationFileFormat.GuiColumns.IconColumn; - Ui.GuiColumns.AppColumn.Value = configurationFileFormat.GuiColumns.AppColumn; - Ui.GuiColumns.DevColumn.Value = configurationFileFormat.GuiColumns.DevColumn; - Ui.GuiColumns.VersionColumn.Value = configurationFileFormat.GuiColumns.VersionColumn; - Ui.GuiColumns.TimePlayedColumn.Value = configurationFileFormat.GuiColumns.TimePlayedColumn; - Ui.GuiColumns.LastPlayedColumn.Value = configurationFileFormat.GuiColumns.LastPlayedColumn; - Ui.GuiColumns.FileExtColumn.Value = configurationFileFormat.GuiColumns.FileExtColumn; - Ui.GuiColumns.FileSizeColumn.Value = configurationFileFormat.GuiColumns.FileSizeColumn; - Ui.GuiColumns.PathColumn.Value = configurationFileFormat.GuiColumns.PathColumn; - Ui.ColumnSort.SortColumnId.Value = configurationFileFormat.ColumnSort.SortColumnId; - Ui.ColumnSort.SortAscending.Value = configurationFileFormat.ColumnSort.SortAscending; - Ui.GameDirs.Value = configurationFileFormat.GameDirs; - Ui.ShownFileTypes.NSP.Value = configurationFileFormat.ShownFileTypes.NSP; - Ui.ShownFileTypes.PFS0.Value = configurationFileFormat.ShownFileTypes.PFS0; - Ui.ShownFileTypes.XCI.Value = configurationFileFormat.ShownFileTypes.XCI; - Ui.ShownFileTypes.NCA.Value = configurationFileFormat.ShownFileTypes.NCA; - Ui.ShownFileTypes.NRO.Value = configurationFileFormat.ShownFileTypes.NRO; - Ui.ShownFileTypes.NSO.Value = configurationFileFormat.ShownFileTypes.NSO; - Ui.EnableCustomTheme.Value = configurationFileFormat.EnableCustomTheme; - Ui.LanguageCode.Value = configurationFileFormat.LanguageCode; - Ui.CustomThemePath.Value = configurationFileFormat.CustomThemePath; - Ui.BaseStyle.Value = configurationFileFormat.BaseStyle; - Ui.GameListViewMode.Value = configurationFileFormat.GameListViewMode; - Ui.ShowNames.Value = configurationFileFormat.ShowNames; - Ui.IsAscendingOrder.Value = configurationFileFormat.IsAscendingOrder; - Ui.GridSize.Value = configurationFileFormat.GridSize; - Ui.ApplicationSort.Value = configurationFileFormat.ApplicationSort; - Ui.StartFullscreen.Value = configurationFileFormat.StartFullscreen; - Ui.ShowConsole.Value = configurationFileFormat.ShowConsole; - Ui.WindowStartup.WindowSizeWidth.Value = configurationFileFormat.WindowStartup.WindowSizeWidth; - Ui.WindowStartup.WindowSizeHeight.Value = configurationFileFormat.WindowStartup.WindowSizeHeight; - Ui.WindowStartup.WindowPositionX.Value = configurationFileFormat.WindowStartup.WindowPositionX; - Ui.WindowStartup.WindowPositionY.Value = configurationFileFormat.WindowStartup.WindowPositionY; - Ui.WindowStartup.WindowMaximized.Value = configurationFileFormat.WindowStartup.WindowMaximized; - Hid.EnableKeyboard.Value = configurationFileFormat.EnableKeyboard; - Hid.EnableMouse.Value = configurationFileFormat.EnableMouse; - Hid.Hotkeys.Value = configurationFileFormat.Hotkeys; - Hid.InputConfig.Value = configurationFileFormat.InputConfig; - - if (Hid.InputConfig.Value == null) - { - Hid.InputConfig.Value = new List(); - } - - Multiplayer.LanInterfaceId.Value = configurationFileFormat.MultiplayerLanInterfaceId; - Multiplayer.Mode.Value = configurationFileFormat.MultiplayerMode; - - if (configurationFileUpdated) - { - ToFileFormat().SaveConfig(configurationFilePath); - - Ryujinx.Common.Logging.Logger.Notice.Print(LogClass.Application, $"Configuration file updated to version {ConfigurationFileFormat.CurrentVersion}"); - } + ]; } private static GraphicsBackend DefaultGraphicsBackend() { // Any system running macOS or returning any amount of valid Vulkan devices should default to Vulkan. // Checks for if the Vulkan version and featureset is compatible should be performed within VulkanRenderer. - if (OperatingSystem.IsMacOS() || OperatingSystem.IsIOS() || VulkanRenderer.GetPhysicalDevices().Length > 0) + if (OperatingSystem.IsMacOS() || VulkanRenderer.GetPhysicalDevices().Length > 0) { return GraphicsBackend.Vulkan; } return GraphicsBackend.OpenGl; } - - private static void LogValueChange(ReactiveEventArgs eventArgs, string valueName) - { - Ryujinx.Common.Logging.Logger.Info?.Print(LogClass.Configuration, $"{valueName} set to: {eventArgs.NewValue}"); - } - - public static void Initialize() - { - if (Instance != null) - { - throw new InvalidOperationException("Configuration is already initialized"); } - - Instance = new ConfigurationState(); } - } -} diff --git a/src/Ryujinx.Ui.Common/Configuration/FileTypes.cs b/src/Ryujinx.Ui.Common/Configuration/FileTypes.cs index 9d2f63864..1974207b6 100644 --- a/src/Ryujinx.Ui.Common/Configuration/FileTypes.cs +++ b/src/Ryujinx.Ui.Common/Configuration/FileTypes.cs @@ -1,4 +1,4 @@ -namespace Ryujinx.Ui.Common +namespace Ryujinx.UI.Common { public enum FileTypes { diff --git a/src/Ryujinx.Ui.Common/Configuration/LoggerModule.cs b/src/Ryujinx.Ui.Common/Configuration/LoggerModule.cs index 54ad20dd7..a7913f142 100644 --- a/src/Ryujinx.Ui.Common/Configuration/LoggerModule.cs +++ b/src/Ryujinx.Ui.Common/Configuration/LoggerModule.cs @@ -1,95 +1,78 @@ -using Ryujinx.Common; +using Ryujinx.Common.Configuration; using Ryujinx.Common.Logging; using Ryujinx.Common.Logging.Targets; using System; +using System.IO; -namespace Ryujinx.Ui.Common.Configuration +namespace Ryujinx.UI.Common.Configuration { public static class LoggerModule { public static void Initialize() { - ConfigurationState.Instance.Logger.EnableDebug.Event += ReloadEnableDebug; - ConfigurationState.Instance.Logger.EnableStub.Event += ReloadEnableStub; - ConfigurationState.Instance.Logger.EnableInfo.Event += ReloadEnableInfo; - ConfigurationState.Instance.Logger.EnableWarn.Event += ReloadEnableWarning; - ConfigurationState.Instance.Logger.EnableError.Event += ReloadEnableError; - ConfigurationState.Instance.Logger.EnableTrace.Event += ReloadEnableTrace; - ConfigurationState.Instance.Logger.EnableGuest.Event += ReloadEnableGuest; - ConfigurationState.Instance.Logger.EnableFsAccessLog.Event += ReloadEnableFsAccessLog; - ConfigurationState.Instance.Logger.FilteredClasses.Event += ReloadFilteredClasses; - ConfigurationState.Instance.Logger.EnableFileLog.Event += ReloadFileLogger; - } - - private static void ReloadEnableDebug(object sender, ReactiveEventArgs e) - { - Logger.SetEnable(LogLevel.Debug, e.NewValue); - } - - private static void ReloadEnableStub(object sender, ReactiveEventArgs e) - { - Logger.SetEnable(LogLevel.Stub, e.NewValue); - } - - private static void ReloadEnableInfo(object sender, ReactiveEventArgs e) - { - Logger.SetEnable(LogLevel.Info, e.NewValue); - } - - private static void ReloadEnableWarning(object sender, ReactiveEventArgs e) - { - Logger.SetEnable(LogLevel.Warning, e.NewValue); - } - - private static void ReloadEnableError(object sender, ReactiveEventArgs e) - { - Logger.SetEnable(LogLevel.Error, e.NewValue); - } - - private static void ReloadEnableTrace(object sender, ReactiveEventArgs e) - { - Logger.SetEnable(LogLevel.Trace, e.NewValue); - } - - private static void ReloadEnableGuest(object sender, ReactiveEventArgs e) - { - Logger.SetEnable(LogLevel.Guest, e.NewValue); - } - - private static void ReloadEnableFsAccessLog(object sender, ReactiveEventArgs e) - { - Logger.SetEnable(LogLevel.AccessLog, e.NewValue); - } - - private static void ReloadFilteredClasses(object sender, ReactiveEventArgs e) - { - bool noFilter = e.NewValue.Length == 0; - - foreach (var logClass in Enum.GetValues()) + ConfigurationState.Instance.Logger.EnableDebug.Event += + (_, e) => Logger.SetEnable(LogLevel.Debug, e.NewValue); + ConfigurationState.Instance.Logger.EnableStub.Event += + (_, e) => Logger.SetEnable(LogLevel.Stub, e.NewValue); + ConfigurationState.Instance.Logger.EnableInfo.Event += + (_, e) => Logger.SetEnable(LogLevel.Info, e.NewValue); + ConfigurationState.Instance.Logger.EnableWarn.Event += + (_, e) => Logger.SetEnable(LogLevel.Warning, e.NewValue); + ConfigurationState.Instance.Logger.EnableError.Event += + (_, e) => Logger.SetEnable(LogLevel.Error, e.NewValue); + ConfigurationState.Instance.Logger.EnableTrace.Event += + (_, e) => Logger.SetEnable(LogLevel.Trace, e.NewValue); + ConfigurationState.Instance.Logger.EnableGuest.Event += + (_, e) => Logger.SetEnable(LogLevel.Guest, e.NewValue); + ConfigurationState.Instance.Logger.EnableFsAccessLog.Event += + (_, e) => Logger.SetEnable(LogLevel.AccessLog, e.NewValue); + + ConfigurationState.Instance.Logger.FilteredClasses.Event += (_, e) => { - Logger.SetEnable(logClass, noFilter); - } + bool noFilter = e.NewValue.Length == 0; - foreach (var logClass in e.NewValue) - { - Logger.SetEnable(logClass, true); - } - } + foreach (var logClass in Enum.GetValues()) + { + Logger.SetEnable(logClass, noFilter); + } - private static void ReloadFileLogger(object sender, ReactiveEventArgs e) - { - if (e.NewValue) + foreach (var logClass in e.NewValue) + { + Logger.SetEnable(logClass, true); + } + }; + + ConfigurationState.Instance.Logger.EnableFileLog.Event += (_, e) => { - Logger.AddTarget(new AsyncLogTargetWrapper( - new FileLogTarget(ReleaseInformation.GetBaseApplicationDirectory(), "file"), - 1000, - AsyncLogTargetOverflowAction.Block - )); - } - else - { - Logger.RemoveTarget("file"); - } + if (e.NewValue) + { + string logDir = AppDataManager.LogsDirPath; + FileStream logFile = null; + + if (!string.IsNullOrEmpty(logDir)) + { + logFile = FileLogTarget.PrepareLogFile(logDir); + } + + if (logFile == null) + { + Logger.Error?.Print(LogClass.Application, + "No writable log directory available. Make sure either the Logs directory, Application Data, or the Ryujinx directory is writable."); + Logger.RemoveTarget("file"); + + return; + } + + Logger.AddTarget(new AsyncLogTargetWrapper( + new FileLogTarget("file", logFile), + 1000 + )); + } + else + { + Logger.RemoveTarget("file"); + } + }; } } } diff --git a/src/Ryujinx.Ui.Common/Configuration/System/Language.cs b/src/Ryujinx.Ui.Common/Configuration/System/Language.cs index 72416bfe7..d1d395b00 100644 --- a/src/Ryujinx.Ui.Common/Configuration/System/Language.cs +++ b/src/Ryujinx.Ui.Common/Configuration/System/Language.cs @@ -1,7 +1,7 @@ using Ryujinx.Common.Utilities; using System.Text.Json.Serialization; -namespace Ryujinx.Ui.Common.Configuration.System +namespace Ryujinx.UI.Common.Configuration.System { [JsonConverter(typeof(TypedStringEnumConverter))] public enum Language diff --git a/src/Ryujinx.Ui.Common/Configuration/System/Region.cs b/src/Ryujinx.Ui.Common/Configuration/System/Region.cs index 2478b40f9..6087c70e5 100644 --- a/src/Ryujinx.Ui.Common/Configuration/System/Region.cs +++ b/src/Ryujinx.Ui.Common/Configuration/System/Region.cs @@ -1,7 +1,7 @@ using Ryujinx.Common.Utilities; using System.Text.Json.Serialization; -namespace Ryujinx.Ui.Common.Configuration.System +namespace Ryujinx.UI.Common.Configuration.System { [JsonConverter(typeof(TypedStringEnumConverter))] public enum Region diff --git a/src/Ryujinx.Ui.Common/Configuration/Ui/ColumnSort.cs b/src/Ryujinx.Ui.Common/Configuration/Ui/ColumnSort.cs index 88cf7cdac..44e98c407 100644 --- a/src/Ryujinx.Ui.Common/Configuration/Ui/ColumnSort.cs +++ b/src/Ryujinx.Ui.Common/Configuration/Ui/ColumnSort.cs @@ -1,4 +1,4 @@ -namespace Ryujinx.Ui.Common.Configuration.Ui +namespace Ryujinx.UI.Common.Configuration.UI { public struct ColumnSort { diff --git a/src/Ryujinx.Ui.Common/Configuration/Ui/GuiColumns.cs b/src/Ryujinx.Ui.Common/Configuration/Ui/GuiColumns.cs index 7e944015b..c486492e0 100644 --- a/src/Ryujinx.Ui.Common/Configuration/Ui/GuiColumns.cs +++ b/src/Ryujinx.Ui.Common/Configuration/Ui/GuiColumns.cs @@ -1,4 +1,4 @@ -namespace Ryujinx.Ui.Common.Configuration.Ui +namespace Ryujinx.UI.Common.Configuration.UI { public struct GuiColumns { @@ -7,6 +7,7 @@ namespace Ryujinx.Ui.Common.Configuration.Ui public bool AppColumn { get; set; } public bool DevColumn { get; set; } public bool VersionColumn { get; set; } + public bool LdnInfoColumn { get; set; } public bool TimePlayedColumn { get; set; } public bool LastPlayedColumn { get; set; } public bool FileExtColumn { get; set; } diff --git a/src/Ryujinx.Ui.Common/Configuration/Ui/ShownFileTypes.cs b/src/Ryujinx.Ui.Common/Configuration/Ui/ShownFileTypes.cs index 1b14fd467..6c72a6930 100644 --- a/src/Ryujinx.Ui.Common/Configuration/Ui/ShownFileTypes.cs +++ b/src/Ryujinx.Ui.Common/Configuration/Ui/ShownFileTypes.cs @@ -1,4 +1,4 @@ -namespace Ryujinx.Ui.Common.Configuration.Ui +namespace Ryujinx.UI.Common.Configuration.UI { public struct ShownFileTypes { diff --git a/src/Ryujinx.Ui.Common/Configuration/Ui/WindowStartup.cs b/src/Ryujinx.Ui.Common/Configuration/Ui/WindowStartup.cs index ce0dde6aa..0df459134 100644 --- a/src/Ryujinx.Ui.Common/Configuration/Ui/WindowStartup.cs +++ b/src/Ryujinx.Ui.Common/Configuration/Ui/WindowStartup.cs @@ -1,4 +1,4 @@ -namespace Ryujinx.Ui.Common.Configuration.Ui +namespace Ryujinx.UI.Common.Configuration.UI { public struct WindowStartup { diff --git a/src/Ryujinx.Ui.Common/DiscordIntegrationModule.cs b/src/Ryujinx.Ui.Common/DiscordIntegrationModule.cs index edc634aa5..338d28531 100644 --- a/src/Ryujinx.Ui.Common/DiscordIntegrationModule.cs +++ b/src/Ryujinx.Ui.Common/DiscordIntegrationModule.cs @@ -1,13 +1,31 @@ using DiscordRPC; +using Humanizer; +using Humanizer.Localisation; using Ryujinx.Common; -using Ryujinx.Ui.Common.Configuration; +using Ryujinx.HLE.Loaders.Processes; +using Ryujinx.UI.App.Common; +using Ryujinx.UI.Common.Configuration; +using System.Linq; +using System.Text; -namespace Ryujinx.Ui.Common +namespace Ryujinx.UI.Common { public static class DiscordIntegrationModule { - private const string Description = "A simple, experimental Nintendo Switch emulator."; - private const string CliendId = "568815339807309834"; + public static Timestamps StartedAt { get; set; } + + private static string VersionString + => (ReleaseInformation.IsCanaryBuild ? "Canary " : string.Empty) + $"v{ReleaseInformation.Version}"; + + private static readonly string _description = + ReleaseInformation.IsValid + ? $"{VersionString} {ReleaseInformation.ReleaseChannelOwner}/{ReleaseInformation.ReleaseChannelSourceRepo}@{ReleaseInformation.BuildGitHash}" + : "dev build"; + + private const string ApplicationId = "1293250299716173864"; + + private const int ApplicationByteLimit = 128; + private const string Ellipsis = "…"; private static DiscordRpcClient _discordClient; private static RichPresence _discordPresenceMain; @@ -19,19 +37,11 @@ namespace Ryujinx.Ui.Common Assets = new Assets { LargeImageKey = "ryujinx", - LargeImageText = Description, + LargeImageText = TruncateToByteLength(_description) }, Details = "Main Menu", State = "Idling", - Timestamps = Timestamps.Now, - Buttons = new[] - { - new Button - { - Label = "Website", - Url = "https://ryujinx.org/", - }, - }, + Timestamps = StartedAt }; ConfigurationState.Instance.EnableDiscordIntegration.Event += Update; @@ -52,7 +62,7 @@ namespace Ryujinx.Ui.Common // If we need to activate it and the client isn't active, initialize it if (evnt.NewValue && _discordClient == null) { - _discordClient = new DiscordRpcClient(CliendId); + _discordClient = new DiscordRpcClient(ApplicationId); _discordClient.Initialize(); _discordClient.SetPresence(_discordPresenceMain); @@ -60,39 +70,203 @@ namespace Ryujinx.Ui.Common } } - public static void SwitchToPlayingState(string titleId, string titleName) + public static void SwitchToPlayingState(ApplicationMetadata appMeta, ProcessResult procRes) { _discordClient?.SetPresence(new RichPresence { Assets = new Assets { - LargeImageKey = "game", - LargeImageText = titleName, + LargeImageKey = _discordGameAssetKeys.Contains(procRes.ProgramIdText) ? procRes.ProgramIdText : "game", + LargeImageText = TruncateToByteLength($"{appMeta.Title} (v{procRes.DisplayVersion})"), SmallImageKey = "ryujinx", - SmallImageText = Description, - }, - Details = $"Playing {titleName}", - State = (titleId == "0000000000000000") ? "Homebrew" : titleId.ToUpper(), - Timestamps = Timestamps.Now, - Buttons = new[] - { - new Button - { - Label = "Website", - Url = "https://ryujinx.org/", - }, + SmallImageText = TruncateToByteLength(_description) }, + Details = TruncateToByteLength($"Playing {appMeta.Title}"), + State = appMeta.LastPlayed.HasValue && appMeta.TimePlayed.TotalSeconds > 5 + ? $"Total play time: {appMeta.TimePlayed.Humanize(2, false, maxUnit: TimeUnit.Hour)}" + : "Never played", + Timestamps = Timestamps.Now }); } - public static void SwitchToMainMenu() + public static void SwitchToMainState() => _discordClient?.SetPresence(_discordPresenceMain); + + private static string TruncateToByteLength(string input) { - _discordClient?.SetPresence(_discordPresenceMain); + if (Encoding.UTF8.GetByteCount(input) <= ApplicationByteLimit) + { + return input; + } + + // Find the length to trim the string to guarantee we have space for the trailing ellipsis. + int trimLimit = ApplicationByteLimit - Encoding.UTF8.GetByteCount(Ellipsis); + + // Make sure the string is long enough to perform the basic trim. + // Amount of bytes != Length of the string + if (input.Length > trimLimit) + { + // Basic trim to best case scenario of 1 byte characters. + input = input[..trimLimit]; + } + + while (Encoding.UTF8.GetByteCount(input) > trimLimit) + { + // Remove one character from the end of the string at a time. + input = input[..^1]; + } + + return input.TrimEnd() + Ellipsis; } public static void Exit() { _discordClient?.Dispose(); } + + private static readonly string[] _discordGameAssetKeys = + [ + "010055d009f78000", // Fire Emblem: Three Houses + "0100a12011cc8000", // Fire Emblem: Shadow Dragon + "0100a6301214e000", // Fire Emblem Engage + "0100f15003e64000", // Fire Emblem Warriors + "010071f0143ea000", // Fire Emblem Warriors: Three Hopes + + "01007e3006dda000", // Kirby Star Allies + "01004d300c5ae000", // Kirby and the Forgotten Land + "01006b601380e000", // Kirby's Return to Dream Land Deluxe + "01003fb00c5a8000", // Super Kirby Clash + "0100227010460000", // Kirby Fighters 2 + "0100a8e016236000", // Kirby's Dream Buffet + + "01007ef00011e000", // The Legend of Zelda: Breath of the Wild + "01006bb00c6f0000", // The Legend of Zelda: Link's Awakening + "01002da013484000", // The Legend of Zelda: Skyward Sword HD + "0100f2c0115b6000", // The Legend of Zelda: Tears of the Kingdom + "01008cf01baac000", // The Legend of Zelda: Echoes of Wisdom + "01000b900d8b0000", // Cadence of Hyrule + "0100ae00096ea000", // Hyrule Warriors: Definitive Edition + "01002b00111a2000", // Hyrule Warriors: Age of Calamity + + "010048701995e000", // Luigi's Mansion 2 HD + "0100dca0064a6000", // Luigi's Mansion 3 + + "010093801237c000", // Metroid Dread + "010012101468c000", // Metroid Prime Remastered + + "0100000000010000", // SUPER MARIO ODYSSEY + "0100ea80032ea000", // Super Mario Bros. U Deluxe + "01009b90006dc000", // Super Mario Maker 2 + "010049900f546000", // Super Mario 3D All-Stars + "010049900F546001", // ^ 64 + "010049900F546002", // ^ Sunshine + "010049900F546003", // ^ Galaxy + "010028600ebda000", // Super Mario 3D World + Bowser's Fury + "010015100b514000", // Super Mario Bros. Wonder + "0100152000022000", // Mario Kart 8 Deluxe + "010036b0034e4000", // Super Mario Party + "01006fe013472000", // Mario Party Superstars + "0100965017338000", // Super Mario Party Jamboree + "01006d0017f7a000", // Mario & Luigi: Brothership + "010067300059a000", // Mario + Rabbids: Kingdom Battle + "0100317013770000", // Mario + Rabbids: Sparks of Hope + "0100a3900c3e2000", // Paper Mario: The Origami King + "0100ecd018ebe000", // Paper Mario: The Thousand-Year Door + "0100bc0018138000", // Super Mario RPG + "0100bde00862a000", // Mario Tennis Aces + "0100c9c00e25c000", // Mario Golf: Super Rush + "010019401051c000", // Mario Strikers: Battle League + "010003000e146000", // Mario & Sonic at the Olympic Games Tokyo 2020 + "0100b99019412000", // Mario vs. Donkey Kong + + "0100aa80194b0000", // Pikmin 1 + "0100d680194b2000", // Pikmin 2 + "0100f4c009322000", // Pikmin 3 Deluxe + "0100b7c00933a000", // Pikmin 4 + + "010003f003a34000", // Pokémon: Let's Go Pikachu! + "0100187003a36000", // Pokémon: Let's Go Eevee! + "0100abf008968000", // Pokémon Sword + "01008db008c2c000", // Pokémon Shield + "0100000011d90000", // Pokémon Brilliant Diamond + "010018e011d92000", // Pokémon Shining Pearl + "01001f5010dfa000", // Pokémon Legends: Arceus + "0100a3d008c5c000", // Pokémon Scarlet + "01008f6008c5e000", // Pokémon Violet + "0100b3f000be2000", // Pokkén Tournament DX + "0100f4300bf2c000", // New Pokémon Snap + + "01003bc0000a0000", // Splatoon 2 (US) + "0100f8f0000a2000", // Splatoon 2 (EU) + "01003c700009c000", // Splatoon 2 (JP) + "0100c2500fc20000", // Splatoon 3 + "0100ba0018500000", // Splatoon 3: Splatfest World Premiere + + "010040600c5ce000", // Tetris 99 + "0100277011f1a000", // Super Mario Bros. 35 + "0100ad9012510000", // PAC-MAN 99 + "0100ccf019c8c000", // F-ZERO 99 + "0100d870045b6000", // NES - Nintendo Switch Online + "01008d300c50c000", // SNES - Nintendo Switch Online + "0100c9a00ece6000", // N64 - Nintendo Switch Online + "0100e0601c632000", // N64 - Nintendo Switch Online 18+ + "0100c62011050000", // GB - Nintendo Switch Online + "010012f017576000", // GBA - Nintendo Switch Online + + "01000320000cc000", // 1-2 Switch + "0100300012f2a000", // Advance Wars 1+2: Re-Boot Camp + "01006f8002326000", // Animal Crossing: New Horizons + "0100620012d6e000", // Big Brain Academy: Brain vs. Brain + "010018300d006000", // BOXBOY! + BOXGIRL! + "0100c1f0051b6000", // Donkey Kong Country: Tropical Freeze + "0100ed000d390000", // Dr. Kawashima's Brain Training + "010067b017588000", // Endless Ocean Luminous + "0100d2f00d5c0000", // Nintendo Switch Sports + "01006b5012b32000", // Part Time UFO + "0100704000B3A000", // Snipperclips + "01006a800016e000", // Super Smash Bros. Ultimate + "0100a9400c9c2000", // Tokyo Mirage Sessions #FE Encore + + "010076f0049a2000", // Bayonetta + "01007960049a0000", // Bayonetta 2 + "01004a4010fea000", // Bayonetta 3 + "0100cf5010fec000", // Bayonetta Origins: Cereza and the Lost Demon + + "0100dcd01525a000", // Persona 3 Portable + "010062b01525c000", // Persona 4 Golden + "010075a016a3a000", // Persona 4 Arena Ultimax + "01005ca01580e000", // Persona 5 Royal + "0100801011c3e000", // Persona 5 Strikers + "010087701b092000", // Persona 5 Tactica + + "01009aa000faa000", // Sonic Mania + "01004ad014bf0000", // Sonic Frontiers + "01005ea01c0fc000", // SONIC X SHADOW GENERATIONS + "01005ea01c0fc001", // ^ + + "010056e00853a000", // A Hat in Time + "0100dbf01000a000", // Burnout Paradise Remastered + "0100744001588000", // Cars 3: Driven to Win + "0100b41013c82000", // Cruis'n Blast + "01001b300b9be000", // Diablo III: Eternal Collection + "01008c8012920000", // Dying Light Platinum Edition + "010073c01af34000", // LEGO Horizon Adventures + "0100770008dd8000", // Monster Hunter Generations Ultimate + "0100b04011742000", // Monster Hunter Rise + "0100853015e86000", // No Man's Sky + "01007bb017812000", // Portal + "0100abd01785c000", // Portal 2 + "01008e200c5c2000", // Muse Dash + "01007820196a6000", // Red Dead Redemption + "01002f7013224000", // Rune Factory 5 + "01008d100d43e000", // Saints Row IV + "0100de600beee000", // Saints Row: The Third - The Full Package + "01001180021fa000", // Shovel Knight: Specter of Torment + "0100d7a01b7a2000", // Star Wars: Bounty Hunter + "0100800015926000", // Suika Game + "0100e46006708000", // Terraria + "01000a10041ea000", // The Elder Scrolls V: Skyrim + "010057a01e4d4000", // TSUKIHIME -A piece of blue glass moon- + "010080b00ad66000", // Undertale + ]; } } diff --git a/src/Ryujinx.Ui.Common/Extensions/FileTypeExtensions.cs b/src/Ryujinx.Ui.Common/Extensions/FileTypeExtensions.cs index c827f750e..7e71ba7a4 100644 --- a/src/Ryujinx.Ui.Common/Extensions/FileTypeExtensions.cs +++ b/src/Ryujinx.Ui.Common/Extensions/FileTypeExtensions.cs @@ -1,7 +1,7 @@ using System; -using static Ryujinx.Ui.Common.Configuration.ConfigurationState.UiSection; +using static Ryujinx.UI.Common.Configuration.ConfigurationState.UISection; -namespace Ryujinx.Ui.Common +namespace Ryujinx.UI.Common { public static class FileTypesExtensions { diff --git a/src/Ryujinx.Ui.Common/Helper/CommandLineState.cs b/src/Ryujinx.Ui.Common/Helper/CommandLineState.cs index 714cf2f0a..3a96a55c8 100644 --- a/src/Ryujinx.Ui.Common/Helper/CommandLineState.cs +++ b/src/Ryujinx.Ui.Common/Helper/CommandLineState.cs @@ -1,19 +1,22 @@ using Ryujinx.Common.Logging; using System.Collections.Generic; -namespace Ryujinx.Ui.Common.Helper +namespace Ryujinx.UI.Common.Helper { public static class CommandLineState { public static string[] Arguments { get; private set; } public static bool? OverrideDockedMode { get; private set; } + public static bool? OverrideHardwareAcceleration { get; private set; } public static string OverrideGraphicsBackend { get; private set; } public static string OverrideHideCursor { get; private set; } public static string BaseDirPathArg { get; private set; } public static string Profile { get; private set; } public static string LaunchPathArg { get; private set; } + public static string LaunchApplicationId { get; private set; } public static bool StartFullscreenArg { get; private set; } + public static bool HideAvailableUpdates { get; private set; } public static void ParseArguments(string[] args) { @@ -71,6 +74,10 @@ namespace Ryujinx.Ui.Common.Helper OverrideGraphicsBackend = args[++i]; break; + case "-i": + case "--application-id": + LaunchApplicationId = args[++i]; + break; case "--docked-mode": OverrideDockedMode = true; break; @@ -87,6 +94,12 @@ namespace Ryujinx.Ui.Common.Helper OverrideHideCursor = args[++i]; break; + case "--hide-updates": + HideAvailableUpdates = true; + break; + case "--software-gui": + OverrideHardwareAcceleration = false; + break; default: LaunchPathArg = arg; break; diff --git a/src/Ryujinx.Ui.Common/Helper/ConsoleHelper.cs b/src/Ryujinx.Ui.Common/Helper/ConsoleHelper.cs index 65155641f..623952b37 100644 --- a/src/Ryujinx.Ui.Common/Helper/ConsoleHelper.cs +++ b/src/Ryujinx.Ui.Common/Helper/ConsoleHelper.cs @@ -3,7 +3,7 @@ using System; using System.Runtime.InteropServices; using System.Runtime.Versioning; -namespace Ryujinx.Ui.Common.Helper +namespace Ryujinx.UI.Common.Helper { public static partial class ConsoleHelper { @@ -27,9 +27,9 @@ namespace Ryujinx.Ui.Common.Helper const int SW_HIDE = 0; const int SW_SHOW = 5; - IntPtr hWnd = GetConsoleWindow(); + nint hWnd = GetConsoleWindow(); - if (hWnd == IntPtr.Zero) + if (hWnd == nint.Zero) { Logger.Warning?.Print(LogClass.Application, "Attempted to show/hide console window but console window does not exist"); return; @@ -40,11 +40,11 @@ namespace Ryujinx.Ui.Common.Helper [SupportedOSPlatform("windows")] [LibraryImport("kernel32")] - private static partial IntPtr GetConsoleWindow(); + private static partial nint GetConsoleWindow(); [SupportedOSPlatform("windows")] [LibraryImport("user32")] [return: MarshalAs(UnmanagedType.Bool)] - private static partial bool ShowWindow(IntPtr hWnd, int nCmdShow); + private static partial bool ShowWindow(nint hWnd, int nCmdShow); } } diff --git a/src/Ryujinx.Ui.Common/Helper/FileAssociationHelper.cs b/src/Ryujinx.Ui.Common/Helper/FileAssociationHelper.cs index 570fb91e2..9333a1b76 100644 --- a/src/Ryujinx.Ui.Common/Helper/FileAssociationHelper.cs +++ b/src/Ryujinx.Ui.Common/Helper/FileAssociationHelper.cs @@ -4,14 +4,15 @@ using Ryujinx.Common.Logging; using System; using System.Diagnostics; using System.IO; +using System.Linq; using System.Runtime.InteropServices; using System.Runtime.Versioning; -namespace Ryujinx.Ui.Common.Helper +namespace Ryujinx.UI.Common.Helper { public static partial class FileAssociationHelper { - private static readonly string[] _fileExtensions = { ".nca", ".nro", ".nso", ".nsp", ".xci" }; + private static readonly string[] _fileExtensions = [".nca", ".nro", ".nso", ".nsp", ".xci"]; [SupportedOSPlatform("linux")] private static readonly string _mimeDbPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".local", "share", "mime"); @@ -20,9 +21,29 @@ namespace Ryujinx.Ui.Common.Helper private const int SHCNF_FLUSH = 0x1000; [LibraryImport("shell32.dll", SetLastError = true)] - public static partial void SHChangeNotify(uint wEventId, uint uFlags, IntPtr dwItem1, IntPtr dwItem2); + public static partial void SHChangeNotify(uint wEventId, uint uFlags, nint dwItem1, nint dwItem2); - public static bool IsTypeAssociationSupported => (OperatingSystem.IsLinux() || OperatingSystem.IsWindows()) && !ReleaseInformation.IsFlatHubBuild(); + public static bool IsTypeAssociationSupported => (OperatingSystem.IsLinux() || OperatingSystem.IsWindows()) && !ReleaseInformation.IsFlatHubBuild; + + public static bool AreMimeTypesRegistered + { + get + { + if (OperatingSystem.IsLinux()) + { + return AreMimeTypesRegisteredLinux(); + } + + if (OperatingSystem.IsWindows()) + { + return AreMimeTypesRegisteredWindows(); + } + + // TODO: Add macOS support. + + return false; + } + } [SupportedOSPlatform("linux")] private static bool AreMimeTypesRegisteredLinux() => File.Exists(Path.Combine(_mimeDbPath, "packages", "Ryujinx.xml")); @@ -34,8 +55,8 @@ namespace Ryujinx.Ui.Common.Helper if ((uninstall && AreMimeTypesRegisteredLinux()) || (!uninstall && !AreMimeTypesRegisteredLinux())) { - string mimeTypesFile = Path.Combine(ReleaseInformation.GetBaseApplicationDirectory(), "mime", "Ryujinx.xml"); - string additionalArgs = !uninstall ? "--novendor" : ""; + string mimeTypesFile = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "mime", "Ryujinx.xml"); + string additionalArgs = !uninstall ? "--novendor" : string.Empty; using Process mimeProcess = new(); @@ -72,35 +93,39 @@ namespace Ryujinx.Ui.Common.Helper [SupportedOSPlatform("windows")] private static bool AreMimeTypesRegisteredWindows() { + return _fileExtensions.Aggregate(false, + (current, ext) => current | CheckRegistering(ext) + ); + static bool CheckRegistering(string ext) { RegistryKey key = Registry.CurrentUser.OpenSubKey(@$"Software\Classes\{ext}"); - if (key is null) + var openCmd = key?.OpenSubKey(@"shell\open\command"); + + if (openCmd is null) { return false; } - - var openCmd = key.OpenSubKey(@"shell\open\command"); - - string keyValue = (string)openCmd.GetValue(""); + + string keyValue = (string)openCmd.GetValue(string.Empty); return keyValue is not null && (keyValue.Contains("Ryujinx") || keyValue.Contains(AppDomain.CurrentDomain.FriendlyName)); } - - bool registered = false; - - foreach (string ext in _fileExtensions) - { - registered |= CheckRegistering(ext); - } - - return registered; } [SupportedOSPlatform("windows")] private static bool InstallWindowsMimeTypes(bool uninstall = false) { + bool registered = _fileExtensions.Aggregate(false, + (current, ext) => current | RegisterExtension(ext, uninstall) + ); + + // Notify Explorer the file association has been changed. + SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_FLUSH, nint.Zero, nint.Zero); + + return registered; + static bool RegisterExtension(string ext, bool uninstall = false) { string keyString = @$"Software\Classes\{ext}"; @@ -127,42 +152,13 @@ namespace Ryujinx.Ui.Common.Helper Logger.Debug?.Print(LogClass.Application, $"Adding type association {ext}"); using var openCmd = key.CreateSubKey(@"shell\open\command"); - openCmd.SetValue("", $"\"{Environment.ProcessPath}\" \"%1\""); + openCmd.SetValue(string.Empty, $"\"{Environment.ProcessPath}\" \"%1\""); Logger.Debug?.Print(LogClass.Application, $"Added type association {ext}"); } return true; } - - bool registered = false; - - foreach (string ext in _fileExtensions) - { - registered |= RegisterExtension(ext, uninstall); - } - - // Notify Explorer the file association has been changed. - SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_FLUSH, IntPtr.Zero, IntPtr.Zero); - - return registered; - } - - public static bool AreMimeTypesRegistered() - { - if (OperatingSystem.IsLinux()) - { - return AreMimeTypesRegisteredLinux(); - } - - if (OperatingSystem.IsWindows()) - { - return AreMimeTypesRegisteredWindows(); - } - - // TODO: Add macOS support. - - return false; } public static bool Install() diff --git a/src/Ryujinx.Ui.Common/Helper/LinuxHelper.cs b/src/Ryujinx.Ui.Common/Helper/LinuxHelper.cs index bf647719a..b57793791 100644 --- a/src/Ryujinx.Ui.Common/Helper/LinuxHelper.cs +++ b/src/Ryujinx.Ui.Common/Helper/LinuxHelper.cs @@ -3,7 +3,7 @@ using System.Diagnostics; using System.IO; using System.Runtime.Versioning; -namespace Ryujinx.Ui.Common.Helper +namespace Ryujinx.UI.Common.Helper { [SupportedOSPlatform("linux")] public static class LinuxHelper diff --git a/src/Ryujinx.Ui.Common/Helper/ObjectiveC.cs b/src/Ryujinx.Ui.Common/Helper/ObjectiveC.cs index 16a67ecb2..f8f972098 100644 --- a/src/Ryujinx.Ui.Common/Helper/ObjectiveC.cs +++ b/src/Ryujinx.Ui.Common/Helper/ObjectiveC.cs @@ -2,53 +2,52 @@ using System; using System.Runtime.InteropServices; using System.Runtime.Versioning; -namespace Ryujinx.Ui.Common.Helper +namespace Ryujinx.UI.Common.Helper { [SupportedOSPlatform("macos")] - [SupportedOSPlatform("ios")] public static partial class ObjectiveC { private const string ObjCRuntime = "/usr/lib/libobjc.A.dylib"; [LibraryImport(ObjCRuntime, StringMarshalling = StringMarshalling.Utf8)] - private static partial IntPtr sel_getUid(string name); + private static partial nint sel_getUid(string name); [LibraryImport(ObjCRuntime, StringMarshalling = StringMarshalling.Utf8)] - private static partial IntPtr objc_getClass(string name); + private static partial nint objc_getClass(string name); [LibraryImport(ObjCRuntime)] - private static partial void objc_msgSend(IntPtr receiver, Selector selector); + private static partial void objc_msgSend(nint receiver, Selector selector); [LibraryImport(ObjCRuntime)] - private static partial void objc_msgSend(IntPtr receiver, Selector selector, byte value); + private static partial void objc_msgSend(nint receiver, Selector selector, byte value); [LibraryImport(ObjCRuntime)] - private static partial void objc_msgSend(IntPtr receiver, Selector selector, IntPtr value); + private static partial void objc_msgSend(nint receiver, Selector selector, nint value); [LibraryImport(ObjCRuntime)] - private static partial void objc_msgSend(IntPtr receiver, Selector selector, NSRect point); + private static partial void objc_msgSend(nint receiver, Selector selector, NSRect point); [LibraryImport(ObjCRuntime)] - private static partial void objc_msgSend(IntPtr receiver, Selector selector, double value); + private static partial void objc_msgSend(nint receiver, Selector selector, double value); [LibraryImport(ObjCRuntime, EntryPoint = "objc_msgSend")] - private static partial IntPtr IntPtr_objc_msgSend(IntPtr receiver, Selector selector); + private static partial nint nint_objc_msgSend(nint receiver, Selector selector); [LibraryImport(ObjCRuntime, EntryPoint = "objc_msgSend")] - private static partial IntPtr IntPtr_objc_msgSend(IntPtr receiver, Selector selector, IntPtr param); + private static partial nint nint_objc_msgSend(nint receiver, Selector selector, nint param); [LibraryImport(ObjCRuntime, EntryPoint = "objc_msgSend", StringMarshalling = StringMarshalling.Utf8)] - private static partial IntPtr IntPtr_objc_msgSend(IntPtr receiver, Selector selector, string param); + private static partial nint nint_objc_msgSend(nint receiver, Selector selector, string param); [LibraryImport(ObjCRuntime, EntryPoint = "objc_msgSend")] [return: MarshalAs(UnmanagedType.Bool)] - private static partial bool bool_objc_msgSend(IntPtr receiver, Selector selector, IntPtr param); + private static partial bool bool_objc_msgSend(nint receiver, Selector selector, nint param); public readonly struct Object { - public readonly IntPtr ObjPtr; + public readonly nint ObjPtr; - private Object(IntPtr pointer) + private Object(nint pointer) { ObjPtr = pointer; } @@ -85,22 +84,22 @@ namespace Ryujinx.Ui.Common.Helper public Object GetFromMessage(Selector selector) { - return new Object(IntPtr_objc_msgSend(ObjPtr, selector)); + return new Object(nint_objc_msgSend(ObjPtr, selector)); } public Object GetFromMessage(Selector selector, Object obj) { - return new Object(IntPtr_objc_msgSend(ObjPtr, selector, obj.ObjPtr)); + return new Object(nint_objc_msgSend(ObjPtr, selector, obj.ObjPtr)); } public Object GetFromMessage(Selector selector, NSString nsString) { - return new Object(IntPtr_objc_msgSend(ObjPtr, selector, nsString.StrPtr)); + return new Object(nint_objc_msgSend(ObjPtr, selector, nsString.StrPtr)); } public Object GetFromMessage(Selector selector, string param) { - return new Object(IntPtr_objc_msgSend(ObjPtr, selector, param)); + return new Object(nint_objc_msgSend(ObjPtr, selector, param)); } public bool GetBoolFromMessage(Selector selector, Object obj) @@ -111,7 +110,7 @@ namespace Ryujinx.Ui.Common.Helper public readonly struct Selector { - public readonly IntPtr SelPtr; + public readonly nint SelPtr; private Selector(string name) { @@ -123,15 +122,15 @@ namespace Ryujinx.Ui.Common.Helper public readonly struct NSString { - public readonly IntPtr StrPtr; + public readonly nint StrPtr; public NSString(string aString) { - IntPtr nsString = objc_getClass("NSString"); - StrPtr = IntPtr_objc_msgSend(nsString, "stringWithUTF8String:", aString); + nint nsString = objc_getClass("NSString"); + StrPtr = nint_objc_msgSend(nsString, "stringWithUTF8String:", aString); } - public static implicit operator IntPtr(NSString nsString) => nsString.StrPtr; + public static implicit operator nint(NSString nsString) => nsString.StrPtr; } public readonly struct NSPoint diff --git a/src/Ryujinx.Ui.Common/Helper/OpenHelper.cs b/src/Ryujinx.Ui.Common/Helper/OpenHelper.cs index 04ebbf3b0..8b0e1f1fd 100644 --- a/src/Ryujinx.Ui.Common/Helper/OpenHelper.cs +++ b/src/Ryujinx.Ui.Common/Helper/OpenHelper.cs @@ -4,18 +4,18 @@ using System.Diagnostics; using System.IO; using System.Runtime.InteropServices; -namespace Ryujinx.Ui.Common.Helper +namespace Ryujinx.UI.Common.Helper { public static partial class OpenHelper { [LibraryImport("shell32.dll", SetLastError = true)] - private static partial int SHOpenFolderAndSelectItems(IntPtr pidlFolder, uint cidl, IntPtr apidl, uint dwFlags); + private static partial int SHOpenFolderAndSelectItems(nint pidlFolder, uint cidl, nint apidl, uint dwFlags); [LibraryImport("shell32.dll", SetLastError = true)] - private static partial void ILFree(IntPtr pidlList); + private static partial void ILFree(nint pidlList); [LibraryImport("shell32.dll", SetLastError = true)] - private static partial IntPtr ILCreateFromPathW([MarshalAs(UnmanagedType.LPWStr)] string pszPath); + private static partial nint ILCreateFromPathW([MarshalAs(UnmanagedType.LPWStr)] string pszPath); public static void OpenFolder(string path) { @@ -40,12 +40,12 @@ namespace Ryujinx.Ui.Common.Helper { if (OperatingSystem.IsWindows()) { - IntPtr pidlList = ILCreateFromPathW(path); - if (pidlList != IntPtr.Zero) + nint pidlList = ILCreateFromPathW(path); + if (pidlList != nint.Zero) { try { - Marshal.ThrowExceptionForHR(SHOpenFolderAndSelectItems(pidlList, 0, IntPtr.Zero, 0)); + Marshal.ThrowExceptionForHR(SHOpenFolderAndSelectItems(pidlList, 0, nint.Zero, 0)); } finally { diff --git a/src/Ryujinx.Ui.Common/Helper/SetupValidator.cs b/src/Ryujinx.Ui.Common/Helper/SetupValidator.cs index 65c38d7b8..45d9f8f0d 100644 --- a/src/Ryujinx.Ui.Common/Helper/SetupValidator.cs +++ b/src/Ryujinx.Ui.Common/Helper/SetupValidator.cs @@ -3,7 +3,7 @@ using Ryujinx.HLE.FileSystem; using System; using System.IO; -namespace Ryujinx.Ui.Common.Helper +namespace Ryujinx.UI.Common.Helper { /// /// Ensure installation validity @@ -12,18 +12,11 @@ namespace Ryujinx.Ui.Common.Helper { public static bool IsFirmwareValid(ContentManager contentManager, out UserError error) { - bool hasFirmware = contentManager.GetCurrentFirmwareVersion() != null; + error = contentManager.GetCurrentFirmwareVersion() != null + ? UserError.Success + : UserError.NoFirmware; - if (hasFirmware) - { - error = UserError.Success; - - return true; - } - - error = UserError.NoFirmware; - - return false; + return error is UserError.Success; } public static bool CanFixStartApplication(ContentManager contentManager, string baseApplicationPath, UserError error, out SystemVersion firmwareVersion) @@ -75,12 +68,11 @@ namespace Ryujinx.Ui.Common.Helper return true; } - catch (Exception) { } + catch + { + // ignored + } } - - outError = error; - - return false; } } @@ -96,13 +88,12 @@ namespace Ryujinx.Ui.Common.Helper string baseApplicationExtension = Path.GetExtension(baseApplicationPath).ToLowerInvariant(); // NOTE: We don't force homebrew developers to install a system firmware. - if (baseApplicationExtension == ".nro" || baseApplicationExtension == ".nso") + if (baseApplicationExtension is ".nro" or ".nso") { error = UserError.Success; - return true; } - + return IsFirmwareValid(contentManager, out error); } diff --git a/src/Ryujinx.Ui.Common/Helper/ShortcutHelper.cs b/src/Ryujinx.Ui.Common/Helper/ShortcutHelper.cs index 97b9853db..8c006a227 100644 --- a/src/Ryujinx.Ui.Common/Helper/ShortcutHelper.cs +++ b/src/Ryujinx.Ui.Common/Helper/ShortcutHelper.cs @@ -1,59 +1,54 @@ using Ryujinx.Common; using Ryujinx.Common.Configuration; using ShellLink; -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.PixelFormats; +using SkiaSharp; using System; using System.Collections.Generic; -using System.Drawing; -using System.Drawing.Drawing2D; -using System.Drawing.Imaging; using System.IO; using System.Runtime.Versioning; -using Image = System.Drawing.Image; -namespace Ryujinx.Ui.Common.Helper +namespace Ryujinx.UI.Common.Helper { public static class ShortcutHelper { [SupportedOSPlatform("windows")] - private static void CreateShortcutWindows(string applicationFilePath, byte[] iconData, string iconPath, string cleanedAppName, string desktopPath) + private static void CreateShortcutWindows(string applicationFilePath, string applicationId, byte[] iconData, string iconPath, string cleanedAppName, string desktopPath) { string basePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, AppDomain.CurrentDomain.FriendlyName + ".exe"); iconPath += ".ico"; MemoryStream iconDataStream = new(iconData); - using Image image = Image.FromStream(iconDataStream); - using Bitmap bitmap = new(128, 128); - using System.Drawing.Graphics graphic = System.Drawing.Graphics.FromImage(bitmap); - graphic.InterpolationMode = InterpolationMode.HighQualityBicubic; - graphic.DrawImage(image, 0, 0, 128, 128); - SaveBitmapAsIcon(bitmap, iconPath); + using var image = SKBitmap.Decode(iconDataStream); + image.Resize(new SKImageInfo(128, 128), SKFilterQuality.High); + SaveBitmapAsIcon(image, iconPath); - var shortcut = Shortcut.CreateShortcut(basePath, GetArgsString(applicationFilePath), iconPath, 0); + var shortcut = Shortcut.CreateShortcut(basePath, GetArgsString(applicationFilePath, applicationId), iconPath, 0); shortcut.StringData.NameString = cleanedAppName; shortcut.WriteToFile(Path.Combine(desktopPath, cleanedAppName + ".lnk")); } [SupportedOSPlatform("linux")] - private static void CreateShortcutLinux(string applicationFilePath, byte[] iconData, string iconPath, string desktopPath, string cleanedAppName) + private static void CreateShortcutLinux(string applicationFilePath, string applicationId, byte[] iconData, string iconPath, string desktopPath, string cleanedAppName) { string basePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Ryujinx.sh"); - var desktopFile = EmbeddedResources.ReadAllText("Ryujinx.Ui.Common/shortcut-template.desktop"); + var desktopFile = EmbeddedResources.ReadAllText("Ryujinx.UI.Common/shortcut-template.desktop"); iconPath += ".png"; - var image = SixLabors.ImageSharp.Image.Load(iconData); - image.SaveAsPng(iconPath); + var image = SKBitmap.Decode(iconData); + using var data = image.Encode(SKEncodedImageFormat.Png, 100); + using var file = File.OpenWrite(iconPath); + data.SaveTo(file); using StreamWriter outputFile = new(Path.Combine(desktopPath, cleanedAppName + ".desktop")); - outputFile.Write(desktopFile, cleanedAppName, iconPath, $"{basePath} {GetArgsString(applicationFilePath)}"); + outputFile.Write(desktopFile, cleanedAppName, iconPath, $"{basePath} {GetArgsString(applicationFilePath, applicationId)}"); } [SupportedOSPlatform("macos")] - private static void CreateShortcutMacos(string appFilePath, byte[] iconData, string desktopPath, string cleanedAppName) + private static void CreateShortcutMacos(string appFilePath, string applicationId, byte[] iconData, string desktopPath, string cleanedAppName) { string basePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Ryujinx"); - var plistFile = EmbeddedResources.ReadAllText("Ryujinx.Ui.Common/shortcut-template.plist"); + var plistFile = EmbeddedResources.ReadAllText("Ryujinx.UI.Common/shortcut-template.plist"); + var shortcutScript = EmbeddedResources.ReadAllText("Ryujinx.UI.Common/shortcut-launch-script.sh"); // Macos .App folder string contentFolderPath = Path.Combine("/Applications", cleanedAppName + ".app", "Contents"); string scriptFolderPath = Path.Combine(contentFolderPath, "MacOS"); @@ -68,8 +63,7 @@ namespace Ryujinx.Ui.Common.Helper string scriptPath = Path.Combine(scriptFolderPath, ScriptName); using StreamWriter scriptFile = new(scriptPath); - scriptFile.WriteLine("#!/bin/sh"); - scriptFile.WriteLine($"{basePath} {GetArgsString(appFilePath)}"); + scriptFile.Write(shortcutScript, basePath, GetArgsString(appFilePath, applicationId)); // Set execute permission FileInfo fileInfo = new(scriptPath); @@ -83,8 +77,10 @@ namespace Ryujinx.Ui.Common.Helper } const string IconName = "icon.png"; - var image = SixLabors.ImageSharp.Image.Load(iconData); - image.SaveAsPng(Path.Combine(resourceFolderPath, IconName)); + var image = SKBitmap.Decode(iconData); + using var data = image.Encode(SKEncodedImageFormat.Png, 100); + using var file = File.OpenWrite(Path.Combine(resourceFolderPath, IconName)); + data.SaveTo(file); // plist file using StreamWriter outputFile = new(Path.Combine(contentFolderPath, "Info.plist")); @@ -100,7 +96,7 @@ namespace Ryujinx.Ui.Common.Helper { string iconPath = Path.Combine(AppDataManager.BaseDirPath, "games", applicationId, "app"); - CreateShortcutWindows(applicationFilePath, iconData, iconPath, cleanedAppName, desktopPath); + CreateShortcutWindows(applicationFilePath, applicationId, iconData, iconPath, cleanedAppName, desktopPath); return; } @@ -110,14 +106,14 @@ namespace Ryujinx.Ui.Common.Helper string iconPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".local", "share", "icons", "Ryujinx"); Directory.CreateDirectory(iconPath); - CreateShortcutLinux(applicationFilePath, iconData, Path.Combine(iconPath, applicationId), desktopPath, cleanedAppName); + CreateShortcutLinux(applicationFilePath, applicationId, iconData, Path.Combine(iconPath, applicationId), desktopPath, cleanedAppName); return; } if (OperatingSystem.IsMacOS()) { - CreateShortcutMacos(applicationFilePath, iconData, desktopPath, cleanedAppName); + CreateShortcutMacos(applicationFilePath, applicationId, iconData, desktopPath, cleanedAppName); return; } @@ -125,7 +121,7 @@ namespace Ryujinx.Ui.Common.Helper throw new NotImplementedException("Shortcut support has not been implemented yet for this OS."); } - private static string GetArgsString(string appFilePath) + private static string GetArgsString(string appFilePath, string applicationId) { // args are first defined as a list, for easier adjustments in the future var argsList = new List(); @@ -136,18 +132,24 @@ namespace Ryujinx.Ui.Common.Helper argsList.Add($"\"{CommandLineState.BaseDirPathArg}\""); } + if (appFilePath.ToLower().EndsWith(".xci")) + { + argsList.Add("--application-id"); + argsList.Add($"\"{applicationId}\""); + } + argsList.Add($"\"{appFilePath}\""); - return String.Join(" ", argsList); + return string.Join(" ", argsList); } /// - /// Creates a Icon (.ico) file using the source bitmap image at the specified file path. + /// Creates an Icon (.ico) file using the source bitmap image at the specified file path. /// /// The source bitmap image that will be saved as an .ico file /// The location that the new .ico file will be saved too (Make sure to include '.ico' in the path). [SupportedOSPlatform("windows")] - private static void SaveBitmapAsIcon(Bitmap source, string filePath) + private static void SaveBitmapAsIcon(SKBitmap source, string filePath) { // Code Modified From https://stackoverflow.com/a/11448060/368354 by Benlitz byte[] header = { 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 32, 0, 0, 0, 0, 0, 22, 0, 0, 0 }; @@ -155,13 +157,16 @@ namespace Ryujinx.Ui.Common.Helper fs.Write(header); // Writing actual data - source.Save(fs, ImageFormat.Png); + using var data = source.Encode(SKEncodedImageFormat.Png, 100); + data.SaveTo(fs); // Getting data length (file length minus header) long dataLength = fs.Length - header.Length; // Write it in the correct place fs.Seek(14, SeekOrigin.Begin); fs.WriteByte((byte)dataLength); fs.WriteByte((byte)(dataLength >> 8)); + fs.WriteByte((byte)(dataLength >> 16)); + fs.WriteByte((byte)(dataLength >> 24)); } } } diff --git a/src/Ryujinx.Ui.Common/Helper/TitleHelper.cs b/src/Ryujinx.Ui.Common/Helper/TitleHelper.cs index 089b52154..9d73aea75 100644 --- a/src/Ryujinx.Ui.Common/Helper/TitleHelper.cs +++ b/src/Ryujinx.Ui.Common/Helper/TitleHelper.cs @@ -1,16 +1,14 @@ using Ryujinx.HLE.Loaders.Processes; using System; -namespace Ryujinx.Ui.Common.Helper +namespace Ryujinx.UI.Common.Helper { public static class TitleHelper { public static string ActiveApplicationTitle(ProcessResult activeProcess, string applicationVersion, string pauseString = "") { if (activeProcess == null) - { - return String.Empty; - } + return string.Empty; string titleNameSection = string.IsNullOrWhiteSpace(activeProcess.Name) ? string.Empty : $" {activeProcess.Name}"; string titleVersionSection = string.IsNullOrWhiteSpace(activeProcess.DisplayVersion) ? string.Empty : $" v{activeProcess.DisplayVersion}"; @@ -19,12 +17,9 @@ namespace Ryujinx.Ui.Common.Helper string appTitle = $"Ryujinx {applicationVersion} -{titleNameSection}{titleVersionSection}{titleIdSection}{titleArchSection}"; - if (!string.IsNullOrEmpty(pauseString)) - { - appTitle += $" ({pauseString})"; - } - - return appTitle; + return !string.IsNullOrEmpty(pauseString) + ? appTitle + $" ({pauseString})" + : appTitle; } } } diff --git a/src/Ryujinx.Ui.Common/Helper/ValueFormatUtils.cs b/src/Ryujinx.Ui.Common/Helper/ValueFormatUtils.cs index b1597a7cc..c203834f5 100644 --- a/src/Ryujinx.Ui.Common/Helper/ValueFormatUtils.cs +++ b/src/Ryujinx.Ui.Common/Helper/ValueFormatUtils.cs @@ -2,7 +2,7 @@ using System; using System.Globalization; using System.Linq; -namespace Ryujinx.Ui.Common.Helper +namespace Ryujinx.UI.Common.Helper { public static class ValueFormatUtils { @@ -75,13 +75,7 @@ namespace Ryujinx.Ui.Common.Helper { culture ??= CultureInfo.CurrentCulture; - if (!utcDateTime.HasValue) - { - // In the Avalonia UI, this is turned into a localized version of "Never" by LocalizedNeverConverter. - return "Never"; - } - - return utcDateTime.Value.ToLocalTime().ToString(culture); + return utcDateTime?.ToLocalTime().ToString(culture); } /// diff --git a/src/Ryujinx.Ui.Common/Models/Amiibo/AmiiboApi.cs b/src/Ryujinx.Ui.Common/Models/Amiibo/AmiiboApi.cs index ff0b80c46..7989f0f1a 100644 --- a/src/Ryujinx.Ui.Common/Models/Amiibo/AmiiboApi.cs +++ b/src/Ryujinx.Ui.Common/Models/Amiibo/AmiiboApi.cs @@ -2,7 +2,7 @@ using System; using System.Collections.Generic; using System.Text.Json.Serialization; -namespace Ryujinx.Ui.Common.Models.Amiibo +namespace Ryujinx.UI.Common.Models.Amiibo { public struct AmiiboApi : IEquatable { diff --git a/src/Ryujinx.Ui.Common/Models/Amiibo/AmiiboApiGamesSwitch.cs b/src/Ryujinx.Ui.Common/Models/Amiibo/AmiiboApiGamesSwitch.cs index 3c62b7cc4..40e635bf0 100644 --- a/src/Ryujinx.Ui.Common/Models/Amiibo/AmiiboApiGamesSwitch.cs +++ b/src/Ryujinx.Ui.Common/Models/Amiibo/AmiiboApiGamesSwitch.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using System.Text.Json.Serialization; -namespace Ryujinx.Ui.Common.Models.Amiibo +namespace Ryujinx.UI.Common.Models.Amiibo { public class AmiiboApiGamesSwitch { diff --git a/src/Ryujinx.Ui.Common/Models/Amiibo/AmiiboApiUsage.cs b/src/Ryujinx.Ui.Common/Models/Amiibo/AmiiboApiUsage.cs index 3c774fd56..4f8d292b1 100644 --- a/src/Ryujinx.Ui.Common/Models/Amiibo/AmiiboApiUsage.cs +++ b/src/Ryujinx.Ui.Common/Models/Amiibo/AmiiboApiUsage.cs @@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace Ryujinx.Ui.Common.Models.Amiibo +namespace Ryujinx.UI.Common.Models.Amiibo { public class AmiiboApiUsage { diff --git a/src/Ryujinx.Ui.Common/Models/Amiibo/AmiiboJson.cs b/src/Ryujinx.Ui.Common/Models/Amiibo/AmiiboJson.cs index c9d91c50a..15083f505 100644 --- a/src/Ryujinx.Ui.Common/Models/Amiibo/AmiiboJson.cs +++ b/src/Ryujinx.Ui.Common/Models/Amiibo/AmiiboJson.cs @@ -2,7 +2,7 @@ using System; using System.Collections.Generic; using System.Text.Json.Serialization; -namespace Ryujinx.Ui.Common.Models.Amiibo +namespace Ryujinx.UI.Common.Models.Amiibo { public struct AmiiboJson { diff --git a/src/Ryujinx.Ui.Common/Models/Amiibo/AmiiboJsonSerializerContext.cs b/src/Ryujinx.Ui.Common/Models/Amiibo/AmiiboJsonSerializerContext.cs index 4906c6524..bc3f1303c 100644 --- a/src/Ryujinx.Ui.Common/Models/Amiibo/AmiiboJsonSerializerContext.cs +++ b/src/Ryujinx.Ui.Common/Models/Amiibo/AmiiboJsonSerializerContext.cs @@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace Ryujinx.Ui.Common.Models.Amiibo +namespace Ryujinx.UI.Common.Models.Amiibo { [JsonSerializable(typeof(AmiiboJson))] public partial class AmiiboJsonSerializerContext : JsonSerializerContext diff --git a/src/Ryujinx.Ui.Common/Models/Github/GithubReleaseAssetJsonResponse.cs b/src/Ryujinx.Ui.Common/Models/Github/GithubReleaseAssetJsonResponse.cs index 67d238d24..8f528dc0b 100644 --- a/src/Ryujinx.Ui.Common/Models/Github/GithubReleaseAssetJsonResponse.cs +++ b/src/Ryujinx.Ui.Common/Models/Github/GithubReleaseAssetJsonResponse.cs @@ -1,4 +1,4 @@ -namespace Ryujinx.Ui.Common.Models.Github +namespace Ryujinx.UI.Common.Models.Github { public class GithubReleaseAssetJsonResponse { diff --git a/src/Ryujinx.Ui.Common/Models/Github/GithubReleasesJsonResponse.cs b/src/Ryujinx.Ui.Common/Models/Github/GithubReleasesJsonResponse.cs index 0f83e32cc..7bec1bcdc 100644 --- a/src/Ryujinx.Ui.Common/Models/Github/GithubReleasesJsonResponse.cs +++ b/src/Ryujinx.Ui.Common/Models/Github/GithubReleasesJsonResponse.cs @@ -1,10 +1,12 @@ using System.Collections.Generic; -namespace Ryujinx.Ui.Common.Models.Github +namespace Ryujinx.UI.Common.Models.Github { public class GithubReleasesJsonResponse { public string Name { get; set; } + + public string TagName { get; set; } public List Assets { get; set; } } } diff --git a/src/Ryujinx.Ui.Common/Models/Github/GithubReleasesJsonSerializerContext.cs b/src/Ryujinx.Ui.Common/Models/Github/GithubReleasesJsonSerializerContext.cs index 8a19277b3..71864257c 100644 --- a/src/Ryujinx.Ui.Common/Models/Github/GithubReleasesJsonSerializerContext.cs +++ b/src/Ryujinx.Ui.Common/Models/Github/GithubReleasesJsonSerializerContext.cs @@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace Ryujinx.Ui.Common.Models.Github +namespace Ryujinx.UI.Common.Models.Github { [JsonSerializable(typeof(GithubReleasesJsonResponse), GenerationMode = JsonSourceGenerationMode.Metadata)] public partial class GithubReleasesJsonSerializerContext : JsonSerializerContext diff --git a/src/Ryujinx.Ui.Common/Resources/Icon_NCA.png b/src/Ryujinx.Ui.Common/Resources/Icon_NCA.png index feae77b94..99def6cfd 100644 Binary files a/src/Ryujinx.Ui.Common/Resources/Icon_NCA.png and b/src/Ryujinx.Ui.Common/Resources/Icon_NCA.png differ diff --git a/src/Ryujinx.Ui.Common/Resources/Icon_NRO.png b/src/Ryujinx.Ui.Common/Resources/Icon_NRO.png index 3a9da6218..6cec176ad 100644 Binary files a/src/Ryujinx.Ui.Common/Resources/Icon_NRO.png and b/src/Ryujinx.Ui.Common/Resources/Icon_NRO.png differ diff --git a/src/Ryujinx.Ui.Common/Resources/Icon_NSO.png b/src/Ryujinx.Ui.Common/Resources/Icon_NSO.png index 16de84bed..ad88459cf 100644 Binary files a/src/Ryujinx.Ui.Common/Resources/Icon_NSO.png and b/src/Ryujinx.Ui.Common/Resources/Icon_NSO.png differ diff --git a/src/Ryujinx.Ui.Common/Resources/Icon_NSP.png b/src/Ryujinx.Ui.Common/Resources/Icon_NSP.png index 4f98e22ea..a0cef62f4 100644 Binary files a/src/Ryujinx.Ui.Common/Resources/Icon_NSP.png and b/src/Ryujinx.Ui.Common/Resources/Icon_NSP.png differ diff --git a/src/Ryujinx.Ui.Common/Resources/Icon_XCI.png b/src/Ryujinx.Ui.Common/Resources/Icon_XCI.png index f9c34f47f..ef33f2816 100644 Binary files a/src/Ryujinx.Ui.Common/Resources/Icon_XCI.png and b/src/Ryujinx.Ui.Common/Resources/Icon_XCI.png differ diff --git a/src/Ryujinx.Ui.Common/Resources/Logo_Patreon_Dark.png b/src/Ryujinx.Ui.Common/Resources/Logo_Patreon_Dark.png deleted file mode 100644 index 9a521e3fd..000000000 Binary files a/src/Ryujinx.Ui.Common/Resources/Logo_Patreon_Dark.png and /dev/null differ diff --git a/src/Ryujinx.Ui.Common/Resources/Logo_Patreon_Light.png b/src/Ryujinx.Ui.Common/Resources/Logo_Patreon_Light.png deleted file mode 100644 index 44da0ac45..000000000 Binary files a/src/Ryujinx.Ui.Common/Resources/Logo_Patreon_Light.png and /dev/null differ diff --git a/src/Ryujinx.Ui.Common/Resources/Logo_Ryujinx.png b/src/Ryujinx.Ui.Common/Resources/Logo_Ryujinx.png index 0e8da15e6..28067e908 100644 Binary files a/src/Ryujinx.Ui.Common/Resources/Logo_Ryujinx.png and b/src/Ryujinx.Ui.Common/Resources/Logo_Ryujinx.png differ diff --git a/src/Ryujinx.Ui.Common/Resources/Logo_Twitter_Dark.png b/src/Ryujinx.Ui.Common/Resources/Logo_Twitter_Dark.png deleted file mode 100644 index 66962e7d3..000000000 Binary files a/src/Ryujinx.Ui.Common/Resources/Logo_Twitter_Dark.png and /dev/null differ diff --git a/src/Ryujinx.Ui.Common/Resources/Logo_Twitter_Light.png b/src/Ryujinx.Ui.Common/Resources/Logo_Twitter_Light.png deleted file mode 100644 index 040ca1699..000000000 Binary files a/src/Ryujinx.Ui.Common/Resources/Logo_Twitter_Light.png and /dev/null differ diff --git a/src/Ryujinx.Ui.Common/Ryujinx.Ui.Common.csproj b/src/Ryujinx.Ui.Common/Ryujinx.Ui.Common.csproj index 74331fdef..df6532a63 100644 --- a/src/Ryujinx.Ui.Common/Ryujinx.Ui.Common.csproj +++ b/src/Ryujinx.Ui.Common/Ryujinx.Ui.Common.csproj @@ -18,9 +18,7 @@ - - @@ -39,24 +37,21 @@ - - - - - + + + - diff --git a/src/Ryujinx.Ui.Common/SystemInfo/LinuxSystemInfo.cs b/src/Ryujinx.Ui.Common/SystemInfo/LinuxSystemInfo.cs index 5f1ab5416..c7fe05a09 100644 --- a/src/Ryujinx.Ui.Common/SystemInfo/LinuxSystemInfo.cs +++ b/src/Ryujinx.Ui.Common/SystemInfo/LinuxSystemInfo.cs @@ -5,7 +5,7 @@ using System.Globalization; using System.IO; using System.Runtime.Versioning; -namespace Ryujinx.Ui.Common.SystemInfo +namespace Ryujinx.UI.Common.SystemInfo { [SupportedOSPlatform("linux")] class LinuxSystemInfo : SystemInfo diff --git a/src/Ryujinx.Ui.Common/SystemInfo/MacOSSystemInfo.cs b/src/Ryujinx.Ui.Common/SystemInfo/MacOSSystemInfo.cs index 3508ae3a4..894c3cadc 100644 --- a/src/Ryujinx.Ui.Common/SystemInfo/MacOSSystemInfo.cs +++ b/src/Ryujinx.Ui.Common/SystemInfo/MacOSSystemInfo.cs @@ -5,7 +5,7 @@ using System.Runtime.InteropServices; using System.Runtime.Versioning; using System.Text; -namespace Ryujinx.Ui.Common.SystemInfo +namespace Ryujinx.UI.Common.SystemInfo { [SupportedOSPlatform("macos")] partial class MacOSSystemInfo : SystemInfo @@ -68,11 +68,11 @@ namespace Ryujinx.Ui.Common.SystemInfo private const string SystemLibraryName = "libSystem.dylib"; [LibraryImport(SystemLibraryName, SetLastError = true)] - private static partial int sysctlbyname([MarshalAs(UnmanagedType.LPStr)] string name, IntPtr oldValue, ref ulong oldSize, IntPtr newValue, ulong newValueSize); + private static partial int sysctlbyname([MarshalAs(UnmanagedType.LPStr)] string name, nint oldValue, ref ulong oldSize, nint newValue, ulong newValueSize); - private static int SysctlByName(string name, IntPtr oldValue, ref ulong oldSize) + private static int SysctlByName(string name, nint oldValue, ref ulong oldSize) { - if (sysctlbyname(name, oldValue, ref oldSize, IntPtr.Zero, 0) == -1) + if (sysctlbyname(name, oldValue, ref oldSize, nint.Zero, 0) == -1) { int err = Marshal.GetLastWin32Error(); @@ -90,7 +90,7 @@ namespace Ryujinx.Ui.Common.SystemInfo { ulong oldValueSize = (ulong)Unsafe.SizeOf(); - return SysctlByName(name, (IntPtr)Unsafe.AsPointer(ref oldValue), ref oldValueSize); + return SysctlByName(name, (nint)Unsafe.AsPointer(ref oldValue), ref oldValueSize); } } @@ -100,7 +100,7 @@ namespace Ryujinx.Ui.Common.SystemInfo ulong strSize = 0; - int res = SysctlByName(name, IntPtr.Zero, ref strSize); + int res = SysctlByName(name, nint.Zero, ref strSize); if (res == 0) { @@ -110,7 +110,7 @@ namespace Ryujinx.Ui.Common.SystemInfo { fixed (byte* rawDataPtr = rawData) { - res = SysctlByName(name, (IntPtr)rawDataPtr, ref strSize); + res = SysctlByName(name, (nint)rawDataPtr, ref strSize); } if (res == 0) diff --git a/src/Ryujinx.Ui.Common/SystemInfo/SystemInfo.cs b/src/Ryujinx.Ui.Common/SystemInfo/SystemInfo.cs index e78db8af7..2dfa9160d 100644 --- a/src/Ryujinx.Ui.Common/SystemInfo/SystemInfo.cs +++ b/src/Ryujinx.Ui.Common/SystemInfo/SystemInfo.cs @@ -1,11 +1,11 @@ using Ryujinx.Common.Logging; -using Ryujinx.Ui.Common.Helper; +using Ryujinx.UI.Common.Helper; using System; using System.Runtime.InteropServices; using System.Runtime.Intrinsics.X86; using System.Text; -namespace Ryujinx.Ui.Common.SystemInfo +namespace Ryujinx.UI.Common.SystemInfo { public class SystemInfo { @@ -33,17 +33,13 @@ namespace Ryujinx.Ui.Common.SystemInfo public static SystemInfo Gather() { if (OperatingSystem.IsWindows()) - { return new WindowsSystemInfo(); - } - else if (OperatingSystem.IsLinux()) - { + + if (OperatingSystem.IsLinux()) return new LinuxSystemInfo(); - } - else if (OperatingSystem.IsMacOS()) - { + + if (OperatingSystem.IsMacOS()) return new MacOSSystemInfo(); - } Logger.Error?.Print(LogClass.Application, "SystemInfo unsupported on this platform"); diff --git a/src/Ryujinx.Ui.Common/SystemInfo/WindowsSystemInfo.cs b/src/Ryujinx.Ui.Common/SystemInfo/WindowsSystemInfo.cs index 9bb0fbf74..4a2c8795d 100644 --- a/src/Ryujinx.Ui.Common/SystemInfo/WindowsSystemInfo.cs +++ b/src/Ryujinx.Ui.Common/SystemInfo/WindowsSystemInfo.cs @@ -4,7 +4,7 @@ using System.Management; using System.Runtime.InteropServices; using System.Runtime.Versioning; -namespace Ryujinx.Ui.Common.SystemInfo +namespace Ryujinx.UI.Common.SystemInfo { [SupportedOSPlatform("windows")] partial class WindowsSystemInfo : SystemInfo @@ -40,7 +40,7 @@ namespace Ryujinx.Ui.Common.SystemInfo } } - return Environment.GetEnvironmentVariable("PROCESSOR_IDENTIFIER").Trim(); + return Environment.GetEnvironmentVariable("PROCESSOR_IDENTIFIER")?.Trim(); } [StructLayout(LayoutKind.Sequential)] diff --git a/src/Ryujinx.Ui.Common/UserError.cs b/src/Ryujinx.Ui.Common/UserError.cs index 832aae9d6..706971efa 100644 --- a/src/Ryujinx.Ui.Common/UserError.cs +++ b/src/Ryujinx.Ui.Common/UserError.cs @@ -1,4 +1,4 @@ -namespace Ryujinx.Ui.Common +namespace Ryujinx.UI.Common { /// /// Represent a common error that could be reported to the user by the emulator. diff --git a/src/Ryujinx.Ui.LocaleGenerator/LocaleGenerator.cs b/src/Ryujinx.Ui.LocaleGenerator/LocaleGenerator.cs index 27573a8fb..729a166b6 100644 --- a/src/Ryujinx.Ui.LocaleGenerator/LocaleGenerator.cs +++ b/src/Ryujinx.Ui.LocaleGenerator/LocaleGenerator.cs @@ -2,7 +2,7 @@ using Microsoft.CodeAnalysis; using System.Linq; using System.Text; -namespace Ryujinx.Ui.LocaleGenerator +namespace Ryujinx.UI.LocaleGenerator { [Generator] public class LocaleGenerator : IIncrementalGenerator @@ -15,7 +15,7 @@ namespace Ryujinx.Ui.LocaleGenerator context.RegisterSourceOutput(contents, (spc, content) => { - var lines = content.Split('\n').Where(x => x.Trim().StartsWith("\"")).Select(x => x.Split(':')[0].Trim().Replace("\"", "")); + var lines = content.Split('\n').Where(x => x.Trim().StartsWith("\"")).Select(x => x.Split(':')[0].Trim().Replace("\"", string.Empty)); StringBuilder enumSourceBuilder = new(); enumSourceBuilder.AppendLine("namespace Ryujinx.Ava.Common.Locale;"); enumSourceBuilder.AppendLine("internal enum LocaleKeys"); diff --git a/src/Ryujinx/App.axaml b/src/Ryujinx/App.axaml new file mode 100644 index 000000000..5a603509c --- /dev/null +++ b/src/Ryujinx/App.axaml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + diff --git a/src/Ryujinx/App.axaml.cs b/src/Ryujinx/App.axaml.cs new file mode 100644 index 000000000..15ada201c --- /dev/null +++ b/src/Ryujinx/App.axaml.cs @@ -0,0 +1,139 @@ +using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Markup.Xaml; +using Avalonia.Platform; +using Avalonia.Styling; +using Avalonia.Threading; +using FluentAvalonia.UI.Windowing; +using Gommon; +using Ryujinx.Ava.Common; +using Ryujinx.Ava.Common.Locale; +using Ryujinx.Ava.UI.Helpers; +using Ryujinx.Ava.UI.Windows; +using Ryujinx.Common; +using Ryujinx.Common.Logging; +using Ryujinx.UI.Common.Configuration; +using Ryujinx.UI.Common.Helper; +using System; +using System.Diagnostics; + +namespace Ryujinx.Ava +{ + public class App : Application + { + internal static string FormatTitle(LocaleKeys? windowTitleKey = null) + => windowTitleKey is null + ? $"{FullAppName} {Program.Version}" + : $"{FullAppName} {Program.Version} - {LocaleManager.Instance[windowTitleKey.Value]}"; + + public static readonly string FullAppName = ReleaseInformation.IsCanaryBuild ? "Ryujinx Canary" : "Ryujinx"; + + public static MainWindow MainWindow => Current! + .ApplicationLifetime.Cast() + .MainWindow.Cast(); + + public static void SetTaskbarProgress(TaskBarProgressBarState state) => MainWindow.PlatformFeatures.SetTaskBarProgressBarState(state); + public static void SetTaskbarProgressValue(ulong current, ulong total) => MainWindow.PlatformFeatures.SetTaskBarProgressBarValue(current, total); + public static void SetTaskbarProgressValue(long current, long total) => SetTaskbarProgressValue(Convert.ToUInt64(current), Convert.ToUInt64(total)); + + + public override void Initialize() + { + Name = FormatTitle(); + + AvaloniaXamlLoader.Load(this); + + if (OperatingSystem.IsMacOS()) + { + Process.Start("/usr/bin/defaults", "write org.ryujinx.Ryujinx ApplePressAndHoldEnabled -bool false"); + } + } + + public override void OnFrameworkInitializationCompleted() + { + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + desktop.MainWindow = new MainWindow(); + } + + base.OnFrameworkInitializationCompleted(); + + if (Program.PreviewerDetached) + { + ApplyConfiguredTheme(ConfigurationState.Instance.UI.BaseStyle); + + ConfigurationState.Instance.UI.BaseStyle.Event += ThemeChanged_Event; + } + } + + private void ShowRestartDialog() + { + _ = Dispatcher.UIThread.InvokeAsync(async () => + { + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + var result = await ContentDialogHelper.CreateConfirmationDialog( + LocaleManager.Instance[LocaleKeys.DialogThemeRestartMessage], + LocaleManager.Instance[LocaleKeys.DialogThemeRestartSubMessage], + LocaleManager.Instance[LocaleKeys.InputDialogYes], + LocaleManager.Instance[LocaleKeys.InputDialogNo], + LocaleManager.Instance[LocaleKeys.DialogRestartRequiredMessage]); + + if (result == UserResult.Yes) + { + _ = Process.Start(Environment.ProcessPath!, CommandLineState.Arguments); + desktop.Shutdown(); + Environment.Exit(0); + } + } + }); + } + + private void ThemeChanged_Event(object _, ReactiveEventArgs rArgs) => ApplyConfiguredTheme(rArgs.NewValue); + + public void ApplyConfiguredTheme(string baseStyle) + { + try + { + if (string.IsNullOrWhiteSpace(baseStyle)) + { + ConfigurationState.Instance.UI.BaseStyle.Value = "Auto"; + + baseStyle = ConfigurationState.Instance.UI.BaseStyle; + } + + ThemeManager.OnThemeChanged(); + + RequestedThemeVariant = baseStyle switch + { + "Auto" => DetectSystemTheme(), + "Light" => ThemeVariant.Light, + "Dark" => ThemeVariant.Dark, + _ => ThemeVariant.Default, + }; + } + catch (Exception) + { + Logger.Warning?.Print(LogClass.Application, "Failed to apply theme. A restart is needed to apply the selected theme."); + + ShowRestartDialog(); + } + } + + /// + /// Converts a PlatformThemeVariant value to the corresponding ThemeVariant value. + /// + public static ThemeVariant ConvertThemeVariant(PlatformThemeVariant platformThemeVariant) => + platformThemeVariant switch + { + PlatformThemeVariant.Dark => ThemeVariant.Dark, + PlatformThemeVariant.Light => ThemeVariant.Light, + _ => ThemeVariant.Default, + }; + + public static ThemeVariant DetectSystemTheme() => + Current is App { PlatformSettings: not null } app + ? ConvertThemeVariant(app.PlatformSettings.GetColorValues().ThemeVariant) + : ThemeVariant.Default; + } +} diff --git a/src/Ryujinx/AppHost.cs b/src/Ryujinx/AppHost.cs new file mode 100644 index 000000000..5789737d6 --- /dev/null +++ b/src/Ryujinx/AppHost.cs @@ -0,0 +1,1383 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Input; +using Avalonia.Threading; +using LibHac.Tools.FsSystem; +using Ryujinx.Audio.Backends.Dummy; +using Ryujinx.Audio.Backends.OpenAL; +using Ryujinx.Audio.Backends.SDL2; +using Ryujinx.Audio.Backends.SoundIo; +using Ryujinx.Audio.Integration; +using Ryujinx.Ava.Common; +using Ryujinx.Ava.Common.Locale; +using Ryujinx.Ava.Input; +using Ryujinx.Ava.UI.Helpers; +using Ryujinx.Ava.UI.Models; +using Ryujinx.Ava.UI.Renderer; +using Ryujinx.Ava.UI.ViewModels; +using Ryujinx.Ava.UI.Windows; +using Ryujinx.Common; +using Ryujinx.Common.Configuration; +using Ryujinx.Common.Configuration.Multiplayer; +using Ryujinx.Common.Logging; +using Ryujinx.Common.SystemInterop; +using Ryujinx.Common.Utilities; +using Ryujinx.Graphics.GAL; +using Ryujinx.Graphics.GAL.Multithreading; +using Ryujinx.Graphics.Gpu; +using Ryujinx.Graphics.OpenGL; +using Ryujinx.Graphics.Vulkan; +using Ryujinx.HLE; +using Ryujinx.HLE.FileSystem; +using Ryujinx.HLE.HOS; +using Ryujinx.HLE.HOS.Services.Account.Acc; +using Ryujinx.HLE.HOS.SystemState; +using Ryujinx.Input; +using Ryujinx.Input.HLE; +using Ryujinx.UI.App.Common; +using Ryujinx.UI.Common; +using Ryujinx.UI.Common.Configuration; +using Ryujinx.UI.Common.Helper; +using Silk.NET.Vulkan; +using SkiaSharp; +using SPB.Graphics.Vulkan; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using static Ryujinx.Ava.UI.Helpers.Win32NativeInterop; +using AntiAliasing = Ryujinx.Common.Configuration.AntiAliasing; +using InputManager = Ryujinx.Input.HLE.InputManager; +using IRenderer = Ryujinx.Graphics.GAL.IRenderer; +using Key = Ryujinx.Input.Key; +using MouseButton = Ryujinx.Input.MouseButton; +using ScalingFilter = Ryujinx.Common.Configuration.ScalingFilter; +using Size = Avalonia.Size; +using Switch = Ryujinx.HLE.Switch; +using VSyncMode = Ryujinx.Common.Configuration.VSyncMode; + +namespace Ryujinx.Ava +{ + internal class AppHost + { + private const int CursorHideIdleTime = 5; // Hide Cursor seconds. + private const float MaxResolutionScale = 4.0f; // Max resolution hotkeys can scale to before wrapping. + private const int TargetFps = 60; + private const float VolumeDelta = 0.05f; + + private static readonly Cursor _invisibleCursor = new(StandardCursorType.None); + private readonly nint _invisibleCursorWin; + private readonly nint _defaultCursorWin; + + private readonly long _ticksPerFrame; + private readonly Stopwatch _chrono; + private long _ticks; + + private readonly AccountManager _accountManager; + private readonly UserChannelPersistence _userChannelPersistence; + private readonly InputManager _inputManager; + + private readonly MainWindowViewModel _viewModel; + private readonly IKeyboard _keyboardInterface; + private readonly TopLevel _topLevel; + public RendererHost RendererHost; + + private readonly GraphicsDebugLevel _glLogLevel; + private float _newVolume; + private KeyboardHotkeyState _prevHotkeyState; + + private long _lastCursorMoveTime; + private bool _isCursorInRenderer = true; + private bool _ignoreCursorState = false; + + private enum CursorStates + { + CursorIsHidden, + CursorIsVisible, + ForceChangeCursor + }; + + private CursorStates _cursorState = !ConfigurationState.Instance.Hid.EnableMouse.Value ? + CursorStates.CursorIsVisible : CursorStates.CursorIsHidden; + + private DateTime _lastShaderReset; + private uint _displayCount; + private uint _previousCount = 0; + + private bool _isStopped; + private bool _isActive; + private bool _renderingStarted; + + private readonly ManualResetEvent _gpuDoneEvent; + + private IRenderer _renderer; + private readonly Thread _renderingThread; + private readonly CancellationTokenSource _gpuCancellationTokenSource; + private WindowsMultimediaTimerResolution _windowsMultimediaTimerResolution; + + private bool _dialogShown; + private readonly bool _isFirmwareTitle; + + private readonly object _lockObject = new(); + + public event EventHandler AppExit; + public event EventHandler StatusUpdatedEvent; + + public VirtualFileSystem VirtualFileSystem { get; } + public ContentManager ContentManager { get; } + public NpadManager NpadManager { get; } + public TouchScreenManager TouchScreenManager { get; } + public HLE.Switch Device { get; set; } + + public int Width { get; private set; } + public int Height { get; private set; } + public string ApplicationPath { get; private set; } + public ulong ApplicationId { get; private set; } + public bool ScreenshotRequested { get; set; } + + public AppHost( + RendererHost renderer, + InputManager inputManager, + string applicationPath, + ulong applicationId, + VirtualFileSystem virtualFileSystem, + ContentManager contentManager, + AccountManager accountManager, + UserChannelPersistence userChannelPersistence, + MainWindowViewModel viewmodel, + TopLevel topLevel) + { + _viewModel = viewmodel; + _inputManager = inputManager; + _accountManager = accountManager; + _userChannelPersistence = userChannelPersistence; + _renderingThread = new Thread(RenderLoop) { Name = "GUI.RenderThread" }; + _lastCursorMoveTime = Stopwatch.GetTimestamp(); + _glLogLevel = ConfigurationState.Instance.Logger.GraphicsDebugLevel; + _topLevel = topLevel; + + _inputManager.SetMouseDriver(new AvaloniaMouseDriver(_topLevel, renderer)); + + _keyboardInterface = (IKeyboard)_inputManager.KeyboardDriver.GetGamepad("0"); + + NpadManager = _inputManager.CreateNpadManager(); + TouchScreenManager = _inputManager.CreateTouchScreenManager(); + ApplicationPath = applicationPath; + ApplicationId = applicationId; + VirtualFileSystem = virtualFileSystem; + ContentManager = contentManager; + + RendererHost = renderer; + + _chrono = new Stopwatch(); + _ticksPerFrame = Stopwatch.Frequency / TargetFps; + + if (ApplicationPath.StartsWith("@SystemContent")) + { + ApplicationPath = VirtualFileSystem.SwitchPathToSystemPath(ApplicationPath); + + _isFirmwareTitle = true; + } + + ConfigurationState.Instance.HideCursor.Event += HideCursorState_Changed; + + _topLevel.PointerMoved += TopLevel_PointerEnteredOrMoved; + _topLevel.PointerEntered += TopLevel_PointerEnteredOrMoved; + _topLevel.PointerExited += TopLevel_PointerExited; + + if (OperatingSystem.IsWindows()) + { + _invisibleCursorWin = CreateEmptyCursor(); + _defaultCursorWin = CreateArrowCursor(); + } + + ConfigurationState.Instance.System.IgnoreMissingServices.Event += UpdateIgnoreMissingServicesState; + ConfigurationState.Instance.Graphics.AspectRatio.Event += UpdateAspectRatioState; + ConfigurationState.Instance.System.EnableDockedMode.Event += UpdateDockedModeState; + ConfigurationState.Instance.System.AudioVolume.Event += UpdateAudioVolumeState; + ConfigurationState.Instance.System.EnableDockedMode.Event += UpdateDockedModeState; + ConfigurationState.Instance.System.AudioVolume.Event += UpdateAudioVolumeState; + ConfigurationState.Instance.Graphics.AntiAliasing.Event += UpdateAntiAliasing; + ConfigurationState.Instance.Graphics.ScalingFilter.Event += UpdateScalingFilter; + ConfigurationState.Instance.Graphics.ScalingFilterLevel.Event += UpdateScalingFilterLevel; + ConfigurationState.Instance.Graphics.EnableColorSpacePassthrough.Event += UpdateColorSpacePassthrough; + ConfigurationState.Instance.Graphics.VSyncMode.Event += UpdateVSyncMode; + ConfigurationState.Instance.Graphics.CustomVSyncInterval.Event += UpdateCustomVSyncIntervalValue; + ConfigurationState.Instance.Graphics.EnableCustomVSyncInterval.Event += UpdateCustomVSyncIntervalEnabled; + + ConfigurationState.Instance.System.EnableInternetAccess.Event += UpdateEnableInternetAccessState; + ConfigurationState.Instance.Multiplayer.LanInterfaceId.Event += UpdateLanInterfaceIdState; + ConfigurationState.Instance.Multiplayer.Mode.Event += UpdateMultiplayerModeState; + ConfigurationState.Instance.Multiplayer.LdnPassphrase.Event += UpdateLdnPassphraseState; + ConfigurationState.Instance.Multiplayer.LdnServer.Event += UpdateLdnServerState; + ConfigurationState.Instance.Multiplayer.DisableP2p.Event += UpdateDisableP2pState; + + _gpuCancellationTokenSource = new CancellationTokenSource(); + _gpuDoneEvent = new ManualResetEvent(false); + } + + private void TopLevel_PointerEnteredOrMoved(object sender, PointerEventArgs e) + { + if (!_viewModel.IsActive) + { + _isCursorInRenderer = false; + _ignoreCursorState = false; + return; + } + + if (sender is MainWindow window) + { + if (ConfigurationState.Instance.HideCursor.Value == HideCursorMode.OnIdle) + { + _lastCursorMoveTime = Stopwatch.GetTimestamp(); + } + + var point = e.GetCurrentPoint(window).Position; + var bounds = RendererHost.EmbeddedWindow.Bounds; + var windowYOffset = bounds.Y + window.MenuBarHeight; + var windowYLimit = (int)window.Bounds.Height - window.StatusBarHeight - 1; + + if (!_viewModel.ShowMenuAndStatusBar) + { + windowYOffset -= window.MenuBarHeight; + windowYLimit += window.StatusBarHeight + 1; + } + + _isCursorInRenderer = point.X >= bounds.X && + Math.Ceiling(point.X) <= (int)window.Bounds.Width && + point.Y >= windowYOffset && + point.Y <= windowYLimit && + !_viewModel.IsSubMenuOpen; + + _ignoreCursorState = false; + } + } + + private void TopLevel_PointerExited(object sender, PointerEventArgs e) + { + _isCursorInRenderer = false; + + if (sender is MainWindow window) + { + var point = e.GetCurrentPoint(window).Position; + var bounds = RendererHost.EmbeddedWindow.Bounds; + var windowYOffset = bounds.Y + window.MenuBarHeight; + var windowYLimit = (int)window.Bounds.Height - window.StatusBarHeight - 1; + + if (!_viewModel.ShowMenuAndStatusBar) + { + windowYOffset -= window.MenuBarHeight; + windowYLimit += window.StatusBarHeight + 1; + } + + _ignoreCursorState = (point.X == bounds.X || + Math.Ceiling(point.X) == (int)window.Bounds.Width) && + point.Y >= windowYOffset && + point.Y <= windowYLimit; + } + + _cursorState = CursorStates.ForceChangeCursor; + } + + private void UpdateScalingFilterLevel(object sender, ReactiveEventArgs e) + { + _renderer.Window?.SetScalingFilter((Graphics.GAL.ScalingFilter)ConfigurationState.Instance.Graphics.ScalingFilter.Value); + _renderer.Window?.SetScalingFilterLevel(ConfigurationState.Instance.Graphics.ScalingFilterLevel.Value); + } + + private void UpdateScalingFilter(object sender, ReactiveEventArgs e) + { + _renderer.Window?.SetScalingFilter((Graphics.GAL.ScalingFilter)ConfigurationState.Instance.Graphics.ScalingFilter.Value); + _renderer.Window?.SetScalingFilterLevel(ConfigurationState.Instance.Graphics.ScalingFilterLevel.Value); + } + + private void UpdateColorSpacePassthrough(object sender, ReactiveEventArgs e) + { + _renderer.Window?.SetColorSpacePassthrough((bool)ConfigurationState.Instance.Graphics.EnableColorSpacePassthrough.Value); + } + + public void UpdateVSyncMode(object sender, ReactiveEventArgs e) + { + if (Device != null) + { + Device.VSyncMode = e.NewValue; + Device.UpdateVSyncInterval(); + } + _renderer.Window?.ChangeVSyncMode((Ryujinx.Graphics.GAL.VSyncMode)e.NewValue); + + _viewModel.ShowCustomVSyncIntervalPicker = (e.NewValue == VSyncMode.Custom); + } + + public void VSyncModeToggle() + { + VSyncMode oldVSyncMode = Device.VSyncMode; + VSyncMode newVSyncMode = VSyncMode.Switch; + bool customVSyncIntervalEnabled = ConfigurationState.Instance.Graphics.EnableCustomVSyncInterval.Value; + + switch (oldVSyncMode) + { + case VSyncMode.Switch: + newVSyncMode = VSyncMode.Unbounded; + break; + case VSyncMode.Unbounded: + if (customVSyncIntervalEnabled) + { + newVSyncMode = VSyncMode.Custom; + } + else + { + newVSyncMode = VSyncMode.Switch; + } + + break; + case VSyncMode.Custom: + newVSyncMode = VSyncMode.Switch; + break; + } + + UpdateVSyncMode(this, new ReactiveEventArgs(oldVSyncMode, newVSyncMode)); + } + + private void UpdateCustomVSyncIntervalValue(object sender, ReactiveEventArgs e) + { + if (Device != null) + { + Device.TargetVSyncInterval = e.NewValue; + Device.UpdateVSyncInterval(); + } + } + + private void UpdateCustomVSyncIntervalEnabled(object sender, ReactiveEventArgs e) + { + if (Device != null) + { + Device.CustomVSyncIntervalEnabled = e.NewValue; + Device.UpdateVSyncInterval(); + } + } + + private void ShowCursor() + { + Dispatcher.UIThread.Post(() => + { + _viewModel.Cursor = Cursor.Default; + + if (OperatingSystem.IsWindows()) + { + if (_cursorState != CursorStates.CursorIsHidden && !_ignoreCursorState) + { + SetCursor(_defaultCursorWin); + } + } + }); + + _cursorState = CursorStates.CursorIsVisible; + } + + private void HideCursor() + { + Dispatcher.UIThread.Post(() => + { + _viewModel.Cursor = _invisibleCursor; + + if (OperatingSystem.IsWindows()) + { + SetCursor(_invisibleCursorWin); + } + }); + + _cursorState = CursorStates.CursorIsHidden; + } + + private void SetRendererWindowSize(Size size) + { + if (_renderer != null) + { + double scale = _topLevel.RenderScaling; + + _renderer.Window?.SetSize((int)(size.Width * scale), (int)(size.Height * scale)); + } + } + + private void Renderer_ScreenCaptured(object sender, ScreenCaptureImageInfo e) + { + if (e.Data.Length > 0 && e.Height > 0 && e.Width > 0) + { + Task.Run(() => + { + lock (_lockObject) + { + string applicationName = Device.Processes.ActiveApplication.Name; + string sanitizedApplicationName = FileSystemUtils.SanitizeFileName(applicationName); + DateTime currentTime = DateTime.Now; + + string filename = $"{sanitizedApplicationName}_{currentTime.Year}-{currentTime.Month:D2}-{currentTime.Day:D2}_{currentTime.Hour:D2}-{currentTime.Minute:D2}-{currentTime.Second:D2}.png"; + + string directory = Path.Combine(AppDataManager.BaseDirPath, "screenshots"); + + string path = Path.Combine(directory, filename); + + try + { + Directory.CreateDirectory(directory); + } + catch (Exception ex) + { + Logger.Error?.Print(LogClass.Application, $"Failed to create directory at path {directory}. Error : {ex.GetType().Name}", "Screenshot"); + + return; + } + + var colorType = e.IsBgra ? SKColorType.Bgra8888 : SKColorType.Rgba8888; + using SKBitmap bitmap = new(new SKImageInfo(e.Width, e.Height, colorType, SKAlphaType.Premul)); + + Marshal.Copy(e.Data, 0, bitmap.GetPixels(), e.Data.Length); + + using SKBitmap bitmapToSave = new(bitmap.Width, bitmap.Height); + using SKCanvas canvas = new(bitmapToSave); + + canvas.Clear(SKColors.Black); + + float scaleX = e.FlipX ? -1 : 1; + float scaleY = e.FlipY ? -1 : 1; + + var matrix = SKMatrix.CreateScale(scaleX, scaleY, bitmap.Width / 2f, bitmap.Height / 2f); + + canvas.SetMatrix(matrix); + canvas.DrawBitmap(bitmap, SKPoint.Empty); + + SaveBitmapAsPng(bitmapToSave, path); + + Logger.Notice.Print(LogClass.Application, $"Screenshot saved to {path}", "Screenshot"); + } + }); + } + else + { + Logger.Error?.Print(LogClass.Application, $"Screenshot is empty. Size : {e.Data.Length} bytes. Resolution : {e.Width}x{e.Height}", "Screenshot"); + } + } + + private static void SaveBitmapAsPng(SKBitmap bitmap, string path) + { + using var data = bitmap.Encode(SKEncodedImageFormat.Png, 100); + using var stream = File.OpenWrite(path); + + data.SaveTo(stream); + } + + public void Start() + { + if (OperatingSystem.IsWindows()) + { + _windowsMultimediaTimerResolution = new WindowsMultimediaTimerResolution(1); + } + + DisplaySleep.Prevent(); + + NpadManager.Initialize(Device, ConfigurationState.Instance.Hid.InputConfig, ConfigurationState.Instance.Hid.EnableKeyboard, ConfigurationState.Instance.Hid.EnableMouse); + TouchScreenManager.Initialize(Device); + + _viewModel.IsGameRunning = true; + + Dispatcher.UIThread.InvokeAsync(() => + { + _viewModel.Title = TitleHelper.ActiveApplicationTitle(Device.Processes.ActiveApplication, Program.Version); + }); + + _viewModel.SetUiProgressHandlers(Device); + + RendererHost.BoundsChanged += Window_BoundsChanged; + + _isActive = true; + + _renderingThread.Start(); + + _viewModel.Volume = ConfigurationState.Instance.System.AudioVolume.Value; + + MainLoop(); + + Exit(); + } + + private void UpdateIgnoreMissingServicesState(object sender, ReactiveEventArgs args) + { + if (Device != null) + { + Device.Configuration.IgnoreMissingServices = args.NewValue; + } + } + + private void UpdateAspectRatioState(object sender, ReactiveEventArgs args) + { + if (Device != null) + { + Device.Configuration.AspectRatio = args.NewValue; + } + } + + private void UpdateAntiAliasing(object sender, ReactiveEventArgs e) + { + _renderer?.Window?.SetAntiAliasing((Graphics.GAL.AntiAliasing)e.NewValue); + } + + private void UpdateDockedModeState(object sender, ReactiveEventArgs e) + { + Device?.System.ChangeDockedModeState(e.NewValue); + } + + private void UpdateAudioVolumeState(object sender, ReactiveEventArgs e) + { + Device?.SetVolume(e.NewValue); + + Dispatcher.UIThread.Post(() => + { + _viewModel.Volume = e.NewValue; + }); + } + + private void UpdateEnableInternetAccessState(object sender, ReactiveEventArgs e) + { + Device.Configuration.EnableInternetAccess = e.NewValue; + } + + private void UpdateLanInterfaceIdState(object sender, ReactiveEventArgs e) + { + Device.Configuration.MultiplayerLanInterfaceId = e.NewValue; + } + + private void UpdateMultiplayerModeState(object sender, ReactiveEventArgs e) + { + Device.Configuration.MultiplayerMode = e.NewValue; + } + + private void UpdateLdnPassphraseState(object sender, ReactiveEventArgs e) + { + Device.Configuration.MultiplayerLdnPassphrase = e.NewValue; + } + + private void UpdateLdnServerState(object sender, ReactiveEventArgs e) + { + Device.Configuration.MultiplayerLdnServer = e.NewValue; + } + + private void UpdateDisableP2pState(object sender, ReactiveEventArgs e) + { + Device.Configuration.MultiplayerDisableP2p = e.NewValue; + } + + public void Stop() + { + _isActive = false; + DiscordIntegrationModule.SwitchToMainState(); + } + + private void Exit() + { + (_keyboardInterface as AvaloniaKeyboard)?.Clear(); + + if (_isStopped) + { + return; + } + + _isStopped = true; + Stop(); + } + + public void DisposeContext() + { + Dispose(); + + _isActive = false; + + // NOTE: The render loop is allowed to stay alive until the renderer itself is disposed, as it may handle resource dispose. + // We only need to wait for all commands submitted during the main gpu loop to be processed. + _gpuDoneEvent.WaitOne(); + _gpuDoneEvent.Dispose(); + + DisplaySleep.Restore(); + + NpadManager.Dispose(); + TouchScreenManager.Dispose(); + Device.Dispose(); + + DisposeGpu(); + + AppExit?.Invoke(this, EventArgs.Empty); + } + + private void Dispose() + { + if (Device.Processes != null) + MainWindowViewModel.UpdateGameMetadata(Device.Processes.ActiveApplication.ProgramIdText); + + + ConfigurationState.Instance.System.IgnoreMissingServices.Event -= UpdateIgnoreMissingServicesState; + ConfigurationState.Instance.Graphics.AspectRatio.Event -= UpdateAspectRatioState; + ConfigurationState.Instance.System.EnableDockedMode.Event -= UpdateDockedModeState; + ConfigurationState.Instance.System.AudioVolume.Event -= UpdateAudioVolumeState; + ConfigurationState.Instance.Graphics.ScalingFilter.Event -= UpdateScalingFilter; + ConfigurationState.Instance.Graphics.ScalingFilterLevel.Event -= UpdateScalingFilterLevel; + ConfigurationState.Instance.Graphics.AntiAliasing.Event -= UpdateAntiAliasing; + ConfigurationState.Instance.Graphics.EnableColorSpacePassthrough.Event -= UpdateColorSpacePassthrough; + + _topLevel.PointerMoved -= TopLevel_PointerEnteredOrMoved; + _topLevel.PointerEntered -= TopLevel_PointerEnteredOrMoved; + _topLevel.PointerExited -= TopLevel_PointerExited; + + _gpuCancellationTokenSource.Cancel(); + _gpuCancellationTokenSource.Dispose(); + + _chrono.Stop(); + } + + public void DisposeGpu() + { + if (OperatingSystem.IsWindows()) + { + _windowsMultimediaTimerResolution?.Dispose(); + _windowsMultimediaTimerResolution = null; + } + + if (RendererHost.EmbeddedWindow is EmbeddedWindowOpenGL openGlWindow) + { + // Try to bind the OpenGL context before calling the shutdown event. + openGlWindow.MakeCurrent(false, false); + + Device.DisposeGpu(); + + // Unbind context and destroy everything. + openGlWindow.MakeCurrent(true, false); + } + else + { + Device.DisposeGpu(); + } + } + + private void HideCursorState_Changed(object sender, ReactiveEventArgs state) + { + if (state.NewValue == HideCursorMode.OnIdle) + { + _lastCursorMoveTime = Stopwatch.GetTimestamp(); + } + + _cursorState = CursorStates.ForceChangeCursor; + } + + public async Task LoadGuestApplication() + { + InitializeSwitchInstance(); + MainWindow.UpdateGraphicsConfig(); + + SystemVersion firmwareVersion = ContentManager.GetCurrentFirmwareVersion(); + + if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime) + { + if (!SetupValidator.CanStartApplication(ContentManager, ApplicationPath, out UserError userError)) + { + if (SetupValidator.CanFixStartApplication(ContentManager, ApplicationPath, userError, out firmwareVersion)) + { + if (userError is UserError.NoFirmware) + { + UserResult result = await ContentDialogHelper.CreateConfirmationDialog( + LocaleManager.Instance[LocaleKeys.DialogFirmwareNoFirmwareInstalledMessage], + LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogFirmwareInstallEmbeddedMessage, firmwareVersion.VersionString), + LocaleManager.Instance[LocaleKeys.InputDialogYes], + LocaleManager.Instance[LocaleKeys.InputDialogNo], + string.Empty); + + if (result != UserResult.Yes) + { + await UserErrorDialog.ShowUserErrorDialog(userError); + Device.Dispose(); + + return false; + } + } + + if (!SetupValidator.TryFixStartApplication(ContentManager, ApplicationPath, userError, out _)) + { + await UserErrorDialog.ShowUserErrorDialog(userError); + Device.Dispose(); + + return false; + } + + // Tell the user that we installed a firmware for them. + if (userError is UserError.NoFirmware) + { + firmwareVersion = ContentManager.GetCurrentFirmwareVersion(); + + _viewModel.RefreshFirmwareStatus(); + + await ContentDialogHelper.CreateInfoDialog( + LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogFirmwareInstalledMessage, firmwareVersion.VersionString), + LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogFirmwareInstallEmbeddedSuccessMessage, firmwareVersion.VersionString), + LocaleManager.Instance[LocaleKeys.InputDialogOk], + string.Empty, + LocaleManager.Instance[LocaleKeys.RyujinxInfo]); + } + } + else + { + await UserErrorDialog.ShowUserErrorDialog(userError); + Device.Dispose(); + + return false; + } + } + } + + Logger.Notice.Print(LogClass.Application, $"Using Firmware Version: {firmwareVersion?.VersionString}"); + + if (_isFirmwareTitle) + { + Logger.Info?.Print(LogClass.Application, "Loading as Firmware Title (NCA)."); + + if (!Device.LoadNca(ApplicationPath)) + { + Device.Dispose(); + + return false; + } + } + else if (Directory.Exists(ApplicationPath)) + { + string[] romFsFiles = Directory.GetFiles(ApplicationPath, "*.istorage"); + + if (romFsFiles.Length == 0) + { + romFsFiles = Directory.GetFiles(ApplicationPath, "*.romfs"); + } + + if (romFsFiles.Length > 0) + { + Logger.Info?.Print(LogClass.Application, "Loading as cart with RomFS."); + + if (!Device.LoadCart(ApplicationPath, romFsFiles[0])) + { + Device.Dispose(); + + return false; + } + } + else + { + Logger.Info?.Print(LogClass.Application, "Loading as cart WITHOUT RomFS."); + + if (!Device.LoadCart(ApplicationPath)) + { + Device.Dispose(); + + return false; + } + } + } + else if (File.Exists(ApplicationPath)) + { + switch (Path.GetExtension(ApplicationPath).ToLowerInvariant()) + { + case ".xci": + { + Logger.Info?.Print(LogClass.Application, "Loading as XCI."); + + if (!Device.LoadXci(ApplicationPath, ApplicationId)) + { + Device.Dispose(); + + return false; + } + + break; + } + case ".nca": + { + Logger.Info?.Print(LogClass.Application, "Loading as NCA."); + + if (!Device.LoadNca(ApplicationPath)) + { + Device.Dispose(); + + return false; + } + + break; + } + case ".nsp": + case ".pfs0": + { + Logger.Info?.Print(LogClass.Application, "Loading as NSP."); + + if (!Device.LoadNsp(ApplicationPath, ApplicationId)) + { + Device.Dispose(); + + return false; + } + + break; + } + default: + { + Logger.Info?.Print(LogClass.Application, "Loading as homebrew."); + + try + { + if (!Device.LoadProgram(ApplicationPath)) + { + Device.Dispose(); + + return false; + } + } + catch (ArgumentOutOfRangeException) + { + Logger.Error?.Print(LogClass.Application, "The specified file is not supported by Ryujinx."); + + Device.Dispose(); + + return false; + } + + break; + } + } + } + else + { + Logger.Warning?.Print(LogClass.Application, "Please specify a valid XCI/NCA/NSP/PFS0/NRO file."); + + Device.Dispose(); + + return false; + } + + ApplicationMetadata appMeta = ApplicationLibrary.LoadAndSaveMetaData(Device.Processes.ActiveApplication.ProgramIdText, + appMetadata => appMetadata.UpdatePreGame() + ); + + DiscordIntegrationModule.SwitchToPlayingState(appMeta, Device.Processes.ActiveApplication); + + return true; + } + + internal void Resume() + { + Device?.System.TogglePauseEmulation(false); + + _viewModel.IsPaused = false; + _viewModel.Title = TitleHelper.ActiveApplicationTitle(Device?.Processes.ActiveApplication, Program.Version); + Logger.Info?.Print(LogClass.Emulation, "Emulation was resumed"); + } + + internal void Pause() + { + Device?.System.TogglePauseEmulation(true); + + _viewModel.IsPaused = true; + _viewModel.Title = TitleHelper.ActiveApplicationTitle(Device?.Processes.ActiveApplication, Program.Version, LocaleManager.Instance[LocaleKeys.Paused]); + Logger.Info?.Print(LogClass.Emulation, "Emulation was paused"); + } + + private void InitializeSwitchInstance() + { + // Initialize KeySet. + VirtualFileSystem.ReloadKeySet(); + + // Initialize Renderer. + IRenderer renderer = ConfigurationState.Instance.Graphics.GraphicsBackend.Value == GraphicsBackend.OpenGl + ? new OpenGLRenderer() + : VulkanRenderer.Create( + ConfigurationState.Instance.Graphics.PreferredGpu, + (RendererHost.EmbeddedWindow as EmbeddedWindowVulkan)!.CreateSurface, + VulkanHelper.GetRequiredInstanceExtensions); + + BackendThreading threadingMode = ConfigurationState.Instance.Graphics.BackendThreading; + + var isGALThreaded = threadingMode == BackendThreading.On || (threadingMode == BackendThreading.Auto && renderer.PreferThreading); + if (isGALThreaded) + { + renderer = new ThreadedRenderer(renderer); + } + + Logger.Info?.PrintMsg(LogClass.Gpu, $"Backend Threading ({threadingMode}): {isGALThreaded}"); + + // Initialize Configuration. + var memoryConfiguration = ConfigurationState.Instance.System.DramSize.Value; + + Device = new HLE.Switch(new HLEConfiguration( + VirtualFileSystem, + _viewModel.LibHacHorizonManager, + ContentManager, + _accountManager, + _userChannelPersistence, + renderer, + InitializeAudio(), + memoryConfiguration, + _viewModel.UiHandler, + (SystemLanguage)ConfigurationState.Instance.System.Language.Value, + (RegionCode)ConfigurationState.Instance.System.Region.Value, + ConfigurationState.Instance.Graphics.VSyncMode, + ConfigurationState.Instance.System.EnableDockedMode, + ConfigurationState.Instance.System.EnablePtc, + ConfigurationState.Instance.System.EnableInternetAccess, + ConfigurationState.Instance.System.EnableFsIntegrityChecks ? IntegrityCheckLevel.ErrorOnInvalid : IntegrityCheckLevel.None, + ConfigurationState.Instance.System.FsGlobalAccessLogMode, + ConfigurationState.Instance.System.SystemTimeOffset, + ConfigurationState.Instance.System.TimeZone, + ConfigurationState.Instance.System.MemoryManagerMode, + ConfigurationState.Instance.System.IgnoreMissingServices, + ConfigurationState.Instance.Graphics.AspectRatio, + ConfigurationState.Instance.System.AudioVolume, + ConfigurationState.Instance.System.UseHypervisor, + ConfigurationState.Instance.Multiplayer.LanInterfaceId.Value, + ConfigurationState.Instance.Multiplayer.Mode, + ConfigurationState.Instance.Multiplayer.DisableP2p, + ConfigurationState.Instance.Multiplayer.LdnPassphrase, + ConfigurationState.Instance.Multiplayer.LdnServer, + ConfigurationState.Instance.Graphics.CustomVSyncInterval.Value)); + } + + private static IHardwareDeviceDriver InitializeAudio() + { + var availableBackends = new List + { + AudioBackend.SDL2, + AudioBackend.SoundIo, + AudioBackend.OpenAl, + AudioBackend.Dummy, + }; + + AudioBackend preferredBackend = ConfigurationState.Instance.System.AudioBackend.Value; + + for (int i = 0; i < availableBackends.Count; i++) + { + if (availableBackends[i] == preferredBackend) + { + availableBackends.RemoveAt(i); + availableBackends.Insert(0, preferredBackend); + break; + } + } + + static IHardwareDeviceDriver InitializeAudioBackend(AudioBackend backend, AudioBackend nextBackend) where T : IHardwareDeviceDriver, new() + { + if (T.IsSupported) + { + return new T(); + } + + Logger.Warning?.Print(LogClass.Audio, $"{backend} is not supported, falling back to {nextBackend}."); + + return null; + } + + IHardwareDeviceDriver deviceDriver = null; + + for (int i = 0; i < availableBackends.Count; i++) + { + AudioBackend currentBackend = availableBackends[i]; + AudioBackend nextBackend = i + 1 < availableBackends.Count ? availableBackends[i + 1] : AudioBackend.Dummy; + + deviceDriver = currentBackend switch + { + AudioBackend.SDL2 => InitializeAudioBackend(AudioBackend.SDL2, nextBackend), + AudioBackend.SoundIo => InitializeAudioBackend(AudioBackend.SoundIo, nextBackend), + AudioBackend.OpenAl => InitializeAudioBackend(AudioBackend.OpenAl, nextBackend), + _ => new DummyHardwareDeviceDriver(), + }; + + if (deviceDriver != null) + { + ConfigurationState.Instance.System.AudioBackend.Value = currentBackend; + break; + } + } + + MainWindowViewModel.SaveConfig(); + + return deviceDriver; + } + + private void Window_BoundsChanged(object sender, Size e) + { + Width = (int)e.Width; + Height = (int)e.Height; + + SetRendererWindowSize(e); + } + + private void MainLoop() + { + while (UpdateFrame()) + { + // Polling becomes expensive if it's not slept. + Thread.Sleep(1); + } + } + + private void RenderLoop() + { + Dispatcher.UIThread.InvokeAsync(() => + { + if (_viewModel.StartGamesInFullscreen) + { + _viewModel.WindowState = WindowState.FullScreen; + } + + if (_viewModel.WindowState is WindowState.FullScreen) + { + _viewModel.ShowMenuAndStatusBar = false; + } + }); + + _renderer = Device.Gpu.Renderer is ThreadedRenderer tr ? tr.BaseRenderer : Device.Gpu.Renderer; + + _renderer.ScreenCaptured += Renderer_ScreenCaptured; + + (RendererHost.EmbeddedWindow as EmbeddedWindowOpenGL)?.InitializeBackgroundContext(_renderer); + + Device.Gpu.Renderer.Initialize(_glLogLevel); + + _renderer?.Window?.SetAntiAliasing((Graphics.GAL.AntiAliasing)ConfigurationState.Instance.Graphics.AntiAliasing.Value); + _renderer?.Window?.SetScalingFilter((Graphics.GAL.ScalingFilter)ConfigurationState.Instance.Graphics.ScalingFilter.Value); + _renderer?.Window?.SetScalingFilterLevel(ConfigurationState.Instance.Graphics.ScalingFilterLevel.Value); + _renderer?.Window?.SetColorSpacePassthrough(ConfigurationState.Instance.Graphics.EnableColorSpacePassthrough.Value); + + Width = (int)RendererHost.Bounds.Width; + Height = (int)RendererHost.Bounds.Height; + + _renderer.Window.SetSize((int)(Width * _topLevel.RenderScaling), (int)(Height * _topLevel.RenderScaling)); + + _chrono.Start(); + + Device.Gpu.Renderer.RunLoop(() => + { + Device.Gpu.SetGpuThread(); + Device.Gpu.InitializeShaderCache(_gpuCancellationTokenSource.Token); + + _renderer.Window.ChangeVSyncMode((Ryujinx.Graphics.GAL.VSyncMode)Device.VSyncMode); + + while (_isActive) + { + _ticks += _chrono.ElapsedTicks; + + _chrono.Restart(); + + if (Device.WaitFifo()) + { + Device.Statistics.RecordFifoStart(); + Device.ProcessFrame(); + Device.Statistics.RecordFifoEnd(); + } + + while (Device.ConsumeFrameAvailable()) + { + if (!_renderingStarted) + { + _renderingStarted = true; + _viewModel.SwitchToRenderer(false); + InitStatus(); + } + + Device.PresentFrame(() => (RendererHost.EmbeddedWindow as EmbeddedWindowOpenGL)?.SwapBuffers()); + } + + if (_ticks >= _ticksPerFrame) + { + UpdateStatus(); + } + } + + // Make sure all commands in the run loop are fully executed before leaving the loop. + if (Device.Gpu.Renderer is ThreadedRenderer threaded) + { + threaded.FlushThreadedCommands(); + } + + _gpuDoneEvent.Set(); + }); + + (RendererHost.EmbeddedWindow as EmbeddedWindowOpenGL)?.MakeCurrent(true); + } + + public void InitStatus() + { + _viewModel.BackendText = ConfigurationState.Instance.Graphics.GraphicsBackend.Value switch + { + GraphicsBackend.Vulkan => "Vulkan", + GraphicsBackend.OpenGl => "OpenGL", + _ => throw new NotImplementedException() + }; + + _viewModel.GpuNameText = $"GPU: {_renderer.GetHardwareInfo().GpuDriver}"; + } + + public void UpdateStatus() + { + // Run a status update only when a frame is to be drawn. This prevents from updating the ui and wasting a render when no frame is queued. + string dockedMode = ConfigurationState.Instance.System.EnableDockedMode ? LocaleManager.Instance[LocaleKeys.Docked] : LocaleManager.Instance[LocaleKeys.Handheld]; + string vSyncMode = Device.VSyncMode.ToString(); + + UpdateShaderCount(); + + if (GraphicsConfig.ResScale != 1) + { + dockedMode += $" ({GraphicsConfig.ResScale}x)"; + } + + StatusUpdatedEvent?.Invoke(this, new StatusUpdatedEventArgs( + vSyncMode, + LocaleManager.Instance[LocaleKeys.VolumeShort] + $": {(int)(Device.GetVolume() * 100)}%", + dockedMode, + ConfigurationState.Instance.Graphics.AspectRatio.Value.ToText(), + LocaleManager.Instance[LocaleKeys.Game] + $": {Device.Statistics.GetGameFrameRate():00.00} FPS ({Device.Statistics.GetGameFrameTime():00.00} ms)", + $"FIFO: {Device.Statistics.GetFifoPercent():00.00} %", + _displayCount)); + } + + public async Task ShowExitPrompt() + { + bool shouldExit = !ConfigurationState.Instance.ShowConfirmExit; + if (!shouldExit) + { + if (_dialogShown) + { + return; + } + + _dialogShown = true; + + shouldExit = await ContentDialogHelper.CreateStopEmulationDialog(); + + _dialogShown = false; + } + + if (shouldExit) + { + Stop(); + } + } + + private void UpdateShaderCount() + { + // If there is a mismatch between total program compile and previous count + // this means new shaders have been compiled and should be displayed. + if (_renderer.ProgramCount != _previousCount) + { + _displayCount += _renderer.ProgramCount - _previousCount; + _lastShaderReset = DateTime.Now; + _previousCount = _renderer.ProgramCount; + } + // Check if 5s has passed since any new shaders were compiled. + // If yes, reset the counter. + else if (_lastShaderReset.AddSeconds(5) <= DateTime.Now) + { + _displayCount = 0; + } + } + + private bool UpdateFrame() + { + if (!_isActive) + { + return false; + } + + NpadManager.Update(ConfigurationState.Instance.Graphics.AspectRatio.Value.ToFloat()); + + if (_viewModel.IsActive) + { + bool isCursorVisible = true; + + if (_isCursorInRenderer && !_viewModel.ShowLoadProgress) + { + if (ConfigurationState.Instance.Hid.EnableMouse.Value) + { + isCursorVisible = ConfigurationState.Instance.HideCursor.Value == HideCursorMode.Never; + } + else + { + isCursorVisible = ConfigurationState.Instance.HideCursor.Value == HideCursorMode.Never || + (ConfigurationState.Instance.HideCursor.Value == HideCursorMode.OnIdle && + Stopwatch.GetTimestamp() - _lastCursorMoveTime < CursorHideIdleTime * Stopwatch.Frequency); + } + } + + if (_cursorState != (isCursorVisible ? CursorStates.CursorIsVisible : CursorStates.CursorIsHidden)) + { + if (isCursorVisible) + { + ShowCursor(); + } + else + { + HideCursor(); + } + } + + Dispatcher.UIThread.Post(() => + { + if (_keyboardInterface.GetKeyboardStateSnapshot().IsPressed(Key.Delete) && _viewModel.WindowState is not WindowState.FullScreen) + { + Device.Processes.ActiveApplication.DiskCacheLoadState?.Cancel(); + } + }); + + KeyboardHotkeyState currentHotkeyState = GetHotkeyState(); + + if (currentHotkeyState != _prevHotkeyState) + { + switch (currentHotkeyState) + { + case KeyboardHotkeyState.ToggleVSyncMode: + VSyncModeToggle(); + break; + case KeyboardHotkeyState.CustomVSyncIntervalDecrement: + Device.DecrementCustomVSyncInterval(); + _viewModel.CustomVSyncInterval -= 1; + break; + case KeyboardHotkeyState.CustomVSyncIntervalIncrement: + Device.IncrementCustomVSyncInterval(); + _viewModel.CustomVSyncInterval += 1; + break; + case KeyboardHotkeyState.Screenshot: + ScreenshotRequested = true; + break; + case KeyboardHotkeyState.ShowUI: + _viewModel.ShowMenuAndStatusBar = !_viewModel.ShowMenuAndStatusBar; + break; + case KeyboardHotkeyState.Pause: + if (_viewModel.IsPaused) + { + Resume(); + } + else + { + Pause(); + } + break; + case KeyboardHotkeyState.ToggleMute: + if (Device.IsAudioMuted()) + { + Device.SetVolume(_viewModel.VolumeBeforeMute); + } + else + { + _viewModel.VolumeBeforeMute = Device.GetVolume(); + Device.SetVolume(0); + } + + _viewModel.Volume = Device.GetVolume(); + break; + case KeyboardHotkeyState.ResScaleUp: + GraphicsConfig.ResScale = GraphicsConfig.ResScale % MaxResolutionScale + 1; + break; + case KeyboardHotkeyState.ResScaleDown: + GraphicsConfig.ResScale = + (MaxResolutionScale + GraphicsConfig.ResScale - 2) % MaxResolutionScale + 1; + break; + case KeyboardHotkeyState.VolumeUp: + _newVolume = MathF.Round((Device.GetVolume() + VolumeDelta), 2); + Device.SetVolume(_newVolume); + + _viewModel.Volume = Device.GetVolume(); + break; + case KeyboardHotkeyState.VolumeDown: + _newVolume = MathF.Round((Device.GetVolume() - VolumeDelta), 2); + Device.SetVolume(_newVolume); + + _viewModel.Volume = Device.GetVolume(); + break; + case KeyboardHotkeyState.None: + (_keyboardInterface as AvaloniaKeyboard).Clear(); + break; + } + } + + _prevHotkeyState = currentHotkeyState; + + if (ScreenshotRequested) + { + ScreenshotRequested = false; + _renderer.Screenshot(); + } + } + + // Touchscreen. + bool hasTouch = false; + + if (_viewModel.IsActive && !ConfigurationState.Instance.Hid.EnableMouse.Value) + { + hasTouch = TouchScreenManager.Update(true, (_inputManager.MouseDriver as AvaloniaMouseDriver).IsButtonPressed(MouseButton.Button1), ConfigurationState.Instance.Graphics.AspectRatio.Value.ToFloat()); + } + + if (!hasTouch) + { + Device.Hid.Touchscreen.Update(); + } + + Device.Hid.DebugPad.Update(); + + return true; + } + + private KeyboardHotkeyState GetHotkeyState() + { + KeyboardHotkeyState state = KeyboardHotkeyState.None; + + if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.ToggleVSyncMode)) + { + state = KeyboardHotkeyState.ToggleVSyncMode; + } + else if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.Screenshot)) + { + state = KeyboardHotkeyState.Screenshot; + } + else if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.ShowUI)) + { + state = KeyboardHotkeyState.ShowUI; + } + else if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.Pause)) + { + state = KeyboardHotkeyState.Pause; + } + else if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.ToggleMute)) + { + state = KeyboardHotkeyState.ToggleMute; + } + else if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.ResScaleUp)) + { + state = KeyboardHotkeyState.ResScaleUp; + } + else if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.ResScaleDown)) + { + state = KeyboardHotkeyState.ResScaleDown; + } + else if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.VolumeUp)) + { + state = KeyboardHotkeyState.VolumeUp; + } + else if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.VolumeDown)) + { + state = KeyboardHotkeyState.VolumeDown; + } + else if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.CustomVSyncIntervalIncrement)) + { + state = KeyboardHotkeyState.CustomVSyncIntervalIncrement; + } + else if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.CustomVSyncIntervalDecrement)) + { + state = KeyboardHotkeyState.CustomVSyncIntervalDecrement; + } + + return state; + } + } +} diff --git a/src/Ryujinx/Assets/Fonts/SegoeFluentIcons.ttf b/src/Ryujinx/Assets/Fonts/SegoeFluentIcons.ttf new file mode 100644 index 000000000..8f05a4bbc Binary files /dev/null and b/src/Ryujinx/Assets/Fonts/SegoeFluentIcons.ttf differ diff --git a/src/Ryujinx/Assets/Icons/Controller_JoyConLeft.svg b/src/Ryujinx/Assets/Icons/Controller_JoyConLeft.svg new file mode 100644 index 000000000..cf78cf120 --- /dev/null +++ b/src/Ryujinx/Assets/Icons/Controller_JoyConLeft.svg @@ -0,0 +1,155 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Ryujinx/Assets/Icons/Controller_JoyConPair.svg b/src/Ryujinx/Assets/Icons/Controller_JoyConPair.svg new file mode 100644 index 000000000..8097762df --- /dev/null +++ b/src/Ryujinx/Assets/Icons/Controller_JoyConPair.svg @@ -0,0 +1,341 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Ryujinx/Assets/Icons/Controller_JoyConRight.svg b/src/Ryujinx/Assets/Icons/Controller_JoyConRight.svg new file mode 100644 index 000000000..adb6e1e18 --- /dev/null +++ b/src/Ryujinx/Assets/Icons/Controller_JoyConRight.svg @@ -0,0 +1,185 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Ryujinx/Assets/Icons/Controller_ProCon.svg b/src/Ryujinx/Assets/Icons/Controller_ProCon.svg new file mode 100644 index 000000000..53eef82c2 --- /dev/null +++ b/src/Ryujinx/Assets/Icons/Controller_ProCon.svg @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Ryujinx/Assets/Locales/ar_SA.json b/src/Ryujinx/Assets/Locales/ar_SA.json new file mode 100644 index 000000000..62992ff34 --- /dev/null +++ b/src/Ryujinx/Assets/Locales/ar_SA.json @@ -0,0 +1,868 @@ +{ + "Language": "اَلْعَرَبِيَّةُ", + "MenuBarFileOpenApplet": "فتح التطبيق المصغر", + "MenuBarFileOpenAppletOpenMiiApplet": "Mii Edit Applet", + "MenuBarFileOpenAppletOpenMiiAppletToolTip": "‫افتح تطبيق تحرير Mii في الوضع المستقل", + "SettingsTabInputDirectMouseAccess": "الوصول المباشر للفأرة", + "SettingsTabSystemMemoryManagerMode": "وضع إدارة الذاكرة:", + "SettingsTabSystemMemoryManagerModeSoftware": "البرنامج", + "SettingsTabSystemMemoryManagerModeHost": "المُضيف (سريع)", + "SettingsTabSystemMemoryManagerModeHostUnchecked": "المضيف (غير مفحوص) (أسرع، غير آمن)", + "SettingsTabSystemUseHypervisor": "استخدم مراقب الأجهزة الافتراضية", + "MenuBarFile": "_ملف", + "MenuBarFileOpenFromFile": "_تحميل تطبيق من ملف", + "MenuBarFileOpenFromFileError": "No applications found in selected file.", + "MenuBarFileOpenUnpacked": "تحميل لُعْبَة غير محزومة", + "MenuBarFileLoadDlcFromFolder": "Load DLC From Folder", + "MenuBarFileLoadTitleUpdatesFromFolder": "Load Title Updates From Folder", + "MenuBarFileOpenEmuFolder": "‫فتح مجلد Ryujinx", + "MenuBarFileOpenLogsFolder": "فتح مجلد السجلات", + "MenuBarFileExit": "_خروج", + "MenuBarOptions": "_خيارات", + "MenuBarOptionsToggleFullscreen": "التبديل إلى وضع ملء الشاشة", + "MenuBarOptionsStartGamesInFullscreen": "ابدأ الألعاب في وضع ملء الشاشة", + "MenuBarOptionsStopEmulation": "إيقاف المحاكاة", + "MenuBarOptionsSettings": "_الإعدادات", + "MenuBarOptionsManageUserProfiles": "_إدارة الملفات الشخصية للمستخدم", + "MenuBarActions": "_الإجراءات", + "MenuBarOptionsSimulateWakeUpMessage": "محاكاة رسالة الاستيقاظ", + "MenuBarActionsScanAmiibo": "‫فحص Amiibo", + "MenuBarTools": "_الأدوات", + "MenuBarToolsInstallFirmware": "تثبيت البرنامج الثابت", + "MenuBarFileToolsInstallFirmwareFromFile": "تثبيت برنامج ثابت من XCI أو ZIP", + "MenuBarFileToolsInstallFirmwareFromDirectory": "تثبيت برنامج ثابت من مجلد", + "MenuBarToolsManageFileTypes": "إدارة أنواع الملفات", + "MenuBarToolsInstallFileTypes": "تثبيت أنواع الملفات", + "MenuBarToolsUninstallFileTypes": "إزالة أنواع الملفات", + "MenuBarToolsXCITrimmer": "Trim XCI Files", + "MenuBarView": "_عرض", + "MenuBarViewWindow": "حجم النافذة", + "MenuBarViewWindow720": "720p", + "MenuBarViewWindow1080": "1080p", + "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": "إدارة المحتوي الإضافي", + "GameListContextMenuManageDlcToolTip": "يفتح نافذة إدارة المحتوي الإضافي", + "GameListContextMenuCacheManagement": "إدارة ذاكرة التخزين المؤقت", + "GameListContextMenuCacheManagementPurgePptc": "قائمة انتظار إعادة بناء الـ‫PPTC", + "GameListContextMenuCacheManagementPurgePptcToolTip": "تنشيط ‫PPTC لإعادة البناء في وقت الإقلاع عند بدء تشغيل اللعبة التالي", + "GameListContextMenuCacheManagementPurgeShaderCache": "تنظيف ذاكرة مرشحات الفيديو المؤقتة", + "GameListContextMenuCacheManagementPurgeShaderCacheToolTip": "يحذف ذاكرة مرشحات الفيديو المؤقتة الخاصة بالتطبيق", + "GameListContextMenuCacheManagementOpenPptcDirectory": "‫فتح مجلد PPTC", + "GameListContextMenuCacheManagementOpenPptcDirectoryToolTip": "‫‫يفتح المجلد الذي يحتوي على ذاكرة التخزين المؤقت للترجمة المستمرة (PPTC) للتطبيق", + "GameListContextMenuCacheManagementOpenShaderCacheDirectory": "فتح مجلد الذاكرة المؤقتة لمرشحات الفيديو ", + "GameListContextMenuCacheManagementOpenShaderCacheDirectoryToolTip": "يفتح المجلد الذي يحتوي على ذاكرة المظللات المؤقتة للتطبيق", + "GameListContextMenuExtractData": "استخراج البيانات", + "GameListContextMenuExtractDataExeFS": "ExeFS", + "GameListContextMenuExtractDataExeFSToolTip": "‫‫ استخراج قسم نظام الملفات القابل للتنفيذ (ExeFS) من الإعدادات الحالية للتطبيقات (يتضمن التحديثات)", + "GameListContextMenuExtractDataRomFS": "RomFS", + "GameListContextMenuExtractDataRomFSToolTip": "استخراج قسم RomFS من الإعدادات الحالية للتطبيقات (يتضمن التحديثات)", + "GameListContextMenuExtractDataLogo": "شعار", + "GameListContextMenuExtractDataLogoToolTip": "استخراج قسم الشعار من الإعدادات الحالية للتطبيقات (يتضمن التحديثات)", + "GameListContextMenuCreateShortcut": "إنشاء اختصار للتطبيق", + "GameListContextMenuCreateShortcutToolTip": "أنشئ اختصار سطح مكتب لتشغيل التطبيق المحدد", + "GameListContextMenuCreateShortcutToolTipMacOS": "أنشئ اختصار يُشغل التطبيق المحدد في مجلد تطبيقات ‫macOS", + "GameListContextMenuOpenModsDirectory": "‫فتح مجلد التعديلات (Mods)", + "GameListContextMenuOpenModsDirectoryToolTip": "يفتح المجلد الذي يحتوي على تعديلات‫(mods) التطبيق", + "GameListContextMenuOpenSdModsDirectory": "فتح مجلد تعديلات‫(mods) أتموسفير", + "GameListContextMenuOpenSdModsDirectoryToolTip": "يفتح مجلد أتموسفير لبطاقة SD البديلة الذي يحتوي على تعديلات التطبيق. مفيد للتعديلات التي تم تعبئتها للأجهزة الحقيقية.", + "GameListContextMenuTrimXCI": "Check and Trim XCI File", + "GameListContextMenuTrimXCIToolTip": "Check and Trim XCI File to Save Disk Space", + "StatusBarGamesLoaded": "{0}/{1} لعبة تم تحميلها", + "StatusBarSystemVersion": "إصدار النظام: {0}", + "StatusBarXCIFileTrimming": "Trimming XCI File '{0}'", + "LinuxVmMaxMapCountDialogTitle": "الحد الأدنى لتعيينات الذاكرة المكتشفة", + "LinuxVmMaxMapCountDialogTextPrimary": "هل ترغب في زيادة قيمة vm.max_map_count إلى {0}", + "LinuxVmMaxMapCountDialogTextSecondary": "قد تحاول بعض الألعاب إنشاء المزيد من تعيينات الذاكرة أكثر مما هو مسموح به حاليا. سيغلق ريوجينكس بمجرد تجاوز هذا الحد.", + "LinuxVmMaxMapCountDialogButtonUntilRestart": "نعم، حتى إعادة التشغيل التالية", + "LinuxVmMaxMapCountDialogButtonPersistent": "نعم، دائمًا", + "LinuxVmMaxMapCountWarningTextPrimary": "الحد الأقصى لمقدار تعيينات الذاكرة أقل من الموصى به.", + "LinuxVmMaxMapCountWarningTextSecondary": "القيمة الحالية لـ vm.max_map_count ({0}) أقل من {1}. قد تحاول بعض الألعاب إنشاء المزيد من تعيينات الذاكرة أكثر مما هو مسموح به حاليا. سيغلق ريوجينكس بمجرد تجاوز هذا الحد.\n\nقد ترغب إما في زيادة الحد يدويا أو تثبيت pkexec، مما يسمح لـ ريوجينكس بالمساعدة في ذلك.", + "Settings": "إعدادات", + "SettingsTabGeneral": "واجهة المستخدم", + "SettingsTabGeneralGeneral": "عام", + "SettingsTabGeneralEnableDiscordRichPresence": "تمكين وجود ديسكورد الغني", + "SettingsTabGeneralCheckUpdatesOnLaunch": "التحقق من وجود تحديثات عند التشغيل", + "SettingsTabGeneralShowConfirmExitDialog": "إظهار مربع حوار \"تأكيد الخروج\"", + "SettingsTabGeneralRememberWindowState": "تذكر حجم/موضع النافذة", + "SettingsTabGeneralShowTitleBar": "Show Title Bar (Requires restart)", + "SettingsTabGeneralHideCursor": "إخفاء المؤشر:", + "SettingsTabGeneralHideCursorNever": "مطلقا", + "SettingsTabGeneralHideCursorOnIdle": "عند الخمول", + "SettingsTabGeneralHideCursorAlways": "دائما", + "SettingsTabGeneralGameDirectories": "مجلدات الألعاب", + "SettingsTabGeneralAutoloadDirectories": "Autoload DLC/Updates Directories", + "SettingsTabGeneralAutoloadNote": "DLC and Updates which refer to missing files will be unloaded automatically", + "SettingsTabGeneralAdd": "إضافة", + "SettingsTabGeneralRemove": "إزالة", + "SettingsTabSystem": "النظام", + "SettingsTabSystemCore": "النواة", + "SettingsTabSystemSystemRegion": "منطقة النظام:", + "SettingsTabSystemSystemRegionJapan": "اليابان", + "SettingsTabSystemSystemRegionUSA": "الولايات المتحدة الأمريكية", + "SettingsTabSystemSystemRegionEurope": "أوروبا", + "SettingsTabSystemSystemRegionAustralia": "أستراليا", + "SettingsTabSystemSystemRegionChina": "الصين", + "SettingsTabSystemSystemRegionKorea": "كوريا", + "SettingsTabSystemSystemRegionTaiwan": "تايوان", + "SettingsTabSystemSystemLanguage": "لغة النظام:", + "SettingsTabSystemSystemLanguageJapanese": "اليابانية", + "SettingsTabSystemSystemLanguageAmericanEnglish": "الإنجليزية الأمريكية", + "SettingsTabSystemSystemLanguageFrench": "الفرنسية", + "SettingsTabSystemSystemLanguageGerman": "الألمانية", + "SettingsTabSystemSystemLanguageItalian": "الإيطالية", + "SettingsTabSystemSystemLanguageSpanish": "الإسبانية", + "SettingsTabSystemSystemLanguageChinese": "الصينية", + "SettingsTabSystemSystemLanguageKorean": "الكورية", + "SettingsTabSystemSystemLanguageDutch": "الهولندية", + "SettingsTabSystemSystemLanguagePortuguese": "البرتغالية", + "SettingsTabSystemSystemLanguageRussian": "الروسية", + "SettingsTabSystemSystemLanguageTaiwanese": "التايوانية", + "SettingsTabSystemSystemLanguageBritishEnglish": "الإنجليزية البريطانية", + "SettingsTabSystemSystemLanguageCanadianFrench": "الفرنسية الكندية", + "SettingsTabSystemSystemLanguageLatinAmericanSpanish": "إسبانية أمريكا اللاتينية", + "SettingsTabSystemSystemLanguageSimplifiedChinese": "الصينية المبسطة", + "SettingsTabSystemSystemLanguageTraditionalChinese": "الصينية التقليدية", + "SettingsTabSystemSystemTimeZone": "النطاق الزمني للنظام:", + "SettingsTabSystemSystemTime": "توقيت النظام:", + "SettingsTabSystemEnableVsync": "VSync", + "SettingsTabSystemEnablePptc": "PPTC (ذاكرة التخزين المؤقت للترجمة المستمرة)", + "SettingsTabSystemEnableLowPowerPptc": "Low-power PPTC", + "SettingsTabSystemEnableFsIntegrityChecks": "التحقق من سلامة نظام الملفات", + "SettingsTabSystemAudioBackend": "خلفية الصوت:", + "SettingsTabSystemAudioBackendDummy": "زائف", + "SettingsTabSystemAudioBackendOpenAL": "OpenAL", + "SettingsTabSystemAudioBackendSoundIO": "SoundIO", + "SettingsTabSystemAudioBackendSDL2": "SDL2", + "SettingsTabSystemHacks": "هاكات", + "SettingsTabSystemHacksNote": "قد يتسبب في عدم الاستقرار", + "SettingsTabSystemDramSize": "استخدام تخطيط الذاكرة البديل (المطورين)", + "SettingsTabSystemDramSize4GiB": "4GiB", + "SettingsTabSystemDramSize6GiB": "6GiB", + "SettingsTabSystemDramSize8GiB": "8GiB", + "SettingsTabSystemDramSize12GiB": "12GiB", + "SettingsTabSystemIgnoreMissingServices": "تجاهل الخدمات المفقودة", + "SettingsTabSystemIgnoreApplet": "Ignore Applet", + "SettingsTabGraphics": "الرسومات", + "SettingsTabGraphicsAPI": "API الرسومات ", + "SettingsTabGraphicsEnableShaderCache": "تفعيل ذاكرة المظللات المؤقتة", + "SettingsTabGraphicsAnisotropicFiltering": "تصفية:", + "SettingsTabGraphicsAnisotropicFilteringAuto": "تلقائي", + "SettingsTabGraphicsAnisotropicFiltering2x": "2x", + "SettingsTabGraphicsAnisotropicFiltering4x": "4×", + "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": "وحدة تحكم برو", + "ControllerSettingsControllerTypeJoyConPair": "زوج جوي كون", + "ControllerSettingsControllerTypeJoyConLeft": "جوي كون اليسار ", + "ControllerSettingsControllerTypeJoyConRight": " جوي كون اليمين", + "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": "الأزندة اليسرى", + "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": "خانة جويكون اليمين :", + "ControllerSettingsMotionServerHost": "مضيف الخادم:", + "ControllerSettingsMotionGyroSensitivity": "حساسية مستشعر الحركة:", + "ControllerSettingsMotionGyroDeadzone": "منطقة مستشعر الحركة الميتة:", + "ControllerSettingsSave": "حفظ", + "ControllerSettingsClose": "إغلاق", + "KeyUnknown": "مجهول", + "KeyShiftLeft": "زر ‫Shift الأيسر", + "KeyShiftRight": "زر ‫Shift الأيمن", + "KeyControlLeft": "زر ‫Ctrl الأيسر", + "KeyMacControlLeft": "زر ⌃ الأيسر", + "KeyControlRight": "زر ‫Ctrl الأيمن", + "KeyMacControlRight": "زر ⌃ الأيمن", + "KeyAltLeft": "زر ‫Alt الأيسر", + "KeyMacAltLeft": "زر ⌥ الأيسر", + "KeyAltRight": "زر ‫Alt الأيمن", + "KeyMacAltRight": "زر ⌥ الأيمن", + "KeyWinLeft": "زر ⊞ الأيسر", + "KeyMacWinLeft": "زر ⌘ الأيسر", + "KeyWinRight": "زر ⊞ الأيمن", + "KeyMacWinRight": "زر ⌘ الأيمن", + "KeyMenu": "زر القائمة", + "KeyUp": "فوق", + "KeyDown": "اسفل", + "KeyLeft": "يسار", + "KeyRight": "يمين", + "KeyEnter": "مفتاح الإدخال", + "KeyEscape": "زر ‫Escape", + "KeySpace": "مسافة", + "KeyTab": "زر ‫Tab", + "KeyBackSpace": "زر المسح للخلف", + "KeyInsert": "زر Insert", + "KeyDelete": "زر الحذف", + "KeyPageUp": "زر ‫Page Up", + "KeyPageDown": "زر ‫Page Down", + "KeyHome": "زر ‫Home", + "KeyEnd": "زر ‫End", + "KeyCapsLock": "زر الحروف الكبيرة", + "KeyScrollLock": "زر ‫Scroll Lock", + "KeyPrintScreen": "زر ‫Print Screen", + "KeyPause": "زر Pause", + "KeyNumLock": "زر Num Lock", + "KeyClear": "زر Clear", + "KeyKeypad0": "لوحة الأرقام 0", + "KeyKeypad1": "لوحة الأرقام 1", + "KeyKeypad2": "لوحة الأرقام 2", + "KeyKeypad3": "لوحة الأرقام 3", + "KeyKeypad4": "لوحة الأرقام 4", + "KeyKeypad5": "لوحة الأرقام 5", + "KeyKeypad6": "لوحة الأرقام 6", + "KeyKeypad7": "لوحة الأرقام 7", + "KeyKeypad8": "لوحة الأرقام 8", + "KeyKeypad9": "لوحة الأرقام 9", + "KeyKeypadDivide": "لوحة الأرقام علامة القسمة", + "KeyKeypadMultiply": "لوحة الأرقام علامة الضرب", + "KeyKeypadSubtract": "لوحة الأرقام علامة الطرح\n", + "KeyKeypadAdd": "لوحة الأرقام علامة الزائد", + "KeyKeypadDecimal": "لوحة الأرقام الفاصلة العشرية", + "KeyKeypadEnter": "لوحة الأرقام زر الإدخال", + "KeyNumber0": "٠", + "KeyNumber1": "١", + "KeyNumber2": "٢", + "KeyNumber3": "٣", + "KeyNumber4": "٤", + "KeyNumber5": "٥", + "KeyNumber6": "٦", + "KeyNumber7": "٧", + "KeyNumber8": "٨", + "KeyNumber9": "٩", + "KeyTilde": "~", + "KeyGrave": "`", + "KeyMinus": "-", + "KeyPlus": "+", + "KeyBracketLeft": "[", + "KeyBracketRight": "]", + "KeySemicolon": ";", + "KeyQuote": "\"", + "KeyComma": ",", + "KeyPeriod": ".", + "KeySlash": "/", + "KeyBackSlash": "\\", + "KeyUnbound": "غير مرتبط", + "GamepadLeftStick": "زر عصا التحكم اليسرى", + "GamepadRightStick": "زر عصا التحكم اليمنى", + "GamepadLeftShoulder": "زر الكتف الأيسر‫ L", + "GamepadRightShoulder": "زر الكتف الأيمن‫ R", + "GamepadLeftTrigger": "زر الزناد الأيسر‫ (ZL)", + "GamepadRightTrigger": "زر الزناد الأيمن‫ (ZR)", + "GamepadDpadUp": "فوق", + "GamepadDpadDown": "اسفل", + "GamepadDpadLeft": "يسار", + "GamepadDpadRight": "يمين", + "GamepadMinus": "-", + "GamepadPlus": "+", + "GamepadGuide": "دليل", + "GamepadMisc1": "متنوع", + "GamepadPaddle1": "Paddle 1", + "GamepadPaddle2": "Paddle 2", + "GamepadPaddle3": "Paddle 3", + "GamepadPaddle4": "Paddle 4", + "GamepadTouchpad": "لوحة اللمس", + "GamepadSingleLeftTrigger0": "زر الزناد الأيسر 0", + "GamepadSingleRightTrigger0": "زر الزناد الأيمن 0", + "GamepadSingleLeftTrigger1": "زر الزناد الأيسر 1", + "GamepadSingleRightTrigger1": "زر الزناد الأيمن 1", + "StickLeft": "عصا التحكم اليسرى", + "StickRight": "عصا التحكم اليمنى", + "UserProfilesSelectedUserProfile": "الملف الشخصي المحدد للمستخدم:", + "UserProfilesSaveProfileName": "حفظ اسم الملف الشخصي", + "UserProfilesChangeProfileImage": "تغيير صورة الملف الشخصي", + "UserProfilesAvailableUserProfiles": "الملفات الشخصية للمستخدم المتاحة:", + "UserProfilesAddNewProfile": "إنشاء ملف الشخصي", + "UserProfilesDelete": "حذف", + "UserProfilesClose": "إغلاق", + "ProfileNameSelectionWatermark": "اختر اسم مستعار", + "ProfileImageSelectionTitle": "تحديد صورة الملف الشخصي", + "ProfileImageSelectionHeader": "اختر صورة الملف الشخصي", + "ProfileImageSelectionNote": "يمكنك استيراد صورة ملف شخصي مخصصة، أو تحديد صورة رمزية من البرامج الثابتة للنظام", + "ProfileImageSelectionImportImage": "استيراد ملف الصورة", + "ProfileImageSelectionSelectAvatar": "حدد الصورة الرمزية من البرنامج الثابتة", + "InputDialogTitle": "حوار الإدخال", + "InputDialogOk": "موافق", + "InputDialogCancel": "إلغاء", + "InputDialogCancelling": "Cancelling", + "InputDialogClose": "Close", + "InputDialogAddNewProfileTitle": "اختر اسم الملف الشخصي", + "InputDialogAddNewProfileHeader": "الرجاء إدخال اسم الملف الشخصي", + "InputDialogAddNewProfileSubtext": "(الطول الأقصى: {0})", + "AvatarChoose": "اختر الصورة الرمزية", + "AvatarSetBackgroundColor": "تعيين لون الخلفية", + "AvatarClose": "إغلاق", + "ControllerSettingsLoadProfileToolTip": "تحميل الملف الشخصي", + "ControllerSettingsViewProfileToolTip": "View Profile", + "ControllerSettingsAddProfileToolTip": "إضافة ملف شخصي", + "ControllerSettingsRemoveProfileToolTip": "إزالة الملف الشخصي", + "ControllerSettingsSaveProfileToolTip": "حفظ الملف الشخصي", + "MenuBarFileToolsTakeScreenshot": "أخذ لقطة للشاشة", + "MenuBarFileToolsHideUi": "إخفاء واجهة المستخدم", + "GameListContextMenuRunApplication": "تشغيل التطبيق", + "GameListContextMenuToggleFavorite": "تعيين كمفضل", + "GameListContextMenuToggleFavoriteToolTip": "تبديل الحالة المفضلة للعبة", + "SettingsTabGeneralTheme": "السمة:", + "SettingsTabGeneralThemeAuto": "Auto", + "SettingsTabGeneralThemeDark": "داكن", + "SettingsTabGeneralThemeLight": "فاتح", + "ControllerSettingsConfigureGeneral": "ضبط", + "ControllerSettingsRumble": "الاهتزاز", + "ControllerSettingsRumbleStrongMultiplier": "مضاعف اهتزاز قوي", + "ControllerSettingsRumbleWeakMultiplier": "مضاعف اهتزاز ضعيف", + "DialogMessageSaveNotAvailableMessage": "لا توجد بيانات الحفظ لـ {0} [{1:x16}]", + "DialogMessageSaveNotAvailableCreateSaveMessage": "هل ترغب في إنشاء بيانات الحفظ لهذه اللعبة؟", + "DialogConfirmationTitle": "ريوجينكس - تأكيد", + "DialogUpdaterTitle": "ريوجينكس - المحدث", + "DialogErrorTitle": "ريوجينكس - خطأ", + "DialogWarningTitle": "ريوجينكس - تحذير", + "DialogExitTitle": "ريوجينكس - الخروج", + "DialogErrorMessage": "واجه ريوجينكس خطأ", + "DialogExitMessage": "هل أنت متأكد من أنك تريد إغلاق ريوجينكس؟", + "DialogExitSubMessage": "سيتم فقدان كافة البيانات غير المحفوظة!", + "DialogMessageCreateSaveErrorMessage": "حدث خطأ أثناء إنشاء بيانات الحفظ المحددة: {0}", + "DialogMessageFindSaveErrorMessage": "حدث خطأ أثناء البحث عن بيانات الحفظ المحددة: {0}", + "FolderDialogExtractTitle": "اختر المجلد الذي تريد الاستخراج إليه", + "DialogNcaExtractionMessage": "استخراج قسم {0} من {1}...", + "DialogNcaExtractionTitle": "مستخرج قسم NCA", + "DialogNcaExtractionMainNcaNotFoundErrorMessage": "فشل الاستخراج. لم يكن NCA الرئيسي موجودا في الملف المحدد.", + "DialogNcaExtractionCheckLogErrorMessage": "فشل الاستخراج. اقرأ ملف التسجيل لمزيد من المعلومات.", + "DialogNcaExtractionSuccessMessage": "تم الاستخراج بنجاح.", + "DialogUpdaterConvertFailedMessage": "فشل تحويل إصدار ريوجينكس الحالي.", + "DialogUpdaterCancelUpdateMessage": "إلغاء التحديث", + "DialogUpdaterAlreadyOnLatestVersionMessage": "أنت تستخدم بالفعل أحدث إصدار من ريوجينكس!", + "DialogUpdaterFailedToGetVersionMessage": "حدث خطأ أثناء محاولة الحصول على معلومات الإصدار من إصدار غيت هاب. يمكن أن يحدث هذا إذا تم تجميع إصدار جديد بواسطة إجراءات غيت هاب. جرب مجددا بعد دقائق.", + "DialogUpdaterConvertFailedGithubMessage": "فشل تحويل إصدار ريوجينكس المستلم من إصدار غيت هاب.", + "DialogUpdaterDownloadingMessage": "جاري تنزيل التحديث...", + "DialogUpdaterExtractionMessage": "جاري استخراج التحديث...", + "DialogUpdaterRenamingMessage": "إعادة تسمية التحديث...", + "DialogUpdaterAddingFilesMessage": "إضافة تحديث جديد...", + "DialogUpdaterShowChangelogMessage": "Show Changelog", + "DialogUpdaterCompleteMessage": "اكتمل التحديث", + "DialogUpdaterRestartMessage": "هل تريد إعادة تشغيل ريوجينكس الآن؟", + "DialogUpdaterNoInternetMessage": "أنت غير متصل بالإنترنت.", + "DialogUpdaterNoInternetSubMessage": "يرجى التحقق من أن لديك اتصال إنترنت فعال!", + "DialogUpdaterDirtyBuildMessage": "لا يمكنك تحديث نسخة القذرة من ريوجينكس!", + "DialogUpdaterDirtyBuildSubMessage": "الرجاء تحميل ريوجينكس من https://ryujinx.app/download إذا كنت تبحث عن إصدار مدعوم.", + "DialogRestartRequiredMessage": "يتطلب إعادة التشغيل", + "DialogThemeRestartMessage": "تم حفظ السمة. إعادة التشغيل مطلوبة لتطبيق السمة.", + "DialogThemeRestartSubMessage": "هل تريد إعادة التشغيل", + "DialogFirmwareInstallEmbeddedMessage": "هل ترغب في تثبيت البرنامج الثابت المدمج في هذه اللعبة؟ (البرنامج الثابت {0})", + "DialogFirmwareInstallEmbeddedSuccessMessage": "لم يتم العثور على أي برنامج ثابت مثبت ولكن ريوجينكس كان قادرا على تثبيت البرنامج الثابت {0} من اللعبة المقدمة.\nسيبدأ المحاكي الآن.", + "DialogFirmwareNoFirmwareInstalledMessage": "لا يوجد برنامج ثابت مثبت", + "DialogFirmwareInstalledMessage": "تم تثبيت البرنامج الثابت {0}", + "DialogInstallFileTypesSuccessMessage": "تم تثبيت أنواع الملفات بنجاح!", + "DialogInstallFileTypesErrorMessage": "فشل تثبيت أنواع الملفات.", + "DialogUninstallFileTypesSuccessMessage": "تم إلغاء تثبيت أنواع الملفات بنجاح!", + "DialogUninstallFileTypesErrorMessage": "فشل إلغاء تثبيت أنواع الملفات.", + "DialogOpenSettingsWindowLabel": "فتح نافذة الإعدادات", + "DialogOpenXCITrimmerWindowLabel": "XCI Trimmer Window", + "DialogControllerAppletTitle": "تطبيق وحدة التحكم المصغر", + "DialogMessageDialogErrorExceptionMessage": "خطأ في عرض مربع حوار الرسالة: {0}", + "DialogSoftwareKeyboardErrorExceptionMessage": "خطأ في عرض لوحة مفاتيح البرامج: {0}", + "DialogErrorAppletErrorExceptionMessage": "خطأ في عرض مربع حوار خطأ التطبيق المصغر: {0}", + "DialogUserErrorDialogMessage": "{0}: {1}", + "DialogUserErrorDialogInfoMessage": "لمزيد من المعلومات حول كيفية إصلاح هذا الخطأ، اتبع دليل الإعداد الخاص بنا.", + "DialogUserErrorDialogTitle": "خطأ ريوجينكس ({0})", + "DialogAmiiboApiTitle": "أميبو API", + "DialogAmiiboApiFailFetchMessage": "حدث خطأ أثناء جلب المعلومات من API.", + "DialogAmiiboApiConnectErrorMessage": "غير قادر على الاتصال بخادم API أميبو. قد تكون الخدمة معطلة أو قد تحتاج إلى التحقق من اتصالك بالإنترنت.", + "DialogProfileInvalidProfileErrorMessage": "الملف الشخصي {0} غير متوافق مع نظام تكوين الإدخال الحالي.", + "DialogProfileDefaultProfileOverwriteErrorMessage": "لا يمكن الكتابة فوق الملف الشخصي الافتراضي", + "DialogProfileDeleteProfileTitle": "حذف الملف الشخصي", + "DialogProfileDeleteProfileMessage": "هذا الإجراء لا رجعة فيه، هل أنت متأكد من أنك تريد المتابعة؟", + "DialogWarning": "تحذير", + "DialogPPTCDeletionMessage": "أنت على وشك الإنتظار لإعادة بناء ذاكرة التخزين المؤقت للترجمة المستمرة (PPTC) عند الإقلاع التالي لـ:\n\n{0}\n\nأمتأكد من رغبتك في المتابعة؟", + "DialogPPTCDeletionErrorMessage": "خطأ خلال تنظيف ذاكرة التخزين المؤقت للترجمة المستمرة (PPTC) في {0}: {1}", + "DialogShaderDeletionMessage": "أنت على وشك حذف ذاكرة المظللات المؤقتة ل:\n\n{0}\n\nهل انت متأكد انك تريد المتابعة؟", + "DialogShaderDeletionErrorMessage": "حدث خطأ أثناء تنظيف ذاكرة المظللات المؤقتة في {0}: {1}", + "DialogRyujinxErrorMessage": "واجه ريوجينكس خطأ", + "DialogInvalidTitleIdErrorMessage": "خطأ في واجهة المستخدم: اللعبة المحددة لم يكن لديها معرف عنوان صالح", + "DialogFirmwareInstallerFirmwareNotFoundErrorMessage": "لم يتم العثور على برنامج ثابت للنظام صالح في {0}.", + "DialogFirmwareInstallerFirmwareInstallTitle": "تثبيت البرنامج الثابت {0}", + "DialogFirmwareInstallerFirmwareInstallMessage": "سيتم تثبيت إصدار النظام {0}.", + "DialogFirmwareInstallerFirmwareInstallSubMessage": "\n\nهذا سيحل محل إصدار النظام الحالي {0}.", + "DialogFirmwareInstallerFirmwareInstallConfirmMessage": "\nهل تريد المتابعة؟", + "DialogFirmwareInstallerFirmwareInstallWaitMessage": "تثبيت البرنامج الثابت...", + "DialogFirmwareInstallerFirmwareInstallSuccessMessage": "تم تثبيت إصدار النظام {0} بنجاح.", + "DialogUserProfileDeletionWarningMessage": "لن تكون هناك ملفات الشخصية أخرى لفتحها إذا تم حذف الملف الشخصي المحدد", + "DialogUserProfileDeletionConfirmMessage": "هل تريد حذف الملف الشخصي المحدد", + "DialogUserProfileUnsavedChangesTitle": "تحذير - التغييرات غير محفوظة", + "DialogUserProfileUnsavedChangesMessage": "لقد قمت بإجراء تغييرات على الملف الشخصي لهذا المستخدم هذا ولم يتم حفظها.", + "DialogUserProfileUnsavedChangesSubMessage": "هل تريد تجاهل التغييرات؟", + "DialogControllerSettingsModifiedConfirmMessage": "تم تحديث إعدادات وحدة التحكم الحالية.", + "DialogControllerSettingsModifiedConfirmSubMessage": "هل تريد الحفظ ؟", + "DialogLoadFileErrorMessage": "{0}. ملف خاطئ: {1}", + "DialogModAlreadyExistsMessage": "التعديل موجود بالفعل", + "DialogModInvalidMessage": "المجلد المحدد لا يحتوي على تعديل!", + "DialogModDeleteNoParentMessage": "فشل الحذف: لم يمكن العثور على المجلد الرئيسي للتعديل\"{0}\"!", + "DialogDlcNoDlcErrorMessage": "الملف المحدد لا يحتوي على محتوى إضافي للعنوان المحدد!", + "DialogPerformanceCheckLoggingEnabledMessage": "لقد تم تمكين تسجيل التتبع، والذي تم تصميمه ليتم استخدامه من قبل المطورين فقط.", + "DialogPerformanceCheckLoggingEnabledConfirmMessage": "للحصول على الأداء الأمثل، يوصى بتعطيل تسجيل التتبع. هل ترغب في تعطيل تسجيل التتبع الآن؟", + "DialogPerformanceCheckShaderDumpEnabledMessage": "لقد قمت بتمكين تفريغ المظللات، والذي تم تصميمه ليستخدمه المطورون فقط.", + "DialogPerformanceCheckShaderDumpEnabledConfirmMessage": "للحصول على الأداء الأمثل، يوصى بتعطيل تفريغ المظللات. هل ترغب في تعطيل تفريغ المظللات الآن؟", + "DialogLoadAppGameAlreadyLoadedMessage": "تم تحميل لعبة بالفعل", + "DialogLoadAppGameAlreadyLoadedSubMessage": "الرجاء إيقاف المحاكاة أو إغلاق المحاكي قبل بدء لعبة أخرى.", + "DialogUpdateAddUpdateErrorMessage": "الملف المحدد لا يحتوي على تحديث للعنوان المحدد!", + "DialogSettingsBackendThreadingWarningTitle": "تحذير - خلفية متعددة المسارات", + "DialogSettingsBackendThreadingWarningMessage": "يجب إعادة تشغيل ريوجينكس بعد تغيير هذا الخيار حتى يتم تطبيقه بالكامل. اعتمادا على النظام الأساسي الخاص بك، قد تحتاج إلى تعطيل تعدد المسارات الخاص ببرنامج الرسومات التشغيل الخاص بك يدويًا عند استخدام الخاص بريوجينكس.", + "DialogModManagerDeletionWarningMessage": "أنت على وشك حذف التعديل: {0}\n\nهل انت متأكد انك تريد المتابعة؟", + "DialogModManagerDeletionAllWarningMessage": "أنت على وشك حذف كافة التعديلات لهذا العنوان.\n\nهل انت متأكد انك تريد المتابعة؟", + "SettingsTabGraphicsFeaturesOptions": "المميزات", + "SettingsTabGraphicsBackendMultithreading": "تعدد المسارات لخلفية الرسومات:", + "CommonAuto": "تلقائي", + "CommonOff": "معطل", + "CommonOn": "تشغيل", + "InputDialogYes": "نعم", + "InputDialogNo": "لا", + "DialogProfileInvalidProfileNameErrorMessage": "يحتوي اسم الملف على أحرف غير صالحة. يرجى المحاولة مرة أخرى.", + "MenuBarOptionsPauseEmulation": "إيقاف مؤقت", + "MenuBarOptionsResumeEmulation": "استئناف", + "AboutUrlTooltipMessage": "انقر لفتح موقع ريوجينكس في متصفحك الافتراضي.", + "AboutDisclaimerMessage": "ريوجينكس لا ينتمي إلى نينتندو™،\nأو أي من شركائها بأي شكل من الأشكال.", + "AboutAmiiboDisclaimerMessage": "AmiiboAPI (www.amiiboapi.com) يتم \nاستخدامه في محاكاة أمبيو لدينا.", + "AboutPatreonUrlTooltipMessage": "انقر لفتح صفحة ريوجينكس في باتريون في متصفحك الافتراضي.", + "AboutGithubUrlTooltipMessage": "انقر لفتح صفحة ريوجينكس في غيت هاب في متصفحك الافتراضي.", + "AboutDiscordUrlTooltipMessage": "انقر لفتح دعوة إلى خادم ريوجينكس في ديكسورد في متصفحك الافتراضي.", + "AboutTwitterUrlTooltipMessage": "انقر لفتح صفحة ريوجينكس في تويتر في متصفحك الافتراضي.", + "AboutRyujinxAboutTitle": "حول:", + "AboutRyujinxAboutContent": "ريوجينكس هو محاكي لجهاز نينتندو سويتش™.\nمن فضلك ادعمنا على باتريون.\nاحصل على آخر الأخبار على تويتر أو ديسكورد.\nيمكن للمطورين المهتمين بالمساهمة معرفة المزيد على غيت هاب أو ديسكورد.", + "AboutRyujinxMaintainersTitle": "تتم صيانته بواسطة:", + "AboutRyujinxMaintainersContentTooltipMessage": "انقر لفتح صفحة المساهمين في متصفحك الافتراضي.", + "AboutRyujinxSupprtersTitle": "مدعوم على باتريون بواسطة:", + "AmiiboSeriesLabel": "مجموعة أميبو", + "AmiiboCharacterLabel": "شخصية", + "AmiiboScanButtonLabel": "فحصه", + "AmiiboOptionsShowAllLabel": "إظهار كل أميبو", + "AmiiboOptionsUsRandomTagLabel": "هاك: استخدم علامة Uuid عشوائية ", + "DlcManagerTableHeadingEnabledLabel": "مفعل", + "DlcManagerTableHeadingTitleIdLabel": "معرف العنوان", + "DlcManagerTableHeadingContainerPathLabel": "مسار الحاوية", + "DlcManagerTableHeadingFullPathLabel": "المسار كاملا", + "DlcManagerRemoveAllButton": "حذف الكل", + "DlcManagerEnableAllButton": "تشغيل الكل", + "DlcManagerDisableAllButton": "تعطيل الكل", + "ModManagerDeleteAllButton": "حذف الكل", + "MenuBarOptionsChangeLanguage": "تغيير اللغة", + "MenuBarShowFileTypes": "إظهار أنواع الملفات", + "CommonSort": "فرز", + "CommonShowNames": "عرض الأسماء", + "CommonFavorite": "المفضلة", + "OrderAscending": "تصاعدي", + "OrderDescending": "تنازلي", + "SettingsTabGraphicsFeatures": "الميزات والتحسينات", + "ErrorWindowTitle": "نافذة الخطأ", + "ToggleDiscordTooltip": "اختر ما إذا كنت تريد عرض ريوجينكس في نشاط ديسكورد \"يتم تشغيله حاليا\" أم لا", + "AddGameDirBoxTooltip": "أدخل مجلد اللعبة لإضافته إلى القائمة", + "AddGameDirTooltip": "إضافة مجلد اللعبة إلى القائمة", + "RemoveGameDirTooltip": "إزالة مجلد اللعبة المحدد", + "AddAutoloadDirBoxTooltip": "Enter an autoload directory to add to the list", + "AddAutoloadDirTooltip": "Add an autoload directory to the list", + "RemoveAutoloadDirTooltip": "Remove selected autoload directory", + "CustomThemeCheckTooltip": "استخدم سمة أفالونيا المخصصة لواجهة المستخدم الرسومية لتغيير مظهر قوائم المحاكي", + "CustomThemePathTooltip": "مسار سمة واجهة المستخدم المخصصة", + "CustomThemeBrowseTooltip": "تصفح للحصول على سمة واجهة المستخدم المخصصة", + "DockModeToggleTooltip": "يجعل وضع تركيب بالمنصة النظام الذي تمت محاكاته بمثابة جهاز نينتندو سويتش الذي تم تركيبه بالمنصة. يؤدي هذا إلى تحسين الدقة الرسومية في معظم الألعاب. على العكس من ذلك، سيؤدي تعطيل هذا إلى جعل النظام الذي تمت محاكاته يعمل كجهاز نينتندو سويتش محمول، مما يقلل من جودة الرسومات.\n\nقم بتكوين عناصر تحكم اللاعب 1 إذا كنت تخطط لاستخدام وضع تركيب بالمنصة؛ قم بتكوين عناصر التحكم المحمولة إذا كنت تخطط لاستخدام الوضع المحمول.\n\nاتركه مشغل إذا لم تكن متأكدا.", + "DirectKeyboardTooltip": "دعم الوصول المباشر للوحة المفاتيح (HID). يوفر وصول الألعاب إلى لوحة المفاتيح الخاصة بك كجهاز لإدخال النص.\n\nيعمل فقط مع الألعاب التي تدعم استخدام لوحة المفاتيح في الأصل على أجهزة سويتش.\n\nاتركه معطلا إذا كنت غير متأكد.", + "DirectMouseTooltip": "دعم الوصول المباشر للوحة المفاتيح (HID). يوفر وصول الألعاب إلى لوحة المفاتيح الخاصة بك كجهاز لإدخال النص.\n\nيعمل فقط مع الألعاب التي تدعم استخدام لوحة المفاتيح في الأصل على أجهزة سويتش.\n\nاتركه معطلا إذا كنت غير متأكد.", + "RegionTooltip": "تغيير منطقة النظام", + "LanguageTooltip": "تغيير لغة النظام", + "TimezoneTooltip": "تغيير النطاق الزمني للنظام", + "TimeTooltip": "تغيير وقت النظام", + "VSyncToggleTooltip": "محاكاة المزامنة العمودية للجهاز. في الأساس محدد الإطار لغالبية الألعاب؛ قد يؤدي تعطيله إلى تشغيل الألعاب بسرعة أعلى أو جعل شاشات التحميل تستغرق وقتا أطول أو تتعطل.\n\nيمكن تبديله داخل اللعبة باستخدام مفتاح التشغيل السريع الذي تفضله (F1 افتراضيا). نوصي بالقيام بذلك إذا كنت تخطط لتعطيله.\n\nاتركه ممكنا إذا لم تكن متأكدا.", + "PptcToggleTooltip": "يحفظ وظائف JIT المترجمة بحيث لا تحتاج إلى ترجمتها في كل مرة يتم فيها تحميل اللعبة.\n\nيقلل من التقطيع ويسرع بشكل ملحوظ أوقات التشغيل بعد التشغيل الأول للعبة.\n\nاتركه ممكنا إذا لم تكن متأكدا.", + "LowPowerPptcToggleTooltip": "Load the PPTC using a third of the amount of cores.", + "FsIntegrityToggleTooltip": "يتحقق من وجود ملفات تالفة عند تشغيل لعبة ما، وإذا تم اكتشاف ملفات تالفة، فسيتم عرض خطأ تجزئة في السجل.\n\nليس له أي تأثير على الأداء ويهدف إلى المساعدة في استكشاف الأخطاء وإصلاحها.\n\nاتركه مفعلا إذا كنت غير متأكد.", + "AudioBackendTooltip": "يغير الواجهة الخلفية المستخدمة لتقديم الصوت.\n\nSDL2 هو الخيار المفضل، بينما يتم استخدام OpenAL وSoundIO كبديلين. زائف لن يكون لها صوت.\n\nاضبط على SDL2 إذا لم تكن متأكدا.", + "MemoryManagerTooltip": "تغيير كيفية تعيين ذاكرة الضيف والوصول إليها. يؤثر بشكل كبير على أداء وحدة المعالجة المركزية التي تمت محاكاتها.\n\nاضبط على المضيف غير محدد إذا لم تكن متأكدا.", + "MemoryManagerSoftwareTooltip": "استخدام جدول الصفحات البرمجي لترجمة العناوين. أعلى دقة ولكن أبطأ أداء.", + "MemoryManagerHostTooltip": "تعيين الذاكرة مباشرة في مساحة عنوان المضيف. تجميع وتنفيذ JIT أسرع بكثير.", + "MemoryManagerUnsafeTooltip": "تعيين الذاكرة مباشرة، ولكن لا تخفي العنوان داخل مساحة عنوان الضيف قبل الوصول. أسرع، ولكن على حساب السلامة. يمكن لتطبيق الضيف الوصول إلى الذاكرة من أي مكان في ريوجينكس، لذا قم بتشغيل البرامج التي تثق بها فقط مع هذا الوضع.", + "UseHypervisorTooltip": "استخدم هايبرڤايزور بدلا من JIT. يعمل على تحسين الأداء بشكل كبير عند توفره، ولكنه قد يكون غير مستقر في حالته الحالية.", + "DRamTooltip": "يستخدم تخطيط وضع الذاكرة البديل لتقليد نموذج سويتش المطورين.\n\nيعد هذا مفيدا فقط لحزم النسيج عالية الدقة أو تعديلات دقة 4K. لا يحسن الأداء.\n\nاتركه معطلا إذا لم تكن متأكدا.", + "IgnoreMissingServicesTooltip": "يتجاهل خدمات نظام هوريزون غير المنفذة. قد يساعد هذا في تجاوز الأعطال عند تشغيل ألعاب معينة.\n\nاتركه معطلا إذا كنت غير متأكد.", + "IgnoreAppletTooltip": "لن يظهر مربع الحوار الخارجي \"تطبيق وحدة التحكم\" إذا تم فصل لوحة الألعاب أثناء اللعب. ولن تظهر مطالبة بإغلاق مربع الحوار أو إعداد وحدة تحكم جديدة. وبمجرد إعادة توصيل وحدة التحكم التي تم فصلها سابقًا، ستستأنف اللعبة تلقائيًا.", + "GraphicsBackendThreadingTooltip": "ينفذ أوامر الواجهة الخلفية للرسومات على مسار ثاني.\n\nيعمل على تسريع عملية تجميع المظللات وتقليل التقطيع وتحسين الأداء على برامج تشغيل وحدة الرسوميات دون دعم المسارات المتعددة الخاصة بهم. أداء أفضل قليلا على برامج التشغيل ذات المسارات المتعددة.\n\nاضبط على تلقائي إذا لم تكن متأكدا.", + "GalThreadingTooltip": "ينفذ أوامر الواجهة الخلفية للرسومات على مسار ثاني.\n\nيعمل على تسريع عملية تجميع المظللات وتقليل التقطيع وتحسين الأداء على برامج تشغيل وحدة الرسوميات دون دعم المسارات المتعددة الخاصة بهم. أداء أفضل قليلا على برامج التشغيل ذات المسارات المتعددة.\n\nاضبط على تلقائي إذا لم تكن متأكدا.", + "ShaderCacheToggleTooltip": "يحفظ ذاكرة المظللات المؤقتة على القرص مما يقلل من التقطيع في عمليات التشغيل اللاحقة.\n\nاتركه مفعلا إذا لم تكن متأكدا.", + "ResolutionScaleTooltip": "يضاعف دقة عرض اللعبة.\n\nقد لا تعمل بعض الألعاب مع هذا وتبدو منقطة حتى عند زيادة الدقة؛ بالنسبة لهذه الألعاب، قد تحتاج إلى العثور على تعديلات تزيل تنعيم الحواف أو تزيد من دقة العرض الداخلي. لاستخدام الأخير، من المحتمل أن ترغب في تحديد أصلي.\n\nيمكن تغيير هذا الخيار أثناء تشغيل اللعبة بالنقر فوق \"تطبيق\" أدناه؛ يمكنك ببساطة تحريك نافذة الإعدادات جانبًا والتجربة حتى تجد المظهر المفضل للعبة.\n\nضع في اعتبارك أن 4x مبالغة في أي إعداد تقريبًا.", + "ResolutionScaleEntryTooltip": "مقياس دقة النقطة العائمة، مثل 1.5. من المرجح أن تتسبب المقاييس غير المتكاملة في حدوث مشكلات أو تعطل.", + "AnisotropyTooltip": "مستوى تصفية. اضبط على تلقائي لاستخدام القيمة التي تطلبها اللعبة.", + "AspectRatioTooltip": "يتم تطبيق نسبة العرض إلى الارتفاع على نافذة العارض.\n\nقم بتغيير هذا فقط إذا كنت تستخدم تعديل نسبة العرض إلى الارتفاع للعبتك، وإلا سيتم تمديد الرسومات.\n\nاتركه16:9 إذا لم تكن متأكدا.", + "ShaderDumpPathTooltip": "مسار تفريغ المظللات", + "FileLogTooltip": "حفظ تسجيل وحدة التحكم إلى ملف سجل على القرص. لا يؤثر على الأداء.", + "StubLogTooltip": "طباعة رسائل سجل stub في وحدة التحكم. لا يؤثر على الأداء.", + "InfoLogTooltip": "طباعة رسائل سجل المعلومات في وحدة التحكم. لا يؤثر على الأداء.", + "WarnLogTooltip": "طباعة رسائل سجل التحذير في وحدة التحكم. لا يؤثر على الأداء.", + "ErrorLogTooltip": "طباعة رسائل سجل الأخطاء في وحدة التحكم. لا يؤثر على الأداء.", + "TraceLogTooltip": "طباعة رسائل سجل التتبع في وحدة التحكم. لا يؤثر على الأداء.", + "GuestLogTooltip": "طباعة رسائل سجل الضيف في وحدة التحكم. لا يؤثر على الأداء.", + "FileAccessLogTooltip": "طباعة رسائل سجل الوصول إلى الملفات في وحدة التحكم.", + "FSAccessLogModeTooltip": "تمكين إخراج سجل الوصول إلى نظام الملفات إلى وحدة التحكم. الأوضاع الممكنة هي 0-3", + "DeveloperOptionTooltip": "استخدمه بعناية", + "OpenGlLogLevel": "يتطلب تمكين مستويات السجل المناسبة", + "DebugLogTooltip": "طباعة رسائل سجل التصحيح في وحدة التحكم.\n\nاستخدم هذا فقط إذا طلب منك أحد الموظفين تحديدًا ذلك، لأنه سيجعل من الصعب قراءة السجلات وسيؤدي إلى تدهور أداء المحاكي.", + "LoadApplicationFileTooltip": "افتح مستكشف الملفات لاختيار ملف متوافق مع سويتش لتحميله", + "LoadApplicationFolderTooltip": "افتح مستكشف الملفات لاختيار تطبيق متوافق مع سويتش للتحميل", + "LoadDlcFromFolderTooltip": "Open a file explorer to choose one or more folders to bulk load DLC from", + "LoadTitleUpdatesFromFolderTooltip": "Open a file explorer to choose one or more folders to bulk load title updates from", + "OpenRyujinxFolderTooltip": "فتح مجلد نظام ملفات ريوجينكس", + "OpenRyujinxLogsTooltip": "يفتح المجلد الذي تتم كتابة السجلات إليه", + "ExitTooltip": "الخروج من ريوجينكس", + "OpenSettingsTooltip": "فتح نافذة الإعدادات", + "OpenProfileManagerTooltip": "فتح نافذة إدارة الملفات الشخصية للمستخدمين", + "StopEmulationTooltip": "إيقاف محاكاة اللعبة الحالية والعودة إلى اختيار اللعبة", + "CheckUpdatesTooltip": "التحقق من وجود تحديثات لريوجينكس", + "OpenAboutTooltip": "فتح حول النافذة", + "GridSize": "حجم الشبكة", + "GridSizeTooltip": "تغيير حجم عناصر الشبكة", + "SettingsTabSystemSystemLanguageBrazilianPortuguese": "البرتغالية البرازيلية", + "AboutRyujinxContributorsButtonHeader": "رؤية جميع المساهمين", + "SettingsTabSystemAudioVolume": "مستوى الصوت:", + "AudioVolumeTooltip": "تغيير مستوى الصوت", + "SettingsTabSystemEnableInternetAccess": "الوصول إلى إنترنت كضيف/وضع LAN", + "EnableInternetAccessTooltip": "للسماح للتطبيق الذي تمت محاكاته بالاتصال بالإنترنت.\n\nيمكن للألعاب التي تحتوي على وضع LAN الاتصال ببعضها البعض عند تمكين ذلك وتوصيل الأنظمة بنفس نقطة الوصول. وهذا يشمل الأجهزة الحقيقية أيضا.\n\nلا يسمح بالاتصال بخوادم نينتندو. قد يتسبب في حدوث عطل في بعض الألعاب التي تحاول الاتصال بالإنترنت.\n\nاتركه معطلا إذا لم تكن متأكدا.", + "GameListContextMenuManageCheatToolTip": "إدارة الغش", + "GameListContextMenuManageCheat": "إدارة الغش", + "GameListContextMenuManageModToolTip": "إدارة التعديلات", + "GameListContextMenuManageMod": "إدارة التعديلات", + "ControllerSettingsStickRange": "نطاق:", + "DialogStopEmulationTitle": "ريوجينكس - إيقاف المحاكاة", + "DialogStopEmulationMessage": "هل أنت متأكد أنك تريد إيقاف المحاكاة؟", + "SettingsTabCpu": "المعالج", + "SettingsTabAudio": "الصوت", + "SettingsTabNetwork": "الشبكة", + "SettingsTabNetworkConnection": "اتصال الشبكة", + "SettingsTabCpuCache": "ذاكرة المعالج المؤقت", + "SettingsTabCpuMemory": "وضع المعالج", + "DialogUpdaterFlatpakNotSupportedMessage": "الرجاء تحديث ريوجينكس عبر فلات هاب.", + "UpdaterDisabledWarningTitle": "المحدث معطل!", + "ControllerSettingsRotate90": "تدوير 90 درجة في اتجاه عقارب الساعة", + "IconSize": "حجم الأيقونة", + "IconSizeTooltip": "تغيير حجم أيقونات اللعبة", + "MenuBarOptionsShowConsole": "عرض وحدة التحكم", + "ShaderCachePurgeError": "حدث خطأ أثناء تنظيف ذاكرة المظللات المؤقتة في {0}: {1}", + "UserErrorNoKeys": "المفاتيح غير موجودة", + "UserErrorNoFirmware": "لم يتم العثور على البرنامج الثابت", + "UserErrorFirmwareParsingFailed": "خطأ في تحليل البرنامج الثابت", + "UserErrorApplicationNotFound": "التطبيق غير موجود", + "UserErrorUnknown": "خطأ غير معروف", + "UserErrorUndefined": "خطأ غير محدد", + "UserErrorNoKeysDescription": "لم يتمكن ريوجينكس من العثور على ملف 'prod.keys' الخاص بك", + "UserErrorNoFirmwareDescription": "لم يتمكن ريوجينكس من العثور على أية برامج ثابتة مثبتة", + "UserErrorFirmwareParsingFailedDescription": "لم يتمكن ريوجينكس من تحليل البرامج الثابتة المتوفرة. يحدث هذا عادة بسبب المفاتيح القديمة.", + "UserErrorApplicationNotFoundDescription": "تعذر على ريوجينكس العثور على تطبيق صالح في المسار المحدد.", + "UserErrorUnknownDescription": "حدث خطأ غير معروف!", + "UserErrorUndefinedDescription": "حدث خطأ غير محدد! لا ينبغي أن يحدث هذا، يرجى الاتصال بمطور!", + "OpenSetupGuideMessage": "فتح دليل الإعداد", + "NoUpdate": "لا يوجد تحديث", + "TitleUpdateVersionLabel": "الإصدار: {0}", + "TitleBundledUpdateVersionLabel": "Bundled: Version {0}", + "TitleBundledDlcLabel": "Bundled:", + "TitleXCIStatusPartialLabel": "Partial", + "TitleXCIStatusTrimmableLabel": "Untrimmed", + "TitleXCIStatusUntrimmableLabel": "Trimmed", + "TitleXCIStatusFailedLabel": "(Failed)", + "TitleXCICanSaveLabel": "Save {0:n0} Mb", + "TitleXCISavingLabel": "Saved {0:n0} Mb", + "RyujinxInfo": "ريوجينكس - معلومات", + "RyujinxConfirm": "ريوجينكس - تأكيد", + "FileDialogAllTypes": "كل الأنواع", + "Never": "مطلقا", + "SwkbdMinCharacters": "يجب أن يبلغ طوله {0} حرفا على الأقل", + "SwkbdMinRangeCharacters": "يجب أن يتكون من {0}-{1} حرفا", + "SoftwareKeyboard": "لوحة المفاتيح البرمجية", + "SoftwareKeyboardModeNumeric": "يجب أن يكون 0-9 أو '.' فقط", + "SoftwareKeyboardModeAlphabet": "يجب أن تكون الأحرف غير CJK فقط", + "SoftwareKeyboardModeASCII": "يجب أن يكون نص ASCII فقط", + "ControllerAppletControllers": "وحدات التحكم المدعومة:", + "ControllerAppletPlayers": "اللاعبين:", + "ControllerAppletDescription": "الإعدادات الحالية غير صالحة. افتح الإعدادات وأعد تكوين المدخلات الخاصة بك.", + "ControllerAppletDocked": "تم ضبط وضع تركيب بالمنصة. يجب تعطيل التحكم المحمول.", + "UpdaterRenaming": "إعادة تسمية الملفات القديمة...", + "UpdaterRenameFailed": "المحدث غير قادر على إعادة تسمية الملف: {0}", + "UpdaterAddingFiles": "إضافة ملفات جديدة...", + "UpdaterExtracting": "استخراج التحديث...", + "UpdaterDownloading": "تحميل التحديث...", + "Game": "لعبة", + "Docked": "تركيب بالمنصة", + "Handheld": "محمول", + "ConnectionError": "خطأ في الاتصال", + "AboutPageDeveloperListMore": "{0} والمزيد...", + "ApiError": "خطأ في API.", + "LoadingHeading": "جاري تحميل {0}", + "CompilingPPTC": "تجميع الـ‫(PPTC)", + "CompilingShaders": "تجميع المظللات", + "AllKeyboards": "كل لوحات المفاتيح", + "OpenFileDialogTitle": "حدد ملف مدعوم لفتحه", + "OpenFolderDialogTitle": "حدد مجلدا يحتوي على لعبة غير مضغوطة", + "AllSupportedFormats": "كل التنسيقات المدعومة", + "RyujinxUpdater": "محدث ريوجينكس", + "SettingsTabHotkeys": "مفاتيح الاختصار في لوحة المفاتيح", + "SettingsTabHotkeysHotkeys": "مفاتيح الاختصار في لوحة المفاتيح", + "SettingsTabHotkeysToggleVsyncHotkey": "تبديل المزامنة العمودية:", + "SettingsTabHotkeysScreenshotHotkey": "لقطة الشاشة:", + "SettingsTabHotkeysShowUiHotkey": "عرض واجهة المستخدم:", + "SettingsTabHotkeysPauseHotkey": "إيقاف مؤقت:", + "SettingsTabHotkeysToggleMuteHotkey": "كتم:", + "ControllerMotionTitle": "إعدادات التحكم بالحركة", + "ControllerRumbleTitle": "إعدادات الهزاز", + "SettingsSelectThemeFileDialogTitle": "حدد ملف السمة", + "SettingsXamlThemeFile": "ملف سمة Xaml", + "AvatarWindowTitle": "إدارة الحسابات - الصورة الرمزية", + "Amiibo": "أميبو", + "Unknown": "غير معروف", + "Usage": "الاستخدام", + "Writable": "قابل للكتابة", + "SelectDlcDialogTitle": "حدد ملفات المحتوي الإضافي", + "SelectUpdateDialogTitle": "حدد ملفات التحديث", + "SelectModDialogTitle": "حدد مجلد التعديل", + "TrimXCIFileDialogTitle": "Check and Trim XCI File", + "TrimXCIFileDialogPrimaryText": "This function will first check the empty space and then trim the XCI File to save disk space.", + "TrimXCIFileDialogSecondaryText": "Current File Size: {0:n} MB\nGame Data Size: {1:n} MB\nDisk Space Savings: {2:n} MB", + "TrimXCIFileNoTrimNecessary": "XCI File does not need to be trimmed. Check logs for further details", + "TrimXCIFileNoUntrimPossible": "XCI File cannot be untrimmed. Check logs for further details", + "TrimXCIFileReadOnlyFileCannotFix": "XCI File is Read Only and could not be made writable. Check logs for further details", + "TrimXCIFileFileSizeChanged": "XCI File has changed in size since it was scanned. Please check the file is not being written to and try again.", + "TrimXCIFileFreeSpaceCheckFailed": "XCI File has data in the free space area, it is not safe to trim", + "TrimXCIFileInvalidXCIFile": "XCI File contains invalid data. Check logs for further details", + "TrimXCIFileFileIOWriteError": "XCI File could not be opened for writing. Check logs for further details", + "TrimXCIFileFailedPrimaryText": "Trimming of the XCI file failed", + "TrimXCIFileCancelled": "The operation was cancelled", + "TrimXCIFileFileUndertermined": "No operation was performed", + "UserProfileWindowTitle": "مدير الملفات الشخصية للمستخدمين", + "CheatWindowTitle": "مدير الغش", + "DlcWindowTitle": "إدارة المحتوى القابل للتنزيل لـ {0} ({1})", + "ModWindowTitle": "إدارة التعديلات لـ {0} ({1})", + "UpdateWindowTitle": "مدير تحديث العنوان", + "XCITrimmerWindowTitle": "XCI File Trimmer", + "XCITrimmerTitleStatusCount": "{0} of {1} Title(s) Selected", + "XCITrimmerTitleStatusCountWithFilter": "{0} of {1} Title(s) Selected ({2} displayed)", + "XCITrimmerTitleStatusTrimming": "Trimming {0} Title(s)...", + "XCITrimmerTitleStatusUntrimming": "Untrimming {0} Title(s)...", + "XCITrimmerTitleStatusFailed": "Failed", + "XCITrimmerPotentialSavings": "Potential Savings", + "XCITrimmerActualSavings": "Actual Savings", + "XCITrimmerSavingsMb": "{0:n0} Mb", + "XCITrimmerSelectDisplayed": "Select Shown", + "XCITrimmerDeselectDisplayed": "Deselect Shown", + "XCITrimmerSortName": "Title", + "XCITrimmerSortSaved": "Space Savings", + "XCITrimmerTrim": "Trim", + "XCITrimmerUntrim": "Untrim", + "UpdateWindowUpdateAddedMessage": "{0} new update(s) added", + "UpdateWindowBundledContentNotice": "Bundled updates cannot be removed, only disabled.", + "CheatWindowHeading": "الغش متوفر لـ {0} [{1}]", + "BuildId": "معرف البناء:", + "DlcWindowBundledContentNotice": "Bundled DLC cannot be removed, only disabled.", + "DlcWindowHeading": "المحتويات القابلة للتنزيل {0}", + "DlcWindowDlcAddedMessage": "{0} new downloadable content(s) added", + "AutoloadDlcAddedMessage": "{0} new downloadable content(s) added", + "AutoloadDlcRemovedMessage": "{0} missing downloadable content(s) removed", + "AutoloadUpdateAddedMessage": "{0} new update(s) added", + "AutoloadUpdateRemovedMessage": "{0} missing update(s) removed", + "ModWindowHeading": "{0} تعديل", + "UserProfilesEditProfile": "تعديل المحدد", + "Continue": "Continue", + "Cancel": "إلغاء", + "Save": "حفظ", + "Discard": "تجاهل", + "Paused": "متوقف مؤقتا", + "UserProfilesSetProfileImage": "تعيين صورة الملف الشخصي", + "UserProfileEmptyNameError": "الاسم مطلوب", + "UserProfileNoImageError": "يجب تعيين صورة الملف الشخصي", + "GameUpdateWindowHeading": "إدارة التحديثات لـ {0} ({1})", + "SettingsTabHotkeysResScaleUpHotkey": "زيادة الدقة:", + "SettingsTabHotkeysResScaleDownHotkey": "خفض الدقة:", + "UserProfilesName": "الاسم:", + "UserProfilesUserId": "معرف المستخدم:", + "SettingsTabGraphicsBackend": "خلفية الرسومات", + "SettingsTabGraphicsBackendTooltip": "حدد الواجهة الخلفية للرسومات التي سيتم استخدامها في المحاكي.\n\nيعد برنامج فولكان أفضل بشكل عام لجميع بطاقات الرسومات الحديثة، طالما أن برامج التشغيل الخاصة بها محدثة. يتميز فولكان أيضا بتجميع مظللات أسرع (أقل تقطيعا) على جميع بائعي وحدات معالجة الرسومات.\n\nقد يحقق أوبن جي أل نتائج أفضل على وحدات معالجة الرسومات إنفيديا القديمة، أو على وحدات معالجة الرسومات إي إم دي القديمة على لينكس، أو على وحدات معالجة الرسومات ذات ذاكرة الوصول العشوائي للفيديوالأقل، على الرغم من أن تعثرات تجميع المظللات ستكون أكبر.\n\nاضبط على فولكان إذا لم تكن متأكدا. اضبط على أوبن جي أل إذا كانت وحدة معالجة الرسومات الخاصة بك لا تدعم فولكان حتى مع أحدث برامج تشغيل الرسومات.", + "SettingsEnableTextureRecompression": "تمكين إعادة ضغط التكستر", + "SettingsEnableTextureRecompressionTooltip": "يضغط تكستر ASTC من أجل تقليل استخدام ذاكرة الوصول العشوائي للفيديو.\n\nتتضمن الألعاب التي تستخدم تنسيق النسيج هذا Astral Chain وBayonetta 3 وFire Emblem Engage وMetroid Prime Remastered وSuper Mario Bros. Wonder وThe Legend of Zelda: Tears of the Kingdom.\n\nمن المحتمل أن تتعطل بطاقات الرسومات التي تحتوي على 4 جيجا بايت من ذاكرة الوصول العشوائي للفيديو أو أقل في مرحلة ما أثناء تشغيل هذه الألعاب.\n\nقم بالتمكين فقط في حالة نفاد ذاكرة الوصول العشوائي للفيديو في الألعاب المذكورة أعلاه. اتركه معطلا إذا لم تكن متأكدا.", + "SettingsTabGraphicsPreferredGpu": "وحدة معالجة الرسوميات المفضلة", + "SettingsTabGraphicsPreferredGpuTooltip": "حدد بطاقة الرسومات التي سيتم استخدامها مع الواجهة الخلفية لرسومات فولكان.\n\nلا يؤثر على وحدة معالجة الرسومات التي سيستخدمها أوبن جي أل.\n\nاضبط على وحدة معالجة الرسومات التي تم وضع علامة عليها كـ \"dGPU\" إذا لم تكن متأكدًا. إذا لم يكن هناك واحد، اتركه.", + "SettingsAppRequiredRestartMessage": "مطلوب إعادة تشغيل ريوجينكس", + "SettingsGpuBackendRestartMessage": "تم تعديل إعدادات الواجهة الخلفية للرسومات أو وحدة معالجة الرسومات. سيتطلب هذا إعادة التشغيل ليتم تطبيقه", + "SettingsGpuBackendRestartSubMessage": "\n\nهل تريد إعادة التشغيل الآن؟", + "RyujinxUpdaterMessage": "هل تريد تحديث ريوجينكس إلى أحدث إصدار؟", + "SettingsTabHotkeysVolumeUpHotkey": "زيادة مستوى الصوت:", + "SettingsTabHotkeysVolumeDownHotkey": "خفض مستوى الصوت:", + "SettingsEnableMacroHLE": "تمكين Maro HLE", + "SettingsEnableMacroHLETooltip": "محاكاة عالية المستوى لكود مايكرو وحدة معالجة الرسوميات.\n\nيعمل على تحسين الأداء، ولكنه قد يسبب خللا رسوميا في بعض الألعاب.\n\nاتركه مفعلا إذا لم تكن متأكدا.", + "SettingsEnableColorSpacePassthrough": "عبور مساحة اللون", + "SettingsEnableColorSpacePassthroughTooltip": "يوجه واجهة فولكان الخلفية لتمرير معلومات الألوان دون تحديد مساحة اللون. بالنسبة للمستخدمين الذين لديهم شاشات ذات نطاق واسع، قد يؤدي ذلك إلى الحصول على ألوان أكثر حيوية، على حساب صحة الألوان.", + "VolumeShort": "مستوى", + "UserProfilesManageSaves": "إدارة الحفظ", + "DeleteUserSave": "هل تريد حذف حفظ المستخدم لهذه اللعبة؟", + "IrreversibleActionNote": "هذا الإجراء لا يمكن التراجع عنه.", + "SaveManagerHeading": "إدارة الحفظ لـ {0} ({1})", + "SaveManagerTitle": "مدير الحفظ", + "Name": "الاسم", + "Size": "الحجم", + "Search": "بحث", + "UserProfilesRecoverLostAccounts": "استعادة الحسابات المفقودة", + "Recover": "استعادة", + "UserProfilesRecoverHeading": "تم العثور على حفظ للحسابات التالية", + "UserProfilesRecoverEmptyList": "لا توجد ملفات شخصية لاستردادها", + "GraphicsAATooltip": "يتم تطبيق تنعيم الحواف على عرض اللعبة.\n\nسوف يقوم FXAA بتعتيم معظم الصورة، بينما سيحاول SMAA العثور على حواف خشنة وتنعيمها.\n\nلا ينصح باستخدامه مع فلتر FSR لتكبير.\n\nيمكن تغيير هذا الخيار أثناء تشغيل اللعبة بالنقر فوق \"تطبيق\" أدناه؛ يمكنك ببساطة تحريك نافذة الإعدادات جانبا والتجربة حتى تجد المظهر المفضل للعبة.\n\nاتركه على لا شيء إذا لم تكن متأكدا.", + "GraphicsAALabel": "تنعيم الحواف:", + "GraphicsScalingFilterLabel": "فلتر التكبير:", + "GraphicsScalingFilterTooltip": "اختر فلتر التكبير الذي سيتم تطبيقه عند استخدام مقياس الدقة.\n\nيعمل Bilinear بشكل جيد مع الألعاب ثلاثية الأبعاد وهو خيار افتراضي آمن.\n\nيوصى باستخدام Nearest لألعاب البكسل الفنية.\n\nFSR 1.0 هو مجرد مرشح توضيحي، ولا ينصح باستخدامه مع FXAA أو SMAA.\n\nيمكن تغيير هذا الخيار أثناء تشغيل اللعبة بالنقر فوق \"تطبيق\" أدناه؛ يمكنك ببساطة تحريك نافذة الإعدادات جانبا والتجربة حتى تجد المظهر المفضل للعبة.\n\nاتركه على Bilinear إذا لم تكن متأكدا.", + "GraphicsScalingFilterBilinear": "Bilinear", + "GraphicsScalingFilterNearest": "Nearest", + "GraphicsScalingFilterFsr": "FSR", + "GraphicsScalingFilterArea": "Area", + "GraphicsScalingFilterLevelLabel": "المستوى", + "GraphicsScalingFilterLevelTooltip": "اضبط مستوى وضوح FSR 1.0. الأعلى هو أكثر وضوحا.", + "SmaaLow": "SMAA منخفض", + "SmaaMedium": "SMAA متوسط", + "SmaaHigh": "SMAA عالي", + "SmaaUltra": "SMAA فائق", + "UserEditorTitle": "تعديل المستخدم", + "UserEditorTitleCreate": "إنشاء مستخدم", + "SettingsTabNetworkInterface": "واجهة الشبكة:", + "NetworkInterfaceTooltip": "واجهة الشبكة مستخدمة لميزات LAN/LDN.\n\nبالاشتراك مع VPN أو XLink Kai ولعبة تدعم LAN، يمكن استخدامها لتزييف اتصال الشبكة نفسها عبر الإنترنت.\n\nاتركه على الافتراضي إذا لم تكن متأكدا.", + "NetworkInterfaceDefault": "افتراضي", + "PackagingShaders": "تعبئة المظللات", + "AboutChangelogButton": "عرض سجل التغييرات على غيت هاب", + "AboutChangelogButtonTooltipMessage": "انقر لفتح سجل التغيير لهذا الإصدار في متصفحك الافتراضي.", + "SettingsTabNetworkMultiplayer": "لعب جماعي", + "MultiplayerMode": "الوضع:", + "MultiplayerModeTooltip": "تغيير وضع LDN متعدد اللاعبين.\n\nسوف يقوم LdnMitm بتعديل وظيفة اللعب المحلية/اللاسلكية المحلية في الألعاب لتعمل كما لو كانت شبكة LAN، مما يسمح باتصالات الشبكة المحلية نفسها مع محاكيات ريوجينكس الأخرى وأجهزة نينتندو سويتش المخترقة التي تم تثبيت وحدة ldn_mitm عليها.\n\nيتطلب وضع اللاعبين المتعددين أن يكون جميع اللاعبين على نفس إصدار اللعبة (على سبيل المثال، يتعذر على الإصدار 13.0.1 من سوبر سماش برذرز ألتميت الاتصال بالإصدار 13.0.0).\n\nاتركه معطلا إذا لم تكن متأكدا.", + "MultiplayerModeDisabled": "معطل", + "MultiplayerModeLdnMitm": "ldn_mitm", + "MultiplayerModeLdnRyu": "RyuLDN", + "MultiplayerDisableP2P": "Disable P2P Network Hosting (may increase latency)", + "MultiplayerDisableP2PTooltip": "Disable P2P network hosting, peers will proxy through the master server instead of connecting to you directly.", + "LdnPassphrase": "Network Passphrase:", + "LdnPassphraseTooltip": "You will only be able to see hosted games with the same passphrase as you.", + "LdnPassphraseInputTooltip": "Enter a passphrase in the format Ryujinx-<8 hex chars>. You will only be able to see hosted games with the same passphrase as you.", + "LdnPassphraseInputPublic": "(public)", + "GenLdnPass": "Generate Random", + "GenLdnPassTooltip": "Generates a new passphrase, which can be shared with other players.", + "ClearLdnPass": "Clear", + "ClearLdnPassTooltip": "Clears the current passphrase, returning to the public network.", + "InvalidLdnPassphrase": "Invalid Passphrase! Must be in the format \"Ryujinx-<8 hex chars>\"" +} diff --git a/src/Ryujinx/Assets/Locales/de_DE.json b/src/Ryujinx/Assets/Locales/de_DE.json new file mode 100644 index 000000000..91141b7af --- /dev/null +++ b/src/Ryujinx/Assets/Locales/de_DE.json @@ -0,0 +1,868 @@ +{ + "Language": "Deutsch", + "MenuBarFileOpenApplet": "Öffne Anwendung", + "MenuBarFileOpenAppletOpenMiiApplet": "Mii Edit Applet", + "MenuBarFileOpenAppletOpenMiiAppletToolTip": "Öffnet das Mii-Editor-Applet im Standalone-Modus", + "SettingsTabInputDirectMouseAccess": "Direkter Mauszugriff", + "SettingsTabSystemMemoryManagerMode": "Speichermanagermodus:", + "SettingsTabSystemMemoryManagerModeSoftware": "Software", + "SettingsTabSystemMemoryManagerModeHost": "Host (schnell)", + "SettingsTabSystemMemoryManagerModeHostUnchecked": "Host ungeprüft (am schnellsten, unsicher)", + "SettingsTabSystemUseHypervisor": "Hypervisor verwenden", + "MenuBarFile": "_Datei", + "MenuBarFileOpenFromFile": "Datei _öffnen", + "MenuBarFileOpenFromFileError": "No applications found in selected file.", + "MenuBarFileOpenUnpacked": "_Entpacktes Spiel öffnen", + "MenuBarFileLoadDlcFromFolder": "Load DLC From Folder", + "MenuBarFileLoadTitleUpdatesFromFolder": "Load Title Updates From Folder", + "MenuBarFileOpenEmuFolder": "Ryujinx-Ordner öffnen", + "MenuBarFileOpenLogsFolder": "Logs-Ordner öffnen", + "MenuBarFileExit": "_Beenden", + "MenuBarOptions": "_Optionen", + "MenuBarOptionsToggleFullscreen": "Vollbild", + "MenuBarOptionsStartGamesInFullscreen": "Spiele im Vollbildmodus starten", + "MenuBarOptionsStopEmulation": "Emulation beenden", + "MenuBarOptionsSettings": "_Einstellungen", + "MenuBarOptionsManageUserProfiles": "_Benutzerprofile verwalten", + "MenuBarActions": "_Aktionen", + "MenuBarOptionsSimulateWakeUpMessage": "Aufwachnachricht simulieren", + "MenuBarActionsScanAmiibo": "Amiibo scannen", + "MenuBarTools": "_Tools", + "MenuBarToolsInstallFirmware": "Firmware installieren", + "MenuBarFileToolsInstallFirmwareFromFile": "Firmware von einer XCI- oder einer ZIP-Datei installieren", + "MenuBarFileToolsInstallFirmwareFromDirectory": "Firmware aus einem Verzeichnis installieren", + "MenuBarToolsManageFileTypes": "Dateitypen verwalten", + "MenuBarToolsInstallFileTypes": "Dateitypen installieren", + "MenuBarToolsUninstallFileTypes": "Dateitypen deinstallieren", + "MenuBarToolsXCITrimmer": "Trim XCI Files", + "MenuBarView": "_Ansicht", + "MenuBarViewWindow": "Fenstergröße", + "MenuBarViewWindow720": "720p", + "MenuBarViewWindow1080": "1080p", + "MenuBarHelp": "_Hilfe", + "MenuBarHelpCheckForUpdates": "Nach Updates suchen", + "MenuBarHelpAbout": "Über Ryujinx", + "MenuSearch": "Suchen...", + "GameListHeaderFavorite": "Favorit", + "GameListHeaderIcon": "Icon", + "GameListHeaderApplication": "Name", + "GameListHeaderDeveloper": "Entwickler", + "GameListHeaderVersion": "Version", + "GameListHeaderTimePlayed": "Spielzeit", + "GameListHeaderLastPlayed": "Zuletzt gespielt", + "GameListHeaderFileExtension": "Dateiformat", + "GameListHeaderFileSize": "Dateigröße", + "GameListHeaderPath": "Pfad", + "GameListContextMenuOpenUserSaveDirectory": "Spielstand-Verzeichnis öffnen", + "GameListContextMenuOpenUserSaveDirectoryToolTip": "Öffnet das Verzeichnis, welches den Benutzer-Spielstand beinhaltet", + "GameListContextMenuOpenDeviceSaveDirectory": "Benutzer-Geräte-Verzeichnis öffnen", + "GameListContextMenuOpenDeviceSaveDirectoryToolTip": "Öffnet das Verzeichnis, welches den Geräte-Spielstände beinhaltet", + "GameListContextMenuOpenBcatSaveDirectory": "Benutzer-BCAT-Vezeichnis öffnen", + "GameListContextMenuOpenBcatSaveDirectoryToolTip": "Öffnet das Verzeichnis, welches den BCAT-Cache des Spiels beinhaltet", + "GameListContextMenuManageTitleUpdates": "Verwalte Spiel-Updates", + "GameListContextMenuManageTitleUpdatesToolTip": "Öffnet den Spiel-Update-Manager", + "GameListContextMenuManageDlc": "Verwalten von DLC", + "GameListContextMenuManageDlcToolTip": "Öffnet den DLC-Manager", + "GameListContextMenuCacheManagement": "Cache-Verwaltung", + "GameListContextMenuCacheManagementPurgePptc": "PPTC als ungültig markieren", + "GameListContextMenuCacheManagementPurgePptcToolTip": "Markiert den PPTC als ungültig, sodass dieser beim nächsten Spielstart neu erstellt wird", + "GameListContextMenuCacheManagementPurgeShaderCache": "Shader Cache löschen", + "GameListContextMenuCacheManagementPurgeShaderCacheToolTip": "Löscht den Shader-Cache der Anwendung", + "GameListContextMenuCacheManagementOpenPptcDirectory": "PPTC-Verzeichnis öffnen", + "GameListContextMenuCacheManagementOpenPptcDirectoryToolTip": "Öffnet das Verzeichnis, das den PPTC-Cache der Anwendung beinhaltet", + "GameListContextMenuCacheManagementOpenShaderCacheDirectory": "Shader-Cache-Verzeichnis öffnen", + "GameListContextMenuCacheManagementOpenShaderCacheDirectoryToolTip": "Öffnet das Verzeichnis, das den Shader Cache der Anwendung beinhaltet", + "GameListContextMenuExtractData": "Daten extrahieren", + "GameListContextMenuExtractDataExeFS": "ExeFS", + "GameListContextMenuExtractDataExeFSToolTip": "Extrahiert das ExeFS aus der aktuellen Anwendungskonfiguration (einschließlich Updates)", + "GameListContextMenuExtractDataRomFS": "RomFS", + "GameListContextMenuExtractDataRomFSToolTip": "Extrahiert das RomFS aus der aktuellen Anwendungskonfiguration (einschließlich Updates)", + "GameListContextMenuExtractDataLogo": "Logo", + "GameListContextMenuExtractDataLogoToolTip": "Extrahiert das Logo aus der aktuellen Anwendungskonfiguration (einschließlich Updates)", + "GameListContextMenuCreateShortcut": "Erstelle Anwendungsverknüpfung", + "GameListContextMenuCreateShortcutToolTip": "Erstelle eine Desktop-Verknüpfung die die gewählte Anwendung startet", + "GameListContextMenuCreateShortcutToolTipMacOS": "Erstellen Sie eine Verknüpfung im MacOS-Programme-Ordner, die die ausgewählte Anwendung startet", + "GameListContextMenuOpenModsDirectory": "Mod-Verzeichnis öffnen", + "GameListContextMenuOpenModsDirectoryToolTip": "Öffnet das Verzeichnis, welches Mods für die Spiele beinhaltet", + "GameListContextMenuOpenSdModsDirectory": "Atmosphere-Mod-Verzeichnis öffnen", + "GameListContextMenuOpenSdModsDirectoryToolTip": "Öffnet das alternative SD-Karten-Atmosphere-Verzeichnis, das die Mods der Anwendung enthält. Dieser Ordner ist nützlich für Mods, die für echte Hardware erstellt worden sind.", + "GameListContextMenuTrimXCI": "Check and Trim XCI File", + "GameListContextMenuTrimXCIToolTip": "Check and Trim XCI File to Save Disk Space", + "StatusBarGamesLoaded": "{0}/{1} Spiele geladen", + "StatusBarSystemVersion": "Systemversion: {0}", + "StatusBarXCIFileTrimming": "Trimming XCI File '{0}'", + "LinuxVmMaxMapCountDialogTitle": "Niedriges Limit für Speicherzuordnungen erkannt", + "LinuxVmMaxMapCountDialogTextPrimary": "Möchtest Du den Wert von vm.max_map_count auf {0} erhöhen", + "LinuxVmMaxMapCountDialogTextSecondary": "Einige Spiele könnten versuchen, mehr Speicherzuordnungen zu erstellen, als derzeit erlaubt. Ryujinx wird abstürzen, sobald dieses Limit überschritten wird.", + "LinuxVmMaxMapCountDialogButtonUntilRestart": "Ja, bis zum nächsten Neustart", + "LinuxVmMaxMapCountDialogButtonPersistent": "Ja, permanent", + "LinuxVmMaxMapCountWarningTextPrimary": "Maximale Anzahl an Speicherzuordnungen ist niedriger als empfohlen.", + "LinuxVmMaxMapCountWarningTextSecondary": "Der aktuelle Wert von vm.max_map_count ({0}) ist kleiner als {1}. Einige Spiele könnten versuchen, mehr Speicherzuordnungen zu erstellen, als derzeit erlaubt. Ryujinx wird abstürzen, sobald dieses Limit überschritten wird.\n\nDu kannst das Limit entweder manuell erhöhen oder pkexec installieren, damit Ryujinx Dir dabei hilft.", + "Settings": "Einstellungen", + "SettingsTabGeneral": "Oberfläche", + "SettingsTabGeneralGeneral": "Allgemein", + "SettingsTabGeneralEnableDiscordRichPresence": "Aktiviere die Statusanzeige für Discord", + "SettingsTabGeneralCheckUpdatesOnLaunch": "Beim Start nach Updates suchen", + "SettingsTabGeneralShowConfirmExitDialog": "Zeige den \"Beenden bestätigen\"-Dialog", + "SettingsTabGeneralRememberWindowState": "Fenstergröße/-position merken", + "SettingsTabGeneralShowTitleBar": "Show Title Bar (Requires restart)", + "SettingsTabGeneralHideCursor": "Mauszeiger ausblenden", + "SettingsTabGeneralHideCursorNever": "Niemals", + "SettingsTabGeneralHideCursorOnIdle": "Mauszeiger bei Inaktivität ausblenden", + "SettingsTabGeneralHideCursorAlways": "Immer", + "SettingsTabGeneralGameDirectories": "Spielverzeichnisse", + "SettingsTabGeneralAutoloadDirectories": "Autoload DLC/Updates Directories", + "SettingsTabGeneralAutoloadNote": "DLC and Updates which refer to missing files will be unloaded automatically", + "SettingsTabGeneralAdd": "Hinzufügen", + "SettingsTabGeneralRemove": "Entfernen", + "SettingsTabSystem": "System", + "SettingsTabSystemCore": "Kern", + "SettingsTabSystemSystemRegion": "Systemregion:", + "SettingsTabSystemSystemRegionJapan": "Japan", + "SettingsTabSystemSystemRegionUSA": "USA", + "SettingsTabSystemSystemRegionEurope": "Europa", + "SettingsTabSystemSystemRegionAustralia": "Australien", + "SettingsTabSystemSystemRegionChina": "China", + "SettingsTabSystemSystemRegionKorea": "Korea", + "SettingsTabSystemSystemRegionTaiwan": "Taiwan", + "SettingsTabSystemSystemLanguage": "Systemsprache:", + "SettingsTabSystemSystemLanguageJapanese": "Japanisch", + "SettingsTabSystemSystemLanguageAmericanEnglish": "Amerikanisches Englisch", + "SettingsTabSystemSystemLanguageFrench": "Französisch", + "SettingsTabSystemSystemLanguageGerman": "Deutsch", + "SettingsTabSystemSystemLanguageItalian": "Italienisch", + "SettingsTabSystemSystemLanguageSpanish": "Spanisch", + "SettingsTabSystemSystemLanguageChinese": "Chinesisch", + "SettingsTabSystemSystemLanguageKorean": "Koreanisch", + "SettingsTabSystemSystemLanguageDutch": "Niederländisch", + "SettingsTabSystemSystemLanguagePortuguese": "Portugiesisch", + "SettingsTabSystemSystemLanguageRussian": "Russisch", + "SettingsTabSystemSystemLanguageTaiwanese": "Taiwanesisch", + "SettingsTabSystemSystemLanguageBritishEnglish": "Britisches Englisch", + "SettingsTabSystemSystemLanguageCanadianFrench": "Kanadisches Französisch", + "SettingsTabSystemSystemLanguageLatinAmericanSpanish": "Lateinamerikanisches Spanisch", + "SettingsTabSystemSystemLanguageSimplifiedChinese": "Vereinfachtes Chinesisch", + "SettingsTabSystemSystemLanguageTraditionalChinese": "Traditionelles Chinesisch", + "SettingsTabSystemSystemTimeZone": "System-Zeitzone:", + "SettingsTabSystemSystemTime": "Systemzeit:", + "SettingsTabSystemEnableVsync": "VSync", + "SettingsTabSystemEnablePptc": "PPTC (Profiled Persistent Translation Cache)", + "SettingsTabSystemEnableLowPowerPptc": "Kleinleistungs-PPTC", + "SettingsTabSystemEnableFsIntegrityChecks": "FS Integritätsprüfung", + "SettingsTabSystemAudioBackend": "Audio-Backend:", + "SettingsTabSystemAudioBackendDummy": "Ohne Funktion", + "SettingsTabSystemAudioBackendOpenAL": "OpenAL", + "SettingsTabSystemAudioBackendSoundIO": "SoundIO", + "SettingsTabSystemAudioBackendSDL2": "SDL2", + "SettingsTabSystemHacks": "Hacks", + "SettingsTabSystemHacksNote": " (Kann Fehler verursachen)", + "SettingsTabSystemDramSize": "DRAM Größe:", + "SettingsTabSystemDramSize4GiB": "4GiB", + "SettingsTabSystemDramSize6GiB": "6GiB", + "SettingsTabSystemDramSize8GiB": "8GiB", + "SettingsTabSystemDramSize12GiB": "12GiB", + "SettingsTabSystemIgnoreMissingServices": "Ignoriere fehlende Dienste", + "SettingsTabSystemIgnoreApplet": "Applet ignorieren", + "SettingsTabGraphics": "Grafik", + "SettingsTabGraphicsAPI": "Grafik-API", + "SettingsTabGraphicsEnableShaderCache": "Shader-Cache aktivieren", + "SettingsTabGraphicsAnisotropicFiltering": "Anisotrope Filterung:", + "SettingsTabGraphicsAnisotropicFilteringAuto": "Auto", + "SettingsTabGraphicsAnisotropicFiltering2x": "2x", + "SettingsTabGraphicsAnisotropicFiltering4x": "4x", + "SettingsTabGraphicsAnisotropicFiltering8x": "8x", + "SettingsTabGraphicsAnisotropicFiltering16x": "16x", + "SettingsTabGraphicsResolutionScale": "Auflösungsskalierung:", + "SettingsTabGraphicsResolutionScaleCustom": "Benutzerdefiniert (nicht empfohlen)", + "SettingsTabGraphicsResolutionScaleNative": "Nativ (720p/1080p)", + "SettingsTabGraphicsResolutionScale2x": "2x (1440p/2160p)", + "SettingsTabGraphicsResolutionScale3x": "3x (2160p/3240p)", + "SettingsTabGraphicsResolutionScale4x": "4x (2880p/4320p) (Nicht empfohlen)", + "SettingsTabGraphicsAspectRatio": "Bildseitenverhältnis:", + "SettingsTabGraphicsAspectRatio4x3": "4:3", + "SettingsTabGraphicsAspectRatio16x9": "16:9", + "SettingsTabGraphicsAspectRatio16x10": "16:10", + "SettingsTabGraphicsAspectRatio21x9": "21:9", + "SettingsTabGraphicsAspectRatio32x9": "32:9", + "SettingsTabGraphicsAspectRatioStretch": "An Fenster anpassen", + "SettingsTabGraphicsDeveloperOptions": "Optionen für Entwickler", + "SettingsTabGraphicsShaderDumpPath": "Grafik-Shader-Dump-Pfad:", + "SettingsTabLogging": "Logs", + "SettingsTabLoggingLogging": "Logs", + "SettingsTabLoggingEnableLoggingToFile": "Protokollierung in Datei aktivieren", + "SettingsTabLoggingEnableStubLogs": "Aktiviere Stub-Logs", + "SettingsTabLoggingEnableInfoLogs": "Aktiviere Info-Logs", + "SettingsTabLoggingEnableWarningLogs": "Aktiviere Warn-Logs", + "SettingsTabLoggingEnableErrorLogs": "Aktiviere Fehler-Logs", + "SettingsTabLoggingEnableTraceLogs": "Aktiviere Trace-Logs", + "SettingsTabLoggingEnableGuestLogs": "Aktiviere Gast-Logs", + "SettingsTabLoggingEnableFsAccessLogs": "Aktiviere Fs Zugriff-Logs", + "SettingsTabLoggingFsGlobalAccessLogMode": "Fs Globaler Zugriff-Log-Modus:", + "SettingsTabLoggingDeveloperOptions": "Entwickleroptionen", + "SettingsTabLoggingDeveloperOptionsNote": "ACHTUNG: Wird die Leistung reduzieren", + "SettingsTabLoggingGraphicsBackendLogLevel": "Protokollstufe des Grafik-Backends:", + "SettingsTabLoggingGraphicsBackendLogLevelNone": "Keine", + "SettingsTabLoggingGraphicsBackendLogLevelError": "Fehler", + "SettingsTabLoggingGraphicsBackendLogLevelPerformance": "Verlangsamungen", + "SettingsTabLoggingGraphicsBackendLogLevelAll": "Alle", + "SettingsTabLoggingEnableDebugLogs": "Aktiviere Debug-Log", + "SettingsTabInput": "Eingabe", + "SettingsTabInputEnableDockedMode": "Angedockter Modus", + "SettingsTabInputDirectKeyboardAccess": "Direkter Tastaturzugriff", + "SettingsButtonSave": "Speichern", + "SettingsButtonClose": "Schließen", + "SettingsButtonOk": "OK", + "SettingsButtonCancel": "Abbrechen", + "SettingsButtonApply": "Übernehmen", + "ControllerSettingsPlayer": "Spieler", + "ControllerSettingsPlayer1": "Spieler 1", + "ControllerSettingsPlayer2": "Spieler 2", + "ControllerSettingsPlayer3": "Spieler 3", + "ControllerSettingsPlayer4": "Spieler 4", + "ControllerSettingsPlayer5": "Spieler 5", + "ControllerSettingsPlayer6": "Spieler 6", + "ControllerSettingsPlayer7": "Spieler 7", + "ControllerSettingsPlayer8": "Spieler 8", + "ControllerSettingsHandheld": "Handheld", + "ControllerSettingsInputDevice": "Eingabegerät", + "ControllerSettingsRefresh": "Aktualisieren", + "ControllerSettingsDeviceDisabled": "Deaktiviert", + "ControllerSettingsControllerType": "Controller-Typ", + "ControllerSettingsControllerTypeHandheld": "Handheld", + "ControllerSettingsControllerTypeProController": "Pro Controller", + "ControllerSettingsControllerTypeJoyConPair": "Joy-Con-Paar", + "ControllerSettingsControllerTypeJoyConLeft": "Linker Joy-Con", + "ControllerSettingsControllerTypeJoyConRight": "Rechter Joy-Con", + "ControllerSettingsProfile": "Profil", + "ControllerSettingsProfileDefault": "Standard", + "ControllerSettingsLoad": "Laden", + "ControllerSettingsAdd": "Hinzufügen", + "ControllerSettingsRemove": "Entfernen", + "ControllerSettingsButtons": "Aktionstasten", + "ControllerSettingsButtonA": "A", + "ControllerSettingsButtonB": "B", + "ControllerSettingsButtonX": "X", + "ControllerSettingsButtonY": "Y", + "ControllerSettingsButtonPlus": "+", + "ControllerSettingsButtonMinus": "-", + "ControllerSettingsDPad": "Steuerkreuz", + "ControllerSettingsDPadUp": "Hoch", + "ControllerSettingsDPadDown": "Runter", + "ControllerSettingsDPadLeft": "Links", + "ControllerSettingsDPadRight": "Rechts", + "ControllerSettingsStickButton": "Button", + "ControllerSettingsStickUp": "Hoch", + "ControllerSettingsStickDown": "Runter", + "ControllerSettingsStickLeft": "Links", + "ControllerSettingsStickRight": "Rechts", + "ControllerSettingsStickStick": "Stick", + "ControllerSettingsStickInvertXAxis": "X-Achse invertieren", + "ControllerSettingsStickInvertYAxis": "Y-Achse invertieren", + "ControllerSettingsStickDeadzone": "Deadzone:", + "ControllerSettingsLStick": "Linker Analogstick", + "ControllerSettingsRStick": "Rechter Analogstick", + "ControllerSettingsTriggersLeft": "Linker Trigger", + "ControllerSettingsTriggersRight": "Rechter Trigger", + "ControllerSettingsTriggersButtonsLeft": "Linke Schultertaste", + "ControllerSettingsTriggersButtonsRight": "Rechte Schultertaste", + "ControllerSettingsTriggers": "Trigger", + "ControllerSettingsTriggerL": "L", + "ControllerSettingsTriggerR": "R", + "ControllerSettingsTriggerZL": "ZL", + "ControllerSettingsTriggerZR": "ZR", + "ControllerSettingsLeftSL": "SL", + "ControllerSettingsLeftSR": "SR", + "ControllerSettingsRightSL": "SL", + "ControllerSettingsRightSR": "SR", + "ControllerSettingsExtraButtonsLeft": "Linke Aktionstasten", + "ControllerSettingsExtraButtonsRight": "Rechte Aktionstasten", + "ControllerSettingsMisc": "Verschiedenes", + "ControllerSettingsTriggerThreshold": "Empfindlichkeit:", + "ControllerSettingsMotion": "Bewegung", + "ControllerSettingsMotionUseCemuhookCompatibleMotion": "CemuHook kompatible Bewegungssteuerung", + "ControllerSettingsMotionControllerSlot": "Controller-Slot:", + "ControllerSettingsMotionMirrorInput": "Eingabe spiegeln", + "ControllerSettingsMotionRightJoyConSlot": "Rechter Joy-Con-Slot:", + "ControllerSettingsMotionServerHost": "Server Host:", + "ControllerSettingsMotionGyroSensitivity": "Gyro-Empfindlichkeit:", + "ControllerSettingsMotionGyroDeadzone": "Gyro-Deadzone:", + "ControllerSettingsSave": "Speichern", + "ControllerSettingsClose": "Schließen", + "KeyUnknown": "Unbekannt", + "KeyShiftLeft": "Shift Left", + "KeyShiftRight": "Shift Right", + "KeyControlLeft": "Ctrl Left", + "KeyMacControlLeft": "⌃ Left", + "KeyControlRight": "Ctrl Right", + "KeyMacControlRight": "⌃ Right", + "KeyAltLeft": "Alt Left", + "KeyMacAltLeft": "⌥ Left", + "KeyAltRight": "Alt Right", + "KeyMacAltRight": "⌥ Right", + "KeyWinLeft": "⊞ Left", + "KeyMacWinLeft": "⌘ Left", + "KeyWinRight": "⊞ Right", + "KeyMacWinRight": "⌘ Right", + "KeyMenu": "Menu", + "KeyUp": "Up", + "KeyDown": "Down", + "KeyLeft": "Left", + "KeyRight": "Right", + "KeyEnter": "Enter", + "KeyEscape": "Escape", + "KeySpace": "Space", + "KeyTab": "Tab", + "KeyBackSpace": "Backspace", + "KeyInsert": "Insert", + "KeyDelete": "Delete", + "KeyPageUp": "Page Up", + "KeyPageDown": "Page Down", + "KeyHome": "Home", + "KeyEnd": "End", + "KeyCapsLock": "Caps Lock", + "KeyScrollLock": "Scroll Lock", + "KeyPrintScreen": "Print Screen", + "KeyPause": "Pause", + "KeyNumLock": "Num Lock", + "KeyClear": "Clear", + "KeyKeypad0": "Keypad 0", + "KeyKeypad1": "Keypad 1", + "KeyKeypad2": "Keypad 2", + "KeyKeypad3": "Keypad 3", + "KeyKeypad4": "Keypad 4", + "KeyKeypad5": "Keypad 5", + "KeyKeypad6": "Keypad 6", + "KeyKeypad7": "Keypad 7", + "KeyKeypad8": "Keypad 8", + "KeyKeypad9": "Keypad 9", + "KeyKeypadDivide": "Keypad Divide", + "KeyKeypadMultiply": "Keypad Multiply", + "KeyKeypadSubtract": "Keypad Subtract", + "KeyKeypadAdd": "Keypad Add", + "KeyKeypadDecimal": "Keypad Decimal", + "KeyKeypadEnter": "Keypad Enter", + "KeyNumber0": "0", + "KeyNumber1": "1", + "KeyNumber2": "2", + "KeyNumber3": "3", + "KeyNumber4": "4", + "KeyNumber5": "5", + "KeyNumber6": "6", + "KeyNumber7": "7", + "KeyNumber8": "8", + "KeyNumber9": "9", + "KeyTilde": "~", + "KeyGrave": "`", + "KeyMinus": "-", + "KeyPlus": "+", + "KeyBracketLeft": "[", + "KeyBracketRight": "]", + "KeySemicolon": ";", + "KeyQuote": "\"", + "KeyComma": ",", + "KeyPeriod": ".", + "KeySlash": "/", + "KeyBackSlash": "\\", + "KeyUnbound": "Unbound", + "GamepadLeftStick": "L Stick Button", + "GamepadRightStick": "R Stick Button", + "GamepadLeftShoulder": "Left Shoulder", + "GamepadRightShoulder": "Right Shoulder", + "GamepadLeftTrigger": "Left Trigger", + "GamepadRightTrigger": "Right Trigger", + "GamepadDpadUp": "Up", + "GamepadDpadDown": "Down", + "GamepadDpadLeft": "Left", + "GamepadDpadRight": "Right", + "GamepadMinus": "-", + "GamepadPlus": "+", + "GamepadGuide": "Guide", + "GamepadMisc1": "Misc", + "GamepadPaddle1": "Paddle 1", + "GamepadPaddle2": "Paddle 2", + "GamepadPaddle3": "Paddle 3", + "GamepadPaddle4": "Paddle 4", + "GamepadTouchpad": "Touchpad", + "GamepadSingleLeftTrigger0": "Left Trigger 0", + "GamepadSingleRightTrigger0": "Right Trigger 0", + "GamepadSingleLeftTrigger1": "Left Trigger 1", + "GamepadSingleRightTrigger1": "Right Trigger 1", + "StickLeft": "Left Stick", + "StickRight": "Right Stick", + "UserProfilesSelectedUserProfile": "Ausgewähltes Profil:", + "UserProfilesSaveProfileName": "Profilname speichern", + "UserProfilesChangeProfileImage": "Profilbild ändern", + "UserProfilesAvailableUserProfiles": "Verfügbare Profile:", + "UserProfilesAddNewProfile": "Neues Profil", + "UserProfilesDelete": "Löschen", + "UserProfilesClose": "Schließen", + "ProfileNameSelectionWatermark": "Wähle einen Spitznamen", + "ProfileImageSelectionTitle": "Auswahl des Profilbildes", + "ProfileImageSelectionHeader": "Wähle ein Profilbild aus", + "ProfileImageSelectionNote": "Es kann ein eigenes Profilbild importiert werden oder ein Avatar aus der System-Firmware", + "ProfileImageSelectionImportImage": "Bilddatei importieren", + "ProfileImageSelectionSelectAvatar": "Firmware-Avatar auswählen", + "InputDialogTitle": "Eingabe-Dialog", + "InputDialogOk": "OK", + "InputDialogCancel": "Abbrechen", + "InputDialogCancelling": "Cancelling", + "InputDialogClose": "Close", + "InputDialogAddNewProfileTitle": "Wähle den Profilnamen", + "InputDialogAddNewProfileHeader": "Bitte gebe einen Profilnamen ein", + "InputDialogAddNewProfileSubtext": "(Maximale Länge: {0})", + "AvatarChoose": "Bestätigen", + "AvatarSetBackgroundColor": "Hintergrundfarbe auswählen", + "AvatarClose": "Schließen", + "ControllerSettingsLoadProfileToolTip": "Lädt ein Profil", + "ControllerSettingsViewProfileToolTip": "View Profile", + "ControllerSettingsAddProfileToolTip": "Fügt ein Profil hinzu", + "ControllerSettingsRemoveProfileToolTip": "Entfernt ein Profil", + "ControllerSettingsSaveProfileToolTip": "Speichert ein Profil", + "MenuBarFileToolsTakeScreenshot": "Screenshot aufnehmen", + "MenuBarFileToolsHideUi": "Oberfläche ausblenden", + "GameListContextMenuRunApplication": "Anwendung ausführen", + "GameListContextMenuToggleFavorite": "Als Favoriten hinzufügen/entfernen", + "GameListContextMenuToggleFavoriteToolTip": "Aktiviert den Favoriten-Status des Spiels", + "SettingsTabGeneralTheme": "Design:", + "SettingsTabGeneralThemeAuto": "Auto", + "SettingsTabGeneralThemeDark": "Dunkel", + "SettingsTabGeneralThemeLight": "Hell", + "ControllerSettingsConfigureGeneral": "Konfigurieren", + "ControllerSettingsRumble": "Vibration", + "ControllerSettingsRumbleStrongMultiplier": "Starker Vibrations-Multiplikator", + "ControllerSettingsRumbleWeakMultiplier": "Schwacher Vibrations-Multiplikator", + "DialogMessageSaveNotAvailableMessage": "Es existieren keine Speicherdaten für {0} [{1:x16}]", + "DialogMessageSaveNotAvailableCreateSaveMessage": "Sollen Speicherdaten für dieses Spiel erstellt werden?", + "DialogConfirmationTitle": "Ryujinx - Bestätigung", + "DialogUpdaterTitle": "Ryujinx - Updater", + "DialogErrorTitle": "Ryujinx - Fehler", + "DialogWarningTitle": "Ryujinx - Warnung", + "DialogExitTitle": "Ryujinx - Beenden", + "DialogErrorMessage": "Ein Fehler ist aufgetreten", + "DialogExitMessage": "Ryujinx wirklich schließen?", + "DialogExitSubMessage": "Alle nicht gespeicherten Daten gehen verloren!", + "DialogMessageCreateSaveErrorMessage": "Es ist ein Fehler bei der Erstellung der angegebenen Speicherdaten aufgetreten: {0}", + "DialogMessageFindSaveErrorMessage": "Es ist ein Fehler beim Suchen der angegebenen Speicherdaten aufgetreten: {0}", + "FolderDialogExtractTitle": "Wähle den Ordner, in welchen die Dateien entpackt werden sollen", + "DialogNcaExtractionMessage": "Extrahiert {0} abschnitt von {1}...", + "DialogNcaExtractionTitle": "NCA-Abschnitt-Extraktor", + "DialogNcaExtractionMainNcaNotFoundErrorMessage": "Extraktion fehlgeschlagen. Der Hauptheader der NCA war in der ausgewählten Datei nicht vorhanden.", + "DialogNcaExtractionCheckLogErrorMessage": "Extraktion fehlgeschlagen. Überprüfe die Logs für weitere Informationen.", + "DialogNcaExtractionSuccessMessage": "Extraktion erfolgreich abgeschlossen.", + "DialogUpdaterConvertFailedMessage": "Die Konvertierung der aktuellen Ryujinx-Version ist fehlgeschlagen.", + "DialogUpdaterCancelUpdateMessage": "Update wird abgebrochen!", + "DialogUpdaterAlreadyOnLatestVersionMessage": "Es wird bereits die aktuellste Version von Ryujinx benutzt", + "DialogUpdaterFailedToGetVersionMessage": "Beim Versuch, Veröffentlichungs-Info von GitHub Release zu erhalten, ist ein Fehler aufgetreten. Dies kann aufgrund einer neuen Veröffentlichung, die gerade von GitHub Actions kompiliert wird, verursacht werden.", + "DialogUpdaterConvertFailedGithubMessage": "Fehler beim Konvertieren der erhaltenen Ryujinx-Version von GitHub Release.", + "DialogUpdaterDownloadingMessage": "Update wird heruntergeladen...", + "DialogUpdaterExtractionMessage": "Update wird entpackt...", + "DialogUpdaterRenamingMessage": "Update wird umbenannt...", + "DialogUpdaterAddingFilesMessage": "Update wird hinzugefügt...", + "DialogUpdaterShowChangelogMessage": "Show Changelog", + "DialogUpdaterCompleteMessage": "Update abgeschlossen!", + "DialogUpdaterRestartMessage": "Ryujinx jetzt neu starten?", + "DialogUpdaterNoInternetMessage": "Es besteht keine Verbindung mit dem Internet!", + "DialogUpdaterNoInternetSubMessage": "Bitte vergewissern, dass eine funktionierende Internetverbindung existiert!", + "DialogUpdaterDirtyBuildMessage": "Inoffizielle Versionen von Ryujinx können nicht aktualisiert werden", + "DialogUpdaterDirtyBuildSubMessage": "Lade Ryujinx bitte von hier herunter, um eine unterstützte Version zu erhalten: https://ryujinx.app/download", + "DialogRestartRequiredMessage": "Neustart erforderlich", + "DialogThemeRestartMessage": "Das Design wurde gespeichert. Ein Neustart ist erforderlich, um das Design anzuwenden.", + "DialogThemeRestartSubMessage": "Jetzt neu starten?", + "DialogFirmwareInstallEmbeddedMessage": "Die in diesem Spiel enthaltene Firmware installieren? (Firmware {0})", + "DialogFirmwareInstallEmbeddedSuccessMessage": "Es wurde keine installierte Firmware gefunden, aber Ryujinx konnte die Firmware {0} aus dem bereitgestellten Spiel installieren.\nRyujinx wird nun gestartet.", + "DialogFirmwareNoFirmwareInstalledMessage": "Keine Firmware installiert", + "DialogFirmwareInstalledMessage": "Firmware {0} wurde installiert", + "DialogInstallFileTypesSuccessMessage": "Dateitypen erfolgreich installiert!", + "DialogInstallFileTypesErrorMessage": "Dateitypen konnten nicht installiert werden.", + "DialogUninstallFileTypesSuccessMessage": "Dateitypen erfolgreich deinstalliert!", + "DialogUninstallFileTypesErrorMessage": "Deinstallation der Dateitypen fehlgeschlagen.", + "DialogOpenSettingsWindowLabel": "Fenster-Einstellungen öffnen", + "DialogOpenXCITrimmerWindowLabel": "XCI Trimmer Window", + "DialogControllerAppletTitle": "Controller-Applet", + "DialogMessageDialogErrorExceptionMessage": "Fehler bei der Anzeige des Meldungs-Dialogs: {0}", + "DialogSoftwareKeyboardErrorExceptionMessage": "Fehler bei der Anzeige der Software-Tastatur: {0}", + "DialogErrorAppletErrorExceptionMessage": "Fehler beim Anzeigen des ErrorApplet-Dialogs: {0}", + "DialogUserErrorDialogMessage": "{0}: {1}", + "DialogUserErrorDialogInfoMessage": "\nWeitere Informationen zur Behebung dieses Fehlers können in unserem Setup-Guide gefunden werden.", + "DialogUserErrorDialogTitle": "Ryujinx Fehler ({0})", + "DialogAmiiboApiTitle": "Amiibo-API", + "DialogAmiiboApiFailFetchMessage": "Beim Abrufen von Informationen aus der API ist ein Fehler aufgetreten.", + "DialogAmiiboApiConnectErrorMessage": "Verbindung zum Amiibo API Server kann nicht hergestellt werden. Der Dienst ist möglicherweise nicht verfügbar oder es existiert keine Internetverbindung.", + "DialogProfileInvalidProfileErrorMessage": "Das Profil {0} ist mit dem aktuellen Eingabekonfigurationssystem nicht kompatibel.", + "DialogProfileDefaultProfileOverwriteErrorMessage": "Das Standardprofil kann nicht überschrieben werden", + "DialogProfileDeleteProfileTitle": "Profil löschen", + "DialogProfileDeleteProfileMessage": "Diese Aktion kann nicht rückgängig gemacht werden. Wirklich fortfahren?", + "DialogWarning": "Warnung", + "DialogPPTCDeletionMessage": "Du bist dabei den PPTC für das folgende Spiel als ungültig zu markieren:\n\n{0}\n\nWirklich fortfahren?", + "DialogPPTCDeletionErrorMessage": "Fehler bei der Löschung des PPTC Caches bei {0}: {1}", + "DialogShaderDeletionMessage": "Du bist dabei, den Shader Cache zu löschen für :\n\n{0}\n\nWirklich fortfahren?", + "DialogShaderDeletionErrorMessage": "Es ist ein Fehler bei der Löschung des Shader Caches bei {0}: {1} aufgetreten", + "DialogRyujinxErrorMessage": "Ein Fehler ist aufgetreten", + "DialogInvalidTitleIdErrorMessage": "UI Fehler: Das ausgewählte Spiel hat keine gültige Titel-ID", + "DialogFirmwareInstallerFirmwareNotFoundErrorMessage": "Es wurde keine gültige System-Firmware gefunden in {0}.", + "DialogFirmwareInstallerFirmwareInstallTitle": "Installiere Firmware {0}", + "DialogFirmwareInstallerFirmwareInstallMessage": "Systemversion {0} wird jetzt installiert.", + "DialogFirmwareInstallerFirmwareInstallSubMessage": "\n\nDies wird die aktuelle Systemversion {0} ersetzen.", + "DialogFirmwareInstallerFirmwareInstallConfirmMessage": "\n\nMöchtest du fortfahren?", + "DialogFirmwareInstallerFirmwareInstallWaitMessage": "Firmware wird installiert...", + "DialogFirmwareInstallerFirmwareInstallSuccessMessage": "Systemversion {0} wurde erfolgreich installiert.", + "DialogUserProfileDeletionWarningMessage": "Es können keine anderen Profile geöffnet werden, wenn das ausgewählte Profil gelöscht wird.", + "DialogUserProfileDeletionConfirmMessage": "Möchtest du das ausgewählte Profil löschen?", + "DialogUserProfileUnsavedChangesTitle": "Warnung - Nicht gespeicherte Änderungen", + "DialogUserProfileUnsavedChangesMessage": "Sie haben Änderungen an diesem Nutzerprofil vorgenommen, die nicht gespeichert wurden.", + "DialogUserProfileUnsavedChangesSubMessage": "Möchten Sie Ihre Änderungen wirklich verwerfen?", + "DialogControllerSettingsModifiedConfirmMessage": "Die aktuellen Controller-Einstellungen wurden aktualisiert.", + "DialogControllerSettingsModifiedConfirmSubMessage": "Controller-Einstellungen speichern?", + "DialogLoadFileErrorMessage": "{0}. Fehlerhafte Datei: {1}", + "DialogModAlreadyExistsMessage": "Mod ist bereits vorhanden", + "DialogModInvalidMessage": "Das angegebene Verzeichnis enthält keine Mods!", + "DialogModDeleteNoParentMessage": "Löschen fehlgeschlagen: Das übergeordnete Verzeichnis für den Mod \"{0}\" konnte nicht gefunden werden!", + "DialogDlcNoDlcErrorMessage": "Die angegebene Datei enthält keinen DLC für den ausgewählten Titel!", + "DialogPerformanceCheckLoggingEnabledMessage": "Es wurde die Debug Protokollierung aktiviert", + "DialogPerformanceCheckLoggingEnabledConfirmMessage": "Um eine optimale Leistung zu erzielen, wird empfohlen, die Debug Protokollierung zu deaktivieren. Debug Protokollierung jetzt deaktivieren?", + "DialogPerformanceCheckShaderDumpEnabledMessage": "Es wurde das Shader Dumping aktiviert, das nur von Entwicklern verwendet werden soll.", + "DialogPerformanceCheckShaderDumpEnabledConfirmMessage": "Für eine optimale Leistung wird empfohlen, das Shader Dumping zu deaktivieren. Shader Dumping jetzt deaktivieren?", + "DialogLoadAppGameAlreadyLoadedMessage": "Es wurde bereits ein Spiel gestartet", + "DialogLoadAppGameAlreadyLoadedSubMessage": "Bitte beende die Emulation oder schließe den Emulator, vor dem Starten eines neuen Spiels", + "DialogUpdateAddUpdateErrorMessage": "Die angegebene Datei enthält keine Updates für den ausgewählten Titel!", + "DialogSettingsBackendThreadingWarningTitle": "Warnung - Render Threading", + "DialogSettingsBackendThreadingWarningMessage": "Ryujinx muss muss neu gestartet werden, damit die Änderungen wirksam werden. Abhängig von dem Betriebssystem muss möglicherweise das Multithreading des Treibers manuell deaktiviert werden, wenn Ryujinx verwendet wird.", + "DialogModManagerDeletionWarningMessage": "Du bist dabei, diesen Mod zu lösche. {0}\n\nMöchtest du wirklich fortfahren?", + "DialogModManagerDeletionAllWarningMessage": "Du bist dabei, alle Mods für diesen Titel zu löschen.\n\nMöchtest du wirklich fortfahren?", + "SettingsTabGraphicsFeaturesOptions": "Erweiterungen", + "SettingsTabGraphicsBackendMultithreading": "Grafik-Backend Multithreading:", + "CommonAuto": "Auto", + "CommonOff": "Aus", + "CommonOn": "An", + "InputDialogYes": "Ja", + "InputDialogNo": "Nein", + "DialogProfileInvalidProfileNameErrorMessage": "Der Dateiname enthält ungültige Zeichen. Bitte erneut versuchen.", + "MenuBarOptionsPauseEmulation": "Pause", + "MenuBarOptionsResumeEmulation": "Fortsetzen", + "AboutUrlTooltipMessage": "Klicke hier, um die Ryujinx Website im Standardbrowser zu öffnen.", + "AboutDisclaimerMessage": "Ryujinx ist in keinster Weise weder mit Nintendo™, \nnoch mit deren Partnern verbunden.", + "AboutAmiiboDisclaimerMessage": "AmiiboAPI (www.amiiboapi.com) wird in unserer Amiibo \nEmulation benutzt.", + "AboutPatreonUrlTooltipMessage": "Klicke hier, um die Ryujinx Patreon Seite im Standardbrowser zu öffnen.", + "AboutGithubUrlTooltipMessage": "Klicke hier, um die Ryujinx GitHub Seite im Standardbrowser zu öffnen.", + "AboutDiscordUrlTooltipMessage": "Klicke hier, um eine Einladung zum Ryujinx Discord Server im Standardbrowser zu öffnen.", + "AboutTwitterUrlTooltipMessage": "Klicke hier, um die Ryujinx Twitter Seite im Standardbrowser zu öffnen.", + "AboutRyujinxAboutTitle": "Über:", + "AboutRyujinxAboutContent": "Ryujinx ist ein Nintendo Switch™ Emulator.\nBitte unterstütze uns auf Patreon.\nAuf Twitter oder Discord erfährst du alle Neuigkeiten.\nEntwickler, die an einer Mitarbeit interessiert sind, können auf GitHub oder Discord mehr erfahren.", + "AboutRyujinxMaintainersTitle": "Entwickelt von:", + "AboutRyujinxMaintainersContentTooltipMessage": "Klicke hier, um die Liste der Mitwirkenden im Standardbrowser zu öffnen.", + "AboutRyujinxSupprtersTitle": "Unterstützt auf Patreon von:", + "AmiiboSeriesLabel": "Amiibo-Serie", + "AmiiboCharacterLabel": "Charakter", + "AmiiboScanButtonLabel": "Einscannen", + "AmiiboOptionsShowAllLabel": "Zeige alle Amiibos", + "AmiiboOptionsUsRandomTagLabel": "Hack: Benutze zufällige Tag-UUID", + "DlcManagerTableHeadingEnabledLabel": "Aktiviert", + "DlcManagerTableHeadingTitleIdLabel": "Title-ID", + "DlcManagerTableHeadingContainerPathLabel": "Container-Pfad", + "DlcManagerTableHeadingFullPathLabel": "Vollständiger-Pfad", + "DlcManagerRemoveAllButton": "Entferne alle", + "DlcManagerEnableAllButton": "Alle aktivieren", + "DlcManagerDisableAllButton": "Alle deaktivieren", + "ModManagerDeleteAllButton": "Alle löschen", + "MenuBarOptionsChangeLanguage": "Sprache ändern", + "MenuBarShowFileTypes": "Dateitypen anzeigen", + "CommonSort": "Sortieren", + "CommonShowNames": "Spiel-Namen anzeigen", + "CommonFavorite": "Favoriten", + "OrderAscending": "Aufsteigend", + "OrderDescending": "Absteigend", + "SettingsTabGraphicsFeatures": "Erweiterungen", + "ErrorWindowTitle": "Fehler-Fenster", + "ToggleDiscordTooltip": "Zeige momentanes Spiel auf Discord", + "AddGameDirBoxTooltip": "Gibt das Spielverzeichnis an, das der Liste hinzuzufügt wird", + "AddGameDirTooltip": "Fügt ein neues Spielverzeichnis hinzu", + "RemoveGameDirTooltip": "Entfernt das ausgewähltes Spielverzeichnis", + "AddAutoloadDirBoxTooltip": "Enter an autoload directory to add to the list", + "AddAutoloadDirTooltip": "Add an autoload directory to the list", + "RemoveAutoloadDirTooltip": "Remove selected autoload directory", + "CustomThemeCheckTooltip": "Verwende ein eigenes Design für die Emulator-Benutzeroberfläche", + "CustomThemePathTooltip": "Gibt den Pfad zum Design für die Emulator-Benutzeroberfläche an", + "CustomThemeBrowseTooltip": "Ermöglicht die Suche nach einem benutzerdefinierten Design für die Emulator-Benutzeroberfläche", + "DockModeToggleTooltip": "Im gedockten Modus verhält sich das emulierte System wie eine Nintendo Switch im TV Modus. Dies verbessert die grafische Qualität der meisten Spiele. Umgekehrt führt die Deaktivierung dazu, dass sich das emulierte System wie eine Nintendo Switch im Handheld Modus verhält, was die Grafikqualität beeinträchtigt.\n\nKonfiguriere das Eingabegerät für Spieler 1, um im Docked Modus zu spielen; konfiguriere das Controllerprofil via der Handheld Option, wenn geplant wird den Handheld Modus zu nutzen.\n\nIm Zweifelsfall AN lassen.", + "DirectKeyboardTooltip": "Direkter Zugriff auf die Tastatur (HID). Bietet Spielen Zugriff auf Ihre Tastatur als Texteingabegerät.\n\nFunktioniert nur mit Spielen, die die Tastaturnutzung auf Switch-Hardware nativ unterstützen.\n\nAus lassen, wenn unsicher.", + "DirectMouseTooltip": "Unterstützt den direkten Mauszugriff (HID). Bietet Spielen Zugriff auf Ihre Maus als Zeigegerät.\n\nFunktioniert nur mit Spielen, die nativ die Steuerung mit der Maus auf Switch-Hardware unterstützen (nur sehr wenige).\n\nTouchscreen-Funktionalität ist möglicherweise eingeschränkt, wenn dies aktiviert ist.\n\n Aus lassen, wenn unsicher.", + "RegionTooltip": "Ändert die Systemregion", + "LanguageTooltip": "Ändert die Systemsprache", + "TimezoneTooltip": "Ändert die Systemzeitzone", + "TimeTooltip": "Ändert die Systemzeit", + "VSyncToggleTooltip": "Vertikale Synchronisierung der emulierten Konsole. Diese Option ist quasi ein Frame-Limiter für die meisten Spiele; die Deaktivierung kann dazu führen, dass Spiele mit höherer Geschwindigkeit laufen oder Ladebildschirme länger benötigen/hängen bleiben.\n\nKann beim Spielen mit einem frei wählbaren Hotkey ein- und ausgeschaltet werden (standardmäßig F1). \n\nIm Zweifelsfall AN lassen.", + "PptcToggleTooltip": "Speichert übersetzte JIT-Funktionen, sodass jene nicht jedes Mal übersetzt werden müssen, wenn das Spiel geladen wird.\n\nVerringert Stottern und die Zeit beim zweiten und den darauffolgenden Startvorgängen eines Spiels erheblich.\n\nIm Zweifelsfall AN lassen.", + "LowPowerPptcToggleTooltip": "Lädt den PPTC mit einem Drittel der verfügbaren Prozessorkernen", + "FsIntegrityToggleTooltip": "Prüft beim Startvorgang auf beschädigte Dateien und zeigt bei beschädigten Dateien einen Hash-Fehler (Hash Error) im Log an.\n\nDiese Einstellung hat keinen Einfluss auf die Leistung und hilft bei der Fehlersuche.\n\nIm Zweifelsfall AN lassen.", + "AudioBackendTooltip": "Ändert das Backend, das zum Rendern von Audio verwendet wird.\n\nSDL2 ist das bevorzugte Audio-Backend, OpenAL und SoundIO sind als Alternativen vorhanden. Dummy wird keinen Audio-Output haben.\n\nIm Zweifelsfall SDL2 auswählen.", + "MemoryManagerTooltip": "Ändert wie der Gastspeicher abgebildet wird und wie auf ihn zugegriffen wird. Beinflusst die Leistung der emulierten CPU erheblich.\n\nIm Zweifelsfall Host ungeprüft auswählen.", + "MemoryManagerSoftwareTooltip": "Verwendung einer Software-Seitentabelle für die Adressumsetzung. Höchste Genauigkeit, aber langsamste Leistung.", + "MemoryManagerHostTooltip": "Direkte Zuordnung von Speicher im Host-Adressraum. Viel schnellere JIT-Kompilierung und Ausführung.", + "MemoryManagerUnsafeTooltip": "Direkte Zuordnung des Speichers, aber keine Maskierung der Adresse innerhalb des Gastadressraums vor dem Zugriff. Schneller, aber auf Kosten der Sicherheit. Die Gastanwendung kann von überall in Ryujinx auf den Speicher zugreifen, daher sollte in diesem Modus nur Programme ausgeführt werden denen vertraut wird.", + "UseHypervisorTooltip": "Verwende Hypervisor anstelle von JIT. Verbessert die Leistung stark, falls vorhanden, kann jedoch in seinem aktuellen Zustand instabil sein.", + "DRamTooltip": "Erhöht den Arbeitsspeicher des emulierten Systems von 4 GiB auf 6 GiB.\n\nDies ist nur für Texturenpakete mit höherer Auflösung oder Mods mit 4K-Auflösung nützlich. Diese Option verbessert NICHT die Leistung.\n\nIm Zweifelsfall AUS lassen.", + "IgnoreMissingServicesTooltip": "Durch diese Option werden nicht implementierte Dienste der Switch-Firmware ignoriert. Dies kann dabei helfen, Abstürze beim Starten bestimmter Spiele zu umgehen.\n\nIm Zweifelsfall AUS lassen.", + "IgnoreAppletTooltip": "Der externe Dialog \"Controller-Applet\" wird nicht angezeigt, wenn das Gamepad während des Spiels getrennt wird. Es erfolgt keine Aufforderung, den Dialog zu schließen oder einen neuen Controller einzurichten. Sobald der zuvor getrennte Controller wieder angeschlossen wird, wird das Spiel automatisch fortgesetzt.", + "GraphicsBackendThreadingTooltip": "Führt Grafik-Backend Befehle auf einem zweiten Thread aus.\n\nDies beschleunigt die Shader-Kompilierung, reduziert Stottern und verbessert die Leistung auf GPU-Treibern ohne eigene Multithreading-Unterstützung. Geringfügig bessere Leistung bei Treibern mit Multithreading.\n\nIm Zweifelsfall auf AUTO stellen.", + "GalThreadingTooltip": "Führt Grafik-Backend Befehle auf einem zweiten Thread aus.\n\nDies Beschleunigt die Shader-Kompilierung, reduziert Stottern und verbessert die Leistung auf GPU-Treibern ohne eigene Multithreading-Unterstützung. Geringfügig bessere Leistung bei Treibern mit Multithreading.\n\nIm Zweifelsfall auf auf AUTO stellen.", + "ShaderCacheToggleTooltip": "Speichert einen persistenten Shader Cache, der das Stottern bei nachfolgenden Durchläufen reduziert.\n\nIm Zweifelsfall AN lassen.", + "ResolutionScaleTooltip": "Multipliziert die Rendering-Auflösung des Spiels.\n\nEinige wenige Spiele funktionieren damit nicht und sehen auch bei höherer Auflösung pixelig aus; für diese Spiele müssen Sie möglicherweise Mods finden, die Anti-Aliasing entfernen oder die interne Rendering-Auflösung erhöhen. Für die Verwendung von Letzterem sollten Sie Native wählen.\n\nSie können diese Option ändern, während ein Spiel läuft, indem Sie unten auf \"Übernehmen\" klicken; Sie können das Einstellungsfenster einfach zur Seite schieben und experimentieren, bis Sie Ihr bevorzugtes Aussehen für ein Spiel gefunden haben.\n\nDenken Sie daran, dass 4x für praktisch jedes Setup Overkill ist.", + "ResolutionScaleEntryTooltip": "Fließkomma Auflösungsskalierung, wie 1,5.\n Bei nicht ganzzahligen Werten ist die Wahrscheinlichkeit größer, dass Probleme entstehen, die auch zum Absturz führen können.", + "AnisotropyTooltip": "Stufe der Anisotropen Filterung. Auf Auto setzen, um den vom Spiel geforderten Wert zu verwenden.", + "AspectRatioTooltip": "Seitenverhältnis, das auf das Renderer-Fenster angewendet wird.\n\nÄndern Sie dies nur, wenn Sie einen Seitenverhältnis-Mod für Ihr Spiel verwenden, da sonst die Grafik gestreckt wird.\n\nLassen Sie es auf 16:9, wenn Sie unsicher sind.", + "ShaderDumpPathTooltip": "Grafik-Shader-Dump-Pfad", + "FileLogTooltip": "Speichert die Konsolenausgabe in einer Log-Datei auf der Festplatte. Hat keinen Einfluss auf die Leistung.", + "StubLogTooltip": "Ausgabe von Stub-Logs in der Konsole. Hat keinen Einfluss auf die Leistung.", + "InfoLogTooltip": "Ausgabe von Info-Logs in der Konsole. Hat keinen Einfluss auf die Leistung.", + "WarnLogTooltip": "Ausgabe von Warn-Logs in der Konsole. Hat keinen Einfluss auf die Leistung.", + "ErrorLogTooltip": "Ausgabe von Fehler-Logs in der Konsole. Hat keinen Einfluss auf die Leistung.", + "TraceLogTooltip": "Ausgabe von Trace-Log in der Konsole. Hat keinen Einfluss auf die Leistung.", + "GuestLogTooltip": "Ausgabe von Gast-Logs in der Konsole. Hat keinen Einfluss auf die Leistung.", + "FileAccessLogTooltip": "Ausgabe von FS-Zugriff-Logs in der Konsole.", + "FSAccessLogModeTooltip": "Aktiviert die Ausgabe des FS-Zugriff-Logs in der Konsole. Mögliche Modi sind 0-3", + "DeveloperOptionTooltip": "Mit Vorsicht verwenden", + "OpenGlLogLevel": "Erfordert die Aktivierung der entsprechenden Log-Level", + "DebugLogTooltip": "Ausgabe von Debug-Logs in der Konsole.\n\nVerwende diese Option nur auf ausdrückliche Anweisung von Ryujinx Entwicklern, da sie das Lesen der Protokolle erschwert und die Leistung des Emulators verschlechtert.", + "LoadApplicationFileTooltip": "Öffnet die Dateiauswahl um Datei zu laden, welche mit der Switch kompatibel ist", + "LoadApplicationFolderTooltip": "Öffnet die Dateiauswahl um ein Spiel zu laden, welches mit der Switch kompatibel ist", + "LoadDlcFromFolderTooltip": "Open a file explorer to choose one or more folders to bulk load DLC from", + "LoadTitleUpdatesFromFolderTooltip": "Open a file explorer to choose one or more folders to bulk load title updates from", + "OpenRyujinxFolderTooltip": "Öffnet den Ordner, der das Ryujinx Dateisystem enthält", + "OpenRyujinxLogsTooltip": "Öffnet den Ordner, in welchem die Logs gespeichert werden", + "ExitTooltip": "Beendet Ryujinx", + "OpenSettingsTooltip": "Öffnet das Einstellungsfenster", + "OpenProfileManagerTooltip": "Öffnet das Profilverwaltungsfenster", + "StopEmulationTooltip": "Beendet die Emulation des derzeitigen Spiels und kehrt zu der Spielauswahl zurück", + "CheckUpdatesTooltip": "Sucht nach Updates für Ryujinx", + "OpenAboutTooltip": "Öffnet das 'Über Ryujinx'-Fenster", + "GridSize": "Rastergröße", + "GridSizeTooltip": "Ändert die Größe der Rasterelemente", + "SettingsTabSystemSystemLanguageBrazilianPortuguese": "Brasilianisches Portugiesisch", + "AboutRyujinxContributorsButtonHeader": "Alle Mitwirkenden anzeigen", + "SettingsTabSystemAudioVolume": "Lautstärke: ", + "AudioVolumeTooltip": "Ändert die Lautstärke", + "SettingsTabSystemEnableInternetAccess": "Gast-Internet-Zugang/LAN Modus", + "EnableInternetAccessTooltip": "Erlaubt es der emulierten Anwendung sich mit dem Internet zu verbinden.\n\nSpiele die den LAN-Modus unterstützen, ermöglichen es Ryujinx sich sowohl mit anderen Ryujinx-Systemen, als auch mit offiziellen Nintendo Switch Konsolen zu verbinden. Allerdings nur, wenn diese Option aktiviert ist und die Systeme mit demselben lokalen Netzwerk verbunden sind.\n\nDies erlaubt KEINE Verbindung zu Nintendo-Servern. Kann bei bestimmten Spielen die versuchen sich mit dem Internet zu verbinden zum Absturz führen.\n\nIm Zweifelsfall AUS lassen", + "GameListContextMenuManageCheatToolTip": "Öffnet den Cheat-Manager", + "GameListContextMenuManageCheat": "Cheats verwalten", + "GameListContextMenuManageModToolTip": "Mods verwalten", + "GameListContextMenuManageMod": "Mods verwalten", + "ControllerSettingsStickRange": "Bereich:", + "DialogStopEmulationTitle": "Ryujinx - Beende Emulation", + "DialogStopEmulationMessage": "Emulation wirklich beenden?", + "SettingsTabCpu": "CPU", + "SettingsTabAudio": "Audio", + "SettingsTabNetwork": "Netzwerk", + "SettingsTabNetworkConnection": "Netwerkverbindung", + "SettingsTabCpuCache": "CPU-Cache", + "SettingsTabCpuMemory": "CPU-Speicher", + "DialogUpdaterFlatpakNotSupportedMessage": "Bitte aktualisiere Ryujinx über FlatHub", + "UpdaterDisabledWarningTitle": "Updater deaktiviert!", + "ControllerSettingsRotate90": "Um 90° rotieren", + "IconSize": "Cover Größe", + "IconSizeTooltip": "Ändert die Größe der Spiel-Cover", + "MenuBarOptionsShowConsole": "Zeige Konsole", + "ShaderCachePurgeError": "Es ist ein Fehler beim löschen des Shader Caches aufgetreten bei {0}: {1}", + "UserErrorNoKeys": "Keys nicht gefunden", + "UserErrorNoFirmware": "Firmware nicht gefunden", + "UserErrorFirmwareParsingFailed": "Firmware-Analysierung-Fehler", + "UserErrorApplicationNotFound": "Anwendung nicht gefunden", + "UserErrorUnknown": "Unbekannter Fehler", + "UserErrorUndefined": "Undefinierter Fehler", + "UserErrorNoKeysDescription": "Ryujinx konnte deine 'prod.keys' Datei nicht finden", + "UserErrorNoFirmwareDescription": "Ryujinx konnte keine installierte Firmware finden!", + "UserErrorFirmwareParsingFailedDescription": "Ryujinx konnte die zu verfügung gestellte Firmware nicht analysieren. Ein möglicher Grund dafür sind veraltete keys.", + "UserErrorApplicationNotFoundDescription": "Ryujinx konnte keine valide Anwendung an dem gegeben Pfad finden.", + "UserErrorUnknownDescription": "Ein unbekannter Fehler ist aufgetreten!", + "UserErrorUndefinedDescription": "Ein undefinierter Fehler ist aufgetreten! Dies sollte nicht passieren. Bitte kontaktiere einen Entwickler!", + "OpenSetupGuideMessage": "Öffne den 'Setup Guide'", + "NoUpdate": "Kein Update", + "TitleUpdateVersionLabel": "Version {0} - {1}", + "TitleBundledUpdateVersionLabel": "Bundled: Version {0}", + "TitleBundledDlcLabel": "Bundled:", + "TitleXCIStatusPartialLabel": "Partial", + "TitleXCIStatusTrimmableLabel": "Untrimmed", + "TitleXCIStatusUntrimmableLabel": "Trimmed", + "TitleXCIStatusFailedLabel": "(Failed)", + "TitleXCICanSaveLabel": "Save {0:n0} Mb", + "TitleXCISavingLabel": "Saved {0:n0} Mb", + "RyujinxInfo": "Ryujinx - Info", + "RyujinxConfirm": "Ryujinx - Bestätigung", + "FileDialogAllTypes": "Alle Typen", + "Never": "Niemals", + "SwkbdMinCharacters": "Muss mindestens {0} Zeichen lang sein", + "SwkbdMinRangeCharacters": "Muss {0}-{1} Zeichen lang sein", + "SoftwareKeyboard": "Software-Tastatur", + "SoftwareKeyboardModeNumeric": "Darf nur 0-9 oder \".\" sein", + "SoftwareKeyboardModeAlphabet": "Keine CJK-Zeichen", + "SoftwareKeyboardModeASCII": "Nur ASCII-Text", + "ControllerAppletControllers": "Unterstützte Controller:", + "ControllerAppletPlayers": "Spieler:", + "ControllerAppletDescription": "Ihre aktuelle Konfiguration ist ungültig. Öffnen Sie die Einstellungen und konfigurieren Sie Ihre Eingaben neu.", + "ControllerAppletDocked": "Andockmodus gesetzt. Handheld-Steuerung sollte deaktiviert worden sein.", + "UpdaterRenaming": "Alte Dateien umbenennen...", + "UpdaterRenameFailed": "Der Updater konnte die folgende Datei nicht umbenennen: {0}", + "UpdaterAddingFiles": "Neue Dateien hinzufügen...", + "UpdaterExtracting": "Update extrahieren...", + "UpdaterDownloading": "Update herunterladen...", + "Game": "Spiel", + "Docked": "Docked", + "Handheld": "Handheld", + "ConnectionError": "Verbindungsfehler.", + "AboutPageDeveloperListMore": "{0} und mehr...", + "ApiError": "API Fehler.", + "LoadingHeading": "{0} wird gestartet", + "CompilingPPTC": "PTC wird kompiliert", + "CompilingShaders": "Shader werden kompiliert", + "AllKeyboards": "Alle Tastaturen", + "OpenFileDialogTitle": "Wähle eine unterstützte Datei", + "OpenFolderDialogTitle": "Wähle einen Ordner mit einem entpackten Spiel", + "AllSupportedFormats": "Alle unterstützten Formate", + "RyujinxUpdater": "Ryujinx - Updater", + "SettingsTabHotkeys": "Tastatur-Hotkeys", + "SettingsTabHotkeysHotkeys": "Tastatur-Hotkeys", + "SettingsTabHotkeysToggleVsyncHotkey": "VSync:", + "SettingsTabHotkeysScreenshotHotkey": "Screenshot:", + "SettingsTabHotkeysShowUiHotkey": "Zeige UI:", + "SettingsTabHotkeysPauseHotkey": "Pausieren:", + "SettingsTabHotkeysToggleMuteHotkey": "Stummschalten:", + "ControllerMotionTitle": "Bewegungssteuerung - Einstellungen", + "ControllerRumbleTitle": "Vibration - Einstellungen", + "SettingsSelectThemeFileDialogTitle": "Wähle ein Design für die Emulator-Benutzeroberfläche", + "SettingsXamlThemeFile": "Xaml Design-Datei", + "AvatarWindowTitle": "Profile verwalten - Avatar", + "Amiibo": "Amiibo", + "Unknown": "Unbekannt", + "Usage": "Nutzung", + "Writable": "Beschreibbar", + "SelectDlcDialogTitle": "DLC-Dateien auswählen", + "SelectUpdateDialogTitle": "Update-Datei auswählen", + "SelectModDialogTitle": "Mod-Ordner auswählen", + "TrimXCIFileDialogTitle": "Check and Trim XCI File", + "TrimXCIFileDialogPrimaryText": "This function will first check the empty space and then trim the XCI File to save disk space.", + "TrimXCIFileDialogSecondaryText": "Current File Size: {0:n} MB\nGame Data Size: {1:n} MB\nDisk Space Savings: {2:n} MB", + "TrimXCIFileNoTrimNecessary": "XCI File does not need to be trimmed. Check logs for further details", + "TrimXCIFileNoUntrimPossible": "XCI File cannot be untrimmed. Check logs for further details", + "TrimXCIFileReadOnlyFileCannotFix": "XCI File is Read Only and could not be made writable. Check logs for further details", + "TrimXCIFileFileSizeChanged": "XCI File has changed in size since it was scanned. Please check the file is not being written to and try again.", + "TrimXCIFileFreeSpaceCheckFailed": "XCI File has data in the free space area, it is not safe to trim", + "TrimXCIFileInvalidXCIFile": "XCI File contains invalid data. Check logs for further details", + "TrimXCIFileFileIOWriteError": "XCI File could not be opened for writing. Check logs for further details", + "TrimXCIFileFailedPrimaryText": "Trimming of the XCI file failed", + "TrimXCIFileCancelled": "The operation was cancelled", + "TrimXCIFileFileUndertermined": "No operation was performed", + "UserProfileWindowTitle": "Benutzerprofile verwalten", + "CheatWindowTitle": "Spiel-Cheats verwalten", + "DlcWindowTitle": "Spiel-DLC verwalten", + "ModWindowTitle": "Manage Mods for {0} ({1})", + "UpdateWindowTitle": "Spiel-Updates verwalten", + "XCITrimmerWindowTitle": "XCI File Trimmer", + "XCITrimmerTitleStatusCount": "{0} of {1} Title(s) Selected", + "XCITrimmerTitleStatusCountWithFilter": "{0} of {1} Title(s) Selected ({2} displayed)", + "XCITrimmerTitleStatusTrimming": "Trimming {0} Title(s)...", + "XCITrimmerTitleStatusUntrimming": "Untrimming {0} Title(s)...", + "XCITrimmerTitleStatusFailed": "Failed", + "XCITrimmerPotentialSavings": "Potential Savings", + "XCITrimmerActualSavings": "Actual Savings", + "XCITrimmerSavingsMb": "{0:n0} Mb", + "XCITrimmerSelectDisplayed": "Select Shown", + "XCITrimmerDeselectDisplayed": "Deselect Shown", + "XCITrimmerSortName": "Title", + "XCITrimmerSortSaved": "Space Savings", + "XCITrimmerTrim": "Trim", + "XCITrimmerUntrim": "Untrim", + "UpdateWindowUpdateAddedMessage": "{0} new update(s) added", + "UpdateWindowBundledContentNotice": "Bundled updates cannot be removed, only disabled.", + "CheatWindowHeading": "Cheats verfügbar für {0} [{1}]", + "DlcWindowBundledContentNotice": "Bundled DLC cannot be removed, only disabled.", + "BuildId": "BuildId:", + "DlcWindowHeading": "DLC verfügbar für {0} [{1}]", + "DlcWindowDlcAddedMessage": "{0} new downloadable content(s) added", + "AutoloadDlcAddedMessage": "{0} new downloadable content(s) added", + "AutoloadDlcRemovedMessage": "{0} missing downloadable content(s) removed", + "AutoloadUpdateAddedMessage": "{0} new update(s) added", + "AutoloadUpdateRemovedMessage": "{0} missing update(s) removed", + "ModWindowHeading": "{0} Mod(s)", + "UserProfilesEditProfile": "Profil bearbeiten", + "Continue": "Continue", + "Cancel": "Abbrechen", + "Save": "Speichern", + "Discard": "Verwerfen", + "Paused": "Pausiert", + "UserProfilesSetProfileImage": "Profilbild einrichten", + "UserProfileEmptyNameError": "Name ist erforderlich", + "UserProfileNoImageError": "Bitte ein Profilbild auswählen", + "GameUpdateWindowHeading": "Update verfügbar für {0} [{1}]", + "SettingsTabHotkeysResScaleUpHotkey": "Auflösung erhöhen:", + "SettingsTabHotkeysResScaleDownHotkey": "Auflösung verringern:", + "UserProfilesName": "Name:", + "UserProfilesUserId": "Benutzer-ID:", + "SettingsTabGraphicsBackend": "Grafik-Backend:", + "SettingsTabGraphicsBackendTooltip": "Wählen Sie das Grafik-Backend, das im Emulator verwendet werden soll.\n\nVulkan ist insgesamt besser für alle modernen Grafikkarten geeignet, sofern deren Treiber auf dem neuesten Stand sind. Vulkan bietet auch eine schnellere Shader-Kompilierung (weniger Stottern) auf allen GPU-Anbietern.\n\nOpenGL kann auf alten Nvidia-GPUs, alten AMD-GPUs unter Linux oder auf GPUs mit geringerem VRAM bessere Ergebnisse erzielen, obwohl die Shader-Kompilierung stärker stottert.\n\nSetzen Sie auf Vulkan, wenn Sie unsicher sind. Stellen Sie OpenGL ein, wenn Ihr Grafikprozessor selbst mit den neuesten Grafiktreibern Vulkan nicht unterstützt.", + "SettingsEnableTextureRecompression": "Textur-Rekompression", + "SettingsEnableTextureRecompressionTooltip": "Komprimiert ASTC-Texturen, um die VRAM-Nutzung zu reduzieren.\n\nZu den Spielen, die dieses Texturformat verwenden, gehören Astral Chain, Bayonetta 3, Fire Emblem Engage, Metroid Prime Remastered, Super Mario Bros. Wonder und The Legend of Zelda: Tears of the Kingdom.\n\nGrafikkarten mit 4GiB VRAM oder weniger werden beim Ausführen dieser Spiele wahrscheinlich irgendwann abstürzen.\n\nAktivieren Sie diese Option nur, wenn Ihnen bei den oben genannten Spielen der VRAM ausgeht. Lassen Sie es aus, wenn Sie unsicher sind.", + "SettingsTabGraphicsPreferredGpu": "Bevorzugte GPU:", + "SettingsTabGraphicsPreferredGpuTooltip": "Wähle die Grafikkarte aus, die mit dem Vulkan Grafik-Backend verwendet werden soll.\n\nDies hat keinen Einfluss auf die GPU die OpenGL verwendet.\n\nIm Zweifelsfall die als \"dGPU\" gekennzeichnete GPU auswählen. Diese Einstellung unberührt lassen, wenn keine zur Auswahl steht.", + "SettingsAppRequiredRestartMessage": "Ein Neustart von Ryujinx ist erforderlich", + "SettingsGpuBackendRestartMessage": "Das Grafik-Backend oder die Grafikkarteneinstellungen wurden geändert. Ein Neustart ist erforderlich um diese Einstellungen anzuwenden.", + "SettingsGpuBackendRestartSubMessage": "Ryujinx jetzt neu starten?", + "RyujinxUpdaterMessage": "Möchtest du Ryujinx auf die neueste Version aktualisieren?", + "SettingsTabHotkeysVolumeUpHotkey": "Lautstärke erhöhen:", + "SettingsTabHotkeysVolumeDownHotkey": "Lautstärke verringern:", + "SettingsEnableMacroHLE": "HLE Makros aktivieren", + "SettingsEnableMacroHLETooltip": "High-Level-Emulation von GPU-Makrocode.\n\nVerbessert die Leistung, kann aber in einigen Spielen zu Grafikfehlern führen.\n\nBei Unsicherheit AKTIVIEREN.", + "SettingsEnableColorSpacePassthrough": "Farbraum Passthrough", + "SettingsEnableColorSpacePassthroughTooltip": "Weist das Vulkan-Backend an, Farbinformationen ohne Angabe eines Farbraums weiterzuleiten. Für Benutzer mit Wide-Gamut-Displays kann dies zu lebendigeren Farben führen, allerdings auf Kosten der Farbkorrektheit.", + "VolumeShort": "Vol", + "UserProfilesManageSaves": "Speicherstände verwalten", + "DeleteUserSave": "Möchtest du den Spielerstand für dieses Spiel löschen?", + "IrreversibleActionNote": "Diese Option kann nicht rückgängig gemacht werden.", + "SaveManagerHeading": "Spielstände für {0} verwalten", + "SaveManagerTitle": "Speicherdaten Manager", + "Name": "Name", + "Size": "Größe", + "Search": "Suche", + "UserProfilesRecoverLostAccounts": "Konto wiederherstellen", + "Recover": "Wiederherstellen", + "UserProfilesRecoverHeading": "Speicherstände wurden für die folgenden Konten gefunden", + "UserProfilesRecoverEmptyList": "Keine Profile zum Wiederherstellen", + "GraphicsAATooltip": "Wendet Anti-Aliasing auf das Rendering des Spiels an.\n\nFXAA verwischt den größten Teil des Bildes, während SMAA versucht, gezackte Kanten zu finden und sie zu glätten.\n\nEs wird nicht empfohlen, diese Option in Verbindung mit dem FSR-Skalierungsfilter zu verwenden.\n\nDiese Option kann geändert werden, während ein Spiel läuft, indem Sie unten auf \"Anwenden\" klicken; Sie können das Einstellungsfenster einfach zur Seite schieben und experimentieren, bis Sie Ihr bevorzugtes Aussehen für ein Spiel gefunden haben.\n\nLassen Sie die Option auf NONE, wenn Sie unsicher sind.", + "GraphicsAALabel": "Antialiasing:", + "GraphicsScalingFilterLabel": "Skalierungsfilter:", + "GraphicsScalingFilterTooltip": "Wählen Sie den Skalierungsfilter, der bei der Auflösungsskalierung angewendet werden soll.\n\nBilinear eignet sich gut für 3D-Spiele und ist eine sichere Standardoption.\n\nNearest wird für Pixel-Art-Spiele empfohlen.\n\nFSR 1.0 ist lediglich ein Schärfungsfilter und wird nicht für die Verwendung mit FXAA oder SMAA empfohlen.\n\nDiese Option kann geändert werden, während ein Spiel läuft, indem Sie unten auf \"Anwenden\" klicken; Sie können das Einstellungsfenster einfach zur Seite schieben und experimentieren, bis Sie Ihr bevorzugtes Aussehen für ein Spiel gefunden haben.\n\nBleiben Sie auf BILINEAR, wenn Sie unsicher sind.", + "GraphicsScalingFilterBilinear": "Bilinear", + "GraphicsScalingFilterNearest": "Nächstes", + "GraphicsScalingFilterFsr": "FSR", + "GraphicsScalingFilterArea": "Area", + "GraphicsScalingFilterLevelLabel": "Stufe", + "GraphicsScalingFilterLevelTooltip": "FSR 1.0 Schärfelevel festlegen. Höher ist schärfer.", + "SmaaLow": "SMAA Niedrig", + "SmaaMedium": "SMAA Mittel", + "SmaaHigh": "SMAA Hoch", + "SmaaUltra": "SMAA Ultra", + "UserEditorTitle": "Nutzer bearbeiten", + "UserEditorTitleCreate": "Nutzer erstellen", + "SettingsTabNetworkInterface": "Netzwerkschnittstelle:", + "NetworkInterfaceTooltip": "Die für LAN/LDN-Funktionen verwendete Netzwerkschnittstelle.\n\nIn Verbindung mit einem VPN oder XLink Kai und einem Spiel mit LAN-Unterstützung kann eine Verbindung mit demselben Netzwerk über das Internet vorgetäuscht werden.\n\nIm Zweifelsfall auf DEFAULT belassen.", + "NetworkInterfaceDefault": "Standard", + "PackagingShaders": "Verpackt Shader", + "AboutChangelogButton": "Changelog in GitHub öffnen", + "AboutChangelogButtonTooltipMessage": "Klicke hier, um das Changelog für diese Version in Ihrem Standardbrowser zu öffnen.", + "SettingsTabNetworkMultiplayer": "Mehrspieler", + "MultiplayerMode": "Modus:", + "MultiplayerModeTooltip": "Ändert den LDN-Mehrspielermodus.\n\nLdnMitm ändert die lokale drahtlose/lokale Spielfunktionalität in Spielen so, dass sie wie ein LAN funktioniert und lokale, netzwerkgleiche Verbindungen mit anderen Ryujinx-Instanzen und gehackten Nintendo Switch-Konsolen ermöglicht, auf denen das ldn_mitm-Modul installiert ist.\n\nMultiplayer erfordert, dass alle Spieler die gleiche Spielversion verwenden (d.h. Super Smash Bros. Ultimate v13.0.1 kann sich nicht mit v13.0.0 verbinden).\n\nIm Zweifelsfall auf DISABLED lassen.", + "MultiplayerModeDisabled": "Deaktiviert", + "MultiplayerModeLdnMitm": "ldn_mitm", + "MultiplayerModeLdnRyu": "RyuLDN", + "MultiplayerDisableP2P": "Disable P2P Network Hosting (may increase latency)", + "MultiplayerDisableP2PTooltip": "Disable P2P network hosting, peers will proxy through the master server instead of connecting to you directly.", + "LdnPassphrase": "Network Passphrase:", + "LdnPassphraseTooltip": "You will only be able to see hosted games with the same passphrase as you.", + "LdnPassphraseInputTooltip": "Enter a passphrase in the format Ryujinx-<8 hex chars>. You will only be able to see hosted games with the same passphrase as you.", + "LdnPassphraseInputPublic": "(public)", + "GenLdnPass": "Generate Random", + "GenLdnPassTooltip": "Generates a new passphrase, which can be shared with other players.", + "ClearLdnPass": "Clear", + "ClearLdnPassTooltip": "Clears the current passphrase, returning to the public network.", + "InvalidLdnPassphrase": "Invalid Passphrase! Must be in the format \"Ryujinx-<8 hex chars>\"" +} diff --git a/src/Ryujinx/Assets/Locales/el_GR.json b/src/Ryujinx/Assets/Locales/el_GR.json new file mode 100644 index 000000000..a589d31ad --- /dev/null +++ b/src/Ryujinx/Assets/Locales/el_GR.json @@ -0,0 +1,868 @@ +{ + "Language": "Ελληνικά", + "MenuBarFileOpenApplet": "Άνοιγμα Applet", + "MenuBarFileOpenAppletOpenMiiApplet": "Mii Edit Applet", + "MenuBarFileOpenAppletOpenMiiAppletToolTip": "Άνοιγμα του Mii Editor Applet σε Αυτόνομη λειτουργία", + "SettingsTabInputDirectMouseAccess": "Άμεση Πρόσβαση Ποντικιού", + "SettingsTabSystemMemoryManagerMode": "Λειτουργία Διαχείρισης Μνήμης:", + "SettingsTabSystemMemoryManagerModeSoftware": "Λογισμικό", + "SettingsTabSystemMemoryManagerModeHost": "Υπολογιστής (γρήγορο)", + "SettingsTabSystemMemoryManagerModeHostUnchecked": "Χωρίς Ελέγχους (γρηγορότερο, μη ασφαλές)", + "SettingsTabSystemUseHypervisor": "Χρήση Hypervisor", + "MenuBarFile": "_Αρχείο", + "MenuBarFileOpenFromFile": "_Φόρτωση Αρχείου Εφαρμογής", + "MenuBarFileOpenFromFileError": "No applications found in selected file.", + "MenuBarFileOpenUnpacked": "Φόρτωση Απακετάριστου _Παιχνιδιού", + "MenuBarFileLoadDlcFromFolder": "Load DLC From Folder", + "MenuBarFileLoadTitleUpdatesFromFolder": "Load Title Updates From Folder", + "MenuBarFileOpenEmuFolder": "Άνοιγμα Φακέλου Ryujinx", + "MenuBarFileOpenLogsFolder": "Άνοιγμα Φακέλου Καταγραφής", + "MenuBarFileExit": "_Έξοδος", + "MenuBarOptions": "_Επιλογές", + "MenuBarOptionsToggleFullscreen": "Λειτουργία Πλήρους Οθόνης", + "MenuBarOptionsStartGamesInFullscreen": "Εκκίνηση Παιχνιδιών σε Πλήρη Οθόνη", + "MenuBarOptionsStopEmulation": "Διακοπή Εξομοίωσης", + "MenuBarOptionsSettings": "_Ρυθμίσεις", + "MenuBarOptionsManageUserProfiles": "Διαχείριση Προφίλ _Χρηστών", + "MenuBarActions": "_Δράσεις", + "MenuBarOptionsSimulateWakeUpMessage": "Προσομοίωση Μηνύματος Αφύπνισης", + "MenuBarActionsScanAmiibo": "Σάρωση Amiibo", + "MenuBarTools": "_Εργαλεία", + "MenuBarToolsInstallFirmware": "Εγκατάσταση Firmware", + "MenuBarFileToolsInstallFirmwareFromFile": "Εγκατάσταση Firmware από XCI ή ZIP", + "MenuBarFileToolsInstallFirmwareFromDirectory": "Εγκατάσταση Firmware από τοποθεσία", + "MenuBarToolsManageFileTypes": "Διαχείριση τύπων αρχείων", + "MenuBarToolsInstallFileTypes": "Εγκαταστήσετε τύπους αρχείων.", + "MenuBarToolsUninstallFileTypes": "Απεγκαταστήσετε τύπους αρχείων", + "MenuBarToolsXCITrimmer": "Trim XCI Files", + "MenuBarView": "_View", + "MenuBarViewWindow": "Window Size", + "MenuBarViewWindow720": "720p", + "MenuBarViewWindow1080": "1080p", + "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", + "GameListContextMenuCacheManagement": "Διαχείριση Προσωρινής Μνήμης", + "GameListContextMenuCacheManagementPurgePptc": "Εκκαθάριση Προσωρινής Μνήμης PPTC", + "GameListContextMenuCacheManagementPurgePptcToolTip": "Διαγράφει την προσωρινή μνήμη PPTC της εφαρμογής", + "GameListContextMenuCacheManagementPurgeShaderCache": "Εκκαθάριση Προσωρινής Μνήμης Shader", + "GameListContextMenuCacheManagementPurgeShaderCacheToolTip": "Διαγράφει την προσωρινή μνήμη Shader της εφαρμογής", + "GameListContextMenuCacheManagementOpenPptcDirectory": "Άνοιγμα Τοποθεσίας PPTC", + "GameListContextMenuCacheManagementOpenPptcDirectoryToolTip": "Ανοίγει την τοποθεσία που περιέχει τη προσωρινή μνήμη PPTC της εφαρμογής", + "GameListContextMenuCacheManagementOpenShaderCacheDirectory": "Άνοιγμα τοποθεσίας προσωρινής μνήμης Shader", + "GameListContextMenuCacheManagementOpenShaderCacheDirectoryToolTip": "Ανοίγει την τοποθεσία που περιέχει την προσωρινή μνήμη Shader της εφαρμογής", + "GameListContextMenuExtractData": "Εξαγωγή Δεδομένων", + "GameListContextMenuExtractDataExeFS": "ExeFS", + "GameListContextMenuExtractDataExeFSToolTip": "Εξαγωγή της ενότητας ExeFS από την τρέχουσα διαμόρφωση της εφαρμογής (συμπεριλαμβανομένου ενημερώσεων)", + "GameListContextMenuExtractDataRomFS": "RomFS", + "GameListContextMenuExtractDataRomFSToolTip": "Εξαγωγή της ενότητας RomFS από την τρέχουσα διαμόρφωση της εφαρμογής (συμπεριλαμβανομένου ενημερώσεων)", + "GameListContextMenuExtractDataLogo": "Λογότυπο", + "GameListContextMenuExtractDataLogoToolTip": "Εξαγωγή της ενότητας Logo από την τρέχουσα διαμόρφωση της εφαρμογής (συμπεριλαμβανομένου ενημερώσεων)", + "GameListContextMenuCreateShortcut": "Δημιουργία Συντόμευσης Εφαρμογής", + "GameListContextMenuCreateShortcutToolTip": "Δημιουργία συντόμευσης επιφάνειας εργασίας που ανοίγει την επιλεγμένη εφαρμογή", + "GameListContextMenuCreateShortcutToolTipMacOS": "Create a shortcut in macOS's Applications folder that launches the selected Application", + "GameListContextMenuOpenModsDirectory": "Open Mods Directory", + "GameListContextMenuOpenModsDirectoryToolTip": "Opens the directory which contains Application's Mods", + "GameListContextMenuOpenSdModsDirectory": "Open Atmosphere Mods Directory", + "GameListContextMenuOpenSdModsDirectoryToolTip": "Opens the alternative SD card Atmosphere directory which contains Application's Mods. Useful for mods that are packaged for real hardware.", + "GameListContextMenuTrimXCI": "Check and Trim XCI File", + "GameListContextMenuTrimXCIToolTip": "Check and Trim XCI File to Save Disk Space", + "StatusBarGamesLoaded": "{0}/{1} Φορτωμένα Παιχνίδια", + "StatusBarSystemVersion": "Έκδοση Συστήματος: {0}", + "StatusBarXCIFileTrimming": "Trimming XCI File '{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": "Εμφάνιση διαλόγου \"Επιβεβαίωση Εξόδου\".", + "SettingsTabGeneralRememberWindowState": "Remember Window Size/Position", + "SettingsTabGeneralShowTitleBar": "Show Title Bar (Requires restart)", + "SettingsTabGeneralHideCursor": "Απόκρυψη Κέρσορα:", + "SettingsTabGeneralHideCursorNever": "Ποτέ", + "SettingsTabGeneralHideCursorOnIdle": "Απόκρυψη Δρομέα στην Αδράνεια", + "SettingsTabGeneralHideCursorAlways": "Πάντα", + "SettingsTabGeneralGameDirectories": "Τοποθεσίες παιχνιδιών", + "SettingsTabGeneralAutoloadDirectories": "Autoload DLC/Updates Directories", + "SettingsTabGeneralAutoloadNote": "DLC and Updates which refer to missing files will be unloaded automatically", + "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 (Profiled Persistent Translation Cache)", + "SettingsTabSystemEnableLowPowerPptc": "Low-power PPTC", + "SettingsTabSystemEnableFsIntegrityChecks": "Ενεργοποίηση Ελέγχων Ακεραιότητας FS", + "SettingsTabSystemAudioBackend": "Backend Ήχου:", + "SettingsTabSystemAudioBackendDummy": "Απενεργοποιημένο", + "SettingsTabSystemAudioBackendOpenAL": "OpenAL", + "SettingsTabSystemAudioBackendSoundIO": "SoundIO", + "SettingsTabSystemAudioBackendSDL2": "SDL2", + "SettingsTabSystemHacks": "Μικροδιορθώσεις", + "SettingsTabSystemHacksNote": " (Μπορεί να προκαλέσουν αστάθεια)", + "SettingsTabSystemDramSize": "Μέγεθος DRAM:", + "SettingsTabSystemDramSize4GiB": "4GiB", + "SettingsTabSystemDramSize6GiB": "6GiB", + "SettingsTabSystemDramSize8GiB": "8GiB", + "SettingsTabSystemDramSize12GiB": "12GiB", + "SettingsTabSystemIgnoreMissingServices": "Αγνόηση υπηρεσιών που λείπουν", + "SettingsTabSystemIgnoreApplet": "Αγνοήστε το Applet", + "SettingsTabGraphics": "Γραφικά", + "SettingsTabGraphicsAPI": "API Γραφικά", + "SettingsTabGraphicsEnableShaderCache": "Ενεργοποίηση Προσωρινής Μνήμης Shader", + "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) (Not recommended)", + "SettingsTabGraphicsAspectRatio": "Αναλογία Απεικόνισης:", + "SettingsTabGraphicsAspectRatio4x3": "4:3", + "SettingsTabGraphicsAspectRatio16x9": "16:9", + "SettingsTabGraphicsAspectRatio16x10": "16:10", + "SettingsTabGraphicsAspectRatio21x9": "21:9", + "SettingsTabGraphicsAspectRatio32x9": "32:9", + "SettingsTabGraphicsAspectRatioStretch": "Έκταση σε όλο το παράθυρο", + "SettingsTabGraphicsDeveloperOptions": "Επιλογές Προγραμματιστή", + "SettingsTabGraphicsShaderDumpPath": "Τοποθεσία Shaders Γραφικών:", + "SettingsTabLogging": "Καταγραφή", + "SettingsTabLoggingLogging": "Καταγραφή", + "SettingsTabLoggingEnableLoggingToFile": "Ενεργοποίηση Καταγραφής Αρχείου", + "SettingsTabLoggingEnableStubLogs": "Ενεργοποίηση Καταγραφής Stub", + "SettingsTabLoggingEnableInfoLogs": "Ενεργοποίηση Καταγραφής Πληροφοριών", + "SettingsTabLoggingEnableWarningLogs": "Ενεργοποίηση Καταγραφής Προειδοποίησης", + "SettingsTabLoggingEnableErrorLogs": "Ενεργοποίηση Καταγραφής Σφαλμάτων", + "SettingsTabLoggingEnableTraceLogs": "Ενεργοποίηση Καταγραφής Ιχνών", + "SettingsTabLoggingEnableGuestLogs": "Ενεργοποίηση Καταγραφής Επισκεπτών", + "SettingsTabLoggingEnableFsAccessLogs": "Ενεργοποίηση Καταγραφής Πρόσβασης FS", + "SettingsTabLoggingFsGlobalAccessLogMode": "Λειτουργία Καταγραφής Καθολικής Πρόσβασης FS:", + "SettingsTabLoggingDeveloperOptions": "Επιλογές Προγραμματιστή (ΠΡΟΕΙΔΟΠΟΙΗΣΗ: Η απόδοση Θα μειωθεί)", + "SettingsTabLoggingDeveloperOptionsNote": "ΠΡΟΕΙΔΟΠΟΙΗΣΗ: Θα μειώσει την απόδοση", + "SettingsTabLoggingGraphicsBackendLogLevel": "Επίπεδο Καταγραφής Διεπαφής Γραφικών:", + "SettingsTabLoggingGraphicsBackendLogLevelNone": "Κανένα", + "SettingsTabLoggingGraphicsBackendLogLevelError": "Σφάλμα", + "SettingsTabLoggingGraphicsBackendLogLevelPerformance": "Επιβραδύνσεις", + "SettingsTabLoggingGraphicsBackendLogLevelAll": "Όλα", + "SettingsTabLoggingEnableDebugLogs": "Ενεργοποίηση Αρχείων Καταγραφής Εντοπισμού Σφαλμάτων", + "SettingsTabInput": "Χειρισμός", + "SettingsTabInputEnableDockedMode": "Ενεργοποίηση Docked Mode", + "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 Controller", + "ControllerSettingsControllerTypeJoyConPair": "Ζεύγος JoyCon", + "ControllerSettingsControllerTypeJoyConLeft": "Αριστερό JoyCon", + "ControllerSettingsControllerTypeJoyConRight": "Δεξί JoyCon", + "ControllerSettingsProfile": "Προφίλ", + "ControllerSettingsProfileDefault": "Προκαθορισμένο", + "ControllerSettingsLoad": "Φόρτωση", + "ControllerSettingsAdd": "Προσθήκη", + "ControllerSettingsRemove": "Αφαίρεση", + "ControllerSettingsButtons": "Κουμπιά", + "ControllerSettingsButtonA": "Α", + "ControllerSettingsButtonB": "B", + "ControllerSettingsButtonX": "X", + "ControllerSettingsButtonY": "Y", + "ControllerSettingsButtonPlus": "+", + "ControllerSettingsButtonMinus": "-", + "ControllerSettingsDPad": "Κατευθυντικό Pad", + "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": "Κεντρικός Υπολογιστής Διακομιστή:", + "ControllerSettingsMotionGyroSensitivity": "Ευαισθησία Γυροσκοπίου:", + "ControllerSettingsMotionGyroDeadzone": "Νεκρή Ζώνη Γυροσκοπίου:", + "ControllerSettingsSave": "Αποθήκευση", + "ControllerSettingsClose": "Κλείσιμο", + "KeyUnknown": "Unknown", + "KeyShiftLeft": "Shift Left", + "KeyShiftRight": "Shift Right", + "KeyControlLeft": "Ctrl Left", + "KeyMacControlLeft": "⌃ Left", + "KeyControlRight": "Ctrl Right", + "KeyMacControlRight": "⌃ Right", + "KeyAltLeft": "Alt Left", + "KeyMacAltLeft": "⌥ Left", + "KeyAltRight": "Alt Right", + "KeyMacAltRight": "⌥ Right", + "KeyWinLeft": "⊞ Left", + "KeyMacWinLeft": "⌘ Left", + "KeyWinRight": "⊞ Right", + "KeyMacWinRight": "⌘ Right", + "KeyMenu": "Menu", + "KeyUp": "Up", + "KeyDown": "Down", + "KeyLeft": "Left", + "KeyRight": "Right", + "KeyEnter": "Enter", + "KeyEscape": "Escape", + "KeySpace": "Space", + "KeyTab": "Tab", + "KeyBackSpace": "Backspace", + "KeyInsert": "Insert", + "KeyDelete": "Delete", + "KeyPageUp": "Page Up", + "KeyPageDown": "Page Down", + "KeyHome": "Home", + "KeyEnd": "End", + "KeyCapsLock": "Caps Lock", + "KeyScrollLock": "Scroll Lock", + "KeyPrintScreen": "Print Screen", + "KeyPause": "Pause", + "KeyNumLock": "Num Lock", + "KeyClear": "Clear", + "KeyKeypad0": "Keypad 0", + "KeyKeypad1": "Keypad 1", + "KeyKeypad2": "Keypad 2", + "KeyKeypad3": "Keypad 3", + "KeyKeypad4": "Keypad 4", + "KeyKeypad5": "Keypad 5", + "KeyKeypad6": "Keypad 6", + "KeyKeypad7": "Keypad 7", + "KeyKeypad8": "Keypad 8", + "KeyKeypad9": "Keypad 9", + "KeyKeypadDivide": "Keypad Divide", + "KeyKeypadMultiply": "Keypad Multiply", + "KeyKeypadSubtract": "Keypad Subtract", + "KeyKeypadAdd": "Keypad Add", + "KeyKeypadDecimal": "Keypad Decimal", + "KeyKeypadEnter": "Keypad Enter", + "KeyNumber0": "0", + "KeyNumber1": "1", + "KeyNumber2": "2", + "KeyNumber3": "3", + "KeyNumber4": "4", + "KeyNumber5": "5", + "KeyNumber6": "6", + "KeyNumber7": "7", + "KeyNumber8": "8", + "KeyNumber9": "9", + "KeyTilde": "~", + "KeyGrave": "`", + "KeyMinus": "-", + "KeyPlus": "+", + "KeyBracketLeft": "[", + "KeyBracketRight": "]", + "KeySemicolon": ";", + "KeyQuote": "\"", + "KeyComma": ",", + "KeyPeriod": ".", + "KeySlash": "/", + "KeyBackSlash": "\\", + "KeyUnbound": "Unbound", + "GamepadLeftStick": "L Stick Button", + "GamepadRightStick": "R Stick Button", + "GamepadLeftShoulder": "Left Shoulder", + "GamepadRightShoulder": "Right Shoulder", + "GamepadLeftTrigger": "Left Trigger", + "GamepadRightTrigger": "Right Trigger", + "GamepadDpadUp": "Up", + "GamepadDpadDown": "Down", + "GamepadDpadLeft": "Left", + "GamepadDpadRight": "Right", + "GamepadMinus": "-", + "GamepadPlus": "+", + "GamepadGuide": "Guide", + "GamepadMisc1": "Misc", + "GamepadPaddle1": "Paddle 1", + "GamepadPaddle2": "Paddle 2", + "GamepadPaddle3": "Paddle 3", + "GamepadPaddle4": "Paddle 4", + "GamepadTouchpad": "Touchpad", + "GamepadSingleLeftTrigger0": "Left Trigger 0", + "GamepadSingleRightTrigger0": "Right Trigger 0", + "GamepadSingleLeftTrigger1": "Left Trigger 1", + "GamepadSingleRightTrigger1": "Right Trigger 1", + "StickLeft": "Left Stick", + "StickRight": "Right Stick", + "UserProfilesSelectedUserProfile": "Επιλεγμένο Προφίλ Χρήστη:", + "UserProfilesSaveProfileName": "Αποθήκευση Ονόματος Προφίλ", + "UserProfilesChangeProfileImage": "Αλλαγή Εικόνας Προφίλ", + "UserProfilesAvailableUserProfiles": "Διαθέσιμα Προφίλ Χρηστών:", + "UserProfilesAddNewProfile": "Προσθήκη Νέου Προφίλ", + "UserProfilesDelete": "Διαγράφω", + "UserProfilesClose": "Κλείσιμο", + "ProfileNameSelectionWatermark": "Επιλέξτε ψευδώνυμο", + "ProfileImageSelectionTitle": "Επιλογή Εικόνας Προφίλ", + "ProfileImageSelectionHeader": "Επιλέξτε μία Εικόνα Προφίλ", + "ProfileImageSelectionNote": "Μπορείτε να εισαγάγετε μία προσαρμοσμένη εικόνα προφίλ ή να επιλέξετε ένα avatar από το Firmware", + "ProfileImageSelectionImportImage": "Εισαγωγή Αρχείου Εικόνας", + "ProfileImageSelectionSelectAvatar": "Επιλέξτε Avatar από Firmware", + "InputDialogTitle": "Διάλογος Εισαγωγής", + "InputDialogOk": "ΟΚ", + "InputDialogCancel": "Ακύρωση", + "InputDialogCancelling": "Cancelling", + "InputDialogClose": "Close", + "InputDialogAddNewProfileTitle": "Επιλογή Ονόματος Προφίλ", + "InputDialogAddNewProfileHeader": "Εισαγωγή Ονόματος Προφίλ", + "InputDialogAddNewProfileSubtext": "(Σύνολο Χαρακτήρων: {0})", + "AvatarChoose": "Επιλογή", + "AvatarSetBackgroundColor": "Ορισμός Χρώματος Φόντου", + "AvatarClose": "Κλείσιμο", + "ControllerSettingsLoadProfileToolTip": "Φόρτωση Προφίλ", + "ControllerSettingsViewProfileToolTip": "View Profile", + "ControllerSettingsAddProfileToolTip": "Προσθήκη Προφίλ", + "ControllerSettingsRemoveProfileToolTip": "Κατάργηση Προφίλ", + "ControllerSettingsSaveProfileToolTip": "Αποθήκευση Προφίλ", + "MenuBarFileToolsTakeScreenshot": "Λήψη Στιγμιότυπου", + "MenuBarFileToolsHideUi": "Απόκρυψη UI", + "GameListContextMenuRunApplication": "Εκτέλεση Εφαρμογής", + "GameListContextMenuToggleFavorite": "Εναλλαγή Αγαπημένου", + "GameListContextMenuToggleFavoriteToolTip": "Εναλλαγή της Κατάστασης Αγαπημένο του Παιχνιδιού", + "SettingsTabGeneralTheme": "Theme:", + "SettingsTabGeneralThemeAuto": "Auto", + "SettingsTabGeneralThemeDark": "Dark", + "SettingsTabGeneralThemeLight": "Light", + "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": "Εξαγωγή ενότητας {0} από {1}...", + "DialogNcaExtractionTitle": "NCA Εξαγωγέας Τμημάτων", + "DialogNcaExtractionMainNcaNotFoundErrorMessage": "Αποτυχία εξαγωγής. Η κύρια NCA δεν υπήρχε στο επιλεγμένο αρχείο.", + "DialogNcaExtractionCheckLogErrorMessage": "Αποτυχία εξαγωγής. Διαβάστε το αρχείο καταγραφής για περισσότερες πληροφορίες.", + "DialogNcaExtractionSuccessMessage": "Η εξαγωγή ολοκληρώθηκε με επιτυχία.", + "DialogUpdaterConvertFailedMessage": "Αποτυχία μετατροπής της τρέχουσας έκδοσης Ryujinx.", + "DialogUpdaterCancelUpdateMessage": "Ακύρωση Ενημέρωσης!", + "DialogUpdaterAlreadyOnLatestVersionMessage": "Χρησιμοποιείτε ήδη την πιο ενημερωμένη έκδοση του Ryujinx!", + "DialogUpdaterFailedToGetVersionMessage": "Προέκυψε ένα σφάλμα στη λήψη πληροφοριών έκδοσης από τα GitHub Releases. Αυτό δύναται να συμβεί αν μία έκδοση χτίζεται αυτή τη στιγμή στα GitHub Actions. Παρακαλούμε προσπαθήστε αργότερα.", + "DialogUpdaterConvertFailedGithubMessage": "Αποτυχία μετατροπής της ληφθείσας έκδοσης Ryujinx από την έκδοση GitHub.", + "DialogUpdaterDownloadingMessage": "Λήψη Ενημέρωσης...", + "DialogUpdaterExtractionMessage": "Εξαγωγή Ενημέρωσης...", + "DialogUpdaterRenamingMessage": "Μετονομασία Ενημέρωσης...", + "DialogUpdaterAddingFilesMessage": "Προσθήκη Νέας Ενημέρωσης...", + "DialogUpdaterShowChangelogMessage": "Show Changelog", + "DialogUpdaterCompleteMessage": "Η Ενημέρωση Ολοκληρώθηκε!", + "DialogUpdaterRestartMessage": "Θέλετε να επανεκκινήσετε το Ryujinx τώρα;", + "DialogUpdaterNoInternetMessage": "Δεν είστε συνδεδεμένοι στο Διαδίκτυο!", + "DialogUpdaterNoInternetSubMessage": "Επαληθεύστε ότι έχετε σύνδεση στο Διαδίκτυο που λειτουργεί!", + "DialogUpdaterDirtyBuildMessage": "Δεν μπορείτε να ενημερώσετε μία Πρόχειρη Έκδοση του Ryujinx!", + "DialogUpdaterDirtyBuildSubMessage": "Κάντε λήψη του Ryujinx στη διεύθυνση https://ryujinx.app/download εάν αναζητάτε μία υποστηριζόμενη έκδοση.", + "DialogRestartRequiredMessage": "Απαιτείται Επανεκκίνηση", + "DialogThemeRestartMessage": "Το θέμα έχει αποθηκευτεί. Απαιτείται επανεκκίνηση για την εφαρμογή του θέματος.", + "DialogThemeRestartSubMessage": "Θέλετε να κάνετε επανεκκίνηση", + "DialogFirmwareInstallEmbeddedMessage": "Θα θέλατε να εγκαταστήσετε το Firmware που είναι ενσωματωμένο σε αυτό το παιχνίδι; (Firmware {0})", + "DialogFirmwareInstallEmbeddedSuccessMessage": "No installed firmware was found but Ryujinx was able to install firmware {0} from the provided game.\nThe emulator will now start.", + "DialogFirmwareNoFirmwareInstalledMessage": "Δεν έχει εγκατασταθεί Firmware", + "DialogFirmwareInstalledMessage": "Το Firmware {0} εγκαταστάθηκε", + "DialogInstallFileTypesSuccessMessage": "Επιτυχής εγκατάσταση τύπων αρχείων!", + "DialogInstallFileTypesErrorMessage": "Απέτυχε η εγκατάσταση τύπων αρχείων.", + "DialogUninstallFileTypesSuccessMessage": "Επιτυχής απεγκατάσταση τύπων αρχείων!", + "DialogUninstallFileTypesErrorMessage": "Αποτυχία απεγκατάστασης τύπων αρχείων.", + "DialogOpenSettingsWindowLabel": "Άνοιγμα Παραθύρου Ρυθμίσεων", + "DialogOpenXCITrimmerWindowLabel": "XCI Trimmer Window", + "DialogControllerAppletTitle": "Applet Χειρισμού", + "DialogMessageDialogErrorExceptionMessage": "Σφάλμα εμφάνισης του διαλόγου Μηνυμάτων: {0}", + "DialogSoftwareKeyboardErrorExceptionMessage": "Σφάλμα εμφάνισης Λογισμικού Πληκτρολογίου: {0}", + "DialogErrorAppletErrorExceptionMessage": "Σφάλμα εμφάνισης του διαλόγου ErrorApplet: {0}", + "DialogUserErrorDialogMessage": "{0}: {1}", + "DialogUserErrorDialogInfoMessage": "\nΓια πληροφορίες σχετικά με τον τρόπο διόρθωσης του σφάλματος, ακολουθήστε τον Οδηγό Εγκατάστασης.", + "DialogUserErrorDialogTitle": "Σφάλμα Ryujinx ({0})", + "DialogAmiiboApiTitle": "API για Amiibo.", + "DialogAmiiboApiFailFetchMessage": "Παρουσιάστηκε σφάλμα κατά την ανάκτηση πληροφοριών από το API.", + "DialogAmiiboApiConnectErrorMessage": "Δεν είναι δυνατή η σύνδεση με τον διακομιστή Amiibo API. Η υπηρεσία μπορεί να είναι εκτός λειτουργίας ή μπορεί να χρειαστεί να επαληθεύσετε ότι έχετε ενεργή σύνδεσή στο Διαδίκτυο.", + "DialogProfileInvalidProfileErrorMessage": "Το προφίλ {0} δεν είναι συμβατό με το τρέχον σύστημα χειρισμού.", + "DialogProfileDefaultProfileOverwriteErrorMessage": "Το προεπιλεγμένο προφίλ δεν μπορεί να αντικατασταθεί", + "DialogProfileDeleteProfileTitle": "Διαγραφή Προφίλ", + "DialogProfileDeleteProfileMessage": "Αυτή η ενέργεια είναι μη αναστρέψιμη, είστε βέβαιοι ότι θέλετε να συνεχίσετε;", + "DialogWarning": "Προειδοποίηση", + "DialogPPTCDeletionMessage": "Πρόκειται να διαγράψετε την προσωρινή μνήμη PPTC για :\n\n{0}\n\nΕίστε βέβαιοι ότι θέλετε να συνεχίσετε;", + "DialogPPTCDeletionErrorMessage": "Σφάλμα κατά την εκκαθάριση προσωρινής μνήμης PPTC στο {0}: {1}", + "DialogShaderDeletionMessage": "Πρόκειται να διαγράψετε την προσωρινή μνήμη Shader για :\n\n{0}\n\nΕίστε βέβαιοι ότι θέλετε να συνεχίσετε;", + "DialogShaderDeletionErrorMessage": "Σφάλμα κατά την εκκαθάριση προσωρινής μνήμης Shader στο {0}: {1}", + "DialogRyujinxErrorMessage": "Το Ryujinx αντιμετώπισε σφάλμα", + "DialogInvalidTitleIdErrorMessage": "Σφάλμα UI: Το επιλεγμένο παιχνίδι δεν έχει έγκυρο αναγνωριστικό τίτλου", + "DialogFirmwareInstallerFirmwareNotFoundErrorMessage": "Δεν βρέθηκε έγκυρο Firmware συστήματος στο {0}.", + "DialogFirmwareInstallerFirmwareInstallTitle": "Εγκατάσταση Firmware {0}", + "DialogFirmwareInstallerFirmwareInstallMessage": "Θα εγκατασταθεί η έκδοση συστήματος {0}.", + "DialogFirmwareInstallerFirmwareInstallSubMessage": "\n\nΑυτό θα αντικαταστήσει την τρέχουσα έκδοση συστήματος {0}.", + "DialogFirmwareInstallerFirmwareInstallConfirmMessage": "\n\nΘέλετε να συνεχίσετε;", + "DialogFirmwareInstallerFirmwareInstallWaitMessage": "Εγκατάσταση Firmware...", + "DialogFirmwareInstallerFirmwareInstallSuccessMessage": "Η έκδοση συστήματος {0} εγκαταστάθηκε με επιτυχία.", + "DialogUserProfileDeletionWarningMessage": "Δεν θα υπάρχουν άλλα προφίλ εάν διαγραφεί το επιλεγμένο", + "DialogUserProfileDeletionConfirmMessage": "Θέλετε να διαγράψετε το επιλεγμένο προφίλ", + "DialogUserProfileUnsavedChangesTitle": "Προσοχή - Μην Αποθηκευμένες Αλλαγές.", + "DialogUserProfileUnsavedChangesMessage": "Έχετε κάνει αλλαγές σε αυτό το προφίλ χρήστη που δεν έχουν αποθηκευτεί.", + "DialogUserProfileUnsavedChangesSubMessage": "Θέλετε να απορρίψετε τις αλλαγές σας;", + "DialogControllerSettingsModifiedConfirmMessage": "Οι τρέχουσες ρυθμίσεις χειρισμού έχουν ενημερωθεί.", + "DialogControllerSettingsModifiedConfirmSubMessage": "Θέλετε να αποθηκεύσετε;", + "DialogLoadFileErrorMessage": "{0}. Errored File: {1}", + "DialogModAlreadyExistsMessage": "Mod already exists", + "DialogModInvalidMessage": "The specified directory does not contain a mod!", + "DialogModDeleteNoParentMessage": "Failed to Delete: Could not find the parent directory for mod \"{0}\"!", + "DialogDlcNoDlcErrorMessage": "Το αρχείο δεν περιέχει DLC για τον επιλεγμένο τίτλο!", + "DialogPerformanceCheckLoggingEnabledMessage": "Έχετε ενεργοποιημένη την καταγραφή εντοπισμού σφαλμάτων, η οποία έχει σχεδιαστεί για χρήση μόνο από προγραμματιστές.", + "DialogPerformanceCheckLoggingEnabledConfirmMessage": "Για βέλτιστη απόδοση, συνιστάται η απενεργοποίηση καταγραφής εντοπισμού σφαλμάτων. Θέλετε να απενεργοποιήσετε την καταγραφή τώρα;", + "DialogPerformanceCheckShaderDumpEnabledMessage": "Έχετε ενεργοποιήσει το Shader Dumping, το οποίο έχει σχεδιαστεί για χρήση μόνο από προγραμματιστές.", + "DialogPerformanceCheckShaderDumpEnabledConfirmMessage": "Για βέλτιστη απόδοση, συνιστάται να απενεργοποιήσετε το Shader Dumping. Θέλετε να απενεργοποιήσετε τώρα το Shader Dumping;", + "DialogLoadAppGameAlreadyLoadedMessage": "Ένα παιχνίδι έχει ήδη φορτωθεί", + "DialogLoadAppGameAlreadyLoadedSubMessage": "Σταματήστε την εξομοίωση ή κλείστε τον εξομοιωτή πριν ξεκινήσετε ένα άλλο παιχνίδι.", + "DialogUpdateAddUpdateErrorMessage": "Το αρχείο δεν περιέχει ενημέρωση για τον επιλεγμένο τίτλο!", + "DialogSettingsBackendThreadingWarningTitle": "Προειδοποίηση - Backend Threading", + "DialogSettingsBackendThreadingWarningMessage": "Το Ryujinx πρέπει να επανεκκινηθεί αφού αλλάξει αυτή η επιλογή για να εφαρμοστεί πλήρως. Ανάλογα με την πλατφόρμα σας, μπορεί να χρειαστεί να απενεργοποιήσετε με μη αυτόματο τρόπο το multithreading του ίδιου του προγράμματος οδήγησης όταν χρησιμοποιείτε το Ryujinx.", + "DialogModManagerDeletionWarningMessage": "You are about to delete the mod: {0}\n\nAre you sure you want to proceed?", + "DialogModManagerDeletionAllWarningMessage": "You are about to delete all mods for this title.\n\nAre you sure you want to proceed?", + "SettingsTabGraphicsFeaturesOptions": "Χαρακτηριστικά", + "SettingsTabGraphicsBackendMultithreading": "Πολυνηματική Επεξεργασία Γραφικών:", + "CommonAuto": "Αυτόματο", + "CommonOff": "Ανενεργό", + "CommonOn": "Ενεργό", + "InputDialogYes": "Ναι", + "InputDialogNo": "Όχι", + "DialogProfileInvalidProfileNameErrorMessage": "Το όνομα αρχείου περιέχει μη έγκυρους χαρακτήρες. Παρακαλώ προσπαθήστε ξανά.", + "MenuBarOptionsPauseEmulation": "Παύση", + "MenuBarOptionsResumeEmulation": "Συνέχιση", + "AboutUrlTooltipMessage": "Κάντε κλικ για να ανοίξετε τον ιστότοπο Ryujinx στο προεπιλεγμένο πρόγραμμα περιήγησης.", + "AboutDisclaimerMessage": "Το Ryujinx δεν είναι συνδεδεμένο με τη Nintendo™,\nούτε με κανέναν από τους συνεργάτες της, με οποιονδήποτε τρόπο.", + "AboutAmiiboDisclaimerMessage": "Το AmiiboAPI (www.amiiboapi.com) χρησιμοποιείται\nστην προσομοίωση Amiibo.", + "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": "Hack: Χρησιμοποιήστε τυχαίο αναγνωριστικό UUID", + "DlcManagerTableHeadingEnabledLabel": "Ενεργοποιημένο", + "DlcManagerTableHeadingTitleIdLabel": "Αναγνωριστικό τίτλου", + "DlcManagerTableHeadingContainerPathLabel": "Τοποθεσία DLC", + "DlcManagerTableHeadingFullPathLabel": "Πλήρης τοποθεσία", + "DlcManagerRemoveAllButton": "Αφαίρεση όλων", + "DlcManagerEnableAllButton": "Ενεργοποίηση Όλων", + "DlcManagerDisableAllButton": "Απενεργοποίηση Όλων", + "ModManagerDeleteAllButton": "Delete All", + "MenuBarOptionsChangeLanguage": "Αλλαξε γλώσσα", + "MenuBarShowFileTypes": "Εμφάνιση Τύπων Αρχείων", + "CommonSort": "Κατάταξη", + "CommonShowNames": "Εμφάνιση ονομάτων", + "CommonFavorite": "Αγαπημένα", + "OrderAscending": "Αύξουσα", + "OrderDescending": "Φθίνουσα", + "SettingsTabGraphicsFeatures": "Χαρακτηριστικά & Βελτιώσεις", + "ErrorWindowTitle": "Παράθυρο σφάλματος", + "ToggleDiscordTooltip": "Ενεργοποιεί ή απενεργοποιεί την Εμπλουτισμένη Παρουσία σας στο Discord", + "AddGameDirBoxTooltip": "Εισαγάγετε μία τοποθεσία παιχνιδιών για προσθήκη στη λίστα", + "AddGameDirTooltip": "Προσθέστε μία τοποθεσία παιχνιδιών στη λίστα", + "RemoveGameDirTooltip": "Αφαιρέστε την επιλεγμένη τοποθεσία παιχνιδιών", + "AddAutoloadDirBoxTooltip": "Enter an autoload directory to add to the list", + "AddAutoloadDirTooltip": "Add an autoload directory to the list", + "RemoveAutoloadDirTooltip": "Remove selected autoload directory", + "CustomThemeCheckTooltip": "Ενεργοποίηση ή απενεργοποίηση προσαρμοσμένων θεμάτων στο GUI", + "CustomThemePathTooltip": "Διαδρομή προς το προσαρμοσμένο θέμα GUI", + "CustomThemeBrowseTooltip": "Αναζητήστε ένα προσαρμοσμένο θέμα GUI", + "DockModeToggleTooltip": "Ενεργοποιήστε ή απενεργοποιήστε τη λειτουργία σύνδεσης", + "DirectKeyboardTooltip": "Direct keyboard access (HID) support. Provides games access to your keyboard as a text entry device.\n\nOnly works with games that natively support keyboard usage on Switch hardware.\n\nLeave OFF if unsure.", + "DirectMouseTooltip": "Direct mouse access (HID) support. Provides games access to your mouse as a pointing device.\n\nOnly works with games that natively support mouse controls on Switch hardware, which are few and far between.\n\nWhen enabled, touch screen functionality may not work.\n\nLeave OFF if unsure.", + "RegionTooltip": "Αλλαγή Περιοχής Συστήματος", + "LanguageTooltip": "Αλλαγή Γλώσσας Συστήματος", + "TimezoneTooltip": "Αλλαγή Ζώνης Ώρας Συστήματος", + "TimeTooltip": "Αλλαγή Ώρας Συστήματος", + "VSyncToggleTooltip": "Emulated console's Vertical Sync. Essentially a frame-limiter for the majority of games; disabling it may cause games to run at higher speed or make loading screens take longer or get stuck.\n\nCan be toggled in-game with a hotkey of your preference (F1 by default). We recommend doing this if you plan on disabling it.\n\nLeave ON if unsure.", + "PptcToggleTooltip": "Ενεργοποιεί ή απενεργοποιεί το PPTC", + "LowPowerPptcToggleTooltip": "Load the PPTC using a third of the amount of cores.", + "FsIntegrityToggleTooltip": "Ενεργοποιεί τους ελέγχους ακεραιότητας σε αρχεία περιεχομένου παιχνιδιού", + "AudioBackendTooltip": "Αλλαγή ήχου υποστήριξης", + "MemoryManagerTooltip": "Αλλάξτε τον τρόπο αντιστοίχισης και πρόσβασης στη μνήμη επισκέπτη. Επηρεάζει σε μεγάλο βαθμό την απόδοση της προσομοίωσης της CPU.", + "MemoryManagerSoftwareTooltip": "Χρησιμοποιήστε έναν πίνακα σελίδων λογισμικού για τη μετάφραση διευθύνσεων. Υψηλότερη ακρίβεια αλλά πιο αργή απόδοση.", + "MemoryManagerHostTooltip": "Απευθείας αντιστοίχιση της μνήμης στον χώρο διευθύνσεων υπολογιστή υποδοχής. Πολύ πιο γρήγορη μεταγλώττιση και εκτέλεση JIT.", + "MemoryManagerUnsafeTooltip": "Απευθείας χαρτογράφηση της μνήμης, αλλά μην καλύπτετε τη διεύθυνση εντός του χώρου διευθύνσεων επισκέπτη πριν από την πρόσβαση. Πιο γρήγορα, αλλά με κόστος ασφάλειας. Η εφαρμογή μπορεί να έχει πρόσβαση στη μνήμη από οπουδήποτε στο Ryujinx, επομένως εκτελείτε μόνο προγράμματα που εμπιστεύεστε με αυτήν τη λειτουργία.", + "UseHypervisorTooltip": "Χρησιμοποιήστε Hypervisor αντί για JIT. Βελτιώνει σημαντικά την απόδοση όταν διατίθεται, αλλά μπορεί να είναι ασταθής στην τρέχουσα κατάστασή του.", + "DRamTooltip": "Επεκτείνει την ποσότητα της μνήμης στο εξομοιούμενο σύστημα από 4 GiB σε 6 GiB", + "IgnoreMissingServicesTooltip": "Ενεργοποίηση ή απενεργοποίηση της αγνοώησης για υπηρεσίες που λείπουν", + "IgnoreAppletTooltip": "Το εξωτερικό παράθυρο διαλόγου \"Ελεγκτής μικροεφαρμογής\" δεν θα εμφανιστεί εάν το gamepad αποσυνδεθεί κατά τη διάρκεια του παιχνιδιού. Δεν θα σας ζητηθεί να κλείσετε το παράθυρο διαλόγου ή να ρυθμίσετε έναν νέο ελεγκτή. Μόλις επανασυνδεθεί το χειριστήριο που είχε αποσυνδεθεί προηγουμένως, το παιχνίδι θα συνεχιστεί αυτόματα.", + "GraphicsBackendThreadingTooltip": "Ενεργοποίηση Πολυνηματικής Επεξεργασίας Γραφικών", + "GalThreadingTooltip": "Εκτελεί εντολές γραφικών σε ένα δεύτερο νήμα. Επιτρέπει την πολυνηματική μεταγλώττιση Shader σε χρόνο εκτέλεσης, μειώνει το τρεμόπαιγμα και βελτιώνει την απόδοση των προγραμμάτων οδήγησης χωρίς τη δική τους υποστήριξη πολλαπλών νημάτων. Ποικίλες κορυφαίες επιδόσεις σε προγράμματα οδήγησης με multithreading. Μπορεί να χρειαστεί επανεκκίνηση του Ryujinx για να απενεργοποιήσετε σωστά την ενσωματωμένη λειτουργία πολλαπλών νημάτων του προγράμματος οδήγησης ή ίσως χρειαστεί να το κάνετε χειροκίνητα για να έχετε την καλύτερη απόδοση.", + "ShaderCacheToggleTooltip": "Ενεργοποιεί ή απενεργοποιεί την Προσωρινή Μνήμη Shader", + "ResolutionScaleTooltip": "Multiplies the game's rendering resolution.\n\nA few games may not work with this and look pixelated even when the resolution is increased; for those games, you may need to find mods that remove anti-aliasing or that increase their internal rendering resolution. For using the latter, you'll likely want to select Native.\n\nThis option can be changed while a game is running by clicking \"Apply\" below; you can simply move the settings window aside and experiment until you find your preferred look for a game.\n\nKeep in mind 4x is overkill for virtually any setup.", + "ResolutionScaleEntryTooltip": "Κλίμακα ανάλυσης κινητής υποδιαστολής, όπως 1,5. Οι μη αναπόσπαστες τιμές είναι πιθανό να προκαλέσουν προβλήματα ή σφάλματα.", + "AnisotropyTooltip": "Level of Anisotropic Filtering. Set to Auto to use the value requested by the game.", + "AspectRatioTooltip": "Aspect Ratio applied to the renderer window.\n\nOnly change this if you're using an aspect ratio mod for your game, otherwise the graphics will be stretched.\n\nLeave on 16:9 if unsure.", + "ShaderDumpPathTooltip": "Τοποθεσία Εναπόθεσης Προσωρινής Μνήμης Shaders", + "FileLogTooltip": "Ενεργοποιεί ή απενεργοποιεί την καταγραφή σε ένα αρχείο στο δίσκο", + "StubLogTooltip": "Ενεργοποιεί την εκτύπωση μηνυμάτων καταγραφής ατελειών", + "InfoLogTooltip": "Ενεργοποιεί την εκτύπωση μηνυμάτων αρχείου καταγραφής πληροφοριών", + "WarnLogTooltip": "Ενεργοποιεί την εκτύπωση μηνυμάτων καταγραφής προειδοποιήσεων", + "ErrorLogTooltip": "Ενεργοποιεί την εκτύπωση μηνυμάτων αρχείου καταγραφής σφαλμάτων", + "TraceLogTooltip": "Ενεργοποιεί την εκτύπωση μηνυμάτων αρχείου καταγραφής ιχνών", + "GuestLogTooltip": "Ενεργοποιεί την εκτύπωση μηνυμάτων καταγραφής επισκεπτών", + "FileAccessLogTooltip": "Ενεργοποιεί την εκτύπωση μηνυμάτων αρχείου καταγραφής πρόσβασης", + "FSAccessLogModeTooltip": "Ενεργοποιεί την έξοδο καταγραφής πρόσβασης FS στην κονσόλα. Οι πιθανοί τρόποι λειτουργίας είναι 0-3", + "DeveloperOptionTooltip": "Χρησιμοποιήστε με προσοχή", + "OpenGlLogLevel": "Απαιτεί τα κατάλληλα επίπεδα καταγραφής ενεργοποιημένα", + "DebugLogTooltip": "Ενεργοποιεί την εκτύπωση μηνυμάτων αρχείου καταγραφής εντοπισμού σφαλμάτων", + "LoadApplicationFileTooltip": "Ανοίξτε έναν επιλογέα αρχείων για να επιλέξετε ένα αρχείο συμβατό με το Switch για φόρτωση", + "LoadApplicationFolderTooltip": "Ανοίξτε έναν επιλογέα αρχείων για να επιλέξετε μία μη συσκευασμένη εφαρμογή, συμβατή με το Switch για φόρτωση", + "LoadDlcFromFolderTooltip": "Open a file explorer to choose one or more folders to bulk load DLC from", + "LoadTitleUpdatesFromFolderTooltip": "Open a file explorer to choose one or more folders to bulk load title updates from", + "OpenRyujinxFolderTooltip": "Ανοίξτε το φάκελο συστήματος αρχείων Ryujinx", + "OpenRyujinxLogsTooltip": "Ανοίξτε το φάκελο στον οποίο διατηρούνται τα αρχεία καταγραφής", + "ExitTooltip": "Έξοδος από το Ryujinx", + "OpenSettingsTooltip": "Ανοίξτε το παράθυρο Ρυθμίσεων", + "OpenProfileManagerTooltip": "Ανοίξτε το παράθυρο Διαχείρισης Προφίλ Χρήστη", + "StopEmulationTooltip": "Σταματήστε την εξομοίωση του τρέχοντος παιχνιδιού και επιστρέψτε στην επιλογή παιχνιδιού", + "CheckUpdatesTooltip": "Ελέγξτε για ενημερώσεις του Ryujinx", + "OpenAboutTooltip": "Ανοίξτε το Παράθυρο Σχετικά", + "GridSize": "Μέγεθος Πλέγματος", + "GridSizeTooltip": "Αλλαγή μεγέθους στοιχείων πλέγματος", + "SettingsTabSystemSystemLanguageBrazilianPortuguese": "Πορτογαλικά Βραζιλίας", + "AboutRyujinxContributorsButtonHeader": "Δείτε Όλους τους Συντελεστές", + "SettingsTabSystemAudioVolume": "Ενταση Ήχου: ", + "AudioVolumeTooltip": "Αλλαγή Έντασης Ήχου", + "SettingsTabSystemEnableInternetAccess": "Ενεργοποίηση πρόσβασης επισκέπτη στο Διαδίκτυο", + "EnableInternetAccessTooltip": "Επιτρέπει την πρόσβαση επισκέπτη στο Διαδίκτυο. Εάν ενεργοποιηθεί, η εξομοιωμένη κονσόλα Switch θα συμπεριφέρεται σαν να είναι συνδεδεμένη στο Διαδίκτυο. Λάβετε υπόψη ότι σε ορισμένες περιπτώσεις, οι εφαρμογές ενδέχεται να εξακολουθούν να έχουν πρόσβαση στο Διαδίκτυο, ακόμη και όταν αυτή η επιλογή είναι απενεργοποιημένη", + "GameListContextMenuManageCheatToolTip": "Διαχείριση Κόλπων", + "GameListContextMenuManageCheat": "Διαχείριση Κόλπων", + "GameListContextMenuManageModToolTip": "Manage Mods", + "GameListContextMenuManageMod": "Manage Mods", + "ControllerSettingsStickRange": "Εύρος:", + "DialogStopEmulationTitle": "Ryujinx - Διακοπή εξομοίωσης", + "DialogStopEmulationMessage": "Είστε βέβαιοι ότι θέλετε να σταματήσετε την εξομοίωση;", + "SettingsTabCpu": "Επεξεργαστής", + "SettingsTabAudio": "Ήχος", + "SettingsTabNetwork": "Δίκτυο", + "SettingsTabNetworkConnection": "Σύνδεση δικτύου", + "SettingsTabCpuCache": "Προσωρινή Μνήμη CPU", + "SettingsTabCpuMemory": "Μνήμη CPU", + "DialogUpdaterFlatpakNotSupportedMessage": "Παρακαλούμε ενημερώστε το Ryujinx μέσω FlatHub.", + "UpdaterDisabledWarningTitle": "Ο Διαχειριστής Ενημερώσεων Είναι Απενεργοποιημένος!", + "ControllerSettingsRotate90": "Περιστροφή 90° Δεξιόστροφα", + "IconSize": "Μέγεθος Εικονιδίου", + "IconSizeTooltip": "Αλλάξτε μέγεθος εικονιδίων των παιχνιδιών", + "MenuBarOptionsShowConsole": "Εμφάνιση Κονσόλας", + "ShaderCachePurgeError": "Σφάλμα κατά την εκκαθάριση του shader cache στο {0}: {1}", + "UserErrorNoKeys": "Τα κλειδιά δεν βρέθηκαν", + "UserErrorNoFirmware": "Το firmware δε βρέθηκε", + "UserErrorFirmwareParsingFailed": "Σφάλμα ανάλυσης firmware", + "UserErrorApplicationNotFound": "Η εφαρμογή δε βρέθηκε", + "UserErrorUnknown": "Άγνωστο σφάλμα", + "UserErrorUndefined": "Αόριστο σφάλμα", + "UserErrorNoKeysDescription": "Το Ryujinx δεν κατάφερε να εντοπίσει το αρχείο 'prod.keys'", + "UserErrorNoFirmwareDescription": "Το Ryujinx δεν κατάφερε να εντοπίσει κανένα εγκατεστημένο firmware", + "UserErrorFirmwareParsingFailedDescription": "Το Ryujinx δεν κατάφερε να αναλύσει το συγκεκριμένο firmware. Αυτό συνήθως οφείλετε σε ξεπερασμένα/παλιά κλειδιά.", + "UserErrorApplicationNotFoundDescription": "Το Ryujinx δεν κατάφερε να εντοπίσει έγκυρη εφαρμογή στη συγκεκριμένη διαδρομή.", + "UserErrorUnknownDescription": "Παρουσιάστηκε άγνωστο σφάλμα.", + "UserErrorUndefinedDescription": "Παρουσιάστηκε ένα άγνωστο σφάλμα! Αυτό δεν πρέπει να συμβεί, παρακαλώ επικοινωνήστε με έναν προγραμματιστή!", + "OpenSetupGuideMessage": "Ανοίξτε τον Οδηγό Εγκατάστασης.", + "NoUpdate": "Καμία Eνημέρωση", + "TitleUpdateVersionLabel": "Version {0} - {1}", + "TitleBundledUpdateVersionLabel": "Bundled: Version {0}", + "TitleBundledDlcLabel": "Bundled:", + "TitleXCIStatusPartialLabel": "Partial", + "TitleXCIStatusTrimmableLabel": "Untrimmed", + "TitleXCIStatusUntrimmableLabel": "Trimmed", + "TitleXCIStatusFailedLabel": "(Failed)", + "TitleXCICanSaveLabel": "Save {0:n0} Mb", + "TitleXCISavingLabel": "Saved {0:n0} Mb", + "RyujinxInfo": "Ryujinx - Πληροφορίες", + "RyujinxConfirm": "Ryujinx - Επιβεβαίωση", + "FileDialogAllTypes": "Όλοι οι τύποι", + "Never": "Ποτέ", + "SwkbdMinCharacters": "Πρέπει να έχει μήκος τουλάχιστον {0} χαρακτήρες", + "SwkbdMinRangeCharacters": "Πρέπει να έχει μήκος {0}-{1} χαρακτήρες", + "SoftwareKeyboard": "Εικονικό Πληκτρολόγιο", + "SoftwareKeyboardModeNumeric": "Πρέπει να είναι 0-9 ή '.' μόνο", + "SoftwareKeyboardModeAlphabet": "Πρέπει να μην είναι μόνο χαρακτήρες CJK", + "SoftwareKeyboardModeASCII": "Πρέπει να είναι μόνο κείμενο ASCII", + "ControllerAppletControllers": "Supported Controllers:", + "ControllerAppletPlayers": "Players:", + "ControllerAppletDescription": "Your current configuration is invalid. Open settings and reconfigure your inputs.", + "ControllerAppletDocked": "Docked mode set. Handheld control should be disabled.", + "UpdaterRenaming": "Μετονομασία Παλαιών Αρχείων...", + "UpdaterRenameFailed": "Δεν ήταν δυνατή η μετονομασία του αρχείου: {0}", + "UpdaterAddingFiles": "Προσθήκη Νέων Αρχείων...", + "UpdaterExtracting": "Εξαγωγή Ενημέρωσης...", + "UpdaterDownloading": "Λήψη Ενημέρωσης...", + "Game": "Παιχνίδι", + "Docked": "Προσκολλημένο", + "Handheld": "Χειροκίνητο", + "ConnectionError": "Σφάλμα Σύνδεσης.", + "AboutPageDeveloperListMore": "{0} και περισσότερα...", + "ApiError": "Σφάλμα API.", + "LoadingHeading": "Φόρτωση {0}", + "CompilingPPTC": "Μεταγλώττιση του PTC", + "CompilingShaders": "Σύνταξη των Shaders", + "AllKeyboards": "Όλα τα πληκτρολόγια", + "OpenFileDialogTitle": "Επιλέξτε ένα υποστηριζόμενο αρχείο για άνοιγμα", + "OpenFolderDialogTitle": "Επιλέξτε ένα φάκελο με ένα αποσυμπιεσμένο παιχνίδι", + "AllSupportedFormats": "Όλες Οι Υποστηριζόμενες Μορφές", + "RyujinxUpdater": "Ryujinx Ενημερωτής", + "SettingsTabHotkeys": "Συντομεύσεις Πληκτρολογίου", + "SettingsTabHotkeysHotkeys": "Συντομεύσεις Πληκτρολογίου", + "SettingsTabHotkeysToggleVsyncHotkey": "Εναλλαγή VSync:", + "SettingsTabHotkeysScreenshotHotkey": "Στιγμιότυπο Οθόνης:", + "SettingsTabHotkeysShowUiHotkey": "Εμφάνιση Διεπαφής Χρήστη:", + "SettingsTabHotkeysPauseHotkey": "Παύση:", + "SettingsTabHotkeysToggleMuteHotkey": "Σίγαση:", + "ControllerMotionTitle": "Ρυθμίσεις Ελέγχου Κίνησης", + "ControllerRumbleTitle": "Ρυθμίσεις Δόνησης", + "SettingsSelectThemeFileDialogTitle": "Επιλογή Αρχείου Θέματος", + "SettingsXamlThemeFile": "Αρχείο Θέματος Xaml", + "AvatarWindowTitle": "Διαχείριση Λογαριασμών - Avatar", + "Amiibo": "Amiibo", + "Unknown": "Άγνωστο", + "Usage": "Χρήση", + "Writable": "Εγγράψιμο", + "SelectDlcDialogTitle": "Επιλογή αρχείων DLC", + "SelectUpdateDialogTitle": "Επιλογή αρχείων ενημέρωσης", + "SelectModDialogTitle": "Select mod directory", + "TrimXCIFileDialogTitle": "Check and Trim XCI File", + "TrimXCIFileDialogPrimaryText": "This function will first check the empty space and then trim the XCI File to save disk space.", + "TrimXCIFileDialogSecondaryText": "Current File Size: {0:n} MB\nGame Data Size: {1:n} MB\nDisk Space Savings: {2:n} MB", + "TrimXCIFileNoTrimNecessary": "XCI File does not need to be trimmed. Check logs for further details", + "TrimXCIFileNoUntrimPossible": "XCI File cannot be untrimmed. Check logs for further details", + "TrimXCIFileReadOnlyFileCannotFix": "XCI File is Read Only and could not be made writable. Check logs for further details", + "TrimXCIFileFileSizeChanged": "XCI File has changed in size since it was scanned. Please check the file is not being written to and try again.", + "TrimXCIFileFreeSpaceCheckFailed": "XCI File has data in the free space area, it is not safe to trim", + "TrimXCIFileInvalidXCIFile": "XCI File contains invalid data. Check logs for further details", + "TrimXCIFileFileIOWriteError": "XCI File could not be opened for writing. Check logs for further details", + "TrimXCIFileFailedPrimaryText": "Trimming of the XCI file failed", + "TrimXCIFileCancelled": "The operation was cancelled", + "TrimXCIFileFileUndertermined": "No operation was performed", + "UserProfileWindowTitle": "Διαχειριστής Προφίλ Χρήστη", + "CheatWindowTitle": "Διαχειριστής των Cheats", + "DlcWindowTitle": "Downloadable Content Manager", + "ModWindowTitle": "Manage Mods for {0} ({1})", + "UpdateWindowTitle": "Διαχειριστής Ενημερώσεων Τίτλου", + "XCITrimmerWindowTitle": "XCI File Trimmer", + "XCITrimmerTitleStatusCount": "{0} of {1} Title(s) Selected", + "XCITrimmerTitleStatusCountWithFilter": "{0} of {1} Title(s) Selected ({2} displayed)", + "XCITrimmerTitleStatusTrimming": "Trimming {0} Title(s)...", + "XCITrimmerTitleStatusUntrimming": "Untrimming {0} Title(s)...", + "XCITrimmerTitleStatusFailed": "Failed", + "XCITrimmerPotentialSavings": "Potential Savings", + "XCITrimmerActualSavings": "Actual Savings", + "XCITrimmerSavingsMb": "{0:n0} Mb", + "XCITrimmerSelectDisplayed": "Select Shown", + "XCITrimmerDeselectDisplayed": "Deselect Shown", + "XCITrimmerSortName": "Title", + "XCITrimmerSortSaved": "Space Savings", + "XCITrimmerTrim": "Trim", + "XCITrimmerUntrim": "Untrim", + "UpdateWindowUpdateAddedMessage": "{0} new update(s) added", + "UpdateWindowBundledContentNotice": "Bundled updates cannot be removed, only disabled.", + "CheatWindowHeading": "Διαθέσιμα Cheats για {0} [{1}]", + "BuildId": "BuildId:", + "DlcWindowBundledContentNotice": "Bundled DLC cannot be removed, only disabled.", + "DlcWindowHeading": "{0} Downloadable Content(s) available for {1} ({2})", + "DlcWindowDlcAddedMessage": "{0} new downloadable content(s) added", + "AutoloadDlcAddedMessage": "{0} new downloadable content(s) added", + "AutoloadDlcRemovedMessage": "{0} missing downloadable content(s) removed", + "AutoloadUpdateAddedMessage": "{0} new update(s) added", + "AutoloadUpdateRemovedMessage": "{0} missing update(s) removed", + "ModWindowHeading": "{0} Mod(s)", + "UserProfilesEditProfile": "Επεξεργασία Επιλεγμένων", + "Continue": "Continue", + "Cancel": "Ακύρωση", + "Save": "Αποθήκευση", + "Discard": "Απόρριψη", + "Paused": "Σε παύση", + "UserProfilesSetProfileImage": "Ορισμός Εικόνας Προφίλ", + "UserProfileEmptyNameError": "Απαιτείται όνομα", + "UserProfileNoImageError": "Η εικόνα προφίλ πρέπει να οριστεί", + "GameUpdateWindowHeading": "{0} Update(s) available for {1} ({2})", + "SettingsTabHotkeysResScaleUpHotkey": "Αύξηση της ανάλυσης:", + "SettingsTabHotkeysResScaleDownHotkey": "Μείωση της ανάλυσης:", + "UserProfilesName": "Όνομα:", + "UserProfilesUserId": "User Id:", + "SettingsTabGraphicsBackend": "Σύστημα Υποστήριξης Γραφικών", + "SettingsTabGraphicsBackendTooltip": "Select the graphics backend that will be used in the emulator.\n\nVulkan is overall better for all modern graphics cards, as long as their drivers are up to date. Vulkan also features faster shader compilation (less stuttering) on all GPU vendors.\n\nOpenGL may achieve better results on old Nvidia GPUs, on old AMD GPUs on Linux, or on GPUs with lower VRAM, though shader compilation stutters will be greater.\n\nSet to Vulkan if unsure. Set to OpenGL if your GPU does not support Vulkan even with the latest graphics drivers.", + "SettingsEnableTextureRecompression": "Ενεργοποίηση Επανασυμπίεσης Των Texture", + "SettingsEnableTextureRecompressionTooltip": "Compresses ASTC textures in order to reduce VRAM usage.\n\nGames using this texture format include Astral Chain, Bayonetta 3, Fire Emblem Engage, Metroid Prime Remastered, Super Mario Bros. Wonder and The Legend of Zelda: Tears of the Kingdom.\n\nGraphics cards with 4GiB VRAM or less will likely crash at some point while running these games.\n\nEnable only if you're running out of VRAM on the aforementioned games. Leave OFF if unsure.", + "SettingsTabGraphicsPreferredGpu": "Προτιμώμενη GPU", + "SettingsTabGraphicsPreferredGpuTooltip": "Επιλέξτε την κάρτα γραφικών η οποία θα χρησιμοποιηθεί από το Vulkan.\n\nΔεν επηρεάζει το OpenGL.\n\nΔιαλέξτε την GPU που διαθέτει την υπόδειξη \"dGPU\" αν δεν είστε βέβαιοι. Αν δεν υπάρχει κάποιαν, το πειράξετε", + "SettingsAppRequiredRestartMessage": "Απαιτείται Επανεκκίνηση Του Ryujinx", + "SettingsGpuBackendRestartMessage": "Οι ρυθμίσεις GPU έχουν αλλαχτεί. Θα χρειαστεί επανεκκίνηση του Ryujinx για να τεθούν σε ισχύ.", + "SettingsGpuBackendRestartSubMessage": "Θέλετε να κάνετε επανεκκίνηση τώρα;", + "RyujinxUpdaterMessage": "Θέλετε να ενημερώσετε το Ryujinx στην πιο πρόσφατη έκδοση:", + "SettingsTabHotkeysVolumeUpHotkey": "Αύξηση Έντασης:", + "SettingsTabHotkeysVolumeDownHotkey": "Μείωση Έντασης:", + "SettingsEnableMacroHLE": "Ενεργοποίηση του Macro HLE", + "SettingsEnableMacroHLETooltip": "Προσομοίωση του κώδικα GPU Macro .\n\nΒελτιώνει την απόδοση, αλλά μπορεί να προκαλέσει γραφικά προβλήματα σε μερικά παιχνίδια.\n\nΑφήστε ΕΝΕΡΓΟ αν δεν είστε σίγουροι.", + "SettingsEnableColorSpacePassthrough": "Διέλευση Χρωματικού Χώρου", + "SettingsEnableColorSpacePassthroughTooltip": "Σκηνοθετεί το σύστημα υποστήριξης του Vulkan για να περάσει από πληροφορίες χρώματος χωρίς να καθορίσει έναν χρωματικό χώρο. Για χρήστες με ευρείες οθόνες γκάμας, αυτό μπορεί να οδηγήσει σε πιο ζωηρά χρώματα, με κόστος την ορθότητα του χρώματος.", + "VolumeShort": "Έντ.", + "UserProfilesManageSaves": "Διαχείριση Των Save", + "DeleteUserSave": "Επιθυμείτε να διαγράψετε το save χρήστη για το συγκεκριμένο παιχνίδι;", + "IrreversibleActionNote": "Αυτή η ενέργεια είναι μη αναστρέψιμη.", + "SaveManagerHeading": "Manage Saves for {0}", + "SaveManagerTitle": "Διαχειριστής Save", + "Name": "Όνομα", + "Size": "Μέγεθος", + "Search": "Αναζήτηση", + "UserProfilesRecoverLostAccounts": "Ανάκτηση Χαμένων Λογαριασμών", + "Recover": "Ανάκτηση", + "UserProfilesRecoverHeading": "Βρέθηκαν save για τους ακόλουθους λογαριασμούς", + "UserProfilesRecoverEmptyList": "Δεν υπάρχουν προφίλ για ανάκτηση", + "GraphicsAATooltip": "Applies anti-aliasing to the game render.\n\nFXAA will blur most of the image, while SMAA will attempt to find jagged edges and smooth them out.\n\nNot recommended to use in conjunction with the FSR scaling filter.\n\nThis option can be changed while a game is running by clicking \"Apply\" below; you can simply move the settings window aside and experiment until you find your preferred look for a game.\n\nLeave on NONE if unsure.", + "GraphicsAALabel": "Anti-Aliasing", + "GraphicsScalingFilterLabel": "Φίλτρο Κλιμάκωσης:", + "GraphicsScalingFilterTooltip": "Choose the scaling filter that will be applied when using resolution scale.\n\nBilinear works well for 3D games and is a safe default option.\n\nNearest is recommended for pixel art games.\n\nFSR 1.0 is merely a sharpening filter, not recommended for use with FXAA or SMAA.\n\nThis option can be changed while a game is running by clicking \"Apply\" below; you can simply move the settings window aside and experiment until you find your preferred look for a game.\n\nLeave on BILINEAR if unsure.", + "GraphicsScalingFilterBilinear": "Bilinear", + "GraphicsScalingFilterNearest": "Nearest", + "GraphicsScalingFilterFsr": "FSR", + "GraphicsScalingFilterArea": "Area", + "GraphicsScalingFilterLevelLabel": "Επίπεδο", + "GraphicsScalingFilterLevelTooltip": "Set FSR 1.0 sharpening level. Higher is sharper.", + "SmaaLow": "Χαμηλό SMAA", + "SmaaMedium": " Μεσαίο SMAA", + "SmaaHigh": "Υψηλό SMAA", + "SmaaUltra": "Oύλτρα SMAA", + "UserEditorTitle": "Επεξεργασία Χρήστη", + "UserEditorTitleCreate": "Δημιουργία Χρήστη", + "SettingsTabNetworkInterface": "Διεπαφή Δικτύου", + "NetworkInterfaceTooltip": "The network interface used for LAN/LDN features.\n\nIn conjunction with a VPN or XLink Kai and a game with LAN support, can be used to spoof a same-network connection over the Internet.\n\nLeave on DEFAULT if unsure.", + "NetworkInterfaceDefault": "Προεπιλογή", + "PackagingShaders": "Shaders Συσκευασίας", + "AboutChangelogButton": "Προβολή αρχείου αλλαγών στο GitHub", + "AboutChangelogButtonTooltipMessage": "Κάντε κλικ για να ανοίξετε το αρχείο αλλαγών για αυτήν την έκδοση στο προεπιλεγμένο πρόγραμμα περιήγησης σας.", + "SettingsTabNetworkMultiplayer": "Πολλαπλοί παίκτες", + "MultiplayerMode": "Λειτουργία:", + "MultiplayerModeTooltip": "Change LDN multiplayer mode.\n\nLdnMitm will modify local wireless/local play functionality in games to function as if it were LAN, allowing for local, same-network connections with other Ryujinx instances and hacked Nintendo Switch consoles that have the ldn_mitm module installed.\n\nMultiplayer requires all players to be on the same game version (i.e. Super Smash Bros. Ultimate v13.0.1 can't connect to v13.0.0).\n\nLeave DISABLED if unsure.", + "MultiplayerModeDisabled": "Disabled", + "MultiplayerModeLdnMitm": "ldn_mitm", + "MultiplayerModeLdnRyu": "RyuLDN", + "MultiplayerDisableP2P": "Disable P2P Network Hosting (may increase latency)", + "MultiplayerDisableP2PTooltip": "Disable P2P network hosting, peers will proxy through the master server instead of connecting to you directly.", + "LdnPassphrase": "Network Passphrase:", + "LdnPassphraseTooltip": "You will only be able to see hosted games with the same passphrase as you.", + "LdnPassphraseInputTooltip": "Enter a passphrase in the format Ryujinx-<8 hex chars>. You will only be able to see hosted games with the same passphrase as you.", + "LdnPassphraseInputPublic": "(public)", + "GenLdnPass": "Generate Random", + "GenLdnPassTooltip": "Generates a new passphrase, which can be shared with other players.", + "ClearLdnPass": "Clear", + "ClearLdnPassTooltip": "Clears the current passphrase, returning to the public network.", + "InvalidLdnPassphrase": "Invalid Passphrase! Must be in the format \"Ryujinx-<8 hex chars>\"" +} diff --git a/src/Ryujinx/Assets/Locales/en_US.json b/src/Ryujinx/Assets/Locales/en_US.json new file mode 100644 index 000000000..90290b760 --- /dev/null +++ b/src/Ryujinx/Assets/Locales/en_US.json @@ -0,0 +1,882 @@ +{ + "Language": "English (US)", + "MenuBarFileOpenApplet": "Open Applet", + "MenuBarFileOpenAppletOpenMiiApplet": "Mii Edit Applet", + "MenuBarFileOpenAppletOpenMiiAppletToolTip": "Open Mii Editor Applet in Standalone mode", + "SettingsTabInputDirectMouseAccess": "Direct Mouse Access", + "SettingsTabSystemMemoryManagerMode": "Memory Manager Mode:", + "SettingsTabSystemMemoryManagerModeSoftware": "Software", + "SettingsTabSystemMemoryManagerModeHost": "Host (fast)", + "SettingsTabSystemMemoryManagerModeHostUnchecked": "Host Unchecked (fastest, unsafe)", + "SettingsTabSystemUseHypervisor": "Use Hypervisor", + "MenuBarFile": "_File", + "MenuBarFileOpenFromFile": "_Load Application From File", + "MenuBarFileOpenFromFileError": "No applications found in selected file.", + "MenuBarFileOpenUnpacked": "Load _Unpacked Game", + "MenuBarFileLoadDlcFromFolder": "Load DLC From Folder", + "MenuBarFileLoadTitleUpdatesFromFolder": "Load Title Updates From Folder", + "MenuBarFileOpenEmuFolder": "Open Ryujinx Folder", + "MenuBarFileOpenLogsFolder": "Open Logs Folder", + "MenuBarFileExit": "_Exit", + "MenuBarOptions": "_Options", + "MenuBarOptionsToggleFullscreen": "Toggle Fullscreen", + "MenuBarOptionsStartGamesInFullscreen": "Start Games in Fullscreen Mode", + "MenuBarOptionsStopEmulation": "Stop Emulation", + "MenuBarOptionsSettings": "_Settings", + "MenuBarOptionsManageUserProfiles": "_Manage User Profiles", + "MenuBarActions": "_Actions", + "MenuBarOptionsSimulateWakeUpMessage": "Simulate Wake-up message", + "MenuBarActionsScanAmiibo": "Scan An Amiibo", + "MenuBarTools": "_Tools", + "MenuBarToolsInstallFirmware": "Install Firmware", + "MenuBarFileToolsInstallFirmwareFromFile": "Install a firmware from XCI or ZIP", + "MenuBarFileToolsInstallFirmwareFromDirectory": "Install a firmware from a directory", + "MenuBarToolsManageFileTypes": "Manage file types", + "MenuBarToolsInstallFileTypes": "Install file types", + "MenuBarToolsUninstallFileTypes": "Uninstall file types", + "MenuBarToolsXCITrimmer": "Trim XCI Files", + "MenuBarView": "_View", + "MenuBarViewWindow": "Window Size", + "MenuBarViewWindow720": "720p", + "MenuBarViewWindow1080": "1080p", + "MenuBarHelp": "_Help", + "MenuBarHelpCheckForUpdates": "Check for Updates", + "MenuBarHelpAbout": "About", + "MenuSearch": "Search...", + "GameListHeaderFavorite": "Fav", + "GameListHeaderIcon": "Icon", + "GameListHeaderApplication": "Name", + "GameListHeaderDeveloper": "Developer", + "GameListHeaderVersion": "Version", + "GameListHeaderTimePlayed": "Play Time", + "GameListHeaderLastPlayed": "Last Played", + "GameListHeaderFileExtension": "File Ext", + "GameListHeaderFileSize": "File Size", + "GameListHeaderPath": "Path", + "GameListContextMenuOpenUserSaveDirectory": "Open User Save Directory", + "GameListContextMenuOpenUserSaveDirectoryToolTip": "Opens the directory which contains Application's User Save", + "GameListContextMenuOpenDeviceSaveDirectory": "Open Device Save Directory", + "GameListContextMenuOpenDeviceSaveDirectoryToolTip": "Opens the directory which contains Application's Device Save", + "GameListContextMenuOpenBcatSaveDirectory": "Open BCAT Save Directory", + "GameListContextMenuOpenBcatSaveDirectoryToolTip": "Opens the directory which contains Application's BCAT Save", + "GameListContextMenuManageTitleUpdates": "Manage Title Updates", + "GameListContextMenuManageTitleUpdatesToolTip": "Opens the Title Update management window", + "GameListContextMenuManageDlc": "Manage DLC", + "GameListContextMenuManageDlcToolTip": "Opens the DLC management window", + "GameListContextMenuCacheManagement": "Cache Management", + "GameListContextMenuCacheManagementPurgePptc": "Queue PPTC Rebuild", + "GameListContextMenuCacheManagementPurgePptcToolTip": "Trigger PPTC to rebuild at boot time on the next game launch", + "GameListContextMenuCacheManagementPurgeShaderCache": "Purge Shader Cache", + "GameListContextMenuCacheManagementPurgeShaderCacheToolTip": "Deletes Application's shader cache", + "GameListContextMenuCacheManagementOpenPptcDirectory": "Open PPTC Directory", + "GameListContextMenuCacheManagementOpenPptcDirectoryToolTip": "Opens the directory which contains Application's PPTC cache", + "GameListContextMenuCacheManagementOpenShaderCacheDirectory": "Open Shader Cache Directory", + "GameListContextMenuCacheManagementOpenShaderCacheDirectoryToolTip": "Opens the directory which contains Application's shader cache", + "GameListContextMenuExtractData": "Extract Data", + "GameListContextMenuExtractDataExeFS": "ExeFS", + "GameListContextMenuExtractDataExeFSToolTip": "Extract the ExeFS section from Application's current config (including updates)", + "GameListContextMenuExtractDataRomFS": "RomFS", + "GameListContextMenuExtractDataRomFSToolTip": "Extract the RomFS section from Application's current config (including updates)", + "GameListContextMenuExtractDataLogo": "Logo", + "GameListContextMenuExtractDataLogoToolTip": "Extract the Logo section from Application's current config (including updates)", + "GameListContextMenuCreateShortcut": "Create Application Shortcut", + "GameListContextMenuCreateShortcutToolTip": "Create a Desktop Shortcut that launches the selected Application", + "GameListContextMenuCreateShortcutToolTipMacOS": "Create a shortcut in macOS's Applications folder that launches the selected Application", + "GameListContextMenuOpenModsDirectory": "Open Mods Directory", + "GameListContextMenuOpenModsDirectoryToolTip": "Opens the directory which contains Application's Mods", + "GameListContextMenuOpenSdModsDirectory": "Open Atmosphere Mods Directory", + "GameListContextMenuOpenSdModsDirectoryToolTip": "Opens the alternative SD card Atmosphere directory which contains Application's Mods. Useful for mods that are packaged for real hardware.", + "GameListContextMenuTrimXCI": "Check and Trim XCI File", + "GameListContextMenuTrimXCIToolTip": "Check and Trim XCI File to Save Disk Space", + "StatusBarGamesLoaded": "{0}/{1} Games Loaded", + "StatusBarSystemVersion": "System Version: {0}", + "StatusBarXCIFileTrimming": "Trimming XCI File '{0}'", + "LinuxVmMaxMapCountDialogTitle": "Low limit for memory mappings detected", + "LinuxVmMaxMapCountDialogTextPrimary": "Would you like to increase the value of vm.max_map_count to {0}", + "LinuxVmMaxMapCountDialogTextSecondary": "Some games might try to create more memory mappings than currently allowed. Ryujinx will crash as soon as this limit gets exceeded.", + "LinuxVmMaxMapCountDialogButtonUntilRestart": "Yes, until the next restart", + "LinuxVmMaxMapCountDialogButtonPersistent": "Yes, permanently", + "LinuxVmMaxMapCountWarningTextPrimary": "Max amount of memory mappings is lower than recommended.", + "LinuxVmMaxMapCountWarningTextSecondary": "The current value of vm.max_map_count ({0}) is lower than {1}. Some games might try to create more memory mappings than currently allowed. Ryujinx will crash as soon as this limit gets exceeded.\n\nYou might want to either manually increase the limit or install pkexec, which allows Ryujinx to assist with that.", + "Settings": "Settings", + "SettingsTabGeneral": "User Interface", + "SettingsTabGeneralGeneral": "General", + "SettingsTabGeneralEnableDiscordRichPresence": "Enable Discord Rich Presence", + "SettingsTabGeneralCheckUpdatesOnLaunch": "Check for Updates on Launch", + "SettingsTabGeneralShowConfirmExitDialog": "Show \"Confirm Exit\" Dialog", + "SettingsTabGeneralRememberWindowState": "Remember Window Size/Position", + "SettingsTabGeneralShowTitleBar": "Show Title Bar (Requires restart)", + "SettingsTabGeneralHideCursor": "Hide Cursor:", + "SettingsTabGeneralHideCursorNever": "Never", + "SettingsTabGeneralHideCursorOnIdle": "On Idle", + "SettingsTabGeneralHideCursorAlways": "Always", + "SettingsTabGeneralGameDirectories": "Game Directories", + "SettingsTabGeneralAutoloadDirectories": "Autoload DLC/Updates Directories", + "SettingsTabGeneralAutoloadNote": "DLC and Updates which refer to missing files will be unloaded automatically", + "SettingsTabGeneralAdd": "Add", + "SettingsTabGeneralRemove": "Remove", + "SettingsTabSystem": "System", + "SettingsTabSystemCore": "Core", + "SettingsTabSystemSystemRegion": "System Region:", + "SettingsTabSystemSystemRegionJapan": "Japan", + "SettingsTabSystemSystemRegionUSA": "USA", + "SettingsTabSystemSystemRegionEurope": "Europe", + "SettingsTabSystemSystemRegionAustralia": "Australia", + "SettingsTabSystemSystemRegionChina": "China", + "SettingsTabSystemSystemRegionKorea": "Korea", + "SettingsTabSystemSystemRegionTaiwan": "Taiwan", + "SettingsTabSystemSystemLanguage": "System Language:", + "SettingsTabSystemSystemLanguageJapanese": "Japanese", + "SettingsTabSystemSystemLanguageAmericanEnglish": "American English", + "SettingsTabSystemSystemLanguageFrench": "French", + "SettingsTabSystemSystemLanguageGerman": "German", + "SettingsTabSystemSystemLanguageItalian": "Italian", + "SettingsTabSystemSystemLanguageSpanish": "Spanish", + "SettingsTabSystemSystemLanguageChinese": "Chinese", + "SettingsTabSystemSystemLanguageKorean": "Korean", + "SettingsTabSystemSystemLanguageDutch": "Dutch", + "SettingsTabSystemSystemLanguagePortuguese": "Portuguese", + "SettingsTabSystemSystemLanguageRussian": "Russian", + "SettingsTabSystemSystemLanguageTaiwanese": "Taiwanese", + "SettingsTabSystemSystemLanguageBritishEnglish": "British English", + "SettingsTabSystemSystemLanguageCanadianFrench": "Canadian French", + "SettingsTabSystemSystemLanguageLatinAmericanSpanish": "Latin American Spanish", + "SettingsTabSystemSystemLanguageSimplifiedChinese": "Simplified Chinese", + "SettingsTabSystemSystemLanguageTraditionalChinese": "Traditional Chinese", + "SettingsTabSystemSystemTimeZone": "System Time Zone:", + "SettingsTabSystemSystemTime": "System Time:", + "SettingsTabSystemVSyncMode": "VSync:", + "SettingsTabSystemEnableCustomVSyncInterval": "Enable custom refresh rate (Experimental)", + "SettingsTabSystemVSyncModeSwitch": "Switch", + "SettingsTabSystemVSyncModeUnbounded": "Unbounded", + "SettingsTabSystemVSyncModeCustom": "Custom Refresh Rate", + "SettingsTabSystemVSyncModeTooltip": "Emulated Vertical Sync. 'Switch' emulates the Switch's refresh rate of 60Hz. 'Unbounded' is an unbounded refresh rate.", + "SettingsTabSystemVSyncModeTooltipCustom": "Emulated Vertical Sync. 'Switch' emulates the Switch's refresh rate of 60Hz. 'Unbounded' is an unbounded refresh rate. 'Custom' emulates the specified custom refresh rate.", + "SettingsTabSystemEnableCustomVSyncIntervalTooltip": "Allows the user to specify an emulated refresh rate. In some titles, this may speed up or slow down the rate of gameplay logic. In other titles, it may allow for capping FPS at some multiple of the refresh rate, or lead to unpredictable behavior. This is an experimental feature, with no guarantees for how gameplay will be affected. \n\nLeave OFF if unsure.", + "SettingsTabSystemCustomVSyncIntervalValueTooltip": "The custom refresh rate target value.", + "SettingsTabSystemCustomVSyncIntervalSliderTooltip": "The custom refresh rate, as a percentage of the normal Switch refresh rate.", + "SettingsTabSystemCustomVSyncIntervalPercentage": "Custom Refresh Rate %:", + "SettingsTabSystemCustomVSyncIntervalValue": "Custom Refresh Rate Value:", + "SettingsTabSystemEnablePptc": "PPTC (Profiled Persistent Translation Cache)", + "SettingsTabSystemEnableLowPowerPptc": "Low-power PPTC cache", + "SettingsTabSystemEnableFsIntegrityChecks": "FS Integrity Checks", + "SettingsTabSystemAudioBackend": "Audio Backend:", + "SettingsTabSystemAudioBackendDummy": "Dummy", + "SettingsTabSystemAudioBackendOpenAL": "OpenAL", + "SettingsTabSystemAudioBackendSoundIO": "SoundIO", + "SettingsTabSystemAudioBackendSDL2": "SDL2", + "SettingsTabSystemCustomVSyncInterval": "Interval", + "SettingsTabSystemHacks": "Hacks", + "SettingsTabSystemHacksNote": "May cause instability", + "SettingsTabSystemDramSize": "DRAM size:", + "SettingsTabSystemDramSize4GiB": "4GiB", + "SettingsTabSystemDramSize6GiB": "6GiB", + "SettingsTabSystemDramSize8GiB": "8GiB", + "SettingsTabSystemDramSize12GiB": "12GiB", + "SettingsTabSystemIgnoreMissingServices": "Ignore Missing Services", + "SettingsTabSystemIgnoreApplet": "Ignore Applet", + "SettingsTabGraphics": "Graphics", + "SettingsTabGraphicsAPI": "Graphics API", + "SettingsTabGraphicsEnableShaderCache": "Enable Shader Cache", + "SettingsTabGraphicsAnisotropicFiltering": "Anisotropic Filtering:", + "SettingsTabGraphicsAnisotropicFilteringAuto": "Auto", + "SettingsTabGraphicsAnisotropicFiltering2x": "2x", + "SettingsTabGraphicsAnisotropicFiltering4x": "4x", + "SettingsTabGraphicsAnisotropicFiltering8x": "8x", + "SettingsTabGraphicsAnisotropicFiltering16x": "16x", + "SettingsTabGraphicsResolutionScale": "Resolution Scale:", + "SettingsTabGraphicsResolutionScaleCustom": "Custom (Not recommended)", + "SettingsTabGraphicsResolutionScaleNative": "Native (720p/1080p)", + "SettingsTabGraphicsResolutionScale2x": "2x (1440p/2160p)", + "SettingsTabGraphicsResolutionScale3x": "3x (2160p/3240p)", + "SettingsTabGraphicsResolutionScale4x": "4x (2880p/4320p) (Not recommended)", + "SettingsTabGraphicsAspectRatio": "Aspect Ratio:", + "SettingsTabGraphicsAspectRatio4x3": "4:3", + "SettingsTabGraphicsAspectRatio16x9": "16:9", + "SettingsTabGraphicsAspectRatio16x10": "16:10", + "SettingsTabGraphicsAspectRatio21x9": "21:9", + "SettingsTabGraphicsAspectRatio32x9": "32:9", + "SettingsTabGraphicsAspectRatioStretch": "Stretch to Fit Window", + "SettingsTabGraphicsDeveloperOptions": "Developer Options", + "SettingsTabGraphicsShaderDumpPath": "Graphics Shader Dump Path:", + "SettingsTabLogging": "Logging", + "SettingsTabLoggingLogging": "Logging", + "SettingsTabLoggingEnableLoggingToFile": "Enable Logging to File", + "SettingsTabLoggingEnableStubLogs": "Enable Stub Logs", + "SettingsTabLoggingEnableInfoLogs": "Enable Info Logs", + "SettingsTabLoggingEnableWarningLogs": "Enable Warning Logs", + "SettingsTabLoggingEnableErrorLogs": "Enable Error Logs", + "SettingsTabLoggingEnableTraceLogs": "Enable Trace Logs", + "SettingsTabLoggingEnableGuestLogs": "Enable Guest Logs", + "SettingsTabLoggingEnableFsAccessLogs": "Enable Fs Access Logs", + "SettingsTabLoggingFsGlobalAccessLogMode": "Fs Global Access Log Mode:", + "SettingsTabLoggingDeveloperOptions": "Developer Options", + "SettingsTabLoggingDeveloperOptionsNote": "WARNING: Will reduce performance", + "SettingsTabLoggingGraphicsBackendLogLevel": "Graphics Backend Log Level:", + "SettingsTabLoggingGraphicsBackendLogLevelNone": "None", + "SettingsTabLoggingGraphicsBackendLogLevelError": "Error", + "SettingsTabLoggingGraphicsBackendLogLevelPerformance": "Slowdowns", + "SettingsTabLoggingGraphicsBackendLogLevelAll": "All", + "SettingsTabLoggingEnableDebugLogs": "Enable Debug Logs", + "SettingsTabInput": "Input", + "SettingsTabInputEnableDockedMode": "Docked Mode", + "SettingsTabInputDirectKeyboardAccess": "Direct Keyboard Access", + "SettingsButtonSave": "Save", + "SettingsButtonClose": "Close", + "SettingsButtonOk": "OK", + "SettingsButtonCancel": "Cancel", + "SettingsButtonApply": "Apply", + "ControllerSettingsPlayer": "Player", + "ControllerSettingsPlayer1": "Player 1", + "ControllerSettingsPlayer2": "Player 2", + "ControllerSettingsPlayer3": "Player 3", + "ControllerSettingsPlayer4": "Player 4", + "ControllerSettingsPlayer5": "Player 5", + "ControllerSettingsPlayer6": "Player 6", + "ControllerSettingsPlayer7": "Player 7", + "ControllerSettingsPlayer8": "Player 8", + "ControllerSettingsHandheld": "Handheld", + "ControllerSettingsInputDevice": "Input Device", + "ControllerSettingsRefresh": "Refresh", + "ControllerSettingsDeviceDisabled": "Disabled", + "ControllerSettingsControllerType": "Controller Type", + "ControllerSettingsControllerTypeHandheld": "Handheld", + "ControllerSettingsControllerTypeProController": "Pro Controller", + "ControllerSettingsControllerTypeJoyConPair": "JoyCon Pair", + "ControllerSettingsControllerTypeJoyConLeft": "JoyCon Left", + "ControllerSettingsControllerTypeJoyConRight": "JoyCon Right", + "ControllerSettingsProfile": "Profile", + "ControllerSettingsProfileDefault": "Default", + "ControllerSettingsLoad": "Load", + "ControllerSettingsAdd": "Add", + "ControllerSettingsRemove": "Remove", + "ControllerSettingsButtons": "Buttons", + "ControllerSettingsButtonA": "A", + "ControllerSettingsButtonB": "B", + "ControllerSettingsButtonX": "X", + "ControllerSettingsButtonY": "Y", + "ControllerSettingsButtonPlus": "+", + "ControllerSettingsButtonMinus": "-", + "ControllerSettingsDPad": "Directional Pad", + "ControllerSettingsDPadUp": "Up", + "ControllerSettingsDPadDown": "Down", + "ControllerSettingsDPadLeft": "Left", + "ControllerSettingsDPadRight": "Right", + "ControllerSettingsStickButton": "Button", + "ControllerSettingsStickUp": "Up", + "ControllerSettingsStickDown": "Down", + "ControllerSettingsStickLeft": "Left", + "ControllerSettingsStickRight": "Right", + "ControllerSettingsStickStick": "Stick", + "ControllerSettingsStickInvertXAxis": "Invert Stick X", + "ControllerSettingsStickInvertYAxis": "Invert Stick Y", + "ControllerSettingsStickDeadzone": "Deadzone:", + "ControllerSettingsLStick": "Left Stick", + "ControllerSettingsRStick": "Right Stick", + "ControllerSettingsTriggersLeft": "Triggers Left", + "ControllerSettingsTriggersRight": "Triggers Right", + "ControllerSettingsTriggersButtonsLeft": "Trigger Buttons Left", + "ControllerSettingsTriggersButtonsRight": "Trigger Buttons Right", + "ControllerSettingsTriggers": "Triggers", + "ControllerSettingsTriggerL": "L", + "ControllerSettingsTriggerR": "R", + "ControllerSettingsTriggerZL": "ZL", + "ControllerSettingsTriggerZR": "ZR", + "ControllerSettingsLeftSL": "SL", + "ControllerSettingsLeftSR": "SR", + "ControllerSettingsRightSL": "SL", + "ControllerSettingsRightSR": "SR", + "ControllerSettingsExtraButtonsLeft": "Buttons Left", + "ControllerSettingsExtraButtonsRight": "Buttons Right", + "ControllerSettingsMisc": "Miscellaneous", + "ControllerSettingsTriggerThreshold": "Trigger Threshold:", + "ControllerSettingsMotion": "Motion", + "ControllerSettingsMotionUseCemuhookCompatibleMotion": "Use CemuHook compatible motion", + "ControllerSettingsMotionControllerSlot": "Controller Slot:", + "ControllerSettingsMotionMirrorInput": "Mirror Input", + "ControllerSettingsMotionRightJoyConSlot": "Right JoyCon Slot:", + "ControllerSettingsMotionServerHost": "Server Host:", + "ControllerSettingsMotionGyroSensitivity": "Gyro Sensitivity:", + "ControllerSettingsMotionGyroDeadzone": "Gyro Deadzone:", + "ControllerSettingsSave": "Save", + "ControllerSettingsClose": "Close", + "KeyUnknown": "Unknown", + "KeyShiftLeft": "Shift Left", + "KeyShiftRight": "Shift Right", + "KeyControlLeft": "Ctrl Left", + "KeyMacControlLeft": "⌃ Left", + "KeyControlRight": "Ctrl Right", + "KeyMacControlRight": "⌃ Right", + "KeyAltLeft": "Alt Left", + "KeyMacAltLeft": "⌥ Left", + "KeyAltRight": "Alt Right", + "KeyMacAltRight": "⌥ Right", + "KeyWinLeft": "⊞ Left", + "KeyMacWinLeft": "⌘ Left", + "KeyWinRight": "⊞ Right", + "KeyMacWinRight": "⌘ Right", + "KeyMenu": "Menu", + "KeyUp": "Up", + "KeyDown": "Down", + "KeyLeft": "Left", + "KeyRight": "Right", + "KeyEnter": "Enter", + "KeyEscape": "Escape", + "KeySpace": "Space", + "KeyTab": "Tab", + "KeyBackSpace": "Backspace", + "KeyInsert": "Insert", + "KeyDelete": "Delete", + "KeyPageUp": "Page Up", + "KeyPageDown": "Page Down", + "KeyHome": "Home", + "KeyEnd": "End", + "KeyCapsLock": "Caps Lock", + "KeyScrollLock": "Scroll Lock", + "KeyPrintScreen": "Print Screen", + "KeyPause": "Pause", + "KeyNumLock": "Num Lock", + "KeyClear": "Clear", + "KeyKeypad0": "Keypad 0", + "KeyKeypad1": "Keypad 1", + "KeyKeypad2": "Keypad 2", + "KeyKeypad3": "Keypad 3", + "KeyKeypad4": "Keypad 4", + "KeyKeypad5": "Keypad 5", + "KeyKeypad6": "Keypad 6", + "KeyKeypad7": "Keypad 7", + "KeyKeypad8": "Keypad 8", + "KeyKeypad9": "Keypad 9", + "KeyKeypadDivide": "Keypad Divide", + "KeyKeypadMultiply": "Keypad Multiply", + "KeyKeypadSubtract": "Keypad Subtract", + "KeyKeypadAdd": "Keypad Add", + "KeyKeypadDecimal": "Keypad Decimal", + "KeyKeypadEnter": "Keypad Enter", + "KeyNumber0": "0", + "KeyNumber1": "1", + "KeyNumber2": "2", + "KeyNumber3": "3", + "KeyNumber4": "4", + "KeyNumber5": "5", + "KeyNumber6": "6", + "KeyNumber7": "7", + "KeyNumber8": "8", + "KeyNumber9": "9", + "KeyTilde": "~", + "KeyGrave": "`", + "KeyMinus": "-", + "KeyPlus": "+", + "KeyBracketLeft": "[", + "KeyBracketRight": "]", + "KeySemicolon": ";", + "KeyQuote": "\"", + "KeyComma": ",", + "KeyPeriod": ".", + "KeySlash": "/", + "KeyBackSlash": "\\", + "KeyUnbound": "Unbound", + "GamepadLeftStick": "L Stick Button", + "GamepadRightStick": "R Stick Button", + "GamepadLeftShoulder": "Left Shoulder", + "GamepadRightShoulder": "Right Shoulder", + "GamepadLeftTrigger": "Left Trigger", + "GamepadRightTrigger": "Right Trigger", + "GamepadDpadUp": "Up", + "GamepadDpadDown": "Down", + "GamepadDpadLeft": "Left", + "GamepadDpadRight": "Right", + "GamepadMinus": "-", + "GamepadPlus": "+", + "GamepadGuide": "Guide", + "GamepadMisc1": "Misc", + "GamepadPaddle1": "Paddle 1", + "GamepadPaddle2": "Paddle 2", + "GamepadPaddle3": "Paddle 3", + "GamepadPaddle4": "Paddle 4", + "GamepadTouchpad": "Touchpad", + "GamepadSingleLeftTrigger0": "Left Trigger 0", + "GamepadSingleRightTrigger0": "Right Trigger 0", + "GamepadSingleLeftTrigger1": "Left Trigger 1", + "GamepadSingleRightTrigger1": "Right Trigger 1", + "StickLeft": "Left Stick", + "StickRight": "Right Stick", + "UserProfilesSelectedUserProfile": "Selected User Profile:", + "UserProfilesSaveProfileName": "Save Profile Name", + "UserProfilesChangeProfileImage": "Change Profile Image", + "UserProfilesAvailableUserProfiles": "Available User Profiles:", + "UserProfilesAddNewProfile": "Create Profile", + "UserProfilesDelete": "Delete", + "UserProfilesClose": "Close", + "ProfileNameSelectionWatermark": "Choose a nickname", + "ProfileImageSelectionTitle": "Profile Image Selection", + "ProfileImageSelectionHeader": "Choose a profile Image", + "ProfileImageSelectionNote": "You may import a custom profile image, or select an avatar from system firmware", + "ProfileImageSelectionImportImage": "Import Image File", + "ProfileImageSelectionSelectAvatar": "Select Firmware Avatar", + "InputDialogTitle": "Input Dialog", + "InputDialogOk": "OK", + "InputDialogCancel": "Cancel", + "InputDialogCancelling": "Cancelling", + "InputDialogClose": "Close", + "InputDialogAddNewProfileTitle": "Choose the Profile Name", + "InputDialogAddNewProfileHeader": "Please Enter a Profile Name", + "InputDialogAddNewProfileSubtext": "(Max Length: {0})", + "AvatarChoose": "Choose Avatar", + "AvatarSetBackgroundColor": "Set Background Color", + "AvatarClose": "Close", + "ControllerSettingsLoadProfileToolTip": "Load Profile", + "ControllerSettingsViewProfileToolTip": "View Profile", + "ControllerSettingsAddProfileToolTip": "Add Profile", + "ControllerSettingsRemoveProfileToolTip": "Remove Profile", + "ControllerSettingsSaveProfileToolTip": "Save Profile", + "MenuBarFileToolsTakeScreenshot": "Take Screenshot", + "MenuBarFileToolsHideUi": "Hide UI", + "GameListContextMenuRunApplication": "Run Application", + "GameListContextMenuToggleFavorite": "Toggle Favorite", + "GameListContextMenuToggleFavoriteToolTip": "Toggle Favorite status of Game", + "SettingsTabGeneralTheme": "Theme:", + "SettingsTabGeneralThemeAuto": "Auto", + "SettingsTabGeneralThemeDark": "Dark", + "SettingsTabGeneralThemeLight": "Light", + "ControllerSettingsConfigureGeneral": "Configure", + "ControllerSettingsRumble": "Rumble", + "ControllerSettingsRumbleStrongMultiplier": "Strong Rumble Multiplier", + "ControllerSettingsRumbleWeakMultiplier": "Weak Rumble Multiplier", + "DialogMessageSaveNotAvailableMessage": "There is no savedata for {0} [{1:x16}]", + "DialogMessageSaveNotAvailableCreateSaveMessage": "Would you like to create savedata for this game?", + "DialogConfirmationTitle": "Ryujinx - Confirmation", + "DialogUpdaterTitle": "Ryujinx - Updater", + "DialogErrorTitle": "Ryujinx - Error", + "DialogWarningTitle": "Ryujinx - Warning", + "DialogExitTitle": "Ryujinx - Exit", + "DialogErrorMessage": "Ryujinx has encountered an error", + "DialogExitMessage": "Are you sure you want to close Ryujinx?", + "DialogExitSubMessage": "All unsaved data will be lost!", + "DialogMessageCreateSaveErrorMessage": "There was an error creating the specified savedata: {0}", + "DialogMessageFindSaveErrorMessage": "There was an error finding the specified savedata: {0}", + "FolderDialogExtractTitle": "Choose the folder to extract into", + "DialogNcaExtractionMessage": "Extracting {0} section from {1}...", + "DialogNcaExtractionTitle": "NCA Section Extractor", + "DialogNcaExtractionMainNcaNotFoundErrorMessage": "Extraction failure. The main NCA was not present in the selected file.", + "DialogNcaExtractionCheckLogErrorMessage": "Extraction failed. Please check the log file for more details.", + "DialogNcaExtractionSuccessMessage": "Extraction completed successfully.", + "DialogUpdaterConvertFailedMessage": "Unable to convert the current Ryujinx version.", + "DialogUpdaterCancelUpdateMessage": "Update canceled!", + "DialogUpdaterAlreadyOnLatestVersionMessage": "You are already using the latest version of Ryujinx!", + "DialogUpdaterFailedToGetVersionMessage": "An error occurred while trying to retrieve release information from GitHub. This may happen if a new release is currently being compiled by GitHub Actions. Please try again in a few minutes.", + "DialogUpdaterConvertFailedGithubMessage": "Failed to convert the Ryujinx version received from GitHub.", + "DialogUpdaterDownloadingMessage": "Downloading Update...", + "DialogUpdaterExtractionMessage": "Extracting Update...", + "DialogUpdaterRenamingMessage": "Renaming Update...", + "DialogUpdaterAddingFilesMessage": "Adding New Update...", + "DialogUpdaterShowChangelogMessage": "Show Changelog", + "DialogUpdaterCompleteMessage": "Update Complete!", + "DialogUpdaterRestartMessage": "Do you want to restart Ryujinx now?", + "DialogUpdaterNoInternetMessage": "You are not connected to the Internet!", + "DialogUpdaterNoInternetSubMessage": "Please verify that you have a working Internet connection!", + "DialogUpdaterDirtyBuildMessage": "You cannot update a Dirty build of Ryujinx!", + "DialogUpdaterDirtyBuildSubMessage": "Please download Ryujinx at https://ryujinx.app/download if you are looking for a supported version.", + "DialogRestartRequiredMessage": "Restart Required", + "DialogThemeRestartMessage": "Theme has been saved. A restart is needed to apply the theme.", + "DialogThemeRestartSubMessage": "Do you want to restart", + "DialogFirmwareInstallEmbeddedMessage": "Would you like to install the firmware embedded in this game? (Firmware {0})", + "DialogFirmwareInstallEmbeddedSuccessMessage": "No installed firmware was found but Ryujinx was able to install firmware {0} from the provided game.\nThe emulator will now start.", + "DialogFirmwareNoFirmwareInstalledMessage": "No Firmware Installed", + "DialogFirmwareInstalledMessage": "Firmware {0} was installed", + "DialogInstallFileTypesSuccessMessage": "Successfully installed file types!", + "DialogInstallFileTypesErrorMessage": "Failed to install file types.", + "DialogUninstallFileTypesSuccessMessage": "Successfully uninstalled file types!", + "DialogUninstallFileTypesErrorMessage": "Failed to uninstall file types.", + "DialogOpenSettingsWindowLabel": "Open Settings Window", + "DialogOpenXCITrimmerWindowLabel": "XCI Trimmer Window", + "DialogControllerAppletTitle": "Controller Applet", + "DialogMessageDialogErrorExceptionMessage": "Error displaying Message Dialog: {0}", + "DialogSoftwareKeyboardErrorExceptionMessage": "Error displaying Software Keyboard: {0}", + "DialogErrorAppletErrorExceptionMessage": "Error displaying ErrorApplet Dialog: {0}", + "DialogUserErrorDialogMessage": "{0}: {1}", + "DialogUserErrorDialogInfoMessage": "\nFor more information on how to fix this error, follow our Setup Guide.", + "DialogUserErrorDialogTitle": "Ryujinx Error ({0})", + "DialogAmiiboApiTitle": "Amiibo API", + "DialogAmiiboApiFailFetchMessage": "An error occured while fetching information from the API.", + "DialogAmiiboApiConnectErrorMessage": "Unable to connect to Amiibo API server. The service may be down or you may need to verify your internet connection is online.", + "DialogProfileInvalidProfileErrorMessage": "Profile {0} is incompatible with the current input configuration system.", + "DialogProfileDefaultProfileOverwriteErrorMessage": "Default Profile can not be overwritten", + "DialogProfileDeleteProfileTitle": "Deleting Profile", + "DialogProfileDeleteProfileMessage": "This action is irreversible, are you sure you want to continue?", + "DialogWarning": "Warning", + "DialogPPTCDeletionMessage": "You are about to queue a PPTC rebuild on the next boot of:\n\n{0}\n\nAre you sure you want to proceed?", + "DialogPPTCDeletionErrorMessage": "Error purging PPTC cache at {0}: {1}", + "DialogShaderDeletionMessage": "You are about to delete the Shader cache for :\n\n{0}\n\nAre you sure you want to proceed?", + "DialogShaderDeletionErrorMessage": "Error purging Shader cache at {0}: {1}", + "DialogRyujinxErrorMessage": "Ryujinx has encountered an error", + "DialogInvalidTitleIdErrorMessage": "UI error: The selected game did not have a valid title ID", + "DialogFirmwareInstallerFirmwareNotFoundErrorMessage": "A valid system firmware was not found in {0}.", + "DialogFirmwareInstallerFirmwareInstallTitle": "Install Firmware {0}", + "DialogFirmwareInstallerFirmwareInstallMessage": "System version {0} will be installed.", + "DialogFirmwareInstallerFirmwareInstallSubMessage": "\n\nThis will replace the current system version {0}.", + "DialogFirmwareInstallerFirmwareInstallConfirmMessage": "\n\nDo you want to continue?", + "DialogFirmwareInstallerFirmwareInstallWaitMessage": "Installing firmware...", + "DialogFirmwareInstallerFirmwareInstallSuccessMessage": "System version {0} successfully installed.", + "DialogUserProfileDeletionWarningMessage": "There would be no other profiles to be opened if selected profile is deleted", + "DialogUserProfileDeletionConfirmMessage": "Do you want to delete the selected profile", + "DialogUserProfileUnsavedChangesTitle": "Warning - Unsaved Changes", + "DialogUserProfileUnsavedChangesMessage": "You have made changes to this user profile that have not been saved.", + "DialogUserProfileUnsavedChangesSubMessage": "Do you want to discard your changes?", + "DialogControllerSettingsModifiedConfirmMessage": "The current controller settings has been updated.", + "DialogControllerSettingsModifiedConfirmSubMessage": "Do you want to save?", + "DialogLoadFileErrorMessage": "{0}. Errored File: {1}", + "DialogModAlreadyExistsMessage": "Mod already exists", + "DialogModInvalidMessage": "The specified directory does not contain a mod!", + "DialogModDeleteNoParentMessage": "Failed to Delete: Could not find the parent directory for mod \"{0}\"!", + "DialogDlcNoDlcErrorMessage": "The specified file does not contain a DLC for the selected title!", + "DialogPerformanceCheckLoggingEnabledMessage": "You have trace logging enabled, which is designed to be used by developers only.", + "DialogPerformanceCheckLoggingEnabledConfirmMessage": "For optimal performance, it's recommended to disable trace logging. Would you like to disable trace logging now?", + "DialogPerformanceCheckShaderDumpEnabledMessage": "You have shader dumping enabled, which is designed to be used by developers only.", + "DialogPerformanceCheckShaderDumpEnabledConfirmMessage": "For optimal performance, it's recommended to disable shader dumping. Would you like to disable shader dumping now?", + "DialogLoadAppGameAlreadyLoadedMessage": "A game has already been loaded", + "DialogLoadAppGameAlreadyLoadedSubMessage": "Please stop emulation or close the emulator before launching another game.", + "DialogUpdateAddUpdateErrorMessage": "The specified file does not contain an update for the selected title!", + "DialogSettingsBackendThreadingWarningTitle": "Warning - Backend Threading", + "DialogSettingsBackendThreadingWarningMessage": "Ryujinx must be restarted after changing this option for it to apply fully. Depending on your platform, you may need to manually disable your driver's own multithreading when using Ryujinx's.", + "DialogModManagerDeletionWarningMessage": "You are about to delete the mod: {0}\n\nAre you sure you want to proceed?", + "DialogModManagerDeletionAllWarningMessage": "You are about to delete all mods for this title.\n\nAre you sure you want to proceed?", + "SettingsTabGraphicsFeaturesOptions": "Features", + "SettingsTabGraphicsBackendMultithreading": "Graphics Backend Multithreading:", + "CommonAuto": "Auto", + "CommonOff": "Off", + "CommonOn": "On", + "InputDialogYes": "Yes", + "InputDialogNo": "No", + "DialogProfileInvalidProfileNameErrorMessage": "The file name contains invalid characters. Please try again.", + "MenuBarOptionsPauseEmulation": "Pause", + "MenuBarOptionsResumeEmulation": "Resume", + "AboutUrlTooltipMessage": "Click to open the Ryujinx website in your default browser.", + "AboutDisclaimerMessage": "Ryujinx is not affiliated with Nintendo™,\nor any of its partners, in any way.", + "AboutAmiiboDisclaimerMessage": "AmiiboAPI (www.amiiboapi.com) is used\nin our Amiibo emulation.", + "AboutPatreonUrlTooltipMessage": "Click to open the Ryujinx Patreon page in your default browser.", + "AboutGithubUrlTooltipMessage": "Click to open the Ryujinx GitHub page in your default browser.", + "AboutDiscordUrlTooltipMessage": "Click to open an invite to the Ryujinx Discord server in your default browser.", + "AboutTwitterUrlTooltipMessage": "Click to open the Ryujinx Twitter page in your default browser.", + "AboutRyujinxAboutTitle": "About:", + "AboutRyujinxAboutContent": "Ryujinx is an emulator for the Nintendo Switch™.\nPlease support us on Patreon.\nGet all the latest news on our Twitter or Discord.\nDevelopers interested in contributing can find out more on our GitHub or Discord.", + "AboutRyujinxMaintainersTitle": "Maintained By:", + "AboutRyujinxMaintainersContentTooltipMessage": "Click to open the Contributors page in your default browser.", + "AboutRyujinxSupprtersTitle": "Supported on Patreon By:", + "AmiiboSeriesLabel": "Amiibo Series", + "AmiiboCharacterLabel": "Character", + "AmiiboScanButtonLabel": "Scan It", + "AmiiboOptionsShowAllLabel": "Show All Amiibo", + "AmiiboOptionsUsRandomTagLabel": "Hack: Use Random tag Uuid", + "DlcManagerTableHeadingEnabledLabel": "Enabled", + "DlcManagerTableHeadingTitleIdLabel": "Title ID", + "DlcManagerTableHeadingContainerPathLabel": "Container Path", + "DlcManagerTableHeadingFullPathLabel": "Full Path", + "DlcManagerRemoveAllButton": "Remove All", + "DlcManagerEnableAllButton": "Enable All", + "DlcManagerDisableAllButton": "Disable All", + "ModManagerDeleteAllButton": "Delete All", + "MenuBarOptionsChangeLanguage": "Change Language", + "MenuBarShowFileTypes": "Show File Types", + "CommonSort": "Sort", + "CommonShowNames": "Show Names", + "CommonFavorite": "Favorite", + "OrderAscending": "Ascending", + "OrderDescending": "Descending", + "SettingsTabGraphicsFeatures": "Features & Enhancements", + "ErrorWindowTitle": "Error Window", + "ToggleDiscordTooltip": "Choose whether or not to display Ryujinx on your \"currently playing\" Discord activity", + "AddGameDirBoxTooltip": "Enter a game directory to add to the list", + "AddGameDirTooltip": "Add a game directory to the list", + "RemoveGameDirTooltip": "Remove selected game directory", + "AddAutoloadDirBoxTooltip": "Enter an autoload directory to add to the list", + "AddAutoloadDirTooltip": "Add an autoload directory to the list", + "RemoveAutoloadDirTooltip": "Remove selected autoload directory", + "CustomThemeCheckTooltip": "Use a custom Avalonia theme for the GUI to change the appearance of the emulator menus", + "CustomThemePathTooltip": "Path to custom GUI theme", + "CustomThemeBrowseTooltip": "Browse for a custom GUI theme", + "DockModeToggleTooltip": "Docked mode makes the emulated system behave as a docked Nintendo Switch. This improves graphical fidelity in most games. Conversely, disabling this will make the emulated system behave as a handheld Nintendo Switch, reducing graphics quality.\n\nConfigure player 1 controls if planning to use docked mode; configure handheld controls if planning to use handheld mode.\n\nLeave ON if unsure.", + "DirectKeyboardTooltip": "Direct keyboard access (HID) support. Provides games access to your keyboard as a text entry device.\n\nOnly works with games that natively support keyboard usage on Switch hardware.\n\nLeave OFF if unsure.", + "DirectMouseTooltip": "Direct mouse access (HID) support. Provides games access to your mouse as a pointing device.\n\nOnly works with games that natively support mouse controls on Switch hardware, which are few and far between.\n\nWhen enabled, touch screen functionality may not work.\n\nLeave OFF if unsure.", + "RegionTooltip": "Change System Region", + "LanguageTooltip": "Change System Language", + "TimezoneTooltip": "Change System TimeZone", + "TimeTooltip": "Change System Time", + "VSyncToggleTooltip": "Emulated console's Vertical Sync. Essentially a frame-limiter for the majority of games; disabling it may cause games to run at higher speed or make loading screens take longer or get stuck.\n\nCan be toggled in-game with a hotkey of your preference (F1 by default). We recommend doing this if you plan on disabling it.\n\nLeave ON if unsure.", + "PptcToggleTooltip": "Saves translated JIT functions so that they do not need to be translated every time the game loads.\n\nReduces stuttering and significantly speeds up boot times after the first boot of a game.\n\nLeave ON if unsure.", + "LowPowerPptcToggleTooltip": "Load the PPTC using a third of the amount of cores.", + "FsIntegrityToggleTooltip": "Checks for corrupt files when booting a game, and if corrupt files are detected, displays a hash error in the log.\n\nHas no impact on performance and is meant to help troubleshooting.\n\nLeave ON if unsure.", + "AudioBackendTooltip": "Changes the backend used to render audio.\n\nSDL2 is the preferred one, while OpenAL and SoundIO are used as fallbacks. Dummy will have no sound.\n\nSet to SDL2 if unsure.", + "MemoryManagerTooltip": "Change how guest memory is mapped and accessed. Greatly affects emulated CPU performance.\n\nSet to HOST UNCHECKED if unsure.", + "MemoryManagerSoftwareTooltip": "Use a software page table for address translation. Highest accuracy but slowest performance.", + "MemoryManagerHostTooltip": "Directly map memory in the host address space. Much faster JIT compilation and execution.", + "MemoryManagerUnsafeTooltip": "Directly map memory, but do not mask the address within the guest address space before access. Faster, but at the cost of safety. The guest application can access memory from anywhere in Ryujinx, so only run programs you trust with this mode.", + "UseHypervisorTooltip": "Use Hypervisor instead of JIT. Greatly improves performance when available, but can be unstable in its current state.", + "DRamTooltip": "Utilizes an alternative memory mode with 8GiB of DRAM to mimic a Switch development model.\n\nThis is only useful for higher-resolution texture packs or 4k resolution mods. Does NOT improve performance.\n\nLeave OFF if unsure.", + "IgnoreMissingServicesTooltip": "Ignores unimplemented Horizon OS services. This may help in bypassing crashes when booting certain games.\n\nLeave OFF if unsure.", + "IgnoreAppletTooltip": "The external dialog \"Controller Applet\" will not appear if the gamepad is disconnected during gameplay. There will be no prompt to close the dialog or set up a new controller. Once the previously disconnected controller is reconnected, the game will automatically resume.", + "GraphicsBackendThreadingTooltip": "Executes graphics backend commands on a second thread.\n\nSpeeds up shader compilation, reduces stuttering, and improves performance on GPU drivers without multithreading support of their own. Slightly better performance on drivers with multithreading.\n\nSet to AUTO if unsure.", + "GalThreadingTooltip": "Executes graphics backend commands on a second thread.\n\nSpeeds up shader compilation, reduces stuttering, and improves performance on GPU drivers without multithreading support of their own. Slightly better performance on drivers with multithreading.\n\nSet to AUTO if unsure.", + "ShaderCacheToggleTooltip": "Saves a disk shader cache which reduces stuttering in subsequent runs.\n\nLeave ON if unsure.", + "ResolutionScaleTooltip": "Multiplies the game's rendering resolution.\n\nA few games may not work with this and look pixelated even when the resolution is increased; for those games, you may need to find mods that remove anti-aliasing or that increase their internal rendering resolution. For using the latter, you'll likely want to select Native.\n\nThis option can be changed while a game is running by clicking \"Apply\" below; you can simply move the settings window aside and experiment until you find your preferred look for a game.\n\nKeep in mind 4x is overkill for virtually any setup.", + "ResolutionScaleEntryTooltip": "Floating point resolution scale, such as 1.5. Non-integral scales are more likely to cause issues or crash.", + "AnisotropyTooltip": "Level of Anisotropic Filtering. Set to Auto to use the value requested by the game.", + "AspectRatioTooltip": "Aspect Ratio applied to the renderer window.\n\nOnly change this if you're using an aspect ratio mod for your game, otherwise the graphics will be stretched.\n\nLeave on 16:9 if unsure.", + "ShaderDumpPathTooltip": "Graphics Shaders Dump Path", + "FileLogTooltip": "Saves console logging to a log file on disk. Does not affect performance.", + "StubLogTooltip": "Prints stub log messages in the console. Does not affect performance.", + "InfoLogTooltip": "Prints info log messages in the console. Does not affect performance.", + "WarnLogTooltip": "Prints warning log messages in the console. Does not affect performance.", + "ErrorLogTooltip": "Prints error log messages in the console. Does not affect performance.", + "TraceLogTooltip": "Prints trace log messages in the console. Does not affect performance.", + "GuestLogTooltip": "Prints guest log messages in the console. Does not affect performance.", + "FileAccessLogTooltip": "Prints file access log messages in the console.", + "FSAccessLogModeTooltip": "Enables FS access log output to the console. Possible modes are 0-3", + "DeveloperOptionTooltip": "Use with care", + "OpenGlLogLevel": "Requires appropriate log levels enabled", + "DebugLogTooltip": "Prints debug log messages in the console.\n\nOnly use this if specifically instructed by a staff member, as it will make logs difficult to read and worsen emulator performance.", + "LoadApplicationFileTooltip": "Open a file explorer to choose a Switch compatible file to load", + "LoadApplicationFolderTooltip": "Open a file explorer to choose a Switch compatible, unpacked application to load", + "LoadDlcFromFolderTooltip": "Open a file explorer to choose one or more folders to bulk load DLC from", + "LoadTitleUpdatesFromFolderTooltip": "Open a file explorer to choose one or more folders to bulk load title updates from", + "OpenRyujinxFolderTooltip": "Open Ryujinx filesystem folder", + "OpenRyujinxLogsTooltip": "Opens the folder where logs are written to", + "ExitTooltip": "Exit Ryujinx", + "OpenSettingsTooltip": "Open settings window", + "OpenProfileManagerTooltip": "Open User Profiles Manager window", + "StopEmulationTooltip": "Stop emulation of the current game and return to game selection", + "CheckUpdatesTooltip": "Check for updates to Ryujinx", + "OpenAboutTooltip": "Open About Window", + "GridSize": "Grid Size", + "GridSizeTooltip": "Change the size of grid items", + "SettingsTabSystemSystemLanguageBrazilianPortuguese": "Brazilian Portuguese", + "AboutRyujinxContributorsButtonHeader": "See All Contributors", + "SettingsTabSystemAudioVolume": "Volume: ", + "AudioVolumeTooltip": "Change Audio Volume", + "SettingsTabSystemEnableInternetAccess": "Guest Internet Access/LAN Mode", + "EnableInternetAccessTooltip": "Allows the emulated application to connect to the Internet.\n\nGames with a LAN mode can connect to each other when this is enabled and the systems are connected to the same access point. This includes real consoles as well.\n\nDoes NOT allow connecting to Nintendo servers. May cause crashing in certain games that try to connect to the Internet.\n\nLeave OFF if unsure.", + "GameListContextMenuManageCheatToolTip": "Manage Cheats", + "GameListContextMenuManageCheat": "Manage Cheats", + "GameListContextMenuManageModToolTip": "Manage Mods", + "GameListContextMenuManageMod": "Manage Mods", + "ControllerSettingsStickRange": "Range:", + "DialogStopEmulationTitle": "Ryujinx - Stop Emulation", + "DialogStopEmulationMessage": "Are you sure you want to stop emulation?", + "SettingsTabCpu": "CPU", + "SettingsTabAudio": "Audio", + "SettingsTabNetwork": "Network", + "SettingsTabNetworkConnection": "Network Connection", + "SettingsTabCpuCache": "CPU Cache", + "SettingsTabCpuMemory": "CPU Mode", + "DialogUpdaterFlatpakNotSupportedMessage": "Please update Ryujinx via FlatHub.", + "UpdaterDisabledWarningTitle": "Updater Disabled!", + "ControllerSettingsRotate90": "Rotate 90° Clockwise", + "IconSize": "Icon Size", + "IconSizeTooltip": "Change the size of game icons", + "MenuBarOptionsShowConsole": "Show Console", + "ShaderCachePurgeError": "Error purging shader cache at {0}: {1}", + "UserErrorNoKeys": "Keys not found", + "UserErrorNoFirmware": "Firmware not found", + "UserErrorFirmwareParsingFailed": "Firmware parsing error", + "UserErrorApplicationNotFound": "Application not found", + "UserErrorUnknown": "Unknown error", + "UserErrorUndefined": "Undefined error", + "UserErrorNoKeysDescription": "Ryujinx was unable to find your 'prod.keys' file", + "UserErrorNoFirmwareDescription": "Ryujinx was unable to find any firmwares installed", + "UserErrorFirmwareParsingFailedDescription": "Ryujinx was unable to parse the provided firmware. This is usually caused by outdated keys.", + "UserErrorApplicationNotFoundDescription": "Ryujinx couldn't find a valid application at the given path.", + "UserErrorUnknownDescription": "An unknown error occured!", + "UserErrorUndefinedDescription": "An undefined error occured! This shouldn't happen, please contact a dev!", + "OpenSetupGuideMessage": "Open the Setup Guide", + "NoUpdate": "No Update", + "TitleUpdateVersionLabel": "Version {0}", + "TitleBundledUpdateVersionLabel": "Bundled: Version {0}", + "TitleBundledDlcLabel": "Bundled:", + "TitleXCIStatusPartialLabel": "Partial", + "TitleXCIStatusTrimmableLabel": "Untrimmed", + "TitleXCIStatusUntrimmableLabel": "Trimmed", + "TitleXCIStatusFailedLabel": "(Failed)", + "TitleXCICanSaveLabel": "Save {0:n0} Mb", + "TitleXCISavingLabel": "Saved {0:n0} Mb", + "RyujinxInfo": "Ryujinx - Info", + "RyujinxConfirm": "Ryujinx - Confirmation", + "FileDialogAllTypes": "All types", + "Never": "Never", + "SwkbdMinCharacters": "Must be at least {0} characters long", + "SwkbdMinRangeCharacters": "Must be {0}-{1} characters long", + "SoftwareKeyboard": "Software Keyboard", + "SoftwareKeyboardModeNumeric": "Must be 0-9 or '.' only", + "SoftwareKeyboardModeAlphabet": "Must be non CJK-characters only", + "SoftwareKeyboardModeASCII": "Must be ASCII text only", + "ControllerAppletControllers": "Supported Controllers:", + "ControllerAppletPlayers": "Players:", + "ControllerAppletDescription": "Your current configuration is invalid. Open settings and reconfigure your inputs.", + "ControllerAppletDocked": "Docked mode set. Handheld control should be disabled.", + "UpdaterRenaming": "Renaming Old Files...", + "UpdaterRenameFailed": "Updater was unable to rename file: {0}", + "UpdaterAddingFiles": "Adding New Files...", + "UpdaterExtracting": "Extracting Update...", + "UpdaterDownloading": "Downloading Update...", + "Game": "Game", + "Docked": "Docked", + "Handheld": "Handheld", + "ConnectionError": "Connection Error.", + "AboutPageDeveloperListMore": "{0} and more...", + "ApiError": "API Error.", + "LoadingHeading": "Loading {0}", + "CompilingPPTC": "Compiling PTC", + "CompilingShaders": "Compiling Shaders", + "AllKeyboards": "All keyboards", + "OpenFileDialogTitle": "Select a supported file to open", + "OpenFolderDialogTitle": "Select a folder with an unpacked game", + "AllSupportedFormats": "All Supported Formats", + "RyujinxUpdater": "Ryujinx Updater", + "SettingsTabHotkeys": "Keyboard Hotkeys", + "SettingsTabHotkeysHotkeys": "Keyboard Hotkeys", + "SettingsTabHotkeysToggleVSyncModeHotkey": "Toggle VSync mode:", + "SettingsTabHotkeysScreenshotHotkey": "Screenshot:", + "SettingsTabHotkeysShowUiHotkey": "Show UI:", + "SettingsTabHotkeysPauseHotkey": "Pause:", + "SettingsTabHotkeysToggleMuteHotkey": "Mute:", + "SettingsTabHotkeysIncrementCustomVSyncIntervalHotkey": "Raise custom refresh rate", + "SettingsTabHotkeysDecrementCustomVSyncIntervalHotkey": "Lower custom refresh rate", + "ControllerMotionTitle": "Motion Control Settings", + "ControllerRumbleTitle": "Rumble Settings", + "SettingsSelectThemeFileDialogTitle": "Select Theme File", + "SettingsXamlThemeFile": "Xaml Theme File", + "AvatarWindowTitle": "Manage Accounts - Avatar", + "Amiibo": "Amiibo", + "Unknown": "Unknown", + "Usage": "Usage", + "Writable": "Writable", + "SelectDlcDialogTitle": "Select DLC files", + "SelectUpdateDialogTitle": "Select update files", + "SelectModDialogTitle": "Select mod directory", + "TrimXCIFileDialogTitle": "Check and Trim XCI File", + "TrimXCIFileDialogPrimaryText": "This function will first check the empty space and then trim the XCI File to save disk space.", + "TrimXCIFileDialogSecondaryText": "Current File Size: {0:n} MB\nGame Data Size: {1:n} MB\nDisk Space Savings: {2:n} MB", + "TrimXCIFileNoTrimNecessary": "XCI File does not need to be trimmed. Check logs for further details", + "TrimXCIFileNoUntrimPossible": "XCI File cannot be untrimmed. Check logs for further details", + "TrimXCIFileReadOnlyFileCannotFix": "XCI File is Read Only and could not be made writable. Check logs for further details", + "TrimXCIFileFileSizeChanged": "XCI File has changed in size since it was scanned. Please check the file is not being written to and try again.", + "TrimXCIFileFreeSpaceCheckFailed": "XCI File has data in the free space area, it is not safe to trim", + "TrimXCIFileInvalidXCIFile": "XCI File contains invalid data. Check logs for further details", + "TrimXCIFileFileIOWriteError": "XCI File could not be opened for writing. Check logs for further details", + "TrimXCIFileFailedPrimaryText": "Trimming of the XCI file failed", + "TrimXCIFileCancelled": "The operation was cancelled", + "TrimXCIFileFileUndertermined": "No operation was performed", + "UserProfileWindowTitle": "User Profiles Manager", + "CheatWindowTitle": "Cheats Manager", + "DlcWindowTitle": "Manage Downloadable Content for {0} ({1})", + "ModWindowTitle": "Manage Mods for {0} ({1})", + "UpdateWindowTitle": "Title Update Manager", + "XCITrimmerWindowTitle": "XCI File Trimmer", + "XCITrimmerTitleStatusCount": "{0} of {1} Title(s) Selected", + "XCITrimmerTitleStatusCountWithFilter": "{0} of {1} Title(s) Selected ({2} displayed)", + "XCITrimmerTitleStatusTrimming": "Trimming {0} Title(s)...", + "XCITrimmerTitleStatusUntrimming": "Untrimming {0} Title(s)...", + "XCITrimmerTitleStatusFailed": "Failed", + "XCITrimmerPotentialSavings": "Potential Savings", + "XCITrimmerActualSavings": "Actual Savings", + "XCITrimmerSavingsMb": "{0:n0} Mb", + "XCITrimmerSelectDisplayed": "Select Shown", + "XCITrimmerDeselectDisplayed": "Deselect Shown", + "XCITrimmerSortName": "Title", + "XCITrimmerSortSaved": "Space Savings", + "XCITrimmerTrim": "Trim", + "XCITrimmerUntrim": "Untrim", + "UpdateWindowUpdateAddedMessage": "{0} new update(s) added", + "UpdateWindowBundledContentNotice": "Bundled updates cannot be removed, only disabled.", + "CheatWindowHeading": "Cheats Available for {0} [{1}]", + "BuildId": "BuildId:", + "DlcWindowBundledContentNotice": "Bundled DLC cannot be removed, only disabled.", + "DlcWindowHeading": "{0} Downloadable Content(s) available for {1} ({2})", + "DlcWindowDlcAddedMessage": "{0} new downloadable content(s) added", + "AutoloadDlcAddedMessage": "{0} new downloadable content(s) added", + "AutoloadDlcRemovedMessage": "{0} missing downloadable content(s) removed", + "AutoloadUpdateAddedMessage": "{0} new update(s) added", + "AutoloadUpdateRemovedMessage": "{0} missing update(s) removed", + "ModWindowHeading": "{0} Mod(s)", + "UserProfilesEditProfile": "Edit Selected", + "Continue": "Continue", + "Cancel": "Cancel", + "Save": "Save", + "Discard": "Discard", + "Paused": "Paused", + "UserProfilesSetProfileImage": "Set Profile Image", + "UserProfileEmptyNameError": "Name is required", + "UserProfileNoImageError": "Profile image must be set", + "GameUpdateWindowHeading": "Manage Updates for {0} ({1})", + "SettingsTabHotkeysResScaleUpHotkey": "Increase resolution:", + "SettingsTabHotkeysResScaleDownHotkey": "Decrease resolution:", + "UserProfilesName": "Name:", + "UserProfilesUserId": "User ID:", + "SettingsTabGraphicsBackend": "Graphics Backend", + "SettingsTabGraphicsBackendTooltip": "Select the graphics backend that will be used in the emulator.\n\nVulkan is overall better for all modern graphics cards, as long as their drivers are up to date. Vulkan also features faster shader compilation (less stuttering) on all GPU vendors.\n\nOpenGL may achieve better results on old Nvidia GPUs, on old AMD GPUs on Linux, or on GPUs with lower VRAM, though shader compilation stutters will be greater.\n\nSet to Vulkan if unsure. Set to OpenGL if your GPU does not support Vulkan even with the latest graphics drivers.", + "SettingsEnableTextureRecompression": "Enable Texture Recompression", + "SettingsEnableTextureRecompressionTooltip": "Compresses ASTC textures in order to reduce VRAM usage.\n\nGames using this texture format include Astral Chain, Bayonetta 3, Fire Emblem Engage, Metroid Prime Remastered, Super Mario Bros. Wonder and The Legend of Zelda: Tears of the Kingdom.\n\nGraphics cards with 4GiB VRAM or less will likely crash at some point while running these games.\n\nEnable only if you're running out of VRAM on the aforementioned games. Leave OFF if unsure.", + "SettingsTabGraphicsPreferredGpu": "Preferred GPU", + "SettingsTabGraphicsPreferredGpuTooltip": "Select the graphics card that will be used with the Vulkan graphics backend.\n\nDoes not affect the GPU that OpenGL will use.\n\nSet to the GPU flagged as \"dGPU\" if unsure. If there isn't one, leave untouched.", + "SettingsAppRequiredRestartMessage": "Ryujinx Restart Required", + "SettingsGpuBackendRestartMessage": "Graphics Backend or GPU settings have been modified. This will require a restart to be applied", + "SettingsGpuBackendRestartSubMessage": "Do you want to restart now?", + "RyujinxUpdaterMessage": "Do you want to update Ryujinx to the latest version?", + "SettingsTabHotkeysVolumeUpHotkey": "Increase Volume:", + "SettingsTabHotkeysVolumeDownHotkey": "Decrease Volume:", + "SettingsEnableMacroHLE": "Enable Macro HLE", + "SettingsEnableMacroHLETooltip": "High-level emulation of GPU Macro code.\n\nImproves performance, but may cause graphical glitches in some games.\n\nLeave ON if unsure.", + "SettingsEnableColorSpacePassthrough": "Color Space Passthrough", + "SettingsEnableColorSpacePassthroughTooltip": "Directs the Vulkan backend to pass through color information without specifying a color space. For users with wide gamut displays, this may result in more vibrant colors, at the cost of color correctness.", + "VolumeShort": "Vol", + "UserProfilesManageSaves": "Manage Saves", + "DeleteUserSave": "Do you want to delete user save for this game?", + "IrreversibleActionNote": "This action is not reversible.", + "SaveManagerHeading": "Manage Saves for {0} ({1})", + "SaveManagerTitle": "Save Manager", + "Name": "Name", + "Size": "Size", + "Search": "Search", + "UserProfilesRecoverLostAccounts": "Recover Lost Accounts", + "Recover": "Recover", + "UserProfilesRecoverHeading": "Saves were found for the following accounts", + "UserProfilesRecoverEmptyList": "No profiles to recover", + "GraphicsAATooltip": "Applies anti-aliasing to the game render.\n\nFXAA will blur most of the image, while SMAA will attempt to find jagged edges and smooth them out.\n\nNot recommended to use in conjunction with the FSR scaling filter.\n\nThis option can be changed while a game is running by clicking \"Apply\" below; you can simply move the settings window aside and experiment until you find your preferred look for a game.\n\nLeave on NONE if unsure.", + "GraphicsAALabel": "Anti-Aliasing:", + "GraphicsScalingFilterLabel": "Scaling Filter:", + "GraphicsScalingFilterTooltip": "Choose the scaling filter that will be applied when using resolution scale.\n\nBilinear works well for 3D games and is a safe default option.\n\nNearest is recommended for pixel art games.\n\nFSR 1.0 is merely a sharpening filter, not recommended for use with FXAA or SMAA.\n\nArea scaling is recommended when downscaling resolutions that are larger than the output window. It can be used to achieve a supersampled anti-aliasing effect when downscaling by more than 2x.\n\nThis option can be changed while a game is running by clicking \"Apply\" below; you can simply move the settings window aside and experiment until you find your preferred look for a game.\n\nLeave on BILINEAR if unsure.", + "GraphicsScalingFilterBilinear": "Bilinear", + "GraphicsScalingFilterNearest": "Nearest", + "GraphicsScalingFilterFsr": "FSR", + "GraphicsScalingFilterArea": "Area", + "GraphicsScalingFilterLevelLabel": "Level", + "GraphicsScalingFilterLevelTooltip": "Set FSR 1.0 sharpening level. Higher is sharper.", + "SmaaLow": "SMAA Low", + "SmaaMedium": "SMAA Medium", + "SmaaHigh": "SMAA High", + "SmaaUltra": "SMAA Ultra", + "UserEditorTitle": "Edit User", + "UserEditorTitleCreate": "Create User", + "SettingsTabNetworkInterface": "Network Interface:", + "NetworkInterfaceTooltip": "The network interface used for LAN/LDN features.\n\nIn conjunction with a VPN or XLink Kai and a game with LAN support, can be used to spoof a same-network connection over the Internet.\n\nLeave on DEFAULT if unsure.", + "NetworkInterfaceDefault": "Default", + "PackagingShaders": "Packaging Shaders", + "AboutChangelogButton": "View Changelog on GitHub", + "AboutChangelogButtonTooltipMessage": "Click to open the changelog for this version in your default browser.", + "SettingsTabNetworkMultiplayer": "Multiplayer", + "MultiplayerMode": "Mode:", + "MultiplayerModeTooltip": "Change LDN multiplayer mode.\n\nLdnMitm will modify local wireless/local play functionality in games to function as if it were LAN, allowing for local, same-network connections with other Ryujinx instances and hacked Nintendo Switch consoles that have the ldn_mitm module installed.\n\nMultiplayer requires all players to be on the same game version (i.e. Super Smash Bros. Ultimate v13.0.1 can't connect to v13.0.0).\n\nLeave DISABLED if unsure.", + "MultiplayerModeDisabled": "Disabled", + "MultiplayerModeLdnMitm": "ldn_mitm", + "MultiplayerModeLdnRyu": "RyuLDN", + "MultiplayerDisableP2P": "Disable P2P Network Hosting (may increase latency)", + "MultiplayerDisableP2PTooltip": "Disable P2P network hosting, peers will proxy through the master server instead of connecting to you directly.", + "LdnPassphrase": "Network Passphrase:", + "LdnPassphraseTooltip": "You will only be able to see hosted games with the same passphrase as you.", + "LdnPassphraseInputTooltip": "Enter a passphrase in the format Ryujinx-<8 hex chars>. You will only be able to see hosted games with the same passphrase as you.", + "LdnPassphraseInputPublic": "(public)", + "GenLdnPass": "Generate Random", + "GenLdnPassTooltip": "Generates a new passphrase, which can be shared with other players.", + "ClearLdnPass": "Clear", + "ClearLdnPassTooltip": "Clears the current passphrase, returning to the public network.", + "InvalidLdnPassphrase": "Invalid Passphrase! Must be in the format \"Ryujinx-<8 hex chars>\"" +} diff --git a/src/Ryujinx/Assets/Locales/es_ES.json b/src/Ryujinx/Assets/Locales/es_ES.json new file mode 100644 index 000000000..8a426b3a4 --- /dev/null +++ b/src/Ryujinx/Assets/Locales/es_ES.json @@ -0,0 +1,867 @@ +{ + "Language": "Español (ES)", + "MenuBarFileOpenApplet": "Abrir applet", + "MenuBarFileOpenAppletOpenMiiApplet": "Mii Edit Applet", + "MenuBarFileOpenAppletOpenMiiAppletToolTip": "Abre el editor de Mii en modo autónomo", + "SettingsTabInputDirectMouseAccess": "Acceso directo al ratón", + "SettingsTabSystemMemoryManagerMode": "Modo del administrador de memoria:", + "SettingsTabSystemMemoryManagerModeSoftware": "Software", + "SettingsTabSystemMemoryManagerModeHost": "Host (rápido)", + "SettingsTabSystemMemoryManagerModeHostUnchecked": "Host sin verificación (más rápido, inseguro)", + "SettingsTabSystemUseHypervisor": "Usar hipervisor", + "MenuBarFile": "_Archivo", + "MenuBarFileOpenFromFile": "_Cargar aplicación desde un archivo", + "MenuBarFileOpenFromFileError": "No se encontraron aplicaciones en el archivo seleccionado.", + "MenuBarFileOpenUnpacked": "Cargar juego _desempaquetado", + "MenuBarFileLoadDlcFromFolder": "Cargar DLC Desde Carpeta", + "MenuBarFileLoadTitleUpdatesFromFolder": "Cargar Actualizaciones de Títulos Desde Carpeta", + "MenuBarFileOpenEmuFolder": "Abrir carpeta de Ryujinx", + "MenuBarFileOpenLogsFolder": "Abrir carpeta de registros", + "MenuBarFileExit": "_Salir", + "MenuBarOptions": "_Opciones", + "MenuBarOptionsToggleFullscreen": "Cambiar a pantalla completa.", + "MenuBarOptionsStartGamesInFullscreen": "Iniciar juegos en pantalla completa", + "MenuBarOptionsStopEmulation": "Detener emulación", + "MenuBarOptionsSettings": "_Configuración", + "MenuBarOptionsManageUserProfiles": "_Gestionar perfiles de usuario", + "MenuBarActions": "_Acciones", + "MenuBarOptionsSimulateWakeUpMessage": "Simular mensaje de reactivación", + "MenuBarActionsScanAmiibo": "Escanear Amiibo", + "MenuBarTools": "_Herramientas", + "MenuBarToolsInstallFirmware": "Instalar firmware", + "MenuBarFileToolsInstallFirmwareFromFile": "Instalar firmware desde un archivo XCI o ZIP", + "MenuBarFileToolsInstallFirmwareFromDirectory": "Instalar firmware desde una carpeta", + "MenuBarToolsManageFileTypes": "Administrar tipos de archivo", + "MenuBarToolsInstallFileTypes": "Instalar tipos de archivo", + "MenuBarToolsUninstallFileTypes": "Desinstalar tipos de archivo", + "MenuBarToolsXCITrimmer": "Trim XCI Files", + "MenuBarView": "_View", + "MenuBarViewWindow": "Tamaño Ventana", + "MenuBarViewWindow720": "720p", + "MenuBarViewWindow1080": "1080p", + "MenuBarHelp": "_Ayuda", + "MenuBarHelpCheckForUpdates": "Buscar actualizaciones", + "MenuBarHelpAbout": "Acerca de", + "MenuSearch": "Buscar...", + "GameListHeaderFavorite": "Favoritos", + "GameListHeaderIcon": "Icono", + "GameListHeaderApplication": "Nombre", + "GameListHeaderDeveloper": "Desarrollador", + "GameListHeaderVersion": "Versión", + "GameListHeaderTimePlayed": "Tiempo jugado", + "GameListHeaderLastPlayed": "Jugado por última vez", + "GameListHeaderFileExtension": "Extensión", + "GameListHeaderFileSize": "Tamaño del archivo", + "GameListHeaderPath": "Directorio", + "GameListContextMenuOpenUserSaveDirectory": "Abrir carpeta de guardado de este usuario", + "GameListContextMenuOpenUserSaveDirectoryToolTip": "Abre la carpeta que contiene la partida guardada del usuario para esta aplicación", + "GameListContextMenuOpenDeviceSaveDirectory": "Abrir carpeta de guardado del sistema para el usuario actual", + "GameListContextMenuOpenDeviceSaveDirectoryToolTip": "Abre la carpeta que contiene la partida guardada del sistema para esta aplicación", + "GameListContextMenuOpenBcatSaveDirectory": "Abrir carpeta de guardado BCAT del usuario", + "GameListContextMenuOpenBcatSaveDirectoryToolTip": "Abrir la carpeta que contiene el guardado BCAT de esta aplicación", + "GameListContextMenuManageTitleUpdates": "Gestionar actualizaciones del juego", + "GameListContextMenuManageTitleUpdatesToolTip": "Abrir la ventana de gestión de actualizaciones de esta aplicación", + "GameListContextMenuManageDlc": "Gestionar DLC", + "GameListContextMenuManageDlcToolTip": "Abrir la ventana de gestión del DLC", + "GameListContextMenuCacheManagement": "Gestión de caché ", + "GameListContextMenuCacheManagementPurgePptc": "Reconstruir PPTC en cola", + "GameListContextMenuCacheManagementPurgePptcToolTip": "Elimina la caché de PPTC de esta aplicación", + "GameListContextMenuCacheManagementPurgeShaderCache": "Limpiar caché de sombreadores", + "GameListContextMenuCacheManagementPurgeShaderCacheToolTip": "Eliminar la caché de sombreadores de esta aplicación", + "GameListContextMenuCacheManagementOpenPptcDirectory": "Abrir carpeta de PPTC", + "GameListContextMenuCacheManagementOpenPptcDirectoryToolTip": "Abrir la carpeta que contiene la caché de PPTC de esta aplicación", + "GameListContextMenuCacheManagementOpenShaderCacheDirectory": "Abrir carpeta de caché de sombreadores", + "GameListContextMenuCacheManagementOpenShaderCacheDirectoryToolTip": "Abrir la carpeta que contiene la caché de sombreadores de esta aplicación", + "GameListContextMenuExtractData": "Extraer datos", + "GameListContextMenuExtractDataExeFS": "ExeFS", + "GameListContextMenuExtractDataExeFSToolTip": "Extraer la sección ExeFS de la configuración actual de la aplicación (incluyendo actualizaciones)", + "GameListContextMenuExtractDataRomFS": "RomFS", + "GameListContextMenuExtractDataRomFSToolTip": "Extraer la sección RomFS de la configuración actual de la aplicación (incluyendo actualizaciones)", + "GameListContextMenuExtractDataLogo": "Logotipo", + "GameListContextMenuExtractDataLogoToolTip": "Extraer la sección Logo de la configuración actual de la aplicación (incluyendo actualizaciones)", + "GameListContextMenuCreateShortcut": "Crear acceso directo de aplicación", + "GameListContextMenuCreateShortcutToolTip": "Crear un acceso directo en el escritorio que lance la aplicación seleccionada", + "GameListContextMenuCreateShortcutToolTipMacOS": "Crea un acceso directo en la carpeta de Aplicaciones de macOS que inicie la Aplicación seleccionada", + "GameListContextMenuOpenModsDirectory": "Abrir Directorio de Mods", + "GameListContextMenuOpenModsDirectoryToolTip": "Abre el directorio que contiene los Mods de la Aplicación.", + "GameListContextMenuOpenSdModsDirectory": "Abrir Directorio de Mods de Atmosphere\n\n\n\n\n\n", + "GameListContextMenuOpenSdModsDirectoryToolTip": "Abre el directorio alternativo de la tarjeta SD de Atmosphere que contiene los Mods de la Aplicación. Útil para los mods que están empaquetados para el hardware real.", + "GameListContextMenuTrimXCI": "Check and Trim XCI File", + "GameListContextMenuTrimXCIToolTip": "Check and Trim XCI File to Save Disk Space", + "StatusBarGamesLoaded": "{0}/{1} juegos cargados", + "StatusBarSystemVersion": "Versión del sistema: {0}", + "StatusBarXCIFileTrimming": "Trimming XCI File '{0}'", + "LinuxVmMaxMapCountDialogTitle": "Límite inferior para mapeos de memoria detectado", + "LinuxVmMaxMapCountDialogTextPrimary": "¿Quieres aumentar el valor de vm.max_map_count a {0}?", + "LinuxVmMaxMapCountDialogTextSecondary": "Algunos juegos podrían intentar crear más mapeos de memoria de los permitidos. Ryujinx se bloqueará tan pronto como se supere este límite.", + "LinuxVmMaxMapCountDialogButtonUntilRestart": "Sí, hasta el próximo reinicio", + "LinuxVmMaxMapCountDialogButtonPersistent": "Si, permanentemente", + "LinuxVmMaxMapCountWarningTextPrimary": "La cantidad máxima de mapeos de memoria es menor de lo recomendado.", + "LinuxVmMaxMapCountWarningTextSecondary": "El valor actual de vm.max_map_count ({0}) es menor que {1}. Algunos juegos podrían intentar crear más mapeos de memoria de los permitidos actualmente. Ryujinx se bloqueará tan pronto como se supere este límite.\n\nPuede que desee aumentar manualmente el límite o instalar pkexec, lo que permite a Ryujinx ayudar con eso.", + "Settings": "Configuración", + "SettingsTabGeneral": "Interfaz de usuario", + "SettingsTabGeneralGeneral": "General", + "SettingsTabGeneralEnableDiscordRichPresence": "Habilitar estado en Discord", + "SettingsTabGeneralCheckUpdatesOnLaunch": "Buscar actualizaciones al iniciar", + "SettingsTabGeneralShowConfirmExitDialog": "Mostrar diálogo de confirmación al cerrar", + "SettingsTabGeneralRememberWindowState": "Recordar Tamaño/Posición de la Ventana", + "SettingsTabGeneralShowTitleBar": "Mostrar Barra de Título (Requiere reinicio)", + "SettingsTabGeneralHideCursor": "Esconder el cursor:", + "SettingsTabGeneralHideCursorNever": "Nunca", + "SettingsTabGeneralHideCursorOnIdle": "Ocultar cursor cuando esté inactivo", + "SettingsTabGeneralHideCursorAlways": "Siempre", + "SettingsTabGeneralGameDirectories": "Carpetas de juegos", + "SettingsTabGeneralAutoloadDirectories": "Carpetas de DLC/Actualizaciones para Carga Automática", + "SettingsTabGeneralAutoloadNote": "DLC y Actualizaciones que hacen referencia a archivos ausentes serán desactivado automáticamente", + "SettingsTabGeneralAdd": "Agregar", + "SettingsTabGeneralRemove": "Quitar", + "SettingsTabSystem": "Sistema", + "SettingsTabSystemCore": "Núcleo", + "SettingsTabSystemSystemRegion": "Región del sistema:", + "SettingsTabSystemSystemRegionJapan": "Japón", + "SettingsTabSystemSystemRegionUSA": "EEUU", + "SettingsTabSystemSystemRegionEurope": "Europa", + "SettingsTabSystemSystemRegionAustralia": "Australia", + "SettingsTabSystemSystemRegionChina": "China", + "SettingsTabSystemSystemRegionKorea": "Corea", + "SettingsTabSystemSystemRegionTaiwan": "Taiwán", + "SettingsTabSystemSystemLanguage": "Idioma del sistema:", + "SettingsTabSystemSystemLanguageJapanese": "Japonés", + "SettingsTabSystemSystemLanguageAmericanEnglish": "Inglés americano", + "SettingsTabSystemSystemLanguageFrench": "Francés", + "SettingsTabSystemSystemLanguageGerman": "Alemán", + "SettingsTabSystemSystemLanguageItalian": "Italiano", + "SettingsTabSystemSystemLanguageSpanish": "Español", + "SettingsTabSystemSystemLanguageChinese": "Chino", + "SettingsTabSystemSystemLanguageKorean": "Coreano", + "SettingsTabSystemSystemLanguageDutch": "Neerlandés/Holandés", + "SettingsTabSystemSystemLanguagePortuguese": "Portugués", + "SettingsTabSystemSystemLanguageRussian": "Ruso", + "SettingsTabSystemSystemLanguageTaiwanese": "Taiwanés", + "SettingsTabSystemSystemLanguageBritishEnglish": "Inglés británico", + "SettingsTabSystemSystemLanguageCanadianFrench": "Francés canadiense", + "SettingsTabSystemSystemLanguageLatinAmericanSpanish": "Español latinoamericano", + "SettingsTabSystemSystemLanguageSimplifiedChinese": "Chino simplificado", + "SettingsTabSystemSystemLanguageTraditionalChinese": "Chino tradicional", + "SettingsTabSystemSystemTimeZone": "Zona horaria del sistema:", + "SettingsTabSystemSystemTime": "Hora del sistema:", + "SettingsTabSystemEnableVsync": "Sincronización vertical", + "SettingsTabSystemEnablePptc": "PPTC (Cache de Traducción de Perfil Persistente)", + "SettingsTabSystemEnableLowPowerPptc": "Cache PPTC de bajo consumo", + "SettingsTabSystemEnableFsIntegrityChecks": "Comprobar integridad de los archivos", + "SettingsTabSystemAudioBackend": "Motor de audio:", + "SettingsTabSystemAudioBackendDummy": "Vacío", + "SettingsTabSystemAudioBackendOpenAL": "OpenAL", + "SettingsTabSystemAudioBackendSoundIO": "SoundIO", + "SettingsTabSystemAudioBackendSDL2": "SDL2", + "SettingsTabSystemHacks": "Hacks", + "SettingsTabSystemHacksNote": " (Pueden causar inestabilidad)", + "SettingsTabSystemDramSize": "Tamaño DRAM:", + "SettingsTabSystemDramSize4GiB": "4GiB", + "SettingsTabSystemDramSize6GiB": "6GiB", + "SettingsTabSystemDramSize8GiB": "8GiB", + "SettingsTabSystemDramSize12GiB": "12GiB", + "SettingsTabSystemIgnoreMissingServices": "Ignorar servicios no implementados", + "SettingsTabSystemIgnoreApplet": "Ignorar el Applet", + "SettingsTabGraphics": "Gráficos", + "SettingsTabGraphicsAPI": "API de gráficos", + "SettingsTabGraphicsEnableShaderCache": "Habilitar caché de sombreadores", + "SettingsTabGraphicsAnisotropicFiltering": "Filtro anisotrópico:", + "SettingsTabGraphicsAnisotropicFilteringAuto": "Automático", + "SettingsTabGraphicsAnisotropicFiltering2x": "x2", + "SettingsTabGraphicsAnisotropicFiltering4x": "x4", + "SettingsTabGraphicsAnisotropicFiltering8x": "x8", + "SettingsTabGraphicsAnisotropicFiltering16x": "x16", + "SettingsTabGraphicsResolutionScale": "Escala de resolución:", + "SettingsTabGraphicsResolutionScaleCustom": "Personalizada (no recomendado)", + "SettingsTabGraphicsResolutionScaleNative": "Nativa (720p/1080p)", + "SettingsTabGraphicsResolutionScale2x": "x2 (1440p/2160p)", + "SettingsTabGraphicsResolutionScale3x": "x3 (2160p/3240p)", + "SettingsTabGraphicsResolutionScale4x": "4x (2880p/4320p) (no recomendado)", + "SettingsTabGraphicsAspectRatio": "Relación de aspecto:", + "SettingsTabGraphicsAspectRatio4x3": "4:3", + "SettingsTabGraphicsAspectRatio16x9": "16:9", + "SettingsTabGraphicsAspectRatio16x10": "16:10", + "SettingsTabGraphicsAspectRatio21x9": "21:9", + "SettingsTabGraphicsAspectRatio32x9": "32:9", + "SettingsTabGraphicsAspectRatioStretch": "Estirar a la ventana", + "SettingsTabGraphicsDeveloperOptions": "Opciones de desarrollador", + "SettingsTabGraphicsShaderDumpPath": "Directorio de volcado de sombreadores:", + "SettingsTabLogging": "Registros", + "SettingsTabLoggingLogging": "Registros", + "SettingsTabLoggingEnableLoggingToFile": "Habilitar registro a archivo", + "SettingsTabLoggingEnableStubLogs": "Habilitar registros de Stub", + "SettingsTabLoggingEnableInfoLogs": "Habilitar registros de Info", + "SettingsTabLoggingEnableWarningLogs": "Habilitar registros de Advertencia", + "SettingsTabLoggingEnableErrorLogs": "Habilitar registros de Error", + "SettingsTabLoggingEnableTraceLogs": "Habilitar registros de Rastro", + "SettingsTabLoggingEnableGuestLogs": "Habilitar registros de Guest", + "SettingsTabLoggingEnableFsAccessLogs": "Habilitar registros de Fs Access", + "SettingsTabLoggingFsGlobalAccessLogMode": "Modo de registros Fs Global Access:", + "SettingsTabLoggingDeveloperOptions": "Opciones de desarrollador (ADVERTENCIA: empeorarán el rendimiento)", + "SettingsTabLoggingDeveloperOptionsNote": "ADVERTENCIA: Reducirá el rendimiento", + "SettingsTabLoggingGraphicsBackendLogLevel": "Nivel de registro de backend gráficos:", + "SettingsTabLoggingGraphicsBackendLogLevelNone": "Nada", + "SettingsTabLoggingGraphicsBackendLogLevelError": "Errores", + "SettingsTabLoggingGraphicsBackendLogLevelPerformance": "Ralentizaciones", + "SettingsTabLoggingGraphicsBackendLogLevelAll": "Todo", + "SettingsTabLoggingEnableDebugLogs": "Habilitar registros de debug", + "SettingsTabInput": "Entrada", + "SettingsTabInputEnableDockedMode": "Modo dock/TV", + "SettingsTabInputDirectKeyboardAccess": "Acceso directo al teclado", + "SettingsButtonSave": "Guardar", + "SettingsButtonClose": "Cerrar", + "SettingsButtonOk": "Aceptar", + "SettingsButtonCancel": "Cancelar", + "SettingsButtonApply": "Aplicar", + "ControllerSettingsPlayer": "Jugador", + "ControllerSettingsPlayer1": "Jugador 1", + "ControllerSettingsPlayer2": "Jugador 2", + "ControllerSettingsPlayer3": "Jugador 3", + "ControllerSettingsPlayer4": "Jugador 4", + "ControllerSettingsPlayer5": "Jugador 5", + "ControllerSettingsPlayer6": "Jugador 6", + "ControllerSettingsPlayer7": "Jugador 7", + "ControllerSettingsPlayer8": "Jugador 8", + "ControllerSettingsHandheld": "Portátil", + "ControllerSettingsInputDevice": "Dispositivo de entrada", + "ControllerSettingsRefresh": "Actualizar", + "ControllerSettingsDeviceDisabled": "Deshabilitado", + "ControllerSettingsControllerType": "Tipo de Mando", + "ControllerSettingsControllerTypeHandheld": "Portátil", + "ControllerSettingsControllerTypeProController": "Mando Pro", + "ControllerSettingsControllerTypeJoyConPair": "Doble Joy-Con", + "ControllerSettingsControllerTypeJoyConLeft": "Joy-Con Izquierdo", + "ControllerSettingsControllerTypeJoyConRight": "Joy-Con Derecho", + "ControllerSettingsProfile": "Perfil", + "ControllerSettingsProfileDefault": "Predeterminado", + "ControllerSettingsLoad": "Cargar", + "ControllerSettingsAdd": "Agregar", + "ControllerSettingsRemove": "Quitar", + "ControllerSettingsButtons": "Botones", + "ControllerSettingsButtonA": "A", + "ControllerSettingsButtonB": "B", + "ControllerSettingsButtonX": "X", + "ControllerSettingsButtonY": "Y", + "ControllerSettingsButtonPlus": "+", + "ControllerSettingsButtonMinus": "-", + "ControllerSettingsDPad": "Pad direccional", + "ControllerSettingsDPadUp": "Arriba", + "ControllerSettingsDPadDown": "Abajo", + "ControllerSettingsDPadLeft": "Izquierda", + "ControllerSettingsDPadRight": "Derecha", + "ControllerSettingsStickButton": "Botón", + "ControllerSettingsStickUp": "Arriba", + "ControllerSettingsStickDown": "Abajo", + "ControllerSettingsStickLeft": "Izquierda", + "ControllerSettingsStickRight": "Derecha", + "ControllerSettingsStickStick": "Palanca", + "ControllerSettingsStickInvertXAxis": "Invertir eje X", + "ControllerSettingsStickInvertYAxis": "Invertir eje Y", + "ControllerSettingsStickDeadzone": "Zona muerta:", + "ControllerSettingsLStick": "Palanca izquierda", + "ControllerSettingsRStick": "Palanca derecha", + "ControllerSettingsTriggersLeft": "Gatillos izquierdos", + "ControllerSettingsTriggersRight": "Gatillos derechos", + "ControllerSettingsTriggersButtonsLeft": "Botones de gatillo izquierdos", + "ControllerSettingsTriggersButtonsRight": "Botones de gatillo derechos", + "ControllerSettingsTriggers": "Gatillos", + "ControllerSettingsTriggerL": "L", + "ControllerSettingsTriggerR": "R", + "ControllerSettingsTriggerZL": "ZL", + "ControllerSettingsTriggerZR": "ZR", + "ControllerSettingsLeftSL": "SL", + "ControllerSettingsLeftSR": "SR", + "ControllerSettingsRightSL": "SL", + "ControllerSettingsRightSR": "SR", + "ControllerSettingsExtraButtonsLeft": "Botones izquierdos", + "ControllerSettingsExtraButtonsRight": "Botones derechos", + "ControllerSettingsMisc": "Misceláneo", + "ControllerSettingsTriggerThreshold": "Límite de gatillos:", + "ControllerSettingsMotion": "Movimiento", + "ControllerSettingsMotionUseCemuhookCompatibleMotion": "Usar movimiento compatible con CemuHook", + "ControllerSettingsMotionControllerSlot": "Puerto del mando:", + "ControllerSettingsMotionMirrorInput": "Paralelizar derecho e izquierdo", + "ControllerSettingsMotionRightJoyConSlot": "Puerto del Joy-Con derecho:", + "ControllerSettingsMotionServerHost": "Host del servidor:", + "ControllerSettingsMotionGyroSensitivity": "Sensibilidad de Gyro:", + "ControllerSettingsMotionGyroDeadzone": "Zona muerta de Gyro:", + "ControllerSettingsSave": "Guardar", + "ControllerSettingsClose": "Cerrar", + "KeyUnknown": "Desconocido", + "KeyShiftLeft": "Shift Left", + "KeyShiftRight": "Shift Right", + "KeyControlLeft": "Ctrl Left", + "KeyMacControlLeft": "⌃ Left", + "KeyControlRight": "Ctrl Right", + "KeyMacControlRight": "⌃ Right", + "KeyAltLeft": "Alt Left", + "KeyMacAltLeft": "⌥ Left", + "KeyAltRight": "Alt Right", + "KeyMacAltRight": "⌥ Right", + "KeyWinLeft": "⊞ Left", + "KeyMacWinLeft": "⌘ Left", + "KeyWinRight": "⊞ Right", + "KeyMacWinRight": "⌘ Right", + "KeyMenu": "Menu", + "KeyUp": "Up", + "KeyDown": "Down", + "KeyLeft": "Left", + "KeyRight": "Right", + "KeyEnter": "Enter", + "KeyEscape": "Escape", + "KeySpace": "Space", + "KeyTab": "Tab", + "KeyBackSpace": "Backspace", + "KeyInsert": "Insert", + "KeyDelete": "Delete", + "KeyPageUp": "Page Up", + "KeyPageDown": "Page Down", + "KeyHome": "Home", + "KeyEnd": "End", + "KeyCapsLock": "Caps Lock", + "KeyScrollLock": "Scroll Lock", + "KeyPrintScreen": "Print Screen", + "KeyPause": "Pause", + "KeyNumLock": "Num Lock", + "KeyClear": "Clear", + "KeyKeypad0": "Keypad 0", + "KeyKeypad1": "Keypad 1", + "KeyKeypad2": "Keypad 2", + "KeyKeypad3": "Keypad 3", + "KeyKeypad4": "Keypad 4", + "KeyKeypad5": "Keypad 5", + "KeyKeypad6": "Keypad 6", + "KeyKeypad7": "Keypad 7", + "KeyKeypad8": "Keypad 8", + "KeyKeypad9": "Keypad 9", + "KeyKeypadDivide": "Keypad Divide", + "KeyKeypadMultiply": "Keypad Multiply", + "KeyKeypadSubtract": "Keypad Subtract", + "KeyKeypadAdd": "Keypad Add", + "KeyKeypadDecimal": "Keypad Decimal", + "KeyKeypadEnter": "Keypad Enter", + "KeyNumber0": "0", + "KeyNumber1": "1", + "KeyNumber2": "2", + "KeyNumber3": "3", + "KeyNumber4": "4", + "KeyNumber5": "5", + "KeyNumber6": "6", + "KeyNumber7": "7", + "KeyNumber8": "8", + "KeyNumber9": "9", + "KeyTilde": "~", + "KeyGrave": "`", + "KeyMinus": "-", + "KeyPlus": "+", + "KeyBracketLeft": "[", + "KeyBracketRight": "]", + "KeySemicolon": ";", + "KeyQuote": "\"", + "KeyComma": ",", + "KeyPeriod": ".", + "KeySlash": "/", + "KeyBackSlash": "\\", + "KeyUnbound": "Unbound", + "GamepadLeftStick": "L Stick Button", + "GamepadRightStick": "R Stick Button", + "GamepadLeftShoulder": "Left Shoulder", + "GamepadRightShoulder": "Right Shoulder", + "GamepadLeftTrigger": "Left Trigger", + "GamepadRightTrigger": "Right Trigger", + "GamepadDpadUp": "Up", + "GamepadDpadDown": "Down", + "GamepadDpadLeft": "Left", + "GamepadDpadRight": "Right", + "GamepadMinus": "-", + "GamepadPlus": "+", + "GamepadGuide": "Guide", + "GamepadMisc1": "Misc", + "GamepadPaddle1": "Paddle 1", + "GamepadPaddle2": "Paddle 2", + "GamepadPaddle3": "Paddle 3", + "GamepadPaddle4": "Paddle 4", + "GamepadTouchpad": "Touchpad", + "GamepadSingleLeftTrigger0": "Left Trigger 0", + "GamepadSingleRightTrigger0": "Right Trigger 0", + "GamepadSingleLeftTrigger1": "Left Trigger 1", + "GamepadSingleRightTrigger1": "Right Trigger 1", + "StickLeft": "Left Stick", + "StickRight": "Right Stick", + "UserProfilesSelectedUserProfile": "Perfil de usuario seleccionado:", + "UserProfilesSaveProfileName": "Guardar nombre de perfil", + "UserProfilesChangeProfileImage": "Cambiar imagen de perfil", + "UserProfilesAvailableUserProfiles": "Perfiles de usuario disponibles:", + "UserProfilesAddNewProfile": "Añadir nuevo perfil", + "UserProfilesDelete": "Eliminar", + "UserProfilesClose": "Cerrar", + "ProfileNameSelectionWatermark": "Escoge un apodo", + "ProfileImageSelectionTitle": "Selección de imagen de perfil", + "ProfileImageSelectionHeader": "Elige una imagen de perfil", + "ProfileImageSelectionNote": "Puedes importar una imagen de perfil personalizada, o seleccionar un avatar del firmware de sistema", + "ProfileImageSelectionImportImage": "Importar imagen", + "ProfileImageSelectionSelectAvatar": "Seleccionar avatar del firmware", + "InputDialogTitle": "Cuadro de diálogo de entrada", + "InputDialogOk": "Aceptar", + "InputDialogCancel": "Cancelar", + "InputDialogCancelling": "Cancelling", + "InputDialogClose": "Close", + "InputDialogAddNewProfileTitle": "Introducir nombre de perfil", + "InputDialogAddNewProfileHeader": "Por favor elige un nombre de usuario", + "InputDialogAddNewProfileSubtext": "(Máximo de caracteres: {0})", + "AvatarChoose": "Escoger", + "AvatarSetBackgroundColor": "Establecer color de fondo", + "AvatarClose": "Cerrar", + "ControllerSettingsLoadProfileToolTip": "Cargar perfil", + "ControllerSettingsViewProfileToolTip": "Ver perfil", + "ControllerSettingsAddProfileToolTip": "Agregar perfil", + "ControllerSettingsRemoveProfileToolTip": "Eliminar perfil", + "ControllerSettingsSaveProfileToolTip": "Guardar perfil", + "MenuBarFileToolsTakeScreenshot": "Captura de pantalla", + "MenuBarFileToolsHideUi": "Ocultar interfaz", + "GameListContextMenuRunApplication": "Ejecutar aplicación", + "GameListContextMenuToggleFavorite": "Marcar favorito", + "GameListContextMenuToggleFavoriteToolTip": "Marca o desmarca el juego como favorito", + "SettingsTabGeneralTheme": "Tema:", + "SettingsTabGeneralThemeAuto": "Auto", + "SettingsTabGeneralThemeDark": "Oscuro", + "SettingsTabGeneralThemeLight": "Claro", + "ControllerSettingsConfigureGeneral": "Configurar", + "ControllerSettingsRumble": "Vibración", + "ControllerSettingsRumbleStrongMultiplier": "Multiplicador de vibraciones fuertes", + "ControllerSettingsRumbleWeakMultiplier": "Multiplicador de vibraciones débiles", + "DialogMessageSaveNotAvailableMessage": "No hay datos de guardado para {0} [{1:x16}]", + "DialogMessageSaveNotAvailableCreateSaveMessage": "¿Quieres crear datos de guardado para este juego?", + "DialogConfirmationTitle": "Ryujinx - Confirmación", + "DialogUpdaterTitle": "Ryujinx - Actualizador", + "DialogErrorTitle": "Ryujinx - Error", + "DialogWarningTitle": "Ryujinx - Advertencia", + "DialogExitTitle": "Ryujinx - Salir", + "DialogErrorMessage": "Ryujinx encontró un error", + "DialogExitMessage": "¿Seguro que quieres cerrar Ryujinx?", + "DialogExitSubMessage": "¡Se perderán los datos no guardados!", + "DialogMessageCreateSaveErrorMessage": "Hubo un error al crear los datos de guardado especificados: {0}", + "DialogMessageFindSaveErrorMessage": "Hubo un error encontrando los datos de guardado especificados: {0}", + "FolderDialogExtractTitle": "Elige la carpeta en la que deseas extraer", + "DialogNcaExtractionMessage": "Extrayendo {0} sección de {1}...", + "DialogNcaExtractionTitle": "Extractor de sección NCA", + "DialogNcaExtractionMainNcaNotFoundErrorMessage": "Fallo de extracción. El NCA principal no estaba presente en el archivo seleccionado.", + "DialogNcaExtractionCheckLogErrorMessage": "Fallo de extracción. Lee el registro para más información.", + "DialogNcaExtractionSuccessMessage": "Se completó la extracción con éxito.", + "DialogUpdaterConvertFailedMessage": "No se pudo convertir la versión actual de Ryujinx.", + "DialogUpdaterCancelUpdateMessage": "¡Cancelando actualización!", + "DialogUpdaterAlreadyOnLatestVersionMessage": "¡Ya tienes la versión más reciente de Ryujinx!", + "DialogUpdaterFailedToGetVersionMessage": "Se ha producido un error al intentar obtener información de liberación de GitHub Release. Esto puede ser causado si una nueva versión está siendo compilada por GitHub Actions. Inténtalo de nuevo en unos minutos.", + "DialogUpdaterConvertFailedGithubMessage": "No se pudo convertir la versión de Ryujinx recibida de GitHub Release.", + "DialogUpdaterDownloadingMessage": "Descargando actualización...", + "DialogUpdaterExtractionMessage": "Extrayendo actualización...", + "DialogUpdaterRenamingMessage": "Renombrando actualización...", + "DialogUpdaterAddingFilesMessage": "Aplicando actualización...", + "DialogUpdaterShowChangelogMessage": "Show Changelog", + "DialogUpdaterCompleteMessage": "¡Actualización completa!", + "DialogUpdaterRestartMessage": "¿Quieres reiniciar Ryujinx?", + "DialogUpdaterNoInternetMessage": "¡No estás conectado a internet!", + "DialogUpdaterNoInternetSubMessage": "¡Por favor, verifica que tu conexión a Internet funciona!", + "DialogUpdaterDirtyBuildMessage": "¡No puedes actualizar una versión \"dirty\" de Ryujinx!", + "DialogUpdaterDirtyBuildSubMessage": "Por favor, descarga Ryujinx en https://ryujinx.app/download si buscas una versión con soporte.", + "DialogRestartRequiredMessage": "Se necesita reiniciar", + "DialogThemeRestartMessage": "Tema guardado. Se necesita reiniciar para aplicar el tema.", + "DialogThemeRestartSubMessage": "¿Quieres reiniciar?", + "DialogFirmwareInstallEmbeddedMessage": "¿Quieres instalar el firmware incluido en este juego? (Firmware versión {0})", + "DialogFirmwareInstallEmbeddedSuccessMessage": "No se encontró ning{un firmware instalado pero Ryujinx pudo instalar firmware {0} del juego proporcionado.\nEl emulador iniciará.", + "DialogFirmwareNoFirmwareInstalledMessage": "No hay firmware instalado", + "DialogFirmwareInstalledMessage": "Se instaló el firmware {0}", + "DialogInstallFileTypesSuccessMessage": "¡Tipos de archivos instalados con éxito!", + "DialogInstallFileTypesErrorMessage": "No se pudo desinstalar los tipos de archivo.", + "DialogUninstallFileTypesSuccessMessage": "¡Tipos de archivos desinstalados con éxito!", + "DialogUninstallFileTypesErrorMessage": "No se pudo desinstalar los tipos de archivo.", + "DialogOpenSettingsWindowLabel": "Abrir ventana de opciones", + "DialogOpenXCITrimmerWindowLabel": "XCI Trimmer Window", + "DialogControllerAppletTitle": "Applet de mandos", + "DialogMessageDialogErrorExceptionMessage": "Error al mostrar cuadro de diálogo: {0}", + "DialogSoftwareKeyboardErrorExceptionMessage": "Error al mostrar teclado de software: {0}", + "DialogErrorAppletErrorExceptionMessage": "Error al mostrar díalogo ErrorApplet: {0}", + "DialogUserErrorDialogMessage": "{0}: {1}", + "DialogUserErrorDialogInfoMessage": "\nPara más información sobre cómo arreglar este error, sigue nuestra Guía de Instalación.", + "DialogUserErrorDialogTitle": "Ryujinx Error ({0})", + "DialogAmiiboApiTitle": "Amiibo API", + "DialogAmiiboApiFailFetchMessage": "Ocurrió un error al recibir información de la API.", + "DialogAmiiboApiConnectErrorMessage": "No se pudo conectar al servidor de la API Amiibo. El servicio puede estar caído o tu conexión a internet puede haberse desconectado.", + "DialogProfileInvalidProfileErrorMessage": "El perfil {0} no es compatible con el sistema actual de configuración de entrada.", + "DialogProfileDefaultProfileOverwriteErrorMessage": "El perfil predeterminado no se puede sobreescribir", + "DialogProfileDeleteProfileTitle": "Eliminando perfil", + "DialogProfileDeleteProfileMessage": "Esta acción es irreversible, ¿estás seguro de querer continuar?", + "DialogWarning": "Advertencia", + "DialogPPTCDeletionMessage": "Vas a borrar la caché de PPTC para:\n\n{0}\n\n¿Estás seguro de querer continuar?", + "DialogPPTCDeletionErrorMessage": "Error purgando la caché de PPTC en {0}: {1}", + "DialogShaderDeletionMessage": "Vas a borrar la caché de sombreadores para:\n\n{0}\n\n¿Estás seguro de querer continuar?", + "DialogShaderDeletionErrorMessage": "Error purgando la caché de sombreadores en {0}: {1}", + "DialogRyujinxErrorMessage": "Ryujinx ha encontrado un error", + "DialogInvalidTitleIdErrorMessage": "Error de interfaz: El juego seleccionado no tiene una ID válida", + "DialogFirmwareInstallerFirmwareNotFoundErrorMessage": "No se pudo encontrar un firmware válido en {0}.", + "DialogFirmwareInstallerFirmwareInstallTitle": "Instalar firmware {0}", + "DialogFirmwareInstallerFirmwareInstallMessage": "Se instalará la versión de sistema {0}.", + "DialogFirmwareInstallerFirmwareInstallSubMessage": "\n\nEsto reemplazará la versión de sistema actual, {0}.", + "DialogFirmwareInstallerFirmwareInstallConfirmMessage": "\n\n¿Continuar?", + "DialogFirmwareInstallerFirmwareInstallWaitMessage": "Instalando firmware...", + "DialogFirmwareInstallerFirmwareInstallSuccessMessage": "Versión de sistema {0} instalada con éxito.", + "DialogUserProfileDeletionWarningMessage": "Si eliminas el perfil seleccionado no quedará ningún otro perfil", + "DialogUserProfileDeletionConfirmMessage": "¿Quieres eliminar el perfil seleccionado?", + "DialogUserProfileUnsavedChangesTitle": "Advertencia - Cambios sin guardar", + "DialogUserProfileUnsavedChangesMessage": "Ha realizado cambios en este perfil de usuario que no han sido guardados.", + "DialogUserProfileUnsavedChangesSubMessage": "¿Quieres descartar los cambios realizados?", + "DialogControllerSettingsModifiedConfirmMessage": "Se ha actualizado la configuración del mando actual.", + "DialogControllerSettingsModifiedConfirmSubMessage": "¿Guardar cambios?", + "DialogLoadFileErrorMessage": "{0}. Archivo con error: {1}", + "DialogModAlreadyExistsMessage": "El mod ya existe", + "DialogModInvalidMessage": "¡El directorio especificado no contiene un mod!", + "DialogModDeleteNoParentMessage": "Error al eliminar: ¡No se pudo encontrar el directorio principal para el mod \"{0}\"!", + "DialogDlcNoDlcErrorMessage": "¡Ese archivo no contiene contenido descargable para el título seleccionado!", + "DialogPerformanceCheckLoggingEnabledMessage": "Has habilitado los registros debug, diseñados solo para uso de los desarrolladores.", + "DialogPerformanceCheckLoggingEnabledConfirmMessage": "Para un rendimiento óptimo, se recomienda deshabilitar los registros debug. ¿Quieres deshabilitarlos ahora?", + "DialogPerformanceCheckShaderDumpEnabledMessage": "Has habilitado el volcado de sombreadores, diseñado solo para uso de los desarrolladores.", + "DialogPerformanceCheckShaderDumpEnabledConfirmMessage": "Para un rendimiento óptimo, se recomienda deshabilitar el volcado de sombreadores. ¿Quieres deshabilitarlo ahora?", + "DialogLoadAppGameAlreadyLoadedMessage": "Ya has cargado un juego", + "DialogLoadAppGameAlreadyLoadedSubMessage": "Por favor, detén la emulación o cierra el emulador antes de iniciar otro juego.", + "DialogUpdateAddUpdateErrorMessage": "¡Ese archivo no contiene una actualización para el título seleccionado!", + "DialogSettingsBackendThreadingWarningTitle": "Advertencia - multihilado de gráficos", + "DialogSettingsBackendThreadingWarningMessage": "Ryujinx debe reiniciarse para aplicar este cambio. Dependiendo de tu plataforma, puede que tengas que desactivar manualmente la optimización enlazada de tus controladores gráficos para usar el multihilo de Ryujinx.", + "DialogModManagerDeletionWarningMessage": "Estás a punto de eliminar el mod: {0}\n\n¿Estás seguro de que quieres continuar?", + "DialogModManagerDeletionAllWarningMessage": "Estás a punto de eliminar todos los Mods para este título.\n\n¿Estás seguro de que quieres continuar?", + "SettingsTabGraphicsFeaturesOptions": "Funcionalidades", + "SettingsTabGraphicsBackendMultithreading": "Multihilado del motor gráfico:", + "CommonAuto": "Automático", + "CommonOff": "Desactivado", + "CommonOn": "Activado", + "InputDialogYes": "Sí", + "InputDialogNo": "No", + "DialogProfileInvalidProfileNameErrorMessage": "El nombre de archivo contiene caracteres inválidos. Por favor, inténtalo de nuevo.", + "MenuBarOptionsPauseEmulation": "Pausar", + "MenuBarOptionsResumeEmulation": "Reanudar", + "AboutUrlTooltipMessage": "Haz clic para abrir el sitio web de Ryujinx en tu navegador predeterminado.", + "AboutDisclaimerMessage": "Ryujinx no tiene afiliación alguna con Nintendo™,\nni con ninguno de sus socios.", + "AboutAmiiboDisclaimerMessage": "Utilizamos AmiiboAPI (www.amiiboapi.com)\nen nuestra emulación de Amiibo.", + "AboutPatreonUrlTooltipMessage": "Haz clic para abrir el Patreon de Ryujinx en tu navegador predeterminado.", + "AboutGithubUrlTooltipMessage": "Haz clic para abrir el GitHub de Ryujinx en tu navegador predeterminado.", + "AboutDiscordUrlTooltipMessage": "Haz clic para recibir una invitación al Discord de Ryujinx en tu navegador predeterminado.", + "AboutTwitterUrlTooltipMessage": "Haz clic para abrir el Twitter de Ryujinx en tu navegador predeterminado.", + "AboutRyujinxAboutTitle": "Acerca de:", + "AboutRyujinxAboutContent": "Ryujinx es un emulador para Nintendo Switch™.\nPor favor, apóyanos en Patreon.\nEncuentra las noticias más recientes en nuestro Twitter o Discord.\nDesarrolladores interesados en contribuir pueden encontrar más información en GitHub o Discord.", + "AboutRyujinxMaintainersTitle": "Mantenido por:", + "AboutRyujinxMaintainersContentTooltipMessage": "Haz clic para abrir la página de contribuidores en tu navegador predeterminado.", + "AboutRyujinxSupprtersTitle": "Apoyado en Patreon Por:", + "AmiiboSeriesLabel": "Serie de Amiibo", + "AmiiboCharacterLabel": "Personaje", + "AmiiboScanButtonLabel": "Escanear", + "AmiiboOptionsShowAllLabel": "Mostrar todos los Amiibo", + "AmiiboOptionsUsRandomTagLabel": "Hack: usar etiqueta aleatoria Uuid", + "DlcManagerTableHeadingEnabledLabel": "Habilitado", + "DlcManagerTableHeadingTitleIdLabel": "ID de título", + "DlcManagerTableHeadingContainerPathLabel": "Directorio del contenedor", + "DlcManagerTableHeadingFullPathLabel": "Directorio completo", + "DlcManagerRemoveAllButton": "Quitar todo", + "DlcManagerEnableAllButton": "Activar todas", + "DlcManagerDisableAllButton": "Desactivar todos", + "ModManagerDeleteAllButton": "Eliminar Todo", + "MenuBarOptionsChangeLanguage": "Cambiar idioma", + "MenuBarShowFileTypes": "Mostrar tipos de archivo", + "CommonSort": "Orden", + "CommonShowNames": "Mostrar nombres", + "CommonFavorite": "Favorito", + "OrderAscending": "Ascendente", + "OrderDescending": "Descendente", + "SettingsTabGraphicsFeatures": "Funcionalidades Y Mejoras", + "ErrorWindowTitle": "Ventana de error", + "ToggleDiscordTooltip": "Elige si muestras Ryujinx o no en tu actividad de Discord cuando lo estés usando", + "AddGameDirBoxTooltip": "Elige un directorio de juegos para mostrar en la ventana principal", + "AddGameDirTooltip": "Agrega un directorio de juegos a la lista", + "RemoveGameDirTooltip": "Quita el directorio seleccionado de la lista", + "AddAutoloadDirBoxTooltip": "Elige un directorio de carga automática para agregar a la lista", + "AddAutoloadDirTooltip": "Agregar un directorio de carga automática a la lista", + "RemoveAutoloadDirTooltip": "Eliminar el directorio de carga automática seleccionado", + "CustomThemeCheckTooltip": "Activa o desactiva los temas personalizados para la interfaz", + "CustomThemePathTooltip": "Carpeta que contiene los temas personalizados para la interfaz", + "CustomThemeBrowseTooltip": "Busca un tema personalizado para la interfaz", + "DockModeToggleTooltip": "El modo dock o modo TV hace que la consola emulada se comporte como una Nintendo Switch en su dock. Esto mejora la calidad gráfica en la mayoría de los juegos. Del mismo modo, si lo desactivas, el sistema emulado se comportará como una Nintendo Switch en modo portátil, reduciendo la cálidad de los gráficos.\n\nConfigura los controles de \"Jugador\" 1 si planeas jugar en modo dock/TV; configura los controles de \"Portátil\" si planeas jugar en modo portátil.\n\nActívalo si no sabes qué hacer.", + "DirectKeyboardTooltip": "Soporte de acceso directo al teclado (HID). Proporciona a los juegos acceso a su teclado como dispositivo de entrada de texto.\n\nSolo funciona con juegos que permiten de forma nativa el uso del teclado en el hardware de Switch.\n\nDesactívalo si no sabes qué hacer.", + "DirectMouseTooltip": "Soporte de acceso directo al mouse (HID). Proporciona a los juegos acceso a su mouse como puntero.\n\nSolo funciona con juegos que permiten de forma nativa el uso de controles con mouse en el hardware de switch, lo cual son pocos.\n\nCuando esté activado, la funcionalidad de pantalla táctil puede no funcionar.\n\nDesactívalo si no sabes qué hacer.", + "RegionTooltip": "Cambia la región del sistema", + "LanguageTooltip": "Cambia el idioma del sistema", + "TimezoneTooltip": "Cambia la zona horaria del sistema", + "TimeTooltip": "Cambia la hora del sistema", + "VSyncToggleTooltip": "Sincronización vertical de la consola emulada. En práctica un limitador del framerate para la mayoría de los juegos; desactivando puede causar que juegos corran a mayor velocidad o que las pantallas de carga tarden más o queden atascados.\n\nSe puede alternar en juego utilizando una tecla de acceso rápido configurable (F1 by default). Recomendamos hacer esto en caso de querer desactivar sincroniziación vertical.\n\nDesactívalo si no sabes qué hacer.", + "PptcToggleTooltip": "Guarda funciones de JIT traducidas para que no sea necesario traducirlas cada vez que el juego carga.\n\nReduce los tirones y acelera significativamente el tiempo de inicio de los juegos después de haberlos ejecutado al menos una vez.\n\nActívalo si no sabes qué hacer.", + "LowPowerPptcToggleTooltip": "Cargue el PPTC utilizando un tercio de la cantidad de núcleos.", + "FsIntegrityToggleTooltip": "Comprueba si hay archivos corruptos en los juegos que ejecutes al abrirlos, y si detecta archivos corruptos, muestra un error de Hash en los registros.\n\nEsto no tiene impacto alguno en el rendimiento y está pensado para ayudar a resolver problemas.\n\nActívalo si no sabes qué hacer.", + "AudioBackendTooltip": "Cambia el motor usado para renderizar audio.\n\nSDL2 es el preferido, mientras que OpenAL y SoundIO se usan si hay problemas con este. Dummy no produce audio.\n\nSelecciona SDL2 si no sabes qué hacer.", + "MemoryManagerTooltip": "Cambia la forma de mapear y acceder a la memoria del guest. Afecta en gran medida al rendimiento de la CPU emulada.\n\nSelecciona \"Host sin verificación\" si no sabes qué hacer.", + "MemoryManagerSoftwareTooltip": "Usa una tabla de paginación de software para traducir direcciones. Ofrece la precisión más exacta pero el rendimiento más lento.", + "MemoryManagerHostTooltip": "Mapea la memoria directamente en la dirección de espacio del host. Compilación y ejecución JIT mucho más rápida.", + "MemoryManagerUnsafeTooltip": "Mapea la memoria directamente, pero no enmascara la dirección dentro del espacio de dirección del guest antes del acceso. El modo más rápido, pero a costa de seguridad. La aplicación guest puede acceder a la memoria desde cualquier parte en Ryujinx, así que ejecuta solo programas en los que confíes cuando uses este modo.", + "UseHypervisorTooltip": "Usar Hypervisor en lugar de JIT. Mejora enormemente el rendimiento cuando está disponible, pero puede ser inestable en su estado actual.", + "DRamTooltip": "Expande la memoria DRAM del sistema emulado de 4GiB a 6GiB.\n\nUtilizar solo con packs de texturas HD o mods de resolución 4K. NO mejora el rendimiento.\n\nDesactívalo si no sabes qué hacer.", + "IgnoreMissingServicesTooltip": "Hack para ignorar servicios no implementados del Horizon OS. Esto puede ayudar a sobrepasar crasheos cuando inicies ciertos juegos.\n\nDesactívalo si no sabes qué hacer.", + "IgnoreAppletTooltip": "El cuadro de diálogo externo \"Applet del controlador\" no aparecerá si el gamepad se desconecta durante el juego. No aparecerá ningún mensaje para cerrar el cuadro de diálogo o configurar un nuevo controlador. Una vez que se vuelva a conectar el controlador que se había desconectado anteriormente, el juego se reanudará automáticamente.", + "GraphicsBackendThreadingTooltip": "Ejecuta los comandos del motor gráfico en un segundo hilo. Acelera la compilación de sombreadores, reduce los tirones, y mejora el rendimiento en controladores gráficos que no realicen su propio procesamiento con múltiples hilos. Rendimiento ligeramente superior en controladores gráficos que soporten múltiples hilos.\n\nSelecciona \"Auto\" si no sabes qué hacer.", + "GalThreadingTooltip": "Ejecuta los comandos del motor gráfico en un segundo hilo. Acelera la compilación de sombreadores, reduce los tirones, y mejora el rendimiento en controladores gráficos que no realicen su propio procesamiento con múltiples hilos. Rendimiento ligeramente superior en controladores gráficos que soporten múltiples hilos.\n\nSelecciona \"Auto\" si no sabes qué hacer.", + "ShaderCacheToggleTooltip": "Guarda una caché de sombreadores en disco, la cual reduce los tirones a medida que vas jugando.\n\nActívalo si no sabes qué hacer.", + "ResolutionScaleTooltip": "Multiplica la resolución de rendereo del juego.\n\nAlgunos juegos podrían no funcionar con esto y verse pixelado al aumentar la resolución; en esos casos, quizás sería necesario buscar mods que de anti-aliasing o que aumenten la resolución interna. Para usar este último, probablemente necesitarás seleccionar Nativa.\n\nEsta opción puede ser modificada mientras que un juego este corriendo haciendo click en \"Aplicar\" más abajo; simplemente puedes mover la ventana de configuración a un lado y experimentar hasta que encuentres tu estilo preferido para un juego.\n\nTener en cuenta que 4x es excesivo para prácticamente cualquier configuración.", + "ResolutionScaleEntryTooltip": "Escalado de resolución de coma flotante, como por ejemplo 1,5. Los valores no íntegros pueden causar errores gráficos o crashes.", + "AnisotropyTooltip": "Nivel de filtrado anisotrópico. Setear en Auto para utilizar el valor solicitado por el juego.", + "AspectRatioTooltip": "Relación de aspecto aplicada a la ventana del renderizador.\n\nSolamente modificar esto si estás utilizando un mod de relación de aspecto para su juego, en cualquier otro caso los gráficos se estirarán.\n\nDejar en 16:9 si no sabe que hacer.", + "ShaderDumpPathTooltip": "Directorio en el cual se volcarán los sombreadores de los gráficos", + "FileLogTooltip": "Guarda los registros de la consola en archivos en disco. No afectan al rendimiento.", + "StubLogTooltip": "Escribe mensajes de Stub en la consola. No afectan al rendimiento.", + "InfoLogTooltip": "Escribe mensajes de Info en la consola. No afectan al rendimiento.", + "WarnLogTooltip": "Escribe mensajes de Advertencia en la consola. No afectan al rendimiento.", + "ErrorLogTooltip": "Escribe mensajes de Error en la consola. No afectan al rendimiento.", + "TraceLogTooltip": "Escribe mensajes de Rastro en la consola. No afectan al rendimiento.", + "GuestLogTooltip": "Escribe mensajes de Guest en la consola. No afectan al rendimiento.", + "FileAccessLogTooltip": "Activa mensajes de acceso a archivo en la consola", + "FSAccessLogModeTooltip": "Activa registros FS Access en la consola. Los modos posibles son entre 0 y 3", + "DeveloperOptionTooltip": "Usar con cuidado", + "OpenGlLogLevel": "Requiere activar los niveles de registro apropiados", + "DebugLogTooltip": "Escribe mensajes de debug en la consola\n\nActiva esto solo si un miembro del equipo te lo pide expresamente, pues hará que el registro sea difícil de leer y empeorará el rendimiento del emulador.", + "LoadApplicationFileTooltip": "Abre el explorador de archivos para elegir un archivo compatible con Switch para cargar", + "LoadApplicationFolderTooltip": "Abre el explorador de archivos para elegir un archivo desempaquetado y compatible con Switch para cargar", + "LoadDlcFromFolderTooltip": "Abrir un explorador de archivos para seleccionar una o más carpetas para cargar DLC de forma masiva", + "LoadTitleUpdatesFromFolderTooltip": "Abrir un explorador de archivos para seleccionar una o más carpetas para cargar actualizaciones de título de forma masiva", + "OpenRyujinxFolderTooltip": "Abre la carpeta de sistema de Ryujinx", + "OpenRyujinxLogsTooltip": "Abre la carpeta en la que se guardan los registros", + "ExitTooltip": "Cierra Ryujinx", + "OpenSettingsTooltip": "Abre la ventana de configuración", + "OpenProfileManagerTooltip": "Abre la ventana para gestionar los perfiles de usuario", + "StopEmulationTooltip": "Detiene la emulación del juego actual y regresa a la selección de juegos", + "CheckUpdatesTooltip": "Busca actualizaciones para Ryujinx", + "OpenAboutTooltip": "Abre la ventana \"Acerca de\"", + "GridSize": "Tamaño de cuadrícula", + "GridSizeTooltip": "Cambia el tamaño de los objetos en la cuadrícula", + "SettingsTabSystemSystemLanguageBrazilianPortuguese": "Portugués brasileño", + "AboutRyujinxContributorsButtonHeader": "Ver todos los contribuidores", + "SettingsTabSystemAudioVolume": "Volumen: ", + "AudioVolumeTooltip": "Ajusta el nivel de volumen", + "SettingsTabSystemEnableInternetAccess": "Conectar guest a Internet/Modo LAN", + "EnableInternetAccessTooltip": "Permite a la aplicación emulada conectarse a Internet.\n\nLos juegos que tengan modo LAN podrán conectarse entre sí habilitando esta opción y estando conectados al mismo módem. Asimismo, esto permite conexiones con consolas reales.\n\nNO permite conectar con los servidores de Nintendo Online. Puede causar que ciertos juegos crasheen al intentar conectarse a sus servidores.\n\nDesactívalo si no estás seguro.", + "GameListContextMenuManageCheatToolTip": "Activa o desactiva los cheats", + "GameListContextMenuManageCheat": "Administrar cheats", + "GameListContextMenuManageModToolTip": "Gestionar Mods", + "GameListContextMenuManageMod": "Gestionar Mods", + "ControllerSettingsStickRange": "Alcance:", + "DialogStopEmulationTitle": "Ryujinx - Detener emulación", + "DialogStopEmulationMessage": "¿Seguro que quieres detener la emulación actual?", + "SettingsTabCpu": "CPU", + "SettingsTabAudio": "Sonido", + "SettingsTabNetwork": "Red", + "SettingsTabNetworkConnection": "Conexión de red", + "SettingsTabCpuCache": "Caché de CPU", + "SettingsTabCpuMemory": "Memoria de CPU", + "DialogUpdaterFlatpakNotSupportedMessage": "Por favor, actualiza Ryujinx a través de FlatHub.", + "UpdaterDisabledWarningTitle": "¡Actualizador deshabilitado!", + "ControllerSettingsRotate90": "Rotar 90° en el sentido de las agujas del reloj", + "IconSize": "Tamaño de iconos", + "IconSizeTooltip": "Cambia el tamaño de los iconos de juegos", + "MenuBarOptionsShowConsole": "Mostrar consola", + "ShaderCachePurgeError": "Error al eliminar la caché de sombreadores en {0}: {1}", + "UserErrorNoKeys": "No se encontraron keys", + "UserErrorNoFirmware": "No se encontró firmware", + "UserErrorFirmwareParsingFailed": "Error al analizar el firmware", + "UserErrorApplicationNotFound": "No se encontró la aplicación", + "UserErrorUnknown": "Error desconocido", + "UserErrorUndefined": "Error indefinido", + "UserErrorNoKeysDescription": "Ryujinx no pudo encontrar tus 'prod.keys'.", + "UserErrorNoFirmwareDescription": "Ryujinx no pudo encontrar un firmware instalado.", + "UserErrorFirmwareParsingFailedDescription": "Ryujinx no pudo analizar el firmware. Normalmente esto ocurre debido a keys desfasadas.", + "UserErrorApplicationNotFoundDescription": "Ryujinx no pudo encontrar una aplicación válida en ese camino.", + "UserErrorUnknownDescription": "¡Ocurrió un error desconocido!", + "UserErrorUndefinedDescription": "¡Ocurrió un error indefinido! Esto no debería pasar, por favor, ¡contacta con un desarrollador!", + "OpenSetupGuideMessage": "Abrir la guía de instalación", + "NoUpdate": "No actualizado", + "TitleUpdateVersionLabel": "Versión {0} - {1}", + "TitleBundledUpdateVersionLabel": "Bundled: Version {0}", + "TitleBundledDlcLabel": "Bundled:", + "TitleXCIStatusPartialLabel": "Partial", + "TitleXCIStatusTrimmableLabel": "Untrimmed", + "TitleXCIStatusUntrimmableLabel": "Trimmed", + "TitleXCIStatusFailedLabel": "(Failed)", + "TitleXCICanSaveLabel": "Save {0:n0} Mb", + "TitleXCISavingLabel": "Saved {0:n0} Mb", + "RyujinxInfo": "Ryujinx - Info", + "RyujinxConfirm": "Ryujinx - Confirmación", + "FileDialogAllTypes": "Todos los tipos", + "Never": "Nunca", + "SwkbdMinCharacters": "Debe tener al menos {0} caracteres", + "SwkbdMinRangeCharacters": "Debe tener {0}-{1} caracteres", + "SoftwareKeyboard": "Teclado de software", + "SoftwareKeyboardModeNumeric": "Debe ser sólo 0-9 o '.'", + "SoftwareKeyboardModeAlphabet": "Solo deben ser caracteres no CJK", + "SoftwareKeyboardModeASCII": "Solo deben ser texto ASCII", + "ControllerAppletControllers": "Controladores Compatibles:", + "ControllerAppletPlayers": "Jugadores:", + "ControllerAppletDescription": "Tu configuración actual no es válida. Abre la configuración y vuelve a configurar tus entradas", + "ControllerAppletDocked": "Modo acoplado activado. El modo portátil debería estar desactivado.", + "UpdaterRenaming": "Renombrando archivos viejos...", + "UpdaterRenameFailed": "El actualizador no pudo renombrar el archivo: {0}", + "UpdaterAddingFiles": "Añadiendo nuevos archivos...", + "UpdaterExtracting": "Extrayendo actualización...", + "UpdaterDownloading": "Descargando actualización...", + "Game": "Juego", + "Docked": "Dock/TV", + "Handheld": "Portátil", + "ConnectionError": "Error de conexión.", + "AboutPageDeveloperListMore": "{0} y más...", + "ApiError": "Error de API.", + "LoadingHeading": "Cargando {0}", + "CompilingPPTC": "Compilando PTC", + "CompilingShaders": "Compilando sombreadores", + "AllKeyboards": "Todos los teclados", + "OpenFileDialogTitle": "Selecciona un archivo soportado para cargar", + "OpenFolderDialogTitle": "Selecciona una carpeta con un juego desempaquetado", + "AllSupportedFormats": "Todos los formatos soportados", + "RyujinxUpdater": "Actualizador de Ryujinx", + "SettingsTabHotkeys": "Atajos de teclado", + "SettingsTabHotkeysHotkeys": "Atajos de teclado", + "SettingsTabHotkeysToggleVsyncHotkey": "Alternar la sincronización vertical:", + "SettingsTabHotkeysScreenshotHotkey": "Captura de pantalla:", + "SettingsTabHotkeysShowUiHotkey": "Mostrar interfaz:", + "SettingsTabHotkeysPauseHotkey": "Pausar:", + "SettingsTabHotkeysToggleMuteHotkey": "Silenciar:", + "ControllerMotionTitle": "Opciones de controles de movimiento", + "ControllerRumbleTitle": "Opciones de vibración", + "SettingsSelectThemeFileDialogTitle": "Selecciona un archivo de tema", + "SettingsXamlThemeFile": "Archivo de tema Xaml", + "AvatarWindowTitle": "Administrar cuentas - Avatar", + "Amiibo": "Amiibo", + "Unknown": "Desconocido", + "Usage": "Uso", + "Writable": "Escribible", + "SelectDlcDialogTitle": "Selecciona archivo(s) de DLC", + "SelectUpdateDialogTitle": "Selecciona archivo(s) de actualización", + "SelectModDialogTitle": "Seleccionar un directorio de Mods", + "TrimXCIFileDialogTitle": "Check and Trim XCI File", + "TrimXCIFileDialogPrimaryText": "This function will first check the empty space and then trim the XCI File to save disk space.", + "TrimXCIFileDialogSecondaryText": "Current File Size: {0:n} MB\nGame Data Size: {1:n} MB\nDisk Space Savings: {2:n} MB", + "TrimXCIFileNoTrimNecessary": "XCI File does not need to be trimmed. Check logs for further details", + "TrimXCIFileNoUntrimPossible": "XCI File cannot be untrimmed. Check logs for further details", + "TrimXCIFileReadOnlyFileCannotFix": "XCI File is Read Only and could not be made writable. Check logs for further details", + "TrimXCIFileFileSizeChanged": "XCI File has changed in size since it was scanned. Please check the file is not being written to and try again.", + "TrimXCIFileFreeSpaceCheckFailed": "XCI File has data in the free space area, it is not safe to trim", + "TrimXCIFileInvalidXCIFile": "XCI File contains invalid data. Check logs for further details", + "TrimXCIFileFileIOWriteError": "XCI File could not be opened for writing. Check logs for further details", + "TrimXCIFileFailedPrimaryText": "Trimming of the XCI file failed", + "TrimXCIFileCancelled": "The operation was cancelled", + "TrimXCIFileFileUndertermined": "No operation was performed", + "UserProfileWindowTitle": "Administrar perfiles de usuario", + "CheatWindowTitle": "Administrar cheats", + "DlcWindowTitle": "Administrar contenido descargable", + "ModWindowTitle": "Administrar Mods para {0} ({1})", + "UpdateWindowTitle": "Administrar actualizaciones", + "XCITrimmerWindowTitle": "XCI File Trimmer", + "XCITrimmerTitleStatusCount": "{0} of {1} Title(s) Selected", + "XCITrimmerTitleStatusCountWithFilter": "{0} of {1} Title(s) Selected ({2} displayed)", + "XCITrimmerTitleStatusTrimming": "Trimming {0} Title(s)...", + "XCITrimmerTitleStatusUntrimming": "Untrimming {0} Title(s)...", + "XCITrimmerTitleStatusFailed": "Failed", + "XCITrimmerPotentialSavings": "Potential Savings", + "XCITrimmerActualSavings": "Actual Savings", + "XCITrimmerSavingsMb": "{0:n0} Mb", + "XCITrimmerSelectDisplayed": "Select Shown", + "XCITrimmerDeselectDisplayed": "Deselect Shown", + "XCITrimmerSortName": "Title", + "XCITrimmerSortSaved": "Space Savings", + "XCITrimmerTrim": "Trim", + "XCITrimmerUntrim": "Untrim", + "UpdateWindowUpdateAddedMessage": "{0} nueva(s) actualización(es) agregada(s)", + "UpdateWindowBundledContentNotice": "Las actualizaciones agrupadas no pueden ser eliminadas, solamente deshabilitadas.", + "CheatWindowHeading": "Cheats disponibles para {0} [{1}]", + "BuildId": "Id de compilación:", + "DlcWindowHeading": "Contenido descargable disponible para {0} [{1}]", + "DlcWindowDlcAddedMessage": "Se agregaron {0} nuevo(s) contenido(s) descargable(s)", + "AutoloadDlcAddedMessage": "Se agregaron {0} nuevo(s) contenido(s) descargable(s)", + "AutoloadDlcRemovedMessage": "Se eliminaron {0} contenido(s) descargable(s) faltantes", + "AutoloadUpdateAddedMessage": "Se agregaron {0} nueva(s) actualización(es)", + "AutoloadUpdateRemovedMessage": "Se eliminaron {0} actualización(es) faltantes", + "ModWindowHeading": "{0} Mod(s)", + "UserProfilesEditProfile": "Editar selección", + "Continue": "Continue", + "Cancel": "Cancelar", + "Save": "Guardar", + "Discard": "Descartar", + "Paused": "Pausado", + "UserProfilesSetProfileImage": "Elegir Imagen de Perfil ", + "UserProfileEmptyNameError": "El nombre es obligatorio", + "UserProfileNoImageError": "Debe establecerse la imagen de perfil", + "GameUpdateWindowHeading": "Actualizaciones disponibles para {0} [{1}]", + "SettingsTabHotkeysResScaleUpHotkey": "Aumentar la resolución:", + "SettingsTabHotkeysResScaleDownHotkey": "Disminuir la resolución:", + "UserProfilesName": "Nombre:", + "UserProfilesUserId": "Id de Usuario:", + "SettingsTabGraphicsBackend": "Fondo de gráficos", + "SettingsTabGraphicsBackendTooltip": "Seleccione el backend gráfico que utilizará el emulador.\n\nVulkan, en general, es mejor para todas las tarjetas gráficas modernas, mientras que sus controladores estén actualizados. Vulkan también cuenta con complicación más rápida de sombreadores (menos tirones) en todos los proveredores de GPU.\n\nOpenGL puede lograr mejores resultados en GPU Nvidia antiguas, GPU AMD antiguas en Linux o en GPUs con menor VRAM, aunque tirones de compilación de sombreadores serán mayores.\n\nSetear en Vulkan si no sabe que hacer. Setear en OpenGL si su GPU no tiene soporte para Vulkan aún con los últimos controladores gráficos.", + "SettingsEnableTextureRecompression": "Activar recompresión de texturas", + "SettingsEnableTextureRecompressionTooltip": "Comprimir texturas ASTC para reducir uso de VRAM.\n\nJuegos que utilizan este formato de textura incluyen Astral Chain, Bayonetta 3, Fire Emblem Engage, Metroid Prime Remastered, Super Mario Bros. Wonder y The Legend of Zelda: Tears of the Kingdom.\n\nTarjetas gráficas con 4GiB de VRAM o menos probalemente se caeran en algún momento mientras que estén corriendo estos juegos.\n\nActivar solo si está quedan sin VRAM en los juegos antes mencionados. Desactívalo si no sabes qué hacer.", + "SettingsTabGraphicsPreferredGpu": "GPU preferida", + "SettingsTabGraphicsPreferredGpuTooltip": "Selecciona la tarjeta gráfica que se utilizará con los back-end de gráficos Vulkan.\n\nNo afecta la GPU que utilizará OpenGL.\n\nFije a la GPU marcada como \"dGUP\" ante dudas. Si no hay una, no haga modificaciones.", + "SettingsAppRequiredRestartMessage": "Reinicio de Ryujinx requerido.", + "SettingsGpuBackendRestartMessage": "La configuración de la GPU o del back-end de los gráficos fue modificada. Es necesario reiniciar para que se aplique.", + "SettingsGpuBackendRestartSubMessage": "¿Quieres reiniciar ahora?", + "RyujinxUpdaterMessage": "¿Quieres actualizar Ryujinx a la última versión?", + "SettingsTabHotkeysVolumeUpHotkey": "Aumentar volumen:", + "SettingsTabHotkeysVolumeDownHotkey": "Disminuir volumen:", + "SettingsEnableMacroHLE": "Activar Macros HLE", + "SettingsEnableMacroHLETooltip": "Emulación alto-nivel del código de Macros de GPU\n\nIncrementa el rendimiento, pero puede causar errores gráficos en algunos juegos.\n\nDeja esta opción activada si no estás seguro.", + "SettingsEnableColorSpacePassthrough": "Paso de espacio de color", + "SettingsEnableColorSpacePassthroughTooltip": "Dirige el backend de Vulkan a pasar a través de la información del color sin especificar un espacio de color. Para los usuarios con pantallas de gran gama, esto puede resultar en colores más vibrantes, a costa de la corrección del color.", + "VolumeShort": "Volumen", + "UserProfilesManageSaves": "Administrar mis partidas guardadas", + "DeleteUserSave": "¿Quieres borrar los datos de usuario de este juego?", + "IrreversibleActionNote": "Esta acción no es reversible.", + "SaveManagerHeading": "Administrar partidas guardadas para {0}", + "SaveManagerTitle": "Administrador de datos de guardado.", + "Name": "Nombre", + "Size": "Tamaño", + "Search": "Buscar", + "UserProfilesRecoverLostAccounts": "Recuperar cuentas perdidas", + "Recover": "Recuperar", + "UserProfilesRecoverHeading": "Datos de guardado fueron encontrados para las siguientes cuentas", + "UserProfilesRecoverEmptyList": "No hay perfiles a recuperar", + "GraphicsAATooltip": "Aplica antia-aliasing al rendereo del juego.\n\nFXAA desenfocará la mayor parte del la iamgen, mientras que SMAA intentará encontrar bordes irregulares y suavizarlos.\n\nNo se recomienda usar en conjunto con filtro de escala FSR.\n\nEsta opción puede ser modificada mientras que esté corriendo el juego haciendo click en \"Aplicar\" más abajo; simplemente puedes mover la ventana de configuración a un lado y experimentar hasta que encuentres tu estilo preferido para un juego.\n\nDejar en NADA si no está seguro.", + "GraphicsAALabel": "Suavizado de bordes:", + "GraphicsScalingFilterLabel": "Filtro de escalado:", + "GraphicsScalingFilterTooltip": "Elija el filtro de escala que se aplicará al utilizar la escala de resolución.\n\nBilinear funciona bien para juegos 3D y es una opción predeterminada segura.\n\nSe recomienda el bilinear para juegos de pixel art.\n\nFSR 1.0 es simplemente un filtro de afilado, no se recomienda su uso con FXAA o SMAA.\n\nEsta opción se puede cambiar mientras se ejecuta un juego haciendo clic en \"Aplicar\" a continuación; simplemente puedes mover la ventana de configuración a un lado y experimentar hasta que encuentres tu estilo preferido para un juego.\n\nDéjelo en BILINEAR si no está seguro.", + "GraphicsScalingFilterBilinear": "Bilinear", + "GraphicsScalingFilterNearest": "Cercano", + "GraphicsScalingFilterFsr": "FSR", + "GraphicsScalingFilterArea": "Area", + "GraphicsScalingFilterLevelLabel": "Nivel", + "GraphicsScalingFilterLevelTooltip": "Ajuste el nivel de nitidez FSR 1.0. Mayor es más nítido.", + "SmaaLow": "SMAA Bajo", + "SmaaMedium": "SMAA Medio", + "SmaaHigh": "SMAA Alto", + "SmaaUltra": "SMAA Ultra", + "UserEditorTitle": "Editar usuario", + "UserEditorTitleCreate": "Crear Usuario", + "SettingsTabNetworkInterface": "Interfaz de Red", + "NetworkInterfaceTooltip": "Interfaz de red usada para características LAN/LDN.\n\njunto con una VPN o XLink Kai y un juego con soporte LAN, puede usarse para suplantar una conexión de la misma red a través de Internet.\n\nDeje en DEFAULT si no está seguro.", + "NetworkInterfaceDefault": "Predeterminado", + "PackagingShaders": "Empaquetando sombreadores", + "AboutChangelogButton": "Ver registro de cambios en GitHub", + "AboutChangelogButtonTooltipMessage": "Haga clic para abrir el registro de cambios para esta versión en su navegador predeterminado.", + "SettingsTabNetworkMultiplayer": "Multijugador", + "MultiplayerMode": "Modo:", + "MultiplayerModeTooltip": "Cambiar modo LDN multijugador.\n\nLdnMitm modificará la funcionalidad local de juego inalámbrico para funcionar como si fuera LAN, permitiendo locales conexiones de la misma red con otras instancias de Ryujinx y consolas hackeadas de Nintendo Switch que tienen instalado el módulo ldn_mitm.\n\nMultijugador requiere que todos los jugadores estén en la misma versión del juego (por ejemplo, Super Smash Bros. Ultimate v13.0.1 no se puede conectar a v13.0.0).\n\nDejar DESACTIVADO si no está seguro.", + "MultiplayerModeDisabled": "Deshabilitado", + "MultiplayerModeLdnMitm": "ldn_mitm", + "MultiplayerModeLdnRyu": "RyuLDN", + "MultiplayerDisableP2P": "Desactivar El Hosteo De Red P2P (puede aumentar latencia)", + "MultiplayerDisableP2PTooltip": "Desactivar el hosteo de red P2P, pares se conectarán a través del servidor maestro en lugar de conectarse directamente contigo.", + "LdnPassphrase": "Frase de contraseña de la Red:", + "LdnPassphraseTooltip": "Solo podrás ver los juegos hosteados con la misma frase de contraseña que tú.", + "LdnPassphraseInputTooltip": "Ingresar una frase de contraseña en formato Ryujinx-<8 caracteres hexadecimales>. Solamente podrás ver juegos hosteados con la misma frase de contraseña que tú.", + "LdnPassphraseInputPublic": "(público)", + "GenLdnPass": "Generar aleatorio", + "GenLdnPassTooltip": "Genera una nueva frase de contraseña, que puede ser compartida con otros jugadores.", + "ClearLdnPass": "Borrar", + "ClearLdnPassTooltip": "Borra la frase de contraseña actual, regresando a la red pública.", + "InvalidLdnPassphrase": "Frase de Contraseña Inválida! Debe ser en formato \"Ryujinx-<8 caracteres hexadecimales>\"" +} diff --git a/src/Ryujinx/Assets/Locales/fr_FR.json b/src/Ryujinx/Assets/Locales/fr_FR.json new file mode 100644 index 000000000..355c2814d --- /dev/null +++ b/src/Ryujinx/Assets/Locales/fr_FR.json @@ -0,0 +1,868 @@ +{ + "Language": "Français", + "MenuBarFileOpenApplet": "Ouvrir un programme", + "MenuBarFileOpenAppletOpenMiiApplet": "Éditeur de Mii", + "MenuBarFileOpenAppletOpenMiiAppletToolTip": "Ouvrir l'éditeur Mii en mode Standalone", + "SettingsTabInputDirectMouseAccess": "Accès direct à la souris", + "SettingsTabSystemMemoryManagerMode": "Mode de gestion de la mémoire :", + "SettingsTabSystemMemoryManagerModeSoftware": "Logiciel", + "SettingsTabSystemMemoryManagerModeHost": "Hôte (rapide)", + "SettingsTabSystemMemoryManagerModeHostUnchecked": "Hôte non vérifié (plus rapide, non sécurisé)", + "SettingsTabSystemUseHypervisor": "Utiliser l'Hyperviseur", + "MenuBarFile": "_Fichier", + "MenuBarFileOpenFromFile": "_Charger un jeu depuis un fichier", + "MenuBarFileOpenFromFileError": "Aucun jeu trouvé dans le fichier sélectionné", + "MenuBarFileOpenUnpacked": "Charger un jeu extrait", + "MenuBarFileLoadDlcFromFolder": "Charger les DLC depuis le dossier des DLC", + "MenuBarFileLoadTitleUpdatesFromFolder": "Charger les mises à jour depuis le dossier des mises à jour", + "MenuBarFileOpenEmuFolder": "Ouvrir le dossier Ryujinx", + "MenuBarFileOpenLogsFolder": "Ouvrir le dossier des journaux", + "MenuBarFileExit": "_Quitter", + "MenuBarOptions": "_Options", + "MenuBarOptionsToggleFullscreen": "Basculer en plein écran", + "MenuBarOptionsStartGamesInFullscreen": "Démarrer le jeu en plein écran", + "MenuBarOptionsStopEmulation": "Arrêter l'émulation", + "MenuBarOptionsSettings": "_Paramètres", + "MenuBarOptionsManageUserProfiles": "_Gérer les profils d'utilisateurs", + "MenuBarActions": "_Actions", + "MenuBarOptionsSimulateWakeUpMessage": "Simuler un message de réveil", + "MenuBarActionsScanAmiibo": "Scanner un Amiibo", + "MenuBarTools": "_Outils", + "MenuBarToolsInstallFirmware": "Installer un firmware", + "MenuBarFileToolsInstallFirmwareFromFile": "Installer un firmware depuis un fichier XCI ou ZIP", + "MenuBarFileToolsInstallFirmwareFromDirectory": "Installer un firmware depuis un dossier", + "MenuBarToolsManageFileTypes": "Gérer les types de fichiers", + "MenuBarToolsInstallFileTypes": "Installer les types de fichiers", + "MenuBarToolsUninstallFileTypes": "Désinstaller les types de fichiers", + "MenuBarToolsXCITrimmer": "Réduire les fichiers XCI", + "MenuBarView": "_Fenêtre", + "MenuBarViewWindow": "Taille de la fenêtre", + "MenuBarViewWindow720": "720p", + "MenuBarViewWindow1080": "1080p", + "MenuBarHelp": "_Aide", + "MenuBarHelpCheckForUpdates": "Vérifier les mises à jour", + "MenuBarHelpAbout": "À propos", + "MenuSearch": "Rechercher...", + "GameListHeaderFavorite": "Favoris", + "GameListHeaderIcon": "Icône", + "GameListHeaderApplication": "Nom", + "GameListHeaderDeveloper": "Développeur", + "GameListHeaderVersion": "Version", + "GameListHeaderTimePlayed": "Temps de jeu", + "GameListHeaderLastPlayed": "Dernière partie jouée", + "GameListHeaderFileExtension": "Extension du Fichier", + "GameListHeaderFileSize": "Taille du Fichier", + "GameListHeaderPath": "Chemin", + "GameListContextMenuOpenUserSaveDirectory": "Ouvrir le dossier de sauvegarde utilisateur", + "GameListContextMenuOpenUserSaveDirectoryToolTip": "Ouvre le dossier contenant la sauvegarde utilisateur du jeu", + "GameListContextMenuOpenDeviceSaveDirectory": "Ouvrir le dossier de sauvegarde console", + "GameListContextMenuOpenDeviceSaveDirectoryToolTip": "Ouvre le dossier contenant la sauvegarde console du jeu", + "GameListContextMenuOpenBcatSaveDirectory": "Ouvrir le dossier de sauvegarde BCAT", + "GameListContextMenuOpenBcatSaveDirectoryToolTip": "Ouvre le dossier contenant la sauvegarde BCAT du jeu", + "GameListContextMenuManageTitleUpdates": "Gérer les mises à jour", + "GameListContextMenuManageTitleUpdatesToolTip": "Ouvre la fenêtre de gestion des mises à jour du jeu", + "GameListContextMenuManageDlc": "Gérer les DLC", + "GameListContextMenuManageDlcToolTip": "Ouvre la fenêtre de gestion des DLC", + "GameListContextMenuCacheManagement": "Gestion des caches", + "GameListContextMenuCacheManagementPurgePptc": "Reconstruction du PPTC", + "GameListContextMenuCacheManagementPurgePptcToolTip": "Effectuer une reconstruction du PPTC au prochain démarrage du jeu", + "GameListContextMenuCacheManagementPurgeShaderCache": "Purger les shaders", + "GameListContextMenuCacheManagementPurgeShaderCacheToolTip": "Supprime les shaders du jeu", + "GameListContextMenuCacheManagementOpenPptcDirectory": "Ouvrir le dossier du PPTC", + "GameListContextMenuCacheManagementOpenPptcDirectoryToolTip": "Ouvre le dossier contenant le PPTC du jeu", + "GameListContextMenuCacheManagementOpenShaderCacheDirectory": "Ouvrir le dossier des shaders", + "GameListContextMenuCacheManagementOpenShaderCacheDirectoryToolTip": "Ouvre le dossier contenant les shaders du jeu", + "GameListContextMenuExtractData": "Extraire les données", + "GameListContextMenuExtractDataExeFS": "ExeFS", + "GameListContextMenuExtractDataExeFSToolTip": "Extrait la section ExeFS du jeu (mise à jour incluse)", + "GameListContextMenuExtractDataRomFS": "RomFS", + "GameListContextMenuExtractDataRomFSToolTip": "Extrait la section RomFS du jeu (mise à jour incluse)", + "GameListContextMenuExtractDataLogo": "Logo", + "GameListContextMenuExtractDataLogoToolTip": "Extrait la section Logo du jeu (mise à jour incluse)", + "GameListContextMenuCreateShortcut": "Créer un raccourci", + "GameListContextMenuCreateShortcutToolTip": "Créer un raccourci sur le bureau qui lance le jeu sélectionné", + "GameListContextMenuCreateShortcutToolTipMacOS": "Créer un raccourci dans le dossier Applications de macOS qui lance le jeu sélectionné", + "GameListContextMenuOpenModsDirectory": "Ouvrir le dossier des mods", + "GameListContextMenuOpenModsDirectoryToolTip": "Ouvre le dossier contenant les mods du jeu", + "GameListContextMenuOpenSdModsDirectory": "Ouvrir le dossier des mods Atmosphère", + "GameListContextMenuOpenSdModsDirectoryToolTip": "Ouvre le dossier alternatif de la carte SD Atmosphère qui contient les mods de l'application. Utile pour les mods conçus pour console.", + "GameListContextMenuTrimXCI": "Vérifier et réduire les fichiers XCI", + "GameListContextMenuTrimXCIToolTip": "Vérifier et réduire les fichiers XCI pour économiser de l'espace", + "StatusBarGamesLoaded": "{0}/{1} Jeux chargés", + "StatusBarSystemVersion": "Version du Firmware: {0}", + "StatusBarXCIFileTrimming": "Réduction du fichier XCI '{0}'", + "LinuxVmMaxMapCountDialogTitle": "Limite basse pour les mappings mémoire détectée", + "LinuxVmMaxMapCountDialogTextPrimary": "Voulez-vous augmenter la valeur de vm.max_map_count à {0}", + "LinuxVmMaxMapCountDialogTextSecondary": "Certains jeux peuvent essayer de créer plus de mappings mémoire que ce qui est actuellement autorisé. Ryujinx plantera dès que cette limite sera dépassée.", + "LinuxVmMaxMapCountDialogButtonUntilRestart": "Oui, jusqu'au prochain redémarrage", + "LinuxVmMaxMapCountDialogButtonPersistent": "Oui, en permanence", + "LinuxVmMaxMapCountWarningTextPrimary": "La quantité maximale de mappings mémoire est inférieure à la valeur recommandée.", + "LinuxVmMaxMapCountWarningTextSecondary": "La valeur actuelle de vm.max_map_count ({0}) est inférieure à {1}. Certains jeux peuvent essayer de créer plus de mappings mémoire que ceux actuellement autorisés. Ryujinx plantera dès que cette limite sera dépassée.\n\nVous pouvez soit augmenter manuellement la limite, soit installer pkexec, ce qui permet à Ryujinx de l'aider.", + "Settings": "Paramètres", + "SettingsTabGeneral": "Interface Utilisateur", + "SettingsTabGeneralGeneral": "Général", + "SettingsTabGeneralEnableDiscordRichPresence": "Activer Discord Rich Presence", + "SettingsTabGeneralCheckUpdatesOnLaunch": "Vérifier les mises à jour au démarrage", + "SettingsTabGeneralShowConfirmExitDialog": "Afficher le message de \"Confirmation de sortie\"", + "SettingsTabGeneralRememberWindowState": "Mémoriser la taille/position de la fenêtre", + "SettingsTabGeneralShowTitleBar": "Afficher Barre de Titre (Nécessite redémarrage)", + "SettingsTabGeneralHideCursor": "Masquer le Curseur :", + "SettingsTabGeneralHideCursorNever": "Jamais", + "SettingsTabGeneralHideCursorOnIdle": "Masquer le curseur si inactif", + "SettingsTabGeneralHideCursorAlways": "Toujours", + "SettingsTabGeneralGameDirectories": "Dossiers des jeux", + "SettingsTabGeneralAutoloadDirectories": "Dossiers des mises à jour/DLC", + "SettingsTabGeneralAutoloadNote": "Les DLC et les mises à jour faisant référence aux fichiers manquants seront automatiquement déchargés.", + "SettingsTabGeneralAdd": "Ajouter", + "SettingsTabGeneralRemove": "Retirer", + "SettingsTabSystem": "Système", + "SettingsTabSystemCore": "Cœur", + "SettingsTabSystemSystemRegion": "Région du système :", + "SettingsTabSystemSystemRegionJapan": "Japon", + "SettingsTabSystemSystemRegionUSA": "USA", + "SettingsTabSystemSystemRegionEurope": "Europe", + "SettingsTabSystemSystemRegionAustralia": "Australie", + "SettingsTabSystemSystemRegionChina": "Chine", + "SettingsTabSystemSystemRegionKorea": "Corée", + "SettingsTabSystemSystemRegionTaiwan": "Taïwan", + "SettingsTabSystemSystemLanguage": "Langue du système :", + "SettingsTabSystemSystemLanguageJapanese": "Japonais", + "SettingsTabSystemSystemLanguageAmericanEnglish": "Anglais Américain", + "SettingsTabSystemSystemLanguageFrench": "Français", + "SettingsTabSystemSystemLanguageGerman": "Allemand", + "SettingsTabSystemSystemLanguageItalian": "Italien", + "SettingsTabSystemSystemLanguageSpanish": "Espagnol", + "SettingsTabSystemSystemLanguageChinese": "Chinois", + "SettingsTabSystemSystemLanguageKorean": "Coréen", + "SettingsTabSystemSystemLanguageDutch": "Néerlandais", + "SettingsTabSystemSystemLanguagePortuguese": "Portugais", + "SettingsTabSystemSystemLanguageRussian": "Russe", + "SettingsTabSystemSystemLanguageTaiwanese": "Taïwanais", + "SettingsTabSystemSystemLanguageBritishEnglish": "Anglais Britannique ", + "SettingsTabSystemSystemLanguageCanadianFrench": "Français Canadien", + "SettingsTabSystemSystemLanguageLatinAmericanSpanish": "Espagnol Latino-Américain", + "SettingsTabSystemSystemLanguageSimplifiedChinese": "Chinois simplifié", + "SettingsTabSystemSystemLanguageTraditionalChinese": "Chinois traditionnel", + "SettingsTabSystemSystemTimeZone": "Fuseau horaire du système :", + "SettingsTabSystemSystemTime": "Heure du système :", + "SettingsTabSystemEnableVsync": "Synchronisation verticale (VSync)", + "SettingsTabSystemEnablePptc": "Activer le PPTC (Profiled Persistent Translation Cache)", + "SettingsTabSystemEnableLowPowerPptc": "PPTC à faible puissance", + "SettingsTabSystemEnableFsIntegrityChecks": "Activer la vérification de l'intégrité du système de fichiers", + "SettingsTabSystemAudioBackend": "Bibliothèque Audio :", + "SettingsTabSystemAudioBackendDummy": "Désactivée", + "SettingsTabSystemAudioBackendOpenAL": "OpenAL", + "SettingsTabSystemAudioBackendSoundIO": "SoundIO", + "SettingsTabSystemAudioBackendSDL2": "SDL2", + "SettingsTabSystemHacks": "Hacks", + "SettingsTabSystemHacksNote": "Cela peut causer des instabilités", + "SettingsTabSystemDramSize": "Taille de la DRAM :", + "SettingsTabSystemDramSize4GiB": "4GiO", + "SettingsTabSystemDramSize6GiB": "6GiO", + "SettingsTabSystemDramSize8GiB": "8GiO", + "SettingsTabSystemDramSize12GiB": "12GiO", + "SettingsTabSystemIgnoreMissingServices": "Ignorer les services manquants", + "SettingsTabSystemIgnoreApplet": "Ignorer la déconnexion de la manette", + "SettingsTabGraphics": "Graphismes", + "SettingsTabGraphicsAPI": "API Graphique", + "SettingsTabGraphicsEnableShaderCache": "Activer le cache des shaders", + "SettingsTabGraphicsAnisotropicFiltering": "Filtrage anisotrope :", + "SettingsTabGraphicsAnisotropicFilteringAuto": "Auto", + "SettingsTabGraphicsAnisotropicFiltering2x": "x2", + "SettingsTabGraphicsAnisotropicFiltering4x": "x4", + "SettingsTabGraphicsAnisotropicFiltering8x": "x8", + "SettingsTabGraphicsAnisotropicFiltering16x": "x16", + "SettingsTabGraphicsResolutionScale": "Échelle de résolution :", + "SettingsTabGraphicsResolutionScaleCustom": "Personnalisée (Non recommandée)", + "SettingsTabGraphicsResolutionScaleNative": "Natif (720p/1080p)", + "SettingsTabGraphicsResolutionScale2x": "x2 (1440p/2160p)", + "SettingsTabGraphicsResolutionScale3x": "x3 (2160p/3240p)", + "SettingsTabGraphicsResolutionScale4x": "4x (2880p/4320p) (Non recommandé)", + "SettingsTabGraphicsAspectRatio": "Format d'affichage :", + "SettingsTabGraphicsAspectRatio4x3": "4:3", + "SettingsTabGraphicsAspectRatio16x9": "16:9", + "SettingsTabGraphicsAspectRatio16x10": "16:10", + "SettingsTabGraphicsAspectRatio21x9": "21:9", + "SettingsTabGraphicsAspectRatio32x9": "32:9", + "SettingsTabGraphicsAspectRatioStretch": "Étirer pour remplir la fenêtre", + "SettingsTabGraphicsDeveloperOptions": "Options développeur", + "SettingsTabGraphicsShaderDumpPath": "Chemin du dossier de copie des shaders :", + "SettingsTabLogging": "Journaux", + "SettingsTabLoggingLogging": "Journaux", + "SettingsTabLoggingEnableLoggingToFile": "Activer la sauvegarde des journaux vers un fichier", + "SettingsTabLoggingEnableStubLogs": "Activer les journaux stub", + "SettingsTabLoggingEnableInfoLogs": "Activer les journaux d'informations", + "SettingsTabLoggingEnableWarningLogs": "Activer les journaux d'avertissements", + "SettingsTabLoggingEnableErrorLogs": "Activer les journaux d'erreurs", + "SettingsTabLoggingEnableTraceLogs": "Activer les journaux d'erreurs Trace", + "SettingsTabLoggingEnableGuestLogs": "Activer les journaux du programme simulé", + "SettingsTabLoggingEnableFsAccessLogs": "Activer les journaux d'accès au système de fichiers", + "SettingsTabLoggingFsGlobalAccessLogMode": "Niveau des journaux d'accès au système de fichiers :", + "SettingsTabLoggingDeveloperOptions": "Options développeur", + "SettingsTabLoggingDeveloperOptionsNote": "ATTENTION : Réduira les performances", + "SettingsTabLoggingGraphicsBackendLogLevel": "Niveau du journal du backend graphique :", + "SettingsTabLoggingGraphicsBackendLogLevelNone": "Aucun", + "SettingsTabLoggingGraphicsBackendLogLevelError": "Erreur", + "SettingsTabLoggingGraphicsBackendLogLevelPerformance": "Ralentissements", + "SettingsTabLoggingGraphicsBackendLogLevelAll": "Tout", + "SettingsTabLoggingEnableDebugLogs": "Activer les journaux de debug", + "SettingsTabInput": "Contrôles", + "SettingsTabInputEnableDockedMode": "Active le mode station d'accueil", + "SettingsTabInputDirectKeyboardAccess": "Accès direct au clavier", + "SettingsButtonSave": "Enregistrer", + "SettingsButtonClose": "Fermer", + "SettingsButtonOk": "OK", + "SettingsButtonCancel": "Annuler", + "SettingsButtonApply": "Appliquer", + "ControllerSettingsPlayer": "Joueur", + "ControllerSettingsPlayer1": "Joueur 1", + "ControllerSettingsPlayer2": "Joueur 2", + "ControllerSettingsPlayer3": "Joueur 3", + "ControllerSettingsPlayer4": "Joueur 4", + "ControllerSettingsPlayer5": "Joueur 5", + "ControllerSettingsPlayer6": "Joueur 6", + "ControllerSettingsPlayer7": "Joueur 7", + "ControllerSettingsPlayer8": "Joueur 8", + "ControllerSettingsHandheld": "Portable", + "ControllerSettingsInputDevice": "Périphériques", + "ControllerSettingsRefresh": "Actualiser", + "ControllerSettingsDeviceDisabled": "Désactivé", + "ControllerSettingsControllerType": "Type de manette", + "ControllerSettingsControllerTypeHandheld": "Portable", + "ControllerSettingsControllerTypeProController": "Manette Switch Pro", + "ControllerSettingsControllerTypeJoyConPair": "JoyCon Joints", + "ControllerSettingsControllerTypeJoyConLeft": "JoyCon Gauche", + "ControllerSettingsControllerTypeJoyConRight": "JoyCon Droite", + "ControllerSettingsProfile": "Profil", + "ControllerSettingsProfileDefault": "Défaut", + "ControllerSettingsLoad": "Charger", + "ControllerSettingsAdd": "Ajouter", + "ControllerSettingsRemove": "Supprimer", + "ControllerSettingsButtons": "Boutons", + "ControllerSettingsButtonA": "A", + "ControllerSettingsButtonB": "B", + "ControllerSettingsButtonX": "X", + "ControllerSettingsButtonY": "Y", + "ControllerSettingsButtonPlus": "+", + "ControllerSettingsButtonMinus": "-", + "ControllerSettingsDPad": "Croix directionnelle", + "ControllerSettingsDPadUp": "Haut", + "ControllerSettingsDPadDown": "Bas", + "ControllerSettingsDPadLeft": "Gauche", + "ControllerSettingsDPadRight": "Droite", + "ControllerSettingsStickButton": "Bouton", + "ControllerSettingsStickUp": "Haut", + "ControllerSettingsStickDown": "Bas", + "ControllerSettingsStickLeft": "Gauche", + "ControllerSettingsStickRight": "Droite", + "ControllerSettingsStickStick": "Joystick", + "ControllerSettingsStickInvertXAxis": "Inverser l'axe X", + "ControllerSettingsStickInvertYAxis": "Inverser l'axe Y", + "ControllerSettingsStickDeadzone": "Zone morte :", + "ControllerSettingsLStick": "Joystick Gauche", + "ControllerSettingsRStick": "Joystick Droit", + "ControllerSettingsTriggersLeft": "Gachettes Gauche", + "ControllerSettingsTriggersRight": "Gachettes Droite", + "ControllerSettingsTriggersButtonsLeft": "Boutons Gachettes Gauche", + "ControllerSettingsTriggersButtonsRight": "Boutons Gachettes Droite", + "ControllerSettingsTriggers": "Gachettes", + "ControllerSettingsTriggerL": "L", + "ControllerSettingsTriggerR": "R", + "ControllerSettingsTriggerZL": "ZL", + "ControllerSettingsTriggerZR": "ZR", + "ControllerSettingsLeftSL": "SL", + "ControllerSettingsLeftSR": "SR", + "ControllerSettingsRightSL": "SL", + "ControllerSettingsRightSR": "SR", + "ControllerSettingsExtraButtonsLeft": "Boutons Gauche", + "ControllerSettingsExtraButtonsRight": "Boutons Droite", + "ControllerSettingsMisc": "Divers", + "ControllerSettingsTriggerThreshold": "Seuil de gachettes :", + "ControllerSettingsMotion": "Mouvements", + "ControllerSettingsMotionUseCemuhookCompatibleMotion": "Utiliser un capteur de mouvements CemuHook", + "ControllerSettingsMotionControllerSlot": "Contrôleur ID :", + "ControllerSettingsMotionMirrorInput": "Inverser les contrôles", + "ControllerSettingsMotionRightJoyConSlot": "JoyCon Droit ID :", + "ControllerSettingsMotionServerHost": "Serveur d'hébergement :", + "ControllerSettingsMotionGyroSensitivity": "Sensibilitée du gyroscope :", + "ControllerSettingsMotionGyroDeadzone": "Zone morte du gyroscope :", + "ControllerSettingsSave": "Enregistrer", + "ControllerSettingsClose": "Fermer", + "KeyUnknown": "Touche inconnue", + "KeyShiftLeft": "Maj Gauche", + "KeyShiftRight": "Maj Droite", + "KeyControlLeft": "Ctrl Gauche", + "KeyMacControlLeft": "⌃ Gauche", + "KeyControlRight": "Ctrl Droite", + "KeyMacControlRight": "⌃ Droite", + "KeyAltLeft": "Alt Gauche", + "KeyMacAltLeft": "⌥ Gauche", + "KeyAltRight": "Alt Droite", + "KeyMacAltRight": "⌥ Droite", + "KeyWinLeft": "⊞ Gauche", + "KeyMacWinLeft": "⌘ Gauche", + "KeyWinRight": "⊞ Droite", + "KeyMacWinRight": "⌘ Droite", + "KeyMenu": "Menu", + "KeyUp": "Haut", + "KeyDown": "Bas", + "KeyLeft": "Gauche", + "KeyRight": "Droite", + "KeyEnter": "Entrée", + "KeyEscape": "Esc", + "KeySpace": "Espace", + "KeyTab": "Tab", + "KeyBackSpace": "Supprimer", + "KeyInsert": "Ins", + "KeyDelete": "Sup", + "KeyPageUp": "Page Up", + "KeyPageDown": "Page Down", + "KeyHome": "Home", + "KeyEnd": "Fin", + "KeyCapsLock": "Verr. Maj", + "KeyScrollLock": "Arr. Déf.", + "KeyPrintScreen": "Imp. Écran", + "KeyPause": "Pause", + "KeyNumLock": "Verr. Num", + "KeyClear": "Clear", + "KeyKeypad0": "Num. 0", + "KeyKeypad1": "Num. 1", + "KeyKeypad2": "Num. 2", + "KeyKeypad3": "Num. 3", + "KeyKeypad4": "Num. 4", + "KeyKeypad5": "Num. 5", + "KeyKeypad6": "Num. 6", + "KeyKeypad7": "Num. 7", + "KeyKeypad8": "Num. 8", + "KeyKeypad9": "Num. 9", + "KeyKeypadDivide": "Num. Diviser", + "KeyKeypadMultiply": "Num. Multiplier", + "KeyKeypadSubtract": "Num. Soustraire", + "KeyKeypadAdd": "Num. Ajouter", + "KeyKeypadDecimal": "Num. Point", + "KeyKeypadEnter": "Num. Ent", + "KeyNumber0": "0", + "KeyNumber1": "1", + "KeyNumber2": "2", + "KeyNumber3": "3", + "KeyNumber4": "4", + "KeyNumber5": "5", + "KeyNumber6": "6", + "KeyNumber7": "7", + "KeyNumber8": "8", + "KeyNumber9": "9", + "KeyTilde": "~", + "KeyGrave": "`", + "KeyMinus": "-", + "KeyPlus": "+", + "KeyBracketLeft": "[", + "KeyBracketRight": "]", + "KeySemicolon": ";", + "KeyQuote": "\"", + "KeyComma": ",", + "KeyPeriod": ".", + "KeySlash": "/", + "KeyBackSlash": "\\", + "KeyUnbound": "Pas Attribuée", + "GamepadLeftStick": "Bouton Joystick G.", + "GamepadRightStick": "Bouton Joystick D.", + "GamepadLeftShoulder": "Bouton Gachette G.", + "GamepadRightShoulder": "Bouton Gachette D.", + "GamepadLeftTrigger": "Gachette Gauche", + "GamepadRightTrigger": "Gachette Droite", + "GamepadDpadUp": "Haut", + "GamepadDpadDown": "Bas", + "GamepadDpadLeft": "Gauche", + "GamepadDpadRight": "Droite", + "GamepadMinus": "-", + "GamepadPlus": "+", + "GamepadGuide": "Guide", + "GamepadMisc1": "Autre", + "GamepadPaddle1": "Palette 1", + "GamepadPaddle2": "Palette 2", + "GamepadPaddle3": "Palette 3", + "GamepadPaddle4": "Palette 4", + "GamepadTouchpad": "Touchpad", + "GamepadSingleLeftTrigger0": "Gachette Gauche 0", + "GamepadSingleRightTrigger0": "Gachette Droite 0", + "GamepadSingleLeftTrigger1": "Gachette Gauche 1", + "GamepadSingleRightTrigger1": "Gachette Droite 1", + "StickLeft": "Joystick Gauche", + "StickRight": "Joystick Droite", + "UserProfilesSelectedUserProfile": "Profil utilisateur sélectionné :", + "UserProfilesSaveProfileName": "Enregistrer le nom du profil", + "UserProfilesChangeProfileImage": "Changer l'image du profil", + "UserProfilesAvailableUserProfiles": "Profils utilisateurs disponibles :", + "UserProfilesAddNewProfile": "Créer un profil", + "UserProfilesDelete": "Supprimer", + "UserProfilesClose": "Fermer", + "ProfileNameSelectionWatermark": "Choisir un pseudo", + "ProfileImageSelectionTitle": "Sélection de l'image du profil", + "ProfileImageSelectionHeader": "Choisir l'image du profil", + "ProfileImageSelectionNote": "Vous pouvez importer une image de profil personnalisée ou sélectionner un avatar à partir du firmware", + "ProfileImageSelectionImportImage": "Importer une image", + "ProfileImageSelectionSelectAvatar": "Choisir un avatar du firmware", + "InputDialogTitle": "Fenêtre d'entrée de texte", + "InputDialogOk": "OK", + "InputDialogCancel": "Annuler", + "InputDialogCancelling": "Annulation en cours", + "InputDialogClose": "Fermer", + "InputDialogAddNewProfileTitle": "Choisir un nom de profil", + "InputDialogAddNewProfileHeader": "Merci d'entrer un nom de profil", + "InputDialogAddNewProfileSubtext": "(Longueur max.: {0})", + "AvatarChoose": "Choisir un avatar", + "AvatarSetBackgroundColor": "Choisir une couleur de fond", + "AvatarClose": "Fermer", + "ControllerSettingsLoadProfileToolTip": "Charger un profil", + "ControllerSettingsAddProfileToolTip": "Ajouter un profil", + "ControllerSettingsViewProfileToolTip": "View Profile", + "ControllerSettingsRemoveProfileToolTip": "Supprimer un profil", + "ControllerSettingsSaveProfileToolTip": "Enregistrer un profil", + "MenuBarFileToolsTakeScreenshot": "Prendre une capture d'écran", + "MenuBarFileToolsHideUi": "Masquer l'interface utilisateur", + "GameListContextMenuRunApplication": "Démarrer l'application", + "GameListContextMenuToggleFavorite": "Ajouter/Retirer des favoris", + "GameListContextMenuToggleFavoriteToolTip": "Définis un jeu comme faisant parti des favoris ou non", + "SettingsTabGeneralTheme": "Thème :", + "SettingsTabGeneralThemeAuto": "Auto", + "SettingsTabGeneralThemeDark": "Sombre", + "SettingsTabGeneralThemeLight": "Clair", + "ControllerSettingsConfigureGeneral": "Configurer", + "ControllerSettingsRumble": "Vibreur", + "ControllerSettingsRumbleStrongMultiplier": "Multiplicateur de vibrations fortes", + "ControllerSettingsRumbleWeakMultiplier": "Multiplicateur de vibrations faibles", + "DialogMessageSaveNotAvailableMessage": "Il n'y a aucune sauvegarde pour {0} [{1:x16}]", + "DialogMessageSaveNotAvailableCreateSaveMessage": "Voulez-vous créer une sauvegarde pour ce jeu ?", + "DialogConfirmationTitle": "Ryujinx - Confirmation", + "DialogUpdaterTitle": "Ryujinx - Mise à Jour", + "DialogErrorTitle": "Ryujinx - Erreur", + "DialogWarningTitle": "Ryujinx - Avertissement", + "DialogExitTitle": "Ryujinx - Quitter", + "DialogErrorMessage": "Ryujinx a rencontré une erreur", + "DialogExitMessage": "Êtes-vous sûr de vouloir fermer Ryujinx ?", + "DialogExitSubMessage": "Toutes les données non enregistrées seront perdues !", + "DialogMessageCreateSaveErrorMessage": "Une erreur s'est produite lors de la création de la sauvegarde spécifiée : {0}", + "DialogMessageFindSaveErrorMessage": "Une erreur s'est produite lors de la recherche de la sauvegarde spécifiée : {0}", + "FolderDialogExtractTitle": "Choisissez le dossier dans lequel extraire", + "DialogNcaExtractionMessage": "Extraction de la section {0} depuis {1}...", + "DialogNcaExtractionTitle": "Extracteur de la section NCA", + "DialogNcaExtractionMainNcaNotFoundErrorMessage": "Échec de l'extraction. Le NCA principal n'était pas présent dans le fichier sélectionné.", + "DialogNcaExtractionCheckLogErrorMessage": "Échec de l'extraction. Lisez le fichier journal pour plus d'informations.", + "DialogNcaExtractionSuccessMessage": "Extraction terminée avec succès.", + "DialogUpdaterConvertFailedMessage": "Échec de la conversion de la version actuelle de Ryujinx.", + "DialogUpdaterCancelUpdateMessage": "Annuler la mise à jour !", + "DialogUpdaterAlreadyOnLatestVersionMessage": "Vous utilisez déjà la version la plus récente de Ryujinx !", + "DialogUpdaterFailedToGetVersionMessage": "Une erreur s'est produite lors de la tentative d'obtention des informations de publication de la version GitHub. Cela peut survenir lorsqu'une nouvelle version est en cours de compilation par GitHub Actions. Réessayez dans quelques minutes.", + "DialogUpdaterConvertFailedGithubMessage": "Impossible de convertir la version reçue de Ryujinx depuis Github Release.", + "DialogUpdaterDownloadingMessage": "Téléchargement de la mise à jour...", + "DialogUpdaterExtractionMessage": "Extraction de la mise à jour…", + "DialogUpdaterRenamingMessage": "Renommage de la mise à jour...", + "DialogUpdaterAddingFilesMessage": "Ajout d'une nouvelle mise à jour...", + "DialogUpdaterShowChangelogMessage": "Show Changelog", + "DialogUpdaterCompleteMessage": "Mise à jour terminée !", + "DialogUpdaterRestartMessage": "Voulez-vous redémarrer Ryujinx maintenant ?", + "DialogUpdaterNoInternetMessage": "Vous n'êtes pas connecté à Internet !", + "DialogUpdaterNoInternetSubMessage": "Veuillez vérifier que vous disposez d'une connexion Internet fonctionnelle !", + "DialogUpdaterDirtyBuildMessage": "Vous ne pouvez pas mettre à jour une version Dirty de Ryujinx !", + "DialogUpdaterDirtyBuildSubMessage": "Veuillez télécharger Ryujinx sur https://ryujinx.app/download si vous recherchez une version prise en charge.", + "DialogRestartRequiredMessage": "Redémarrage requis", + "DialogThemeRestartMessage": "Le thème a été enregistré. Un redémarrage est requis pour appliquer le thème.", + "DialogThemeRestartSubMessage": "Voulez-vous redémarrer", + "DialogFirmwareInstallEmbeddedMessage": "Voulez-vous installer le firmware intégré dans ce jeu ? (Firmware {0})", + "DialogFirmwareInstallEmbeddedSuccessMessage": "Aucun firmware installé n'a été trouvé mais Ryujinx a pu installer le firmware {0} à partir du jeu fourni.\nL'émulateur va maintenant démarrer.", + "DialogFirmwareNoFirmwareInstalledMessage": "Aucun Firmware installé", + "DialogFirmwareInstalledMessage": "Le firmware {0} a été installé", + "DialogInstallFileTypesSuccessMessage": "Types de fichiers installés avec succès!", + "DialogInstallFileTypesErrorMessage": "Échec de l'installation des types de fichiers.", + "DialogUninstallFileTypesSuccessMessage": "Types de fichiers désinstallés avec succès!", + "DialogUninstallFileTypesErrorMessage": "Échec de la désinstallation des types de fichiers.", + "DialogOpenSettingsWindowLabel": "Ouvrir la fenêtre de configuration", + "DialogOpenXCITrimmerWindowLabel": "Fenêtre de réduction de fichiers XCI", + "DialogControllerAppletTitle": "Programme Manette", + "DialogMessageDialogErrorExceptionMessage": "Erreur lors de l'affichage de la boîte de dialogue : {0}", + "DialogSoftwareKeyboardErrorExceptionMessage": "Erreur lors de l'affichage du clavier logiciel: {0}", + "DialogErrorAppletErrorExceptionMessage": "Erreur lors de l'affichage de la boîte de dialogue ErrorApplet: {0}", + "DialogUserErrorDialogMessage": "{0}: {1}", + "DialogUserErrorDialogInfoMessage": "\nPour plus d'informations sur la manière de corriger cette erreur, suivez notre Guide d'Installation.", + "DialogUserErrorDialogTitle": "Erreur Ryujinx ({0})", + "DialogAmiiboApiTitle": "API Amiibo", + "DialogAmiiboApiFailFetchMessage": "Une erreur est survenue lors de la récupération des informations de l'API.", + "DialogAmiiboApiConnectErrorMessage": "Impossible de se connecter au serveur API Amiibo. Le service est peut-être hors service ou vous devriez peut-être vérifier que votre connexion internet est connectée.", + "DialogProfileInvalidProfileErrorMessage": "Le profil {0} est incompatible avec le système de configuration de manette actuel.", + "DialogProfileDefaultProfileOverwriteErrorMessage": "Le profil par défaut ne peut pas être écrasé", + "DialogProfileDeleteProfileTitle": "Supprimer le profil", + "DialogProfileDeleteProfileMessage": "Cette action est irréversible, êtes-vous sûr de vouloir continuer ?", + "DialogWarning": "Avertissement", + "DialogPPTCDeletionMessage": "Vous êtes sur le point de mettre en file d'attente une reconstruction PPTC au prochain démarrage de :\n\n{0}\n\nÊtes-vous sûr de vouloir continuer ?", + "DialogPPTCDeletionErrorMessage": "Erreur lors de la purge du cache PPTC à {0}: {1}", + "DialogShaderDeletionMessage": "Vous êtes sur le point de supprimer le cache du Shader pour :\n\n{0}\n\nÊtes-vous sûr de vouloir continuer ?", + "DialogShaderDeletionErrorMessage": "Erreur lors de la purge du cache du Shader à {0}: {1}", + "DialogRyujinxErrorMessage": "Ryujinx a rencontré une erreur", + "DialogInvalidTitleIdErrorMessage": "Erreur d'UI : le jeu sélectionné n'a pas d'ID de titre valide", + "DialogFirmwareInstallerFirmwareNotFoundErrorMessage": "Un firmware valide n'a pas été trouvé dans {0}.", + "DialogFirmwareInstallerFirmwareInstallTitle": "Installer le Firmware {0}", + "DialogFirmwareInstallerFirmwareInstallMessage": "La version {0} du système sera installée.", + "DialogFirmwareInstallerFirmwareInstallSubMessage": "\n\nCela remplacera la version actuelle du système {0}.", + "DialogFirmwareInstallerFirmwareInstallConfirmMessage": "\n\nVoulez-vous continuer ?", + "DialogFirmwareInstallerFirmwareInstallWaitMessage": "Installation du firmware...", + "DialogFirmwareInstallerFirmwareInstallSuccessMessage": "Version du système {0} installée avec succès.", + "DialogUserProfileDeletionWarningMessage": "Il n'y aurait aucun autre profil à ouvrir si le profil sélectionné est supprimé", + "DialogUserProfileDeletionConfirmMessage": "Voulez-vous supprimer le profil sélectionné ?", + "DialogUserProfileUnsavedChangesTitle": "Avertissement - Modifications non enregistrées", + "DialogUserProfileUnsavedChangesMessage": "Vous avez effectué des modifications sur ce profil d'utilisateur qui n'ont pas été enregistrées.", + "DialogUserProfileUnsavedChangesSubMessage": "Voulez-vous annuler les modifications ?", + "DialogControllerSettingsModifiedConfirmMessage": "Les paramètres actuels de la manette ont été mis à jour.", + "DialogControllerSettingsModifiedConfirmSubMessage": "Voulez-vous sauvegarder ?", + "DialogLoadFileErrorMessage": "{0}. Fichier erroné : {1}", + "DialogModAlreadyExistsMessage": "Le mod existe déjà", + "DialogModInvalidMessage": "Le répertoire spécifié ne contient pas de mod !", + "DialogModDeleteNoParentMessage": "Impossible de supprimer : impossible de trouver le répertoire parent pour le mod \"{0} \" !", + "DialogDlcNoDlcErrorMessage": "Le fichier spécifié ne contient pas de DLC pour le titre sélectionné !", + "DialogPerformanceCheckLoggingEnabledMessage": "Vous avez activé la journalisation des traces, conçue pour être utilisée uniquement par les développeurs.", + "DialogPerformanceCheckLoggingEnabledConfirmMessage": "Pour des performances optimales, il est recommandé de désactiver la journalisation des traces. Souhaitez-vous désactiver la journalisation des traces maintenant ?", + "DialogPerformanceCheckShaderDumpEnabledMessage": "Vous avez activé l'extraction des shaders, qui est conçu pour être utilisé par les développeurs uniquement.", + "DialogPerformanceCheckShaderDumpEnabledConfirmMessage": "Pour des performances optimales, il est recommandé de désactiver l'extraction des shaders. Souhaitez-vous désactiver l'extraction des shaders maintenant ?", + "DialogLoadAppGameAlreadyLoadedMessage": "Un jeu a déjà été chargé", + "DialogLoadAppGameAlreadyLoadedSubMessage": "Veuillez arrêter l'émulation ou fermer l'émulateur avant de lancer un autre jeu.", + "DialogUpdateAddUpdateErrorMessage": "Le fichier spécifié ne contient pas de mise à jour pour le titre sélectionné !", + "DialogSettingsBackendThreadingWarningTitle": "Avertissement - Backend Threading ", + "DialogSettingsBackendThreadingWarningMessage": "Ryujinx doit être redémarré après avoir changé cette option pour qu'elle s'applique complètement. Selon votre plate-forme, vous devrez peut-être désactiver manuellement le multithreading de votre pilote lorsque vous utilisez Ryujinx.", + "DialogModManagerDeletionWarningMessage": "Vous êtes sur le point de supprimer le mod : {0}\n\nÊtes-vous sûr de vouloir continuer ?", + "DialogModManagerDeletionAllWarningMessage": "Vous êtes sur le point de supprimer tous les mods pour ce titre.\n\nÊtes-vous sûr de vouloir continuer ?", + "SettingsTabGraphicsFeaturesOptions": "Fonctionnalités", + "SettingsTabGraphicsBackendMultithreading": "Interface graphique multithread :", + "CommonAuto": "Auto", + "CommonOff": "Désactivé", + "CommonOn": "Activé", + "InputDialogYes": "Oui", + "InputDialogNo": "Non", + "DialogProfileInvalidProfileNameErrorMessage": "Le nom du fichier contient des caractères invalides. Veuillez réessayer.", + "MenuBarOptionsPauseEmulation": "Suspendre", + "MenuBarOptionsResumeEmulation": "Reprendre", + "AboutUrlTooltipMessage": "Cliquez pour ouvrir le site de Ryujinx dans votre navigateur par défaut.", + "AboutDisclaimerMessage": "Ryujinx n'est pas affilié à Nintendo™,\nou à aucun de ses partenaires, de quelque manière que ce soit.", + "AboutAmiiboDisclaimerMessage": "AmiiboAPI (www.amiiboapi.com) est utilisé\ndans notre émulation Amiibo.", + "AboutPatreonUrlTooltipMessage": "Cliquez pour ouvrir la page Patreon de Ryujinx dans votre navigateur par défaut.", + "AboutGithubUrlTooltipMessage": "Cliquez pour ouvrir la page GitHub de Ryujinx dans votre navigateur par défaut.", + "AboutDiscordUrlTooltipMessage": "Cliquez pour ouvrir une invitation au serveur Discord de Ryujinx dans votre navigateur par défaut.", + "AboutTwitterUrlTooltipMessage": "Cliquez pour ouvrir la page Twitter de Ryujinx dans votre navigateur par défaut.", + "AboutRyujinxAboutTitle": "À propos :", + "AboutRyujinxAboutContent": "Ryujinx est un émulateur pour la Nintendo Switch™.\nMerci de nous soutenir sur Patreon.\nObtenez toutes les dernières actualités sur notre Twitter ou notre Discord.\nLes développeurs intéressés à contribuer peuvent en savoir plus sur notre GitHub ou notre Discord.", + "AboutRyujinxMaintainersTitle": "Maintenu par :", + "AboutRyujinxMaintainersContentTooltipMessage": "Cliquez pour ouvrir la page Contributeurs dans votre navigateur par défaut.", + "AboutRyujinxSupprtersTitle": "Supporté sur Patreon par :", + "AmiiboSeriesLabel": "Séries Amiibo", + "AmiiboCharacterLabel": "Personnage", + "AmiiboScanButtonLabel": "Scanner", + "AmiiboOptionsShowAllLabel": "Afficher tous les Amiibo", + "AmiiboOptionsUsRandomTagLabel": "Hack : Utiliser un tag Uuid aléatoire", + "DlcManagerTableHeadingEnabledLabel": "Activé", + "DlcManagerTableHeadingTitleIdLabel": "ID du titre", + "DlcManagerTableHeadingContainerPathLabel": "Chemin du conteneur", + "DlcManagerTableHeadingFullPathLabel": "Chemin complet", + "DlcManagerRemoveAllButton": "Tout supprimer", + "DlcManagerEnableAllButton": "Tout activer", + "DlcManagerDisableAllButton": "Tout désactiver", + "ModManagerDeleteAllButton": "Tout supprimer", + "MenuBarOptionsChangeLanguage": "Changer la langue", + "MenuBarShowFileTypes": "Afficher les types de fichiers", + "CommonSort": "Trier", + "CommonShowNames": "Afficher les noms", + "CommonFavorite": "Favoris", + "OrderAscending": "Croissant", + "OrderDescending": "Décroissant", + "SettingsTabGraphicsFeatures": "Fonctionnalités & Améliorations", + "ErrorWindowTitle": "Fenêtre d'erreur", + "ToggleDiscordTooltip": "Choisissez d'afficher ou non Ryujinx sur votre activité « en cours de jeu » Discord", + "AddGameDirBoxTooltip": "Entrez un répertoire de jeux à ajouter à la liste", + "AddGameDirTooltip": "Ajouter un répertoire de jeux à la liste", + "RemoveGameDirTooltip": "Supprimer le répertoire de jeu sélectionné", + "AddAutoloadDirBoxTooltip": "Entrez un répertoire de mises à jour/DLC à ajouter à la liste", + "AddAutoloadDirTooltip": "Ajouter un répertoire de mises à jour/DLC à la liste", + "RemoveAutoloadDirTooltip": "Supprimer le répertoire de mises à jour/DLC sélectionné", + "CustomThemeCheckTooltip": "Utilisez un thème personnalisé Avalonia pour modifier l'apparence des menus de l'émulateur", + "CustomThemePathTooltip": "Chemin vers le thème personnalisé de l'interface utilisateur", + "CustomThemeBrowseTooltip": "Parcourir vers un thème personnalisé pour l'interface utilisateur", + "DockModeToggleTooltip": "Le mode station d'accueil permet à la console émulée de se comporter comme une Nintendo Switch en mode station d'accueil, ce qui améliore la fidélité graphique dans la plupart des jeux. Inversement, la désactivation de cette option rendra la console émulée comme une console Nintendo Switch portable, réduisant la qualité graphique.\n\nConfigurer les controles du joueur 1 si vous prévoyez d'utiliser le mode station d'accueil; configurez les commandes portable si vous prévoyez d'utiliser le mode portable.\n\nLaissez ACTIVER si vous n'êtes pas sûr.", + "DirectKeyboardTooltip": "Prise en charge de l'accès direct au clavier (HID). Permet aux jeux d'accéder à votre clavier comme périphérique de saisie de texte.\n\nFonctionne uniquement avec les jeux prenant en charge nativement l'utilisation du clavier sur le matériel Switch.\n\nLaissez OFF si vous n'êtes pas sûr.", + "DirectMouseTooltip": "Prise en charge de l'accès direct à la souris (HID). Permet aux jeux d'accéder à votre souris en tant que dispositif de pointage.\n\nFonctionne uniquement avec les jeux qui prennent en charge nativement les contrôles de souris sur le matériel Switch, ce qui est rare.\n\nLorsqu'il est activé, la fonctionnalité de l'écran tactile peut ne pas fonctionner.\n\nLaissez sur OFF si vous n'êtes pas sûr.", + "RegionTooltip": "Changer la région du système", + "LanguageTooltip": "Changer la langue du système", + "TimezoneTooltip": "Changer le fuseau horaire du système", + "TimeTooltip": "Changer l'heure du système", + "VSyncToggleTooltip": "La synchronisation verticale de la console émulée. Essentiellement un limiteur de trame pour la majorité des jeux ; le désactiver peut entraîner un fonctionnement plus rapide des jeux ou prolonger ou bloquer les écrans de chargement.\n\nPeut être activé ou désactivé en jeu avec un raccourci clavier de votre choix (F1 par défaut). Nous recommandons de le faire si vous envisagez de le désactiver.\n\nLaissez activé si vous n'êtes pas sûr.", + "PptcToggleTooltip": "Sauvegarde les fonctions JIT afin qu'elles n'aient pas besoin d'être à chaque fois recompiler lorsque le jeu se charge.\n\nRéduit les lags et accélère considérablement le temps de chargement après le premier lancement d'un jeu.\n\nLaissez par défaut si vous n'êtes pas sûr.", + "LowPowerPptcToggleTooltip": "Charger le PPTC en utilisant un tiers des coeurs.", + "FsIntegrityToggleTooltip": "Vérifie si des fichiers sont corrompus lors du lancement d'un jeu, et si des fichiers corrompus sont détectés, affiche une erreur de hachage dans la console.\n\nN'a aucun impact sur les performances et est destiné à aider le dépannage.\n\nLaissez activé en cas d'incertitude.", + "AudioBackendTooltip": "Modifie le backend utilisé pour donnée un rendu audio.\n\nSDL2 est recommandé, tandis que OpenAL et SoundIO sont utilisés comme backend secondaire. Le backend Dummy (Désactivé) ne rend aucun son.\n\nLaissez sur SDL2 si vous n'êtes pas sûr.", + "MemoryManagerTooltip": "Change la façon dont la mémoire émulée est mappée et utilisée. Cela affecte grandement les performances du processeur.\n\nRéglez sur Hôte non vérifié en cas d'incertitude.", + "MemoryManagerSoftwareTooltip": "Utilisez une table logicielle pour la traduction d'adresses. La plus grande précision est fournie, mais les performances en seront impactées.", + "MemoryManagerHostTooltip": "Mappez directement la mémoire dans l'espace d'adresses de l'hôte. Compilation et exécution JIT beaucoup plus rapides.", + "MemoryManagerUnsafeTooltip": "Mapper directement la mémoire dans la carte, mais ne pas masquer l'adresse dans l'espace d'adressage du client avant l'accès. Plus rapide, mais la sécurité sera négligée. L'application peut accéder à la mémoire depuis n'importe où dans Ryujinx, donc exécutez uniquement les programmes en qui vous avez confiance avec ce mode.", + "UseHypervisorTooltip": "Utiliser l'Hyperviseur au lieu du JIT. Améliore considérablement les performances lorsqu'il est disponible, mais peut être instable dans son état actuel.", + "DRamTooltip": "Change le montant de DRAM qui est alloué.\n\nActivez cette option pour les packs de textures 4k ou les mods à résolution 4k.\nN'améliore pas les performances.\n\nLaissez à 4GiO en cas de doute.", + "IgnoreMissingServicesTooltip": "Ignore les services Horizon OS non-intégrés. Cela peut aider à contourner les plantages lors du démarrage de certains jeux.\n\nLaissez désactivé en cas d'incertitude.", + "IgnoreAppletTooltip": "La boîte de dialogue externe \"Programme Manette\" n'apparaîtra pas si la manette est déconnectée en jeu. Il n'y aura aucune boîte de dialogue ouverte pour configurer une nouvelle manette. Une fois que la manette précédemment déconnectée est reconnectée, le jeu reprendra automatiquement. \n\nLaissez désactivé en cas d'incertitude.", + "GraphicsBackendThreadingTooltip": "Exécute des commandes du backend graphiques sur un second thread.\n\nAccélère la compilation des shaders, réduit les crashs et les lags, améliore les performances sur les pilotes GPU sans support de multithreading. Légère augementation des performances sur les pilotes avec multithreading intégrer.\n\nRéglez sur Auto en cas d'incertitude.", + "GalThreadingTooltip": "Exécute des commandes du backend graphiques sur un second thread.\n\nAccélère la compilation des shaders, réduit les crashs et les lags, améliore les performances sur les pilotes GPU sans support de multithreading. Légère augementation des performances sur les pilotes avec multithreading intégrer.\n\nRéglez sur Auto en cas d'incertitude.", + "ShaderCacheToggleTooltip": "Enregistre un cache de shaders sur le disque dur, réduit le lag lors de multiples exécutions.\n\nLaissez activé si vous n'êtes pas sûr.", + "ResolutionScaleTooltip": "Multiplie la résolution de rendu du jeu.\n\nQuelques jeux peuvent ne pas fonctionner avec cette fonctionnalité et sembler pixelisés même lorsque la résolution est augmentée ; pour ces jeux, vous devrez peut-être trouver des mods qui suppriment l'anti-aliasing ou qui augmentent leur résolution de rendu interne. Pour utiliser cette dernière option, vous voudrez probablement sélectionner \"Natif\".\n\nCette option peut être modifiée pendant qu'un jeu est en cours d'exécution en cliquant sur \"Appliquer\" ci-dessous ; vous pouvez simplement déplacer la fenêtre des paramètres sur le côté et expérimenter jusqu'à ce que vous trouviez l'apparence souhaitée pour un jeu.\n\nGardez à l'esprit que 4x est excessif pour pratiquement n'importe quelle configuration.", + "ResolutionScaleEntryTooltip": "Échelle de résolution à virgule, telle que : 1.5. Les échelles non intégrales sont plus susceptibles de causer des problèmes ou des crashs.", + "AnisotropyTooltip": "Niveau de filtrage anisotrope. Réglez sur Auto pour utiliser la valeur demandée par le jeu.", + "AspectRatioTooltip": "Format d'affichage appliqué à la fenêtre du moteur de rendu.\n\nChangez cela uniquement si vous utilisez un mod changeant le format d'affichage pour votre jeu, sinon les graphismes seront étirés.\n\nLaissez sur 16:9 si vous n'êtes pas sûr.", + "ShaderDumpPathTooltip": "Chemin de copie des Shaders :", + "FileLogTooltip": "Sauvegarde le journal de la console dans un fichier journal sur le disque. Cela n'affecte pas les performances.", + "StubLogTooltip": "Affiche les messages de journaux dans la console. N'affecte pas les performances.", + "InfoLogTooltip": "Affiche les messages de journaux d'informations dans la console. N'affecte pas les performances.", + "WarnLogTooltip": "Affiche les messages d'avertissement dans la console. N'affecte pas les performances.", + "ErrorLogTooltip": "Affiche les messages de journaux d'erreur dans la console. N'affecte pas les performances.", + "TraceLogTooltip": "Affiche la trace des messages de journaux dans la console. N'affecte pas les performances.", + "GuestLogTooltip": "Affiche les messages de journaux des invités dans la console. N'affecte pas les performances.", + "FileAccessLogTooltip": "Affiche les messages de journaux d'accès aux fichiers dans la console.", + "FSAccessLogModeTooltip": "Active la sortie du journal d'accès FS de la console. Les modes possibles sont 0-3", + "DeveloperOptionTooltip": "À utiliser avec précaution", + "OpenGlLogLevel": "Nécessite l'activation des niveaux de journalisation appropriés", + "DebugLogTooltip": "Affiche les messages de débogage dans la console.\n\nN'utilisez ceci que si un développeur le demande, car cela rendra les logs difficiles à lire et réduit les performances de l'émulateur.", + "LoadApplicationFileTooltip": "Ouvre l'explorateur de fichiers pour choisir un fichier compatible Switch à charger", + "LoadApplicationFolderTooltip": "Ouvre l'explorateur de fichiers pour choisir une application Switch compatible et décompressée à charger", + "LoadDlcFromFolderTooltip": "Ouvre l'explorateur de fichier pour choisir un ou plusieurs dossiers duquel charger les DLC", + "LoadTitleUpdatesFromFolderTooltip": "Ouvre l'explorateur de fichier pour choisir un ou plusieurs dossiers duquel charger les mises à jour", + "OpenRyujinxFolderTooltip": "Ouvrir le dossier du système de fichiers Ryujinx", + "OpenRyujinxLogsTooltip": "Ouvre le dossier dans lequel les journaux sont écrits", + "ExitTooltip": "Quitter Ryujinx", + "OpenSettingsTooltip": "Ouvrir la fenêtre de configuration", + "OpenProfileManagerTooltip": "Ouvrir la fenêtre du gestionnaire de profils d'utilisateurs", + "StopEmulationTooltip": "Arrêter l'émulation du jeu en cours et revenir à la sélection des jeux", + "CheckUpdatesTooltip": "Vérifier les mises à jour de Ryujinx", + "OpenAboutTooltip": "Ouvrir la fenêtre À Propos", + "GridSize": "Taille de la grille", + "GridSizeTooltip": "Modifier la taille des éléments de la grille", + "SettingsTabSystemSystemLanguageBrazilianPortuguese": "Portugais Brésilien", + "AboutRyujinxContributorsButtonHeader": "Voir tous les contributeurs", + "SettingsTabSystemAudioVolume": "Volume :", + "AudioVolumeTooltip": "Modifier le volume audio", + "SettingsTabSystemEnableInternetAccess": "Accès Internet Invité/Mode LAN", + "EnableInternetAccessTooltip": "Permet à l'application émulée de se connecter à Internet.\n\nLes jeux avec un mode LAN peuvent se connecter les uns aux autres lorsque cette option est cochée et que les systèmes sont connectés au même point d'accès. Cela inclut également les vrais consoles.\n\nCette option n'autorise PAS la connexion aux serveurs Nintendo. Elle peut faire planter certains jeux qui essaient de se connecter à l'Internet.\n\nLaissez DÉSACTIVÉ si vous n'êtes pas sûr.", + "GameListContextMenuManageCheatToolTip": "Gérer les cheats", + "GameListContextMenuManageCheat": "Gérer les cheats", + "GameListContextMenuManageModToolTip": "Gérer les mods", + "GameListContextMenuManageMod": "Gérer les mods", + "ControllerSettingsStickRange": "Intervalle :", + "DialogStopEmulationTitle": "Ryujinx - Arrêt de l'émulation", + "DialogStopEmulationMessage": "Êtes-vous sûr de vouloir arrêter l'émulation ?", + "SettingsTabCpu": "Processeur", + "SettingsTabAudio": "Audio", + "SettingsTabNetwork": "Réseau", + "SettingsTabNetworkConnection": "Connexion réseau", + "SettingsTabCpuCache": "Cache Processeur", + "SettingsTabCpuMemory": "Mémoire Processeur", + "DialogUpdaterFlatpakNotSupportedMessage": "Merci de mettre à jour Ryujinx via FlatHub.", + "UpdaterDisabledWarningTitle": "Mises à jour désactivées !", + "ControllerSettingsRotate90": "Faire pivoter de 90° à droite", + "IconSize": "Taille d'icône", + "IconSizeTooltip": "Changer la taille des icônes de jeu", + "MenuBarOptionsShowConsole": "Afficher la console", + "ShaderCachePurgeError": "Erreur lors de la purge des Shaders à {0}: {1}", + "UserErrorNoKeys": "Clés introuvables", + "UserErrorNoFirmware": "Firmware introuvable", + "UserErrorFirmwareParsingFailed": "Erreur d'analyse du firmware", + "UserErrorApplicationNotFound": " Application introuvable", + "UserErrorUnknown": "Erreur inconnue", + "UserErrorUndefined": "Erreur non définie", + "UserErrorNoKeysDescription": "Ryujinx n'a pas pu trouver votre fichier 'prod.keys'", + "UserErrorNoFirmwareDescription": "Ryujinx n'a pas trouvé de firmware installé", + "UserErrorFirmwareParsingFailedDescription": "Ryujinx n'a pas pu analyser le firmware fourni. Cela est généralement dû à des clés obsolètes.", + "UserErrorApplicationNotFoundDescription": "Ryujinx n'a pas pu trouver une application valide dans le chemin indiqué.", + "UserErrorUnknownDescription": "Une erreur inconnue est survenue !", + "UserErrorUndefinedDescription": "Une erreur inconnue est survenue ! Cela ne devrait pas se produire, merci de contacter un développeur !", + "OpenSetupGuideMessage": "Ouvrir le guide d'installation", + "NoUpdate": "Aucune mise à jour", + "TitleUpdateVersionLabel": "Version {0}", + "TitleBundledUpdateVersionLabel": "Inclus avec le jeu: Version {0}", + "TitleBundledDlcLabel": "Inclus avec le jeu :", + "TitleXCIStatusPartialLabel": "Partiel", + "TitleXCIStatusTrimmableLabel": "Non réduit", + "TitleXCIStatusUntrimmableLabel": "Réduit", + "TitleXCIStatusFailedLabel": "(Échoué)", + "TitleXCICanSaveLabel": "Sauvegarde de {0:n0} Mo", + "TitleXCISavingLabel": "Sauvegardé {0:n0} Mo", + "RyujinxInfo": "Ryujinx - Info", + "RyujinxConfirm": "Ryujinx - Confirmation", + "FileDialogAllTypes": "Tous les types", + "Never": "Jamais", + "SwkbdMinCharacters": "Doit comporter au moins {0} caractères", + "SwkbdMinRangeCharacters": "Doit comporter entre {0} et {1} caractères", + "SoftwareKeyboard": "Clavier logiciel", + "SoftwareKeyboardModeNumeric": "Doit être 0-9 ou '.' uniquement", + "SoftwareKeyboardModeAlphabet": "Doit être uniquement des caractères non CJK", + "SoftwareKeyboardModeASCII": "Doit être uniquement du texte ASCII", + "ControllerAppletControllers": "Contrôleurs pris en charge :", + "ControllerAppletPlayers": "Joueurs :", + "ControllerAppletDescription": "Votre configuration actuelle n'est pas valide. Ouvrez les paramètres et reconfigurez vos contrôles.", + "ControllerAppletDocked": "Mode station d'accueil défini. Le mode contrôle portable doit être désactivé.", + "UpdaterRenaming": "Renommage des anciens fichiers...", + "UpdaterRenameFailed": "Impossible de renommer le fichier : {0}", + "UpdaterAddingFiles": "Ajout des nouveaux fichiers...", + "UpdaterExtracting": "Extraction de la mise à jour…", + "UpdaterDownloading": "Téléchargement de la mise à jour...", + "Game": "Jeu", + "Docked": "Mode station d'accueil", + "Handheld": "Mode Portable", + "ConnectionError": "Erreur de connexion.", + "AboutPageDeveloperListMore": "{0} et plus...", + "ApiError": "Erreur API.", + "LoadingHeading": "Chargement {0}", + "CompilingPPTC": "Compilation PTC", + "CompilingShaders": "Compilation des shaders", + "AllKeyboards": "Tous les claviers", + "OpenFileDialogTitle": "Sélectionnez un fichier supporté à ouvrir", + "OpenFolderDialogTitle": "Sélectionnez un dossier avec un jeu décompressé", + "AllSupportedFormats": "Tous les formats supportés", + "RyujinxUpdater": "Mise à jour de Ryujinx", + "SettingsTabHotkeys": "Raccourcis clavier", + "SettingsTabHotkeysHotkeys": "Raccourcis clavier", + "SettingsTabHotkeysToggleVsyncHotkey": "Activer/désactiver la VSync :", + "SettingsTabHotkeysScreenshotHotkey": "Capture d'écran :", + "SettingsTabHotkeysShowUiHotkey": "Afficher UI :", + "SettingsTabHotkeysPauseHotkey": "Suspendre :", + "SettingsTabHotkeysToggleMuteHotkey": "Couper le son :", + "ControllerMotionTitle": "Réglages des contrôles par mouvement", + "ControllerRumbleTitle": "Paramètres de vibration", + "SettingsSelectThemeFileDialogTitle": "Sélectionner un fichier de thème", + "SettingsXamlThemeFile": "Fichier thème Xaml", + "AvatarWindowTitle": "Gérer les Comptes - Avatar", + "Amiibo": "Amiibo", + "Unknown": "Inconnu", + "Usage": "Utilisation", + "Writable": "Ecriture possible", + "SelectDlcDialogTitle": "Sélectionner les fichiers DLC", + "SelectUpdateDialogTitle": "Sélectionner les fichiers de mise à jour", + "SelectModDialogTitle": "Sélectionner le répertoire du mod", + "TrimXCIFileDialogTitle": "Vérifier et Réduire le fichier XCI", + "TrimXCIFileDialogPrimaryText": "Cette fonction va vérifier l'espace vide, puis réduire le fichier XCI pour économiser de l'espace de disque dur.", + "TrimXCIFileDialogSecondaryText": "Taille actuelle du fichier: {0:n} MB\nTaille des données de jeux: {1:n} MB\nÉconomie d'espaces sur le disque: {2:n} MB", + "TrimXCIFileNoTrimNecessary": "Fichier XCI n'a pas besoin d'être réduit. Regarder les journaux pour plus de détails", + "TrimXCIFileNoUntrimPossible": "Fichier XCI ne peut pas être dé-réduit. Regarder les journaux pour plus de détails", + "TrimXCIFileReadOnlyFileCannotFix": "Fichier XCI est en Lecture Seule et n'a pas pu être rendu accessible en écriture. Regarder les journaux pour plus de détails", + "TrimXCIFileFileSizeChanged": "Fichier XCI a changé en taille depuis qu'il a été scanné. Vérifier que le fichier n'est pas en cours d'écriture et réessayer.", + "TrimXCIFileFreeSpaceCheckFailed": "Fichier XCI a des données dans la zone d'espace libre, ce n'est pas sûr de réduire", + "TrimXCIFileInvalidXCIFile": "Fichier XCI contient des données invalides. Regarder les journaux pour plus de détails", + "TrimXCIFileFileIOWriteError": "Fichier XCI n'a pas pu été ouvert pour écriture. Regarder les journaux pour plus de détails", + "TrimXCIFileFailedPrimaryText": "Réduction du fichier XCI a échoué", + "TrimXCIFileCancelled": "L'opération a été annulée", + "TrimXCIFileFileUndertermined": "Aucune opération a été faite", + "UserProfileWindowTitle": "Gestionnaire de profils utilisateur", + "CheatWindowTitle": "Gestionnaire de cheats", + "DlcWindowTitle": "Gérer le contenu téléchargeable pour {0} ({1})", + "ModWindowTitle": "Gérer les mods pour {0} ({1})", + "UpdateWindowTitle": "Gestionnaire de mises à jour", + "XCITrimmerWindowTitle": "Rogneur de fichier XCI", + "XCITrimmerTitleStatusCount": "{0} sur {1} Fichier(s) Sélectionnés", + "XCITrimmerTitleStatusCountWithFilter": "{0} sur {1} Fichier(s) Sélectionnés ({2} affiché(s)", + "XCITrimmerTitleStatusTrimming": "Réduction de {0} Fichier(s)...", + "XCITrimmerTitleStatusUntrimming": "Dé-Réduction de {0} Fichier(s)...", + "XCITrimmerTitleStatusFailed": "Échoué", + "XCITrimmerPotentialSavings": "Économies potentielles d'espace de disque dur", + "XCITrimmerActualSavings": "Économies actualles d'espace de disque dur", + "XCITrimmerSavingsMb": "{0:n0} Mo", + "XCITrimmerSelectDisplayed": "Sélectionner Affiché", + "XCITrimmerDeselectDisplayed": "Désélectionner Affiché", + "XCITrimmerSortName": "Titre", + "XCITrimmerSortSaved": "Économies de disque dur", + "XCITrimmerTrim": "Réduire", + "XCITrimmerUntrim": "Dé-Réduire", + "UpdateWindowUpdateAddedMessage": "{0} nouvelle(s) mise(s) à jour ajoutée(s)", + "UpdateWindowBundledContentNotice": "Les mises à jour incluses avec le jeu ne peuvent pas être supprimées mais peuvent être désactivées.", + "CheatWindowHeading": "Cheats disponibles pour {0} [{1}]", + "BuildId": "BuildId :", + "DlcWindowBundledContentNotice": "Les DLC inclus avec le jeu ne peuvent pas être supprimés mais peuvent être désactivés.", + "DlcWindowHeading": "{0} Contenu(s) téléchargeable(s)", + "DlcWindowDlcAddedMessage": "{0} nouveau(x) contenu(s) téléchargeable(s) ajouté(s)", + "AutoloadDlcAddedMessage": "{0} nouveau(x) contenu(s) téléchargeable(s) ajouté(s)", + "AutoloadDlcRemovedMessage": "{0} contenu(s) téléchargeable(s) manquant(s) supprimé(s)", + "AutoloadUpdateAddedMessage": "{0} nouvelle(s) mise(s) à jour ajoutée(s)", + "AutoloadUpdateRemovedMessage": "{0} mises à jour manquantes supprimées", + "ModWindowHeading": "{0} Mod(s)", + "UserProfilesEditProfile": "Éditer la sélection", + "Continue": "Continuer", + "Cancel": "Annuler", + "Save": "Enregistrer", + "Discard": "Abandonner", + "Paused": "Suspendu", + "UserProfilesSetProfileImage": "Définir l'image de profil", + "UserProfileEmptyNameError": "Le nom est requis", + "UserProfileNoImageError": "L'image du profil doit être définie", + "GameUpdateWindowHeading": "Gérer les mises à jour pour {0} ({1})", + "SettingsTabHotkeysResScaleUpHotkey": "Augmenter la résolution :", + "SettingsTabHotkeysResScaleDownHotkey": "Diminuer la résolution :", + "UserProfilesName": "Nom :", + "UserProfilesUserId": "Identifiant de l'utilisateur :", + "SettingsTabGraphicsBackend": "API de Rendu", + "SettingsTabGraphicsBackendTooltip": "Sélectionnez le moteur graphique qui sera utilisé dans l'émulateur.\n\nVulkan est globalement meilleur pour toutes les cartes graphiques modernes, tant que leurs pilotes sont à jour. Vulkan offre également une compilation de shaders plus rapide (moins de saccades) sur tous les fournisseurs de GPU.\n\nOpenGL peut obtenir de meilleurs résultats sur d'anciennes cartes graphiques Nvidia, sur d'anciennes cartes graphiques AMD sous Linux, ou sur des GPU avec moins de VRAM, bien que les saccades dues à la compilation des shaders soient plus importantes.\n\nRéglez sur Vulkan si vous n'êtes pas sûr. Réglez sur OpenGL si votre GPU ne prend pas en charge Vulkan même avec les derniers pilotes graphiques.", + "SettingsEnableTextureRecompression": "Activer la recompression des textures", + "SettingsEnableTextureRecompressionTooltip": "Les jeux utilisant ce format de texture incluent Astral Chain, Bayonetta 3, Fire Emblem Engage, Metroid Prime Remastered, Super Mario Bros. Wonder et The Legend of Zelda: Tears of the Kingdom.\n\nLes cartes graphiques avec 4 Go ou moins de VRAM risquent probablement de planter à un moment donné lors de l'exécution de ces jeux.\n\nActivez uniquement si vous manquez de VRAM sur les jeux mentionnés ci-dessus. Laissez DÉSACTIVÉ si vous n'êtes pas sûr.", + "SettingsTabGraphicsPreferredGpu": "GPU préféré", + "SettingsTabGraphicsPreferredGpuTooltip": "Sélectionnez la carte graphique qui sera utilisée avec l'interface graphique Vulkan.\n\nCela ne change pas le GPU qu'OpenGL utilisera.\n\nChoisissez le GPU noté \"dGPU\" si vous n'êtes pas sûr. S'il n'y en a pas, ne pas modifier.", + "SettingsAppRequiredRestartMessage": "Redémarrage de Ryujinx requis", + "SettingsGpuBackendRestartMessage": "Les paramètres de l'interface graphique ou du GPU ont été modifiés. Cela nécessitera un redémarrage pour être appliqué", + "SettingsGpuBackendRestartSubMessage": "\n\nVoulez-vous redémarrer maintenant ?", + "RyujinxUpdaterMessage": "Voulez-vous mettre à jour Ryujinx vers la dernière version ?", + "SettingsTabHotkeysVolumeUpHotkey": "Augmenter le volume :", + "SettingsTabHotkeysVolumeDownHotkey": "Diminuer le volume :", + "SettingsEnableMacroHLE": "Activer les macros HLE", + "SettingsEnableMacroHLETooltip": "Émulation de haut niveau du code de Macro GPU.\n\nAméliore les performances, mais peut causer des artefacts graphiques dans certains jeux.\n\nLaissez ACTIVÉ si vous n'êtes pas sûr.", + "SettingsEnableColorSpacePassthrough": "Traversée de l'espace colorimétrique", + "SettingsEnableColorSpacePassthroughTooltip": "Dirige l'interface graphique Vulkan pour qu'il transmette les informations de couleur sans spécifier d'espace colorimétrique. Pour les utilisateurs possédant des écrans Wide Color Gamut, cela peut entraîner des couleurs plus vives, au détriment de l'exactitude des couleurs.", + "VolumeShort": "Vol", + "UserProfilesManageSaves": "Gérer les sauvegardes", + "DeleteUserSave": "Voulez-vous supprimer la sauvegarde de l'utilisateur pour ce jeu ?", + "IrreversibleActionNote": "Cette action n'est pas réversible.", + "SaveManagerHeading": "Gérer les sauvegardes pour {0} ({1})", + "SaveManagerTitle": "Gestionnaire de sauvegarde", + "Name": "Nom ", + "Size": "Taille", + "Search": "Rechercher", + "UserProfilesRecoverLostAccounts": "Récupérer les profils perdus", + "Recover": "Récupérer", + "UserProfilesRecoverHeading": "Des sauvegardes ont été trouvées pour les profils suivants", + "UserProfilesRecoverEmptyList": "Aucun profil à restaurer", + "GraphicsAATooltip": "FXAA floute la plupart de l'image, tandis que SMAA tente de détecter les contours dentelés et de les lisser.\n\nIl n'est pas recommandé de l'utiliser en conjonction avec le filtre de mise à l'échelle FSR.\n\nCette option peut être modifiée pendant qu'un jeu est en cours d'exécution en cliquant sur \"Appliquer\" ci-dessous ; vous pouvez simplement déplacer la fenêtre des paramètres sur le côté et expérimenter jusqu'à ce que vous trouviez l'apparence souhaitée pour un jeu.\n\nLaissez sur AUCUN si vous n'êtes pas sûr.", + "GraphicsAALabel": "Anticrénelage :", + "GraphicsScalingFilterLabel": "Filtre de mise à l'échelle :", + "GraphicsScalingFilterTooltip": "Choisissez le filtre de mise à l'échelle qui sera appliqué lors de l'utilisation de la mise à l'échelle de la résolution.\n\nLe filtre bilinéaire fonctionne bien pour les jeux en 3D et constitue une option par défaut sûre.\n\nLe filtre le plus proche est recommandé pour les jeux de pixel art.\n\nFSR 1.0 est simplement un filtre de netteté, non recommandé pour une utilisation avec FXAA ou SMAA.\n\nCette option peut être modifiée pendant qu'un jeu est en cours d'exécution en cliquant sur \"Appliquer\" ci-dessous ; vous pouvez simplement déplacer la fenêtre des paramètres de côté et expérimenter jusqu'à ce que vous trouviez l'aspect souhaité pour un jeu.\n\nLaissez sur BILINÉAIRE si vous n'êtes pas sûr.", + "GraphicsScalingFilterBilinear": "Bilinéaire", + "GraphicsScalingFilterNearest": "Le plus proche", + "GraphicsScalingFilterFsr": "FSR", + "GraphicsScalingFilterArea": "Zone", + "GraphicsScalingFilterLevelLabel": "Niveau ", + "GraphicsScalingFilterLevelTooltip": "Définissez le niveau de netteté FSR 1.0. Plus élevé signifie plus net.", + "SmaaLow": "SMAA Faible", + "SmaaMedium": "SMAA moyen", + "SmaaHigh": "SMAA Élevé", + "SmaaUltra": "SMAA Ultra", + "UserEditorTitle": "Modifier Utilisateur", + "UserEditorTitleCreate": "Créer Utilisateur", + "SettingsTabNetworkInterface": "Interface Réseau :", + "NetworkInterfaceTooltip": "L'interface réseau utilisée pour les fonctionnalités LAN/LDN.\n\nEn conjonction avec un VPN ou XLink Kai et un jeu prenant en charge le LAN, peut être utilisée pour simuler une connexion sur le même réseau via Internet.\n\nLaissez sur PAR DÉFAUT si vous n'êtes pas sûr.", + "NetworkInterfaceDefault": "Par défaut", + "PackagingShaders": "Empaquetage des Shaders", + "AboutChangelogButton": "Voir le Changelog sur GitHub", + "AboutChangelogButtonTooltipMessage": "Cliquez pour ouvrir le changelog de cette version dans votre navigateur par défaut.", + "SettingsTabNetworkMultiplayer": "Multijoueur", + "MultiplayerMode": "Mode :", + "MultiplayerModeTooltip": "Changer le mode multijoueur LDN.\n\nLdnMitm modifiera la fonctionnalité de jeu sans fil local/jeu local dans les jeux pour fonctionner comme s'il s'agissait d'un LAN, permettant des connexions locales sur le même réseau avec d'autres instances de Ryujinx et des consoles Nintendo Switch piratées ayant le module ldn_mitm installé.\n\nLe multijoueur nécessite que tous les joueurs soient sur la même version du jeu (par exemple, Super Smash Bros. Ultimate v13.0.1 ne peut pas se connecter à v13.0.0).\n\nLaissez DÉSACTIVÉ si vous n'êtes pas sûr.", + "MultiplayerModeDisabled": "Désactivé", + "MultiplayerModeLdnMitm": "ldn_mitm", + "MultiplayerModeLdnRyu": "RyuLDN", + "MultiplayerDisableP2P": "Désactiver PàP Hébergement de Réseau (pourrait augmenter la latence)", + "MultiplayerDisableP2PTooltip": "Désactiver PàP hébergement de réseau, les postes vont proxy avec le serveur principal au lieu de se connecter directement à vous.", + "LdnPassphrase": "Mot de passe Réseau :", + "LdnPassphraseTooltip": "Vous pourez seulement voir les jeux hébergé avec le même mot de passe que vous.", + "LdnPassphraseInputTooltip": "Entrer un mot de passe dans le format Ryujinx-<8 hex chars>. Vous pourez seulement voir les jeux hébergé avec le même mot de passe que vous.", + "LdnPassphraseInputPublic": "(publique)", + "GenLdnPass": "Générer Aléatoire", + "GenLdnPassTooltip": "Génére un nouveau mot de passe, qui peut être partagé avec les autres.", + "ClearLdnPass": "Supprimer", + "ClearLdnPassTooltip": "Supprime le mot de passe actuel, ce qui vous remet sur le réseau public.", + "InvalidLdnPassphrase": "Mot de passe invalide! Il doit être dans le format \"Ryujinx-<8 hex chars>\"" +} diff --git a/src/Ryujinx/Assets/Locales/he_IL.json b/src/Ryujinx/Assets/Locales/he_IL.json new file mode 100644 index 000000000..51c3c8835 --- /dev/null +++ b/src/Ryujinx/Assets/Locales/he_IL.json @@ -0,0 +1,868 @@ +{ + "Language": "עִברִית", + "MenuBarFileOpenApplet": "פתח יישומון", + "MenuBarFileOpenAppletOpenMiiApplet": "Mii Edit Applet", + "MenuBarFileOpenAppletOpenMiiAppletToolTip": "פתח את יישומון עורך ה- Mii במצב עצמאי", + "SettingsTabInputDirectMouseAccess": "גישה ישירה לעכבר", + "SettingsTabSystemMemoryManagerMode": "מצב מנהל זיכרון:", + "SettingsTabSystemMemoryManagerModeSoftware": "תוכנה", + "SettingsTabSystemMemoryManagerModeHost": "מארח (מהיר)", + "SettingsTabSystemMemoryManagerModeHostUnchecked": "מארח לא מבוקר (המהיר ביותר, לא בטוח)", + "SettingsTabSystemUseHypervisor": "השתמש ב Hypervisor", + "MenuBarFile": "_קובץ", + "MenuBarFileOpenFromFile": "_טען יישום מקובץ", + "MenuBarFileOpenFromFileError": "No applications found in selected file.", + "MenuBarFileOpenUnpacked": "טען משחק _שאינו ארוז", + "MenuBarFileLoadDlcFromFolder": "Load DLC From Folder", + "MenuBarFileLoadTitleUpdatesFromFolder": "Load Title Updates From Folder", + "MenuBarFileOpenEmuFolder": "פתח את תיקיית ריוג'ינקס", + "MenuBarFileOpenLogsFolder": "פתח את תיקיית קבצי הלוג", + "MenuBarFileExit": "_יציאה", + "MenuBarOptions": "_אפשרויות", + "MenuBarOptionsToggleFullscreen": "שנה מצב- מסך מלא", + "MenuBarOptionsStartGamesInFullscreen": "התחל משחקים במסך מלא", + "MenuBarOptionsStopEmulation": "עצור אמולציה", + "MenuBarOptionsSettings": "_הגדרות", + "MenuBarOptionsManageUserProfiles": "_נהל פרופילי משתמש", + "MenuBarActions": "_פעולות", + "MenuBarOptionsSimulateWakeUpMessage": "דמה הודעת השכמה", + "MenuBarActionsScanAmiibo": "סרוק אמיבו", + "MenuBarTools": "_כלים", + "MenuBarToolsInstallFirmware": "התקן קושחה", + "MenuBarFileToolsInstallFirmwareFromFile": "התקן קושחה מקובץ- ZIP/XCI", + "MenuBarFileToolsInstallFirmwareFromDirectory": "התקן קושחה מתוך תקייה", + "MenuBarToolsManageFileTypes": "ניהול סוגי קבצים", + "MenuBarToolsInstallFileTypes": "סוגי קבצי התקנה", + "MenuBarToolsUninstallFileTypes": "סוגי קבצי הסרה", + "MenuBarToolsXCITrimmer": "Trim XCI Files", + "MenuBarView": "_View", + "MenuBarViewWindow": "Window Size", + "MenuBarViewWindow720": "720p", + "MenuBarViewWindow1080": "1080p", + "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": "מנהל הרחבות", + "GameListContextMenuManageDlcToolTip": "פותח את חלון מנהל הרחבות המשחקים", + "GameListContextMenuCacheManagement": "ניהול מטמון", + "GameListContextMenuCacheManagementPurgePptc": "הוסף PPTC לתור בנייה מחדש", + "GameListContextMenuCacheManagementPurgePptcToolTip": "גרום ל-PPTC להבנות מחדש בפתיחה הבאה של המשחק", + "GameListContextMenuCacheManagementPurgeShaderCache": "ניקוי מטמון הצללות", + "GameListContextMenuCacheManagementPurgeShaderCacheToolTip": "מוחק את מטמון ההצללות של היישום", + "GameListContextMenuCacheManagementOpenPptcDirectory": "פתח את תקיית PPTC", + "GameListContextMenuCacheManagementOpenPptcDirectoryToolTip": "פותח את התקייה של מטמון ה-PPTC של האפליקציה", + "GameListContextMenuCacheManagementOpenShaderCacheDirectory": "פתח את תקיית המטמון של ההצללות", + "GameListContextMenuCacheManagementOpenShaderCacheDirectoryToolTip": "פותח את תקיית מטמון ההצללות של היישום", + "GameListContextMenuExtractData": "חילוץ נתונים", + "GameListContextMenuExtractDataExeFS": "ExeFS", + "GameListContextMenuExtractDataExeFSToolTip": "חלץ את קטע ה-ExeFS מתצורת היישום הנוכחית (כולל עדכונים)", + "GameListContextMenuExtractDataRomFS": "RomFS", + "GameListContextMenuExtractDataRomFSToolTip": "חלץ את קטע ה-RomFS מתצורת היישום הנוכחית (כולל עדכונים)", + "GameListContextMenuExtractDataLogo": "Logo", + "GameListContextMenuExtractDataLogoToolTip": "חלץ את קטע ה-Logo מתצורת היישום הנוכחית (כולל עדכונים)", + "GameListContextMenuCreateShortcut": "ליצור קיצור דרך לאפליקציה", + "GameListContextMenuCreateShortcutToolTip": "ליצור קיצור דרך בשולחן העבודה שיפתח את אפליקציה זו", + "GameListContextMenuCreateShortcutToolTipMacOS": "ליצור קיצור דרך בתיקיית האפליקציות של macOS שיפתח את אפליקציה זו", + "GameListContextMenuOpenModsDirectory": "פתח תיקיית מודים", + "GameListContextMenuOpenModsDirectoryToolTip": "פותח את התיקייה שמכילה מודים של האפליקציה", + "GameListContextMenuOpenSdModsDirectory": "פתח תיקיית מודים של Atmosphere", + "GameListContextMenuOpenSdModsDirectoryToolTip": "פותח את תיקיית כרטיס ה-SD החלופית של Atmosphere המכילה את המודים של האפליקציה. שימושי עבור מודים שארוזים עבור חומרה אמיתית.", + "GameListContextMenuTrimXCI": "Check and Trim XCI File", + "GameListContextMenuTrimXCIToolTip": "Check and Trim XCI File to Save Disk Space", + "StatusBarGamesLoaded": "{1}/{0} משחקים נטענו", + "StatusBarSystemVersion": "גרסת מערכת: {0}", + "StatusBarXCIFileTrimming": "Trimming XCI File '{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": "הפעלת תצוגה עשירה בדיסקורד", + "SettingsTabGeneralCheckUpdatesOnLaunch": "בדוק אם קיימים עדכונים בהפעלה", + "SettingsTabGeneralShowConfirmExitDialog": "הראה דיאלוג \"אשר יציאה\"", + "SettingsTabGeneralRememberWindowState": "Remember Window Size/Position", + "SettingsTabGeneralShowTitleBar": "Show Title Bar (Requires restart)", + "SettingsTabGeneralHideCursor": "הסתר את הסמן", + "SettingsTabGeneralHideCursorNever": "אף פעם", + "SettingsTabGeneralHideCursorOnIdle": "במצב סרק", + "SettingsTabGeneralHideCursorAlways": "תמיד", + "SettingsTabGeneralGameDirectories": "תקיות משחקים", + "SettingsTabGeneralAutoloadDirectories": "Autoload DLC/Updates Directories", + "SettingsTabGeneralAutoloadNote": "DLC and Updates which refer to missing files will be unloaded automatically", + "SettingsTabGeneralAdd": "הוסף", + "SettingsTabGeneralRemove": "הסר", + "SettingsTabSystem": "מערכת", + "SettingsTabSystemCore": "ליבה", + "SettingsTabSystemSystemRegion": "אזור מערכת:", + "SettingsTabSystemSystemRegionJapan": "יפן", + "SettingsTabSystemSystemRegionUSA": "ארה\"ב", + "SettingsTabSystemSystemRegionEurope": "אירופה", + "SettingsTabSystemSystemRegionAustralia": "אוסטרליה", + "SettingsTabSystemSystemRegionChina": "סין", + "SettingsTabSystemSystemRegionKorea": "קוריאה", + "SettingsTabSystemSystemRegionTaiwan": "טייוואן", + "SettingsTabSystemSystemLanguage": "שפת המערכת:", + "SettingsTabSystemSystemLanguageJapanese": "יפנית", + "SettingsTabSystemSystemLanguageAmericanEnglish": "אנגלית אמריקאית", + "SettingsTabSystemSystemLanguageFrench": "צרפתית", + "SettingsTabSystemSystemLanguageGerman": "גרמנית", + "SettingsTabSystemSystemLanguageItalian": "איטלקית", + "SettingsTabSystemSystemLanguageSpanish": "ספרדית", + "SettingsTabSystemSystemLanguageChinese": "סינית", + "SettingsTabSystemSystemLanguageKorean": "קוריאנית", + "SettingsTabSystemSystemLanguageDutch": "הולנדית", + "SettingsTabSystemSystemLanguagePortuguese": "פורטוגזית", + "SettingsTabSystemSystemLanguageRussian": "רוסית", + "SettingsTabSystemSystemLanguageTaiwanese": "טייוואנית", + "SettingsTabSystemSystemLanguageBritishEnglish": "אנגלית בריטית", + "SettingsTabSystemSystemLanguageCanadianFrench": "צרפתית קנדית", + "SettingsTabSystemSystemLanguageLatinAmericanSpanish": "ספרדית אמריקה הלטינית", + "SettingsTabSystemSystemLanguageSimplifiedChinese": "סינית פשוטה", + "SettingsTabSystemSystemLanguageTraditionalChinese": "סינית מסורתית", + "SettingsTabSystemSystemTimeZone": "אזור זמן מערכת:", + "SettingsTabSystemSystemTime": "זמן מערכת:", + "SettingsTabSystemEnableVsync": "VSync", + "SettingsTabSystemEnablePptc": "PPTC (Profiled Persistent Translation Cache)", + "SettingsTabSystemEnableLowPowerPptc": "Low-power PPTC", + "SettingsTabSystemEnableFsIntegrityChecks": "FS בדיקות תקינות", + "SettingsTabSystemAudioBackend": "אחראי שמע:", + "SettingsTabSystemAudioBackendDummy": "גולם", + "SettingsTabSystemAudioBackendOpenAL": "OpenAL", + "SettingsTabSystemAudioBackendSoundIO": "SoundIO", + "SettingsTabSystemAudioBackendSDL2": "SDL2", + "SettingsTabSystemHacks": "האצות", + "SettingsTabSystemHacksNote": "עלול לגרום לאי יציבות", + "SettingsTabSystemDramSize": "השתמש בפריסת זיכרון חלופית (נועד למפתחים)", + "SettingsTabSystemDramSize4GiB": "4GiB", + "SettingsTabSystemDramSize6GiB": "6GiB", + "SettingsTabSystemDramSize8GiB": "8GiB", + "SettingsTabSystemDramSize12GiB": "12GiB", + "SettingsTabSystemIgnoreMissingServices": "התעלם משירותים חסרים", + "SettingsTabSystemIgnoreApplet": "Ignore Applet", + "SettingsTabGraphics": "גרפיקה", + "SettingsTabGraphicsAPI": "ממשק גראפי", + "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": "Graphics Shader Dump Path:", + "SettingsTabLogging": "רישום", + "SettingsTabLoggingLogging": "רישום", + "SettingsTabLoggingEnableLoggingToFile": "אפשר רישום לקובץ", + "SettingsTabLoggingEnableStubLogs": "אפשר רישום בדל", + "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": "שלט פרו ", + "ControllerSettingsControllerTypeJoyConPair": "ג'ויקון הותאם", + "ControllerSettingsControllerTypeJoyConLeft": "ג'ויקון שמאלי ", + "ControllerSettingsControllerTypeJoyConRight": "ג'ויקון ימני", + "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": "מיקום ג'ויקון ימני", + "ControllerSettingsMotionServerHost": "מארח השרת:", + "ControllerSettingsMotionGyroSensitivity": "רגישות ג'ירוסקופ:", + "ControllerSettingsMotionGyroDeadzone": "שטח מת של הג'ירוסקופ:", + "ControllerSettingsSave": "שמירה", + "ControllerSettingsClose": "סגירה", + "KeyUnknown": "Unknown", + "KeyShiftLeft": "Shift Left", + "KeyShiftRight": "Shift Right", + "KeyControlLeft": "Ctrl Left", + "KeyMacControlLeft": "⌃ Left", + "KeyControlRight": "Ctrl Right", + "KeyMacControlRight": "⌃ Right", + "KeyAltLeft": "Alt Left", + "KeyMacAltLeft": "⌥ Left", + "KeyAltRight": "Alt Right", + "KeyMacAltRight": "⌥ Right", + "KeyWinLeft": "⊞ Left", + "KeyMacWinLeft": "⌘ Left", + "KeyWinRight": "⊞ Right", + "KeyMacWinRight": "⌘ Right", + "KeyMenu": "Menu", + "KeyUp": "Up", + "KeyDown": "Down", + "KeyLeft": "Left", + "KeyRight": "Right", + "KeyEnter": "Enter", + "KeyEscape": "Escape", + "KeySpace": "Space", + "KeyTab": "Tab", + "KeyBackSpace": "Backspace", + "KeyInsert": "Insert", + "KeyDelete": "Delete", + "KeyPageUp": "Page Up", + "KeyPageDown": "Page Down", + "KeyHome": "Home", + "KeyEnd": "End", + "KeyCapsLock": "Caps Lock", + "KeyScrollLock": "Scroll Lock", + "KeyPrintScreen": "Print Screen", + "KeyPause": "Pause", + "KeyNumLock": "Num Lock", + "KeyClear": "Clear", + "KeyKeypad0": "Keypad 0", + "KeyKeypad1": "Keypad 1", + "KeyKeypad2": "Keypad 2", + "KeyKeypad3": "Keypad 3", + "KeyKeypad4": "Keypad 4", + "KeyKeypad5": "Keypad 5", + "KeyKeypad6": "Keypad 6", + "KeyKeypad7": "Keypad 7", + "KeyKeypad8": "Keypad 8", + "KeyKeypad9": "Keypad 9", + "KeyKeypadDivide": "Keypad Divide", + "KeyKeypadMultiply": "Keypad Multiply", + "KeyKeypadSubtract": "Keypad Subtract", + "KeyKeypadAdd": "Keypad Add", + "KeyKeypadDecimal": "Keypad Decimal", + "KeyKeypadEnter": "Keypad Enter", + "KeyNumber0": "0", + "KeyNumber1": "1", + "KeyNumber2": "2", + "KeyNumber3": "3", + "KeyNumber4": "4", + "KeyNumber5": "5", + "KeyNumber6": "6", + "KeyNumber7": "7", + "KeyNumber8": "8", + "KeyNumber9": "9", + "KeyTilde": "~", + "KeyGrave": "`", + "KeyMinus": "-", + "KeyPlus": "+", + "KeyBracketLeft": "[", + "KeyBracketRight": "]", + "KeySemicolon": ";", + "KeyQuote": "\"", + "KeyComma": ",", + "KeyPeriod": ".", + "KeySlash": "/", + "KeyBackSlash": "\\", + "KeyUnbound": "Unbound", + "GamepadLeftStick": "L Stick Button", + "GamepadRightStick": "R Stick Button", + "GamepadLeftShoulder": "Left Shoulder", + "GamepadRightShoulder": "Right Shoulder", + "GamepadLeftTrigger": "Left Trigger", + "GamepadRightTrigger": "Right Trigger", + "GamepadDpadUp": "Up", + "GamepadDpadDown": "Down", + "GamepadDpadLeft": "Left", + "GamepadDpadRight": "Right", + "GamepadMinus": "-", + "GamepadPlus": "+", + "GamepadGuide": "Guide", + "GamepadMisc1": "Misc", + "GamepadPaddle1": "Paddle 1", + "GamepadPaddle2": "Paddle 2", + "GamepadPaddle3": "Paddle 3", + "GamepadPaddle4": "Paddle 4", + "GamepadTouchpad": "Touchpad", + "GamepadSingleLeftTrigger0": "Left Trigger 0", + "GamepadSingleRightTrigger0": "Right Trigger 0", + "GamepadSingleLeftTrigger1": "Left Trigger 1", + "GamepadSingleRightTrigger1": "Right Trigger 1", + "StickLeft": "Left Stick", + "StickRight": "Right Stick", + "UserProfilesSelectedUserProfile": "פרופיל המשתמש הנבחר:", + "UserProfilesSaveProfileName": "שמור שם פרופיל", + "UserProfilesChangeProfileImage": "שנה תמונת פרופיל", + "UserProfilesAvailableUserProfiles": "פרופילי משתמש זמינים:", + "UserProfilesAddNewProfile": "צור פרופיל", + "UserProfilesDelete": "מחיקה", + "UserProfilesClose": "סגור", + "ProfileNameSelectionWatermark": "בחרו כינוי", + "ProfileImageSelectionTitle": "בחירת תמונת פרופיל", + "ProfileImageSelectionHeader": "בחרו תמונת פרופיל", + "ProfileImageSelectionNote": "אתם יכולים לייבא תמונת פרופיל מותאמת אישית, או לבחור אווטאר מקושחת המערכת", + "ProfileImageSelectionImportImage": "ייבוא קובץ תמונה", + "ProfileImageSelectionSelectAvatar": "בחרו אוואטר קושחה", + "InputDialogTitle": "דיאלוג קלט", + "InputDialogOk": "בסדר", + "InputDialogCancel": "ביטול", + "InputDialogCancelling": "Cancelling", + "InputDialogClose": "Close", + "InputDialogAddNewProfileTitle": "בחרו את שם הפרופיל", + "InputDialogAddNewProfileHeader": "אנא הזינו שם לפרופיל", + "InputDialogAddNewProfileSubtext": "(אורך מרבי: {0})", + "AvatarChoose": "בחרו דמות", + "AvatarSetBackgroundColor": "הגדר צבע רקע", + "AvatarClose": "סגור", + "ControllerSettingsLoadProfileToolTip": "טען פרופיל", + "ControllerSettingsViewProfileToolTip": "View Profile", + "ControllerSettingsAddProfileToolTip": "הוסף פרופיל", + "ControllerSettingsRemoveProfileToolTip": "הסר פרופיל", + "ControllerSettingsSaveProfileToolTip": "שמור פרופיל", + "MenuBarFileToolsTakeScreenshot": "צלם מסך", + "MenuBarFileToolsHideUi": "הסתר ממשק משתמש ", + "GameListContextMenuRunApplication": "הרץ יישום", + "GameListContextMenuToggleFavorite": "למתג העדפה", + "GameListContextMenuToggleFavoriteToolTip": "למתג סטטוס העדפה של משחק", + "SettingsTabGeneralTheme": "ערכת נושא:", + "SettingsTabGeneralThemeAuto": "Auto", + "SettingsTabGeneralThemeDark": "כהה", + "SettingsTabGeneralThemeLight": "בהיר", + "ControllerSettingsConfigureGeneral": "הגדר", + "ControllerSettingsRumble": "רטט", + "ControllerSettingsRumbleStrongMultiplier": "העצמת רטט חזק", + "ControllerSettingsRumbleWeakMultiplier": "מכפיל רטט חלש", + "DialogMessageSaveNotAvailableMessage": "אין שמור משחק עבור [{1:x16}] {0}", + "DialogMessageSaveNotAvailableCreateSaveMessage": "האם תרצה ליצור שמור משחק עבור המשחק הזה?", + "DialogConfirmationTitle": "ריוג'ינקס - אישור", + "DialogUpdaterTitle": "ריוג'ינקס - מעדכן", + "DialogErrorTitle": "ריוג'ינקס - שגיאה", + "DialogWarningTitle": "ריוג'ינקס - אזהרה", + "DialogExitTitle": "ריוג'ינקס - יציאה", + "DialogErrorMessage": "ריוג'ינקס נתקל בשגיאה", + "DialogExitMessage": "האם אתם בטוחים שאתם רוצים לסגור את ריוג'ינקס?", + "DialogExitSubMessage": "כל הנתונים שלא נשמרו יאבדו!", + "DialogMessageCreateSaveErrorMessage": "אירעה שגיאה ביצירת שמור המשחק שצויין: {0}", + "DialogMessageFindSaveErrorMessage": "אירעה שגיאה במציאת שמור המשחק שצויין: {0}", + "FolderDialogExtractTitle": "בחרו את התיקייה לחילוץ", + "DialogNcaExtractionMessage": "מלחץ {0} ממקטע {1}...", + "DialogNcaExtractionTitle": "מחלץ מקטע NCA", + "DialogNcaExtractionMainNcaNotFoundErrorMessage": "כשל בחילוץ. ה-NCA הראשי לא היה קיים בקובץ שנבחר.", + "DialogNcaExtractionCheckLogErrorMessage": "כשל בחילוץ. קרא את קובץ הרישום למידע נוסף.", + "DialogNcaExtractionSuccessMessage": "החילוץ הושלם בהצלחה.", + "DialogUpdaterConvertFailedMessage": "המרת הגרסה הנוכחית של ריוג'ינקס נכשלה.", + "DialogUpdaterCancelUpdateMessage": "מבטל עדכון!", + "DialogUpdaterAlreadyOnLatestVersionMessage": "אתם כבר משתמשים בגרסה המעודכנת ביותר של ריוג'ינקס!", + "DialogUpdaterFailedToGetVersionMessage": "אירעה שגיאה בעת ניסיון לקבל עדכונים מ-גיטהב. זה יכול להיגרם אם הגרסה המעודכנת האחרונה נוצרה על ידי פעולות של גיטהב. נסה שוב בעוד מספר דקות.", + "DialogUpdaterConvertFailedGithubMessage": "המרת גרסת ריוג'ינקס שהתקבלה מ-עדכון הגרסאות של גיטהב נכשלה.", + "DialogUpdaterDownloadingMessage": "מוריד עדכון...", + "DialogUpdaterExtractionMessage": "מחלץ עדכון...", + "DialogUpdaterRenamingMessage": "משנה את שם העדכון...", + "DialogUpdaterAddingFilesMessage": "מוסיף עדכון חדש...", + "DialogUpdaterShowChangelogMessage": "Show Changelog", + "DialogUpdaterCompleteMessage": "העדכון הושלם!", + "DialogUpdaterRestartMessage": "האם אתם רוצים להפעיל מחדש את ריוג'ינקס עכשיו?", + "DialogUpdaterNoInternetMessage": "אתם לא מחוברים לאינטרנט!", + "DialogUpdaterNoInternetSubMessage": "אנא ודא שיש לך חיבור אינטרנט תקין!", + "DialogUpdaterDirtyBuildMessage": "אתם לא יכולים לעדכן מבנה מלוכלך של ריוג'ינקס!", + "DialogUpdaterDirtyBuildSubMessage": "אם אתם מחפשים גרסא נתמכת, אנא הורידו את ריוג'ינקס בכתובת https://ryujinx.app/download", + "DialogRestartRequiredMessage": "אתחול נדרש", + "DialogThemeRestartMessage": "ערכת הנושא נשמרה. יש צורך בהפעלה מחדש כדי להחיל את ערכת הנושא.", + "DialogThemeRestartSubMessage": "האם ברצונך להפעיל מחדש?", + "DialogFirmwareInstallEmbeddedMessage": "האם תרצו להתקין את הקושחה המוטמעת במשחק הזה? (קושחה {0})", + "DialogFirmwareInstallEmbeddedSuccessMessage": "לא נמצאה קושחה מותקנת אבל ריוג'ינקס הצליח להתקין קושחה {0} מהמשחק שסופק. \nהאמולטור יופעל כעת.", + "DialogFirmwareNoFirmwareInstalledMessage": "לא מותקנת קושחה", + "DialogFirmwareInstalledMessage": "הקושחה {0} הותקנה", + "DialogInstallFileTypesSuccessMessage": "סוגי קבצים הותקנו בהצלחה!", + "DialogInstallFileTypesErrorMessage": "נכשל בהתקנת סוגי קבצים.", + "DialogUninstallFileTypesSuccessMessage": "סוגי קבצים הוסרו בהצלחה!", + "DialogUninstallFileTypesErrorMessage": "נכשל בהסרת סוגי קבצים.", + "DialogOpenSettingsWindowLabel": "פתח את חלון ההגדרות", + "DialogOpenXCITrimmerWindowLabel": "XCI Trimmer Window", + "DialogControllerAppletTitle": "יישומון בקר", + "DialogMessageDialogErrorExceptionMessage": "שגיאה בהצגת דיאלוג ההודעה: {0}", + "DialogSoftwareKeyboardErrorExceptionMessage": "שגיאה בהצגת תוכנת המקלדת: {0}", + "DialogErrorAppletErrorExceptionMessage": "שגיאה בהצגת דיאלוג ErrorApplet: {0}", + "DialogUserErrorDialogMessage": "{0}: {1}", + "DialogUserErrorDialogInfoMessage": "\nלמידע נוסף על איך לתקן שגיאה זו, עקוב אחר מדריך ההתקנה שלנו.", + "DialogUserErrorDialogTitle": "שגיאת Ryujinx ({0})", + "DialogAmiiboApiTitle": "ממשק תכנות אמיבו", + "DialogAmiiboApiFailFetchMessage": "אירעה שגיאה בעת שליפת מידע מהממשק.", + "DialogAmiiboApiConnectErrorMessage": "לא ניתן להתחבר לממשק שרת האמיבו. ייתכן שהשירות מושבת או שתצטרך לוודא שהחיבור לאינטרנט שלך מקוון.", + "DialogProfileInvalidProfileErrorMessage": "הפרופיל {0} אינו תואם למערכת תצורת הקלט הנוכחית.", + "DialogProfileDefaultProfileOverwriteErrorMessage": "לא ניתן להחליף את פרופיל ברירת המחדל", + "DialogProfileDeleteProfileTitle": "מוחק פרופיל", + "DialogProfileDeleteProfileMessage": "פעולה זו היא בלתי הפיכה, האם אתם בטוחים שברצונכם להמשיך?", + "DialogWarning": "אזהרה", + "DialogPPTCDeletionMessage": "אם תמשיכו אתם עומדים לגרום לבנייה מחדש של מטמון ה-PPTC עבור:\n\n{0}", + "DialogPPTCDeletionErrorMessage": "שגיאה בטיהור מטמון PPTC ב-{0}: {1}", + "DialogShaderDeletionMessage": "אם תמשיכו אתם עומדים למחוק את מטמון ההצללות עבור:\n\n{0}", + "DialogShaderDeletionErrorMessage": "שגיאה בניקוי מטמון ההצללות ב-{0}: {1}", + "DialogRyujinxErrorMessage": "ריוג'ינקס נתקלה בשגיאה", + "DialogInvalidTitleIdErrorMessage": "שגיאת ממשק משתמש: למשחק שנבחר לא קיים מזהה משחק", + "DialogFirmwareInstallerFirmwareNotFoundErrorMessage": "לא נמצאה קושחת מערכת תקפה ב-{0}.", + "DialogFirmwareInstallerFirmwareInstallTitle": "התקן קושחה {0}", + "DialogFirmwareInstallerFirmwareInstallMessage": "גירסת המערכת {0} תותקן.", + "DialogFirmwareInstallerFirmwareInstallSubMessage": "\n\nזה יחליף את גרסת המערכת הנוכחית {0}.", + "DialogFirmwareInstallerFirmwareInstallConfirmMessage": "\n\nהאם ברצונך להמשיך?", + "DialogFirmwareInstallerFirmwareInstallWaitMessage": "מתקין קושחה...", + "DialogFirmwareInstallerFirmwareInstallSuccessMessage": "גרסת המערכת {0} הותקנה בהצלחה.", + "DialogUserProfileDeletionWarningMessage": "לא יהיו פרופילים אחרים שייפתחו אם הפרופיל שנבחר יימחק", + "DialogUserProfileDeletionConfirmMessage": "האם ברצונך למחוק את הפרופיל שנבחר", + "DialogUserProfileUnsavedChangesTitle": "אזהרה - שינויים לא שמורים", + "DialogUserProfileUnsavedChangesMessage": "ביצעת שינויים בפרופיל משתמש זה שלא נשמרו.", + "DialogUserProfileUnsavedChangesSubMessage": "האם ברצונך למחוק את השינויים האחרונים?", + "DialogControllerSettingsModifiedConfirmMessage": "הגדרות השלט הנוכחי עודכנו.", + "DialogControllerSettingsModifiedConfirmSubMessage": "האם ברצונך לשמור?", + "DialogLoadFileErrorMessage": "{0}. קובץ שגוי: {1}", + "DialogModAlreadyExistsMessage": "מוד כבר קיים", + "DialogModInvalidMessage": "התיקייה שצוינה אינה מכילה מוד", + "DialogModDeleteNoParentMessage": "נכשל למחוק: לא היה ניתן למצוא את תיקיית האב למוד \"{0}\"!\n", + "DialogDlcNoDlcErrorMessage": "הקובץ שצוין אינו מכיל DLC עבור המשחק שנבחר!", + "DialogPerformanceCheckLoggingEnabledMessage": "הפעלת רישום מעקב, אשר נועד לשמש מפתחים בלבד.", + "DialogPerformanceCheckLoggingEnabledConfirmMessage": "לביצועים מיטביים, מומלץ להשבית את רישום המעקב. האם ברצונך להשבית את רישום המעקב כעת?", + "DialogPerformanceCheckShaderDumpEnabledMessage": "הפעלת השלכת הצללה, שנועדה לשמש מפתחים בלבד.", + "DialogPerformanceCheckShaderDumpEnabledConfirmMessage": "לביצועים מיטביים, מומלץ להשבית את השלכת הצללה. האם ברצונך להשבית את השלכת הצללה עכשיו?", + "DialogLoadAppGameAlreadyLoadedMessage": "ישנו משחק שכבר רץ כעת", + "DialogLoadAppGameAlreadyLoadedSubMessage": "אנא הפסק את האמולציה או סגור את האמולטור לפני הפעלת משחק אחר.", + "DialogUpdateAddUpdateErrorMessage": "הקובץ שצוין אינו מכיל עדכון עבור המשחק שנבחר!", + "DialogSettingsBackendThreadingWarningTitle": "אזהרה - ריבוי תהליכי רקע", + "DialogSettingsBackendThreadingWarningMessage": "יש להפעיל מחדש את ריוג'ינקס לאחר שינוי אפשרות זו כדי שהיא תחול במלואה. בהתאם לפלטפורמה שלך, ייתכן שיהיה עליך להשבית ידנית את ריבוי ההליכים של ההתקן שלך בעת השימוש ב-ריוג'ינקס.", + "DialogModManagerDeletionWarningMessage": "אתה עומד למחוק את המוד: {0}\nהאם אתה בטוח שאתה רוצה להמשיך?", + "DialogModManagerDeletionAllWarningMessage": "אתה עומד למחוק את כל המודים בשביל משחק זה.\n\nהאם אתה בטוח שאתה רוצה להמשיך?", + "SettingsTabGraphicsFeaturesOptions": "אפשרויות", + "SettingsTabGraphicsBackendMultithreading": "אחראי גרפיקה רב-תהליכי:", + "CommonAuto": "אוטומטי", + "CommonOff": "כבוי", + "CommonOn": "דלוק", + "InputDialogYes": "כן", + "InputDialogNo": "לא", + "DialogProfileInvalidProfileNameErrorMessage": "שם הקובץ מכיל תווים לא חוקיים. אנא נסה שוב.", + "MenuBarOptionsPauseEmulation": "הפסק", + "MenuBarOptionsResumeEmulation": "המשך", + "AboutUrlTooltipMessage": "לחץ כדי לפתוח את אתר ריוג'ינקס בדפדפן ברירת המחדל שלך.", + "AboutDisclaimerMessage": "ריוג'ינקס אינה מזוהת עם נינטנדו,\nאו שוטפייה בכל דרך שהיא.", + "AboutAmiiboDisclaimerMessage": "ממשק אמיבו (www.amiiboapi.com) משומש בהדמיית האמיבו שלנו.", + "AboutPatreonUrlTooltipMessage": "לחץ כדי לפתוח את דף הפטראון של ריוג'ינקס בדפדפן ברירת המחדל שלך.", + "AboutGithubUrlTooltipMessage": "לחץ כדי לפתוח את דף הגיטהב של ריוג'ינקס בדפדפן ברירת המחדל שלך.", + "AboutDiscordUrlTooltipMessage": "לחץ כדי לפתוח הזמנה לשרת הדיסקורד של ריוג'ינקס בדפדפן ברירת המחדל שלך.", + "AboutTwitterUrlTooltipMessage": "לחץ כדי לפתוח את דף הטוויטר של Ryujinx בדפדפן ברירת המחדל שלך.", + "AboutRyujinxAboutTitle": "אודות:", + "AboutRyujinxAboutContent": "ריוג'ינקס הוא אמולטור עבור הנינטנדו סוויץ' (כל הזכויות שמורות).\nבבקשה תתמכו בנו בפטראון.\nקבל את כל החדשות האחרונות בטוויטר או בדיסקורד שלנו.\nמפתחים המעוניינים לתרום יכולים לקבל מידע נוסף ב-גיטהאב או ב-דיסקורד שלנו.", + "AboutRyujinxMaintainersTitle": "מתוחזק על ידי:", + "AboutRyujinxMaintainersContentTooltipMessage": "לחץ כדי לפתוח את דף התורמים בדפדפן ברירת המחדל שלך.", + "AboutRyujinxSupprtersTitle": "תמוך באמצעות Patreon", + "AmiiboSeriesLabel": "סדרת אמיבו", + "AmiiboCharacterLabel": "דמות", + "AmiiboScanButtonLabel": "סרוק את זה", + "AmiiboOptionsShowAllLabel": "הצג את כל האמיבואים", + "AmiiboOptionsUsRandomTagLabel": "האצה: השתמש בתג Uuid אקראי", + "DlcManagerTableHeadingEnabledLabel": "מאופשר", + "DlcManagerTableHeadingTitleIdLabel": "מזהה משחק", + "DlcManagerTableHeadingContainerPathLabel": "נתיב מכיל", + "DlcManagerTableHeadingFullPathLabel": "נתיב מלא", + "DlcManagerRemoveAllButton": "מחק הכל", + "DlcManagerEnableAllButton": "אפשר הכל", + "DlcManagerDisableAllButton": "השבת הכל", + "ModManagerDeleteAllButton": "מחק הכל", + "MenuBarOptionsChangeLanguage": "החלף שפה", + "MenuBarShowFileTypes": "הצג מזהה סוג קובץ", + "CommonSort": "מיין", + "CommonShowNames": "הצג שמות", + "CommonFavorite": "מועדף", + "OrderAscending": "סדר עולה", + "OrderDescending": "סדר יורד", + "SettingsTabGraphicsFeatures": "תכונות ושיפורים", + "ErrorWindowTitle": "חלון שגיאה", + "ToggleDiscordTooltip": "בחרו להציג את ריוג'ינקס או לא בפעילות הדיסקורד שלכם \"משוחק כרגע\".", + "AddGameDirBoxTooltip": "הזן תקיית משחקים כדי להוסיף לרשימה", + "AddGameDirTooltip": "הוסף תקיית משחקים לרשימה", + "RemoveGameDirTooltip": "הסר את תקיית המשחקים שנבחרה", + "AddAutoloadDirBoxTooltip": "Enter an autoload directory to add to the list", + "AddAutoloadDirTooltip": "Add an autoload directory to the list", + "RemoveAutoloadDirTooltip": "Remove selected autoload directory", + "CustomThemeCheckTooltip": "השתמש בעיצוב מותאם אישית של אבלוניה עבור ה-ממשק הגראפי כדי לשנות את המראה של תפריטי האמולטור", + "CustomThemePathTooltip": "נתיב לערכת נושא לממשק גראפי מותאם אישית", + "CustomThemeBrowseTooltip": "חפש עיצוב ממשק גראפי מותאם אישית", + "DockModeToggleTooltip": "מצב עגינה גורם למערכת המדומה להתנהג כ-נינטנדו סוויץ' בתחנת עגינתו. זה משפר את הנאמנות הגרפית ברוב המשחקים.\n לעומת זאת, השבתה של תכונה זו תגרום למערכת המדומה להתנהג כ- נינטנדו סוויץ' נייד, ולהפחית את איכות הגרפיקה.\n\nהגדירו את שלט שחקן 1 אם אתם מתכננים להשתמש במצב עגינה; הגדירו את פקדי כף היד אם אתם מתכננים להשתמש במצב נייד.\n\nמוטב להשאיר דלוק אם אתם לא בטוחים.", + "DirectKeyboardTooltip": "Direct keyboard access (HID) support. Provides games access to your keyboard as a text entry device.\n\nOnly works with games that natively support keyboard usage on Switch hardware.\n\nLeave OFF if unsure.", + "DirectMouseTooltip": "Direct mouse access (HID) support. Provides games access to your mouse as a pointing device.\n\nOnly works with games that natively support mouse controls on Switch hardware, which are few and far between.\n\nWhen enabled, touch screen functionality may not work.\n\nLeave OFF if unsure.", + "RegionTooltip": "שנה אזור מערכת", + "LanguageTooltip": "שנה שפת מערכת", + "TimezoneTooltip": "שנה את אזור הזמן של המערכת", + "TimeTooltip": "שנה זמן מערכת", + "VSyncToggleTooltip": "Emulated console's Vertical Sync. Essentially a frame-limiter for the majority of games; disabling it may cause games to run at higher speed or make loading screens take longer or get stuck.\n\nCan be toggled in-game with a hotkey of your preference (F1 by default). We recommend doing this if you plan on disabling it.\n\nLeave ON if unsure.", + "PptcToggleTooltip": "שומר את פונקציות ה-JIT המתורגמות כך שלא יצטרכו לעבור תרגום שוב כאשר משחק עולה.\n\nמפחית תקיעות ומשפר מהירות עלייה של המערכת אחרי הפתיחה הראשונה של המשחק.\n\nמוטב להשאיר דלוק אם לא בטוחים.", + "LowPowerPptcToggleTooltip": "Load the PPTC using a third of the amount of cores.", + "FsIntegrityToggleTooltip": "בודק לקבצים שגויים כאשר משחק עולה, ואם מתגלים כאלו, מציג את מזהה השגיאה שלהם לקובץ הלוג.\n\nאין לכך השפעה על הביצועים ונועד לעזור לבדיקה וניפוי שגיאות של האמולטור.\n\nמוטב להשאיר דלוק אם לא בטוחים.", + "AudioBackendTooltip": "משנה את אחראי השמע.\n\nSDL2 הוא הנבחר, למראת שOpenAL וגם SoundIO משומשים כאפשרויות חלופיות. אפשרות הDummy לא תשמיע קול כלל.\n\nמוטב להשאיר על SDL2 אם לא בטוחים.", + "MemoryManagerTooltip": "שנה איך שזיכרון מארח מיוחד ומונגד. משפיע מאוד על ביצועי המעבד המדומה.\n\nמוטב להשאיר על מארח לא מבוקר אם לא בטוחים.", + "MemoryManagerSoftwareTooltip": "השתמש בתוכנת ה-page table בכדי להתייחס לתרגומים. דיוק מרבי לקונסולה אך המימוש הכי איטי.", + "MemoryManagerHostTooltip": "ממפה זיכרון ישירות לכתובת המארח. מהיר בהרבה ביכולות קימפול ה-JIT והריצה.", + "MemoryManagerUnsafeTooltip": "ממפה זיכרון ישירות, אך לא ממסך את הכתובת בתוך כתובת המארח לפני הגישה. מהיר, אך במחיר של הגנה. יישום המארח בעל גישה לזיכרון מכל מקום בריוג'ינקס, לכן הריצו איתו רק קבצים שאתם סומכים עליהם.", + "UseHypervisorTooltip": "השתמש ב- Hypervisor במקום JIT. משפר מאוד ביצועים כשניתן, אבל יכול להיות לא יציב במצבו הנוכחי.", + "DRamTooltip": "מנצל תצורת מצב-זיכרון חלופית לחכות את מכשיר הפיתוח של הסוויץ'.\n\nזה שימושי להחלפת חבילות מרקמים באיכותיים יותר או כאלו ברזולוציית 4k. לא משפר ביצועים.\n\nמוטב להשאיר כבוי אם לא בטוחים.", + "IgnoreMissingServicesTooltip": "מתעלם מפעולות שלא קיבלו מימוש במערכת ההפעלה Horizon OS. זה עלול לעזור לעקוף קריסות של היישום במשחקים מסויימים.\n\nמוטב להשאיר כבוי אם לא בטוחים.", + "IgnoreAppletTooltip": "תיבת הדו-שיח החיצונית \"Controller Applet\" לא תופיע אם ה-Gamepad מנותק במהלך המשחק. לא תהיה הנחיה לסגור את תיבת הדו-שיח או להגדיר בקר חדש. ברגע שהבקר שנותק בעבר יתחבר מחדש, המשחק יתחדש אוטומטית.", + "GraphicsBackendThreadingTooltip": "מריץ פקודות גראפיקה בתהליך שני נפרד.\n\nמאיץ עיבוד הצללות, מפחית תקיעות ומשפר ביצועים של דרייבר כרטיסי מסך אשר לא תומכים בהרצה רב-תהליכית.\n\nמוטב להשאיר על אוטומטי אם לא בטוחים.", + "GalThreadingTooltip": "מריץ פקודות גראפיקה בתהליך שני נפרד.\n\nמאיץ עיבוד הצללות, מפחית תקיעות ומשפר ביצועים של דרייבר כרטיסי מסך אשר לא תומכים בהרצה רב-תהליכית.\n\nמוטב להשאיר על אוטומטי אם לא בטוחים.", + "ShaderCacheToggleTooltip": "שומר זכרון מטמון של הצללות, דבר שמפחית תקיעות בריצות מסוימות.\n\nמוטב להשאיר דלוק אם לא בטוחים.", + "ResolutionScaleTooltip": "Multiplies the game's rendering resolution.\n\nA few games may not work with this and look pixelated even when the resolution is increased; for those games, you may need to find mods that remove anti-aliasing or that increase their internal rendering resolution. For using the latter, you'll likely want to select Native.\n\nThis option can be changed while a game is running by clicking \"Apply\" below; you can simply move the settings window aside and experiment until you find your preferred look for a game.\n\nKeep in mind 4x is overkill for virtually any setup.", + "ResolutionScaleEntryTooltip": "שיפור רזולוציית נקודה צפה, כגון 1.5. הוא שיפור לא אינטגרלי הנוטה לגרום יותר בעיות או להקריס.", + "AnisotropyTooltip": "Level of Anisotropic Filtering. Set to Auto to use the value requested by the game.", + "AspectRatioTooltip": "Aspect Ratio applied to the renderer window.\n\nOnly change this if you're using an aspect ratio mod for your game, otherwise the graphics will be stretched.\n\nLeave on 16:9 if unsure.", + "ShaderDumpPathTooltip": "נתיב השלכת הצללות גראפיות", + "FileLogTooltip": "שומר את רישומי שורת הפקודות לזיכרון, לא משפיע על ביצועי היישום.", + "StubLogTooltip": "מדפיס רישומים כושלים לשורת הפקודות. לא משפיע על ביצועי היישום.", + "InfoLogTooltip": "מדפיק רישומי מידע לשורת הפקודות. לא משפיע על ביצועי היישום.", + "WarnLogTooltip": "מדפיק רישומי הערות לשורת הפקודות. לא משפיע על ביצועי היישום.", + "ErrorLogTooltip": "מדפיס רישומי שגיאות לשורת הפקודות. לא משפיע על ביצועי היישום.", + "TraceLogTooltip": "מדפיק רישומי זיכרון לשורת הפקודות. לא משפיע על ביצועי היישום.", + "GuestLogTooltip": "מדפיס רישומי אורח לשורת הפקודות. לא משפיע על ביצועי היישום.", + "FileAccessLogTooltip": "מדפיס גישות לקבצי רישום לשורת הפקודות.", + "FSAccessLogModeTooltip": "מאפשר גישה לרישומי FS ליציאת שורת הפקודות. האפשרויות הינן 0-3.", + "DeveloperOptionTooltip": "השתמש בזהירות", + "OpenGlLogLevel": "דורש הפעלת רמות רישום מתאימות", + "DebugLogTooltip": "מדפיס הודעות יומן ניפוי באגים בשורת הפקודות.", + "LoadApplicationFileTooltip": "פתח סייר קבצים כדי לבחור קובץ תואם סוויץ' לטעינה", + "LoadApplicationFolderTooltip": "פתח סייר קבצים כדי לבחור יישום תואם סוויץ', לא ארוז לטעינה.", + "LoadDlcFromFolderTooltip": "Open a file explorer to choose one or more folders to bulk load DLC from", + "LoadTitleUpdatesFromFolderTooltip": "Open a file explorer to choose one or more folders to bulk load title updates from", + "OpenRyujinxFolderTooltip": "פתח את תיקיית מערכת הקבצים ריוג'ינקס", + "OpenRyujinxLogsTooltip": "פותח את התיקיה שאליה נכתבים רישומים", + "ExitTooltip": "צא מריוג'ינקס", + "OpenSettingsTooltip": "פתח את חלון ההגדרות", + "OpenProfileManagerTooltip": "פתח את חלון מנהל פרופילי המשתמש", + "StopEmulationTooltip": "הפסק את הדמייה של המשחק הנוכחי וחזור למסך בחירת המשחק", + "CheckUpdatesTooltip": "בדוק אם קיימים עדכונים לריוג'ינקס", + "OpenAboutTooltip": "פתח את חלון אודות היישום", + "GridSize": "גודל רשת", + "GridSizeTooltip": "שנה את גודל המוצרים על הרשת.", + "SettingsTabSystemSystemLanguageBrazilianPortuguese": "פורטוגלית ברזילאית", + "AboutRyujinxContributorsButtonHeader": "צפה בכל התורמים", + "SettingsTabSystemAudioVolume": "עוצמת קול: ", + "AudioVolumeTooltip": "שנה עוצמת קול", + "SettingsTabSystemEnableInternetAccess": "אפשר גישה לאינטרנט בתור אורח/חיבור לאן", + "EnableInternetAccessTooltip": "מאפשר ליישומים באמולצייה להתחבר לאינטרנט.\n\nמשחקים עם חיבור לאן יכולים להתחבר אחד לשני כשאופצייה זו מופעלת והמערכות מתחברות לאותה נקודת גישה. כמו כן זה כולל שורות פקודות אמיתיות גם.\n\nאופצייה זו לא מאפשרת חיבור לשרתי נינטנדו. כשהאופצייה דלוקה היא עלולה לגרום לקריסת היישום במשחקים מסויימים שמנסים להתחבר לאינטרנט.\n\nמוטב להשאיר כבוי אם לא בטוחים.", + "GameListContextMenuManageCheatToolTip": "נהל צ'יטים", + "GameListContextMenuManageCheat": "נהל צ'יטים", + "GameListContextMenuManageModToolTip": "נהל מודים", + "GameListContextMenuManageMod": "נהל מודים", + "ControllerSettingsStickRange": "טווח:", + "DialogStopEmulationTitle": "ריוג'ינקס - עצור אמולציה", + "DialogStopEmulationMessage": "האם אתם בטוחים שאתם רוצים לסגור את האמולצייה?", + "SettingsTabCpu": "מעבד", + "SettingsTabAudio": "שמע", + "SettingsTabNetwork": "רשת", + "SettingsTabNetworkConnection": "חיבור רשת", + "SettingsTabCpuCache": "מטמון מעבד", + "SettingsTabCpuMemory": "מצב מעבד", + "DialogUpdaterFlatpakNotSupportedMessage": "בבקשה עדכן את ריוג'ינקס דרך פלאטהב.", + "UpdaterDisabledWarningTitle": "מעדכן מושבת!", + "ControllerSettingsRotate90": "סובב 90° עם בכיוון השעון", + "IconSize": "גודל הסמל", + "IconSizeTooltip": "שנה את גודל הסמלים של משחקים", + "MenuBarOptionsShowConsole": "הצג שורת פקודות", + "ShaderCachePurgeError": "שגיאה בניקוי מטמון ההצללות ב-{0}: {1}", + "UserErrorNoKeys": "המפתחות לא נמצאו", + "UserErrorNoFirmware": "קושחה לא נמצאה", + "UserErrorFirmwareParsingFailed": "שגיאת ניתוח קושחה", + "UserErrorApplicationNotFound": "יישום לא נמצא", + "UserErrorUnknown": "שגיאה לא ידועה", + "UserErrorUndefined": "שגיאה לא מוגדרת", + "UserErrorNoKeysDescription": "ריוג'ינקס לא הצליח למצוא את קובץ ה-'prod.keys' שלך", + "UserErrorNoFirmwareDescription": "ריוג'ינקס לא הצליחה למצוא קושחה מותקנת", + "UserErrorFirmwareParsingFailedDescription": "ריוג'ינקס לא הצליחה לנתח את הקושחה שסופקה. זה נגרם בדרך כלל על ידי מפתחות לא עדכניים.", + "UserErrorApplicationNotFoundDescription": "ריוג'ינקס לא מצאה יישום תקין בנתיב הנתון", + "UserErrorUnknownDescription": "קרתה שגיאה לא ידועה!", + "UserErrorUndefinedDescription": "קרתה שגיאה לא מוגדרת! זה לא אמור לקרות, אנא צרו קשר עם מפתח!", + "OpenSetupGuideMessage": "פתח מדריך התקנה", + "NoUpdate": "אין עדכון", + "TitleUpdateVersionLabel": "גרסה {0}", + "TitleBundledUpdateVersionLabel": "Bundled: Version {0}", + "TitleBundledDlcLabel": "Bundled:", + "TitleXCIStatusPartialLabel": "Partial", + "TitleXCIStatusTrimmableLabel": "Untrimmed", + "TitleXCIStatusUntrimmableLabel": "Trimmed", + "TitleXCIStatusFailedLabel": "(Failed)", + "TitleXCICanSaveLabel": "Save {0:n0} Mb", + "TitleXCISavingLabel": "Saved {0:n0} Mb", + "RyujinxInfo": "ריוג'ינקס - מידע", + "RyujinxConfirm": "ריוג'ינקס - אישור", + "FileDialogAllTypes": "כל הסוגים", + "Never": "אף פעם", + "SwkbdMinCharacters": "לפחות {0} תווים", + "SwkbdMinRangeCharacters": "באורך {0}-{1} תווים", + "SoftwareKeyboard": "מקלדת וירטואלית", + "SoftwareKeyboardModeNumeric": "חייב להיות בין 0-9 או '.' בלבד", + "SoftwareKeyboardModeAlphabet": "מחויב להיות ללא אותיות CJK", + "SoftwareKeyboardModeASCII": "מחויב להיות טקסט אסקיי", + "ControllerAppletControllers": "בקרים נתמכים:", + "ControllerAppletPlayers": "שחקנים:", + "ControllerAppletDescription": "התצורה הנוכחית אינה תקינה. פתח הגדרות והגדר מחדש את הקלטים שלך.", + "ControllerAppletDocked": "מצב עגינה מוגדר. כדאי ששליטה ניידת תהיה מושבתת.", + "UpdaterRenaming": "משנה שמות של קבצים ישנים...", + "UpdaterRenameFailed": "המעדכן לא הצליח לשנות את שם הקובץ: {0}", + "UpdaterAddingFiles": "מוסיף קבצים חדשים...", + "UpdaterExtracting": "מחלץ עדכון...", + "UpdaterDownloading": "מוריד עדכון...", + "Game": "משחק", + "Docked": "בתחנת עגינה", + "Handheld": "נייד", + "ConnectionError": "שגיאת חיבור", + "AboutPageDeveloperListMore": "{0} ועוד...", + "ApiError": "שגיאת ממשק.", + "LoadingHeading": "טוען {0}", + "CompilingPPTC": "קימפול PTC", + "CompilingShaders": "קימפול הצללות", + "AllKeyboards": "כל המקלדות", + "OpenFileDialogTitle": "בחר קובץ נתמך לפתיחה", + "OpenFolderDialogTitle": "בחר תיקיה עם משחק לא ארוז", + "AllSupportedFormats": "כל הפורמטים הנתמכים", + "RyujinxUpdater": "מעדכן ריוג'ינקס", + "SettingsTabHotkeys": "מקשי קיצור במקלדת", + "SettingsTabHotkeysHotkeys": "מקשי קיצור במקלדת", + "SettingsTabHotkeysToggleVsyncHotkey": "שנה סינכרון אנכי:", + "SettingsTabHotkeysScreenshotHotkey": "צילום מסך:", + "SettingsTabHotkeysShowUiHotkey": "הצג ממשק משתמש:", + "SettingsTabHotkeysPauseHotkey": "הפסק:", + "SettingsTabHotkeysToggleMuteHotkey": "השתק:", + "ControllerMotionTitle": "הגדרות שליטת תנועות ג'ירוסקופ", + "ControllerRumbleTitle": "הגדרות רטט", + "SettingsSelectThemeFileDialogTitle": "בחרו קובץ ערכת נושא", + "SettingsXamlThemeFile": "קובץ ערכת נושא Xaml", + "AvatarWindowTitle": "ניהול חשבונות - אוואטר", + "Amiibo": "אמיבו", + "Unknown": "לא ידוע", + "Usage": "שימוש", + "Writable": "ניתן לכתיבה", + "SelectDlcDialogTitle": "בחרו קבצי הרחבות משחק", + "SelectUpdateDialogTitle": "בחרו קבצי עדכון", + "SelectModDialogTitle": "בחר תיקיית מודים", + "TrimXCIFileDialogTitle": "Check and Trim XCI File", + "TrimXCIFileDialogPrimaryText": "This function will first check the empty space and then trim the XCI File to save disk space.", + "TrimXCIFileDialogSecondaryText": "Current File Size: {0:n} MB\nGame Data Size: {1:n} MB\nDisk Space Savings: {2:n} MB", + "TrimXCIFileNoTrimNecessary": "XCI File does not need to be trimmed. Check logs for further details", + "TrimXCIFileNoUntrimPossible": "XCI File cannot be untrimmed. Check logs for further details", + "TrimXCIFileReadOnlyFileCannotFix": "XCI File is Read Only and could not be made writable. Check logs for further details", + "TrimXCIFileFileSizeChanged": "XCI File has changed in size since it was scanned. Please check the file is not being written to and try again.", + "TrimXCIFileFreeSpaceCheckFailed": "XCI File has data in the free space area, it is not safe to trim", + "TrimXCIFileInvalidXCIFile": "XCI File contains invalid data. Check logs for further details", + "TrimXCIFileFileIOWriteError": "XCI File could not be opened for writing. Check logs for further details", + "TrimXCIFileFailedPrimaryText": "Trimming of the XCI file failed", + "TrimXCIFileCancelled": "The operation was cancelled", + "TrimXCIFileFileUndertermined": "No operation was performed", + "UserProfileWindowTitle": "ניהול פרופילי משתמש", + "CheatWindowTitle": "נהל צ'יטים למשחק", + "DlcWindowTitle": "נהל הרחבות משחק עבור {0} ({1})", + "ModWindowTitle": "Manage Mods for {0} ({1})", + "UpdateWindowTitle": "נהל עדכוני משחקים", + "XCITrimmerWindowTitle": "XCI File Trimmer", + "XCITrimmerTitleStatusCount": "{0} of {1} Title(s) Selected", + "XCITrimmerTitleStatusCountWithFilter": "{0} of {1} Title(s) Selected ({2} displayed)", + "XCITrimmerTitleStatusTrimming": "Trimming {0} Title(s)...", + "XCITrimmerTitleStatusUntrimming": "Untrimming {0} Title(s)...", + "XCITrimmerTitleStatusFailed": "Failed", + "XCITrimmerPotentialSavings": "Potential Savings", + "XCITrimmerActualSavings": "Actual Savings", + "XCITrimmerSavingsMb": "{0:n0} Mb", + "XCITrimmerSelectDisplayed": "Select Shown", + "XCITrimmerDeselectDisplayed": "Deselect Shown", + "XCITrimmerSortName": "Title", + "XCITrimmerSortSaved": "Space Savings", + "XCITrimmerTrim": "Trim", + "XCITrimmerUntrim": "Untrim", + "UpdateWindowUpdateAddedMessage": "{0} new update(s) added", + "UpdateWindowBundledContentNotice": "Bundled updates cannot be removed, only disabled.", + "CheatWindowHeading": "צ'יטים זמינים עבור {0} [{1}]", + "BuildId": "מזהה בניה:", + "DlcWindowBundledContentNotice": "Bundled DLC cannot be removed, only disabled.", + "DlcWindowHeading": "{0} הרחבות משחק", + "DlcWindowDlcAddedMessage": "{0} new downloadable content(s) added", + "AutoloadDlcAddedMessage": "{0} new downloadable content(s) added", + "AutoloadDlcRemovedMessage": "{0} missing downloadable content(s) removed", + "AutoloadUpdateAddedMessage": "{0} new update(s) added", + "AutoloadUpdateRemovedMessage": "{0} missing update(s) removed", + "ModWindowHeading": "{0} מוד(ים)", + "UserProfilesEditProfile": "ערוך נבחר/ים", + "Continue": "Continue", + "Cancel": "בטל", + "Save": "שמור", + "Discard": "השלך", + "Paused": "מושהה", + "UserProfilesSetProfileImage": "הגדר תמונת פרופיל", + "UserProfileEmptyNameError": "נדרש שם", + "UserProfileNoImageError": "נדרשת תמונת פרופיל", + "GameUpdateWindowHeading": "נהל עדכונים עבור {0} ({1})", + "SettingsTabHotkeysResScaleUpHotkey": "שפר רזולוציה:", + "SettingsTabHotkeysResScaleDownHotkey": "הפחת רזולוציה:", + "UserProfilesName": "שם:", + "UserProfilesUserId": "מזהה משתמש:", + "SettingsTabGraphicsBackend": "אחראי גראפיקה", + "SettingsTabGraphicsBackendTooltip": "Select the graphics backend that will be used in the emulator.\n\nVulkan is overall better for all modern graphics cards, as long as their drivers are up to date. Vulkan also features faster shader compilation (less stuttering) on all GPU vendors.\n\nOpenGL may achieve better results on old Nvidia GPUs, on old AMD GPUs on Linux, or on GPUs with lower VRAM, though shader compilation stutters will be greater.\n\nSet to Vulkan if unsure. Set to OpenGL if your GPU does not support Vulkan even with the latest graphics drivers.", + "SettingsEnableTextureRecompression": "אפשר דחיסה מחדש של המרקם", + "SettingsEnableTextureRecompressionTooltip": "Compresses ASTC textures in order to reduce VRAM usage.\n\nGames using this texture format include Astral Chain, Bayonetta 3, Fire Emblem Engage, Metroid Prime Remastered, Super Mario Bros. Wonder and The Legend of Zelda: Tears of the Kingdom.\n\nGraphics cards with 4GiB VRAM or less will likely crash at some point while running these games.\n\nEnable only if you're running out of VRAM on the aforementioned games. Leave OFF if unsure.", + "SettingsTabGraphicsPreferredGpu": "כרטיס גראפי מועדף", + "SettingsTabGraphicsPreferredGpuTooltip": "בחר את הכרטיס הגראפי שישומש עם הגראפיקה של וולקאן.\n\nדבר זה לא משפיע על הכרטיס הגראפי שישומש עם OpenGL.\n\nמוטב לבחור את ה-GPU המסומן כ-\"dGPU\" אם אינכם בטוחים, אם זו לא אופצייה, אל תשנו דבר.", + "SettingsAppRequiredRestartMessage": "ריוג'ינקס דורש אתחול מחדש", + "SettingsGpuBackendRestartMessage": "הגדרות אחראי גרפיקה או כרטיס גראפי שונו. זה ידרוש הפעלה מחדש כדי להחיל שינויים", + "SettingsGpuBackendRestartSubMessage": "האם ברצונך להפעיל מחדש כעט?", + "RyujinxUpdaterMessage": "האם ברצונך לעדכן את ריוג'ינקס לגרסא האחרונה?", + "SettingsTabHotkeysVolumeUpHotkey": "הגבר את עוצמת הקול:", + "SettingsTabHotkeysVolumeDownHotkey": "הנמך את עוצמת הקול:", + "SettingsEnableMacroHLE": "Enable Macro HLE", + "SettingsEnableMacroHLETooltip": "אמולצייה ברמה גבוהה של כרטיס גראפי עם קוד מקרו.\n\nמשפר את ביצועי היישום אך עלול לגרום לגליצ'ים חזותיים במשחקים מסויימים.\n\nמוטב להשאיר דלוק אם אינך בטוח.", + "SettingsEnableColorSpacePassthrough": "שקיפות מרחב צבע", + "SettingsEnableColorSpacePassthroughTooltip": "מנחה את המנוע Vulkan להעביר שקיפות בצבעים מבלי לציין מרחב צבע. עבור משתמשים עם מסכים רחבים, הדבר עשוי לגרום לצבעים מרהיבים יותר, בחוסר דיוק בצבעים האמתיים.", + "VolumeShort": "שמע", + "UserProfilesManageSaves": "נהל שמורים", + "DeleteUserSave": "האם ברצונך למחוק את תקיית השמור למשחק זה?", + "IrreversibleActionNote": "הפעולה הזו בלתי הפיכה.", + "SaveManagerHeading": "נהל שמורי משחק עבור {0} ({1})", + "SaveManagerTitle": "מנהל שמירות", + "Name": "שם", + "Size": "גודל", + "Search": "חפש", + "UserProfilesRecoverLostAccounts": "שחזר חשבון שאבד", + "Recover": "שחזר", + "UserProfilesRecoverHeading": "שמורים נמצאו לחשבונות הבאים", + "UserProfilesRecoverEmptyList": "אין פרופילים לשחזור", + "GraphicsAATooltip": "Applies anti-aliasing to the game render.\n\nFXAA will blur most of the image, while SMAA will attempt to find jagged edges and smooth them out.\n\nNot recommended to use in conjunction with the FSR scaling filter.\n\nThis option can be changed while a game is running by clicking \"Apply\" below; you can simply move the settings window aside and experiment until you find your preferred look for a game.\n\nLeave on NONE if unsure.", + "GraphicsAALabel": "החלקת-עקומות:", + "GraphicsScalingFilterLabel": "מסנן מידת איכות:", + "GraphicsScalingFilterTooltip": "Choose the scaling filter that will be applied when using resolution scale.\n\nBilinear works well for 3D games and is a safe default option.\n\nNearest is recommended for pixel art games.\n\nFSR 1.0 is merely a sharpening filter, not recommended for use with FXAA or SMAA.\n\nThis option can be changed while a game is running by clicking \"Apply\" below; you can simply move the settings window aside and experiment until you find your preferred look for a game.\n\nLeave on BILINEAR if unsure.", + "GraphicsScalingFilterBilinear": "Bilinear", + "GraphicsScalingFilterNearest": "Nearest", + "GraphicsScalingFilterFsr": "FSR", + "GraphicsScalingFilterArea": "Area", + "GraphicsScalingFilterLevelLabel": "רמה", + "GraphicsScalingFilterLevelTooltip": "Set FSR 1.0 sharpening level. Higher is sharper.", + "SmaaLow": "SMAA נמוך", + "SmaaMedium": "SMAA בינוני", + "SmaaHigh": "SMAA גבוהה", + "SmaaUltra": "SMAA אולטרה", + "UserEditorTitle": "ערוך משתמש", + "UserEditorTitleCreate": "צור משתמש", + "SettingsTabNetworkInterface": "ממשק רשת", + "NetworkInterfaceTooltip": "The network interface used for LAN/LDN features.\n\nIn conjunction with a VPN or XLink Kai and a game with LAN support, can be used to spoof a same-network connection over the Internet.\n\nLeave on DEFAULT if unsure.", + "NetworkInterfaceDefault": "ברירת המחדל", + "PackagingShaders": "אורז הצללות", + "AboutChangelogButton": "צפה במידע אודות שינויים בגיטהב", + "AboutChangelogButtonTooltipMessage": "לחץ כדי לפתוח את יומן השינויים עבור גרסה זו בדפדפן ברירת המחדל שלך.", + "SettingsTabNetworkMultiplayer": "רב משתתפים", + "MultiplayerMode": "מצב:", + "MultiplayerModeTooltip": "Change LDN multiplayer mode.\n\nLdnMitm will modify local wireless/local play functionality in games to function as if it were LAN, allowing for local, same-network connections with other Ryujinx instances and hacked Nintendo Switch consoles that have the ldn_mitm module installed.\n\nMultiplayer requires all players to be on the same game version (i.e. Super Smash Bros. Ultimate v13.0.1 can't connect to v13.0.0).\n\nLeave DISABLED if unsure.", + "MultiplayerModeDisabled": "Disabled", + "MultiplayerModeLdnMitm": "ldn_mitm", + "MultiplayerModeLdnRyu": "RyuLDN", + "MultiplayerDisableP2P": "Disable P2P Network Hosting (may increase latency)", + "MultiplayerDisableP2PTooltip": "Disable P2P network hosting, peers will proxy through the master server instead of connecting to you directly.", + "LdnPassphrase": "Network Passphrase:", + "LdnPassphraseTooltip": "You will only be able to see hosted games with the same passphrase as you.", + "LdnPassphraseInputTooltip": "Enter a passphrase in the format Ryujinx-<8 hex chars>. You will only be able to see hosted games with the same passphrase as you.", + "LdnPassphraseInputPublic": "(public)", + "GenLdnPass": "Generate Random", + "GenLdnPassTooltip": "Generates a new passphrase, which can be shared with other players.", + "ClearLdnPass": "Clear", + "ClearLdnPassTooltip": "Clears the current passphrase, returning to the public network.", + "InvalidLdnPassphrase": "Invalid Passphrase! Must be in the format \"Ryujinx-<8 hex chars>\"" +} diff --git a/src/Ryujinx/Assets/Locales/it_IT.json b/src/Ryujinx/Assets/Locales/it_IT.json new file mode 100644 index 000000000..52ea833d3 --- /dev/null +++ b/src/Ryujinx/Assets/Locales/it_IT.json @@ -0,0 +1,868 @@ +{ + "Language": "Italiano", + "MenuBarFileOpenApplet": "Apri applet", + "MenuBarFileOpenAppletOpenMiiApplet": "Mii Edit Applet", + "MenuBarFileOpenAppletOpenMiiAppletToolTip": "Apri l'applet Mii Editor in modalità Standalone", + "SettingsTabInputDirectMouseAccess": "Accesso diretto al mouse", + "SettingsTabSystemMemoryManagerMode": "Modalità di gestione della memoria:", + "SettingsTabSystemMemoryManagerModeSoftware": "Software", + "SettingsTabSystemMemoryManagerModeHost": "Host (veloce)", + "SettingsTabSystemMemoryManagerModeHostUnchecked": "Host Unchecked (più veloce, non sicura)", + "SettingsTabSystemUseHypervisor": "Usa Hypervisor", + "MenuBarFile": "_File", + "MenuBarFileOpenFromFile": "_Carica applicazione da un file", + "MenuBarFileOpenUnpacked": "Carica _gioco estratto", + "MenuBarFileOpenEmuFolder": "Apri cartella di Ryujinx", + "MenuBarFileOpenLogsFolder": "Apri cartella dei log", + "MenuBarFileExit": "_Esci", + "MenuBarOptions": "_Opzioni", + "MenuBarOptionsToggleFullscreen": "Schermo intero", + "MenuBarOptionsStartGamesInFullscreen": "Avvia i giochi a schermo intero", + "MenuBarOptionsStopEmulation": "Ferma emulazione", + "MenuBarOptionsSettings": "_Impostazioni", + "MenuBarOptionsManageUserProfiles": "_Gestisci i profili utente", + "MenuBarActions": "_Azioni", + "MenuBarOptionsSimulateWakeUpMessage": "Simula messaggio Wake-up", + "MenuBarActionsScanAmiibo": "Scansiona un Amiibo", + "MenuBarTools": "_Strumenti", + "MenuBarToolsInstallFirmware": "Installa firmware", + "MenuBarFileToolsInstallFirmwareFromFile": "Installa un firmware da file XCI o ZIP", + "MenuBarFileToolsInstallFirmwareFromDirectory": "Installa un firmare da una cartella", + "MenuBarToolsManageFileTypes": "Gestisci i tipi di file", + "MenuBarToolsInstallFileTypes": "Installa i tipi di file", + "MenuBarToolsUninstallFileTypes": "Disinstalla i tipi di file", + "MenuBarFileLoadDlcFromFolder": "Carica DLC Da una Cartella", + "MenuBarFileLoadTitleUpdatesFromFolder": "Carica Aggiornamenti Da una Cartella", + "MenuBarFileOpenFromFileError": "Nessuna applicazione trovata nel file selezionato", + "MenuBarToolsXCITrimmer": "Trim XCI Files", + "MenuBarView": "_Vista", + "MenuBarViewWindow": "Dimensione Finestra", + "MenuBarViewWindow720": "720p", + "MenuBarViewWindow1080": "1080p", + "MenuBarHelp": "_Aiuto", + "MenuBarHelpCheckForUpdates": "Controlla aggiornamenti", + "MenuBarHelpAbout": "Informazioni", + "MenuSearch": "Cerca...", + "GameListHeaderFavorite": "Preferito", + "GameListHeaderIcon": "Icona", + "GameListHeaderApplication": "Nome", + "GameListHeaderDeveloper": "Sviluppatore", + "GameListHeaderVersion": "Versione", + "GameListHeaderTimePlayed": "Tempo di gioco", + "GameListHeaderLastPlayed": "Ultima partita", + "GameListHeaderFileExtension": "Estensione", + "GameListHeaderFileSize": "Dimensione file", + "GameListHeaderPath": "Percorso", + "GameListContextMenuOpenUserSaveDirectory": "Apri la cartella dei salvataggi dell'utente", + "GameListContextMenuOpenUserSaveDirectoryToolTip": "Apre la cartella che contiene i dati di salvataggio dell'utente", + "GameListContextMenuOpenDeviceSaveDirectory": "Apri la cartella dei salvataggi del dispositivo", + "GameListContextMenuOpenDeviceSaveDirectoryToolTip": "Apre la cartella che contiene i dati di salvataggio del dispositivo", + "GameListContextMenuOpenBcatSaveDirectory": "Apri la cartella del salvataggio BCAT", + "GameListContextMenuOpenBcatSaveDirectoryToolTip": "Apre la cartella che contiene il salvataggio BCAT dell'applicazione", + "GameListContextMenuManageTitleUpdates": "Gestisci aggiornamenti del gioco", + "GameListContextMenuManageTitleUpdatesToolTip": "Apre la finestra di gestione aggiornamenti del gioco", + "GameListContextMenuManageDlc": "Gestisci DLC", + "GameListContextMenuManageDlcToolTip": "Apre la finestra di gestione dei DLC", + "GameListContextMenuCacheManagement": "Gestione della cache", + "GameListContextMenuCacheManagementPurgePptc": "Accoda rigenerazione della cache PPTC", + "GameListContextMenuCacheManagementPurgePptcToolTip": "Esegue la rigenerazione della cache PPTC al prossimo avvio del gioco", + "GameListContextMenuCacheManagementPurgeShaderCache": "Elimina la cache degli shader", + "GameListContextMenuCacheManagementPurgeShaderCacheToolTip": "Elimina la cache degli shader dell'applicazione", + "GameListContextMenuCacheManagementOpenPptcDirectory": "Apri la cartella della cache PPTC", + "GameListContextMenuCacheManagementOpenPptcDirectoryToolTip": "Apre la cartella che contiene la cache PPTC dell'applicazione", + "GameListContextMenuCacheManagementOpenShaderCacheDirectory": "Apri la cartella della cache degli shader", + "GameListContextMenuCacheManagementOpenShaderCacheDirectoryToolTip": "Apre la cartella che contiene la cache degli shader dell'applicazione", + "GameListContextMenuExtractData": "Estrai dati", + "GameListContextMenuExtractDataExeFS": "ExeFS", + "GameListContextMenuExtractDataExeFSToolTip": "Estrae la sezione ExeFS dall'attuale configurazione dell'applicazione (includendo aggiornamenti)", + "GameListContextMenuExtractDataRomFS": "RomFS", + "GameListContextMenuExtractDataRomFSToolTip": "Estrae la sezione RomFS dall'attuale configurazione dell'applicazione (includendo aggiornamenti)", + "GameListContextMenuExtractDataLogo": "Logo", + "GameListContextMenuExtractDataLogoToolTip": "Estrae la sezione Logo dall'attuale configurazione dell'applicazione (includendo aggiornamenti)", + "GameListContextMenuCreateShortcut": "Crea collegamento", + "GameListContextMenuCreateShortcutToolTip": "Crea un collegamento sul desktop che avvia l'applicazione selezionata", + "GameListContextMenuCreateShortcutToolTipMacOS": "Crea un collegamento nella cartella Applicazioni di macOS che avvia l'applicazione selezionata", + "GameListContextMenuOpenModsDirectory": "Apri la cartella delle mod", + "GameListContextMenuOpenModsDirectoryToolTip": "Apre la cartella che contiene le mod dell'applicazione", + "GameListContextMenuOpenSdModsDirectory": "Apri la cartella delle mod Atmosphere", + "GameListContextMenuOpenSdModsDirectoryToolTip": "Apre la cartella alternativa di Atmosphere sulla scheda SD che contiene le mod dell'applicazione. Utile per le mod create per funzionare sull'hardware reale.", + "GameListContextMenuTrimXCI": "Controlla e Trimma i file XCI", + "GameListContextMenuTrimXCIToolTip": "Controlla e Trimma i file XCI da Salvare Sullo Spazio del Disco", + "StatusBarGamesLoaded": "{0}/{1} Giochi Caricati", + "StatusBarSystemVersion": "Versione di sistema: {0}", + "StatusBarXCIFileTrimming": "Trimmando i file XCI '{0}'", + "LinuxVmMaxMapCountDialogTitle": "Rilevato limite basso per le mappature di memoria", + "LinuxVmMaxMapCountDialogTextPrimary": "Vuoi aumentare il valore di vm.max_map_count a {0}?", + "LinuxVmMaxMapCountDialogTextSecondary": "Alcuni giochi potrebbero provare a creare più mappature di memoria di quanto sia attualmente consentito. Ryujinx si bloccherà non appena questo limite viene superato.", + "LinuxVmMaxMapCountDialogButtonUntilRestart": "Sì, fino al prossimo riavvio", + "LinuxVmMaxMapCountDialogButtonPersistent": "Sì, in modo permanente", + "LinuxVmMaxMapCountWarningTextPrimary": "La quantità massima di mappature di memoria è inferiore a quella consigliata.", + "LinuxVmMaxMapCountWarningTextSecondary": "Il valore corrente di vm.max_map_count ({0}) è inferiore a {1}. Alcuni giochi potrebbero provare a creare più mappature di memoria di quanto sia attualmente consentito. Ryujinx si bloccherà non appena questo limite viene superato.\n\nPotresti voler aumentare manualmente il limite o installare pkexec, il che permette a Ryujinx di assisterlo.", + "Settings": "Impostazioni", + "SettingsTabGeneral": "Interfaccia utente", + "SettingsTabGeneralGeneral": "Generali", + "SettingsTabGeneralEnableDiscordRichPresence": "Attiva Discord Rich Presence", + "SettingsTabGeneralCheckUpdatesOnLaunch": "Controlla aggiornamenti all'avvio", + "SettingsTabGeneralShowConfirmExitDialog": "Mostra dialogo \"Conferma Uscita\"", + "SettingsTabGeneralRememberWindowState": "Ricorda Dimensione/Posizione Finestra", + "SettingsTabGeneralShowTitleBar": "Mostra barra del titolo (Richiede il riavvio)", + "SettingsTabGeneralHideCursor": "Nascondi il cursore:", + "SettingsTabGeneralHideCursorNever": "Mai", + "SettingsTabGeneralHideCursorOnIdle": "Quando è inattivo", + "SettingsTabGeneralHideCursorAlways": "Sempre", + "SettingsTabGeneralGameDirectories": "Cartelle dei giochi", + "SettingsTabGeneralAdd": "Aggiungi", + "SettingsTabGeneralRemove": "Rimuovi", + "SettingsTabSystem": "Sistema", + "SettingsTabSystemCore": "Principale", + "SettingsTabSystemSystemRegion": "Regione del sistema:", + "SettingsTabSystemSystemRegionJapan": "Giappone", + "SettingsTabSystemSystemRegionUSA": "Stati Uniti d'America", + "SettingsTabSystemSystemRegionEurope": "Europa", + "SettingsTabGeneralAutoloadDirectories": "Directory di Caricamento Automatico per DLC/Aggiornamenti", + "SettingsTabGeneralAutoloadNote": "Aggiornamenti e DLC che collegano a file mancanti verranno disabilitati automaticamente", + "SettingsTabSystemSystemRegionAustralia": "Australia", + "SettingsTabSystemSystemRegionChina": "Cina", + "SettingsTabSystemSystemRegionKorea": "Corea", + "SettingsTabSystemSystemRegionTaiwan": "Taiwan", + "SettingsTabSystemSystemLanguage": "Lingua del sistema:", + "SettingsTabSystemSystemLanguageJapanese": "Giapponese", + "SettingsTabSystemSystemLanguageAmericanEnglish": "Inglese americano", + "SettingsTabSystemSystemLanguageFrench": "Francese", + "SettingsTabSystemSystemLanguageGerman": "Tedesco", + "SettingsTabSystemSystemLanguageItalian": "Italiano", + "SettingsTabSystemSystemLanguageSpanish": "Spagnolo", + "SettingsTabSystemSystemLanguageChinese": "Cinese", + "SettingsTabSystemSystemLanguageKorean": "Coreano", + "SettingsTabSystemSystemLanguageDutch": "Olandese", + "SettingsTabSystemSystemLanguagePortuguese": "Portoghese", + "SettingsTabSystemSystemLanguageRussian": "Russo", + "SettingsTabSystemSystemLanguageTaiwanese": "Taiwanese", + "SettingsTabSystemSystemLanguageBritishEnglish": "Inglese britannico", + "SettingsTabSystemSystemLanguageCanadianFrench": "Francese canadese", + "SettingsTabSystemSystemLanguageLatinAmericanSpanish": "Spagnolo latino americano", + "SettingsTabSystemSystemLanguageSimplifiedChinese": "Cinese semplificato", + "SettingsTabSystemSystemLanguageTraditionalChinese": "Cinese tradizionale", + "SettingsTabSystemSystemTimeZone": "Fuso orario del sistema:", + "SettingsTabSystemSystemTime": "Data e ora del sistema:", + "SettingsTabSystemEnableVsync": "Attiva VSync", + "SettingsTabSystemEnablePptc": "Attiva PPTC (Profiled Persistent Translation Cache)", + "SettingsTabSystemEnableLowPowerPptc": "Low-power PPTC", + "SettingsTabSystemEnableFsIntegrityChecks": "Attiva controlli d'integrità FS", + "SettingsTabSystemAudioBackend": "Backend audio:", + "SettingsTabSystemAudioBackendDummy": "Dummy", + "SettingsTabSystemAudioBackendOpenAL": "OpenAL", + "SettingsTabSystemAudioBackendSoundIO": "SoundIO", + "SettingsTabSystemAudioBackendSDL2": "SDL2", + "SettingsTabSystemHacks": "Espedienti", + "SettingsTabSystemHacksNote": "Possono causare instabilità", + "SettingsTabSystemDramSize": "Usa layout di memoria alternativo (per sviluppatori)", + "SettingsTabSystemDramSize4GiB": "4GiB", + "SettingsTabSystemDramSize6GiB": "6GiB", + "SettingsTabSystemDramSize8GiB": "8GiB", + "SettingsTabSystemDramSize12GiB": "12GiB", + "SettingsTabSystemIgnoreMissingServices": "Ignora servizi mancanti", + "SettingsTabSystemIgnoreApplet": "Ignora l'applet", + "SettingsTabGraphics": "Grafica", + "SettingsTabGraphicsAPI": "API grafica", + "SettingsTabGraphicsEnableShaderCache": "Attiva la cache degli shader", + "SettingsTabGraphicsAnisotropicFiltering": "Filtro anisotropico:", + "SettingsTabGraphicsAnisotropicFilteringAuto": "Auto", + "SettingsTabGraphicsAnisotropicFiltering2x": "2x", + "SettingsTabGraphicsAnisotropicFiltering4x": "4x", + "SettingsTabGraphicsAnisotropicFiltering8x": "8x", + "SettingsTabGraphicsAnisotropicFiltering16x": "16x", + "SettingsTabGraphicsResolutionScale": "Scala della risoluzione:", + "SettingsTabGraphicsResolutionScaleCustom": "Personalizzata (Non raccomandata)", + "SettingsTabGraphicsResolutionScaleNative": "Nativa (720p/1080p)", + "SettingsTabGraphicsResolutionScale2x": "2x (1440p/2160p)", + "SettingsTabGraphicsResolutionScale3x": "3x (2160p/3240p)", + "SettingsTabGraphicsResolutionScale4x": "4x (2880p/4320p) (Non consigliato)", + "SettingsTabGraphicsAspectRatio": "Rapporto d'aspetto:", + "SettingsTabGraphicsAspectRatio4x3": "4:3", + "SettingsTabGraphicsAspectRatio16x9": "16:9", + "SettingsTabGraphicsAspectRatio16x10": "16:10", + "SettingsTabGraphicsAspectRatio21x9": "21:9", + "SettingsTabGraphicsAspectRatio32x9": "32:9", + "SettingsTabGraphicsAspectRatioStretch": "Adatta alla finestra", + "SettingsTabGraphicsDeveloperOptions": "Opzioni per sviluppatori", + "SettingsTabGraphicsShaderDumpPath": "Percorso di dump degli shader:", + "SettingsTabLogging": "Log", + "SettingsTabLoggingLogging": "Log", + "SettingsTabLoggingEnableLoggingToFile": "Salva i log su file", + "SettingsTabLoggingEnableStubLogs": "Attiva log di stub", + "SettingsTabLoggingEnableInfoLogs": "Attiva log di informazioni", + "SettingsTabLoggingEnableWarningLogs": "Attiva log di avviso", + "SettingsTabLoggingEnableErrorLogs": "Attiva log di errore", + "SettingsTabLoggingEnableTraceLogs": "Attiva log di trace", + "SettingsTabLoggingEnableGuestLogs": "Attiva log del guest", + "SettingsTabLoggingEnableFsAccessLogs": "Attiva log di accesso FS", + "SettingsTabLoggingFsGlobalAccessLogMode": "Modalità log di accesso globale FS:", + "SettingsTabLoggingDeveloperOptions": "Opzioni per sviluppatori", + "SettingsTabLoggingDeveloperOptionsNote": "ATTENZIONE: ridurrà le prestazioni", + "SettingsTabLoggingGraphicsBackendLogLevel": "Livello di log del backend grafico:", + "SettingsTabLoggingGraphicsBackendLogLevelNone": "Nessuno", + "SettingsTabLoggingGraphicsBackendLogLevelError": "Errore", + "SettingsTabLoggingGraphicsBackendLogLevelPerformance": "Rallentamenti", + "SettingsTabLoggingGraphicsBackendLogLevelAll": "Tutto", + "SettingsTabLoggingEnableDebugLogs": "Attiva log di debug", + "SettingsTabInput": "Input", + "SettingsTabInputEnableDockedMode": "Attiva modalità TV", + "SettingsTabInputDirectKeyboardAccess": "Accesso diretto alla tastiera", + "SettingsButtonSave": "Salva", + "SettingsButtonClose": "Chiudi", + "SettingsButtonOk": "OK", + "SettingsButtonCancel": "Annulla", + "SettingsButtonApply": "Applica", + "ControllerSettingsPlayer": "Giocatore", + "ControllerSettingsPlayer1": "Giocatore 1", + "ControllerSettingsPlayer2": "Giocatore 2", + "ControllerSettingsPlayer3": "Giocatore 3", + "ControllerSettingsPlayer4": "Giocatore 4", + "ControllerSettingsPlayer5": "Giocatore 5", + "ControllerSettingsPlayer6": "Giocatore 6", + "ControllerSettingsPlayer7": "Giocatore 7", + "ControllerSettingsPlayer8": "Giocatore 8", + "ControllerSettingsHandheld": "Portatile", + "ControllerSettingsInputDevice": "Dispositivo di input", + "ControllerSettingsRefresh": "Ricarica", + "ControllerSettingsDeviceDisabled": "Disabilitato", + "ControllerSettingsControllerType": "Tipo di controller", + "ControllerSettingsControllerTypeHandheld": "Portatile", + "ControllerSettingsControllerTypeProController": "Pro Controller", + "ControllerSettingsControllerTypeJoyConPair": "Coppia di JoyCon", + "ControllerSettingsControllerTypeJoyConLeft": "JoyCon sinistro", + "ControllerSettingsControllerTypeJoyConRight": "JoyCon destro", + "ControllerSettingsProfile": "Profilo", + "ControllerSettingsProfileDefault": "Predefinito", + "ControllerSettingsLoad": "Carica", + "ControllerSettingsAdd": "Aggiungi", + "ControllerSettingsRemove": "Rimuovi", + "ControllerSettingsButtons": "Pulsanti", + "ControllerSettingsButtonA": "A", + "ControllerSettingsButtonB": "B", + "ControllerSettingsButtonX": "X", + "ControllerSettingsButtonY": "Y", + "ControllerSettingsButtonPlus": "+", + "ControllerSettingsButtonMinus": "-", + "ControllerSettingsDPad": "Croce direzionale", + "ControllerSettingsDPadUp": "Su", + "ControllerSettingsDPadDown": "Giù", + "ControllerSettingsDPadLeft": "Sinistra", + "ControllerSettingsDPadRight": "Destra", + "ControllerSettingsStickButton": "Pulsante", + "ControllerSettingsStickUp": "Su", + "ControllerSettingsStickDown": "Giù", + "ControllerSettingsStickLeft": "Sinistra", + "ControllerSettingsStickRight": "Destra", + "ControllerSettingsStickStick": "Levetta", + "ControllerSettingsStickInvertXAxis": "Inverti levetta X", + "ControllerSettingsStickInvertYAxis": "Inverti levetta Y", + "ControllerSettingsStickDeadzone": "Zona morta:", + "ControllerSettingsLStick": "Levetta sinistra", + "ControllerSettingsRStick": "Levetta destra", + "ControllerSettingsTriggersLeft": "Grilletto sinistro", + "ControllerSettingsTriggersRight": "Grilletto destro", + "ControllerSettingsTriggersButtonsLeft": "Pulsante dorsale sinistro", + "ControllerSettingsTriggersButtonsRight": "Pulsante dorsale destro", + "ControllerSettingsTriggers": "Grilletti", + "ControllerSettingsTriggerL": "L", + "ControllerSettingsTriggerR": "R", + "ControllerSettingsTriggerZL": "ZL", + "ControllerSettingsTriggerZR": "ZR", + "ControllerSettingsLeftSL": "SL", + "ControllerSettingsLeftSR": "SR", + "ControllerSettingsRightSL": "SL", + "ControllerSettingsRightSR": "SR", + "ControllerSettingsExtraButtonsLeft": "Tasto sinistro", + "ControllerSettingsExtraButtonsRight": "Tasto destro", + "ControllerSettingsMisc": "Varie", + "ControllerSettingsTriggerThreshold": "Sensibilità dei grilletti:", + "ControllerSettingsMotion": "Movimento", + "ControllerSettingsMotionUseCemuhookCompatibleMotion": "Usa sensore compatibile con CemuHook", + "ControllerSettingsMotionControllerSlot": "Slot del controller:", + "ControllerSettingsMotionMirrorInput": "Input specchiato", + "ControllerSettingsMotionRightJoyConSlot": "Slot JoyCon destro:", + "ControllerSettingsMotionServerHost": "Server:", + "ControllerSettingsMotionGyroSensitivity": "Sensibilità del giroscopio:", + "ControllerSettingsMotionGyroDeadzone": "Zona morta del giroscopio:", + "ControllerSettingsSave": "Salva", + "ControllerSettingsClose": "Chiudi", + "KeyUnknown": "Sconosciuto", + "KeyShiftLeft": "Maiusc sinistro", + "KeyShiftRight": "Maiusc destro", + "KeyControlLeft": "Ctrl sinistro", + "KeyMacControlLeft": "⌃ sinistro", + "KeyControlRight": "Ctrl destro", + "KeyMacControlRight": "⌃ destro", + "KeyAltLeft": "Alt sinistro", + "KeyMacAltLeft": "⌥ sinistro", + "KeyAltRight": "Alt destro", + "KeyMacAltRight": "⌥ destro", + "KeyWinLeft": "⊞ sinistro", + "KeyMacWinLeft": "⌘ sinistro", + "KeyWinRight": "⊞ destro", + "KeyMacWinRight": "⌘ destro", + "KeyMenu": "Menù", + "KeyUp": "Su", + "KeyDown": "Giù", + "KeyLeft": "Sinistra", + "KeyRight": "Destra", + "KeyEnter": "Invio", + "KeyEscape": "Esc", + "KeySpace": "Spazio", + "KeyTab": "Tab", + "KeyBackSpace": "Backspace", + "KeyInsert": "Ins", + "KeyDelete": "Canc", + "KeyPageUp": "Pag. Su", + "KeyPageDown": "Pag. Giù", + "KeyHome": "Inizio", + "KeyEnd": "Fine", + "KeyCapsLock": "Bloc Maiusc", + "KeyScrollLock": "Bloc Scorr", + "KeyPrintScreen": "Stamp", + "KeyPause": "Pausa", + "KeyNumLock": "Bloc Num", + "KeyClear": "Clear", + "KeyKeypad0": "Tast. num. 0", + "KeyKeypad1": "Tast. num. 1", + "KeyKeypad2": "Tast. num. 2", + "KeyKeypad3": "Tast. num. 3", + "KeyKeypad4": "Tast. num. 4", + "KeyKeypad5": "Tast. num. 5", + "KeyKeypad6": "Tast. num. 6", + "KeyKeypad7": "Tast. num. 7", + "KeyKeypad8": "Tast. num. 8", + "KeyKeypad9": "Tast. num. 9", + "KeyKeypadDivide": "Tast. num. /", + "KeyKeypadMultiply": "Tast. num. *", + "KeyKeypadSubtract": "Tast. num. -", + "KeyKeypadAdd": "Tast. num. +", + "KeyKeypadDecimal": "Tast. num. sep. decimale", + "KeyKeypadEnter": "Tast. num. Invio", + "KeyNumber0": "0", + "KeyNumber1": "1", + "KeyNumber2": "2", + "KeyNumber3": "3", + "KeyNumber4": "4", + "KeyNumber5": "5", + "KeyNumber6": "6", + "KeyNumber7": "7", + "KeyNumber8": "8", + "KeyNumber9": "9", + "KeyTilde": "ò", + "KeyGrave": "`", + "KeyMinus": "-", + "KeyPlus": "+", + "KeyBracketLeft": "'", + "KeyBracketRight": "ì", + "KeySemicolon": "è", + "KeyQuote": "à", + "KeyComma": ",", + "KeyPeriod": ".", + "KeySlash": "ù", + "KeyBackSlash": "<", + "KeyUnbound": "Non assegnato", + "GamepadLeftStick": "Pulsante levetta sinistra", + "GamepadRightStick": "Pulsante levetta destra", + "GamepadLeftShoulder": "Pulsante dorsale sinistro", + "GamepadRightShoulder": "Pulsante dorsale destro", + "GamepadLeftTrigger": "Grilletto sinistro", + "GamepadRightTrigger": "Grilletto destro", + "GamepadDpadUp": "Su", + "GamepadDpadDown": "Giù", + "GamepadDpadLeft": "Sinistra", + "GamepadDpadRight": "Destra", + "GamepadMinus": "-", + "GamepadPlus": "+", + "GamepadGuide": "Guide", + "GamepadMisc1": "Misc", + "GamepadPaddle1": "Paddle 1", + "GamepadPaddle2": "Paddle 2", + "GamepadPaddle3": "Paddle 3", + "GamepadPaddle4": "Paddle 4", + "GamepadTouchpad": "Touchpad", + "GamepadSingleLeftTrigger0": "Grilletto sinistro 0", + "GamepadSingleRightTrigger0": "Grilletto destro 0", + "GamepadSingleLeftTrigger1": "Grilletto sinistro 1", + "GamepadSingleRightTrigger1": "Grilletto destro 1", + "StickLeft": "Levetta sinistra", + "StickRight": "Levetta destra", + "UserProfilesSelectedUserProfile": "Profilo utente selezionato:", + "UserProfilesSaveProfileName": "Salva nome del profilo", + "UserProfilesChangeProfileImage": "Cambia immagine profilo", + "UserProfilesAvailableUserProfiles": "Profili utente disponibili:", + "UserProfilesAddNewProfile": "Aggiungi nuovo profilo", + "UserProfilesDelete": "Elimina", + "UserProfilesClose": "Chiudi", + "ProfileNameSelectionWatermark": "Scegli un soprannome", + "ProfileImageSelectionTitle": "Selezione dell'immagine profilo", + "ProfileImageSelectionHeader": "Scegli un'immagine profilo", + "ProfileImageSelectionNote": "Puoi importare un'immagine profilo personalizzata o selezionare un avatar dal firmware del sistema", + "ProfileImageSelectionImportImage": "Importa file immagine", + "ProfileImageSelectionSelectAvatar": "Seleziona avatar dal firmware", + "InputDialogTitle": "Finestra di input", + "InputDialogOk": "OK", + "InputDialogCancel": "Annulla", + "InputDialogCancelling": "Cancellando", + "InputDialogClose": "Chiudi", + "InputDialogAddNewProfileTitle": "Scegli il nome del profilo", + "InputDialogAddNewProfileHeader": "Digita un nome profilo", + "InputDialogAddNewProfileSubtext": "(Lunghezza massima: {0})", + "AvatarChoose": "Scegli", + "AvatarSetBackgroundColor": "Imposta colore di sfondo", + "AvatarClose": "Chiudi", + "ControllerSettingsLoadProfileToolTip": "Carica profilo", + "ControllerSettingsViewProfileToolTip": "Visualizza profilo", + "ControllerSettingsAddProfileToolTip": "Aggiungi profilo", + "ControllerSettingsRemoveProfileToolTip": "Rimuovi profilo", + "ControllerSettingsSaveProfileToolTip": "Salva profilo", + "MenuBarFileToolsTakeScreenshot": "Cattura uno screenshot", + "MenuBarFileToolsHideUi": "Nascondi l'interfaccia", + "GameListContextMenuRunApplication": "Esegui applicazione", + "GameListContextMenuToggleFavorite": "Preferito", + "GameListContextMenuToggleFavoriteToolTip": "Segna il gioco come preferito", + "SettingsTabGeneralTheme": "Tema:", + "SettingsTabGeneralThemeAuto": "Automatico", + "SettingsTabGeneralThemeDark": "Scuro", + "SettingsTabGeneralThemeLight": "Chiaro", + "ControllerSettingsConfigureGeneral": "Configura", + "ControllerSettingsRumble": "Vibrazione", + "ControllerSettingsRumbleStrongMultiplier": "Moltiplicatore vibrazione forte", + "ControllerSettingsRumbleWeakMultiplier": "Moltiplicatore vibrazione debole", + "DialogMessageSaveNotAvailableMessage": "Non ci sono dati di salvataggio per {0} [{1:x16}]", + "DialogMessageSaveNotAvailableCreateSaveMessage": "Vuoi creare dei dati di salvataggio per questo gioco?", + "DialogConfirmationTitle": "Ryujinx - Conferma", + "DialogUpdaterTitle": "Ryujinx - Aggiornamento", + "DialogErrorTitle": "Ryujinx - Errore", + "DialogWarningTitle": "Ryujinx - Avviso", + "DialogExitTitle": "Ryujinx - Esci", + "DialogErrorMessage": "Ryujinx ha riscontrato un problema", + "DialogExitMessage": "Sei sicuro di voler chiudere Ryujinx?", + "DialogExitSubMessage": "Tutti i dati non salvati andranno persi!", + "DialogMessageCreateSaveErrorMessage": "C'è stato un errore durante la creazione dei dati di salvataggio: {0}", + "DialogMessageFindSaveErrorMessage": "C'è stato un errore durante la ricerca dei dati di salvataggio: {0}", + "FolderDialogExtractTitle": "Scegli una cartella in cui estrarre", + "DialogNcaExtractionMessage": "Estrazione della sezione {0} da {1}...", + "DialogNcaExtractionTitle": "Estrazione sezione NCA", + "DialogNcaExtractionMainNcaNotFoundErrorMessage": "L'estrazione è fallita. L'NCA principale non era presente nel file selezionato.", + "DialogNcaExtractionCheckLogErrorMessage": "L'estrazione è fallita. Consulta il file di log per maggiori informazioni.", + "DialogNcaExtractionSuccessMessage": "Estrazione completata con successo.", + "DialogUpdaterConvertFailedMessage": "La conversione dell'attuale versione di Ryujinx è fallita.", + "DialogUpdaterCancelUpdateMessage": "Annullamento dell'aggiornamento in corso!", + "DialogUpdaterAlreadyOnLatestVersionMessage": "Stai già usando la versione più recente di Ryujinx!", + "DialogUpdaterFailedToGetVersionMessage": "Si è verificato un errore durante il tentativo di recuperare le informazioni sulla versione da GitHub Release. Ciò può verificarsi se una nuova versione è in fase di compilazione da GitHub Actions. Riprova tra qualche minuto.", + "DialogUpdaterConvertFailedGithubMessage": "La conversione della versione di Ryujinx ricevuta da Github Release è fallita.", + "DialogUpdaterDownloadingMessage": "Download dell'aggiornamento...", + "DialogUpdaterExtractionMessage": "Estrazione dell'aggiornamento...", + "DialogUpdaterRenamingMessage": "Rinominazione dell'aggiornamento...", + "DialogUpdaterAddingFilesMessage": "Aggiunta del nuovo aggiornamento...", + "DialogUpdaterShowChangelogMessage": "Show Changelog", + "DialogUpdaterCompleteMessage": "Aggiornamento completato!", + "DialogUpdaterRestartMessage": "Vuoi riavviare Ryujinx adesso?", + "DialogUpdaterNoInternetMessage": "Non sei connesso ad Internet!", + "DialogUpdaterNoInternetSubMessage": "Verifica di avere una connessione ad Internet funzionante!", + "DialogUpdaterDirtyBuildMessage": "Non puoi aggiornare una Dirty build di Ryujinx!", + "DialogUpdaterDirtyBuildSubMessage": "Scarica Ryujinx da https://ryujinx.app/download se stai cercando una versione supportata.", + "DialogRestartRequiredMessage": "Riavvio richiesto", + "DialogThemeRestartMessage": "Il tema è stato salvato. È richiesto un riavvio per applicare il tema.", + "DialogThemeRestartSubMessage": "Vuoi riavviare?", + "DialogFirmwareInstallEmbeddedMessage": "Vuoi installare il firmware incorporato in questo gioco? (Firmware {0})", + "DialogFirmwareInstallEmbeddedSuccessMessage": "Non è stato trovato alcun firmware installato, ma Ryujinx è riuscito ad installare il firmware {0} dal gioco fornito.\nL'emulatore si avvierà adesso.", + "DialogFirmwareNoFirmwareInstalledMessage": "Nessun firmware installato", + "DialogFirmwareInstalledMessage": "Il firmware {0} è stato installato", + "DialogInstallFileTypesSuccessMessage": "Tipi di file installati con successo!", + "DialogInstallFileTypesErrorMessage": "Impossibile installare i tipi di file.", + "DialogUninstallFileTypesSuccessMessage": "Tipi di file disinstallati con successo!", + "DialogUninstallFileTypesErrorMessage": "Disinstallazione dei tipi di file non riuscita.", + "DialogOpenSettingsWindowLabel": "Apri finestra delle impostazioni", + "DialogOpenXCITrimmerWindowLabel": "Finestra XCI Trimmer", + "DialogControllerAppletTitle": "Applet del controller", + "DialogMessageDialogErrorExceptionMessage": "Errore nella visualizzazione del Message Dialog: {0}", + "DialogSoftwareKeyboardErrorExceptionMessage": "Errore nella visualizzazione della tastiera software: {0}", + "DialogErrorAppletErrorExceptionMessage": "Errore nella visualizzazione dell'ErrorApplet Dialog: {0}", + "DialogUserErrorDialogMessage": "{0}: {1}", + "DialogUserErrorDialogInfoMessage": "\nPer maggiori informazioni su come risolvere questo errore, segui la nostra guida all'installazione.", + "DialogUserErrorDialogTitle": "Errore di Ryujinx ({0})", + "DialogAmiiboApiTitle": "Amiibo API", + "DialogAmiiboApiFailFetchMessage": "Si è verificato un errore durante il recupero delle informazioni dall'API.", + "DialogAmiiboApiConnectErrorMessage": "Impossibile connettersi al server Amiibo API. Il servizio potrebbe essere fuori uso o potresti dover verificare che la tua connessione internet sia online.", + "DialogProfileInvalidProfileErrorMessage": "Il profilo {0} è incompatibile con l'attuale sistema di configurazione input.", + "DialogProfileDefaultProfileOverwriteErrorMessage": "Il profilo predefinito non può essere sovrascritto", + "DialogProfileDeleteProfileTitle": "Eliminazione profilo", + "DialogProfileDeleteProfileMessage": "Quest'azione è irreversibile, sei sicuro di voler continuare?", + "DialogWarning": "Avviso", + "DialogPPTCDeletionMessage": "Stai per accodare la rigenerazione della cache PPTC al prossimo avvio per:\n\n{0}\n\nSei sicuro di voler proseguire?", + "DialogPPTCDeletionErrorMessage": "Errore nell'eliminazione della cache PPTC a {0}: {1}", + "DialogShaderDeletionMessage": "Stai per eliminare la cache degli shader per:\n\n{0}\n\nSei sicuro di voler proseguire?", + "DialogShaderDeletionErrorMessage": "Errore nell'eliminazione della cache degli shader a {0}: {1}", + "DialogRyujinxErrorMessage": "Ryujinx ha incontrato un errore", + "DialogInvalidTitleIdErrorMessage": "Errore UI: Il gioco selezionato non ha un ID titolo valido", + "DialogFirmwareInstallerFirmwareNotFoundErrorMessage": "Un firmware del sistema valido non è stato trovato in {0}.", + "DialogFirmwareInstallerFirmwareInstallTitle": "Installa firmware {0}", + "DialogFirmwareInstallerFirmwareInstallMessage": "La versione del sistema {0} sarà installata.", + "DialogFirmwareInstallerFirmwareInstallSubMessage": "\n\nQuesta sostituirà l'attuale versione di sistema {0}.", + "DialogFirmwareInstallerFirmwareInstallConfirmMessage": "\n\nVuoi continuare?", + "DialogFirmwareInstallerFirmwareInstallWaitMessage": "Installazione del firmware...", + "DialogFirmwareInstallerFirmwareInstallSuccessMessage": "La versione del sistema {0} è stata installata.", + "DialogUserProfileDeletionWarningMessage": "Non ci sarebbero altri profili da aprire se il profilo selezionato viene cancellato", + "DialogUserProfileDeletionConfirmMessage": "Vuoi eliminare il profilo selezionato?", + "DialogUserProfileUnsavedChangesTitle": "Attenzione - Modifiche Non Salvate", + "DialogUserProfileUnsavedChangesMessage": "Hai apportato modifiche a questo profilo utente che non sono state salvate.", + "DialogUserProfileUnsavedChangesSubMessage": "Vuoi scartare le modifiche?", + "DialogControllerSettingsModifiedConfirmMessage": "Le attuali impostazioni del controller sono state aggiornate.", + "DialogControllerSettingsModifiedConfirmSubMessage": "Vuoi salvare?", + "DialogLoadFileErrorMessage": "{0}. Errore File: {1}", + "DialogModAlreadyExistsMessage": "La mod risulta già installata", + "DialogModInvalidMessage": "La cartella specificata non contiene nessuna mod!", + "DialogModDeleteNoParentMessage": "Eliminazione non riuscita: impossibile trovare la directory superiore per la mod \"{0}\"!", + "DialogDlcNoDlcErrorMessage": "Il file specificato non contiene un DLC per il titolo selezionato!", + "DialogPerformanceCheckLoggingEnabledMessage": "Hai abilitato il trace logging, che è progettato per essere usato solo dagli sviluppatori.", + "DialogPerformanceCheckLoggingEnabledConfirmMessage": "Per prestazioni ottimali, si raccomanda di disabilitare il trace logging. Vuoi disabilitarlo adesso?", + "DialogPerformanceCheckShaderDumpEnabledMessage": "Hai abilitato il dump degli shader, che è progettato per essere usato solo dagli sviluppatori.", + "DialogPerformanceCheckShaderDumpEnabledConfirmMessage": "Per prestazioni ottimali, si raccomanda di disabilitare il dump degli shader. Vuoi disabilitarlo adesso?", + "DialogLoadAppGameAlreadyLoadedMessage": "Un gioco è già stato caricato", + "DialogLoadAppGameAlreadyLoadedSubMessage": "Ferma l'emulazione o chiudi l'emulatore prima di avviare un altro gioco.", + "DialogUpdateAddUpdateErrorMessage": "Il file specificato non contiene un aggiornamento per il titolo selezionato!", + "DialogSettingsBackendThreadingWarningTitle": "Avviso - Backend Threading", + "DialogSettingsBackendThreadingWarningMessage": "Ryujinx deve essere riavviato dopo aver cambiato questa opzione per applicarla completamente. A seconda della tua piattaforma, potrebbe essere necessario disabilitare manualmente il multithreading del driver quando usi quello di Ryujinx.", + "DialogModManagerDeletionWarningMessage": "Stai per eliminare la mod: {0}\n\nConfermi di voler procedere?", + "DialogModManagerDeletionAllWarningMessage": "Stai per eliminare tutte le mod per questo titolo.\n\nVuoi davvero procedere?", + "SettingsTabGraphicsFeaturesOptions": "Funzionalità", + "SettingsTabGraphicsBackendMultithreading": "Multithreading del backend grafico:", + "CommonAuto": "Automatico", + "CommonOff": "Disattivato", + "CommonOn": "Attivo", + "InputDialogYes": "Sì", + "InputDialogNo": "No", + "DialogProfileInvalidProfileNameErrorMessage": "Il nome del file contiene dei caratteri non validi. Riprova.", + "MenuBarOptionsPauseEmulation": "Metti in pausa", + "MenuBarOptionsResumeEmulation": "Riprendi", + "AboutUrlTooltipMessage": "Clicca per aprire il sito web di Ryujinx nel tuo browser predefinito.", + "AboutDisclaimerMessage": "Ryujinx non è affiliato con Nintendo™,\no i suoi partner, in alcun modo.", + "AboutAmiiboDisclaimerMessage": "AmiiboAPI (www.amiiboapi.com) è usata\nnella nostra emulazione Amiibo.", + "AboutPatreonUrlTooltipMessage": "Clicca per aprire la pagina Patreon di Ryujinx nel tuo browser predefinito.", + "AboutGithubUrlTooltipMessage": "Clicca per aprire la pagina GitHub di Ryujinx nel tuo browser predefinito.", + "AboutDiscordUrlTooltipMessage": "Clicca per aprire un invito al server Discord di Ryujinx nel tuo browser predefinito.", + "AboutTwitterUrlTooltipMessage": "Clicca per aprire la pagina Twitter di Ryujinx nel tuo browser predefinito.", + "AboutRyujinxAboutTitle": "Informazioni:", + "AboutRyujinxAboutContent": "Ryujinx è un emulatore per la console Nintendo Switch™.\nSostienici su Patreon.\nRicevi tutte le ultime notizie sul nostro Twitter o su Discord.\nGli sviluppatori interessati a contribuire possono trovare più informazioni sul nostro GitHub o Discord.", + "AboutRyujinxMaintainersTitle": "Mantenuto da:", + "AboutRyujinxMaintainersContentTooltipMessage": "Clicca per aprire la pagina dei contributori nel tuo browser predefinito.", + "AboutRyujinxSupprtersTitle": "Supportato su Patreon da:", + "AmiiboSeriesLabel": "Serie Amiibo", + "AmiiboCharacterLabel": "Personaggio", + "AmiiboScanButtonLabel": "Scansiona", + "AmiiboOptionsShowAllLabel": "Mostra tutti gli amiibo", + "AmiiboOptionsUsRandomTagLabel": "Espediente: Usa un UUID del tag casuale", + "DlcManagerTableHeadingEnabledLabel": "Abilitato", + "DlcManagerTableHeadingTitleIdLabel": "ID Titolo", + "DlcManagerTableHeadingContainerPathLabel": "Percorso del contenitore", + "DlcManagerTableHeadingFullPathLabel": "Percorso completo", + "DlcManagerRemoveAllButton": "Rimuovi tutti", + "DlcManagerEnableAllButton": "Abilita tutto", + "DlcManagerDisableAllButton": "Disabilita tutto", + "ModManagerDeleteAllButton": "Elimina tutto", + "MenuBarOptionsChangeLanguage": "Cambia lingua", + "MenuBarShowFileTypes": "Mostra tipi di file", + "CommonSort": "Ordina", + "CommonShowNames": "Mostra nomi", + "CommonFavorite": "Preferito", + "OrderAscending": "Crescente", + "OrderDescending": "Decrescente", + "SettingsTabGraphicsFeatures": "Funzionalità e miglioramenti", + "ErrorWindowTitle": "Finestra di errore", + "ToggleDiscordTooltip": "Scegli se mostrare o meno Ryujinx nella tua attività su Discord", + "AddGameDirBoxTooltip": "Inserisci una cartella dei giochi per aggiungerla alla lista", + "AddGameDirTooltip": "Aggiungi una cartella dei giochi alla lista", + "RemoveGameDirTooltip": "Rimuovi la cartella dei giochi selezionata", + "CustomThemeCheckTooltip": "Attiva o disattiva temi personalizzati nella GUI", + "CustomThemePathTooltip": "Percorso al tema GUI personalizzato", + "CustomThemeBrowseTooltip": "Sfoglia per cercare un tema GUI personalizzato", + "RemoveAutoloadDirTooltip": "Rimuovi la directory di autoload selezionata", + "DockModeToggleTooltip": "La modalità TV fa sì che il sistema emulato si comporti come una Nintendo Switch posizionata nella sua base. Ciò migliora la qualità grafica nella maggior parte dei giochi. Al contrario, disabilitandola il sistema emulato si comporterà come una Nintendo Switch in modalità portatile, riducendo la qualità grafica.\n\nConfigura i controlli del giocatore 1 se intendi usare la modalità TV; configura i controlli della modalità portatile se intendi usare quest'ultima.\n\nNel dubbio, lascia l'opzione attiva.", + "DirectKeyboardTooltip": "Supporto per l'accesso diretto alla tastiera (HID). Fornisce ai giochi l'accesso alla tastiera come dispositivo di inserimento del testo.\n\nFunziona solo con i giochi che supportano nativamente l'utilizzo della tastiera su hardware Switch.\n\nNel dubbio, lascia l'opzione disattivata.", + "DirectMouseTooltip": "Supporto per l'accesso diretto al mouse (HID). Fornisce ai giochi l'accesso al mouse come dispositivo di puntamento.\n\nFunziona solo con i rari giochi che supportano nativamente l'utilizzo del mouse su hardware Switch.\n\nQuando questa opzione è attivata, il touchscreen potrebbe non funzionare.\n\nNel dubbio, lascia l'opzione disattivata.", + "RegionTooltip": "Cambia regione di sistema", + "LanguageTooltip": "Cambia lingua di sistema", + "TimezoneTooltip": "Cambia fuso orario di sistema", + "TimeTooltip": "Cambia data e ora di sistema", + "VSyncToggleTooltip": "Sincronizzazione verticale della console Emulata. Essenzialmente un limitatore di frame per la maggior parte dei giochi; disabilitarlo può far girare giochi a velocità più alta, allungare le schermate di caricamento o farle bloccare.\n\nPuò essere attivata in gioco con un tasto di scelta rapida (F1 per impostazione predefinita). Ti consigliamo di farlo se hai intenzione di disabilitarlo.\n\nLascia ON se non sei sicuro.", + "PptcToggleTooltip": "Salva le funzioni JIT tradotte in modo che non debbano essere tradotte tutte le volte che si avvia un determinato gioco.\n\nRiduce i fenomeni di stuttering e velocizza sensibilmente gli avvii successivi del gioco.\n\nNel dubbio, lascia l'opzione attiva.", + "AddAutoloadDirBoxTooltip": "Inserisci una directory di \"autoload\" da aggiungere alla lista", + "AddAutoloadDirTooltip": "Aggiungi una directory di \"autoload\" alla lista", + "LowPowerPptcToggleTooltip": "Carica il PPTC usando un terzo dei core.", + "FsIntegrityToggleTooltip": "Controlla la presenza di file corrotti quando si avvia un gioco. Se vengono rilevati dei file corrotti, verrà mostrato un errore di hash nel log.\n\nQuesta opzione non influisce sulle prestazioni ed è pensata per facilitare la risoluzione dei problemi.\n\nNel dubbio, lascia l'opzione attiva.", + "AudioBackendTooltip": "Cambia il backend usato per riprodurre l'audio.\n\nSDL2 è quello preferito, mentre OpenAL e SoundIO sono usati come ripiego. Dummy non riprodurrà alcun suono.\n\nNel dubbio, imposta l'opzione su SDL2.", + "MemoryManagerTooltip": "Cambia il modo in cui la memoria guest è mappata e vi si accede. Influisce notevolmente sulle prestazioni della CPU emulata.\n\nNel dubbio, imposta l'opzione su Host Unchecked.", + "MemoryManagerSoftwareTooltip": "Usa una software page table per la traduzione degli indirizzi. Massima precisione ma prestazioni più lente.", + "MemoryManagerHostTooltip": "Mappa direttamente la memoria nello spazio degli indirizzi dell'host. Compilazione ed esecuzione JIT molto più veloce.", + "MemoryManagerUnsafeTooltip": "Mappa direttamente la memoria, ma non maschera l'indirizzo all'interno dello spazio degli indirizzi guest prima dell'accesso. Più veloce, ma a costo della sicurezza. L'applicazione guest può accedere alla memoria da qualsiasi punto di Ryujinx, quindi esegui solo programmi di cui ti fidi con questa modalità.", + "UseHypervisorTooltip": "Usa Hypervisor invece di JIT. Migliora notevolmente le prestazioni quando disponibile, ma può essere instabile nel suo stato attuale.", + "DRamTooltip": "Utilizza un layout di memoria alternativo per imitare un'unità di sviluppo di Switch.\n\nQuesta opzione è utile soltanto per i pacchetti di texture ad alta risoluzione o per le mod che aumentano la risoluzione a 4K. NON migliora le prestazioni.\n\nNel dubbio, lascia l'opzione disattivata.", + "IgnoreMissingServicesTooltip": "Ignora i servizi non implementati del sistema operativo Horizon. Può aiutare ad aggirare gli arresti anomali che si verificano avviando alcuni giochi.\n\nNel dubbio, lascia l'opzione disattivata.", + "IgnoreAppletTooltip": "La finestra di dialogo esterna \"Controller Applet\" non apparirà se il gamepad viene disconnesso durante il gioco. Non ci sarà alcun prompt per chiudere la finestra di dialogo o impostare un nuovo controller. Una volta che il controller disconnesso in precedenza viene ricollegato, il gioco riprenderà automaticamente.", + "GraphicsBackendThreadingTooltip": "Esegue i comandi del backend grafico su un secondo thread.\n\nVelocizza la compilazione degli shader, riduce lo stuttering e migliora le prestazioni sui driver grafici senza il supporto integrato al multithreading. Migliora leggermente le prestazioni sui driver che supportano il multithreading.\n\nNel dubbio, imposta l'opzione su Auto.", + "GalThreadingTooltip": "Esegue i comandi del backend grafico su un secondo thread.\n\nVelocizza la compilazione degli shader, riduce lo stuttering e migliora le prestazioni sui driver grafici senza il supporto integrato al multithreading. Migliora leggermente le prestazioni sui driver che supportano il multithreading.\n\nNel dubbio, imposta l'opzione su Auto.", + "ShaderCacheToggleTooltip": "Salva una cache degli shader su disco che riduce i fenomeni di stuttering nelle esecuzioni successive.\n\nNel dubbio, lascia l'opzione attiva.", + "ResolutionScaleTooltip": "Moltiplica la risoluzione di rendering del gioco.\n\nAlcuni giochi potrebbero non funzionare con questa opzione e sembrare pixelati anche quando la risoluzione è aumentata; per quei giochi, potrebbe essere necessario trovare mod che rimuovono l'anti-aliasing o che aumentano la risoluzione di rendering interna. Per quest'ultimo caso, probabilmente dovrai selezionare Nativo (1x).\n\nQuesta opzione può essere modificata mentre un gioco è in esecuzione facendo clic su \"Applica\" qui sotto; puoi semplicemente spostare la finestra delle impostazioni da parte e sperimentare fino a quando non trovi il tuo look preferito per un gioco.\n\nTenete a mente che 4x è troppo per praticamente qualsiasi configurazione.", + "ResolutionScaleEntryTooltip": "Scala della risoluzione in virgola mobile, come 1,5. Le scale non integrali hanno maggiori probabilità di causare problemi o crash.", + "AnisotropyTooltip": "Livello del filtro anisotropico. Imposta su Auto per usare il valore richiesto dal gioco.", + "AspectRatioTooltip": "Proporzioni dello schermo applicate alla finestra di renderizzazione.\n\nCambialo solo se stai usando una mod di proporzioni per il tuo gioco, altrimenti la grafica verrà allungata.\n\nLasciare il 16:9 se incerto.", + "ShaderDumpPathTooltip": "Percorso di dump degli shader", + "FileLogTooltip": "Salva il log della console in un file su disco. Non influisce sulle prestazioni.", + "StubLogTooltip": "Stampa i messaggi di log relativi alle stub nella console. Non influisce sulle prestazioni.", + "InfoLogTooltip": "Stampa i messaggi di log informativi nella console. Non influisce sulle prestazioni.", + "WarnLogTooltip": "Stampa i messaggi di log relativi agli avvisi nella console. Non influisce sulle prestazioni.", + "ErrorLogTooltip": "Stampa i messaggi di log relativi agli errori nella console. Non influisce sulle prestazioni.", + "TraceLogTooltip": "Stampa i messaggi di log relativi al trace nella console. Non influisce sulle prestazioni.", + "GuestLogTooltip": "Stampa i messaggi di log del guest nella console. Non influisce sulle prestazioni.", + "FileAccessLogTooltip": "Stampa i messaggi di log relativi all'accesso ai file nella console.", + "FSAccessLogModeTooltip": "Attiva l'output dei log di accesso FS nella console. Le modalità possibili vanno da 0 a 3", + "DeveloperOptionTooltip": "Usa con attenzione", + "OpenGlLogLevel": "Richiede che i livelli di log appropriati siano abilitati", + "DebugLogTooltip": "Stampa i messaggi di log per il debug nella console.\n\nUsa questa opzione solo se specificatamente richiesto da un membro del team, dal momento che rende i log difficili da leggere e riduce le prestazioni dell'emulatore.", + "LoadApplicationFileTooltip": "Apri un file explorer per scegliere un file compatibile Switch da caricare", + "LoadApplicationFolderTooltip": "Apri un file explorer per scegliere un file compatibile Switch, applicazione sfusa da caricare", + "OpenRyujinxFolderTooltip": "Apri la cartella del filesystem di Ryujinx", + "OpenRyujinxLogsTooltip": "Apre la cartella dove vengono salvati i log", + "ExitTooltip": "Esci da Ryujinx", + "OpenSettingsTooltip": "Apri la finestra delle impostazioni", + "OpenProfileManagerTooltip": "Apri la finestra di gestione dei profili utente", + "StopEmulationTooltip": "Ferma l'emulazione del gioco attuale e torna alla selezione dei giochi", + "CheckUpdatesTooltip": "Controlla la presenza di aggiornamenti di Ryujinx", + "OpenAboutTooltip": "Apri la finestra delle informazioni", + "GridSize": "Dimensione griglia", + "GridSizeTooltip": "Cambia la dimensione dei riquadri della griglia", + "SettingsTabSystemSystemLanguageBrazilianPortuguese": "Portoghese brasiliano", + "AboutRyujinxContributorsButtonHeader": "Mostra tutti i contributori", + "LoadDlcFromFolderTooltip": "Apri un esploratore file per scegliere una o più cartelle dalle quali caricare DLC in massa", + "LoadTitleUpdatesFromFolderTooltip": "Apri un esploratore file per scegliere una o più cartelle dalle quali caricare aggiornamenti in massa", + "SettingsTabSystemAudioVolume": "Volume: ", + "AudioVolumeTooltip": "Cambia volume audio", + "SettingsTabSystemEnableInternetAccess": "Attiva l'accesso a Internet da parte del guest/Modalità LAN", + "EnableInternetAccessTooltip": "Consente all'applicazione emulata di connettersi a Internet.\n\nI giochi che dispongono di una modalità LAN possono connettersi tra di loro quando questa opzione è abilitata e sono connessi alla stessa rete, comprese le console reali.\n\nQuesta opzione NON consente la connessione ai server di Nintendo. Potrebbe causare arresti anomali in alcuni giochi che provano a connettersi a Internet.\n\nNel dubbio, lascia l'opzione disattivata.", + "GameListContextMenuManageCheatToolTip": "Gestisci trucchi", + "GameListContextMenuManageCheat": "Gestisci trucchi", + "GameListContextMenuManageModToolTip": "Gestisci mod", + "GameListContextMenuManageMod": "Gestisci mod", + "ControllerSettingsStickRange": "Raggio:", + "DialogStopEmulationTitle": "Ryujinx - Ferma emulazione", + "DialogStopEmulationMessage": "Sei sicuro di voler fermare l'emulazione?", + "SettingsTabCpu": "CPU", + "SettingsTabAudio": "Audio", + "SettingsTabNetwork": "Rete", + "SettingsTabNetworkConnection": "Connessione di rete", + "SettingsTabCpuCache": "Cache CPU", + "SettingsTabCpuMemory": "Modalità CPU", + "DialogUpdaterFlatpakNotSupportedMessage": "Aggiorna Ryujinx tramite FlatHub.", + "UpdaterDisabledWarningTitle": "Updater disabilitato!", + "ControllerSettingsRotate90": "Ruota in senso orario di 90°", + "IconSize": "Dimensioni icone", + "IconSizeTooltip": "Cambia le dimensioni delle icone dei giochi", + "MenuBarOptionsShowConsole": "Mostra console", + "ShaderCachePurgeError": "Errore nell'eliminazione della cache degli shader a {0}: {1}", + "UserErrorNoKeys": "Chiavi non trovate", + "UserErrorNoFirmware": "Firmware non trovato", + "UserErrorFirmwareParsingFailed": "Errori di analisi del firmware", + "UserErrorApplicationNotFound": "Applicazione non trovata", + "UserErrorUnknown": "Errore sconosciuto", + "UserErrorUndefined": "Errore non definito", + "UserErrorNoKeysDescription": "Ryujinx non è riuscito a trovare il file 'prod.keys'", + "UserErrorNoFirmwareDescription": "Ryujinx non è riuscito a trovare alcun firmware installato", + "UserErrorFirmwareParsingFailedDescription": "Ryujinx non è riuscito ad analizzare il firmware. Questo di solito è causato da chiavi non aggiornate.", + "UserErrorApplicationNotFoundDescription": "Ryujinx non è riuscito a trovare un'applicazione valida nel percorso specificato.", + "UserErrorUnknownDescription": "Si è verificato un errore sconosciuto!", + "UserErrorUndefinedDescription": "Si è verificato un errore sconosciuto! Ciò non dovrebbe accadere, contatta uno sviluppatore!", + "OpenSetupGuideMessage": "Apri la guida all'installazione", + "NoUpdate": "Nessun aggiornamento", + "TitleUpdateVersionLabel": "Versione {0}", + "TitleBundledUpdateVersionLabel": "In bundle: Versione {0}", + "TitleBundledDlcLabel": "In bundle:", + "TitleXCIStatusPartialLabel": "Parziale", + "TitleXCIStatusTrimmableLabel": "Non Trimmato", + "TitleXCIStatusUntrimmableLabel": "Trimmato", + "TitleXCIStatusFailedLabel": "(Fallito)", + "TitleXCICanSaveLabel": "Salva {0:n0} Mb", + "TitleXCISavingLabel": "Salva {0:n0} Mb", + "RyujinxInfo": "Ryujinx - Informazioni", + "RyujinxConfirm": "Ryujinx - Conferma", + "FileDialogAllTypes": "Tutti i tipi", + "Never": "Mai", + "SwkbdMinCharacters": "Non può avere meno di {0} caratteri", + "SwkbdMinRangeCharacters": "Può avere da {0} a {1} caratteri", + "SoftwareKeyboard": "Tastiera software", + "SoftwareKeyboardModeNumeric": "Deve essere solo 0-9 o '.'", + "SoftwareKeyboardModeAlphabet": "Deve essere solo caratteri non CJK", + "SoftwareKeyboardModeASCII": "Deve essere solo testo ASCII", + "ControllerAppletControllers": "Controller supportati:", + "ControllerAppletPlayers": "Giocatori:", + "ControllerAppletDescription": "La configurazione corrente non è valida. Aprire le impostazioni e riconfigurare gli input.", + "ControllerAppletDocked": "Modalità TV attivata. Gli input della modalità portatile dovrebbero essere disabilitati.", + "UpdaterRenaming": "Rinominazione dei vecchi files...", + "UpdaterRenameFailed": "Non è stato possibile rinominare il file: {0}", + "UpdaterAddingFiles": "Aggiunta dei nuovi file...", + "UpdaterExtracting": "Estrazione dell'aggiornamento...", + "UpdaterDownloading": "Download dell'aggiornamento...", + "Game": "Gioco", + "Docked": "TV", + "Handheld": "Portatile", + "ConnectionError": "Errore di connessione.", + "AboutPageDeveloperListMore": "{0} e altri ancora...", + "ApiError": "Errore dell'API.", + "LoadingHeading": "Caricamento di {0}", + "CompilingPPTC": "Compilazione PPTC", + "CompilingShaders": "Compilazione degli shader", + "AllKeyboards": "Tutte le tastiere", + "OpenFileDialogTitle": "Seleziona un file supportato da aprire", + "OpenFolderDialogTitle": "Seleziona una cartella con un gioco estratto", + "AllSupportedFormats": "Tutti i formati supportati", + "RyujinxUpdater": "Aggiornamento di Ryujinx", + "SettingsTabHotkeys": "Tasti di scelta rapida", + "SettingsTabHotkeysHotkeys": "Tasti di scelta rapida", + "SettingsTabHotkeysToggleVsyncHotkey": "Attiva/disattiva VSync:", + "SettingsTabHotkeysScreenshotHotkey": "Cattura uno screenshot:", + "SettingsTabHotkeysShowUiHotkey": "Mostra l'interfaccia:", + "SettingsTabHotkeysPauseHotkey": "Metti in pausa:", + "SettingsTabHotkeysToggleMuteHotkey": "Disattiva l'audio:", + "ControllerMotionTitle": "Impostazioni dei sensori di movimento", + "ControllerRumbleTitle": "Impostazioni di vibrazione", + "SettingsSelectThemeFileDialogTitle": "Seleziona file del tema", + "SettingsXamlThemeFile": "File del tema xaml", + "AvatarWindowTitle": "Gestisci account - Avatar", + "Amiibo": "Amiibo", + "Unknown": "Sconosciuto", + "Usage": "Utilizzo", + "Writable": "Scrivibile", + "SelectDlcDialogTitle": "Seleziona file dei DLC", + "SelectUpdateDialogTitle": "Seleziona file di aggiornamento", + "SelectModDialogTitle": "Seleziona cartella delle mod", + "TrimXCIFileDialogTitle": "Controlla e Trimma i file XCI ", + "TrimXCIFileDialogPrimaryText": "Questa funzionalita controllerà prima lo spazio libero e poi trimmerà il file XCI per liberare dello spazio.", + "TrimXCIFileDialogSecondaryText": "Dimensioni Attuali File: {0:n} MB\nDimensioni Dati Gioco: {1:n} MB\nRisparimio Spazio Disco: {2:n} MB", + "TrimXCIFileNoTrimNecessary": "Il file XCI non deve essere trimmato. Controlla i log per ulteriori dettagli", + "TrimXCIFileNoUntrimPossible": "Il file XCI non può essere untrimmato. Controlla i log per ulteriori dettagli", + "TrimXCIFileReadOnlyFileCannotFix": "Il file XCI è in sola lettura e non può essere reso Scrivibile. Controlla i log per ulteriori dettagli", + "TrimXCIFileFileSizeChanged": "Il file XCI ha cambiato dimensioni da quando è stato scansionato. Controlla che il file non stia venendo scritto da qualche altro programma e poi riprova.", + "TrimXCIFileFreeSpaceCheckFailed": "Il file XCI ha dati nello spazio libero, non è sicuro effettuare il trimming", + "TrimXCIFileInvalidXCIFile": "Il file XCI contiene dati invlidi. Controlla i log per ulteriori dettagli", + "TrimXCIFileFileIOWriteError": "Il file XCI non può essere aperto per essere scritto. Controlla i log per ulteriori dettagli", + "TrimXCIFileFailedPrimaryText": "Trimming del file XCI fallito", + "TrimXCIFileCancelled": "Operazione Cancellata", + "TrimXCIFileFileUndertermined": "Nessuna operazione è stata effettuata", + "UserProfileWindowTitle": "Gestione profili utente", + "CheatWindowTitle": "Gestione trucchi", + "DlcWindowTitle": "Gestisci DLC per {0} ({1})", + "ModWindowTitle": "Gestisci mod per {0} ({1})", + "UpdateWindowTitle": "Gestione aggiornamenti", + "XCITrimmerWindowTitle": "XCI File Trimmer", + "XCITrimmerTitleStatusCount": "{0} di {1} Titolo(i) Selezionati", + "XCITrimmerTitleStatusCountWithFilter": "{0} of {1} Titolo(i) Selezionati ({2} visualizzato)", + "XCITrimmerTitleStatusTrimming": "Trimming {0} Titolo(i)...", + "XCITrimmerTitleStatusUntrimming": "Untrimming {0} Titolo(i)...", + "XCITrimmerTitleStatusFailed": "Fallito", + "XCITrimmerPotentialSavings": "Potenziali Salvataggi", + "XCITrimmerActualSavings": "Effettivi Salvataggi", + "XCITrimmerSavingsMb": "{0:n0} Mb", + "XCITrimmerSelectDisplayed": "Seleziona Visualizzati", + "XCITrimmerDeselectDisplayed": "Deselziona Visualizzati", + "XCITrimmerSortName": "Titolo", + "XCITrimmerSortSaved": "Salvataggio Spazio", + "XCITrimmerTrim": "Trim", + "XCITrimmerUntrim": "Untrim", + "UpdateWindowUpdateAddedMessage": "{0} aggiornamento/i aggiunto/i", + "UpdateWindowBundledContentNotice": "Gli aggiornamenti inclusi non possono essere eliminati, ma solo disattivati", + "CheatWindowHeading": "Trucchi disponibili per {0} [{1}]", + "BuildId": "ID Build", + "DlcWindowBundledContentNotice": "i DLC \"impacchettati\" non possono essere rimossi, ma solo disabilitati.", + "DlcWindowHeading": "DLC disponibili per {0} [{1}]", + "DlcWindowDlcAddedMessage": "{0} nuovo/i contenuto/i scaricabile/i aggiunto/i", + "AutoloadDlcAddedMessage": "{0} contenuto/i scaricabile/i aggiunto/i", + "AutoloadDlcRemovedMessage": "{0} contenuto/i scaricabile/i mancante/i rimosso/i", + "AutoloadUpdateAddedMessage": "{0} aggiornamento/i aggiunto/i", + "AutoloadUpdateRemovedMessage": "{0} aggiornamento/i mancante/i rimosso/i", + "ModWindowHeading": "{0} mod", + "UserProfilesEditProfile": "Modifica selezionati", + "Continue": "Continua", + "Cancel": "Annulla", + "Save": "Salva", + "Discard": "Scarta", + "Paused": "In pausa", + "UserProfilesSetProfileImage": "Imposta immagine profilo", + "UserProfileEmptyNameError": "Il nome è obbligatorio", + "UserProfileNoImageError": "Dev'essere impostata un'immagine profilo", + "GameUpdateWindowHeading": "Gestisci aggiornamenti per {0} ({1})", + "SettingsTabHotkeysResScaleUpHotkey": "Aumenta la risoluzione:", + "SettingsTabHotkeysResScaleDownHotkey": "Riduci la risoluzione:", + "UserProfilesName": "Nome:", + "UserProfilesUserId": "ID utente:", + "SettingsTabGraphicsBackend": "Backend grafico", + "SettingsTabGraphicsBackendTooltip": "Seleziona il backend grafico che verrà utilizzato nell'emulatore.\n\nVulkan è nel complesso migliore per tutte le schede grafiche moderne, a condizione che i relativi driver siano aggiornati. Vulkan dispone anche di una compilazione degli shader più veloce (con minore stuttering) su tutte le marche di GPU.\n\nOpenGL può ottenere risultati migliori su vecchie GPU Nvidia, su vecchie GPU AMD su Linux, o su GPU con poca VRAM, anche se lo stuttering dovuto alla compilazione degli shader sarà maggiore.\n\nNel dubbio, scegli Vulkan. Seleziona OpenGL se la GPU non supporta Vulkan nemmeno con i driver grafici più recenti.", + "SettingsEnableTextureRecompression": "Attiva la ricompressione delle texture", + "SettingsEnableTextureRecompressionTooltip": "Comprime le texture ASTC per ridurre l'utilizzo di VRAM.\n\nI giochi che utilizzano questo formato di texture includono Astral Chain, Bayonetta 3, Fire Emblem Engage, Metroid Prime Remastered, Super Mario Bros. Wonder e The Legend of Zelda: Tears of the Kingdom.\n\nLe schede grafiche con 4GiB o meno di VRAM probabilmente si bloccheranno ad un certo punto durante l'esecuzione di questi giochi.\n\nAttiva questa opzione solo se sei a corto di VRAM nei giochi sopra menzionati. Nel dubbio, lascia l'opzione disattivata.", + "SettingsTabGraphicsPreferredGpu": "GPU preferita", + "SettingsTabGraphicsPreferredGpuTooltip": "Seleziona la scheda grafica che verrà usata con la backend grafica Vulkan.\n\nNon influenza la GPU che userà OpenGL.\n\nImposta la GPU contrassegnata come \"dGPU\" se non sei sicuro. Se non ce n'è una, lascia intatta quest'impostazione.", + "SettingsAppRequiredRestartMessage": "È richiesto un riavvio di Ryujinx", + "SettingsGpuBackendRestartMessage": "Le impostazioni della backend grafica o della GPU sono state modificate. Questo richiederà un riavvio perché le modifiche siano applicate", + "SettingsGpuBackendRestartSubMessage": "Vuoi riavviare ora?", + "RyujinxUpdaterMessage": "Vuoi aggiornare Ryujinx all'ultima versione?", + "SettingsTabHotkeysVolumeUpHotkey": "Alza il volume:", + "SettingsTabHotkeysVolumeDownHotkey": "Abbassa il volume:", + "SettingsEnableMacroHLE": "Attiva HLE macro", + "SettingsEnableMacroHLETooltip": "Emulazione di alto livello del codice macro della GPU.\n\nMigliora le prestazioni, ma può causare anomalie grafiche in alcuni giochi.\n\nNel dubbio, lascia l'opzione attiva.", + "SettingsEnableColorSpacePassthrough": "Passthrough dello spazio dei colori", + "SettingsEnableColorSpacePassthroughTooltip": "Indica al backend Vulkan di passare le informazioni sul colore senza specificare uno spazio dei colori. Per gli utenti con schermi ad ampia gamma, ciò può rendere i colori più vivaci, sacrificando la correttezza del colore.", + "VolumeShort": "Vol", + "UserProfilesManageSaves": "Gestisci i salvataggi", + "DeleteUserSave": "Vuoi eliminare il salvataggio utente per questo gioco?", + "IrreversibleActionNote": "Questa azione non è reversibile.", + "SaveManagerHeading": "Gestisci salvataggi per {0} ({1})", + "SaveManagerTitle": "Gestione salvataggi", + "Name": "Nome", + "Size": "Dimensione", + "Search": "Cerca", + "UserProfilesRecoverLostAccounts": "Recupera account persi", + "Recover": "Recupera", + "UserProfilesRecoverHeading": "Sono stati trovati dei salvataggi per i seguenti account", + "UserProfilesRecoverEmptyList": "Nessun profilo da recuperare", + "GraphicsAATooltip": "Applica anti-aliasing al rendering del gioco.\n\nFXAA sfocerà la maggior parte dell'immagine, mentre SMAA tenterà di trovare bordi frastagliati e lisciarli.\n\nNon si consiglia di usarlo in combinazione con il filtro di scala FSR.\n\nQuesta opzione può essere modificata mentre un gioco è in esecuzione facendo clic su \"Applica\" qui sotto; puoi semplicemente spostare la finestra delle impostazioni da parte e sperimentare fino a quando non trovi il tuo look preferito per un gioco.\n\nLasciare su Nessuno se incerto.", + "GraphicsAALabel": "Anti-Aliasing:", + "GraphicsScalingFilterLabel": "Filtro di scala:", + "GraphicsScalingFilterTooltip": "Scegli il filtro di scaling che verrà applicato quando si utilizza o scaling di risoluzione.\n\nBilineare funziona bene per i giochi 3D ed è un'opzione predefinita affidabile.\n\nNearest è consigliato per i giochi in pixel art.\n\nFSR 1.0 è solo un filtro di nitidezza, non raccomandato per l'uso con FXAA o SMAA.\n\nQuesta opzione può essere modificata mentre un gioco è in esecuzione facendo clic su \"Applica\" qui sotto; puoi semplicemente spostare la finestra delle impostazioni da parte e sperimentare fino a quando non trovi il tuo look preferito per un gioco.\n\nLasciare su Bilineare se incerto.", + "GraphicsScalingFilterBilinear": "Bilineare", + "GraphicsScalingFilterNearest": "Nearest", + "GraphicsScalingFilterFsr": "FSR", + "GraphicsScalingFilterArea": "Area", + "GraphicsScalingFilterLevelLabel": "Livello", + "GraphicsScalingFilterLevelTooltip": "Imposta il livello di nitidezza di FSR 1.0. Valori più alti comportano una maggiore nitidezza.", + "SmaaLow": "SMAA Basso", + "SmaaMedium": "SMAA Medio", + "SmaaHigh": "SMAA Alto", + "SmaaUltra": "SMAA Ultra", + "UserEditorTitle": "Modificare L'Utente", + "UserEditorTitleCreate": "Crea Un Utente", + "SettingsTabNetworkInterface": "Interfaccia di rete:", + "NetworkInterfaceTooltip": "L'interfaccia di rete utilizzata per le funzionalità LAN/LDN.\n\nIn combinazione con una VPN o XLink Kai e un gioco che supporta la modalità LAN, questa opzione può essere usata per simulare la connessione alla stessa rete attraverso Internet.\n\nNel dubbio, lascia l'opzione su Predefinito.", + "NetworkInterfaceDefault": "Predefinito", + "PackagingShaders": "Salvataggio degli shader", + "AboutChangelogButton": "Visualizza changelog su GitHub", + "AboutChangelogButtonTooltipMessage": "Clicca per aprire il changelog per questa versione nel tuo browser predefinito.", + "SettingsTabNetworkMultiplayer": "Multigiocatore", + "MultiplayerMode": "Modalità:", + "MultiplayerModeTooltip": "Cambia la modalità multigiocatore LDN.\n\nLdnMitm modificherà la funzionalità locale wireless/local play nei giochi per funzionare come se fosse in modalità LAN, consentendo connessioni locali sulla stessa rete con altre istanze di Ryujinx e console Nintendo Switch modificate che hanno il modulo ldn_mitm installato.\n\nLa modalità multigiocatore richiede che tutti i giocatori usino la stessa versione del gioco (es. Super Smash Bros. Ultimate v13.0.1 non può connettersi con la v13.0.0).\n\nNel dubbio, lascia l'opzione su Disabilitato.", + "MultiplayerModeDisabled": "Disabilitato", + "MultiplayerModeLdnMitm": "ldn_mitm", + "MultiplayerModeLdnRyu": "RyuLDN", + "MultiplayerDisableP2P": "Disable P2P Network Hosting (may increase latency)", + "MultiplayerDisableP2PTooltip": "Disable P2P network hosting, peers will proxy through the master server instead of connecting to you directly.", + "LdnPassphrase": "Network Passphrase:", + "LdnPassphraseTooltip": "You will only be able to see hosted games with the same passphrase as you.", + "LdnPassphraseInputTooltip": "Enter a passphrase in the format Ryujinx-<8 hex chars>. You will only be able to see hosted games with the same passphrase as you.", + "LdnPassphraseInputPublic": "(public)", + "GenLdnPass": "Generate Random", + "GenLdnPassTooltip": "Generates a new passphrase, which can be shared with other players.", + "ClearLdnPass": "Clear", + "ClearLdnPassTooltip": "Clears the current passphrase, returning to the public network.", + "InvalidLdnPassphrase": "Invalid Passphrase! Must be in the format \"Ryujinx-<8 hex chars>\"" +} diff --git a/src/Ryujinx/Assets/Locales/ja_JP.json b/src/Ryujinx/Assets/Locales/ja_JP.json new file mode 100644 index 000000000..59b7aa3b3 --- /dev/null +++ b/src/Ryujinx/Assets/Locales/ja_JP.json @@ -0,0 +1,867 @@ +{ + "Language": "日本語", + "MenuBarFileOpenApplet": "アプレットを開く", + "MenuBarFileOpenAppletOpenMiiApplet": "Mii Edit Applet", + "MenuBarFileOpenAppletOpenMiiAppletToolTip": "スタンドアロンモードで Mii エディタアプレットを開きます", + "SettingsTabInputDirectMouseAccess": "マウス直接アクセス", + "SettingsTabSystemMemoryManagerMode": "メモリ管理モード:", + "SettingsTabSystemMemoryManagerModeSoftware": "ソフトウェア", + "SettingsTabSystemMemoryManagerModeHost": "ホスト (高速)", + "SettingsTabSystemMemoryManagerModeHostUnchecked": "ホスト, チェックなし (最高速, 安全でない)", + "SettingsTabSystemUseHypervisor": "ハイパーバイザーを使用", + "MenuBarFile": "ファイル(_F)", + "MenuBarFileOpenFromFile": "ファイルからアプリケーションをロード(_L)", + "MenuBarFileOpenFromFileError": "No applications found in selected file.", + "MenuBarFileOpenUnpacked": "展開されたゲームをロード", + "MenuBarFileLoadDlcFromFolder": "Load DLC From Folder", + "MenuBarFileLoadTitleUpdatesFromFolder": "Load Title Updates From Folder", + "MenuBarFileOpenEmuFolder": "Ryujinx フォルダを開く", + "MenuBarFileOpenLogsFolder": "ログフォルダを開く", + "MenuBarFileExit": "終了(_E)", + "MenuBarOptions": "オプション(_O)", + "MenuBarOptionsToggleFullscreen": "全画面切り替え", + "MenuBarOptionsStartGamesInFullscreen": "全画面モードでゲームを開始", + "MenuBarOptionsStopEmulation": "エミュレーションを中止", + "MenuBarOptionsSettings": "設定(_S)", + "MenuBarOptionsManageUserProfiles": "ユーザプロファイルを管理(_M)", + "MenuBarActions": "アクション(_A)", + "MenuBarOptionsSimulateWakeUpMessage": "スリープ復帰メッセージをシミュレート", + "MenuBarActionsScanAmiibo": "Amiibo をスキャン", + "MenuBarTools": "ツール(_T)", + "MenuBarToolsInstallFirmware": "ファームウェアをインストール", + "MenuBarFileToolsInstallFirmwareFromFile": "XCI または ZIP からファームウェアをインストール", + "MenuBarFileToolsInstallFirmwareFromDirectory": "ディレクトリからファームウェアをインストール", + "MenuBarToolsManageFileTypes": "ファイル形式を管理", + "MenuBarToolsInstallFileTypes": "ファイル形式をインストール", + "MenuBarToolsUninstallFileTypes": "ファイル形式をアンインストール", + "MenuBarToolsXCITrimmer": "Trim XCI Files", + "MenuBarView": "_View", + "MenuBarViewWindow": "Window Size", + "MenuBarViewWindow720": "720p", + "MenuBarViewWindow1080": "1080p", + "MenuBarHelp": "ヘルプ(_H)", + "MenuBarHelpCheckForUpdates": "アップデートを確認", + "MenuBarHelpAbout": "Ryujinx について", + "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管理ウインドウを開きます", + "GameListContextMenuCacheManagement": "キャッシュ管理", + "GameListContextMenuCacheManagementPurgePptc": "PPTC を再構築", + "GameListContextMenuCacheManagementPurgePptcToolTip": "次回のゲーム起動時に PPTC を再構築します", + "GameListContextMenuCacheManagementPurgeShaderCache": "シェーダーキャッシュを破棄", + "GameListContextMenuCacheManagementPurgeShaderCacheToolTip": "アプリケーションのシェーダーキャッシュを破棄します", + "GameListContextMenuCacheManagementOpenPptcDirectory": "PPTC ディレクトリを開く", + "GameListContextMenuCacheManagementOpenPptcDirectoryToolTip": "アプリケーションの PPTC キャッシュを格納するディレクトリを開きます", + "GameListContextMenuCacheManagementOpenShaderCacheDirectory": "シェーダーキャッシュディレクトリを開く", + "GameListContextMenuCacheManagementOpenShaderCacheDirectoryToolTip": "アプリケーションのシェーダーキャッシュを格納するディレクトリを開きます", + "GameListContextMenuExtractData": "データを展開", + "GameListContextMenuExtractDataExeFS": "ExeFS", + "GameListContextMenuExtractDataExeFSToolTip": "現在のアプリケーション設定(アップデート含む)から ExeFS セクションを展開します", + "GameListContextMenuExtractDataRomFS": "RomFS", + "GameListContextMenuExtractDataRomFSToolTip": "現在のアプリケーション設定(アップデート含む)から RomFS セクションを展開します", + "GameListContextMenuExtractDataLogo": "ロゴ", + "GameListContextMenuExtractDataLogoToolTip": "現在のアプリケーション設定(アップデート含む)からロゴセクションを展開します", + "GameListContextMenuCreateShortcut": "アプリケーションのショートカットを作成", + "GameListContextMenuCreateShortcutToolTip": "選択したアプリケーションを起動するデスクトップショートカットを作成します", + "GameListContextMenuCreateShortcutToolTipMacOS": "選択したアプリケーションを起動する ショートカットを macOS の Applications フォルダに作成します", + "GameListContextMenuOpenModsDirectory": "Modディレクトリを開く", + "GameListContextMenuOpenModsDirectoryToolTip": "アプリケーションの Mod データを格納するディレクトリを開きます", + "GameListContextMenuOpenSdModsDirectory": "Atmosphere Mods ディレクトリを開く", + "GameListContextMenuOpenSdModsDirectoryToolTip": "アプリケーションの Mod データを格納する SD カードの Atmosphere ディレクトリを開きます. 実際のハードウェア用に作成された Mod データに有用です.", + "GameListContextMenuTrimXCI": "Check and Trim XCI File", + "GameListContextMenuTrimXCIToolTip": "Check and Trim XCI File to Save Disk Space", + "StatusBarGamesLoaded": "{0}/{1} ゲーム", + "StatusBarSystemVersion": "システムバージョン: {0}", + "StatusBarXCIFileTrimming": "Trimming XCI File '{0}'", + "LinuxVmMaxMapCountDialogTitle": "メモリマッピング上限値が小さすぎます", + "LinuxVmMaxMapCountDialogTextPrimary": "vm.max_map_count の値を {0}に増やしますか?", + "LinuxVmMaxMapCountDialogTextSecondary": "ゲームによっては, 現在許可されているサイズより大きなメモリマッピングを作成しようとすることがあります. この制限を超えると, Ryjinx はすぐにクラッシュします.", + "LinuxVmMaxMapCountDialogButtonUntilRestart": "はい, 次回再起動まで", + "LinuxVmMaxMapCountDialogButtonPersistent": "はい, 恒久的に", + "LinuxVmMaxMapCountWarningTextPrimary": "メモリマッピングの最大量が推奨値よりも小さいです.", + "LinuxVmMaxMapCountWarningTextSecondary": "vm.max_map_count の現在値 {0} は {1} よりも小さいです. ゲームによっては現在許可されている値よりも大きなメモリマッピングを作成しようとする場合があります. 上限を越えた場合, Ryujinx はクラッシュします.", + "Settings": "設定", + "SettingsTabGeneral": "ユーザインタフェース", + "SettingsTabGeneralGeneral": "一般", + "SettingsTabGeneralEnableDiscordRichPresence": "Discord リッチプレゼンスを有効にする", + "SettingsTabGeneralCheckUpdatesOnLaunch": "起動時にアップデートを確認する", + "SettingsTabGeneralShowConfirmExitDialog": "\"終了を確認\" ダイアログを表示する", + "SettingsTabGeneralRememberWindowState": "Remember Window Size/Position", + "SettingsTabGeneralShowTitleBar": "Show Title Bar (Requires restart)", + "SettingsTabGeneralHideCursor": "マウスカーソルを非表示", + "SettingsTabGeneralHideCursorNever": "決して", + "SettingsTabGeneralHideCursorOnIdle": "アイドル時", + "SettingsTabGeneralHideCursorAlways": "常時", + "SettingsTabGeneralGameDirectories": "ゲームディレクトリ", + "SettingsTabGeneralAutoloadDirectories": "Autoload DLC/Updates Directories", + "SettingsTabGeneralAutoloadNote": "DLC and Updates which refer to missing files will be unloaded automatically", + "SettingsTabGeneralAdd": "追加", + "SettingsTabGeneralRemove": "削除", + "SettingsTabSystem": "システム", + "SettingsTabSystemCore": "コア", + "SettingsTabSystemSystemRegion": "地域:", + "SettingsTabSystemSystemRegionJapan": "日本", + "SettingsTabSystemSystemRegionUSA": "アメリカ", + "SettingsTabSystemSystemRegionEurope": "ヨーロッパ", + "SettingsTabSystemSystemRegionAustralia": "オーストラリア", + "SettingsTabSystemSystemRegionChina": "中国", + "SettingsTabSystemSystemRegionKorea": "韓国", + "SettingsTabSystemSystemRegionTaiwan": "台湾", + "SettingsTabSystemSystemLanguage": "言語:", + "SettingsTabSystemSystemLanguageJapanese": "日本語", + "SettingsTabSystemSystemLanguageAmericanEnglish": "英語(アメリカ)", + "SettingsTabSystemSystemLanguageFrench": "フランス語", + "SettingsTabSystemSystemLanguageGerman": "ドイツ語", + "SettingsTabSystemSystemLanguageItalian": "イタリア語", + "SettingsTabSystemSystemLanguageSpanish": "スペイン語", + "SettingsTabSystemSystemLanguageChinese": "中国語", + "SettingsTabSystemSystemLanguageKorean": "韓国語", + "SettingsTabSystemSystemLanguageDutch": "オランダ語", + "SettingsTabSystemSystemLanguagePortuguese": "ポルトガル語", + "SettingsTabSystemSystemLanguageRussian": "ロシア語", + "SettingsTabSystemSystemLanguageTaiwanese": "台湾語", + "SettingsTabSystemSystemLanguageBritishEnglish": "英語(イギリス)", + "SettingsTabSystemSystemLanguageCanadianFrench": "フランス語(カナダ)", + "SettingsTabSystemSystemLanguageLatinAmericanSpanish": "スペイン語(ラテンアメリカ)", + "SettingsTabSystemSystemLanguageSimplifiedChinese": "中国語", + "SettingsTabSystemSystemLanguageTraditionalChinese": "台湾語", + "SettingsTabSystemSystemTimeZone": "タイムゾーン:", + "SettingsTabSystemSystemTime": "時刻:", + "SettingsTabSystemEnableVsync": "VSync", + "SettingsTabSystemEnablePptc": "PPTC (Profiled Persistent Translation Cache)", + "SettingsTabSystemEnableLowPowerPptc": "Low-power PPTC", + "SettingsTabSystemEnableFsIntegrityChecks": "ファイルシステム整合性チェック", + "SettingsTabSystemAudioBackend": "音声バックエンド:", + "SettingsTabSystemAudioBackendDummy": "ダミー", + "SettingsTabSystemAudioBackendOpenAL": "OpenAL", + "SettingsTabSystemAudioBackendSoundIO": "SoundIO", + "SettingsTabSystemAudioBackendSDL2": "SDL2", + "SettingsTabSystemHacks": "ハック", + "SettingsTabSystemHacksNote": " (挙動が不安定になる可能性があります)", + "SettingsTabSystemDramSize": "DRAMサイズ:", + "SettingsTabSystemDramSize4GiB": "4GiB", + "SettingsTabSystemDramSize6GiB": "6GiB", + "SettingsTabSystemDramSize8GiB": "8GiB", + "SettingsTabSystemDramSize12GiB": "12GiB", + "SettingsTabSystemIgnoreMissingServices": "未実装サービスを無視する", + "SettingsTabSystemIgnoreApplet": "アプレットを無視する", + "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": "Info ログを有効にする", + "SettingsTabLoggingEnableWarningLogs": "Warning ログを有効にする", + "SettingsTabLoggingEnableErrorLogs": "Error ログを有効にする", + "SettingsTabLoggingEnableTraceLogs": "Trace ログを有効にする", + "SettingsTabLoggingEnableGuestLogs": "Guest ログを有効にする", + "SettingsTabLoggingEnableFsAccessLogs": "Fs アクセスログを有効にする", + "SettingsTabLoggingFsGlobalAccessLogMode": "Fs グローバルアクセスログモード:", + "SettingsTabLoggingDeveloperOptions": "開発者オプション", + "SettingsTabLoggingDeveloperOptionsNote": "警告: パフォーマンスを低下させます", + "SettingsTabLoggingGraphicsBackendLogLevel": "グラフィックスバックエンド ログレベル:", + "SettingsTabLoggingGraphicsBackendLogLevelNone": "なし", + "SettingsTabLoggingGraphicsBackendLogLevelError": "エラー", + "SettingsTabLoggingGraphicsBackendLogLevelPerformance": "パフォーマンス低下", + "SettingsTabLoggingGraphicsBackendLogLevelAll": "すべて", + "SettingsTabLoggingEnableDebugLogs": "デバッグログを有効にする", + "SettingsTabInput": "入力", + "SettingsTabInputEnableDockedMode": "ドッキングモード", + "SettingsTabInputDirectKeyboardAccess": "キーボード直接アクセス", + "SettingsButtonSave": "セーブ", + "SettingsButtonClose": "閉じる", + "SettingsButtonOk": "OK", + "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": "サーバ:", + "ControllerSettingsMotionGyroSensitivity": "ジャイロ感度:", + "ControllerSettingsMotionGyroDeadzone": "ジャイロ遊び:", + "ControllerSettingsSave": "セーブ", + "ControllerSettingsClose": "閉じる", + "KeyUnknown": "Unknown", + "KeyShiftLeft": "Shift Left", + "KeyShiftRight": "Shift Right", + "KeyControlLeft": "Ctrl Left", + "KeyMacControlLeft": "⌃ Left", + "KeyControlRight": "Ctrl Right", + "KeyMacControlRight": "⌃ Right", + "KeyAltLeft": "Alt Left", + "KeyMacAltLeft": "⌥ Left", + "KeyAltRight": "Alt Right", + "KeyMacAltRight": "⌥ Right", + "KeyWinLeft": "⊞ Left", + "KeyMacWinLeft": "⌘ Left", + "KeyWinRight": "⊞ Right", + "KeyMacWinRight": "⌘ Right", + "KeyMenu": "Menu", + "KeyUp": "Up", + "KeyDown": "Down", + "KeyLeft": "Left", + "KeyRight": "Right", + "KeyEnter": "Enter", + "KeyEscape": "Escape", + "KeySpace": "Space", + "KeyTab": "Tab", + "KeyBackSpace": "Backspace", + "KeyInsert": "Insert", + "KeyDelete": "Delete", + "KeyPageUp": "Page Up", + "KeyPageDown": "Page Down", + "KeyHome": "Home", + "KeyEnd": "End", + "KeyCapsLock": "Caps Lock", + "KeyScrollLock": "Scroll Lock", + "KeyPrintScreen": "Print Screen", + "KeyPause": "Pause", + "KeyNumLock": "Num Lock", + "KeyClear": "Clear", + "KeyKeypad0": "Keypad 0", + "KeyKeypad1": "Keypad 1", + "KeyKeypad2": "Keypad 2", + "KeyKeypad3": "Keypad 3", + "KeyKeypad4": "Keypad 4", + "KeyKeypad5": "Keypad 5", + "KeyKeypad6": "Keypad 6", + "KeyKeypad7": "Keypad 7", + "KeyKeypad8": "Keypad 8", + "KeyKeypad9": "Keypad 9", + "KeyKeypadDivide": "Keypad Divide", + "KeyKeypadMultiply": "Keypad Multiply", + "KeyKeypadSubtract": "Keypad Subtract", + "KeyKeypadAdd": "Keypad Add", + "KeyKeypadDecimal": "Keypad Decimal", + "KeyKeypadEnter": "Keypad Enter", + "KeyNumber0": "0", + "KeyNumber1": "1", + "KeyNumber2": "2", + "KeyNumber3": "3", + "KeyNumber4": "4", + "KeyNumber5": "5", + "KeyNumber6": "6", + "KeyNumber7": "7", + "KeyNumber8": "8", + "KeyNumber9": "9", + "KeyTilde": "~", + "KeyGrave": "`", + "KeyMinus": "-", + "KeyPlus": "+", + "KeyBracketLeft": "[", + "KeyBracketRight": "]", + "KeySemicolon": ";", + "KeyQuote": "\"", + "KeyComma": ",", + "KeyPeriod": ".", + "KeySlash": "/", + "KeyBackSlash": "\\", + "KeyUnbound": "Unbound", + "GamepadLeftStick": "L Stick Button", + "GamepadRightStick": "R Stick Button", + "GamepadLeftShoulder": "Left Shoulder", + "GamepadRightShoulder": "Right Shoulder", + "GamepadLeftTrigger": "Left Trigger", + "GamepadRightTrigger": "Right Trigger", + "GamepadDpadUp": "Up", + "GamepadDpadDown": "Down", + "GamepadDpadLeft": "Left", + "GamepadDpadRight": "Right", + "GamepadMinus": "-", + "GamepadPlus": "+", + "GamepadGuide": "Guide", + "GamepadMisc1": "Misc", + "GamepadPaddle1": "Paddle 1", + "GamepadPaddle2": "Paddle 2", + "GamepadPaddle3": "Paddle 3", + "GamepadPaddle4": "Paddle 4", + "GamepadTouchpad": "Touchpad", + "GamepadSingleLeftTrigger0": "Left Trigger 0", + "GamepadSingleRightTrigger0": "Right Trigger 0", + "GamepadSingleLeftTrigger1": "Left Trigger 1", + "GamepadSingleRightTrigger1": "Right Trigger 1", + "StickLeft": "Left Stick", + "StickRight": "Right Stick", + "UserProfilesSelectedUserProfile": "選択されたユーザプロファイル:", + "UserProfilesSaveProfileName": "プロファイル名をセーブ", + "UserProfilesChangeProfileImage": "プロファイル画像を変更", + "UserProfilesAvailableUserProfiles": "利用可能なユーザプロファイル:", + "UserProfilesAddNewProfile": "プロファイルを作成", + "UserProfilesDelete": "削除", + "UserProfilesClose": "閉じる", + "ProfileNameSelectionWatermark": "ニックネームを選択", + "ProfileImageSelectionTitle": "プロファイル画像選択", + "ProfileImageSelectionHeader": "プロファイル画像を選択", + "ProfileImageSelectionNote": "カスタム画像をインポート, またはファームウェア内のアバターを選択できます", + "ProfileImageSelectionImportImage": "画像ファイルをインポート", + "ProfileImageSelectionSelectAvatar": "ファームウェア内のアバターを選択", + "InputDialogTitle": "入力ダイアログ", + "InputDialogOk": "OK", + "InputDialogCancel": "キャンセル", + "InputDialogCancelling": "Cancelling", + "InputDialogClose": "Close", + "InputDialogAddNewProfileTitle": "プロファイル名を選択", + "InputDialogAddNewProfileHeader": "プロファイル名を入力してください", + "InputDialogAddNewProfileSubtext": "(最大長: {0})", + "AvatarChoose": "選択", + "AvatarSetBackgroundColor": "背景色を指定", + "AvatarClose": "閉じる", + "ControllerSettingsLoadProfileToolTip": "プロファイルをロード", + "ControllerSettingsViewProfileToolTip": "View Profile", + "ControllerSettingsAddProfileToolTip": "プロファイルを追加", + "ControllerSettingsRemoveProfileToolTip": "プロファイルを削除", + "ControllerSettingsSaveProfileToolTip": "プロファイルをセーブ", + "MenuBarFileToolsTakeScreenshot": "スクリーンショットを撮影", + "MenuBarFileToolsHideUi": "UIを隠す", + "GameListContextMenuRunApplication": "アプリケーションを実行", + "GameListContextMenuToggleFavorite": "お気に入りを切り替え", + "GameListContextMenuToggleFavoriteToolTip": "ゲームをお気に入りに含めるかどうかを切り替えます", + "SettingsTabGeneralTheme": "テーマ:", + "SettingsTabGeneralThemeAuto": "Auto", + "SettingsTabGeneralThemeDark": "ダーク", + "SettingsTabGeneralThemeLight": "ライト", + "ControllerSettingsConfigureGeneral": "設定", + "ControllerSettingsRumble": "振動", + "ControllerSettingsRumbleStrongMultiplier": "強振動の補正値", + "ControllerSettingsRumbleWeakMultiplier": "弱振動の補正値", + "DialogMessageSaveNotAvailableMessage": "{0} [{1:x16}] のセーブデータはありません", + "DialogMessageSaveNotAvailableCreateSaveMessage": "このゲームのセーブデータを作成してよろしいですか?", + "DialogConfirmationTitle": "Ryujinx - 確認", + "DialogUpdaterTitle": "Ryujinx - アップデータ", + "DialogErrorTitle": "Ryujinx - エラー", + "DialogWarningTitle": "Ryujinx - 警告", + "DialogExitTitle": "Ryujinx - 終了", + "DialogErrorMessage": "エラーが発生しました", + "DialogExitMessage": "Ryujinx を閉じてよろしいですか?", + "DialogExitSubMessage": "セーブされていないデータはすべて失われます!", + "DialogMessageCreateSaveErrorMessage": "セーブデータ: {0} の作成中にエラーが発生しました", + "DialogMessageFindSaveErrorMessage": "セーブデータ: {0} の検索中にエラーが発生しました", + "FolderDialogExtractTitle": "展開フォルダを選択", + "DialogNcaExtractionMessage": "{1} から {0} セクションを展開中...", + "DialogNcaExtractionTitle": "NCA セクション展開", + "DialogNcaExtractionMainNcaNotFoundErrorMessage": "展開に失敗しました. 選択されたファイルにはメイン NCA が存在しません.", + "DialogNcaExtractionCheckLogErrorMessage": "展開に失敗しました. 詳細はログを確認してください.", + "DialogNcaExtractionSuccessMessage": "展開が正常終了しました", + "DialogUpdaterConvertFailedMessage": "現在の Ryujinx バージョンの変換に失敗しました.", + "DialogUpdaterCancelUpdateMessage": "アップデータをキャンセル中!", + "DialogUpdaterAlreadyOnLatestVersionMessage": "最新バージョンの Ryujinx を使用中です!", + "DialogUpdaterFailedToGetVersionMessage": "Github からのリリース情報取得時にエラーが発生しました. Github Actions でリリースファイルを作成中かもしれません. 後ほどもう一度試してみてください.", + "DialogUpdaterConvertFailedGithubMessage": "Github から取得した Ryujinx バージョンの変換に失敗しました.", + "DialogUpdaterDownloadingMessage": "アップデートをダウンロード中...", + "DialogUpdaterExtractionMessage": "アップデートを展開中...", + "DialogUpdaterRenamingMessage": "アップデートをリネーム中...", + "DialogUpdaterAddingFilesMessage": "新規アップデートを追加中...", + "DialogUpdaterShowChangelogMessage": "Show Changelog", + "DialogUpdaterCompleteMessage": "アップデート完了!", + "DialogUpdaterRestartMessage": "すぐに Ryujinx を再起動しますか?", + "DialogUpdaterNoInternetMessage": "インターネットに接続されていません!", + "DialogUpdaterNoInternetSubMessage": "インターネット接続が正常動作しているか確認してください!", + "DialogUpdaterDirtyBuildMessage": "Dirty ビルドの Ryujinx はアップデートできません!", + "DialogUpdaterDirtyBuildSubMessage": "サポートされているバージョンをお探しなら, https://ryujinx.app/download で Ryujinx をダウンロードしてください.", + "DialogRestartRequiredMessage": "再起動が必要", + "DialogThemeRestartMessage": "テーマがセーブされました. テーマを適用するには再起動が必要です.", + "DialogThemeRestartSubMessage": "再起動しますか", + "DialogFirmwareInstallEmbeddedMessage": "このゲームに含まれるファームウェアをインストールしてよろしいですか? (ファームウェア {0})", + "DialogFirmwareInstallEmbeddedSuccessMessage": "ファームウェアがインストールされていませんが, ゲームに含まれるファームウェア {0} をインストールできます.\nエミュレータが開始します.", + "DialogFirmwareNoFirmwareInstalledMessage": "ファームウェアがインストールされていません", + "DialogFirmwareInstalledMessage": "ファームウェア {0} がインストールされました", + "DialogInstallFileTypesSuccessMessage": "ファイル形式のインストールに成功しました!", + "DialogInstallFileTypesErrorMessage": "ファイル形式のインストールに失敗しました.", + "DialogUninstallFileTypesSuccessMessage": "ファイル形式のアンインストールに成功しました!", + "DialogUninstallFileTypesErrorMessage": "ファイル形式のアンインストールに失敗しました.", + "DialogOpenSettingsWindowLabel": "設定ウインドウを開く", + "DialogOpenXCITrimmerWindowLabel": "XCI Trimmer Window", + "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": "次回起動時に PPTC を再構築します:\n\n{0}\n\n実行してよろしいですか?", + "DialogPPTCDeletionErrorMessage": "PPTC キャッシュ破棄エラー {0}: {1}", + "DialogShaderDeletionMessage": "シェーダーキャッシュを破棄しようとしています:\n\n{0}\n\n実行してよろしいですか?", + "DialogShaderDeletionErrorMessage": "シェーダーキャッシュ破棄エラー {0}: {1}", + "DialogRyujinxErrorMessage": "エラーが発生しました", + "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": "セーブしますか?", + "DialogLoadFileErrorMessage": "{0}. エラー発生ファイル: {1}", + "DialogModAlreadyExistsMessage": "Modはすでに存在します", + "DialogModInvalidMessage": "指定したディレクトリにはmodが含まれていません!", + "DialogModDeleteNoParentMessage": "削除に失敗しました: Mod \"{0}\" の親ディレクトリが見つかりませんでした!", + "DialogDlcNoDlcErrorMessage": "選択されたファイルはこのタイトル用の DLC ではありません!", + "DialogPerformanceCheckLoggingEnabledMessage": "トレースロギングを有効にします. これは開発者のみに有用な機能です.", + "DialogPerformanceCheckLoggingEnabledConfirmMessage": "パフォーマンス最適化のためには,トレースロギングを無効にすることを推奨します. トレースロギングを無効にしてよろしいですか?", + "DialogPerformanceCheckShaderDumpEnabledMessage": "シェーダーダンプを有効にします. これは開発者のみに有用な機能です.", + "DialogPerformanceCheckShaderDumpEnabledConfirmMessage": "パフォーマンス最適化のためには, シェーダーダンプを無効にすることを推奨します. シェーダーダンプを無効にしてよろしいですか?", + "DialogLoadAppGameAlreadyLoadedMessage": "ゲームはすでにロード済みです", + "DialogLoadAppGameAlreadyLoadedSubMessage": "別のゲームを起動する前に, エミュレーションを中止またはエミュレータを閉じてください.", + "DialogUpdateAddUpdateErrorMessage": "選択されたファイルはこのタイトル用のアップデートではありません!", + "DialogSettingsBackendThreadingWarningTitle": "警告 - バックエンドスレッディング", + "DialogSettingsBackendThreadingWarningMessage": "このオプションの変更を完全に適用するには Ryujinx の再起動が必要です. プラットフォームによっては, Ryujinx のものを使用する前に手動でドライバ自身のマルチスレッディングを無効にする必要があるかもしれません.", + "DialogModManagerDeletionWarningMessage": "以下のModを削除しようとしています: {0}\n\n続行してもよろしいですか?", + "DialogModManagerDeletionAllWarningMessage": "このタイトルの Mod をすべて削除しようとしています.\n\n続行してもよろしいですか?", + "SettingsTabGraphicsFeaturesOptions": "機能", + "SettingsTabGraphicsBackendMultithreading": "グラフィックスバックエンドのマルチスレッド実行:", + "CommonAuto": "自動", + "CommonOff": "オフ", + "CommonOn": "オン", + "InputDialogYes": "はい", + "InputDialogNo": "いいえ", + "DialogProfileInvalidProfileNameErrorMessage": "プロファイル名に無効な文字が含まれています. 再度試してみてください.", + "MenuBarOptionsPauseEmulation": "一時停止", + "MenuBarOptionsResumeEmulation": "再開", + "AboutUrlTooltipMessage": "クリックするとデフォルトのブラウザで Ryujinx のウェブサイトを開きます.", + "AboutDisclaimerMessage": "Ryujinx は Nintendo™ および\nそのパートナー企業とは一切関係ありません.", + "AboutAmiiboDisclaimerMessage": "AmiiboAPI (www.amiiboapi.com) は\nAmiibo エミュレーションに使用されています.", + "AboutPatreonUrlTooltipMessage": "クリックするとデフォルトのブラウザで Ryujinx の Patreon ページを開きます.", + "AboutGithubUrlTooltipMessage": "クリックするとデフォルトのブラウザで Ryujinx の Github ページを開きます.", + "AboutDiscordUrlTooltipMessage": "クリックするとデフォルトのブラウザで Ryujinx の Discord サーバを開きます.", + "AboutTwitterUrlTooltipMessage": "クリックするとデフォルトのブラウザで Ryujinx の Twitter ページを開きます.", + "AboutRyujinxAboutTitle": "Ryujinx について:", + "AboutRyujinxAboutContent": "Ryujinx は Nintendo Switch™ のエミュレータです.\nPatreon で私達の活動を支援してください.\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": "すべて無効", + "ModManagerDeleteAllButton": "すべて削除", + "MenuBarOptionsChangeLanguage": "言語を変更", + "MenuBarShowFileTypes": "ファイル形式を表示", + "CommonSort": "並べ替え", + "CommonShowNames": "名称を表示", + "CommonFavorite": "お気に入り", + "OrderAscending": "昇順", + "OrderDescending": "降順", + "SettingsTabGraphicsFeatures": "機能", + "ErrorWindowTitle": "エラーウインドウ", + "ToggleDiscordTooltip": "Discord の \"現在プレイ中\" アクティビティに Ryujinx を表示するかどうかを選択します", + "AddGameDirBoxTooltip": "リストに追加するゲームディレクトリを入力します", + "AddGameDirTooltip": "リストにゲームディレクトリを追加します", + "RemoveGameDirTooltip": "選択したゲームディレクトリを削除します", + "AddAutoloadDirBoxTooltip": "Enter an autoload directory to add to the list", + "AddAutoloadDirTooltip": "Add an autoload directory to the list", + "RemoveAutoloadDirTooltip": "Remove selected autoload directory", + "CustomThemeCheckTooltip": "エミュレータのメニュー外観を変更するためカスタム Avalonia テーマを使用します", + "CustomThemePathTooltip": "カスタム GUI テーマのパスです", + "CustomThemeBrowseTooltip": "カスタム GUI テーマを参照します", + "DockModeToggleTooltip": "有効にすると,ドッキングされた Nintendo Switch をエミュレートします.多くのゲームではグラフィックス品質が向上します.\n無効にすると,携帯モードの Nintendo Switch をエミュレートします.グラフィックスの品質は低下します.\n\nドッキングモード有効ならプレイヤー1の,無効なら携帯の入力を設定してください.\n\nよくわからない場合はオンのままにしてください.", + "DirectKeyboardTooltip": "直接キーボード アクセス (HID) のサポートです. テキスト入力デバイスとしてキーボードへのゲームアクセスを提供します.\n\nSwitchハードウェアでキーボードの使用をネイティブにサポートしているゲームでのみ動作します.\n\nわからない場合はオフのままにしてください.", + "DirectMouseTooltip": "直接マウスアクセス (HID) のサポートです. ポインティングデバイスとしてマウスへのゲームアクセスを提供します.\n\nSwitchハードウェアでマウスの使用をネイティブにサポートしているゲームでのみ動作します.\n\n有効にしている場合, タッチスクリーン機能は動作しない場合があります.\n\nわからない場合はオフのままにしてください.", + "RegionTooltip": "システムの地域を変更します", + "LanguageTooltip": "システムの言語を変更します", + "TimezoneTooltip": "システムのタイムゾーンを変更します", + "TimeTooltip": "システムの時刻を変更します", + "VSyncToggleTooltip": "エミュレートされたゲーム機の垂直同期です. 多くのゲームにおいて, フレームリミッタとして機能します. 無効にすると, ゲームが高速で実行されたり, ロード中に時間がかかったり, 止まったりすることがあります.\n\n設定したホットキー(デフォルトではF1)で, ゲーム内で切り替え可能です. 無効にする場合は, この操作を行うことをおすすめします.\n\nよくわからない場合はオンのままにしてください.", + "PptcToggleTooltip": "翻訳されたJIT関数をセーブすることで, ゲームをロードするたびに毎回翻訳する処理を不要とします.\n\n一度ゲームを起動すれば,二度目以降の起動時遅延を大きく軽減できます.\n\nよくわからない場合はオンのままにしてください.", + "LowPowerPptcToggleTooltip": "Load the PPTC using a third of the amount of cores.", + "FsIntegrityToggleTooltip": "ゲーム起動時にファイル破損をチェックし,破損が検出されたらログにハッシュエラーを表示します..\n\nパフォーマンスには影響なく, トラブルシューティングに役立ちます.\n\nよくわからない場合はオンのままにしてください.", + "AudioBackendTooltip": "音声レンダリングに使用するバックエンドを変更します.\n\nSDL2 が優先され, OpenAL と SoundIO はフォールバックとして使用されます. ダミーは音声出力しません.\n\nよくわからない場合は SDL2 を設定してください.", + "MemoryManagerTooltip": "ゲストメモリのマップ/アクセス方式を変更します. エミュレートされるCPUのパフォーマンスに大きな影響を与えます.\n\nよくわからない場合は「ホスト,チェックなし」を設定してください.", + "MemoryManagerSoftwareTooltip": "アドレス変換にソフトウェアページテーブルを使用します. 非常に正確ですがパフォーマンスが大きく低下します.", + "MemoryManagerHostTooltip": "ホストのアドレス空間にメモリを直接マップします.JITのコンパイルと実行速度が大きく向上します.", + "MemoryManagerUnsafeTooltip": "メモリを直接マップしますが, アクセス前にゲストのアドレス空間内のアドレスをマスクしません. より高速になりますが, 安全性が犠牲になります. ゲストアプリケーションは Ryujinx のどこからでもメモリにアクセスできるので,このモードでは信頼できるプログラムだけを実行するようにしてください.", + "UseHypervisorTooltip": "JIT の代わりにハイパーバイザーを使用します. 利用可能な場合, パフォーマンスが大幅に向上しますが, 現在の状態では不安定になる可能性があります.", + "DRamTooltip": "エミュレートされたシステムのメモリ容量を 4GiB から 6GiB に増加します.\n\n高解像度のテクスチャパックや 4K解像度の mod を使用する場合に有用です. パフォーマンスを改善するものではありません.\n\nよくわからない場合はオフのままにしてください.", + "IgnoreMissingServicesTooltip": "未実装の Horizon OS サービスを無視します. 特定のゲームにおいて起動時のクラッシュを回避できる場合があります.\n\nよくわからない場合はオフのままにしてください.", + "IgnoreAppletTooltip": "ゲームプレイ中にゲームパッドが切断された場合、外部ダイアログ「コントローラーアプレット」は表示されません。このダイアログを閉じるか、新しいコントローラーを設定するように求めるプロンプトは表示されません。以前に切断されたコントローラーが再接続されると、ゲームは自動的に再開されます。", + "GraphicsBackendThreadingTooltip": "グラフィックスバックエンドのコマンドを別スレッドで実行します.\n\nシェーダのコンパイルを高速化し, 遅延を軽減し, マルチスレッド非対応の GPU ドライバにおいてパフォーマンスを改善します. マルチスレッド対応のドライバでも若干パフォーマンス改善が見られます.\n\nよくわからない場合は自動に設定してください.", + "GalThreadingTooltip": "グラフィックスバックエンドのコマンドを別スレッドで実行します.\n\nシェーダのコンパイルを高速化し, 遅延を軽減し, マルチスレッド非対応の GPU ドライバにおいてパフォーマンスを改善します. マルチスレッド対応のドライバでも若干パフォーマンス改善が見られます.\n\nよくわからない場合は自動に設定してください.", + "ShaderCacheToggleTooltip": "ディスクシェーダーキャッシュをセーブし,次回以降の実行時遅延を軽減します.\n\nよくわからない場合はオンのままにしてください.", + "ResolutionScaleTooltip": "ゲームのレンダリング解像度倍率を設定します.\n\n解像度を上げてもピクセルのように見えるゲームもあります. そのようなゲームでは, アンチエイリアスを削除するか, 内部レンダリング解像度を上げる mod を見つける必要があるかもしれません. その場合, ようなゲームでは、ネイティブを選択してください.\n\nこのオプションはゲーム実行中に下の「適用」をクリックすることで変更できます. 設定ウィンドウを脇に移動して, ゲームが好みの表示になるよう試してみてください.\n\nどのような設定でも, \"4x\" はやり過ぎであることを覚えておいてください.", + "ResolutionScaleEntryTooltip": "1.5 のような整数でない倍率を指定すると,問題が発生したりクラッシュしたりする場合があります.", + "AnisotropyTooltip": "異方性フィルタリングのレベルです. ゲームが要求する値を使用する場合は「自動」を設定してください.", + "AspectRatioTooltip": "レンダリングウインドウに適用するアスペクト比です.\n\nゲームにアスペクト比を変更する mod を使用している場合のみ変更してください.\n\nわからない場合は16:9のままにしておいてください.\n", + "ShaderDumpPathTooltip": "グラフィックス シェーダー ダンプのパスです", + "FileLogTooltip": "コンソール出力されるログをディスク上のログファイルにセーブします. パフォーマンスには影響を与えません.", + "StubLogTooltip": "stub ログメッセージをコンソールに出力します. パフォーマンスには影響を与えません.", + "InfoLogTooltip": "info ログメッセージをコンソールに出力します. パフォーマンスには影響を与えません.", + "WarnLogTooltip": "warning ログメッセージをコンソールに出力します. パフォーマンスには影響を与えません.", + "ErrorLogTooltip": "error ログメッセージをコンソールに出力します. パフォーマンスには影響を与えません.", + "TraceLogTooltip": "trace ログメッセージをコンソールに出力します. パフォーマンスには影響を与えません.", + "GuestLogTooltip": "guest ログメッセージをコンソールに出力します. パフォーマンスには影響を与えません.", + "FileAccessLogTooltip": "ファイルアクセスログメッセージをコンソールに出力します.", + "FSAccessLogModeTooltip": "コンソールへのファイルシステムアクセスログ出力を有効にします.0-3 のモードが有効です", + "DeveloperOptionTooltip": "使用上の注意", + "OpenGlLogLevel": "適切なログレベルを有効にする必要があります", + "DebugLogTooltip": "デバッグログメッセージをコンソールに出力します.\n\nログが読みづらくなり,エミュレータのパフォーマンスが低下するため,開発者から特別な指示がある場合のみ使用してください.", + "LoadApplicationFileTooltip": "ロードする Switch 互換のファイルを選択するためファイルエクスプローラを開きます", + "LoadApplicationFolderTooltip": "ロードする Switch 互換の展開済みアプリケーションを選択するためファイルエクスプローラを開きます", + "LoadDlcFromFolderTooltip": "Open a file explorer to choose one or more folders to bulk load DLC from", + "LoadTitleUpdatesFromFolderTooltip": "Open a file explorer to choose one or more folders to bulk load title updates from", + "OpenRyujinxFolderTooltip": "Ryujinx ファイルシステムフォルダを開きます", + "OpenRyujinxLogsTooltip": "ログが格納されるフォルダを開きます", + "ExitTooltip": "Ryujinx を終了します", + "OpenSettingsTooltip": "設定ウインドウを開きます", + "OpenProfileManagerTooltip": "ユーザプロファイル管理ウインドウを開きます", + "StopEmulationTooltip": "ゲームのエミュレーションを中止してゲーム選択画面に戻ります", + "CheckUpdatesTooltip": "Ryujinx のアップデートを確認します", + "OpenAboutTooltip": "Ryujinx についてのウインドウを開きます", + "GridSize": "グリッドサイズ", + "GridSizeTooltip": "グリッドサイズを変更します", + "SettingsTabSystemSystemLanguageBrazilianPortuguese": "ポルトガル語(ブラジル)", + "AboutRyujinxContributorsButtonHeader": "すべての貢献者を確認", + "SettingsTabSystemAudioVolume": "音量: ", + "AudioVolumeTooltip": "音量を変更します", + "SettingsTabSystemEnableInternetAccess": "ゲストインターネットアクセス / LAN モード", + "EnableInternetAccessTooltip": "エミュレートしたアプリケーションをインターネットに接続できるようにします.\n\nLAN モードを持つゲーム同士は,この機能を有効にして同じアクセスポイントに接続すると接続できます. 実機も含まれます.\n\n任天堂のサーバーには接続できません. インターネットに接続しようとすると,特定のゲームでクラッシュすることがあります.\n\nよくわからない場合はオフのままにしてください.", + "GameListContextMenuManageCheatToolTip": "チートを管理します", + "GameListContextMenuManageCheat": "チートを管理", + "GameListContextMenuManageModToolTip": "Modを管理します", + "GameListContextMenuManageMod": "Manage Mods", + "ControllerSettingsStickRange": "範囲:", + "DialogStopEmulationTitle": "Ryujinx - エミュレーションを中止", + "DialogStopEmulationMessage": "エミュレーションを中止してよろしいですか?", + "SettingsTabCpu": "CPU", + "SettingsTabAudio": "音声", + "SettingsTabNetwork": "ネットワーク", + "SettingsTabNetworkConnection": "ネットワーク接続", + "SettingsTabCpuCache": "CPU キャッシュ", + "SettingsTabCpuMemory": "CPU メモリ", + "DialogUpdaterFlatpakNotSupportedMessage": "FlatHub を使用して Ryujinx をアップデートしてください.", + "UpdaterDisabledWarningTitle": "アップデータは無効です!", + "ControllerSettingsRotate90": "時計回りに 90° 回転", + "IconSize": "アイコンサイズ", + "IconSizeTooltip": "ゲームアイコンのサイズを変更します", + "MenuBarOptionsShowConsole": "コンソールを表示", + "ShaderCachePurgeError": "シェーダーキャッシュの破棄エラー {0}: {1}", + "UserErrorNoKeys": "Keys がありません", + "UserErrorNoFirmware": "ファームウェアがありません", + "UserErrorFirmwareParsingFailed": "ファームウェアのパーズエラー", + "UserErrorApplicationNotFound": "アプリケーションがありません", + "UserErrorUnknown": "不明なエラー", + "UserErrorUndefined": "未定義エラー", + "UserErrorNoKeysDescription": "'prod.keys' が見つかりませんでした", + "UserErrorNoFirmwareDescription": "インストールされたファームウェアが見つかりませんでした", + "UserErrorFirmwareParsingFailedDescription": "ファームウェアをパーズできませんでした.通常,古いキーが原因です.", + "UserErrorApplicationNotFoundDescription": "指定されたパスに有効なアプリケーションがありませんでした.", + "UserErrorUnknownDescription": "不明なエラーが発生しました!", + "UserErrorUndefinedDescription": "未定義のエラーが発生しました! 発生すべきものではないので,開発者にご連絡ください!", + "OpenSetupGuideMessage": "セットアップガイドを開く", + "NoUpdate": "アップデートなし", + "TitleUpdateVersionLabel": "バージョン {0} - {1}", + "TitleBundledUpdateVersionLabel": "Bundled: Version {0}", + "TitleBundledDlcLabel": "Bundled:", + "TitleXCIStatusPartialLabel": "Partial", + "TitleXCIStatusTrimmableLabel": "Untrimmed", + "TitleXCIStatusUntrimmableLabel": "Trimmed", + "TitleXCIStatusFailedLabel": "(Failed)", + "TitleXCICanSaveLabel": "Save {0:n0} Mb", + "TitleXCISavingLabel": "Saved {0:n0} Mb", + "RyujinxInfo": "Ryujinx - 情報", + "RyujinxConfirm": "Ryujinx - 確認", + "FileDialogAllTypes": "すべての種別", + "Never": "決して", + "SwkbdMinCharacters": "最低 {0} 文字必要です", + "SwkbdMinRangeCharacters": "{0}-{1} 文字にしてください", + "SoftwareKeyboard": "ソフトウェアキーボード", + "SoftwareKeyboardModeNumeric": "0-9 または '.' のみでなければなりません", + "SoftwareKeyboardModeAlphabet": "CJK文字以外のみ", + "SoftwareKeyboardModeASCII": "ASCII文字列のみ", + "ControllerAppletControllers": "サポートされているコントローラ:", + "ControllerAppletPlayers": "プレイヤー:", + "ControllerAppletDescription": "現在の設定は無効です. 設定を開いて入力を再設定してください.", + "ControllerAppletDocked": "ドッキングモードが設定されています. 携帯コントロールは無効にする必要があります.", + "UpdaterRenaming": "古いファイルをリネーム中...", + "UpdaterRenameFailed": "ファイルをリネームできませんでした: {0}", + "UpdaterAddingFiles": "新規ファイルを追加中...", + "UpdaterExtracting": "アップデートを展開中...", + "UpdaterDownloading": "アップデートをダウンロード中...", + "Game": "ゲーム", + "Docked": "ドッキング", + "Handheld": "携帯", + "ConnectionError": "接続エラー.", + "AboutPageDeveloperListMore": "{0}, その他大勢...", + "ApiError": "API エラー.", + "LoadingHeading": "ロード中: {0}", + "CompilingPPTC": "PTC をコンパイル中", + "CompilingShaders": "シェーダーをコンパイル中", + "AllKeyboards": "すべてのキーボード", + "OpenFileDialogTitle": "開くファイルを選択", + "OpenFolderDialogTitle": "展開されたゲームフォルダを選択", + "AllSupportedFormats": "すべての対応フォーマット", + "RyujinxUpdater": "Ryujinx アップデータ", + "SettingsTabHotkeys": "キーボード ホットキー", + "SettingsTabHotkeysHotkeys": "キーボード ホットキー", + "SettingsTabHotkeysToggleVsyncHotkey": "VSync 切り替え:", + "SettingsTabHotkeysScreenshotHotkey": "スクリーンショット:", + "SettingsTabHotkeysShowUiHotkey": "UI表示:", + "SettingsTabHotkeysPauseHotkey": "一時停止:", + "SettingsTabHotkeysToggleMuteHotkey": "ミュート:", + "ControllerMotionTitle": "モーションコントロール設定", + "ControllerRumbleTitle": "振動設定", + "SettingsSelectThemeFileDialogTitle": "テーマファイルを選択", + "SettingsXamlThemeFile": "Xaml テーマファイル", + "AvatarWindowTitle": "アカウント - アバター管理", + "Amiibo": "Amiibo", + "Unknown": "不明", + "Usage": "使用法", + "Writable": "書き込み可能", + "SelectDlcDialogTitle": "DLC ファイルを選択", + "SelectUpdateDialogTitle": "アップデートファイルを選択", + "SelectModDialogTitle": "modディレクトリを選択", + "TrimXCIFileDialogTitle": "Check and Trim XCI File", + "TrimXCIFileDialogPrimaryText": "This function will first check the empty space and then trim the XCI File to save disk space.", + "TrimXCIFileDialogSecondaryText": "Current File Size: {0:n} MB\nGame Data Size: {1:n} MB\nDisk Space Savings: {2:n} MB", + "TrimXCIFileNoTrimNecessary": "XCI File does not need to be trimmed. Check logs for further details", + "TrimXCIFileNoUntrimPossible": "XCI File cannot be untrimmed. Check logs for further details", + "TrimXCIFileReadOnlyFileCannotFix": "XCI File is Read Only and could not be made writable. Check logs for further details", + "TrimXCIFileFileSizeChanged": "XCI File has changed in size since it was scanned. Please check the file is not being written to and try again.", + "TrimXCIFileFreeSpaceCheckFailed": "XCI File has data in the free space area, it is not safe to trim", + "TrimXCIFileInvalidXCIFile": "XCI File contains invalid data. Check logs for further details", + "TrimXCIFileFileIOWriteError": "XCI File could not be opened for writing. Check logs for further details", + "TrimXCIFileFailedPrimaryText": "Trimming of the XCI file failed", + "TrimXCIFileCancelled": "The operation was cancelled", + "TrimXCIFileFileUndertermined": "No operation was performed", + "UserProfileWindowTitle": "ユーザプロファイルを管理", + "CheatWindowTitle": "チート管理", + "DlcWindowTitle": "DLC 管理", + "ModWindowTitle": "Manage Mods for {0} ({1})", + "UpdateWindowTitle": "アップデート管理", + "XCITrimmerWindowTitle": "XCI File Trimmer", + "XCITrimmerTitleStatusCount": "{0} of {1} Title(s) Selected", + "XCITrimmerTitleStatusCountWithFilter": "{0} of {1} Title(s) Selected ({2} displayed)", + "XCITrimmerTitleStatusTrimming": "Trimming {0} Title(s)...", + "XCITrimmerTitleStatusUntrimming": "Untrimming {0} Title(s)...", + "XCITrimmerTitleStatusFailed": "Failed", + "XCITrimmerPotentialSavings": "Potential Savings", + "XCITrimmerActualSavings": "Actual Savings", + "XCITrimmerSavingsMb": "{0:n0} Mb", + "XCITrimmerSelectDisplayed": "Select Shown", + "XCITrimmerDeselectDisplayed": "Deselect Shown", + "XCITrimmerSortName": "Title", + "XCITrimmerSortSaved": "Space Savings", + "XCITrimmerTrim": "Trim", + "XCITrimmerUntrim": "Untrim", + "UpdateWindowUpdateAddedMessage": "{0} new update(s) added", + "UpdateWindowBundledContentNotice": "Bundled updates cannot be removed, only disabled.", + "CheatWindowHeading": "利用可能なチート {0} [{1}]", + "BuildId": "ビルドID:", + "DlcWindowHeading": "利用可能な DLC {0} [{1}]", + "DlcWindowDlcAddedMessage": "{0} new downloadable content(s) added", + "AutoloadDlcAddedMessage": "{0} new downloadable content(s) added", + "AutoloadDlcRemovedMessage": "{0} missing downloadable content(s) removed", + "AutoloadUpdateAddedMessage": "{0} new update(s) added", + "AutoloadUpdateRemovedMessage": "{0} missing update(s) removed", + "ModWindowHeading": "{0} Mod(s)", + "UserProfilesEditProfile": "編集", + "Continue": "Continue", + "Cancel": "キャンセル", + "Save": "セーブ", + "Discard": "破棄", + "Paused": "一時停止中", + "UserProfilesSetProfileImage": "プロファイル画像を設定", + "UserProfileEmptyNameError": "名称が必要です", + "UserProfileNoImageError": "プロファイル画像が必要です", + "GameUpdateWindowHeading": "利用可能なアップデート {0} [{1}]", + "SettingsTabHotkeysResScaleUpHotkey": "解像度を上げる:", + "SettingsTabHotkeysResScaleDownHotkey": "解像度を下げる:", + "UserProfilesName": "名称:", + "UserProfilesUserId": "ユーザID:", + "SettingsTabGraphicsBackend": "グラフィックスバックエンド", + "SettingsTabGraphicsBackendTooltip": "エミュレーションに使用するグラフィックスバックエンドを選択します.\n\nVulkanは, 最近のグラフィックカードでドライバが最新であれば, 全体的に優れています. すべてのGPUベンダーで, シェーダーコンパイルがより高速で, スタッタリングが少ないのが特徴です.\n\n古いNvidia GPU, Linuxでの古いAMD GPU, VRAMの少ないGPUなどでは, OpenGLの方が良い結果が得られるかもしれません. ですが, シェーダーコンパイルのスタッターは大きくなります.\n\n不明な場合はVulkanに設定してください。最新のグラフィックドライバでもVulkanをサポートしていないGPUの場合は, OpenGLに設定してください.", + "SettingsEnableTextureRecompression": "テクスチャの再圧縮を有効にする", + "SettingsEnableTextureRecompressionTooltip": "VRAM使用量を減らすためにASTCテクスチャを圧縮します.\n\nこのテクスチャフォーマットを使用するゲームには, Astral Chain, Bayonetta 3, Fire Emblem Engage, Metroid Prime Remastered, Super Mario Bros. Wonder, The Legend of Zelda: Tears of the Kingdomが含まれます.\n\nVRAMが4GB以下のグラフィックカードでは, これらのゲームを実行中にクラッシュする可能性があります.\n\n前述のゲームでVRAMが不足している場合のみ有効にしてください. 不明な場合はオフにしてください.", + "SettingsTabGraphicsPreferredGpu": "優先使用するGPU", + "SettingsTabGraphicsPreferredGpuTooltip": "Vulkanグラフィックスバックエンドで使用されるグラフィックスカードを選択します.\n\nOpenGLが使用するGPUには影響しません.\n\n不明な場合は, \"dGPU\" としてフラグが立っているGPUに設定します. ない場合はそのままにします.", + "SettingsAppRequiredRestartMessage": "Ryujinx の再起動が必要です", + "SettingsGpuBackendRestartMessage": "グラフィックスバックエンドまたはGPUの設定が変更されました. 変更を適用するには再起動する必要があります", + "SettingsGpuBackendRestartSubMessage": "今すぐ再起動しますか?", + "RyujinxUpdaterMessage": "Ryujinx を最新版にアップデートしますか?", + "SettingsTabHotkeysVolumeUpHotkey": "音量を上げる:", + "SettingsTabHotkeysVolumeDownHotkey": "音量を下げる:", + "SettingsEnableMacroHLE": "マクロの高レベルエミュレーション (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": "ゲームレンダリングにアンチエイリアスを適用します.\n\nFXAAは画像の大部分をぼかし, SMAAはギザギザのエッジを見つけて滑らかにします.\n\nFSRスケーリングフィルタとの併用は推奨しません.\n\nこのオプションは, ゲーム実行中に下の「適用」をクリックして変更できます. 設定ウィンドウを脇に移動し, ゲームが好みの表示になるように試してみてください.\n\n不明な場合は「なし」のままにしておいてください.", + "GraphicsAALabel": "アンチエイリアス:", + "GraphicsScalingFilterLabel": "スケーリングフィルタ:", + "GraphicsScalingFilterTooltip": "解像度変更時に適用されるスケーリングフィルタを選択します.\n\nBilinearは3Dゲームに適しており, 安全なデフォルトオプションです.\n\nピクセルアートゲームにはNearestを推奨します.\n\nFSR 1.0は単なるシャープニングフィルタであり, FXAAやSMAAとの併用は推奨されません.\n\nこのオプションは, ゲーム実行中に下の「適用」をクリックすることで変更できます. 設定ウィンドウを脇に移動し, ゲームが好みの表示になるように試してみてください.\n\n不明な場合はBilinearのままにしておいてください.", + "GraphicsScalingFilterBilinear": "Bilinear", + "GraphicsScalingFilterNearest": "Nearest", + "GraphicsScalingFilterFsr": "FSR", + "GraphicsScalingFilterArea": "Area", + "GraphicsScalingFilterLevelLabel": "レベル", + "GraphicsScalingFilterLevelTooltip": "FSR 1.0のシャープ化レベルを設定します. 高い値ほどシャープになります.", + "SmaaLow": "SMAA Low", + "SmaaMedium": "SMAA Medium", + "SmaaHigh": "SMAA High", + "SmaaUltra": "SMAA Ultra", + "UserEditorTitle": "ユーザを編集", + "UserEditorTitleCreate": "ユーザを作成", + "SettingsTabNetworkInterface": "ネットワークインタフェース:", + "NetworkInterfaceTooltip": "LAN/LDN機能に使用されるネットワークインタフェースです.\n\nVPNやXLink Kai、LAN対応のゲームと併用することで, インターネット上の同一ネットワーク接続になりすますことができます.\n\n不明な場合はデフォルトのままにしてください.", + "NetworkInterfaceDefault": "デフォルト", + "PackagingShaders": "シェーダーを構築中", + "AboutChangelogButton": "GitHub で更新履歴を表示", + "AboutChangelogButtonTooltipMessage": "クリックして, このバージョンの更新履歴をデフォルトのブラウザで開きます.", + "SettingsTabNetworkMultiplayer": "マルチプレイヤー", + "MultiplayerMode": "モード:", + "MultiplayerModeTooltip": "LDNマルチプレイヤーモードを変更します.\n\nldn_mitmモジュールがインストールされた, 他のRyujinxインスタンスや,ハックされたNintendo Switchコンソールとのローカル/同一ネットワーク接続を可能にします.\n\nマルチプレイでは, すべてのプレイヤーが同じゲームバージョンである必要があります(例:Super Smash Bros. Ultimate v13.0.1はv13.0.0に接続できません).\n\n不明な場合は「無効」のままにしてください.", + "MultiplayerModeDisabled": "無効", + "MultiplayerModeLdnMitm": "ldn_mitm", + "MultiplayerModeLdnRyu": "RyuLDN", + "MultiplayerDisableP2P": "Disable P2P Network Hosting (may increase latency)", + "MultiplayerDisableP2PTooltip": "Disable P2P network hosting, peers will proxy through the master server instead of connecting to you directly.", + "LdnPassphrase": "Network Passphrase:", + "LdnPassphraseTooltip": "You will only be able to see hosted games with the same passphrase as you.", + "LdnPassphraseInputTooltip": "Enter a passphrase in the format Ryujinx-<8 hex chars>. You will only be able to see hosted games with the same passphrase as you.", + "LdnPassphraseInputPublic": "(public)", + "GenLdnPass": "Generate Random", + "GenLdnPassTooltip": "Generates a new passphrase, which can be shared with other players.", + "ClearLdnPass": "Clear", + "ClearLdnPassTooltip": "Clears the current passphrase, returning to the public network.", + "InvalidLdnPassphrase": "Invalid Passphrase! Must be in the format \"Ryujinx-<8 hex chars>\"" +} diff --git a/src/Ryujinx/Assets/Locales/ko_KR.json b/src/Ryujinx/Assets/Locales/ko_KR.json new file mode 100644 index 000000000..aeeb84c62 --- /dev/null +++ b/src/Ryujinx/Assets/Locales/ko_KR.json @@ -0,0 +1,868 @@ +{ + "Language": "한국어", + "MenuBarFileOpenApplet": "애플릿 열기", + "MenuBarFileOpenAppletOpenMiiApplet": "Mii Edit Applet", + "MenuBarFileOpenAppletOpenMiiAppletToolTip": "독립 실행형 모드로 Mii 편집기 애플릿 열기", + "SettingsTabInputDirectMouseAccess": "마우스 직접 접근", + "SettingsTabSystemMemoryManagerMode": "메모리 관리자 모드 :", + "SettingsTabSystemMemoryManagerModeSoftware": "소프트웨어", + "SettingsTabSystemMemoryManagerModeHost": "호스트(빠름)", + "SettingsTabSystemMemoryManagerModeHostUnchecked": "호스트 확인 안함(가장 빠르나 위험)", + "SettingsTabSystemUseHypervisor": "하이퍼바이저 사용", + "MenuBarFile": "파일(_F)", + "MenuBarFileOpenFromFile": "파일에서 앱 불러오기(_L)", + "MenuBarFileOpenFromFileError": "선택한 파일에서 앱을 찾을 수 없습니다.", + "MenuBarFileOpenUnpacked": "압축 푼 게임 불러오기(_U)", + "MenuBarFileLoadDlcFromFolder": "폴더에서 DLC 불러오기", + "MenuBarFileLoadTitleUpdatesFromFolder": "폴더에서 타이틀 업데이트 불러오기", + "MenuBarFileOpenEmuFolder": "Ryujinx 폴더 열기", + "MenuBarFileOpenLogsFolder": "로그 폴더 열기", + "MenuBarFileExit": "종료(_E)", + "MenuBarOptions": "옵션(_O)", + "MenuBarOptionsToggleFullscreen": "전체 화면 전환", + "MenuBarOptionsStartGamesInFullscreen": "전체 화면 모드로 게임 시작", + "MenuBarOptionsStopEmulation": "에뮬레이션 중지", + "MenuBarOptionsSettings": "설정(_S)", + "MenuBarOptionsManageUserProfiles": "사용자 프로필 관리(_M)", + "MenuBarActions": "동작(_A)", + "MenuBarOptionsSimulateWakeUpMessage": "웨이크업 메시지 시뮬레이션", + "MenuBarActionsScanAmiibo": "Amiibo 스캔", + "MenuBarTools": "도구(_T)", + "MenuBarToolsInstallFirmware": "펌웨어 설치", + "MenuBarFileToolsInstallFirmwareFromFile": "XCI 또는 ZIP으로 펌웨어 설치", + "MenuBarFileToolsInstallFirmwareFromDirectory": "디렉터리에서 펌웨어 설치", + "MenuBarToolsManageFileTypes": "파일 형식 관리", + "MenuBarToolsInstallFileTypes": "파일 형식 설치", + "MenuBarToolsUninstallFileTypes": "파일 형식 제거", + "MenuBarToolsXCITrimmer": "XCI 파일 트리머", + "MenuBarView": "보기(_V)", + "MenuBarViewWindow": "윈도 창", + "MenuBarViewWindow720": "720p", + "MenuBarViewWindow1080": "1080p", + "MenuBarHelp": "도움말(_H)", + "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 관리 창 열기", + "GameListContextMenuCacheManagement": "캐시 관리", + "GameListContextMenuCacheManagementPurgePptc": "대기열 PPTC 재구성", + "GameListContextMenuCacheManagementPurgePptcToolTip": "다음 게임 실행 부팅 시, PPTC를 트리거하여 다시 구성", + "GameListContextMenuCacheManagementPurgeShaderCache": "퍼지 셰이더 캐시", + "GameListContextMenuCacheManagementPurgeShaderCacheToolTip": "앱의 셰이더 캐시 삭제", + "GameListContextMenuCacheManagementOpenPptcDirectory": "PPTC 디렉터리 열기", + "GameListContextMenuCacheManagementOpenPptcDirectoryToolTip": "앱의 PPTC 캐시가 포함된 디렉터리 열기", + "GameListContextMenuCacheManagementOpenShaderCacheDirectory": "셰이더 캐시 디렉터리 열기", + "GameListContextMenuCacheManagementOpenShaderCacheDirectoryToolTip": "앱의 셰이더 캐시가 포함된 디렉터리 열기", + "GameListContextMenuExtractData": "데이터 추출", + "GameListContextMenuExtractDataExeFS": "ExeFS", + "GameListContextMenuExtractDataExeFSToolTip": "앱의 현재 구성에서 ExeFS 추출(업데이트 포함)", + "GameListContextMenuExtractDataRomFS": "RomFS", + "GameListContextMenuExtractDataRomFSToolTip": "앱의 현재 구성에서 RomFS 추출(업데이트 포함)", + "GameListContextMenuExtractDataLogo": "로고", + "GameListContextMenuExtractDataLogoToolTip": "앱의 현재 구성에서 로고 섹션 추출 (업데이트 포함)", + "GameListContextMenuCreateShortcut": "바로 가기 만들기", + "GameListContextMenuCreateShortcutToolTip": "선택한 앱을 실행하는 바탕 화면에 바로 가기를 생성", + "GameListContextMenuCreateShortcutToolTipMacOS": "선택한 앱을 실행하는 macOS 앱 폴더에 바로 가기 만들기", + "GameListContextMenuOpenModsDirectory": "모드 디렉터리 열기", + "GameListContextMenuOpenModsDirectoryToolTip": "앱의 모드가 포함된 디렉터리 열기", + "GameListContextMenuOpenSdModsDirectory": "Atmosphere 모드 디렉터리 열기", + "GameListContextMenuOpenSdModsDirectoryToolTip": "해당 게임의 모드가 포함된 대체 SD 카드 Atmosphere 디렉터리를 엽니다. 실제 하드웨어용으로 패키징된 모드에 유용합니다.", + "GameListContextMenuTrimXCI": "XCI 파일 확인 및 트림", + "GameListContextMenuTrimXCIToolTip": "디스크 공간을 절약하기 위해 XCI 파일 확인 및 트림", + "StatusBarGamesLoaded": "{0}/{1}개의 게임 불러옴", + "StatusBarSystemVersion": "시스템 버전 : {0}", + "StatusBarXCIFileTrimming": "XCI 파일 '{0}' 트리밍", + "LinuxVmMaxMapCountDialogTitle": "메모리 매핑 한계 감지", + "LinuxVmMaxMapCountDialogTextPrimary": "vm.max_map_count의 값을 {0}으로 늘리시겠습니까?", + "LinuxVmMaxMapCountDialogTextSecondary": "일부 게임은 현재 허용된 것보다 더 많은 메모리 매핑을 만들려고 할 수 있습니다. 이 제한을 초과하면 Ryujinx가 충돌이 발생할 수 있습니다.", + "LinuxVmMaxMapCountDialogButtonUntilRestart": "예, 다음에 다시 시작할 때까지", + "LinuxVmMaxMapCountDialogButtonPersistent": "예, 영구적으로", + "LinuxVmMaxMapCountWarningTextPrimary": "메모리 매핑의 최대 용량이 권장 용량보다 부족합니다.", + "LinuxVmMaxMapCountWarningTextSecondary": "vm.max_map_count({0})의 현재 값은 {1}보다 낮습니다. 일부 게임은 현재 허용된 것보다 더 많은 메모리 매핑을 만들려고 할 수 있습니다. Ryujinx는 이 제한을 초과하자마자 충돌할 것입니다.\n\n제한을 수동으로 늘리거나 Ryujinx가 이를 지원할 수 있도록 pkexec를 설치하는 것을 추천합니다.", + "Settings": "설정", + "SettingsTabGeneral": "사용자 인터페이스", + "SettingsTabGeneralGeneral": "일반", + "SettingsTabGeneralEnableDiscordRichPresence": "디스코드 활동 상태 활성화", + "SettingsTabGeneralCheckUpdatesOnLaunch": "시작 시, 업데이트 확인", + "SettingsTabGeneralShowConfirmExitDialog": "\"종료 확인\" 대화 상자 표시", + "SettingsTabGeneralRememberWindowState": "창 크기/위치 기억", + "SettingsTabGeneralShowTitleBar": "제목 표시줄 표시(다시 시작해야 함)", + "SettingsTabGeneralHideCursor": "커서 숨기기 :", + "SettingsTabGeneralHideCursorNever": "절대 안 함", + "SettingsTabGeneralHideCursorOnIdle": "유휴 상태", + "SettingsTabGeneralHideCursorAlways": "항상", + "SettingsTabGeneralGameDirectories": "게임 데릭터리", + "SettingsTabGeneralAutoloadDirectories": "DLC/업데이트 디렉터리 자동 불러오기", + "SettingsTabGeneralAutoloadNote": "누락된 파일을 참조하는 DLC 및 업데이트가 자동으로 언로드", + "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(프로파일된 영구 번역 캐시)", + "SettingsTabSystemEnableLowPowerPptc": "저전력 PPTC 캐시", + "SettingsTabSystemEnableFsIntegrityChecks": "파일 시스템 무결성 검사", + "SettingsTabSystemAudioBackend": "음향 후단부 :", + "SettingsTabSystemAudioBackendDummy": "더미", + "SettingsTabSystemAudioBackendOpenAL": "OpenAL", + "SettingsTabSystemAudioBackendSoundIO": "SoundIO", + "SettingsTabSystemAudioBackendSDL2": "SDL2", + "SettingsTabSystemHacks": "핵", + "SettingsTabSystemHacksNote": "불안정성을 유발할 수 있음", + "SettingsTabSystemDramSize": "DRAM 크기 :", + "SettingsTabSystemDramSize4GiB": "4GB", + "SettingsTabSystemDramSize6GiB": "6GB", + "SettingsTabSystemDramSize8GiB": "8GB", + "SettingsTabSystemDramSize12GiB": "12GB", + "SettingsTabSystemIgnoreMissingServices": "누락된 서비스 무시", + "SettingsTabSystemIgnoreApplet": "애플릿 무시", + "SettingsTabGraphics": "그래픽", + "SettingsTabGraphicsAPI": "그래픽 API", + "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": "조각 기록 활성화", + "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": "프로 컨트롤러", + "ControllerSettingsControllerTypeJoyConPair": "조이콘 페어링", + "ControllerSettingsControllerTypeJoyConLeft": "좌측 조이콘", + "ControllerSettingsControllerTypeJoyConRight": "우측 조이콘", + "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": "우측 조이콘 슬롯:", + "ControllerSettingsMotionServerHost": "서버 호스트 :", + "ControllerSettingsMotionGyroSensitivity": "자이로 감도 :", + "ControllerSettingsMotionGyroDeadzone": "자이로 데드존 :", + "ControllerSettingsSave": "저장", + "ControllerSettingsClose": "닫기", + "KeyUnknown": "알 수 없음", + "KeyShiftLeft": "좌측 Shift", + "KeyShiftRight": "우측 Shift", + "KeyControlLeft": "좌측 Ctrl", + "KeyMacControlLeft": "좌측 ⌃", + "KeyControlRight": "우측 Ctrl", + "KeyMacControlRight": "우측 ⌃", + "KeyAltLeft": "좌측 Alt", + "KeyMacAltLeft": "좌측 ⌥", + "KeyAltRight": "우측 Alt", + "KeyMacAltRight": "우측 ⌥", + "KeyWinLeft": "좌측 ⊞", + "KeyMacWinLeft": "좌측 ⌘", + "KeyWinRight": "우측 ⊞", + "KeyMacWinRight": "우측 ⌘", + "KeyMenu": "메뉴", + "KeyUp": "↑", + "KeyDown": "↓", + "KeyLeft": "←", + "KeyRight": "→", + "KeyEnter": "엔터", + "KeyEscape": "Esc", + "KeySpace": "스페이스", + "KeyTab": "탭", + "KeyBackSpace": "백스페이스", + "KeyInsert": "Insert", + "KeyDelete": "Delete", + "KeyPageUp": "Page Up", + "KeyPageDown": "Page Down", + "KeyHome": "Home", + "KeyEnd": "End", + "KeyCapsLock": "Caps Lock", + "KeyScrollLock": "Scroll Lock", + "KeyPrintScreen": "Print Screen", + "KeyPause": "Pause", + "KeyNumLock": "Num Lock", + "KeyClear": "지우기", + "KeyKeypad0": "키패드 0", + "KeyKeypad1": "키패드 1", + "KeyKeypad2": "키패드 2", + "KeyKeypad3": "키패드 3", + "KeyKeypad4": "키패드 4", + "KeyKeypad5": "키패드 5", + "KeyKeypad6": "키패드 6", + "KeyKeypad7": "키패드 7", + "KeyKeypad8": "키패드 8", + "KeyKeypad9": "키패드 9", + "KeyKeypadDivide": "키패드 분할", + "KeyKeypadMultiply": "키패드 멀티플", + "KeyKeypadSubtract": "키패드 빼기", + "KeyKeypadAdd": "키패드 추가", + "KeyKeypadDecimal": "숫자 키패드", + "KeyKeypadEnter": "키패드 입력", + "KeyNumber0": "0", + "KeyNumber1": "1", + "KeyNumber2": "2", + "KeyNumber3": "3", + "KeyNumber4": "4", + "KeyNumber5": "5", + "KeyNumber6": "6", + "KeyNumber7": "7", + "KeyNumber8": "8", + "KeyNumber9": "9", + "KeyTilde": "~", + "KeyGrave": "`", + "KeyMinus": "-", + "KeyPlus": "+", + "KeyBracketLeft": "[", + "KeyBracketRight": "]", + "KeySemicolon": ";", + "KeyQuote": "\"", + "KeyComma": ",", + "KeyPeriod": ".", + "KeySlash": "/", + "KeyBackSlash": "\\", + "KeyUnbound": "연동 해제", + "GamepadLeftStick": "좌측 스틱 버튼", + "GamepadRightStick": "우측 스틱 버튼", + "GamepadLeftShoulder": "좌측 숄더", + "GamepadRightShoulder": "우측 숄더", + "GamepadLeftTrigger": "좌측 트리거", + "GamepadRightTrigger": "우측 트리거", + "GamepadDpadUp": "↑", + "GamepadDpadDown": "↓", + "GamepadDpadLeft": "←", + "GamepadDpadRight": "→", + "GamepadMinus": "-", + "GamepadPlus": "+", + "GamepadGuide": "가이드", + "GamepadMisc1": "기타", + "GamepadPaddle1": "패들 1", + "GamepadPaddle2": "패들 2", + "GamepadPaddle3": "패들 3", + "GamepadPaddle4": "패들 4", + "GamepadTouchpad": "터치패드", + "GamepadSingleLeftTrigger0": "좌측 트리거 0", + "GamepadSingleRightTrigger0": "우측 트리거 0", + "GamepadSingleLeftTrigger1": "좌측 트리거 1", + "GamepadSingleRightTrigger1": "우측 트리거 1", + "StickLeft": "좌측 스틱", + "StickRight": "우측 스틱", + "UserProfilesSelectedUserProfile": "선택된 사용자 프로필 :", + "UserProfilesSaveProfileName": "프로필 이름 저장", + "UserProfilesChangeProfileImage": "프로필 이미지 변경", + "UserProfilesAvailableUserProfiles": "사용 가능한 사용자 프로필 :", + "UserProfilesAddNewProfile": "프로필 만들기", + "UserProfilesDelete": "삭제", + "UserProfilesClose": "닫기", + "ProfileNameSelectionWatermark": "별명 선택", + "ProfileImageSelectionTitle": "프로필 이미지 선택", + "ProfileImageSelectionHeader": "프로필 이미지를 선택", + "ProfileImageSelectionNote": "사용자 지정 프로필 이미지를 가져오거나 시스템 펌웨어에서 아바타 선택 가능", + "ProfileImageSelectionImportImage": "이미지 파일 가져오기", + "ProfileImageSelectionSelectAvatar": "펌웨어 아바타 선택", + "InputDialogTitle": "대화 상자 입력", + "InputDialogOk": "확인", + "InputDialogCancel": "취소", + "InputDialogCancelling": "취소하기", + "InputDialogClose": "닫기", + "InputDialogAddNewProfileTitle": "프로필 이름 선택", + "InputDialogAddNewProfileHeader": "프로필 이름을 입력", + "InputDialogAddNewProfileSubtext": "(최대 길이 : {0})", + "AvatarChoose": "아바타 선택", + "AvatarSetBackgroundColor": "배경색 설정", + "AvatarClose": "닫기", + "ControllerSettingsLoadProfileToolTip": "프로필 불러오기", + "ControllerSettingsViewProfileToolTip": "프로필 보기", + "ControllerSettingsAddProfileToolTip": "프로필 추가", + "ControllerSettingsRemoveProfileToolTip": "프로필 삭제", + "ControllerSettingsSaveProfileToolTip": "프로필 추가", + "MenuBarFileToolsTakeScreenshot": "스크린샷 찍기", + "MenuBarFileToolsHideUi": "UI 숨기기", + "GameListContextMenuRunApplication": "앱 실행", + "GameListContextMenuToggleFavorite": "즐겨찾기 전환", + "GameListContextMenuToggleFavoriteToolTip": "게임의 즐겨찾기 상태 전환", + "SettingsTabGeneralTheme": "테마 :", + "SettingsTabGeneralThemeAuto": "자동", + "SettingsTabGeneralThemeDark": "다크", + "SettingsTabGeneralThemeLight": "라이트", + "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": "NCA 단면 추출기", + "DialogNcaExtractionMainNcaNotFoundErrorMessage": "추출에 실패했습니다. 선택한 파일에 기본 NCA가 없습니다.", + "DialogNcaExtractionCheckLogErrorMessage": "추출에 실패했습니다. 자세한 내용은 로그 파일을 확인하시기 바랍니다.", + "DialogNcaExtractionSuccessMessage": "성공적으로 추출이 완료되었습니다.", + "DialogUpdaterConvertFailedMessage": "현재 Ryujinx 버전을 변환할 수 없습니다.", + "DialogUpdaterCancelUpdateMessage": "업데이트가 취소되었습니다!", + "DialogUpdaterAlreadyOnLatestVersionMessage": "이미 최신 버전의 Ryujinx를 사용 중입니다!", + "DialogUpdaterFailedToGetVersionMessage": "GitHub에서 릴리스 정보를 검색하는 동안 오류가 발생했습니다. 현재 GitHub Actions에서 새 릴리스를 컴파일하는 중일 때 발생할 수 있습니다. 몇 분 후에 다시 시도해 주세요.", + "DialogUpdaterConvertFailedGithubMessage": "GitHub에서 받은 Ryujinx 버전을 변환하지 못했습니다.", + "DialogUpdaterDownloadingMessage": "업데이트 내려받는 중...", + "DialogUpdaterExtractionMessage": "업데이트 추출 중...", + "DialogUpdaterRenamingMessage": "이름 변경 업데이트...", + "DialogUpdaterAddingFilesMessage": "새 업데이트 추가 중...", + "DialogUpdaterShowChangelogMessage": "변경 로그 보기", + "DialogUpdaterCompleteMessage": "업데이트가 완료되었습니다!", + "DialogUpdaterRestartMessage": "지금 Ryujinx를 다시 시작하시겠습니까?", + "DialogUpdaterNoInternetMessage": "인터넷에 연결되어 있지 않습니다!", + "DialogUpdaterNoInternetSubMessage": "인터넷이 제대로 연결되어 있는지 확인하세요!", + "DialogUpdaterDirtyBuildMessage": "Ryujinx의 더티 빌드는 업데이트할 수 없습니다!", + "DialogUpdaterDirtyBuildSubMessage": "지원되는 버전을 찾으신다면 https://ryujinx.app/download 에서 Ryujinx를 내려받으세요.", + "DialogRestartRequiredMessage": "다시 시작 필요", + "DialogThemeRestartMessage": "테마를 저장했습니다. 테마를 적용하려면 다시 시작해야 합니다.", + "DialogThemeRestartSubMessage": "다시 시작하시겠습니까?", + "DialogFirmwareInstallEmbeddedMessage": "이 게임에 포함된 펌웨어를 설치하시겠습니까?(Firmware {0})", + "DialogFirmwareInstallEmbeddedSuccessMessage": "설치된 펌웨어를 찾을 수 없지만 Ryujinx는 제공된 게임에서 펌웨어 {0}을(를) 설치할 수 있습니다.\n이제 에뮬레이터가 시작됩니다.", + "DialogFirmwareNoFirmwareInstalledMessage": "펌웨어가 설치되어 있지 않음", + "DialogFirmwareInstalledMessage": "펌웨어 {0}이(가) 설치됨", + "DialogInstallFileTypesSuccessMessage": "파일 형식을 성공적으로 설치했습니다!", + "DialogInstallFileTypesErrorMessage": "파일 형식을 설치하지 못했습니다.", + "DialogUninstallFileTypesSuccessMessage": "파일 형식이 성공적으로 제거되었습니다!", + "DialogUninstallFileTypesErrorMessage": "파일 형식을 제거하지 못했습니다.", + "DialogOpenSettingsWindowLabel": "설정 창 열기", + "DialogOpenXCITrimmerWindowLabel": "XCI 트리머 창", + "DialogControllerAppletTitle": "컨트롤러 애플릿", + "DialogMessageDialogErrorExceptionMessage": "메시지 대화 상자 표시 오류 : {0}", + "DialogSoftwareKeyboardErrorExceptionMessage": "소프트웨어 키보드 표시 오류 : {0}", + "DialogErrorAppletErrorExceptionMessage": "ErrorApplet 대화 상자 표시 오류 : {0}", + "DialogUserErrorDialogMessage": "{0}: {1}", + "DialogUserErrorDialogInfoMessage": "\n이 오류를 해결하는 방법에 대한 자세한 내용은 설정 가이드를 참조하세요.", + "DialogUserErrorDialogTitle": "Ryujinx 오류 ({0})", + "DialogAmiiboApiTitle": "Amiibo API", + "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": "\n\n계속하시겠습니까?", + "DialogFirmwareInstallerFirmwareInstallWaitMessage": "펌웨어 설치 중...", + "DialogFirmwareInstallerFirmwareInstallSuccessMessage": "시스템 버전 {0}이(가) 설치되었습니다.", + "DialogUserProfileDeletionWarningMessage": "선택한 프로필을 삭제하면 다른 프로필을 열 수 없음", + "DialogUserProfileDeletionConfirmMessage": "선택한 프로필을 삭제하시겠습니까?", + "DialogUserProfileUnsavedChangesTitle": "경고 - 저장되지 않은 변경 사항", + "DialogUserProfileUnsavedChangesMessage": "저장되지 않은 사용자 프로필의 변경 사항이 있습니다.", + "DialogUserProfileUnsavedChangesSubMessage": "변경 사항을 취소하시겠습니까?", + "DialogControllerSettingsModifiedConfirmMessage": "현재 컨트롤러 설정이 업데이트되었습니다.", + "DialogControllerSettingsModifiedConfirmSubMessage": "저장하시겠습니까?", + "DialogLoadFileErrorMessage": "{0}. 오류 파일 : {1}", + "DialogModAlreadyExistsMessage": "이미 존재하는 모드", + "DialogModInvalidMessage": "지정한 디렉터리에 모드가 없습니다!", + "DialogModDeleteNoParentMessage": "삭제 실패 : \"{0}\" 모드의 상위 디렉터리를 찾을 수 없습니다!", + "DialogDlcNoDlcErrorMessage": "지정된 파일에 선택한 타이틀의 DLC가 포함되어 있지 않습니다!", + "DialogPerformanceCheckLoggingEnabledMessage": "개발자만 사용하도록 설계된 추적 기록이 활성화되어 있습니다.", + "DialogPerformanceCheckLoggingEnabledConfirmMessage": "최적의 성능을 위해서는 추적 기록을 비활성화하는 것이 좋습니다. 지금 추적 기록을 비활성화하시겠습니까?", + "DialogPerformanceCheckShaderDumpEnabledMessage": "개발자만 사용하도록 설계된 셰이더 덤핑이 활성화되어 있습니다.", + "DialogPerformanceCheckShaderDumpEnabledConfirmMessage": "최적의 성능을 위해서는 셰이더 덤핑을 비활성화하는 것이 좋습니다. 지금 셰이더 덤핑을 비활성화하시겠습니까?", + "DialogLoadAppGameAlreadyLoadedMessage": "이미 게임을 불러옴", + "DialogLoadAppGameAlreadyLoadedSubMessage": "다른 게임을 실행하기 전에 에뮬레이션을 중지하거나 에뮬레이터를 닫으세요.", + "DialogUpdateAddUpdateErrorMessage": "지정한 파일에 선택한 타이틀에 대한 업데이트가 포함되어 있지 않습니다!", + "DialogSettingsBackendThreadingWarningTitle": "경고 - 후단부 스레딩", + "DialogSettingsBackendThreadingWarningMessage": "완전히 적용하려면 이 옵션을 변경한 후 Ryujinx를 다시 시작해야 합니다. 플랫폼에 따라 Ryujinx를 사용할 때 드라이버 자체의 다중 스레딩을 수동으로 비활성화해야 할 수도 있습니다.", + "DialogModManagerDeletionWarningMessage": "모드 삭제 : {0}\n\n계속하시겠습니까?", + "DialogModManagerDeletionAllWarningMessage": "이 타이틀에 대한 모드를 모두 삭제하려고 합니다.\n\n계속하시겠습니까?", + "SettingsTabGraphicsFeaturesOptions": "기능", + "SettingsTabGraphicsBackendMultithreading": "그래픽 후단부 다중 스레딩 :", + "CommonAuto": "자동", + "CommonOff": "끔", + "CommonOn": "켬", + "InputDialogYes": "예", + "InputDialogNo": "아니오", + "DialogProfileInvalidProfileNameErrorMessage": "파일 이름에 잘못된 문자가 포함되어 있습니다. 다시 시도하세요.", + "MenuBarOptionsPauseEmulation": "일시 중지", + "MenuBarOptionsResumeEmulation": "다시 시작", + "AboutUrlTooltipMessage": "클릭하면 기본 브라우저에서 Ryujinx 웹사이트가 열립니다.", + "AboutDisclaimerMessage": "Ryujinx는 Nintendo™\n또는 그 파트너와 제휴한 바가 없습니다.", + "AboutAmiiboDisclaimerMessage": "AmiiboAPI(www.amiiboapi.com)는\nAmiibo 에뮬레이션에 사용됩니다.", + "AboutPatreonUrlTooltipMessage": "클릭하면 기본 브라우저에서 Ryujinx Patreon 페이지가 열립니다.", + "AboutGithubUrlTooltipMessage": "클릭하면 기본 브라우저에서 Ryujinx GitHub 페이지가 열립니다.", + "AboutDiscordUrlTooltipMessage": "클릭하면 기본 브라우저에서 Ryujinx 디스코드 서버 초대장이 열립니다.", + "AboutTwitterUrlTooltipMessage": "클릭하면 기본 브라우저에서 Ryujinx 트위터 페이지가 열립니다.", + "AboutRyujinxAboutTitle": "정보 :", + "AboutRyujinxAboutContent": "Ryujinx는 Nintendo Switch™용 에뮬레이터입니다.\nPatreon에서 저희를 후원해 주세요.\nTwitter나 Discord에서 최신 뉴스를 모두 받아보세요.\n기여에 관심이 있는 개발자는 GitHub이나 Discord에서 자세한 내용을 알아볼 수 있습니다.", + "AboutRyujinxMaintainersTitle": "유지 관리 :", + "AboutRyujinxMaintainersContentTooltipMessage": "클릭하면 기본 브라우저에서 기여자 페이지가 열립니다.", + "AboutRyujinxSupprtersTitle": "Patreon에서 후원 :", + "AmiiboSeriesLabel": "Amiibo 시리즈", + "AmiiboCharacterLabel": "캐릭터", + "AmiiboScanButtonLabel": "스캔하기", + "AmiiboOptionsShowAllLabel": "모든 Amiibo 표시", + "AmiiboOptionsUsRandomTagLabel": "핵 : 무작위 태그 Uuid 사용", + "DlcManagerTableHeadingEnabledLabel": "활성화", + "DlcManagerTableHeadingTitleIdLabel": "타이틀 ID", + "DlcManagerTableHeadingContainerPathLabel": "컨테이너 경로", + "DlcManagerTableHeadingFullPathLabel": "전체 경로", + "DlcManagerRemoveAllButton": "모두 제거", + "DlcManagerEnableAllButton": "모두 활성화", + "DlcManagerDisableAllButton": "모두 비활성화", + "ModManagerDeleteAllButton": "모두 삭제", + "MenuBarOptionsChangeLanguage": "언어 변경", + "MenuBarShowFileTypes": "파일 형식 표시", + "CommonSort": "정렬", + "CommonShowNames": "이름 표시", + "CommonFavorite": "즐겨찾기", + "OrderAscending": "오름차순", + "OrderDescending": "내림차순", + "SettingsTabGraphicsFeatures": "기능 및 개선 사항", + "ErrorWindowTitle": "오류 창", + "ToggleDiscordTooltip": "\"현재 진행 중인\" 디스코드 활동에 Ryujinx를 표시할지 여부를 선택", + "AddGameDirBoxTooltip": "목록에 추가할 게임 디렉터리를 입력", + "AddGameDirTooltip": "목록에 게임 디렉터리 추가", + "RemoveGameDirTooltip": "선택한 게임 디렉터리 제거", + "AddAutoloadDirBoxTooltip": "목록에 추가할 자동 불러오기 디렉터리를 입력", + "AddAutoloadDirTooltip": "목록에 자동 불러오기 디렉터리 추가", + "RemoveAutoloadDirTooltip": "선택한 자동 불러오기 디렉터리 제거", + "CustomThemeCheckTooltip": "GUI용 사용자 정의 Avalonia 테마를 사용하여 에뮬레이터 메뉴의 모양 변경", + "CustomThemePathTooltip": "사용자 정의 GUI 테마 경로", + "CustomThemeBrowseTooltip": "사용자 정의 GUI 테마 찾아보기", + "DockModeToggleTooltip": "도킹 모드를 사용하면 에뮬레이트된 시스템이 도킹된 Nintendo Switch처럼 동작합니다. 이 경우, 대부분의 게임에서 그래픽 충실도를 향상시킵니다. 반대로 이 기능을 비활성화하면 에뮬레이트된 시스템이 휴대용 Nintendo Switch처럼 작동하여 그래픽 품질이 저하됩니다.\n\n도킹 모드를 사용할 계획이라면 플레이어 1 컨트롤을 구성하세요. 휴대용 모드를 사용하려는 경우 휴대용 컨트롤을 구성하십시오.\n\n모르면 켬으로 두세요.", + "DirectKeyboardTooltip": "키보드 직접 접속(HID)을 지원합니다. 텍스트 입력 장치로 키보드에 대한 게임 접속을 제공합니다.\n\nSwitch 하드웨어에서 키보드 사용을 기본적으로 지원하는 게임에서만 작동합니다.\n\n모르면 끔으로 두세요.", + "DirectMouseTooltip": "마우스 직접 접속(HID)을 지원합니다. 마우스에 대한 게임 접속을 포인팅 장치로 제공합니다.\n\nSwitch 하드웨어에서 마우스 컨트롤을 기본적으로 지원하는 게임에서만 작동하며 거의 없습니다.\n\n활성화하면 터치 스크린 기능이 작동하지 않을 수 있습니다.\n\n모르면 끔으로 두세요.", + "RegionTooltip": "시스템 지역 변경", + "LanguageTooltip": "시스템 언어 변경", + "TimezoneTooltip": "시스템 시간대 변경", + "TimeTooltip": "시스템 시간 변경", + "VSyncToggleTooltip": "에뮬레이트된 콘솔의 수직 동기화입니다. 기본적으로 대부분의 게임에서 프레임 제한 기능으로, 비활성화하면 게임이 더 빠른 속도로 실행되거나 로딩 화면이 더 오래 걸리거나 멈출 수 있습니다.\n\n게임 내에서 원하는 단축키(기본값은 F1)로 전환할 수 있습니다. 비활성화하려면 이 작업을 수행하는 것이 좋습니다.\n\n모르면 켬으로 두세요.", + "PptcToggleTooltip": "번역된 JIT 함수를 저장하여 게임을 불러올 때마다 번역할 필요가 없도록 합니다.\n\n게임을 처음 부팅한 후 끊김 현상을 줄이고 부팅 시간을 크게 단축합니다.\n\n모르면 켬으로 두세요.", + "LowPowerPptcToggleTooltip": "코어의 3분의 1을 사용하여 PPTC를 불러옵니다.", + "FsIntegrityToggleTooltip": "게임을 부팅할 때 손상된 파일을 확인하고, 손상된 파일이 감지되면 로그에 해시 오류를 표시합니다.\n\n성능에 영향을 미치지 않으며 문제 해결에 도움이 됩니다.\n\n모르면 켬으로 두세요.", + "AudioBackendTooltip": "오디오 렌더링에 사용되는 백엔드를 변경합니다.\n\nSDL2가 선호되는 반면 OpenAL 및 SoundIO는 대체 수단으로 사용됩니다. 더미에는 소리가 나지 않습니다.\n\n모르면 SDL2로 설정하세요.", + "MemoryManagerTooltip": "게스트 메모리 매핑 및 접속 방법을 변경합니다. 에뮬레이트된 CPU 성능에 큰 영향을 미칩니다.\n\n모르면 호스트 확인 안 함으로 설정합니다.", + "MemoryManagerSoftwareTooltip": "주소 번역에 소프트웨어 페이지 테이블을 사용합니다. 정확도는 가장 높지만 가장 느립니다.", + "MemoryManagerHostTooltip": "호스트 주소 공간에 메모리를 직접 매핑합니다. JIT 컴파일 및 실행 속도가 훨씬 빨라집니다.", + "MemoryManagerUnsafeTooltip": "메모리를 직접 매핑하되 접속하기 전에 게스트 주소 공간 내의 주소를 마스킹하지 않습니다. 더 빠르지만 안전성이 희생됩니다. 게스트 애플리케이션은 Ryujinx의 어느 곳에서나 메모리에 접속할 수 있으므로 이 모드에서는 신뢰할 수 있는 프로그램만 실행하세요.", + "UseHypervisorTooltip": "JIT 대신 Hypervisor를 사용하세요. 사용 가능한 경우 성능이 크게 향상되지만 현재 상태에서는 불안정할 수 있습니다.", + "DRamTooltip": "Switch 개발 모델을 모방하기 위해 8GB DRAM이 포함된 대체 메모리 모드를 활용합니다.\n\n이는 고해상도 텍스처 팩 또는 4K 해상도 모드에만 유용합니다. 성능을 개선하지 않습니다.\n\n모르면 끔으로 두세요.", + "IgnoreMissingServicesTooltip": "구현되지 않은 Horizon OS 서비스는 무시됩니다. 특정 게임을 부팅할 때, 발생하는 충돌을 우회하는 데 도움이 될 수 있습니다.\n\n모르면 끔으로 두세요.", + "IgnoreAppletTooltip": "게임 플레이 중에 게임패드 연결이 끊어지면 외부 대화 상자 \"컨트롤러 애플릿\"이 나타나지 않습니다. 대화 상자를 닫거나 새 컨트롤러를 설정하라는 메시지가 표시되지 않습니다. 이전에 연결이 끊어진 컨트롤러가 다시 연결되면 게임이 자동으로 다시 시작됩니다.", + "GraphicsBackendThreadingTooltip": "2번째 스레드에서 그래픽 후단부 명령을 실행합니다.\n\n셰이더 컴파일 속도를 높이고, 끊김 현상을 줄이며, 자체 다중 스레딩 지원 없이 GPU 드라이버의 성능을 향상시킵니다. 다중 스레딩이 있는 드라이버에서 성능이 좀 더 좋습니다.\n\n모르면 자동으로 설정합니다.", + "GalThreadingTooltip": "2번째 스레드에서 그래픽 후단부 명령을 실행합니다.\n\n셰이더 컴파일 속도를 높이고 끊김 현상을 줄이며 자체 다중 스레딩 지원 없이 GPU 드라이버의 성능을 향상시킵니다. 다중 스레딩이 있는 드라이버에서 성능이 좀 더 좋습니다.\n\n모르면 자동으로 설정합니다.", + "ShaderCacheToggleTooltip": "후속 실행 시 끊김 현상을 줄이는 디스크 셰이더 캐시를 저장합니다.\n\n모르면 켬으로 두세요.", + "ResolutionScaleTooltip": "게임의 렌더링 해상도를 배가시킵니다.\n\n일부 게임에서는 이 기능이 작동하지 않고 해상도가 높아져도 픽셀화되어 보일 수 있습니다. 해당 게임의 경우 앤티 앨리어싱을 제거하거나 내부 렌더링 해상도를 높이는 모드를 찾아야 할 수 있습니다. 후자를 사용하려면 기본을 선택하는 것이 좋습니다.\n\n이 옵션은 아래의 \"적용\"을 클릭하여 게임이 실행되는 동안 변경할 수 있습니다. 설정 창을 옆으로 옮기고 원하는 게임 모양을 찾을 때까지 실험해 보세요.\n\n4배는 거의 모든 설정에서 과하다는 점을 명심하세요.", + "ResolutionScaleEntryTooltip": "부동 소수점 해상도 스케일(예: 1.5)입니다. 적분이 아닌 스케일은 문제나 충돌을 일으킬 가능성이 높습니다.", + "AnisotropyTooltip": "이방성 필터링 수준입니다. 게임에서 요청한 값을 사용하려면 자동으로 설정하세요.", + "AspectRatioTooltip": "렌더러 창에 적용되는 종횡비입니다.\n\n게임에 종횡비 모드를 사용하는 경우에만 이 설정을 변경하세요. 그렇지 않으면 그래픽이 늘어납니다.\n\n모르면 16:9로 두세요.", + "ShaderDumpPathTooltip": "그래픽 셰이더 덤프 경로", + "FileLogTooltip": "디스크의 로그 파일에 콘솔 기록을 저장합니다. 성능에 영향을 주지 않습니다.", + "StubLogTooltip": "콘솔에 조각 기록 메시지를 출력합니다. 성능에 영향을 주지 않습니다.", + "InfoLogTooltip": "콘솔에 정보 기록 메시지를 출력합니다. 성능에 영향을 주지 않습니다.", + "WarnLogTooltip": "콘솔에 경고 기록 메시지를 출력합니다. 성능에 영향을 주지 않습니다.", + "ErrorLogTooltip": "콘솔에 오류 기록 메시지를 출력합니다. 성능에 영향을 주지 않습니다.", + "TraceLogTooltip": "콘솔에 추적 기록 메시지를 출력합니다. 성능에 영향을 주지 않습니다.", + "GuestLogTooltip": "콘솔에 게스트 로그 메시지를 출력합니다. 성능에 영향을 주지 않습니다.", + "FileAccessLogTooltip": "콘솔에 파일 접속 기록 메시지를 출력합니다.", + "FSAccessLogModeTooltip": "콘솔에 파일 시스템 접속 기록 출력을 활성화합니다. 가능한 모드는 0-3", + "DeveloperOptionTooltip": "주의해서 사용", + "OpenGlLogLevel": "적절한 기록 수준이 활성화되어 있어야 함", + "DebugLogTooltip": "콘솔에 디버그 기록 메시지를 출력합니다.\n\n담당자가 특별히 요청한 경우에만 이 기능을 사용하십시오. 로그를 읽기 어렵게 만들고 에뮬레이터 성능을 저하시킬 수 있기 때문입니다.", + "LoadApplicationFileTooltip": "파일 탐색기를 열어 불러올 Switch 호환 파일을 선택", + "LoadApplicationFolderTooltip": "Switch와 호환되는 압축 해제된 앱을 선택하여 불러오려면 파일 탐색기를 엽니다.", + "LoadDlcFromFolderTooltip": "파일 탐색기를 열어 DLC를 일괄 불러오기할 폴더를 하나 이상 선택", + "LoadTitleUpdatesFromFolderTooltip": "파일 탐색기를 열어 하나 이상의 폴더를 선택하여 대량으로 타이틀 업데이트 불러오기", + "OpenRyujinxFolderTooltip": "Ryujinx 파일 시스템 폴더 열기", + "OpenRyujinxLogsTooltip": "로그가 기록되는 폴더 열기", + "ExitTooltip": "Ryujinx 종료", + "OpenSettingsTooltip": "설정 창 열기", + "OpenProfileManagerTooltip": "사용자 프로필 관리자 창 열기", + "StopEmulationTooltip": "현재 게임의 에뮬레이션을 중지하고 게임 선택으로 돌아가기", + "CheckUpdatesTooltip": "Ryujinx 업데이트 확인", + "OpenAboutTooltip": "정보 창 열기", + "GridSize": "그리드 크기", + "GridSizeTooltip": "그리드 항목의 크기 변경", + "SettingsTabSystemSystemLanguageBrazilianPortuguese": "브라질 포르투갈어", + "AboutRyujinxContributorsButtonHeader": "모든 기여자 보기", + "SettingsTabSystemAudioVolume": "음량 : ", + "AudioVolumeTooltip": "음량 변경", + "SettingsTabSystemEnableInternetAccess": "게스트 인터넷 접속/LAN 모드", + "EnableInternetAccessTooltip": "에뮬레이트된 앱을 인터넷에 연결할 수 있습니다.\n\nLAN 모드가 있는 게임은 이 기능이 활성화되고 시스템이 동일한 접속 포인트에 연결되어 있을 때 서로 연결할 수 있습니다. 이는 실제 콘솔도 포함됩니다.\n\nNintendo 서버 연결을 허용하지 않습니다. 인터넷에 연결을 시도하는 특정 게임에서 충돌이 발생할 수 있습니다.\n\n모르면 끔으로 두세요.", + "GameListContextMenuManageCheatToolTip": "치트 관리", + "GameListContextMenuManageCheat": "치트 관리", + "GameListContextMenuManageModToolTip": "모드 관리", + "GameListContextMenuManageMod": "모드 관리", + "ControllerSettingsStickRange": "범위 :", + "DialogStopEmulationTitle": "Ryujinx - 에뮬레이션 중지", + "DialogStopEmulationMessage": "에뮬레이션을 중지하시겠습니까?", + "SettingsTabCpu": "CPU", + "SettingsTabAudio": "음향", + "SettingsTabNetwork": "네트워크", + "SettingsTabNetworkConnection": "네트워크 연결", + "SettingsTabCpuCache": "CPU 캐시", + "SettingsTabCpuMemory": "CPU 모드", + "DialogUpdaterFlatpakNotSupportedMessage": "FlatHub를 통해 Ryujinx를 업데이트하세요.", + "UpdaterDisabledWarningTitle": "업데이터가 비활성화되었습니다!", + "ControllerSettingsRotate90": "시계 방향으로 90° 회전", + "IconSize": "아이콘 크기", + "IconSizeTooltip": "게임 아이콘 크기 변경", + "MenuBarOptionsShowConsole": "콘솔 표시", + "ShaderCachePurgeError": "{0}에서 셰이더 캐시를 삭제하는 중 오류 발생 : {1}", + "UserErrorNoKeys": "키를 찾을 수 없음", + "UserErrorNoFirmware": "펌웨어를 찾을 수 없음", + "UserErrorFirmwareParsingFailed": "펌웨어 구문 분석 오류", + "UserErrorApplicationNotFound": "앱을 찾을 수 없음", + "UserErrorUnknown": "알 수 없는 오류", + "UserErrorUndefined": "정의되지 않은 오류", + "UserErrorNoKeysDescription": "Ryujinx가 'prod.keys' 파일을 찾지 못함", + "UserErrorNoFirmwareDescription": "설치된 펌웨어를 찾을 수 없음", + "UserErrorFirmwareParsingFailedDescription": "Ryujinx가 제공된 펌웨어를 구문 분석하지 못했습니다. 이는 일반적으로 오래된 키로 인해 발생합니다.", + "UserErrorApplicationNotFoundDescription": "Ryujinx가 해당 경로에서 유효한 앱을 찾을 수 없습니다.", + "UserErrorUnknownDescription": "알 수 없는 오류가 발생했습니다!", + "UserErrorUndefinedDescription": "정의되지 않은 오류가 발생했습니다! 이런 일이 발생하면 안 되니 개발자에게 문의하세요!", + "OpenSetupGuideMessage": "설정 가이드 열기", + "NoUpdate": "업데이트 없음", + "TitleUpdateVersionLabel": "버전 {0}", + "TitleBundledUpdateVersionLabel": "번들 : 버전 {0}", + "TitleBundledDlcLabel": "번들 :", + "TitleXCIStatusPartialLabel": "일부", + "TitleXCIStatusTrimmableLabel": "트리밍되지 않음", + "TitleXCIStatusUntrimmableLabel": "트리밍됨", + "TitleXCIStatusFailedLabel": "(실패)", + "TitleXCICanSaveLabel": "{0:n0} Mb 저장", + "TitleXCISavingLabel": "{0:n0}Mb 저장됨", + "RyujinxInfo": "Ryujinx - 정보", + "RyujinxConfirm": "Ryujinx - 확인", + "FileDialogAllTypes": "모든 형식", + "Never": "절대 안 함", + "SwkbdMinCharacters": "{0}자 이상이어야 함", + "SwkbdMinRangeCharacters": "{0}-{1}자 길이여야 함", + "SoftwareKeyboard": "소프트웨어 키보드", + "SoftwareKeyboardModeNumeric": "0-9 또는 '.'만 가능", + "SoftwareKeyboardModeAlphabet": "CJK 문자가 아닌 문자만 가능", + "SoftwareKeyboardModeASCII": "ASCII 텍스트만 가능", + "ControllerAppletControllers": "지원되는 컨트롤러 :", + "ControllerAppletPlayers": "플레이어 :", + "ControllerAppletDescription": "현재 구성이 유효하지 않습니다. 설정을 열고 입력을 다시 구성하십시오.", + "ControllerAppletDocked": "도킹 모드가 설정되었습니다. 휴대용 제어 기능을 비활성화해야 합니다.", + "UpdaterRenaming": "오래된 파일 이름 바꾸기...", + "UpdaterRenameFailed": "업데이터가 파일 이름을 바꿀 수 없음 : {0}", + "UpdaterAddingFiles": "새 파일 추가...", + "UpdaterExtracting": "업데이트 추출...", + "UpdaterDownloading": "업데이트 내려받기 중...", + "Game": "게임", + "Docked": "도킹", + "Handheld": "휴대", + "ConnectionError": "연결 오류가 발생했습니다.", + "AboutPageDeveloperListMore": "{0} 외...", + "ApiError": "API 오류.", + "LoadingHeading": "{0} 불러오는 중", + "CompilingPPTC": "PTC 컴파일", + "CompilingShaders": "셰이더 컴파일", + "AllKeyboards": "모든 키보드", + "OpenFileDialogTitle": "지원되는 파일을 선택하여 열기", + "OpenFolderDialogTitle": "압축 해제된 게임이 있는 폴더를 선택", + "AllSupportedFormats": "지원되는 모든 형식", + "RyujinxUpdater": "Ryujinx 업데이터", + "SettingsTabHotkeys": "키보드 단축키", + "SettingsTabHotkeysHotkeys": "키보드 단축키", + "SettingsTabHotkeysToggleVsyncHotkey": "수직 동기화 전환 :", + "SettingsTabHotkeysScreenshotHotkey": "스크린샷 :", + "SettingsTabHotkeysShowUiHotkey": "UI 표시 :", + "SettingsTabHotkeysPauseHotkey": "중지 :", + "SettingsTabHotkeysToggleMuteHotkey": "음소거 :", + "ControllerMotionTitle": "모션 컨트롤 설정", + "ControllerRumbleTitle": "진동 설정", + "SettingsSelectThemeFileDialogTitle": "테마 파일 선택", + "SettingsXamlThemeFile": "Xaml 테마 파일", + "AvatarWindowTitle": "계정 관리 - 아바타", + "Amiibo": "Amiibo", + "Unknown": "알 수 없음", + "Usage": "사용법", + "Writable": "쓰기 가능", + "SelectDlcDialogTitle": "DLC 파일 선택", + "SelectUpdateDialogTitle": "업데이트 파일 선택", + "SelectModDialogTitle": "모드 디렉터리 선택", + "TrimXCIFileDialogTitle": "XCI 파일 확인 및 정리", + "TrimXCIFileDialogPrimaryText": "이 기능은 먼저 충분한 공간을 확보한 다음 XCI 파일을 트리밍하여 디스크 공간을 절약합니다.", + "TrimXCIFileDialogSecondaryText": "현재 파일 크기 : {0:n}MB\n게임 데이터 크기 : {1:n}MB\n디스크 공간 절약 : {2:n}MB", + "TrimXCIFileNoTrimNecessary": "XCI 파일은 트리밍할 필요가 없습니다. 자세한 내용은 로그를 확인", + "TrimXCIFileNoUntrimPossible": "XCI 파일은 트리밍을 해제할 수 없습니다. 자세한 내용은 로그를 확인", + "TrimXCIFileReadOnlyFileCannotFix": "XCI 파일은 읽기 전용이므로 쓰기 가능하게 만들 수 없습니다. 자세한 내용은 로그를 확인", + "TrimXCIFileFileSizeChanged": "XCI 파일이 스캔된 후 크기가 변경되었습니다. 파일이 쓰여지고 있지 않은지 확인하고 다시 시도하세요.", + "TrimXCIFileFreeSpaceCheckFailed": "XCI 파일에 여유 공간 영역에 데이터가 있으므로 트리밍하는 것이 안전하지 않음", + "TrimXCIFileInvalidXCIFile": "XCI 파일에 유효하지 않은 데이터가 포함되어 있습니다. 자세한 내용은 로그를 확인", + "TrimXCIFileFileIOWriteError": "XCI 파일을 쓰기 위해 열 수 없습니다. 자세한 내용은 로그를 확인", + "TrimXCIFileFailedPrimaryText": "XCI 파일 트리밍에 실패", + "TrimXCIFileCancelled": "작업이 취소됨", + "TrimXCIFileFileUndertermined": "작업이 수행되지 않음", + "UserProfileWindowTitle": "사용자 프로필 관리자", + "CheatWindowTitle": "치트 관리자", + "DlcWindowTitle": "{0} ({1})의 내려받기 가능한 콘텐츠 관리", + "ModWindowTitle": "{0}({1})의 모드 관리", + "UpdateWindowTitle": "타이틀 업데이트 관리자", + "XCITrimmerWindowTitle": "XCI 파일 트리머", + "XCITrimmerTitleStatusCount": "{1}개 타이틀 중 {0}개 선택됨", + "XCITrimmerTitleStatusCountWithFilter": "{1}개 타이틀 중 {0}개 선택됨({2}개 표시됨)", + "XCITrimmerTitleStatusTrimming": "{0}개의 타이틀을 트리밍 중...", + "XCITrimmerTitleStatusUntrimming": "{0}개의 타이틀을 트리밍 해제 중...", + "XCITrimmerTitleStatusFailed": "실패", + "XCITrimmerPotentialSavings": "잠재적 비용 절감", + "XCITrimmerActualSavings": "실제 비용 절감", + "XCITrimmerSavingsMb": "{0:n0} Mb", + "XCITrimmerSelectDisplayed": "표시됨 선택", + "XCITrimmerDeselectDisplayed": "표시됨 선택 취소", + "XCITrimmerSortName": "타이틀", + "XCITrimmerSortSaved": "공간 절약s", + "XCITrimmerTrim": "Trim", + "XCITrimmerUntrim": "Untrim", + "UpdateWindowUpdateAddedMessage": "{0}개의 새 업데이트가 추가됨", + "UpdateWindowBundledContentNotice": "번들 업데이트는 제거할 수 없으며, 비활성화만 가능합니다.", + "CheatWindowHeading": "{0} [{1}]에 사용 가능한 치트", + "BuildId": "빌드ID:", + "DlcWindowBundledContentNotice": "번들 DLC는 제거할 수 없으며 비활성화만 가능합니다.", + "DlcWindowHeading": "{1} ({2})에 내려받기 가능한 콘텐츠 {0}개 사용 가능", + "DlcWindowDlcAddedMessage": "{0}개의 새로운 내려받기 가능한 콘텐츠가 추가됨", + "AutoloadDlcAddedMessage": "{0}개의 새로운 내려받기 가능한 콘텐츠가 추가됨", + "AutoloadDlcRemovedMessage": "{0}개의 내려받기 가능한 콘텐츠가 제거됨", + "AutoloadUpdateAddedMessage": "{0}개의 새 업데이트가 추가됨", + "AutoloadUpdateRemovedMessage": "누락된 업데이트 {0}개 삭제", + "ModWindowHeading": "{0} 모드", + "UserProfilesEditProfile": "선택 항목 편집", + "Continue": "계속", + "Cancel": "취소", + "Save": "저장", + "Discard": "폐기", + "Paused": "일시 중지됨", + "UserProfilesSetProfileImage": "프로필 이미지 설정", + "UserProfileEmptyNameError": "이름 필수 입력", + "UserProfileNoImageError": "프로필 이미지를 설정해야 함", + "GameUpdateWindowHeading": "{0} ({1})에 대한 업데이트 관리", + "SettingsTabHotkeysResScaleUpHotkey": "해상도 증가 :", + "SettingsTabHotkeysResScaleDownHotkey": "해상도 감소 :", + "UserProfilesName": "이름 :", + "UserProfilesUserId": "사용자 ID :", + "SettingsTabGraphicsBackend": "그래픽 후단부", + "SettingsTabGraphicsBackendTooltip": "에뮬레이터에서 사용할 그래픽 후단부를 선택합니다.\n\nVulkan은 드라이버가 최신 상태인 한 모든 최신 그래픽 카드에 전반적으로 더 좋습니다. Vulkan은 또한 모든 GPU 공급업체에서 더 빠른 셰이더 컴파일(덜 끊김)을 제공합니다.\n\nOpenGL은 오래된 Nvidia GPU, Linux의 오래된 AMD GPU 또는 VRAM이 낮은 GPU에서 더 나은 결과를 얻을 수 있지만 셰이더 컴파일 끊김이 더 큽니다.\n\n모르면 Vulkan으로 설정합니다. 최신 그래픽 드라이버를 사용해도 GPU가 Vulkan을 지원하지 않는 경우 OpenGL로 설정하세요..", + "SettingsEnableTextureRecompression": "텍스처 재압축 활성화", + "SettingsEnableTextureRecompressionTooltip": "VRAM 사용량을 줄이기 위해 ASTC 텍스처를 압축합니다.\n\n이 텍스처 형식을 사용하는 게임에는 Astral Chain, Bayonetta 3, Fire Emblem Engage, Metroid Prime Remastered, Super Mario Bros. Wonder, The Legend of Zelda: Tears of the Kingdom이 있습니다.\n\n4GiB VRAM 이하의 그래픽 카드는 이러한 게임을 실행하는 동안 어느 시점에서 충돌할 가능성이 있습니다.\n\n위에서 언급한 게임에서 VRAM이 부족한 경우에만 활성화합니다. 모르면 끔으로 두세요.", + "SettingsTabGraphicsPreferredGpu": "기본 GPU", + "SettingsTabGraphicsPreferredGpuTooltip": "Vulkan 그래픽 후단부와 함께 사용할 그래픽 카드를 선택하세요.\n\nOpenGL에서 사용할 GPU에는 영향을 미치지 않습니다.\n\n모르면 \"dGPU\"로 플래그가 지정된 GPU로 설정하세요. 없으면 그대로 두세요.", + "SettingsAppRequiredRestartMessage": "Ryujinx 다시 시작 필요", + "SettingsGpuBackendRestartMessage": "그래픽 후단부 또는 GPU 설정이 수정되었습니다. 이를 적용하려면 다시 시작이 필요", + "SettingsGpuBackendRestartSubMessage": "지금 다시 시작하시겠습니까?", + "RyujinxUpdaterMessage": "Ryujinx를 최신 버전으로 업데이트하시겠습니까?", + "SettingsTabHotkeysVolumeUpHotkey": "음량 증가 :", + "SettingsTabHotkeysVolumeDownHotkey": "음량 감소 :", + "SettingsEnableMacroHLE": "매크로 HLE 활성화", + "SettingsEnableMacroHLETooltip": "GPU 매크로 코드의 고수준 에뮬레이션입니다.\n\n성능은 향상되지만 일부 게임에서 그래픽 오류가 발생할 수 있습니다.\n\n모르면 켬으로 두세요.", + "SettingsEnableColorSpacePassthrough": "색 공간 통과", + "SettingsEnableColorSpacePassthroughTooltip": "Vulkan 후단부가 색 공간을 지정하지 않고 색상 정보를 전달하도록 지시합니다. 넓은 색역 화면 표시 장치를 사용하는 사용자의 경우 색상 정확성을 희생하고 더 생생한 색상이 나올 수 있습니다.", + "VolumeShort": "음량", + "UserProfilesManageSaves": "저장 관리", + "DeleteUserSave": "이 게임의 사용자 저장을 삭제하시겠습니까?", + "IrreversibleActionNote": "이 작업은 되돌릴 수 없습니다.", + "SaveManagerHeading": "{0} ({1})에 대한 저장 관리", + "SaveManagerTitle": "관리자 저장", + "Name": "이름", + "Size": "크기", + "Search": "찾기", + "UserProfilesRecoverLostAccounts": "잃어버린 계정 복구", + "Recover": "복구", + "UserProfilesRecoverHeading": "다음 계정에 대한 저장 발견", + "UserProfilesRecoverEmptyList": "복구할 프로필 없음", + "GraphicsAATooltip": "게임 렌더에 앤티 앨리어싱을 적용합니다.\n\nFXAA는 이미지 대부분을 흐리게 처리하지만 SMAA는 들쭉날쭉한 가장자리를 찾아 부드럽게 처리합니다.\n\nFSR 스케일링 필터와 함께 사용하지 않는 것이 좋습니다.\n\n이 옵션은 아래의 \"적용\"을 클릭하여 게임을 실행하는 동안 변경할 수 있습니다. 설정 창을 옆으로 옮겨 원하는 게임의 모습을 찾을 때까지 실험해 볼 수 있습니다.\n\n모르면 없음으로 두세요.", + "GraphicsAALabel": "앤티 앨리어싱 :", + "GraphicsScalingFilterLabel": "크기 조정 필터 :", + "GraphicsScalingFilterTooltip": "해상도 스케일을 사용할 때 적용될 스케일링 필터를 선택합니다.\n\n쌍선형은 3D 게임에 적합하며 안전한 기본 옵션입니다.\n\nNearest는 픽셀 아트 게임에 권장됩니다.\n\nFSR 1.0은 단순히 선명도 필터일 뿐이며 FXAA 또는 SMAA와 함께 사용하는 것은 권장되지 않습니다.\n\nArea 스케일링은 출력 창보다 큰 해상도를 다운스케일링할 때 권장됩니다. 2배 이상 다운스케일링할 때 슈퍼샘플링된 앤티앨리어싱 효과를 얻는 데 사용할 수 있습니다.\n\n이 옵션은 아래의 \"적용\"을 클릭하여 게임을 실행하는 동안 변경할 수 있습니다. 설정 창을 옆으로 옮겨 원하는 게임 모양을 찾을 때까지 실험하면 됩니다.\n\n모르면 쌍선형을 그대로 두세요.", + "GraphicsScalingFilterBilinear": "쌍선형", + "GraphicsScalingFilterNearest": "근린", + "GraphicsScalingFilterFsr": "FSR", + "GraphicsScalingFilterArea": "영역", + "GraphicsScalingFilterLevelLabel": "레벨", + "GraphicsScalingFilterLevelTooltip": "FSR 1.0 선명도 레벨을 설정합니다. 높을수록 더 선명합니다.", + "SmaaLow": "SMAA 낮음", + "SmaaMedium": "SMAA 중간", + "SmaaHigh": "SMAA 높음", + "SmaaUltra": "SMAA 울트라", + "UserEditorTitle": "사용자 편집", + "UserEditorTitleCreate": "사용자 만들기", + "SettingsTabNetworkInterface": "네트워크 인터페이스:", + "NetworkInterfaceTooltip": "LAN/LDN 기능에 사용되는 네트워크 인터페이스입니다.\n\nVPN이나 ​​XLink Kai와 LAN 지원 게임과 함께 사용하면 인터넷을 통한 동일 네트워크 연결을 스푸핑하는 데 사용할 수 있습니다.\n\n모르면 기본값으로 두세요.", + "NetworkInterfaceDefault": "기본값", + "PackagingShaders": "패키징 셰이더", + "AboutChangelogButton": "GitHub에서 변경 내역 보기", + "AboutChangelogButtonTooltipMessage": "기본 브라우저에서 이 버전의 변경 내역을 열람하려면 클릭하세요.", + "SettingsTabNetworkMultiplayer": "멀티플레이어", + "MultiplayerMode": "모드 :", + "MultiplayerModeTooltip": "LDN 멀티플레이어 모드를 변경합니다.\n\nLdnMitm은 게임의 로컬 무선/로컬 플레이 기능을 LAN처럼 작동하도록 수정하여 다른 Ryujinx 인스턴스나 ldn_mitm 모듈이 설치된 해킹된 Nintendo Switch 콘솔과 로컬, 동일 네트워크 연결이 가능합니다.\n\n멀티플레이어는 모든 플레이어가 동일한 게임 버전을 사용해야 합니다(예: Super Smash Bros. Ultimate v13.0.1은 v13.0.0에 연결할 수 없음).\n\n모르면 비활성화 상태로 두세요.", + "MultiplayerModeDisabled": "비활성화됨", + "MultiplayerModeLdnMitm": "ldn_mitm", + "MultiplayerModeLdnRyu": "RyuLDN", + "MultiplayerDisableP2P": "P2P 네트워크 호스팅 비활성화(대기 시간이 늘어날 수 있음)", + "MultiplayerDisableP2PTooltip": "P2P 네트워크 호스팅을 비활성화하면 피어가 직접 연결하지 않고 마스터 서버를 통해 프록시합니다.", + "LdnPassphrase": "네트워크 암호 문구 :", + "LdnPassphraseTooltip": "귀하는 귀하와 동일한 암호를 사용하는 호스팅 게임만 볼 수 있습니다.", + "LdnPassphraseInputTooltip": "Ryujinx-<8 hex chars> 형식으로 암호를 입력하세요. 귀하는 귀하와 동일한 암호를 사용하는 호스팅 게임만 볼 수 있습니다.", + "LdnPassphraseInputPublic": "(일반)", + "GenLdnPass": "무작위 생성", + "GenLdnPassTooltip": "다른 플레이어와 공유할 수 있는 새로운 암호 문구를 생성합니다.", + "ClearLdnPass": "지우기", + "ClearLdnPassTooltip": "현재 암호를 지우고 공용 네트워크로 돌아갑니다.", + "InvalidLdnPassphrase": "유효하지 않은 암호입니다! \"Ryujinx-<8 hex chars>\" 형식이어야 합니다." +} diff --git a/src/Ryujinx/Assets/Locales/pl_PL.json b/src/Ryujinx/Assets/Locales/pl_PL.json new file mode 100644 index 000000000..1d8cf4f03 --- /dev/null +++ b/src/Ryujinx/Assets/Locales/pl_PL.json @@ -0,0 +1,868 @@ +{ + "Language": "Polski", + "MenuBarFileOpenApplet": "Otwórz Aplet", + "MenuBarFileOpenAppletOpenMiiApplet": "Mii Edit Applet", + "MenuBarFileOpenAppletOpenMiiAppletToolTip": "Otwórz aplet Mii Editor w trybie indywidualnym", + "SettingsTabInputDirectMouseAccess": "Bezpośredni dostęp do myszy", + "SettingsTabSystemMemoryManagerMode": "Tryb menedżera pamięci:", + "SettingsTabSystemMemoryManagerModeSoftware": "Oprogramowanie", + "SettingsTabSystemMemoryManagerModeHost": "Gospodarz (szybki)", + "SettingsTabSystemMemoryManagerModeHostUnchecked": "Gospodarza (NIESPRAWDZONY, najszybszy, niebezpieczne)", + "SettingsTabSystemUseHypervisor": "Użyj Hipernadzorcy", + "MenuBarFile": "_Plik", + "MenuBarFileOpenFromFile": "_Załaduj aplikację z pliku", + "MenuBarFileOpenFromFileError": "No applications found in selected file.", + "MenuBarFileOpenUnpacked": "Załaduj _rozpakowaną grę", + "MenuBarFileLoadDlcFromFolder": "Load DLC From Folder", + "MenuBarFileLoadTitleUpdatesFromFolder": "Load Title Updates From Folder", + "MenuBarFileOpenEmuFolder": "Otwórz folder Ryujinx", + "MenuBarFileOpenLogsFolder": "Otwórz folder plików dziennika zdarzeń", + "MenuBarFileExit": "_Wyjdź", + "MenuBarOptions": "_Opcje", + "MenuBarOptionsToggleFullscreen": "Przełącz na tryb pełnoekranowy", + "MenuBarOptionsStartGamesInFullscreen": "Uruchamiaj gry w trybie pełnoekranowym", + "MenuBarOptionsStopEmulation": "Zatrzymaj emulację", + "MenuBarOptionsSettings": "_Ustawienia", + "MenuBarOptionsManageUserProfiles": "_Zarządzaj profilami użytkowników", + "MenuBarActions": "_Akcje", + "MenuBarOptionsSimulateWakeUpMessage": "Symuluj wiadomość wybudzania", + "MenuBarActionsScanAmiibo": "Skanuj Amiibo", + "MenuBarTools": "_Narzędzia", + "MenuBarToolsInstallFirmware": "Zainstaluj oprogramowanie", + "MenuBarFileToolsInstallFirmwareFromFile": "Zainstaluj oprogramowanie z XCI lub ZIP", + "MenuBarFileToolsInstallFirmwareFromDirectory": "Zainstaluj oprogramowanie z katalogu", + "MenuBarToolsManageFileTypes": "Zarządzaj rodzajami plików", + "MenuBarToolsInstallFileTypes": "Typy plików instalacyjnych", + "MenuBarToolsUninstallFileTypes": "Typy plików dezinstalacyjnych", + "MenuBarToolsXCITrimmer": "Trim XCI Files", + "MenuBarView": "_View", + "MenuBarViewWindow": "Window Size", + "MenuBarViewWindow720": "720p", + "MenuBarViewWindow1080": "1080p", + "MenuBarHelp": "_Pomoc", + "MenuBarHelpCheckForUpdates": "Sprawdź aktualizacje", + "MenuBarHelpAbout": "O programie", + "MenuSearch": "Wyszukaj...", + "GameListHeaderFavorite": "Ulubione", + "GameListHeaderIcon": "Ikona", + "GameListHeaderApplication": "Nazwa", + "GameListHeaderDeveloper": "Twórca", + "GameListHeaderVersion": "Wersja", + "GameListHeaderTimePlayed": "Czas w grze:", + "GameListHeaderLastPlayed": "Ostatnio grane", + "GameListHeaderFileExtension": "Rozszerzenie pliku", + "GameListHeaderFileSize": "Rozmiar pliku", + "GameListHeaderPath": "Ścieżka", + "GameListContextMenuOpenUserSaveDirectory": "Otwórz katalog zapisów użytkownika", + "GameListContextMenuOpenUserSaveDirectoryToolTip": "Otwiera katalog, który zawiera zapis użytkownika dla tej aplikacji", + "GameListContextMenuOpenDeviceSaveDirectory": "Otwórz katalog zapisów urządzenia", + "GameListContextMenuOpenDeviceSaveDirectoryToolTip": "Otwiera katalog, który zawiera zapis urządzenia dla tej aplikacji", + "GameListContextMenuOpenBcatSaveDirectory": "Otwórz katalog zapisu BCAT obecnego użytkownika", + "GameListContextMenuOpenBcatSaveDirectoryToolTip": "Otwiera katalog, który zawiera zapis BCAT dla tej aplikacji", + "GameListContextMenuManageTitleUpdates": "Zarządzaj aktualizacjami", + "GameListContextMenuManageTitleUpdatesToolTip": "Otwiera okno zarządzania aktualizacjami danej aplikacji", + "GameListContextMenuManageDlc": "Zarządzaj dodatkową zawartością (DLC)", + "GameListContextMenuManageDlcToolTip": "Otwiera okno zarządzania dodatkową zawartością", + "GameListContextMenuCacheManagement": "Zarządzanie Cache", + "GameListContextMenuCacheManagementPurgePptc": "Zakolejkuj rekompilację PPTC", + "GameListContextMenuCacheManagementPurgePptcToolTip": "Zainicjuj Rekompilację PPTC przy następnym uruchomieniu gry", + "GameListContextMenuCacheManagementPurgeShaderCache": "Wyczyść pamięć podręczną cieni", + "GameListContextMenuCacheManagementPurgeShaderCacheToolTip": "Usuwa pamięć podręczną cieni danej aplikacji", + "GameListContextMenuCacheManagementOpenPptcDirectory": "Otwórz katalog PPTC", + "GameListContextMenuCacheManagementOpenPptcDirectoryToolTip": "Otwiera katalog, który zawiera pamięć podręczną PPTC aplikacji", + "GameListContextMenuCacheManagementOpenShaderCacheDirectory": "Otwórz katalog pamięci podręcznej cieni", + "GameListContextMenuCacheManagementOpenShaderCacheDirectoryToolTip": "Otwiera katalog, który zawiera pamięć podręczną cieni aplikacji", + "GameListContextMenuExtractData": "Wypakuj dane", + "GameListContextMenuExtractDataExeFS": "ExeFS", + "GameListContextMenuExtractDataExeFSToolTip": "Wyodrębnij sekcję ExeFS z bieżącej konfiguracji aplikacji (w tym aktualizacje)", + "GameListContextMenuExtractDataRomFS": "RomFS", + "GameListContextMenuExtractDataRomFSToolTip": "Wyodrębnij sekcję RomFS z bieżącej konfiguracji aplikacji (w tym aktualizacje)", + "GameListContextMenuExtractDataLogo": "Logo", + "GameListContextMenuExtractDataLogoToolTip": "Wyodrębnij sekcję z logiem z bieżącej konfiguracji aplikacji (w tym aktualizacje)", + "GameListContextMenuCreateShortcut": "Utwórz skrót aplikacji", + "GameListContextMenuCreateShortcutToolTip": "Utwórz skrót na pulpicie, który uruchamia wybraną aplikację", + "GameListContextMenuCreateShortcutToolTipMacOS": "Utwórz skrót w folderze 'Aplikacje' w systemie macOS, który uruchamia wybraną aplikację", + "GameListContextMenuOpenModsDirectory": "Otwórz katalog modów", + "GameListContextMenuOpenModsDirectoryToolTip": "Otwiera katalog zawierający mody dla danej aplikacji", + "GameListContextMenuOpenSdModsDirectory": "Otwórz katalog modów Atmosphere", + "GameListContextMenuOpenSdModsDirectoryToolTip": "Otwiera alternatywny katalog Atmosphere na karcie SD, który zawiera mody danej aplikacji. Przydatne dla modów przygotowanych pod prawdziwy sprzęt.", + "GameListContextMenuTrimXCI": "Check and Trim XCI File", + "GameListContextMenuTrimXCIToolTip": "Check and Trim XCI File to Save Disk Space", + "StatusBarGamesLoaded": "{0}/{1} Załadowane gry", + "StatusBarSystemVersion": "Wersja systemu: {0}", + "StatusBarXCIFileTrimming": "Trimming XCI File '{0}'", + "LinuxVmMaxMapCountDialogTitle": "Wykryto niski limit dla przypisań pamięci", + "LinuxVmMaxMapCountDialogTextPrimary": "Czy chcesz zwiększyć wartość vm.max_map_count do {0}", + "LinuxVmMaxMapCountDialogTextSecondary": "Niektóre gry mogą próbować przypisać sobie więcej pamięci niż obecnie, jest to dozwolone. Ryujinx ulegnie awarii, gdy limit zostanie przekroczony.", + "LinuxVmMaxMapCountDialogButtonUntilRestart": "Tak, do następnego ponownego uruchomienia", + "LinuxVmMaxMapCountDialogButtonPersistent": "Tak, permanentnie ", + "LinuxVmMaxMapCountWarningTextPrimary": "Maksymalna ilość przypisanej pamięci jest mniejsza niż zalecana.", + "LinuxVmMaxMapCountWarningTextSecondary": "Obecna wartość vm.max_map_count ({0}) jest mniejsza niż {1}. Niektóre gry mogą próbować stworzyć więcej mapowań pamięci niż obecnie jest to dozwolone. Ryujinx napotka crash, gdy dojdzie do takiej sytuacji.\n\nMożesz chcieć ręcznie zwiększyć limit lub zainstalować pkexec, co pozwala Ryujinx na pomoc w tym zakresie.", + "Settings": "Ustawienia", + "SettingsTabGeneral": "Interfejs użytkownika", + "SettingsTabGeneralGeneral": "Ogólne", + "SettingsTabGeneralEnableDiscordRichPresence": "Włącz Bogatą Obecność Discord", + "SettingsTabGeneralCheckUpdatesOnLaunch": "Sprawdzaj aktualizacje przy uruchomieniu", + "SettingsTabGeneralShowConfirmExitDialog": "Pokazuj okno dialogowe \"Potwierdź wyjście\"", + "SettingsTabGeneralRememberWindowState": "Remember Window Size/Position", + "SettingsTabGeneralShowTitleBar": "Show Title Bar (Requires restart)", + "SettingsTabGeneralHideCursor": "Ukryj kursor:", + "SettingsTabGeneralHideCursorNever": "Nigdy", + "SettingsTabGeneralHideCursorOnIdle": "Gdy bezczynny", + "SettingsTabGeneralHideCursorAlways": "Zawsze", + "SettingsTabGeneralGameDirectories": "Katalogi gier", + "SettingsTabGeneralAutoloadDirectories": "Autoload DLC/Updates Directories", + "SettingsTabGeneralAutoloadNote": "DLC and Updates which refer to missing files will be unloaded automatically", + "SettingsTabGeneralAdd": "Dodaj", + "SettingsTabGeneralRemove": "Usuń", + "SettingsTabSystem": "System", + "SettingsTabSystemCore": "Główne", + "SettingsTabSystemSystemRegion": "Region systemu:", + "SettingsTabSystemSystemRegionJapan": "Japonia", + "SettingsTabSystemSystemRegionUSA": "Stany Zjednoczone", + "SettingsTabSystemSystemRegionEurope": "Europa", + "SettingsTabSystemSystemRegionAustralia": "Australia", + "SettingsTabSystemSystemRegionChina": "Chiny", + "SettingsTabSystemSystemRegionKorea": "Korea", + "SettingsTabSystemSystemRegionTaiwan": "Tajwan", + "SettingsTabSystemSystemLanguage": "Język systemu:", + "SettingsTabSystemSystemLanguageJapanese": "Japoński", + "SettingsTabSystemSystemLanguageAmericanEnglish": "Angielski (Stany Zjednoczone)", + "SettingsTabSystemSystemLanguageFrench": "Francuski", + "SettingsTabSystemSystemLanguageGerman": "Niemiecki", + "SettingsTabSystemSystemLanguageItalian": "Włoski", + "SettingsTabSystemSystemLanguageSpanish": "Hiszpański", + "SettingsTabSystemSystemLanguageChinese": "Chiński", + "SettingsTabSystemSystemLanguageKorean": "Koreański", + "SettingsTabSystemSystemLanguageDutch": "Holenderski", + "SettingsTabSystemSystemLanguagePortuguese": "Portugalski", + "SettingsTabSystemSystemLanguageRussian": "Rosyjski", + "SettingsTabSystemSystemLanguageTaiwanese": "Tajwański", + "SettingsTabSystemSystemLanguageBritishEnglish": "Angielski (Wielka Brytania)", + "SettingsTabSystemSystemLanguageCanadianFrench": "Kanadyjski Francuski", + "SettingsTabSystemSystemLanguageLatinAmericanSpanish": "Hiszpański (Ameryka Łacińska)", + "SettingsTabSystemSystemLanguageSimplifiedChinese": "Chiński (Uproszczony)", + "SettingsTabSystemSystemLanguageTraditionalChinese": "Chiński (Tradycyjny)", + "SettingsTabSystemSystemTimeZone": "Strefa czasowa systemu:", + "SettingsTabSystemSystemTime": "Czas systemu:", + "SettingsTabSystemEnableVsync": "Synchronizacja pionowa", + "SettingsTabSystemEnablePptc": "PPTC (Profilowana pamięć podręczna trwałych łłumaczeń)", + "SettingsTabSystemEnableLowPowerPptc": "Low-power PPTC", + "SettingsTabSystemEnableFsIntegrityChecks": "Sprawdzanie integralności systemu plików", + "SettingsTabSystemAudioBackend": "Backend Dżwięku:", + "SettingsTabSystemAudioBackendDummy": "Atrapa", + "SettingsTabSystemAudioBackendOpenAL": "OpenAL", + "SettingsTabSystemAudioBackendSoundIO": "SoundIO", + "SettingsTabSystemAudioBackendSDL2": "SDL2", + "SettingsTabSystemHacks": "Hacki", + "SettingsTabSystemHacksNote": " (mogą powodować niestabilność)", + "SettingsTabSystemDramSize": "Użyj alternatywnego układu pamięci (Deweloperzy)", + "SettingsTabSystemDramSize4GiB": "4GiB", + "SettingsTabSystemDramSize6GiB": "6GiB", + "SettingsTabSystemDramSize8GiB": "8GiB", + "SettingsTabSystemDramSize12GiB": "12GiB", + "SettingsTabSystemIgnoreMissingServices": "Ignoruj Brakujące Usługi", + "SettingsTabSystemIgnoreApplet": "Ignoruj ​​aplet", + "SettingsTabGraphics": "Grafika", + "SettingsTabGraphicsAPI": "Graficzne API", + "SettingsTabGraphicsEnableShaderCache": "Włącz pamięć podręczną cieni", + "SettingsTabGraphicsAnisotropicFiltering": "Filtrowanie anizotropowe:", + "SettingsTabGraphicsAnisotropicFilteringAuto": "Automatyczne", + "SettingsTabGraphicsAnisotropicFiltering2x": "2x", + "SettingsTabGraphicsAnisotropicFiltering4x": "4x", + "SettingsTabGraphicsAnisotropicFiltering8x": "8x", + "SettingsTabGraphicsAnisotropicFiltering16x": "16x", + "SettingsTabGraphicsResolutionScale": "Skalowanie rozdzielczości:", + "SettingsTabGraphicsResolutionScaleCustom": "Niestandardowa (Niezalecane)", + "SettingsTabGraphicsResolutionScaleNative": "Natywna (720p/1080p)", + "SettingsTabGraphicsResolutionScale2x": "2x (1440p/2160p)", + "SettingsTabGraphicsResolutionScale3x": "3x (2160p/3240p)", + "SettingsTabGraphicsResolutionScale4x": "4x (2880p/4320p) (niezalecane)", + "SettingsTabGraphicsAspectRatio": "Format obrazu:", + "SettingsTabGraphicsAspectRatio4x3": "4:3", + "SettingsTabGraphicsAspectRatio16x9": "16:9", + "SettingsTabGraphicsAspectRatio16x10": "16:10", + "SettingsTabGraphicsAspectRatio21x9": "21:9", + "SettingsTabGraphicsAspectRatio32x9": "32:9", + "SettingsTabGraphicsAspectRatioStretch": "Rozciągnij do Okna", + "SettingsTabGraphicsDeveloperOptions": "Opcje programisty", + "SettingsTabGraphicsShaderDumpPath": "Ścieżka do zgranych cieni graficznych:", + "SettingsTabLogging": "Dziennik zdarzeń", + "SettingsTabLoggingLogging": "Dziennik zdarzeń", + "SettingsTabLoggingEnableLoggingToFile": "Włącz rejestrowanie zdarzeń do pliku", + "SettingsTabLoggingEnableStubLogs": "Wlącz Skróty Logów", + "SettingsTabLoggingEnableInfoLogs": "Włącz Logi Informacyjne", + "SettingsTabLoggingEnableWarningLogs": "Włącz Logi Ostrzeżeń", + "SettingsTabLoggingEnableErrorLogs": "Włącz Logi Błędów", + "SettingsTabLoggingEnableTraceLogs": "Włącz Logi Śledzenia", + "SettingsTabLoggingEnableGuestLogs": "Włącz Logi Gości", + "SettingsTabLoggingEnableFsAccessLogs": "Włącz Logi Dostępu do Systemu Plików", + "SettingsTabLoggingFsGlobalAccessLogMode": "Tryb globalnego dziennika zdarzeń systemu plików:", + "SettingsTabLoggingDeveloperOptions": "Opcje programisty (UWAGA: wpływa na wydajność)", + "SettingsTabLoggingDeveloperOptionsNote": "UWAGA: Pogrorszy wydajność", + "SettingsTabLoggingGraphicsBackendLogLevel": "Poziom rejestrowania do dziennika zdarzeń Backendu Graficznego:", + "SettingsTabLoggingGraphicsBackendLogLevelNone": "Nic", + "SettingsTabLoggingGraphicsBackendLogLevelError": "Błędy", + "SettingsTabLoggingGraphicsBackendLogLevelPerformance": "Spowolnienia", + "SettingsTabLoggingGraphicsBackendLogLevelAll": "Wszystko", + "SettingsTabLoggingEnableDebugLogs": "Włącz dzienniki zdarzeń do debugowania", + "SettingsTabInput": "Sterowanie", + "SettingsTabInputEnableDockedMode": "Tryb zadokowany", + "SettingsTabInputDirectKeyboardAccess": "Bezpośredni dostęp do klawiatury", + "SettingsButtonSave": "Zapisz", + "SettingsButtonClose": "Zamknij", + "SettingsButtonOk": "OK", + "SettingsButtonCancel": "Anuluj", + "SettingsButtonApply": "Zastosuj", + "ControllerSettingsPlayer": "Gracz", + "ControllerSettingsPlayer1": "Gracz 1", + "ControllerSettingsPlayer2": "Gracz 2", + "ControllerSettingsPlayer3": "Gracz 3", + "ControllerSettingsPlayer4": "Gracz 4", + "ControllerSettingsPlayer5": "Gracz 5", + "ControllerSettingsPlayer6": "Gracz 6", + "ControllerSettingsPlayer7": "Gracz 7", + "ControllerSettingsPlayer8": "Gracz 8", + "ControllerSettingsHandheld": "Przenośny", + "ControllerSettingsInputDevice": "Urządzenie wejściowe", + "ControllerSettingsRefresh": "Odśwież", + "ControllerSettingsDeviceDisabled": "Wyłączone", + "ControllerSettingsControllerType": "Typ kontrolera", + "ControllerSettingsControllerTypeHandheld": "Przenośny", + "ControllerSettingsControllerTypeProController": "Pro Kontroler", + "ControllerSettingsControllerTypeJoyConPair": "Para JoyCon-ów", + "ControllerSettingsControllerTypeJoyConLeft": "Lewy JoyCon", + "ControllerSettingsControllerTypeJoyConRight": "Prawy JoyCon", + "ControllerSettingsProfile": "Profil", + "ControllerSettingsProfileDefault": "Domyślny", + "ControllerSettingsLoad": "Wczytaj", + "ControllerSettingsAdd": "Dodaj", + "ControllerSettingsRemove": "Usuń", + "ControllerSettingsButtons": "Przyciski", + "ControllerSettingsButtonA": "A", + "ControllerSettingsButtonB": "B", + "ControllerSettingsButtonX": "X", + "ControllerSettingsButtonY": "Y", + "ControllerSettingsButtonPlus": "+", + "ControllerSettingsButtonMinus": "-", + "ControllerSettingsDPad": "Krzyżak (D-Pad)", + "ControllerSettingsDPadUp": "Góra", + "ControllerSettingsDPadDown": "Dół", + "ControllerSettingsDPadLeft": "Lewo", + "ControllerSettingsDPadRight": "Prawo", + "ControllerSettingsStickButton": "Przycisk", + "ControllerSettingsStickUp": "Góra ", + "ControllerSettingsStickDown": "Dół ", + "ControllerSettingsStickLeft": "Lewo", + "ControllerSettingsStickRight": "Prawo", + "ControllerSettingsStickStick": "Gałka", + "ControllerSettingsStickInvertXAxis": "Odwróć gałkę X", + "ControllerSettingsStickInvertYAxis": "Odwróć gałkę Y", + "ControllerSettingsStickDeadzone": "Martwa strefa:", + "ControllerSettingsLStick": "Lewa Gałka", + "ControllerSettingsRStick": "Prawa Gałka", + "ControllerSettingsTriggersLeft": "Lewe Triggery", + "ControllerSettingsTriggersRight": "Prawe Triggery", + "ControllerSettingsTriggersButtonsLeft": "Lewe Przyciski Triggerów", + "ControllerSettingsTriggersButtonsRight": "Prawe Przyciski Triggerów", + "ControllerSettingsTriggers": "Triggery", + "ControllerSettingsTriggerL": "L", + "ControllerSettingsTriggerR": "R", + "ControllerSettingsTriggerZL": "ZL", + "ControllerSettingsTriggerZR": "ZR", + "ControllerSettingsLeftSL": "SL", + "ControllerSettingsLeftSR": "SR", + "ControllerSettingsRightSL": "SL", + "ControllerSettingsRightSR": "SR", + "ControllerSettingsExtraButtonsLeft": "Lewe Przyciski", + "ControllerSettingsExtraButtonsRight": "Prawe Przyciski", + "ControllerSettingsMisc": "Różne", + "ControllerSettingsTriggerThreshold": "Próg Triggerów:", + "ControllerSettingsMotion": "Ruch", + "ControllerSettingsMotionUseCemuhookCompatibleMotion": "Użyj ruchu zgodnego z CemuHook", + "ControllerSettingsMotionControllerSlot": "Slot Kontrolera:", + "ControllerSettingsMotionMirrorInput": "Odzwierciedlaj Sterowanie", + "ControllerSettingsMotionRightJoyConSlot": "Prawy Slot JoyCon:", + "ControllerSettingsMotionServerHost": "Host Serwera:", + "ControllerSettingsMotionGyroSensitivity": "Czułość Żyroskopu:", + "ControllerSettingsMotionGyroDeadzone": "Deadzone Żyroskopu:", + "ControllerSettingsSave": "Zapisz", + "ControllerSettingsClose": "Zamknij", + "KeyUnknown": "Unknown", + "KeyShiftLeft": "Shift Left", + "KeyShiftRight": "Shift Right", + "KeyControlLeft": "Ctrl Left", + "KeyMacControlLeft": "⌃ Left", + "KeyControlRight": "Ctrl Right", + "KeyMacControlRight": "⌃ Right", + "KeyAltLeft": "Alt Left", + "KeyMacAltLeft": "⌥ Left", + "KeyAltRight": "Alt Right", + "KeyMacAltRight": "⌥ Right", + "KeyWinLeft": "⊞ Left", + "KeyMacWinLeft": "⌘ Left", + "KeyWinRight": "⊞ Right", + "KeyMacWinRight": "⌘ Right", + "KeyMenu": "Menu", + "KeyUp": "Up", + "KeyDown": "Down", + "KeyLeft": "Left", + "KeyRight": "Right", + "KeyEnter": "Enter", + "KeyEscape": "Escape", + "KeySpace": "Space", + "KeyTab": "Tab", + "KeyBackSpace": "Backspace", + "KeyInsert": "Insert", + "KeyDelete": "Delete", + "KeyPageUp": "Page Up", + "KeyPageDown": "Page Down", + "KeyHome": "Home", + "KeyEnd": "End", + "KeyCapsLock": "Caps Lock", + "KeyScrollLock": "Scroll Lock", + "KeyPrintScreen": "Print Screen", + "KeyPause": "Pause", + "KeyNumLock": "Num Lock", + "KeyClear": "Clear", + "KeyKeypad0": "Keypad 0", + "KeyKeypad1": "Keypad 1", + "KeyKeypad2": "Keypad 2", + "KeyKeypad3": "Keypad 3", + "KeyKeypad4": "Keypad 4", + "KeyKeypad5": "Keypad 5", + "KeyKeypad6": "Keypad 6", + "KeyKeypad7": "Keypad 7", + "KeyKeypad8": "Keypad 8", + "KeyKeypad9": "Keypad 9", + "KeyKeypadDivide": "Keypad Divide", + "KeyKeypadMultiply": "Keypad Multiply", + "KeyKeypadSubtract": "Keypad Subtract", + "KeyKeypadAdd": "Keypad Add", + "KeyKeypadDecimal": "Keypad Decimal", + "KeyKeypadEnter": "Keypad Enter", + "KeyNumber0": "0", + "KeyNumber1": "1", + "KeyNumber2": "2", + "KeyNumber3": "3", + "KeyNumber4": "4", + "KeyNumber5": "5", + "KeyNumber6": "6", + "KeyNumber7": "7", + "KeyNumber8": "8", + "KeyNumber9": "9", + "KeyTilde": "~", + "KeyGrave": "`", + "KeyMinus": "-", + "KeyPlus": "+", + "KeyBracketLeft": "[", + "KeyBracketRight": "]", + "KeySemicolon": ";", + "KeyQuote": "\"", + "KeyComma": ",", + "KeyPeriod": ".", + "KeySlash": "/", + "KeyBackSlash": "\\", + "KeyUnbound": "Unbound", + "GamepadLeftStick": "L Stick Button", + "GamepadRightStick": "R Stick Button", + "GamepadLeftShoulder": "Left Shoulder", + "GamepadRightShoulder": "Right Shoulder", + "GamepadLeftTrigger": "Left Trigger", + "GamepadRightTrigger": "Right Trigger", + "GamepadDpadUp": "Up", + "GamepadDpadDown": "Down", + "GamepadDpadLeft": "Left", + "GamepadDpadRight": "Right", + "GamepadMinus": "-", + "GamepadPlus": "+", + "GamepadGuide": "Guide", + "GamepadMisc1": "Misc", + "GamepadPaddle1": "Paddle 1", + "GamepadPaddle2": "Paddle 2", + "GamepadPaddle3": "Paddle 3", + "GamepadPaddle4": "Paddle 4", + "GamepadTouchpad": "Touchpad", + "GamepadSingleLeftTrigger0": "Left Trigger 0", + "GamepadSingleRightTrigger0": "Right Trigger 0", + "GamepadSingleLeftTrigger1": "Left Trigger 1", + "GamepadSingleRightTrigger1": "Right Trigger 1", + "StickLeft": "Left Stick", + "StickRight": "Right Stick", + "UserProfilesSelectedUserProfile": "Wybrany profil użytkownika:", + "UserProfilesSaveProfileName": "Zapisz nazwę profilu", + "UserProfilesChangeProfileImage": "Zmień obrazek profilu", + "UserProfilesAvailableUserProfiles": "Dostępne profile użytkownika:", + "UserProfilesAddNewProfile": "Utwórz profil", + "UserProfilesDelete": "Usuń", + "UserProfilesClose": "Zamknij", + "ProfileNameSelectionWatermark": "Wybierz pseudonim", + "ProfileImageSelectionTitle": "Wybór Obrazu Profilu", + "ProfileImageSelectionHeader": "Wybierz zdjęcie profilowe", + "ProfileImageSelectionNote": "Możesz zaimportować niestandardowy obraz profilu lub wybrać awatar z firmware'u systemowego", + "ProfileImageSelectionImportImage": "Importuj Plik Obrazu", + "ProfileImageSelectionSelectAvatar": "Wybierz domyślny awatar z oprogramowania konsoli", + "InputDialogTitle": "Okno Dialogowe Wprowadzania", + "InputDialogOk": "OK", + "InputDialogCancel": "Anuluj", + "InputDialogCancelling": "Cancelling", + "InputDialogClose": "Close", + "InputDialogAddNewProfileTitle": "Wybierz nazwę profilu", + "InputDialogAddNewProfileHeader": "Wprowadź nazwę profilu", + "InputDialogAddNewProfileSubtext": "(Maksymalna długość: {0})", + "AvatarChoose": "Wybierz awatar", + "AvatarSetBackgroundColor": "Ustaw kolor tła", + "AvatarClose": "Zamknij", + "ControllerSettingsLoadProfileToolTip": "Wczytaj profil", + "ControllerSettingsViewProfileToolTip": "View Profile", + "ControllerSettingsAddProfileToolTip": "Dodaj profil", + "ControllerSettingsRemoveProfileToolTip": "Usuń profil", + "ControllerSettingsSaveProfileToolTip": "Zapisz profil", + "MenuBarFileToolsTakeScreenshot": "Zrób zrzut ekranu", + "MenuBarFileToolsHideUi": "Ukryj interfejs użytkownika", + "GameListContextMenuRunApplication": "Uruchom aplikację ", + "GameListContextMenuToggleFavorite": "Przełącz na ulubione", + "GameListContextMenuToggleFavoriteToolTip": "Przełącz status Ulubionej Gry", + "SettingsTabGeneralTheme": "Motyw:", + "SettingsTabGeneralThemeAuto": "Auto", + "SettingsTabGeneralThemeDark": "Ciemny", + "SettingsTabGeneralThemeLight": "Jasny", + "ControllerSettingsConfigureGeneral": "Konfiguruj", + "ControllerSettingsRumble": "Wibracje", + "ControllerSettingsRumbleStrongMultiplier": "Mnożnik mocnych wibracji", + "ControllerSettingsRumbleWeakMultiplier": "Mnożnik słabych wibracji", + "DialogMessageSaveNotAvailableMessage": "Nie ma zapisanych danych dla {0} [{1:x16}]", + "DialogMessageSaveNotAvailableCreateSaveMessage": "Czy chcesz utworzyć zapis danych dla tej gry?", + "DialogConfirmationTitle": "Ryujinx - Potwierdzenie", + "DialogUpdaterTitle": "Ryujinx - Asystent aktualizacji", + "DialogErrorTitle": "Ryujinx - Błąd", + "DialogWarningTitle": "Ryujinx - Ostrzeżenie", + "DialogExitTitle": "Ryujinx - Wyjdź", + "DialogErrorMessage": "Ryujinx napotkał błąd", + "DialogExitMessage": "Czy na pewno chcesz zamknąć Ryujinx?", + "DialogExitSubMessage": "Wszystkie niezapisane dane zostaną utracone!", + "DialogMessageCreateSaveErrorMessage": "Wystąpił błąd podczas tworzenia określonych zapisanych danych: {0}", + "DialogMessageFindSaveErrorMessage": "Wystąpił błąd podczas próby znalezienia określonych zapisanych danych: {0}", + "FolderDialogExtractTitle": "Wybierz folder, do którego chcesz rozpakować", + "DialogNcaExtractionMessage": "Wypakowywanie sekcji {0} z {1}...", + "DialogNcaExtractionTitle": "Asystent wypakowania sekcji NCA", + "DialogNcaExtractionMainNcaNotFoundErrorMessage": "Niepowodzenie podczas wypakowywania. W wybranym pliku nie było głównego NCA.", + "DialogNcaExtractionCheckLogErrorMessage": "Niepowodzenie podczas wypakowywania. Przeczytaj plik dziennika, aby uzyskać więcej informacji.", + "DialogNcaExtractionSuccessMessage": "Wypakowywanie zakończone pomyślnie.", + "DialogUpdaterConvertFailedMessage": "Nie udało się przekonwertować obecnej wersji Ryujinx.", + "DialogUpdaterCancelUpdateMessage": "Anulowanie aktualizacji!", + "DialogUpdaterAlreadyOnLatestVersionMessage": "Używasz już najnowszej wersji Ryujinx!", + "DialogUpdaterFailedToGetVersionMessage": "Wystąpił błąd podczas próby uzyskania informacji o obecnej wersji z GitHub Release. Może to być spowodowane nową wersją kompilowaną przez GitHub Actions. Spróbuj ponownie za kilka minut.", + "DialogUpdaterConvertFailedGithubMessage": "Nie udało się przekonwertować otrzymanej wersji Ryujinx z Github Release.", + "DialogUpdaterDownloadingMessage": "Pobieranie aktualizacji...", + "DialogUpdaterExtractionMessage": "Wypakowywanie Aktualizacji...", + "DialogUpdaterRenamingMessage": "Zmiana Nazwy Aktualizacji...", + "DialogUpdaterAddingFilesMessage": "Dodawanie Nowej Aktualizacji...", + "DialogUpdaterShowChangelogMessage": "Show Changelog", + "DialogUpdaterCompleteMessage": "Aktualizacja Zakończona!", + "DialogUpdaterRestartMessage": "Czy chcesz teraz zrestartować Ryujinx?", + "DialogUpdaterNoInternetMessage": "Nie masz połączenia z Internetem!", + "DialogUpdaterNoInternetSubMessage": "Sprawdź, czy masz działające połączenie internetowe!", + "DialogUpdaterDirtyBuildMessage": "Nie możesz zaktualizować Dirty wersji Ryujinx!", + "DialogUpdaterDirtyBuildSubMessage": "Pobierz Ryujinx ze strony https://ryujinx.app/download, jeśli szukasz obsługiwanej wersji.", + "DialogRestartRequiredMessage": "Wymagane Ponowne Uruchomienie", + "DialogThemeRestartMessage": "Motyw został zapisany. Aby zastosować motyw, konieczne jest ponowne uruchomienie.", + "DialogThemeRestartSubMessage": "Czy chcesz uruchomić ponownie?", + "DialogFirmwareInstallEmbeddedMessage": "Czy chcesz zainstalować firmware wbudowany w tę grę? (Firmware {0})", + "DialogFirmwareInstallEmbeddedSuccessMessage": "Nie znaleziono zainstalowanego oprogramowania, ale Ryujinx był w stanie zainstalować oprogramowanie {0} z dostarczonej gry.\n\nEmulator uruchomi się teraz.", + "DialogFirmwareNoFirmwareInstalledMessage": "Brak Zainstalowanego Firmware'u", + "DialogFirmwareInstalledMessage": "Firmware {0} został zainstalowany", + "DialogInstallFileTypesSuccessMessage": "Pomyślnie zainstalowano typy plików!", + "DialogInstallFileTypesErrorMessage": "Nie udało się zainstalować typów plików.", + "DialogUninstallFileTypesSuccessMessage": "Pomyślnie odinstalowano typy plików!", + "DialogUninstallFileTypesErrorMessage": "Nie udało się odinstalować typów plików.", + "DialogOpenSettingsWindowLabel": "Otwórz Okno Ustawień", + "DialogOpenXCITrimmerWindowLabel": "XCI Trimmer Window", + "DialogControllerAppletTitle": "Aplet Kontrolera", + "DialogMessageDialogErrorExceptionMessage": "Błąd wyświetlania okna Dialogowego Wiadomości: {0}", + "DialogSoftwareKeyboardErrorExceptionMessage": "Błąd wyświetlania Klawiatury Oprogramowania: {0}", + "DialogErrorAppletErrorExceptionMessage": "Błąd wyświetlania okna Dialogowego ErrorApplet: {0}", + "DialogUserErrorDialogMessage": "{0}: {1}", + "DialogUserErrorDialogInfoMessage": "\nAby uzyskać więcej informacji o tym, jak naprawić ten błąd, zapoznaj się z naszym Przewodnikiem instalacji.", + "DialogUserErrorDialogTitle": "Błąd Ryujinxa ({0})", + "DialogAmiiboApiTitle": "API Amiibo", + "DialogAmiiboApiFailFetchMessage": "Wystąpił błąd podczas pobierania informacji z API.", + "DialogAmiiboApiConnectErrorMessage": "Nie można połączyć się z serwerem API Amiibo. Usługa może nie działać lub może być konieczne sprawdzenie, czy połączenie internetowe jest online.", + "DialogProfileInvalidProfileErrorMessage": "Profil {0} jest niezgodny z bieżącym systemem konfiguracji sterowania.", + "DialogProfileDefaultProfileOverwriteErrorMessage": "Profil Domyślny nie może zostać nadpisany", + "DialogProfileDeleteProfileTitle": "Usuwanie Profilu", + "DialogProfileDeleteProfileMessage": "Ta czynność jest nieodwracalna, czy na pewno chcesz kontynuować?", + "DialogWarning": "Uwaga", + "DialogPPTCDeletionMessage": "Masz zamiar umieścić w kolejce rekompilację PPTC przy następnym uruchomieniu:\n\n{0}\n\nCzy na pewno chcesz kontynuować?", + "DialogPPTCDeletionErrorMessage": "Błąd czyszczenia cache PPTC w {0}: {1}", + "DialogShaderDeletionMessage": "Zamierzasz usunąć cache Shaderów dla :\n\n{0}\n\nNa pewno chcesz kontynuować?", + "DialogShaderDeletionErrorMessage": "Błąd czyszczenia cache Shaderów w {0}: {1}", + "DialogRyujinxErrorMessage": "Ryujinx napotkał błąd", + "DialogInvalidTitleIdErrorMessage": "Błąd UI: Wybrana gra nie miała prawidłowego ID tytułu", + "DialogFirmwareInstallerFirmwareNotFoundErrorMessage": "Nie znaleziono prawidłowego firmware'u systemowego w {0}.", + "DialogFirmwareInstallerFirmwareInstallTitle": "Zainstaluj Firmware {0}", + "DialogFirmwareInstallerFirmwareInstallMessage": "Wersja systemu {0} zostanie zainstalowana.", + "DialogFirmwareInstallerFirmwareInstallSubMessage": "\n\nZastąpi to obecną wersję systemu {0}.", + "DialogFirmwareInstallerFirmwareInstallConfirmMessage": "\n\nCzy chcesz kontynuować?", + "DialogFirmwareInstallerFirmwareInstallWaitMessage": "Instalowanie firmware'u...", + "DialogFirmwareInstallerFirmwareInstallSuccessMessage": "Wersja systemu {0} została pomyślnie zainstalowana.", + "DialogUserProfileDeletionWarningMessage": "Nie będzie innych profili do otwarcia, jeśli wybrany profil zostanie usunięty", + "DialogUserProfileDeletionConfirmMessage": "Czy chcesz usunąć wybrany profil", + "DialogUserProfileUnsavedChangesTitle": "Uwaga - Niezapisane zmiany", + "DialogUserProfileUnsavedChangesMessage": "Wprowadziłeś zmiany dla tego profilu użytkownika, które nie zostały zapisane.", + "DialogUserProfileUnsavedChangesSubMessage": "Czy chcesz odrzucić zmiany?", + "DialogControllerSettingsModifiedConfirmMessage": "Aktualne ustawienia kontrolera zostały zaktualizowane.", + "DialogControllerSettingsModifiedConfirmSubMessage": "Czy chcesz zapisać?", + "DialogLoadFileErrorMessage": "{0}. Błędny plik: {1}", + "DialogModAlreadyExistsMessage": "Modyfikacja już istnieje", + "DialogModInvalidMessage": "Podany katalog nie zawiera modyfikacji!", + "DialogModDeleteNoParentMessage": "Nie udało się usunąć: Nie można odnaleźć katalogu nadrzędnego dla modyfikacji \"{0}\"!", + "DialogDlcNoDlcErrorMessage": "Określony plik nie zawiera DLC dla wybranego tytułu!", + "DialogPerformanceCheckLoggingEnabledMessage": "Masz włączone rejestrowanie śledzenia, które jest przeznaczone tylko dla programistów.", + "DialogPerformanceCheckLoggingEnabledConfirmMessage": "Aby uzyskać optymalną wydajność, zaleca się wyłączenie rejestrowania śledzenia. Czy chcesz teraz wyłączyć rejestrowanie śledzenia?", + "DialogPerformanceCheckShaderDumpEnabledMessage": "Masz włączone zrzucanie shaderów, które jest przeznaczone tylko dla programistów.", + "DialogPerformanceCheckShaderDumpEnabledConfirmMessage": "Aby uzyskać optymalną wydajność, zaleca się wyłączenie zrzucania shaderów. Czy chcesz teraz wyłączyć zrzucanie shaderów?", + "DialogLoadAppGameAlreadyLoadedMessage": "Gra została już załadowana", + "DialogLoadAppGameAlreadyLoadedSubMessage": "Zatrzymaj emulację lub zamknij emulator przed uruchomieniem innej gry.", + "DialogUpdateAddUpdateErrorMessage": "Określony plik nie zawiera aktualizacji dla wybranego tytułu!", + "DialogSettingsBackendThreadingWarningTitle": "Ostrzeżenie — Wątki Backend", + "DialogSettingsBackendThreadingWarningMessage": "Ryujinx musi zostać ponownie uruchomiony po zmianie tej opcji, aby działał w pełni. W zależności od platformy może być konieczne ręczne wyłączenie sterownika wielowątkowości podczas korzystania z Ryujinx.", + "DialogModManagerDeletionWarningMessage": "Zamierzasz usunąć modyfikacje: {0}\n\nCzy na pewno chcesz kontynuować?", + "DialogModManagerDeletionAllWarningMessage": "Zamierzasz usunąć wszystkie modyfikacje dla wybranego tytułu: {0}\n\nCzy na pewno chcesz kontynuować?", + "SettingsTabGraphicsFeaturesOptions": "Funkcje", + "SettingsTabGraphicsBackendMultithreading": "Wielowątkowość Backendu Graficznego:", + "CommonAuto": "Auto", + "CommonOff": "Wyłączone", + "CommonOn": "Włączone", + "InputDialogYes": "Tak", + "InputDialogNo": "Nie", + "DialogProfileInvalidProfileNameErrorMessage": "Nazwa pliku zawiera nieprawidłowe znaki. Proszę spróbuj ponownie.", + "MenuBarOptionsPauseEmulation": "Pauza", + "MenuBarOptionsResumeEmulation": "Wznów", + "AboutUrlTooltipMessage": "Kliknij, aby otworzyć stronę Ryujinx w domyślnej przeglądarce.", + "AboutDisclaimerMessage": "Ryujinx nie jest w żaden sposób powiązany z Nintendo™,\nani z żadnym z jej partnerów.", + "AboutAmiiboDisclaimerMessage": "AmiiboAPI (www.amiiboapi.com) jest używane\nw naszej emulacji Amiibo.", + "AboutPatreonUrlTooltipMessage": "Kliknij, aby otworzyć stronę Patreon Ryujinx w domyślnej przeglądarce.", + "AboutGithubUrlTooltipMessage": "Kliknij, aby otworzyć stronę GitHub Ryujinx w domyślnej przeglądarce.", + "AboutDiscordUrlTooltipMessage": "Kliknij, aby otworzyć zaproszenie na serwer Discord Ryujinx w domyślnej przeglądarce.", + "AboutTwitterUrlTooltipMessage": "Kliknij, aby otworzyć stronę Twitter Ryujinx w domyślnej przeglądarce.", + "AboutRyujinxAboutTitle": "O Aplikacji:", + "AboutRyujinxAboutContent": "Ryujinx to emulator Nintendo Switch™.\nWspieraj nas na Patreonie.\nOtrzymuj najnowsze wiadomości na naszym Twitterze lub Discordzie.\nDeweloperzy zainteresowani współpracą mogą dowiedzieć się więcej na naszym GitHubie lub Discordzie.", + "AboutRyujinxMaintainersTitle": "Utrzymywany Przez:", + "AboutRyujinxMaintainersContentTooltipMessage": "Kliknij, aby otworzyć stronę Współtwórcy w domyślnej przeglądarce.", + "AboutRyujinxSupprtersTitle": "Wspierani na Patreonie Przez:", + "AmiiboSeriesLabel": "Seria Amiibo", + "AmiiboCharacterLabel": "Postać", + "AmiiboScanButtonLabel": "Zeskanuj", + "AmiiboOptionsShowAllLabel": "Pokaż Wszystkie Amiibo", + "AmiiboOptionsUsRandomTagLabel": "Hack: Użyj losowego UUID tagu", + "DlcManagerTableHeadingEnabledLabel": "Włączone", + "DlcManagerTableHeadingTitleIdLabel": "ID Tytułu", + "DlcManagerTableHeadingContainerPathLabel": "Ścieżka Kontenera", + "DlcManagerTableHeadingFullPathLabel": "Pełna Ścieżka", + "DlcManagerRemoveAllButton": "Usuń Wszystkie", + "DlcManagerEnableAllButton": "Włącz Wszystkie", + "DlcManagerDisableAllButton": "Wyłącz Wszystkie", + "ModManagerDeleteAllButton": "Usuń wszystko", + "MenuBarOptionsChangeLanguage": "Zmień język", + "MenuBarShowFileTypes": "Pokaż typy plików", + "CommonSort": "Sortuj", + "CommonShowNames": "Pokaż Nazwy", + "CommonFavorite": "Ulubione", + "OrderAscending": "Rosnąco", + "OrderDescending": "Malejąco", + "SettingsTabGraphicsFeatures": "Funkcje i Ulepszenia", + "ErrorWindowTitle": "Okno Błędu", + "ToggleDiscordTooltip": "Wybierz, czy chcesz wyświetlać Ryujinx w swojej \"aktualnie grane\" aktywności Discord", + "AddGameDirBoxTooltip": "Wprowadź katalog gier aby dodać go do listy", + "AddGameDirTooltip": "Dodaj katalog gier do listy", + "RemoveGameDirTooltip": "Usuń wybrany katalog gier", + "AddAutoloadDirBoxTooltip": "Enter an autoload directory to add to the list", + "AddAutoloadDirTooltip": "Add an autoload directory to the list", + "RemoveAutoloadDirTooltip": "Remove selected autoload directory", + "CustomThemeCheckTooltip": "Użyj niestandardowego motywu Avalonia dla GUI, aby zmienić wygląd menu emulatora", + "CustomThemePathTooltip": "Ścieżka do niestandardowego motywu GUI", + "CustomThemeBrowseTooltip": "Wyszukaj niestandardowy motyw GUI", + "DockModeToggleTooltip": "Tryb Zadokowany sprawia, że emulowany system zachowuje się jak zadokowany Nintendo Switch. Poprawia to jakość grafiki w większości gier. I odwrotnie, wyłączenie tej opcji sprawi, że emulowany system będzie zachowywał się jak przenośny Nintendo Switch, zmniejszając jakość grafiki.\n\nSkonfiguruj sterowanie gracza 1, jeśli planujesz używać trybu Zadokowanego; Skonfiguruj sterowanie przenośne, jeśli planujesz używać trybu przenośnego.\n\nPozostaw WŁĄCZONY, jeśli nie masz pewności.", + "DirectKeyboardTooltip": "Obsługa bezpośredniego dostępu do klawiatury (HID). Zapewnia dostęp gier do klawiatury jako urządzenia do wprowadzania tekstu.\n\nDziała tylko z grami, które natywnie wspierają użycie klawiatury w urządzeniu Switch hardware.\n\nPozostaw wyłączone w razie braku pewności.", + "DirectMouseTooltip": "Obsługa bezpośredniego dostępu do myszy (HID). Zapewnia dostęp gier do myszy jako urządzenia wskazującego.\n\nDziała tylko z grami, które natywnie obsługują przyciski myszy w urządzeniu Switch, które są nieliczne i daleko między nimi.\n\nPo włączeniu funkcja ekranu dotykowego może nie działać.\n\nPozostaw wyłączone w razie wątpliwości.", + "RegionTooltip": "Zmień Region Systemu", + "LanguageTooltip": "Zmień język systemu", + "TimezoneTooltip": "Zmień Strefę Czasową Systemu", + "TimeTooltip": "Zmień czas systemowy", + "VSyncToggleTooltip": "Synchronizacja pionowa emulowanej konsoli. Zasadniczo ogranicznik klatek dla większości gier; wyłączenie jej może spowodować, że gry będą działać z większą szybkością, ekrany wczytywania wydłużą się lub nawet utkną.\n\nMoże być przełączana w grze za pomocą preferowanego skrótu klawiszowego. Zalecamy to zrobić, jeśli planujesz ją wyłączyć.\n\nW razie wątpliwości pozostaw WŁĄCZONĄ.", + "PptcToggleTooltip": "Zapisuje przetłumaczone funkcje JIT, dzięki czemu nie muszą być tłumaczone za każdym razem, gdy gra się ładuje.\n\nZmniejsza zacinanie się i znacznie przyspiesza uruchamianie po pierwszym uruchomieniu gry.\n\nJeśli nie masz pewności, pozostaw WŁĄCZONE", + "LowPowerPptcToggleTooltip": "Load the PPTC using a third of the amount of cores.", + "FsIntegrityToggleTooltip": "Sprawdza pliki podczas uruchamiania gry i jeśli zostaną wykryte uszkodzone pliki, wyświetla w dzienniku błąd hash.\n\nNie ma wpływu na wydajność i ma pomóc w rozwiązywaniu problemów.\n\nPozostaw WŁĄCZONE, jeśli nie masz pewności.", + "AudioBackendTooltip": "Zmienia backend używany do renderowania dźwięku.\n\nSDL2 jest preferowany, podczas gdy OpenAL i SoundIO są używane jako rezerwy. Dummy nie będzie odtwarzać dźwięku.\n\nW razie wątpliwości ustaw SDL2.", + "MemoryManagerTooltip": "Zmień sposób mapowania i uzyskiwania dostępu do pamięci gości. Znacznie wpływa na wydajność emulowanego procesora.\n\nUstaw na HOST UNCHECKED, jeśli nie masz pewności.", + "MemoryManagerSoftwareTooltip": "Użyj tabeli stron oprogramowania do translacji adresów. Najwyższa celność, ale najwolniejsza wydajność.", + "MemoryManagerHostTooltip": "Bezpośrednio mapuj pamięć w przestrzeni adresowej hosta. Znacznie szybsza kompilacja i wykonanie JIT.", + "MemoryManagerUnsafeTooltip": "Bezpośrednio mapuj pamięć, ale nie maskuj adresu w przestrzeni adresowej gościa przed uzyskaniem dostępu. Szybciej, ale kosztem bezpieczeństwa. Aplikacja gościa może uzyskać dostęp do pamięci z dowolnego miejsca w Ryujinx, więc w tym trybie uruchamiaj tylko programy, którym ufasz.", + "UseHypervisorTooltip": "Użyj Hiperwizora zamiast JIT. Znacznie poprawia wydajność, gdy jest dostępny, ale może być niestabilny w swoim obecnym stanie ", + "DRamTooltip": "Wykorzystuje alternatywny układ MemoryMode, aby naśladować model rozwojowy Switcha.\n\nJest to przydatne tylko w przypadku pakietów tekstur o wyższej rozdzielczości lub modów w rozdzielczości 4k. NIE poprawia wydajności.\n\nW razie wątpliwości pozostaw WYŁĄCZONE.", + "IgnoreMissingServicesTooltip": "Ignoruje niezaimplementowane usługi Horizon OS. Może to pomóc w ominięciu awarii podczas uruchamiania niektórych gier.\n\nW razie wątpliwości pozostaw WYŁĄCZONE.", + "IgnoreAppletTooltip": "Zewnętrzny dialog \"Controller Applet\" nie pojawi się, jeśli gamepad zostanie odłączony podczas rozgrywki. Nie pojawi się monit o zamknięcie dialogu lub skonfigurowanie nowego kontrolera. Po ponownym podłączeniu poprzednio odłączonego kontrolera gra zostanie automatycznie wznowiona.", + "GraphicsBackendThreadingTooltip": "Wykonuje polecenia backend'u graficznego w drugim wątku.\n\nPrzyspiesza kompilację shaderów, zmniejsza zacinanie się i poprawia wydajność sterowników GPU bez własnej obsługi wielowątkowości. Nieco lepsza wydajność w sterownikach z wielowątkowością.\n\nUstaw na AUTO, jeśli nie masz pewności.", + "GalThreadingTooltip": "Wykonuje polecenia backend'u graficznego w drugim wątku.\n\nPrzyspiesza kompilację shaderów, zmniejsza zacinanie się i poprawia wydajność sterowników GPU bez własnej obsługi wielowątkowości. Nieco lepsza wydajność w sterownikach z wielowątkowością.\n\nUstaw na AUTO, jeśli nie masz pewności.", + "ShaderCacheToggleTooltip": "Zapisuje pamięć podręczną shaderów na dysku, co zmniejsza zacinanie się w kolejnych uruchomieniach.\n\nPozostaw WŁĄCZONE, jeśli nie masz pewności.", + "ResolutionScaleTooltip": "Multiplies the game's rendering resolution.\n\nA few games may not work with this and look pixelated even when the resolution is increased; for those games, you may need to find mods that remove anti-aliasing or that increase their internal rendering resolution. For using the latter, you'll likely want to select Native.\n\nThis option can be changed while a game is running by clicking \"Apply\" below; you can simply move the settings window aside and experiment until you find your preferred look for a game.\n\nKeep in mind 4x is overkill for virtually any setup.", + "ResolutionScaleEntryTooltip": "Skala rozdzielczości zmiennoprzecinkowej, np. 1,5. Skale niecałkowite częściej powodują problemy lub awarie.", + "AnisotropyTooltip": "Level of Anisotropic Filtering. Set to Auto to use the value requested by the game.", + "AspectRatioTooltip": "Aspect Ratio applied to the renderer window.\n\nOnly change this if you're using an aspect ratio mod for your game, otherwise the graphics will be stretched.\n\nLeave on 16:9 if unsure.", + "ShaderDumpPathTooltip": "Ścieżka Zrzutu Shaderów Grafiki", + "FileLogTooltip": "Zapisuje logowanie konsoli w pliku dziennika na dysku. Nie wpływa na wydajność.", + "StubLogTooltip": "Wyświetla w konsoli skrótowe komunikaty dziennika. Nie wpływa na wydajność.", + "InfoLogTooltip": "Wyświetla komunikaty dziennika informacyjnego w konsoli. Nie wpływa na wydajność.", + "WarnLogTooltip": "Wyświetla komunikaty dziennika ostrzeżeń w konsoli. Nie wpływa na wydajność.", + "ErrorLogTooltip": "Wyświetla w konsoli komunikaty dziennika błędów. Nie wpływa na wydajność.", + "TraceLogTooltip": "Wyświetla komunikaty dziennika śledzenia w konsoli. Nie wpływa na wydajność.", + "GuestLogTooltip": "Wyświetla komunikaty dziennika gości w konsoli. Nie wpływa na wydajność.", + "FileAccessLogTooltip": "Wyświetla w konsoli komunikaty dziennika dostępu do plików.", + "FSAccessLogModeTooltip": "Włącza wyjście dziennika dostępu FS do konsoli. Możliwe tryby to 0-3", + "DeveloperOptionTooltip": "Używaj ostrożnie", + "OpenGlLogLevel": "Wymaga włączonych odpowiednich poziomów logów", + "DebugLogTooltip": "Wyświetla komunikaty dziennika debugowania w konsoli.\n\nUżywaj tego tylko na wyraźne polecenie członka załogi, ponieważ utrudni to odczytanie dzienników i pogorszy wydajność emulatora.", + "LoadApplicationFileTooltip": "Otwórz eksplorator plików, aby wybrać plik kompatybilny z Switch do wczytania", + "LoadApplicationFolderTooltip": "Otwórz eksplorator plików, aby wybrać zgodną z Switch, rozpakowaną aplikację do załadowania", + "LoadDlcFromFolderTooltip": "Open a file explorer to choose one or more folders to bulk load DLC from", + "LoadTitleUpdatesFromFolderTooltip": "Open a file explorer to choose one or more folders to bulk load title updates from", + "OpenRyujinxFolderTooltip": "Otwórz folder systemu plików Ryujinx", + "OpenRyujinxLogsTooltip": "Otwiera folder, w którym zapisywane są logi", + "ExitTooltip": "Wyjdź z Ryujinx", + "OpenSettingsTooltip": "Otwórz okno ustawień", + "OpenProfileManagerTooltip": "Otwórz okno Menedżera Profili Użytkownika", + "StopEmulationTooltip": "Zatrzymaj emulację bieżącej gry i wróć do wyboru gier", + "CheckUpdatesTooltip": "Sprawdź aktualizacje Ryujinx", + "OpenAboutTooltip": "Otwórz Okno Informacje", + "GridSize": "Wielkość siatki", + "GridSizeTooltip": "Zmień rozmiar elementów siatki", + "SettingsTabSystemSystemLanguageBrazilianPortuguese": "Brazylijski Portugalski", + "AboutRyujinxContributorsButtonHeader": "Zobacz Wszystkich Współtwórców", + "SettingsTabSystemAudioVolume": "Głośność: ", + "AudioVolumeTooltip": "Zmień Głośność Dźwięku", + "SettingsTabSystemEnableInternetAccess": "Dostęp do Internetu Gościa/Tryb LAN", + "EnableInternetAccessTooltip": "Pozwala emulowanej aplikacji na łączenie się z Internetem.\n\nGry w trybie LAN mogą łączyć się ze sobą, gdy ta opcja jest włączona, a systemy są połączone z tym samym punktem dostępu. Dotyczy to również prawdziwych konsol.\n\nNie pozwala na łączenie się z serwerami Nintendo. Może powodować awarie niektórych gier, które próbują połączyć się z Internetem.\n\nPozostaw WYŁĄCZONE, jeśli nie masz pewności.", + "GameListContextMenuManageCheatToolTip": "Zarządzaj Kodami", + "GameListContextMenuManageCheat": "Zarządzaj Kodami", + "GameListContextMenuManageModToolTip": "Zarządzaj modyfikacjami", + "GameListContextMenuManageMod": "Zarządzaj modyfikacjami", + "ControllerSettingsStickRange": "Zasięg:", + "DialogStopEmulationTitle": "Ryujinx - Zatrzymaj Emulację", + "DialogStopEmulationMessage": "Czy na pewno chcesz zatrzymać emulację?", + "SettingsTabCpu": "CPU", + "SettingsTabAudio": "Dżwięk", + "SettingsTabNetwork": "Sieć", + "SettingsTabNetworkConnection": "Połączenie Sieciowe", + "SettingsTabCpuCache": "Cache CPU", + "SettingsTabCpuMemory": "Pamięć CPU", + "DialogUpdaterFlatpakNotSupportedMessage": "Zaktualizuj Ryujinx przez FlatHub.", + "UpdaterDisabledWarningTitle": "Aktualizator Wyłączony!", + "ControllerSettingsRotate90": "Obróć o 90° w Prawo", + "IconSize": "Rozmiar ikon", + "IconSizeTooltip": "Zmień rozmiar ikon gry", + "MenuBarOptionsShowConsole": "Pokaż Konsolę", + "ShaderCachePurgeError": "Błąd podczas czyszczenia cache shaderów w {0}: {1}", + "UserErrorNoKeys": "Nie znaleziono kluczy", + "UserErrorNoFirmware": "Nie znaleziono firmware'u", + "UserErrorFirmwareParsingFailed": "Błąd parsowania firmware'u", + "UserErrorApplicationNotFound": "Aplikacja nie znaleziona", + "UserErrorUnknown": "Nieznany błąd", + "UserErrorUndefined": "Niezdefiniowany błąd", + "UserErrorNoKeysDescription": "Ryujinx nie mógł znaleźć twojego pliku 'prod.keys'", + "UserErrorNoFirmwareDescription": "Ryujinx nie mógł znaleźć żadnego zainstalowanego firmware'u", + "UserErrorFirmwareParsingFailedDescription": "Ryujinx nie był w stanie zparsować dostarczonego firmware'u. Jest to zwykle spowodowane nieaktualnymi kluczami.", + "UserErrorApplicationNotFoundDescription": "Ryujinx nie mógł znaleźć prawidłowej aplikacji na podanej ścieżce.", + "UserErrorUnknownDescription": "Wystąpił nieznany błąd!", + "UserErrorUndefinedDescription": "Wystąpił niezdefiniowany błąd! To nie powinno się zdarzyć, skontaktuj się z deweloperem!", + "OpenSetupGuideMessage": "Otwórz Podręcznik Konfiguracji", + "NoUpdate": "Brak Aktualizacji", + "TitleUpdateVersionLabel": "Wersja {0} - {1}", + "TitleBundledUpdateVersionLabel": "Bundled: Version {0}", + "TitleBundledDlcLabel": "Bundled:", + "TitleXCIStatusPartialLabel": "Partial", + "TitleXCIStatusTrimmableLabel": "Untrimmed", + "TitleXCIStatusUntrimmableLabel": "Trimmed", + "TitleXCIStatusFailedLabel": "(Failed)", + "TitleXCICanSaveLabel": "Save {0:n0} Mb", + "TitleXCISavingLabel": "Saved {0:n0} Mb", + "RyujinxInfo": "Ryujinx - Info", + "RyujinxConfirm": "Ryujinx - Potwierdzenie", + "FileDialogAllTypes": "Wszystkie typy", + "Never": "Nigdy", + "SwkbdMinCharacters": "Musi mieć co najmniej {0} znaków", + "SwkbdMinRangeCharacters": "Musi mieć długość od {0}-{1} znaków", + "SoftwareKeyboard": "Klawiatura Oprogramowania", + "SoftwareKeyboardModeNumeric": "Może składać się jedynie z 0-9 lub '.'", + "SoftwareKeyboardModeAlphabet": "Nie może zawierać znaków CJK", + "SoftwareKeyboardModeASCII": "Musi zawierać tylko tekst ASCII", + "ControllerAppletControllers": "Obsługiwane Kontrolery:", + "ControllerAppletPlayers": "Gracze:", + "ControllerAppletDescription": "Twoja aktualna konfiguracja jest nieprawidłowa. Otwórz ustawienia i skonfiguruj swoje wejścia.", + "ControllerAppletDocked": "Ustawiony tryb zadokowany. Sterowanie przenośne powinno być wyłączone.", + "UpdaterRenaming": "Zmienianie Nazw Starych Plików...", + "UpdaterRenameFailed": "Aktualizator nie mógł zmienić nazwy pliku: {0}", + "UpdaterAddingFiles": "Dodawanie Nowych Plików...", + "UpdaterExtracting": "Wypakowywanie Aktualizacji...", + "UpdaterDownloading": "Pobieranie Aktualizacji...", + "Game": "Gra", + "Docked": "Zadokowany", + "Handheld": "Przenośny", + "ConnectionError": "Błąd Połączenia.", + "AboutPageDeveloperListMore": "{0} i więcej...", + "ApiError": "Błąd API.", + "LoadingHeading": "Wczytywanie {0}", + "CompilingPPTC": "Kompilowanie PTC", + "CompilingShaders": "Kompilowanie Shaderów", + "AllKeyboards": "Wszystkie klawiatury", + "OpenFileDialogTitle": "Wybierz obsługiwany plik do otwarcia", + "OpenFolderDialogTitle": "Wybierz folder z rozpakowaną grą", + "AllSupportedFormats": "Wszystkie Obsługiwane Formaty", + "RyujinxUpdater": "Aktualizator Ryujinx", + "SettingsTabHotkeys": "Skróty Klawiszowe Klawiatury", + "SettingsTabHotkeysHotkeys": "Skróty Klawiszowe Klawiatury", + "SettingsTabHotkeysToggleVsyncHotkey": "Przełącz VSync:", + "SettingsTabHotkeysScreenshotHotkey": "Zrzut Ekranu:", + "SettingsTabHotkeysShowUiHotkey": "Pokaż UI:", + "SettingsTabHotkeysPauseHotkey": "Pauza:", + "SettingsTabHotkeysToggleMuteHotkey": "Wycisz:", + "ControllerMotionTitle": "Ustawienia Sterowania Ruchowego", + "ControllerRumbleTitle": "Ustawienia Wibracji", + "SettingsSelectThemeFileDialogTitle": "Wybierz Plik Motywu", + "SettingsXamlThemeFile": "Plik Motywu Xaml", + "AvatarWindowTitle": "Zarządzaj Kontami — Avatar", + "Amiibo": "Amiibo", + "Unknown": "Nieznane", + "Usage": "Użycie", + "Writable": "Zapisywalne", + "SelectDlcDialogTitle": "Wybierz pliki DLC", + "SelectUpdateDialogTitle": "Wybierz pliki aktualizacji", + "SelectModDialogTitle": "Wybierz katalog modów", + "TrimXCIFileDialogTitle": "Check and Trim XCI File", + "TrimXCIFileDialogPrimaryText": "This function will first check the empty space and then trim the XCI File to save disk space.", + "TrimXCIFileDialogSecondaryText": "Current File Size: {0:n} MB\nGame Data Size: {1:n} MB\nDisk Space Savings: {2:n} MB", + "TrimXCIFileNoTrimNecessary": "XCI File does not need to be trimmed. Check logs for further details", + "TrimXCIFileNoUntrimPossible": "XCI File cannot be untrimmed. Check logs for further details", + "TrimXCIFileReadOnlyFileCannotFix": "XCI File is Read Only and could not be made writable. Check logs for further details", + "TrimXCIFileFileSizeChanged": "XCI File has changed in size since it was scanned. Please check the file is not being written to and try again.", + "TrimXCIFileFreeSpaceCheckFailed": "XCI File has data in the free space area, it is not safe to trim", + "TrimXCIFileInvalidXCIFile": "XCI File contains invalid data. Check logs for further details", + "TrimXCIFileFileIOWriteError": "XCI File could not be opened for writing. Check logs for further details", + "TrimXCIFileFailedPrimaryText": "Trimming of the XCI file failed", + "TrimXCIFileCancelled": "The operation was cancelled", + "TrimXCIFileFileUndertermined": "No operation was performed", + "UserProfileWindowTitle": "Menedżer Profili Użytkowników", + "CheatWindowTitle": "Menedżer Kodów", + "DlcWindowTitle": "Menedżer Zawartości do Pobrania", + "ModWindowTitle": "Zarządzaj modami dla {0} ({1})", + "UpdateWindowTitle": "Menedżer Aktualizacji Tytułu", + "XCITrimmerWindowTitle": "XCI File Trimmer", + "XCITrimmerTitleStatusCount": "{0} of {1} Title(s) Selected", + "XCITrimmerTitleStatusCountWithFilter": "{0} of {1} Title(s) Selected ({2} displayed)", + "XCITrimmerTitleStatusTrimming": "Trimming {0} Title(s)...", + "XCITrimmerTitleStatusUntrimming": "Untrimming {0} Title(s)...", + "XCITrimmerTitleStatusFailed": "Failed", + "XCITrimmerPotentialSavings": "Potential Savings", + "XCITrimmerActualSavings": "Actual Savings", + "XCITrimmerSavingsMb": "{0:n0} Mb", + "XCITrimmerSelectDisplayed": "Select Shown", + "XCITrimmerDeselectDisplayed": "Deselect Shown", + "XCITrimmerSortName": "Title", + "XCITrimmerSortSaved": "Space Savings", + "XCITrimmerTrim": "Trim", + "XCITrimmerUntrim": "Untrim", + "UpdateWindowUpdateAddedMessage": "{0} new update(s) added", + "UpdateWindowBundledContentNotice": "Bundled updates cannot be removed, only disabled.", + "CheatWindowHeading": "Kody Dostępne dla {0} [{1}]", + "BuildId": "Identyfikator wersji:", + "DlcWindowBundledContentNotice": "Bundled DLC cannot be removed, only disabled.", + "DlcWindowHeading": "{0} Zawartości do Pobrania dostępna dla {1} ({2})", + "DlcWindowDlcAddedMessage": "{0} new downloadable content(s) added", + "AutoloadDlcAddedMessage": "{0} new downloadable content(s) added", + "AutoloadDlcRemovedMessage": "{0} missing downloadable content(s) removed", + "AutoloadUpdateAddedMessage": "{0} new update(s) added", + "AutoloadUpdateRemovedMessage": "{0} missing update(s) removed", + "ModWindowHeading": "{0} Mod(y/ów)", + "UserProfilesEditProfile": "Edytuj Zaznaczone", + "Continue": "Continue", + "Cancel": "Anuluj", + "Save": "Zapisz", + "Discard": "Odrzuć", + "Paused": "Wstrzymano", + "UserProfilesSetProfileImage": "Ustaw Obraz Profilu", + "UserProfileEmptyNameError": "Nazwa jest wymagana", + "UserProfileNoImageError": "Należy ustawić obraz profilowy", + "GameUpdateWindowHeading": "{0} Aktualizacje dostępne dla {1} ({2})", + "SettingsTabHotkeysResScaleUpHotkey": "Zwiększ Rozdzielczość:", + "SettingsTabHotkeysResScaleDownHotkey": "Zmniejsz Rozdzielczość:", + "UserProfilesName": "Nazwa:", + "UserProfilesUserId": "ID Użytkownika:", + "SettingsTabGraphicsBackend": "Backend Graficzny", + "SettingsTabGraphicsBackendTooltip": "Select the graphics backend that will be used in the emulator.\n\nVulkan is overall better for all modern graphics cards, as long as their drivers are up to date. Vulkan also features faster shader compilation (less stuttering) on all GPU vendors.\n\nOpenGL may achieve better results on old Nvidia GPUs, on old AMD GPUs on Linux, or on GPUs with lower VRAM, though shader compilation stutters will be greater.\n\nSet to Vulkan if unsure. Set to OpenGL if your GPU does not support Vulkan even with the latest graphics drivers.", + "SettingsEnableTextureRecompression": "Włącz Rekompresję Tekstur", + "SettingsEnableTextureRecompressionTooltip": "Compresses ASTC textures in order to reduce VRAM usage.\n\nGames using this texture format include Astral Chain, Bayonetta 3, Fire Emblem Engage, Metroid Prime Remastered, Super Mario Bros. Wonder and The Legend of Zelda: Tears of the Kingdom.\n\nGraphics cards with 4GiB VRAM or less will likely crash at some point while running these games.\n\nEnable only if you're running out of VRAM on the aforementioned games. Leave OFF if unsure.", + "SettingsTabGraphicsPreferredGpu": "Preferowane GPU", + "SettingsTabGraphicsPreferredGpuTooltip": "Wybierz kartę graficzną, która będzie używana z backendem graficznym Vulkan.\n\nNie wpływa na GPU używane przez OpenGL.\n\nW razie wątpliwości ustaw flagę GPU jako \"dGPU\". Jeśli żadnej nie ma, pozostaw nietknięte.", + "SettingsAppRequiredRestartMessage": "Wymagane Zrestartowanie Ryujinx", + "SettingsGpuBackendRestartMessage": "Zmieniono ustawienia Backendu Graficznego lub GPU. Będzie to wymagało ponownego uruchomienia", + "SettingsGpuBackendRestartSubMessage": "Czy chcesz zrestartować teraz?", + "RyujinxUpdaterMessage": "Czy chcesz zaktualizować Ryujinx do najnowszej wersji?", + "SettingsTabHotkeysVolumeUpHotkey": "Zwiększ Głośność:", + "SettingsTabHotkeysVolumeDownHotkey": "Zmniejsz Głośność:", + "SettingsEnableMacroHLE": "Włącz Macro HLE", + "SettingsEnableMacroHLETooltip": "Wysokopoziomowa emulacja kodu GPU Macro.\n\nPoprawia wydajność, ale może powodować błędy graficzne w niektórych grach.\n\nW razie wątpliwości pozostaw WŁĄCZONE.", + "SettingsEnableColorSpacePassthrough": "Przekazywanie przestrzeni kolorów", + "SettingsEnableColorSpacePassthroughTooltip": "Nakazuje API Vulkan przekazywać informacje o kolorze bez określania przestrzeni kolorów. Dla użytkowników z wyświetlaczami o szerokim zakresie kolorów może to skutkować bardziej żywymi kolorami, kosztem ich poprawności.", + "VolumeShort": "Głoś", + "UserProfilesManageSaves": "Zarządzaj Zapisami", + "DeleteUserSave": "Czy chcesz usunąć zapis użytkownika dla tej gry?", + "IrreversibleActionNote": "Ta czynność nie jest odwracalna.", + "SaveManagerHeading": "Zarządzaj Zapisami dla {0}", + "SaveManagerTitle": "Menedżer Zapisów", + "Name": "Nazwa", + "Size": "Rozmiar", + "Search": "Wyszukaj", + "UserProfilesRecoverLostAccounts": "Odzyskaj Utracone Konta", + "Recover": "Odzyskaj", + "UserProfilesRecoverHeading": "Znaleziono zapisy dla następujących kont", + "UserProfilesRecoverEmptyList": "Brak profili do odzyskania", + "GraphicsAATooltip": "Applies anti-aliasing to the game render.\n\nFXAA will blur most of the image, while SMAA will attempt to find jagged edges and smooth them out.\n\nNot recommended to use in conjunction with the FSR scaling filter.\n\nThis option can be changed while a game is running by clicking \"Apply\" below; you can simply move the settings window aside and experiment until you find your preferred look for a game.\n\nLeave on NONE if unsure.", + "GraphicsAALabel": "Antyaliasing:", + "GraphicsScalingFilterLabel": "Filtr skalowania:", + "GraphicsScalingFilterTooltip": "Choose the scaling filter that will be applied when using resolution scale.\n\nBilinear works well for 3D games and is a safe default option.\n\nNearest is recommended for pixel art games.\n\nFSR 1.0 is merely a sharpening filter, not recommended for use with FXAA or SMAA.\n\nThis option can be changed while a game is running by clicking \"Apply\" below; you can simply move the settings window aside and experiment until you find your preferred look for a game.\n\nLeave on BILINEAR if unsure.", + "GraphicsScalingFilterBilinear": "Dwuliniowe", + "GraphicsScalingFilterNearest": "Najbliższe", + "GraphicsScalingFilterFsr": "FSR", + "GraphicsScalingFilterArea": "Area", + "GraphicsScalingFilterLevelLabel": "Poziom", + "GraphicsScalingFilterLevelTooltip": "Ustaw poziom ostrzeżenia FSR 1.0. Wyższy jest ostrzejszy.", + "SmaaLow": "SMAA Niskie", + "SmaaMedium": "SMAA Średnie", + "SmaaHigh": "SMAA Wysokie", + "SmaaUltra": "SMAA Ultra", + "UserEditorTitle": "Edytuj użytkownika", + "UserEditorTitleCreate": "Utwórz użytkownika", + "SettingsTabNetworkInterface": "Interfejs sieci:", + "NetworkInterfaceTooltip": "Interfejs sieciowy używany dla funkcji LAN/LDN.\n\nw połączeniu z VPN lub XLink Kai i grą z obsługą sieci LAN, może być użyty do spoofowania połączenia z tą samą siecią przez Internet.\n\nZostaw DOMYŚLNE, jeśli nie ma pewności.", + "NetworkInterfaceDefault": "Domyślny", + "PackagingShaders": "Pakuje Shadery ", + "AboutChangelogButton": "Zobacz listę zmian na GitHubie", + "AboutChangelogButtonTooltipMessage": "Kliknij, aby otworzyć listę zmian dla tej wersji w domyślnej przeglądarce.", + "SettingsTabNetworkMultiplayer": "Gra Wieloosobowa", + "MultiplayerMode": "Tryb:", + "MultiplayerModeTooltip": "Change LDN multiplayer mode.\n\nLdnMitm will modify local wireless/local play functionality in games to function as if it were LAN, allowing for local, same-network connections with other Ryujinx instances and hacked Nintendo Switch consoles that have the ldn_mitm module installed.\n\nMultiplayer requires all players to be on the same game version (i.e. Super Smash Bros. Ultimate v13.0.1 can't connect to v13.0.0).\n\nLeave DISABLED if unsure.", + "MultiplayerModeDisabled": "Wyłączone", + "MultiplayerModeLdnMitm": "ldn_mitm", + "MultiplayerModeLdnRyu": "RyuLDN", + "MultiplayerDisableP2P": "Disable P2P Network Hosting (may increase latency)", + "MultiplayerDisableP2PTooltip": "Disable P2P network hosting, peers will proxy through the master server instead of connecting to you directly.", + "LdnPassphrase": "Network Passphrase:", + "LdnPassphraseTooltip": "You will only be able to see hosted games with the same passphrase as you.", + "LdnPassphraseInputTooltip": "Enter a passphrase in the format Ryujinx-<8 hex chars>. You will only be able to see hosted games with the same passphrase as you.", + "LdnPassphraseInputPublic": "(public)", + "GenLdnPass": "Generate Random", + "GenLdnPassTooltip": "Generates a new passphrase, which can be shared with other players.", + "ClearLdnPass": "Clear", + "ClearLdnPassTooltip": "Clears the current passphrase, returning to the public network.", + "InvalidLdnPassphrase": "Invalid Passphrase! Must be in the format \"Ryujinx-<8 hex chars>\"" +} diff --git a/src/Ryujinx/Assets/Locales/pt_BR.json b/src/Ryujinx/Assets/Locales/pt_BR.json new file mode 100644 index 000000000..7574c1d20 --- /dev/null +++ b/src/Ryujinx/Assets/Locales/pt_BR.json @@ -0,0 +1,867 @@ +{ + "Language": "Português (BR)", + "MenuBarFileOpenApplet": "Abrir Applet", + "MenuBarFileOpenAppletOpenMiiApplet": "Mii Edit Applet", + "MenuBarFileOpenAppletOpenMiiAppletToolTip": "Abrir editor Mii em modo avulso", + "SettingsTabInputDirectMouseAccess": "Acesso direto ao mouse", + "SettingsTabSystemMemoryManagerMode": "Modo de gerenciamento de memória:", + "SettingsTabSystemMemoryManagerModeSoftware": "Software", + "SettingsTabSystemMemoryManagerModeHost": "Hóspede (rápido)", + "SettingsTabSystemMemoryManagerModeHostUnchecked": "Hóspede sem verificação (mais rápido, inseguro)", + "SettingsTabSystemUseHypervisor": "Usar Hipervisor", + "MenuBarFile": "_Arquivo", + "MenuBarFileOpenFromFile": "_Abrir ROM do jogo...", + "MenuBarFileOpenFromFileError": "Nenhum aplicativo encontrado no arquivo selecionado.", + "MenuBarFileOpenUnpacked": "Abrir jogo _extraído...", + "MenuBarFileLoadDlcFromFolder": "Carregar DLC da Pasta", + "MenuBarFileLoadTitleUpdatesFromFolder": "Carregar Atualizações de Jogo da Pasta", + "MenuBarFileOpenEmuFolder": "Abrir diretório do e_mulador...", + "MenuBarFileOpenLogsFolder": "Abrir diretório de _logs...", + "MenuBarFileExit": "_Sair", + "MenuBarOptions": "_Opções", + "MenuBarOptionsToggleFullscreen": "_Mudar para tela cheia", + "MenuBarOptionsStartGamesInFullscreen": "Iniciar jogos em tela cheia", + "MenuBarOptionsStopEmulation": "_Encerrar emulação", + "MenuBarOptionsSettings": "_Configurações", + "MenuBarOptionsManageUserProfiles": "_Gerenciar perfis de usuário", + "MenuBarActions": "_Ações", + "MenuBarOptionsSimulateWakeUpMessage": "_Simular mensagem de acordar console", + "MenuBarActionsScanAmiibo": "Escanear um Amiibo", + "MenuBarTools": "_Ferramentas", + "MenuBarToolsInstallFirmware": "_Instalar firmware", + "MenuBarFileToolsInstallFirmwareFromFile": "Instalar firmware a partir de um arquivo ZIP/XCI", + "MenuBarFileToolsInstallFirmwareFromDirectory": "Instalar firmware a partir de um diretório", + "MenuBarToolsManageFileTypes": "Gerenciar tipos de arquivo", + "MenuBarToolsInstallFileTypes": "Instalar tipos de arquivo", + "MenuBarToolsUninstallFileTypes": "Desinstalar tipos de arquivos", + "MenuBarToolsXCITrimmer": "Trim XCI Files", + "MenuBarView": "_View", + "MenuBarViewWindow": "Window Size", + "MenuBarViewWindow720": "720p", + "MenuBarViewWindow1080": "1080p", + "MenuBarHelp": "_Ajuda", + "MenuBarHelpCheckForUpdates": "_Verificar se há atualizações", + "MenuBarHelpAbout": "_Sobre", + "MenuSearch": "Buscar...", + "GameListHeaderFavorite": "Favorito", + "GameListHeaderIcon": "Ícone", + "GameListHeaderApplication": "Nome", + "GameListHeaderDeveloper": "Desenvolvedor", + "GameListHeaderVersion": "Versão", + "GameListHeaderTimePlayed": "Tempo de jogo", + "GameListHeaderLastPlayed": "Último jogo", + "GameListHeaderFileExtension": "Extensão", + "GameListHeaderFileSize": "Tamanho", + "GameListHeaderPath": "Caminho", + "GameListContextMenuOpenUserSaveDirectory": "Abrir diretório de saves do usuário", + "GameListContextMenuOpenUserSaveDirectoryToolTip": "Abre o diretório que contém jogos salvos para o usuário atual", + "GameListContextMenuOpenDeviceSaveDirectory": "Abrir diretório de saves de dispositivo do usuário", + "GameListContextMenuOpenDeviceSaveDirectoryToolTip": "Abre o diretório que contém saves do dispositivo para o usuário atual", + "GameListContextMenuOpenBcatSaveDirectory": "Abrir diretório de saves BCAT do usuário", + "GameListContextMenuOpenBcatSaveDirectoryToolTip": "Abre o diretório que contém saves BCAT para o usuário atual", + "GameListContextMenuManageTitleUpdates": "Gerenciar atualizações do jogo", + "GameListContextMenuManageTitleUpdatesToolTip": "Abre a janela de gerenciamento de atualizações", + "GameListContextMenuManageDlc": "Gerenciar DLCs", + "GameListContextMenuManageDlcToolTip": "Abre a janela de gerenciamento de DLCs", + "GameListContextMenuCacheManagement": "Gerenciamento de cache", + "GameListContextMenuCacheManagementPurgePptc": "Limpar cache PPTC", + "GameListContextMenuCacheManagementPurgePptcToolTip": "Deleta o cache PPTC armazenado em disco do jogo", + "GameListContextMenuCacheManagementPurgeShaderCache": "Limpar cache de Shader", + "GameListContextMenuCacheManagementPurgeShaderCacheToolTip": "Deleta o cache de Shader armazenado em disco do jogo", + "GameListContextMenuCacheManagementOpenPptcDirectory": "Abrir diretório do cache PPTC", + "GameListContextMenuCacheManagementOpenPptcDirectoryToolTip": "Abre o diretório contendo os arquivos do cache PPTC", + "GameListContextMenuCacheManagementOpenShaderCacheDirectory": "Abrir diretório do cache de Shader", + "GameListContextMenuCacheManagementOpenShaderCacheDirectoryToolTip": "Abre o diretório contendo os arquivos do cache de Shader", + "GameListContextMenuExtractData": "Extrair dados", + "GameListContextMenuExtractDataExeFS": "ExeFS", + "GameListContextMenuExtractDataExeFSToolTip": "Extrai a seção ExeFS do jogo (incluindo atualizações)", + "GameListContextMenuExtractDataRomFS": "RomFS", + "GameListContextMenuExtractDataRomFSToolTip": "Extrai a seção RomFS do jogo (incluindo atualizações)", + "GameListContextMenuExtractDataLogo": "Logo", + "GameListContextMenuExtractDataLogoToolTip": "Extrai a seção Logo do jogo (incluindo atualizações)", + "GameListContextMenuCreateShortcut": "Criar atalho da aplicação", + "GameListContextMenuCreateShortcutToolTip": "Criar um atalho de área de trabalho que inicia o aplicativo selecionado", + "GameListContextMenuCreateShortcutToolTipMacOS": "Crie um atalho na pasta Aplicativos do macOS que abre o Aplicativo selecionado", + "GameListContextMenuOpenModsDirectory": "Abrir pasta de Mods", + "GameListContextMenuOpenModsDirectoryToolTip": "Abre a pasta que contém os mods da aplicação ", + "GameListContextMenuOpenSdModsDirectory": "Abrir diretório de mods Atmosphere", + "GameListContextMenuOpenSdModsDirectoryToolTip": "Opens the alternative SD card Atmosphere directory which contains Application's Mods. Useful for mods that are packaged for real hardware.", + "GameListContextMenuTrimXCI": "Check and Trim XCI File", + "GameListContextMenuTrimXCIToolTip": "Check and Trim XCI File to Save Disk Space", + "StatusBarGamesLoaded": "{0}/{1} jogos carregados", + "StatusBarSystemVersion": "Versão do firmware: {0}", + "StatusBarXCIFileTrimming": "Trimming XCI File '{0}'", + "LinuxVmMaxMapCountDialogTitle": "Limite baixo para mapeamentos de memória detectado", + "LinuxVmMaxMapCountDialogTextPrimary": "Você gostaria de aumentar o valor de vm.max_map_count para {0}", + "LinuxVmMaxMapCountDialogTextSecondary": "Alguns jogos podem tentar criar mais mapeamentos de memória do que o atualmente permitido. Ryujinx irá falhar assim que este limite for excedido.", + "LinuxVmMaxMapCountDialogButtonUntilRestart": "Sim, até a próxima reinicialização", + "LinuxVmMaxMapCountDialogButtonPersistent": "Sim, permanentemente", + "LinuxVmMaxMapCountWarningTextPrimary": "A quantidade máxima de mapeamentos de memória é menor que a recomendada.", + "LinuxVmMaxMapCountWarningTextSecondary": "O valor atual de vm.max_map_count ({0}) é menor que {1}. Alguns jogos podem tentar criar mais mapeamentos de memória do que o permitido no momento. Ryujinx vai falhar assim que este limite for excedido.\n\nTalvez você queira aumentar o limite manualmente ou instalar pkexec, o que permite que Ryujinx ajude com isso.", + "Settings": "Configurações", + "SettingsTabGeneral": "Geral", + "SettingsTabGeneralGeneral": "Geral", + "SettingsTabGeneralEnableDiscordRichPresence": "Habilitar Rich Presence do Discord", + "SettingsTabGeneralCheckUpdatesOnLaunch": "Verificar se há atualizações ao iniciar", + "SettingsTabGeneralShowConfirmExitDialog": "Exibir diálogo de confirmação ao sair", + "SettingsTabGeneralRememberWindowState": "Lembrar tamanho/posição da Janela", + "SettingsTabGeneralShowTitleBar": "Show Title Bar (Requires restart)", + "SettingsTabGeneralHideCursor": "Esconder o cursor do mouse:", + "SettingsTabGeneralHideCursorNever": "Nunca", + "SettingsTabGeneralHideCursorOnIdle": "Esconder o cursor quando ocioso", + "SettingsTabGeneralHideCursorAlways": "Sempre", + "SettingsTabGeneralGameDirectories": "Diretórios de jogo", + "SettingsTabGeneralAutoloadDirectories": "Carregar Automaticamente Diretórios de DLC/Atualizações", + "SettingsTabGeneralAutoloadNote": "DLCs e Atualizações que se referem a arquivos ausentes serão descarregadas automaticamente", + "SettingsTabGeneralAdd": "Adicionar", + "SettingsTabGeneralRemove": "Remover", + "SettingsTabSystem": "Sistema", + "SettingsTabSystemCore": "Principal", + "SettingsTabSystemSystemRegion": "Região do sistema:", + "SettingsTabSystemSystemRegionJapan": "Japão", + "SettingsTabSystemSystemRegionUSA": "EUA", + "SettingsTabSystemSystemRegionEurope": "Europa", + "SettingsTabSystemSystemRegionAustralia": "Austrália", + "SettingsTabSystemSystemRegionChina": "China", + "SettingsTabSystemSystemRegionKorea": "Coreia", + "SettingsTabSystemSystemRegionTaiwan": "Taiwan", + "SettingsTabSystemSystemLanguage": "Idioma do sistema:", + "SettingsTabSystemSystemLanguageJapanese": "Japonês", + "SettingsTabSystemSystemLanguageAmericanEnglish": "Inglês americano", + "SettingsTabSystemSystemLanguageFrench": "Francês", + "SettingsTabSystemSystemLanguageGerman": "Alemão", + "SettingsTabSystemSystemLanguageItalian": "Italiano", + "SettingsTabSystemSystemLanguageSpanish": "Espanhol", + "SettingsTabSystemSystemLanguageChinese": "Chinês", + "SettingsTabSystemSystemLanguageKorean": "Coreano", + "SettingsTabSystemSystemLanguageDutch": "Holandês", + "SettingsTabSystemSystemLanguagePortuguese": "Português", + "SettingsTabSystemSystemLanguageRussian": "Russo", + "SettingsTabSystemSystemLanguageTaiwanese": "Taiwanês", + "SettingsTabSystemSystemLanguageBritishEnglish": "Inglês britânico", + "SettingsTabSystemSystemLanguageCanadianFrench": "Francês canadense", + "SettingsTabSystemSystemLanguageLatinAmericanSpanish": "Espanhol latino", + "SettingsTabSystemSystemLanguageSimplifiedChinese": "Chinês simplificado", + "SettingsTabSystemSystemLanguageTraditionalChinese": "Chinês tradicional", + "SettingsTabSystemSystemTimeZone": "Fuso horário do sistema:", + "SettingsTabSystemSystemTime": "Hora do sistema:", + "SettingsTabSystemEnableVsync": "Habilitar sincronia vertical", + "SettingsTabSystemEnablePptc": "Habilitar PPTC (Profiled Persistent Translation Cache)", + "SettingsTabSystemEnableLowPowerPptc": "Low-power PPTC", + "SettingsTabSystemEnableFsIntegrityChecks": "Habilitar verificação de integridade do sistema de arquivos", + "SettingsTabSystemAudioBackend": "Biblioteca de saída de áudio:", + "SettingsTabSystemAudioBackendDummy": "Nenhuma", + "SettingsTabSystemAudioBackendOpenAL": "OpenAL", + "SettingsTabSystemAudioBackendSoundIO": "SoundIO", + "SettingsTabSystemAudioBackendSDL2": "SDL2", + "SettingsTabSystemHacks": "Hacks", + "SettingsTabSystemHacksNote": " (Pode causar instabilidade)", + "SettingsTabSystemDramSize": "Tamanho da DRAM:", + "SettingsTabSystemDramSize4GiB": "4GiB", + "SettingsTabSystemDramSize6GiB": "6GiB", + "SettingsTabSystemDramSize8GiB": "8GiB", + "SettingsTabSystemDramSize12GiB": "12GiB", + "SettingsTabSystemIgnoreMissingServices": "Ignorar serviços não implementados", + "SettingsTabSystemIgnoreApplet": "Ignorar applet", + "SettingsTabGraphics": "Gráficos", + "SettingsTabGraphicsAPI": "API gráfica", + "SettingsTabGraphicsEnableShaderCache": "Habilitar cache de shader", + "SettingsTabGraphicsAnisotropicFiltering": "Filtragem anisotrópica:", + "SettingsTabGraphicsAnisotropicFilteringAuto": "Automático", + "SettingsTabGraphicsAnisotropicFiltering2x": "2x", + "SettingsTabGraphicsAnisotropicFiltering4x": "4x", + "SettingsTabGraphicsAnisotropicFiltering8x": "8x", + "SettingsTabGraphicsAnisotropicFiltering16x": "16x", + "SettingsTabGraphicsResolutionScale": "Escala de resolução:", + "SettingsTabGraphicsResolutionScaleCustom": "Customizada (não recomendado)", + "SettingsTabGraphicsResolutionScaleNative": "Nativa (720p/1080p)", + "SettingsTabGraphicsResolutionScale2x": "2x (1440p/2160p)", + "SettingsTabGraphicsResolutionScale3x": "3x (2160p/3240p)", + "SettingsTabGraphicsResolutionScale4x": "4x (2880p/4320p) (não recomendado)", + "SettingsTabGraphicsAspectRatio": "Proporção:", + "SettingsTabGraphicsAspectRatio4x3": "4:3", + "SettingsTabGraphicsAspectRatio16x9": "16:9", + "SettingsTabGraphicsAspectRatio16x10": "16:10", + "SettingsTabGraphicsAspectRatio21x9": "21:9", + "SettingsTabGraphicsAspectRatio32x9": "32:9", + "SettingsTabGraphicsAspectRatioStretch": "Esticar até caber", + "SettingsTabGraphicsDeveloperOptions": "Opções do desenvolvedor", + "SettingsTabGraphicsShaderDumpPath": "Diretório para despejo de shaders:", + "SettingsTabLogging": "Log", + "SettingsTabLoggingLogging": "Log", + "SettingsTabLoggingEnableLoggingToFile": "Salvar logs em arquivo", + "SettingsTabLoggingEnableStubLogs": "Habilitar logs de stub", + "SettingsTabLoggingEnableInfoLogs": "Habilitar logs de informação", + "SettingsTabLoggingEnableWarningLogs": "Habilitar logs de alerta", + "SettingsTabLoggingEnableErrorLogs": "Habilitar logs de erro", + "SettingsTabLoggingEnableTraceLogs": "Habilitar logs de rastreamento", + "SettingsTabLoggingEnableGuestLogs": "Habilitar logs do programa convidado", + "SettingsTabLoggingEnableFsAccessLogs": "Habilitar logs de acesso ao sistema de arquivos", + "SettingsTabLoggingFsGlobalAccessLogMode": "Modo global de logs do sistema de arquivos:", + "SettingsTabLoggingDeveloperOptions": "Opções do desenvolvedor (AVISO: Vai reduzir a performance)", + "SettingsTabLoggingDeveloperOptionsNote": "AVISO: Reduzirá o desempenho", + "SettingsTabLoggingGraphicsBackendLogLevel": "Nível de log do backend gráfico:", + "SettingsTabLoggingGraphicsBackendLogLevelNone": "Nenhum", + "SettingsTabLoggingGraphicsBackendLogLevelError": "Erro", + "SettingsTabLoggingGraphicsBackendLogLevelPerformance": "Lentidão", + "SettingsTabLoggingGraphicsBackendLogLevelAll": "Todos", + "SettingsTabLoggingEnableDebugLogs": "Habilitar logs de depuração", + "SettingsTabInput": "Controle", + "SettingsTabInputEnableDockedMode": "Habilitar modo TV", + "SettingsTabInputDirectKeyboardAccess": "Acesso direto ao teclado", + "SettingsButtonSave": "Salvar", + "SettingsButtonClose": "Fechar", + "SettingsButtonOk": "OK", + "SettingsButtonCancel": "Cancelar", + "SettingsButtonApply": "Aplicar", + "ControllerSettingsPlayer": "Jogador", + "ControllerSettingsPlayer1": "Jogador 1", + "ControllerSettingsPlayer2": "Jogador 2", + "ControllerSettingsPlayer3": "Jogador 3", + "ControllerSettingsPlayer4": "Jogador 4", + "ControllerSettingsPlayer5": "Jogador 5", + "ControllerSettingsPlayer6": "Jogador 6", + "ControllerSettingsPlayer7": "Jogador 7", + "ControllerSettingsPlayer8": "Jogador 8", + "ControllerSettingsHandheld": "Portátil", + "ControllerSettingsInputDevice": "Dispositivo de entrada", + "ControllerSettingsRefresh": "Atualizar", + "ControllerSettingsDeviceDisabled": "Desabilitado", + "ControllerSettingsControllerType": "Tipo do controle", + "ControllerSettingsControllerTypeHandheld": "Portátil", + "ControllerSettingsControllerTypeProController": "Pro Controller", + "ControllerSettingsControllerTypeJoyConPair": "Par de JoyCon", + "ControllerSettingsControllerTypeJoyConLeft": "JoyCon esquerdo", + "ControllerSettingsControllerTypeJoyConRight": "JoyCon direito", + "ControllerSettingsProfile": "Perfil", + "ControllerSettingsProfileDefault": "Padrão", + "ControllerSettingsLoad": "Carregar", + "ControllerSettingsAdd": "Adicionar", + "ControllerSettingsRemove": "Remover", + "ControllerSettingsButtons": "Botões", + "ControllerSettingsButtonA": "A", + "ControllerSettingsButtonB": "B", + "ControllerSettingsButtonX": "X", + "ControllerSettingsButtonY": "Y", + "ControllerSettingsButtonPlus": "+", + "ControllerSettingsButtonMinus": "-", + "ControllerSettingsDPad": "Direcional", + "ControllerSettingsDPadUp": "Cima", + "ControllerSettingsDPadDown": "Baixo", + "ControllerSettingsDPadLeft": "Esquerda", + "ControllerSettingsDPadRight": "Direita", + "ControllerSettingsStickButton": "Botão", + "ControllerSettingsStickUp": "Cima", + "ControllerSettingsStickDown": "Baixo", + "ControllerSettingsStickLeft": "Esquerda", + "ControllerSettingsStickRight": "Direita", + "ControllerSettingsStickStick": "Analógico", + "ControllerSettingsStickInvertXAxis": "Inverter eixo X", + "ControllerSettingsStickInvertYAxis": "Inverter eixo Y", + "ControllerSettingsStickDeadzone": "Zona morta:", + "ControllerSettingsLStick": "Analógico esquerdo", + "ControllerSettingsRStick": "Analógico direito", + "ControllerSettingsTriggersLeft": "Gatilhos esquerda", + "ControllerSettingsTriggersRight": "Gatilhos direita", + "ControllerSettingsTriggersButtonsLeft": "Botões de gatilho esquerda", + "ControllerSettingsTriggersButtonsRight": "Botões de gatilho direita", + "ControllerSettingsTriggers": "Gatilhos", + "ControllerSettingsTriggerL": "L", + "ControllerSettingsTriggerR": "R", + "ControllerSettingsTriggerZL": "ZL", + "ControllerSettingsTriggerZR": "ZR", + "ControllerSettingsLeftSL": "SL", + "ControllerSettingsLeftSR": "SR", + "ControllerSettingsRightSL": "SL", + "ControllerSettingsRightSR": "SR", + "ControllerSettingsExtraButtonsLeft": "Botões esquerda", + "ControllerSettingsExtraButtonsRight": "Botões direita", + "ControllerSettingsMisc": "Miscelâneas", + "ControllerSettingsTriggerThreshold": "Sensibilidade do gatilho:", + "ControllerSettingsMotion": "Sensor de movimento", + "ControllerSettingsMotionUseCemuhookCompatibleMotion": "Usar sensor compatível com CemuHook", + "ControllerSettingsMotionControllerSlot": "Slot do controle:", + "ControllerSettingsMotionMirrorInput": "Espelhar movimento", + "ControllerSettingsMotionRightJoyConSlot": "Slot do JoyCon direito:", + "ControllerSettingsMotionServerHost": "Endereço do servidor:", + "ControllerSettingsMotionGyroSensitivity": "Sensibilidade do giroscópio:", + "ControllerSettingsMotionGyroDeadzone": "Zona morta do giroscópio:", + "ControllerSettingsSave": "Salvar", + "ControllerSettingsClose": "Fechar", + "KeyUnknown": "Unknown", + "KeyShiftLeft": "Shift Left", + "KeyShiftRight": "Shift Right", + "KeyControlLeft": "Ctrl Left", + "KeyMacControlLeft": "⌃ Left", + "KeyControlRight": "Ctrl Right", + "KeyMacControlRight": "⌃ Right", + "KeyAltLeft": "Alt Left", + "KeyMacAltLeft": "⌥ Left", + "KeyAltRight": "Alt Right", + "KeyMacAltRight": "⌥ Right", + "KeyWinLeft": "⊞ Left", + "KeyMacWinLeft": "⌘ Left", + "KeyWinRight": "⊞ Right", + "KeyMacWinRight": "⌘ Right", + "KeyMenu": "Menu", + "KeyUp": "Up", + "KeyDown": "Down", + "KeyLeft": "Left", + "KeyRight": "Right", + "KeyEnter": "Enter", + "KeyEscape": "Escape", + "KeySpace": "Space", + "KeyTab": "Tab", + "KeyBackSpace": "Backspace", + "KeyInsert": "Insert", + "KeyDelete": "Delete", + "KeyPageUp": "Page Up", + "KeyPageDown": "Page Down", + "KeyHome": "Home", + "KeyEnd": "End", + "KeyCapsLock": "Caps Lock", + "KeyScrollLock": "Scroll Lock", + "KeyPrintScreen": "Print Screen", + "KeyPause": "Pause", + "KeyNumLock": "Num Lock", + "KeyClear": "Clear", + "KeyKeypad0": "Keypad 0", + "KeyKeypad1": "Keypad 1", + "KeyKeypad2": "Keypad 2", + "KeyKeypad3": "Keypad 3", + "KeyKeypad4": "Keypad 4", + "KeyKeypad5": "Keypad 5", + "KeyKeypad6": "Keypad 6", + "KeyKeypad7": "Keypad 7", + "KeyKeypad8": "Keypad 8", + "KeyKeypad9": "Keypad 9", + "KeyKeypadDivide": "Keypad Divide", + "KeyKeypadMultiply": "Keypad Multiply", + "KeyKeypadSubtract": "Keypad Subtract", + "KeyKeypadAdd": "Keypad Add", + "KeyKeypadDecimal": "Keypad Decimal", + "KeyKeypadEnter": "Keypad Enter", + "KeyNumber0": "0", + "KeyNumber1": "1", + "KeyNumber2": "2", + "KeyNumber3": "3", + "KeyNumber4": "4", + "KeyNumber5": "5", + "KeyNumber6": "6", + "KeyNumber7": "7", + "KeyNumber8": "8", + "KeyNumber9": "9", + "KeyTilde": "~", + "KeyGrave": "`", + "KeyMinus": "-", + "KeyPlus": "+", + "KeyBracketLeft": "[", + "KeyBracketRight": "]", + "KeySemicolon": ";", + "KeyQuote": "\"", + "KeyComma": ",", + "KeyPeriod": ".", + "KeySlash": "/", + "KeyBackSlash": "\\", + "KeyUnbound": "Unbound", + "GamepadLeftStick": "L Stick Button", + "GamepadRightStick": "R Stick Button", + "GamepadLeftShoulder": "Left Shoulder", + "GamepadRightShoulder": "Right Shoulder", + "GamepadLeftTrigger": "Left Trigger", + "GamepadRightTrigger": "Right Trigger", + "GamepadDpadUp": "Up", + "GamepadDpadDown": "Down", + "GamepadDpadLeft": "Left", + "GamepadDpadRight": "Right", + "GamepadMinus": "-", + "GamepadPlus": "+", + "GamepadGuide": "Guide", + "GamepadMisc1": "Misc", + "GamepadPaddle1": "Paddle 1", + "GamepadPaddle2": "Paddle 2", + "GamepadPaddle3": "Paddle 3", + "GamepadPaddle4": "Paddle 4", + "GamepadTouchpad": "Touchpad", + "GamepadSingleLeftTrigger0": "Left Trigger 0", + "GamepadSingleRightTrigger0": "Right Trigger 0", + "GamepadSingleLeftTrigger1": "Left Trigger 1", + "GamepadSingleRightTrigger1": "Right Trigger 1", + "StickLeft": "Left Stick", + "StickRight": "Right Stick", + "UserProfilesSelectedUserProfile": "Perfil de usuário selecionado:", + "UserProfilesSaveProfileName": "Salvar nome de perfil", + "UserProfilesChangeProfileImage": "Mudar imagem de perfil", + "UserProfilesAvailableUserProfiles": "Perfis de usuário disponíveis:", + "UserProfilesAddNewProfile": "Adicionar novo perfil", + "UserProfilesDelete": "Apagar", + "UserProfilesClose": "Fechar", + "ProfileNameSelectionWatermark": "Escolha um apelido", + "ProfileImageSelectionTitle": "Seleção da imagem de perfil", + "ProfileImageSelectionHeader": "Escolha uma imagem de perfil", + "ProfileImageSelectionNote": "Você pode importar uma imagem customizada, ou selecionar um avatar do firmware", + "ProfileImageSelectionImportImage": "Importar arquivo de imagem", + "ProfileImageSelectionSelectAvatar": "Selecionar avatar do firmware", + "InputDialogTitle": "Diálogo de texto", + "InputDialogOk": "OK", + "InputDialogCancel": "Cancelar", + "InputDialogCancelling": "Cancelling", + "InputDialogClose": "Close", + "InputDialogAddNewProfileTitle": "Escolha o nome de perfil", + "InputDialogAddNewProfileHeader": "Escreva o nome do perfil", + "InputDialogAddNewProfileSubtext": "(Máximo de caracteres: {0})", + "AvatarChoose": "Escolher", + "AvatarSetBackgroundColor": "Definir cor de fundo", + "AvatarClose": "Fechar", + "ControllerSettingsLoadProfileToolTip": "Carregar perfil", + "ControllerSettingsViewProfileToolTip": "View Profile", + "ControllerSettingsAddProfileToolTip": "Adicionar perfil", + "ControllerSettingsRemoveProfileToolTip": "Remover perfil", + "ControllerSettingsSaveProfileToolTip": "Salvar perfil", + "MenuBarFileToolsTakeScreenshot": "Salvar captura de tela", + "MenuBarFileToolsHideUi": "Esconder Interface", + "GameListContextMenuRunApplication": "Executar Aplicativo", + "GameListContextMenuToggleFavorite": "Alternar favorito", + "GameListContextMenuToggleFavoriteToolTip": "Marca ou desmarca jogo como favorito", + "SettingsTabGeneralTheme": "Tema:", + "SettingsTabGeneralThemeDark": "Escuro", + "SettingsTabGeneralThemeAuto": "Automático", + "SettingsTabGeneralThemeLight": "Claro", + "ControllerSettingsConfigureGeneral": "Configurar", + "ControllerSettingsRumble": "Vibração", + "ControllerSettingsRumbleStrongMultiplier": "Multiplicador de vibração forte", + "ControllerSettingsRumbleWeakMultiplier": "Multiplicador de vibração fraca", + "DialogMessageSaveNotAvailableMessage": "Não há jogos salvos para {0} [{1:x16}]", + "DialogMessageSaveNotAvailableCreateSaveMessage": "Gostaria de criar o diretório de salvamento para esse jogo?", + "DialogConfirmationTitle": "Ryujinx - Confirmação", + "DialogUpdaterTitle": "Ryujinx - Atualizador", + "DialogErrorTitle": "Ryujinx - Erro", + "DialogWarningTitle": "Ryujinx - Alerta", + "DialogExitTitle": "Ryujinx - Sair", + "DialogErrorMessage": "Ryujinx encontrou um erro", + "DialogExitMessage": "Tem certeza que deseja fechar o Ryujinx?", + "DialogExitSubMessage": "Todos os dados que não foram salvos serão perdidos!", + "DialogMessageCreateSaveErrorMessage": "Ocorreu um erro ao criar o diretório de salvamento: {0}", + "DialogMessageFindSaveErrorMessage": "Ocorreu um erro ao tentar encontrar o diretório de salvamento: {0}", + "FolderDialogExtractTitle": "Escolha o diretório onde os arquivos serão extraídos", + "DialogNcaExtractionMessage": "Extraindo seção {0} de {1}...", + "DialogNcaExtractionTitle": "Extrator de seções NCA", + "DialogNcaExtractionMainNcaNotFoundErrorMessage": "Falha na extração. O NCA principal não foi encontrado no arquivo selecionado.", + "DialogNcaExtractionCheckLogErrorMessage": "Falha na extração. Leia o arquivo de log para mais informações.", + "DialogNcaExtractionSuccessMessage": "Extração concluída com êxito.", + "DialogUpdaterConvertFailedMessage": "Falha ao converter a versão atual do Ryujinx.", + "DialogUpdaterCancelUpdateMessage": "Cancelando atualização!", + "DialogUpdaterAlreadyOnLatestVersionMessage": "Você já está usando a versão mais recente do Ryujinx!", + "DialogUpdaterFailedToGetVersionMessage": "Ocorreu um erro ao tentar obter as informações de atualização do GitHub Release. Isso pode ser causado se uma nova versão estiver sendo compilado pelas Ações do GitHub. Tente novamente em alguns minutos.", + "DialogUpdaterConvertFailedGithubMessage": "Falha ao converter a versão do Ryujinx recebida do AppVeyor.", + "DialogUpdaterDownloadingMessage": "Baixando atualização...", + "DialogUpdaterExtractionMessage": "Extraindo atualização...", + "DialogUpdaterRenamingMessage": "Renomeando atualização...", + "DialogUpdaterAddingFilesMessage": "Adicionando nova atualização...", + "DialogUpdaterShowChangelogMessage": "Show Changelog", + "DialogUpdaterCompleteMessage": "Atualização concluída!", + "DialogUpdaterRestartMessage": "Deseja reiniciar o Ryujinx agora?", + "DialogUpdaterNoInternetMessage": "Você não está conectado à Internet!", + "DialogUpdaterNoInternetSubMessage": "Por favor, certifique-se de que você tem uma conexão funcional à Internet!", + "DialogUpdaterDirtyBuildMessage": "Você não pode atualizar uma compilação Dirty do Ryujinx!", + "DialogUpdaterDirtyBuildSubMessage": "Por favor, baixe o Ryujinx em https://ryujinx.app/download se está procurando por uma versão suportada.", + "DialogRestartRequiredMessage": "Reinicialização necessária", + "DialogThemeRestartMessage": "O tema foi salvo. Uma reinicialização é necessária para aplicar o tema.", + "DialogThemeRestartSubMessage": "Deseja reiniciar?", + "DialogFirmwareInstallEmbeddedMessage": "Gostaria de instalar o firmware incluso neste jogo? (Firmware {0})", + "DialogFirmwareInstallEmbeddedSuccessMessage": "Nenhum firmware instalado foi encontrado, mas o Ryujinx conseguiu instalar o firmware {0} a partir do jogo fornecido.\nO emulador será iniciado agora.", + "DialogFirmwareNoFirmwareInstalledMessage": "Firmware não foi instalado", + "DialogFirmwareInstalledMessage": "Firmware {0} foi instalado", + "DialogInstallFileTypesSuccessMessage": "Tipos de arquivo instalados com sucesso!", + "DialogInstallFileTypesErrorMessage": "Falha ao instalar tipos de arquivo.", + "DialogUninstallFileTypesSuccessMessage": "Tipos de arquivo desinstalados com sucesso!", + "DialogUninstallFileTypesErrorMessage": "Falha ao desinstalar tipos de arquivo.", + "DialogOpenSettingsWindowLabel": "Abrir janela de configurações", + "DialogOpenXCITrimmerWindowLabel": "XCI Trimmer Window", + "DialogControllerAppletTitle": "Applet de controle", + "DialogMessageDialogErrorExceptionMessage": "Erro ao exibir diálogo de mensagem: {0}", + "DialogSoftwareKeyboardErrorExceptionMessage": "Erro ao exibir teclado virtual: {0}", + "DialogErrorAppletErrorExceptionMessage": "Erro ao exibir applet ErrorApplet: {0}", + "DialogUserErrorDialogMessage": "{0}: {1}", + "DialogUserErrorDialogInfoMessage": "\nPara mais informações sobre como corrigir esse erro, siga nosso Guia de Configuração.", + "DialogUserErrorDialogTitle": "Erro do Ryujinx ({0})", + "DialogAmiiboApiTitle": "API Amiibo", + "DialogAmiiboApiFailFetchMessage": "Um erro ocorreu ao tentar obter informações da API.", + "DialogAmiiboApiConnectErrorMessage": "Não foi possível conectar ao servidor da API Amiibo. O serviço pode estar fora do ar ou você precisa verificar sua conexão com a Internet.", + "DialogProfileInvalidProfileErrorMessage": "Perfil {0} é incompatível com o sistema de configuração de controle atual.", + "DialogProfileDefaultProfileOverwriteErrorMessage": "O perfil Padrão não pode ser substituído", + "DialogProfileDeleteProfileTitle": "Apagando perfil", + "DialogProfileDeleteProfileMessage": "Essa ação é irreversível, tem certeza que deseja continuar?", + "DialogWarning": "Alerta", + "DialogPPTCDeletionMessage": "Você está prestes a apagar o cache PPTC para :\n\n{0}\n\nTem certeza que deseja continuar?", + "DialogPPTCDeletionErrorMessage": "Erro apagando cache PPTC em {0}: {1}", + "DialogShaderDeletionMessage": "Você está prestes a apagar o cache de Shader para :\n\n{0}\n\nTem certeza que deseja continuar?", + "DialogShaderDeletionErrorMessage": "Erro apagando o cache de Shader em {0}: {1}", + "DialogRyujinxErrorMessage": "Ryujinx encontrou um erro", + "DialogInvalidTitleIdErrorMessage": "Erro de interface: O jogo selecionado não tem um ID de título válido", + "DialogFirmwareInstallerFirmwareNotFoundErrorMessage": "Um firmware de sistema válido não foi encontrado em {0}.", + "DialogFirmwareInstallerFirmwareInstallTitle": "Instalar firmware {0}", + "DialogFirmwareInstallerFirmwareInstallMessage": "A versão do sistema {0} será instalada.", + "DialogFirmwareInstallerFirmwareInstallSubMessage": "\n\nIsso substituirá a versão do sistema atual {0}.", + "DialogFirmwareInstallerFirmwareInstallConfirmMessage": "\n\nDeseja continuar?", + "DialogFirmwareInstallerFirmwareInstallWaitMessage": "Instalando firmware...", + "DialogFirmwareInstallerFirmwareInstallSuccessMessage": "Versão do sistema {0} instalada com sucesso.", + "DialogUserProfileDeletionWarningMessage": "Não haveria nenhum perfil selecionado se o perfil atual fosse deletado", + "DialogUserProfileDeletionConfirmMessage": "Deseja deletar o perfil selecionado", + "DialogUserProfileUnsavedChangesTitle": "Alerta - Alterações não salvas", + "DialogUserProfileUnsavedChangesMessage": "Você fez alterações para este perfil de usuário que não foram salvas.", + "DialogUserProfileUnsavedChangesSubMessage": "Deseja descartar as alterações?", + "DialogControllerSettingsModifiedConfirmMessage": "As configurações de controle atuais foram atualizadas.", + "DialogControllerSettingsModifiedConfirmSubMessage": "Deseja salvar?", + "DialogLoadFileErrorMessage": "{0}. Arquivo com erro: {1}", + "DialogModAlreadyExistsMessage": "O mod já existe", + "DialogModInvalidMessage": "O diretório especificado não contém um mod!", + "DialogModDeleteNoParentMessage": "Falha ao excluir: Não foi possível encontrar o diretório pai do mod \"{0}\"!", + "DialogDlcNoDlcErrorMessage": "O arquivo especificado não contém DLCs para o título selecionado!", + "DialogPerformanceCheckLoggingEnabledMessage": "Os logs de depuração estão ativos, esse recurso é feito para ser usado apenas por desenvolvedores.", + "DialogPerformanceCheckLoggingEnabledConfirmMessage": "Para melhor performance, é recomendável desabilitar os logs de depuração. Gostaria de desabilitar os logs de depuração agora?", + "DialogPerformanceCheckShaderDumpEnabledMessage": "O despejo de shaders está ativo, esse recurso é feito para ser usado apenas por desenvolvedores.", + "DialogPerformanceCheckShaderDumpEnabledConfirmMessage": "Para melhor performance, é recomendável desabilitar o despejo de shaders. Gostaria de desabilitar o despejo de shaders agora?", + "DialogLoadAppGameAlreadyLoadedMessage": "Um jogo já foi carregado", + "DialogLoadAppGameAlreadyLoadedSubMessage": "Por favor, pare a emulação ou feche o emulador antes de abrir outro jogo.", + "DialogUpdateAddUpdateErrorMessage": "O arquivo especificado não contém atualizações para o título selecionado!", + "DialogSettingsBackendThreadingWarningTitle": "Alerta - Threading da API gráfica", + "DialogSettingsBackendThreadingWarningMessage": "Ryujinx precisa ser reiniciado após mudar essa opção para que ela tenha efeito. Dependendo da sua plataforma, pode ser preciso desabilitar o multithreading do driver de vídeo quando usar o Ryujinx.", + "DialogModManagerDeletionWarningMessage": "Você está prestes a excluir o mod: {0}\n\nTem certeza de que deseja continuar?", + "DialogModManagerDeletionAllWarningMessage": "Você está prestes a excluir todos os mods para este jogo.\n\nTem certeza de que deseja continuar?", + "SettingsTabGraphicsFeaturesOptions": "Recursos", + "SettingsTabGraphicsBackendMultithreading": "Multithreading da API gráfica:", + "CommonAuto": "Automático", + "CommonOff": "Desligado", + "CommonOn": "Ligado", + "InputDialogYes": "Sim", + "InputDialogNo": "Não", + "DialogProfileInvalidProfileNameErrorMessage": "O nome do arquivo contém caracteres inválidos. Por favor, tente novamente.", + "MenuBarOptionsPauseEmulation": "Pausar", + "MenuBarOptionsResumeEmulation": "Resumir", + "AboutUrlTooltipMessage": "Clique para abrir o site do Ryujinx no seu navegador padrão.", + "AboutDisclaimerMessage": "Ryujinx não é afiliado com a Nintendo™,\nou qualquer um de seus parceiros, de nenhum modo.", + "AboutAmiiboDisclaimerMessage": "AmiiboAPI (www.amiiboapi.com) é usado\nem nossa emulação de Amiibo.", + "AboutPatreonUrlTooltipMessage": "Clique para abrir a página do Patreon do Ryujinx no seu navegador padrão.", + "AboutGithubUrlTooltipMessage": "Clique para abrir a página do GitHub do Ryujinx no seu navegador padrão.", + "AboutDiscordUrlTooltipMessage": "Clique para abrir um convite ao servidor do Discord do Ryujinx no seu navegador padrão.", + "AboutTwitterUrlTooltipMessage": "Clique para abrir a página do Twitter do Ryujinx no seu navegador padrão.", + "AboutRyujinxAboutTitle": "Sobre:", + "AboutRyujinxAboutContent": "Ryujinx é um emulador de Nintendo Switch™.\nPor favor, nos dê apoio no Patreon.\nFique por dentro de todas as novidades no Twitter ou Discord.\nDesenvolvedores com interesse em contribuir podem conseguir mais informações no GitHub ou Discord.", + "AboutRyujinxMaintainersTitle": "Mantido por:", + "AboutRyujinxMaintainersContentTooltipMessage": "Clique para abrir a página de contribuidores no seu navegador padrão.", + "AboutRyujinxSupprtersTitle": "Apoiado no Patreon por:", + "AmiiboSeriesLabel": "Franquia Amiibo", + "AmiiboCharacterLabel": "Personagem", + "AmiiboScanButtonLabel": "Escanear", + "AmiiboOptionsShowAllLabel": "Exibir todos os Amiibos", + "AmiiboOptionsUsRandomTagLabel": "Hack: Usar Uuid de tag aleatório", + "DlcManagerTableHeadingEnabledLabel": "Habilitado", + "DlcManagerTableHeadingTitleIdLabel": "ID do título", + "DlcManagerTableHeadingContainerPathLabel": "Caminho do container", + "DlcManagerTableHeadingFullPathLabel": "Caminho completo", + "DlcManagerRemoveAllButton": "Remover todos", + "DlcManagerEnableAllButton": "Habilitar todos", + "DlcManagerDisableAllButton": "Desabilitar todos", + "ModManagerDeleteAllButton": "Apagar Tudo", + "MenuBarOptionsChangeLanguage": "Mudar idioma", + "MenuBarShowFileTypes": "Mostrar tipos de arquivo", + "CommonSort": "Ordenar", + "CommonShowNames": "Exibir nomes", + "CommonFavorite": "Favorito", + "OrderAscending": "Ascendente", + "OrderDescending": "Descendente", + "SettingsTabGraphicsFeatures": "Recursos & Melhorias", + "ErrorWindowTitle": "Janela de erro", + "ToggleDiscordTooltip": "Habilita ou desabilita Discord Rich Presence", + "AddGameDirBoxTooltip": "Escreva um diretório de jogo para adicionar à lista", + "AddGameDirTooltip": "Adicionar um diretório de jogo à lista", + "RemoveGameDirTooltip": "Remover diretório de jogo selecionado", + "AddAutoloadDirBoxTooltip": "Insira um diretório de carregamento automático para adicionar à lista", + "AddAutoloadDirTooltip": "Adicionar um diretório de carregamento automático à lista", + "RemoveAutoloadDirTooltip": "Remover o diretório de carregamento automático selecionado", + "CustomThemeCheckTooltip": "Habilita ou desabilita temas customizados na interface gráfica", + "CustomThemePathTooltip": "Diretório do tema customizado", + "CustomThemeBrowseTooltip": "Navegar até um tema customizado", + "DockModeToggleTooltip": "O modo TV faz o sistema emulado se comportar como um Nintendo Switch na TV, o que melhora a fidelidade gráfica na maioria dos jogos. Por outro lado, desativar essa opção fará o sistema emulado se comportar como um Nintendo Switch portátil, reduzindo a qualidade gráfica.\n\nConfigure os controles do jogador 1 se planeja usar o modo TV; configure os controles de portátil se planeja usar o modo Portátil.\n\nMantenha ativado se estiver em dúvida.", + "DirectKeyboardTooltip": "Suporte para acesso direto ao teclado (HID). Permite que os jogos acessem seu teclado como um dispositivo de entrada de texto.\n\nFunciona apenas com jogos que suportam o uso de teclado nativamente no hardware do Switch.\n\nDeixe desativado se estiver em dúvida.", + "DirectMouseTooltip": "Suporte para acesso direto ao mouse (HID). Permite que os jogos acessem seu mouse como um dispositivo de apontamento.\n\nFunciona apenas com jogos que suportam controles de mouse nativamente no hardware do Switch, o que é raro.\n\nQuando ativado, a funcionalidade de tela sensível ao toque pode não funcionar.\n\nDeixe desativado se estiver em dúvida.", + "RegionTooltip": "Mudar a região do sistema", + "LanguageTooltip": "Mudar o idioma do sistema", + "TimezoneTooltip": "Mudar o fuso-horário do sistema", + "TimeTooltip": "Mudar a hora do sistema", + "VSyncToggleTooltip": "V-Sync do console emulado. Funciona essencialmente como um limitador de quadros para a maioria dos jogos; desativá-lo pode fazer com que os jogos rodem em uma velocidade mais alta ou que telas de carregamento demorem mais ou travem.\n\nPode ser alternado durante o jogo com uma tecla de atalho de sua preferência (F1 por padrão). Recomendamos isso caso planeje desativá-lo.\n\nMantenha ligado se estiver em dúvida.", + "PptcToggleTooltip": "Habilita ou desabilita PPTC", + "LowPowerPptcToggleTooltip": "Carregar o PPTC usando um terço da quantidade de núcleos.", + "FsIntegrityToggleTooltip": "Habilita ou desabilita verificação de integridade dos arquivos do jogo", + "AudioBackendTooltip": "Mudar biblioteca de áudio", + "MemoryManagerTooltip": "Muda como a memória do sistema convidado é acessada. Tem um grande impacto na performance da CPU emulada.", + "MemoryManagerSoftwareTooltip": "Usar uma tabela de página via software para tradução de endereços. Maior precisão, porém performance mais baixa.", + "MemoryManagerHostTooltip": "Mapeia memória no espaço de endereço hóspede diretamente. Compilação e execução do JIT muito mais rápida.", + "MemoryManagerUnsafeTooltip": "Mapeia memória diretamente, mas sem limitar o acesso ao espaço de endereçamento do sistema convidado. Mais rápido, porém menos seguro. O aplicativo convidado pode acessar memória de qualquer parte do Ryujinx, então apenas rode programas em que você confia nesse modo.", + "UseHypervisorTooltip": "Usa o Hypervisor em vez de JIT (recompilador dinâmico). Melhora significativamente o desempenho quando disponível, mas pode ser instável no seu estado atual.", + "DRamTooltip": "Expande a memória do sistema emulado de 4GiB para 6GiB", + "IgnoreMissingServicesTooltip": "Habilita ou desabilita a opção de ignorar serviços não implementados", + "IgnoreAppletTooltip": "O diálogo externo \"Controller Applet\" não aparecerá se o gamepad for desconectado durante o jogo. Não haverá prompt para fechar o diálogo ou configurar um novo controle. Assim que o controle desconectado anteriormente for reconectado, o jogo será retomado automaticamente.", + "GraphicsBackendThreadingTooltip": "Habilita multithreading do backend gráfico", + "GalThreadingTooltip": "Executa comandos do backend gráfico em uma segunda thread. Permite multithreading em tempo de execução da compilação de shader, diminui os travamentos, e melhora performance em drivers sem suporte embutido a multithreading. Pequena variação na performance máxima em drivers com suporte a multithreading. Ryujinx pode precisar ser reiniciado para desabilitar adequadamente o multithreading embutido do driver, ou você pode precisar fazer isso manualmente para ter a melhor performance.", + "ShaderCacheToggleTooltip": "Habilita ou desabilita o cache de shader", + "ResolutionScaleTooltip": "Multiplica a resolução de renderização do jogo.\n\nAlguns jogos podem não funcionar bem com essa opção e apresentar uma aparência pixelada, mesmo com o aumento da resolução; para esses jogos, talvez seja necessário encontrar mods que removam o anti-aliasing ou aumentem a resolução de renderização interna. Ao usar a segunda opção, provavelmente desejará selecionar Nativa.\n\nEssa opção pode ser alterada enquanto um jogo está em execução, clicando em \"Aplicar\" abaixo; basta mover a janela de configurações para o lado e experimentar até encontrar o visual preferido para o jogo.\n\nLembre-se de que 4x é exagerado para praticamente qualquer configuração.", + "ResolutionScaleEntryTooltip": "Escala de resolução de ponto flutuante, como 1.5. Valores não inteiros tem probabilidade maior de causar problemas ou quebras.", + "AnisotropyTooltip": "Nível de Filtragem Anisotrópica. Defina como Automático para usar o valor solicitado pelo jogo.", + "AspectRatioTooltip": "Proporção de Tela aplicada à janela do renderizador.\n\nAltere isso apenas se estiver usando um mod de proporção para o seu jogo; caso contrário, os gráficos ficarão esticados.\n\nMantenha em 16:9 se estiver em dúvida.", + "ShaderDumpPathTooltip": "Diretòrio de despejo de shaders", + "FileLogTooltip": "Habilita ou desabilita log para um arquivo no disco", + "StubLogTooltip": "Habilita ou desabilita exibição de mensagens de stub", + "InfoLogTooltip": "Habilita ou desabilita exibição de mensagens informativas", + "WarnLogTooltip": "Habilita ou desabilita exibição de mensagens de alerta", + "ErrorLogTooltip": "Habilita ou desabilita exibição de mensagens de erro", + "TraceLogTooltip": "Habilita ou desabilita exibição de mensagens de rastreamento", + "GuestLogTooltip": "Habilita ou desabilita exibição de mensagens do programa convidado", + "FileAccessLogTooltip": "Habilita ou desabilita exibição de mensagens do acesso de arquivos", + "FSAccessLogModeTooltip": "Habilita exibição de mensagens de acesso ao sistema de arquivos no console. Modos permitidos são 0-3", + "DeveloperOptionTooltip": "Use com cuidado", + "OpenGlLogLevel": "Requer que os níveis de log apropriados estejaam habilitados", + "DebugLogTooltip": "Habilita exibição de mensagens de depuração", + "LoadApplicationFileTooltip": "Abre o navegador de arquivos para seleção de um arquivo do Switch compatível a ser carregado", + "LoadApplicationFolderTooltip": "Abre o navegador de pastas para seleção de pasta extraída do Switch compatível a ser carregada", + "OpenRyujinxFolderTooltip": "Abre o diretório do sistema de arquivos do Ryujinx", + "LoadTitleUpdatesFromFolderTooltip": "Abra o explorador de arquivos para selecionar uma ou mais pastas e carregar atualizações de jogo em massa.", + "OpenRyujinxLogsTooltip": "Abre o diretório onde os logs são salvos", + "ExitTooltip": "Sair do Ryujinx", + "OpenSettingsTooltip": "Abrir janela de configurações", + "OpenProfileManagerTooltip": "Abrir janela de gerenciamento de perfis", + "StopEmulationTooltip": "Parar emulação do jogo atual e voltar a seleção de jogos", + "CheckUpdatesTooltip": "Verificar por atualizações para o Ryujinx", + "OpenAboutTooltip": "Abrir janela sobre", + "GridSize": "Tamanho da grade", + "GridSizeTooltip": "Mudar tamanho dos items da grade", + "SettingsTabSystemSystemLanguageBrazilianPortuguese": "Português do Brasil", + "AboutRyujinxContributorsButtonHeader": "Ver todos os contribuidores", + "SettingsTabSystemAudioVolume": "Volume:", + "AudioVolumeTooltip": "Mudar volume do áudio", + "SettingsTabSystemEnableInternetAccess": "Habilitar acesso à internet do programa convidado", + "EnableInternetAccessTooltip": "Habilita acesso à internet do programa convidado. Se habilitado, o aplicativo vai se comportar como se o sistema Switch emulado estivesse conectado a Internet. Note que em alguns casos, aplicativos podem acessar a Internet mesmo com essa opção desabilitada", + "GameListContextMenuManageCheatToolTip": "Gerenciar Cheats", + "GameListContextMenuManageCheat": "Gerenciar Cheats", + "GameListContextMenuManageModToolTip": "Gerenciar Mods", + "GameListContextMenuManageMod": "Gerenciar Mods", + "ControllerSettingsStickRange": "Intervalo:", + "DialogStopEmulationTitle": "Ryujinx - Parar emulação", + "DialogStopEmulationMessage": "Tem certeza que deseja parar a emulação?", + "SettingsTabCpu": "CPU", + "SettingsTabAudio": "Áudio", + "SettingsTabNetwork": "Rede", + "SettingsTabNetworkConnection": "Conexão de rede", + "SettingsTabCpuCache": "Cache da CPU", + "SettingsTabCpuMemory": "Memória da CPU", + "DialogUpdaterFlatpakNotSupportedMessage": "Por favor, atualize o Ryujinx pelo FlatHub.", + "UpdaterDisabledWarningTitle": "Atualizador desabilitado!", + "ControllerSettingsRotate90": "Rodar 90° sentido horário", + "IconSize": "Tamanho do ícone", + "IconSizeTooltip": "Muda o tamanho do ícone do jogo", + "MenuBarOptionsShowConsole": "Exibir console", + "ShaderCachePurgeError": "Erro ao deletar o shader em {0}: {1}", + "UserErrorNoKeys": "Chaves não encontradas", + "UserErrorNoFirmware": "Firmware não encontrado", + "UserErrorFirmwareParsingFailed": "Erro na leitura do Firmware", + "UserErrorApplicationNotFound": "Aplicativo não encontrado", + "UserErrorUnknown": "Erro desconhecido", + "UserErrorUndefined": "Erro indefinido", + "UserErrorNoKeysDescription": "Ryujinx não conseguiu encontrar o seu arquivo 'prod.keys'", + "UserErrorNoFirmwareDescription": "Ryujinx não conseguiu encontrar nenhum Firmware instalado", + "UserErrorFirmwareParsingFailedDescription": "Ryujinx não conseguiu ler o Firmware fornecido. Geralmente isso é causado por chaves desatualizadas.", + "UserErrorApplicationNotFoundDescription": "Ryujinx não conseguiu encontrar um aplicativo válido no caminho fornecido.", + "UserErrorUnknownDescription": "Um erro desconhecido foi encontrado!", + "UserErrorUndefinedDescription": "Um erro indefinido occoreu! Isso não deveria acontecer, por favor contate um desenvolvedor!", + "OpenSetupGuideMessage": "Abrir o guia de configuração", + "NoUpdate": "Sem atualizações", + "TitleUpdateVersionLabel": "Versão {0}", + "TitleBundledUpdateVersionLabel": "Empacotado: Versão {0}", + "TitleBundledDlcLabel": "Empacotado:", + "TitleXCIStatusPartialLabel": "Partial", + "TitleXCIStatusTrimmableLabel": "Untrimmed", + "TitleXCIStatusUntrimmableLabel": "Trimmed", + "TitleXCIStatusFailedLabel": "(Failed)", + "TitleXCICanSaveLabel": "Save {0:n0} Mb", + "TitleXCISavingLabel": "Saved {0:n0} Mb", + "RyujinxInfo": "Ryujinx - Informação", + "RyujinxConfirm": "Ryujinx - Confirmação", + "FileDialogAllTypes": "Todos os tipos", + "Never": "Nunca", + "SwkbdMinCharacters": "Deve ter pelo menos {0} caracteres", + "SwkbdMinRangeCharacters": "Deve ter entre {0}-{1} caracteres", + "SoftwareKeyboard": "Teclado por Software", + "SoftwareKeyboardModeNumeric": "Deve ser somente 0-9 ou '.'", + "SoftwareKeyboardModeAlphabet": "Apenas devem ser caracteres não CJK.", + "SoftwareKeyboardModeASCII": "Deve ser apenas texto ASCII", + "ControllerAppletControllers": "Supported Controllers:", + "ControllerAppletPlayers": "Jogadores:", + "ControllerAppletDescription": "Your current configuration is invalid. Open settings and reconfigure your inputs.", + "ControllerAppletDocked": "Docked mode set. Handheld control should be disabled.", + "UpdaterRenaming": "Renomeando arquivos antigos...", + "UpdaterRenameFailed": "O atualizador não conseguiu renomear o arquivo: {0}", + "UpdaterAddingFiles": "Adicionando novos arquivos...", + "UpdaterExtracting": "Extraíndo atualização...", + "UpdaterDownloading": "Baixando atualização...", + "Game": "Jogo", + "Docked": "TV", + "Handheld": "Portátil", + "ConnectionError": "Erro de conexão.", + "AboutPageDeveloperListMore": "{0} e mais...", + "ApiError": "Erro de API.", + "LoadingHeading": "Carregando {0}", + "CompilingPPTC": "Compilando PTC", + "CompilingShaders": "Compilando Shaders", + "AllKeyboards": "Todos os teclados", + "OpenFileDialogTitle": "Selecione um arquivo suportado para abrir", + "OpenFolderDialogTitle": "Selecione um diretório com um jogo extraído", + "AllSupportedFormats": "Todos os formatos suportados", + "RyujinxUpdater": "Atualizador do Ryujinx", + "SettingsTabHotkeys": "Atalhos do teclado", + "SettingsTabHotkeysHotkeys": "Atalhos do teclado", + "SettingsTabHotkeysToggleVsyncHotkey": "Mudar VSync:", + "SettingsTabHotkeysScreenshotHotkey": "Captura de tela:", + "SettingsTabHotkeysShowUiHotkey": "Exibir UI:", + "SettingsTabHotkeysPauseHotkey": "Pausar:", + "SettingsTabHotkeysToggleMuteHotkey": "Mudo:", + "ControllerMotionTitle": "Configurações do controle de movimento", + "ControllerRumbleTitle": "Configurações de vibração", + "SettingsSelectThemeFileDialogTitle": "Selecionar arquivo do tema", + "SettingsXamlThemeFile": "Arquivo de tema Xaml", + "AvatarWindowTitle": "Gerenciar contas - Avatar", + "Amiibo": "Amiibo", + "Unknown": "Desconhecido", + "Usage": "Uso", + "Writable": "Gravável", + "SelectDlcDialogTitle": "Selecionar arquivos de DLC", + "SelectUpdateDialogTitle": "Selecionar arquivos de atualização", + "SelectModDialogTitle": "Select mod directory", + "UserProfileWindowTitle": "Gerenciador de perfis de usuário", + "TrimXCIFileDialogTitle": "Check and Trim XCI File", + "TrimXCIFileDialogPrimaryText": "This function will first check the empty space and then trim the XCI File to save disk space.", + "TrimXCIFileDialogSecondaryText": "Current File Size: {0:n} MB\nGame Data Size: {1:n} MB\nDisk Space Savings: {2:n} MB", + "TrimXCIFileNoTrimNecessary": "XCI File does not need to be trimmed. Check logs for further details", + "TrimXCIFileNoUntrimPossible": "XCI File cannot be untrimmed. Check logs for further details", + "TrimXCIFileReadOnlyFileCannotFix": "XCI File is Read Only and could not be made writable. Check logs for further details", + "TrimXCIFileFileSizeChanged": "XCI File has changed in size since it was scanned. Please check the file is not being written to and try again.", + "TrimXCIFileFreeSpaceCheckFailed": "XCI File has data in the free space area, it is not safe to trim", + "TrimXCIFileInvalidXCIFile": "XCI File contains invalid data. Check logs for further details", + "TrimXCIFileFileIOWriteError": "XCI File could not be opened for writing. Check logs for further details", + "TrimXCIFileFailedPrimaryText": "Trimming of the XCI file failed", + "TrimXCIFileCancelled": "The operation was cancelled", + "TrimXCIFileFileUndertermined": "No operation was performed", + "CheatWindowTitle": "Gerenciador de Cheats", + "DlcWindowTitle": "Gerenciador de DLC", + "ModWindowTitle": "Gerenciar Mods para {0} ({1})", + "UpdateWindowTitle": "Gerenciador de atualizações", + "XCITrimmerWindowTitle": "XCI File Trimmer", + "XCITrimmerTitleStatusCount": "{0} of {1} Title(s) Selected", + "XCITrimmerTitleStatusCountWithFilter": "{0} of {1} Title(s) Selected ({2} displayed)", + "XCITrimmerTitleStatusTrimming": "Trimming {0} Title(s)...", + "XCITrimmerTitleStatusUntrimming": "Untrimming {0} Title(s)...", + "XCITrimmerTitleStatusFailed": "Failed", + "XCITrimmerPotentialSavings": "Potential Savings", + "XCITrimmerActualSavings": "Actual Savings", + "XCITrimmerSavingsMb": "{0:n0} Mb", + "XCITrimmerSelectDisplayed": "Select Shown", + "XCITrimmerDeselectDisplayed": "Deselect Shown", + "XCITrimmerSortName": "Title", + "XCITrimmerSortSaved": "Space Savings", + "XCITrimmerTrim": "Trim", + "XCITrimmerUntrim": "Untrim", + "UpdateWindowUpdateAddedMessage": "{0} nova(s) atualização(ões) adicionada(s)", + "UpdateWindowBundledContentNotice": "Atualizações incorporadas não podem ser removidas, apenas desativadas.", + "CheatWindowHeading": "Cheats disponíveis para {0} [{1}]", + "BuildId": "ID da Build:", + "DlcWindowBundledContentNotice": "DLCs incorporadas não podem ser removidas, apenas desativadas.", + "DlcWindowHeading": "{0} DLCs disponíveis para {1} ({2})", + "DlcWindowDlcAddedMessage": "{0} novo(s) conteúdo(s) para download adicionado(s)", + "AutoloadDlcAddedMessage": "{0} novo(s) conteúdo(s) para download adicionado(s)", + "AutoloadDlcRemovedMessage": "{0} conteúdo(s) para download ausente(s) removido(s)", + "AutoloadUpdateAddedMessage": "{0} nova(s) atualização(ões) adicionada(s)", + "AutoloadUpdateRemovedMessage": "{0} atualização(ões) ausente(s) removida(s)", + "ModWindowHeading": "{0} Mod(s)", + "UserProfilesEditProfile": "Editar selecionado", + "Continue": "Continue", + "Cancel": "Cancelar", + "Save": "Salvar", + "Discard": "Descartar", + "Paused": "Paused", + "UserProfilesSetProfileImage": "Definir imagem de perfil", + "UserProfileEmptyNameError": "É necessário um nome", + "UserProfileNoImageError": "A imagem de perfil deve ser definida", + "GameUpdateWindowHeading": "{0} atualizações disponíveis para {1} ({2})", + "SettingsTabHotkeysResScaleUpHotkey": "Aumentar a resolução:", + "SettingsTabHotkeysResScaleDownHotkey": "Diminuir a resolução:", + "UserProfilesName": "Nome:", + "UserProfilesUserId": "ID de usuário:", + "SettingsTabGraphicsBackend": "Backend gráfico", + "SettingsTabGraphicsBackendTooltip": "Select the graphics backend that will be used in the emulator.\n\nVulkan is overall better for all modern graphics cards, as long as their drivers are up to date. Vulkan also features faster shader compilation (less stuttering) on all GPU vendors.\n\nOpenGL may achieve better results on old Nvidia GPUs, on old AMD GPUs on Linux, or on GPUs with lower VRAM, though shader compilation stutters will be greater.\n\nSet to Vulkan if unsure. Set to OpenGL if your GPU does not support Vulkan even with the latest graphics drivers.", + "SettingsEnableTextureRecompression": "Habilitar recompressão de texturas", + "SettingsEnableTextureRecompressionTooltip": "Compresses ASTC textures in order to reduce VRAM usage.\n\nGames using this texture format include Astral Chain, Bayonetta 3, Fire Emblem Engage, Metroid Prime Remastered, Super Mario Bros. Wonder and The Legend of Zelda: Tears of the Kingdom.\n\nGraphics cards with 4GiB VRAM or less will likely crash at some point while running these games.\n\nEnable only if you're running out of VRAM on the aforementioned games. Leave OFF if unsure.", + "SettingsTabGraphicsPreferredGpu": "GPU preferencial", + "SettingsTabGraphicsPreferredGpuTooltip": "Selecione a placa de vídeo que será usada com o backend gráfico Vulkan.\n\nNão afeta a GPU que OpenGL usará.\n\nSelecione \"dGPU\" em caso de dúvida. Se não houver nenhuma, não mexa.", + "SettingsAppRequiredRestartMessage": "Reinicialização do Ryujinx necessária", + "SettingsGpuBackendRestartMessage": "Configurações do backend gráfico ou da GPU foram alteradas. Uma reinicialização é necessária para que as mudanças tenham efeito.", + "SettingsGpuBackendRestartSubMessage": "Deseja reiniciar agora?", + "RyujinxUpdaterMessage": "Você quer atualizar o Ryujinx para a última versão?", + "SettingsTabHotkeysVolumeUpHotkey": "Aumentar volume:", + "SettingsTabHotkeysVolumeDownHotkey": "Diminuir volume:", + "SettingsEnableMacroHLE": "Habilitar emulação de alto nível para Macros", + "SettingsEnableMacroHLETooltip": "Habilita emulação de alto nível de códigos Macro da GPU.\n\nMelhora a performance, mas pode causar problemas gráficos em alguns jogos.\n\nEm caso de dúvida, deixe ATIVADO.", + "SettingsEnableColorSpacePassthrough": "Passagem de Espaço Cor", + "SettingsEnableColorSpacePassthroughTooltip": "Direciona o backend Vulkan para passar informações de cores sem especificar um espaço de cores. Para usuários com telas de ampla gama, isso pode resultar em cores mais vibrantes, ao custo da correção de cores.", + "VolumeShort": "Vol", + "UserProfilesManageSaves": "Gerenciar jogos salvos", + "DeleteUserSave": "Deseja apagar o jogo salvo do usuário para este jogo?", + "IrreversibleActionNote": "Esta ação não é reversível.", + "SaveManagerHeading": "Gerenciar jogos salvos para {0}", + "SaveManagerTitle": "Gerenciador de jogos salvos", + "Name": "Nome", + "Size": "Tamanho", + "Search": "Buscar", + "UserProfilesRecoverLostAccounts": "Recuperar contas perdidas", + "Recover": "Recuperar", + "UserProfilesRecoverHeading": "Jogos salvos foram encontrados para as seguintes contas", + "UserProfilesRecoverEmptyList": "Nenhum perfil para recuperar", + "GraphicsAATooltip": "Aplica anti-aliasing à renderização do jogo.\n\nFXAA borrará a maior parte da imagem, enquanto SMAA tentará identificar e suavizar bordas serrilhadas.\n\nNão é recomendado usar em conjunto com o filtro de escala FSR.\n\nEssa opção pode ser alterada enquanto o jogo está em execução clicando em \"Aplicar\" abaixo; basta mover a janela de configurações para o lado e experimentar até encontrar o visual preferido para o jogo.\n\nDeixe em NENHUM se estiver em dúvida.", + "GraphicsAALabel": "Anti-serrilhado:", + "GraphicsScalingFilterLabel": "Filtro de escala:", + "GraphicsScalingFilterTooltip": "Escolha o filtro de escala que será aplicado ao usar a escala de resolução.\n\nBilinear funciona bem para jogos 3D e é uma opção padrão segura.\n\nNearest é recomendado para jogos em pixel art.\n\nFSR 1.0 é apenas um filtro de nitidez, não recomendado para uso com FXAA ou SMAA.\n\nEssa opção pode ser alterada enquanto o jogo está em execução, clicando em \"Aplicar\" abaixo; basta mover a janela de configurações para o lado e experimentar até encontrar o visual preferido para o jogo.\n\nMantenha em BILINEAR se estiver em dúvida.", + "GraphicsScalingFilterBilinear": "Bilinear", + "GraphicsScalingFilterNearest": "Nearest", + "GraphicsScalingFilterFsr": "FSR", + "GraphicsScalingFilterArea": "Area", + "GraphicsScalingFilterLevelLabel": "Nível", + "GraphicsScalingFilterLevelTooltip": "Defina o nível de nitidez do FSR 1.0. Quanto maior, mais nítido.", + "SmaaLow": "SMAA Baixo", + "SmaaMedium": "SMAA Médio", + "SmaaHigh": "SMAA Alto", + "SmaaUltra": "SMAA Ultra", + "UserEditorTitle": "Editar usuário", + "UserEditorTitleCreate": "Criar usuário", + "SettingsTabNetworkInterface": "Interface de rede:", + "NetworkInterfaceTooltip": "A interface de rede usada para recursos de LAN/LDN.\n\nEm conjunto com uma VPN ou XLink Kai e um jogo com suporte a LAN, pode ser usada para simular uma conexão na mesma rede pela Internet.\n\nMantenha em PADRÃO se estiver em dúvida.", + "NetworkInterfaceDefault": "Padrão", + "PackagingShaders": "Empacotamento de Shaders", + "AboutChangelogButton": "Ver mudanças no GitHub", + "AboutChangelogButtonTooltipMessage": "Clique para abrir o relatório de alterações para esta versão no seu navegador padrão.", + "SettingsTabNetworkMultiplayer": "Multiplayer", + "MultiplayerMode": "Modo:", + "MultiplayerModeTooltip": "Alterar o modo multiplayer LDN.\n\nLdnMitm modificará a funcionalidade de jogo sem fio/local nos jogos para funcionar como se fosse LAN, permitindo conexões locais, na mesma rede, com outras instâncias do Ryujinx e consoles Nintendo Switch hackeados que possuem o módulo ldn_mitm instalado.\n\nO multiplayer exige que todos os jogadores estejam na mesma versão do jogo (ex.: Super Smash Bros. Ultimate v13.0.1 não consegue se conectar à v13.0.0).\n\nDeixe DESATIVADO se estiver em dúvida.", + "MultiplayerModeDisabled": "Desativado", + "MultiplayerModeLdnMitm": "ldn_mitm", + "MultiplayerModeLdnRyu": "RyuLDN", + "MultiplayerDisableP2P": "Disable P2P Network Hosting (may increase latency)", + "MultiplayerDisableP2PTooltip": "Disable P2P network hosting, peers will proxy through the master server instead of connecting to you directly.", + "LdnPassphrase": "Network Passphrase:", + "LdnPassphraseTooltip": "You will only be able to see hosted games with the same passphrase as you.", + "LdnPassphraseInputTooltip": "Enter a passphrase in the format Ryujinx-<8 hex chars>. You will only be able to see hosted games with the same passphrase as you.", + "LdnPassphraseInputPublic": "(public)", + "GenLdnPass": "Generate Random", + "GenLdnPassTooltip": "Generates a new passphrase, which can be shared with other players.", + "ClearLdnPass": "Clear", + "ClearLdnPassTooltip": "Clears the current passphrase, returning to the public network.", + "InvalidLdnPassphrase": "Invalid Passphrase! Must be in the format \"Ryujinx-<8 hex chars>\"" +} diff --git a/src/Ryujinx/Assets/Locales/ru_RU.json b/src/Ryujinx/Assets/Locales/ru_RU.json new file mode 100644 index 000000000..86e51f09f --- /dev/null +++ b/src/Ryujinx/Assets/Locales/ru_RU.json @@ -0,0 +1,868 @@ +{ + "Language": "Русский (RU)", + "MenuBarFileOpenApplet": "Открыть апплет", + "MenuBarFileOpenAppletOpenMiiApplet": "Mii Edit Applet", + "MenuBarFileOpenAppletOpenMiiAppletToolTip": "Открывает апплет Mii Editor в автономном режиме", + "SettingsTabInputDirectMouseAccess": "Прямой ввод мыши", + "SettingsTabSystemMemoryManagerMode": "Режим менеджера памяти:", + "SettingsTabSystemMemoryManagerModeSoftware": "Программное обеспечение", + "SettingsTabSystemMemoryManagerModeHost": "Хост (быстро)", + "SettingsTabSystemMemoryManagerModeHostUnchecked": "Хост не установлен (самый быстрый, небезопасный)", + "SettingsTabSystemUseHypervisor": "Использовать Hypervisor", + "MenuBarFile": "_Файл", + "MenuBarFileOpenFromFile": "_Добавить приложение из файла", + "MenuBarFileOpenFromFileError": "No applications found in selected file.", + "MenuBarFileOpenUnpacked": "Добавить _распакованную игру", + "MenuBarFileLoadDlcFromFolder": "Load DLC From Folder", + "MenuBarFileLoadTitleUpdatesFromFolder": "Load Title Updates From Folder", + "MenuBarFileOpenEmuFolder": "Открыть папку Ryujinx", + "MenuBarFileOpenLogsFolder": "Открыть папку с логами", + "MenuBarFileExit": "_Выход", + "MenuBarOptions": "_Настройки", + "MenuBarOptionsToggleFullscreen": "Включить полноэкранный режим", + "MenuBarOptionsStartGamesInFullscreen": "Запускать игры в полноэкранном режиме", + "MenuBarOptionsStopEmulation": "Остановить эмуляцию", + "MenuBarOptionsSettings": "_Параметры", + "MenuBarOptionsManageUserProfiles": "_Менеджер учетных записей", + "MenuBarActions": "_Действия", + "MenuBarOptionsSimulateWakeUpMessage": "Имитировать сообщение пробуждения", + "MenuBarActionsScanAmiibo": "Сканировать Amiibo", + "MenuBarTools": "_Инструменты", + "MenuBarToolsInstallFirmware": "Установка прошивки", + "MenuBarFileToolsInstallFirmwareFromFile": "Установить прошивку из XCI или ZIP", + "MenuBarFileToolsInstallFirmwareFromDirectory": "Установить прошивку из папки", + "MenuBarToolsManageFileTypes": "Управление типами файлов", + "MenuBarToolsInstallFileTypes": "Установить типы файлов", + "MenuBarToolsUninstallFileTypes": "Удалить типы файлов", + "MenuBarToolsXCITrimmer": "Trim XCI Files", + "MenuBarView": "_Вид", + "MenuBarViewWindow": "Размер окна", + "MenuBarViewWindow720": "720p", + "MenuBarViewWindow1080": "1080p", + "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", + "GameListContextMenuCacheManagement": "Управление кэшем", + "GameListContextMenuCacheManagementPurgePptc": "Перестроить очередь PPTC", + "GameListContextMenuCacheManagementPurgePptcToolTip": "Запускает перестройку PPTC во время следующего запуска игры.", + "GameListContextMenuCacheManagementPurgeShaderCache": "Очистить кэш шейдеров", + "GameListContextMenuCacheManagementPurgeShaderCacheToolTip": "Удаляет кеш шейдеров приложения", + "GameListContextMenuCacheManagementOpenPptcDirectory": "Открыть папку PPTC", + "GameListContextMenuCacheManagementOpenPptcDirectoryToolTip": "Открывает папку, содержащую PPTC кэш приложений и игр", + "GameListContextMenuCacheManagementOpenShaderCacheDirectory": "Открыть папку с кэшем шейдеров", + "GameListContextMenuCacheManagementOpenShaderCacheDirectoryToolTip": "Открывает папку, содержащую кэш шейдеров приложений и игр", + "GameListContextMenuExtractData": "Извлечь данные", + "GameListContextMenuExtractDataExeFS": "ExeFS", + "GameListContextMenuExtractDataExeFSToolTip": "Извлечение раздела ExeFS из текущих настроек приложения (включая обновления)", + "GameListContextMenuExtractDataRomFS": "RomFS", + "GameListContextMenuExtractDataRomFSToolTip": "Извлечение раздела RomFS из текущих настроек приложения (включая обновления)", + "GameListContextMenuExtractDataLogo": "Логотип", + "GameListContextMenuExtractDataLogoToolTip": "Извлечение раздела с логотипом из текущих настроек приложения (включая обновления)", + "GameListContextMenuCreateShortcut": "Создать ярлык приложения", + "GameListContextMenuCreateShortcutToolTip": "Создает ярлык на рабочем столе, с помощью которого можно запустить игру или приложение", + "GameListContextMenuCreateShortcutToolTipMacOS": "Создает ярлык игры или приложения в папке Программы macOS", + "GameListContextMenuOpenModsDirectory": "Открыть папку с модами", + "GameListContextMenuOpenModsDirectoryToolTip": "Открывает папку, содержащую моды для приложений и игр", + "GameListContextMenuOpenSdModsDirectory": "Открыть папку с модами Atmosphere", + "GameListContextMenuOpenSdModsDirectoryToolTip": "Открывает папку Atmosphere на альтернативной SD-карте, которая содержит моды для приложений и игр. Полезно для модов, сделанных для реальной консоли.", + "GameListContextMenuTrimXCI": "Check and Trim XCI File", + "GameListContextMenuTrimXCIToolTip": "Check and Trim XCI File to Save Disk Space", + "StatusBarGamesLoaded": "{0}/{1} игр загружено", + "StatusBarSystemVersion": "Версия прошивки: {0}", + "StatusBarXCIFileTrimming": "Trimming XCI File '{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": "Подтверждать выход из приложения", + "SettingsTabGeneralRememberWindowState": "Запомнить размер/положение окна", + "SettingsTabGeneralShowTitleBar": "Show Title Bar (Requires restart)", + "SettingsTabGeneralHideCursor": "Скрывать курсор", + "SettingsTabGeneralHideCursorNever": "Никогда", + "SettingsTabGeneralHideCursorOnIdle": "В простое", + "SettingsTabGeneralHideCursorAlways": "Всегда", + "SettingsTabGeneralGameDirectories": "Папки с играми", + "SettingsTabGeneralAutoloadDirectories": "Autoload DLC/Updates Directories", + "SettingsTabGeneralAutoloadNote": "DLC and Updates which refer to missing files will be unloaded automatically", + "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 (Profiled Persistent Translation Cache)", + "SettingsTabSystemEnableLowPowerPptc": "Low-power PPTC", + "SettingsTabSystemEnableFsIntegrityChecks": "Проверка целостности файловой системы", + "SettingsTabSystemAudioBackend": "Аудио бэкенд:", + "SettingsTabSystemAudioBackendDummy": "Без звука", + "SettingsTabSystemAudioBackendOpenAL": "OpenAL", + "SettingsTabSystemAudioBackendSoundIO": "SoundIO", + "SettingsTabSystemAudioBackendSDL2": "SDL2", + "SettingsTabSystemHacks": "Хаки", + "SettingsTabSystemHacksNote": "Возможна нестабильная работа", + "SettingsTabSystemDramSize": "Использовать альтернативный макет памяти (для разработчиков)", + "SettingsTabSystemDramSize4GiB": "4GiB", + "SettingsTabSystemDramSize6GiB": "6GiB", + "SettingsTabSystemDramSize8GiB": "8GiB", + "SettingsTabSystemDramSize12GiB": "12GiB", + "SettingsTabSystemIgnoreMissingServices": "Игнорировать отсутствующие службы", + "SettingsTabSystemIgnoreApplet": "Игнорировать Апплет", + "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": "Включить журнал-заглушку", + "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 Controller", + "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": "Хост сервера:", + "ControllerSettingsMotionGyroSensitivity": "Чувствительность гироскопа:", + "ControllerSettingsMotionGyroDeadzone": "Мертвая зона гироскопа:", + "ControllerSettingsSave": "Сохранить", + "ControllerSettingsClose": "Закрыть", + "KeyUnknown": "Неизвестно", + "KeyShiftLeft": "Левый Shift", + "KeyShiftRight": "Правый Shift", + "KeyControlLeft": "Левый Ctrl", + "KeyMacControlLeft": "Левый ⌃", + "KeyControlRight": "Правый Ctrl", + "KeyMacControlRight": "Правый ⌃", + "KeyAltLeft": "Левый Alt", + "KeyMacAltLeft": "Левый ⌥", + "KeyAltRight": "Правый Alt", + "KeyMacAltRight": "Правый ⌥", + "KeyWinLeft": "Левый ⊞", + "KeyMacWinLeft": "Левый ⌘", + "KeyWinRight": "Правый ⊞", + "KeyMacWinRight": "Правый ⌘", + "KeyMenu": "Меню", + "KeyUp": "Вверх", + "KeyDown": "Вниз", + "KeyLeft": "Влево", + "KeyRight": "Вправо", + "KeyEnter": "Enter", + "KeyEscape": "Escape", + "KeySpace": "Пробел", + "KeyTab": "Tab", + "KeyBackSpace": "Backspace", + "KeyInsert": "Insert", + "KeyDelete": "Delete", + "KeyPageUp": "Page Up", + "KeyPageDown": "Page Down", + "KeyHome": "Home", + "KeyEnd": "End", + "KeyCapsLock": "Caps Lock", + "KeyScrollLock": "Scroll Lock", + "KeyPrintScreen": "Print Screen", + "KeyPause": "Pause", + "KeyNumLock": "Num Lock", + "KeyClear": "Очистить", + "KeyKeypad0": "Блок цифр 0", + "KeyKeypad1": "Блок цифр 1", + "KeyKeypad2": "Блок цифр 2", + "KeyKeypad3": "Блок цифр 3", + "KeyKeypad4": "Блок цифр 4", + "KeyKeypad5": "Блок цифр 5", + "KeyKeypad6": "Блок цифр 6", + "KeyKeypad7": "Блок цифр 7", + "KeyKeypad8": "Блок цифр 8", + "KeyKeypad9": "Блок цифр 9", + "KeyKeypadDivide": "/ (блок цифр)", + "KeyKeypadMultiply": "* (блок цифр)", + "KeyKeypadSubtract": "- (блок цифр)", + "KeyKeypadAdd": "+ (блок цифр)", + "KeyKeypadDecimal": ". (блок цифр)", + "KeyKeypadEnter": "Enter (блок цифр)", + "KeyNumber0": "0", + "KeyNumber1": "1", + "KeyNumber2": "2", + "KeyNumber3": "3", + "KeyNumber4": "4", + "KeyNumber5": "5", + "KeyNumber6": "6", + "KeyNumber7": "7", + "KeyNumber8": "8", + "KeyNumber9": "9", + "KeyTilde": "~", + "KeyGrave": "`", + "KeyMinus": "-", + "KeyPlus": "+", + "KeyBracketLeft": "[", + "KeyBracketRight": "]", + "KeySemicolon": ";", + "KeyQuote": "\"", + "KeyComma": ",", + "KeyPeriod": ".", + "KeySlash": "/", + "KeyBackSlash": "\\", + "KeyUnbound": "Не привязано", + "GamepadLeftStick": "Кнопка лев. стика", + "GamepadRightStick": "Кнопка пр. стика", + "GamepadLeftShoulder": "Левый бампер", + "GamepadRightShoulder": "Правый бампер", + "GamepadLeftTrigger": "Левый триггер", + "GamepadRightTrigger": "Правый триггер", + "GamepadDpadUp": "Вверх", + "GamepadDpadDown": "Вниз", + "GamepadDpadLeft": "Влево", + "GamepadDpadRight": "Вправо", + "GamepadMinus": "-", + "GamepadPlus": "+", + "GamepadGuide": "Кнопка Xbox", + "GamepadMisc1": "Прочее", + "GamepadPaddle1": "Доп.кнопка 1", + "GamepadPaddle2": "Доп.кнопка 2", + "GamepadPaddle3": "Доп.кнопка 3", + "GamepadPaddle4": "Доп.кнопка 4", + "GamepadTouchpad": "Тачпад", + "GamepadSingleLeftTrigger0": "Левый триггер 0", + "GamepadSingleRightTrigger0": "Правый триггер 0", + "GamepadSingleLeftTrigger1": "Левый триггер 1", + "GamepadSingleRightTrigger1": "Правый триггер 1", + "StickLeft": "Левый стик", + "StickRight": "Правый стик", + "UserProfilesSelectedUserProfile": "Выбранный пользовательский профиль:", + "UserProfilesSaveProfileName": "Сохранить пользовательский профиль", + "UserProfilesChangeProfileImage": "Изменить аватар", + "UserProfilesAvailableUserProfiles": "Доступные профили пользователей:", + "UserProfilesAddNewProfile": "Добавить новый профиль", + "UserProfilesDelete": "Удалить", + "UserProfilesClose": "Закрыть", + "ProfileNameSelectionWatermark": "Укажите никнейм", + "ProfileImageSelectionTitle": "Выбор изображения профиля", + "ProfileImageSelectionHeader": "Выбор аватара", + "ProfileImageSelectionNote": "Вы можете импортировать собственное изображение или выбрать аватар из системной прошивки.", + "ProfileImageSelectionImportImage": "Импорт изображения", + "ProfileImageSelectionSelectAvatar": "Встроенные аватары", + "InputDialogTitle": "Диалоговое окно ввода", + "InputDialogOk": "ОК", + "InputDialogCancel": "Отмена", + "InputDialogCancelling": "Cancelling", + "InputDialogClose": "Close", + "InputDialogAddNewProfileTitle": "Выберите никнейм", + "InputDialogAddNewProfileHeader": "Пожалуйста, введите никнейм", + "InputDialogAddNewProfileSubtext": "(Максимальная длина: {0})", + "AvatarChoose": "Выбрать аватар", + "AvatarSetBackgroundColor": "Установить цвет фона", + "AvatarClose": "Закрыть", + "ControllerSettingsLoadProfileToolTip": "Загрузить профиль", + "ControllerSettingsViewProfileToolTip": "View Profile", + "ControllerSettingsAddProfileToolTip": "Добавить профиль", + "ControllerSettingsRemoveProfileToolTip": "Удалить профиль", + "ControllerSettingsSaveProfileToolTip": "Сохранить профиль", + "MenuBarFileToolsTakeScreenshot": "Сделать снимок экрана", + "MenuBarFileToolsHideUi": "Скрыть интерфейс", + "GameListContextMenuRunApplication": "Запуск приложения", + "GameListContextMenuToggleFavorite": "Добавить в избранное", + "GameListContextMenuToggleFavoriteToolTip": "Добавляет игру в избранное и помечает звездочкой", + "SettingsTabGeneralTheme": "Тема:", + "SettingsTabGeneralThemeAuto": "Auto", + "SettingsTabGeneralThemeDark": "Темная", + "SettingsTabGeneralThemeLight": "Светлая", + "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": "Извлечение {0} раздела из {1}...", + "DialogNcaExtractionTitle": "Извлечение разделов NCA", + "DialogNcaExtractionMainNcaNotFoundErrorMessage": "Ошибка извлечения. Основной NCA не присутствовал в выбранном файле.", + "DialogNcaExtractionCheckLogErrorMessage": "Ошибка извлечения. Прочтите файл журнала для получения дополнительной информации.", + "DialogNcaExtractionSuccessMessage": "Извлечение завершено успешно.", + "DialogUpdaterConvertFailedMessage": "Не удалось преобразовать текущую версию Ryujinx.", + "DialogUpdaterCancelUpdateMessage": "Отмена обновления...", + "DialogUpdaterAlreadyOnLatestVersionMessage": "Вы используете самую последнюю версию Ryujinx", + "DialogUpdaterFailedToGetVersionMessage": "Произошла ошибка при попытке получить информацию о выпуске от GitHub Release. Это может быть вызвано тем, что в данный момент в GitHub Actions компилируется новый релиз. Повторите попытку позже.", + "DialogUpdaterConvertFailedGithubMessage": "Не удалось преобразовать полученную версию Ryujinx из Github Release.", + "DialogUpdaterDownloadingMessage": "Загрузка обновления...", + "DialogUpdaterExtractionMessage": "Извлечение обновления...", + "DialogUpdaterRenamingMessage": "Переименование обновления...", + "DialogUpdaterAddingFilesMessage": "Добавление нового обновления...", + "DialogUpdaterShowChangelogMessage": "Show Changelog", + "DialogUpdaterCompleteMessage": "Обновление завершено", + "DialogUpdaterRestartMessage": "Перезапустить Ryujinx?", + "DialogUpdaterNoInternetMessage": "Вы не подключены к интернету", + "DialogUpdaterNoInternetSubMessage": "Убедитесь, что у вас работает подключение к интернету", + "DialogUpdaterDirtyBuildMessage": "Вы не можете обновлять Dirty Build", + "DialogUpdaterDirtyBuildSubMessage": "Загрузите Ryujinx по адресу https://ryujinx.app/download если вам нужна поддерживаемая версия.", + "DialogRestartRequiredMessage": "Требуется перезагрузка", + "DialogThemeRestartMessage": "Тема сохранена. Для применения темы требуется перезапуск.", + "DialogThemeRestartSubMessage": "Хотите перезапустить", + "DialogFirmwareInstallEmbeddedMessage": "Хотите установить прошивку, встроенную в эту игру? (Прошивка {0})", + "DialogFirmwareInstallEmbeddedSuccessMessage": "Установленная прошивка не была найдена, но Ryujinx удалось установить прошивку {0} из предоставленной игры.\nТеперь эмулятор запустится.", + "DialogFirmwareNoFirmwareInstalledMessage": "Прошивка не установлена", + "DialogFirmwareInstalledMessage": "Прошивка {0} была установлена", + "DialogInstallFileTypesSuccessMessage": "Типы файлов успешно установлены", + "DialogInstallFileTypesErrorMessage": "Не удалось установить типы файлов.", + "DialogUninstallFileTypesSuccessMessage": "Типы файлов успешно удалены", + "DialogUninstallFileTypesErrorMessage": "Не удалось удалить типы файлов.", + "DialogOpenSettingsWindowLabel": "Открывает окно параметров", + "DialogOpenXCITrimmerWindowLabel": "XCI Trimmer Window", + "DialogControllerAppletTitle": "Апплет контроллера", + "DialogMessageDialogErrorExceptionMessage": "Ошибка отображения сообщения: {0}", + "DialogSoftwareKeyboardErrorExceptionMessage": "Ошибка отображения программной клавиатуры: {0}", + "DialogErrorAppletErrorExceptionMessage": "Ошибка отображения диалогового окна ErrorApplet: {0}", + "DialogUserErrorDialogMessage": "{0}: {1}", + "DialogUserErrorDialogInfoMessage": "\nДля получения дополнительной информации о том, как исправить эту ошибку, следуйте нашему Руководству по установке.", + "DialogUserErrorDialogTitle": "Ошибка Ryujinx ({0})", + "DialogAmiiboApiTitle": "Amiibo API", + "DialogAmiiboApiFailFetchMessage": "Произошла ошибка при получении информации из API.", + "DialogAmiiboApiConnectErrorMessage": "Не удалось подключиться к серверу Amiibo API. Служба может быть недоступна или вам может потребоваться проверить ваше интернет-соединение.", + "DialogProfileInvalidProfileErrorMessage": "Профиль {0} несовместим с текущей системой конфигурации ввода.", + "DialogProfileDefaultProfileOverwriteErrorMessage": "Профиль по умолчанию не может быть перезаписан", + "DialogProfileDeleteProfileTitle": "Удаление профиля", + "DialogProfileDeleteProfileMessage": "Это действие необратимо. Вы уверены, что хотите продолжить?", + "DialogWarning": "Внимание", + "DialogPPTCDeletionMessage": "Вы собираетесь перестроить кэш PPTC при следующем запуске для:\n\n{0}\n\nВы уверены, что хотите продолжить?", + "DialogPPTCDeletionErrorMessage": "Ошибка очистки кэша PPTC в {0}: {1}", + "DialogShaderDeletionMessage": "Вы собираетесь удалить кэш шейдеров для:\n\n{0}\n\nВы уверены, что хотите продолжить?", + "DialogShaderDeletionErrorMessage": "Ошибка очистки кэша шейдеров в {0}: {1}", + "DialogRyujinxErrorMessage": "Ryujinx обнаружил ошибку", + "DialogInvalidTitleIdErrorMessage": "Ошибка пользовательского интерфейса: выбранная игра не имеет действительного ID.", + "DialogFirmwareInstallerFirmwareNotFoundErrorMessage": "Валидная системная прошивка не найдена в {0}.", + "DialogFirmwareInstallerFirmwareInstallTitle": "Установить прошивку {0}", + "DialogFirmwareInstallerFirmwareInstallMessage": "Будет установлена версия прошивки {0}.", + "DialogFirmwareInstallerFirmwareInstallSubMessage": "\n\nЭто заменит текущую версию прошивки {0}.", + "DialogFirmwareInstallerFirmwareInstallConfirmMessage": "\n\nПродолжить?", + "DialogFirmwareInstallerFirmwareInstallWaitMessage": "Установка прошивки...", + "DialogFirmwareInstallerFirmwareInstallSuccessMessage": "Прошивка версии {0} успешно установлена.", + "DialogUserProfileDeletionWarningMessage": "Если выбранный профиль будет удален, другие профили не будут открываться.", + "DialogUserProfileDeletionConfirmMessage": "Удалить выбранный профиль?", + "DialogUserProfileUnsavedChangesTitle": "Внимание - Несохраненные изменения", + "DialogUserProfileUnsavedChangesMessage": "В эту учетную запись внесены изменения, которые не были сохранены.", + "DialogUserProfileUnsavedChangesSubMessage": "Отменить изменения?", + "DialogControllerSettingsModifiedConfirmMessage": "Текущие настройки управления обновлены.", + "DialogControllerSettingsModifiedConfirmSubMessage": "Сохранить?", + "DialogLoadFileErrorMessage": "{0}. Файл с ошибкой: {1}", + "DialogModAlreadyExistsMessage": "Мод уже существует", + "DialogModInvalidMessage": "Выбранная папка не содержит модов", + "DialogModDeleteNoParentMessage": "Невозможно удалить: не удалось найти папку мода \"{0}\"", + "DialogDlcNoDlcErrorMessage": "Указанный файл не содержит DLC для выбранной игры", + "DialogPerformanceCheckLoggingEnabledMessage": "У вас включено ведение журнала отладки, предназначенное только для разработчиков.", + "DialogPerformanceCheckLoggingEnabledConfirmMessage": "Для оптимальной производительности рекомендуется отключить ведение журнала отладки. Хотите отключить ведение журнала отладки?", + "DialogPerformanceCheckShaderDumpEnabledMessage": "У вас включен дамп шейдеров, который предназначен только для разработчиков.", + "DialogPerformanceCheckShaderDumpEnabledConfirmMessage": "Для оптимальной производительности рекомендуется отключить дамп шейдеров. Хотите отключить дамп шейдеров?", + "DialogLoadAppGameAlreadyLoadedMessage": "Игра уже загружена", + "DialogLoadAppGameAlreadyLoadedSubMessage": "Пожалуйста, остановите эмуляцию или закройте эмулятор перед запуском другой игры.", + "DialogUpdateAddUpdateErrorMessage": "Указанный файл не содержит обновлений для выбранного приложения", + "DialogSettingsBackendThreadingWarningTitle": "Предупреждение: многопоточность в бэкенде", + "DialogSettingsBackendThreadingWarningMessage": "Для применения этой настройки необходимо перезапустить Ryujinx. В зависимости от используемой вами операционной системы вам может потребоваться вручную отключить многопоточность драйвера при использовании Ryujinx.", + "DialogModManagerDeletionWarningMessage": "Вы сейчас удалите мод: {0}\n\nВы уверены, что хотите продолжить?", + "DialogModManagerDeletionAllWarningMessage": "Вы сейчас удалите все выбранные моды для этой игры.\n\nВы уверены, что хотите продолжить?", + "SettingsTabGraphicsFeaturesOptions": "Функции & Улучшения", + "SettingsTabGraphicsBackendMultithreading": "Многопоточность графического бэкенда:", + "CommonAuto": "Автоматически", + "CommonOff": "Выключено", + "CommonOn": "Включено", + "InputDialogYes": "Да", + "InputDialogNo": "Нет", + "DialogProfileInvalidProfileNameErrorMessage": "Имя файла содержит недопустимые символы. Пожалуйста, попробуйте еще раз.", + "MenuBarOptionsPauseEmulation": "Пауза эмуляции", + "MenuBarOptionsResumeEmulation": "Продолжить", + "AboutUrlTooltipMessage": "Нажмите, чтобы открыть веб-сайт Ryujinx", + "AboutDisclaimerMessage": "Ryujinx никоим образом не связан ни с Nintendo™, ни с кем-либо из ее партнеров.", + "AboutAmiiboDisclaimerMessage": "Amiibo API (www.amiiboapi.com) используется для эмуляции Amiibo.", + "AboutPatreonUrlTooltipMessage": "Нажмите, чтобы открыть страницу Ryujinx на Patreon", + "AboutGithubUrlTooltipMessage": "Нажмите, чтобы открыть страницу Ryujinx на GitHub", + "AboutDiscordUrlTooltipMessage": "Нажмите, чтобы открыть приглашение на сервер Ryujinx в Discord", + "AboutTwitterUrlTooltipMessage": "Нажмите, чтобы открыть страницу Ryujinx в X (бывший Twitter)", + "AboutRyujinxAboutTitle": "О программе:", + "AboutRyujinxAboutContent": "Ryujinx — это эмулятор Nintendo Switch™.\nПожалуйста, поддержите нас на Patreon.\nЧитайте последние новости в наших X (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": "Отключить все", + "ModManagerDeleteAllButton": "Удалить все", + "MenuBarOptionsChangeLanguage": "Сменить язык", + "MenuBarShowFileTypes": "Показывать форматы файлов", + "CommonSort": "Сортировка", + "CommonShowNames": "Показывать названия", + "CommonFavorite": "Избранное", + "OrderAscending": "По возрастанию", + "OrderDescending": "По убыванию", + "SettingsTabGraphicsFeatures": "Функции", + "ErrorWindowTitle": "Окно ошибки", + "ToggleDiscordTooltip": "Включает или отключает отображение статуса \"Играет в игру\" в Discord", + "AddGameDirBoxTooltip": "Введите путь к папке с играми для добавления ее в список выше", + "AddGameDirTooltip": "Добавить папку с играми в список", + "RemoveGameDirTooltip": "Удалить выбранную папку игры", + "AddAutoloadDirBoxTooltip": "Enter an autoload directory to add to the list", + "AddAutoloadDirTooltip": "Add an autoload directory to the list", + "RemoveAutoloadDirTooltip": "Remove selected autoload directory", + "CustomThemeCheckTooltip": "Включить или отключить пользовательские темы", + "CustomThemePathTooltip": "Путь к пользовательской теме для интерфейса", + "CustomThemeBrowseTooltip": "Просмотр пользовательской темы интерфейса", + "DockModeToggleTooltip": "\"Стационарный\" режим запускает эмулятор, как если бы Nintendo Switch находилась в доке, что улучшает графику и разрешение в большинстве игр. И наоборот, при отключении этого режима эмулятор будет запускать игры в \"Портативном\" режиме, снижая качество графики.\n\nНастройте управление для Игрока 1 если планируете использовать в \"Стационарном\" режиме; настройте портативное управление если планируете использовать эмулятор в \"Портативном\" режиме.\n\nРекомендуется оставить включенным.", + "DirectKeyboardTooltip": "Поддержка прямого ввода с клавиатуры (HID). Предоставляет игре прямой доступ к клавиатуре в качестве устройства ввода текста.\nРаботает только с играми, которые изначально поддерживают использование клавиатуры с Switch.\nРекомендуется оставить выключенным.", + "DirectMouseTooltip": "Поддержка прямого ввода мыши (HID). Предоставляет игре прямой доступ к мыши в качестве указывающего устройства.\nРаботает только с играми, которые изначально поддерживают использование мыши совместно с железом Switch.\nРекомендуется оставить выключенным.", + "RegionTooltip": "Сменяет регион прошивки", + "LanguageTooltip": "Меняет язык прошивки", + "TimezoneTooltip": "Меняет часовой пояс прошивки", + "TimeTooltip": "Меняет системное время прошивки", + "VSyncToggleTooltip": "Эмуляция вертикальной синхронизации консоли, которая ограничивает количество кадров в секунду в большинстве игр; отключение может привести к тому, что игры будут запущены с более высокой частотой кадров, но загрузка игры может занять больше времени, либо игра не запустится вообще.\n\nМожно включать и выключать эту настройку непосредственно в игре с помощью горячих клавиш (F1 по умолчанию). Если планируете отключить вертикальную синхронизацию, рекомендуем настроить горячие клавиши.\n\nРекомендуется оставить включенным.", + "PptcToggleTooltip": "Сохраняет скомпилированные JIT-функции для того, чтобы не преобразовывать их по новой каждый раз при запуске игры.\n\nУменьшает статтеры и значительно ускоряет последующую загрузку игр.\n\nРекомендуется оставить включенным.", + "LowPowerPptcToggleTooltip": "Load the PPTC using a third of the amount of cores.", + "FsIntegrityToggleTooltip": "Проверяет файлы при загрузке игры и если обнаружены поврежденные файлы, выводит сообщение о поврежденном хэше в журнале.\n\nНе влияет на производительность и необходим для помощи в устранении неполадок.\n\nРекомендуется оставить включенным.", + "AudioBackendTooltip": "Изменяет используемый аудио бэкенд для рендера звука.\n\nSDL2 является предпочтительным вариантом, в то время как OpenAL и SoundIO используются в качестве резервных.\n\nРекомендуется использование SDL2.", + "MemoryManagerTooltip": "Меняет разметку и доступ к гостевой памяти. Значительно влияет на производительность процессора.\n\nРекомендуется оставить \"Хост не установлен\"", + "MemoryManagerSoftwareTooltip": "Использует таблицу страниц для преобразования адресов. \nСамая высокая точность, но самая низкая производительность.", + "MemoryManagerHostTooltip": "Прямая разметка памяти в адресном пространстве хоста. \nЗначительно более быстрые запуск и компиляция JIT.", + "MemoryManagerUnsafeTooltip": "Производит прямую разметку памяти, но не маскирует адрес в гостевом адресном пространстве перед получением доступа. \nБыстро, но небезопасно. Гостевое приложение может получить доступ к памяти из Ryujinx, поэтому в этом режиме рекомендуется запускать только те программы, которым вы доверяете.", + "UseHypervisorTooltip": "Использует Hypervisor вместо JIT. Значительно увеличивает производительность, но может работать нестабильно.", + "DRamTooltip": "Использует альтернативный макет MemoryMode для имитации использования Nintendo Switch в режиме разработчика.\n\nПолезно только для пакетов текстур с высоким разрешением или модов добавляющих разрешение 4К. Не улучшает производительность.\n\nРекомендуется оставить выключенным.", + "IgnoreMissingServicesTooltip": "Игнорирует нереализованные сервисы Horizon в новых прошивках. Эта настройка поможет избежать вылеты при запуске определенных игр.\n\nРекомендуется оставить выключенным.", + "IgnoreAppletTooltip": "Внешний диалог \"Апплет контроллера\" не появится, если геймпад будет отключен во время игры. Не будет предложено закрыть диалог или настроить новый контроллер. После повторного подключения ранее отключенного контроллера игра автоматически возобновится.", + "GraphicsBackendThreadingTooltip": "Выполняет команды графического бэкенда на втором потоке.\n\nУскоряет компиляцию шейдеров, уменьшает статтеры и повышает производительность на драйверах видеоадаптера без поддержки многопоточности. Производительность на драйверах с многопоточностью немного выше.\n\nРекомендуется оставить Автоматически.", + "GalThreadingTooltip": "Выполняет команды графического бэкенда на втором потоке.\n\nУскоряет компиляцию шейдеров, уменьшает статтеры и повышает производительность на драйверах видеоадаптера без поддержки многопоточности. Производительность на драйверах с многопоточностью немного выше.\n\nРекомендуется оставить Автоматически.", + "ShaderCacheToggleTooltip": "Сохраняет кэш шейдеров на диске, для уменьшения статтеров при последующих запусках.\n\nРекомендуется оставить включенным.", + "ResolutionScaleTooltip": "Увеличивает разрешение рендера игры.\n\nНекоторые игры могут не работать с этой настройкой и выглядеть смазано даже когда разрешение увеличено. Для таких игр может потребоваться установка модов, которые убирают сглаживание или увеличивают разрешение рендеринга. \nДля использования последнего, вам нужно будет выбрать опцию \"Нативное\".\n\nЭта опция может быть изменена во время игры по нажатию кнопки \"Применить\" ниже. Вы можете просто переместить окно настроек в сторону и поэкспериментировать, пока не подберете подходящие настройки для конкретной игры.\n\nИмейте в виду, что \"4x\" является излишеством.", + "ResolutionScaleEntryTooltip": "Масштабирование разрешения с плавающей запятой, например 1,5. Неинтегральное масштабирование с большой вероятностью вызовет сбои в работе.", + "AnisotropyTooltip": "Уровень анизотропной фильтрации. \n\nУстановите значение Автоматически, чтобы использовать значение по умолчанию игры.", + "AspectRatioTooltip": "Соотношение сторон окна рендерера.\n\nИзмените эту настройку только если вы используете мод для соотношения сторон, иначе изображение будет растянуто.\n\nРекомендуется настройка 16:9.", + "ShaderDumpPathTooltip": "Путь с дампами графических шейдеров", + "FileLogTooltip": "Включает ведение журнала в файл на диске. Не влияет на производительность.", + "StubLogTooltip": "Включает ведение журнала-заглушки. Не влияет на производительность.", + "InfoLogTooltip": "Включает вывод сообщений информационного журнала в консоль. Не влияет на производительность.", + "WarnLogTooltip": "Включает вывод сообщений журнала предупреждений в консоль. Не влияет на производительность.", + "ErrorLogTooltip": "Включает вывод сообщений журнала ошибок. Не влияет на производительность.", + "TraceLogTooltip": "Выводит сообщения журнала трассировки в консоли. Не влияет на производительность.", + "GuestLogTooltip": "Включает вывод сообщений гостевого журнала. Не влияет на производительность.", + "FileAccessLogTooltip": "Включает вывод сообщений журнала доступа к файлам.", + "FSAccessLogModeTooltip": "Включает вывод журнала доступа к файловой системе. Возможные режимы: 0-3", + "DeveloperOptionTooltip": "Используйте с осторожностью", + "OpenGlLogLevel": "Требует включения соответствующих уровней ведения журнала", + "DebugLogTooltip": "Выводит журнал сообщений отладки в консоли.\n\nИспользуйте только в случае просьбы разработчика, так как включение этой функции затруднит чтение журналов и ухудшит работу эмулятора.", + "LoadApplicationFileTooltip": "Открывает файловый менеджер для выбора файла, совместимого с Nintendo Switch.", + "LoadApplicationFolderTooltip": "Открывает файловый менеджер для выбора распакованного приложения, совместимого с Nintendo Switch.", + "LoadDlcFromFolderTooltip": "Open a file explorer to choose one or more folders to bulk load DLC from", + "LoadTitleUpdatesFromFolderTooltip": "Open a file explorer to choose one or more folders to bulk load title updates from", + "OpenRyujinxFolderTooltip": "Открывает папку с файлами Ryujinx. ", + "OpenRyujinxLogsTooltip": "Открывает папку в которую записываются логи", + "ExitTooltip": "Выйти из Ryujinx", + "OpenSettingsTooltip": "Открывает окно параметров", + "OpenProfileManagerTooltip": "Открыть менеджер учетных записей", + "StopEmulationTooltip": "Остановка эмуляции текущей игры и возврат к списку игр", + "CheckUpdatesTooltip": "Проверяет наличие обновлений для Ryujinx", + "OpenAboutTooltip": "Открывает окно «О программе»", + "GridSize": "Размер сетки", + "GridSizeTooltip": "Меняет размер сетки элементов", + "SettingsTabSystemSystemLanguageBrazilianPortuguese": "Португальский язык (Бразилия)", + "AboutRyujinxContributorsButtonHeader": "Посмотреть всех участников", + "SettingsTabSystemAudioVolume": "Громкость: ", + "AudioVolumeTooltip": "Изменяет громкость звука", + "SettingsTabSystemEnableInternetAccess": "Гостевой доступ в интернет/сетевой режим", + "EnableInternetAccessTooltip": "Позволяет эмулированному приложению подключаться к Интернету.\n\nПри включении этой функции игры с возможностью сетевой игры могут подключаться друг к другу, если все эмуляторы (или реальные консоли) подключены к одной и той же точке доступа.\n\nНЕ разрешает подключение к серверам Nintendo. Может вызвать сбой в некоторых играх, которые пытаются подключиться к Интернету.\n\nРекомендутеся оставить выключенным.", + "GameListContextMenuManageCheatToolTip": "Открывает окно управления читами", + "GameListContextMenuManageCheat": "Управление читами", + "GameListContextMenuManageModToolTip": "Открывает окно управления модами", + "GameListContextMenuManageMod": "Управление модами", + "ControllerSettingsStickRange": "Диапазон:", + "DialogStopEmulationTitle": "Ryujinx - Остановка эмуляции", + "DialogStopEmulationMessage": "Вы уверены, что хотите остановить эмуляцию?", + "SettingsTabCpu": "Процессор", + "SettingsTabAudio": "Аудио", + "SettingsTabNetwork": "Сеть", + "SettingsTabNetworkConnection": "Подключение к сети", + "SettingsTabCpuCache": "Кэш процессора", + "SettingsTabCpuMemory": "Режим процессора", + "DialogUpdaterFlatpakNotSupportedMessage": "Пожалуйста, обновите Ryujinx через FlatHub.", + "UpdaterDisabledWarningTitle": "Средство обновления отключено", + "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": "Version {0} - {1}", + "TitleBundledUpdateVersionLabel": "Bundled: Version {0}", + "TitleBundledDlcLabel": "Bundled:", + "TitleXCIStatusPartialLabel": "Partial", + "TitleXCIStatusTrimmableLabel": "Untrimmed", + "TitleXCIStatusUntrimmableLabel": "Trimmed", + "TitleXCIStatusFailedLabel": "(Failed)", + "TitleXCICanSaveLabel": "Save {0:n0} Mb", + "TitleXCISavingLabel": "Saved {0:n0} Mb", + "RyujinxInfo": "Ryujinx - Информация", + "RyujinxConfirm": "Ryujinx - Подтверждение", + "FileDialogAllTypes": "Все типы", + "Never": "Никогда", + "SwkbdMinCharacters": "Должно быть не менее {0} символов.", + "SwkbdMinRangeCharacters": "Должно быть {0}-{1} символов", + "SoftwareKeyboard": "Программная клавиатура", + "SoftwareKeyboardModeNumeric": "Должно быть в диапазоне 0-9 или '.'", + "SoftwareKeyboardModeAlphabet": "Не должно быть CJK-символов", + "SoftwareKeyboardModeASCII": "Текст должен быть только в ASCII кодировке", + "ControllerAppletControllers": "Поддерживаемые геймпады:", + "ControllerAppletPlayers": "Игроки:", + "ControllerAppletDescription": "Текущая конфигурация некорректна. Откройте параметры и перенастройте управление.", + "ControllerAppletDocked": "Используется стационарный режим. Управление в портативном режиме должно быть отключено.", + "UpdaterRenaming": "Переименование старых файлов...", + "UpdaterRenameFailed": "Программе обновления не удалось переименовать файл: {0}", + "UpdaterAddingFiles": "Добавление новых файлов...", + "UpdaterExtracting": "Извлечение обновления...", + "UpdaterDownloading": "Загрузка обновления...", + "Game": "Игра", + "Docked": "Стационарный режим", + "Handheld": "Портативный режим", + "ConnectionError": "Ошибка соединения", + "AboutPageDeveloperListMore": "{0} и другие...", + "ApiError": "Ошибка API.", + "LoadingHeading": "Загрузка {0}", + "CompilingPPTC": "Компиляция PTC", + "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": "Выберите файлы обновлений", + "SelectModDialogTitle": "Выбрать папку с модами", + "TrimXCIFileDialogTitle": "Check and Trim XCI File", + "TrimXCIFileDialogPrimaryText": "This function will first check the empty space and then trim the XCI File to save disk space.", + "TrimXCIFileDialogSecondaryText": "Current File Size: {0:n} MB\nGame Data Size: {1:n} MB\nDisk Space Savings: {2:n} MB", + "TrimXCIFileNoTrimNecessary": "XCI File does not need to be trimmed. Check logs for further details", + "TrimXCIFileNoUntrimPossible": "XCI File cannot be untrimmed. Check logs for further details", + "TrimXCIFileReadOnlyFileCannotFix": "XCI File is Read Only and could not be made writable. Check logs for further details", + "TrimXCIFileFileSizeChanged": "XCI File has changed in size since it was scanned. Please check the file is not being written to and try again.", + "TrimXCIFileFreeSpaceCheckFailed": "XCI File has data in the free space area, it is not safe to trim", + "TrimXCIFileInvalidXCIFile": "XCI File contains invalid data. Check logs for further details", + "TrimXCIFileFileIOWriteError": "XCI File could not be opened for writing. Check logs for further details", + "TrimXCIFileFailedPrimaryText": "Trimming of the XCI file failed", + "TrimXCIFileCancelled": "The operation was cancelled", + "TrimXCIFileFileUndertermined": "No operation was performed", + "UserProfileWindowTitle": "Менеджер учетных записей", + "CheatWindowTitle": "Менеджер читов", + "DlcWindowTitle": "Управление DLC для {0} ({1})", + "ModWindowTitle": "Управление модами для {0} ({1})", + "UpdateWindowTitle": "Менеджер обновлений игр", + "XCITrimmerWindowTitle": "XCI File Trimmer", + "XCITrimmerTitleStatusCount": "{0} of {1} Title(s) Selected", + "XCITrimmerTitleStatusCountWithFilter": "{0} of {1} Title(s) Selected ({2} displayed)", + "XCITrimmerTitleStatusTrimming": "Trimming {0} Title(s)...", + "XCITrimmerTitleStatusUntrimming": "Untrimming {0} Title(s)...", + "XCITrimmerTitleStatusFailed": "Failed", + "XCITrimmerPotentialSavings": "Potential Savings", + "XCITrimmerActualSavings": "Actual Savings", + "XCITrimmerSavingsMb": "{0:n0} Mb", + "XCITrimmerSelectDisplayed": "Select Shown", + "XCITrimmerDeselectDisplayed": "Deselect Shown", + "XCITrimmerSortName": "Title", + "XCITrimmerSortSaved": "Space Savings", + "XCITrimmerTrim": "Trim", + "XCITrimmerUntrim": "Untrim", + "UpdateWindowUpdateAddedMessage": "{0} new update(s) added", + "UpdateWindowBundledContentNotice": "Bundled updates cannot be removed, only disabled.", + "CheatWindowHeading": "Доступные читы для {0} [{1}]", + "BuildId": "ID версии:", + "DlcWindowBundledContentNotice": "Bundled DLC cannot be removed, only disabled.", + "DlcWindowHeading": "{0} DLC", + "DlcWindowDlcAddedMessage": "{0} new downloadable content(s) added", + "AutoloadDlcAddedMessage": "{0} new downloadable content(s) added", + "AutoloadDlcRemovedMessage": "{0} missing downloadable content(s) removed", + "AutoloadUpdateAddedMessage": "{0} new update(s) added", + "AutoloadUpdateRemovedMessage": "{0} missing update(s) removed", + "ModWindowHeading": "Моды для {0} ", + "UserProfilesEditProfile": "Изменить выбранные", + "Continue": "Continue", + "Cancel": "Отмена", + "Save": "Сохранить", + "Discard": "Отменить", + "Paused": "Приостановлено", + "UserProfilesSetProfileImage": "Установить аватар", + "UserProfileEmptyNameError": "Необходимо ввести никнейм", + "UserProfileNoImageError": "Необходимо установить аватар", + "GameUpdateWindowHeading": "Доступные обновления для {0} ({1})", + "SettingsTabHotkeysResScaleUpHotkey": "Увеличить разрешение:", + "SettingsTabHotkeysResScaleDownHotkey": "Уменьшить разрешение:", + "UserProfilesName": "Никнейм:", + "UserProfilesUserId": "ID пользователя:", + "SettingsTabGraphicsBackend": "Графический бэкенд", + "SettingsTabGraphicsBackendTooltip": "Выберает бэкенд, который будет использован в эмуляторе.\n\nVulkan является лучшим выбором для всех современных графических карт с актуальными драйверами. В Vulkan также включена более быстрая компиляция шейдеров (меньше статтеров) для всех видеоадаптеров.\n\nПри использовании OpenGL можно достичь лучших результатов на старых видеоадаптерах Nvidia и AMD в Linux или на видеоадаптерах с небольшим количеством видеопамяти, хотя статтеров при компиляции шейдеров будет больше.\n\nРекомендуется использовать Vulkan. Используйте OpenGL, если ваш видеоадаптер не поддерживает Vulkan даже с актуальными драйверами.", + "SettingsEnableTextureRecompression": "Пережимать текстуры", + "SettingsEnableTextureRecompressionTooltip": "Сжатие ASTC текстур для уменьшения использования VRAM. \n\nИгры, использующие этот формат текстур: Astral Chain, Bayonetta 3, Fire Emblem Engage, Metroid Prime Remastered, Super Mario Bros. Wonder и The Legend of Zelda: Tears of the Kingdom. \nНа видеоадаптерах с 4GiB видеопамяти или менее возможны вылеты при запуске этих игр. \n\nВключите, только если у вас заканчивается видеопамять в вышеупомянутых играх. \n\nРекомендуется оставить выключенным.", + "SettingsTabGraphicsPreferredGpu": "Предпочтительный видеоадаптер", + "SettingsTabGraphicsPreferredGpuTooltip": "Выберает видеоадаптер, который будет использоваться графическим бэкендом Vulkan.\n\nЭта настройка не влияет на видеоадаптер, который будет использоваться с OpenGL.\n\nЕсли вы не уверены что нужно выбрать, используйте графический процессор, помеченный как \"dGPU\". Если его нет, оставьте выбор по умолчанию.", + "SettingsAppRequiredRestartMessage": "Требуется перезапуск Ryujinx", + "SettingsGpuBackendRestartMessage": "Графический бэкенд или настройки графического процессора были изменены. Требуется перезапуск для вступления в силу изменений.", + "SettingsGpuBackendRestartSubMessage": "Перезапустить сейчас?", + "RyujinxUpdaterMessage": "Обновить Ryujinx до последней версии?", + "SettingsTabHotkeysVolumeUpHotkey": "Увеличить громкость:", + "SettingsTabHotkeysVolumeDownHotkey": "Уменьшить громкость:", + "SettingsEnableMacroHLE": "Использовать макрос высокоуровневой эмуляции видеоадаптера", + "SettingsEnableMacroHLETooltip": "Высокоуровневая эмуляции макрокода видеоадаптера.\n\nПовышает производительность, но может вызывать графические артефакты в некоторых играх.\n\nРекомендуется оставить включенным.", + "SettingsEnableColorSpacePassthrough": "Пропускать цветовое пространство", + "SettingsEnableColorSpacePassthroughTooltip": "Направляет бэкенд Vulkan на передачу информации о цвете без указания цветового пространства. Для пользователей с экранами с расширенной гаммой данная настройка приводит к получению более ярких цветов за счет снижения корректности цветопередачи.", + "VolumeShort": "Громкость", + "UserProfilesManageSaves": "Управление сохранениями", + "DeleteUserSave": "Удалить сохранения для этой игры?", + "IrreversibleActionNote": "Данное действие является необратимым.", + "SaveManagerHeading": "Редактирование сохранений для {0} ({1})", + "SaveManagerTitle": "Менеджер сохранений", + "Name": "Название", + "Size": "Размер", + "Search": "Поиск", + "UserProfilesRecoverLostAccounts": "Восстановить учетные записи", + "Recover": "Восстановление", + "UserProfilesRecoverHeading": "Были найдены сохранения для следующих аккаунтов", + "UserProfilesRecoverEmptyList": "Нет учетных записей для восстановления", + "GraphicsAATooltip": "Применимое сглаживание для рендера.\n\nFXAA размывает большую часть изображения, SMAA попытается найти \"зазубренные\" края и сгладить их.\n\nНе рекомендуется использовать вместе с масштабирующим фильтром FSR.\n\nЭта опция может быть изменена во время игры по нажатию \"Применить\" ниже; \nВы можете просто переместить окно настроек в сторону и поэкспериментировать, пока не найдёте подходящую настройку игры.\n\nРекомендуется использовать \"Нет\".", + "GraphicsAALabel": "Сглаживание:", + "GraphicsScalingFilterLabel": "Интерполяция:", + "GraphicsScalingFilterTooltip": "Фильтрация текстур, которая будет применяться при масштабировании.\n\nБилинейная хорошо работает для 3D-игр и является настройкой по умолчанию.\n\nСтупенчатая рекомендуется для пиксельных игр.\n\nFSR это фильтр резкости, который не рекомендуется использовать с FXAA или SMAA.\n\nЭта опция может быть изменена во время игры по нажатию кнопки \"Применить\" ниже; \nВы можете просто переместить окно настроек в сторону и поэкспериментировать, пока не подберете подходящие настройки для конкретной игры.\n\nРекомендуется использовать \"Билинейная\".", + "GraphicsScalingFilterBilinear": "Билинейная", + "GraphicsScalingFilterNearest": "Ступенчатая", + "GraphicsScalingFilterFsr": "FSR", + "GraphicsScalingFilterArea": "Area", + "GraphicsScalingFilterLevelLabel": "Уровень", + "GraphicsScalingFilterLevelTooltip": "Выбор режима работы FSR 1.0. Выше - четче.", + "SmaaLow": "SMAA Низкое", + "SmaaMedium": "SMAA Среднее", + "SmaaHigh": "SMAA Высокое", + "SmaaUltra": "SMAA Ультра", + "UserEditorTitle": "Редактирование пользователя", + "UserEditorTitleCreate": "Создание пользователя", + "SettingsTabNetworkInterface": "Сетевой интерфейс:", + "NetworkInterfaceTooltip": "Сетевой интерфейс, используемый для функций LAN/LDN.\n\nМожет использоваться для игры через интернет в сочетании с VPN или XLink Kai и игрой с поддержкой LAN.\n\nРекомендуется использовать \"По умолчанию\".", + "NetworkInterfaceDefault": "По умолчанию", + "PackagingShaders": "Упаковка шейдеров", + "AboutChangelogButton": "Список изменений на GitHub", + "AboutChangelogButtonTooltipMessage": "Нажмите, чтобы открыть список изменений для этой версии", + "SettingsTabNetworkMultiplayer": "Мультиплеер", + "MultiplayerMode": "Режим:", + "MultiplayerModeTooltip": "Меняет многопользовательский режим LDN.\n\nLdnMitm модифицирует функциональность локальной беспроводной/игры на одном устройстве в играх, позволяя играть с другими пользователями Ryujinx или взломанными консолями Nintendo Switch с установленным модулем ldn_mitm, находящимися в одной локальной сети друг с другом.\n\nМногопользовательская игра требует наличия у всех игроков одной и той же версии игры (т.е. Super Smash Bros. Ultimate v13.0.1 не может подключиться к v13.0.0).\n\nРекомендуется оставить отключенным.", + "MultiplayerModeDisabled": "Отключено", + "MultiplayerModeLdnMitm": "ldn_mitm", + "MultiplayerModeLdnRyu": "RyuLDN", + "MultiplayerDisableP2P": "Disable P2P Network Hosting (may increase latency)", + "MultiplayerDisableP2PTooltip": "Disable P2P network hosting, peers will proxy through the master server instead of connecting to you directly.", + "LdnPassphrase": "Network Passphrase:", + "LdnPassphraseTooltip": "You will only be able to see hosted games with the same passphrase as you.", + "LdnPassphraseInputTooltip": "Enter a passphrase in the format Ryujinx-<8 hex chars>. You will only be able to see hosted games with the same passphrase as you.", + "LdnPassphraseInputPublic": "(public)", + "GenLdnPass": "Generate Random", + "GenLdnPassTooltip": "Generates a new passphrase, which can be shared with other players.", + "ClearLdnPass": "Clear", + "ClearLdnPassTooltip": "Clears the current passphrase, returning to the public network.", + "InvalidLdnPassphrase": "Invalid Passphrase! Must be in the format \"Ryujinx-<8 hex chars>\"" +} diff --git a/src/Ryujinx/Assets/Locales/th_TH.json b/src/Ryujinx/Assets/Locales/th_TH.json new file mode 100644 index 000000000..259828583 --- /dev/null +++ b/src/Ryujinx/Assets/Locales/th_TH.json @@ -0,0 +1,868 @@ +{ + "Language": "ภาษาไทย", + "MenuBarFileOpenApplet": "เปิด Applet", + "MenuBarFileOpenAppletOpenMiiApplet": "Mii Edit Applet", + "MenuBarFileOpenAppletOpenMiiAppletToolTip": "เปิดโปรแกรม Mii Editor Applet", + "SettingsTabInputDirectMouseAccess": "เข้าถึงเมาส์ได้โดยตรง", + "SettingsTabSystemMemoryManagerMode": "โหมดจัดการหน่วยความจำ:", + "SettingsTabSystemMemoryManagerModeSoftware": "ซอฟต์แวร์", + "SettingsTabSystemMemoryManagerModeHost": "โฮสต์ (เร็ว)", + "SettingsTabSystemMemoryManagerModeHostUnchecked": "ไม่ได้ตรวจสอบโฮสต์ (เร็วที่สุด, แต่ไม่ปลอดภัย)", + "SettingsTabSystemUseHypervisor": "ใช้งาน Hypervisor", + "MenuBarFile": "ไฟล์", + "MenuBarFileOpenFromFile": "โหลดแอปพลิเคชั่นจากไฟล์", + "MenuBarFileOpenFromFileError": "ไม่พบแอปพลิเคชั่นจากไฟล์ที่เลือก", + "MenuBarFileOpenUnpacked": "โหลดเกมที่แตกไฟล์แล้ว", + "MenuBarFileLoadDlcFromFolder": "โหลด DLC จากโฟลเดอร์", + "MenuBarFileLoadTitleUpdatesFromFolder": "โหลดไฟล์อัพเดตจากโฟลเดอร์", + "MenuBarFileOpenEmuFolder": "เปิดโฟลเดอร์ Ryujinx", + "MenuBarFileOpenLogsFolder": "เปิดโฟลเดอร์ Logs", + "MenuBarFileExit": "_ออก", + "MenuBarOptions": "_ตัวเลือก", + "MenuBarOptionsToggleFullscreen": "สลับเป็นโหมดเต็มหน้าจอ", + "MenuBarOptionsStartGamesInFullscreen": "เริ่มเกมในโหมดเต็มหน้าจอ", + "MenuBarOptionsStopEmulation": "หยุดการจำลอง", + "MenuBarOptionsSettings": "_ตั้งค่า", + "MenuBarOptionsManageUserProfiles": "_จัดการโปรไฟล์ผู้ใช้งาน", + "MenuBarActions": "การดำเนินการ", + "MenuBarOptionsSimulateWakeUpMessage": "จำลองข้อความปลุก", + "MenuBarActionsScanAmiibo": "สแกนหา Amiibo", + "MenuBarTools": "_เครื่องมือ", + "MenuBarToolsInstallFirmware": "ติดตั้งเฟิร์มแวร์", + "MenuBarFileToolsInstallFirmwareFromFile": "ติดตั้งเฟิร์มแวร์จาก ไฟล์ XCI หรือ ไฟล์ ZIP", + "MenuBarFileToolsInstallFirmwareFromDirectory": "ติดตั้งเฟิร์มแวร์จากไดเร็กทอรี", + "MenuBarToolsManageFileTypes": "จัดการประเภทไฟล์", + "MenuBarToolsInstallFileTypes": "ติดตั้งประเภทไฟล์", + "MenuBarToolsUninstallFileTypes": "ถอนการติดตั้งประเภทไฟล์", + "MenuBarToolsXCITrimmer": "Trim XCI Files", + "MenuBarView": "_มุมมอง", + "MenuBarViewWindow": "ขนาดหน้าต่าง", + "MenuBarViewWindow720": "720p", + "MenuBarViewWindow1080": "1080p", + "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", + "GameListContextMenuCacheManagement": "จัดการแคช", + "GameListContextMenuCacheManagementPurgePptc": "เพิ่มคิวการสร้าง PPTC ใหม่", + "GameListContextMenuCacheManagementPurgePptcToolTip": "ให้ PPTC สร้างใหม่ในเวลาบูตเมื่อเปิดเกมครั้งถัดไป", + "GameListContextMenuCacheManagementPurgeShaderCache": "ล้างแคช แสงเงา", + "GameListContextMenuCacheManagementPurgeShaderCacheToolTip": "ลบแคช แสงเงา ของแอปพลิเคชัน", + "GameListContextMenuCacheManagementOpenPptcDirectory": "เปิดไดเรกทอรี่ PPTC", + "GameListContextMenuCacheManagementOpenPptcDirectoryToolTip": "เปิดไดเร็กทอรี่ของ แคช PPTC ในแอปพลิเคชัน", + "GameListContextMenuCacheManagementOpenShaderCacheDirectory": "เปิดไดเรกทอรี่ แคช แสงเงา", + "GameListContextMenuCacheManagementOpenShaderCacheDirectoryToolTip": "เปิดไดเรกทอรี่ของ แคช แสงเงา ในแอปพลิเคชัน", + "GameListContextMenuExtractData": "แยกส่วนข้อมูล", + "GameListContextMenuExtractDataExeFS": "ExeFS", + "GameListContextMenuExtractDataExeFSToolTip": "แยกส่วน ExeFS ออกจากการตั้งค่าปัจจุบันของแอปพลิเคชัน (รวมถึงอัปเดต)", + "GameListContextMenuExtractDataRomFS": "RomFS", + "GameListContextMenuExtractDataRomFSToolTip": "แยกส่วน RomFS ออกจากการตั้งค่าปัจจุบันของแอปพลิเคชัน (รวมถึงอัพเดต)", + "GameListContextMenuExtractDataLogo": "โลโก้", + "GameListContextMenuExtractDataLogoToolTip": "แยกส่วน โลโก้ ออกจากการตั้งค่าปัจจุบันของแอปพลิเคชัน (รวมถึงอัปเดต)", + "GameListContextMenuCreateShortcut": "สร้างทางลัดของแอปพลิเคชัน", + "GameListContextMenuCreateShortcutToolTip": "สร้างทางลัดบนเดสก์ท็อปสำหรับใช้แอปพลิเคชันที่เลือก", + "GameListContextMenuCreateShortcutToolTipMacOS": "สร้างทางลัดในโฟลเดอร์ Applications ของ macOS สำหรับใช้แอปพลิเคชันที่เลือก", + "GameListContextMenuOpenModsDirectory": "เปิดไดเร็กทอรี่ Mods", + "GameListContextMenuOpenModsDirectoryToolTip": "เปิดไดเร็กทอรี่ Mods ของแอปพลิเคชัน", + "GameListContextMenuOpenSdModsDirectory": "เปิดไดเร็กทอรี่ Mods Atmosphere", + "GameListContextMenuOpenSdModsDirectoryToolTip": "เปิดไดเร็กทอรี่ Atmosphere ของการ์ด SD สำรองซึ่งมี Mods ของแอปพลิเคชัน ซึ่งมีประโยชน์สำหรับ Mods ที่บรรจุมากับฮาร์ดแวร์จริง", + "GameListContextMenuTrimXCI": "Check and Trim XCI File", + "GameListContextMenuTrimXCIToolTip": "Check and Trim XCI File to Save Disk Space", + "StatusBarGamesLoaded": "เกมส์โหลดแล้ว {0}/{1}", + "StatusBarSystemVersion": "เวอร์ชั่นของระบบ: {0}", + "StatusBarXCIFileTrimming": "Trimming XCI File '{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 Rich Presence", + "SettingsTabGeneralCheckUpdatesOnLaunch": "ตรวจหาการอัปเดตเมื่อเปิดโปรแกรม", + "SettingsTabGeneralShowConfirmExitDialog": "แสดง \"ปุ่มยืนยันการออก\" เมื่อออกเกม", + "SettingsTabGeneralRememberWindowState": "จดจำ ขนาดหน้าต่างแอพพลิเคชั่น/คำแหน่ง", + "SettingsTabGeneralShowTitleBar": "Show Title Bar (Requires restart)", + "SettingsTabGeneralHideCursor": "ซ่อน เคอร์เซอร์:", + "SettingsTabGeneralHideCursorNever": "ไม่ต้อง", + "SettingsTabGeneralHideCursorOnIdle": "เมื่อไม่ได้ใช้งาน", + "SettingsTabGeneralHideCursorAlways": "ตลอดเวลา", + "SettingsTabGeneralGameDirectories": "ไดเรกทอรี่ของเกม", + "SettingsTabGeneralAutoloadDirectories": "โหลดไดเรกทอรี DLC/ไฟล์อัปเดต อัตโนมัติ", + "SettingsTabGeneralAutoloadNote": "DLC and Updates which refer to missing files will be unloaded automatically", + "SettingsTabGeneralAdd": "เพิ่ม", + "SettingsTabGeneralRemove": "เอาออก", + "SettingsTabSystem": "ระบบ", + "SettingsTabSystemCore": "แกนกลาง", + "SettingsTabSystemSystemRegion": "ภูมิภาคของระบบ:", + "SettingsTabSystemSystemRegionJapan": "ญี่ปุ่น", + "SettingsTabSystemSystemRegionUSA": "สหรัฐอเมริกา", + "SettingsTabSystemSystemRegionEurope": "ยุโรป", + "SettingsTabSystemSystemRegionAustralia": "ออสเตรเลีย", + "SettingsTabSystemSystemRegionChina": "จีน", + "SettingsTabSystemSystemRegionKorea": "เกาหลี", + "SettingsTabSystemSystemRegionTaiwan": "ไต้หวัน", + "SettingsTabSystemSystemLanguage": "ภาษาของระบบ:", + "SettingsTabSystemSystemLanguageJapanese": "ญี่ปุ่น", + "SettingsTabSystemSystemLanguageAmericanEnglish": "อังกฤษ (อเมริกัน)", + "SettingsTabSystemSystemLanguageFrench": "ฝรั่งเศส", + "SettingsTabSystemSystemLanguageGerman": "เยอรมัน", + "SettingsTabSystemSystemLanguageItalian": "อิตาลี", + "SettingsTabSystemSystemLanguageSpanish": "สเปน", + "SettingsTabSystemSystemLanguageChinese": "จีน", + "SettingsTabSystemSystemLanguageKorean": "เกาหลี", + "SettingsTabSystemSystemLanguageDutch": "ดัตช์", + "SettingsTabSystemSystemLanguagePortuguese": "โปรตุเกส", + "SettingsTabSystemSystemLanguageRussian": "รัสเซีย", + "SettingsTabSystemSystemLanguageTaiwanese": "จีนตัวเต็ม (ไต้หวัน)", + "SettingsTabSystemSystemLanguageBritishEnglish": "อังกฤษ (บริติช)", + "SettingsTabSystemSystemLanguageCanadianFrench": "ฝรั่งเศส (แคนาดา)", + "SettingsTabSystemSystemLanguageLatinAmericanSpanish": "สเปน (ลาตินอเมริกา)", + "SettingsTabSystemSystemLanguageSimplifiedChinese": "จีน (ตัวย่อ)", + "SettingsTabSystemSystemLanguageTraditionalChinese": "จีน (ดั้งเดิม)", + "SettingsTabSystemSystemTimeZone": "เขตเวลาของระบบ:", + "SettingsTabSystemSystemTime": "เวลาของระบบ:", + "SettingsTabSystemEnableVsync": "VSync", + "SettingsTabSystemEnablePptc": "PPTC (แคชโปรไฟล์การแปลแบบถาวร)", + "SettingsTabSystemEnableLowPowerPptc": "PPTC แบบพลังงานตํ่า", + "SettingsTabSystemEnableFsIntegrityChecks": "ตรวจสอบความถูกต้องของ FS", + "SettingsTabSystemAudioBackend": "ระบบเสียงเบื้องหลัง:", + "SettingsTabSystemAudioBackendDummy": "Dummy", + "SettingsTabSystemAudioBackendOpenAL": "OpenAL", + "SettingsTabSystemAudioBackendSoundIO": "SoundIO", + "SettingsTabSystemAudioBackendSDL2": "SDL2", + "SettingsTabSystemHacks": "แฮ็ก", + "SettingsTabSystemHacksNote": "อาจทำให้เกิดข้อผิดพลาดได้", + "SettingsTabSystemDramSize": "ใช้หน่วยความจำสำรอง (โหมดนักพัฒนา)", + "SettingsTabSystemDramSize4GiB": "4GiB", + "SettingsTabSystemDramSize6GiB": "6GiB", + "SettingsTabSystemDramSize8GiB": "8GiB", + "SettingsTabSystemDramSize12GiB": "12GiB", + "SettingsTabSystemIgnoreMissingServices": "เมินเฉยบริการที่หายไป", + "SettingsTabSystemIgnoreApplet": "เมินเฉย Applet", + "SettingsTabGraphics": "กราฟฟิก", + "SettingsTabGraphicsAPI": "API กราฟฟิก", + "SettingsTabGraphicsEnableShaderCache": "เปิดใช้งาน แคชแสงเงา", + "SettingsTabGraphicsAnisotropicFiltering": "ตัวกรองแบบ Anisotropic:", + "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": "เปิดใช้งานการบันทึกประวัติ", + "SettingsTabLoggingEnableInfoLogs": "เปิดใช้งานการบันทึกประวัติการใช้งาน", + "SettingsTabLoggingEnableWarningLogs": "เปิดใช้งานการบันทึกประวัติคำเตือน", + "SettingsTabLoggingEnableErrorLogs": "เปิดใช้งานการบันทึกประวัติข้อผิดพลาด", + "SettingsTabLoggingEnableTraceLogs": "เปิดใช้งานการบันทึกประวัติการติดตาม", + "SettingsTabLoggingEnableGuestLogs": "เปิดใช้งานการบันทึกประวัติผู้เยี่ยมชม", + "SettingsTabLoggingEnableFsAccessLogs": "เปิดใช้งานการบันทึกประวัติการเข้าถึง Fs", + "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": "โปรคอนโทรลเลอร์", + "ControllerSettingsControllerTypeJoyConPair": "จับคู่ จอยคอน", + "ControllerSettingsControllerTypeJoyConLeft": "จอยคอน ด้านซ้าย", + "ControllerSettingsControllerTypeJoyConRight": "จอยคอน ด้านขวา", + "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": "ช่องเสียบ จอยคอน ด้านขวา:", + "ControllerSettingsMotionServerHost": "เจ้าของเซิร์ฟเวอร์:", + "ControllerSettingsMotionGyroSensitivity": "ความไวของ Gyro:", + "ControllerSettingsMotionGyroDeadzone": "ส่วนไม่ทำงานของ Gyro:", + "ControllerSettingsSave": "บันทึก", + "ControllerSettingsClose": "ปิด", + "KeyUnknown": "ไม่รู้จัก", + "KeyShiftLeft": "Shift Left", + "KeyShiftRight": "Shift Right", + "KeyControlLeft": "Ctrl Left", + "KeyMacControlLeft": "⌃ Left", + "KeyControlRight": "Ctrl Right", + "KeyMacControlRight": "⌃ Right", + "KeyAltLeft": "Alt Left", + "KeyMacAltLeft": "⌥ Left", + "KeyAltRight": "Alt Right", + "KeyMacAltRight": "⌥ Right", + "KeyWinLeft": "⊞ Left", + "KeyMacWinLeft": "⌘ Left", + "KeyWinRight": "⊞ Right", + "KeyMacWinRight": "⌘ Right", + "KeyMenu": "Menu", + "KeyUp": "Up", + "KeyDown": "Down", + "KeyLeft": "Left", + "KeyRight": "Right", + "KeyEnter": "Enter", + "KeyEscape": "Escape", + "KeySpace": "Space", + "KeyTab": "Tab", + "KeyBackSpace": "Backspace", + "KeyInsert": "Insert", + "KeyDelete": "Delete", + "KeyPageUp": "Page Up", + "KeyPageDown": "Page Down", + "KeyHome": "Home", + "KeyEnd": "End", + "KeyCapsLock": "Caps Lock", + "KeyScrollLock": "Scroll Lock", + "KeyPrintScreen": "Print Screen", + "KeyPause": "Pause", + "KeyNumLock": "Num Lock", + "KeyClear": "Clear", + "KeyKeypad0": "Keypad 0", + "KeyKeypad1": "Keypad 1", + "KeyKeypad2": "Keypad 2", + "KeyKeypad3": "Keypad 3", + "KeyKeypad4": "Keypad 4", + "KeyKeypad5": "Keypad 5", + "KeyKeypad6": "Keypad 6", + "KeyKeypad7": "Keypad 7", + "KeyKeypad8": "Keypad 8", + "KeyKeypad9": "Keypad 9", + "KeyKeypadDivide": "Keypad Divide", + "KeyKeypadMultiply": "Keypad Multiply", + "KeyKeypadSubtract": "Keypad Subtract", + "KeyKeypadAdd": "Keypad Add", + "KeyKeypadDecimal": "Keypad Decimal", + "KeyKeypadEnter": "Keypad Enter", + "KeyNumber0": "0", + "KeyNumber1": "1", + "KeyNumber2": "2", + "KeyNumber3": "3", + "KeyNumber4": "4", + "KeyNumber5": "5", + "KeyNumber6": "6", + "KeyNumber7": "7", + "KeyNumber8": "8", + "KeyNumber9": "9", + "KeyTilde": "~", + "KeyGrave": "`", + "KeyMinus": "-", + "KeyPlus": "+", + "KeyBracketLeft": "[", + "KeyBracketRight": "]", + "KeySemicolon": ";", + "KeyQuote": "\"", + "KeyComma": ",", + "KeyPeriod": ".", + "KeySlash": "/", + "KeyBackSlash": "\\", + "KeyUnbound": "Unbound", + "GamepadLeftStick": "L Stick Button", + "GamepadRightStick": "R Stick Button", + "GamepadLeftShoulder": "Left Shoulder", + "GamepadRightShoulder": "Right Shoulder", + "GamepadLeftTrigger": "Left Trigger", + "GamepadRightTrigger": "Right Trigger", + "GamepadDpadUp": "Up", + "GamepadDpadDown": "Down", + "GamepadDpadLeft": "Left", + "GamepadDpadRight": "Right", + "GamepadMinus": "-", + "GamepadPlus": "+", + "GamepadGuide": "Guide", + "GamepadMisc1": "Misc", + "GamepadPaddle1": "Paddle 1", + "GamepadPaddle2": "Paddle 2", + "GamepadPaddle3": "Paddle 3", + "GamepadPaddle4": "Paddle 4", + "GamepadTouchpad": "Touchpad", + "GamepadSingleLeftTrigger0": "Left Trigger 0", + "GamepadSingleRightTrigger0": "Right Trigger 0", + "GamepadSingleLeftTrigger1": "Left Trigger 1", + "GamepadSingleRightTrigger1": "Right Trigger 1", + "StickLeft": "Left Stick", + "StickRight": "Right Stick", + "UserProfilesSelectedUserProfile": "เลือกโปรไฟล์ผู้ใช้งาน:", + "UserProfilesSaveProfileName": "บันทึกชื่อโปรไฟล์", + "UserProfilesChangeProfileImage": "เปลี่ยนรูปโปรไฟล์", + "UserProfilesAvailableUserProfiles": "โปรไฟล์ผู้ใช้ที่ใช้งานได้:", + "UserProfilesAddNewProfile": "สร้างโปรไฟล์ใหม่", + "UserProfilesDelete": "ลบ", + "UserProfilesClose": "ปิด", + "ProfileNameSelectionWatermark": "เลือก ชื่อเล่น", + "ProfileImageSelectionTitle": "เลือก รูปโปรไฟล์ ของคุณ", + "ProfileImageSelectionHeader": "เลือก รูปโปรไฟล์", + "ProfileImageSelectionNote": "คุณสามารถนำเข้ารูปโปรไฟล์ที่กำหนดเองได้ หรือ เลือกรูปที่มีจากระบบ", + "ProfileImageSelectionImportImage": "นำเข้า ไฟล์รูปภาพ", + "ProfileImageSelectionSelectAvatar": "เลือก รูปอวาต้า จากระบบ", + "InputDialogTitle": "กล่องโต้ตอบการป้อนข้อมูล", + "InputDialogOk": "ตกลง", + "InputDialogCancel": "ยกเลิก", + "InputDialogCancelling": "Cancelling", + "InputDialogClose": "Close", + "InputDialogAddNewProfileTitle": "เลือก ชื่อโปรไฟล์", + "InputDialogAddNewProfileHeader": "กรุณาใส่ชื่อโปรไฟล์", + "InputDialogAddNewProfileSubtext": "(ความยาวสูงสุด: {0})", + "AvatarChoose": "เลือก รูปอวาต้า ของคุณ", + "AvatarSetBackgroundColor": "ตั้งค่าสีพื้นหลัง", + "AvatarClose": "ปิด", + "ControllerSettingsLoadProfileToolTip": "โหลด โปรไฟล์", + "ControllerSettingsViewProfileToolTip": "View Profile", + "ControllerSettingsAddProfileToolTip": "เพิ่ม โปรไฟล์", + "ControllerSettingsRemoveProfileToolTip": "ลบ โปรไฟล์", + "ControllerSettingsSaveProfileToolTip": "บันทึก โปรไฟล์", + "MenuBarFileToolsTakeScreenshot": "ถ่ายภาพหน้าจอ", + "MenuBarFileToolsHideUi": "ซ่อน UI", + "GameListContextMenuRunApplication": "เปิดใช้งานแอปพลิเคชัน", + "GameListContextMenuToggleFavorite": "สลับรายการโปรด", + "GameListContextMenuToggleFavoriteToolTip": "สลับสถานะเกมที่ชื่นชอบ", + "SettingsTabGeneralTheme": "ธีม:", + "SettingsTabGeneralThemeAuto": "อัตโนมัติ", + "SettingsTabGeneralThemeDark": "มืด", + "SettingsTabGeneralThemeLight": "สว่าง", + "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": "กำลังแตกไฟล์ {0} จากส่วน {1}...", + "DialogNcaExtractionTitle": "เครื่องมือแตกไฟล์ของ NCA", + "DialogNcaExtractionMainNcaNotFoundErrorMessage": "เกิดความล้มเหลวในการแตกไฟล์เนื่องจากไม่พบ NCA หลักในไฟล์ที่เลือก", + "DialogNcaExtractionCheckLogErrorMessage": "เกิดความล้มเหลวในการแตกไฟล์ โปรดอ่านไฟล์บันทึกประวัติเพื่อดูข้อมูลเพิ่มเติม", + "DialogNcaExtractionSuccessMessage": "การแตกไฟล์เสร็จสมบูรณ์แล้ว", + "DialogUpdaterConvertFailedMessage": "ไม่สามารถแปลงเวอร์ชั่น Ryujinx ปัจจุบันได้", + "DialogUpdaterCancelUpdateMessage": "ยกเลิกการอัพเดต!", + "DialogUpdaterAlreadyOnLatestVersionMessage": "คุณกำลังใช้ Ryujinx เวอร์ชั่นที่อัปเดตล่าสุด!", + "DialogUpdaterFailedToGetVersionMessage": "เกิดข้อผิดพลาดขณะพยายามรับข้อมูลเวอร์ชั่นจาก GitHub Release ปัญหานี้อาจเกิดขึ้นได้หากมีการรวบรวมเวอร์ชั่นใหม่โดย GitHub โปรดลองอีกครั้งในอีกไม่กี่นาทีข้างหน้า", + "DialogUpdaterConvertFailedGithubMessage": "ไม่สามารถแปลงเวอร์ชั่น Ryujinx ที่ได้รับจาก Github Release", + "DialogUpdaterDownloadingMessage": "กำลังดาวน์โหลดอัปเดต...", + "DialogUpdaterExtractionMessage": "กำลังแตกไฟล์อัปเดต...", + "DialogUpdaterRenamingMessage": "กำลังลบไฟล์เก่า...", + "DialogUpdaterAddingFilesMessage": "กำลังเพิ่มไฟล์อัปเดตใหม่...", + "DialogUpdaterShowChangelogMessage": "Show Changelog", + "DialogUpdaterCompleteMessage": "อัปเดตเสร็จสมบูรณ์แล้ว!", + "DialogUpdaterRestartMessage": "คุณต้องการรีสตาร์ท Ryujinx ตอนนี้หรือไม่?", + "DialogUpdaterNoInternetMessage": "คุณไม่ได้เชื่อมต่อกับอินเทอร์เน็ต!", + "DialogUpdaterNoInternetSubMessage": "โปรดตรวจสอบว่าคุณมีการเชื่อมต่ออินเทอร์เน็ตว่ามีการใช้งานได้หรือไม่!", + "DialogUpdaterDirtyBuildMessage": "คุณไม่สามารถอัปเดต Dirty build ของ Ryujinx ได้!", + "DialogUpdaterDirtyBuildSubMessage": "โปรดดาวน์โหลด Ryujinx ได้ที่ https://ryujinx.app/download หากคุณกำลังมองหาเวอร์ชั่นที่รองรับ", + "DialogRestartRequiredMessage": "จำเป็นต้องรีสตาร์ทเพื่อให้การอัพเดตสามารถให้งานได้", + "DialogThemeRestartMessage": "บันทึกธีมแล้ว จำเป็นต้องรีสตาร์ทเพื่อใช้ธีม", + "DialogThemeRestartSubMessage": "คุณต้องการรีสตาร์ทหรือไม่?", + "DialogFirmwareInstallEmbeddedMessage": "คุณต้องการติดตั้งเฟิร์มแวร์ที่ฝังอยู่ในเกมนี้หรือไม่? (เฟิร์มแวร์ {0})", + "DialogFirmwareInstallEmbeddedSuccessMessage": "ไม่พบเฟิร์มแวร์ที่ติดตั้งไว้ แต่ Ryujinx จะติดตั้งเฟิร์มแวร์ได้ {0} จากเกมที่ให้มา\nขณะนี้โปรแกรมจำลองจะเริ่มทำงาน", + "DialogFirmwareNoFirmwareInstalledMessage": "ไม่มีการติดตั้งเฟิร์มแวร์", + "DialogFirmwareInstalledMessage": "เฟิร์มแวร์ {0} ติดตั้งแล้ว", + "DialogInstallFileTypesSuccessMessage": "ติดตั้งตามประเภทของไฟล์สำเร็จแล้ว!", + "DialogInstallFileTypesErrorMessage": "ติดตั้งตามประเภทของไฟล์ไม่สำเร็จ", + "DialogUninstallFileTypesSuccessMessage": "ถอนการติดตั้งตามประเภทของไฟล์สำเร็จแล้ว!", + "DialogUninstallFileTypesErrorMessage": "ไม่สามารถถอนการติดตั้งตามประเภทของไฟล์ได้", + "DialogOpenSettingsWindowLabel": "เปิดหน้าต่างการตั้งค่า", + "DialogOpenXCITrimmerWindowLabel": "XCI Trimmer Window", + "DialogControllerAppletTitle": "คอนโทรลเลอร์ Applet", + "DialogMessageDialogErrorExceptionMessage": "เกิดข้อผิดพลาดในการแสดงกล่องโต้ตอบข้อความ: {0}", + "DialogSoftwareKeyboardErrorExceptionMessage": "เกิดข้อผิดพลาดในการแสดงซอฟต์แวร์แป้นพิมพ์: {0}", + "DialogErrorAppletErrorExceptionMessage": "เกิดข้อผิดพลาดในการแสดงกล่องโต้ตอบ ข้อผิดพลาดของ Applet: {0}", + "DialogUserErrorDialogMessage": "{0}: {1}", + "DialogUserErrorDialogInfoMessage": "\nสำหรับข้อมูลเพิ่มเติมเกี่ยวกับวิธีแก้ไขข้อผิดพลาดนี้ โปรดทำตามคำแนะนำในการตั้งค่าของเรา", + "DialogUserErrorDialogTitle": "ข้อผิดพลาด Ryujinx ({0})", + "DialogAmiiboApiTitle": "Amiibo API", + "DialogAmiiboApiFailFetchMessage": "เกิดข้อผิดพลาดขณะเรียกข้อมูลจาก API", + "DialogAmiiboApiConnectErrorMessage": "ไม่สามารถเชื่อมต่อกับเซิร์ฟเวอร์ Amiibo API บางบริการอาจหยุดทำงาน หรือไม่คุณต้องทำการตรวจสอบว่าอินเทอร์เน็ตของคุณอยู่ในสถานะเชื่อมต่ออยู่หรือไม่", + "DialogProfileInvalidProfileErrorMessage": "โปรไฟล์ {0} ไม่สามารถทำงานได้กับระบบกำหนดค่าอินพุตปัจจุบัน", + "DialogProfileDefaultProfileOverwriteErrorMessage": "โปรไฟล์เริ่มต้นไม่สามารถเขียนทับได้", + "DialogProfileDeleteProfileTitle": "กำลังลบโปรไฟล์", + "DialogProfileDeleteProfileMessage": "การดำเนินการนี้ไม่สามารถย้อนกลับได้ คุณแน่ใจหรือไม่ว่าต้องการดำเนินการต่อหรือไม่?", + "DialogWarning": "คำเตือน", + "DialogPPTCDeletionMessage": "คุณกำลังตั้งค่าให้มีการสร้าง PPTC ใหม่ในการบูตครั้งถัดไป:\n\n{0}\n\nคุณแน่ใจหรือไม่ว่าต้องการดำเนินการต่อหรือไม่?", + "DialogPPTCDeletionErrorMessage": "มีข้อผิดพลาดในการล้างแคช PPTC {0}: {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": "คุณต้องการบันทึกหรือไม่?", + "DialogLoadFileErrorMessage": "{0} ไฟล์เกิดข้อผิดพลาด: {1}", + "DialogModAlreadyExistsMessage": "มีม็อดนี้อยู่แล้ว", + "DialogModInvalidMessage": "ไดเร็กทอรีที่ระบุไม่มี ม็อดอยู่!", + "DialogModDeleteNoParentMessage": "ไม่สามารถลบ: ไม่พบไดเร็กทอรีหลักสำหรับ ม็อด \"{0}\"!", + "DialogDlcNoDlcErrorMessage": "ไฟล์ที่ระบุไม่มี DLC สำหรับชื่อที่เลือก!", + "DialogPerformanceCheckLoggingEnabledMessage": "คุณได้เปิดใช้งานการบันทึกการติดตาม ซึ่งออกแบบมาเพื่อให้นักพัฒนาใช้เท่านั้น", + "DialogPerformanceCheckLoggingEnabledConfirmMessage": "เพื่อประสิทธิภาพสูงสุด ขอแนะนำให้ปิดใช้งานการบันทึกการติดตาม คุณต้องการปิดใช้การบันทึกการติดตามตอนนี้หรือไม่?", + "DialogPerformanceCheckShaderDumpEnabledMessage": "คุณได้เปิดใช้งาน การดัมพ์เชเดอร์ ซึ่งออกแบบมาเพื่อให้นักพัฒนาใช้งานเท่านั้น", + "DialogPerformanceCheckShaderDumpEnabledConfirmMessage": "เพื่อประสิทธิภาพสูงสุด ขอแนะนำให้ปิดใช้การดัมพ์เชเดอร์ คุณต้องการปิดการใช้งานการ ดัมพ์เชเดอร์ ตอนนี้หรือไม่?", + "DialogLoadAppGameAlreadyLoadedMessage": "ทำการโหลดเกมเรียบร้อยแล้ว", + "DialogLoadAppGameAlreadyLoadedSubMessage": "โปรดหยุดการจำลอง หรือปิดโปรแกรมจำลองก่อนที่จะเปิดเกมอื่น", + "DialogUpdateAddUpdateErrorMessage": "ไฟล์ที่ระบุไม่มีการอัพเดตสำหรับชื่อเรื่องที่เลือก!", + "DialogSettingsBackendThreadingWarningTitle": "คำเตือน - การทำเธรดแบ็กเอนด์", + "DialogSettingsBackendThreadingWarningMessage": "Ryujinx ต้องรีสตาร์ทหลังจากเปลี่ยนตัวเลือกนี้จึงจะใช้งานได้อย่างสมบูรณ์ คุณอาจต้องปิดการใช้งาน มัลติเธรด ของไดรเวอร์ของคุณด้วยตนเองเมื่อใช้ Ryujinx ทั้งนี้ขึ้นอยู่กับแพลตฟอร์มของคุณ", + "DialogModManagerDeletionWarningMessage": "คุณกำลังจะลบ ม็อด: {0}\n\nคุณแน่ใจหรือไม่ว่าต้องการดำเนินการต่อ?", + "DialogModManagerDeletionAllWarningMessage": "คุณกำลังจะลบม็อดทั้งหมดสำหรับชื่อนี้\n\nคุณแน่ใจหรือไม่ว่าต้องการดำเนินการต่อ?", + "SettingsTabGraphicsFeaturesOptions": "คุณสมบัติ", + "SettingsTabGraphicsBackendMultithreading": "มัลติเธรด กราฟิกเบื้องหลัง:", + "CommonAuto": "อัตโนมัติ", + "CommonOff": "ปิดการใช้งาน", + "CommonOn": "เปิดใช้งาน", + "InputDialogYes": "ใช่", + "InputDialogNo": "ไม่ใช่", + "DialogProfileInvalidProfileNameErrorMessage": "ชื่อไฟล์ประกอบด้วยอักขระที่ไม่ถูกต้อง กรุณาลองอีกครั้ง", + "MenuBarOptionsPauseEmulation": "หยุดชั่วคราว", + "MenuBarOptionsResumeEmulation": "ดำเนินการต่อ", + "AboutUrlTooltipMessage": "คลิกเพื่อเปิดเว็บไซต์ Ryujinx บนเบราว์เซอร์เริ่มต้นของคุณ", + "AboutDisclaimerMessage": "ทางผู้พัฒนาโปรแกรม Ryujinx ไม่มีส่วนเกี่ยวข้องกับทางบริษัท Nintendo™\nหรือพันธมิตรใดๆ ทั้งสิ้น!", + "AboutAmiiboDisclaimerMessage": "AmiiboAPI (www.amiiboapi.com) ถูกใช้\nในการจำลอง อะมิโบ ของเรา", + "AboutPatreonUrlTooltipMessage": "คลิกเพื่อเปิดหน้า Patreon ของ Ryujinx บนเบราว์เซอร์เริ่มต้นของคุณ", + "AboutGithubUrlTooltipMessage": "คลิกเพื่อเปิดหน้า Github ของ Ryujinx บนเบราว์เซอร์เริ่มต้นของคุณ", + "AboutDiscordUrlTooltipMessage": "คลิกเพื่อเปิดคำเชิญเข้าสู่เซิร์ฟเวอร์ Discord ของ Ryujinx บนเบราว์เซอร์เริ่มต้นของคุณ", + "AboutTwitterUrlTooltipMessage": "คลิกเพื่อเปิดหน้าเพจ Twitter ของ Ryujinx บนเบราว์เซอร์เริ่มต้นของคุณ", + "AboutRyujinxAboutTitle": "เกี่ยวกับ:", + "AboutRyujinxAboutContent": "Ryujinx เป็นอีมูเลเตอร์สำหรับ Nintendo Switch™\nโปรดสนับสนุนเราบน Patreon\nรับข่าวสารล่าสุดทั้งหมดบน Twitter หรือ Discord ของเรา\nนักพัฒนาที่สนใจจะมีส่วนร่วมสามารถดูข้อมูลเพิ่มเติมได้ที่ GitHub หรือ Discord ของเรา", + "AboutRyujinxMaintainersTitle": "ได้รับการดูแลโดย:", + "AboutRyujinxMaintainersContentTooltipMessage": "คลิกเพื่อเปิดหน้าผู้มีส่วนร่วมบนเบราว์เซอร์เริ่มต้นของคุณ", + "AboutRyujinxSupprtersTitle": "ผู้สนับสนุนบน Patreon:", + "AmiiboSeriesLabel": "Amiibo Series", + "AmiiboCharacterLabel": "ตัวละคร", + "AmiiboScanButtonLabel": "สแกนเลย", + "AmiiboOptionsShowAllLabel": "แสดง Amiibo ทั้งหมด", + "AmiiboOptionsUsRandomTagLabel": "แฮ็ค: สุ่มแท็ก Uuid", + "DlcManagerTableHeadingEnabledLabel": "เปิดใช้งานแล้ว", + "DlcManagerTableHeadingTitleIdLabel": "ชื่อไอดี", + "DlcManagerTableHeadingContainerPathLabel": "คอนเทนเนอร์เก็บไฟล์", + "DlcManagerTableHeadingFullPathLabel": "ที่เก็บไฟล์แบบเต็ม", + "DlcManagerRemoveAllButton": "ลบทั้งหมด", + "DlcManagerEnableAllButton": "เปิดใช้งานทั้งหมด", + "DlcManagerDisableAllButton": "ปิดใช้งานทั้งหมด", + "ModManagerDeleteAllButton": "ลบทั้งหมด", + "MenuBarOptionsChangeLanguage": "เปลี่ยนภาษา", + "MenuBarShowFileTypes": "แสดงประเภทของไฟล์", + "CommonSort": "เรียงลำดับ", + "CommonShowNames": "แสดงชื่อ", + "CommonFavorite": "สิ่งที่ชื่นชอบ", + "OrderAscending": "จากน้อยไปมาก", + "OrderDescending": "จากมากไปน้อย", + "SettingsTabGraphicsFeatures": "คุณสมบัติ และ การเพิ่มประสิทธิภาพ", + "ErrorWindowTitle": "หน้าต่างแสดงข้อผิดพลาด", + "ToggleDiscordTooltip": "เลือกว่าจะแสดง Ryujinx ในกิจกรรม Discord \"ที่กำลังเล่นอยู่\" ของคุณหรือไม่?", + "AddGameDirBoxTooltip": "ป้อนไดเรกทอรี่เกมที่จะทำการเพิ่มลงในรายการ", + "AddGameDirTooltip": "เพิ่มไดเรกทอรี่เกมลงในรายการ", + "RemoveGameDirTooltip": "ลบไดเรกทอรี่เกมที่เลือก", + "AddAutoloadDirBoxTooltip": "ป้อนไดเร็กทอรีสำหรับโหลดอัตโนมัติเพื่อเพิ่มลงในรายการ", + "AddAutoloadDirTooltip": "ป้อนไดเร็กทอรีสำหรับโหลดอัตโนมัติเพื่อเพิ่มลงในรายการ", + "RemoveAutoloadDirTooltip": "ลบไดเรกทอรีสำหรับโหลดอัตโนมัติที่เลือก", + "CustomThemeCheckTooltip": "ใช้ธีม Avalonia แบบกำหนดเองสำหรับ GUI เพื่อเปลี่ยนรูปลักษณ์ของเมนูโปรแกรมจำลอง", + "CustomThemePathTooltip": "ไปยังที่เก็บไฟล์ธีม GUI แบบกำหนดเอง", + "CustomThemeBrowseTooltip": "เรียกดูธีม GUI ที่กำหนดเอง", + "DockModeToggleTooltip": "ด็อกโหมด ทำให้ระบบจำลองการทำงานเสมือน Nintendo ที่กำลังเชื่อมต่ออยู่ด็อก สิ่งนี้จะปรับปรุงความเสถียรภาพของกราฟิกในเกมส่วนใหญ่ ในทางกลับกัน การปิดใช้จะทำให้ระบบจำลองทำงานเหมือนกับ Nintendo Switch แบบพกพา ส่งผลให้คุณภาพกราฟิกลดลง\n\nแนะนำกำหนดค่าควบคุมของผู้เล่น 1 หากวางแผนที่จะใช้ด็อกโหมด กำหนดค่าการควบคุมแบบ แฮนด์เฮลด์ หากวางแผนที่จะใช้โหมดแฮนด์เฮลด์\n\nเปิดทิ้งไว้หากคุณไม่แน่ใจ", + "DirectKeyboardTooltip": "รองรับการเข้าถึงแป้นพิมพ์โดยตรง (HID) ให้เกมเข้าถึงคีย์บอร์ดของคุณเป็นอุปกรณ์ป้อนข้อความ\n\nใช้งานได้กับเกมที่รองรับการใช้งานคีย์บอร์ดบนฮาร์ดแวร์ของ Switch เท่านั้น\n\nหากคุณไม่แน่ใจให้ปิดใช้งานไว้", + "DirectMouseTooltip": "รองรับการเข้าถึงเมาส์โดยตรง (HID) ให้เกมเข้าถึงเมาส์ของคุณเป็นอุปกรณ์ชี้ตำแหน่ง\n\nใช้งานได้เฉพาะกับเกมที่รองรับการควบคุมเมาส์บนฮาร์ดแวร์ของ Switch เท่านั้น ซึ่งมีอยู่ไม่มากนัก\n\nเมื่อเปิดใช้งาน ฟังก์ชั่นหน้าจอสัมผัสอาจไม่ทำงาน\n\nหากคุณไม่แน่ใจให้ปิดใช้งานไว้", + "RegionTooltip": "เปลี่ยนภูมิภาคของระบบ", + "LanguageTooltip": "เปลี่ยนภาษาของระบบ", + "TimezoneTooltip": "เปลี่ยนโซนเวลาของระบบ", + "TimeTooltip": "เปลี่ยนเวลาของระบบ", + "VSyncToggleTooltip": "Vertical Sync ของคอนโซลจำลอง โดยพื้นฐานแล้วเป็นตัวจำกัดเฟรมสำหรับเกมส่วนใหญ่ การปิดใช้งานอาจทำให้เกมทำงานด้วยความเร็วสูงขึ้น หรือทำให้หน้าจอการโหลดใช้เวลานานขึ้นหรือค้าง\n\nสามารถสลับได้ในเกมด้วยปุ่มลัดตามที่คุณต้องการ (F1 เป็นค่าเริ่มต้น) เราขอแนะนำให้ทำเช่นนี้หากคุณวางแผนที่จะปิดการใช้งาน\n\nเปิดทิ้งไว้หากคุณไม่แน่ใจ", + "PptcToggleTooltip": "บันทึกฟังก์ชั่น JIT ที่แปลแล้ว ดังนั้นจึงไม่จำเป็นต้องแปลทุกครั้งที่โหลดเกม\n\nลดอาการกระตุกและเร่งความเร็วการบูตได้อย่างมากหลังจากการบูตครั้งแรกของเกม\n\nเปิดทิ้งไว้หากคุณไม่แน่ใจ", + "LowPowerPptcToggleTooltip": "โหลด PPTC โดยใช้หนึ่งในสามของจำนวนคอร์", + "FsIntegrityToggleTooltip": "ตรวจสอบไฟล์ที่เสียหายเมื่อบูตเกม และหากตรวจพบไฟล์ที่เสียหาย จะแสดงข้อผิดพลาดของแฮชในบันทึก\n\nไม่มีผลกระทบต่อประสิทธิภาพการทำงานและมีไว้เพื่อช่วยในการแก้ไขปัญหา\n\nเปิดทิ้งไว้หากคุณไม่แน่ใจ", + "AudioBackendTooltip": "เปลี่ยนแบ็กเอนด์ที่ใช้ในการเรนเดอร์เสียง\n\nแนะนำเป็น SDL2 ในขณะที่ OpenAL และ SoundIO ถูกใช้เป็นทางเลือกสำรอง ดัมมี่จะไม่มีเสียง\n\nตั้งค่าเป็น SDL2 หากคุณไม่แน่ใจ", + "MemoryManagerTooltip": "เปลี่ยนวิธีการเข้าถึงหน่วยความจำของผู้เยี่ยมชม ส่งผลอย่างมากต่อประสิทธิภาพการทำงานของ CPU ที่จำลอง\n\nตั้งค่าเป็น ไม่ได้ตรวจสอบโฮสต์ หากคุณไม่แน่ใจ", + "MemoryManagerSoftwareTooltip": "ใช้ตารางหน้าซอฟต์แวร์สำหรับการแปลที่อยู่ ความแม่นยำสูงสุดแต่ประสิทธิภาพช้าที่สุด", + "MemoryManagerHostTooltip": "แมปหน่วยความจำในพื้นที่ที่อยู่โฮสต์โดยตรง การคอมไพล์และดำเนินการของ JIT เร็วขึ้นมาก", + "MemoryManagerUnsafeTooltip": "แมปหน่วยความจำโดยตรง แต่อย่าตั้งค่าที่อยู่ของผู้เยี่ยมชมก่อนที่จะเข้าถึง เร็วกว่า แต่ต้องแลกกับความปลอดภัย แอปพลิเคชั่นของผู้เยี่ยมชมสามารถเข้าถึงหน่วยความจำได้จากทุกที่ใน Ryujinx แนะนำให้รันเฉพาะโปรแกรมที่คุณเชื่อถือในโหมดนี้", + "UseHypervisorTooltip": "ใช้ Hypervisor แทน JIT ปรับปรุงประสิทธิภาพอย่างมากเมื่อพร้อมใช้งาน แต่อาจไม่เสถียรในสถานะปัจจุบัน", + "DRamTooltip": "ใช้รูปแบบ MemoryMode ทางเลือกเพื่อเลียนแบบโมเดลการพัฒนาสวิตช์\n\nสิ่งนี้มีประโยชน์สำหรับแพ็กพื้นผิวที่มีความละเอียดสูงกว่าหรือม็อดที่มีความละเอียด 4k เท่านั้น\n\nปล่อยให้ปิดหากคุณไม่แน่ใจ", + "IgnoreMissingServicesTooltip": "ละเว้นบริการ Horizon OS ที่ยังไม่ได้ใช้งาน วิธีนี้อาจช่วยในการหลีกเลี่ยงข้อผิดพลาดเมื่อบูตเกมบางเกม\n\nปล่อยให้ปิดหากคุณไม่แน่ใจ", + "IgnoreAppletTooltip": "กล่องโต้ตอบภายนอก \"แอปเพล็ตตัวควบคุม\" จะไม่ปรากฏขึ้นหากแป้นเกมถูกตัดการเชื่อมต่อระหว่างการเล่นเกม จะไม่มีข้อความแจ้งให้ปิดกล่องโต้ตอบหรือตั้งค่าตัวควบคุมใหม่ เมื่อเชื่อมต่อคอนโทรลเลอร์ที่ตัดการเชื่อมต่อก่อนหน้านี้อีกครั้ง เกมจะดำเนินการต่อโดยอัตโนมัติ", + "GraphicsBackendThreadingTooltip": "ดำเนินการคำสั่งแบ็กเอนด์กราฟิกบนเธรดที่สอง\n\nเร่งความเร็วการคอมไพล์ ลดการกระตุก และปรับปรุงประสิทธิภาพการทำงานของไดรเวอร์ GPU โดยไม่ต้องรองรับมัลติเธรดในตัว ประสิทธิภาพที่ดีขึ้นเล็กน้อยสำหรับไดรเวอร์ที่มีมัลติเธรด\n\nตั้งเป็น อัตโนมัติ หากคุณไม่แน่ใจ", + "GalThreadingTooltip": "ดำเนินการคำสั่งแบ็กเอนด์กราฟิกบนเธรดที่สอง\n\nเร่งความเร็วการคอมไพล์เชเดอร์ ลดการกระตุก และปรับปรุงประสิทธิภาพการทำงานของไดรเวอร์ GPU โดยไม่ต้องรองรับมัลติเธรดในตัว ประสิทธิภาพที่ดีขึ้นเล็กน้อยสำหรับไดรเวอร์ที่มีมัลติเธรด\n\nตั้งเป็น อัตโนมัติ หากคุณไม่แน่ใจ", + "ShaderCacheToggleTooltip": "บันทึกแคชแสงเงาของดิสก์ซึ่งช่วยลดการกระตุกในการรันครั้งต่อๆ ไป\n\nเปิดทิ้งไว้หากคุณไม่แน่ใจ", + "ResolutionScaleTooltip": "คูณความละเอียดการเรนเดอร์ของเกม\n\nเกมบางเกมอาจไม่สามารถใช้งานได้และดูเป็นพิกเซลแม้ว่าความละเอียดจะเพิ่มขึ้นก็ตาม สำหรับเกมเหล่านั้น คุณอาจต้องค้นหาม็อดที่ลบรอยหยักของภาพหรือเพิ่มความละเอียดในการเรนเดอร์ภายใน หากต้องการใช้อย่างหลัง คุณอาจต้องเลือก Native\n\nตัวเลือกนี้สามารถเปลี่ยนแปลงได้ในขณะที่เกมกำลังทำงานอยู่โดยคลิก \"นำมาใช้\" ด้านล่าง คุณสามารถย้ายหน้าต่างการตั้งค่าไปด้านข้างและทดลองจนกว่าคุณจะพบรูปลักษณ์ที่คุณต้องการสำหรับเกม\n\nโปรดทราบว่า 4x นั้นเกินความจำเป็นสำหรับการตั้งค่าแทบทุกประเภท", + "ResolutionScaleEntryTooltip": "สเกลความละเอียดจุดทศนิยม เช่น 1.5 ไม่ใช่จำนวนเต็มของสเกล มีแนวโน้มที่จะก่อให้เกิดปัญหาหรือความผิดพลาดได้", + "AnisotropyTooltip": "ระดับของ Anisotropic ตั้งค่าเป็นอัตโนมัติเพื่อใช้ค่าพื้นฐานของเกม", + "AspectRatioTooltip": "อัตราส่วนภาพที่ใช้กับหน้าต่างตัวแสดงภาพ\n\nเปลี่ยนสิ่งนี้หากคุณใช้ตัวดัดแปลงอัตราส่วนกว้างยาวสำหรับเกมของคุณ ไม่เช่นนั้นกราฟิกจะถูกยืดออก\n\nทิ้งไว้ที่ 16:9 หากไม่แน่ใจ", + "ShaderDumpPathTooltip": "ที่เก็บ ดัมพ์ไฟล์เชเดอร์", + "FileLogTooltip": "บันทึกประวัติคอนโซลลงในไฟล์บันทึก จะไม่ส่งผลกระทบต่อประสิทธิภาพการทำงาน", + "StubLogTooltip": "พิมพ์ข้อความประวัติในคอนโซล จะไม่ส่งผลกระทบต่อประสิทธิภาพการทำงาน", + "InfoLogTooltip": "พิมพ์ข้อความบันทึกข้อมูลในคอนโซล จะไม่ส่งผลกระทบต่อประสิทธิภาพการทำงาน", + "WarnLogTooltip": "พิมพ์ข้อความประวัติการเตือนในคอนโซล จะไม่ส่งผลกระทบต่อประสิทธิภาพการทำงาน", + "ErrorLogTooltip": "พิมพ์ข้อความบันทึกข้อผิดพลาดในคอนโซล จะไม่ส่งผลกระทบต่อประสิทธิภาพการทำงาน", + "TraceLogTooltip": "พิมพ์ข้อความประวัติการติดตามในคอนโซล ไม่ส่งผลกระทบต่อประสิทธิภาพการทำงาน", + "GuestLogTooltip": "พิมพ์ข้อความประวัติของผู้เยี่ยมชมในคอนโซล ไม่ส่งผลกระทบต่อประสิทธิภาพการทำงาน", + "FileAccessLogTooltip": "พิมพ์ข้อความบันทึกการเข้าถึงไฟล์ในคอนโซล", + "FSAccessLogModeTooltip": "เปิดใช้งาน เอาต์พุตประวัติการเข้าถึง FS ไปยังคอนโซล โหมดที่เป็นไปได้คือ 0-3", + "DeveloperOptionTooltip": "โปรดใช้ด้วยความระมัดระวัง", + "OpenGlLogLevel": "จำเป็นต้องเปิดใช้งานระดับบันทึกที่เหมาะสม", + "DebugLogTooltip": "พิมพ์ข้อความประวัติการแก้ไขข้อบกพร่องในคอนโซล\n\nใช้สิ่งนี้เฉพาะเมื่อได้รับคำแนะนำจากผู้ดูแลเท่านั้น เนื่องจากจะทำให้บันทึกอ่านยากและทำให้ประสิทธิภาพของโปรแกรมจำลองแย่ลง", + "LoadApplicationFileTooltip": "เปิดตัวสำรวจไฟล์เพื่อเลือกไฟล์ที่เข้ากันได้กับ Switch ที่จะโหลด", + "LoadApplicationFolderTooltip": "เปิดตัวสำรวจไฟล์เพื่อเลือกไฟล์ที่เข้ากันได้กับ Switch ที่จะโหลด", + "LoadDlcFromFolderTooltip": "เปิดตัวสำรวจไฟล์เพื่อเลือกหนึ่งโฟลเดอร์ขึ้นไปเพื่อโหลด DLC จำนวนมาก", + "LoadTitleUpdatesFromFolderTooltip": "เปิดตัวสำรวจไฟล์เพื่อเลือกหนึ่งโฟลเดอร์ขึ้นไปเพื่อโหลดไฟล์อัปเดตจำนวนมาก", + "OpenRyujinxFolderTooltip": "เปิดโฟลเดอร์ระบบไฟล์ Ryujinx", + "OpenRyujinxLogsTooltip": "เปิดโฟลเดอร์ ที่เก็บไฟล์ประวัติ", + "ExitTooltip": "ออกจากโปรแกรม Ryujinx", + "OpenSettingsTooltip": "เปิดหน้าต่างการตั้งค่า", + "OpenProfileManagerTooltip": "เปิดหน้าต่างตัวจัดการโปรไฟล์ผู้ใช้", + "StopEmulationTooltip": "หยุดการจำลองของเกมที่เปิดอยู่ในปัจจุบันและกลับไปยังการเลือกเกม", + "CheckUpdatesTooltip": "ตรวจสอบอัปเดตของ Ryujinx", + "OpenAboutTooltip": "เปิดหน้าต่าง เกี่ยวกับ", + "GridSize": "ขนาดตาราง", + "GridSizeTooltip": "เปลี่ยนขนาด ของตาราง", + "SettingsTabSystemSystemLanguageBrazilianPortuguese": "บราซิล โปรตุเกส", + "AboutRyujinxContributorsButtonHeader": "ดูผู้มีส่วนร่วมทั้งหมด", + "SettingsTabSystemAudioVolume": "ระดับเสียง: ", + "AudioVolumeTooltip": "ปรับระดับเสียง", + "SettingsTabSystemEnableInternetAccess": "การเข้าถึงอินเทอร์เน็ตของผู้เยี่ยมชม/โหมด LAN", + "EnableInternetAccessTooltip": "อนุญาตให้แอปพลิเคชันจำลองเชื่อมต่ออินเทอร์เน็ต\n\nเกมที่มีโหมด LAN สามารถเชื่อมต่อระหว่างกันได้เมื่อเปิดใช้งานและระบบเชื่อมต่อกับจุดเชื่อมต่อเดียวกัน รวมถึงคอนโซลจริงด้วย\n\nไม่อนุญาตให้มีการเชื่อมต่อกับเซิร์ฟเวอร์ Nintendo อาจทำให้เกิดการหยุดทำงานในบางเกมที่พยายามเชื่อมต่ออินเทอร์เน็ต\n\nปล่อยให้ปิดหากคุณไม่แน่ใจ", + "GameListContextMenuManageCheatToolTip": "ฟังก์ชั่นจัดการสูตรโกง", + "GameListContextMenuManageCheat": "ฟังก์ชั่นจัดการสูตรโกง", + "GameListContextMenuManageModToolTip": "ฟังก์ชั่นจัดการม็อด", + "GameListContextMenuManageMod": "ฟังก์ชั่นจัดการม็อด", + "ControllerSettingsStickRange": "ขอบเขต:", + "DialogStopEmulationTitle": "Ryujinx - หยุดการจำลอง", + "DialogStopEmulationMessage": "คุณแน่ใจหรือไม่ว่าต้องการหยุดการจำลองหรือไม่?", + "SettingsTabCpu": "ซีพียู", + "SettingsTabAudio": "เสียง", + "SettingsTabNetwork": "เครือข่าย", + "SettingsTabNetworkConnection": "การเชื่อมต่อเครือข่าย", + "SettingsTabCpuCache": "แคชซีพียู", + "SettingsTabCpuMemory": "โหมดซีพียู", + "DialogUpdaterFlatpakNotSupportedMessage": "โปรดอัปเดต Ryujinx ผ่านช่องทาง FlatHub", + "UpdaterDisabledWarningTitle": "ปิดใช้งานการอัปเดตแล้ว!", + "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}", + "TitleBundledUpdateVersionLabel": "Bundled: เวอร์ชั่น {0}", + "TitleBundledDlcLabel": "Bundled:", + "TitleXCIStatusPartialLabel": "Partial", + "TitleXCIStatusTrimmableLabel": "Untrimmed", + "TitleXCIStatusUntrimmableLabel": "Trimmed", + "TitleXCIStatusFailedLabel": "(Failed)", + "TitleXCICanSaveLabel": "Save {0:n0} Mb", + "TitleXCISavingLabel": "Saved {0:n0} Mb", + "RyujinxInfo": "Ryujinx – ข้อมูล", + "RyujinxConfirm": "Ryujinx - ยืนยัน", + "FileDialogAllTypes": "ทุกประเภท", + "Never": "ไม่ต้อง", + "SwkbdMinCharacters": "ต้องมีความยาวของตัวอักษรอย่างน้อย {0} ตัว", + "SwkbdMinRangeCharacters": "ต้องมีความยาวของตัวอักษร {0}-{1} ตัว", + "SoftwareKeyboard": "ซอฟต์แวร์คีย์บอร์ด", + "SoftwareKeyboardModeNumeric": "ต้องเป็น 0-9 หรือ '.' เท่านั้น", + "SoftwareKeyboardModeAlphabet": "ต้องเป็นตัวอักษรที่ไม่ใช่ประเภท CJK เท่านั้น", + "SoftwareKeyboardModeASCII": "ต้องเป็นตัวอักษร ASCII เท่านั้น", + "ControllerAppletControllers": "คอนโทรลเลอร์ที่รองรับ:", + "ControllerAppletPlayers": "ผู้เล่น:", + "ControllerAppletDescription": "การกำหนดค่าปัจจุบันของคุณไม่ถูกต้อง กรุณาเปิดการตั้งค่าและกำหนดค่าอินพุตของคุณใหม่", + "ControllerAppletDocked": "ตั้งค่าด็อกโหมด ควรปิดใช้งานการควบคุมแบบแฮนด์เฮลด์", + "UpdaterRenaming": "กำลังเปลี่ยนชื่อไฟล์เก่า...", + "UpdaterRenameFailed": "โปรแกรมอัปเดตไม่สามารถเปลี่ยนชื่อไฟล์ได้: {0}", + "UpdaterAddingFiles": "กำลังเพิ่มไฟล์ใหม่...", + "UpdaterExtracting": "กำลังแยกการอัปเดต...", + "UpdaterDownloading": "กำลังดาวน์โหลดอัปเดต...", + "Game": "เกมส์", + "Docked": "ด็อก", + "Handheld": "แฮนด์เฮลด์", + "ConnectionError": "การเชื่อมต่อล้มเหลว", + "AboutPageDeveloperListMore": "{0} และอื่นๆ ...", + "ApiError": "ข้อผิดพลาดของ API", + "LoadingHeading": "กำลังโหลด {0}", + "CompilingPPTC": "กำลังคอมไพล์ PTC", + "CompilingShaders": "กำลังคอมไพล์ พื้นผิวและแสงเงา", + "AllKeyboards": "คีย์บอร์ดทั้งหมด", + "OpenFileDialogTitle": "เลือกไฟล์ที่สนับสนุนเพื่อเปิด", + "OpenFolderDialogTitle": "เลือกโฟลเดอร์ที่มีเกมที่แตกไฟล์แล้ว", + "AllSupportedFormats": "รูปแบบที่รองรับทั้งหมด", + "RyujinxUpdater": "ตัวอัปเดต Ryujinx", + "SettingsTabHotkeys": "ปุ่มลัดของคีย์บอร์ด", + "SettingsTabHotkeysHotkeys": "ปุ่มลัดของคีย์บอร์ด", + "SettingsTabHotkeysToggleVsyncHotkey": "สลับเป็น VSync:", + "SettingsTabHotkeysScreenshotHotkey": "ภาพหน้าจอ:", + "SettingsTabHotkeysShowUiHotkey": "แสดง UI:", + "SettingsTabHotkeysPauseHotkey": "หยุดชั่วคราว:", + "SettingsTabHotkeysToggleMuteHotkey": "ปิดเสียง:", + "ControllerMotionTitle": "ตั้งค่าควบคุมการเคลื่อนไหว", + "ControllerRumbleTitle": "ตั้งค่าการสั่นไหว", + "SettingsSelectThemeFileDialogTitle": "เลือกธีมไฟล์", + "SettingsXamlThemeFile": "ไฟล์ธีมรูปแบบ XAML", + "AvatarWindowTitle": "จัดการบัญชี - อวาต้า", + "Amiibo": "Amiibo", + "Unknown": "ไม่รู้จัก", + "Usage": "การใช้งาน", + "Writable": "สามารถเขียนทับได้", + "SelectDlcDialogTitle": "เลือกไฟล์ DLC", + "SelectUpdateDialogTitle": "เลือกไฟล์อัพเดต", + "SelectModDialogTitle": "เลือกไดเรกทอรี Mods", + "TrimXCIFileDialogTitle": "Check and Trim XCI File", + "TrimXCIFileDialogPrimaryText": "This function will first check the empty space and then trim the XCI File to save disk space.", + "TrimXCIFileDialogSecondaryText": "Current File Size: {0:n} MB\nGame Data Size: {1:n} MB\nDisk Space Savings: {2:n} MB", + "TrimXCIFileNoTrimNecessary": "XCI File does not need to be trimmed. Check logs for further details", + "TrimXCIFileNoUntrimPossible": "XCI File cannot be untrimmed. Check logs for further details", + "TrimXCIFileReadOnlyFileCannotFix": "XCI File is Read Only and could not be made writable. Check logs for further details", + "TrimXCIFileFileSizeChanged": "XCI File has changed in size since it was scanned. Please check the file is not being written to and try again.", + "TrimXCIFileFreeSpaceCheckFailed": "XCI File has data in the free space area, it is not safe to trim", + "TrimXCIFileInvalidXCIFile": "XCI File contains invalid data. Check logs for further details", + "TrimXCIFileFileIOWriteError": "XCI File could not be opened for writing. Check logs for further details", + "TrimXCIFileFailedPrimaryText": "Trimming of the XCI file failed", + "TrimXCIFileCancelled": "The operation was cancelled", + "TrimXCIFileFileUndertermined": "No operation was performed", + "UserProfileWindowTitle": "จัดการโปรไฟล์ผู้ใช้", + "CheatWindowTitle": "จัดการสูตรโกง", + "DlcWindowTitle": "จัดการ DLC ที่ดาวน์โหลดได้สำหรับ {0} ({1})", + "ModWindowTitle": "จัดการม็อดที่ดาวน์โหลดได้สำหรับ {0} ({1})", + "UpdateWindowTitle": "จัดการอัปเดตหัวข้อ", + "XCITrimmerWindowTitle": "XCI File Trimmer", + "XCITrimmerTitleStatusCount": "{0} of {1} Title(s) Selected", + "XCITrimmerTitleStatusCountWithFilter": "{0} of {1} Title(s) Selected ({2} displayed)", + "XCITrimmerTitleStatusTrimming": "Trimming {0} Title(s)...", + "XCITrimmerTitleStatusUntrimming": "Untrimming {0} Title(s)...", + "XCITrimmerTitleStatusFailed": "Failed", + "XCITrimmerPotentialSavings": "Potential Savings", + "XCITrimmerActualSavings": "Actual Savings", + "XCITrimmerSavingsMb": "{0:n0} Mb", + "XCITrimmerSelectDisplayed": "Select Shown", + "XCITrimmerDeselectDisplayed": "Deselect Shown", + "XCITrimmerSortName": "Title", + "XCITrimmerSortSaved": "Space Savings", + "XCITrimmerTrim": "Trim", + "XCITrimmerUntrim": "Untrim", + "UpdateWindowUpdateAddedMessage": "{0} อัพเดตที่เพิ่มมาใหม่", + "UpdateWindowBundledContentNotice": "แพ็คที่อัพเดตมาไม่สามารถลบทิ้งได้ สามารถปิดใช้งานได้เท่านั้น", + "CheatWindowHeading": "สูตรโกงมีให้สำหรับ {0} [{1}]", + "BuildId": "รหัสการสร้าง:", + "DlcWindowBundledContentNotice": "แพ็ค DLC ไม่สามารถลบทิ้งได้ สามารถปิดใช้งานได้เท่านั้น", + "DlcWindowHeading": "{0} DLC ที่สามารถดาวน์โหลดได้", + "DlcWindowDlcAddedMessage": "{0} DLC ใหม่ที่เพิ่มเข้ามา", + "AutoloadDlcAddedMessage": "{0} ใหม่ที่เพิ่มเข้ามา", + "AutoloadDlcRemovedMessage": "{0} missing downloadable content(s) removed", + "AutoloadUpdateAddedMessage": "{0} อัพเดตใหม่ที่เพิ่มเข้ามา", + "AutoloadUpdateRemovedMessage": "{0} missing update(s) removed", + "ModWindowHeading": "{0} ม็อด", + "UserProfilesEditProfile": "แก้ไขที่เลือกแล้ว", + "Continue": "Continue", + "Cancel": "ยกเลิก", + "Save": "บันทึก", + "Discard": "ละทิ้ง", + "Paused": "หยุดชั่วคราว", + "UserProfilesSetProfileImage": "ตั้งค่ารูปโปรไฟล์", + "UserProfileEmptyNameError": "จำเป็นต้องระบุชื่อ", + "UserProfileNoImageError": "จำเป็นต้องตั้งค่ารูปโปรไฟล์", + "GameUpdateWindowHeading": "จัดการอัพเดตสำหรับ {0} ({1})", + "SettingsTabHotkeysResScaleUpHotkey": "เพิ่มความละเอียด:", + "SettingsTabHotkeysResScaleDownHotkey": "ลดความละเอียด:", + "UserProfilesName": "ชื่อ:", + "UserProfilesUserId": "รหัสผู้ใช้:", + "SettingsTabGraphicsBackend": "กราฟิกเบื้องหลัง", + "SettingsTabGraphicsBackendTooltip": "เลือกกราฟิกเบื้องหลังที่จะใช้ในโปรแกรมจำลอง\n\nโดยรวมแล้ว Vulkan นั้นดีกว่าสำหรับการ์ดจอรุ่นใหม่ทั้งหมด ตราบใดที่ไดรเวอร์ยังอัพเดทอยู่เสมอ Vulkan ยังมีคุณสมบัติการคอมไพล์เชเดอร์ที่เร็วขึ้น(และลดอาการกระตุก) สำหรับ GPU อื่นๆทุกอัน\n\nOpenGL อาจได้รับผลลัพธ์ที่ดีกว่าบน Nvidia GPU รุ่นเก่า, AMD GPU รุ่นเก่าบน Linux หรือบน GPU ที่มี VRAM น้อย แม้ว่าการคอมไพล์เชเดอร์ จะทำให้อาการกระตุกมากขึ้นก็ตาม\n\nตั้งค่าเป็น Vulkan หากไม่แน่ใจ ตั้งค่าเป็น OpenGL หาก GPU ของคุณไม่รองรับ Vulkan แม้จะมีไดรเวอร์กราฟิกล่าสุดก็ตาม", + "SettingsEnableTextureRecompression": "เปิดใช้งาน การบีบอัดพื้นผิวอีกครั้ง", + "SettingsEnableTextureRecompressionTooltip": "บีบอัดพื้นผิว ASTC เพื่อลดการใช้งาน VRAM\n\nเกมที่ใช้รูปแบบพื้นผิวนี้ ได้แก่ Astral Chain, Bayonetta 3, Fire Emblem Engage, Metroid Prime Remastered, Super Mario Bros. Wonder และ The Legend of Zelda: Tears of the Kingdom\n\nการ์ดจอที่มี 4GiB VRAM หรือน้อยกว่ามีแนวโน้มที่จะพังในบางจุดขณะเล่นเกมเหล่านี้\n\nเปิดใช้งานเฉพาะในกรณีที่ VRAM ของคุณใกล้หมดในเกมที่กล่าวมาข้างต้น ปล่อยให้ปิดหากไม่แน่ใจ", + "SettingsTabGraphicsPreferredGpu": "GPU ที่ต้องการ", + "SettingsTabGraphicsPreferredGpuTooltip": "เลือกการ์ดจอที่จะใช้กับแบ็กเอนด์กราฟิก Vulkan\n\nไม่ส่งผลต่อ GPU ที่ OpenGL จะใช้\n\nตั้งค่าเป็น GPU ที่ถูกตั้งค่าสถานะเป็น \"dGPU\" ถ้าหากคุณไม่แน่ใจ ,หากไม่มีก็ปล่อยทิ้งไว้โดยไม่ต้องแตะต้องมัน", + "SettingsAppRequiredRestartMessage": "จำเป็นต้องรีสตาร์ท Ryujinx", + "SettingsGpuBackendRestartMessage": "การตั้งค่ากราฟิกเบื้องหลังหรือ GPU ได้รับการแก้ไขแล้ว สิ่งนี้จะต้องมีการรีสตาร์ทจึงจะสามารถใช้งานได้", + "SettingsGpuBackendRestartSubMessage": "คุณต้องการรีสตาร์ทตอนนี้หรือไม่?", + "RyujinxUpdaterMessage": "คุณต้องการอัพเดต Ryujinx เป็นเวอร์ชั่นล่าสุดหรือไม่?", + "SettingsTabHotkeysVolumeUpHotkey": "เพิ่มระดับเสียง:", + "SettingsTabHotkeysVolumeDownHotkey": "ลดระดับเสียง:", + "SettingsEnableMacroHLE": "เปิดใช้งาน มาโคร HLE", + "SettingsEnableMacroHLETooltip": "การจำลองระดับสูงของโค้ดมาโคร GPU\n\nปรับปรุงประสิทธิภาพ แต่อาจทำให้เกิดข้อผิดพลาดด้านกราฟิกในบางเกม\n\nเปิดทิ้งไว้หากคุณไม่แน่ใจ", + "SettingsEnableColorSpacePassthrough": "ทะลุผ่านพื้นที่สี", + "SettingsEnableColorSpacePassthroughTooltip": "สั่งให้แบ็กเอนด์ Vulkan ส่งผ่านข้อมูลสีโดยไม่ต้องระบุค่าของสี สำหรับผู้ใช้ที่มีการแสดงกระจายตัวของสี อาจส่งผลให้สีสดใสมากขึ้น โดยต้องแลกกับความถูกต้องของสี", + "VolumeShort": "ระดับเสียง", + "UserProfilesManageSaves": "จัดการบันทึก", + "DeleteUserSave": "คุณต้องการลบบันทึกผู้ใช้สำหรับเกมนี้หรือไม่?", + "IrreversibleActionNote": "การดำเนินการนี้ไม่สามารถย้อนกลับได้", + "SaveManagerHeading": "จัดการบันทึกสำหรับ {0} ({1})", + "SaveManagerTitle": "จัดการบันทึก", + "Name": "ชื่อ", + "Size": "ขนาด", + "Search": "ค้นหา", + "UserProfilesRecoverLostAccounts": "กู้คืนบัญชีที่สูญหาย", + "Recover": "กู้คืน", + "UserProfilesRecoverHeading": "พบบันทึกสำหรับบัญชีดังต่อไปนี้", + "UserProfilesRecoverEmptyList": "ไม่มีโปรไฟล์ที่สามารถกู้คืนได้", + "GraphicsAATooltip": "ใช้การลดรอยหยักกับการเรนเดอร์เกม\n\nFXAA จะเบลอภาพส่วนใหญ่ ในขณะที่ SMAA จะพยายามค้นหารอยหยักและปรับให้เรียบ\n\nไม่แนะนำให้ใช้ร่วมกับตัวกรองสเกล FSR\n\nตัวเลือกนี้สามารถเปลี่ยนแปลงได้ในขณะที่เกมกำลังทำงานอยู่โดยคลิก \"นำไปใช้\" ด้านล่าง คุณสามารถย้ายหน้าต่างการตั้งค่าไปด้านข้างและทดลองจนกว่าคุณจะพบรูปลักษณ์ที่คุณต้องการสำหรับเกม\n\nปล่อยไว้ที่ NONE หากไม่แน่ใจ", + "GraphicsAALabel": "ลดการฉีกขาดของภาพ:", + "GraphicsScalingFilterLabel": "ปรับขนาดตัวกรอง:", + "GraphicsScalingFilterTooltip": "เลือกตัวกรองสเกลที่จะใช้เมื่อใช้สเกลความละเอียด\n\nBilinear ทำงานได้ดีกับเกม 3D และเป็นตัวเลือกเริ่มต้นที่ปลอดภัย\n\nแนะนำให้ใช้เกมภาพพิกเซลที่ใกล้เคียงที่สุด\n\nFSR 1.0 เป็นเพียงตัวกรองความคมชัด ไม่แนะนำให้ใช้กับ FXAA หรือ SMAA\n\nตัวเลือกนี้สามารถเปลี่ยนแปลงได้ในขณะที่เกมกำลังทำงานอยู่โดยคลิก \"นำไปใช้\" ด้านล่าง คุณสามารถย้ายหน้าต่างการตั้งค่าไปด้านข้างและทดลองจนกว่าคุณจะพบรูปลักษณ์ที่คุณต้องการสำหรับเกม", + "GraphicsScalingFilterBilinear": "Bilinear", + "GraphicsScalingFilterNearest": "ใกล้สุด", + "GraphicsScalingFilterFsr": "FSR", + "GraphicsScalingFilterArea": "Area", + "GraphicsScalingFilterLevelLabel": "ระดับ", + "GraphicsScalingFilterLevelTooltip": "ตั้งค่าระดับความคมชัด FSR 1.0 ยิ่งสูงกว่าจะยิ่งคมชัดกว่า", + "SmaaLow": "SMAA ต่ำ", + "SmaaMedium": "SMAA ปานกลาง", + "SmaaHigh": "SMAA สูง", + "SmaaUltra": "SMAA สูงมาก", + "UserEditorTitle": "แก้ไขผู้ใช้", + "UserEditorTitleCreate": "สร้างผู้ใช้", + "SettingsTabNetworkInterface": "เชื่อมต่อเครือข่าย:", + "NetworkInterfaceTooltip": "อินเทอร์เฟซเครือข่ายที่ใช้สำหรับคุณสมบัติ LAN/LDN\n\nเมื่อใช้ร่วมกับ VPN หรือ XLink Kai และเกมที่รองรับ LAN สามารถใช้เพื่อปลอมการเชื่อมต่อเครือข่ายเดียวกันผ่านทางอินเทอร์เน็ต\n\nปล่อยให้เป็น ค่าเริ่มต้น หากคุณไม่แน่ใจ", + "NetworkInterfaceDefault": "ค่าเริ่มต้น", + "PackagingShaders": "รวม Shaders เข้าด้วยกัน", + "AboutChangelogButton": "ดูประวัติการเปลี่ยนแปลงบน GitHub", + "AboutChangelogButtonTooltipMessage": "คลิกเพื่อเปิดประวัติการเปลี่ยนแปลงสำหรับเวอร์ชั่นนี้ บนเบราว์เซอร์เริ่มต้นของคุณ", + "SettingsTabNetworkMultiplayer": "ผู้เล่นหลายคน", + "MultiplayerMode": "โหมด:", + "MultiplayerModeTooltip": "เปลี่ยนโหมดผู้เล่นหลายคนของ LDN\n\nLdnMitm จะปรับเปลี่ยนฟังก์ชันการเล่นแบบไร้สาย/ภายใน จะให้เกมทำงานเหมือนกับว่าเป็น LAN ช่วยให้สามารถเชื่อมต่อภายในเครือข่ายเดียวกันกับอินสแตนซ์ Ryujinx อื่น ๆ และคอนโซล Nintendo Switch ที่ถูกแฮ็กซึ่งมีโมดูล ldn_mitm ติดตั้งอยู่\n\nผู้เล่นหลายคนต้องการให้ผู้เล่นทุกคนอยู่ในเกมเวอร์ชันเดียวกัน (เช่น Super Smash Bros. Ultimate v13.0.1 ไม่สามารถเชื่อมต่อกับ v13.0.0)\n\nปล่อยให้ปิดการใช้งานหากไม่แน่ใจ", + "MultiplayerModeDisabled": "ปิดใช้งาน", + "MultiplayerModeLdnMitm": "ldn_mitm", + "MultiplayerModeLdnRyu": "RyuLDN", + "MultiplayerDisableP2P": "Disable P2P Network Hosting (may increase latency)", + "MultiplayerDisableP2PTooltip": "Disable P2P network hosting, peers will proxy through the master server instead of connecting to you directly.", + "LdnPassphrase": "Network Passphrase:", + "LdnPassphraseTooltip": "You will only be able to see hosted games with the same passphrase as you.", + "LdnPassphraseInputTooltip": "Enter a passphrase in the format Ryujinx-<8 hex chars>. You will only be able to see hosted games with the same passphrase as you.", + "LdnPassphraseInputPublic": "(public)", + "GenLdnPass": "Generate Random", + "GenLdnPassTooltip": "Generates a new passphrase, which can be shared with other players.", + "ClearLdnPass": "Clear", + "ClearLdnPassTooltip": "Clears the current passphrase, returning to the public network.", + "InvalidLdnPassphrase": "Invalid Passphrase! Must be in the format \"Ryujinx-<8 hex chars>\"" +} diff --git a/src/Ryujinx/Assets/Locales/tr_TR.json b/src/Ryujinx/Assets/Locales/tr_TR.json new file mode 100644 index 000000000..18dbb12b0 --- /dev/null +++ b/src/Ryujinx/Assets/Locales/tr_TR.json @@ -0,0 +1,868 @@ +{ + "Language": "Türkçe", + "MenuBarFileOpenApplet": "Applet'i Aç", + "MenuBarFileOpenAppletOpenMiiApplet": "Mii Edit Applet", + "MenuBarFileOpenAppletOpenMiiAppletToolTip": "Mii Editör Applet'ini Bağımsız Mod'da Aç", + "SettingsTabInputDirectMouseAccess": "Doğrudan Mouse Erişimi", + "SettingsTabSystemMemoryManagerMode": "Hafıza Yönetim Modu:", + "SettingsTabSystemMemoryManagerModeSoftware": "Yazılım", + "SettingsTabSystemMemoryManagerModeHost": "Host (hızlı)", + "SettingsTabSystemMemoryManagerModeHostUnchecked": "Host Unchecked (en hızlısı, tehlikeli)", + "SettingsTabSystemUseHypervisor": "Hypervisor Kullan", + "MenuBarFile": "_Dosya", + "MenuBarFileOpenFromFile": "_Dosyadan Uygulama Aç", + "MenuBarFileOpenFromFileError": "No applications found in selected file.", + "MenuBarFileOpenUnpacked": "_Sıkıştırılmamış Oyun Aç", + "MenuBarFileLoadDlcFromFolder": "Load DLC From Folder", + "MenuBarFileLoadTitleUpdatesFromFolder": "Load Title Updates From Folder", + "MenuBarFileOpenEmuFolder": "Ryujinx Klasörünü aç", + "MenuBarFileOpenLogsFolder": "Logs Klasörünü aç", + "MenuBarFileExit": "_Çıkış", + "MenuBarOptions": "_Seçenekler", + "MenuBarOptionsToggleFullscreen": "Tam Ekran Modunu Aç", + "MenuBarOptionsStartGamesInFullscreen": "Oyunları Tam Ekran Modunda Başlat", + "MenuBarOptionsStopEmulation": "Emülasyonu Durdur", + "MenuBarOptionsSettings": "_Seçenekler", + "MenuBarOptionsManageUserProfiles": "_Kullanıcı Profillerini Yönet", + "MenuBarActions": "_Eylemler", + "MenuBarOptionsSimulateWakeUpMessage": "Uyandırma Mesajı Simüle Et", + "MenuBarActionsScanAmiibo": "Bir Amiibo Tara", + "MenuBarTools": "_Araçlar", + "MenuBarToolsInstallFirmware": "Yazılım Yükle", + "MenuBarFileToolsInstallFirmwareFromFile": "XCI veya ZIP'ten Yazılım Yükle", + "MenuBarFileToolsInstallFirmwareFromDirectory": "Bir Dizin Üzerinden Yazılım Yükle", + "MenuBarToolsManageFileTypes": "Dosya uzantılarını yönet", + "MenuBarToolsInstallFileTypes": "Dosya uzantılarını yükle", + "MenuBarToolsUninstallFileTypes": "Dosya uzantılarını kaldır", + "MenuBarToolsXCITrimmer": "Trim XCI Files", + "MenuBarView": "_Görüntüle", + "MenuBarViewWindow": "Pencere Boyutu", + "MenuBarViewWindow720": "720p", + "MenuBarViewWindow1080": "1080p", + "MenuBarHelp": "_Yardım", + "MenuBarHelpCheckForUpdates": "Güncellemeleri Denetle", + "MenuBarHelpAbout": "Hakkında", + "MenuSearch": "Ara...", + "GameListHeaderFavorite": "Favori", + "GameListHeaderIcon": "Simge", + "GameListHeaderApplication": "Oyun Adı", + "GameListHeaderDeveloper": "Geliştirici", + "GameListHeaderVersion": "Sürüm", + "GameListHeaderTimePlayed": "Oynama Süresi", + "GameListHeaderLastPlayed": "Son Oynama Tarihi", + "GameListHeaderFileExtension": "Dosya Uzantısı", + "GameListHeaderFileSize": "Dosya Boyutu", + "GameListHeaderPath": "Yol", + "GameListContextMenuOpenUserSaveDirectory": "Kullanıcı Kayıt Dosyası Dizinini Aç", + "GameListContextMenuOpenUserSaveDirectoryToolTip": "Uygulamanın Kullanıcı Kaydı'nın bulunduğu dizini açar", + "GameListContextMenuOpenDeviceSaveDirectory": "Kullanıcı Cihaz Dizinini Aç", + "GameListContextMenuOpenDeviceSaveDirectoryToolTip": "Uygulamanın Kullanıcı Cihaz Kaydı'nın bulunduğu dizini açar", + "GameListContextMenuOpenBcatSaveDirectory": "Kullanıcı BCAT Dizinini Aç", + "GameListContextMenuOpenBcatSaveDirectoryToolTip": "Uygulamanın Kullanıcı BCAT Kaydı'nın bulunduğu dizini açar", + "GameListContextMenuManageTitleUpdates": "Oyun Güncellemelerini Yönet", + "GameListContextMenuManageTitleUpdatesToolTip": "Oyun Güncelleme Yönetim Penceresini Açar", + "GameListContextMenuManageDlc": "DLC'leri Yönet", + "GameListContextMenuManageDlcToolTip": "DLC yönetim penceresini açar", + "GameListContextMenuCacheManagement": "Önbellek Yönetimi", + "GameListContextMenuCacheManagementPurgePptc": "PPTC Yeniden Yapılandırmasını Başlat", + "GameListContextMenuCacheManagementPurgePptcToolTip": "Oyunun bir sonraki açılışında PPTC'yi yeniden yapılandır", + "GameListContextMenuCacheManagementPurgeShaderCache": "Shader Önbelleğini Temizle", + "GameListContextMenuCacheManagementPurgeShaderCacheToolTip": "Uygulamanın shader önbelleğini temizler", + "GameListContextMenuCacheManagementOpenPptcDirectory": "PPTC Dizinini Aç", + "GameListContextMenuCacheManagementOpenPptcDirectoryToolTip": "Uygulamanın PPTC Önbelleğinin bulunduğu dizini açar", + "GameListContextMenuCacheManagementOpenShaderCacheDirectory": "Shader Önbelleği Dizinini Aç", + "GameListContextMenuCacheManagementOpenShaderCacheDirectoryToolTip": "Uygulamanın shader önbelleğinin bulunduğu dizini açar", + "GameListContextMenuExtractData": "Veriyi Ayıkla", + "GameListContextMenuExtractDataExeFS": "ExeFS", + "GameListContextMenuExtractDataExeFSToolTip": "Uygulamanın geçerli yapılandırmasından ExeFS kısmını ayıkla (Güncellemeler dahil)", + "GameListContextMenuExtractDataRomFS": "RomFS", + "GameListContextMenuExtractDataRomFSToolTip": "Uygulamanın geçerli yapılandırmasından RomFS kısmını ayıkla (Güncellemeler dahil)", + "GameListContextMenuExtractDataLogo": "Simge", + "GameListContextMenuExtractDataLogoToolTip": "Uygulamanın geçerli yapılandırmasından Logo kısmını ayıkla (Güncellemeler dahil)", + "GameListContextMenuCreateShortcut": "Uygulama Kısayolu Oluştur", + "GameListContextMenuCreateShortcutToolTip": "Seçilmiş uygulamayı çalıştıracak bir masaüstü kısayolu oluştur", + "GameListContextMenuCreateShortcutToolTipMacOS": "Create a shortcut in macOS's Applications folder that launches the selected Application", + "GameListContextMenuOpenModsDirectory": "Mod Dizinini Aç", + "GameListContextMenuOpenModsDirectoryToolTip": "Opens the directory which contains Application's Mods", + "GameListContextMenuOpenSdModsDirectory": "Open Atmosphere Mods Directory", + "GameListContextMenuOpenSdModsDirectoryToolTip": "Opens the alternative SD card Atmosphere directory which contains Application's Mods. Useful for mods that are packaged for real hardware.", + "GameListContextMenuTrimXCI": "Check and Trim XCI File", + "GameListContextMenuTrimXCIToolTip": "Check and Trim XCI File to Save Disk Space", + "StatusBarGamesLoaded": "{0}/{1} Oyun Yüklendi", + "StatusBarSystemVersion": "Sistem Sürümü: {0}", + "StatusBarXCIFileTrimming": "Trimming XCI File '{0}'", + "LinuxVmMaxMapCountDialogTitle": "Bellek Haritaları İçin Düşük Limit Tespit Edildi ", + "LinuxVmMaxMapCountDialogTextPrimary": "vm.max_map_count değerini {0} sayısına yükseltmek ister misiniz", + "LinuxVmMaxMapCountDialogTextSecondary": "Bazı oyunlar şu an izin verilen bellek haritası limitinden daha fazlasını yaratmaya çalışabilir. Ryujinx bu limitin geçildiği takdirde kendini kapatıcaktır.", + "LinuxVmMaxMapCountDialogButtonUntilRestart": "Evet, bir sonraki yeniden başlatmaya kadar", + "LinuxVmMaxMapCountDialogButtonPersistent": "Evet, kalıcı olarak", + "LinuxVmMaxMapCountWarningTextPrimary": "İzin verilen maksimum bellek haritası değeri tavsiye edildiğinden daha düşük. ", + "LinuxVmMaxMapCountWarningTextSecondary": "Şu anki vm.max_map_count değeri {0}, bu {1} değerinden daha az. Bazı oyunlar şu an izin verilen bellek haritası limitinden daha fazlasını yaratmaya çalışabilir. Ryujinx bu limitin geçildiği takdirde kendini kapatıcaktır.\n\nManuel olarak bu limiti arttırmayı deneyebilir ya da pkexec'i yükleyebilirsiniz, bu da Ryujinx'in yardımcı olmasına izin verir.", + "Settings": "Ayarlar", + "SettingsTabGeneral": "Kullancı Arayüzü", + "SettingsTabGeneralGeneral": "Genel", + "SettingsTabGeneralEnableDiscordRichPresence": "Discord Zengin İçerik'i Etkinleştir", + "SettingsTabGeneralCheckUpdatesOnLaunch": "Her Açılışta Güncellemeleri Denetle", + "SettingsTabGeneralShowConfirmExitDialog": "\"Çıkışı Onayla\" Diyaloğunu Göster", + "SettingsTabGeneralRememberWindowState": "Remember Window Size/Position", + "SettingsTabGeneralShowTitleBar": "Show Title Bar (Requires restart)", + "SettingsTabGeneralHideCursor": "İşaretçiyi Gizle:", + "SettingsTabGeneralHideCursorNever": "Hiçbir Zaman", + "SettingsTabGeneralHideCursorOnIdle": "Hareketsiz Durumda", + "SettingsTabGeneralHideCursorAlways": "Her Zaman", + "SettingsTabGeneralGameDirectories": "Oyun Dizinleri", + "SettingsTabGeneralAutoloadDirectories": "Autoload DLC/Updates Directories", + "SettingsTabGeneralAutoloadNote": "DLC and Updates which refer to missing files will be unloaded automatically", + "SettingsTabGeneralAdd": "Ekle", + "SettingsTabGeneralRemove": "Kaldır", + "SettingsTabSystem": "Sistem", + "SettingsTabSystemCore": "Çekirdek", + "SettingsTabSystemSystemRegion": "Sistem Bölgesi:", + "SettingsTabSystemSystemRegionJapan": "Japonya", + "SettingsTabSystemSystemRegionUSA": "ABD", + "SettingsTabSystemSystemRegionEurope": "Avrupa", + "SettingsTabSystemSystemRegionAustralia": "Avustralya", + "SettingsTabSystemSystemRegionChina": "Çin", + "SettingsTabSystemSystemRegionKorea": "Kore", + "SettingsTabSystemSystemRegionTaiwan": "Tayvan", + "SettingsTabSystemSystemLanguage": "Sistem Dili:", + "SettingsTabSystemSystemLanguageJapanese": "Japonca", + "SettingsTabSystemSystemLanguageAmericanEnglish": "Amerikan İngilizcesi", + "SettingsTabSystemSystemLanguageFrench": "Fransızca", + "SettingsTabSystemSystemLanguageGerman": "Almanca", + "SettingsTabSystemSystemLanguageItalian": "İtalyanca", + "SettingsTabSystemSystemLanguageSpanish": "İspanyolca", + "SettingsTabSystemSystemLanguageChinese": "Çince", + "SettingsTabSystemSystemLanguageKorean": "Korece", + "SettingsTabSystemSystemLanguageDutch": "Flemenkçe", + "SettingsTabSystemSystemLanguagePortuguese": "Portekizce", + "SettingsTabSystemSystemLanguageRussian": "Rusça", + "SettingsTabSystemSystemLanguageTaiwanese": "Tayvanca", + "SettingsTabSystemSystemLanguageBritishEnglish": "İngiliz İngilizcesi", + "SettingsTabSystemSystemLanguageCanadianFrench": "Kanada Fransızcası", + "SettingsTabSystemSystemLanguageLatinAmericanSpanish": "Latin Amerika İspanyolcası", + "SettingsTabSystemSystemLanguageSimplifiedChinese": "Basitleştirilmiş Çince", + "SettingsTabSystemSystemLanguageTraditionalChinese": "Geleneksel Çince", + "SettingsTabSystemSystemTimeZone": "Sistem Saat Dilimi:", + "SettingsTabSystemSystemTime": "Sistem Saati:", + "SettingsTabSystemEnableVsync": "Dikey Eşitleme", + "SettingsTabSystemEnablePptc": "PPTC (Profilli Sürekli Çeviri Önbelleği)", + "SettingsTabSystemEnableLowPowerPptc": "Low-power PPTC", + "SettingsTabSystemEnableFsIntegrityChecks": "FS Bütünlük Kontrolleri", + "SettingsTabSystemAudioBackend": "Ses Motoru:", + "SettingsTabSystemAudioBackendDummy": "Yapay", + "SettingsTabSystemAudioBackendOpenAL": "OpenAL", + "SettingsTabSystemAudioBackendSoundIO": "SoundIO", + "SettingsTabSystemAudioBackendSDL2": "SDL2", + "SettingsTabSystemHacks": "Hack'ler", + "SettingsTabSystemHacksNote": " (dengesizlik oluşturabilir)", + "SettingsTabSystemDramSize": "Alternatif bellek düzeni kullan (Geliştirici)", + "SettingsTabSystemDramSize4GiB": "4GiB", + "SettingsTabSystemDramSize6GiB": "6GiB", + "SettingsTabSystemDramSize8GiB": "8GiB", + "SettingsTabSystemDramSize12GiB": "12GiB", + "SettingsTabSystemIgnoreMissingServices": "Eksik Servisleri Görmezden Gel", + "SettingsTabSystemIgnoreApplet": "Ignore Applet", + "SettingsTabGraphics": "Grafikler", + "SettingsTabGraphicsAPI": "Grafikler API", + "SettingsTabGraphicsEnableShaderCache": "Shader Önbelleğini Etkinleştir", + "SettingsTabGraphicsAnisotropicFiltering": "Eşyönsüz Doku Süzmesi:", + "SettingsTabGraphicsAnisotropicFilteringAuto": "Otomatik", + "SettingsTabGraphicsAnisotropicFiltering2x": "2x", + "SettingsTabGraphicsAnisotropicFiltering4x": "4x", + "SettingsTabGraphicsAnisotropicFiltering8x": "8x", + "SettingsTabGraphicsAnisotropicFiltering16x": "16x", + "SettingsTabGraphicsResolutionScale": "Çözünürlük Ölçeği:", + "SettingsTabGraphicsResolutionScaleCustom": "Özel (Tavsiye Edilmez)", + "SettingsTabGraphicsResolutionScaleNative": "Yerel (720p/1080p)", + "SettingsTabGraphicsResolutionScale2x": "2x (1440p/2160p)", + "SettingsTabGraphicsResolutionScale3x": "3x (2160p/3240p)", + "SettingsTabGraphicsResolutionScale4x": "4x (2880p/4320p) (Tavsiye Edilmez)", + "SettingsTabGraphicsAspectRatio": "En-Boy Oranı:", + "SettingsTabGraphicsAspectRatio4x3": "4:3", + "SettingsTabGraphicsAspectRatio16x9": "16:9", + "SettingsTabGraphicsAspectRatio16x10": "16:10", + "SettingsTabGraphicsAspectRatio21x9": "21:9", + "SettingsTabGraphicsAspectRatio32x9": "32:9", + "SettingsTabGraphicsAspectRatioStretch": "Pencereye Sığdırmak İçin Genişlet", + "SettingsTabGraphicsDeveloperOptions": "Geliştirici Seçenekleri", + "SettingsTabGraphicsShaderDumpPath": "Grafik Shader Döküm Yolu:", + "SettingsTabLogging": "Loglama", + "SettingsTabLoggingLogging": "Loglama", + "SettingsTabLoggingEnableLoggingToFile": "Logları Dosyaya Kaydetmeyi Etkinleştir", + "SettingsTabLoggingEnableStubLogs": "Stub Loglarını Etkinleştir", + "SettingsTabLoggingEnableInfoLogs": "Bilgi Loglarını Etkinleştir", + "SettingsTabLoggingEnableWarningLogs": "Uyarı Loglarını Etkinleştir", + "SettingsTabLoggingEnableErrorLogs": "Hata Loglarını Etkinleştir", + "SettingsTabLoggingEnableTraceLogs": "Trace Loglarını Etkinleştir", + "SettingsTabLoggingEnableGuestLogs": "Guest Loglarını Etkinleştir", + "SettingsTabLoggingEnableFsAccessLogs": "Fs Erişim Loglarını Etkinleştir", + "SettingsTabLoggingFsGlobalAccessLogMode": "Fs Evrensel Erişim Log Modu:", + "SettingsTabLoggingDeveloperOptions": "Geliştirici Seçenekleri (UYARI: Performansı düşürecektir)", + "SettingsTabLoggingDeveloperOptionsNote": "UYARI: Oyun performansı azalacak", + "SettingsTabLoggingGraphicsBackendLogLevel": "Grafik Arka Uç Günlük Düzeyi", + "SettingsTabLoggingGraphicsBackendLogLevelNone": "Hiçbiri", + "SettingsTabLoggingGraphicsBackendLogLevelError": "Hata", + "SettingsTabLoggingGraphicsBackendLogLevelPerformance": "Yavaşlamalar", + "SettingsTabLoggingGraphicsBackendLogLevelAll": "Hepsi", + "SettingsTabLoggingEnableDebugLogs": "Hata Ayıklama Loglarını Etkinleştir", + "SettingsTabInput": "Giriş Yöntemi", + "SettingsTabInputEnableDockedMode": "Docked Modu Etkinleştir", + "SettingsTabInputDirectKeyboardAccess": "Doğrudan Klavye Erişimi", + "SettingsButtonSave": "Kaydet", + "SettingsButtonClose": "Kapat", + "SettingsButtonOk": "Tamam", + "SettingsButtonCancel": "İptal", + "SettingsButtonApply": "Uygula", + "ControllerSettingsPlayer": "Oyuncu", + "ControllerSettingsPlayer1": "Oyuncu 1", + "ControllerSettingsPlayer2": "Oyuncu 2", + "ControllerSettingsPlayer3": "Oyuncu 3", + "ControllerSettingsPlayer4": "Oyuncu 4", + "ControllerSettingsPlayer5": "Oyuncu 5", + "ControllerSettingsPlayer6": "Oyuncu 6", + "ControllerSettingsPlayer7": "Oyuncu 7", + "ControllerSettingsPlayer8": "Oyuncu 8", + "ControllerSettingsHandheld": "Portatif Mod", + "ControllerSettingsInputDevice": "Giriş Cihazı", + "ControllerSettingsRefresh": "Yenile", + "ControllerSettingsDeviceDisabled": "Devre Dışı", + "ControllerSettingsControllerType": "Kumanda Tipi", + "ControllerSettingsControllerTypeHandheld": "Portatif Mod", + "ControllerSettingsControllerTypeProController": "Profesyonel Kumanda", + "ControllerSettingsControllerTypeJoyConPair": "JoyCon Çifti", + "ControllerSettingsControllerTypeJoyConLeft": "JoyCon Sol", + "ControllerSettingsControllerTypeJoyConRight": "JoyCon Sağ", + "ControllerSettingsProfile": "Profil", + "ControllerSettingsProfileDefault": "Varsayılan", + "ControllerSettingsLoad": "Yükle", + "ControllerSettingsAdd": "Ekle", + "ControllerSettingsRemove": "Kaldır", + "ControllerSettingsButtons": "Tuşlar", + "ControllerSettingsButtonA": "A", + "ControllerSettingsButtonB": "B", + "ControllerSettingsButtonX": "X", + "ControllerSettingsButtonY": "Y", + "ControllerSettingsButtonPlus": "+", + "ControllerSettingsButtonMinus": "-", + "ControllerSettingsDPad": "Yön Tuşları", + "ControllerSettingsDPadUp": "Yukarı", + "ControllerSettingsDPadDown": "Aşağı", + "ControllerSettingsDPadLeft": "Sol", + "ControllerSettingsDPadRight": "Sağ", + "ControllerSettingsStickButton": "Tuş", + "ControllerSettingsStickUp": "Yukarı", + "ControllerSettingsStickDown": "Aşağı", + "ControllerSettingsStickLeft": "Sol", + "ControllerSettingsStickRight": "Sağ", + "ControllerSettingsStickStick": "Analog", + "ControllerSettingsStickInvertXAxis": "X Eksenini Tersine Çevir", + "ControllerSettingsStickInvertYAxis": "Y Eksenini Tersine Çevir", + "ControllerSettingsStickDeadzone": "Ölü Bölge", + "ControllerSettingsLStick": "Sol Analog", + "ControllerSettingsRStick": "Sağ Analog", + "ControllerSettingsTriggersLeft": "Tetikler Sol", + "ControllerSettingsTriggersRight": "Tetikler Sağ", + "ControllerSettingsTriggersButtonsLeft": "Tetik Tuşları Sol", + "ControllerSettingsTriggersButtonsRight": "Tetik Tuşları Sağ", + "ControllerSettingsTriggers": "Tetikler", + "ControllerSettingsTriggerL": "L", + "ControllerSettingsTriggerR": "R", + "ControllerSettingsTriggerZL": "ZL", + "ControllerSettingsTriggerZR": "ZR", + "ControllerSettingsLeftSL": "SL", + "ControllerSettingsLeftSR": "SR", + "ControllerSettingsRightSL": "SL", + "ControllerSettingsRightSR": "SR", + "ControllerSettingsExtraButtonsLeft": "Tuşlar Sol", + "ControllerSettingsExtraButtonsRight": "Tuşlar Sağ", + "ControllerSettingsMisc": "Diğer", + "ControllerSettingsTriggerThreshold": "Tetik Eşiği:", + "ControllerSettingsMotion": "Hareket", + "ControllerSettingsMotionUseCemuhookCompatibleMotion": "CemuHook uyumlu hareket kullan", + "ControllerSettingsMotionControllerSlot": "Kumanda Yuvası:", + "ControllerSettingsMotionMirrorInput": "Girişi Aynala", + "ControllerSettingsMotionRightJoyConSlot": "Sağ JoyCon Yuvası:", + "ControllerSettingsMotionServerHost": "Sunucu Sahibi:", + "ControllerSettingsMotionGyroSensitivity": "Gyro Hassasiyeti:", + "ControllerSettingsMotionGyroDeadzone": "Gyro Ölü Bölgesi:", + "ControllerSettingsSave": "Kaydet", + "ControllerSettingsClose": "Kapat", + "KeyUnknown": "Unknown", + "KeyShiftLeft": "Sol Shift", + "KeyShiftRight": "Sağ Shift", + "KeyControlLeft": "Sol Ctrl", + "KeyMacControlLeft": "⌃ Sol", + "KeyControlRight": "Sağ Control", + "KeyMacControlRight": "⌃ Sağ", + "KeyAltLeft": "Sol Alt", + "KeyMacAltLeft": "⌥ Sol", + "KeyAltRight": "Sağ Alt", + "KeyMacAltRight": "⌥ Sağ", + "KeyWinLeft": "⊞ Sol", + "KeyMacWinLeft": "⌘ Sol", + "KeyWinRight": "⊞ Sağ", + "KeyMacWinRight": "⌘ Sağ", + "KeyMenu": "Menü", + "KeyUp": "Yukarı", + "KeyDown": "Aşağı", + "KeyLeft": "Sol", + "KeyRight": "Sağ", + "KeyEnter": "Enter", + "KeyEscape": "Esc", + "KeySpace": "Space", + "KeyTab": "Tab", + "KeyBackSpace": "Geri tuşu", + "KeyInsert": "Insert", + "KeyDelete": "Delete", + "KeyPageUp": "Page Up", + "KeyPageDown": "Page Down", + "KeyHome": "Home", + "KeyEnd": "End", + "KeyCapsLock": "Caps Lock", + "KeyScrollLock": "Scroll Lock", + "KeyPrintScreen": "Print Screen", + "KeyPause": "Pause", + "KeyNumLock": "Num Lock", + "KeyClear": "Clear", + "KeyKeypad0": "Keypad 0", + "KeyKeypad1": "Keypad 1", + "KeyKeypad2": "Keypad 2", + "KeyKeypad3": "Keypad 3", + "KeyKeypad4": "Keypad 4", + "KeyKeypad5": "Keypad 5", + "KeyKeypad6": "Keypad 6", + "KeyKeypad7": "Keypad 7", + "KeyKeypad8": "Keypad 8", + "KeyKeypad9": "Keypad 9", + "KeyKeypadDivide": "Keypad Divide", + "KeyKeypadMultiply": "Keypad Multiply", + "KeyKeypadSubtract": "Keypad Subtract", + "KeyKeypadAdd": "Keypad Add", + "KeyKeypadDecimal": "Keypad Decimal", + "KeyKeypadEnter": "Keypad Enter", + "KeyNumber0": "0", + "KeyNumber1": "1", + "KeyNumber2": "2", + "KeyNumber3": "3", + "KeyNumber4": "4", + "KeyNumber5": "5", + "KeyNumber6": "6", + "KeyNumber7": "7", + "KeyNumber8": "8", + "KeyNumber9": "9", + "KeyTilde": "~", + "KeyGrave": "`", + "KeyMinus": "-", + "KeyPlus": "+", + "KeyBracketLeft": "[", + "KeyBracketRight": "]", + "KeySemicolon": ";", + "KeyQuote": "\"", + "KeyComma": ",", + "KeyPeriod": ".", + "KeySlash": "/", + "KeyBackSlash": "\\", + "KeyUnbound": "Unbound", + "GamepadLeftStick": "L Stick Button", + "GamepadRightStick": "R Stick Button", + "GamepadLeftShoulder": "Left Shoulder", + "GamepadRightShoulder": "Right Shoulder", + "GamepadLeftTrigger": "Left Trigger", + "GamepadRightTrigger": "Right Trigger", + "GamepadDpadUp": "Up", + "GamepadDpadDown": "Down", + "GamepadDpadLeft": "Left", + "GamepadDpadRight": "Sağ", + "GamepadMinus": "-", + "GamepadPlus": "4", + "GamepadGuide": "Rehber", + "GamepadMisc1": "Diğer", + "GamepadPaddle1": "Pedal 1", + "GamepadPaddle2": "Pedal 2", + "GamepadPaddle3": "Pedal 3", + "GamepadPaddle4": "Pedal 4", + "GamepadTouchpad": "Touchpad", + "GamepadSingleLeftTrigger0": "Sol Tetik 0", + "GamepadSingleRightTrigger0": "Sağ Tetik 0", + "GamepadSingleLeftTrigger1": "Sol Tetik 1", + "GamepadSingleRightTrigger1": "Sağ Tetik 1", + "StickLeft": "Sol Çubuk", + "StickRight": "Sağ çubuk", + "UserProfilesSelectedUserProfile": "Seçili Kullanıcı Profili:", + "UserProfilesSaveProfileName": "Profil İsmini Kaydet", + "UserProfilesChangeProfileImage": "Profil Resmini Değiştir", + "UserProfilesAvailableUserProfiles": "Mevcut Kullanıcı Profilleri:", + "UserProfilesAddNewProfile": "Yeni Profil Ekle", + "UserProfilesDelete": "Sil", + "UserProfilesClose": "Kapat", + "ProfileNameSelectionWatermark": "Kullanıcı Adı Seç", + "ProfileImageSelectionTitle": "Profil Resmi Seçimi", + "ProfileImageSelectionHeader": "Profil Resmi Seç", + "ProfileImageSelectionNote": "Özel bir profil resmi içeri aktarabilir veya sistem avatarlarından birini seçebilirsiniz", + "ProfileImageSelectionImportImage": "Resim İçeri Aktar", + "ProfileImageSelectionSelectAvatar": "Yazılım Avatarı Seç", + "InputDialogTitle": "Giriş Yöntemi Diyaloğu", + "InputDialogOk": "Tamam", + "InputDialogCancel": "İptal", + "InputDialogCancelling": "Cancelling", + "InputDialogClose": "Close", + "InputDialogAddNewProfileTitle": "Profil İsmini Seç", + "InputDialogAddNewProfileHeader": "Lütfen Bir Profil İsmi Girin", + "InputDialogAddNewProfileSubtext": "(Maksimum Uzunluk: {0})", + "AvatarChoose": "Seç", + "AvatarSetBackgroundColor": "Arka Plan Rengi Ayarla", + "AvatarClose": "Kapat", + "ControllerSettingsLoadProfileToolTip": "Profil Yükle", + "ControllerSettingsViewProfileToolTip": "View Profile", + "ControllerSettingsAddProfileToolTip": "Profil Ekle", + "ControllerSettingsRemoveProfileToolTip": "Profili Kaldır", + "ControllerSettingsSaveProfileToolTip": "Profili Kaydet", + "MenuBarFileToolsTakeScreenshot": "Ekran Görüntüsü Al", + "MenuBarFileToolsHideUi": "Arayüzü Gizle", + "GameListContextMenuRunApplication": "Uygulamayı Çalıştır", + "GameListContextMenuToggleFavorite": "Favori Ayarla", + "GameListContextMenuToggleFavoriteToolTip": "Oyunu Favorilere Ekle/Çıkar", + "SettingsTabGeneralTheme": "Tema:", + "SettingsTabGeneralThemeAuto": "Auto", + "SettingsTabGeneralThemeDark": "Karanlık", + "SettingsTabGeneralThemeLight": "Aydınlık", + "ControllerSettingsConfigureGeneral": "Ayarla", + "ControllerSettingsRumble": "Titreşim", + "ControllerSettingsRumbleStrongMultiplier": "Güçlü Titreşim Çoklayıcı", + "ControllerSettingsRumbleWeakMultiplier": "Zayıf Titreşim Seviyesi", + "DialogMessageSaveNotAvailableMessage": "{0} [{1:x16}] için kayıt verisi bulunamadı", + "DialogMessageSaveNotAvailableCreateSaveMessage": "Bu oyun için kayıt verisi oluşturmak ister misiniz?", + "DialogConfirmationTitle": "Ryujinx - Onay", + "DialogUpdaterTitle": "Ryujinx - Güncelleyici", + "DialogErrorTitle": "Ryujinx - Hata", + "DialogWarningTitle": "Ryujinx - Uyarı", + "DialogExitTitle": "Ryujinx - Çıkış", + "DialogErrorMessage": "Ryujinx bir hata ile karşılaştı", + "DialogExitMessage": "Ryujinx'i kapatmak istediğinizden emin misiniz?", + "DialogExitSubMessage": "Kaydedilmeyen bütün veriler kaybedilecek!", + "DialogMessageCreateSaveErrorMessage": "Belirtilen kayıt verisi oluşturulurken bir hata oluştu: {0}", + "DialogMessageFindSaveErrorMessage": "Belirtilen kayıt verisi bulunmaya çalışırken hata: {0}", + "FolderDialogExtractTitle": "İçine ayıklanacak klasörü seç", + "DialogNcaExtractionMessage": "{1} den {0} kısmı ayıklanıyor...", + "DialogNcaExtractionTitle": "NCA Kısmı Ayıklayıcısı", + "DialogNcaExtractionMainNcaNotFoundErrorMessage": "Ayıklama hatası. Ana NCA seçilen dosyada bulunamadı.", + "DialogNcaExtractionCheckLogErrorMessage": "Ayıklama hatası. Ek bilgi için kayıt dosyasını okuyun.", + "DialogNcaExtractionSuccessMessage": "Ayıklama başarıyla tamamlandı.", + "DialogUpdaterConvertFailedMessage": "Güncel Ryujinx sürümü dönüştürülemedi.", + "DialogUpdaterCancelUpdateMessage": "Güncelleme iptal ediliyor!", + "DialogUpdaterAlreadyOnLatestVersionMessage": "Zaten Ryujinx'in en güncel sürümünü kullanıyorsunuz!", + "DialogUpdaterFailedToGetVersionMessage": "GitHub tarafından sürüm bilgileri alınırken bir hata oluştu. Eğer yeni sürüm için hazırlıklar yapılıyorsa bu hatayı almanız olasıdır. Lütfen birkaç dakika sonra tekrar deneyiniz.", + "DialogUpdaterConvertFailedGithubMessage": "Github Release'den alınan Ryujinx sürümü dönüştürülemedi.", + "DialogUpdaterDownloadingMessage": "Güncelleme İndiriliyor...", + "DialogUpdaterExtractionMessage": "Güncelleme Ayıklanıyor...", + "DialogUpdaterRenamingMessage": "Güncelleme Yeniden Adlandırılıyor...", + "DialogUpdaterAddingFilesMessage": "Yeni Güncelleme Ekleniyor...", + "DialogUpdaterShowChangelogMessage": "Show Changelog", + "DialogUpdaterCompleteMessage": "Güncelleme Tamamlandı!", + "DialogUpdaterRestartMessage": "Ryujinx'i şimdi yeniden başlatmak istiyor musunuz?", + "DialogUpdaterNoInternetMessage": "İnternete bağlı değilsiniz!", + "DialogUpdaterNoInternetSubMessage": "Lütfen aktif bir internet bağlantınız olduğunu kontrol edin!", + "DialogUpdaterDirtyBuildMessage": "Ryujinx'in Dirty build'lerini güncelleyemezsiniz!", + "DialogUpdaterDirtyBuildSubMessage": "Desteklenen bir sürüm için lütfen Ryujinx'i https://ryujinx.app/download sitesinden indirin.", + "DialogRestartRequiredMessage": "Yeniden Başlatma Gerekli", + "DialogThemeRestartMessage": "Tema kaydedildi. Temayı uygulamak için yeniden başlatma gerekiyor.", + "DialogThemeRestartSubMessage": "Yeniden başlatmak ister misiniz", + "DialogFirmwareInstallEmbeddedMessage": "Bu oyunun içine gömülü olan yazılımı yüklemek ister misiniz? (Firmware {0})", + "DialogFirmwareInstallEmbeddedSuccessMessage": "No installed firmware was found but Ryujinx was able to install firmware {0} from the provided game.\nThe emulator will now start.", + "DialogFirmwareNoFirmwareInstalledMessage": "Yazılım Yüklü Değil", + "DialogFirmwareInstalledMessage": "Yazılım {0} yüklendi", + "DialogInstallFileTypesSuccessMessage": "Dosya uzantıları başarıyla yüklendi!", + "DialogInstallFileTypesErrorMessage": "Dosya uzantıları yükleme işlemi başarısız oldu.", + "DialogUninstallFileTypesSuccessMessage": "Dosya uzantıları başarıyla kaldırıldı!", + "DialogUninstallFileTypesErrorMessage": "Dosya uzantıları kaldırma işlemi başarısız oldu.", + "DialogOpenSettingsWindowLabel": "Seçenekler Penceresini Aç", + "DialogOpenXCITrimmerWindowLabel": "XCI Trimmer Window", + "DialogControllerAppletTitle": "Kumanda Applet'i", + "DialogMessageDialogErrorExceptionMessage": "Mesaj diyaloğu gösterilirken hata: {0}", + "DialogSoftwareKeyboardErrorExceptionMessage": "Mesaj diyaloğu gösterilirken hata: {0}", + "DialogErrorAppletErrorExceptionMessage": "Applet diyaloğu gösterilirken hata: {0}", + "DialogUserErrorDialogMessage": "{0}: {1}", + "DialogUserErrorDialogInfoMessage": "\nBu hatayı düzeltmek adına daha fazla bilgi için kurulum kılavuzumuzu takip edin.", + "DialogUserErrorDialogTitle": "Ryujinx Hatası ({0})", + "DialogAmiiboApiTitle": "Amiibo API", + "DialogAmiiboApiFailFetchMessage": "API'dan bilgi alırken bir hata oluştu.", + "DialogAmiiboApiConnectErrorMessage": "Amiibo API sunucusuna bağlanılamadı. Sunucu çevrimdışı olabilir veya uygun bir internet bağlantınızın olduğunu kontrol etmeniz gerekebilir.", + "DialogProfileInvalidProfileErrorMessage": "Profil {0} güncel giriş konfigürasyon sistemi ile uyumlu değil.", + "DialogProfileDefaultProfileOverwriteErrorMessage": "Varsayılan Profil'in üstüne yazılamaz", + "DialogProfileDeleteProfileTitle": "Profil Siliniyor", + "DialogProfileDeleteProfileMessage": "Bu eylem geri döndürülemez, devam etmek istediğinizden emin misiniz?", + "DialogWarning": "Uyarı", + "DialogPPTCDeletionMessage": "Belirtilen PPTC cache silinecek :\n\n{0}\n\nDevam etmek istediğinizden emin misiniz?", + "DialogPPTCDeletionErrorMessage": "Belirtilen PPTC cache temizlenirken hata {0}: {1}", + "DialogShaderDeletionMessage": "Belirtilen Shader cache silinecek :\n\n{0}\n\nDevam etmek istediğinizden emin misiniz?", + "DialogShaderDeletionErrorMessage": "Belirtilen Shader cache temizlenirken hata {0}: {1}", + "DialogRyujinxErrorMessage": "Ryujinx bir hata ile karşılaştı", + "DialogInvalidTitleIdErrorMessage": "Arayüz hatası: Seçilen oyun geçerli bir title ID'ye sahip değil", + "DialogFirmwareInstallerFirmwareNotFoundErrorMessage": "{0} da geçerli bir sistem firmware'i bulunamadı.", + "DialogFirmwareInstallerFirmwareInstallTitle": "Firmware {0} Yükle", + "DialogFirmwareInstallerFirmwareInstallMessage": "Sistem sürümü {0} yüklenecek.", + "DialogFirmwareInstallerFirmwareInstallSubMessage": "\n\nBu şimdiki sistem sürümünün yerini alacak {0}.", + "DialogFirmwareInstallerFirmwareInstallConfirmMessage": "\n\nDevam etmek istiyor musunuz?", + "DialogFirmwareInstallerFirmwareInstallWaitMessage": "Firmware yükleniyor...", + "DialogFirmwareInstallerFirmwareInstallSuccessMessage": "Sistem sürümü {0} başarıyla yüklendi.", + "DialogUserProfileDeletionWarningMessage": "Seçilen profil silinirse kullanılabilen başka profil kalmayacak", + "DialogUserProfileDeletionConfirmMessage": "Seçilen profili silmek istiyor musunuz", + "DialogUserProfileUnsavedChangesTitle": "Uyarı - Kaydedilmemiş Değişiklikler", + "DialogUserProfileUnsavedChangesMessage": "Kullanıcı profilinizde kaydedilmemiş değişiklikler var.", + "DialogUserProfileUnsavedChangesSubMessage": "Yaptığınız değişiklikleri iptal etmek istediğinize emin misiniz?", + "DialogControllerSettingsModifiedConfirmMessage": "Geçerli kumanda seçenekleri güncellendi.", + "DialogControllerSettingsModifiedConfirmSubMessage": "Kaydetmek istiyor musunuz?", + "DialogLoadFileErrorMessage": "{0}. Hatalı Dosya: {1}", + "DialogModAlreadyExistsMessage": "Mod zaten var", + "DialogModInvalidMessage": "The specified directory does not contain a mod!", + "DialogModDeleteNoParentMessage": "Silme Başarısız: \"{0}\" Modu için üst dizin bulunamadı! ", + "DialogDlcNoDlcErrorMessage": "Belirtilen dosya seçilen oyun için DLC içermiyor!", + "DialogPerformanceCheckLoggingEnabledMessage": "Sadece geliştiriler için dizayn edilen Trace Loglama seçeneği etkin.", + "DialogPerformanceCheckLoggingEnabledConfirmMessage": "En iyi performans için trace loglama'nın devre dışı bırakılması tavsiye edilir. Trace loglama seçeneğini şimdi devre dışı bırakmak ister misiniz?", + "DialogPerformanceCheckShaderDumpEnabledMessage": "Sadece geliştiriler için dizayn edilen Shader Dumping seçeneği etkin.", + "DialogPerformanceCheckShaderDumpEnabledConfirmMessage": "En iyi performans için Shader Dumping'in devre dışı bırakılması tavsiye edilir. Shader Dumping seçeneğini şimdi devre dışı bırakmak ister misiniz?", + "DialogLoadAppGameAlreadyLoadedMessage": "Bir oyun zaten yüklendi", + "DialogLoadAppGameAlreadyLoadedSubMessage": "Lütfen yeni bir oyun açmadan önce emülasyonu durdurun veya emülatörü kapatın.", + "DialogUpdateAddUpdateErrorMessage": "Belirtilen dosya seçilen oyun için güncelleme içermiyor!", + "DialogSettingsBackendThreadingWarningTitle": "Uyarı - Backend Threading", + "DialogSettingsBackendThreadingWarningMessage": "Bu seçeneğin tamamen uygulanması için Ryujinx'in kapatıp açılması gerekir. Kullandığınız işletim sistemine bağlı olarak, Ryujinx'in multithreading'ini kullanırken driver'ınızın multithreading seçeneğini kapatmanız gerekebilir.", + "DialogModManagerDeletionWarningMessage": "You are about to delete the mod: {0}\n\nAre you sure you want to proceed?", + "DialogModManagerDeletionAllWarningMessage": "You are about to delete all mods for this title.\n\nAre you sure you want to proceed?", + "SettingsTabGraphicsFeaturesOptions": "Özellikler", + "SettingsTabGraphicsBackendMultithreading": "Grafik Backend Multithreading:", + "CommonAuto": "Otomatik", + "CommonOff": "Kapalı", + "CommonOn": "Açık", + "InputDialogYes": "Evet", + "InputDialogNo": "Hayır", + "DialogProfileInvalidProfileNameErrorMessage": "Dosya adı geçersiz karakter içeriyor. Lütfen tekrar deneyin.", + "MenuBarOptionsPauseEmulation": "Durdur", + "MenuBarOptionsResumeEmulation": "Devam Et", + "AboutUrlTooltipMessage": "Ryujinx'in websitesini varsayılan tarayıcınızda açmak için tıklayın.", + "AboutDisclaimerMessage": "Ryujinx, Nintendo™ veya ortaklarıyla herhangi bir şekilde bağlantılı değildir.", + "AboutAmiiboDisclaimerMessage": "Amiibo emülasyonumuzda \nAmiiboAPI (www.amiiboapi.com) kullanılmaktadır.", + "AboutPatreonUrlTooltipMessage": "Ryujinx'in Patreon sayfasını varsayılan tarayıcınızda açmak için tıklayın.", + "AboutGithubUrlTooltipMessage": "Ryujinx'in GitHub sayfasını varsayılan tarayıcınızda açmak için tıklayın.", + "AboutDiscordUrlTooltipMessage": "Varsayılan tarayıcınızda Ryujinx'in Discord'una bir davet açmak için tıklayın.", + "AboutTwitterUrlTooltipMessage": "Ryujinx'in Twitter sayfasını varsayılan tarayıcınızda açmak için tıklayın.", + "AboutRyujinxAboutTitle": "Hakkında:", + "AboutRyujinxAboutContent": "Ryujinx bir Nintendo Switch™ emülatörüdür.\nLütfen bizi Patreon'da destekleyin.\nEn son haberleri Twitter veya Discord'umuzdan alın.\nKatkıda bulunmak isteyen geliştiriciler GitHub veya Discord üzerinden daha fazla bilgi edinebilir.", + "AboutRyujinxMaintainersTitle": "Geliştiriciler:", + "AboutRyujinxMaintainersContentTooltipMessage": "Katkıda bulunanlar sayfasını varsayılan tarayıcınızda açmak için tıklayın.", + "AboutRyujinxSupprtersTitle": "Patreon Destekleyicileri:", + "AmiiboSeriesLabel": "Amiibo Serisi", + "AmiiboCharacterLabel": "Karakter", + "AmiiboScanButtonLabel": "Tarat", + "AmiiboOptionsShowAllLabel": "Tüm Amiibo'ları Göster", + "AmiiboOptionsUsRandomTagLabel": "Hack: Rastgele bir Uuid kullan", + "DlcManagerTableHeadingEnabledLabel": "Etkin", + "DlcManagerTableHeadingTitleIdLabel": "Başlık ID", + "DlcManagerTableHeadingContainerPathLabel": "Container Yol", + "DlcManagerTableHeadingFullPathLabel": "Tam Yol", + "DlcManagerRemoveAllButton": "Tümünü kaldır", + "DlcManagerEnableAllButton": "Tümünü Aktif Et", + "DlcManagerDisableAllButton": "Tümünü Devre Dışı Bırak", + "ModManagerDeleteAllButton": "Hepsini Sil", + "MenuBarOptionsChangeLanguage": "Dili Değiştir", + "MenuBarShowFileTypes": "Dosya Uzantılarını Göster", + "CommonSort": "Sırala", + "CommonShowNames": "İsimleri Göster", + "CommonFavorite": "Favori", + "OrderAscending": "Artan", + "OrderDescending": "Azalan", + "SettingsTabGraphicsFeatures": "Özellikler & İyileştirmeler", + "ErrorWindowTitle": "Hata Penceresi", + "ToggleDiscordTooltip": "Ryujinx'i \"şimdi oynanıyor\" Discord aktivitesinde göstermeyi veya göstermemeyi seçin", + "AddGameDirBoxTooltip": "Listeye eklemek için oyun dizini seçin", + "AddGameDirTooltip": "Listeye oyun dizini ekle", + "RemoveGameDirTooltip": "Seçili oyun dizinini kaldır", + "AddAutoloadDirBoxTooltip": "Enter an autoload directory to add to the list", + "AddAutoloadDirTooltip": "Add an autoload directory to the list", + "RemoveAutoloadDirTooltip": "Remove selected autoload directory", + "CustomThemeCheckTooltip": "Emülatör pencerelerinin görünümünü değiştirmek için özel bir Avalonia teması kullan", + "CustomThemePathTooltip": "Özel arayüz temasının yolu", + "CustomThemeBrowseTooltip": "Özel arayüz teması için göz at", + "DockModeToggleTooltip": "Docked modu emüle edilen sistemin yerleşik Nintendo Switch gibi davranmasını sağlar. Bu çoğu oyunda grafik kalitesini arttırır. Diğer yandan, bu seçeneği devre dışı bırakmak emüle edilen sistemin portatif Ninendo Switch gibi davranmasını sağlayıp grafik kalitesini düşürür.\n\nDocked modu kullanmayı düşünüyorsanız 1. Oyuncu kontrollerini; Handheld modunu kullanmak istiyorsanız portatif kontrollerini konfigüre edin.\n\nEmin değilseniz aktif halde bırakın.", + "DirectKeyboardTooltip": "Direct keyboard access (HID) support. Provides games access to your keyboard as a text entry device.\n\nOnly works with games that natively support keyboard usage on Switch hardware.\n\nLeave OFF if unsure.", + "DirectMouseTooltip": "Direct mouse access (HID) support. Provides games access to your mouse as a pointing device.\n\nOnly works with games that natively support mouse controls on Switch hardware, which are few and far between.\n\nWhen enabled, touch screen functionality may not work.\n\nLeave OFF if unsure.", + "RegionTooltip": "Sistem Bölgesini Değiştir", + "LanguageTooltip": "Sistem Dilini Değiştir", + "TimezoneTooltip": "Sistem Saat Dilimini Değiştir", + "TimeTooltip": "Sistem Saatini Değiştir", + "VSyncToggleTooltip": "Emulated console's Vertical Sync. Essentially a frame-limiter for the majority of games; disabling it may cause games to run at higher speed or make loading screens take longer or get stuck.\n\nCan be toggled in-game with a hotkey of your preference (F1 by default). We recommend doing this if you plan on disabling it.\n\nLeave ON if unsure.", + "PptcToggleTooltip": "Çevrilen JIT fonksiyonlarını oyun her açıldığında çevrilmek zorunda kalmaması için kaydeder.\n\nTeklemeyi azaltır ve ilk açılıştan sonra oyunların ilk açılış süresini ciddi biçimde hızlandırır.\n\nEmin değilseniz aktif halde bırakın.", + "LowPowerPptcToggleTooltip": "Load the PPTC using a third of the amount of cores.", + "FsIntegrityToggleTooltip": "Oyun açarken hatalı dosyaların olup olmadığını kontrol eder, ve hatalı dosya bulursa log dosyasında hash hatası görüntüler.\n\nPerformansa herhangi bir etkisi yoktur ve sorun gidermeye yardımcı olur.\n\nEmin değilseniz aktif halde bırakın.", + "AudioBackendTooltip": "Ses çıkış motorunu değiştirir.\n\nSDL2 tercih edilen seçenektir, OpenAL ve SoundIO ise alternatif olarak kullanılabilir. Dummy seçeneğinde ses çıkışı olmayacaktır.\n\nEmin değilseniz SDL2 seçeneğine ayarlayın.", + "MemoryManagerTooltip": "Guest hafızasının nasıl tahsis edilip erişildiğini değiştirir. Emüle edilen CPU performansını ciddi biçimde etkiler.\n\nEmin değilseniz HOST UNCHECKED seçeneğine ayarlayın.", + "MemoryManagerSoftwareTooltip": "Adres çevirisi için bir işlemci sayfası kullanır. En yüksek doğruluğu ve en yavaş performansı sunar.", + "MemoryManagerHostTooltip": "Hafızayı doğrudan host adres aralığında tahsis eder. Çok daha hızlı JIT derleme ve işletimi sunar.", + "MemoryManagerUnsafeTooltip": "Hafızayı doğrudan tahsis eder, ancak host aralığına erişimden önce adresi maskelemez. Daha iyi performansa karşılık emniyetten ödün verir. Misafir uygulama Ryujinx içerisinden istediği hafızaya erişebilir, bu sebeple bu seçenek ile sadece güvendiğiniz uygulamaları çalıştırın.", + "UseHypervisorTooltip": "JIT yerine Hypervisor kullan. Uygun durumlarda performansı büyük oranda arttırır. Ancak şu anki halinde stabil durumda çalışmayabilir.", + "DRamTooltip": "Emüle edilen sistem hafızasını 4GiB'dan 6GiB'a yükseltir.\n\nBu seçenek yalnızca yüksek çözünürlük doku paketleri veya 4k çözünürlük modları için kullanılır. Performansı artırMAZ!\n\nEmin değilseniz devre dışı bırakın.", + "IgnoreMissingServicesTooltip": "Henüz programlanmamış Horizon işletim sistemi servislerini görmezden gelir. Bu seçenek belirli oyunların açılırken çökmesinin önüne geçmeye yardımcı olabilir.\n\nEmin değilseniz devre dışı bırakın.", + "IgnoreAppletTooltip": "Oyun sırasında oyun kumandasının bağlantısı kesilirse, harici \"Controller Applet\" iletişim kutusu görünmez. İletişim kutusunu kapatma veya yeni bir kumanda ayarlama isteği olmaz. Daha önce bağlantısı kesilen kumanda tekrar bağlandığında oyun otomatik olarak devam eder.", + "GraphicsBackendThreadingTooltip": "Grafik arka uç komutlarını ikinci bir iş parçacığında işletir.\n\nKendi multithreading desteği olmayan sürücülerde shader derlemeyi hızlandırıp performansı artırır. Multithreading desteği olan sürücülerde çok az daha iyi performans sağlar.\n\nEmin değilseniz Otomatik seçeneğine ayarlayın.", + "GalThreadingTooltip": "Grafik arka uç komutlarını ikinci bir iş parçacığında işletir.\n\nKendi multithreading desteği olmayan sürücülerde shader derlemeyi hızlandırıp performansı artırır. Multithreading desteği olan sürücülerde çok az daha iyi performans sağlar.\n\nEmin değilseniz Otomatik seçeneğine ayarlayın.", + "ShaderCacheToggleTooltip": "Sonraki çalışmalarda takılmaları engelleyen bir gölgelendirici disk önbelleğine kaydeder.", + "ResolutionScaleTooltip": "Multiplies the game's rendering resolution.\n\nA few games may not work with this and look pixelated even when the resolution is increased; for those games, you may need to find mods that remove anti-aliasing or that increase their internal rendering resolution. For using the latter, you'll likely want to select Native.\n\nThis option can be changed while a game is running by clicking \"Apply\" below; you can simply move the settings window aside and experiment until you find your preferred look for a game.\n\nKeep in mind 4x is overkill for virtually any setup.", + "ResolutionScaleEntryTooltip": "Küsüratlı çözünürlük ölçeği, 1.5 gibi. Küsüratlı ölçekler hata oluşturmaya ve çökmeye daha yatkındır.", + "AnisotropyTooltip": "Level of Anisotropic Filtering. Set to Auto to use the value requested by the game.", + "AspectRatioTooltip": "Aspect Ratio applied to the renderer window.\n\nOnly change this if you're using an aspect ratio mod for your game, otherwise the graphics will be stretched.\n\nLeave on 16:9 if unsure.", + "ShaderDumpPathTooltip": "Grafik Shader Döküm Yolu", + "FileLogTooltip": "Konsol loglarını diskte bir log dosyasına kaydeder. Performansı etkilemez.", + "StubLogTooltip": "Stub log mesajlarını konsola yazdırır. Performansı etkilemez.", + "InfoLogTooltip": "Bilgi log mesajlarını konsola yazdırır. Performansı etkilemez.", + "WarnLogTooltip": "Uyarı log mesajlarını konsola yazdırır. Performansı etkilemez.", + "ErrorLogTooltip": "Hata log mesajlarını konsola yazdırır. Performansı etkilemez.", + "TraceLogTooltip": "Trace log mesajlarını konsola yazdırır. Performansı etkilemez.", + "GuestLogTooltip": "Guest log mesajlarını konsola yazdırır. Performansı etkilemez.", + "FileAccessLogTooltip": "Dosya sistemi erişim log mesajlarını konsola yazdırır.", + "FSAccessLogModeTooltip": "Konsola FS erişim loglarının yazılmasını etkinleştirir. Kullanılabilir modlar 0-3'tür", + "DeveloperOptionTooltip": "Dikkatli kullanın", + "OpenGlLogLevel": "Uygun log seviyesinin aktif olmasını gerektirir", + "DebugLogTooltip": "Debug log mesajlarını konsola yazdırır.\n\nBu seçeneği yalnızca geliştirici üyemiz belirtirse aktifleştirin, çünkü bu seçenek log dosyasını okumayı zorlaştırır ve emülatörün performansını düşürür.", + "LoadApplicationFileTooltip": "Switch ile uyumlu bir dosya yüklemek için dosya tarayıcısını açar", + "LoadApplicationFolderTooltip": "Switch ile uyumlu ayrıştırılmamış bir uygulama yüklemek için dosya tarayıcısını açar", + "LoadDlcFromFolderTooltip": "Open a file explorer to choose one or more folders to bulk load DLC from", + "LoadTitleUpdatesFromFolderTooltip": "Open a file explorer to choose one or more folders to bulk load title updates from", + "OpenRyujinxFolderTooltip": "Ryujinx dosya sistem klasörünü açar", + "OpenRyujinxLogsTooltip": "Log dosyalarının bulunduğu klasörü açar", + "ExitTooltip": "Ryujinx'ten çıkış yapmayı sağlar", + "OpenSettingsTooltip": "Seçenekler penceresini açar", + "OpenProfileManagerTooltip": "Kullanıcı profil yöneticisi penceresini açar", + "StopEmulationTooltip": "Oynanmakta olan oyunun emülasyonunu durdurup oyun seçimine geri döndürür", + "CheckUpdatesTooltip": "Ryujinx güncellemelerini denetlemeyi sağlar", + "OpenAboutTooltip": "Hakkında penceresini açar", + "GridSize": "Öge Boyutu", + "GridSizeTooltip": "Grid ögelerinin boyutunu değiştirmeyi sağlar", + "SettingsTabSystemSystemLanguageBrazilianPortuguese": "Brezilya Portekizcesi", + "AboutRyujinxContributorsButtonHeader": "Tüm katkıda bulunanları gör", + "SettingsTabSystemAudioVolume": "Ses Seviyesi: ", + "AudioVolumeTooltip": "Ses seviyesini değiştirir", + "SettingsTabSystemEnableInternetAccess": "Guest Internet Erişimi/LAN Modu", + "EnableInternetAccessTooltip": "Emüle edilen uygulamanın internete bağlanmasını sağlar.\n\nLAN modu bulunan oyunlar bu seçenek ile birbirine bağlanabilir ve sistemler aynı access point'e bağlanır. Bu gerçek konsolları da kapsar.\n\nNintendo sunucularına bağlanmayı sağlaMAZ. Internete bağlanmaya çalışan baz oyunların çökmesine sebep olabilr.\n\nEmin değilseniz devre dışı bırakın.", + "GameListContextMenuManageCheatToolTip": "Hileleri yönetmeyi sağlar", + "GameListContextMenuManageCheat": "Hileleri Yönet", + "GameListContextMenuManageModToolTip": "Modları Yönet", + "GameListContextMenuManageMod": "Modları Yönet", + "ControllerSettingsStickRange": "Menzil:", + "DialogStopEmulationTitle": "Ryujinx - Emülasyonu Durdur", + "DialogStopEmulationMessage": "Emülasyonu durdurmak istediğinizden emin misiniz?", + "SettingsTabCpu": "İşlemci", + "SettingsTabAudio": "Ses", + "SettingsTabNetwork": "Ağ", + "SettingsTabNetworkConnection": "Ağ Bağlantısı", + "SettingsTabCpuCache": "İşlemci Belleği", + "SettingsTabCpuMemory": "CPU Hafızası", + "DialogUpdaterFlatpakNotSupportedMessage": "Lütfen Ryujinx'i FlatHub aracılığıyla güncelleyin.", + "UpdaterDisabledWarningTitle": "Güncelleyici Devre Dışı!", + "ControllerSettingsRotate90": "Saat yönünde 90° Döndür", + "IconSize": "Ikon Boyutu", + "IconSizeTooltip": "Oyun ikonlarının boyutunu değiştirmeyi sağlar", + "MenuBarOptionsShowConsole": "Konsol'u Göster", + "ShaderCachePurgeError": "Belirtilen shader cache temizlenirken hata {0}: {1}", + "UserErrorNoKeys": "Keys bulunamadı", + "UserErrorNoFirmware": "Firmware bulunamadı", + "UserErrorFirmwareParsingFailed": "Firmware çözümleme hatası", + "UserErrorApplicationNotFound": "Uygulama bulunamadı", + "UserErrorUnknown": "Bilinmeyen hata", + "UserErrorUndefined": "Tanımlanmayan hata", + "UserErrorNoKeysDescription": "Ryujinx 'prod.keys' dosyasını bulamadı", + "UserErrorNoFirmwareDescription": "Ryujinx yüklü herhangi firmware bulamadı", + "UserErrorFirmwareParsingFailedDescription": "Ryujinx temin edilen firmware'i çözümleyemedi. Bu durum genellikle güncel olmayan keys'den kaynaklanır.", + "UserErrorApplicationNotFoundDescription": "Ryujinx belirtilen yolda geçerli bir uygulama bulamadı.", + "UserErrorUnknownDescription": "Bilinmeyen bir hata oluştu!", + "UserErrorUndefinedDescription": "Tanımlanmayan bir hata oluştu! Bu durum ile karşılaşılmamalıydı, lütfen bir geliştirici ile iletişime geçin!", + "OpenSetupGuideMessage": "Kurulum Kılavuzunu Aç", + "NoUpdate": "Güncelleme Yok", + "TitleUpdateVersionLabel": "Sürüm {0} - {1}", + "TitleBundledUpdateVersionLabel": "Bundled: Version {0}", + "TitleBundledDlcLabel": "Bundled:", + "TitleXCIStatusPartialLabel": "Partial", + "TitleXCIStatusTrimmableLabel": "Untrimmed", + "TitleXCIStatusUntrimmableLabel": "Trimmed", + "TitleXCIStatusFailedLabel": "(Failed)", + "TitleXCICanSaveLabel": "Save {0:n0} Mb", + "TitleXCISavingLabel": "Saved {0:n0} Mb", + "RyujinxInfo": "Ryujinx - Bilgi", + "RyujinxConfirm": "Ryujinx - Doğrulama", + "FileDialogAllTypes": "Tüm türler", + "Never": "Hiçbir Zaman", + "SwkbdMinCharacters": "En az {0} karakter uzunluğunda olmalı", + "SwkbdMinRangeCharacters": "{0}-{1} karakter uzunluğunda olmalı", + "SoftwareKeyboard": "Yazılım Klavyesi", + "SoftwareKeyboardModeNumeric": "Sadece 0-9 veya '.' olabilir", + "SoftwareKeyboardModeAlphabet": "Sadece CJK-characters olmayan karakterler olabilir", + "SoftwareKeyboardModeASCII": "Sadece ASCII karakterler olabilir", + "ControllerAppletControllers": "Desteklenen Kumandalar:", + "ControllerAppletPlayers": "Oyuncular:", + "ControllerAppletDescription": "Halihazırdaki konfigürasyonunuz geçersiz. Ayarları açın ve girişlerinizi yeniden konfigüre edin.", + "ControllerAppletDocked": "Docked mod ayarlandı. Portatif denetim devre dışı bırakılmalı.", + "UpdaterRenaming": "Eski dosyalar yeniden adlandırılıyor...", + "UpdaterRenameFailed": "Güncelleyici belirtilen dosyayı yeniden adlandıramadı: {0}", + "UpdaterAddingFiles": "Yeni Dosyalar Ekleniyor...", + "UpdaterExtracting": "Güncelleme Ayrıştırılıyor...", + "UpdaterDownloading": "Güncelleme İndiriliyor...", + "Game": "Oyun", + "Docked": "Docked", + "Handheld": "El tipi", + "ConnectionError": "Bağlantı Hatası.", + "AboutPageDeveloperListMore": "{0} ve daha fazla...", + "ApiError": "API Hatası.", + "LoadingHeading": "{0} Yükleniyor", + "CompilingPPTC": "PTC Derleniyor", + "CompilingShaders": "Shaderlar Derleniyor", + "AllKeyboards": "Tüm Klavyeler", + "OpenFileDialogTitle": "Açmak için desteklenen bir dosya seçin", + "OpenFolderDialogTitle": "Ayrıştırılmamış oyun içeren bir klasör seçin", + "AllSupportedFormats": "Tüm Desteklenen Formatlar", + "RyujinxUpdater": "Ryujinx Güncelleyicisi", + "SettingsTabHotkeys": "Klavye Kısayolları", + "SettingsTabHotkeysHotkeys": "Klavye Kısayolları", + "SettingsTabHotkeysToggleVsyncHotkey": "VSync'i Etkinleştir/Devre Dışı Bırak:", + "SettingsTabHotkeysScreenshotHotkey": "Ekran Görüntüsü Al:", + "SettingsTabHotkeysShowUiHotkey": "Arayüzü Göster:", + "SettingsTabHotkeysPauseHotkey": "Durdur:", + "SettingsTabHotkeysToggleMuteHotkey": "Sustur:", + "ControllerMotionTitle": "Hareket Kontrol Seçenekleri", + "ControllerRumbleTitle": "Titreşim Seçenekleri", + "SettingsSelectThemeFileDialogTitle": "Tema Dosyası Seç", + "SettingsXamlThemeFile": "Xaml Tema Dosyası", + "AvatarWindowTitle": "Hesapları Yönet - Avatar", + "Amiibo": "Amiibo", + "Unknown": "Bilinmeyen", + "Usage": "Kullanım", + "Writable": "Yazılabilir", + "SelectDlcDialogTitle": "DLC dosyalarını seç", + "SelectUpdateDialogTitle": "Güncelleme dosyalarını seç", + "SelectModDialogTitle": "Mod Dizinini Seç", + "TrimXCIFileDialogTitle": "Check and Trim XCI File", + "TrimXCIFileDialogPrimaryText": "This function will first check the empty space and then trim the XCI File to save disk space.", + "TrimXCIFileDialogSecondaryText": "Current File Size: {0:n} MB\nGame Data Size: {1:n} MB\nDisk Space Savings: {2:n} MB", + "TrimXCIFileNoTrimNecessary": "XCI File does not need to be trimmed. Check logs for further details", + "TrimXCIFileNoUntrimPossible": "XCI File cannot be untrimmed. Check logs for further details", + "TrimXCIFileReadOnlyFileCannotFix": "XCI File is Read Only and could not be made writable. Check logs for further details", + "TrimXCIFileFileSizeChanged": "XCI File has changed in size since it was scanned. Please check the file is not being written to and try again.", + "TrimXCIFileFreeSpaceCheckFailed": "XCI File has data in the free space area, it is not safe to trim", + "TrimXCIFileInvalidXCIFile": "XCI File contains invalid data. Check logs for further details", + "TrimXCIFileFileIOWriteError": "XCI File could not be opened for writing. Check logs for further details", + "TrimXCIFileFailedPrimaryText": "Trimming of the XCI file failed", + "TrimXCIFileCancelled": "The operation was cancelled", + "TrimXCIFileFileUndertermined": "No operation was performed", + "UserProfileWindowTitle": "Kullanıcı Profillerini Yönet", + "CheatWindowTitle": "Oyun Hilelerini Yönet", + "DlcWindowTitle": "Oyun DLC'lerini Yönet", + "ModWindowTitle": "Manage Mods for {0} ({1})", + "UpdateWindowTitle": "Oyun Güncellemelerini Yönet", + "XCITrimmerWindowTitle": "XCI File Trimmer", + "XCITrimmerTitleStatusCount": "{0} of {1} Title(s) Selected", + "XCITrimmerTitleStatusCountWithFilter": "{0} of {1} Title(s) Selected ({2} displayed)", + "XCITrimmerTitleStatusTrimming": "Trimming {0} Title(s)...", + "XCITrimmerTitleStatusUntrimming": "Untrimming {0} Title(s)...", + "XCITrimmerTitleStatusFailed": "Failed", + "XCITrimmerPotentialSavings": "Potential Savings", + "XCITrimmerActualSavings": "Actual Savings", + "XCITrimmerSavingsMb": "{0:n0} Mb", + "XCITrimmerSelectDisplayed": "Select Shown", + "XCITrimmerDeselectDisplayed": "Deselect Shown", + "XCITrimmerSortName": "Title", + "XCITrimmerSortSaved": "Space Savings", + "XCITrimmerTrim": "Trim", + "XCITrimmerUntrim": "Untrim", + "UpdateWindowUpdateAddedMessage": "{0} new update(s) added", + "UpdateWindowBundledContentNotice": "Bundled updates cannot be removed, only disabled.", + "CheatWindowHeading": "{0} için Hile mevcut [{1}]", + "BuildId": "BuildId:", + "DlcWindowBundledContentNotice": "Bundled DLC cannot be removed, only disabled.", + "DlcWindowHeading": "{0} Downloadable Content(s) available for {1} ({2})", + "DlcWindowDlcAddedMessage": "{0} new downloadable content(s) added", + "AutoloadDlcAddedMessage": "{0} new downloadable content(s) added", + "AutoloadDlcRemovedMessage": "{0} missing downloadable content(s) removed", + "AutoloadUpdateAddedMessage": "{0} new update(s) added", + "AutoloadUpdateRemovedMessage": "{0} missing update(s) removed", + "ModWindowHeading": "{0} Mod(lar)", + "UserProfilesEditProfile": "Seçiliyi Düzenle", + "Continue": "Continue", + "Cancel": "İptal", + "Save": "Kaydet", + "Discard": "Iskarta", + "Paused": "Durduruldu", + "UserProfilesSetProfileImage": "Profil Resmi Ayarla", + "UserProfileEmptyNameError": "İsim gerekli", + "UserProfileNoImageError": "Profil resmi ayarlanmalıdır", + "GameUpdateWindowHeading": "{0} için güncellemeler mevcut [{1}]", + "SettingsTabHotkeysResScaleUpHotkey": "Çözünürlüğü artır:", + "SettingsTabHotkeysResScaleDownHotkey": "Çözünürlüğü azalt:", + "UserProfilesName": "İsim:", + "UserProfilesUserId": "Kullanıcı Adı:", + "SettingsTabGraphicsBackend": "Grafik Arka Ucu", + "SettingsTabGraphicsBackendTooltip": "Select the graphics backend that will be used in the emulator.\n\nVulkan is overall better for all modern graphics cards, as long as their drivers are up to date. Vulkan also features faster shader compilation (less stuttering) on all GPU vendors.\n\nOpenGL may achieve better results on old Nvidia GPUs, on old AMD GPUs on Linux, or on GPUs with lower VRAM, though shader compilation stutters will be greater.\n\nSet to Vulkan if unsure. Set to OpenGL if your GPU does not support Vulkan even with the latest graphics drivers.", + "SettingsEnableTextureRecompression": "Yeniden Doku Sıkıştırılmasını Aktif Et", + "SettingsEnableTextureRecompressionTooltip": "Compresses ASTC textures in order to reduce VRAM usage.\n\nGames using this texture format include Astral Chain, Bayonetta 3, Fire Emblem Engage, Metroid Prime Remastered, Super Mario Bros. Wonder and The Legend of Zelda: Tears of the Kingdom.\n\nGraphics cards with 4GiB VRAM or less will likely crash at some point while running these games.\n\nEnable only if you're running out of VRAM on the aforementioned games. Leave OFF if unsure.", + "SettingsTabGraphicsPreferredGpu": "Kullanılan GPU", + "SettingsTabGraphicsPreferredGpuTooltip": "Vulkan Grafik Arka Ucu ile kullanılacak Ekran Kartını Seçin.\n\nOpenGL'nin kullanacağı GPU'yu etkilemez.\n\n Emin değilseniz \"dGPU\" olarak işaretlenmiş GPU'ya ayarlayın. Eğer yoksa, dokunmadan bırakın.\n", + "SettingsAppRequiredRestartMessage": "Ryujinx'i Yeniden Başlatma Gerekli", + "SettingsGpuBackendRestartMessage": "Grafik Motoru ya da GPU ayarları değiştirildi. Bu işlemin uygulanması için yeniden başlatma gerekli.", + "SettingsGpuBackendRestartSubMessage": "Şimdi yeniden başlatmak istiyor musunuz?", + "RyujinxUpdaterMessage": "Ryujinx'i en son sürüme güncellemek ister misiniz?", + "SettingsTabHotkeysVolumeUpHotkey": "Sesi Arttır:", + "SettingsTabHotkeysVolumeDownHotkey": "Sesi Azalt:", + "SettingsEnableMacroHLE": "Macro HLE'yi Aktifleştir", + "SettingsEnableMacroHLETooltip": "GPU Macro kodunun yüksek seviye emülasyonu.\n\nPerformansı arttırır, ama bazı oyunlarda grafik hatalarına yol açabilir.\n\nEmin değilseniz AÇIK bırakın.", + "SettingsEnableColorSpacePassthrough": "Renk Alanı Geçişi", + "SettingsEnableColorSpacePassthroughTooltip": "Vulkan Backend'ini renk alanı belirtmeden renk bilgisinden geçmeye yönlendirir. Geniş gam ekranlı kullanıcılar için bu, renk doğruluğu pahasına daha canlı renklerle sonuçlanabilir.", + "VolumeShort": "Ses", + "UserProfilesManageSaves": "Kayıtları Yönet", + "DeleteUserSave": "Bu oyun için kullanıcı kaydını silmek istiyor musunuz?", + "IrreversibleActionNote": "Bu eylem geri alınamaz.", + "SaveManagerHeading": "{0} için Kayıt Dosyalarını Yönet", + "SaveManagerTitle": "Kayıt Yöneticisi", + "Name": "İsim", + "Size": "Boyut", + "Search": "Ara", + "UserProfilesRecoverLostAccounts": "Kayıp Hesapları Kurtar", + "Recover": "Kurtar", + "UserProfilesRecoverHeading": "Aşağıdaki hesaplar için kayıtlar bulundu", + "UserProfilesRecoverEmptyList": "Kurtarılacak profil bulunamadı", + "GraphicsAATooltip": "Applies anti-aliasing to the game render.\n\nFXAA will blur most of the image, while SMAA will attempt to find jagged edges and smooth them out.\n\nNot recommended to use in conjunction with the FSR scaling filter.\n\nThis option can be changed while a game is running by clicking \"Apply\" below; you can simply move the settings window aside and experiment until you find your preferred look for a game.\n\nLeave on NONE if unsure.", + "GraphicsAALabel": "Kenar Yumuşatma:", + "GraphicsScalingFilterLabel": "Ölçekleme Filtresi:", + "GraphicsScalingFilterTooltip": "Choose the scaling filter that will be applied when using resolution scale.\n\nBilinear works well for 3D games and is a safe default option.\n\nNearest is recommended for pixel art games.\n\nFSR 1.0 is merely a sharpening filter, not recommended for use with FXAA or SMAA.\n\nThis option can be changed while a game is running by clicking \"Apply\" below; you can simply move the settings window aside and experiment until you find your preferred look for a game.\n\nLeave on BILINEAR if unsure.", + "GraphicsScalingFilterBilinear": "Bilinear", + "GraphicsScalingFilterNearest": "Nearest", + "GraphicsScalingFilterFsr": "FSR", + "GraphicsScalingFilterArea": "Area", + "GraphicsScalingFilterLevelLabel": "Seviye", + "GraphicsScalingFilterLevelTooltip": "Set FSR 1.0 sharpening level. Higher is sharper.", + "SmaaLow": "Düşük SMAA", + "SmaaMedium": "Orta SMAA", + "SmaaHigh": "Yüksek SMAA", + "SmaaUltra": "En Yüksek SMAA", + "UserEditorTitle": "Kullanıcıyı Düzenle", + "UserEditorTitleCreate": "Kullanıcı Oluştur", + "SettingsTabNetworkInterface": "Ağ Bağlantısı:", + "NetworkInterfaceTooltip": "The network interface used for LAN/LDN features.\n\nIn conjunction with a VPN or XLink Kai and a game with LAN support, can be used to spoof a same-network connection over the Internet.\n\nLeave on DEFAULT if unsure.", + "NetworkInterfaceDefault": "Varsayılan", + "PackagingShaders": "Gölgeler Paketleniyor", + "AboutChangelogButton": "GitHub'da Değişiklikleri Görüntüle", + "AboutChangelogButtonTooltipMessage": "Kullandığınız versiyon için olan değişiklikleri varsayılan tarayıcınızda görmek için tıklayın", + "SettingsTabNetworkMultiplayer": "Çok Oyunculu", + "MultiplayerMode": "Mod:", + "MultiplayerModeTooltip": "Change LDN multiplayer mode.\n\nLdnMitm will modify local wireless/local play functionality in games to function as if it were LAN, allowing for local, same-network connections with other Ryujinx instances and hacked Nintendo Switch consoles that have the ldn_mitm module installed.\n\nMultiplayer requires all players to be on the same game version (i.e. Super Smash Bros. Ultimate v13.0.1 can't connect to v13.0.0).\n\nLeave DISABLED if unsure.", + "MultiplayerModeDisabled": "Devre Dışı", + "MultiplayerModeLdnMitm": "ldn_mitm", + "MultiplayerModeLdnRyu": "RyuLDN", + "MultiplayerDisableP2P": "Disable P2P Network Hosting (may increase latency)", + "MultiplayerDisableP2PTooltip": "Disable P2P network hosting, peers will proxy through the master server instead of connecting to you directly.", + "LdnPassphrase": "Network Passphrase:", + "LdnPassphraseTooltip": "You will only be able to see hosted games with the same passphrase as you.", + "LdnPassphraseInputTooltip": "Enter a passphrase in the format Ryujinx-<8 hex chars>. You will only be able to see hosted games with the same passphrase as you.", + "LdnPassphraseInputPublic": "(public)", + "GenLdnPass": "Generate Random", + "GenLdnPassTooltip": "Generates a new passphrase, which can be shared with other players.", + "ClearLdnPass": "Clear", + "ClearLdnPassTooltip": "Clears the current passphrase, returning to the public network.", + "InvalidLdnPassphrase": "Invalid Passphrase! Must be in the format \"Ryujinx-<8 hex chars>\"" +} diff --git a/src/Ryujinx/Assets/Locales/uk_UA.json b/src/Ryujinx/Assets/Locales/uk_UA.json new file mode 100644 index 000000000..e123afa6b --- /dev/null +++ b/src/Ryujinx/Assets/Locales/uk_UA.json @@ -0,0 +1,868 @@ +{ + "Language": "Українська", + "MenuBarFileOpenApplet": "Відкрити аплет", + "MenuBarFileOpenAppletOpenMiiApplet": "Mii Edit Applet", + "MenuBarFileOpenAppletOpenMiiAppletToolTip": "Відкрити аплет Mii Editor в автономному режимі", + "SettingsTabInputDirectMouseAccess": "Прямий доступ мишею", + "SettingsTabSystemMemoryManagerMode": "Режим диспетчера пам’яті:", + "SettingsTabSystemMemoryManagerModeSoftware": "Програмне забезпечення", + "SettingsTabSystemMemoryManagerModeHost": "Хост (швидко)", + "SettingsTabSystemMemoryManagerModeHostUnchecked": "Неперевірений хост (найшвидший, небезпечний)", + "SettingsTabSystemUseHypervisor": "Використовувати гіпервізор", + "MenuBarFile": "_Файл", + "MenuBarFileOpenFromFile": "_Завантажити програму з файлу", + "MenuBarFileOpenFromFileError": "No applications found in selected file.", + "MenuBarFileOpenUnpacked": "Завантажити _розпаковану гру", + "MenuBarFileLoadDlcFromFolder": "Load DLC From Folder", + "MenuBarFileLoadTitleUpdatesFromFolder": "Load Title Updates From Folder", + "MenuBarFileOpenEmuFolder": "Відкрити теку Ryujinx", + "MenuBarFileOpenLogsFolder": "Відкрити теку журналів змін", + "MenuBarFileExit": "_Вихід", + "MenuBarOptions": "_Параметри", + "MenuBarOptionsToggleFullscreen": "На весь екран", + "MenuBarOptionsStartGamesInFullscreen": "Запускати ігри на весь екран", + "MenuBarOptionsStopEmulation": "Зупинити емуляцію", + "MenuBarOptionsSettings": "_Налаштування", + "MenuBarOptionsManageUserProfiles": "_Керувати профілями користувачів", + "MenuBarActions": "_Дії", + "MenuBarOptionsSimulateWakeUpMessage": "Симулювати повідомлення про пробудження", + "MenuBarActionsScanAmiibo": "Сканувати Amiibo", + "MenuBarTools": "_Інструменти", + "MenuBarToolsInstallFirmware": "Установити прошивку", + "MenuBarFileToolsInstallFirmwareFromFile": "Установити прошивку з XCI або ZIP", + "MenuBarFileToolsInstallFirmwareFromDirectory": "Установити прошивку з теки", + "MenuBarToolsManageFileTypes": "Керувати типами файлів", + "MenuBarToolsInstallFileTypes": "Установити типи файлів", + "MenuBarToolsUninstallFileTypes": "Видалити типи файлів", + "MenuBarToolsXCITrimmer": "Trim XCI Files", + "MenuBarView": "_View", + "MenuBarViewWindow": "Window Size", + "MenuBarViewWindow720": "720p", + "MenuBarViewWindow1080": "1080p", + "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", + "GameListContextMenuCacheManagement": "Керування кешем", + "GameListContextMenuCacheManagementPurgePptc": "Очистити кеш PPTC", + "GameListContextMenuCacheManagementPurgePptcToolTip": "Видаляє кеш PPTC програми", + "GameListContextMenuCacheManagementPurgeShaderCache": "Очистити кеш шейдерів", + "GameListContextMenuCacheManagementPurgeShaderCacheToolTip": "Видаляє кеш шейдерів програми", + "GameListContextMenuCacheManagementOpenPptcDirectory": "Відкрити каталог PPTC", + "GameListContextMenuCacheManagementOpenPptcDirectoryToolTip": "Відкриває каталог, який містить кеш PPTC програми", + "GameListContextMenuCacheManagementOpenShaderCacheDirectory": "Відкрити каталог кешу шейдерів", + "GameListContextMenuCacheManagementOpenShaderCacheDirectoryToolTip": "Відкриває каталог, який містить кеш шейдерів програми", + "GameListContextMenuExtractData": "Видобути дані", + "GameListContextMenuExtractDataExeFS": "ExeFS", + "GameListContextMenuExtractDataExeFSToolTip": "Видобуває розділ ExeFS із поточної конфігурації програми (включаючи оновлення)", + "GameListContextMenuExtractDataRomFS": "RomFS", + "GameListContextMenuExtractDataRomFSToolTip": "Видобуває розділ RomFS із поточної конфігурації програми (включаючи оновлення)", + "GameListContextMenuExtractDataLogo": "Логотип", + "GameListContextMenuExtractDataLogoToolTip": "Видобуває розділ логотипу з поточної конфігурації програми (включаючи оновлення)", + "GameListContextMenuCreateShortcut": "Створити ярлик застосунку", + "GameListContextMenuCreateShortcutToolTip": "Створити ярлик на робочому столі, який запускає вибраний застосунок", + "GameListContextMenuCreateShortcutToolTipMacOS": "Створити ярлик у каталозі macOS програм, що запускає обраний Додаток", + "GameListContextMenuOpenModsDirectory": "Відкрити теку з модами", + "GameListContextMenuOpenModsDirectoryToolTip": "Відкриває каталог, який містить модифікації Додатків", + "GameListContextMenuOpenSdModsDirectory": "Відкрити каталог модифікацій Atmosphere", + "GameListContextMenuOpenSdModsDirectoryToolTip": "Відкриває альтернативний каталог SD-карти Atmosphere, що містить модифікації Додатків. Корисно для модифікацій, зроблених для реального обладнання.", + "GameListContextMenuTrimXCI": "Check and Trim XCI File", + "GameListContextMenuTrimXCIToolTip": "Check and Trim XCI File to Save Disk Space", + "StatusBarGamesLoaded": "{0}/{1} ігор завантажено", + "StatusBarSystemVersion": "Версія системи: {0}", + "StatusBarXCIFileTrimming": "Trimming XCI File '{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": "Показати діалогове вікно «Підтвердити вихід».", + "SettingsTabGeneralRememberWindowState": "Remember Window Size/Position", + "SettingsTabGeneralShowTitleBar": "Show Title Bar (Requires restart)", + "SettingsTabGeneralHideCursor": "Сховати вказівник:", + "SettingsTabGeneralHideCursorNever": "Ніколи", + "SettingsTabGeneralHideCursorOnIdle": "Сховати у режимі очікування", + "SettingsTabGeneralHideCursorAlways": "Завжди", + "SettingsTabGeneralGameDirectories": "Тека ігор", + "SettingsTabGeneralAutoloadDirectories": "Autoload DLC/Updates Directories", + "SettingsTabGeneralAutoloadNote": "DLC and Updates which refer to missing files will be unloaded automatically", + "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 (профільований постійний кеш перекладу)", + "SettingsTabSystemEnableLowPowerPptc": "Low-power PPTC", + "SettingsTabSystemEnableFsIntegrityChecks": "Перевірка цілісності FS", + "SettingsTabSystemAudioBackend": "Аудіосистема:", + "SettingsTabSystemAudioBackendDummy": "Dummy", + "SettingsTabSystemAudioBackendOpenAL": "OpenAL", + "SettingsTabSystemAudioBackendSoundIO": "SoundIO", + "SettingsTabSystemAudioBackendSDL2": "SDL2", + "SettingsTabSystemHacks": "Хитрощі", + "SettingsTabSystemHacksNote": " (може викликати нестабільність)", + "SettingsTabSystemDramSize": "Використовувати альтернативне розташування пам'яті (розробники)", + "SettingsTabSystemDramSize4GiB": "4GiB", + "SettingsTabSystemDramSize6GiB": "6GiB", + "SettingsTabSystemDramSize8GiB": "8GiB", + "SettingsTabSystemDramSize12GiB": "12GiB", + "SettingsTabSystemIgnoreMissingServices": "Ігнорувати відсутні служби", + "SettingsTabSystemIgnoreApplet": "Ігнорувати Аплет", + "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": "Увімкнути журнали заглушки", + "SettingsTabLoggingEnableInfoLogs": "Увімкнути інформаційні журнали", + "SettingsTabLoggingEnableWarningLogs": "Увімкнути журнали попереджень", + "SettingsTabLoggingEnableErrorLogs": "Увімкнути журнали помилок", + "SettingsTabLoggingEnableTraceLogs": "Увімкнути журнали трасування", + "SettingsTabLoggingEnableGuestLogs": "Увімкнути журнали гостей", + "SettingsTabLoggingEnableFsAccessLogs": "Увімкнути журнали доступу Fs", + "SettingsTabLoggingFsGlobalAccessLogMode": "Режим журналу глобального доступу Fs:", + "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": "Хост сервера:", + "ControllerSettingsMotionGyroSensitivity": "Чутливість гіроскопа:", + "ControllerSettingsMotionGyroDeadzone": "Мертва зона гіроскопа:", + "ControllerSettingsSave": "Зберегти", + "ControllerSettingsClose": "Закрити", + "KeyUnknown": "Unknown", + "KeyShiftLeft": "Shift Left", + "KeyShiftRight": "Shift Right", + "KeyControlLeft": "Ctrl Left", + "KeyMacControlLeft": "⌃ Left", + "KeyControlRight": "Ctrl Right", + "KeyMacControlRight": "⌃ Right", + "KeyAltLeft": "Alt Left", + "KeyMacAltLeft": "⌥ Left", + "KeyAltRight": "Alt Right", + "KeyMacAltRight": "⌥ Right", + "KeyWinLeft": "⊞ Left", + "KeyMacWinLeft": "⌘ Left", + "KeyWinRight": "⊞ Right", + "KeyMacWinRight": "⌘ Right", + "KeyMenu": "Menu", + "KeyUp": "Up", + "KeyDown": "Down", + "KeyLeft": "Left", + "KeyRight": "Right", + "KeyEnter": "Enter", + "KeyEscape": "Escape", + "KeySpace": "Space", + "KeyTab": "Tab", + "KeyBackSpace": "Backspace", + "KeyInsert": "Insert", + "KeyDelete": "Delete", + "KeyPageUp": "Page Up", + "KeyPageDown": "Page Down", + "KeyHome": "Home", + "KeyEnd": "End", + "KeyCapsLock": "Caps Lock", + "KeyScrollLock": "Scroll Lock", + "KeyPrintScreen": "Print Screen", + "KeyPause": "Pause", + "KeyNumLock": "Num Lock", + "KeyClear": "Clear", + "KeyKeypad0": "Keypad 0", + "KeyKeypad1": "Keypad 1", + "KeyKeypad2": "Keypad 2", + "KeyKeypad3": "Keypad 3", + "KeyKeypad4": "Keypad 4", + "KeyKeypad5": "Keypad 5", + "KeyKeypad6": "Keypad 6", + "KeyKeypad7": "Keypad 7", + "KeyKeypad8": "Keypad 8", + "KeyKeypad9": "Keypad 9", + "KeyKeypadDivide": "Keypad Divide", + "KeyKeypadMultiply": "Keypad Multiply", + "KeyKeypadSubtract": "Keypad Subtract", + "KeyKeypadAdd": "Keypad Add", + "KeyKeypadDecimal": "Keypad Decimal", + "KeyKeypadEnter": "Keypad Enter", + "KeyNumber0": "0", + "KeyNumber1": "1", + "KeyNumber2": "2", + "KeyNumber3": "3", + "KeyNumber4": "4", + "KeyNumber5": "5", + "KeyNumber6": "6", + "KeyNumber7": "7", + "KeyNumber8": "8", + "KeyNumber9": "9", + "KeyTilde": "~", + "KeyGrave": "`", + "KeyMinus": "-", + "KeyPlus": "+", + "KeyBracketLeft": "[", + "KeyBracketRight": "]", + "KeySemicolon": ";", + "KeyQuote": "\"", + "KeyComma": ",", + "KeyPeriod": ".", + "KeySlash": "/", + "KeyBackSlash": "\\", + "KeyUnbound": "Unbound", + "GamepadLeftStick": "L Stick Button", + "GamepadRightStick": "R Stick Button", + "GamepadLeftShoulder": "Left Shoulder", + "GamepadRightShoulder": "Right Shoulder", + "GamepadLeftTrigger": "Left Trigger", + "GamepadRightTrigger": "Right Trigger", + "GamepadDpadUp": "Up", + "GamepadDpadDown": "Down", + "GamepadDpadLeft": "Left", + "GamepadDpadRight": "Right", + "GamepadMinus": "-", + "GamepadPlus": "+", + "GamepadGuide": "Guide", + "GamepadMisc1": "Misc", + "GamepadPaddle1": "Paddle 1", + "GamepadPaddle2": "Paddle 2", + "GamepadPaddle3": "Paddle 3", + "GamepadPaddle4": "Paddle 4", + "GamepadTouchpad": "Touchpad", + "GamepadSingleLeftTrigger0": "Left Trigger 0", + "GamepadSingleRightTrigger0": "Right Trigger 0", + "GamepadSingleLeftTrigger1": "Left Trigger 1", + "GamepadSingleRightTrigger1": "Right Trigger 1", + "StickLeft": "Left Stick", + "StickRight": "Right Stick", + "UserProfilesSelectedUserProfile": "Вибраний профіль користувача:", + "UserProfilesSaveProfileName": "Зберегти ім'я профілю", + "UserProfilesChangeProfileImage": "Змінити зображення профілю", + "UserProfilesAvailableUserProfiles": "Доступні профілі користувачів:", + "UserProfilesAddNewProfile": "Створити профіль", + "UserProfilesDelete": "Видалити", + "UserProfilesClose": "Закрити", + "ProfileNameSelectionWatermark": "Оберіть псевдонім", + "ProfileImageSelectionTitle": "Вибір зображення профілю", + "ProfileImageSelectionHeader": "Виберіть зображення профілю", + "ProfileImageSelectionNote": "Ви можете імпортувати власне зображення профілю або вибрати аватар із мікропрограми системи", + "ProfileImageSelectionImportImage": "Імпорт файлу зображення", + "ProfileImageSelectionSelectAvatar": "Виберіть аватар прошивки ", + "InputDialogTitle": "Діалог введення", + "InputDialogOk": "Гаразд", + "InputDialogCancel": "Скасувати", + "InputDialogCancelling": "Cancelling", + "InputDialogClose": "Close", + "InputDialogAddNewProfileTitle": "Виберіть ім'я профілю", + "InputDialogAddNewProfileHeader": "Будь ласка, введіть ім'я профілю", + "InputDialogAddNewProfileSubtext": "(Макс. довжина: {0})", + "AvatarChoose": "Вибрати", + "AvatarSetBackgroundColor": "Встановити колір фону", + "AvatarClose": "Закрити", + "ControllerSettingsLoadProfileToolTip": "Завантажити профіль", + "ControllerSettingsViewProfileToolTip": "View Profile", + "ControllerSettingsAddProfileToolTip": "Додати профіль", + "ControllerSettingsRemoveProfileToolTip": "Видалити профіль", + "ControllerSettingsSaveProfileToolTip": "Зберегти профіль", + "MenuBarFileToolsTakeScreenshot": "Зробити знімок екрана", + "MenuBarFileToolsHideUi": "Сховати інтерфейс", + "GameListContextMenuRunApplication": "Запустити додаток", + "GameListContextMenuToggleFavorite": "Перемкнути вибране", + "GameListContextMenuToggleFavoriteToolTip": "Перемкнути улюблений статус гри", + "SettingsTabGeneralTheme": "Тема:", + "SettingsTabGeneralThemeAuto": "Auto", + "SettingsTabGeneralThemeDark": "Темна", + "SettingsTabGeneralThemeLight": "Світла", + "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": "Видобування розділу {0} з {1}...", + "DialogNcaExtractionTitle": "Екстрактор розділів NCA", + "DialogNcaExtractionMainNcaNotFoundErrorMessage": "Помилка видобування. Основний NCA не був присутній у вибраному файлі.", + "DialogNcaExtractionCheckLogErrorMessage": "Помилка видобування. Прочитайте файл журналу для отримання додаткової інформації.", + "DialogNcaExtractionSuccessMessage": "Видобування успішно завершено.", + "DialogUpdaterConvertFailedMessage": "Не вдалося конвертувати поточну версію Ryujinx.", + "DialogUpdaterCancelUpdateMessage": "Скасування оновлення!", + "DialogUpdaterAlreadyOnLatestVersionMessage": "Ви вже використовуєте останню версію Ryujinx!", + "DialogUpdaterFailedToGetVersionMessage": "Під час спроби отримати інформацію про випуск із GitHub Release сталася помилка. Це може бути спричинено, якщо новий випуск компілюється GitHub Actions. Повторіть спробу через кілька хвилин.", + "DialogUpdaterConvertFailedGithubMessage": "Не вдалося конвертувати отриману версію Ryujinx із випуску Github.", + "DialogUpdaterDownloadingMessage": "Завантаження оновлення...", + "DialogUpdaterExtractionMessage": "Видобування оновлення...", + "DialogUpdaterRenamingMessage": "Перейменування оновлення...", + "DialogUpdaterAddingFilesMessage": "Додавання нового оновлення...", + "DialogUpdaterShowChangelogMessage": "Show Changelog", + "DialogUpdaterCompleteMessage": "Оновлення завершено!", + "DialogUpdaterRestartMessage": "Перезапустити Ryujinx зараз?", + "DialogUpdaterNoInternetMessage": "Ви не підключені до Інтернету!", + "DialogUpdaterNoInternetSubMessage": "Будь ласка, переконайтеся, що у вас є робоче підключення до Інтернету!", + "DialogUpdaterDirtyBuildMessage": "Ви не можете оновити брудну збірку Ryujinx!", + "DialogUpdaterDirtyBuildSubMessage": "Будь ласка, завантажте Ryujinx на https://ryujinx.app/download, якщо ви шукаєте підтримувану версію.", + "DialogRestartRequiredMessage": "Потрібен перезапуск", + "DialogThemeRestartMessage": "Тему збережено. Щоб застосувати тему, потрібен перезапуск.", + "DialogThemeRestartSubMessage": "Ви хочете перезапустити", + "DialogFirmwareInstallEmbeddedMessage": "Бажаєте встановити прошивку, вбудовану в цю гру? (Прошивка {0})", + "DialogFirmwareInstallEmbeddedSuccessMessage": "Встановлену прошивку не знайдено, але Ryujinx вдалося встановити прошивку {0} з наданої гри.\nТепер запуститься емулятор.", + "DialogFirmwareNoFirmwareInstalledMessage": "Прошивка не встановлена", + "DialogFirmwareInstalledMessage": "Встановлено прошивку {0}", + "DialogInstallFileTypesSuccessMessage": "Успішно встановлено типи файлів!", + "DialogInstallFileTypesErrorMessage": "Не вдалося встановити типи файлів.", + "DialogUninstallFileTypesSuccessMessage": "Успішно видалено типи файлів!", + "DialogUninstallFileTypesErrorMessage": "Не вдалося видалити типи файлів.", + "DialogOpenSettingsWindowLabel": "Відкрити вікно налаштувань", + "DialogOpenXCITrimmerWindowLabel": "XCI Trimmer Window", + "DialogControllerAppletTitle": "Аплет контролера", + "DialogMessageDialogErrorExceptionMessage": "Помилка показу діалогового вікна повідомлення: {0}", + "DialogSoftwareKeyboardErrorExceptionMessage": "Помилка показу програмної клавіатури: {0}", + "DialogErrorAppletErrorExceptionMessage": "Помилка показу діалогового вікна ErrorApplet: {0}", + "DialogUserErrorDialogMessage": "{0}: {1}", + "DialogUserErrorDialogInfoMessage": "\nДля отримання додаткової інформації про те, як виправити цю помилку, дотримуйтесь нашого посібника з налаштування.", + "DialogUserErrorDialogTitle": "Помилка Ryujinx ({0})", + "DialogAmiiboApiTitle": "Amiibo API", + "DialogAmiiboApiFailFetchMessage": "Під час отримання інформації з API сталася помилка.", + "DialogAmiiboApiConnectErrorMessage": "Неможливо підключитися до сервера Amiibo API. Можливо, служба не працює або вам потрібно перевірити, чи є підключення до Інтернету.", + "DialogProfileInvalidProfileErrorMessage": "Профіль {0} несумісний із поточною системою конфігурації вводу.", + "DialogProfileDefaultProfileOverwriteErrorMessage": "Стандартний профіль не можна перезаписати", + "DialogProfileDeleteProfileTitle": "Видалення профілю", + "DialogProfileDeleteProfileMessage": "Цю дію неможливо скасувати. Ви впевнені, що бажаєте продовжити?", + "DialogWarning": "Увага", + "DialogPPTCDeletionMessage": "Ви збираєтеся видалити кеш PPTC для:\n\n{0}\n\nВи впевнені, що бажаєте продовжити?", + "DialogPPTCDeletionErrorMessage": "Помилка очищення кешу PPTC на {0}: {1}", + "DialogShaderDeletionMessage": "Ви збираєтеся видалити кеш шейдерів для:\n\n{0}\n\nВи впевнені, що бажаєте продовжити?", + "DialogShaderDeletionErrorMessage": "Помилка очищення кешу шейдерів на {0}: {1}", + "DialogRyujinxErrorMessage": "У Ryujinx сталася помилка", + "DialogInvalidTitleIdErrorMessage": "Помилка інтерфейсу: вибрана гра не мала дійсного ідентифікатора назви", + "DialogFirmwareInstallerFirmwareNotFoundErrorMessage": "Дійсна прошивка системи не знайдена в {0}.", + "DialogFirmwareInstallerFirmwareInstallTitle": "Встановити прошивку {0}", + "DialogFirmwareInstallerFirmwareInstallMessage": "Буде встановлено версію системи {0}.", + "DialogFirmwareInstallerFirmwareInstallSubMessage": "\n\nЦе замінить поточну версію системи {0}.", + "DialogFirmwareInstallerFirmwareInstallConfirmMessage": "\n\nВи хочете продовжити?", + "DialogFirmwareInstallerFirmwareInstallWaitMessage": "Встановлення прошивки...", + "DialogFirmwareInstallerFirmwareInstallSuccessMessage": "Версію системи {0} успішно встановлено.", + "DialogUserProfileDeletionWarningMessage": "Якщо вибраний профіль буде видалено, інші профілі не відкриватимуться", + "DialogUserProfileDeletionConfirmMessage": "Ви хочете видалити вибраний профіль", + "DialogUserProfileUnsavedChangesTitle": "Увага — Незбережені зміни", + "DialogUserProfileUnsavedChangesMessage": "Ви зробили зміни у цьому профілю користувача які не було збережено.", + "DialogUserProfileUnsavedChangesSubMessage": "Бажаєте скасувати зміни?", + "DialogControllerSettingsModifiedConfirmMessage": "Поточні налаштування контролера оновлено.", + "DialogControllerSettingsModifiedConfirmSubMessage": "Ви хочете зберегти?", + "DialogLoadFileErrorMessage": "{0}. Файл з помилкою: {1}", + "DialogModAlreadyExistsMessage": "Модифікація вже існує", + "DialogModInvalidMessage": "Вказаний каталог не містить модифікації!", + "DialogModDeleteNoParentMessage": "Не видалено: Не знайдено батьківський каталог для модифікації \"{0}\"!", + "DialogDlcNoDlcErrorMessage": "Зазначений файл не містить DLC для вибраного заголовку!", + "DialogPerformanceCheckLoggingEnabledMessage": "Ви увімкнули журнал налагодження, призначений лише для розробників.", + "DialogPerformanceCheckLoggingEnabledConfirmMessage": "Для оптимальної продуктивності рекомендується вимкнути ведення журналу налагодження. Ви хочете вимкнути ведення журналу налагодження зараз?", + "DialogPerformanceCheckShaderDumpEnabledMessage": "Ви увімкнули скидання шейдерів, призначений лише для розробників.", + "DialogPerformanceCheckShaderDumpEnabledConfirmMessage": "Для оптимальної продуктивності рекомендується вимкнути скидання шейдерів. Ви хочете вимкнути скидання шейдерів зараз?", + "DialogLoadAppGameAlreadyLoadedMessage": "Гру вже завантажено", + "DialogLoadAppGameAlreadyLoadedSubMessage": "Зупиніть емуляцію або закрийте емулятор перед запуском іншої гри.", + "DialogUpdateAddUpdateErrorMessage": "Зазначений файл не містить оновлення для вибраного заголовка!", + "DialogSettingsBackendThreadingWarningTitle": "Попередження - потокове керування сервером", + "DialogSettingsBackendThreadingWarningMessage": "Ryujinx потрібно перезапустити після зміни цього параметра, щоб він застосовувався повністю. Залежно від вашої платформи вам може знадобитися вручну вимкнути власну багатопотоковість драйвера під час використання Ryujinx.", + "DialogModManagerDeletionWarningMessage": "Ви збираєтесь видалити модифікацію: {0}\n\nВи дійсно бажаєте продовжити?", + "DialogModManagerDeletionAllWarningMessage": "Ви збираєтесь видалити всі модифікації для цього Додатка.\n\nВи дійсно бажаєте продовжити?", + "SettingsTabGraphicsFeaturesOptions": "Особливості", + "SettingsTabGraphicsBackendMultithreading": "Багатопотоковість графічного сервера:", + "CommonAuto": "Авто", + "CommonOff": "Вимкнути", + "CommonOn": "Увімкнути", + "InputDialogYes": "Так", + "InputDialogNo": "Ні", + "DialogProfileInvalidProfileNameErrorMessage": "Ім'я файлу містить неприпустимі символи. Будь ласка, спробуйте ще раз.", + "MenuBarOptionsPauseEmulation": "Пауза", + "MenuBarOptionsResumeEmulation": "Продовжити", + "AboutUrlTooltipMessage": "Натисніть, щоб відкрити сайт Ryujinx у браузері за замовчування.", + "AboutDisclaimerMessage": "Ryujinx жодним чином не пов’язано з Nintendo™,\nчи будь-яким із їхніх партнерів.", + "AboutAmiiboDisclaimerMessage": "AmiiboAPI (www.amiiboapi.com) використовується в нашій емуляції Amiibo.", + "AboutPatreonUrlTooltipMessage": "Натисніть, щоб відкрити сторінку Patreon Ryujinx у вашому браузері за замовчування.", + "AboutGithubUrlTooltipMessage": "Натисніть, щоб відкрити сторінку GitHub Ryujinx у браузері за замовчуванням.", + "AboutDiscordUrlTooltipMessage": "Натисніть, щоб відкрити запрошення на сервер Discord Ryujinx у браузері за замовчуванням.", + "AboutTwitterUrlTooltipMessage": "Натисніть, щоб відкрити сторінку Twitter Ryujinx у браузері за замовчуванням.", + "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": "Вимкнути всі", + "ModManagerDeleteAllButton": "Видалити все", + "MenuBarOptionsChangeLanguage": "Змінити мову", + "MenuBarShowFileTypes": "Показати типи файлів", + "CommonSort": "Сортувати", + "CommonShowNames": "Показати назви", + "CommonFavorite": "Вибрані", + "OrderAscending": "За зростанням", + "OrderDescending": "За спаданням", + "SettingsTabGraphicsFeatures": "Функції та вдосконалення", + "ErrorWindowTitle": "Вікно помилок", + "ToggleDiscordTooltip": "Виберіть, чи відображати Ryujinx у вашій «поточній грі» в Discord", + "AddGameDirBoxTooltip": "Введіть каталог ігор, щоб додати до списку", + "AddGameDirTooltip": "Додати каталог гри до списку", + "RemoveGameDirTooltip": "Видалити вибраний каталог гри", + "AddAutoloadDirBoxTooltip": "Enter an autoload directory to add to the list", + "AddAutoloadDirTooltip": "Add an autoload directory to the list", + "RemoveAutoloadDirTooltip": "Remove selected autoload directory", + "CustomThemeCheckTooltip": "Використовуйте користувацьку тему Avalonia для графічного інтерфейсу, щоб змінити вигляд меню емулятора", + "CustomThemePathTooltip": "Шлях до користувацької теми графічного інтерфейсу", + "CustomThemeBrowseTooltip": "Огляд користувацької теми графічного інтерфейсу", + "DockModeToggleTooltip": "У режимі док-станції емульована система веде себе як приєднаний Nintendo Switch. Це покращує точність графіки в більшості ігор. І навпаки, вимкнення цього призведе до того, що емульована система поводитиметься як портативний комутатор Nintendo, погіршуючи якість графіки.\n\nНалаштуйте елементи керування для гравця 1, якщо плануєте використовувати режим док-станції; налаштуйте ручні елементи керування, якщо плануєте використовувати портативний режим.\n\nЗалиште увімкненим, якщо не впевнені.", + "DirectKeyboardTooltip": "Підтримка прямого доступу до клавіатури (HID). Надає іграм доступ до клавіатури для вводу тексту.\n\nПрацює тільки з іграми, які підтримують клавіатуру на обладнанні Switch.\n\nЗалиште вимкненим, якщо не впевнені.", + "DirectMouseTooltip": "Підтримка прямого доступу до миші (HID). Надає іграм доступ до миші, як пристрій вказування.\n\nПрацює тільки з іграми, які підтримують мишу на обладнанні Switch, їх небагато.\n\nФункціонал сенсорного екрана може не працювати, якщо функція ввімкнена.\n\nЗалиште вимкненим, якщо не впевнені.", + "RegionTooltip": "Змінити регіон системи", + "LanguageTooltip": "Змінити мову системи", + "TimezoneTooltip": "Змінити часовий пояс системи", + "TimeTooltip": "Змінити час системи", + "VSyncToggleTooltip": "Емульована вертикальна синхронізація консолі. По суті, обмежувач кадрів для більшості ігор; його вимкнення може призвести до того, що ігри працюватимуть на вищій швидкості, екрани завантаження триватимуть довше чи зупинятимуться.\n\nМожна перемикати в грі гарячою клавішею (За умовчанням F1). Якщо ви плануєте вимкнути функцію, рекомендуємо зробити це через гарячу клавішу.\n\nЗалиште увімкненим, якщо не впевнені.", + "PptcToggleTooltip": "Зберігає перекладені функції JIT, щоб їх не потрібно було перекладати кожного разу, коли гра завантажується.\n\nЗменшує заїкання та значно прискорює час завантаження після першого завантаження гри.\n\nЗалиште увімкненим, якщо не впевнені.", + "LowPowerPptcToggleTooltip": "Load the PPTC using a third of the amount of cores.", + "FsIntegrityToggleTooltip": "Перевіряє наявність пошкоджених файлів під час завантаження гри, і якщо виявлено пошкоджені файли, показує помилку хешу в журналі.\n\nНе впливає на продуктивність і призначений для усунення несправностей.\n\nЗалиште увімкненим, якщо не впевнені.", + "AudioBackendTooltip": "Змінює серверну частину, яка використовується для відтворення аудіо.\n\nSDL2 є кращим, тоді як OpenAL і SoundIO використовуються як резервні варіанти. Dummy не матиме звуку.\n\nВстановіть SDL2, якщо не впевнені.", + "MemoryManagerTooltip": "Змінює спосіб відображення та доступу до гостьової пам’яті. Значно впливає на продуктивність емульованого ЦП.\n\nВстановіть «Неперевірений хост», якщо не впевнені.", + "MemoryManagerSoftwareTooltip": "Використовує програмну таблицю сторінок для перекладу адрес. Найвища точність, але найповільніша продуктивність.", + "MemoryManagerHostTooltip": "Пряме відображення пам'яті в адресному просторі хосту. Набагато швидша компіляція та виконання JIT.", + "MemoryManagerUnsafeTooltip": "Пряме відображення пам’яті, але не маскує адресу в гостьовому адресному просторі перед доступом. Швидше, але ціною безпеки. Гостьова програма може отримати доступ до пам’яті з будь-якого місця в Ryujinx, тому запускайте в цьому режимі лише програми, яким ви довіряєте.", + "UseHypervisorTooltip": "Використання гіпервізор замість JIT. Значно покращує продуктивність, коли доступний, але може бути нестабільним у поточному стані.", + "DRamTooltip": "Використовує альтернативний макет MemoryMode для імітації моделі розробки Switch.\n\nЦе корисно лише для пакетів текстур з вищою роздільною здатністю або модифікацій із роздільною здатністю 4K. НЕ покращує продуктивність.\n\nЗалиште вимкненим, якщо не впевнені.", + "IgnoreMissingServicesTooltip": "Ігнорує нереалізовані служби Horizon OS. Це може допомогти в обході збоїв під час завантаження певних ігор.\n\nЗалиште вимкненим, якщо не впевнені.", + "IgnoreAppletTooltip": "Зовнішнє діалогове вікно \"Аплет контролера\" не з’являтиметься, якщо геймпад буде від’єднано під час гри. Не буде запиту закрити діалогове вікно чи налаштувати новий контролер. Після повторного підключення раніше від’єднаного контролера гра автоматично відновиться.", + "GraphicsBackendThreadingTooltip": "Виконує команди графічного сервера в другому потоці.\n\nПрискорює компіляцію шейдерів, зменшує затримки та покращує продуктивність драйверів GPU без власної підтримки багатопоточності. Трохи краща продуктивність на драйверах з багатопотоковістю.\nВстановіть значення «Авто», якщо не впевнені", + "GalThreadingTooltip": "Виконує команди графічного сервера в другому потоці.\n\nПрискорює компіляцію шейдерів, зменшує затримки та покращує продуктивність драйверів GPU без власної підтримки багатопоточності. Трохи краща продуктивність на драйверах з багатопотоковістю.\n\nВстановіть значення «Авто», якщо не впевнені.", + "ShaderCacheToggleTooltip": "Зберігає кеш дискового шейдера, що зменшує затримки під час наступних запусків.\n\nЗалиште увімкненим, якщо не впевнені.", + "ResolutionScaleTooltip": "Множить роздільну здатність гри.\n\nДеякі ігри можуть не працювати з цією функцією, і виглядатимуть піксельними; для цих ігор треба знайти модифікації, що зупиняють згладжування або підвищують роздільну здатність. Для останніх модифікацій, вибирайте \"Native\".\n\nЦей параметр можна міняти коли гра запущена кліком на \"Застосувати\"; ви можете перемістити вікно налаштувань і поекспериментувати з видом гри.\n\nМайте на увазі, що 4x це занадто для будь-якого комп'ютера.", + "ResolutionScaleEntryTooltip": "Масштаб роздільної здатності з плаваючою комою, наприклад 1,5. Не інтегральні масштаби, швидше за все, спричинять проблеми або збій.", + "AnisotropyTooltip": "Рівень анізотропної фільтрації. Встановіть на «Авто», щоб використовувати значення, яке вимагає гра.", + "AspectRatioTooltip": "Співвідношення сторін застосовано до вікна рендера.\n\nМіняйте тільки, якщо використовуєте модифікацію співвідношення сторін для гри, інакше графіка буде розтягнута.\n\nЗалиште на \"16:9\", якщо не впевнені.", + "ShaderDumpPathTooltip": "Шлях скидання графічних шейдерів", + "FileLogTooltip": "Зберігає журнал консолі у файл журналу на диску. Не впливає на продуктивність.", + "StubLogTooltip": "Друкує повідомлення журналу-заглушки на консолі. Не впливає на продуктивність.", + "InfoLogTooltip": "Друкує повідомлення інформаційного журналу на консолі. Не впливає на продуктивність.", + "WarnLogTooltip": "Друкує повідомлення журналу попереджень у консолі. Не впливає на продуктивність.", + "ErrorLogTooltip": "Друкує повідомлення журналу помилок у консолі. Не впливає на продуктивність.", + "TraceLogTooltip": "Друкує повідомлення журналу трасування на консолі. Не впливає на продуктивність.", + "GuestLogTooltip": "Друкує повідомлення журналу гостей у консолі. Не впливає на продуктивність.", + "FileAccessLogTooltip": "Друкує повідомлення журналу доступу до файлів у консолі.", + "FSAccessLogModeTooltip": "Вмикає виведення журналу доступу до FS на консоль. Можливі режими 0-3", + "DeveloperOptionTooltip": "Використовуйте з обережністю", + "OpenGlLogLevel": "Потрібно увімкнути відповідні рівні журналу", + "DebugLogTooltip": "Друкує повідомлення журналу налагодження на консолі.\n\nВикористовуйте це лише за спеціальною вказівкою співробітника, оскільки це ускладнить читання журналів і погіршить роботу емулятора.", + "LoadApplicationFileTooltip": "Відкриває файловий провідник, щоб вибрати для завантаження сумісний файл Switch", + "LoadApplicationFolderTooltip": "Відкриває файловий провідник, щоб вибрати сумісну з комутатором розпаковану програму для завантаження", + "LoadDlcFromFolderTooltip": "Open a file explorer to choose one or more folders to bulk load DLC from", + "LoadTitleUpdatesFromFolderTooltip": "Open a file explorer to choose one or more folders to bulk load title updates from", + "OpenRyujinxFolderTooltip": "Відкриває папку файлової системи Ryujinx", + "OpenRyujinxLogsTooltip": "Відкриває папку, куди записуються журнали", + "ExitTooltip": "Виходить з Ryujinx", + "OpenSettingsTooltip": "Відкриває вікно налаштувань", + "OpenProfileManagerTooltip": "Відкриває вікно диспетчера профілів користувачів", + "StopEmulationTooltip": "Зупиняє емуляцію поточної гри та повертається до вибору гри", + "CheckUpdatesTooltip": "Перевіряє наявність оновлень для Ryujinx", + "OpenAboutTooltip": "Відкриває вікно «Про програму».", + "GridSize": "Розмір сітки", + "GridSizeTooltip": "Змінити розмір елементів сітки", + "SettingsTabSystemSystemLanguageBrazilianPortuguese": "Португальська (Бразилія)", + "AboutRyujinxContributorsButtonHeader": "Переглянути всіх співавторів", + "SettingsTabSystemAudioVolume": "Гучність: ", + "AudioVolumeTooltip": "Змінити гучність звуку", + "SettingsTabSystemEnableInternetAccess": "Гостьовий доступ до Інтернету/режим LAN", + "EnableInternetAccessTooltip": "Дозволяє емульованій програмі підключатися до Інтернету.\n\nІгри з режимом локальної мережі можуть підключатися одна до одної, якщо це увімкнено, і системи підключені до однієї точки доступу. Сюди входять і справжні консолі.\n\nНЕ дозволяє підключатися до серверів Nintendo. Може призвести до збою в деяких іграх, які намагаються підключитися до Інтернету.\n\nЗалиште вимкненим, якщо не впевнені.", + "GameListContextMenuManageCheatToolTip": "Керування читами", + "GameListContextMenuManageCheat": "Керування читами", + "GameListContextMenuManageModToolTip": "Керування модами", + "GameListContextMenuManageMod": "Керування модами", + "ControllerSettingsStickRange": "Діапазон:", + "DialogStopEmulationTitle": "Ryujinx - Зупинити емуляцію", + "DialogStopEmulationMessage": "Ви впевнені, що хочете зупинити емуляцію?", + "SettingsTabCpu": "ЦП", + "SettingsTabAudio": "Аудіо", + "SettingsTabNetwork": "Мережа", + "SettingsTabNetworkConnection": "Підключення до мережі", + "SettingsTabCpuCache": "Кеш ЦП", + "SettingsTabCpuMemory": "Пам'ять ЦП", + "DialogUpdaterFlatpakNotSupportedMessage": "Будь ласка, оновіть Ryujinx через FlatHub.", + "UpdaterDisabledWarningTitle": "Програму оновлення вимкнено!", + "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}", + "TitleBundledUpdateVersionLabel": "Bundled: Version {0}", + "TitleBundledDlcLabel": "Bundled:", + "TitleXCIStatusPartialLabel": "Partial", + "TitleXCIStatusTrimmableLabel": "Untrimmed", + "TitleXCIStatusUntrimmableLabel": "Trimmed", + "TitleXCIStatusFailedLabel": "(Failed)", + "TitleXCICanSaveLabel": "Save {0:n0} Mb", + "TitleXCISavingLabel": "Saved {0:n0} Mb", + "RyujinxInfo": "Ryujin x - Інформація", + "RyujinxConfirm": "Ryujinx - Підтвердження", + "FileDialogAllTypes": "Всі типи", + "Never": "Ніколи", + "SwkbdMinCharacters": "Мінімальна кількість символів: {0}", + "SwkbdMinRangeCharacters": "Має бути {0}-{1} символів", + "SoftwareKeyboard": "Програмна клавіатура", + "SoftwareKeyboardModeNumeric": "Повинно бути лише 0-9 або “.”", + "SoftwareKeyboardModeAlphabet": "Повинно бути лише не CJK-символи", + "SoftwareKeyboardModeASCII": "Повинно бути лише ASCII текст", + "ControllerAppletControllers": "Підтримувані контролери:", + "ControllerAppletPlayers": "Гравці:", + "ControllerAppletDescription": "Поточна конфігурація невірна. Відкрийте налаштування та переналаштуйте Ваші дані.", + "ControllerAppletDocked": "Встановлений режим в док-станції. Вимкніть портативні контролери.", + "UpdaterRenaming": "Перейменування старих файлів...", + "UpdaterRenameFailed": "Програмі оновлення не вдалося перейменувати файл: {0}", + "UpdaterAddingFiles": "Додавання нових файлів...", + "UpdaterExtracting": "Видобування оновлення...", + "UpdaterDownloading": "Завантаження оновлення...", + "Game": "Гра", + "Docked": "Док-станція", + "Handheld": "Портативний", + "ConnectionError": "Помилка з'єднання.", + "AboutPageDeveloperListMore": "{0} та інші...", + "ApiError": "Помилка API.", + "LoadingHeading": "Завантаження {0}", + "CompilingPPTC": "Компіляція PTC", + "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": "Виберіть файли оновлення", + "SelectModDialogTitle": "Виберіть теку з модами", + "TrimXCIFileDialogTitle": "Check and Trim XCI File", + "TrimXCIFileDialogPrimaryText": "This function will first check the empty space and then trim the XCI File to save disk space.", + "TrimXCIFileDialogSecondaryText": "Current File Size: {0:n} MB\nGame Data Size: {1:n} MB\nDisk Space Savings: {2:n} MB", + "TrimXCIFileNoTrimNecessary": "XCI File does not need to be trimmed. Check logs for further details", + "TrimXCIFileNoUntrimPossible": "XCI File cannot be untrimmed. Check logs for further details", + "TrimXCIFileReadOnlyFileCannotFix": "XCI File is Read Only and could not be made writable. Check logs for further details", + "TrimXCIFileFileSizeChanged": "XCI File has changed in size since it was scanned. Please check the file is not being written to and try again.", + "TrimXCIFileFreeSpaceCheckFailed": "XCI File has data in the free space area, it is not safe to trim", + "TrimXCIFileInvalidXCIFile": "XCI File contains invalid data. Check logs for further details", + "TrimXCIFileFileIOWriteError": "XCI File could not be opened for writing. Check logs for further details", + "TrimXCIFileFailedPrimaryText": "Trimming of the XCI file failed", + "TrimXCIFileCancelled": "The operation was cancelled", + "TrimXCIFileFileUndertermined": "No operation was performed", + "UserProfileWindowTitle": "Менеджер профілів користувачів", + "CheatWindowTitle": "Менеджер читів", + "DlcWindowTitle": "Менеджер вмісту для завантаження", + "ModWindowTitle": "Керувати модами для {0} ({1})", + "UpdateWindowTitle": "Менеджер оновлення назв", + "XCITrimmerWindowTitle": "XCI File Trimmer", + "XCITrimmerTitleStatusCount": "{0} of {1} Title(s) Selected", + "XCITrimmerTitleStatusCountWithFilter": "{0} of {1} Title(s) Selected ({2} displayed)", + "XCITrimmerTitleStatusTrimming": "Trimming {0} Title(s)...", + "XCITrimmerTitleStatusUntrimming": "Untrimming {0} Title(s)...", + "XCITrimmerTitleStatusFailed": "Failed", + "XCITrimmerPotentialSavings": "Potential Savings", + "XCITrimmerActualSavings": "Actual Savings", + "XCITrimmerSavingsMb": "{0:n0} Mb", + "XCITrimmerSelectDisplayed": "Select Shown", + "XCITrimmerDeselectDisplayed": "Deselect Shown", + "XCITrimmerSortName": "Title", + "XCITrimmerSortSaved": "Space Savings", + "XCITrimmerTrim": "Trim", + "XCITrimmerUntrim": "Untrim", + "UpdateWindowUpdateAddedMessage": "{0} new update(s) added", + "UpdateWindowBundledContentNotice": "Bundled updates cannot be removed, only disabled.", + "CheatWindowHeading": "Коди доступні для {0} [{1}]", + "BuildId": "ID збірки:", + "DlcWindowBundledContentNotice": "Bundled DLC cannot be removed, only disabled.", + "DlcWindowHeading": "Вміст для завантаження, доступний для {1} ({2}): {0}", + "DlcWindowDlcAddedMessage": "{0} new downloadable content(s) added", + "AutoloadDlcAddedMessage": "{0} new downloadable content(s) added", + "AutoloadDlcRemovedMessage": "{0} missing downloadable content(s) removed", + "AutoloadUpdateAddedMessage": "{0} new update(s) added", + "AutoloadUpdateRemovedMessage": "{0} missing update(s) removed", + "ModWindowHeading": "{0} мод(ів)", + "UserProfilesEditProfile": "Редагувати вибране", + "Continue": "Continue", + "Cancel": "Скасувати", + "Save": "Зберегти", + "Discard": "Скасувати", + "Paused": "Призупинено", + "UserProfilesSetProfileImage": "Встановити зображення профілю", + "UserProfileEmptyNameError": "Імʼя обовʼязкове", + "UserProfileNoImageError": "Зображення профілю обовʼязкове", + "GameUpdateWindowHeading": "{0} Доступні оновлення для {1} ({2})", + "SettingsTabHotkeysResScaleUpHotkey": "Збільшити роздільність:", + "SettingsTabHotkeysResScaleDownHotkey": "Зменшити роздільність:", + "UserProfilesName": "Імʼя", + "UserProfilesUserId": "ID користувача:", + "SettingsTabGraphicsBackend": "Графічний сервер", + "SettingsTabGraphicsBackendTooltip": "Виберіть backend графіки, що буде використовуватись в емуляторі.\n\n\"Vulkan\" краще для всіх сучасних відеокарт, якщо драйвери вчасно оновлюються. У Vulkan також швидше компілюються шейдери (менше \"заїкання\" зображення) на відеокартах всіх компаній.\n\n\"OpenGL\" може дати кращі результати на старих відеокартах Nvidia, старих відеокартах AMD на Linux, або на відеокартах з маленькою кількістю VRAM, але \"заїкання\" через компіляцію шейдерів будуть частіші.\n\nЯкщо не впевнені, встановіть на \"Vulkan\". Встановіть на \"OpenGL\", якщо Ваша відеокарта не підтримує Vulkan навіть на останніх драйверах.", + "SettingsEnableTextureRecompression": "Увімкнути рекомпресію текстури", + "SettingsEnableTextureRecompressionTooltip": "Стискає текстури ASTC, щоб зменшити використання VRAM.\n\nЦим форматом текстур користуються такі ігри, як Astral Chain, Bayonetta 3, Fire Emblem Engage, Metroid Prime Remastered, Super Mario Bros. Wonder і The Legend of Zelda: Tears of the Kingdom.\n\nЦі ігри, скоріше всього крашнуться на відеокартах з розміром VRAM в 4 Гб і менше.\n\nВмикайте тільки якщо у Вас закінчується VRAM на цих іграх. Залиште на \"Вимкнути\", якщо не впевнені.", + "SettingsTabGraphicsPreferredGpu": "Бажаний GPU", + "SettingsTabGraphicsPreferredGpuTooltip": "Виберіть відеокарту, яка використовуватиметься з графічним сервером Vulkan.\n\nНе впливає на графічний процесор, який використовуватиме OpenGL.\n\nВстановіть графічний процесор, позначений як «dGPU», якщо не впевнені. Якщо такого немає, не чіпайте.", + "SettingsAppRequiredRestartMessage": "Необхідно перезапустити Ryujinx", + "SettingsGpuBackendRestartMessage": "Налаштування графічного сервера або GPU було змінено. Для цього знадобиться перезапуск", + "SettingsGpuBackendRestartSubMessage": "Бажаєте перезапустити зараз?", + "RyujinxUpdaterMessage": "Бажаєте оновити Ryujinx до останньої версії?", + "SettingsTabHotkeysVolumeUpHotkey": "Збільшити гучність:", + "SettingsTabHotkeysVolumeDownHotkey": "Зменшити гучність:", + "SettingsEnableMacroHLE": "Увімкнути макрос 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": "Застосовує згладження до рендера гри.\n\nFXAA розмиє більшість зображення, а SMAA спробує знайти нерівні краї та згладити їх.\n\nНе рекомендується використовувати разом з фільтром масштабування FSR.\n\nЦю опцію можна міняти коли гра запущена кліком на \"Застосувати; ви можете відсунути вікно налаштувань і поекспериментувати з видом гри.\n\nЗалиште на \"Немає\", якщо не впевнені.", + "GraphicsAALabel": "Згладжування:", + "GraphicsScalingFilterLabel": "Фільтр масштабування:", + "GraphicsScalingFilterTooltip": "Виберіть фільтр масштабування, що використається при збільшенні роздільної здатності.\n\n\"Білінійний\" добре виглядає в 3D іграх, і хороше налаштування за умовчуванням.\n\n\"Найближчий\" рекомендується для ігор з піксель-артом.\n\n\"FSR 1.0\" - це просто фільтр різкості, не рекомендується використовувати разом з FXAA або SMAA.\n\nЦю опцію можна міняти коли гра запущена кліком на \"Застосувати; ви можете відсунути вікно налаштувань і поекспериментувати з видом гри.\n\nЗалиште на \"Білінійний\", якщо не впевнені.", + "GraphicsScalingFilterBilinear": "Білінійний", + "GraphicsScalingFilterNearest": "Найближчий", + "GraphicsScalingFilterFsr": "FSR", + "GraphicsScalingFilterArea": "Area", + "GraphicsScalingFilterLevelLabel": "Рівень", + "GraphicsScalingFilterLevelTooltip": "Встановити рівень різкості в FSR 1.0. Чим вище - тим різкіше.", + "SmaaLow": "SMAA Низький", + "SmaaMedium": "SMAA Середній", + "SmaaHigh": "SMAA Високий", + "SmaaUltra": "SMAA Ультра", + "UserEditorTitle": "Редагувати користувача", + "UserEditorTitleCreate": "Створити користувача", + "SettingsTabNetworkInterface": "Мережевий інтерфейс:", + "NetworkInterfaceTooltip": "Мережевий інтерфейс, що використовується для LAN/LDN.\n\nРазом з VPN або XLink Kai, і грою що підтримує LAN, може імітувати з'єднання в однаковій мережі через Інтернет.", + "NetworkInterfaceDefault": "Стандартний", + "PackagingShaders": "Пакування шейдерів", + "AboutChangelogButton": "Переглянути журнал змін на GitHub", + "AboutChangelogButtonTooltipMessage": "Клацніть, щоб відкрити журнал змін для цієї версії у стандартному браузері.", + "SettingsTabNetworkMultiplayer": "Мережева гра", + "MultiplayerMode": "Режим:", + "MultiplayerModeTooltip": "Змінити LDN мультиплеєру.\n\nLdnMitm змінить функціонал бездротової/локальної гри в іграх, щоб вони працювали так, ніби це LAN, що дозволяє локальні підключення в тій самій мережі з іншими екземплярами Ryujinx та хакнутими консолями Nintendo Switch, які мають встановлений модуль ldn_mitm.\n\nМультиплеєр вимагає, щоб усі гравці були на одній і тій же версії гри (наприклад Super Smash Bros. Ultimate v13.0.1 не зможе під'єднатися до v13.0.0).\n\nЗалиште на \"Вимкнено\", якщо не впевнені, ", + "MultiplayerModeDisabled": "Вимкнено", + "MultiplayerModeLdnMitm": "ldn_mitm", + "MultiplayerModeLdnRyu": "RyuLDN", + "MultiplayerDisableP2P": "Disable P2P Network Hosting (may increase latency)", + "MultiplayerDisableP2PTooltip": "Disable P2P network hosting, peers will proxy through the master server instead of connecting to you directly.", + "LdnPassphrase": "Network Passphrase:", + "LdnPassphraseTooltip": "You will only be able to see hosted games with the same passphrase as you.", + "LdnPassphraseInputTooltip": "Enter a passphrase in the format Ryujinx-<8 hex chars>. You will only be able to see hosted games with the same passphrase as you.", + "LdnPassphraseInputPublic": "(public)", + "GenLdnPass": "Generate Random", + "GenLdnPassTooltip": "Generates a new passphrase, which can be shared with other players.", + "ClearLdnPass": "Clear", + "ClearLdnPassTooltip": "Clears the current passphrase, returning to the public network.", + "InvalidLdnPassphrase": "Invalid Passphrase! Must be in the format \"Ryujinx-<8 hex chars>\"" +} diff --git a/src/Ryujinx/Assets/Locales/zh_CN.json b/src/Ryujinx/Assets/Locales/zh_CN.json new file mode 100644 index 000000000..8fcd41cd2 --- /dev/null +++ b/src/Ryujinx/Assets/Locales/zh_CN.json @@ -0,0 +1,868 @@ +{ + "Language": "简体中文", + "MenuBarFileOpenApplet": "打开小程序", + "MenuBarFileOpenAppletOpenMiiApplet": "Mii Edit Applet", + "MenuBarFileOpenAppletOpenMiiAppletToolTip": "打开独立的 Mii 小程序", + "SettingsTabInputDirectMouseAccess": "直通鼠标操作", + "SettingsTabSystemMemoryManagerMode": "内存管理模式:", + "SettingsTabSystemMemoryManagerModeSoftware": "软件管理", + "SettingsTabSystemMemoryManagerModeHost": "本机映射 (较快)", + "SettingsTabSystemMemoryManagerModeHostUnchecked": "跳过检查的本机映射 (最快,不安全)", + "SettingsTabSystemUseHypervisor": "使用 Hypervisor 虚拟化", + "MenuBarFile": "文件(_F)", + "MenuBarFileOpenFromFile": "加载游戏文件(_L)", + "MenuBarFileOpenFromFileError": "未发现应用", + "MenuBarFileOpenUnpacked": "加载解包后的游戏(_U)", + "MenuBarFileLoadDlcFromFolder": "从文件夹加载DLC", + "MenuBarFileLoadTitleUpdatesFromFolder": "从文件夹加载游戏更新", + "MenuBarFileOpenEmuFolder": "打开 Ryujinx 系统目录", + "MenuBarFileOpenLogsFolder": "打开日志目录", + "MenuBarFileExit": "退出(_E)", + "MenuBarOptions": "选项(_O)", + "MenuBarOptionsToggleFullscreen": "切换全屏", + "MenuBarOptionsStartGamesInFullscreen": "全屏模式启动游戏", + "MenuBarOptionsStopEmulation": "停止模拟", + "MenuBarOptionsSettings": "设置(_S)", + "MenuBarOptionsManageUserProfiles": "管理用户账户(_M)", + "MenuBarActions": "操作(_A)", + "MenuBarOptionsSimulateWakeUpMessage": "模拟唤醒消息", + "MenuBarActionsScanAmiibo": "扫描 Amiibo", + "MenuBarTools": "工具(_T)", + "MenuBarToolsInstallFirmware": "安装系统固件", + "MenuBarFileToolsInstallFirmwareFromFile": "从 XCI 或 ZIP 文件中安装系统固件", + "MenuBarFileToolsInstallFirmwareFromDirectory": "从文件夹中安装系统固件", + "MenuBarToolsManageFileTypes": "管理文件扩展名", + "MenuBarToolsInstallFileTypes": "关联文件扩展名", + "MenuBarToolsUninstallFileTypes": "取消关联扩展名", + "MenuBarToolsXCITrimmer": "Trim XCI Files", + "MenuBarView": "视图(_V)", + "MenuBarViewWindow": "窗口大小", + "MenuBarViewWindow720": "720p", + "MenuBarViewWindow1080": "1080p", + "MenuBarHelp": "帮助(_H)", + "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 管理窗口", + "GameListContextMenuCacheManagement": "缓存管理", + "GameListContextMenuCacheManagementPurgePptc": "清除 PPTC 缓存文件", + "GameListContextMenuCacheManagementPurgePptcToolTip": "删除游戏的 PPTC 缓存文件,下次启动游戏时重新编译生成 PPTC 缓存文件", + "GameListContextMenuCacheManagementPurgeShaderCache": "清除着色器缓存文件", + "GameListContextMenuCacheManagementPurgeShaderCacheToolTip": "删除游戏的着色器缓存文件,下次启动游戏时重新生成着色器缓存文件", + "GameListContextMenuCacheManagementOpenPptcDirectory": "打开 PPTC 缓存目录", + "GameListContextMenuCacheManagementOpenPptcDirectoryToolTip": "打开储存游戏 PPTC 缓存文件的目录", + "GameListContextMenuCacheManagementOpenShaderCacheDirectory": "打开着色器缓存目录", + "GameListContextMenuCacheManagementOpenShaderCacheDirectoryToolTip": "打开储存游戏着色器缓存文件的目录", + "GameListContextMenuExtractData": "提取数据", + "GameListContextMenuExtractDataExeFS": "ExeFS", + "GameListContextMenuExtractDataExeFSToolTip": "从游戏的当前状态中提取 ExeFS 分区 (包括更新)", + "GameListContextMenuExtractDataRomFS": "RomFS", + "GameListContextMenuExtractDataRomFSToolTip": "从游戏的当前状态中提取 RomFS 分区 (包括更新)", + "GameListContextMenuExtractDataLogo": "图标", + "GameListContextMenuExtractDataLogoToolTip": "从游戏的当前状态中提取图标 (包括更新)", + "GameListContextMenuCreateShortcut": "创建游戏快捷方式", + "GameListContextMenuCreateShortcutToolTip": "创建一个直接启动此游戏的桌面快捷方式", + "GameListContextMenuCreateShortcutToolTipMacOS": "在 macOS 的应用程序目录中创建一个直接启动此游戏的快捷方式", + "GameListContextMenuOpenModsDirectory": "打开 MOD 目录", + "GameListContextMenuOpenModsDirectoryToolTip": "打开存放游戏 MOD 的目录", + "GameListContextMenuOpenSdModsDirectory": "打开大气层系统 MOD 目录", + "GameListContextMenuOpenSdModsDirectoryToolTip": "打开存放适用于大气层系统的游戏 MOD 的目录,对于为真实硬件打包的 MOD 非常有用", + "GameListContextMenuTrimXCI": "Check and Trim XCI File", + "GameListContextMenuTrimXCIToolTip": "Check and Trim XCI File to Save Disk Space", + "StatusBarGamesLoaded": "{0}/{1} 游戏加载完成", + "StatusBarSystemVersion": "系统固件版本:{0}", + "StatusBarXCIFileTrimming": "Trimming XCI File '{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": "退出游戏时需要确认", + "SettingsTabGeneralRememberWindowState": "记住窗口大小和位置", + "SettingsTabGeneralShowTitleBar": "显示标题栏 (需要重启)", + "SettingsTabGeneralHideCursor": "隐藏鼠标指针:", + "SettingsTabGeneralHideCursorNever": "从不隐藏", + "SettingsTabGeneralHideCursorOnIdle": "自动隐藏", + "SettingsTabGeneralHideCursorAlways": "始终隐藏", + "SettingsTabGeneralGameDirectories": "游戏目录", + "SettingsTabGeneralAutoloadDirectories": "自动加载DLC/游戏更新目录", + "SettingsTabGeneralAutoloadNote": "DLC/游戏更新可自动加载和卸载", + "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 缓存", + "SettingsTabSystemEnableLowPowerPptc": "低功耗 PPTC 加载", + "SettingsTabSystemEnableFsIntegrityChecks": "启用文件系统完整性检查", + "SettingsTabSystemAudioBackend": "音频处理引擎:", + "SettingsTabSystemAudioBackendDummy": "无", + "SettingsTabSystemAudioBackendOpenAL": "OpenAL", + "SettingsTabSystemAudioBackendSoundIO": "SoundIO", + "SettingsTabSystemAudioBackendSDL2": "SDL2", + "SettingsTabSystemHacks": "修改", + "SettingsTabSystemHacksNote": "会导致模拟器不稳定", + "SettingsTabSystemDramSize": "使用开发机的内存布局(开发人员使用)", + "SettingsTabSystemDramSize4GiB": "4GiB", + "SettingsTabSystemDramSize6GiB": "6GiB", + "SettingsTabSystemDramSize8GiB": "8GiB", + "SettingsTabSystemDramSize12GiB": "12GiB", + "SettingsTabSystemIgnoreMissingServices": "忽略缺失的服务", + "SettingsTabSystemIgnoreApplet": "忽略小程序", + "SettingsTabGraphics": "图形", + "SettingsTabGraphicsAPI": "图形 API", + "SettingsTabGraphicsEnableShaderCache": "启用着色器缓存", + "SettingsTabGraphicsAnisotropicFiltering": "各向异性过滤:", + "SettingsTabGraphicsAnisotropicFilteringAuto": "自动", + "SettingsTabGraphicsAnisotropicFiltering2x": "2x", + "SettingsTabGraphicsAnisotropicFiltering4x": "4x", + "SettingsTabGraphicsAnisotropicFiltering8x": "8x", + "SettingsTabGraphicsAnisotropicFiltering16x": "16x", + "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": "启用存根日志", + "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": "摇杆左右反转", + "ControllerSettingsStickInvertYAxis": "摇杆上下反转", + "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": "服务器地址:", + "ControllerSettingsMotionGyroSensitivity": "陀螺仪敏感度:", + "ControllerSettingsMotionGyroDeadzone": "陀螺仪死区:", + "ControllerSettingsSave": "保存", + "ControllerSettingsClose": "关闭", + "KeyUnknown": "未知", + "KeyShiftLeft": "左侧Shift", + "KeyShiftRight": "右侧Shift", + "KeyControlLeft": "左侧Ctrl", + "KeyMacControlLeft": "左侧⌃", + "KeyControlRight": "右侧Ctrl", + "KeyMacControlRight": "右侧⌃", + "KeyAltLeft": "左侧Alt", + "KeyMacAltLeft": "左侧⌥", + "KeyAltRight": "右侧Alt", + "KeyMacAltRight": "右侧⌥", + "KeyWinLeft": "左侧⊞", + "KeyMacWinLeft": "左侧⌘", + "KeyWinRight": "右侧⊞", + "KeyMacWinRight": "右侧⌘", + "KeyMenu": "菜单键", + "KeyUp": "上", + "KeyDown": "下", + "KeyLeft": "左", + "KeyRight": "右", + "KeyEnter": "回车键", + "KeyEscape": "Esc", + "KeySpace": "空格键", + "KeyTab": "Tab", + "KeyBackSpace": "退格键", + "KeyInsert": "Insert", + "KeyDelete": "Delete", + "KeyPageUp": "Page Up", + "KeyPageDown": "Page Down", + "KeyHome": "Home", + "KeyEnd": "End", + "KeyCapsLock": "Caps Lock", + "KeyScrollLock": "Scroll Lock", + "KeyPrintScreen": "Print Screen", + "KeyPause": "Pause", + "KeyNumLock": "Num Lock", + "KeyClear": "清除键", + "KeyKeypad0": "小键盘0", + "KeyKeypad1": "小键盘1", + "KeyKeypad2": "小键盘2", + "KeyKeypad3": "小键盘3", + "KeyKeypad4": "小键盘4", + "KeyKeypad5": "小键盘5", + "KeyKeypad6": "小键盘6", + "KeyKeypad7": "小键盘7", + "KeyKeypad8": "小键盘8", + "KeyKeypad9": "小键盘9", + "KeyKeypadDivide": "小键盘/", + "KeyKeypadMultiply": "小键盘*", + "KeyKeypadSubtract": "小键盘-", + "KeyKeypadAdd": "小键盘+", + "KeyKeypadDecimal": "小键盘.", + "KeyKeypadEnter": "小键盘回车键", + "KeyNumber0": "0", + "KeyNumber1": "1", + "KeyNumber2": "2", + "KeyNumber3": "3", + "KeyNumber4": "4", + "KeyNumber5": "5", + "KeyNumber6": "6", + "KeyNumber7": "7", + "KeyNumber8": "8", + "KeyNumber9": "9", + "KeyTilde": "~", + "KeyGrave": "`", + "KeyMinus": "-", + "KeyPlus": "+", + "KeyBracketLeft": "[", + "KeyBracketRight": "]", + "KeySemicolon": ";", + "KeyQuote": "\"", + "KeyComma": ",", + "KeyPeriod": ".", + "KeySlash": "/", + "KeyBackSlash": "\\", + "KeyUnbound": "未分配", + "GamepadLeftStick": "左摇杆按键", + "GamepadRightStick": "右摇杆按键", + "GamepadLeftShoulder": "左肩键L", + "GamepadRightShoulder": "右肩键R", + "GamepadLeftTrigger": "左扳机键ZL", + "GamepadRightTrigger": "右扳机键ZR", + "GamepadDpadUp": "上键", + "GamepadDpadDown": "下键", + "GamepadDpadLeft": "左键", + "GamepadDpadRight": "右键", + "GamepadMinus": "-键", + "GamepadPlus": "+键", + "GamepadGuide": "主页键", + "GamepadMisc1": "截图键", + "GamepadPaddle1": "其他按键1", + "GamepadPaddle2": "其他按键2", + "GamepadPaddle3": "其他按键3", + "GamepadPaddle4": "其他按键4", + "GamepadTouchpad": "触摸板", + "GamepadSingleLeftTrigger0": "左扳机0", + "GamepadSingleRightTrigger0": "右扳机0", + "GamepadSingleLeftTrigger1": "左扳机1", + "GamepadSingleRightTrigger1": "右扳机1", + "StickLeft": "左摇杆", + "StickRight": "右摇杆", + "UserProfilesSelectedUserProfile": "选定的用户账户:", + "UserProfilesSaveProfileName": "保存名称", + "UserProfilesChangeProfileImage": "更换头像", + "UserProfilesAvailableUserProfiles": "现有用户账户:", + "UserProfilesAddNewProfile": "新建账户", + "UserProfilesDelete": "删除", + "UserProfilesClose": "关闭", + "ProfileNameSelectionWatermark": "输入昵称", + "ProfileImageSelectionTitle": "选择头像", + "ProfileImageSelectionHeader": "选择合适的头像图片", + "ProfileImageSelectionNote": "您可以导入自定义头像,或从模拟器系统固件中选择预设头像", + "ProfileImageSelectionImportImage": "导入图像文件", + "ProfileImageSelectionSelectAvatar": "选择预设头像", + "InputDialogTitle": "输入对话框", + "InputDialogOk": "完成", + "InputDialogCancel": "取消", + "InputDialogCancelling": "Cancelling", + "InputDialogClose": "Close", + "InputDialogAddNewProfileTitle": "选择用户名称", + "InputDialogAddNewProfileHeader": "请输入账户名称", + "InputDialogAddNewProfileSubtext": "(最大长度:{0})", + "AvatarChoose": "保存选定头像", + "AvatarSetBackgroundColor": "设置背景色", + "AvatarClose": "关闭", + "ControllerSettingsLoadProfileToolTip": "加载配置文件", + "ControllerSettingsViewProfileToolTip": "预览配置文件", + "ControllerSettingsAddProfileToolTip": "新增配置文件", + "ControllerSettingsRemoveProfileToolTip": "删除配置文件", + "ControllerSettingsSaveProfileToolTip": "保存配置文件", + "MenuBarFileToolsTakeScreenshot": "保存截屏", + "MenuBarFileToolsHideUi": "隐藏菜单栏和状态栏", + "GameListContextMenuRunApplication": "启动游戏", + "GameListContextMenuToggleFavorite": "收藏", + "GameListContextMenuToggleFavoriteToolTip": "切换游戏的收藏状态", + "SettingsTabGeneralTheme": "主题:", + "SettingsTabGeneralThemeAuto": "自动", + "SettingsTabGeneralThemeDark": "深色(暗黑)", + "SettingsTabGeneralThemeLight": "浅色(亮色)", + "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": "NCA 分区提取", + "DialogNcaExtractionMainNcaNotFoundErrorMessage": "提取失败,所选文件中没有 NCA 文件", + "DialogNcaExtractionCheckLogErrorMessage": "提取失败,请查看日志文件获取详情", + "DialogNcaExtractionSuccessMessage": "提取成功!", + "DialogUpdaterConvertFailedMessage": "无法切换当前 Ryujinx 版本。", + "DialogUpdaterCancelUpdateMessage": "取消更新!", + "DialogUpdaterAlreadyOnLatestVersionMessage": "您使用的 Ryujinx 模拟器是最新版本。", + "DialogUpdaterFailedToGetVersionMessage": "尝试从 Github 获取版本信息时无效,可能由于 GitHub Actions 正在编译新版本。\n请过一会再试。", + "DialogUpdaterConvertFailedGithubMessage": "无法切换至从 Github 接收到的新版 Ryujinx 模拟器。", + "DialogUpdaterDownloadingMessage": "下载更新中...", + "DialogUpdaterExtractionMessage": "正在提取更新...", + "DialogUpdaterRenamingMessage": "正在重命名更新...", + "DialogUpdaterAddingFilesMessage": "安装更新中...", + "DialogUpdaterShowChangelogMessage": "Show Changelog", + "DialogUpdaterCompleteMessage": "更新成功!", + "DialogUpdaterRestartMessage": "是否立即重启 Ryujinx 模拟器?", + "DialogUpdaterNoInternetMessage": "没有连接到网络", + "DialogUpdaterNoInternetSubMessage": "请确保互联网连接正常。", + "DialogUpdaterDirtyBuildMessage": "无法更新非官方版本的 Ryujinx 模拟器!", + "DialogUpdaterDirtyBuildSubMessage": "如果想使用受支持的版本,请您在 https://ryujinx.app/download 下载官方版本。", + "DialogRestartRequiredMessage": "需要重启模拟器", + "DialogThemeRestartMessage": "主题设置已保存,需要重启模拟器才能生效。", + "DialogThemeRestartSubMessage": "是否要重启模拟器?", + "DialogFirmwareInstallEmbeddedMessage": "要安装游戏文件中内嵌的系统固件吗?(固件版本 {0})", + "DialogFirmwareInstallEmbeddedSuccessMessage": "Ryujinx 模拟器已经从当前游戏文件中安装了系统固件 {0} 。\n模拟器现在可以正常运行了。", + "DialogFirmwareNoFirmwareInstalledMessage": "未安装系统固件", + "DialogFirmwareInstalledMessage": "已安装系统固件 {0}", + "DialogInstallFileTypesSuccessMessage": "关联文件类型成功!", + "DialogInstallFileTypesErrorMessage": "关联文件类型失败!", + "DialogUninstallFileTypesSuccessMessage": "成功解除文件类型关联!", + "DialogUninstallFileTypesErrorMessage": "解除文件类型关联失败!", + "DialogOpenSettingsWindowLabel": "打开设置窗口", + "DialogOpenXCITrimmerWindowLabel": "XCI Trimmer Window", + "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": "用户界面错误:所选游戏没有有效的游戏 ID", + "DialogFirmwareInstallerFirmwareNotFoundErrorMessage": "在路径 {0} 中找不到有效的 Switch 系统固件。", + "DialogFirmwareInstallerFirmwareInstallTitle": "安装系统固件 {0}", + "DialogFirmwareInstallerFirmwareInstallMessage": "即将安装系统固件版本 {0} 。", + "DialogFirmwareInstallerFirmwareInstallSubMessage": "\n\n替换当前系统固件版本 {0} 。", + "DialogFirmwareInstallerFirmwareInstallConfirmMessage": "\n\n是否继续?", + "DialogFirmwareInstallerFirmwareInstallWaitMessage": "安装系统固件中...", + "DialogFirmwareInstallerFirmwareInstallSuccessMessage": "成功安装系统固件版本 {0} 。", + "DialogUserProfileDeletionWarningMessage": "删除后将没有可用的账户", + "DialogUserProfileDeletionConfirmMessage": "是否删除所选账户", + "DialogUserProfileUnsavedChangesTitle": "警告 - 有未保存的更改", + "DialogUserProfileUnsavedChangesMessage": "您对该账户的更改尚未保存。", + "DialogUserProfileUnsavedChangesSubMessage": "确定要放弃更改吗?", + "DialogControllerSettingsModifiedConfirmMessage": "当前的输入设置已更新", + "DialogControllerSettingsModifiedConfirmSubMessage": "是否保存?", + "DialogLoadFileErrorMessage": "{0}. 错误的文件:{1}", + "DialogModAlreadyExistsMessage": "MOD 已存在", + "DialogModInvalidMessage": "指定的目录找不到 MOD 文件!", + "DialogModDeleteNoParentMessage": "删除失败:找不到 MOD 的父目录“{0}”!", + "DialogDlcNoDlcErrorMessage": "选择的文件不是当前游戏的 DLC!", + "DialogPerformanceCheckLoggingEnabledMessage": "您启用了跟踪日志,该功能仅供开发人员使用。", + "DialogPerformanceCheckLoggingEnabledConfirmMessage": "为了获得最佳性能,建议禁用跟踪日志记录。您是否要立即禁用?", + "DialogPerformanceCheckShaderDumpEnabledMessage": "您启用了着色器转储,该功能仅供开发人员使用。", + "DialogPerformanceCheckShaderDumpEnabledConfirmMessage": "为了获得最佳性能,建议禁用着色器转储。您是否要立即禁用?", + "DialogLoadAppGameAlreadyLoadedMessage": "游戏已经启动", + "DialogLoadAppGameAlreadyLoadedSubMessage": "请停止模拟或关闭模拟器,再启动另一个游戏。", + "DialogUpdateAddUpdateErrorMessage": "选择的文件不是当前游戏的更新!", + "DialogSettingsBackendThreadingWarningTitle": "警告 - 图形引擎多线程", + "DialogSettingsBackendThreadingWarningMessage": "更改此选项后,必须重启 Ryujinx 模拟器才能生效。\n\n当启用图形引擎多线程时,根据显卡不同,您可能需要手动禁用显卡驱动程序自身的多线程(线程优化)。", + "DialogModManagerDeletionWarningMessage": "您即将删除 MOD:{0} \n\n确定吗?", + "DialogModManagerDeletionAllWarningMessage": "您即将删除该游戏的所有 MOD,\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": "修改:使用随机生成的Amiibo ID", + "DlcManagerTableHeadingEnabledLabel": "已启用", + "DlcManagerTableHeadingTitleIdLabel": "游戏 ID", + "DlcManagerTableHeadingContainerPathLabel": "容器路径", + "DlcManagerTableHeadingFullPathLabel": "完整路径", + "DlcManagerRemoveAllButton": "全部删除", + "DlcManagerEnableAllButton": "全部启用", + "DlcManagerDisableAllButton": "全部停用", + "ModManagerDeleteAllButton": "全部刪除", + "MenuBarOptionsChangeLanguage": "更改界面语言", + "MenuBarShowFileTypes": "主页显示的文件类型", + "CommonSort": "排序", + "CommonShowNames": "显示名称", + "CommonFavorite": "收藏", + "OrderAscending": "升序", + "OrderDescending": "降序", + "SettingsTabGraphicsFeatures": "功能与优化", + "ErrorWindowTitle": "错误窗口", + "ToggleDiscordTooltip": "选择是否在 Discord 中显示您的游玩状态", + "AddGameDirBoxTooltip": "输入要添加的游戏目录", + "AddGameDirTooltip": "添加游戏目录到列表中", + "RemoveGameDirTooltip": "移除选中的目录", + "AddAutoloadDirBoxTooltip": "输入需要添加到列表中的自动加载目录", + "AddAutoloadDirTooltip": "添加一个自动加载目录到列表中", + "RemoveAutoloadDirTooltip": "移除被选中的自动加载目录", + "CustomThemeCheckTooltip": "使用自定义的 Avalonia 主题作为模拟器菜单的外观", + "CustomThemePathTooltip": "自定义主题的目录", + "CustomThemeBrowseTooltip": "查找自定义主题", + "DockModeToggleTooltip": "启用 Switch 的主机模式(电视模式、底座模式),就是模拟 Switch 连接底座的情况;若禁用主机模式,则使用 Switch 的掌机模式,就是模拟手持 Switch 运行游戏的情况。\n对于绝大多数游戏而言,主机模式会比掌机模式,画质更高,同时性能消耗也更高。\n\n简而言之,想要更好画质则启用主机模式;电脑硬件性能不足则禁用主机模式。\n\n如果使用主机模式,请选择“玩家 1”的手柄设置;如果使用掌机模式,请选择“掌机模式”的手柄设置。\n\n如果不确定,请保持开启状态。", + "DirectKeyboardTooltip": "直接键盘访问(HID)支持,游戏可以直接访问键盘作为文本输入设备。\n\n仅适用于在 Switch 硬件上原生支持键盘的游戏。\n\n如果不确定,请保持关闭状态。", + "DirectMouseTooltip": "直接鼠标访问(HID)支持,游戏可以直接访问鼠标作为指针输入设备。\n\n只适用于在 Switch 硬件上原生支持鼠标控制的游戏,这种游戏很少。\n\n启用后,触屏功能可能无法正常工作。\n\n如果不确定,请保持关闭状态。", + "RegionTooltip": "更改系统区域", + "LanguageTooltip": "更改系统语言", + "TimezoneTooltip": "更改系统时区", + "TimeTooltip": "更改系统时间", + "VSyncToggleTooltip": "模拟控制台的垂直同步,开启后会降低大部分游戏的帧率。关闭后,可以获得更高的帧率,但也可能导致游戏画面加载耗时更长或卡住。\n\n在游戏中可以使用热键进行切换(默认为 F1 键)。\n\n如果不确定,请保持开启状态。", + "PptcToggleTooltip": "缓存已编译的游戏指令,这样每次游戏加载时就无需重新编译。\n\n可以减少卡顿和启动时间,提高游戏响应速度。\n\n如果不确定,请保持开启状态。", + "LowPowerPptcToggleTooltip": "使用三分之一的核心数加载PPTC.", + "FsIntegrityToggleTooltip": "启动游戏时检查游戏文件的完整性,并在日志中记录损坏的文件。\n\n对性能没有影响,用于排查故障。\n\n如果不确定,请保持开启状态。", + "AudioBackendTooltip": "更改音频处理引擎。\n\n推荐选择“SDL2”,另外“OpenAL”和“SoundIO”可以作为备选,选择“无”将没有声音。\n\n如果不确定,请设置为“SDL2”。", + "MemoryManagerTooltip": "更改模拟器内存映射和访问的方式,对模拟器 CPU 的性能影响很大。\n\n如果不确定,请设置为“跳过检查的本机映射”。", + "MemoryManagerSoftwareTooltip": "使用软件内存页进行内存地址映射,最准确但是速度最慢。", + "MemoryManagerHostTooltip": "直接映射内存页到电脑内存,使得即时编译和执行的效率更高。", + "MemoryManagerUnsafeTooltip": "直接映射内存页到电脑内存,并且不检查内存溢出,使得效率更高,但牺牲了安全。\n游戏程序可以访问模拟器内存的任意地址,所以不安全。\n建议此模式下只运行您信任的游戏程序。", + "UseHypervisorTooltip": "使用 Hypervisor 虚拟机代替即时编译,在可用的情况下能大幅提高性能,但目前可能还不稳定。", + "DRamTooltip": "模拟 Switch 开发机的内存布局。\n\n不会提高性能,某些高清纹理包或 4k 分辨率 MOD 可能需要使用此选项。\n\n如果不确定,请保持关闭状态。", + "IgnoreMissingServicesTooltip": "开启后,游戏会忽略未实现的系统服务,从而继续运行。\n少部分新发布的游戏由于使用了新的未知系统服务,可能需要此选项来避免闪退。\n模拟器更新完善系统服务之后,则无需开启此选项。\n\n如果不确定,请保持关闭状态。", + "IgnoreAppletTooltip": "如果游戏手柄在游戏过程中断开连接,则不会出现外部对话框“控制器小程序”。不会提示关闭对话框或设置新控制器。一旦先前断开连接的控制器重新连接,游戏将自动恢复。", + "GraphicsBackendThreadingTooltip": "在第二个线程上执行图形引擎指令。\n\n可以加速着色器编译,减少卡顿,提高 GPU 的性能。\n\n如果不确定,请设置为“自动”。", + "GalThreadingTooltip": "在第二个线程上执行图形引擎指令。\n\n可以加速着色器编译,减少卡顿,提高 GPU 的性能。\n\n如果不确定,请设置为“自动”。", + "ShaderCacheToggleTooltip": "模拟器将已编译的着色器保存到硬盘,可以减少游戏再次渲染相同图形导致的卡顿。\n\n如果不确定,请保持开启状态。", + "ResolutionScaleTooltip": "将游戏的渲染分辨率乘以一个倍数。\n\n有些游戏可能不适用这项设置,而且即使提高了分辨率仍然看起来像素化;对于这些游戏,您可能需要找到移除抗锯齿或提高内部渲染分辨率的 MOD。当使用这些 MOD 时,建议设置为“原生”。\n\n在游戏运行时,通过点击下面的“应用”按钮可以使设置生效;你可以将设置窗口移开,并试验找到您喜欢的游戏画面效果。\n\n请记住,对于几乎所有人而言,4倍分辨率都是过度的。", + "ResolutionScaleEntryTooltip": "建议设置为整数倍,带小数的分辨率缩放倍数(例如1.5),非整数倍的缩放容易导致问题或闪退。", + "AnisotropyTooltip": "各向异性过滤等级,可以提高倾斜视角纹理的清晰度。\n当设置为“自动”时,使用游戏自身设定的等级。", + "AspectRatioTooltip": "游戏渲染窗口的宽高比。\n\n只有当游戏使用了修改宽高比的 MOD 时才需要修改这个设置,否则图像会被拉伸。\n\n如果不确定,请保持为“16:9”。", + "ShaderDumpPathTooltip": "转储图形着色器的路径", + "FileLogTooltip": "将控制台日志保存到硬盘文件,不影响性能。", + "StubLogTooltip": "在控制台中显示存根日志,不影响性能。", + "InfoLogTooltip": "在控制台中显示信息日志,不影响性能。", + "WarnLogTooltip": "在控制台中显示警告日志,不影响性能。", + "ErrorLogTooltip": "在控制台中显示错误日志,不影响性能。", + "TraceLogTooltip": "在控制台中显示跟踪日志。", + "GuestLogTooltip": "在控制台中显示访客日志,不影响性能。", + "FileAccessLogTooltip": "在控制台中显示文件访问日志。", + "FSAccessLogModeTooltip": "在控制台中显示文件系统访问日志,可选模式为 0-3。", + "DeveloperOptionTooltip": "请谨慎使用", + "OpenGlLogLevel": "需要启用适当的日志级别", + "DebugLogTooltip": "在控制台中显示调试日志。\n\n仅在特别需要时使用此功能,因为它会导致日志信息难以阅读,并降低模拟器性能。", + "LoadApplicationFileTooltip": "选择 Switch 游戏文件并加载", + "LoadApplicationFolderTooltip": "选择解包后的 Switch 游戏目录并加载", + "LoadDlcFromFolderTooltip": "打开文件资源管理器以选择一个或多个文件夹来批量加载DLC。", + "LoadTitleUpdatesFromFolderTooltip": "打开文件资源管理器以选择一个或多个文件夹来批量加载游戏更新。", + "OpenRyujinxFolderTooltip": "打开 Ryujinx 模拟器系统目录", + "OpenRyujinxLogsTooltip": "打开日志存放的目录", + "ExitTooltip": "退出 Ryujinx 模拟器", + "OpenSettingsTooltip": "打开设置窗口", + "OpenProfileManagerTooltip": "打开用户账户管理窗口", + "StopEmulationTooltip": "停止运行当前游戏,并回到主界面", + "CheckUpdatesTooltip": "检查 Ryujinx 新版本", + "OpenAboutTooltip": "打开关于窗口", + "GridSize": "网格尺寸", + "GridSizeTooltip": "调整网格项目的大小", + "SettingsTabSystemSystemLanguageBrazilianPortuguese": "巴西葡萄牙语", + "AboutRyujinxContributorsButtonHeader": "查看所有贡献者", + "SettingsTabSystemAudioVolume": "音量:", + "AudioVolumeTooltip": "调节音量", + "SettingsTabSystemEnableInternetAccess": "启用网络连接(局域网)", + "EnableInternetAccessTooltip": "允许模拟的游戏程序访问网络。\n\n当多个模拟器或实体 Switch 连接到同一个网络时,带有局域网模式的游戏便可以相互通信。\n\n即使开启此选项也无法访问 Nintendo 服务器,有可能导致某些尝试联网的游戏闪退。\n\n如果不确定,请保持关闭状态。", + "GameListContextMenuManageCheatToolTip": "管理当前游戏的金手指", + "GameListContextMenuManageCheat": "管理金手指", + "GameListContextMenuManageModToolTip": "管理当前游戏的 MOD", + "GameListContextMenuManageMod": "管理 MOD", + "ControllerSettingsStickRange": "范围:", + "DialogStopEmulationTitle": "Ryujinx - 停止模拟", + "DialogStopEmulationMessage": "确定要停止模拟?", + "SettingsTabCpu": "CPU", + "SettingsTabAudio": "音频", + "SettingsTabNetwork": "网络", + "SettingsTabNetworkConnection": "网络连接", + "SettingsTabCpuCache": "CPU 缓存", + "SettingsTabCpuMemory": "CPU 模式", + "DialogUpdaterFlatpakNotSupportedMessage": "请通过 FlatHub 更新 Ryujinx 模拟器。", + "UpdaterDisabledWarningTitle": "已禁用更新!", + "ControllerSettingsRotate90": "顺时针旋转 90°", + "IconSize": "图标尺寸", + "IconSizeTooltip": "更改游戏图标的显示尺寸", + "MenuBarOptionsShowConsole": "显示控制台", + "ShaderCachePurgeError": "清除 {0} 的着色器缓存文件时出错:{1}", + "UserErrorNoKeys": "找不到密钥Keys", + "UserErrorNoFirmware": "未安装系统固件", + "UserErrorFirmwareParsingFailed": "固件文件解析出错", + "UserErrorApplicationNotFound": "找不到游戏程序", + "UserErrorUnknown": "未知错误", + "UserErrorUndefined": "未定义错误", + "UserErrorNoKeysDescription": "Ryujinx 模拟器找不到“prod.keys”密钥文件", + "UserErrorNoFirmwareDescription": "Ryujinx 模拟器未安装 Switch 系统固件", + "UserErrorFirmwareParsingFailedDescription": "Ryujinx 模拟器无法解密当前固件,一般是由于使用了旧版的密钥导致的。", + "UserErrorApplicationNotFoundDescription": "Ryujinx 模拟器在所选路径中找不到有效的游戏程序。", + "UserErrorUnknownDescription": "出现未知错误!", + "UserErrorUndefinedDescription": "出现未定义错误!此类错误不应出现,请联系开发者!", + "OpenSetupGuideMessage": "打开安装指南", + "NoUpdate": "无更新(默认版本)", + "TitleUpdateVersionLabel": "游戏更新的版本 {0}", + "TitleBundledUpdateVersionLabel": "捆绑:版本 {0}", + "TitleBundledDlcLabel": "捆绑:", + "TitleXCIStatusPartialLabel": "Partial", + "TitleXCIStatusTrimmableLabel": "Untrimmed", + "TitleXCIStatusUntrimmableLabel": "Trimmed", + "TitleXCIStatusFailedLabel": "(Failed)", + "TitleXCICanSaveLabel": "Save {0:n0} Mb", + "TitleXCISavingLabel": "Saved {0:n0} Mb", + "RyujinxInfo": "Ryujinx - 信息", + "RyujinxConfirm": "Ryujinx - 确认", + "FileDialogAllTypes": "全部类型", + "Never": "从不", + "SwkbdMinCharacters": "不少于 {0} 个字符", + "SwkbdMinRangeCharacters": "必须为 {0}-{1} 个字符", + "SoftwareKeyboard": "软键盘", + "SoftwareKeyboardModeNumeric": "只能输入 0-9 或 \".\"", + "SoftwareKeyboardModeAlphabet": "仅支持非中文字符", + "SoftwareKeyboardModeASCII": "仅支持 ASCII 字符", + "ControllerAppletControllers": "支持的手柄:", + "ControllerAppletPlayers": "玩家:", + "ControllerAppletDescription": "您当前的输入配置无效。打开设置并重新设置您的输入选项。", + "ControllerAppletDocked": "已经设置为主机模式,应禁用掌机手柄操控。", + "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": "选择更新文件", + "SelectModDialogTitle": "选择 MOD 目录", + "TrimXCIFileDialogTitle": "Check and Trim XCI File", + "TrimXCIFileDialogPrimaryText": "This function will first check the empty space and then trim the XCI File to save disk space.", + "TrimXCIFileDialogSecondaryText": "Current File Size: {0:n} MB\nGame Data Size: {1:n} MB\nDisk Space Savings: {2:n} MB", + "TrimXCIFileNoTrimNecessary": "XCI File does not need to be trimmed. Check logs for further details", + "TrimXCIFileNoUntrimPossible": "XCI File cannot be untrimmed. Check logs for further details", + "TrimXCIFileReadOnlyFileCannotFix": "XCI File is Read Only and could not be made writable. Check logs for further details", + "TrimXCIFileFileSizeChanged": "XCI File has changed in size since it was scanned. Please check the file is not being written to and try again.", + "TrimXCIFileFreeSpaceCheckFailed": "XCI File has data in the free space area, it is not safe to trim", + "TrimXCIFileInvalidXCIFile": "XCI File contains invalid data. Check logs for further details", + "TrimXCIFileFileIOWriteError": "XCI File could not be opened for writing. Check logs for further details", + "TrimXCIFileFailedPrimaryText": "Trimming of the XCI file failed", + "TrimXCIFileCancelled": "The operation was cancelled", + "TrimXCIFileFileUndertermined": "No operation was performed", + "UserProfileWindowTitle": "管理用户账户", + "CheatWindowTitle": "金手指管理器", + "DlcWindowTitle": "管理 {0} ({1}) 的 DLC", + "ModWindowTitle": "管理 {0} ({1}) 的 MOD", + "UpdateWindowTitle": "游戏更新管理器", + "XCITrimmerWindowTitle": "XCI File Trimmer", + "XCITrimmerTitleStatusCount": "{0} of {1} Title(s) Selected", + "XCITrimmerTitleStatusCountWithFilter": "{0} of {1} Title(s) Selected ({2} displayed)", + "XCITrimmerTitleStatusTrimming": "Trimming {0} Title(s)...", + "XCITrimmerTitleStatusUntrimming": "Untrimming {0} Title(s)...", + "XCITrimmerTitleStatusFailed": "Failed", + "XCITrimmerPotentialSavings": "Potential Savings", + "XCITrimmerActualSavings": "Actual Savings", + "XCITrimmerSavingsMb": "{0:n0} Mb", + "XCITrimmerSelectDisplayed": "Select Shown", + "XCITrimmerDeselectDisplayed": "Deselect Shown", + "XCITrimmerSortName": "Title", + "XCITrimmerSortSaved": "Space Savings", + "XCITrimmerTrim": "Trim", + "XCITrimmerUntrim": "Untrim", + "UpdateWindowUpdateAddedMessage": "{0} 个更新被添加", + "UpdateWindowBundledContentNotice": "游戏整合的更新无法移除,可尝试禁用。", + "CheatWindowHeading": "适用于 {0} [{1}] 的金手指", + "BuildId": "游戏版本 ID:", + "DlcWindowBundledContentNotice": "游戏整合的DLC无法移除,可尝试禁用。", + "DlcWindowHeading": "{0} 个 DLC", + "DlcWindowDlcAddedMessage": "{0} 个DLC被添加", + "AutoloadDlcAddedMessage": "{0} 个DLC被添加", + "AutoloadDlcRemovedMessage": "{0} 个失效的DLC已移除", + "AutoloadUpdateAddedMessage": "{0} 个游戏更新被添加", + "AutoloadUpdateRemovedMessage": "{0} 个失效的游戏更新已移除", + "ModWindowHeading": "{0} Mod", + "UserProfilesEditProfile": "编辑所选", + "Continue": "Continue", + "Cancel": "取消", + "Save": "保存", + "Discard": "放弃", + "Paused": "已暂停", + "UserProfilesSetProfileImage": "选择头像", + "UserProfileEmptyNameError": "必须输入名称", + "UserProfileNoImageError": "必须设置头像", + "GameUpdateWindowHeading": "管理 {0} ({1}) 的更新", + "SettingsTabHotkeysResScaleUpHotkey": "提高分辨率:", + "SettingsTabHotkeysResScaleDownHotkey": "降低分辨率:", + "UserProfilesName": "名称:", + "UserProfilesUserId": "用户 ID:", + "SettingsTabGraphicsBackend": "图形渲染引擎:", + "SettingsTabGraphicsBackendTooltip": "选择模拟器中使用的图像渲染引擎。\n\n安装了最新显卡驱动程序的所有现代显卡基本都支持 Vulkan,Vulkan 能够提供更快的着色器编译(较少的卡顿)。\n\n在旧版 Nvidia 显卡上、Linux 上的旧版 AMD 显卡,或者显存较低的显卡上,OpenGL 可能会取得更好的效果,但着色器编译更慢(更多的卡顿)。\n\n如果不确定,请设置为“Vulkan”。如果您的 GPU 已安装了最新显卡驱动程序也不支持 Vulkan,那请设置为“OpenGL”。", + "SettingsEnableTextureRecompression": "启用纹理压缩", + "SettingsEnableTextureRecompressionTooltip": "压缩 ASTC 纹理以减少 VRAM (显存)的占用。\n\n使用此纹理格式的游戏包括:异界锁链(Astral Chain),蓓优妮塔3(Bayonetta 3),火焰纹章Engage(Fire Emblem Engage),密特罗德 究极(Metroid Prime Remased),超级马力欧兄弟 惊奇(Super Mario Bros. Wonder)以及塞尔达传说 王国之泪(The Legend of Zelda: Tears of the Kingdom)。\n\n显存小于4GB的显卡在运行这些游戏时可能会偶发闪退。\n\n只有当您在上述游戏中的显存不足时才需要启用此选项。\n\n如果不确定,请保持关闭状态。", + "SettingsTabGraphicsPreferredGpu": "首选 GPU:", + "SettingsTabGraphicsPreferredGpuTooltip": "选择 Vulkan 图形引擎使用的 GPU。\n\n此选项不会影响 OpenGL 使用的 GPU。\n\n如果不确定,建议选择\"独立显卡(dGPU)\"。如果没有独立显卡,则无需改动此选项。", + "SettingsAppRequiredRestartMessage": "Ryujinx 模拟器需要重启", + "SettingsGpuBackendRestartMessage": "您修改了图形引擎或 GPU 设置,需要重启模拟器才能生效", + "SettingsGpuBackendRestartSubMessage": "是否要立即重启模拟器?", + "RyujinxUpdaterMessage": "是否更新 Ryujinx 到最新的版本?", + "SettingsTabHotkeysVolumeUpHotkey": "音量加:", + "SettingsTabHotkeysVolumeDownHotkey": "音量减:", + "SettingsEnableMacroHLE": "启用 HLE 宏加速", + "SettingsEnableMacroHLETooltip": "GPU 宏指令的高级模拟。\n\n提高性能表现,但一些游戏可能会出现图形错误。\n\n如果不确定,请保持开启状态。", + "SettingsEnableColorSpacePassthrough": "色彩空间直通", + "SettingsEnableColorSpacePassthroughTooltip": "使 Vulkan 图形引擎直接传输原始色彩信息。对于广色域 (例如 DCI-P3) 显示器的用户来说,可以产生更鲜艳的颜色,代价是损失部分色彩准确度。", + "VolumeShort": "音量", + "UserProfilesManageSaves": "管理存档", + "DeleteUserSave": "确定删除此游戏的用户存档吗?", + "IrreversibleActionNote": "删除后不可恢复。", + "SaveManagerHeading": "管理 {0} ({1}) 的存档", + "SaveManagerTitle": "存档管理器", + "Name": "名称", + "Size": "大小", + "Search": "搜索", + "UserProfilesRecoverLostAccounts": "恢复丢失的账户", + "Recover": "恢复", + "UserProfilesRecoverHeading": "找到了这些用户的存档数据", + "UserProfilesRecoverEmptyList": "没有可以恢复的用户数据", + "GraphicsAATooltip": "抗锯齿是一种图形处理技术,用于减少图像边缘的锯齿状现象,使图像更加平滑。\n\nFXAA(快速近似抗锯齿)是一种性能开销相对较小的抗锯齿方法,但可能会使得整体图像看起来有些模糊。\n\nSMAA(增强型子像素抗锯齿)则更加精细,它会尝试找到锯齿边缘并平滑它们,相比 FXAA 有更好的图像质量,但性能开销可能会稍大一些。\n\n如果开启了 FSR(FidelityFX Super Resolution,超级分辨率锐画技术)来提高性能或图像质量,不建议再启用抗锯齿,因为它们会产生不必要的图形处理开销,或者相互之间效果不协调。\n\n在游戏运行时,通过点击下面的“应用”按钮可以使设置生效;你可以将设置窗口移开,并试验找到您喜欢的游戏画面效果。\n\n如果不确定,请保持为“无”。", + "GraphicsAALabel": "抗锯齿:", + "GraphicsScalingFilterLabel": "缩放过滤:", + "GraphicsScalingFilterTooltip": "选择在分辨率缩放时将使用的缩放过滤器。\n\nBilinear(双线性过滤)对于3D游戏效果较好,是一个安全的默认选项。\n\nNearest(最近邻过滤)推荐用于像素艺术游戏。\n\nFSR(超级分辨率锐画)只是一个锐化过滤器,不推荐与 FXAA 或 SMAA 抗锯齿一起使用。\n\nArea(局部过滤),当渲染分辨率大于窗口实际分辨率,推荐该选项。该选项在渲染比例大于2.0的情况下,可以实现超采样的效果。\n\n在游戏运行时,通过点击下面的“应用”按钮可以使设置生效;你可以将设置窗口移开,并试验找到您喜欢的游戏画面效果。\n\n如果不确定,请保持为“Bilinear(双线性过滤)”。", + "GraphicsScalingFilterBilinear": "Bilinear(双线性过滤)", + "GraphicsScalingFilterNearest": "Nearest(邻近过滤)", + "GraphicsScalingFilterFsr": "FSR(超级分辨率锐画技术)", + "GraphicsScalingFilterArea": "Area(区域过滤)", + "GraphicsScalingFilterLevelLabel": "等级", + "GraphicsScalingFilterLevelTooltip": "设置 FSR 1.0 的锐化等级,数值越高,图像越锐利。", + "SmaaLow": "SMAA 低质量", + "SmaaMedium": "SMAA 中质量", + "SmaaHigh": "SMAA 高质量", + "SmaaUltra": "SMAA 超高质量", + "UserEditorTitle": "编辑用户", + "UserEditorTitleCreate": "创建用户", + "SettingsTabNetworkInterface": "网络接口:", + "NetworkInterfaceTooltip": "用于局域网(LAN)/本地网络发现(LDN)功能的网络接口。\n\n结合 VPN 或 XLink Kai 以及支持局域网功能的游戏,可以在互联网上伪造为同一网络连接。\n\n如果不确定,请保持为“默认”。", + "NetworkInterfaceDefault": "默认", + "PackagingShaders": "整合着色器中", + "AboutChangelogButton": "在 Github 上查看更新日志", + "AboutChangelogButtonTooltipMessage": "点击这里在浏览器中打开此版本的更新日志。", + "SettingsTabNetworkMultiplayer": "多人联机游玩", + "MultiplayerMode": "联机模式:", + "MultiplayerModeTooltip": "修改 LDN 多人联机游玩模式。\n\nldn_mitm 联机插件将修改游戏中的本地无线和本地游玩功能,使其表现得像局域网一样,允许和其他安装了 ldn_mitm 插件的 Ryujinx 模拟器和破解的任天堂 Switch 主机在同一网络下进行本地连接,实现多人联机游玩。\n\n多人联机游玩要求所有玩家必须运行相同的游戏版本(例如,游戏版本 v13.0.1 无法与 v13.0.0 联机)。\n\n如果不确定,请保持为“禁用”。", + "MultiplayerModeDisabled": "禁用", + "MultiplayerModeLdnMitm": "ldn_mitm", + "MultiplayerModeLdnRyu": "RyuLDN", + "MultiplayerDisableP2P": "Disable P2P Network Hosting (may increase latency)", + "MultiplayerDisableP2PTooltip": "Disable P2P network hosting, peers will proxy through the master server instead of connecting to you directly.", + "LdnPassphrase": "Network Passphrase:", + "LdnPassphraseTooltip": "You will only be able to see hosted games with the same passphrase as you.", + "LdnPassphraseInputTooltip": "Enter a passphrase in the format Ryujinx-<8 hex chars>. You will only be able to see hosted games with the same passphrase as you.", + "LdnPassphraseInputPublic": "(public)", + "GenLdnPass": "Generate Random", + "GenLdnPassTooltip": "Generates a new passphrase, which can be shared with other players.", + "ClearLdnPass": "Clear", + "ClearLdnPassTooltip": "Clears the current passphrase, returning to the public network.", + "InvalidLdnPassphrase": "Invalid Passphrase! Must be in the format \"Ryujinx-<8 hex chars>\"" +} diff --git a/src/Ryujinx/Assets/Locales/zh_TW.json b/src/Ryujinx/Assets/Locales/zh_TW.json new file mode 100644 index 000000000..d219bc708 --- /dev/null +++ b/src/Ryujinx/Assets/Locales/zh_TW.json @@ -0,0 +1,868 @@ +{ + "Language": "繁體中文 (台灣)", + "MenuBarFileOpenApplet": "開啟小程式", + "MenuBarFileOpenAppletOpenMiiApplet": "Mii Edit Applet", + "MenuBarFileOpenAppletOpenMiiAppletToolTip": "在獨立模式下開啟 Mii 編輯器小程式", + "SettingsTabInputDirectMouseAccess": "滑鼠直接存取", + "SettingsTabSystemMemoryManagerMode": "記憶體管理員模式:", + "SettingsTabSystemMemoryManagerModeSoftware": "軟體模式", + "SettingsTabSystemMemoryManagerModeHost": "主體模式 (快速)", + "SettingsTabSystemMemoryManagerModeHostUnchecked": "主體略過檢查模式 (最快,不安全)", + "SettingsTabSystemUseHypervisor": "使用 Hypervisor", + "MenuBarFile": "檔案(_F)", + "MenuBarFileOpenFromFile": "從檔案載入應用程式(_L)", + "MenuBarFileOpenFromFileError": "未能從已選擇的檔案中找到應用程式。", + "MenuBarFileOpenUnpacked": "載入未封裝的遊戲(_U)", + "MenuBarFileLoadDlcFromFolder": "從資料夾中載入 DLC", + "MenuBarFileLoadTitleUpdatesFromFolder": "從資料夾中載入遊戲更新", + "MenuBarFileOpenEmuFolder": "開啟 Ryujinx 資料夾", + "MenuBarFileOpenLogsFolder": "開啟日誌資料夾", + "MenuBarFileExit": "結束(_E)", + "MenuBarOptions": "選項(_O)", + "MenuBarOptionsToggleFullscreen": "切換全螢幕模式", + "MenuBarOptionsStartGamesInFullscreen": "使用全螢幕模式啟動遊戲", + "MenuBarOptionsStopEmulation": "停止模擬", + "MenuBarOptionsSettings": "設定(_S)", + "MenuBarOptionsManageUserProfiles": "管理使用者設定檔(_M)", + "MenuBarActions": "動作(_A)", + "MenuBarOptionsSimulateWakeUpMessage": "模擬喚醒訊息", + "MenuBarActionsScanAmiibo": "掃描 Amiibo", + "MenuBarTools": "工具(_T)", + "MenuBarToolsInstallFirmware": "安裝韌體", + "MenuBarFileToolsInstallFirmwareFromFile": "從 XCI 或 ZIP 安裝韌體", + "MenuBarFileToolsInstallFirmwareFromDirectory": "從資料夾安裝韌體", + "MenuBarToolsManageFileTypes": "管理檔案類型", + "MenuBarToolsInstallFileTypes": "安裝檔案類型", + "MenuBarToolsUninstallFileTypes": "移除檔案類型", + "MenuBarToolsXCITrimmer": "Trim XCI Files", + "MenuBarView": "檢視(_V)", + "MenuBarViewWindow": "視窗大小", + "MenuBarViewWindow720": "720p", + "MenuBarViewWindow1080": "1080p", + "MenuBarHelp": "說明(_H)", + "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 管理視窗", + "GameListContextMenuCacheManagement": "快取管理", + "GameListContextMenuCacheManagementPurgePptc": "佇列 PPTC 重建", + "GameListContextMenuCacheManagementPurgePptcToolTip": "下一次啟動遊戲時,觸發 PPTC 進行重建", + "GameListContextMenuCacheManagementPurgeShaderCache": "清除著色器快取", + "GameListContextMenuCacheManagementPurgeShaderCacheToolTip": "刪除應用程式的著色器快取", + "GameListContextMenuCacheManagementOpenPptcDirectory": "開啟 PPTC 資料夾", + "GameListContextMenuCacheManagementOpenPptcDirectoryToolTip": "開啟此應用程式的 PPTC 快取資料夾", + "GameListContextMenuCacheManagementOpenShaderCacheDirectory": "開啟著色器快取資料夾", + "GameListContextMenuCacheManagementOpenShaderCacheDirectoryToolTip": "開啟此應用程式的著色器快取資料夾", + "GameListContextMenuExtractData": "提取資料", + "GameListContextMenuExtractDataExeFS": "ExeFS", + "GameListContextMenuExtractDataExeFSToolTip": "從應用程式的目前配置中提取 ExeFS 分區 (包含更新)", + "GameListContextMenuExtractDataRomFS": "RomFS", + "GameListContextMenuExtractDataRomFSToolTip": "從應用程式的目前配置中提取 RomFS 分區 (包含更新)", + "GameListContextMenuExtractDataLogo": "Logo", + "GameListContextMenuExtractDataLogoToolTip": "從應用程式的目前配置中提取 Logo 分區 (包含更新)", + "GameListContextMenuCreateShortcut": "建立應用程式捷徑", + "GameListContextMenuCreateShortcutToolTip": "建立桌面捷徑,啟動選取的應用程式", + "GameListContextMenuCreateShortcutToolTipMacOS": "在 macOS 的應用程式資料夾中建立捷徑,啟動選取的應用程式", + "GameListContextMenuOpenModsDirectory": "開啟模組資料夾", + "GameListContextMenuOpenModsDirectoryToolTip": "開啟此應用程式模組的資料夾", + "GameListContextMenuOpenSdModsDirectory": "開啟 Atmosphere 模組資料夾", + "GameListContextMenuOpenSdModsDirectoryToolTip": "開啟此應用程式模組的另一個 SD 卡 Atmosphere 資料夾。適用於為真實硬體封裝的模組。", + "GameListContextMenuTrimXCI": "Check and Trim XCI File", + "GameListContextMenuTrimXCIToolTip": "Check and Trim XCI File to Save Disk Space", + "StatusBarGamesLoaded": "{0}/{1} 遊戲已載入", + "StatusBarSystemVersion": "系統版本: {0}", + "StatusBarXCIFileTrimming": "Trimming XCI File '{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": "顯示「確認結束」對話方塊", + "SettingsTabGeneralRememberWindowState": "記住視窗大小/位置", + "SettingsTabGeneralShowTitleBar": "顯示「標題列」 (需要重新開啟Ryujinx)", + "SettingsTabGeneralHideCursor": "隱藏滑鼠游標:", + "SettingsTabGeneralHideCursorNever": "從不", + "SettingsTabGeneralHideCursorOnIdle": "閒置時", + "SettingsTabGeneralHideCursorAlways": "總是", + "SettingsTabGeneralGameDirectories": "遊戲資料夾", + "SettingsTabGeneralAutoloadDirectories": "自動載入 DLC/遊戲更新資料夾", + "SettingsTabGeneralAutoloadNote": "遺失的 DLC 及遊戲更新檔案將會在自動載入中移除", + "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 (剖析式持久轉譯快取, Profiled Persistent Translation Cache)", + "SettingsTabSystemEnableLowPowerPptc": "低功耗 PPTC", + "SettingsTabSystemEnableFsIntegrityChecks": "檔案系統完整性檢查", + "SettingsTabSystemAudioBackend": "音效後端:", + "SettingsTabSystemAudioBackendDummy": "虛設 (Dummy)", + "SettingsTabSystemAudioBackendOpenAL": "OpenAL", + "SettingsTabSystemAudioBackendSoundIO": "SoundIO", + "SettingsTabSystemAudioBackendSDL2": "SDL2", + "SettingsTabSystemHacks": "補釘修正", + "SettingsTabSystemHacksNote": "可能導致模擬器不穩定", + "SettingsTabSystemDramSize": "使用替代的記憶體配置 (開發者專用)", + "SettingsTabSystemDramSize4GiB": "4GiB", + "SettingsTabSystemDramSize6GiB": "6GiB", + "SettingsTabSystemDramSize8GiB": "8GiB", + "SettingsTabSystemDramSize12GiB": "12GiB", + "SettingsTabSystemIgnoreMissingServices": "忽略缺少的模擬器功能", + "SettingsTabSystemIgnoreApplet": "忽略小程式", + "SettingsTabGraphics": "圖形", + "SettingsTabGraphicsAPI": "圖形 API", + "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": "底座模式", + "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": "搖桿左右反向", + "ControllerSettingsStickInvertYAxis": "搖桿上下反向", + "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": "伺服器主機位址:", + "ControllerSettingsMotionGyroSensitivity": "陀螺儀靈敏度:", + "ControllerSettingsMotionGyroDeadzone": "陀螺儀無感帶:", + "ControllerSettingsSave": "儲存", + "ControllerSettingsClose": "關閉", + "KeyUnknown": "未知", + "KeyShiftLeft": "左 Shift", + "KeyShiftRight": "右 Shift", + "KeyControlLeft": "左 Ctrl", + "KeyMacControlLeft": "左 ⌃", + "KeyControlRight": "右 Ctrl", + "KeyMacControlRight": "右 ⌃", + "KeyAltLeft": "左 Alt", + "KeyMacAltLeft": "左 ⌥", + "KeyAltRight": "右 Alt", + "KeyMacAltRight": "右 ⌥", + "KeyWinLeft": "左 ⊞", + "KeyMacWinLeft": "左 ⌘", + "KeyWinRight": "右 ⊞", + "KeyMacWinRight": "右 ⌘", + "KeyMenu": "功能表", + "KeyUp": "上", + "KeyDown": "下", + "KeyLeft": "左", + "KeyRight": "右", + "KeyEnter": "Enter 鍵", + "KeyEscape": "Esc 鍵", + "KeySpace": "空白鍵", + "KeyTab": "Tab 鍵", + "KeyBackSpace": "Backspace 鍵", + "KeyInsert": "Insert 鍵", + "KeyDelete": "Delete 鍵", + "KeyPageUp": "向上捲頁鍵", + "KeyPageDown": "向下捲頁鍵", + "KeyHome": "Home 鍵", + "KeyEnd": "End 鍵", + "KeyCapsLock": "Caps Lock 鍵", + "KeyScrollLock": "Scroll Lock 鍵", + "KeyPrintScreen": "Print Screen 鍵", + "KeyPause": "Pause 鍵", + "KeyNumLock": "Num Lock 鍵", + "KeyClear": "清除", + "KeyKeypad0": "數字鍵 0", + "KeyKeypad1": "數字鍵 1", + "KeyKeypad2": "數字鍵 2", + "KeyKeypad3": "數字鍵 3", + "KeyKeypad4": "數字鍵 4", + "KeyKeypad5": "數字鍵 5", + "KeyKeypad6": "數字鍵 6", + "KeyKeypad7": "數字鍵 7", + "KeyKeypad8": "數字鍵 8", + "KeyKeypad9": "數字鍵 9", + "KeyKeypadDivide": "數字鍵除號", + "KeyKeypadMultiply": "數字鍵乘號", + "KeyKeypadSubtract": "數字鍵減號", + "KeyKeypadAdd": "數字鍵加號", + "KeyKeypadDecimal": "數字鍵小數點", + "KeyKeypadEnter": "數字鍵 Enter", + "KeyNumber0": "0", + "KeyNumber1": "1", + "KeyNumber2": "2", + "KeyNumber3": "3", + "KeyNumber4": "4", + "KeyNumber5": "5", + "KeyNumber6": "6", + "KeyNumber7": "7", + "KeyNumber8": "8", + "KeyNumber9": "9", + "KeyTilde": "~", + "KeyGrave": "`", + "KeyMinus": "-", + "KeyPlus": "+", + "KeyBracketLeft": "[", + "KeyBracketRight": "]", + "KeySemicolon": ";", + "KeyQuote": "\"", + "KeyComma": ",", + "KeyPeriod": ".", + "KeySlash": "/", + "KeyBackSlash": "\\", + "KeyUnbound": "未分配", + "GamepadLeftStick": "左搖桿按鍵", + "GamepadRightStick": "右搖桿按鍵", + "GamepadLeftShoulder": "左肩鍵", + "GamepadRightShoulder": "右肩鍵", + "GamepadLeftTrigger": "左扳機", + "GamepadRightTrigger": "右扳機", + "GamepadDpadUp": "上", + "GamepadDpadDown": "下", + "GamepadDpadLeft": "左", + "GamepadDpadRight": "右", + "GamepadMinus": "-", + "GamepadPlus": "+", + "GamepadGuide": "快顯功能表鍵", + "GamepadMisc1": "其他按鍵", + "GamepadPaddle1": "其他按鍵 1", + "GamepadPaddle2": "其他按鍵 2", + "GamepadPaddle3": "其他按鍵 3", + "GamepadPaddle4": "其他按鍵 4", + "GamepadTouchpad": "觸控板", + "GamepadSingleLeftTrigger0": "左扳機 0", + "GamepadSingleRightTrigger0": "右扳機 0", + "GamepadSingleLeftTrigger1": "左扳機 1", + "GamepadSingleRightTrigger1": "右扳機 1", + "StickLeft": "左搖桿", + "StickRight": "右搖桿", + "UserProfilesSelectedUserProfile": "選取的使用者設定檔:", + "UserProfilesSaveProfileName": "儲存設定檔名稱", + "UserProfilesChangeProfileImage": "變更設定檔圖像", + "UserProfilesAvailableUserProfiles": "可用的使用者設定檔:", + "UserProfilesAddNewProfile": "建立設定檔", + "UserProfilesDelete": "刪除", + "UserProfilesClose": "關閉", + "ProfileNameSelectionWatermark": "選擇暱稱", + "ProfileImageSelectionTitle": "設定檔圖像選取", + "ProfileImageSelectionHeader": "選擇設定檔圖像", + "ProfileImageSelectionNote": "您可以匯入自訂的設定檔圖像,或從系統韌體中選取大頭貼。", + "ProfileImageSelectionImportImage": "匯入圖像檔案", + "ProfileImageSelectionSelectAvatar": "選取韌體大頭貼", + "InputDialogTitle": "輸入對話方塊", + "InputDialogOk": "確定", + "InputDialogCancel": "取消", + "InputDialogCancelling": "Cancelling", + "InputDialogClose": "Close", + "InputDialogAddNewProfileTitle": "選擇設定檔名稱", + "InputDialogAddNewProfileHeader": "請輸入設定檔名稱", + "InputDialogAddNewProfileSubtext": "(最大長度: {0})", + "AvatarChoose": "選擇大頭貼", + "AvatarSetBackgroundColor": "設定背景顏色", + "AvatarClose": "關閉", + "ControllerSettingsLoadProfileToolTip": "載入設定檔", + "ControllerSettingsViewProfileToolTip": "View Profile", + "ControllerSettingsAddProfileToolTip": "新增設定檔", + "ControllerSettingsRemoveProfileToolTip": "刪除設定檔", + "ControllerSettingsSaveProfileToolTip": "儲存設定檔", + "MenuBarFileToolsTakeScreenshot": "儲存擷取畫面", + "MenuBarFileToolsHideUi": "隱藏 UI", + "GameListContextMenuRunApplication": "執行應用程式", + "GameListContextMenuToggleFavorite": "加入/移除為我的最愛", + "GameListContextMenuToggleFavoriteToolTip": "切換遊戲的我的最愛狀態", + "SettingsTabGeneralTheme": "佈景主題:", + "SettingsTabGeneralThemeAuto": "自動", + "SettingsTabGeneralThemeDark": "深色", + "SettingsTabGeneralThemeLight": "淺色", + "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": "NCA 分區提取器", + "DialogNcaExtractionMainNcaNotFoundErrorMessage": "提取失敗。所選檔案中不存在主 NCA 檔案。", + "DialogNcaExtractionCheckLogErrorMessage": "提取失敗。請閱讀日誌檔案了解更多資訊。", + "DialogNcaExtractionSuccessMessage": "提取成功。", + "DialogUpdaterConvertFailedMessage": "無法轉換目前的 Ryujinx 版本。", + "DialogUpdaterCancelUpdateMessage": "取消更新!", + "DialogUpdaterAlreadyOnLatestVersionMessage": "您已經在使用最新版本的 Ryujinx!", + "DialogUpdaterFailedToGetVersionMessage": "嘗試從 GitHub Release 取得發布資訊時發生錯誤。如果 GitHub Actions 正在編譯新版本,則可能會出現這種情況。請幾分鐘後再試一次。", + "DialogUpdaterConvertFailedGithubMessage": "無法轉換從 Github Release 接收到的 Ryujinx 版本。", + "DialogUpdaterDownloadingMessage": "正在下載更新...", + "DialogUpdaterExtractionMessage": "正在提取更新...", + "DialogUpdaterRenamingMessage": "重新命名更新...", + "DialogUpdaterAddingFilesMessage": "加入新更新...", + "DialogUpdaterShowChangelogMessage": "Show Changelog", + "DialogUpdaterCompleteMessage": "更新成功!", + "DialogUpdaterRestartMessage": "您現在要重新啟動 Ryujinx 嗎?", + "DialogUpdaterNoInternetMessage": "您沒有連線到網際網路!", + "DialogUpdaterNoInternetSubMessage": "請確認您的網際網路連線正常!", + "DialogUpdaterDirtyBuildMessage": "您無法更新非官方版本的 Ryujinx!", + "DialogUpdaterDirtyBuildSubMessage": "如果您正在尋找受官方支援的版本,請從 https://ryujinx.app/download 下載 Ryujinx。", + "DialogRestartRequiredMessage": "需要重新啟動", + "DialogThemeRestartMessage": "佈景主題設定已儲存。需要重新啟動才能套用主題。", + "DialogThemeRestartSubMessage": "您要重新啟動嗎", + "DialogFirmwareInstallEmbeddedMessage": "您想安裝遊戲內建的韌體嗎? (韌體 {0})", + "DialogFirmwareInstallEmbeddedSuccessMessage": "未找到已安裝的韌體,但 Ryujinx 可以從現有的遊戲安裝韌體{0}。\n模擬器現在可以執行。", + "DialogFirmwareNoFirmwareInstalledMessage": "未安裝韌體", + "DialogFirmwareInstalledMessage": "已安裝韌體{0}", + "DialogInstallFileTypesSuccessMessage": "成功安裝檔案類型!", + "DialogInstallFileTypesErrorMessage": "無法安裝檔案類型。", + "DialogUninstallFileTypesSuccessMessage": "成功移除檔案類型!", + "DialogUninstallFileTypesErrorMessage": "無法移除檔案類型。", + "DialogOpenSettingsWindowLabel": "開啟設定視窗", + "DialogOpenXCITrimmerWindowLabel": "XCI Trimmer Window", + "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": "您將在下一次啟動時佇列重建以下遊戲的 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": "\n\n您確定要繼續嗎?", + "DialogFirmwareInstallerFirmwareInstallWaitMessage": "正在安裝韌體...", + "DialogFirmwareInstallerFirmwareInstallSuccessMessage": "成功安裝系統版本 {0}。", + "DialogUserProfileDeletionWarningMessage": "如果刪除選取的設定檔,將無法開啟其他設定檔", + "DialogUserProfileDeletionConfirmMessage": "您是否要刪除所選設定檔", + "DialogUserProfileUnsavedChangesTitle": "警告 - 未儲存的變更", + "DialogUserProfileUnsavedChangesMessage": "您對該使用者設定檔所做的變更尚未儲存。", + "DialogUserProfileUnsavedChangesSubMessage": "您確定要放棄變更嗎?", + "DialogControllerSettingsModifiedConfirmMessage": "目前控制器設定已更新。", + "DialogControllerSettingsModifiedConfirmSubMessage": "您想要儲存嗎?", + "DialogLoadFileErrorMessage": "{0}。出錯檔案: {1}", + "DialogModAlreadyExistsMessage": "模組已經存在", + "DialogModInvalidMessage": "指定資料夾不包含模組!", + "DialogModDeleteNoParentMessage": "刪除失敗: 無法找到模組「{0}」的父資料夾!", + "DialogDlcNoDlcErrorMessage": "指定檔案不包含所選遊戲的 DLC!", + "DialogPerformanceCheckLoggingEnabledMessage": "您已啟用追蹤日誌,該功能僅供開發者使用。", + "DialogPerformanceCheckLoggingEnabledConfirmMessage": "為獲得最佳效能,建議停用追蹤日誌。您是否要立即停用追蹤日誌嗎?", + "DialogPerformanceCheckShaderDumpEnabledMessage": "您已啟用著色器傾印,該功能僅供開發者使用。", + "DialogPerformanceCheckShaderDumpEnabledConfirmMessage": "為獲得最佳效能,建議停用著色器傾印。您是否要立即停用著色器傾印嗎?", + "DialogLoadAppGameAlreadyLoadedMessage": "已載入此遊戲", + "DialogLoadAppGameAlreadyLoadedSubMessage": "請停止模擬或關閉模擬器,然後再啟動另一款遊戲。", + "DialogUpdateAddUpdateErrorMessage": "指定檔案不包含所選遊戲的更新!", + "DialogSettingsBackendThreadingWarningTitle": "警告 - 後端執行緒處理中", + "DialogSettingsBackendThreadingWarningMessage": "變更此選項後,必須重新啟動 Ryujinx 才能完全生效。使用 Ryujinx 的多執行緒功能時,可能需要手動停用驅動程式本身的多執行緒功能,這取決於您的平台。", + "DialogModManagerDeletionWarningMessage": "您將刪除模組: {0}\n\n您確定要繼續嗎?", + "DialogModManagerDeletionAllWarningMessage": "您即將刪除此遊戲的所有模組。\n\n您確定要繼續嗎?", + "SettingsTabGraphicsFeaturesOptions": "功能", + "SettingsTabGraphicsBackendMultithreading": "圖形後端多執行緒:", + "CommonAuto": "自動", + "CommonOff": "關閉", + "CommonOn": "開啟", + "InputDialogYes": "是", + "InputDialogNo": "否", + "DialogProfileInvalidProfileNameErrorMessage": "檔案名稱包含無效字元。請重試。", + "MenuBarOptionsPauseEmulation": "暫停", + "MenuBarOptionsResumeEmulation": "繼續", + "AboutUrlTooltipMessage": "在預設瀏覽器中開啟 Ryujinx 網站。", + "AboutDisclaimerMessage": "Ryujinx 和 Nintendo™\n或其任何合作夥伴完全沒有關聯。", + "AboutAmiiboDisclaimerMessage": "我們在 Amiibo 模擬中\n使用了 AmiiboAPI (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": "全部停用", + "ModManagerDeleteAllButton": "全部刪除", + "MenuBarOptionsChangeLanguage": "變更語言", + "MenuBarShowFileTypes": "顯示檔案類型", + "CommonSort": "排序", + "CommonShowNames": "顯示名稱", + "CommonFavorite": "我的最愛", + "OrderAscending": "從小到大", + "OrderDescending": "從大到小", + "SettingsTabGraphicsFeatures": "功能與改進", + "ErrorWindowTitle": "錯誤視窗", + "ToggleDiscordTooltip": "啟用或關閉 Discord 動態狀態展示", + "AddGameDirBoxTooltip": "輸入要新增到清單中的遊戲資料夾", + "AddGameDirTooltip": "新增遊戲資料夾到清單中", + "RemoveGameDirTooltip": "移除選取的遊戲資料夾", + "AddAutoloadDirBoxTooltip": "輸入要新增到清單中的「自動載入 DLC/遊戲更新資料夾」", + "AddAutoloadDirTooltip": "新增「自動載入 DLC/遊戲更新資料夾」到清單中", + "RemoveAutoloadDirTooltip": "移除選取的「自動載入 DLC/遊戲更新資料夾」", + "CustomThemeCheckTooltip": "為圖形使用者介面使用自訂 Avalonia 佈景主題,變更模擬器功能表的外觀", + "CustomThemePathTooltip": "自訂 GUI 佈景主題的路徑", + "CustomThemeBrowseTooltip": "瀏覽自訂 GUI 佈景主題", + "DockModeToggleTooltip": "底座模式可使模擬系統表現為底座的 Nintendo Switch。這可以提高大多數遊戲的圖形保真度。反之,停用該模式將使模擬系統表現為手提模式的 Nintendo Switch,從而降低圖形品質。\n\n如果計劃使用底座模式,請配置玩家 1 控制;如果計劃使用手提模式,請配置手提控制。\n\n如果不確定,請保持開啟狀態。", + "DirectKeyboardTooltip": "支援直接鍵盤存取 (HID)。遊戲可將鍵盤作為文字輸入裝置。\n\n僅適用於在 Switch 硬體上原生支援使用鍵盤的遊戲。\n\n如果不確定,請保持關閉狀態。", + "DirectMouseTooltip": "支援滑鼠直接存取 (HID)。遊戲可將滑鼠作為指向裝置使用。\n\n僅適用於在 Switch 硬體上原生支援滑鼠控制的遊戲,這類遊戲很少。\n\n啟用後,觸控螢幕功能可能無法使用。\n\n如果不確定,請保持關閉狀態。", + "RegionTooltip": "變更系統區域", + "LanguageTooltip": "變更系統語言", + "TimezoneTooltip": "變更系統時區", + "TimeTooltip": "變更系統時鐘", + "VSyncToggleTooltip": "模擬遊戲機的垂直同步。對大多數遊戲來說,它本質上是一個幀率限制器;停用它可能會導致遊戲以更高的速度執行,或使載入畫面耗時更長或卡住。\n\n可以在遊戲中使用快速鍵進行切換 (預設為 F1)。如果您打算停用,我們建議您這樣做。\n\n如果不確定,請保持開啟狀態。", + "PptcToggleTooltip": "儲存已轉譯的 JIT 函數,這樣每次載入遊戲時就無需再轉譯這些函數。\n\n減少遊戲首次啟動後的卡頓現象,並大大加快啟動時間。\n\n如果不確定,請保持開啟狀態。", + "LowPowerPptcToggleTooltip": "使用 CPU 核心數量的三分之一載入 PPTC。", + "FsIntegrityToggleTooltip": "在啟動遊戲時檢查損壞的檔案,如果檢測到損壞的檔案,則在日誌中顯示雜湊值錯誤。\n\n對效能沒有影響,旨在幫助排除故障。\n\n如果不確定,請保持開啟狀態。", + "AudioBackendTooltip": "變更用於繪製音訊的後端。\n\nSDL2 是首選,而 OpenAL 和 SoundIO 則作為備用。虛設 (Dummy) 將沒有聲音。\n\n如果不確定,請設定為 SDL2。", + "MemoryManagerTooltip": "變更客體記憶體的映射和存取方式。這會極大地影響模擬 CPU 效能。\n\n如果不確定,請設定為主體略過檢查模式。", + "MemoryManagerSoftwareTooltip": "使用軟體分頁表進行位址轉換。精度最高,但效能最差。", + "MemoryManagerHostTooltip": "直接映射主體位址空間中的記憶體。更快的 JIT 編譯和執行速度。", + "MemoryManagerUnsafeTooltip": "直接映射記憶體,但在存取前不封鎖客體位址空間內的位址。速度更快,但相對不安全。訪客應用程式可以從 Ryujinx 中的任何地方存取記憶體,因此只能使用該模式執行您信任的程式。", + "UseHypervisorTooltip": "使用 Hypervisor 取代 JIT。使用時可大幅提高效能,但在目前狀態下可能不穩定。", + "DRamTooltip": "利用另一種 MemoryMode 配置來模仿 Switch 開發模式。\n\n這僅對高解析度紋理套件或 4K 解析度模組有用。不會提高效能。\n\n如果不確定,請設定為 4GiB。", + "IgnoreMissingServicesTooltip": "忽略未實現的 Horizon OS 服務。這可能有助於在啟動某些遊戲時避免崩潰。\n\n如果不確定,請保持關閉狀態。", + "IgnoreAppletTooltip": "如果遊戲手把在遊戲過程中斷開連接,則外部對話方塊「控制器小程式」將不會出現。不會提示關閉對話方塊或設定新控制器。一旦先前斷開的控制器重新連接,遊戲將自動恢復。", + "GraphicsBackendThreadingTooltip": "在第二個執行緒上執行圖形後端指令。\n\n在本身不支援多執行緒的 GPU 驅動程式上,可加快著色器編譯、減少卡頓並提高效能。在支援多執行緒的驅動程式上效能略有提升。\n\n如果不確定,請設定為自動。", + "GalThreadingTooltip": "在第二個執行緒上執行圖形後端指令。\n\n在本身不支援多執行緒的 GPU 驅動程式上,可加快著色器編譯、減少卡頓並提高效能。在支援多執行緒的驅動程式上效能略有提升。\n\n如果不確定,請設定為自動。", + "ShaderCacheToggleTooltip": "儲存磁碟著色器快取,減少後續執行時的卡頓。\n\n如果不確定,請保持開啟狀態。", + "ResolutionScaleTooltip": "使用倍數提升遊戲的繪製解析度。\n\n少數遊戲可能無法使用此功能,即使提高解析度也會顯得像素化;對於這些遊戲,您可能需要找到去除反鋸齒或提高內部繪製解析度的模組。對於後者,您可能需要選擇原生。\n\n此選項可在遊戲執行時透過點選下方的「套用」進行變更;您只需將設定視窗移到一旁,然後進行試驗,直到找到您喜歡的遊戲效果。\n\n請記住,4 倍幾乎對任何設定都是過度的。", + "ResolutionScaleEntryTooltip": "浮點解析度刻度,如 1.5。非整數刻度更容易出現問題或崩潰。", + "AnisotropyTooltip": "各向異性過濾等級。設定為自動可使用遊戲要求的值。", + "AspectRatioTooltip": "套用於繪製器視窗的長寬比。\n\n只有在遊戲中使用長寬比模組時才可變更,否則圖形會被拉伸。\n\n如果不確定,請保持 16:9 狀態。", + "ShaderDumpPathTooltip": "圖形著色器傾印路徑", + "FileLogTooltip": "將控制台日誌儲存到磁碟上的日誌檔案中。不會影響效能。", + "StubLogTooltip": "在控制台中輸出日誌訊息。不會影響效能。", + "InfoLogTooltip": "在控制台中輸出資訊日誌訊息。不會影響效能。", + "WarnLogTooltip": "在控制台中輸出警告日誌訊息。不會影響效能。", + "ErrorLogTooltip": "在控制台中輸出錯誤日誌訊息。不會影響效能。", + "TraceLogTooltip": "在控制台中輸出追蹤日誌訊息。不會影響效能。", + "GuestLogTooltip": "在控制台中輸出客體日誌訊息。不會影響效能。", + "FileAccessLogTooltip": "在控制台中輸出檔案存取日誌訊息。", + "FSAccessLogModeTooltip": "啟用檔案系統存取日誌輸出到控制台中。可能的模式為 0 到 3", + "DeveloperOptionTooltip": "謹慎使用", + "OpenGlLogLevel": "需要啟用適當的日誌等級", + "DebugLogTooltip": "在控制台中輸出偵錯日誌訊息。\n\n只有在人員特別指示的情況下才能使用,因為這會導致日誌難以閱讀,並降低模擬器效能。", + "LoadApplicationFileTooltip": "開啟檔案總管,選擇與 Switch 相容的檔案來載入", + "LoadApplicationFolderTooltip": "開啟檔案總管,選擇與 Switch 相容且未封裝的應用程式來載入", + "LoadDlcFromFolderTooltip": "開啟檔案總管,選擇一個或多個資料夾來大量載入 DLC", + "LoadTitleUpdatesFromFolderTooltip": "開啟檔案總管,選擇一個或多個資料夾來大量載入遊戲更新", + "OpenRyujinxFolderTooltip": "開啟 Ryujinx 檔案系統資料夾", + "OpenRyujinxLogsTooltip": "開啟日誌被寫入的資料夾", + "ExitTooltip": "結束 Ryujinx", + "OpenSettingsTooltip": "開啟設定視窗", + "OpenProfileManagerTooltip": "開啟使用者設定檔管理員視窗", + "StopEmulationTooltip": "停止模擬目前遊戲,返回遊戲選擇介面", + "CheckUpdatesTooltip": "檢查 Ryujinx 的更新", + "OpenAboutTooltip": "開啟關於視窗", + "GridSize": "網格尺寸", + "GridSizeTooltip": "調整網格的大小", + "SettingsTabSystemSystemLanguageBrazilianPortuguese": "巴西葡萄牙文", + "AboutRyujinxContributorsButtonHeader": "查看所有貢獻者", + "SettingsTabSystemAudioVolume": "音量:", + "AudioVolumeTooltip": "調節音量", + "SettingsTabSystemEnableInternetAccess": "訪客網際網路存取/區域網路模式", + "EnableInternetAccessTooltip": "允許模擬應用程式連線網際網路。\n\n當啟用此功能且系統連線到同一接入點時,具有區域網路模式的遊戲可相互連線。這也包括真正的遊戲機。\n\n不允許連接 Nintendo 伺服器。可能會導致某些嘗試連線網際網路的遊戲崩潰。\n\n如果不確定,請保持關閉狀態。", + "GameListContextMenuManageCheatToolTip": "管理密技", + "GameListContextMenuManageCheat": "管理密技", + "GameListContextMenuManageModToolTip": "管理模組", + "GameListContextMenuManageMod": "管理模組", + "ControllerSettingsStickRange": "範圍:", + "DialogStopEmulationTitle": "Ryujinx - 停止模擬", + "DialogStopEmulationMessage": "您確定要停止模擬嗎?", + "SettingsTabCpu": "CPU", + "SettingsTabAudio": "音訊", + "SettingsTabNetwork": "網路", + "SettingsTabNetworkConnection": "網路連線", + "SettingsTabCpuCache": "CPU 快取", + "SettingsTabCpuMemory": "CPU 模式", + "DialogUpdaterFlatpakNotSupportedMessage": "請透過 Flathub 更新 Ryujinx。", + "UpdaterDisabledWarningTitle": "更新已停用!", + "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}", + "TitleBundledUpdateVersionLabel": "附帶: 版本 {0}", + "TitleBundledDlcLabel": "附帶:", + "TitleXCIStatusPartialLabel": "Partial", + "TitleXCIStatusTrimmableLabel": "Untrimmed", + "TitleXCIStatusUntrimmableLabel": "Trimmed", + "TitleXCIStatusFailedLabel": "(Failed)", + "TitleXCICanSaveLabel": "Save {0:n0} Mb", + "TitleXCISavingLabel": "Saved {0:n0} Mb", + "RyujinxInfo": "Ryujinx - 資訊", + "RyujinxConfirm": "Ryujinx - 確認", + "FileDialogAllTypes": "全部類型", + "Never": "從不", + "SwkbdMinCharacters": "長度必須至少為 {0} 個字元", + "SwkbdMinRangeCharacters": "長度必須為 {0} 到 {1} 個字元", + "SoftwareKeyboard": "軟體鍵盤", + "SoftwareKeyboardModeNumeric": "必須是 0 到 9 或「.」", + "SoftwareKeyboardModeAlphabet": "必須是「非中日韓字元」 (non CJK)", + "SoftwareKeyboardModeASCII": "必須是 ASCII 文字", + "ControllerAppletControllers": "支援的控制器:", + "ControllerAppletPlayers": "玩家:", + "ControllerAppletDescription": "您目前的配置無效。開啟設定並重新配置輸入。", + "ControllerAppletDocked": "已設定底座模式。手提控制應該停用。", + "UpdaterRenaming": "正在重新命名舊檔案...", + "UpdaterRenameFailed": "更新程式無法重新命名檔案: {0}", + "UpdaterAddingFiles": "正在加入新檔案...", + "UpdaterExtracting": "正在提取更新...", + "UpdaterDownloading": "正在下載更新...", + "Game": "遊戲", + "Docked": "底座模式", + "Handheld": "手提模式", + "ConnectionError": "連線錯誤。", + "AboutPageDeveloperListMore": "{0} 等人...", + "ApiError": "API 錯誤。", + "LoadingHeading": "正在載入 {0}", + "CompilingPPTC": "正在編譯 PTC", + "CompilingShaders": "正在編譯著色器", + "AllKeyboards": "所有鍵盤", + "OpenFileDialogTitle": "選取支援的檔案格式", + "OpenFolderDialogTitle": "選取未封裝遊戲的資料夾", + "AllSupportedFormats": "所有支援的格式", + "RyujinxUpdater": "Ryujinx 更新程式", + "SettingsTabHotkeys": "鍵盤快速鍵", + "SettingsTabHotkeysHotkeys": "鍵盤快捷鍵", + "SettingsTabHotkeysToggleVsyncHotkey": "切換垂直同步:", + "SettingsTabHotkeysScreenshotHotkey": "擷取畫面:", + "SettingsTabHotkeysShowUiHotkey": "顯示 UI:", + "SettingsTabHotkeysPauseHotkey": "暫停:", + "SettingsTabHotkeysToggleMuteHotkey": "靜音:", + "ControllerMotionTitle": "體感控制設定", + "ControllerRumbleTitle": "震動設定", + "SettingsSelectThemeFileDialogTitle": "選取佈景主題檔案", + "SettingsXamlThemeFile": "Xaml 佈景主題檔案", + "AvatarWindowTitle": "管理帳戶 - 大頭貼", + "Amiibo": "Amiibo", + "Unknown": "未知", + "Usage": "用途", + "Writable": "可寫入", + "SelectDlcDialogTitle": "選取 DLC 檔案", + "SelectUpdateDialogTitle": "選取更新檔", + "SelectModDialogTitle": "選取模組資料夾", + "TrimXCIFileDialogTitle": "Check and Trim XCI File", + "TrimXCIFileDialogPrimaryText": "This function will first check the empty space and then trim the XCI File to save disk space.", + "TrimXCIFileDialogSecondaryText": "Current File Size: {0:n} MB\nGame Data Size: {1:n} MB\nDisk Space Savings: {2:n} MB", + "TrimXCIFileNoTrimNecessary": "XCI File does not need to be trimmed. Check logs for further details", + "TrimXCIFileNoUntrimPossible": "XCI File cannot be untrimmed. Check logs for further details", + "TrimXCIFileReadOnlyFileCannotFix": "XCI File is Read Only and could not be made writable. Check logs for further details", + "TrimXCIFileFileSizeChanged": "XCI File has changed in size since it was scanned. Please check the file is not being written to and try again.", + "TrimXCIFileFreeSpaceCheckFailed": "XCI File has data in the free space area, it is not safe to trim", + "TrimXCIFileInvalidXCIFile": "XCI File contains invalid data. Check logs for further details", + "TrimXCIFileFileIOWriteError": "XCI File could not be opened for writing. Check logs for further details", + "TrimXCIFileFailedPrimaryText": "Trimming of the XCI file failed", + "TrimXCIFileCancelled": "The operation was cancelled", + "TrimXCIFileFileUndertermined": "No operation was performed", + "UserProfileWindowTitle": "使用者設定檔管理員", + "CheatWindowTitle": "密技管理員", + "DlcWindowTitle": "管理 {0} 的可下載內容 ({1})", + "ModWindowTitle": "管理 {0} 的模組 ({1})", + "UpdateWindowTitle": "遊戲更新管理員", + "XCITrimmerWindowTitle": "XCI File Trimmer", + "XCITrimmerTitleStatusCount": "{0} of {1} Title(s) Selected", + "XCITrimmerTitleStatusCountWithFilter": "{0} of {1} Title(s) Selected ({2} displayed)", + "XCITrimmerTitleStatusTrimming": "Trimming {0} Title(s)...", + "XCITrimmerTitleStatusUntrimming": "Untrimming {0} Title(s)...", + "XCITrimmerTitleStatusFailed": "Failed", + "XCITrimmerPotentialSavings": "Potential Savings", + "XCITrimmerActualSavings": "Actual Savings", + "XCITrimmerSavingsMb": "{0:n0} Mb", + "XCITrimmerSelectDisplayed": "Select Shown", + "XCITrimmerDeselectDisplayed": "Deselect Shown", + "XCITrimmerSortName": "Title", + "XCITrimmerSortSaved": "Space Savings", + "XCITrimmerTrim": "Trim", + "XCITrimmerUntrim": "Untrim", + "UpdateWindowUpdateAddedMessage": "已加入 {0} 個遊戲更新", + "UpdateWindowBundledContentNotice": "附帶的遊戲更新只能被停用而無法被刪除。", + "CheatWindowHeading": "可用於 {0} [{1}] 的密技", + "BuildId": "組建識別碼:", + "DlcWindowBundledContentNotice": "附帶的 DLC 只能被停用而無法被刪除。", + "DlcWindowHeading": "{0} 個可下載內容", + "DlcWindowDlcAddedMessage": "已加入 {0} 個 DLC", + "AutoloadDlcAddedMessage": "已加入 {0} 個 DLC", + "AutoloadDlcRemovedMessage": "已刪除 {0} 個遺失的 DLC", + "AutoloadUpdateAddedMessage": "已加入 {0} 個遊戲更新", + "AutoloadUpdateRemovedMessage": "已刪除 {0} 個遺失的遊戲更新", + "ModWindowHeading": "{0} 模組", + "UserProfilesEditProfile": "編輯所選", + "Continue": "Continue", + "Cancel": "取消", + "Save": "儲存", + "Discard": "放棄變更", + "Paused": "暫停", + "UserProfilesSetProfileImage": "設定設定檔圖像", + "UserProfileEmptyNameError": "名稱為必填", + "UserProfileNoImageError": "必須設定設定檔圖像", + "GameUpdateWindowHeading": "管理 {0} 的更新 ({1})", + "SettingsTabHotkeysResScaleUpHotkey": "提高解析度:", + "SettingsTabHotkeysResScaleDownHotkey": "降低解析度:", + "UserProfilesName": "名稱:", + "UserProfilesUserId": "使用者 ID:", + "SettingsTabGraphicsBackend": "圖形後端", + "SettingsTabGraphicsBackendTooltip": "選擇模擬器將使用的圖形後端。\n\n只要驅動程式是最新的,Vulkan 對所有現代顯示卡來說都更好用。Vulkan 還能在所有 GPU 廠商上實現更快的著色器編譯 (減少卡頓)。\n\nOpenGL 在舊式 Nvidia GPU、Linux 上的舊式 AMD GPU 或 VRAM 較低的 GPU 上可能會取得更好的效果,不過著色器編譯的卡頓會更嚴重。\n\n如果不確定,請設定為 Vulkan。如果您的 GPU 使用最新的圖形驅動程式也不支援 Vulkan,請設定為 OpenGL。", + "SettingsEnableTextureRecompression": "開啟材質重新壓縮", + "SettingsEnableTextureRecompressionTooltip": "壓縮 ASTC 紋理,以減少 VRAM 占用。\n\n使用這種紋理格式的遊戲包括 Astral Chain、Bayonetta 3、Fire Emblem Engage、Metroid Prime Remastered、Super Mario Bros. Wonder 和 The Legend of Zelda: Tears of the Kingdom。\n\n使用 4GB 或更低 VRAM 的顯示卡在執行這些遊戲時可能會崩潰。\n\n只有在上述遊戲的 VRAM 即將耗盡時才啟用。如果不確定,請保持關閉狀態。", + "SettingsTabGraphicsPreferredGpu": "優先選取的 GPU", + "SettingsTabGraphicsPreferredGpuTooltip": "選擇將與 Vulkan 圖形後端一起使用的顯示卡。\n\n不會影響 OpenGL 將使用的 GPU。\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} 的存檔 ({1})", + "SaveManagerTitle": "存檔管理員", + "Name": "名稱", + "Size": "大小", + "Search": "搜尋", + "UserProfilesRecoverLostAccounts": "復原遺失的帳戶", + "Recover": "復原", + "UserProfilesRecoverHeading": "發現下列帳戶有一些存檔", + "UserProfilesRecoverEmptyList": "無設定檔可復原", + "GraphicsAATooltip": "對遊戲繪製進行反鋸齒處理。\n\nFXAA 會模糊大部分圖像,而 SMAA 則會嘗試找出鋸齒邊緣並將其平滑化。\n\n不建議與 FSR 縮放濾鏡一起使用。\n\n此選項可在遊戲執行時透過點選下方的「套用」進行變更;您只需將設定視窗移到一旁,然後進行試驗,直到找到您喜歡的遊戲效果。\n\n如果不確定,請選擇無狀態。", + "GraphicsAALabel": "反鋸齒:", + "GraphicsScalingFilterLabel": "縮放過濾器:", + "GraphicsScalingFilterTooltip": "選擇使用解析度縮放時套用的縮放過濾器。\n\n雙線性 (Bilinear) 濾鏡適用於 3D 遊戲,是一個安全的預設選項。\n\n建議像素美術遊戲使用近鄰性 (Nearest) 濾鏡。\n\nFSR 1.0 只是一個銳化濾鏡,不建議與 FXAA 或 SMAA 一起使用。\n\n此選項可在遊戲執行時透過點選下方的「套用」進行變更;您只需將設定視窗移到一旁,然後進行試驗,直到找到您喜歡的遊戲效果。\n\n如果不確定,請保持雙線性 (Bilinear) 狀態。", + "GraphicsScalingFilterBilinear": "雙線性 (Bilinear)", + "GraphicsScalingFilterNearest": "近鄰性 (Nearest)", + "GraphicsScalingFilterFsr": "FSR", + "GraphicsScalingFilterArea": "Area", + "GraphicsScalingFilterLevelLabel": "日誌等級", + "GraphicsScalingFilterLevelTooltip": "設定 FSR 1.0 銳化等級。越高越清晰。", + "SmaaLow": "低階 SMAA", + "SmaaMedium": "中階 SMAA", + "SmaaHigh": "高階 SMAA", + "SmaaUltra": "超高階 SMAA", + "UserEditorTitle": "編輯使用者", + "UserEditorTitleCreate": "建立使用者", + "SettingsTabNetworkInterface": "網路介面:", + "NetworkInterfaceTooltip": "用於 LAN/LDN 功能的網路介面。\n\n與 VPN 或 XLink Kai 以及支援區域網路的遊戲配合使用,可用於在網路上偽造同網際網路連線。\n\n如果不確定,請保持預設狀態。", + "NetworkInterfaceDefault": "預設", + "PackagingShaders": "封裝著色器", + "AboutChangelogButton": "在 GitHub 上檢視更新日誌", + "AboutChangelogButtonTooltipMessage": "在預設瀏覽器中開啟此版本的更新日誌。", + "SettingsTabNetworkMultiplayer": "多人遊戲", + "MultiplayerMode": "模式:", + "MultiplayerModeTooltip": "變更 LDN 多人遊戲模式。\n\nLdnMitm 將修改遊戲中的本機無線/本機遊戲功能,使其如同區域網路一樣執行,允許與其他安裝了 ldn_mitm 模組的 Ryujinx 實例和已破解的 Nintendo Switch 遊戲機進行本機同網路連線。\n\n多人遊戲要求所有玩家使用相同的遊戲版本 (例如,Super Smash Bros. Ultimate v13.0.1 無法連接 v13.0.0)。\n\n如果不確定,請保持 Disabled (停用) 狀態。", + "MultiplayerModeDisabled": "已停用", + "MultiplayerModeLdnMitm": "ldn_mitm", + "MultiplayerModeLdnRyu": "RyuLDN", + "MultiplayerDisableP2P": "Disable P2P Network Hosting (may increase latency)", + "MultiplayerDisableP2PTooltip": "Disable P2P network hosting, peers will proxy through the master server instead of connecting to you directly.", + "LdnPassphrase": "Network Passphrase:", + "LdnPassphraseTooltip": "You will only be able to see hosted games with the same passphrase as you.", + "LdnPassphraseInputTooltip": "Enter a passphrase in the format Ryujinx-<8 hex chars>. You will only be able to see hosted games with the same passphrase as you.", + "LdnPassphraseInputPublic": "(public)", + "GenLdnPass": "Generate Random", + "GenLdnPassTooltip": "Generates a new passphrase, which can be shared with other players.", + "ClearLdnPass": "Clear", + "ClearLdnPassTooltip": "Clears the current passphrase, returning to the public network.", + "InvalidLdnPassphrase": "Invalid Passphrase! Must be in the format \"Ryujinx-<8 hex chars>\"" +} diff --git a/src/Ryujinx/Assets/Styles/Styles.xaml b/src/Ryujinx/Assets/Styles/Styles.xaml new file mode 100644 index 000000000..878b5e7f1 --- /dev/null +++ b/src/Ryujinx/Assets/Styles/Styles.xaml @@ -0,0 +1,408 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Ryujinx/UI/Applet/ControllerAppletDialog.axaml.cs b/src/Ryujinx/UI/Applet/ControllerAppletDialog.axaml.cs new file mode 100644 index 000000000..ee0e884d2 --- /dev/null +++ b/src/Ryujinx/UI/Applet/ControllerAppletDialog.axaml.cs @@ -0,0 +1,132 @@ +using Avalonia.Controls; +using Avalonia.Styling; +using Avalonia.Svg.Skia; +using Avalonia.Threading; +using FluentAvalonia.UI.Controls; +using Ryujinx.Ava.Common.Locale; +using Ryujinx.Ava.UI.Helpers; +using Ryujinx.Ava.UI.Windows; +using Ryujinx.Common; +using Ryujinx.HLE.HOS.Applets; +using Ryujinx.HLE.HOS.Services.Hid; +using System.Linq; +using System.Threading.Tasks; + +namespace Ryujinx.Ava.UI.Applet +{ + internal partial class ControllerAppletDialog : UserControl + { + private const string ProControllerResource = "Ryujinx/Assets/Icons/Controller_ProCon.svg"; + private const string JoyConPairResource = "Ryujinx/Assets/Icons/Controller_JoyConPair.svg"; + private const string JoyConLeftResource = "Ryujinx/Assets/Icons/Controller_JoyConLeft.svg"; + private const string JoyConRightResource = "Ryujinx/Assets/Icons/Controller_JoyConRight.svg"; + + public static SvgImage ProControllerImage => GetResource(ProControllerResource); + public static SvgImage JoyconPairImage => GetResource(JoyConPairResource); + public static SvgImage JoyconLeftImage => GetResource(JoyConLeftResource); + public static SvgImage JoyconRightImage => GetResource(JoyConRightResource); + + public string PlayerCount { get; set; } = string.Empty; + public bool SupportsProController { get; set; } + public bool SupportsLeftJoycon { get; set; } + public bool SupportsRightJoycon { get; set; } + public bool SupportsJoyconPair { get; set; } + public bool IsDocked { get; set; } + + private readonly MainWindow _mainWindow; + + public ControllerAppletDialog(MainWindow mainWindow, ControllerAppletUIArgs args) + { + PlayerCount = args.PlayerCountMin == args.PlayerCountMax + ? args.PlayerCountMin.ToString() + : $"{args.PlayerCountMin} - {args.PlayerCountMax}"; + + SupportsProController = (args.SupportedStyles & ControllerType.ProController) != 0; + SupportsLeftJoycon = (args.SupportedStyles & ControllerType.JoyconLeft) != 0; + SupportsRightJoycon = (args.SupportedStyles & ControllerType.JoyconRight) != 0; + SupportsJoyconPair = (args.SupportedStyles & ControllerType.JoyconPair) != 0; + + IsDocked = args.IsDocked; + + _mainWindow = mainWindow; + + DataContext = this; + + InitializeComponent(); + } + + public ControllerAppletDialog(MainWindow mainWindow) + { + _mainWindow = mainWindow; + DataContext = this; + + InitializeComponent(); + } + + public static async Task ShowControllerAppletDialog(MainWindow window, ControllerAppletUIArgs args) + { + ContentDialog contentDialog = new(); + UserResult result = UserResult.Cancel; + ControllerAppletDialog content = new(window, args); + + contentDialog.Title = LocaleManager.Instance[LocaleKeys.DialogControllerAppletTitle]; + contentDialog.Content = content; + + void Handler(ContentDialog sender, ContentDialogClosedEventArgs eventArgs) + { + if (eventArgs.Result == ContentDialogResult.Primary) + { + result = UserResult.Ok; + } + } + + contentDialog.Closed += Handler; + + Style bottomBorder = new(x => x.OfType().Name("DialogSpace").Child().OfType()); + bottomBorder.Setters.Add(new Setter(IsVisibleProperty, false)); + + contentDialog.Styles.Add(bottomBorder); + + await ContentDialogHelper.ShowAsync(contentDialog); + + return result; + } + + private static SvgImage GetResource(string path) + { + SvgImage image = new(); + + if (!string.IsNullOrWhiteSpace(path)) + { + SvgSource source = SvgSource.LoadFromStream(EmbeddedResources.GetStream(path)); + + image.Source = source; + } + + return image; + } + + public void OpenSettingsWindow() + { + if (_mainWindow.SettingsWindow == null) + { + Dispatcher.UIThread.InvokeAsync(async () => + { + _mainWindow.SettingsWindow = new SettingsWindow(_mainWindow.VirtualFileSystem, _mainWindow.ContentManager); + _mainWindow.SettingsWindow.NavPanel.Content = _mainWindow.SettingsWindow.InputPage; + _mainWindow.SettingsWindow.NavPanel.SelectedItem = _mainWindow.SettingsWindow.NavPanel.MenuItems.ElementAt(1); + + await ContentDialogHelper.ShowWindowAsync(_mainWindow.SettingsWindow, _mainWindow); + _mainWindow.SettingsWindow = null; + this.Close(); + }); + } + } + + public void Close() + { + ((ContentDialog)Parent)?.Hide(); + } + } +} + diff --git a/src/Ryujinx/UI/Applet/ErrorAppletWindow.axaml b/src/Ryujinx/UI/Applet/ErrorAppletWindow.axaml new file mode 100644 index 000000000..c7aa56fb8 --- /dev/null +++ b/src/Ryujinx/UI/Applet/ErrorAppletWindow.axaml @@ -0,0 +1,45 @@ + + + + + + + diff --git a/src/Ryujinx/UI/Applet/ErrorAppletWindow.axaml.cs b/src/Ryujinx/UI/Applet/ErrorAppletWindow.axaml.cs new file mode 100644 index 000000000..552aaaa19 --- /dev/null +++ b/src/Ryujinx/UI/Applet/ErrorAppletWindow.axaml.cs @@ -0,0 +1,74 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Threading; +using Ryujinx.Ava.Common.Locale; +using Ryujinx.Ava.UI.Windows; +using System.Threading.Tasks; + +namespace Ryujinx.Ava.UI.Applet +{ + internal partial class ErrorAppletWindow : StyleableAppWindow + { + private readonly Window _owner; + private object _buttonResponse; + + public ErrorAppletWindow(Window owner, string[] buttons, string message) + { + _owner = owner; + Message = message; + DataContext = this; + InitializeComponent(); + + int responseId = 0; + + if (buttons != null) + { + foreach (string buttonText in buttons) + { + AddButton(buttonText, responseId); + responseId++; + } + } + else + { + AddButton(LocaleManager.Instance[LocaleKeys.InputDialogOk], 0); + } + } + + public ErrorAppletWindow() + { + DataContext = this; + InitializeComponent(); + } + + public string Message { get; set; } + + private void AddButton(string label, object tag) + { + Dispatcher.UIThread.InvokeAsync(() => + { + Button button = new() { Content = label, Tag = tag }; + + button.Click += Button_Click; + ButtonStack.Children.Add(button); + }); + } + + private void Button_Click(object sender, RoutedEventArgs e) + { + if (sender is Button button) + { + _buttonResponse = button.Tag; + } + + Close(); + } + + public async Task Run() + { + await ShowDialog(_owner); + + return _buttonResponse; + } + } +} diff --git a/src/Ryujinx/UI/Applet/SwkbdAppletDialog.axaml b/src/Ryujinx/UI/Applet/SwkbdAppletDialog.axaml new file mode 100644 index 000000000..0b7304936 --- /dev/null +++ b/src/Ryujinx/UI/Applet/SwkbdAppletDialog.axaml @@ -0,0 +1,56 @@ + + + + + + + + + diff --git a/src/Ryujinx/UI/Applet/SwkbdAppletDialog.axaml.cs b/src/Ryujinx/UI/Applet/SwkbdAppletDialog.axaml.cs new file mode 100644 index 000000000..75a9b3d41 --- /dev/null +++ b/src/Ryujinx/UI/Applet/SwkbdAppletDialog.axaml.cs @@ -0,0 +1,188 @@ +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Media; +using FluentAvalonia.UI.Controls; +using Ryujinx.Ava.Common.Locale; +using Ryujinx.Ava.UI.Helpers; +using Ryujinx.HLE.HOS.Applets; +using Ryujinx.HLE.HOS.Applets.SoftwareKeyboard; +using System; +using System.Linq; +using System.Threading.Tasks; + +namespace Ryujinx.Ava.UI.Controls +{ + internal partial class SwkbdAppletDialog : UserControl + { + private Predicate _checkLength = _ => true; + private Predicate _checkInput = _ => true; + private int _inputMax; + private int _inputMin; + private readonly string _placeholder; + + private ContentDialog _host; + + public SwkbdAppletDialog(string mainText, string secondaryText, string placeholder, string message) + { + MainText = mainText; + SecondaryText = secondaryText; + Message = message ?? string.Empty; + DataContext = this; + _placeholder = placeholder; + InitializeComponent(); + + Input.Watermark = _placeholder; + + if (string.IsNullOrWhiteSpace(Input.Watermark)) + { + Input.UseFloatingWatermark = false; + } + + Input.AddHandler(TextInputEvent, Message_TextInput, RoutingStrategies.Tunnel, true); + } + + public SwkbdAppletDialog() + { + DataContext = this; + InitializeComponent(); + } + + protected override void OnGotFocus(GotFocusEventArgs e) + { + // FIXME: This does not work. Might be a bug in Avalonia with DialogHost + // Currently focus will be redirected to the overlay window instead. + Input.Focus(); + } + + public string Message { get; set; } = string.Empty; + public string MainText { get; set; } = string.Empty; + public string SecondaryText { get; set; } = string.Empty; + + public static async Task<(UserResult Result, string Input)> ShowInputDialog(string title, SoftwareKeyboardUIArgs args) + { + ContentDialog contentDialog = new(); + + UserResult result = UserResult.Cancel; + + SwkbdAppletDialog content = new(args.HeaderText, args.SubtitleText, args.GuideText, args.InitialText); + + string input = string.Empty; + + content.SetInputLengthValidation(args.StringLengthMin, args.StringLengthMax); + content.SetInputValidation(args.KeyboardMode); + + content._host = contentDialog; + contentDialog.Title = title; + contentDialog.PrimaryButtonText = args.SubmitText; + contentDialog.IsPrimaryButtonEnabled = content._checkLength(content.Message.Length); + contentDialog.SecondaryButtonText = string.Empty; + contentDialog.CloseButtonText = LocaleManager.Instance[LocaleKeys.InputDialogCancel]; + contentDialog.Content = content; + + void Handler(ContentDialog sender, ContentDialogClosedEventArgs eventArgs) + { + if (eventArgs.Result == ContentDialogResult.Primary) + { + result = UserResult.Ok; + input = content.Input.Text; + } + } + + contentDialog.Closed += Handler; + + await ContentDialogHelper.ShowAsync(contentDialog); + + return (result, input); + } + + private void ApplyValidationInfo(string text) + { + Error.IsVisible = !string.IsNullOrEmpty(text); + Error.Text = text; + } + + public void SetInputLengthValidation(int min, int max) + { + _inputMin = Math.Min(min, max); + _inputMax = Math.Max(min, max); + + Error.IsVisible = false; + Error.FontStyle = FontStyle.Italic; + + string validationInfoText = string.Empty; + + if (_inputMin <= 0 && _inputMax == int.MaxValue) // Disable. + { + Error.IsVisible = false; + + _checkLength = _ => true; + } + else if (_inputMin > 0 && _inputMax == int.MaxValue) + { + validationInfoText = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.SwkbdMinCharacters, _inputMin); + + _checkLength = length => _inputMin <= length; + } + else + { + validationInfoText = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.SwkbdMinRangeCharacters, _inputMin, _inputMax); + + _checkLength = length => _inputMin <= length && length <= _inputMax; + } + + ApplyValidationInfo(validationInfoText); + Message_TextInput(this, new TextInputEventArgs()); + } + + private void SetInputValidation(KeyboardMode mode) + { + string validationInfoText = Error.Text; + string localeText; + switch (mode) + { + case KeyboardMode.Numeric: + localeText = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.SoftwareKeyboardModeNumeric); + validationInfoText = string.IsNullOrEmpty(validationInfoText) ? localeText : string.Join("\n", validationInfoText, localeText); + _checkInput = text => text.All(NumericCharacterValidation.IsNumeric); + break; + case KeyboardMode.Alphabet: + localeText = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.SoftwareKeyboardModeAlphabet); + validationInfoText = string.IsNullOrEmpty(validationInfoText) ? localeText : string.Join("\n", validationInfoText, localeText); + _checkInput = text => text.All(value => !CJKCharacterValidation.IsCJK(value)); + break; + case KeyboardMode.ASCII: + localeText = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.SoftwareKeyboardModeASCII); + validationInfoText = string.IsNullOrEmpty(validationInfoText) ? localeText : string.Join("\n", validationInfoText, localeText); + _checkInput = text => text.All(char.IsAscii); + break; + default: + _checkInput = _ => true; + break; + } + + ApplyValidationInfo(validationInfoText); + Message_TextInput(this, new TextInputEventArgs()); + } + + private void Message_TextInput(object sender, TextInputEventArgs e) + { + if (_host != null) + { + _host.IsPrimaryButtonEnabled = _checkLength(Message.Length) && _checkInput(Message); + } + } + + private void Message_KeyUp(object sender, KeyEventArgs e) + { + if (e.Key == Key.Enter && _host.IsPrimaryButtonEnabled) + { + _host.Hide(ContentDialogResult.Primary); + } + else + { + _host.IsPrimaryButtonEnabled = _checkLength(Message.Length) && _checkInput(Message); + } + } + } +} diff --git a/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml b/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml new file mode 100644 index 000000000..951f7f616 --- /dev/null +++ b/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml @@ -0,0 +1,115 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml.cs b/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml.cs new file mode 100644 index 000000000..74fb41efa --- /dev/null +++ b/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml.cs @@ -0,0 +1,340 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Markup.Xaml; +using Avalonia.Platform.Storage; +using Avalonia.Threading; +using LibHac.Fs; +using LibHac.Tools.FsSystem.NcaUtils; +using Ryujinx.Ava.Common; +using Ryujinx.Ava.Common.Locale; +using Ryujinx.Ava.UI.Helpers; +using Ryujinx.Ava.UI.ViewModels; +using Ryujinx.Ava.UI.Windows; +using Ryujinx.Common.Configuration; +using Ryujinx.HLE.HOS; +using Ryujinx.UI.App.Common; +using Ryujinx.UI.Common.Helper; +using SkiaSharp; +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Path = System.IO.Path; + +namespace Ryujinx.Ava.UI.Controls +{ + public class ApplicationContextMenu : MenuFlyout + { + public ApplicationContextMenu() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + + public void ToggleFavorite_Click(object sender, RoutedEventArgs args) + { + if (sender is not MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel }) + return; + + viewModel.SelectedApplication.Favorite = !viewModel.SelectedApplication.Favorite; + + ApplicationLibrary.LoadAndSaveMetaData(viewModel.SelectedApplication.IdString, appMetadata => + { + appMetadata.Favorite = viewModel.SelectedApplication.Favorite; + }); + + viewModel.RefreshView(); + } + + public void OpenUserSaveDirectory_Click(object sender, RoutedEventArgs args) + { + if (sender is MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel }) + OpenSaveDirectory(viewModel, SaveDataType.Account, new UserId((ulong)viewModel.AccountManager.LastOpenedUser.UserId.High, (ulong)viewModel.AccountManager.LastOpenedUser.UserId.Low)); + } + + public void OpenDeviceSaveDirectory_Click(object sender, RoutedEventArgs args) + { + if (sender is MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel }) + OpenSaveDirectory(viewModel, SaveDataType.Device, default); + } + + public void OpenBcatSaveDirectory_Click(object sender, RoutedEventArgs args) + { + if (sender is MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel }) + OpenSaveDirectory(viewModel, SaveDataType.Bcat, default); + } + + private static void OpenSaveDirectory(MainWindowViewModel viewModel, SaveDataType saveDataType, UserId userId) + { + var saveDataFilter = SaveDataFilter.Make(viewModel.SelectedApplication.Id, saveDataType, userId, saveDataId: default, index: default); + + ApplicationHelper.OpenSaveDir(in saveDataFilter, viewModel.SelectedApplication.Id, viewModel.SelectedApplication.ControlHolder, viewModel.SelectedApplication.Name); + } + + public async void OpenTitleUpdateManager_Click(object sender, RoutedEventArgs args) + { + if (sender is MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel }) + await TitleUpdateWindow.Show(viewModel.ApplicationLibrary, viewModel.SelectedApplication); + } + + public async void OpenDownloadableContentManager_Click(object sender, RoutedEventArgs args) + { + if (sender is MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel }) + await DownloadableContentManagerWindow.Show(viewModel.ApplicationLibrary, viewModel.SelectedApplication); + } + + public async void OpenCheatManager_Click(object sender, RoutedEventArgs args) + { + if (sender is MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel }) + await new CheatWindow( + viewModel.VirtualFileSystem, + viewModel.SelectedApplication.IdString, + viewModel.SelectedApplication.Name, + viewModel.SelectedApplication.Path).ShowDialog((Window)viewModel.TopLevel); + } + + public void OpenModsDirectory_Click(object sender, RoutedEventArgs args) + { + if (sender is not MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel }) + return; + + string modsBasePath = ModLoader.GetModsBasePath(); + string titleModsPath = ModLoader.GetApplicationDir(modsBasePath, viewModel.SelectedApplication.IdString); + + OpenHelper.OpenFolder(titleModsPath); + } + + public void OpenSdModsDirectory_Click(object sender, RoutedEventArgs args) + { + if (sender is not MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel }) + return; + + string sdModsBasePath = ModLoader.GetSdModsBasePath(); + string titleModsPath = ModLoader.GetApplicationDir(sdModsBasePath, viewModel.SelectedApplication.IdString); + + OpenHelper.OpenFolder(titleModsPath); + } + + public async void OpenModManager_Click(object sender, RoutedEventArgs args) + { + if (sender is MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel }) + await ModManagerWindow.Show(viewModel.SelectedApplication.Id, viewModel.SelectedApplication.Name); + } + + public async void PurgePtcCache_Click(object sender, RoutedEventArgs args) + { + if (sender is not MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel }) + return; + + UserResult result = await ContentDialogHelper.CreateLocalizedConfirmationDialog( + LocaleManager.Instance[LocaleKeys.DialogWarning], + LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogPPTCDeletionMessage, viewModel.SelectedApplication.Name) + ); + + if (result == UserResult.Yes) + { + DirectoryInfo mainDir = new(Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.IdString, "cache", "cpu", "0")); + DirectoryInfo backupDir = new(Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.IdString, "cache", "cpu", "1")); + + List cacheFiles = new(); + + if (mainDir.Exists) + { + cacheFiles.AddRange(mainDir.EnumerateFiles("*.cache")); + } + + if (backupDir.Exists) + { + cacheFiles.AddRange(backupDir.EnumerateFiles("*.cache")); + } + + if (cacheFiles.Count > 0) + { + foreach (FileInfo file in cacheFiles) + { + try + { + file.Delete(); + } + catch (Exception ex) + { + await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogPPTCDeletionErrorMessage, file.Name, ex)); + } + } + } + } + } + + public async void PurgeShaderCache_Click(object sender, RoutedEventArgs args) + { + if (sender is not MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel }) + return; + + UserResult result = await ContentDialogHelper.CreateLocalizedConfirmationDialog( + LocaleManager.Instance[LocaleKeys.DialogWarning], + LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogShaderDeletionMessage, viewModel.SelectedApplication.Name) + ); + + if (result == UserResult.Yes) + { + DirectoryInfo shaderCacheDir = new(Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.IdString, "cache", "shader")); + + List oldCacheDirectories = new(); + List newCacheFiles = new(); + + if (shaderCacheDir.Exists) + { + oldCacheDirectories.AddRange(shaderCacheDir.EnumerateDirectories("*")); + newCacheFiles.AddRange(shaderCacheDir.GetFiles("*.toc")); + newCacheFiles.AddRange(shaderCacheDir.GetFiles("*.data")); + } + + if ((oldCacheDirectories.Count > 0 || newCacheFiles.Count > 0)) + { + foreach (DirectoryInfo directory in oldCacheDirectories) + { + try + { + directory.Delete(true); + } + catch (Exception ex) + { + await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogPPTCDeletionErrorMessage, directory.Name, ex)); + } + } + + foreach (FileInfo file in newCacheFiles) + { + try + { + file.Delete(); + } + catch (Exception ex) + { + await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.ShaderCachePurgeError, file.Name, ex)); + } + } + } + } + } + + public void OpenPtcDirectory_Click(object sender, RoutedEventArgs args) + { + if (sender is not MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel }) + return; + + string ptcDir = Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.IdString, "cache", "cpu"); + string mainDir = Path.Combine(ptcDir, "0"); + string backupDir = Path.Combine(ptcDir, "1"); + + if (!Directory.Exists(ptcDir)) + { + Directory.CreateDirectory(ptcDir); + Directory.CreateDirectory(mainDir); + Directory.CreateDirectory(backupDir); + } + + OpenHelper.OpenFolder(ptcDir); + } + + public void OpenShaderCacheDirectory_Click(object sender, RoutedEventArgs args) + { + if (sender is MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel }) + { + string shaderCacheDir = Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.IdString, "cache", "shader"); + + if (!Directory.Exists(shaderCacheDir)) + { + Directory.CreateDirectory(shaderCacheDir); + } + + OpenHelper.OpenFolder(shaderCacheDir); + } + } + + public async void ExtractApplicationExeFs_Click(object sender, RoutedEventArgs args) + { + if (sender is MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel }) + { + await ApplicationHelper.ExtractSection( + viewModel.StorageProvider, + NcaSectionType.Code, + viewModel.SelectedApplication.Path, + viewModel.SelectedApplication.Name); + } + } + + public async void ExtractApplicationRomFs_Click(object sender, RoutedEventArgs args) + { + if (sender is MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel }) + await ApplicationHelper.ExtractSection( + viewModel.StorageProvider, + NcaSectionType.Data, + viewModel.SelectedApplication.Path, + viewModel.SelectedApplication.Name); + } + + public async void ExtractApplicationLogo_Click(object sender, RoutedEventArgs args) + { + if (sender is not MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel }) + return; + + var result = await viewModel.StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions + { + Title = LocaleManager.Instance[LocaleKeys.FolderDialogExtractTitle], + AllowMultiple = false, + }); + + if (result.Count == 0) + return; + + ApplicationHelper.ExtractSection( + result[0].Path.LocalPath, + NcaSectionType.Logo, + viewModel.SelectedApplication.Path, + viewModel.SelectedApplication.Name); + + var iconFile = await result[0].CreateFileAsync($"{viewModel.SelectedApplication.IdString}.png"); + await using var fileStream = await iconFile.OpenWriteAsync(); + + using var bitmap = SKBitmap.Decode(viewModel.SelectedApplication.Icon) + .Resize(new SKSizeI(512, 512), SKFilterQuality.High); + + using var png = bitmap.Encode(SKEncodedImageFormat.Png, 100); + + png.SaveTo(fileStream); + } + + public void CreateApplicationShortcut_Click(object sender, RoutedEventArgs args) + { + if (sender is MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel }) + ShortcutHelper.CreateAppShortcut( + viewModel.SelectedApplication.Path, + viewModel.SelectedApplication.Name, + viewModel.SelectedApplication.IdString, + viewModel.SelectedApplication.Icon + ); + } + + public async void RunApplication_Click(object sender, RoutedEventArgs args) + { + if (sender is MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel }) + await viewModel.LoadApplication(viewModel.SelectedApplication); + } + + public async void TrimXCI_Click(object sender, RoutedEventArgs args) + { + var viewModel = (sender as MenuItem)?.DataContext as MainWindowViewModel; + + if (viewModel?.SelectedApplication != null) + { + await viewModel.TrimXCIFile(viewModel.SelectedApplication.Path); + } + } + } +} diff --git a/src/Ryujinx/UI/Controls/ApplicationGridView.axaml b/src/Ryujinx/UI/Controls/ApplicationGridView.axaml new file mode 100644 index 000000000..98a1c004b --- /dev/null +++ b/src/Ryujinx/UI/Controls/ApplicationGridView.axaml @@ -0,0 +1,102 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Ryujinx/UI/Controls/ApplicationGridView.axaml.cs b/src/Ryujinx/UI/Controls/ApplicationGridView.axaml.cs new file mode 100644 index 000000000..25a34b423 --- /dev/null +++ b/src/Ryujinx/UI/Controls/ApplicationGridView.axaml.cs @@ -0,0 +1,36 @@ +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Interactivity; +using Ryujinx.Ava.UI.Helpers; +using Ryujinx.Ava.UI.ViewModels; +using Ryujinx.UI.App.Common; +using System; + +namespace Ryujinx.Ava.UI.Controls +{ + public partial class ApplicationGridView : UserControl + { + public static readonly RoutedEvent ApplicationOpenedEvent = + RoutedEvent.Register(nameof(ApplicationOpened), RoutingStrategies.Bubble); + + public event EventHandler ApplicationOpened + { + add => AddHandler(ApplicationOpenedEvent, value); + remove => RemoveHandler(ApplicationOpenedEvent, value); + } + + public ApplicationGridView() => InitializeComponent(); + + public void GameList_DoubleTapped(object sender, TappedEventArgs args) + { + if (sender is ListBox { SelectedItem: ApplicationData selected }) + RaiseEvent(new ApplicationOpenedEventArgs(selected, ApplicationOpenedEvent)); + } + + public void GameList_SelectionChanged(object sender, SelectionChangedEventArgs args) + { + if (DataContext is MainWindowViewModel viewModel && sender is ListBox { SelectedItem: ApplicationData selected }) + viewModel.GridSelectedApplication = selected; + } + } +} diff --git a/src/Ryujinx/UI/Controls/ApplicationListView.axaml b/src/Ryujinx/UI/Controls/ApplicationListView.axaml new file mode 100644 index 000000000..0daa77ac4 --- /dev/null +++ b/src/Ryujinx/UI/Controls/ApplicationListView.axaml @@ -0,0 +1,159 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Ryujinx/UI/Controls/ApplicationListView.axaml.cs b/src/Ryujinx/UI/Controls/ApplicationListView.axaml.cs new file mode 100644 index 000000000..5b4a6b8d4 --- /dev/null +++ b/src/Ryujinx/UI/Controls/ApplicationListView.axaml.cs @@ -0,0 +1,36 @@ +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Interactivity; +using Ryujinx.Ava.UI.Helpers; +using Ryujinx.Ava.UI.ViewModels; +using Ryujinx.UI.App.Common; +using System; + +namespace Ryujinx.Ava.UI.Controls +{ + public partial class ApplicationListView : UserControl + { + public static readonly RoutedEvent ApplicationOpenedEvent = + RoutedEvent.Register(nameof(ApplicationOpened), RoutingStrategies.Bubble); + + public event EventHandler ApplicationOpened + { + add => AddHandler(ApplicationOpenedEvent, value); + remove => RemoveHandler(ApplicationOpenedEvent, value); + } + + public ApplicationListView() => InitializeComponent(); + + public void GameList_DoubleTapped(object sender, TappedEventArgs args) + { + if (sender is ListBox { SelectedItem: ApplicationData selected }) + RaiseEvent(new ApplicationOpenedEventArgs(selected, ApplicationOpenedEvent)); + } + + public void GameList_SelectionChanged(object sender, SelectionChangedEventArgs args) + { + if (DataContext is MainWindowViewModel viewModel && sender is ListBox { SelectedItem: ApplicationData selected }) + viewModel.ListSelectedApplication = selected; + } + } +} diff --git a/src/Ryujinx/UI/Controls/NavigationDialogHost.axaml b/src/Ryujinx/UI/Controls/NavigationDialogHost.axaml new file mode 100644 index 000000000..bf34b303a --- /dev/null +++ b/src/Ryujinx/UI/Controls/NavigationDialogHost.axaml @@ -0,0 +1,17 @@ + + + + \ No newline at end of file diff --git a/src/Ryujinx/UI/Controls/NavigationDialogHost.axaml.cs b/src/Ryujinx/UI/Controls/NavigationDialogHost.axaml.cs new file mode 100644 index 000000000..c27a3ac9b --- /dev/null +++ b/src/Ryujinx/UI/Controls/NavigationDialogHost.axaml.cs @@ -0,0 +1,204 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Styling; +using Avalonia.Threading; +using FluentAvalonia.UI.Controls; +using Gommon; +using LibHac; +using LibHac.Common; +using LibHac.Fs; +using LibHac.Fs.Shim; +using Ryujinx.Ava.Common.Locale; +using Ryujinx.Ava.UI.Helpers; +using Ryujinx.Ava.UI.ViewModels; +using Ryujinx.Ava.UI.Views.User; +using Ryujinx.HLE.FileSystem; +using Ryujinx.HLE.HOS.Services.Account.Acc; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using UserId = Ryujinx.HLE.HOS.Services.Account.Acc.UserId; +using UserProfile = Ryujinx.Ava.UI.Models.UserProfile; + +namespace Ryujinx.Ava.UI.Controls +{ + public partial class NavigationDialogHost : UserControl + { + public AccountManager AccountManager { get; } + public ContentManager ContentManager { get; } + public VirtualFileSystem VirtualFileSystem { get; } + public HorizonClient HorizonClient { get; } + public UserProfileViewModel ViewModel { get; set; } + + public NavigationDialogHost() + { + InitializeComponent(); + } + + public NavigationDialogHost(AccountManager accountManager, ContentManager contentManager, + VirtualFileSystem virtualFileSystem, HorizonClient horizonClient) + { + AccountManager = accountManager; + ContentManager = contentManager; + VirtualFileSystem = virtualFileSystem; + HorizonClient = horizonClient; + ViewModel = new UserProfileViewModel(); + LoadProfiles(); + + if (contentManager.GetCurrentFirmwareVersion() != null) + Task.Run(() => UserFirmwareAvatarSelectorViewModel.PreloadAvatars(contentManager, virtualFileSystem)); + + InitializeComponent(); + } + + public void GoBack() + { + if (ContentFrame.BackStack.Count > 0) + ContentFrame.GoBack(); + + LoadProfiles(); + } + + public void Navigate(Type sourcePageType, object parameter) + => ContentFrame.Navigate(sourcePageType, parameter); + + public static async Task Show( + AccountManager ownerAccountManager, + ContentManager ownerContentManager, + VirtualFileSystem ownerVirtualFileSystem, + HorizonClient ownerHorizonClient) + { + var content = new NavigationDialogHost(ownerAccountManager, ownerContentManager, ownerVirtualFileSystem, ownerHorizonClient); + ContentDialog contentDialog = new() + { + Title = LocaleManager.Instance[LocaleKeys.UserProfileWindowTitle], + PrimaryButtonText = string.Empty, + SecondaryButtonText = string.Empty, + CloseButtonText = string.Empty, + Content = content, + Padding = new Thickness(0) + }; + + contentDialog.Closed += (_, _) => content.ViewModel.Dispose(); + + Style footer = new(x => x.Name("DialogSpace").Child().OfType()); + footer.Setters.Add(new Setter(IsVisibleProperty, false)); + + contentDialog.Styles.Add(footer); + + await contentDialog.ShowAsync(); + } + + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + + Navigate(typeof(UserSelectorViews), this); + } + + public void LoadProfiles() + { + ViewModel.Profiles.Clear(); + ViewModel.LostProfiles.Clear(); + + AccountManager.GetAllUsers() + .OrderBy(x => x.Name) + .ForEach(profile => ViewModel.Profiles.Add(new UserProfile(profile, this))); + + var saveDataFilter = SaveDataFilter.Make(programId: default, saveType: SaveDataType.Account, default, saveDataId: default, index: default); + + using var saveDataIterator = new UniqueRef(); + + HorizonClient.Fs.OpenSaveDataIterator(ref saveDataIterator.Ref, SaveDataSpaceId.User, in saveDataFilter).ThrowIfFailure(); + + Span saveDataInfo = stackalloc SaveDataInfo[10]; + + HashSet lostAccounts = new(); + + while (true) + { + saveDataIterator.Get.ReadSaveDataInfo(out long readCount, saveDataInfo).ThrowIfFailure(); + + if (readCount == 0) + { + break; + } + + for (int i = 0; i < readCount; i++) + { + var save = saveDataInfo[i]; + var id = new UserId((long)save.UserId.Id.Low, (long)save.UserId.Id.High); + if (ViewModel.Profiles.Cast().FirstOrDefault(x => x.UserId == id) == null) + { + lostAccounts.Add(id); + } + } + } + + foreach (var account in lostAccounts) + { + ViewModel.LostProfiles.Add(new UserProfile(new HLE.HOS.Services.Account.Acc.UserProfile(account, string.Empty, null), this)); + } + + ViewModel.Profiles.Add(new BaseModel()); + } + + public async void DeleteUser(UserProfile userProfile) + { + var lastUserId = AccountManager.LastOpenedUser.UserId; + + if (userProfile.UserId == lastUserId) + { + // If we are deleting the currently open profile, then we must open something else before deleting. + var profile = ViewModel.Profiles.Cast().FirstOrDefault(x => x.UserId != lastUserId); + + if (profile == null) + { + _ = Dispatcher.UIThread.InvokeAsync(async () + => await ContentDialogHelper.CreateErrorDialog( + LocaleManager.Instance[LocaleKeys.DialogUserProfileDeletionWarningMessage])); + + return; + } + + AccountManager.OpenUser(profile.UserId); + } + + var result = await ContentDialogHelper.CreateConfirmationDialog( + LocaleManager.Instance[LocaleKeys.DialogUserProfileDeletionConfirmMessage], + string.Empty, + LocaleManager.Instance[LocaleKeys.InputDialogYes], + LocaleManager.Instance[LocaleKeys.InputDialogNo], + string.Empty); + + if (result == UserResult.Yes) + { + GoBack(); + AccountManager.DeleteUser(userProfile.UserId); + } + + LoadProfiles(); + } + + public void AddUser() + { + Navigate(typeof(UserEditorView), (this, (UserProfile)null, true)); + } + + public void EditUser(UserProfile userProfile) + { + Navigate(typeof(UserEditorView), (this, userProfile, false)); + } + + public void RecoverLostAccounts() + { + Navigate(typeof(UserRecovererView), this); + } + + public void ManageSaves() + { + Navigate(typeof(UserSaveManagerView), (this, AccountManager, HorizonClient, VirtualFileSystem)); + } + } +} diff --git a/src/Ryujinx/UI/Controls/SliderScroll.axaml.cs b/src/Ryujinx/UI/Controls/SliderScroll.axaml.cs new file mode 100644 index 000000000..2f421e331 --- /dev/null +++ b/src/Ryujinx/UI/Controls/SliderScroll.axaml.cs @@ -0,0 +1,18 @@ +using Avalonia.Controls; +using Avalonia.Input; +using System; + +namespace Ryujinx.Ava.UI.Controls +{ + public class SliderScroll : Slider + { + protected override Type StyleKeyOverride => typeof(Slider); + + protected override void OnPointerWheelChanged(PointerWheelEventArgs e) + { + Value = Math.Clamp(Value + e.Delta.Y * TickFrequency, Minimum, Maximum); + + e.Handled = true; + } + } +} diff --git a/src/Ryujinx/UI/Controls/UpdateWaitWindow.axaml b/src/Ryujinx/UI/Controls/UpdateWaitWindow.axaml new file mode 100644 index 000000000..09fa04045 --- /dev/null +++ b/src/Ryujinx/UI/Controls/UpdateWaitWindow.axaml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + diff --git a/src/Ryujinx/UI/Controls/UpdateWaitWindow.axaml.cs b/src/Ryujinx/UI/Controls/UpdateWaitWindow.axaml.cs new file mode 100644 index 000000000..7ad1ee332 --- /dev/null +++ b/src/Ryujinx/UI/Controls/UpdateWaitWindow.axaml.cs @@ -0,0 +1,31 @@ +using Avalonia.Controls; +using Ryujinx.Ava.UI.Windows; +using System.Threading; + +namespace Ryujinx.Ava.UI.Controls +{ + public partial class UpdateWaitWindow : StyleableWindow + { + public UpdateWaitWindow(string primaryText, string secondaryText, CancellationTokenSource cancellationToken) : this(primaryText, secondaryText) + { + SystemDecorations = SystemDecorations.Full; + ShowInTaskbar = true; + + Closing += (_, _) => cancellationToken.Cancel(); + } + + public UpdateWaitWindow(string primaryText, string secondaryText) : this() + { + PrimaryText.Text = primaryText; + SecondaryText.Text = secondaryText; + WindowStartupLocation = WindowStartupLocation.CenterOwner; + SystemDecorations = SystemDecorations.BorderOnly; + ShowInTaskbar = false; + } + + public UpdateWaitWindow() + { + InitializeComponent(); + } + } +} diff --git a/src/Ryujinx/UI/Helpers/ApplicationOpenedEventArgs.cs b/src/Ryujinx/UI/Helpers/ApplicationOpenedEventArgs.cs new file mode 100644 index 000000000..bc5622b54 --- /dev/null +++ b/src/Ryujinx/UI/Helpers/ApplicationOpenedEventArgs.cs @@ -0,0 +1,16 @@ +using Avalonia.Interactivity; +using Ryujinx.UI.App.Common; + +namespace Ryujinx.Ava.UI.Helpers +{ + public class ApplicationOpenedEventArgs : RoutedEventArgs + { + public ApplicationData Application { get; } + + public ApplicationOpenedEventArgs(ApplicationData application, RoutedEvent routedEvent) + { + Application = application; + RoutedEvent = routedEvent; + } + } +} diff --git a/src/Ryujinx/UI/Helpers/AvaloniaListExtensions.cs b/src/Ryujinx/UI/Helpers/AvaloniaListExtensions.cs new file mode 100644 index 000000000..b3bb53bd0 --- /dev/null +++ b/src/Ryujinx/UI/Helpers/AvaloniaListExtensions.cs @@ -0,0 +1,62 @@ +using Avalonia.Collections; +using System.Collections.Generic; + +namespace Ryujinx.Ava.UI.Helpers +{ + public static class AvaloniaListExtensions + { + /// + /// Adds or Replaces an item in an AvaloniaList irrespective of whether the item already exists + /// + /// The type of the element in the AvaoloniaList + /// The list containing the item to replace + /// The item to replace + /// True to add the item if its not found + /// True if the item was found and replaced, false if it was addded + /// + /// The indexes on the AvaloniaList will only replace if the item does not match, + /// this causes the items to not be replaced if the Equality is customised on the + /// items. This method will instead find, remove and add the item to ensure it is + /// replaced correctly. + /// + public static bool ReplaceWith(this AvaloniaList list, T item, bool addIfNotFound = true) + { + var index = list.IndexOf(item); + + if (index != -1) + { + list.RemoveAt(index); + list.Insert(index, item); + return true; + } + else + { + list.Add(item); + return false; + } + } + + /// + /// Adds or Replaces items in an AvaloniaList from another list irrespective of whether the item already exists + /// + /// The type of the element in the AvaoloniaList + /// The list containing the item to replace + /// The list of items to be actually added to `list` + /// The items to use as matching records to search for in the `sourceList', if not found this item will be added instead + public static void AddOrReplaceMatching(this AvaloniaList list, IList sourceList, IList matchingList) + { + foreach (var match in matchingList) + { + var index = sourceList.IndexOf(match); + if (index != -1) + { + list.ReplaceWith(sourceList[index]); + } + else + { + list.ReplaceWith(match); + } + } + } + } +} \ No newline at end of file diff --git a/src/Ryujinx/UI/Helpers/BitmapArrayValueConverter.cs b/src/Ryujinx/UI/Helpers/BitmapArrayValueConverter.cs new file mode 100644 index 000000000..7b599a48b --- /dev/null +++ b/src/Ryujinx/UI/Helpers/BitmapArrayValueConverter.cs @@ -0,0 +1,36 @@ +using Avalonia.Data.Converters; +using Avalonia.Media; +using Avalonia.Media.Imaging; +using System; +using System.Globalization; +using System.IO; + +namespace Ryujinx.Ava.UI.Helpers +{ + internal class BitmapArrayValueConverter : IValueConverter + { + public static BitmapArrayValueConverter Instance = new(); + + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + switch (value) + { + case null: + return null; + case byte[] buffer when targetType == typeof(IImage): + { + MemoryStream mem = new(buffer); + + return new Bitmap(mem); + } + default: + throw new NotSupportedException(); + } + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotSupportedException(); + } + } +} diff --git a/src/Ryujinx/UI/Helpers/ButtonKeyAssigner.cs b/src/Ryujinx/UI/Helpers/ButtonKeyAssigner.cs new file mode 100644 index 000000000..2781a32b1 --- /dev/null +++ b/src/Ryujinx/UI/Helpers/ButtonKeyAssigner.cs @@ -0,0 +1,98 @@ +using Avalonia.Controls.Primitives; +using Avalonia.Threading; +using Ryujinx.Input; +using Ryujinx.Input.Assigner; +using System; +using System.Threading.Tasks; + +namespace Ryujinx.Ava.UI.Helpers +{ + internal class ButtonKeyAssigner + { + internal class ButtonAssignedEventArgs : EventArgs + { + public ToggleButton Button { get; } + public Button? ButtonValue { get; } + + public ButtonAssignedEventArgs(ToggleButton button, Button? buttonValue) + { + Button = button; + ButtonValue = buttonValue; + } + } + + public ToggleButton ToggledButton { get; set; } + + private bool _isWaitingForInput; + private bool _shouldUnbind; + public event EventHandler ButtonAssigned; + + public ButtonKeyAssigner(ToggleButton toggleButton) + { + ToggledButton = toggleButton; + } + + public async void GetInputAndAssign(IButtonAssigner assigner, IKeyboard keyboard = null) + { + Dispatcher.UIThread.Post(() => + { + ToggledButton.IsChecked = true; + }); + + if (_isWaitingForInput) + { + Dispatcher.UIThread.Post(() => Cancel()); + return; + } + + _isWaitingForInput = true; + + assigner.Initialize(); + + await Task.Run(async () => + { + while (true) + { + if (!_isWaitingForInput) + { + return; + } + + await Task.Delay(10); + + assigner.ReadInput(); + + if (assigner.IsAnyButtonPressed() || assigner.ShouldCancel() || (keyboard != null && keyboard.IsPressed(Key.Escape))) + { + break; + } + } + }); + + await Dispatcher.UIThread.InvokeAsync(() => + { + Button? pressedButton = assigner.GetPressedButton(); + + if (_shouldUnbind) + { + pressedButton = null; + } + + _shouldUnbind = false; + _isWaitingForInput = false; + + ToggledButton.IsChecked = false; + + ButtonAssigned?.Invoke(this, new ButtonAssignedEventArgs(ToggledButton, pressedButton)); + + }); + } + + public void Cancel(bool shouldUnbind = false) + { + _isWaitingForInput = false; + ToggledButton.IsChecked = false; + _shouldUnbind = shouldUnbind; + } + } +} diff --git a/src/Ryujinx/UI/Helpers/ContentDialogHelper.cs b/src/Ryujinx/UI/Helpers/ContentDialogHelper.cs new file mode 100644 index 000000000..3f0f0f033 --- /dev/null +++ b/src/Ryujinx/UI/Helpers/ContentDialogHelper.cs @@ -0,0 +1,475 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Layout; +using Avalonia.Media; +using Avalonia.Threading; +using FluentAvalonia.Core; +using FluentAvalonia.UI.Controls; +using Ryujinx.Ava.Common.Locale; +using Ryujinx.Ava.UI.Windows; +using Ryujinx.Common.Logging; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Ryujinx.Ava.UI.Helpers +{ + public static class ContentDialogHelper + { + private static bool _isChoiceDialogOpen; + private static ContentDialogOverlayWindow _contentDialogOverlayWindow; + + private async static Task ShowContentDialog( + string title, + object content, + string primaryButton, + string secondaryButton, + string closeButton, + UserResult primaryButtonResult = UserResult.Ok, + ManualResetEvent deferResetEvent = null, + TypedEventHandler deferCloseAction = null) + { + UserResult result = UserResult.None; + + ContentDialog contentDialog = new() + { + Title = title, + PrimaryButtonText = primaryButton, + SecondaryButtonText = secondaryButton, + CloseButtonText = closeButton, + Content = content, + PrimaryButtonCommand = MiniCommand.Create(() => + { + result = primaryButtonResult; + }) + }; + + contentDialog.SecondaryButtonCommand = MiniCommand.Create(() => + { + result = UserResult.No; + contentDialog.PrimaryButtonClick -= deferCloseAction; + }); + + contentDialog.CloseButtonCommand = MiniCommand.Create(() => + { + result = UserResult.Cancel; + contentDialog.PrimaryButtonClick -= deferCloseAction; + }); + + if (deferResetEvent != null) + { + contentDialog.PrimaryButtonClick += deferCloseAction; + } + + await ShowAsync(contentDialog); + + return result; + } + + public async static Task ShowTextDialog( + string title, + string primaryText, + string secondaryText, + string primaryButton, + string secondaryButton, + string closeButton, + int iconSymbol, + UserResult primaryButtonResult = UserResult.Ok, + ManualResetEvent deferResetEvent = null, + TypedEventHandler deferCloseAction = null) + { + Grid content = CreateTextDialogContent(primaryText, secondaryText, iconSymbol); + + return await ShowContentDialog(title, content, primaryButton, secondaryButton, closeButton, primaryButtonResult, deferResetEvent, deferCloseAction); + } + + public static async Task ShowDeferredContentDialog( + Window window, + string title, + string primaryText, + string secondaryText, + string primaryButton, + string secondaryButton, + string closeButton, + int iconSymbol, + ManualResetEvent deferResetEvent, + Func doWhileDeferred = null) + { + bool startedDeferring = false; + + return await ShowTextDialog( + title, + primaryText, + secondaryText, + primaryButton, + secondaryButton, + closeButton, + iconSymbol, + primaryButton == LocaleManager.Instance[LocaleKeys.InputDialogYes] ? UserResult.Yes : UserResult.Ok, + deferResetEvent, + DeferClose); + + async void DeferClose(ContentDialog sender, ContentDialogButtonClickEventArgs args) + { + if (startedDeferring) + { + return; + } + + sender.PrimaryButtonClick -= DeferClose; + + startedDeferring = true; + + var deferral = args.GetDeferral(); + + sender.PrimaryButtonClick -= DeferClose; + + _ = Task.Run(() => + { + deferResetEvent.WaitOne(); + + Dispatcher.UIThread.Post(() => + { + deferral.Complete(); + }); + }); + + if (doWhileDeferred != null) + { + await doWhileDeferred(window); + + deferResetEvent.Set(); + } + } + } + + private static Grid CreateTextDialogContent(string primaryText, string secondaryText, int symbol) + { + Grid content = new() + { + RowDefinitions = [new(), new()], + ColumnDefinitions = [new(GridLength.Auto), new()], + + MinHeight = 80, + }; + + SymbolIcon icon = new() + { + Symbol = (Symbol)symbol, + Margin = new Thickness(10), + FontSize = 40, + VerticalAlignment = VerticalAlignment.Center, + }; + + Grid.SetColumn(icon, 0); + Grid.SetRowSpan(icon, 2); + Grid.SetRow(icon, 0); + + TextBlock primaryLabel = new() + { + Text = primaryText, + Margin = new Thickness(5), + TextWrapping = TextWrapping.Wrap, + MaxWidth = 450, + }; + + TextBlock secondaryLabel = new() + { + Text = secondaryText, + Margin = new Thickness(5), + TextWrapping = TextWrapping.Wrap, + MaxWidth = 450, + }; + + Grid.SetColumn(primaryLabel, 1); + Grid.SetColumn(secondaryLabel, 1); + Grid.SetRow(primaryLabel, 0); + Grid.SetRow(secondaryLabel, 1); + + content.Children.Add(icon); + content.Children.Add(primaryLabel); + content.Children.Add(secondaryLabel); + + return content; + } + + public static Task CreateInfoDialog( + string primary, + string secondaryText, + string acceptButton, + string closeButton, + string title) + => ShowTextDialog( + title, + primary, + secondaryText, + acceptButton, + string.Empty, + closeButton, + (int)Symbol.Important); + + internal static async Task CreateConfirmationDialog( + string primaryText, + string secondaryText, + string acceptButtonText, + string cancelButtonText, + string title, + UserResult primaryButtonResult = UserResult.Yes) + => await ShowTextDialog( + string.IsNullOrWhiteSpace(title) ? LocaleManager.Instance[LocaleKeys.DialogConfirmationTitle] : title, + primaryText, + secondaryText, + acceptButtonText, + string.Empty, + cancelButtonText, + (int)Symbol.Help, + primaryButtonResult); + + internal static async Task CreateDeniableConfirmationDialog( + string primaryText, + string secondaryText, + string acceptButtonText, + string noAcceptButtonText, + string cancelButtonText, + string title, + UserResult primaryButtonResult = UserResult.Yes) + => await ShowTextDialog( + string.IsNullOrWhiteSpace(title) ? LocaleManager.Instance[LocaleKeys.DialogConfirmationTitle] : title, + primaryText, + secondaryText, + acceptButtonText, + noAcceptButtonText, + cancelButtonText, + (int)Symbol.Help, + primaryButtonResult); + + internal static async Task CreateLocalizedConfirmationDialog(string primaryText, string secondaryText) + => await CreateConfirmationDialog( + primaryText, + secondaryText, + LocaleManager.Instance[LocaleKeys.InputDialogYes], + LocaleManager.Instance[LocaleKeys.InputDialogNo], + LocaleManager.Instance[LocaleKeys.RyujinxConfirm]); + + internal static async Task CreateUpdaterInfoDialog(string primary, string secondaryText) + => await ShowTextDialog( + LocaleManager.Instance[LocaleKeys.DialogUpdaterTitle], + primary, + secondaryText, + string.Empty, + string.Empty, + LocaleManager.Instance[LocaleKeys.InputDialogOk], + (int)Symbol.Important); + + internal static async Task CreateUpdaterUpToDateInfoDialog(string primary, string secondaryText) + => await ShowTextDialog( + LocaleManager.Instance[LocaleKeys.DialogUpdaterTitle], + primary, + secondaryText, + LocaleManager.Instance[LocaleKeys.DialogUpdaterShowChangelogMessage], + string.Empty, + LocaleManager.Instance[LocaleKeys.InputDialogOk], + (int)Symbol.Important); + + internal static async Task CreateWarningDialog(string primary, string secondaryText) + => await ShowTextDialog( + LocaleManager.Instance[LocaleKeys.DialogWarningTitle], + primary, + secondaryText, + string.Empty, + string.Empty, + LocaleManager.Instance[LocaleKeys.InputDialogOk], + (int)Symbol.Important); + + internal static async Task CreateErrorDialog(string errorMessage, string secondaryErrorMessage = "") + { + Logger.Error?.Print(LogClass.Application, errorMessage); + + await ShowTextDialog( + LocaleManager.Instance[LocaleKeys.DialogErrorTitle], + LocaleManager.Instance[LocaleKeys.DialogErrorMessage], + errorMessage, + secondaryErrorMessage, + string.Empty, + LocaleManager.Instance[LocaleKeys.InputDialogOk], + (int)Symbol.Dismiss); + } + + internal static async Task CreateChoiceDialog(string title, string primary, string secondaryText) + { + if (_isChoiceDialogOpen) + { + return false; + } + + _isChoiceDialogOpen = true; + + UserResult response = await ShowTextDialog( + title, + primary, + secondaryText, + LocaleManager.Instance[LocaleKeys.InputDialogYes], + string.Empty, + LocaleManager.Instance[LocaleKeys.InputDialogNo], + (int)Symbol.Help, + UserResult.Yes); + + _isChoiceDialogOpen = false; + + return response == UserResult.Yes; + } + + internal static async Task CreateUpdaterChoiceDialog(string title, string primary, string secondaryText) + { + if (_isChoiceDialogOpen) + { + return UserResult.Cancel; + } + + _isChoiceDialogOpen = true; + + UserResult response = await ShowTextDialog( + title, + primary, + secondaryText, + LocaleManager.Instance[LocaleKeys.InputDialogYes], + LocaleManager.Instance[LocaleKeys.DialogUpdaterShowChangelogMessage], + LocaleManager.Instance[LocaleKeys.InputDialogNo], + (int)Symbol.Help, + UserResult.Yes); + + _isChoiceDialogOpen = false; + + return response; + } + + internal static async Task CreateExitDialog() + { + return await CreateChoiceDialog( + LocaleManager.Instance[LocaleKeys.DialogExitTitle], + LocaleManager.Instance[LocaleKeys.DialogExitMessage], + LocaleManager.Instance[LocaleKeys.DialogExitSubMessage]); + } + + internal static async Task CreateStopEmulationDialog() + { + return await CreateChoiceDialog( + LocaleManager.Instance[LocaleKeys.DialogStopEmulationTitle], + LocaleManager.Instance[LocaleKeys.DialogStopEmulationMessage], + LocaleManager.Instance[LocaleKeys.DialogExitSubMessage]); + } + + public static async Task ShowAsync(ContentDialog contentDialog) + { + ContentDialogResult result; + bool isTopDialog = true; + + Window parent = GetMainWindow(); + + if (_contentDialogOverlayWindow != null) + { + isTopDialog = false; + } + + if (parent is MainWindow window) + { + parent.Activate(); + + _contentDialogOverlayWindow = new ContentDialogOverlayWindow + { + Height = parent.Bounds.Height, + Width = parent.Bounds.Width, + Position = parent.PointToScreen(new Point()), + ShowInTaskbar = false, + }; + + parent.PositionChanged += OverlayOnPositionChanged; + + void OverlayOnPositionChanged(object sender, PixelPointEventArgs e) + { + if (_contentDialogOverlayWindow is null) + { + return; + } + + _contentDialogOverlayWindow.Position = parent.PointToScreen(new Point()); + } + + _contentDialogOverlayWindow.ContentDialog = contentDialog; + + bool opened = false; + + _contentDialogOverlayWindow.Opened += OverlayOnActivated; + + async void OverlayOnActivated(object sender, EventArgs e) + { + if (opened) + { + return; + } + + opened = true; + + _contentDialogOverlayWindow.Position = parent.PointToScreen(new Point()); + + result = await ShowDialog(); + } + + result = await _contentDialogOverlayWindow.ShowDialog(parent); + } + else + { + result = await ShowDialog(); + } + + async Task ShowDialog() + { + if (_contentDialogOverlayWindow is not null) + { + result = await contentDialog.ShowAsync(_contentDialogOverlayWindow); + + _contentDialogOverlayWindow!.Close(); + } + else + { + result = ContentDialogResult.None; + + Logger.Warning?.Print(LogClass.UI, "Content dialog overlay failed to populate. Default value has been returned."); + } + + return result; + } + + if (isTopDialog && _contentDialogOverlayWindow is not null) + { + _contentDialogOverlayWindow.Content = null; + _contentDialogOverlayWindow.Close(); + _contentDialogOverlayWindow = null; + } + + return result; + } + + public static async Task ShowWindowAsync(Window dialogWindow, Window mainWindow = null) + { + await dialogWindow.ShowDialog(_contentDialogOverlayWindow ?? mainWindow ?? GetMainWindow()); + } + + private static Window GetMainWindow() + { + if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime al) + { + foreach (Window item in al.Windows) + { + if (item is MainWindow window) + { + return window; + } + } + } + + return null; + } + } +} diff --git a/src/Ryujinx/UI/Helpers/DownloadableContentLabelConverter.cs b/src/Ryujinx/UI/Helpers/DownloadableContentLabelConverter.cs new file mode 100644 index 000000000..22193b97e --- /dev/null +++ b/src/Ryujinx/UI/Helpers/DownloadableContentLabelConverter.cs @@ -0,0 +1,42 @@ +using Avalonia; +using Avalonia.Data; +using Avalonia.Data.Converters; +using Ryujinx.Ava.Common.Locale; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; + +namespace Ryujinx.Ava.UI.Helpers +{ + internal class DownloadableContentLabelConverter : IMultiValueConverter + { + public static DownloadableContentLabelConverter Instance = new(); + + public object Convert(IList values, Type targetType, object parameter, CultureInfo culture) + { + if (values.Any(it => it is UnsetValueType)) + { + return BindingOperations.DoNothing; + } + + if (values.Count != 2 || !targetType.IsAssignableFrom(typeof(string))) + { + return null; + } + + if (values is not [string label, bool isBundled]) + { + return null; + } + + return isBundled ? $"{LocaleManager.Instance[LocaleKeys.TitleBundledDlcLabel]} {label}" : label; + } + + public object[] ConvertBack(object[] values, Type[] targetTypes, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + + } +} diff --git a/src/Ryujinx/UI/Helpers/Glyph.cs b/src/Ryujinx/UI/Helpers/Glyph.cs new file mode 100644 index 000000000..a6888a67b --- /dev/null +++ b/src/Ryujinx/UI/Helpers/Glyph.cs @@ -0,0 +1,10 @@ +namespace Ryujinx.Ava.UI.Helpers +{ + public enum Glyph + { + List, + Grid, + Chip, + Important, + } +} diff --git a/src/Ryujinx/UI/Helpers/GlyphValueConverter.cs b/src/Ryujinx/UI/Helpers/GlyphValueConverter.cs new file mode 100644 index 000000000..6196421c8 --- /dev/null +++ b/src/Ryujinx/UI/Helpers/GlyphValueConverter.cs @@ -0,0 +1,32 @@ +using Avalonia.Markup.Xaml; +using FluentAvalonia.UI.Controls; +using System; +using System.Collections.Generic; + +namespace Ryujinx.Ava.UI.Helpers +{ + public class GlyphValueConverter : MarkupExtension + { + private readonly string _key; + + private static readonly Dictionary _glyphs = new() + { + { Glyph.List, char.ConvertFromUtf32((int)Symbol.List) }, + { Glyph.Grid, char.ConvertFromUtf32((int)Symbol.ViewAll) }, + { Glyph.Chip, char.ConvertFromUtf32(59748) }, + { Glyph.Important, char.ConvertFromUtf32((int)Symbol.Important) }, + }; + + public GlyphValueConverter(string key) + { + _key = key; + } + + public string this[string key] => + _glyphs.TryGetValue(Enum.Parse(key), out var val) + ? val + : string.Empty; + + public override object ProvideValue(IServiceProvider serviceProvider) => this[_key]; + } +} diff --git a/src/Ryujinx/UI/Helpers/KeyValueConverter.cs b/src/Ryujinx/UI/Helpers/KeyValueConverter.cs new file mode 100644 index 000000000..d20098426 --- /dev/null +++ b/src/Ryujinx/UI/Helpers/KeyValueConverter.cs @@ -0,0 +1,184 @@ +using Avalonia.Data.Converters; +using Ryujinx.Ava.Common.Locale; +using Ryujinx.Common.Configuration.Hid; +using Ryujinx.Common.Configuration.Hid.Controller; +using System; +using System.Collections.Generic; +using System.Globalization; + +namespace Ryujinx.Ava.UI.Helpers +{ + internal class KeyValueConverter : IValueConverter + { + public static KeyValueConverter Instance = new(); + + private static readonly Dictionary _keysMap = new() + { + { Key.Unknown, LocaleKeys.KeyUnknown }, + { Key.ShiftLeft, LocaleKeys.KeyShiftLeft }, + { Key.ShiftRight, LocaleKeys.KeyShiftRight }, + { Key.ControlLeft, LocaleKeys.KeyControlLeft }, + { Key.ControlRight, LocaleKeys.KeyControlRight }, + { Key.AltLeft, LocaleKeys.KeyAltLeft }, + { Key.AltRight, LocaleKeys.KeyAltRight }, + { Key.WinLeft, LocaleKeys.KeyWinLeft }, + { Key.WinRight, LocaleKeys.KeyWinRight }, + { Key.Up, LocaleKeys.KeyUp }, + { Key.Down, LocaleKeys.KeyDown }, + { Key.Left, LocaleKeys.KeyLeft }, + { Key.Right, LocaleKeys.KeyRight }, + { Key.Enter, LocaleKeys.KeyEnter }, + { Key.Escape, LocaleKeys.KeyEscape }, + { Key.Space, LocaleKeys.KeySpace }, + { Key.Tab, LocaleKeys.KeyTab }, + { Key.BackSpace, LocaleKeys.KeyBackSpace }, + { Key.Insert, LocaleKeys.KeyInsert }, + { Key.Delete, LocaleKeys.KeyDelete }, + { Key.PageUp, LocaleKeys.KeyPageUp }, + { Key.PageDown, LocaleKeys.KeyPageDown }, + { Key.Home, LocaleKeys.KeyHome }, + { Key.End, LocaleKeys.KeyEnd }, + { Key.CapsLock, LocaleKeys.KeyCapsLock }, + { Key.ScrollLock, LocaleKeys.KeyScrollLock }, + { Key.PrintScreen, LocaleKeys.KeyPrintScreen }, + { Key.Pause, LocaleKeys.KeyPause }, + { Key.NumLock, LocaleKeys.KeyNumLock }, + { Key.Clear, LocaleKeys.KeyClear }, + { Key.Keypad0, LocaleKeys.KeyKeypad0 }, + { Key.Keypad1, LocaleKeys.KeyKeypad1 }, + { Key.Keypad2, LocaleKeys.KeyKeypad2 }, + { Key.Keypad3, LocaleKeys.KeyKeypad3 }, + { Key.Keypad4, LocaleKeys.KeyKeypad4 }, + { Key.Keypad5, LocaleKeys.KeyKeypad5 }, + { Key.Keypad6, LocaleKeys.KeyKeypad6 }, + { Key.Keypad7, LocaleKeys.KeyKeypad7 }, + { Key.Keypad8, LocaleKeys.KeyKeypad8 }, + { Key.Keypad9, LocaleKeys.KeyKeypad9 }, + { Key.KeypadDivide, LocaleKeys.KeyKeypadDivide }, + { Key.KeypadMultiply, LocaleKeys.KeyKeypadMultiply }, + { Key.KeypadSubtract, LocaleKeys.KeyKeypadSubtract }, + { Key.KeypadAdd, LocaleKeys.KeyKeypadAdd }, + { Key.KeypadDecimal, LocaleKeys.KeyKeypadDecimal }, + { Key.KeypadEnter, LocaleKeys.KeyKeypadEnter }, + { Key.Number0, LocaleKeys.KeyNumber0 }, + { Key.Number1, LocaleKeys.KeyNumber1 }, + { Key.Number2, LocaleKeys.KeyNumber2 }, + { Key.Number3, LocaleKeys.KeyNumber3 }, + { Key.Number4, LocaleKeys.KeyNumber4 }, + { Key.Number5, LocaleKeys.KeyNumber5 }, + { Key.Number6, LocaleKeys.KeyNumber6 }, + { Key.Number7, LocaleKeys.KeyNumber7 }, + { Key.Number8, LocaleKeys.KeyNumber8 }, + { Key.Number9, LocaleKeys.KeyNumber9 }, + { Key.Tilde, LocaleKeys.KeyTilde }, + { Key.Grave, LocaleKeys.KeyGrave }, + { Key.Minus, LocaleKeys.KeyMinus }, + { Key.Plus, LocaleKeys.KeyPlus }, + { Key.BracketLeft, LocaleKeys.KeyBracketLeft }, + { Key.BracketRight, LocaleKeys.KeyBracketRight }, + { Key.Semicolon, LocaleKeys.KeySemicolon }, + { Key.Quote, LocaleKeys.KeyQuote }, + { Key.Comma, LocaleKeys.KeyComma }, + { Key.Period, LocaleKeys.KeyPeriod }, + { Key.Slash, LocaleKeys.KeySlash }, + { Key.BackSlash, LocaleKeys.KeyBackSlash }, + { Key.Unbound, LocaleKeys.KeyUnbound }, + }; + + private static readonly Dictionary _gamepadInputIdMap = new() + { + { GamepadInputId.LeftStick, LocaleKeys.GamepadLeftStick }, + { GamepadInputId.RightStick, LocaleKeys.GamepadRightStick }, + { GamepadInputId.LeftShoulder, LocaleKeys.GamepadLeftShoulder }, + { GamepadInputId.RightShoulder, LocaleKeys.GamepadRightShoulder }, + { GamepadInputId.LeftTrigger, LocaleKeys.GamepadLeftTrigger }, + { GamepadInputId.RightTrigger, LocaleKeys.GamepadRightTrigger }, + { GamepadInputId.DpadUp, LocaleKeys.GamepadDpadUp}, + { GamepadInputId.DpadDown, LocaleKeys.GamepadDpadDown}, + { GamepadInputId.DpadLeft, LocaleKeys.GamepadDpadLeft}, + { GamepadInputId.DpadRight, LocaleKeys.GamepadDpadRight}, + { GamepadInputId.Minus, LocaleKeys.GamepadMinus}, + { GamepadInputId.Plus, LocaleKeys.GamepadPlus}, + { GamepadInputId.Guide, LocaleKeys.GamepadGuide}, + { GamepadInputId.Misc1, LocaleKeys.GamepadMisc1}, + { GamepadInputId.Paddle1, LocaleKeys.GamepadPaddle1}, + { GamepadInputId.Paddle2, LocaleKeys.GamepadPaddle2}, + { GamepadInputId.Paddle3, LocaleKeys.GamepadPaddle3}, + { GamepadInputId.Paddle4, LocaleKeys.GamepadPaddle4}, + { GamepadInputId.Touchpad, LocaleKeys.GamepadTouchpad}, + { GamepadInputId.SingleLeftTrigger0, LocaleKeys.GamepadSingleLeftTrigger0}, + { GamepadInputId.SingleRightTrigger0, LocaleKeys.GamepadSingleRightTrigger0}, + { GamepadInputId.SingleLeftTrigger1, LocaleKeys.GamepadSingleLeftTrigger1}, + { GamepadInputId.SingleRightTrigger1, LocaleKeys.GamepadSingleRightTrigger1}, + { GamepadInputId.Unbound, LocaleKeys.KeyUnbound}, + }; + + private static readonly Dictionary _stickInputIdMap = new() + { + { StickInputId.Left, LocaleKeys.StickLeft}, + { StickInputId.Right, LocaleKeys.StickRight}, + { StickInputId.Unbound, LocaleKeys.KeyUnbound}, + }; + + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + string keyString = string.Empty; + LocaleKeys localeKey; + + switch (value) + { + case Key key: + if (_keysMap.TryGetValue(key, out localeKey)) + { + if (OperatingSystem.IsMacOS()) + { + localeKey = localeKey switch + { + LocaleKeys.KeyControlLeft => LocaleKeys.KeyMacControlLeft, + LocaleKeys.KeyControlRight => LocaleKeys.KeyMacControlRight, + LocaleKeys.KeyAltLeft => LocaleKeys.KeyMacAltLeft, + LocaleKeys.KeyAltRight => LocaleKeys.KeyMacAltRight, + LocaleKeys.KeyWinLeft => LocaleKeys.KeyMacWinLeft, + LocaleKeys.KeyWinRight => LocaleKeys.KeyMacWinRight, + _ => localeKey + }; + } + + keyString = LocaleManager.Instance[localeKey]; + } + else + { + keyString = key.ToString(); + } + break; + case GamepadInputId gamepadInputId: + if (_gamepadInputIdMap.TryGetValue(gamepadInputId, out localeKey)) + { + keyString = LocaleManager.Instance[localeKey]; + } + else + { + keyString = gamepadInputId.ToString(); + } + break; + case StickInputId stickInputId: + if (_stickInputIdMap.TryGetValue(stickInputId, out localeKey)) + { + keyString = LocaleManager.Instance[localeKey]; + } + else + { + keyString = stickInputId.ToString(); + } + break; + } + + return keyString; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotSupportedException(); + } + } +} diff --git a/src/Ryujinx/UI/Helpers/LoggerAdapter.cs b/src/Ryujinx/UI/Helpers/LoggerAdapter.cs new file mode 100644 index 000000000..7982c17a6 --- /dev/null +++ b/src/Ryujinx/UI/Helpers/LoggerAdapter.cs @@ -0,0 +1,103 @@ +using Avalonia.Logging; +using Avalonia.Utilities; +using Gommon; +using Ryujinx.Common.Logging; +using System; +using System.Text; + +namespace Ryujinx.Ava.UI.Helpers +{ + using AvaLogger = Avalonia.Logging.Logger; + using AvaLogLevel = LogEventLevel; + using RyuLogClass = LogClass; + using RyuLogger = Ryujinx.Common.Logging.Logger; + + internal class LoggerAdapter : ILogSink + { + public static void Register() + { + AvaLogger.Sink = new LoggerAdapter(); + } + + private static RyuLogger.Log? GetLog(AvaLogLevel level) + { + return level switch + { + AvaLogLevel.Verbose => RyuLogger.Debug, + AvaLogLevel.Debug => RyuLogger.Debug, + AvaLogLevel.Information => RyuLogger.Debug, + AvaLogLevel.Warning => RyuLogger.Debug, + AvaLogLevel.Error => RyuLogger.Error, + AvaLogLevel.Fatal => RyuLogger.Error, + _ => throw new ArgumentOutOfRangeException(nameof(level), level, null), + }; + } + + public bool IsEnabled(AvaLogLevel level, string area) + { + return GetLog(level) != null; + } + + public void Log(AvaLogLevel level, string area, object source, string messageTemplate) + { + GetLog(level)?.PrintMsg(RyuLogClass.UI, Format(level, area, messageTemplate, source, null)); + } + + public void Log(AvaLogLevel level, string area, object source, string messageTemplate, params object[] propertyValues) + { + GetLog(level)?.PrintMsg(RyuLogClass.UI, Format(level, area, messageTemplate, source, propertyValues)); + } + + private static string Format(AvaLogLevel level, string area, string template, object source, object[] v) + { + var result = new StringBuilder(); + var r = new CharacterReader(template.AsSpan()); + int i = 0; + + result.Append('['); + result.Append(level); + result.Append("] "); + + result.Append('['); + result.Append(area); + result.Append("] "); + + while (!r.End) + { + var c = r.Take(); + + if (c != '{') + { + result.Append(c); + } + else + { + if (r.Peek != '{') + { + result.Append('\''); + result.Append(i < v.Length ? v[i++] : null); + result.Append('\''); + r.TakeUntil('}'); + r.Take(); + } + else + { + result.Append('{'); + r.Take(); + } + } + } + + if (source != null) + { + result.Append(" ("); + result.Append(source.GetType().AsFullNamePrettyString()); + result.Append(" #"); + result.Append(source.GetHashCode()); + result.Append(')'); + } + + return result.ToString(); + } + } +} diff --git a/src/Ryujinx/UI/Helpers/MiniCommand.cs b/src/Ryujinx/UI/Helpers/MiniCommand.cs new file mode 100644 index 000000000..7e1bb9a68 --- /dev/null +++ b/src/Ryujinx/UI/Helpers/MiniCommand.cs @@ -0,0 +1,71 @@ +using System; +using System.Threading.Tasks; +using System.Windows.Input; + +namespace Ryujinx.Ava.UI.Helpers +{ + public sealed class MiniCommand : MiniCommand, ICommand + { + private readonly Action _callback; + private bool _busy; + private readonly Func _asyncCallback; + + public MiniCommand(Action callback) + { + _callback = callback; + } + + public MiniCommand(Func callback) + { + _asyncCallback = callback; + } + + private bool Busy + { + get => _busy; + set + { + _busy = value; + CanExecuteChanged?.Invoke(this, EventArgs.Empty); + } + } + + public override event EventHandler CanExecuteChanged; + public override bool CanExecute(object parameter) => !_busy; + + public override async void Execute(object parameter) + { + if (Busy) + { + return; + } + try + { + Busy = true; + if (_callback != null) + { + _callback((T)parameter); + } + else + { + await _asyncCallback((T)parameter); + } + } + finally + { + Busy = false; + } + } + } + + public abstract class MiniCommand : ICommand + { + public static MiniCommand Create(Action callback) => new MiniCommand(_ => callback()); + public static MiniCommand Create(Action callback) => new MiniCommand(callback); + public static MiniCommand CreateFromTask(Func callback) => new MiniCommand(_ => callback()); + + public abstract bool CanExecute(object parameter); + public abstract void Execute(object parameter); + public abstract event EventHandler CanExecuteChanged; + } +} diff --git a/src/Ryujinx/UI/Helpers/MultiplayerInfoConverter.cs b/src/Ryujinx/UI/Helpers/MultiplayerInfoConverter.cs new file mode 100644 index 000000000..8bd8b5f0d --- /dev/null +++ b/src/Ryujinx/UI/Helpers/MultiplayerInfoConverter.cs @@ -0,0 +1,44 @@ +using Avalonia.Data.Converters; +using Avalonia.Markup.Xaml; +using Ryujinx.Ava.Common.Locale; +using Ryujinx.UI.App.Common; +using Ryujinx.UI.Common.Helper; +using System; +using System.Globalization; + +namespace Ryujinx.Ava.UI.Helpers +{ + internal class MultiplayerInfoConverter : MarkupExtension, IValueConverter + { + private static readonly MultiplayerInfoConverter _instance = new(); + + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is ApplicationData applicationData) + { + if (applicationData.PlayerCount != 0 && applicationData.GameCount != 0) + { + return $"Hosted Games: {applicationData.GameCount}\nOnline Players: {applicationData.PlayerCount}"; + } + else + { + return ""; + } + } + else + { + return ""; + } + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotSupportedException(); + } + + public override object ProvideValue(IServiceProvider serviceProvider) + { + return _instance; + } + } +} diff --git a/src/Ryujinx/UI/Helpers/NotificationHelper.cs b/src/Ryujinx/UI/Helpers/NotificationHelper.cs new file mode 100644 index 000000000..656a8b52f --- /dev/null +++ b/src/Ryujinx/UI/Helpers/NotificationHelper.cs @@ -0,0 +1,70 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Notifications; +using Avalonia.Threading; +using Ryujinx.Ava.Common.Locale; +using Ryujinx.Common; +using System; +using System.Collections.Concurrent; +using System.Threading; + +namespace Ryujinx.Ava.UI.Helpers +{ + public static class NotificationHelper + { + private const int MaxNotifications = 4; + private const int NotificationDelayInMs = 5000; + + private static WindowNotificationManager _notificationManager; + + private static readonly BlockingCollection _notifications = new(); + + public static void SetNotificationManager(Window host) + { + _notificationManager = new WindowNotificationManager(host) + { + Position = NotificationPosition.BottomRight, + MaxItems = MaxNotifications, + Margin = new Thickness(0, 0, 15, 40), + }; + + var maybeAsyncWorkQueue = new Lazy>( + () => new AsyncWorkQueue(notification => + { + Dispatcher.UIThread.Post(() => + { + _notificationManager.Show(notification); + }); + }, + "UI.NotificationThread", + _notifications), + LazyThreadSafetyMode.ExecutionAndPublication); + + _notificationManager.TemplateApplied += (sender, args) => + { + // NOTE: Force creation of the AsyncWorkQueue. + _ = maybeAsyncWorkQueue.Value; + }; + + host.Closing += (sender, args) => + { + if (maybeAsyncWorkQueue.IsValueCreated) + { + maybeAsyncWorkQueue.Value.Dispose(); + } + }; + } + + public static void Show(string title, string text, NotificationType type, bool waitingExit = false, Action onClick = null, Action onClose = null) + { + var delay = waitingExit ? TimeSpan.FromMilliseconds(0) : TimeSpan.FromMilliseconds(NotificationDelayInMs); + + _notifications.Add(new Notification(title, text, type, delay, onClick, onClose)); + } + + public static void ShowError(string message) + { + Show(LocaleManager.Instance[LocaleKeys.DialogErrorTitle], $"{LocaleManager.Instance[LocaleKeys.DialogErrorMessage]}\n\n{message}", NotificationType.Error); + } + } +} diff --git a/src/Ryujinx/UI/Helpers/OffscreenTextBox.cs b/src/Ryujinx/UI/Helpers/OffscreenTextBox.cs new file mode 100644 index 000000000..dd9698918 --- /dev/null +++ b/src/Ryujinx/UI/Helpers/OffscreenTextBox.cs @@ -0,0 +1,42 @@ +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Interactivity; +using System; + +namespace Ryujinx.Ava.UI.Helpers +{ + public class OffscreenTextBox : TextBox + { + protected override Type StyleKeyOverride => typeof(TextBox); + + public static RoutedEvent GetKeyDownRoutedEvent() + { + return KeyDownEvent; + } + + public static RoutedEvent GetKeyUpRoutedEvent() + { + return KeyUpEvent; + } + + public void SendKeyDownEvent(KeyEventArgs keyEvent) + { + OnKeyDown(keyEvent); + } + + public void SendKeyUpEvent(KeyEventArgs keyEvent) + { + OnKeyUp(keyEvent); + } + + public void SendText(string text) + { + OnTextInput(new TextInputEventArgs + { + Text = text, + Source = this, + RoutedEvent = TextInputEvent + }); + } + } +} diff --git a/src/Ryujinx/UI/Helpers/TimeZoneConverter.cs b/src/Ryujinx/UI/Helpers/TimeZoneConverter.cs new file mode 100644 index 000000000..0e5525c7b --- /dev/null +++ b/src/Ryujinx/UI/Helpers/TimeZoneConverter.cs @@ -0,0 +1,20 @@ +using Avalonia.Data.Converters; +using System; +using System.Globalization; +using TimeZone = Ryujinx.Ava.UI.Models.TimeZone; + +namespace Ryujinx.Ava.UI.Helpers +{ + internal class TimeZoneConverter : IValueConverter + { + public static TimeZoneConverter Instance = new(); + + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + => value is TimeZone timeZone + ? $"{timeZone.UtcDifference} {timeZone.Location} {timeZone.Abbreviation}" + : null; + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + => throw new NotImplementedException(); + } +} diff --git a/src/Ryujinx/UI/Helpers/TitleUpdateLabelConverter.cs b/src/Ryujinx/UI/Helpers/TitleUpdateLabelConverter.cs new file mode 100644 index 000000000..cbb6edff1 --- /dev/null +++ b/src/Ryujinx/UI/Helpers/TitleUpdateLabelConverter.cs @@ -0,0 +1,42 @@ +using Avalonia; +using Avalonia.Data; +using Avalonia.Data.Converters; +using Ryujinx.Ava.Common.Locale; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; + +namespace Ryujinx.Ava.UI.Helpers +{ + internal class TitleUpdateLabelConverter : IMultiValueConverter + { + public static TitleUpdateLabelConverter Instance = new(); + + public object Convert(IList values, Type targetType, object parameter, CultureInfo culture) + { + if (values.Any(it => it is UnsetValueType)) + { + return BindingOperations.DoNothing; + } + + if (values.Count != 2 || !targetType.IsAssignableFrom(typeof(string))) + { + return null; + } + + if (values is not [string label, bool isBundled]) + { + return null; + } + + var key = isBundled ? LocaleKeys.TitleBundledUpdateVersionLabel : LocaleKeys.TitleUpdateVersionLabel; + return LocaleManager.Instance.UpdateAndGetDynamicValue(key, label); + } + + public object[] ConvertBack(object[] values, Type[] targetTypes, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/Ryujinx/UI/Helpers/UserErrorDialog.cs b/src/Ryujinx/UI/Helpers/UserErrorDialog.cs new file mode 100644 index 000000000..b981a8275 --- /dev/null +++ b/src/Ryujinx/UI/Helpers/UserErrorDialog.cs @@ -0,0 +1,48 @@ +using Ryujinx.Ava.Common.Locale; +using Ryujinx.UI.Common; +using System.Threading.Tasks; + +namespace Ryujinx.Ava.UI.Helpers +{ + internal class UserErrorDialog + { + private static string GetErrorCode(UserError error) + { + return $"RYU-{(uint)error:X4}"; + } + + private static string GetErrorTitle(UserError error) => + error switch + { + UserError.NoKeys => LocaleManager.Instance[LocaleKeys.UserErrorNoKeys], + UserError.NoFirmware => LocaleManager.Instance[LocaleKeys.UserErrorNoFirmware], + UserError.FirmwareParsingFailed => LocaleManager.Instance[LocaleKeys.UserErrorFirmwareParsingFailed], + UserError.ApplicationNotFound => LocaleManager.Instance[LocaleKeys.UserErrorApplicationNotFound], + UserError.Unknown => LocaleManager.Instance[LocaleKeys.UserErrorUnknown], + _ => LocaleManager.Instance[LocaleKeys.UserErrorUndefined], + }; + + private static string GetErrorDescription(UserError error) => + error switch + { + UserError.NoKeys => LocaleManager.Instance[LocaleKeys.UserErrorNoKeysDescription], + UserError.NoFirmware => LocaleManager.Instance[LocaleKeys.UserErrorNoFirmwareDescription], + UserError.FirmwareParsingFailed => LocaleManager.Instance[LocaleKeys.UserErrorFirmwareParsingFailedDescription], + UserError.ApplicationNotFound => LocaleManager.Instance[LocaleKeys.UserErrorApplicationNotFoundDescription], + UserError.Unknown => LocaleManager.Instance[LocaleKeys.UserErrorUnknownDescription], + _ => LocaleManager.Instance[LocaleKeys.UserErrorUndefinedDescription], + }; + + public static async Task ShowUserErrorDialog(UserError error) + { + string errorCode = GetErrorCode(error); + + await ContentDialogHelper.CreateInfoDialog( + LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogUserErrorDialogMessage, errorCode, GetErrorTitle(error)), + GetErrorDescription(error), + string.Empty, + LocaleManager.Instance[LocaleKeys.InputDialogOk], + LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogUserErrorDialogTitle, errorCode)); + } + } +} diff --git a/src/Ryujinx/UI/Helpers/UserResult.cs b/src/Ryujinx/UI/Helpers/UserResult.cs new file mode 100644 index 000000000..2fcd35ae4 --- /dev/null +++ b/src/Ryujinx/UI/Helpers/UserResult.cs @@ -0,0 +1,12 @@ +namespace Ryujinx.Ava.UI.Helpers +{ + public enum UserResult + { + Ok, + Yes, + No, + Abort, + Cancel, + None, + } +} diff --git a/src/Ryujinx/UI/Helpers/Win32NativeInterop.cs b/src/Ryujinx/UI/Helpers/Win32NativeInterop.cs new file mode 100644 index 000000000..48f98f44a --- /dev/null +++ b/src/Ryujinx/UI/Helpers/Win32NativeInterop.cs @@ -0,0 +1,115 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +using System.Runtime.Versioning; + +namespace Ryujinx.Ava.UI.Helpers +{ + [SupportedOSPlatform("windows")] + internal partial class Win32NativeInterop + { + internal const int GWLP_WNDPROC = -4; + + [Flags] + public enum ClassStyles : uint + { + CsClassdc = 0x40, + CsOwndc = 0x20, + } + + [Flags] + public enum WindowStyles : uint + { + WsChild = 0x40000000, + } + + public enum Cursors : uint + { + IdcArrow = 32512, + } + + [SuppressMessage("Design", "CA1069: Enums values should not be duplicated")] + public enum WindowsMessages : uint + { + NcHitTest = 0x0084, + } + + [UnmanagedFunctionPointer(CallingConvention.Winapi)] + internal delegate nint WindowProc(nint hWnd, WindowsMessages msg, nint wParam, nint lParam); + + [StructLayout(LayoutKind.Sequential)] + public struct WndClassEx + { + public int cbSize; + public ClassStyles style; + public nint lpfnWndProc; // not WndProc + public int cbClsExtra; + public int cbWndExtra; + public nint hInstance; + public nint hIcon; + public nint hCursor; + public nint hbrBackground; + public nint lpszMenuName; + public nint lpszClassName; + public nint hIconSm; + + public WndClassEx() + { + cbSize = Marshal.SizeOf(); + } + } + + public static nint CreateEmptyCursor() + { + return CreateCursor(nint.Zero, 0, 0, 1, 1, [0xFF], [0x00]); + } + + public static nint CreateArrowCursor() + { + return LoadCursor(nint.Zero, (nint)Cursors.IdcArrow); + } + + [LibraryImport("user32.dll")] + public static partial nint SetCursor(nint handle); + + [LibraryImport("user32.dll")] + public static partial nint CreateCursor(nint hInst, int xHotSpot, int yHotSpot, int nWidth, int nHeight, [In] byte[] pvAndPlane, [In] byte[] pvXorPlane); + + [LibraryImport("user32.dll", SetLastError = true, EntryPoint = "RegisterClassExW")] + public static partial ushort RegisterClassEx(ref WndClassEx param); + + [LibraryImport("user32.dll", SetLastError = true, EntryPoint = "UnregisterClassW")] + public static partial short UnregisterClass([MarshalAs(UnmanagedType.LPWStr)] string lpClassName, nint instance); + + [LibraryImport("user32.dll", EntryPoint = "DefWindowProcW")] + public static partial nint DefWindowProc(nint hWnd, WindowsMessages msg, nint wParam, nint lParam); + + [LibraryImport("kernel32.dll", EntryPoint = "GetModuleHandleA")] + public static partial nint GetModuleHandle([MarshalAs(UnmanagedType.LPStr)] string lpModuleName); + + [LibraryImport("user32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + public static partial bool DestroyWindow(nint hwnd); + + [LibraryImport("user32.dll", SetLastError = true, EntryPoint = "LoadCursorA")] + public static partial nint LoadCursor(nint hInstance, nint lpCursorName); + + [LibraryImport("user32.dll", SetLastError = true, EntryPoint = "CreateWindowExW")] + public static partial nint CreateWindowEx( + uint dwExStyle, + [MarshalAs(UnmanagedType.LPWStr)] string lpClassName, + [MarshalAs(UnmanagedType.LPWStr)] string lpWindowName, + WindowStyles dwStyle, + int x, + int y, + int nWidth, + int nHeight, + nint hWndParent, + nint hMenu, + nint hInstance, + nint lpParam); + + [LibraryImport("user32.dll", SetLastError = true)] + public static partial nint SetWindowLongPtrW(nint hWnd, int nIndex, nint value); + } +} diff --git a/src/Ryujinx/UI/Helpers/XCITrimmerFileSpaceSavingsConverter.cs b/src/Ryujinx/UI/Helpers/XCITrimmerFileSpaceSavingsConverter.cs new file mode 100644 index 000000000..c6e814e90 --- /dev/null +++ b/src/Ryujinx/UI/Helpers/XCITrimmerFileSpaceSavingsConverter.cs @@ -0,0 +1,48 @@ +using Avalonia; +using Avalonia.Data; +using Avalonia.Data.Converters; +using Ryujinx.Ava.Common.Locale; +using Ryujinx.UI.Common.Models; +using System; +using System.Globalization; + +namespace Ryujinx.Ava.UI.Helpers +{ + internal class XCITrimmerFileSpaceSavingsConverter : IValueConverter + { + private const long _bytesPerMB = 1024 * 1024; + public static XCITrimmerFileSpaceSavingsConverter Instance = new(); + + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is UnsetValueType) + { + return BindingOperations.DoNothing; + } + + if (!targetType.IsAssignableFrom(typeof(string))) + { + return null; + } + + if (value is not XCITrimmerFileModel app) + { + return null; + } + + if (app.CurrentSavingsB < app.PotentialSavingsB) + { + return LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.TitleXCICanSaveLabel, (app.PotentialSavingsB - app.CurrentSavingsB) / _bytesPerMB); + } + else + { + return LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.TitleXCISavingLabel, app.CurrentSavingsB / _bytesPerMB); + } + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotSupportedException(); + } + } +} diff --git a/src/Ryujinx/UI/Helpers/XCITrimmerFileStatusConverter.cs b/src/Ryujinx/UI/Helpers/XCITrimmerFileStatusConverter.cs new file mode 100644 index 000000000..56a102415 --- /dev/null +++ b/src/Ryujinx/UI/Helpers/XCITrimmerFileStatusConverter.cs @@ -0,0 +1,46 @@ +using Avalonia; +using Avalonia.Data; +using Avalonia.Data.Converters; +using Ryujinx.Ava.Common.Locale; +using Ryujinx.UI.Common.Models; +using System; +using System.Globalization; +using static Ryujinx.Common.Utilities.XCIFileTrimmer; + +namespace Ryujinx.Ava.UI.Helpers +{ + internal class XCITrimmerFileStatusConverter : IValueConverter + { + public static XCITrimmerFileStatusConverter Instance = new(); + + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is UnsetValueType) + { + return BindingOperations.DoNothing; + } + + if (!targetType.IsAssignableFrom(typeof(string))) + { + return null; + } + + if (value is not XCITrimmerFileModel app) + { + return null; + } + + return app.PercentageProgress != null ? String.Empty : + app.ProcessingOutcome != OperationOutcome.Successful && app.ProcessingOutcome != OperationOutcome.Undetermined ? LocaleManager.Instance[LocaleKeys.TitleXCIStatusFailedLabel] : + app.Trimmable & app.Untrimmable ? LocaleManager.Instance[LocaleKeys.TitleXCIStatusPartialLabel] : + app.Trimmable ? LocaleManager.Instance[LocaleKeys.TitleXCIStatusTrimmableLabel] : + app.Untrimmable ? LocaleManager.Instance[LocaleKeys.TitleXCIStatusUntrimmableLabel] : + String.Empty; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotSupportedException(); + } + } +} diff --git a/src/Ryujinx/UI/Helpers/XCITrimmerFileStatusDetailConverter.cs b/src/Ryujinx/UI/Helpers/XCITrimmerFileStatusDetailConverter.cs new file mode 100644 index 000000000..cd4e27f01 --- /dev/null +++ b/src/Ryujinx/UI/Helpers/XCITrimmerFileStatusDetailConverter.cs @@ -0,0 +1,42 @@ +using Avalonia; +using Avalonia.Data; +using Avalonia.Data.Converters; +using Ryujinx.UI.Common.Models; +using System; +using System.Globalization; +using static Ryujinx.Common.Utilities.XCIFileTrimmer; + +namespace Ryujinx.Ava.UI.Helpers +{ + internal class XCITrimmerFileStatusDetailConverter : IValueConverter + { + public static XCITrimmerFileStatusDetailConverter Instance = new(); + + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is UnsetValueType) + { + return BindingOperations.DoNothing; + } + + if (!targetType.IsAssignableFrom(typeof(string))) + { + return null; + } + + if (value is not XCITrimmerFileModel app) + { + return null; + } + + return app.PercentageProgress != null ? null : + app.ProcessingOutcome != OperationOutcome.Successful && app.ProcessingOutcome != OperationOutcome.Undetermined ? app.ProcessingOutcome.ToLocalisedText() : + null; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotSupportedException(); + } + } +} diff --git a/src/Ryujinx/UI/Helpers/XCITrimmerOperationOutcomeHelper.cs b/src/Ryujinx/UI/Helpers/XCITrimmerOperationOutcomeHelper.cs new file mode 100644 index 000000000..93489f806 --- /dev/null +++ b/src/Ryujinx/UI/Helpers/XCITrimmerOperationOutcomeHelper.cs @@ -0,0 +1,36 @@ +using Ryujinx.Ava.Common.Locale; +using static Ryujinx.Common.Utilities.XCIFileTrimmer; + +namespace Ryujinx.Ava.UI.Helpers +{ + public static class XCIFileTrimmerOperationOutcomeExtensions + { + public static string ToLocalisedText(this OperationOutcome operationOutcome) + { + switch (operationOutcome) + { + case OperationOutcome.NoTrimNecessary: + return LocaleManager.Instance[LocaleKeys.TrimXCIFileNoTrimNecessary]; + case OperationOutcome.NoUntrimPossible: + return LocaleManager.Instance[LocaleKeys.TrimXCIFileNoUntrimPossible]; + case OperationOutcome.ReadOnlyFileCannotFix: + return LocaleManager.Instance[LocaleKeys.TrimXCIFileReadOnlyFileCannotFix]; + case OperationOutcome.FreeSpaceCheckFailed: + return LocaleManager.Instance[LocaleKeys.TrimXCIFileFreeSpaceCheckFailed]; + case OperationOutcome.InvalidXCIFile: + return LocaleManager.Instance[LocaleKeys.TrimXCIFileInvalidXCIFile]; + case OperationOutcome.FileIOWriteError: + return LocaleManager.Instance[LocaleKeys.TrimXCIFileFileIOWriteError]; + case OperationOutcome.FileSizeChanged: + return LocaleManager.Instance[LocaleKeys.TrimXCIFileFileSizeChanged]; + case OperationOutcome.Cancelled: + return LocaleManager.Instance[LocaleKeys.TrimXCIFileCancelled]; + case OperationOutcome.Undetermined: + return LocaleManager.Instance[LocaleKeys.TrimXCIFileFileUndertermined]; + case OperationOutcome.Successful: + default: + return null; + } + } + } +} \ No newline at end of file diff --git a/src/Ryujinx/UI/Models/CheatNode.cs b/src/Ryujinx/UI/Models/CheatNode.cs new file mode 100644 index 000000000..8e9aee254 --- /dev/null +++ b/src/Ryujinx/UI/Models/CheatNode.cs @@ -0,0 +1,57 @@ +using Ryujinx.Ava.UI.ViewModels; +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.Linq; + +namespace Ryujinx.Ava.UI.Models +{ + public class CheatNode : BaseModel + { + private bool _isEnabled = false; + public ObservableCollection SubNodes { get; } = new(); + public string CleanName => Name[1..^7]; + public string BuildIdKey => $"{BuildId}-{Name}"; + public bool IsRootNode { get; } + public string Name { get; } + public string BuildId { get; } + public string Path { get; } + public bool IsEnabled + { + get + { + if (SubNodes.Count > 0) + { + return SubNodes.ToList().TrueForAll(x => x.IsEnabled); + } + + return _isEnabled; + } + set + { + foreach (var cheat in SubNodes) + { + cheat.IsEnabled = value; + cheat.OnPropertyChanged(); + } + + _isEnabled = value; + } + } + + public CheatNode(string name, string buildId, string path, bool isRootNode, bool isEnabled = false) + { + Name = name; + BuildId = buildId; + Path = path; + IsEnabled = isEnabled; + IsRootNode = isRootNode; + + SubNodes.CollectionChanged += CheatsList_CollectionChanged; + } + + private void CheatsList_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + OnPropertyChanged(nameof(IsEnabled)); + } + } +} diff --git a/src/Ryujinx/UI/Models/ControllerModel.cs b/src/Ryujinx/UI/Models/ControllerModel.cs new file mode 100644 index 000000000..98de7757f --- /dev/null +++ b/src/Ryujinx/UI/Models/ControllerModel.cs @@ -0,0 +1,6 @@ +using Ryujinx.Common.Configuration.Hid; + +namespace Ryujinx.Ava.UI.Models +{ + internal record ControllerModel(ControllerType Type, string Name); +} diff --git a/src/Ryujinx/UI/Models/DeviceType.cs b/src/Ryujinx/UI/Models/DeviceType.cs new file mode 100644 index 000000000..bb4fc3b30 --- /dev/null +++ b/src/Ryujinx/UI/Models/DeviceType.cs @@ -0,0 +1,9 @@ +namespace Ryujinx.Ava.UI.Models +{ + public enum DeviceType + { + None, + Keyboard, + Controller, + } +} diff --git a/src/Ryujinx/UI/Models/Generic/LastPlayedSortComparer.cs b/src/Ryujinx/UI/Models/Generic/LastPlayedSortComparer.cs new file mode 100644 index 000000000..224f78f45 --- /dev/null +++ b/src/Ryujinx/UI/Models/Generic/LastPlayedSortComparer.cs @@ -0,0 +1,31 @@ +using Ryujinx.UI.App.Common; +using System; +using System.Collections.Generic; + +namespace Ryujinx.Ava.UI.Models.Generic +{ + internal class LastPlayedSortComparer : IComparer + { + public LastPlayedSortComparer() { } + public LastPlayedSortComparer(bool isAscending) { IsAscending = isAscending; } + + public bool IsAscending { get; } + + public int Compare(ApplicationData x, ApplicationData y) + { + DateTime aValue = DateTime.UnixEpoch, bValue = DateTime.UnixEpoch; + + if (x?.LastPlayed != null) + { + aValue = x.LastPlayed.Value; + } + + if (y?.LastPlayed != null) + { + bValue = y.LastPlayed.Value; + } + + return (IsAscending ? 1 : -1) * DateTime.Compare(aValue, bValue); + } + } +} diff --git a/src/Ryujinx/UI/Models/Generic/TimePlayedSortComparer.cs b/src/Ryujinx/UI/Models/Generic/TimePlayedSortComparer.cs new file mode 100644 index 000000000..f0fb035d1 --- /dev/null +++ b/src/Ryujinx/UI/Models/Generic/TimePlayedSortComparer.cs @@ -0,0 +1,31 @@ +using Ryujinx.UI.App.Common; +using System; +using System.Collections.Generic; + +namespace Ryujinx.Ava.UI.Models.Generic +{ + internal class TimePlayedSortComparer : IComparer + { + public TimePlayedSortComparer() { } + public TimePlayedSortComparer(bool isAscending) { IsAscending = isAscending; } + + public bool IsAscending { get; } + + public int Compare(ApplicationData x, ApplicationData y) + { + TimeSpan aValue = TimeSpan.Zero, bValue = TimeSpan.Zero; + + if (x?.TimePlayed != null) + { + aValue = x.TimePlayed; + } + + if (y?.TimePlayed != null) + { + bValue = y.TimePlayed; + } + + return (IsAscending ? 1 : -1) * TimeSpan.Compare(aValue, bValue); + } + } +} diff --git a/src/Ryujinx/UI/Models/Input/GamepadInputConfig.cs b/src/Ryujinx/UI/Models/Input/GamepadInputConfig.cs new file mode 100644 index 000000000..833670bdc --- /dev/null +++ b/src/Ryujinx/UI/Models/Input/GamepadInputConfig.cs @@ -0,0 +1,580 @@ +using Ryujinx.Ava.UI.ViewModels; +using Ryujinx.Common.Configuration.Hid; +using Ryujinx.Common.Configuration.Hid.Controller; +using Ryujinx.Common.Configuration.Hid.Controller.Motion; +using System; + +namespace Ryujinx.Ava.UI.Models.Input +{ + public class GamepadInputConfig : BaseModel + { + public bool EnableCemuHookMotion { get; set; } + public string DsuServerHost { get; set; } + public int DsuServerPort { get; set; } + public int Slot { get; set; } + public int AltSlot { get; set; } + public bool MirrorInput { get; set; } + public int Sensitivity { get; set; } + public double GyroDeadzone { get; set; } + + public float WeakRumble { get; set; } + public float StrongRumble { get; set; } + + public string Id { get; set; } + public ControllerType ControllerType { get; set; } + public PlayerIndex PlayerIndex { get; set; } + + private StickInputId _leftJoystick; + public StickInputId LeftJoystick + { + get => _leftJoystick; + set + { + _leftJoystick = value; + OnPropertyChanged(); + } + } + + private bool _leftInvertStickX; + public bool LeftInvertStickX + { + get => _leftInvertStickX; + set + { + _leftInvertStickX = value; + OnPropertyChanged(); + } + } + + private bool _leftInvertStickY; + public bool LeftInvertStickY + { + get => _leftInvertStickY; + set + { + _leftInvertStickY = value; + OnPropertyChanged(); + } + } + + private bool _leftRotate90; + public bool LeftRotate90 + { + get => _leftRotate90; + set + { + _leftRotate90 = value; + OnPropertyChanged(); + } + } + + private GamepadInputId _leftStickButton; + public GamepadInputId LeftStickButton + { + get => _leftStickButton; + set + { + _leftStickButton = value; + OnPropertyChanged(); + } + } + + private StickInputId _rightJoystick; + public StickInputId RightJoystick + { + get => _rightJoystick; + set + { + _rightJoystick = value; + OnPropertyChanged(); + } + } + + private bool _rightInvertStickX; + public bool RightInvertStickX + { + get => _rightInvertStickX; + set + { + _rightInvertStickX = value; + OnPropertyChanged(); + } + } + + private bool _rightInvertStickY; + public bool RightInvertStickY + { + get => _rightInvertStickY; + set + { + _rightInvertStickY = value; + OnPropertyChanged(); + } + } + + private bool _rightRotate90; + public bool RightRotate90 + { + get => _rightRotate90; + set + { + _rightRotate90 = value; + OnPropertyChanged(); + } + } + + private GamepadInputId _rightStickButton; + public GamepadInputId RightStickButton + { + get => _rightStickButton; + set + { + _rightStickButton = value; + OnPropertyChanged(); + } + } + + private GamepadInputId _dpadUp; + public GamepadInputId DpadUp + { + get => _dpadUp; + set + { + _dpadUp = value; + OnPropertyChanged(); + } + } + + private GamepadInputId _dpadDown; + public GamepadInputId DpadDown + { + get => _dpadDown; + set + { + _dpadDown = value; + OnPropertyChanged(); + } + } + + private GamepadInputId _dpadLeft; + public GamepadInputId DpadLeft + { + get => _dpadLeft; + set + { + _dpadLeft = value; + OnPropertyChanged(); + } + } + + private GamepadInputId _dpadRight; + public GamepadInputId DpadRight + { + get => _dpadRight; + set + { + _dpadRight = value; + OnPropertyChanged(); + } + } + + private GamepadInputId _buttonL; + public GamepadInputId ButtonL + { + get => _buttonL; + set + { + _buttonL = value; + OnPropertyChanged(); + } + } + + private GamepadInputId _buttonMinus; + public GamepadInputId ButtonMinus + { + get => _buttonMinus; + set + { + _buttonMinus = value; + OnPropertyChanged(); + } + } + + private GamepadInputId _leftButtonSl; + public GamepadInputId LeftButtonSl + { + get => _leftButtonSl; + set + { + _leftButtonSl = value; + OnPropertyChanged(); + } + } + + private GamepadInputId _leftButtonSr; + public GamepadInputId LeftButtonSr + { + get => _leftButtonSr; + set + { + _leftButtonSr = value; + OnPropertyChanged(); + } + } + + private GamepadInputId _buttonZl; + public GamepadInputId ButtonZl + { + get => _buttonZl; + set + { + _buttonZl = value; + OnPropertyChanged(); + } + } + + private GamepadInputId _buttonA; + public GamepadInputId ButtonA + { + get => _buttonA; + set + { + _buttonA = value; + OnPropertyChanged(); + } + } + + private GamepadInputId _buttonB; + public GamepadInputId ButtonB + { + get => _buttonB; + set + { + _buttonB = value; + OnPropertyChanged(); + } + } + + private GamepadInputId _buttonX; + public GamepadInputId ButtonX + { + get => _buttonX; + set + { + _buttonX = value; + OnPropertyChanged(); + } + } + + private GamepadInputId _buttonY; + public GamepadInputId ButtonY + { + get => _buttonY; + set + { + _buttonY = value; + OnPropertyChanged(); + } + } + + private GamepadInputId _buttonR; + public GamepadInputId ButtonR + { + get => _buttonR; + set + { + _buttonR = value; + OnPropertyChanged(); + } + } + + private GamepadInputId _buttonPlus; + public GamepadInputId ButtonPlus + { + get => _buttonPlus; + set + { + _buttonPlus = value; + OnPropertyChanged(); + } + } + + private GamepadInputId _rightButtonSl; + public GamepadInputId RightButtonSl + { + get => _rightButtonSl; + set + { + _rightButtonSl = value; + OnPropertyChanged(); + } + } + + private GamepadInputId _rightButtonSr; + public GamepadInputId RightButtonSr + { + get => _rightButtonSr; + set + { + _rightButtonSr = value; + OnPropertyChanged(); + } + } + + private GamepadInputId _buttonZr; + public GamepadInputId ButtonZr + { + get => _buttonZr; + set + { + _buttonZr = value; + OnPropertyChanged(); + } + } + + private float _deadzoneLeft; + public float DeadzoneLeft + { + get => _deadzoneLeft; + set + { + _deadzoneLeft = MathF.Round(value, 3); + OnPropertyChanged(); + } + } + + private float _deadzoneRight; + public float DeadzoneRight + { + get => _deadzoneRight; + set + { + _deadzoneRight = MathF.Round(value, 3); + OnPropertyChanged(); + } + } + + private float _rangeLeft; + public float RangeLeft + { + get => _rangeLeft; + set + { + _rangeLeft = MathF.Round(value, 3); + OnPropertyChanged(); + } + } + + private float _rangeRight; + public float RangeRight + { + get => _rangeRight; + set + { + _rangeRight = MathF.Round(value, 3); + OnPropertyChanged(); + } + } + + private float _triggerThreshold; + public float TriggerThreshold + { + get => _triggerThreshold; + set + { + _triggerThreshold = MathF.Round(value, 3); + OnPropertyChanged(); + } + } + + private bool _enableMotion; + public bool EnableMotion + { + get => _enableMotion; + set + { + _enableMotion = value; + OnPropertyChanged(); + } + } + + private bool _enableRumble; + public bool EnableRumble + { + get => _enableRumble; + set + { + _enableRumble = value; + OnPropertyChanged(); + } + } + + public GamepadInputConfig(InputConfig config) + { + if (config != null) + { + Id = config.Id; + ControllerType = config.ControllerType; + PlayerIndex = config.PlayerIndex; + + if (config is not StandardControllerInputConfig controllerInput) + { + return; + } + + LeftJoystick = controllerInput.LeftJoyconStick.Joystick; + LeftInvertStickX = controllerInput.LeftJoyconStick.InvertStickX; + LeftInvertStickY = controllerInput.LeftJoyconStick.InvertStickY; + LeftRotate90 = controllerInput.LeftJoyconStick.Rotate90CW; + LeftStickButton = controllerInput.LeftJoyconStick.StickButton; + + RightJoystick = controllerInput.RightJoyconStick.Joystick; + RightInvertStickX = controllerInput.RightJoyconStick.InvertStickX; + RightInvertStickY = controllerInput.RightJoyconStick.InvertStickY; + RightRotate90 = controllerInput.RightJoyconStick.Rotate90CW; + RightStickButton = controllerInput.RightJoyconStick.StickButton; + + DpadUp = controllerInput.LeftJoycon.DpadUp; + DpadDown = controllerInput.LeftJoycon.DpadDown; + DpadLeft = controllerInput.LeftJoycon.DpadLeft; + DpadRight = controllerInput.LeftJoycon.DpadRight; + ButtonL = controllerInput.LeftJoycon.ButtonL; + ButtonMinus = controllerInput.LeftJoycon.ButtonMinus; + LeftButtonSl = controllerInput.LeftJoycon.ButtonSl; + LeftButtonSr = controllerInput.LeftJoycon.ButtonSr; + ButtonZl = controllerInput.LeftJoycon.ButtonZl; + + ButtonA = controllerInput.RightJoycon.ButtonA; + ButtonB = controllerInput.RightJoycon.ButtonB; + ButtonX = controllerInput.RightJoycon.ButtonX; + ButtonY = controllerInput.RightJoycon.ButtonY; + ButtonR = controllerInput.RightJoycon.ButtonR; + ButtonPlus = controllerInput.RightJoycon.ButtonPlus; + RightButtonSl = controllerInput.RightJoycon.ButtonSl; + RightButtonSr = controllerInput.RightJoycon.ButtonSr; + ButtonZr = controllerInput.RightJoycon.ButtonZr; + + DeadzoneLeft = controllerInput.DeadzoneLeft; + DeadzoneRight = controllerInput.DeadzoneRight; + RangeLeft = controllerInput.RangeLeft; + RangeRight = controllerInput.RangeRight; + TriggerThreshold = controllerInput.TriggerThreshold; + + if (controllerInput.Motion != null) + { + EnableMotion = controllerInput.Motion.EnableMotion; + GyroDeadzone = controllerInput.Motion.GyroDeadzone; + Sensitivity = controllerInput.Motion.Sensitivity; + + if (controllerInput.Motion is CemuHookMotionConfigController cemuHook) + { + EnableCemuHookMotion = true; + DsuServerHost = cemuHook.DsuServerHost; + DsuServerPort = cemuHook.DsuServerPort; + Slot = cemuHook.Slot; + AltSlot = cemuHook.AltSlot; + MirrorInput = cemuHook.MirrorInput; + } + } + + if (controllerInput.Rumble != null) + { + EnableRumble = controllerInput.Rumble.EnableRumble; + WeakRumble = controllerInput.Rumble.WeakRumble; + StrongRumble = controllerInput.Rumble.StrongRumble; + } + } + } + + public InputConfig GetConfig() + { + var config = new StandardControllerInputConfig + { + Id = Id, + Backend = InputBackendType.GamepadSDL2, + PlayerIndex = PlayerIndex, + ControllerType = ControllerType, + LeftJoycon = new LeftJoyconCommonConfig + { + DpadUp = DpadUp, + DpadDown = DpadDown, + DpadLeft = DpadLeft, + DpadRight = DpadRight, + ButtonL = ButtonL, + ButtonMinus = ButtonMinus, + ButtonSl = LeftButtonSl, + ButtonSr = LeftButtonSr, + ButtonZl = ButtonZl, + }, + RightJoycon = new RightJoyconCommonConfig + { + ButtonA = ButtonA, + ButtonB = ButtonB, + ButtonX = ButtonX, + ButtonY = ButtonY, + ButtonPlus = ButtonPlus, + ButtonSl = RightButtonSl, + ButtonSr = RightButtonSr, + ButtonR = ButtonR, + ButtonZr = ButtonZr, + }, + LeftJoyconStick = new JoyconConfigControllerStick + { + Joystick = LeftJoystick, + InvertStickX = LeftInvertStickX, + InvertStickY = LeftInvertStickY, + Rotate90CW = LeftRotate90, + StickButton = LeftStickButton, + }, + RightJoyconStick = new JoyconConfigControllerStick + { + Joystick = RightJoystick, + InvertStickX = RightInvertStickX, + InvertStickY = RightInvertStickY, + Rotate90CW = RightRotate90, + StickButton = RightStickButton, + }, + Rumble = new RumbleConfigController + { + EnableRumble = EnableRumble, + WeakRumble = WeakRumble, + StrongRumble = StrongRumble, + }, + Version = InputConfig.CurrentVersion, + DeadzoneLeft = DeadzoneLeft, + DeadzoneRight = DeadzoneRight, + RangeLeft = RangeLeft, + RangeRight = RangeRight, + TriggerThreshold = TriggerThreshold, + }; + + if (EnableCemuHookMotion) + { + config.Motion = new CemuHookMotionConfigController + { + EnableMotion = EnableMotion, + MotionBackend = MotionInputBackendType.CemuHook, + GyroDeadzone = GyroDeadzone, + Sensitivity = Sensitivity, + DsuServerHost = DsuServerHost, + DsuServerPort = DsuServerPort, + Slot = Slot, + AltSlot = AltSlot, + MirrorInput = MirrorInput, + }; + } + else + { + config.Motion = new StandardMotionConfigController + { + EnableMotion = EnableMotion, + MotionBackend = MotionInputBackendType.GamepadDriver, + GyroDeadzone = GyroDeadzone, + Sensitivity = Sensitivity, + }; + } + + return config; + } + } +} diff --git a/src/Ryujinx/UI/Models/Input/HotkeyConfig.cs b/src/Ryujinx/UI/Models/Input/HotkeyConfig.cs new file mode 100644 index 000000000..4c7a6bd02 --- /dev/null +++ b/src/Ryujinx/UI/Models/Input/HotkeyConfig.cs @@ -0,0 +1,167 @@ +using Ryujinx.Ava.UI.ViewModels; +using Ryujinx.Common.Configuration.Hid; + +namespace Ryujinx.Ava.UI.Models.Input +{ + public class HotkeyConfig : BaseModel + { + private Key _toggleVSyncMode; + public Key ToggleVSyncMode + { + get => _toggleVSyncMode; + set + { + _toggleVSyncMode = value; + OnPropertyChanged(); + } + } + + private Key _screenshot; + public Key Screenshot + { + get => _screenshot; + set + { + _screenshot = value; + OnPropertyChanged(); + } + } + + private Key _showUI; + public Key ShowUI + { + get => _showUI; + set + { + _showUI = value; + OnPropertyChanged(); + } + } + + private Key _pause; + public Key Pause + { + get => _pause; + set + { + _pause = value; + OnPropertyChanged(); + } + } + + private Key _toggleMute; + public Key ToggleMute + { + get => _toggleMute; + set + { + _toggleMute = value; + OnPropertyChanged(); + } + } + + private Key _resScaleUp; + public Key ResScaleUp + { + get => _resScaleUp; + set + { + _resScaleUp = value; + OnPropertyChanged(); + } + } + + private Key _resScaleDown; + public Key ResScaleDown + { + get => _resScaleDown; + set + { + _resScaleDown = value; + OnPropertyChanged(); + } + } + + private Key _volumeUp; + public Key VolumeUp + { + get => _volumeUp; + set + { + _volumeUp = value; + OnPropertyChanged(); + } + } + + private Key _volumeDown; + public Key VolumeDown + { + get => _volumeDown; + set + { + _volumeDown = value; + OnPropertyChanged(); + } + } + + private Key _customVSyncIntervalIncrement; + public Key CustomVSyncIntervalIncrement + { + get => _customVSyncIntervalIncrement; + set + { + _customVSyncIntervalIncrement = value; + OnPropertyChanged(); + } + } + + private Key _customVSyncIntervalDecrement; + public Key CustomVSyncIntervalDecrement + { + get => _customVSyncIntervalDecrement; + set + { + _customVSyncIntervalDecrement = value; + OnPropertyChanged(); + } + } + + public HotkeyConfig(KeyboardHotkeys config) + { + if (config != null) + { + ToggleVSyncMode = config.ToggleVSyncMode; + Screenshot = config.Screenshot; + ShowUI = config.ShowUI; + Pause = config.Pause; + ToggleMute = config.ToggleMute; + ResScaleUp = config.ResScaleUp; + ResScaleDown = config.ResScaleDown; + VolumeUp = config.VolumeUp; + VolumeDown = config.VolumeDown; + CustomVSyncIntervalIncrement = config.CustomVSyncIntervalIncrement; + CustomVSyncIntervalDecrement = config.CustomVSyncIntervalDecrement; + } + } + + public KeyboardHotkeys GetConfig() + { + var config = new KeyboardHotkeys + { + ToggleVSyncMode = ToggleVSyncMode, + Screenshot = Screenshot, + ShowUI = ShowUI, + Pause = Pause, + ToggleMute = ToggleMute, + ResScaleUp = ResScaleUp, + ResScaleDown = ResScaleDown, + VolumeUp = VolumeUp, + VolumeDown = VolumeDown, + CustomVSyncIntervalIncrement = CustomVSyncIntervalIncrement, + CustomVSyncIntervalDecrement = CustomVSyncIntervalDecrement, + }; + + return config; + } + } +} diff --git a/src/Ryujinx/UI/Models/Input/KeyboardInputConfig.cs b/src/Ryujinx/UI/Models/Input/KeyboardInputConfig.cs new file mode 100644 index 000000000..66f1f62a2 --- /dev/null +++ b/src/Ryujinx/UI/Models/Input/KeyboardInputConfig.cs @@ -0,0 +1,422 @@ +using Ryujinx.Ava.UI.ViewModels; +using Ryujinx.Common.Configuration.Hid; +using Ryujinx.Common.Configuration.Hid.Keyboard; + +namespace Ryujinx.Ava.UI.Models.Input +{ + public class KeyboardInputConfig : BaseModel + { + public string Id { get; set; } + public ControllerType ControllerType { get; set; } + public PlayerIndex PlayerIndex { get; set; } + + private Key _leftStickUp; + public Key LeftStickUp + { + get => _leftStickUp; + set + { + _leftStickUp = value; + OnPropertyChanged(); + } + } + + private Key _leftStickDown; + public Key LeftStickDown + { + get => _leftStickDown; + set + { + _leftStickDown = value; + OnPropertyChanged(); + } + } + + private Key _leftStickLeft; + public Key LeftStickLeft + { + get => _leftStickLeft; + set + { + _leftStickLeft = value; + OnPropertyChanged(); + } + } + + private Key _leftStickRight; + public Key LeftStickRight + { + get => _leftStickRight; + set + { + _leftStickRight = value; + OnPropertyChanged(); + } + } + + private Key _leftStickButton; + public Key LeftStickButton + { + get => _leftStickButton; + set + { + _leftStickButton = value; + OnPropertyChanged(); + } + } + + private Key _rightStickUp; + public Key RightStickUp + { + get => _rightStickUp; + set + { + _rightStickUp = value; + OnPropertyChanged(); + } + } + + private Key _rightStickDown; + public Key RightStickDown + { + get => _rightStickDown; + set + { + _rightStickDown = value; + OnPropertyChanged(); + } + } + + private Key _rightStickLeft; + public Key RightStickLeft + { + get => _rightStickLeft; + set + { + _rightStickLeft = value; + OnPropertyChanged(); + } + } + + private Key _rightStickRight; + public Key RightStickRight + { + get => _rightStickRight; + set + { + _rightStickRight = value; + OnPropertyChanged(); + } + } + + private Key _rightStickButton; + public Key RightStickButton + { + get => _rightStickButton; + set + { + _rightStickButton = value; + OnPropertyChanged(); + } + } + + private Key _dpadUp; + public Key DpadUp + { + get => _dpadUp; + set + { + _dpadUp = value; + OnPropertyChanged(); + } + } + + private Key _dpadDown; + public Key DpadDown + { + get => _dpadDown; + set + { + _dpadDown = value; + OnPropertyChanged(); + } + } + + private Key _dpadLeft; + public Key DpadLeft + { + get => _dpadLeft; + set + { + _dpadLeft = value; + OnPropertyChanged(); + } + } + + private Key _dpadRight; + public Key DpadRight + { + get => _dpadRight; + set + { + _dpadRight = value; + OnPropertyChanged(); + } + } + + private Key _buttonL; + public Key ButtonL + { + get => _buttonL; + set + { + _buttonL = value; + OnPropertyChanged(); + } + } + + private Key _buttonMinus; + public Key ButtonMinus + { + get => _buttonMinus; + set + { + _buttonMinus = value; + OnPropertyChanged(); + } + } + + private Key _leftButtonSl; + public Key LeftButtonSl + { + get => _leftButtonSl; + set + { + _leftButtonSl = value; + OnPropertyChanged(); + } + } + + private Key _leftButtonSr; + public Key LeftButtonSr + { + get => _leftButtonSr; + set + { + _leftButtonSr = value; + OnPropertyChanged(); + } + } + + private Key _buttonZl; + public Key ButtonZl + { + get => _buttonZl; + set + { + _buttonZl = value; + OnPropertyChanged(); + } + } + + private Key _buttonA; + public Key ButtonA + { + get => _buttonA; + set + { + _buttonA = value; + OnPropertyChanged(); + } + } + + private Key _buttonB; + public Key ButtonB + { + get => _buttonB; + set + { + _buttonB = value; + OnPropertyChanged(); + } + } + + private Key _buttonX; + public Key ButtonX + { + get => _buttonX; + set + { + _buttonX = value; + OnPropertyChanged(); + } + } + + private Key _buttonY; + public Key ButtonY + { + get => _buttonY; + set + { + _buttonY = value; + OnPropertyChanged(); + } + } + + private Key _buttonR; + public Key ButtonR + { + get => _buttonR; + set + { + _buttonR = value; + OnPropertyChanged(); + } + } + + private Key _buttonPlus; + public Key ButtonPlus + { + get => _buttonPlus; + set + { + _buttonPlus = value; + OnPropertyChanged(); + } + } + + private Key _rightButtonSl; + public Key RightButtonSl + { + get => _rightButtonSl; + set + { + _rightButtonSl = value; + OnPropertyChanged(); + } + } + + private Key _rightButtonSr; + public Key RightButtonSr + { + get => _rightButtonSr; + set + { + _rightButtonSr = value; + OnPropertyChanged(); + } + } + + private Key _buttonZr; + public Key ButtonZr + { + get => _buttonZr; + set + { + _buttonZr = value; + OnPropertyChanged(); + } + } + + public KeyboardInputConfig(InputConfig config) + { + if (config != null) + { + Id = config.Id; + ControllerType = config.ControllerType; + PlayerIndex = config.PlayerIndex; + + if (config is not StandardKeyboardInputConfig keyboardConfig) + { + return; + } + + LeftStickUp = keyboardConfig.LeftJoyconStick.StickUp; + LeftStickDown = keyboardConfig.LeftJoyconStick.StickDown; + LeftStickLeft = keyboardConfig.LeftJoyconStick.StickLeft; + LeftStickRight = keyboardConfig.LeftJoyconStick.StickRight; + LeftStickButton = keyboardConfig.LeftJoyconStick.StickButton; + + RightStickUp = keyboardConfig.RightJoyconStick.StickUp; + RightStickDown = keyboardConfig.RightJoyconStick.StickDown; + RightStickLeft = keyboardConfig.RightJoyconStick.StickLeft; + RightStickRight = keyboardConfig.RightJoyconStick.StickRight; + RightStickButton = keyboardConfig.RightJoyconStick.StickButton; + + DpadUp = keyboardConfig.LeftJoycon.DpadUp; + DpadDown = keyboardConfig.LeftJoycon.DpadDown; + DpadLeft = keyboardConfig.LeftJoycon.DpadLeft; + DpadRight = keyboardConfig.LeftJoycon.DpadRight; + ButtonL = keyboardConfig.LeftJoycon.ButtonL; + ButtonMinus = keyboardConfig.LeftJoycon.ButtonMinus; + LeftButtonSl = keyboardConfig.LeftJoycon.ButtonSl; + LeftButtonSr = keyboardConfig.LeftJoycon.ButtonSr; + ButtonZl = keyboardConfig.LeftJoycon.ButtonZl; + + ButtonA = keyboardConfig.RightJoycon.ButtonA; + ButtonB = keyboardConfig.RightJoycon.ButtonB; + ButtonX = keyboardConfig.RightJoycon.ButtonX; + ButtonY = keyboardConfig.RightJoycon.ButtonY; + ButtonR = keyboardConfig.RightJoycon.ButtonR; + ButtonPlus = keyboardConfig.RightJoycon.ButtonPlus; + RightButtonSl = keyboardConfig.RightJoycon.ButtonSl; + RightButtonSr = keyboardConfig.RightJoycon.ButtonSr; + ButtonZr = keyboardConfig.RightJoycon.ButtonZr; + } + } + + public InputConfig GetConfig() + { + var config = new StandardKeyboardInputConfig + { + Id = Id, + Backend = InputBackendType.WindowKeyboard, + PlayerIndex = PlayerIndex, + ControllerType = ControllerType, + LeftJoycon = new LeftJoyconCommonConfig + { + DpadUp = DpadUp, + DpadDown = DpadDown, + DpadLeft = DpadLeft, + DpadRight = DpadRight, + ButtonL = ButtonL, + ButtonMinus = ButtonMinus, + ButtonZl = ButtonZl, + ButtonSl = LeftButtonSl, + ButtonSr = LeftButtonSr, + }, + RightJoycon = new RightJoyconCommonConfig + { + ButtonA = ButtonA, + ButtonB = ButtonB, + ButtonX = ButtonX, + ButtonY = ButtonY, + ButtonPlus = ButtonPlus, + ButtonSl = RightButtonSl, + ButtonSr = RightButtonSr, + ButtonR = ButtonR, + ButtonZr = ButtonZr, + }, + LeftJoyconStick = new JoyconConfigKeyboardStick + { + StickUp = LeftStickUp, + StickDown = LeftStickDown, + StickRight = LeftStickRight, + StickLeft = LeftStickLeft, + StickButton = LeftStickButton, + }, + RightJoyconStick = new JoyconConfigKeyboardStick + { + StickUp = RightStickUp, + StickDown = RightStickDown, + StickLeft = RightStickLeft, + StickRight = RightStickRight, + StickButton = RightStickButton, + }, + Version = InputConfig.CurrentVersion, + }; + + return config; + } + } +} diff --git a/src/Ryujinx/UI/Models/ModModel.cs b/src/Ryujinx/UI/Models/ModModel.cs new file mode 100644 index 000000000..ee28ca5f5 --- /dev/null +++ b/src/Ryujinx/UI/Models/ModModel.cs @@ -0,0 +1,32 @@ +using Ryujinx.Ava.UI.ViewModels; +using System.IO; + +namespace Ryujinx.Ava.UI.Models +{ + public class ModModel : BaseModel + { + private bool _enabled; + + public bool Enabled + { + get => _enabled; + set + { + _enabled = value; + OnPropertyChanged(); + } + } + + public bool InSd { get; } + public string Path { get; } + public string Name { get; } + + public ModModel(string path, string name, bool enabled, bool inSd) + { + Path = path; + Name = name; + Enabled = enabled; + InSd = inSd; + } + } +} diff --git a/src/Ryujinx/UI/Models/PlayerModel.cs b/src/Ryujinx/UI/Models/PlayerModel.cs new file mode 100644 index 000000000..a19852b94 --- /dev/null +++ b/src/Ryujinx/UI/Models/PlayerModel.cs @@ -0,0 +1,6 @@ +using Ryujinx.Common.Configuration.Hid; + +namespace Ryujinx.Ava.UI.Models +{ + public record PlayerModel(PlayerIndex Id, string Name); +} diff --git a/src/Ryujinx/UI/Models/ProfileImageModel.cs b/src/Ryujinx/UI/Models/ProfileImageModel.cs new file mode 100644 index 000000000..99365dfc7 --- /dev/null +++ b/src/Ryujinx/UI/Models/ProfileImageModel.cs @@ -0,0 +1,32 @@ +using Avalonia.Media; +using Ryujinx.Ava.UI.ViewModels; + +namespace Ryujinx.Ava.UI.Models +{ + public class ProfileImageModel : BaseModel + { + public ProfileImageModel(string name, byte[] data) + { + Name = name; + Data = data; + } + + public string Name { get; set; } + public byte[] Data { get; set; } + + private SolidColorBrush _backgroundColor = new(Colors.White); + + public SolidColorBrush BackgroundColor + { + get + { + return _backgroundColor; + } + set + { + _backgroundColor = value; + OnPropertyChanged(); + } + } + } +} diff --git a/src/Ryujinx/UI/Models/SaveModel.cs b/src/Ryujinx/UI/Models/SaveModel.cs new file mode 100644 index 000000000..cfc397c6e --- /dev/null +++ b/src/Ryujinx/UI/Models/SaveModel.cs @@ -0,0 +1,97 @@ +using LibHac.Fs; +using LibHac.Ncm; +using Ryujinx.Ava.UI.ViewModels; +using Ryujinx.Ava.UI.Windows; +using Ryujinx.HLE.FileSystem; +using Ryujinx.UI.App.Common; +using Ryujinx.UI.Common.Helper; +using System; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Path = System.IO.Path; + +namespace Ryujinx.Ava.UI.Models +{ + public class SaveModel : BaseModel + { + private long _size; + + public ulong SaveId { get; } + public ProgramId TitleId { get; } + public string TitleIdString => TitleId.ToString(); + public UserId UserId { get; } + public bool InGameList { get; } + public string Title { get; } + public byte[] Icon { get; } + + public long Size + { + get => _size; set + { + _size = value; + SizeAvailable = true; + OnPropertyChanged(); + OnPropertyChanged(nameof(SizeString)); + OnPropertyChanged(nameof(SizeAvailable)); + } + } + + public bool SizeAvailable { get; set; } + + public string SizeString => ValueFormatUtils.FormatFileSize(Size); + + public SaveModel(SaveDataInfo info) + { + SaveId = info.SaveDataId; + TitleId = info.ProgramId; + UserId = info.UserId; + + var appData = MainWindow.MainWindowViewModel.Applications.FirstOrDefault(x => x.IdString.Equals(TitleIdString, StringComparison.OrdinalIgnoreCase)); + + InGameList = appData != null; + + if (InGameList) + { + Icon = appData.Icon; + Title = appData.Name; + } + else + { + var appMetadata = ApplicationLibrary.LoadAndSaveMetaData(TitleIdString); + Title = appMetadata.Title ?? TitleIdString; + } + + Task.Run(() => + { + var saveRoot = Path.Combine(VirtualFileSystem.GetNandPath(), $"user/save/{info.SaveDataId:x16}"); + + long totalSize = GetDirectorySize(saveRoot); + + static long GetDirectorySize(string path) + { + long size = 0; + if (Directory.Exists(path)) + { + var directories = Directory.GetDirectories(path); + foreach (var directory in directories) + { + size += GetDirectorySize(directory); + } + + var files = Directory.GetFiles(path); + foreach (var file in files) + { + size += new FileInfo(file).Length; + } + } + + return size; + } + + Size = totalSize; + }); + + } + } +} diff --git a/src/Ryujinx/UI/Models/StatusUpdatedEventArgs.cs b/src/Ryujinx/UI/Models/StatusUpdatedEventArgs.cs new file mode 100644 index 000000000..6f0f5ab5d --- /dev/null +++ b/src/Ryujinx/UI/Models/StatusUpdatedEventArgs.cs @@ -0,0 +1,26 @@ +using System; + +namespace Ryujinx.Ava.UI.Models +{ + internal class StatusUpdatedEventArgs : EventArgs + { + public string VSyncMode { get; } + public string VolumeStatus { get; } + public string AspectRatio { get; } + public string DockedMode { get; } + public string FifoStatus { get; } + public string GameStatus { get; } + public uint ShaderCount { get; } + + public StatusUpdatedEventArgs(string vSyncMode, string volumeStatus, string dockedMode, string aspectRatio, string gameStatus, string fifoStatus, uint shaderCount) + { + VSyncMode = vSyncMode; + VolumeStatus = volumeStatus; + DockedMode = dockedMode; + AspectRatio = aspectRatio; + GameStatus = gameStatus; + FifoStatus = fifoStatus; + ShaderCount = shaderCount; + } + } +} diff --git a/src/Ryujinx/UI/Models/TempProfile.cs b/src/Ryujinx/UI/Models/TempProfile.cs new file mode 100644 index 000000000..659092e6d --- /dev/null +++ b/src/Ryujinx/UI/Models/TempProfile.cs @@ -0,0 +1,61 @@ +using Ryujinx.Ava.UI.ViewModels; +using Ryujinx.HLE.HOS.Services.Account.Acc; +using System; + +namespace Ryujinx.Ava.UI.Models +{ + public class TempProfile : BaseModel + { + private readonly UserProfile _profile; + private byte[] _image; + private string _name = String.Empty; + private UserId _userId; + + public static uint MaxProfileNameLength => 0x20; + + public byte[] Image + { + get => _image; + set + { + _image = value; + OnPropertyChanged(); + } + } + + public UserId UserId + { + get => _userId; + set + { + _userId = value; + OnPropertyChanged(); + OnPropertyChanged(nameof(UserIdString)); + } + } + + public string UserIdString => _userId.ToString(); + + public string Name + { + get => _name; + set + { + _name = value; + OnPropertyChanged(); + } + } + + public TempProfile(UserProfile profile) + { + _profile = profile; + + if (_profile != null) + { + Image = profile.Image; + Name = profile.Name; + UserId = profile.UserId; + } + } + } +} diff --git a/src/Ryujinx/UI/Models/TimeZone.cs b/src/Ryujinx/UI/Models/TimeZone.cs new file mode 100644 index 000000000..950fbce43 --- /dev/null +++ b/src/Ryujinx/UI/Models/TimeZone.cs @@ -0,0 +1,16 @@ +namespace Ryujinx.Ava.UI.Models +{ + internal class TimeZone + { + public TimeZone(string utcDifference, string location, string abbreviation) + { + UtcDifference = utcDifference; + Location = location; + Abbreviation = abbreviation; + } + + public string UtcDifference { get; set; } + public string Location { get; set; } + public string Abbreviation { get; set; } + } +} diff --git a/src/Ryujinx/UI/Models/UserProfile.cs b/src/Ryujinx/UI/Models/UserProfile.cs new file mode 100644 index 000000000..7a9237fe1 --- /dev/null +++ b/src/Ryujinx/UI/Models/UserProfile.cs @@ -0,0 +1,104 @@ +using Avalonia.Media; +using Ryujinx.Ava.UI.Controls; +using Ryujinx.Ava.UI.ViewModels; +using Ryujinx.Ava.UI.Views.User; +using Ryujinx.HLE.HOS.Services.Account.Acc; +using Profile = Ryujinx.HLE.HOS.Services.Account.Acc.UserProfile; + +namespace Ryujinx.Ava.UI.Models +{ + public class UserProfile : BaseModel + { + private readonly Profile _profile; + private readonly NavigationDialogHost _owner; + private byte[] _image; + private string _name; + private UserId _userId; + private bool _isPointerOver; + private IBrush _backgroundColor; + + public byte[] Image + { + get => _image; + set + { + _image = value; + OnPropertyChanged(); + } + } + + public UserId UserId + { + get => _userId; + set + { + _userId = value; + OnPropertyChanged(); + } + } + + public string Name + { + get => _name; + set + { + _name = value; + OnPropertyChanged(); + } + } + + public bool IsPointerOver + { + get => _isPointerOver; + set + { + _isPointerOver = value; + OnPropertyChanged(); + } + } + + public IBrush BackgroundColor + { + get => _backgroundColor; + set + { + _backgroundColor = value; + OnPropertyChanged(); + } + } + + public UserProfile(Profile profile, NavigationDialogHost owner) + { + _profile = profile; + _owner = owner; + + UpdateBackground(); + + Image = profile.Image; + Name = profile.Name; + UserId = profile.UserId; + } + + public void UpdateState() + { + UpdateBackground(); + OnPropertyChanged(nameof(Name)); + } + + private void UpdateBackground() + { + var currentApplication = Avalonia.Application.Current; + currentApplication.Styles.TryGetResource("ControlFillColorSecondary", currentApplication.ActualThemeVariant, out object color); + + if (color is not null) + { + BackgroundColor = _profile.AccountState == AccountState.Open ? new SolidColorBrush((Color)color) : Brushes.Transparent; + } + } + + public void Recover(UserProfile userProfile) + { + _owner.Navigate(typeof(UserEditorView), (_owner, userProfile, true)); + } + } +} diff --git a/src/Ryujinx/UI/Renderer/EmbeddedWindow.cs b/src/Ryujinx/UI/Renderer/EmbeddedWindow.cs new file mode 100644 index 000000000..ea5a8dbdd --- /dev/null +++ b/src/Ryujinx/UI/Renderer/EmbeddedWindow.cs @@ -0,0 +1,226 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Platform; +using Ryujinx.Common.Configuration; +using Ryujinx.UI.Common.Configuration; +using Ryujinx.UI.Common.Helper; +using SPB.Graphics; +using SPB.Platform; +using SPB.Platform.GLX; +using SPB.Platform.X11; +using SPB.Windowing; +using System; +using System.Runtime.InteropServices; +using System.Runtime.Versioning; +using System.Threading.Tasks; +using static Ryujinx.Ava.UI.Helpers.Win32NativeInterop; + +namespace Ryujinx.Ava.UI.Renderer +{ + public class EmbeddedWindow : NativeControlHost + { + private WindowProc _wndProcDelegate; + private string _className; + + protected GLXWindow X11Window { get; set; } + + protected nint WindowHandle { get; set; } + protected nint X11Display { get; set; } + protected nint NsView { get; set; } + protected nint MetalLayer { get; set; } + + public delegate void UpdateBoundsCallbackDelegate(Rect rect); + private UpdateBoundsCallbackDelegate _updateBoundsCallback; + + public event EventHandler WindowCreated; + public event EventHandler BoundsChanged; + + public EmbeddedWindow() + { + this.GetObservable(BoundsProperty).Subscribe(StateChanged); + + Initialized += OnNativeEmbeddedWindowCreated; + } + + public virtual void OnWindowCreated() { } + + protected virtual void OnWindowDestroyed() { } + + protected virtual void OnWindowDestroying() + { + WindowHandle = nint.Zero; + X11Display = nint.Zero; + NsView = nint.Zero; + MetalLayer = nint.Zero; + } + + private void OnNativeEmbeddedWindowCreated(object sender, EventArgs e) + { + OnWindowCreated(); + + Task.Run(() => + { + WindowCreated?.Invoke(this, WindowHandle); + }); + } + + private void StateChanged(Rect rect) + { + BoundsChanged?.Invoke(this, rect.Size); + _updateBoundsCallback?.Invoke(rect); + } + + protected override IPlatformHandle CreateNativeControlCore(IPlatformHandle control) + { + if (OperatingSystem.IsLinux()) + { + return CreateLinux(control); + } + + if (OperatingSystem.IsWindows()) + { + return CreateWin32(control); + } + + if (OperatingSystem.IsMacOS()) + { + return CreateMacOS(); + } + + return base.CreateNativeControlCore(control); + } + + protected override void DestroyNativeControlCore(IPlatformHandle control) + { + OnWindowDestroying(); + + if (OperatingSystem.IsLinux()) + { + DestroyLinux(); + } + else if (OperatingSystem.IsWindows()) + { + DestroyWin32(control); + } + else if (OperatingSystem.IsMacOS()) + { + DestroyMacOS(); + } + else + { + base.DestroyNativeControlCore(control); + } + + OnWindowDestroyed(); + } + + [SupportedOSPlatform("linux")] + private IPlatformHandle CreateLinux(IPlatformHandle control) + { + if (ConfigurationState.Instance.Graphics.GraphicsBackend.Value == GraphicsBackend.Vulkan) + { + X11Window = new GLXWindow(new NativeHandle(X11.DefaultDisplay), new NativeHandle(control.Handle)); + X11Window.Hide(); + } + else + { + X11Window = PlatformHelper.CreateOpenGLWindow(new FramebufferFormat(new ColorFormat(8, 8, 8, 0), 16, 0, ColorFormat.Zero, 0, 2, false), 0, 0, 100, 100) as GLXWindow; + } + + WindowHandle = X11Window.WindowHandle.RawHandle; + X11Display = X11Window.DisplayHandle.RawHandle; + + return new PlatformHandle(WindowHandle, "X11"); + } + + [SupportedOSPlatform("windows")] + IPlatformHandle CreateWin32(IPlatformHandle control) + { + _className = "NativeWindow-" + Guid.NewGuid(); + + _wndProcDelegate = delegate (nint hWnd, WindowsMessages msg, nint wParam, nint lParam) + { + switch (msg) + { + case WindowsMessages.NcHitTest: + return -1; + } + + return DefWindowProc(hWnd, msg, wParam, lParam); + }; + + WndClassEx wndClassEx = new() + { + cbSize = Marshal.SizeOf(), + hInstance = GetModuleHandle(null), + lpfnWndProc = Marshal.GetFunctionPointerForDelegate(_wndProcDelegate), + style = ClassStyles.CsOwndc, + lpszClassName = Marshal.StringToHGlobalUni(_className), + hCursor = CreateArrowCursor() + }; + + RegisterClassEx(ref wndClassEx); + + WindowHandle = CreateWindowEx(0, _className, "NativeWindow", WindowStyles.WsChild, 0, 0, 640, 480, control.Handle, nint.Zero, nint.Zero, nint.Zero); + + SetWindowLongPtrW(control.Handle, GWLP_WNDPROC, wndClassEx.lpfnWndProc); + + Marshal.FreeHGlobal(wndClassEx.lpszClassName); + + return new PlatformHandle(WindowHandle, "HWND"); + } + + [SupportedOSPlatform("macos")] + IPlatformHandle CreateMacOS() + { + // Create a new CAMetalLayer. + ObjectiveC.Object layerObject = new("CAMetalLayer"); + ObjectiveC.Object metalLayer = layerObject.GetFromMessage("alloc"); + metalLayer.SendMessage("init"); + + // Create a child NSView to render into. + ObjectiveC.Object nsViewObject = new("NSView"); + ObjectiveC.Object child = nsViewObject.GetFromMessage("alloc"); + child.SendMessage("init", new ObjectiveC.NSRect(0, 0, 0, 0)); + + // Make its renderer our metal layer. + child.SendMessage("setWantsLayer:", 1); + child.SendMessage("setLayer:", metalLayer); + metalLayer.SendMessage("setContentsScale:", Program.DesktopScaleFactor); + + // Ensure the scale factor is up to date. + _updateBoundsCallback = rect => + { + metalLayer.SendMessage("setContentsScale:", Program.DesktopScaleFactor); + }; + + nint nsView = child.ObjPtr; + MetalLayer = metalLayer.ObjPtr; + NsView = nsView; + + return new PlatformHandle(nsView, "NSView"); + } + + [SupportedOSPlatform("Linux")] + void DestroyLinux() + { + X11Window?.Dispose(); + } + + [SupportedOSPlatform("windows")] + void DestroyWin32(IPlatformHandle handle) + { + DestroyWindow(handle.Handle); + UnregisterClass(_className, GetModuleHandle(null)); + } + + [SupportedOSPlatform("macos")] +#pragma warning disable CA1822 // Mark member as static + void DestroyMacOS() + { + // TODO + } +#pragma warning restore CA1822 + } +} diff --git a/src/Ryujinx/UI/Renderer/EmbeddedWindowOpenGL.cs b/src/Ryujinx/UI/Renderer/EmbeddedWindowOpenGL.cs new file mode 100644 index 000000000..3842301de --- /dev/null +++ b/src/Ryujinx/UI/Renderer/EmbeddedWindowOpenGL.cs @@ -0,0 +1,94 @@ +using OpenTK.Graphics.OpenGL; +using Ryujinx.Common.Configuration; +using Ryujinx.Common.Logging; +using Ryujinx.Graphics.GAL; +using Ryujinx.Graphics.OpenGL; +using Ryujinx.UI.Common.Configuration; +using SPB.Graphics; +using SPB.Graphics.Exceptions; +using SPB.Graphics.OpenGL; +using SPB.Platform; +using SPB.Platform.WGL; +using SPB.Windowing; +using System; + +namespace Ryujinx.Ava.UI.Renderer +{ + public class EmbeddedWindowOpenGL : EmbeddedWindow + { + private SwappableNativeWindowBase _window; + + public OpenGLContextBase Context { get; set; } + + protected override void OnWindowDestroying() + { + Context.Dispose(); + + base.OnWindowDestroying(); + } + + public override void OnWindowCreated() + { + base.OnWindowCreated(); + + if (OperatingSystem.IsWindows()) + { + _window = new WGLWindow(new NativeHandle(WindowHandle)); + } + else if (OperatingSystem.IsLinux()) + { + _window = X11Window; + } + else + { + throw new PlatformNotSupportedException(); + } + + var flags = OpenGLContextFlags.Compat; + if (ConfigurationState.Instance.Logger.GraphicsDebugLevel != GraphicsDebugLevel.None) + { + flags |= OpenGLContextFlags.Debug; + } + + var graphicsMode = Environment.OSVersion.Platform == PlatformID.Unix ? new FramebufferFormat(new ColorFormat(8, 8, 8, 0), 16, 0, ColorFormat.Zero, 0, 2, false) : FramebufferFormat.Default; + + Context = PlatformHelper.CreateOpenGLContext(graphicsMode, 3, 3, flags); + + Context.Initialize(_window); + Context.MakeCurrent(_window); + + GL.LoadBindings(new OpenTKBindingsContext(Context.GetProcAddress)); + + Context.MakeCurrent(null); + } + + public void MakeCurrent(bool unbind = false, bool shouldThrow = true) + { + try + { + Context?.MakeCurrent(!unbind ? _window : null); + } + catch (ContextException e) + { + if (shouldThrow) + { + throw; + } + + Logger.Warning?.Print(LogClass.UI, $"Failed to {(!unbind ? "bind" : "unbind")} OpenGL context: {e}"); + } + } + + public void SwapBuffers() + { + _window?.SwapBuffers(); + } + + public void InitializeBackgroundContext(IRenderer renderer) + { + (renderer as OpenGLRenderer)?.InitializeBackgroundContext(SPBOpenGLContext.CreateBackgroundContext(Context)); + + MakeCurrent(); + } + } +} diff --git a/src/Ryujinx/UI/Renderer/EmbeddedWindowVulkan.cs b/src/Ryujinx/UI/Renderer/EmbeddedWindowVulkan.cs new file mode 100644 index 000000000..fafbec207 --- /dev/null +++ b/src/Ryujinx/UI/Renderer/EmbeddedWindowVulkan.cs @@ -0,0 +1,42 @@ +using Silk.NET.Vulkan; +using SPB.Graphics.Vulkan; +using SPB.Platform.Metal; +using SPB.Platform.Win32; +using SPB.Platform.X11; +using SPB.Windowing; +using System; + +namespace Ryujinx.Ava.UI.Renderer +{ + public class EmbeddedWindowVulkan : EmbeddedWindow + { + public SurfaceKHR CreateSurface(Instance instance) + { + NativeWindowBase nativeWindowBase; + + if (OperatingSystem.IsWindows()) + { + nativeWindowBase = new SimpleWin32Window(new NativeHandle(WindowHandle)); + } + else if (OperatingSystem.IsLinux()) + { + nativeWindowBase = new SimpleX11Window(new NativeHandle(X11Display), new NativeHandle(WindowHandle)); + } + else if (OperatingSystem.IsMacOS()) + { + nativeWindowBase = new SimpleMetalWindow(new NativeHandle(NsView), new NativeHandle(MetalLayer)); + } + else + { + throw new PlatformNotSupportedException(); + } + + return new SurfaceKHR((ulong?)VulkanHelper.CreateWindowSurface(instance.Handle, nativeWindowBase)); + } + + public SurfaceKHR CreateSurface(Instance instance, Vk _) + { + return CreateSurface(instance); + } + } +} diff --git a/src/Ryujinx/UI/Renderer/OpenTKBindingsContext.cs b/src/Ryujinx/UI/Renderer/OpenTKBindingsContext.cs new file mode 100644 index 000000000..2e5dff733 --- /dev/null +++ b/src/Ryujinx/UI/Renderer/OpenTKBindingsContext.cs @@ -0,0 +1,20 @@ +using OpenTK; +using System; + +namespace Ryujinx.Ava.UI.Renderer +{ + internal class OpenTKBindingsContext : IBindingsContext + { + private readonly Func _getProcAddress; + + public OpenTKBindingsContext(Func getProcAddress) + { + _getProcAddress = getProcAddress; + } + + public nint GetProcAddress(string procName) + { + return _getProcAddress(procName); + } + } +} diff --git a/src/Ryujinx/UI/Renderer/RendererHost.axaml b/src/Ryujinx/UI/Renderer/RendererHost.axaml new file mode 100644 index 000000000..e0b586b45 --- /dev/null +++ b/src/Ryujinx/UI/Renderer/RendererHost.axaml @@ -0,0 +1,12 @@ + + diff --git a/src/Ryujinx/UI/Renderer/RendererHost.axaml.cs b/src/Ryujinx/UI/Renderer/RendererHost.axaml.cs new file mode 100644 index 000000000..4bf10d0d7 --- /dev/null +++ b/src/Ryujinx/UI/Renderer/RendererHost.axaml.cs @@ -0,0 +1,68 @@ +using Avalonia; +using Avalonia.Controls; +using Ryujinx.Common.Configuration; +using Ryujinx.UI.Common.Configuration; +using System; + +namespace Ryujinx.Ava.UI.Renderer +{ + public partial class RendererHost : UserControl, IDisposable + { + public readonly EmbeddedWindow EmbeddedWindow; + + public event EventHandler WindowCreated; + public event Action BoundsChanged; + + public RendererHost() + { + InitializeComponent(); + + if (ConfigurationState.Instance.Graphics.GraphicsBackend.Value == GraphicsBackend.OpenGl) + { + EmbeddedWindow = new EmbeddedWindowOpenGL(); + } + else + { + EmbeddedWindow = new EmbeddedWindowVulkan(); + } + + Initialize(); + } + + private void Initialize() + { + EmbeddedWindow.WindowCreated += CurrentWindow_WindowCreated; + EmbeddedWindow.BoundsChanged += CurrentWindow_BoundsChanged; + + Content = EmbeddedWindow; + } + + public void Dispose() + { + if (EmbeddedWindow != null) + { + EmbeddedWindow.WindowCreated -= CurrentWindow_WindowCreated; + EmbeddedWindow.BoundsChanged -= CurrentWindow_BoundsChanged; + } + + GC.SuppressFinalize(this); + } + + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnDetachedFromVisualTree(e); + + Dispose(); + } + + private void CurrentWindow_BoundsChanged(object sender, Size e) + { + BoundsChanged?.Invoke(sender, e); + } + + private void CurrentWindow_WindowCreated(object sender, nint e) + { + WindowCreated?.Invoke(this, EventArgs.Empty); + } + } +} diff --git a/src/Ryujinx/Ui/SPBOpenGLContext.cs b/src/Ryujinx/UI/Renderer/SPBOpenGLContext.cs similarity index 88% rename from src/Ryujinx/Ui/SPBOpenGLContext.cs rename to src/Ryujinx/UI/Renderer/SPBOpenGLContext.cs index 6f2db697a..63bf6cf7c 100644 --- a/src/Ryujinx/Ui/SPBOpenGLContext.cs +++ b/src/Ryujinx/UI/Renderer/SPBOpenGLContext.cs @@ -5,7 +5,7 @@ using SPB.Graphics.OpenGL; using SPB.Platform; using SPB.Windowing; -namespace Ryujinx.Ui +namespace Ryujinx.Ava.UI.Renderer { class SPBOpenGLContext : IOpenGLContext { @@ -29,6 +29,8 @@ namespace Ryujinx.Ui _context.MakeCurrent(_window); } + public bool HasContext() => _context.IsCurrent; + public static SPBOpenGLContext CreateBackgroundContext(OpenGLContextBase sharedContext) { OpenGLContextBase context = PlatformHelper.CreateOpenGLContext(FramebufferFormat.Default, 3, 3, OpenGLContextFlags.Compat, true, sharedContext); @@ -37,7 +39,7 @@ namespace Ryujinx.Ui context.Initialize(window); context.MakeCurrent(window); - GL.LoadBindings(new OpenToolkitBindingsContext(context)); + GL.LoadBindings(new OpenTKBindingsContext(context.GetProcAddress)); context.MakeCurrent(null); diff --git a/src/Ryujinx/UI/ViewModels/AboutWindowViewModel.cs b/src/Ryujinx/UI/ViewModels/AboutWindowViewModel.cs new file mode 100644 index 000000000..c48ad378f --- /dev/null +++ b/src/Ryujinx/UI/ViewModels/AboutWindowViewModel.cs @@ -0,0 +1,82 @@ +using Avalonia.Media.Imaging; +using Avalonia.Styling; +using Avalonia.Threading; +using Ryujinx.Ava.Common; +using Ryujinx.Ava.Common.Locale; +using Ryujinx.UI.Common.Configuration; +using System; + +namespace Ryujinx.Ava.UI.ViewModels +{ + public class AboutWindowViewModel : BaseModel, IDisposable + { + private Bitmap _githubLogo; + private Bitmap _discordLogo; + + private string _version; + + public Bitmap GithubLogo + { + get => _githubLogo; + set + { + _githubLogo = value; + OnPropertyChanged(); + } + } + + public Bitmap DiscordLogo + { + get => _discordLogo; + set + { + _discordLogo = value; + OnPropertyChanged(); + } + } + + public string Version + { + get => _version; + set + { + _version = value; + OnPropertyChanged(); + } + } + + public string Developers => LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.AboutPageDeveloperListMore, "gdkchan, Ac_K, marysaka, rip in peri peri, LDj3SNuD, emmaus, Thealexbarney, GoffyDude, TSRBerry, IsaacMarovitz, GreemDev"); + + public AboutWindowViewModel() + { + Version = App.FullAppName + "\n" + Program.Version; + UpdateLogoTheme(ConfigurationState.Instance.UI.BaseStyle.Value); + + ThemeManager.ThemeChanged += ThemeManager_ThemeChanged; + } + + private void ThemeManager_ThemeChanged(object sender, EventArgs e) + { + Dispatcher.UIThread.Post(() => UpdateLogoTheme(ConfigurationState.Instance.UI.BaseStyle.Value)); + } + + private void UpdateLogoTheme(string theme) + { + bool isDarkTheme = theme == "Dark" || (theme == "Auto" && App.DetectSystemTheme() == ThemeVariant.Dark); + + string basePath = "resm:Ryujinx.UI.Common.Resources."; + string themeSuffix = isDarkTheme ? "Dark.png" : "Light.png"; + + GithubLogo = LoadBitmap($"{basePath}Logo_GitHub_{themeSuffix}?assembly=Ryujinx.UI.Common"); + DiscordLogo = LoadBitmap($"{basePath}Logo_Discord_{themeSuffix}?assembly=Ryujinx.UI.Common"); + } + + private static Bitmap LoadBitmap(string uri) => new(Avalonia.Platform.AssetLoader.Open(new Uri(uri))); + + public void Dispose() + { + ThemeManager.ThemeChanged -= ThemeManager_ThemeChanged; + GC.SuppressFinalize(this); + } + } +} diff --git a/src/Ryujinx/UI/ViewModels/AmiiboWindowViewModel.cs b/src/Ryujinx/UI/ViewModels/AmiiboWindowViewModel.cs new file mode 100644 index 000000000..a852d474c --- /dev/null +++ b/src/Ryujinx/UI/ViewModels/AmiiboWindowViewModel.cs @@ -0,0 +1,537 @@ +using Avalonia; +using Avalonia.Collections; +using Avalonia.Media.Imaging; +using Avalonia.Threading; +using Ryujinx.Ava.Common.Locale; +using Ryujinx.Ava.UI.Helpers; +using Ryujinx.Ava.UI.Windows; +using Ryujinx.Common; +using Ryujinx.Common.Configuration; +using Ryujinx.Common.Logging; +using Ryujinx.Common.Utilities; +using Ryujinx.UI.Common.Models.Amiibo; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; + +namespace Ryujinx.Ava.UI.ViewModels +{ + public class AmiiboWindowViewModel : BaseModel, IDisposable + { + // ReSharper disable once InconsistentNaming + private static bool _cachedUseRandomUuid; + + private const string DefaultJson = "{ \"amiibo\": [] }"; + private const float AmiiboImageSize = 350f; + + private readonly string _amiiboJsonPath; + private readonly byte[] _amiiboLogoBytes; + private readonly HttpClient _httpClient; + private readonly AmiiboWindow _owner; + + private Bitmap _amiiboImage; + private List _amiiboList; + private AvaloniaList _amiibos; + private ObservableCollection _amiiboSeries; + + private int _amiiboSelectedIndex; + private int _seriesSelectedIndex; + private bool _enableScanning; + private bool _showAllAmiibo; + private bool _useRandomUuid = _cachedUseRandomUuid; + private string _usage; + + private static readonly AmiiboJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions()); + + public AmiiboWindowViewModel(AmiiboWindow owner, string lastScannedAmiiboId, string titleId) + { + _owner = owner; + + _httpClient = new HttpClient + { + Timeout = TimeSpan.FromSeconds(30), + }; + + LastScannedAmiiboId = lastScannedAmiiboId; + TitleId = titleId; + + Directory.CreateDirectory(Path.Join(AppDataManager.BaseDirPath, "system", "amiibo")); + + _amiiboJsonPath = Path.Join(AppDataManager.BaseDirPath, "system", "amiibo", "Amiibo.json"); + _amiiboList = new List(); + _amiiboSeries = new ObservableCollection(); + _amiibos = new AvaloniaList(); + + _amiiboLogoBytes = EmbeddedResources.Read("Ryujinx.UI.Common/Resources/Logo_Amiibo.png"); + + _ = LoadContentAsync(); + } + + public AmiiboWindowViewModel() { } + + public string TitleId { get; set; } + public string LastScannedAmiiboId { get; set; } + + public UserResult Response { get; private set; } + + public bool UseRandomUuid + { + get => _useRandomUuid; + set + { + _cachedUseRandomUuid = _useRandomUuid = value; + + OnPropertyChanged(); + } + } + + public bool ShowAllAmiibo + { + get => _showAllAmiibo; + set + { + _showAllAmiibo = value; + + ParseAmiiboData(); + + OnPropertyChanged(); + } + } + + public AvaloniaList AmiiboList + { + get => _amiibos; + set + { + _amiibos = value; + + OnPropertyChanged(); + } + } + + public ObservableCollection AmiiboSeries + { + get => _amiiboSeries; + set + { + _amiiboSeries = value; + OnPropertyChanged(); + } + } + + public int SeriesSelectedIndex + { + get => _seriesSelectedIndex; + set + { + _seriesSelectedIndex = value; + + FilterAmiibo(); + + OnPropertyChanged(); + } + } + + public int AmiiboSelectedIndex + { + get => _amiiboSelectedIndex; + set + { + _amiiboSelectedIndex = value; + + EnableScanning = _amiiboSelectedIndex >= 0 && _amiiboSelectedIndex < _amiibos.Count; + + SetAmiiboDetails(); + + OnPropertyChanged(); + } + } + + public Bitmap AmiiboImage + { + get => _amiiboImage; + set + { + _amiiboImage = value; + + OnPropertyChanged(); + } + } + + public string Usage + { + get => _usage; + set + { + _usage = value; + + OnPropertyChanged(); + } + } + + public bool EnableScanning + { + get => _enableScanning; + set + { + _enableScanning = value; + + OnPropertyChanged(); + } + } + + public void Scan() + { + if (AmiiboSelectedIndex > -1) + { + _owner.ScannedAmiibo = AmiiboList[AmiiboSelectedIndex]; + _owner.IsScanned = true; + _owner.Close(); + } + } + + public void Cancel() + { + _owner.IsScanned = false; + _owner.Close(); + } + + public void Dispose() + { + GC.SuppressFinalize(this); + _httpClient.Dispose(); + } + + private static bool TryGetAmiiboJson(string json, out AmiiboJson amiiboJson) + { + if (string.IsNullOrEmpty(json)) + { + amiiboJson = JsonHelper.Deserialize(DefaultJson, _serializerContext.AmiiboJson); + + return false; + } + + try + { + amiiboJson = JsonHelper.Deserialize(json, _serializerContext.AmiiboJson); + + return true; + } + catch (JsonException exception) + { + Logger.Error?.Print(LogClass.Application, $"Unable to deserialize amiibo data: {exception}"); + amiiboJson = JsonHelper.Deserialize(DefaultJson, _serializerContext.AmiiboJson); + + return false; + } + } + + private async Task GetMostRecentAmiiboListOrDefaultJson() + { + bool localIsValid = false; + bool remoteIsValid = false; + AmiiboJson amiiboJson = new(); + + try + { + try + { + if (File.Exists(_amiiboJsonPath)) + { + localIsValid = TryGetAmiiboJson(await File.ReadAllTextAsync(_amiiboJsonPath), out amiiboJson); + } + } + catch (Exception exception) + { + Logger.Warning?.Print(LogClass.Application, $"Unable to read data from '{_amiiboJsonPath}': {exception}"); + } + + if (!localIsValid || await NeedsUpdate(amiiboJson.LastUpdated)) + { + remoteIsValid = TryGetAmiiboJson(await DownloadAmiiboJson(), out amiiboJson); + } + } + catch (Exception exception) + { + if (!(localIsValid || remoteIsValid)) + { + Logger.Error?.Print(LogClass.Application, $"Couldn't get valid amiibo data: {exception}"); + + // Neither local or remote files are valid JSON, close window. + ShowInfoDialog(); + Close(); + } + else if (!remoteIsValid) + { + Logger.Warning?.Print(LogClass.Application, $"Couldn't update amiibo data: {exception}"); + + // Only the local file is valid, the local one should be used + // but the user should be warned. + ShowInfoDialog(); + } + } + + return amiiboJson; + } + + private async Task LoadContentAsync() + { + AmiiboJson amiiboJson = await GetMostRecentAmiiboListOrDefaultJson(); + + _amiiboList = amiiboJson.Amiibo.OrderBy(amiibo => amiibo.AmiiboSeries).ToList(); + + ParseAmiiboData(); + } + + private void ParseAmiiboData() + { + _amiiboSeries.Clear(); + _amiibos.Clear(); + + for (int i = 0; i < _amiiboList.Count; i++) + { + if (!_amiiboSeries.Contains(_amiiboList[i].AmiiboSeries)) + { + if (!ShowAllAmiibo) + { + foreach (AmiiboApiGamesSwitch game in _amiiboList[i].GamesSwitch) + { + if (game != null) + { + if (game.GameId.Contains(TitleId)) + { + AmiiboSeries.Add(_amiiboList[i].AmiiboSeries); + + break; + } + } + } + } + else + { + AmiiboSeries.Add(_amiiboList[i].AmiiboSeries); + } + } + } + + if (LastScannedAmiiboId != string.Empty) + { + SelectLastScannedAmiibo(); + } + else + { + SeriesSelectedIndex = 0; + } + } + + private void SelectLastScannedAmiibo() + { + AmiiboApi scanned = _amiiboList.Find(amiibo => amiibo.GetId() == LastScannedAmiiboId); + + SeriesSelectedIndex = AmiiboSeries.IndexOf(scanned.AmiiboSeries); + AmiiboSelectedIndex = AmiiboList.IndexOf(scanned); + } + + private void FilterAmiibo() + { + _amiibos.Clear(); + + if (_seriesSelectedIndex < 0) + { + return; + } + + List amiiboSortedList = _amiiboList + .Where(amiibo => amiibo.AmiiboSeries == _amiiboSeries[SeriesSelectedIndex]) + .OrderBy(amiibo => amiibo.Name).ToList(); + + for (int i = 0; i < amiiboSortedList.Count; i++) + { + if (!_amiibos.Contains(amiiboSortedList[i])) + { + if (!_showAllAmiibo) + { + foreach (AmiiboApiGamesSwitch game in amiiboSortedList[i].GamesSwitch) + { + if (game != null) + { + if (game.GameId.Contains(TitleId)) + { + _amiibos.Add(amiiboSortedList[i]); + + break; + } + } + } + } + else + { + _amiibos.Add(amiiboSortedList[i]); + } + } + } + + AmiiboSelectedIndex = 0; + } + + private void SetAmiiboDetails() + { + ResetAmiiboPreview(); + + Usage = string.Empty; + + if (_amiiboSelectedIndex < 0) + { + return; + } + + AmiiboApi selected = _amiibos[_amiiboSelectedIndex]; + + string imageUrl = _amiiboList.Find(amiibo => amiibo.Equals(selected)).Image; + + StringBuilder usageStringBuilder = new(); + + for (int i = 0; i < _amiiboList.Count; i++) + { + if (_amiiboList[i].Equals(selected)) + { + bool writable = false; + + foreach (AmiiboApiGamesSwitch item in _amiiboList[i].GamesSwitch) + { + if (item.GameId.Contains(TitleId)) + { + foreach (AmiiboApiUsage usageItem in item.AmiiboUsage) + { + usageStringBuilder.Append($"{Environment.NewLine}- {usageItem.Usage.Replace("/", Environment.NewLine + "-")}"); + + writable = usageItem.Write; + } + } + } + + if (usageStringBuilder.Length == 0) + { + usageStringBuilder.Append($"{LocaleManager.Instance[LocaleKeys.Unknown]}."); + } + + Usage = $"{LocaleManager.Instance[LocaleKeys.Usage]} {(writable ? $" ({LocaleManager.Instance[LocaleKeys.Writable]})" : string.Empty)} : {usageStringBuilder}"; + } + } + + _ = UpdateAmiiboPreview(imageUrl); + } + + private async Task NeedsUpdate(DateTime oldLastModified) + { + try + { + HttpResponseMessage response = await _httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Head, "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/Amiibo.json")); + + if (response.IsSuccessStatusCode) + { + return response.Content.Headers.LastModified != oldLastModified; + } + } + catch (HttpRequestException exception) + { + Logger.Error?.Print(LogClass.Application, $"Unable to check for amiibo data updates: {exception}"); + } + + return false; + } + + private async Task DownloadAmiiboJson() + { + try + { + HttpResponseMessage response = await _httpClient.GetAsync($"https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/Amiibo.json"); + + if (response.IsSuccessStatusCode) + { + string amiiboJsonString = await response.Content.ReadAsStringAsync(); + + try + { + using FileStream dlcJsonStream = File.Create(_amiiboJsonPath, 4096, FileOptions.WriteThrough); + dlcJsonStream.Write(Encoding.UTF8.GetBytes(amiiboJsonString)); + } + catch (Exception exception) + { + Logger.Warning?.Print(LogClass.Application, $"Couldn't write amiibo data to file '{_amiiboJsonPath}: {exception}'"); + } + + return amiiboJsonString; + } + + Logger.Error?.Print(LogClass.Application, $"Failed to download amiibo data. Response status code: {response.StatusCode}"); + } + catch (HttpRequestException exception) + { + Logger.Error?.Print(LogClass.Application, $"Failed to request amiibo data: {exception}"); + } + + await ContentDialogHelper.CreateInfoDialog(LocaleManager.Instance[LocaleKeys.DialogAmiiboApiTitle], + LocaleManager.Instance[LocaleKeys.DialogAmiiboApiFailFetchMessage], + LocaleManager.Instance[LocaleKeys.InputDialogOk], + string.Empty, + LocaleManager.Instance[LocaleKeys.RyujinxInfo]); + + return null; + } + + private void Close() + { + Dispatcher.UIThread.Post(_owner.Close); + } + + private async Task UpdateAmiiboPreview(string imageUrl) + { + HttpResponseMessage response = await _httpClient.GetAsync(imageUrl); + + if (response.IsSuccessStatusCode) + { + byte[] amiiboPreviewBytes = await response.Content.ReadAsByteArrayAsync(); + using MemoryStream memoryStream = new(amiiboPreviewBytes); + + Bitmap bitmap = new(memoryStream); + + double ratio = Math.Min(AmiiboImageSize / bitmap.Size.Width, + AmiiboImageSize / bitmap.Size.Height); + + int resizeHeight = (int)(bitmap.Size.Height * ratio); + int resizeWidth = (int)(bitmap.Size.Width * ratio); + + AmiiboImage = bitmap.CreateScaledBitmap(new PixelSize(resizeWidth, resizeHeight)); + } + else + { + Logger.Error?.Print(LogClass.Application, $"Failed to get amiibo preview. Response status code: {response.StatusCode}"); + } + } + + private void ResetAmiiboPreview() + { + using MemoryStream memoryStream = new(_amiiboLogoBytes); + + Bitmap bitmap = new(memoryStream); + + AmiiboImage = bitmap; + } + + private static async void ShowInfoDialog() + { + await ContentDialogHelper.CreateInfoDialog(LocaleManager.Instance[LocaleKeys.DialogAmiiboApiTitle], + LocaleManager.Instance[LocaleKeys.DialogAmiiboApiConnectErrorMessage], + LocaleManager.Instance[LocaleKeys.InputDialogOk], + string.Empty, + LocaleManager.Instance[LocaleKeys.RyujinxInfo]); + } + } +} diff --git a/src/Ryujinx/UI/ViewModels/AppListFavoriteComparable.cs b/src/Ryujinx/UI/ViewModels/AppListFavoriteComparable.cs new file mode 100644 index 000000000..e80984508 --- /dev/null +++ b/src/Ryujinx/UI/ViewModels/AppListFavoriteComparable.cs @@ -0,0 +1,43 @@ +using Ryujinx.UI.App.Common; +using System; + +namespace Ryujinx.Ava.UI.ViewModels +{ + /// + /// Implements a custom comparer which is used for sorting titles by favorite on a UI. + /// Returns a sorted list of favorites in alphabetical order, followed by all non-favorites sorted alphabetical. + /// + public readonly struct AppListFavoriteComparable : IComparable + { + /// + /// The application data being compared. + /// + private readonly ApplicationData app; + + /// + /// Constructs a new with the specified application data. + /// + /// The app data being compared. + public AppListFavoriteComparable(ApplicationData app) + { + ArgumentNullException.ThrowIfNull(app, nameof(app)); + this.app = app; + } + + /// + public readonly int CompareTo(object o) + { + if (o is AppListFavoriteComparable other) + { + if (app.Favorite == other.app.Favorite) + { + return string.Compare(app.Name, other.app.Name, StringComparison.OrdinalIgnoreCase); + } + + return app.Favorite ? -1 : 1; + } + + throw new InvalidCastException($"Cannot cast {o.GetType()} to {nameof(AppListFavoriteComparable)}"); + } + } +} diff --git a/src/Ryujinx/UI/ViewModels/BaseModel.cs b/src/Ryujinx/UI/ViewModels/BaseModel.cs new file mode 100644 index 000000000..4db9cf812 --- /dev/null +++ b/src/Ryujinx/UI/ViewModels/BaseModel.cs @@ -0,0 +1,15 @@ +using System.ComponentModel; +using System.Runtime.CompilerServices; + +namespace Ryujinx.Ava.UI.ViewModels +{ + public class BaseModel : INotifyPropertyChanged + { + public event PropertyChangedEventHandler PropertyChanged; + + protected void OnPropertyChanged([CallerMemberName] string propertyName = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + } +} diff --git a/src/Ryujinx/UI/ViewModels/DownloadableContentManagerViewModel.cs b/src/Ryujinx/UI/ViewModels/DownloadableContentManagerViewModel.cs new file mode 100644 index 000000000..3abaae3ae --- /dev/null +++ b/src/Ryujinx/UI/ViewModels/DownloadableContentManagerViewModel.cs @@ -0,0 +1,301 @@ +using Avalonia.Collections; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Platform.Storage; +using Avalonia.Threading; +using DynamicData; +using FluentAvalonia.UI.Controls; +using Ryujinx.Ava.Common.Locale; +using Ryujinx.Ava.UI.Helpers; +using Ryujinx.HLE.FileSystem; +using Ryujinx.UI.App.Common; +using Ryujinx.UI.Common.Models; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Application = Avalonia.Application; + +namespace Ryujinx.Ava.UI.ViewModels +{ + public class DownloadableContentManagerViewModel : BaseModel + { + private readonly ApplicationLibrary _applicationLibrary; + private AvaloniaList _downloadableContents = new(); + private AvaloniaList _selectedDownloadableContents = new(); + private AvaloniaList _views = new(); + private bool _showBundledContentNotice = false; + + private string _search; + private readonly ApplicationData _applicationData; + private readonly IStorageProvider _storageProvider; + + public AvaloniaList DownloadableContents + { + get => _downloadableContents; + set + { + _downloadableContents = value; + OnPropertyChanged(); + OnPropertyChanged(nameof(UpdateCount)); + Sort(); + } + } + + public AvaloniaList Views + { + get => _views; + set + { + _views = value; + OnPropertyChanged(); + } + } + + public AvaloniaList SelectedDownloadableContents + { + get => _selectedDownloadableContents; + set + { + _selectedDownloadableContents = value; + OnPropertyChanged(); + } + } + + public string Search + { + get => _search; + set + { + _search = value; + OnPropertyChanged(); + Sort(); + } + } + + public string UpdateCount + { + get => string.Format(LocaleManager.Instance[LocaleKeys.DlcWindowHeading], DownloadableContents.Count); + } + + public bool ShowBundledContentNotice + { + get => _showBundledContentNotice; + set + { + _showBundledContentNotice = value; + OnPropertyChanged(); + } + } + + public DownloadableContentManagerViewModel(ApplicationLibrary applicationLibrary, ApplicationData applicationData) + { + _applicationLibrary = applicationLibrary; + + _applicationData = applicationData; + + if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + _storageProvider = desktop.MainWindow.StorageProvider; + } + + LoadDownloadableContents(); + } + + private void LoadDownloadableContents() + { + var dlcs = _applicationLibrary.DownloadableContents.Items + .Where(it => it.Dlc.TitleIdBase == _applicationData.IdBase); + + bool hasBundledContent = false; + foreach ((DownloadableContentModel dlc, bool isEnabled) in dlcs) + { + DownloadableContents.Add(dlc); + hasBundledContent = hasBundledContent || dlc.IsBundled; + + if (isEnabled) + { + SelectedDownloadableContents.Add(dlc); + } + + OnPropertyChanged(nameof(UpdateCount)); + } + + ShowBundledContentNotice = hasBundledContent; + + Sort(); + } + + public void Sort() + { + DownloadableContents + // Sort bundled last + .OrderBy(it => it.IsBundled ? 0 : 1) + .ThenBy(it => it.TitleId) + .AsObservableChangeSet() + .Filter(Filter) + .Bind(out var view).AsObservableList(); + + // NOTE(jpr): this works around a bug where calling _views.Clear also clears SelectedDownloadableContents for + // some reason. so we save the items here and add them back after + var items = SelectedDownloadableContents.ToArray(); + + _views.Clear(); + _views.AddRange(view); + + foreach (DownloadableContentModel item in items) + { + SelectedDownloadableContents.ReplaceOrAdd(item, item); + } + + OnPropertyChanged(nameof(Views)); + } + + private bool Filter(T arg) + { + if (arg is DownloadableContentModel content) + { + return string.IsNullOrWhiteSpace(_search) || content.FileName.ToLower().Contains(_search.ToLower()) || content.TitleIdStr.ToLower().Contains(_search.ToLower()); + } + + return false; + } + + public async void Add() + { + var result = await _storageProvider.OpenFilePickerAsync(new FilePickerOpenOptions + { + Title = LocaleManager.Instance[LocaleKeys.SelectDlcDialogTitle], + AllowMultiple = true, + FileTypeFilter = new List + { + new("NSP") + { + Patterns = new[] { "*.nsp" }, + AppleUniformTypeIdentifiers = new[] { "com.ryujinx.nsp" }, + MimeTypes = new[] { "application/x-nx-nsp" }, + }, + }, + }); + + var totalDlcAdded = 0; + foreach (var file in result) + { + if (!AddDownloadableContent(file.Path.LocalPath, out var newDlcAdded)) + { + await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogDlcNoDlcErrorMessage]); + } + + totalDlcAdded += newDlcAdded; + } + + if (totalDlcAdded > 0) + { + await ShowNewDlcAddedDialog(totalDlcAdded); + } + } + + private bool AddDownloadableContent(string path, out int numDlcAdded) + { + numDlcAdded = 0; + + if (!File.Exists(path)) + { + return false; + } + + if (!_applicationLibrary.TryGetDownloadableContentFromFile(path, out var dlcs) || dlcs.Count == 0) + { + return false; + } + + var dlcsForThisGame = dlcs.Where(it => it.TitleIdBase == _applicationData.IdBase).ToList(); + if (dlcsForThisGame.Count == 0) + { + return false; + } + + foreach (var dlc in dlcsForThisGame) + { + if (!DownloadableContents.Contains(dlc)) + { + DownloadableContents.Add(dlc); + SelectedDownloadableContents.ReplaceOrAdd(dlc, dlc); + + numDlcAdded++; + } + } + + if (numDlcAdded > 0) + { + OnPropertyChanged(nameof(UpdateCount)); + Sort(); + } + + return true; + } + + public void Remove(DownloadableContentModel model) + { + SelectedDownloadableContents.Remove(model); + + if (!model.IsBundled) + { + DownloadableContents.Remove(model); + OnPropertyChanged(nameof(UpdateCount)); + Sort(); + } + } + + public void RemoveAll() + { + SelectedDownloadableContents.Clear(); + DownloadableContents.RemoveMany(DownloadableContents.Where(it => !it.IsBundled)); + + OnPropertyChanged(nameof(UpdateCount)); + Sort(); + } + + public void EnableAll() + { + SelectedDownloadableContents.Clear(); + SelectedDownloadableContents.AddRange(DownloadableContents); + } + + public void DisableAll() + { + SelectedDownloadableContents.Clear(); + } + + public void Enable(DownloadableContentModel model) + { + SelectedDownloadableContents.ReplaceOrAdd(model, model); + } + + public void Disable(DownloadableContentModel model) + { + SelectedDownloadableContents.Remove(model); + } + + public void Save() + { + var dlcs = DownloadableContents.Select(it => (it, SelectedDownloadableContents.Contains(it))).ToList(); + _applicationLibrary.SaveDownloadableContentsForGame(_applicationData, dlcs); + } + + private Task ShowNewDlcAddedDialog(int numAdded) + { + var msg = string.Format(LocaleManager.Instance[LocaleKeys.DlcWindowDlcAddedMessage], numAdded); + return Dispatcher.UIThread.InvokeAsync(async () => + { + await ContentDialogHelper.ShowTextDialog( + LocaleManager.Instance[LocaleKeys.DialogConfirmationTitle], + msg, + string.Empty, + string.Empty, + string.Empty, + LocaleManager.Instance[LocaleKeys.InputDialogOk], + (int)Symbol.Checkmark); + }); + } + } +} diff --git a/src/Ryujinx/UI/ViewModels/Input/ControllerInputViewModel.cs b/src/Ryujinx/UI/ViewModels/Input/ControllerInputViewModel.cs new file mode 100644 index 000000000..6ee79a371 --- /dev/null +++ b/src/Ryujinx/UI/ViewModels/Input/ControllerInputViewModel.cs @@ -0,0 +1,84 @@ +using Avalonia.Svg.Skia; +using Ryujinx.Ava.UI.Models.Input; +using Ryujinx.Ava.UI.Views.Input; + +namespace Ryujinx.Ava.UI.ViewModels.Input +{ + public class ControllerInputViewModel : BaseModel + { + private GamepadInputConfig _config; + public GamepadInputConfig Config + { + get => _config; + set + { + _config = value; + OnPropertyChanged(); + } + } + + private bool _isLeft; + public bool IsLeft + { + get => _isLeft; + set + { + _isLeft = value; + OnPropertyChanged(); + OnPropertyChanged(nameof(HasSides)); + } + } + + private bool _isRight; + public bool IsRight + { + get => _isRight; + set + { + _isRight = value; + OnPropertyChanged(); + OnPropertyChanged(nameof(HasSides)); + } + } + + public bool HasSides => IsLeft ^ IsRight; + + private SvgImage _image; + public SvgImage Image + { + get => _image; + set + { + _image = value; + OnPropertyChanged(); + } + } + + public readonly InputViewModel ParentModel; + + public ControllerInputViewModel(InputViewModel model, GamepadInputConfig config) + { + ParentModel = model; + model.NotifyChangesEvent += OnParentModelChanged; + OnParentModelChanged(); + Config = config; + } + + public async void ShowMotionConfig() + { + await MotionInputView.Show(this); + } + + public async void ShowRumbleConfig() + { + await RumbleInputView.Show(this); + } + + public void OnParentModelChanged() + { + IsLeft = ParentModel.IsLeft; + IsRight = ParentModel.IsRight; + Image = ParentModel.Image; + } + } +} diff --git a/src/Ryujinx/UI/ViewModels/Input/InputViewModel.cs b/src/Ryujinx/UI/ViewModels/Input/InputViewModel.cs new file mode 100644 index 000000000..54f278cec --- /dev/null +++ b/src/Ryujinx/UI/ViewModels/Input/InputViewModel.cs @@ -0,0 +1,903 @@ +using Avalonia; +using Avalonia.Collections; +using Avalonia.Controls; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Svg.Skia; +using Avalonia.Threading; +using Ryujinx.Ava.Common.Locale; +using Ryujinx.Ava.Input; +using Ryujinx.Ava.UI.Helpers; +using Ryujinx.Ava.UI.Models; +using Ryujinx.Ava.UI.Models.Input; +using Ryujinx.Ava.UI.Windows; +using Ryujinx.Common; +using Ryujinx.Common.Configuration; +using Ryujinx.Common.Configuration.Hid; +using Ryujinx.Common.Configuration.Hid.Controller; +using Ryujinx.Common.Configuration.Hid.Controller.Motion; +using Ryujinx.Common.Configuration.Hid.Keyboard; +using Ryujinx.Common.Logging; +using Ryujinx.Common.Utilities; +using Ryujinx.Input; +using Ryujinx.UI.Common.Configuration; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.IO; +using System.Linq; +using System.Text.Json; +using ConfigGamepadInputId = Ryujinx.Common.Configuration.Hid.Controller.GamepadInputId; +using ConfigStickInputId = Ryujinx.Common.Configuration.Hid.Controller.StickInputId; +using Key = Ryujinx.Common.Configuration.Hid.Key; + +namespace Ryujinx.Ava.UI.ViewModels.Input +{ + public class InputViewModel : BaseModel, IDisposable + { + private const string Disabled = "disabled"; + private const string ProControllerResource = "Ryujinx.UI.Common/Resources/Controller_ProCon.svg"; + private const string JoyConPairResource = "Ryujinx.UI.Common/Resources/Controller_JoyConPair.svg"; + private const string JoyConLeftResource = "Ryujinx.UI.Common/Resources/Controller_JoyConLeft.svg"; + private const string JoyConRightResource = "Ryujinx.UI.Common/Resources/Controller_JoyConRight.svg"; + private const string KeyboardString = "keyboard"; + private const string ControllerString = "controller"; + private readonly MainWindow _mainWindow; + + private PlayerIndex _playerId; + private PlayerIndex _playerIdChoose; + private int _controller; + private string _controllerImage; + private int _device; + private object _configViewModel; + private string _profileName; + private bool _isLoaded; + + private static readonly InputConfigJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions()); + + public IGamepadDriver AvaloniaKeyboardDriver { get; } + public IGamepad SelectedGamepad { get; private set; } + + public ObservableCollection PlayerIndexes { get; set; } + public ObservableCollection<(DeviceType Type, string Id, string Name)> Devices { get; set; } + internal ObservableCollection Controllers { get; set; } + public AvaloniaList ProfilesList { get; set; } + public AvaloniaList DeviceList { get; set; } + + // XAML Flags + public bool ShowSettings => _device > 0; + public bool IsController => _device > 1; + public bool IsKeyboard => !IsController; + public bool IsRight { get; set; } + public bool IsLeft { get; set; } + + public bool IsModified { get; set; } + public event Action NotifyChangesEvent; + + public object ConfigViewModel + { + get => _configViewModel; + set + { + _configViewModel = value; + + OnPropertyChanged(); + } + } + + public PlayerIndex PlayerIdChoose + { + get => _playerIdChoose; + set { } + } + + public PlayerIndex PlayerId + { + get => _playerId; + set + { + if (IsModified) + { + + _playerIdChoose = value; + return; + } + + IsModified = false; + _playerId = value; + + if (!Enum.IsDefined(typeof(PlayerIndex), _playerId)) + { + _playerId = PlayerIndex.Player1; + + } + _isLoaded = false; + + LoadConfiguration(); + LoadDevice(); + LoadProfiles(); + + _isLoaded = true; + + OnPropertyChanged(); + } + } + + public int Controller + { + get => _controller; + set + { + _controller = value; + + if (_controller == -1) + { + _controller = 0; + } + + if (Controllers.Count > 0 && value < Controllers.Count && _controller > -1) + { + ControllerType controller = Controllers[_controller].Type; + + IsLeft = true; + IsRight = true; + + switch (controller) + { + case ControllerType.Handheld: + ControllerImage = JoyConPairResource; + break; + case ControllerType.ProController: + ControllerImage = ProControllerResource; + break; + case ControllerType.JoyconPair: + ControllerImage = JoyConPairResource; + break; + case ControllerType.JoyconLeft: + ControllerImage = JoyConLeftResource; + IsRight = false; + break; + case ControllerType.JoyconRight: + ControllerImage = JoyConRightResource; + IsLeft = false; + break; + } + + LoadInputDriver(); + LoadProfiles(); + } + + OnPropertyChanged(); + NotifyChanges(); + } + } + + 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 = SvgSource.LoadFromStream(EmbeddedResources.GetStream(_controllerImage)); + + image.Source = source; + } + + return image; + } + } + + public string ProfileName + { + get => _profileName; set + { + _profileName = value; + + OnPropertyChanged(); + } + } + + public int Device + { + get => _device; + set + { + _device = value < 0 ? 0 : value; + + if (_device >= Devices.Count) + { + return; + } + + var selected = Devices[_device].Type; + + if (selected != DeviceType.None) + { + LoadControllers(); + + if (_isLoaded) + { + LoadConfiguration(LoadDefaultConfiguration()); + } + } + + OnPropertyChanged(); + NotifyChanges(); + } + } + + public InputConfig Config { get; set; } + + public InputViewModel(UserControl owner) : this() + { + if (Program.PreviewerDetached) + { + _mainWindow = + (MainWindow)((IClassicDesktopStyleApplicationLifetime)Application.Current + .ApplicationLifetime).MainWindow; + + AvaloniaKeyboardDriver = new AvaloniaKeyboardDriver(owner); + + _mainWindow.InputManager.GamepadDriver.OnGamepadConnected += HandleOnGamepadConnected; + _mainWindow.InputManager.GamepadDriver.OnGamepadDisconnected += HandleOnGamepadDisconnected; + + _mainWindow.ViewModel.AppHost?.NpadManager.BlockInputUpdates(); + + _isLoaded = false; + + LoadDevices(); + + PlayerId = PlayerIndex.Player1; + } + } + + public InputViewModel() + { + PlayerIndexes = new ObservableCollection(); + Controllers = new ObservableCollection(); + Devices = new ObservableCollection<(DeviceType Type, string Id, string Name)>(); + ProfilesList = new AvaloniaList(); + DeviceList = new AvaloniaList(); + + ControllerImage = ProControllerResource; + + PlayerIndexes.Add(new(PlayerIndex.Player1, LocaleManager.Instance[LocaleKeys.ControllerSettingsPlayer1])); + PlayerIndexes.Add(new(PlayerIndex.Player2, LocaleManager.Instance[LocaleKeys.ControllerSettingsPlayer2])); + PlayerIndexes.Add(new(PlayerIndex.Player3, LocaleManager.Instance[LocaleKeys.ControllerSettingsPlayer3])); + PlayerIndexes.Add(new(PlayerIndex.Player4, LocaleManager.Instance[LocaleKeys.ControllerSettingsPlayer4])); + PlayerIndexes.Add(new(PlayerIndex.Player5, LocaleManager.Instance[LocaleKeys.ControllerSettingsPlayer5])); + PlayerIndexes.Add(new(PlayerIndex.Player6, LocaleManager.Instance[LocaleKeys.ControllerSettingsPlayer6])); + PlayerIndexes.Add(new(PlayerIndex.Player7, LocaleManager.Instance[LocaleKeys.ControllerSettingsPlayer7])); + PlayerIndexes.Add(new(PlayerIndex.Player8, LocaleManager.Instance[LocaleKeys.ControllerSettingsPlayer8])); + PlayerIndexes.Add(new(PlayerIndex.Handheld, LocaleManager.Instance[LocaleKeys.ControllerSettingsHandheld])); + } + + private void LoadConfiguration(InputConfig inputConfig = null) + { + Config = inputConfig ?? ConfigurationState.Instance.Hid.InputConfig.Value.Find(inputConfig => inputConfig.PlayerIndex == _playerId); + + if (Config is StandardKeyboardInputConfig keyboardInputConfig) + { + ConfigViewModel = new KeyboardInputViewModel(this, new KeyboardInputConfig(keyboardInputConfig)); + } + + if (Config is StandardControllerInputConfig controllerInputConfig) + { + ConfigViewModel = new ControllerInputViewModel(this, new GamepadInputConfig(controllerInputConfig)); + } + } + + public void LoadDevice() + { + if (Config == null || Config.Backend == InputBackendType.Invalid) + { + Device = 0; + } + else + { + var type = DeviceType.None; + + if (Config is StandardKeyboardInputConfig) + { + type = DeviceType.Keyboard; + } + + if (Config is StandardControllerInputConfig) + { + type = DeviceType.Controller; + } + + var item = Devices.FirstOrDefault(x => x.Type == type && x.Id == Config.Id); + if (item != default) + { + Device = Devices.ToList().FindIndex(x => x.Id == item.Id); + } + else + { + Device = 0; + } + } + } + + private void LoadInputDriver() + { + if (_device < 0) + { + return; + } + + string id = GetCurrentGamepadId(); + var type = Devices[Device].Type; + + if (type == DeviceType.None) + { + return; + } + + if (type == DeviceType.Keyboard) + { + if (_mainWindow.InputManager.KeyboardDriver is AvaloniaKeyboardDriver) + { + // NOTE: To get input in this window, we need to bind a custom keyboard driver instead of using the InputManager one as the main window isn't focused... + SelectedGamepad = AvaloniaKeyboardDriver.GetGamepad(id); + } + else + { + SelectedGamepad = _mainWindow.InputManager.KeyboardDriver.GetGamepad(id); + } + } + else + { + SelectedGamepad = _mainWindow.InputManager.GamepadDriver.GetGamepad(id); + } + } + + private void HandleOnGamepadDisconnected(string id) + { + Dispatcher.UIThread.Post(LoadDevices); + } + + private void HandleOnGamepadConnected(string id) + { + Dispatcher.UIThread.Post(LoadDevices); + } + + private string GetCurrentGamepadId() + { + if (_device < 0) + { + return string.Empty; + } + + var device = Devices[Device]; + + if (device.Type == DeviceType.None) + { + return null; + } + + return device.Id.Split(" ")[0]; + } + + public void LoadControllers() + { + Controllers.Clear(); + + if (_playerId == PlayerIndex.Handheld) + { + Controllers.Add(new(ControllerType.Handheld, LocaleManager.Instance[LocaleKeys.ControllerSettingsControllerTypeHandheld])); + + Controller = 0; + } + else + { + Controllers.Add(new(ControllerType.ProController, LocaleManager.Instance[LocaleKeys.ControllerSettingsControllerTypeProController])); + Controllers.Add(new(ControllerType.JoyconPair, LocaleManager.Instance[LocaleKeys.ControllerSettingsControllerTypeJoyConPair])); + Controllers.Add(new(ControllerType.JoyconLeft, LocaleManager.Instance[LocaleKeys.ControllerSettingsControllerTypeJoyConLeft])); + Controllers.Add(new(ControllerType.JoyconRight, LocaleManager.Instance[LocaleKeys.ControllerSettingsControllerTypeJoyConRight])); + + if (Config != null && Controllers.ToList().FindIndex(x => x.Type == Config.ControllerType) != -1) + { + Controller = Controllers.ToList().FindIndex(x => x.Type == Config.ControllerType); + } + else + { + Controller = 0; + } + } + } + + private static string GetShortGamepadName(string str) + { + const string Ellipsis = "..."; + const int MaxSize = 50; + + if (str.Length > MaxSize) + { + return $"{str.AsSpan(0, MaxSize - Ellipsis.Length)}{Ellipsis}"; + } + + return str; + } + + private static string GetShortGamepadId(string str) + { + const string Hyphen = "-"; + const int Offset = 1; + + return str[(str.IndexOf(Hyphen) + Offset)..]; + } + + public void LoadDevices() + { + string GetGamepadName(IGamepad gamepad, int controllerNumber) + { + return $"{GetShortGamepadName(gamepad.Name)} ({controllerNumber})"; + } + string GetUniqueGamepadName(IGamepad gamepad, ref int controllerNumber) + { + string name = GetGamepadName(gamepad, controllerNumber); + if (Devices.Any(controller => controller.Name == name)) + { + controllerNumber++; + name = GetGamepadName(gamepad, controllerNumber); + } + return name; + } + + lock (Devices) + { + Devices.Clear(); + DeviceList.Clear(); + Devices.Add((DeviceType.None, Disabled, LocaleManager.Instance[LocaleKeys.ControllerSettingsDeviceDisabled])); + + int controllerNumber = 0; + foreach (string id in _mainWindow.InputManager.KeyboardDriver.GamepadsIds) + { + using IGamepad gamepad = _mainWindow.InputManager.KeyboardDriver.GetGamepad(id); + + if (gamepad != null) + { + Devices.Add((DeviceType.Keyboard, id, $"{GetShortGamepadName(gamepad.Name)}")); + } + } + + foreach (string id in _mainWindow.InputManager.GamepadDriver.GamepadsIds) + { + using IGamepad gamepad = _mainWindow.InputManager.GamepadDriver.GetGamepad(id); + + if (gamepad != null) + { + string name = GetUniqueGamepadName(gamepad, ref controllerNumber); + Devices.Add((DeviceType.Controller, id, name)); + } + } + + DeviceList.AddRange(Devices.Select(x => x.Name)); + Device = Math.Min(Device, DeviceList.Count); + } + } + + private string GetProfileBasePath() + { + string path = AppDataManager.ProfilesDirPath; + var type = Devices[Device == -1 ? 0 : Device].Type; + + if (type == DeviceType.Keyboard) + { + path = Path.Combine(path, KeyboardString); + } + else if (type == DeviceType.Controller) + { + path = Path.Combine(path, ControllerString); + } + + return path; + } + + private void LoadProfiles() + { + ProfilesList.Clear(); + + string basePath = GetProfileBasePath(); + + if (!Directory.Exists(basePath)) + { + Directory.CreateDirectory(basePath); + } + + ProfilesList.Add((LocaleManager.Instance[LocaleKeys.ControllerSettingsProfileDefault])); + + foreach (string profile in Directory.GetFiles(basePath, "*.json", SearchOption.AllDirectories)) + { + ProfilesList.Add(Path.GetFileNameWithoutExtension(profile)); + } + + if (string.IsNullOrWhiteSpace(ProfileName)) + { + ProfileName = LocaleManager.Instance[LocaleKeys.ControllerSettingsProfileDefault]; + } + } + + public InputConfig LoadDefaultConfiguration() + { + var activeDevice = Devices.FirstOrDefault(); + + if (Devices.Count > 0 && Device < Devices.Count && Device >= 0) + { + activeDevice = Devices[Device]; + } + + InputConfig config; + if (activeDevice.Type == DeviceType.Keyboard) + { + string id = activeDevice.Id; + + config = new StandardKeyboardInputConfig + { + Version = InputConfig.CurrentVersion, + Backend = InputBackendType.WindowKeyboard, + Id = id, + ControllerType = ControllerType.ProController, + LeftJoycon = new LeftJoyconCommonConfig + { + DpadUp = Key.Up, + DpadDown = Key.Down, + DpadLeft = Key.Left, + DpadRight = Key.Right, + ButtonMinus = Key.Minus, + ButtonL = Key.E, + ButtonZl = Key.Q, + ButtonSl = Key.Unbound, + ButtonSr = Key.Unbound, + }, + LeftJoyconStick = + new JoyconConfigKeyboardStick + { + StickUp = Key.W, + StickDown = Key.S, + StickLeft = Key.A, + StickRight = Key.D, + StickButton = Key.F, + }, + RightJoycon = new RightJoyconCommonConfig + { + ButtonA = Key.Z, + ButtonB = Key.X, + ButtonX = Key.C, + ButtonY = Key.V, + ButtonPlus = Key.Plus, + ButtonR = Key.U, + ButtonZr = Key.O, + ButtonSl = Key.Unbound, + ButtonSr = Key.Unbound, + }, + RightJoyconStick = new JoyconConfigKeyboardStick + { + StickUp = Key.I, + StickDown = Key.K, + StickLeft = Key.J, + StickRight = Key.L, + StickButton = Key.H, + }, + }; + } + else if (activeDevice.Type == DeviceType.Controller) + { + bool isNintendoStyle = Devices.ToList().Find(x => x.Id == activeDevice.Id).Name.Contains("Nintendo"); + + string id = activeDevice.Id.Split(" ")[0]; + + config = new StandardControllerInputConfig + { + Version = InputConfig.CurrentVersion, + Backend = InputBackendType.GamepadSDL2, + Id = id, + ControllerType = ControllerType.ProController, + DeadzoneLeft = 0.1f, + DeadzoneRight = 0.1f, + RangeLeft = 1.0f, + RangeRight = 1.0f, + TriggerThreshold = 0.5f, + LeftJoycon = new LeftJoyconCommonConfig + { + DpadUp = ConfigGamepadInputId.DpadUp, + DpadDown = ConfigGamepadInputId.DpadDown, + DpadLeft = ConfigGamepadInputId.DpadLeft, + DpadRight = ConfigGamepadInputId.DpadRight, + ButtonMinus = ConfigGamepadInputId.Minus, + ButtonL = ConfigGamepadInputId.LeftShoulder, + ButtonZl = ConfigGamepadInputId.LeftTrigger, + ButtonSl = ConfigGamepadInputId.Unbound, + ButtonSr = ConfigGamepadInputId.Unbound, + }, + LeftJoyconStick = new JoyconConfigControllerStick + { + Joystick = ConfigStickInputId.Left, + StickButton = ConfigGamepadInputId.LeftStick, + InvertStickX = false, + InvertStickY = false, + }, + RightJoycon = new RightJoyconCommonConfig + { + ButtonA = isNintendoStyle ? ConfigGamepadInputId.A : ConfigGamepadInputId.B, + ButtonB = isNintendoStyle ? ConfigGamepadInputId.B : ConfigGamepadInputId.A, + ButtonX = isNintendoStyle ? ConfigGamepadInputId.X : ConfigGamepadInputId.Y, + ButtonY = isNintendoStyle ? ConfigGamepadInputId.Y : ConfigGamepadInputId.X, + ButtonPlus = ConfigGamepadInputId.Plus, + ButtonR = ConfigGamepadInputId.RightShoulder, + ButtonZr = ConfigGamepadInputId.RightTrigger, + ButtonSl = ConfigGamepadInputId.Unbound, + ButtonSr = ConfigGamepadInputId.Unbound, + }, + RightJoyconStick = new JoyconConfigControllerStick + { + Joystick = ConfigStickInputId.Right, + StickButton = ConfigGamepadInputId.RightStick, + InvertStickX = false, + InvertStickY = false, + }, + Motion = new StandardMotionConfigController + { + MotionBackend = MotionInputBackendType.GamepadDriver, + EnableMotion = true, + Sensitivity = 100, + GyroDeadzone = 1, + }, + Rumble = new RumbleConfigController + { + StrongRumble = 1f, + WeakRumble = 1f, + EnableRumble = false, + }, + }; + } + else + { + config = new InputConfig(); + } + + config.PlayerIndex = _playerId; + + return config; + } + + public async void LoadProfile() + { + if (Device == 0) + { + return; + } + + InputConfig config = null; + + if (string.IsNullOrWhiteSpace(ProfileName)) + { + return; + } + + if (ProfileName == LocaleManager.Instance[LocaleKeys.ControllerSettingsProfileDefault]) + { + config = LoadDefaultConfiguration(); + } + else + { + string path = Path.Combine(GetProfileBasePath(), ProfileName + ".json"); + + if (!File.Exists(path)) + { + int index = ProfilesList.IndexOf(ProfileName); + if (index != -1) + { + ProfilesList.RemoveAt(index); + } + return; + } + + try + { + config = JsonHelper.DeserializeFromFile(path, _serializerContext.InputConfig); + } + catch (JsonException) { } + catch (InvalidOperationException) + { + Logger.Error?.Print(LogClass.Configuration, $"Profile {ProfileName} is incompatible with the current input configuration system."); + + await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogProfileInvalidProfileErrorMessage, ProfileName)); + + return; + } + } + + if (config != null) + { + _isLoaded = false; + + LoadConfiguration(config); + + LoadDevice(); + + _isLoaded = true; + + NotifyChanges(); + } + } + + public async void SaveProfile() + { + if (Device == 0) + { + return; + } + + if (ConfigViewModel == null) + { + return; + } + + if (ProfileName == LocaleManager.Instance[LocaleKeys.ControllerSettingsProfileDefault]) + { + await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogProfileDefaultProfileOverwriteErrorMessage]); + + return; + } + else + { + bool validFileName = ProfileName.IndexOfAny(Path.GetInvalidFileNameChars()) == -1; + + if (validFileName) + { + string path = Path.Combine(GetProfileBasePath(), ProfileName + ".json"); + + InputConfig config = null; + + if (IsKeyboard) + { + config = (ConfigViewModel as KeyboardInputViewModel).Config.GetConfig(); + } + else if (IsController) + { + config = (ConfigViewModel as ControllerInputViewModel).Config.GetConfig(); + } + + config.ControllerType = Controllers[_controller].Type; + + string jsonString = JsonHelper.Serialize(config, _serializerContext.InputConfig); + + await File.WriteAllTextAsync(path, jsonString); + + LoadProfiles(); + } + else + { + await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogProfileInvalidProfileNameErrorMessage]); + } + } + } + + public async void RemoveProfile() + { + if (Device == 0 || ProfileName == LocaleManager.Instance[LocaleKeys.ControllerSettingsProfileDefault] || ProfilesList.IndexOf(ProfileName) == -1) + { + return; + } + + UserResult result = await ContentDialogHelper.CreateConfirmationDialog( + LocaleManager.Instance[LocaleKeys.DialogProfileDeleteProfileTitle], + LocaleManager.Instance[LocaleKeys.DialogProfileDeleteProfileMessage], + LocaleManager.Instance[LocaleKeys.InputDialogYes], + LocaleManager.Instance[LocaleKeys.InputDialogNo], + LocaleManager.Instance[LocaleKeys.RyujinxConfirm]); + + if (result == UserResult.Yes) + { + string path = Path.Combine(GetProfileBasePath(), ProfileName + ".json"); + + if (File.Exists(path)) + { + File.Delete(path); + } + + LoadProfiles(); + } + } + + public void Save() + { + IsModified = false; + + List newConfig = new(); + + newConfig.AddRange(ConfigurationState.Instance.Hid.InputConfig.Value); + + newConfig.Remove(newConfig.Find(x => x == null)); + + if (Device == 0) + { + newConfig.Remove(newConfig.Find(x => x.PlayerIndex == this.PlayerId)); + } + else + { + var device = Devices[Device]; + + if (device.Type == DeviceType.Keyboard) + { + var inputConfig = (ConfigViewModel as KeyboardInputViewModel).Config; + inputConfig.Id = device.Id; + } + else + { + var inputConfig = (ConfigViewModel as ControllerInputViewModel).Config; + inputConfig.Id = device.Id.Split(" ")[0]; + } + + var config = !IsController + ? (ConfigViewModel as KeyboardInputViewModel).Config.GetConfig() + : (ConfigViewModel as ControllerInputViewModel).Config.GetConfig(); + config.ControllerType = Controllers[_controller].Type; + config.PlayerIndex = _playerId; + + int i = newConfig.FindIndex(x => x.PlayerIndex == PlayerId); + if (i == -1) + { + newConfig.Add(config); + } + else + { + newConfig[i] = config; + } + } + + _mainWindow.ViewModel.AppHost?.NpadManager.ReloadConfiguration(newConfig, ConfigurationState.Instance.Hid.EnableKeyboard, ConfigurationState.Instance.Hid.EnableMouse); + + // Atomically replace and signal input change. + // NOTE: Do not modify InputConfig.Value directly as other code depends on the on-change event. + ConfigurationState.Instance.Hid.InputConfig.Value = newConfig; + + ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.ConfigurationPath); + } + + public void NotifyChange(string property) + { + OnPropertyChanged(property); + } + + public void NotifyChanges() + { + OnPropertyChanged(nameof(ConfigViewModel)); + OnPropertyChanged(nameof(IsController)); + OnPropertyChanged(nameof(ShowSettings)); + OnPropertyChanged(nameof(IsKeyboard)); + OnPropertyChanged(nameof(IsRight)); + OnPropertyChanged(nameof(IsLeft)); + NotifyChangesEvent?.Invoke(); + } + + public void Dispose() + { + GC.SuppressFinalize(this); + + _mainWindow.InputManager.GamepadDriver.OnGamepadConnected -= HandleOnGamepadConnected; + _mainWindow.InputManager.GamepadDriver.OnGamepadDisconnected -= HandleOnGamepadDisconnected; + + _mainWindow.ViewModel.AppHost?.NpadManager.UnblockInputUpdates(); + + SelectedGamepad?.Dispose(); + + AvaloniaKeyboardDriver.Dispose(); + } + } +} diff --git a/src/Ryujinx/UI/ViewModels/Input/KeyboardInputViewModel.cs b/src/Ryujinx/UI/ViewModels/Input/KeyboardInputViewModel.cs new file mode 100644 index 000000000..0b530eb09 --- /dev/null +++ b/src/Ryujinx/UI/ViewModels/Input/KeyboardInputViewModel.cs @@ -0,0 +1,73 @@ +using Avalonia.Svg.Skia; +using Ryujinx.Ava.UI.Models.Input; + +namespace Ryujinx.Ava.UI.ViewModels.Input +{ + public class KeyboardInputViewModel : BaseModel + { + private KeyboardInputConfig _config; + public KeyboardInputConfig Config + { + get => _config; + set + { + _config = value; + OnPropertyChanged(); + } + } + + private bool _isLeft; + public bool IsLeft + { + get => _isLeft; + set + { + _isLeft = value; + OnPropertyChanged(); + OnPropertyChanged(nameof(HasSides)); + } + } + + private bool _isRight; + public bool IsRight + { + get => _isRight; + set + { + _isRight = value; + OnPropertyChanged(); + OnPropertyChanged(nameof(HasSides)); + } + } + + public bool HasSides => IsLeft ^ IsRight; + + private SvgImage _image; + public SvgImage Image + { + get => _image; + set + { + _image = value; + OnPropertyChanged(); + } + } + + public readonly InputViewModel ParentModel; + + public KeyboardInputViewModel(InputViewModel model, KeyboardInputConfig config) + { + ParentModel = model; + model.NotifyChangesEvent += OnParentModelChanged; + OnParentModelChanged(); + Config = config; + } + + public void OnParentModelChanged() + { + IsLeft = ParentModel.IsLeft; + IsRight = ParentModel.IsRight; + Image = ParentModel.Image; + } + } +} diff --git a/src/Ryujinx/UI/ViewModels/Input/MotionInputViewModel.cs b/src/Ryujinx/UI/ViewModels/Input/MotionInputViewModel.cs new file mode 100644 index 000000000..c9ed8f2d4 --- /dev/null +++ b/src/Ryujinx/UI/ViewModels/Input/MotionInputViewModel.cs @@ -0,0 +1,93 @@ +namespace Ryujinx.Ava.UI.ViewModels.Input +{ + public class MotionInputViewModel : BaseModel + { + private int _slot; + public int Slot + { + get => _slot; + set + { + _slot = value; + OnPropertyChanged(); + } + } + + private int _altSlot; + public int AltSlot + { + get => _altSlot; + set + { + _altSlot = value; + OnPropertyChanged(); + } + } + + private string _dsuServerHost; + public string DsuServerHost + { + get => _dsuServerHost; + set + { + _dsuServerHost = value; + OnPropertyChanged(); + } + } + + private int _dsuServerPort; + public int DsuServerPort + { + get => _dsuServerPort; + set + { + _dsuServerPort = value; + OnPropertyChanged(); + } + } + + private bool _mirrorInput; + public bool MirrorInput + { + get => _mirrorInput; + set + { + _mirrorInput = value; + OnPropertyChanged(); + } + } + + private int _sensitivity; + public int Sensitivity + { + get => _sensitivity; + set + { + _sensitivity = value; + OnPropertyChanged(); + } + } + + private double _gryoDeadzone; + public double GyroDeadzone + { + get => _gryoDeadzone; + set + { + _gryoDeadzone = value; + OnPropertyChanged(); + } + } + + private bool _enableCemuHookMotion; + public bool EnableCemuHookMotion + { + get => _enableCemuHookMotion; + set + { + _enableCemuHookMotion = value; + OnPropertyChanged(); + } + } + } +} diff --git a/src/Ryujinx/UI/ViewModels/Input/RumbleInputViewModel.cs b/src/Ryujinx/UI/ViewModels/Input/RumbleInputViewModel.cs new file mode 100644 index 000000000..8ad33cf4c --- /dev/null +++ b/src/Ryujinx/UI/ViewModels/Input/RumbleInputViewModel.cs @@ -0,0 +1,27 @@ +namespace Ryujinx.Ava.UI.ViewModels.Input +{ + public class RumbleInputViewModel : BaseModel + { + private float _strongRumble; + public float StrongRumble + { + get => _strongRumble; + set + { + _strongRumble = value; + OnPropertyChanged(); + } + } + + private float _weakRumble; + public float WeakRumble + { + get => _weakRumble; + set + { + _weakRumble = value; + OnPropertyChanged(); + } + } + } +} diff --git a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs new file mode 100644 index 000000000..824fdd717 --- /dev/null +++ b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs @@ -0,0 +1,2078 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Input; +using Avalonia.Media; +using Avalonia.Media.Imaging; +using Avalonia.Platform.Storage; +using Avalonia.Threading; +using DynamicData; +using DynamicData.Binding; +using FluentAvalonia.UI.Controls; +using LibHac.Common; +using Ryujinx.Ava.Common; +using Ryujinx.Ava.Common.Locale; +using Ryujinx.Ava.Input; +using Ryujinx.Ava.UI.Controls; +using Ryujinx.Ava.UI.Helpers; +using Ryujinx.Ava.UI.Models; +using Ryujinx.Ava.UI.Models.Generic; +using Ryujinx.Ava.UI.Renderer; +using Ryujinx.Ava.UI.Windows; +using Ryujinx.Common; +using Ryujinx.Common.Configuration; +using Ryujinx.Common.Logging; +using Ryujinx.Common.Utilities; +using Ryujinx.Cpu; +using Ryujinx.HLE; +using Ryujinx.HLE.FileSystem; +using Ryujinx.HLE.HOS; +using Ryujinx.HLE.HOS.Services.Account.Acc; +using Ryujinx.HLE.UI; +using Ryujinx.Input.HLE; +using Ryujinx.UI.App.Common; +using Ryujinx.UI.Common; +using Ryujinx.UI.Common.Configuration; +using Ryujinx.UI.Common.Helper; +using SkiaSharp; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Key = Ryujinx.Input.Key; +using MissingKeyException = LibHac.Common.Keys.MissingKeyException; +using ShaderCacheLoadingState = Ryujinx.Graphics.Gpu.Shader.ShaderCacheState; + +namespace Ryujinx.Ava.UI.ViewModels +{ + public class MainWindowViewModel : BaseModel + { + private const int HotKeyPressDelayMs = 500; + private delegate int LoadContentFromFolderDelegate(List dirs, out int numRemoved); + + private ObservableCollectionExtended _applications; + private string _aspectStatusText; + + private string _loadHeading; + private string _cacheLoadStatus; + private string _searchText; + private Timer _searchTimer; + private string _dockedStatusText; + private string _vSyncModeText; + private string _fifoStatusText; + private string _gameStatusText; + private string _volumeStatusText; + private string _gpuStatusText; + private string _shaderCountText; + private bool _isAmiiboRequested; + private bool _showRightmostSeparator; + private bool _isGameRunning; + private bool _isFullScreen; + private int _progressMaximum; + private int _progressValue; + private long _lastFullscreenToggle = Environment.TickCount64; + private bool _showLoadProgress; + private bool _showMenuAndStatusBar = true; + private bool _showStatusSeparator; + private Brush _progressBarForegroundColor; + private Brush _progressBarBackgroundColor; + private Brush _vSyncModeColor; + private byte[] _selectedIcon; + private bool _isAppletMenuActive; + private int _statusBarProgressMaximum; + private int _statusBarProgressValue; + private string _statusBarProgressStatusText; + private bool _statusBarProgressStatusVisible; + private bool _isPaused; + private bool _showContent = true; + private bool _isLoadingIndeterminate = true; + private bool _showAll; + private string _lastScannedAmiiboId; + private bool _statusBarVisible; + private ReadOnlyObservableCollection _appsObservableList; + + private string _showUiKey = "F4"; + private string _pauseKey = "F5"; + private string _screenshotKey = "F8"; + private float _volume; + private float _volumeBeforeMute; + private string _backendText; + + private bool _areMimeTypesRegistered = FileAssociationHelper.AreMimeTypesRegistered; + private bool _canUpdate = true; + private Cursor _cursor; + private string _title; + private ApplicationData _currentApplicationData; + private readonly AutoResetEvent _rendererWaitEvent; + private WindowState _windowState; + private double _windowWidth; + private double _windowHeight; + private int _customVSyncInterval; + private int _customVSyncIntervalPercentageProxy; + + private bool _isActive; + private bool _isSubMenuOpen; + + public ApplicationData ListSelectedApplication; + public ApplicationData GridSelectedApplication; + + public IEnumerable LastLdnGameData; + + public static readonly Bitmap IconBitmap = + new(Assembly.GetAssembly(typeof(ConfigurationState))!.GetManifestResourceStream("Ryujinx.UI.Common.Resources.Logo_Ryujinx.png")!); + + public MainWindow Window { get; init; } + + internal AppHost AppHost { get; set; } + + public MainWindowViewModel() + { + Applications = []; + + Applications.ToObservableChangeSet() + .Filter(Filter) + .Sort(GetComparer()) + .Bind(out _appsObservableList) + .AsObservableList(); + + _rendererWaitEvent = new AutoResetEvent(false); + + if (Program.PreviewerDetached) + { + LoadConfigurableHotKeys(); + + Volume = ConfigurationState.Instance.System.AudioVolume; + } + CustomVSyncInterval = ConfigurationState.Instance.Graphics.CustomVSyncInterval.Value; + } + + public void Initialize( + ContentManager contentManager, + IStorageProvider storageProvider, + ApplicationLibrary applicationLibrary, + VirtualFileSystem virtualFileSystem, + AccountManager accountManager, + InputManager inputManager, + UserChannelPersistence userChannelPersistence, + LibHacHorizonManager libHacHorizonManager, + IHostUIHandler uiHandler, + Action showLoading, + Action switchToGameControl, + Action setMainContent, + TopLevel topLevel) + { + ContentManager = contentManager; + StorageProvider = storageProvider; + ApplicationLibrary = applicationLibrary; + VirtualFileSystem = virtualFileSystem; + AccountManager = accountManager; + InputManager = inputManager; + UserChannelPersistence = userChannelPersistence; + LibHacHorizonManager = libHacHorizonManager; + UiHandler = uiHandler; + + ShowLoading = showLoading; + SwitchToGameControl = switchToGameControl; + SetMainContent = setMainContent; + TopLevel = topLevel; + +#if DEBUG + topLevel.AttachDevTools(new KeyGesture(Avalonia.Input.Key.F12, KeyModifiers.Control)); +#endif + } + + #region Properties + + public string SearchText + { + get => _searchText; + set + { + _searchText = value; + + _searchTimer?.Dispose(); + + _searchTimer = new Timer(_ => + { + RefreshView(); + + _searchTimer.Dispose(); + _searchTimer = null; + }, null, 1000, 0); + } + } + + public bool CanUpdate + { + get => _canUpdate && EnableNonGameRunningControls && Updater.CanUpdate(false); + set + { + _canUpdate = value; + OnPropertyChanged(); + } + } + + public Cursor Cursor + { + get => _cursor; + set + { + _cursor = value; + OnPropertyChanged(); + } + } + + public ReadOnlyObservableCollection AppsObservableList + { + get => _appsObservableList; + set + { + _appsObservableList = value; + + OnPropertyChanged(); + } + } + + public bool IsPaused + { + get => _isPaused; + set + { + _isPaused = value; + + OnPropertyChanged(); + } + } + + public long LastFullscreenToggle + { + get => _lastFullscreenToggle; + set + { + _lastFullscreenToggle = value; + + OnPropertyChanged(); + } + } + + public bool StatusBarVisible + { + get => _statusBarVisible && EnableNonGameRunningControls; + set + { + _statusBarVisible = value; + + OnPropertyChanged(); + } + } + + public bool EnableNonGameRunningControls => !IsGameRunning; + + public bool ShowFirmwareStatus => !ShowLoadProgress; + + public bool ShowRightmostSeparator + { + get => _showRightmostSeparator; + set + { + _showRightmostSeparator = value; + + OnPropertyChanged(); + } + } + + public bool IsGameRunning + { + get => _isGameRunning; + set + { + _isGameRunning = value; + + if (!value) + { + ShowMenuAndStatusBar = false; + } + + OnPropertyChanged(); + OnPropertyChanged(nameof(EnableNonGameRunningControls)); + OnPropertyChanged(nameof(IsAppletMenuActive)); + OnPropertyChanged(nameof(StatusBarVisible)); + OnPropertyChanged(nameof(ShowFirmwareStatus)); + } + } + + public bool IsAmiiboRequested + { + get => _isAmiiboRequested && _isGameRunning; + set + { + _isAmiiboRequested = value; + + OnPropertyChanged(); + } + } + + public bool ShowLoadProgress + { + get => _showLoadProgress; + set + { + _showLoadProgress = value; + + OnPropertyChanged(); + OnPropertyChanged(nameof(ShowFirmwareStatus)); + } + } + + public string GameStatusText + { + get => _gameStatusText; + set + { + _gameStatusText = value; + + OnPropertyChanged(); + } + } + + public bool IsFullScreen + { + get => _isFullScreen; + set + { + _isFullScreen = value; + + OnPropertyChanged(); + } + } + + public bool IsSubMenuOpen + { + get => _isSubMenuOpen; + set + { + _isSubMenuOpen = value; + + OnPropertyChanged(); + } + } + + public bool ShowAll + { + get => _showAll; + set + { + _showAll = value; + + OnPropertyChanged(); + } + } + + public string LastScannedAmiiboId + { + get => _lastScannedAmiiboId; + set + { + _lastScannedAmiiboId = value; + + OnPropertyChanged(); + } + } + + public ApplicationData SelectedApplication + { + get + { + return Glyph switch + { + Glyph.List => ListSelectedApplication, + Glyph.Grid => GridSelectedApplication, + _ => null, + }; + } + } + + public bool OpenUserSaveDirectoryEnabled => !SelectedApplication.ControlHolder.ByteSpan.IsZeros() && SelectedApplication.ControlHolder.Value.UserAccountSaveDataSize > 0; + + public bool OpenDeviceSaveDirectoryEnabled => !SelectedApplication.ControlHolder.ByteSpan.IsZeros() && SelectedApplication.ControlHolder.Value.DeviceSaveDataSize > 0; + + public bool TrimXCIEnabled => Ryujinx.Common.Utilities.XCIFileTrimmer.CanTrim(SelectedApplication.Path, new Common.XCIFileTrimmerMainWindowLog(this)); + + public bool OpenBcatSaveDirectoryEnabled => !SelectedApplication.ControlHolder.ByteSpan.IsZeros() && SelectedApplication.ControlHolder.Value.BcatDeliveryCacheStorageSize > 0; + + public bool CreateShortcutEnabled => !ReleaseInformation.IsFlatHubBuild; + + public string LoadHeading + { + get => _loadHeading; + set + { + _loadHeading = value; + + OnPropertyChanged(); + } + } + + public string CacheLoadStatus + { + get => _cacheLoadStatus; + set + { + _cacheLoadStatus = value; + + OnPropertyChanged(); + } + } + + public Brush ProgressBarBackgroundColor + { + get => _progressBarBackgroundColor; + set + { + _progressBarBackgroundColor = value; + + OnPropertyChanged(); + } + } + + public Brush ProgressBarForegroundColor + { + get => _progressBarForegroundColor; + set + { + _progressBarForegroundColor = value; + + OnPropertyChanged(); + } + } + + public Brush VSyncModeColor + { + get => _vSyncModeColor; + set + { + _vSyncModeColor = value; + + OnPropertyChanged(); + } + } + + public bool ShowCustomVSyncIntervalPicker + { + get + { + if (_isGameRunning) + { + return AppHost.Device.VSyncMode == + VSyncMode.Custom; + } + else + { + return false; + } + } + set + { + OnPropertyChanged(); + } + } + + public int CustomVSyncIntervalPercentageProxy + { + get => _customVSyncIntervalPercentageProxy; + set + { + int newInterval = (int)((value / 100f) * 60); + _customVSyncInterval = newInterval; + _customVSyncIntervalPercentageProxy = value; + if (_isGameRunning) + { + AppHost.Device.CustomVSyncInterval = newInterval; + AppHost.Device.UpdateVSyncInterval(); + } + OnPropertyChanged((nameof(CustomVSyncInterval))); + OnPropertyChanged((nameof(CustomVSyncIntervalPercentageText))); + } + } + + public string CustomVSyncIntervalPercentageText + { + get + { + string text = CustomVSyncIntervalPercentageProxy.ToString() + "%"; + return text; + } + set + { + + } + } + + public int CustomVSyncInterval + { + get => _customVSyncInterval; + set + { + _customVSyncInterval = value; + int newPercent = (int)((value / 60f) * 100); + _customVSyncIntervalPercentageProxy = newPercent; + if (_isGameRunning) + { + AppHost.Device.CustomVSyncInterval = value; + AppHost.Device.UpdateVSyncInterval(); + } + OnPropertyChanged(nameof(CustomVSyncIntervalPercentageProxy)); + OnPropertyChanged(nameof(CustomVSyncIntervalPercentageText)); + OnPropertyChanged(); + } + } + + public byte[] SelectedIcon + { + get => _selectedIcon; + set + { + _selectedIcon = value; + + OnPropertyChanged(); + } + } + + public int ProgressMaximum + { + get => _progressMaximum; + set + { + _progressMaximum = value; + + OnPropertyChanged(); + } + } + + public int ProgressValue + { + get => _progressValue; + set + { + _progressValue = value; + + OnPropertyChanged(); + } + } + + public int StatusBarProgressMaximum + { + get => _statusBarProgressMaximum; + set + { + _statusBarProgressMaximum = value; + + OnPropertyChanged(); + } + } + + public int StatusBarProgressValue + { + get => _statusBarProgressValue; + set + { + _statusBarProgressValue = value; + + OnPropertyChanged(); + } + } + + public bool StatusBarProgressStatusVisible + { + get => _statusBarProgressStatusVisible; + set + { + _statusBarProgressStatusVisible = value; + + OnPropertyChanged(); + } + } + + public string StatusBarProgressStatusText + { + get => _statusBarProgressStatusText; + set + { + _statusBarProgressStatusText = value; + + OnPropertyChanged(); + } + } + + public string FifoStatusText + { + get => _fifoStatusText; + set + { + _fifoStatusText = value; + + OnPropertyChanged(); + } + } + + public string GpuNameText + { + get => _gpuStatusText; + set + { + _gpuStatusText = value; + + OnPropertyChanged(); + } + } + + public string ShaderCountText + { + get => _shaderCountText; + set + { + _shaderCountText = value; + OnPropertyChanged(); + } + } + + public string BackendText + { + get => _backendText; + set + { + _backendText = value; + + OnPropertyChanged(); + } + } + + public string VSyncModeText + { + get => _vSyncModeText; + set + { + _vSyncModeText = value; + + OnPropertyChanged(); + } + } + + public string DockedStatusText + { + get => _dockedStatusText; + set + { + _dockedStatusText = value; + + OnPropertyChanged(); + } + } + + public string AspectRatioStatusText + { + get => _aspectStatusText; + set + { + _aspectStatusText = value; + + OnPropertyChanged(); + } + } + + public string VolumeStatusText + { + get => _volumeStatusText; + set + { + _volumeStatusText = value; + + OnPropertyChanged(); + } + } + + public bool VolumeMuted => _volume == 0; + + public float Volume + { + get => _volume; + set + { + _volume = value; + + if (_isGameRunning) + { + AppHost.Device.SetVolume(_volume); + } + + OnPropertyChanged(nameof(VolumeStatusText)); + OnPropertyChanged(nameof(VolumeMuted)); + OnPropertyChanged(); + } + } + + public float VolumeBeforeMute + { + get => _volumeBeforeMute; + set + { + _volumeBeforeMute = value; + + OnPropertyChanged(); + } + } + + public bool ShowStatusSeparator + { + get => _showStatusSeparator; + set + { + _showStatusSeparator = value; + + OnPropertyChanged(); + } + } + + public bool ShowMenuAndStatusBar + { + get => _showMenuAndStatusBar; + set + { + _showMenuAndStatusBar = value; + + OnPropertyChanged(); + } + } + + public bool IsLoadingIndeterminate + { + get => _isLoadingIndeterminate; + set + { + _isLoadingIndeterminate = value; + + OnPropertyChanged(); + } + } + + public bool IsActive + { + get => _isActive; + set + { + _isActive = value; + + OnPropertyChanged(); + } + } + + + public bool ShowContent + { + get => _showContent; + set + { + _showContent = value; + + OnPropertyChanged(); + } + } + + public bool IsAppletMenuActive + { + get => _isAppletMenuActive && EnableNonGameRunningControls; + set + { + _isAppletMenuActive = value; + + OnPropertyChanged(); + } + } + + public WindowState WindowState + { + get => _windowState; + internal set + { + _windowState = value; + + OnPropertyChanged(); + } + } + + public double WindowWidth + { + get => _windowWidth; + set + { + _windowWidth = value; + + OnPropertyChanged(); + } + } + + public double WindowHeight + { + get => _windowHeight; + set + { + _windowHeight = value; + + OnPropertyChanged(); + } + } + + public bool IsGrid => Glyph == Glyph.Grid; + public bool IsList => Glyph == Glyph.List; + + internal void Sort(bool isAscending) + { + IsAscending = isAscending; + + RefreshView(); + } + + internal void Sort(ApplicationSort sort) + { + SortMode = sort; + + RefreshView(); + } + + public bool StartGamesInFullscreen + { + get => ConfigurationState.Instance.UI.StartFullscreen; + set + { + ConfigurationState.Instance.UI.StartFullscreen.Value = value; + + ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.ConfigurationPath); + + OnPropertyChanged(); + } + } + + public bool ShowConsole + { + get => ConfigurationState.Instance.UI.ShowConsole; + set + { + ConfigurationState.Instance.UI.ShowConsole.Value = value; + + ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.ConfigurationPath); + + OnPropertyChanged(); + } + } + + public string Title + { + get => _title; + set + { + _title = value; + + OnPropertyChanged(); + } + } + + public bool ShowConsoleVisible + { + get => ConsoleHelper.SetConsoleWindowStateSupported; + } + + public bool ManageFileTypesVisible + { + get => FileAssociationHelper.IsTypeAssociationSupported; + } + + public bool AreMimeTypesRegistered + { + get => _areMimeTypesRegistered; + set { + _areMimeTypesRegistered = value; + + OnPropertyChanged(); + } + } + + public ObservableCollectionExtended Applications + { + get => _applications; + set + { + _applications = value; + + OnPropertyChanged(); + } + } + + public Glyph Glyph + { + get => (Glyph)ConfigurationState.Instance.UI.GameListViewMode.Value; + set + { + ConfigurationState.Instance.UI.GameListViewMode.Value = (int)value; + + OnPropertyChanged(); + OnPropertyChanged(nameof(IsGrid)); + OnPropertyChanged(nameof(IsList)); + + ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.ConfigurationPath); + } + } + + public bool ShowNames + { + get => ConfigurationState.Instance.UI.ShowNames && ConfigurationState.Instance.UI.GridSize > 1; set + { + ConfigurationState.Instance.UI.ShowNames.Value = value; + + OnPropertyChanged(); + OnPropertyChanged(nameof(GridSizeScale)); + OnPropertyChanged(nameof(GridItemSelectorSize)); + + ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.ConfigurationPath); + } + } + + internal ApplicationSort SortMode + { + get => (ApplicationSort)ConfigurationState.Instance.UI.ApplicationSort.Value; + private set + { + ConfigurationState.Instance.UI.ApplicationSort.Value = (int)value; + + OnPropertyChanged(); + OnPropertyChanged(nameof(SortName)); + + ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.ConfigurationPath); + } + } + + public int ListItemSelectorSize + { + get + { + return ConfigurationState.Instance.UI.GridSize.Value switch + { + 1 => 78, + 2 => 100, + 3 => 120, + 4 => 140, + _ => 16, + }; + } + } + + public int GridItemSelectorSize + { + get + { + return ConfigurationState.Instance.UI.GridSize.Value switch + { + 1 => 120, + 2 => ShowNames ? 210 : 150, + 3 => ShowNames ? 240 : 180, + 4 => ShowNames ? 280 : 220, + _ => 16, + }; + } + } + + public int GridSizeScale + { + get => ConfigurationState.Instance.UI.GridSize; + set + { + ConfigurationState.Instance.UI.GridSize.Value = value; + + if (value < 2) + { + ShowNames = false; + } + + OnPropertyChanged(); + OnPropertyChanged(nameof(IsGridSmall)); + OnPropertyChanged(nameof(IsGridMedium)); + OnPropertyChanged(nameof(IsGridLarge)); + OnPropertyChanged(nameof(IsGridHuge)); + OnPropertyChanged(nameof(ListItemSelectorSize)); + OnPropertyChanged(nameof(GridItemSelectorSize)); + OnPropertyChanged(nameof(ShowNames)); + + ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.ConfigurationPath); + } + } + + public string SortName + { + get + { + return SortMode switch + { + ApplicationSort.Title => LocaleManager.Instance[LocaleKeys.GameListHeaderApplication], + ApplicationSort.Developer => LocaleManager.Instance[LocaleKeys.GameListHeaderDeveloper], + ApplicationSort.LastPlayed => LocaleManager.Instance[LocaleKeys.GameListHeaderLastPlayed], + ApplicationSort.TotalTimePlayed => LocaleManager.Instance[LocaleKeys.GameListHeaderTimePlayed], + ApplicationSort.FileType => LocaleManager.Instance[LocaleKeys.GameListHeaderFileExtension], + ApplicationSort.FileSize => LocaleManager.Instance[LocaleKeys.GameListHeaderFileSize], + ApplicationSort.Path => LocaleManager.Instance[LocaleKeys.GameListHeaderPath], + ApplicationSort.Favorite => LocaleManager.Instance[LocaleKeys.CommonFavorite], + _ => string.Empty, + }; + } + } + + public bool IsAscending + { + get => ConfigurationState.Instance.UI.IsAscendingOrder; + private set + { + ConfigurationState.Instance.UI.IsAscendingOrder.Value = value; + + OnPropertyChanged(); + OnPropertyChanged(nameof(SortMode)); + OnPropertyChanged(nameof(SortName)); + + ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.ConfigurationPath); + } + } + + public KeyGesture ShowUiKey + { + get => KeyGesture.Parse(_showUiKey); + set + { + _showUiKey = value.ToString(); + + OnPropertyChanged(); + } + } + + public KeyGesture ScreenshotKey + { + get => KeyGesture.Parse(_screenshotKey); + set + { + _screenshotKey = value.ToString(); + + OnPropertyChanged(); + } + } + + public KeyGesture PauseKey + { + get => KeyGesture.Parse(_pauseKey); + set + { + _pauseKey = value.ToString(); + + OnPropertyChanged(); + } + } + + public ContentManager ContentManager { get; private set; } + public IStorageProvider StorageProvider { get; private set; } + public ApplicationLibrary ApplicationLibrary { get; private set; } + public VirtualFileSystem VirtualFileSystem { get; private set; } + public AccountManager AccountManager { get; private set; } + public InputManager InputManager { get; private set; } + public UserChannelPersistence UserChannelPersistence { get; private set; } + public Action ShowLoading { get; private set; } + public Action SwitchToGameControl { get; private set; } + public Action SetMainContent { get; private set; } + public TopLevel TopLevel { get; private set; } + public RendererHost RendererHostControl { get; private set; } + public bool IsClosing { get; set; } + public LibHacHorizonManager LibHacHorizonManager { get; internal set; } + public IHostUIHandler UiHandler { get; internal set; } + public bool IsSortedByFavorite => SortMode == ApplicationSort.Favorite; + public bool IsSortedByTitle => SortMode == ApplicationSort.Title; + public bool IsSortedByDeveloper => SortMode == ApplicationSort.Developer; + public bool IsSortedByLastPlayed => SortMode == ApplicationSort.LastPlayed; + public bool IsSortedByTimePlayed => SortMode == ApplicationSort.TotalTimePlayed; + public bool IsSortedByType => SortMode == ApplicationSort.FileType; + public bool IsSortedBySize => SortMode == ApplicationSort.FileSize; + public bool IsSortedByPath => SortMode == ApplicationSort.Path; + public bool IsGridSmall => ConfigurationState.Instance.UI.GridSize == 1; + public bool IsGridMedium => ConfigurationState.Instance.UI.GridSize == 2; + public bool IsGridLarge => ConfigurationState.Instance.UI.GridSize == 3; + public bool IsGridHuge => ConfigurationState.Instance.UI.GridSize == 4; + + #endregion + + #region PrivateMethods + + private static IComparer CreateComparer(bool ascending, Func selector) => + ascending + ? SortExpressionComparer.Ascending(selector) + : SortExpressionComparer.Descending(selector); + + private IComparer GetComparer() + => SortMode switch + { +#pragma warning disable IDE0055 // Disable formatting + ApplicationSort.Title => CreateComparer(IsAscending, app => app.Name), + ApplicationSort.Developer => CreateComparer(IsAscending, app => app.Developer), + ApplicationSort.LastPlayed => new LastPlayedSortComparer(IsAscending), + ApplicationSort.TotalTimePlayed => new TimePlayedSortComparer(IsAscending), + ApplicationSort.FileType => CreateComparer(IsAscending, app => app.FileExtension), + ApplicationSort.FileSize => CreateComparer(IsAscending, app => app.FileSize), + ApplicationSort.Path => CreateComparer(IsAscending, app => app.Path), + ApplicationSort.Favorite => CreateComparer(IsAscending, app => new AppListFavoriteComparable(app)), + _ => null, +#pragma warning restore IDE0055 + }; + + public void RefreshView() + { + RefreshGrid(); + } + + private void RefreshGrid() + { + Applications.ToObservableChangeSet() + .Filter(Filter) + .Sort(GetComparer()) + .Bind(out _appsObservableList).AsObservableList(); + + OnPropertyChanged(nameof(AppsObservableList)); + } + + private bool Filter(object arg) + { + if (arg is ApplicationData app) + { + if (string.IsNullOrWhiteSpace(_searchText)) + { + return true; + } + + CompareInfo compareInfo = CultureInfo.CurrentCulture.CompareInfo; + + return compareInfo.IndexOf(app.Name, _searchText, CompareOptions.IgnoreCase | CompareOptions.IgnoreNonSpace) >= 0; + } + + return false; + } + + private async Task HandleFirmwareInstallation(string filename) + { + try + { + SystemVersion firmwareVersion = ContentManager.VerifyFirmwarePackage(filename); + + if (firmwareVersion == null) + { + await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogFirmwareInstallerFirmwareNotFoundErrorMessage, filename)); + + return; + } + + string dialogTitle = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogFirmwareInstallerFirmwareInstallTitle, firmwareVersion.VersionString); + string dialogMessage = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogFirmwareInstallerFirmwareInstallMessage, firmwareVersion.VersionString); + + SystemVersion currentVersion = ContentManager.GetCurrentFirmwareVersion(); + if (currentVersion != null) + { + dialogMessage += LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogFirmwareInstallerFirmwareInstallSubMessage, currentVersion.VersionString); + } + + dialogMessage += LocaleManager.Instance[LocaleKeys.DialogFirmwareInstallerFirmwareInstallConfirmMessage]; + + UserResult result = await ContentDialogHelper.CreateConfirmationDialog( + dialogTitle, + dialogMessage, + LocaleManager.Instance[LocaleKeys.InputDialogYes], + LocaleManager.Instance[LocaleKeys.InputDialogNo], + LocaleManager.Instance[LocaleKeys.RyujinxConfirm]); + + UpdateWaitWindow waitingDialog = new(dialogTitle, LocaleManager.Instance[LocaleKeys.DialogFirmwareInstallerFirmwareInstallWaitMessage]); + + if (result == UserResult.Yes) + { + Logger.Info?.Print(LogClass.Application, $"Installing firmware {firmwareVersion.VersionString}"); + + Thread thread = new(() => + { + Dispatcher.UIThread.InvokeAsync(delegate + { + waitingDialog.Show(); + }); + + try + { + ContentManager.InstallFirmware(filename); + + Dispatcher.UIThread.InvokeAsync(async delegate + { + waitingDialog.Close(); + + string message = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogFirmwareInstallerFirmwareInstallSuccessMessage, firmwareVersion.VersionString); + + await ContentDialogHelper.CreateInfoDialog( + dialogTitle, + message, + LocaleManager.Instance[LocaleKeys.InputDialogOk], + string.Empty, + LocaleManager.Instance[LocaleKeys.RyujinxInfo]); + + Logger.Info?.Print(LogClass.Application, message); + + // Purge Applet Cache. + + DirectoryInfo miiEditorCacheFolder = new(Path.Combine(AppDataManager.GamesDirPath, "0100000000001009", "cache")); + + if (miiEditorCacheFolder.Exists) + { + miiEditorCacheFolder.Delete(true); + } + }); + } + catch (Exception ex) + { + Dispatcher.UIThread.InvokeAsync(async () => + { + waitingDialog.Close(); + + await ContentDialogHelper.CreateErrorDialog(ex.Message); + }); + } + finally + { + RefreshFirmwareStatus(); + } + }) + { + Name = "GUI.FirmwareInstallerThread", + }; + + thread.Start(); + } + } + catch (MissingKeyException ex) + { + if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime) + { + Logger.Error?.Print(LogClass.Application, ex.ToString()); + + await UserErrorDialog.ShowUserErrorDialog(UserError.NoKeys); + } + } + catch (Exception ex) + { + await ContentDialogHelper.CreateErrorDialog(ex.Message); + } + } + + private void ProgressHandler(T state, int current, int total) where T : Enum + { + Dispatcher.UIThread.Post(() => + { + ProgressMaximum = total; + ProgressValue = current; + + switch (state) + { + case LoadState ptcState: + CacheLoadStatus = $"{current} / {total}"; + switch (ptcState) + { + case LoadState.Unloaded: + case LoadState.Loading: + LoadHeading = LocaleManager.Instance[LocaleKeys.CompilingPPTC]; + IsLoadingIndeterminate = false; + break; + case LoadState.Loaded: + LoadHeading = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.LoadingHeading, _currentApplicationData.Name); + IsLoadingIndeterminate = true; + CacheLoadStatus = string.Empty; + break; + } + break; + case ShaderCacheLoadingState shaderCacheState: + CacheLoadStatus = $"{current} / {total}"; + switch (shaderCacheState) + { + case ShaderCacheLoadingState.Start: + case ShaderCacheLoadingState.Loading: + LoadHeading = LocaleManager.Instance[LocaleKeys.CompilingShaders]; + IsLoadingIndeterminate = false; + break; + case ShaderCacheLoadingState.Packaging: + LoadHeading = LocaleManager.Instance[LocaleKeys.PackagingShaders]; + IsLoadingIndeterminate = false; + break; + case ShaderCacheLoadingState.Loaded: + LoadHeading = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.LoadingHeading, _currentApplicationData.Name); + IsLoadingIndeterminate = true; + CacheLoadStatus = string.Empty; + break; + } + break; + default: + throw new ArgumentException($"Unknown Progress Handler type {typeof(T)}"); + } + }); + } + + private void PrepareLoadScreen() + { + using MemoryStream stream = new(SelectedIcon); + using var gameIconBmp = SKBitmap.Decode(stream); + + var dominantColor = IconColorPicker.GetFilteredColor(gameIconBmp); + + const float ColorMultiple = 0.5f; + + Color progressFgColor = Color.FromRgb(dominantColor.Red, dominantColor.Green, dominantColor.Blue); + Color progressBgColor = Color.FromRgb( + (byte)(dominantColor.Red * ColorMultiple), + (byte)(dominantColor.Green * ColorMultiple), + (byte)(dominantColor.Blue * ColorMultiple)); + + ProgressBarForegroundColor = new SolidColorBrush(progressFgColor); + ProgressBarBackgroundColor = new SolidColorBrush(progressBgColor); + } + + private void InitializeGame() + { + RendererHostControl.WindowCreated += RendererHost_Created; + + AppHost.StatusUpdatedEvent += Update_StatusBar; + AppHost.AppExit += AppHost_AppExit; + + _rendererWaitEvent.WaitOne(); + + AppHost?.Start(); + + AppHost?.DisposeContext(); + } + + private async Task HandleRelaunch() + { + if (UserChannelPersistence.PreviousIndex != -1 && UserChannelPersistence.ShouldRestart) + { + UserChannelPersistence.ShouldRestart = false; + + await LoadApplication(_currentApplicationData); + } + else + { + // Otherwise, clear state. + UserChannelPersistence = new UserChannelPersistence(); + _currentApplicationData = null; + } + } + + private void Update_StatusBar(object sender, StatusUpdatedEventArgs args) + { + if (ShowMenuAndStatusBar && !ShowLoadProgress) + { + Dispatcher.UIThread.InvokeAsync(() => + { + Application.Current!.Styles.TryGetResource(args.VSyncMode, + Application.Current.ActualThemeVariant, + out object color); + + if (color is Color clr) + { + VSyncModeColor = new SolidColorBrush(clr); + } + + VSyncModeText = args.VSyncMode == "Custom" ? "Custom" : "VSync"; + ShowCustomVSyncIntervalPicker = + args.VSyncMode == VSyncMode.Custom.ToString(); + DockedStatusText = args.DockedMode; + AspectRatioStatusText = args.AspectRatio; + GameStatusText = args.GameStatus; + VolumeStatusText = args.VolumeStatus; + FifoStatusText = args.FifoStatus; + + ShaderCountText = (ShowRightmostSeparator = args.ShaderCount > 0) + ? $"{LocaleManager.Instance[LocaleKeys.CompilingShaders]}: {args.ShaderCount}" + : string.Empty; + + ShowStatusSeparator = true; + }); + } + } + + private void RendererHost_Created(object sender, EventArgs e) + { + ShowLoading(false); + + _rendererWaitEvent.Set(); + } + + private async Task LoadContentFromFolder(LocaleKeys localeMessageAddedKey, LocaleKeys localeMessageRemovedKey, LoadContentFromFolderDelegate onDirsSelected) + { + var result = await StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions + { + Title = LocaleManager.Instance[LocaleKeys.OpenFolderDialogTitle], + AllowMultiple = true, + }); + + if (result.Count > 0) + { + var dirs = result.Select(it => it.Path.LocalPath).ToList(); + var numAdded = onDirsSelected(dirs, out int numRemoved); + + var msg = String.Join("\r\n", new string[] { + string.Format(LocaleManager.Instance[localeMessageRemovedKey], numRemoved), + string.Format(LocaleManager.Instance[localeMessageAddedKey], numAdded) + }); + + await Dispatcher.UIThread.InvokeAsync(async () => + { + await ContentDialogHelper.ShowTextDialog( + LocaleManager.Instance[numAdded > 0 || numRemoved > 0 ? LocaleKeys.RyujinxConfirm : LocaleKeys.RyujinxInfo], + msg, + string.Empty, + string.Empty, + string.Empty, + LocaleManager.Instance[LocaleKeys.InputDialogOk], + (int)Symbol.Checkmark); + }); + } + } + + #endregion + + #region PublicMethods + + public void SetUiProgressHandlers(Switch emulationContext) + { + if (emulationContext.Processes.ActiveApplication.DiskCacheLoadState != null) + { + emulationContext.Processes.ActiveApplication.DiskCacheLoadState.StateChanged -= ProgressHandler; + emulationContext.Processes.ActiveApplication.DiskCacheLoadState.StateChanged += ProgressHandler; + } + + emulationContext.Gpu.ShaderCacheStateChanged -= ProgressHandler; + emulationContext.Gpu.ShaderCacheStateChanged += ProgressHandler; + } + + public void LoadConfigurableHotKeys() + { + if (AvaloniaKeyboardMappingHelper.TryGetAvaKey((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.ShowUI, out var showUiKey)) + { + ShowUiKey = new KeyGesture(showUiKey); + } + + if (AvaloniaKeyboardMappingHelper.TryGetAvaKey((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.Screenshot, out var screenshotKey)) + { + ScreenshotKey = new KeyGesture(screenshotKey); + } + + if (AvaloniaKeyboardMappingHelper.TryGetAvaKey((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.Pause, out var pauseKey)) + { + PauseKey = new KeyGesture(pauseKey); + } + } + + public void TakeScreenshot() + { + AppHost.ScreenshotRequested = true; + } + + public void HideUi() + { + ShowMenuAndStatusBar = false; + } + + public void ToggleStartGamesInFullscreen() + { + StartGamesInFullscreen = !StartGamesInFullscreen; + } + + public void ToggleShowConsole() + { + ShowConsole = !ShowConsole; + } + + public void SetListMode() + { + Glyph = Glyph.List; + } + + public void SetGridMode() + { + Glyph = Glyph.Grid; + } + + public void SetAspectRatio(AspectRatio aspectRatio) + { + ConfigurationState.Instance.Graphics.AspectRatio.Value = aspectRatio; + } + + public async Task InstallFirmwareFromFile() + { + var result = await StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions + { + AllowMultiple = false, + FileTypeFilter = new List + { + new(LocaleManager.Instance[LocaleKeys.FileDialogAllTypes]) + { + Patterns = new[] { "*.xci", "*.zip" }, + AppleUniformTypeIdentifiers = new[] { "com.ryujinx.xci", "public.zip-archive" }, + MimeTypes = new[] { "application/x-nx-xci", "application/zip" }, + }, + new("XCI") + { + Patterns = new[] { "*.xci" }, + AppleUniformTypeIdentifiers = new[] { "com.ryujinx.xci" }, + MimeTypes = new[] { "application/x-nx-xci" }, + }, + new("ZIP") + { + Patterns = new[] { "*.zip" }, + AppleUniformTypeIdentifiers = new[] { "public.zip-archive" }, + MimeTypes = new[] { "application/zip" }, + }, + }, + }); + + if (result.Count > 0) + { + await HandleFirmwareInstallation(result[0].Path.LocalPath); + } + } + + public async Task InstallFirmwareFromFolder() + { + var result = await StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions + { + AllowMultiple = false, + }); + + if (result.Count > 0) + { + await HandleFirmwareInstallation(result[0].Path.LocalPath); + } + } + + public void OpenRyujinxFolder() + { + OpenHelper.OpenFolder(AppDataManager.BaseDirPath); + } + + public void OpenLogsFolder() + { + string logPath = AppDataManager.GetOrCreateLogsDir(); + if (!string.IsNullOrEmpty(logPath)) + { + OpenHelper.OpenFolder(logPath); + } + } + + public void ToggleDockMode() + { + if (IsGameRunning) + { + ConfigurationState.Instance.System.EnableDockedMode.Toggle(); + } + } + + public void ToggleVSyncMode() + { + AppHost.VSyncModeToggle(); + OnPropertyChanged(nameof(ShowCustomVSyncIntervalPicker)); + } + + public void VSyncModeSettingChanged() + { + if (_isGameRunning) + { + AppHost.Device.CustomVSyncInterval = ConfigurationState.Instance.Graphics.CustomVSyncInterval.Value; + AppHost.Device.UpdateVSyncInterval(); + } + + CustomVSyncInterval = ConfigurationState.Instance.Graphics.CustomVSyncInterval.Value; + OnPropertyChanged(nameof(ShowCustomVSyncIntervalPicker)); + OnPropertyChanged(nameof(CustomVSyncIntervalPercentageProxy)); + OnPropertyChanged(nameof(CustomVSyncIntervalPercentageText)); + OnPropertyChanged(nameof(CustomVSyncInterval)); + } + + public async Task ExitCurrentState() + { + if (WindowState is WindowState.FullScreen) + { + ToggleFullscreen(); + } + else if (IsGameRunning) + { + await Task.Delay(100); + + AppHost?.ShowExitPrompt(); + } + } + + public static void ChangeLanguage(object languageCode) + { + LocaleManager.Instance.LoadLanguage((string)languageCode); + + if (Program.PreviewerDetached) + { + ConfigurationState.Instance.UI.LanguageCode.Value = (string)languageCode; + ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.ConfigurationPath); + } + } + + public async Task ManageProfiles() + { + await NavigationDialogHost.Show(AccountManager, ContentManager, VirtualFileSystem, LibHacHorizonManager.RyujinxClient); + } + + public void SimulateWakeUpMessage() + { + AppHost.Device.System.SimulateWakeUpMessage(); + } + + public async Task OpenFile() + { + var result = await StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions + { + Title = LocaleManager.Instance[LocaleKeys.OpenFileDialogTitle], + AllowMultiple = false, + FileTypeFilter = new List + { + new(LocaleManager.Instance[LocaleKeys.AllSupportedFormats]) + { + Patterns = new[] { "*.nsp", "*.xci", "*.nca", "*.nro", "*.nso" }, + AppleUniformTypeIdentifiers = new[] + { + "com.ryujinx.nsp", + "com.ryujinx.xci", + "com.ryujinx.nca", + "com.ryujinx.nro", + "com.ryujinx.nso", + }, + MimeTypes = new[] + { + "application/x-nx-nsp", + "application/x-nx-xci", + "application/x-nx-nca", + "application/x-nx-nro", + "application/x-nx-nso", + }, + }, + new("NSP") + { + Patterns = new[] { "*.nsp" }, + AppleUniformTypeIdentifiers = new[] { "com.ryujinx.nsp" }, + MimeTypes = new[] { "application/x-nx-nsp" }, + }, + new("XCI") + { + Patterns = new[] { "*.xci" }, + AppleUniformTypeIdentifiers = new[] { "com.ryujinx.xci" }, + MimeTypes = new[] { "application/x-nx-xci" }, + }, + new("NCA") + { + Patterns = new[] { "*.nca" }, + AppleUniformTypeIdentifiers = new[] { "com.ryujinx.nca" }, + MimeTypes = new[] { "application/x-nx-nca" }, + }, + new("NRO") + { + Patterns = new[] { "*.nro" }, + AppleUniformTypeIdentifiers = new[] { "com.ryujinx.nro" }, + MimeTypes = new[] { "application/x-nx-nro" }, + }, + new("NSO") + { + Patterns = new[] { "*.nso" }, + AppleUniformTypeIdentifiers = new[] { "com.ryujinx.nso" }, + MimeTypes = new[] { "application/x-nx-nso" }, + }, + }, + }); + + if (result.Count > 0) + { + if (ApplicationLibrary.TryGetApplicationsFromFile(result[0].Path.LocalPath, + out List applications)) + { + await LoadApplication(applications[0]); + } + else + { + await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.MenuBarFileOpenFromFileError]); + } + } + } + + public async Task LoadDlcFromFolder() + { + await LoadContentFromFolder( + LocaleKeys.AutoloadDlcAddedMessage, + LocaleKeys.AutoloadDlcRemovedMessage, + ApplicationLibrary.AutoLoadDownloadableContents); + } + + public async Task LoadTitleUpdatesFromFolder() + { + await LoadContentFromFolder( + LocaleKeys.AutoloadUpdateAddedMessage, + LocaleKeys.AutoloadUpdateRemovedMessage, + ApplicationLibrary.AutoLoadTitleUpdates); + } + + public async Task OpenFolder() + { + var result = await StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions + { + Title = LocaleManager.Instance[LocaleKeys.OpenFolderDialogTitle], + AllowMultiple = false, + }); + + if (result.Count > 0) + { + ApplicationData applicationData = new() + { + Name = Path.GetFileNameWithoutExtension(result[0].Path.LocalPath), + Path = result[0].Path.LocalPath, + }; + + await LoadApplication(applicationData); + } + } + + public async Task LoadApplication(ApplicationData application, bool startFullscreen = false) + { + if (AppHost != null) + { + await ContentDialogHelper.CreateInfoDialog( + LocaleManager.Instance[LocaleKeys.DialogLoadAppGameAlreadyLoadedMessage], + LocaleManager.Instance[LocaleKeys.DialogLoadAppGameAlreadyLoadedSubMessage], + LocaleManager.Instance[LocaleKeys.InputDialogOk], + string.Empty, + LocaleManager.Instance[LocaleKeys.RyujinxInfo]); + + return; + } + +#if RELEASE + await PerformanceCheck(); +#endif + + Logger.RestartTime(); + + SelectedIcon ??= ApplicationLibrary.GetApplicationIcon(application.Path, ConfigurationState.Instance.System.Language, application.Id); + + PrepareLoadScreen(); + + RendererHostControl = new RendererHost(); + + AppHost = new AppHost( + RendererHostControl, + InputManager, + application.Path, + application.Id, + VirtualFileSystem, + ContentManager, + AccountManager, + UserChannelPersistence, + this, + TopLevel); + + if (!await AppHost.LoadGuestApplication()) + { + AppHost.DisposeContext(); + AppHost = null; + + return; + } + + CanUpdate = false; + + LoadHeading = application.Name; + + if (string.IsNullOrWhiteSpace(application.Name)) + { + LoadHeading = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.LoadingHeading, AppHost.Device.Processes.ActiveApplication.Name); + application.Name = AppHost.Device.Processes.ActiveApplication.Name; + } + + SwitchToRenderer(startFullscreen); + + _currentApplicationData = application; + + Thread gameThread = new(InitializeGame) { Name = "GUI.WindowThread" }; + gameThread.Start(); + } + + public void SwitchToRenderer(bool startFullscreen) => + Dispatcher.UIThread.Post(() => + { + SwitchToGameControl(startFullscreen); + + SetMainContent(RendererHostControl); + + RendererHostControl.Focus(); + }); + + public static void UpdateGameMetadata(string titleId) + => ApplicationLibrary.LoadAndSaveMetaData(titleId, appMetadata => appMetadata.UpdatePostGame()); + + public void RefreshFirmwareStatus() + { + SystemVersion version = null; + try + { + version = ContentManager.GetCurrentFirmwareVersion(); + } + catch (Exception) + { + // ignored + } + + bool hasApplet = false; + + if (version != null) + { + LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.StatusBarSystemVersion, version.VersionString); + + hasApplet = version.Major > 3; + } + else + { + LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.StatusBarSystemVersion, "0.0"); + } + + IsAppletMenuActive = hasApplet; + } + + public void AppHost_AppExit(object sender, EventArgs e) + { + if (IsClosing) + { + return; + } + + IsGameRunning = false; + + Dispatcher.UIThread.InvokeAsync(async () => + { + ShowMenuAndStatusBar = true; + ShowContent = true; + ShowLoadProgress = false; + IsLoadingIndeterminate = false; + CanUpdate = true; + Cursor = Cursor.Default; + + SetMainContent(null); + + AppHost = null; + + await HandleRelaunch(); + }); + + RendererHostControl.WindowCreated -= RendererHost_Created; + RendererHostControl = null; + + SelectedIcon = null; + + Dispatcher.UIThread.InvokeAsync(() => + { + Title = App.FormatTitle(); + }); + } + + public async Task OpenAmiiboWindow() + { + if (!IsAmiiboRequested) + return; + + if (AppHost.Device.System.SearchingForAmiibo(out int deviceId)) + { + string titleId = AppHost.Device.Processes.ActiveApplication.ProgramIdText.ToUpper(); + AmiiboWindow window = new(ShowAll, LastScannedAmiiboId, titleId); + + await window.ShowDialog(Window); + + if (window.IsScanned) + { + ShowAll = window.ViewModel.ShowAllAmiibo; + LastScannedAmiiboId = window.ScannedAmiibo.GetId(); + + AppHost.Device.System.ScanAmiibo(deviceId, LastScannedAmiiboId, window.ViewModel.UseRandomUuid); + } + } + } + + public void ToggleFullscreen() + { + if (Environment.TickCount64 - LastFullscreenToggle < HotKeyPressDelayMs) + { + return; + } + + LastFullscreenToggle = Environment.TickCount64; + + if (WindowState is not WindowState.Normal) + { + WindowState = WindowState.Normal; + Window.TitleBar.ExtendsContentIntoTitleBar = !ConfigurationState.Instance.ShowTitleBar; + + if (IsGameRunning) + { + ShowMenuAndStatusBar = true; + } + } + else + { + WindowState = WindowState.FullScreen; + Window.TitleBar.ExtendsContentIntoTitleBar = true; + + if (IsGameRunning) + { + ShowMenuAndStatusBar = false; + } + } + + IsFullScreen = WindowState is WindowState.FullScreen; + } + + public static void SaveConfig() + { + ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.ConfigurationPath); + } + + public static async Task PerformanceCheck() + { + if (ConfigurationState.Instance.Logger.EnableTrace.Value) + { + string mainMessage = LocaleManager.Instance[LocaleKeys.DialogPerformanceCheckLoggingEnabledMessage]; + string secondaryMessage = LocaleManager.Instance[LocaleKeys.DialogPerformanceCheckLoggingEnabledConfirmMessage]; + + UserResult result = await ContentDialogHelper.CreateLocalizedConfirmationDialog(mainMessage, secondaryMessage); + + if (result == UserResult.Yes) + { + ConfigurationState.Instance.Logger.EnableTrace.Value = false; + + SaveConfig(); + } + } + + if (!string.IsNullOrWhiteSpace(ConfigurationState.Instance.Graphics.ShadersDumpPath.Value)) + { + string mainMessage = LocaleManager.Instance[LocaleKeys.DialogPerformanceCheckShaderDumpEnabledMessage]; + string secondaryMessage = LocaleManager.Instance[LocaleKeys.DialogPerformanceCheckShaderDumpEnabledConfirmMessage]; + + UserResult result = await ContentDialogHelper.CreateLocalizedConfirmationDialog(mainMessage, secondaryMessage); + + if (result == UserResult.Yes) + { + ConfigurationState.Instance.Graphics.ShadersDumpPath.Value = string.Empty; + + SaveConfig(); + } + } + } + + public async void ProcessTrimResult(String filename, Ryujinx.Common.Utilities.XCIFileTrimmer.OperationOutcome operationOutcome) + { + string notifyUser = operationOutcome.ToLocalisedText(); + + if (notifyUser != null) + { + await ContentDialogHelper.CreateWarningDialog( + LocaleManager.Instance[LocaleKeys.TrimXCIFileFailedPrimaryText], + notifyUser + ); + } + else + { + switch (operationOutcome) + { + case Ryujinx.Common.Utilities.XCIFileTrimmer.OperationOutcome.Successful: + if (Avalonia.Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + if (desktop.MainWindow is MainWindow mainWindow) + mainWindow.LoadApplications(); + } + break; + } + } + } + + public async Task TrimXCIFile(string filename) + { + if (filename == null) + { + return; + } + + var trimmer = new XCIFileTrimmer(filename, new Common.XCIFileTrimmerMainWindowLog(this)); + + if (trimmer.CanBeTrimmed) + { + var savings = (double)trimmer.DiskSpaceSavingsB / 1024.0 / 1024.0; + var currentFileSize = (double)trimmer.FileSizeB / 1024.0 / 1024.0; + var cartDataSize = (double)trimmer.DataSizeB / 1024.0 / 1024.0; + string secondaryText = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.TrimXCIFileDialogSecondaryText, currentFileSize, cartDataSize, savings); + + var result = await ContentDialogHelper.CreateConfirmationDialog( + LocaleManager.Instance[LocaleKeys.TrimXCIFileDialogPrimaryText], + secondaryText, + LocaleManager.Instance[LocaleKeys.Continue], + LocaleManager.Instance[LocaleKeys.Cancel], + LocaleManager.Instance[LocaleKeys.TrimXCIFileDialogTitle] + ); + + if (result == UserResult.Yes) + { + Thread XCIFileTrimThread = new(() => + { + Dispatcher.UIThread.Post(() => + { + StatusBarProgressStatusText = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.StatusBarXCIFileTrimming, Path.GetFileName(filename)); + StatusBarProgressStatusVisible = true; + StatusBarProgressMaximum = 1; + StatusBarProgressValue = 0; + StatusBarVisible = true; + }); + + try + { + XCIFileTrimmer.OperationOutcome operationOutcome = trimmer.Trim(); + + Dispatcher.UIThread.Post(() => + { + ProcessTrimResult(filename, operationOutcome); + }); + } + finally + { + Dispatcher.UIThread.Post(() => + { + StatusBarProgressStatusVisible = false; + StatusBarProgressStatusText = string.Empty; + StatusBarVisible = false; + }); + } + }) + { + Name = "GUI.XCIFileTrimmerThread", + IsBackground = true, + }; + XCIFileTrimThread.Start(); + } + } + } + + #endregion + } +} diff --git a/src/Ryujinx/UI/ViewModels/ModManagerViewModel.cs b/src/Ryujinx/UI/ViewModels/ModManagerViewModel.cs new file mode 100644 index 000000000..df2ef266e --- /dev/null +++ b/src/Ryujinx/UI/ViewModels/ModManagerViewModel.cs @@ -0,0 +1,336 @@ +using Avalonia; +using Avalonia.Collections; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Platform.Storage; +using Avalonia.Threading; +using DynamicData; +using Gommon; +using Ryujinx.Ava.Common.Locale; +using Ryujinx.Ava.UI.Helpers; +using Ryujinx.Ava.UI.Models; +using Ryujinx.Common.Configuration; +using Ryujinx.Common.Logging; +using Ryujinx.Common.Utilities; +using Ryujinx.HLE.HOS; +using System; +using System.IO; +using System.Linq; + +namespace Ryujinx.Ava.UI.ViewModels +{ + public class ModManagerViewModel : BaseModel + { + private readonly string _modJsonPath; + + private AvaloniaList _mods = new(); + private AvaloniaList _views = new(); + private AvaloniaList _selectedMods = new(); + + private string _search; + private readonly ulong _applicationId; + private readonly IStorageProvider _storageProvider; + + private static readonly ModMetadataJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions()); + + public AvaloniaList Mods + { + get => _mods; + set + { + _mods = value; + OnPropertyChanged(); + OnPropertyChanged(nameof(ModCount)); + Sort(); + } + } + + public AvaloniaList Views + { + get => _views; + set + { + _views = value; + OnPropertyChanged(); + } + } + + public AvaloniaList SelectedMods + { + get => _selectedMods; + set + { + _selectedMods = value; + OnPropertyChanged(); + } + } + + public string Search + { + get => _search; + set + { + _search = value; + OnPropertyChanged(); + Sort(); + } + } + + public string ModCount + { + get => string.Format(LocaleManager.Instance[LocaleKeys.ModWindowHeading], Mods.Count); + } + + public ModManagerViewModel(ulong applicationId) + { + _applicationId = applicationId; + + _modJsonPath = Path.Combine(AppDataManager.GamesDirPath, applicationId.ToString("x16"), "mods.json"); + + if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + _storageProvider = desktop.MainWindow.StorageProvider; + } + + LoadMods(applicationId); + } + + private void LoadMods(ulong applicationId) + { + Mods.Clear(); + SelectedMods.Clear(); + + string[] modsBasePaths = [ModLoader.GetSdModsBasePath(), ModLoader.GetModsBasePath()]; + + foreach (var path in modsBasePaths) + { + var inSd = path == ModLoader.GetSdModsBasePath(); + var modCache = new ModLoader.ModCache(); + + ModLoader.QueryContentsDir(modCache, new DirectoryInfo(Path.Combine(path, "contents")), applicationId); + + foreach (var mod in modCache.RomfsDirs) + { + var modModel = new ModModel(mod.Path.Parent.FullName, mod.Name, mod.Enabled, inSd); + if (Mods.All(x => x.Path != mod.Path.Parent.FullName)) + { + Mods.Add(modModel); + } + } + + foreach (var mod in modCache.RomfsContainers) + { + Mods.Add(new ModModel(mod.Path.FullName, mod.Name, mod.Enabled, inSd)); + } + + foreach (var mod in modCache.ExefsDirs) + { + var modModel = new ModModel(mod.Path.Parent.FullName, mod.Name, mod.Enabled, inSd); + if (Mods.All(x => x.Path != mod.Path.Parent.FullName)) + { + Mods.Add(modModel); + } + } + + foreach (var mod in modCache.ExefsContainers) + { + Mods.Add(new ModModel(mod.Path.FullName, mod.Name, mod.Enabled, inSd)); + } + } + + Sort(); + } + + public void Sort() + { + Mods.AsObservableChangeSet() + .Filter(Filter) + .Bind(out var view).AsObservableList(); + + _views.Clear(); + _views.AddRange(view); + + SelectedMods = new(Views.Where(x => x.Enabled)); + + OnPropertyChanged(nameof(ModCount)); + OnPropertyChanged(nameof(Views)); + OnPropertyChanged(nameof(SelectedMods)); + } + + private bool Filter(object arg) + { + if (arg is ModModel content) + { + return string.IsNullOrWhiteSpace(_search) || content.Name.ToLower().Contains(_search.ToLower()); + } + + return false; + } + + public void Save() + { + ModMetadata modData = new(); + + foreach (ModModel mod in Mods) + { + modData.Mods.Add(new Mod + { + Name = mod.Name, + Path = mod.Path, + Enabled = SelectedMods.Contains(mod), + }); + } + + JsonHelper.SerializeToFile(_modJsonPath, modData, _serializerContext.ModMetadata); + } + + public void Delete(ModModel model, bool removeFromList = true) + { + var isSubdir = true; + var pathToDelete = model.Path; + var basePath = model.InSd ? ModLoader.GetSdModsBasePath() : ModLoader.GetModsBasePath(); + var modsDir = ModLoader.GetApplicationDir(basePath, _applicationId.ToString("x16")); + + if (new DirectoryInfo(model.Path).Parent?.FullName == modsDir) + { + isSubdir = false; + } + + if (isSubdir) + { + var parentDir = String.Empty; + + foreach (var dir in Directory.GetDirectories(modsDir, "*", SearchOption.TopDirectoryOnly)) + { + if (Directory.GetDirectories(dir, "*", SearchOption.AllDirectories).Contains(model.Path)) + { + parentDir = dir; + break; + } + } + + if (parentDir == String.Empty) + { + Dispatcher.UIThread.Post(async () => + { + await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance.UpdateAndGetDynamicValue( + LocaleKeys.DialogModDeleteNoParentMessage, + model.Path)); + }); + return; + } + } + + Logger.Info?.Print(LogClass.Application, $"Deleting mod at \"{pathToDelete}\""); + Directory.Delete(pathToDelete, true); + + if (removeFromList) + { + Mods.Remove(model); + OnPropertyChanged(nameof(ModCount)); + } + Sort(); + } + + private void AddMod(DirectoryInfo directory) + { + string[] directories; + + try + { + directories = Directory.GetDirectories(directory.ToString(), "*", SearchOption.AllDirectories); + } + catch (Exception exception) + { + Dispatcher.UIThread.Post(async () => + { + await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance.UpdateAndGetDynamicValue( + LocaleKeys.DialogLoadFileErrorMessage, + exception.ToString(), + directory)); + }); + return; + } + + var destinationDir = ModLoader.GetApplicationDir(ModLoader.GetSdModsBasePath(), _applicationId.ToString("x16")); + + // TODO: More robust checking for valid mod folders + var isDirectoryValid = true; + + if (directories.Length == 0) + { + isDirectoryValid = false; + } + + if (!isDirectoryValid) + { + Dispatcher.UIThread.Post(async () => + { + await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogModInvalidMessage]); + }); + return; + } + + foreach (var dir in directories) + { + string dirToCreate = dir.Replace(directory.Parent.ToString(), destinationDir); + + // Mod already exists + if (Directory.Exists(dirToCreate)) + { + Dispatcher.UIThread.Post(async () => + { + await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance.UpdateAndGetDynamicValue( + LocaleKeys.DialogLoadFileErrorMessage, + LocaleManager.Instance[LocaleKeys.DialogModAlreadyExistsMessage], + dirToCreate)); + }); + + return; + } + + Directory.CreateDirectory(dirToCreate); + } + + var files = Directory.GetFiles(directory.ToString(), "*", SearchOption.AllDirectories); + + foreach (var file in files) + { + File.Copy(file, file.Replace(directory.Parent.ToString(), destinationDir), true); + } + + LoadMods(_applicationId); + } + + public async void Add() + { + var result = await _storageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions + { + Title = LocaleManager.Instance[LocaleKeys.SelectModDialogTitle], + AllowMultiple = true, + }); + + foreach (var folder in result) + { + AddMod(new DirectoryInfo(folder.Path.LocalPath)); + } + } + + public void DeleteAll() + { + Mods.ForEach(it => Delete(it, false)); + Mods.Clear(); + OnPropertyChanged(nameof(ModCount)); + Sort(); + } + + public void EnableAll() + { + SelectedMods = new(Mods); + } + + public void DisableAll() + { + SelectedMods.Clear(); + } + } +} diff --git a/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs b/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs new file mode 100644 index 000000000..a5abeb36b --- /dev/null +++ b/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs @@ -0,0 +1,768 @@ +using Avalonia.Collections; +using Avalonia.Controls; +using Avalonia.Threading; +using LibHac.Tools.FsSystem; +using Ryujinx.Audio.Backends.OpenAL; +using Ryujinx.Audio.Backends.SDL2; +using Ryujinx.Audio.Backends.SoundIo; +using Ryujinx.Ava.Common.Locale; +using Ryujinx.Ava.UI.Helpers; +using Ryujinx.Ava.UI.Models.Input; +using Ryujinx.Ava.UI.Windows; +using Ryujinx.Common.Configuration; +using Ryujinx.Common.Configuration.Multiplayer; +using Ryujinx.Common.GraphicsDriver; +using Ryujinx.Common.Logging; +using Ryujinx.Graphics.Vulkan; +using Ryujinx.HLE; +using Ryujinx.HLE.FileSystem; +using Ryujinx.HLE.HOS.Services.Time.TimeZone; +using Ryujinx.UI.Common.Configuration; +using Ryujinx.UI.Common.Configuration.System; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Net.NetworkInformation; +using System.Runtime.InteropServices; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using TimeZone = Ryujinx.Ava.UI.Models.TimeZone; + +namespace Ryujinx.Ava.UI.ViewModels +{ + public partial class SettingsViewModel : BaseModel + { + private readonly VirtualFileSystem _virtualFileSystem; + private readonly ContentManager _contentManager; + private TimeZoneContentManager _timeZoneContentManager; + + private readonly List _validTzRegions; + + private readonly Dictionary _networkInterfaces; + + private float _customResolutionScale; + private int _resolutionScale; + private int _graphicsBackendMultithreadingIndex; + private float _volume; + private bool _isVulkanAvailable = true; + private bool _gameDirectoryChanged; + private bool _autoloadDirectoryChanged; + private readonly List _gpuIds = new(); + private int _graphicsBackendIndex; + private int _scalingFilter; + private int _scalingFilterLevel; + private int _customVSyncInterval; + private bool _enableCustomVSyncInterval; + private int _customVSyncIntervalPercentageProxy; + private VSyncMode _vSyncMode; + + public event Action CloseWindow; + public event Action SaveSettingsEvent; + private int _networkInterfaceIndex; + private int _multiplayerModeIndex; + private string _ldnPassphrase; + private string _LdnServer; + + public int ResolutionScale + { + get => _resolutionScale; + set + { + _resolutionScale = value; + + OnPropertyChanged(nameof(CustomResolutionScale)); + OnPropertyChanged(nameof(IsCustomResolutionScaleActive)); + } + } + + public int GraphicsBackendMultithreadingIndex + { + get => _graphicsBackendMultithreadingIndex; + set + { + _graphicsBackendMultithreadingIndex = value; + + if (_graphicsBackendMultithreadingIndex != (int)ConfigurationState.Instance.Graphics.BackendThreading.Value) + { + Dispatcher.UIThread.InvokeAsync(() => + ContentDialogHelper.CreateInfoDialog(LocaleManager.Instance[LocaleKeys.DialogSettingsBackendThreadingWarningMessage], + string.Empty, + string.Empty, + LocaleManager.Instance[LocaleKeys.InputDialogOk], + LocaleManager.Instance[LocaleKeys.DialogSettingsBackendThreadingWarningTitle]) + ); + } + + OnPropertyChanged(); + } + } + + public float CustomResolutionScale + { + get => _customResolutionScale; + set + { + _customResolutionScale = MathF.Round(value, 1); + + OnPropertyChanged(); + } + } + + public bool IsVulkanAvailable + { + get => _isVulkanAvailable; + set + { + _isVulkanAvailable = value; + + OnPropertyChanged(); + } + } + + public bool IsOpenGLAvailable => !OperatingSystem.IsMacOS(); + + public bool IsHypervisorAvailable => OperatingSystem.IsMacOS() && RuntimeInformation.ProcessArchitecture == Architecture.Arm64; + + public bool GameDirectoryChanged + { + get => _gameDirectoryChanged; + set + { + _gameDirectoryChanged = value; + + OnPropertyChanged(); + } + } + + public bool AutoloadDirectoryChanged + { + get => _autoloadDirectoryChanged; + set + { + _autoloadDirectoryChanged = value; + + OnPropertyChanged(); + } + } + + public bool IsMacOS => OperatingSystem.IsMacOS(); + + public bool EnableDiscordIntegration { get; set; } + public bool CheckUpdatesOnStart { get; set; } + public bool ShowConfirmExit { get; set; } + public bool IgnoreApplet { get; set; } + public bool RememberWindowState { get; set; } + public bool ShowTitleBar { get; set; } + public int HideCursor { get; set; } + public bool EnableDockedMode { get; set; } + public bool EnableKeyboard { get; set; } + public bool EnableMouse { get; set; } + public VSyncMode VSyncMode + { + get => _vSyncMode; + set + { + if (value == VSyncMode.Custom || + value == VSyncMode.Switch || + value == VSyncMode.Unbounded) + { + _vSyncMode = value; + OnPropertyChanged(); + } + } + } + + public int CustomVSyncIntervalPercentageProxy + { + get => _customVSyncIntervalPercentageProxy; + set + { + int newInterval = (int)((value / 100f) * 60); + _customVSyncInterval = newInterval; + _customVSyncIntervalPercentageProxy = value; + OnPropertyChanged((nameof(CustomVSyncInterval))); + OnPropertyChanged((nameof(CustomVSyncIntervalPercentageText))); + } + } + + public string CustomVSyncIntervalPercentageText + { + get + { + string text = CustomVSyncIntervalPercentageProxy.ToString() + "%"; + return text; + } + } + + public bool EnableCustomVSyncInterval + { + get => _enableCustomVSyncInterval; + set + { + _enableCustomVSyncInterval = value; + if (_vSyncMode == VSyncMode.Custom && !value) + { + VSyncMode = VSyncMode.Switch; + } + else if (value) + { + VSyncMode = VSyncMode.Custom; + } + OnPropertyChanged(); + } + } + + public int CustomVSyncInterval + { + get => _customVSyncInterval; + set + { + _customVSyncInterval = value; + int newPercent = (int)((value / 60f) * 100); + _customVSyncIntervalPercentageProxy = newPercent; + OnPropertyChanged(nameof(CustomVSyncIntervalPercentageProxy)); + OnPropertyChanged(nameof(CustomVSyncIntervalPercentageText)); + OnPropertyChanged(); + } + } + public bool EnablePptc { get; set; } + public bool EnableLowPowerPptc { get; set; } + public bool EnableInternetAccess { get; set; } + public bool EnableFsIntegrityChecks { get; set; } + public bool IgnoreMissingServices { get; set; } + public MemoryConfiguration DramSize { get; set; } + public bool EnableShaderCache { get; set; } + public bool EnableTextureRecompression { get; set; } + public bool EnableMacroHLE { get; set; } + public bool EnableColorSpacePassthrough { get; set; } + public bool ColorSpacePassthroughAvailable => IsMacOS; + public bool EnableFileLog { get; set; } + public bool EnableStub { get; set; } + public bool EnableInfo { get; set; } + public bool EnableWarn { get; set; } + public bool EnableError { get; set; } + public bool EnableTrace { get; set; } + public bool EnableGuest { get; set; } + public bool EnableFsAccessLog { get; set; } + public bool EnableDebug { get; set; } + public bool IsOpenAlEnabled { get; set; } + public bool IsSoundIoEnabled { get; set; } + public bool IsSDL2Enabled { get; set; } + public bool IsCustomResolutionScaleActive => _resolutionScale == 4; + public bool IsScalingFilterActive => _scalingFilter == (int)Ryujinx.Common.Configuration.ScalingFilter.Fsr; + + public bool IsVulkanSelected => GraphicsBackendIndex == 0; + public bool UseHypervisor { get; set; } + public bool DisableP2P { get; set; } + + public string TimeZone { get; set; } + public string ShaderDumpPath { get; set; } + + public string LdnPassphrase + { + get => _ldnPassphrase; + set + { + _ldnPassphrase = value; + IsInvalidLdnPassphraseVisible = !ValidateLdnPassphrase(value); + + OnPropertyChanged(); + OnPropertyChanged(nameof(IsInvalidLdnPassphraseVisible)); + } + } + + public int Language { get; set; } + public int Region { get; set; } + public int FsGlobalAccessLogMode { get; set; } + public int AudioBackend { get; set; } + public int MaxAnisotropy { get; set; } + public int AspectRatio { get; set; } + public int AntiAliasingEffect { get; set; } + public string ScalingFilterLevelText => ScalingFilterLevel.ToString("0"); + public int ScalingFilterLevel + { + get => _scalingFilterLevel; + set + { + _scalingFilterLevel = value; + OnPropertyChanged(); + OnPropertyChanged(nameof(ScalingFilterLevelText)); + } + } + public int OpenglDebugLevel { get; set; } + public int MemoryMode { get; set; } + public int BaseStyleIndex { get; set; } + public int GraphicsBackendIndex + { + get => _graphicsBackendIndex; + set + { + _graphicsBackendIndex = value; + OnPropertyChanged(); + OnPropertyChanged(nameof(IsVulkanSelected)); + } + } + public int ScalingFilter + { + get => _scalingFilter; + set + { + _scalingFilter = value; + OnPropertyChanged(); + OnPropertyChanged(nameof(IsScalingFilterActive)); + } + } + + public int PreferredGpuIndex { get; set; } + + public float Volume + { + get => _volume; + set + { + _volume = value; + + ConfigurationState.Instance.System.AudioVolume.Value = _volume / 100; + + OnPropertyChanged(); + } + } + + public DateTimeOffset CurrentDate { get; set; } + public TimeSpan CurrentTime { get; set; } + + internal AvaloniaList TimeZones { get; set; } + public AvaloniaList GameDirectories { get; set; } + public AvaloniaList AutoloadDirectories { get; set; } + public ObservableCollection AvailableGpus { get; set; } + + public AvaloniaList NetworkInterfaceList + { + get => new(_networkInterfaces.Keys); + } + + public HotkeyConfig KeyboardHotkey { get; set; } + + public int NetworkInterfaceIndex + { + get => _networkInterfaceIndex; + set + { + _networkInterfaceIndex = value != -1 ? value : 0; + ConfigurationState.Instance.Multiplayer.LanInterfaceId.Value = _networkInterfaces[NetworkInterfaceList[_networkInterfaceIndex]]; + } + } + + public int MultiplayerModeIndex + { + get => _multiplayerModeIndex; + set + { + _multiplayerModeIndex = value; + ConfigurationState.Instance.Multiplayer.Mode.Value = (MultiplayerMode)_multiplayerModeIndex; + } + } + + [GeneratedRegex("Ryujinx-[0-9a-f]{8}")] + private static partial Regex LdnPassphraseRegex(); + + public bool IsInvalidLdnPassphraseVisible { get; set; } + + public string LdnServer + { + get => _LdnServer; + set + { + _LdnServer = value; + OnPropertyChanged(); + } + } + + public SettingsViewModel(VirtualFileSystem virtualFileSystem, ContentManager contentManager) : this() + { + _virtualFileSystem = virtualFileSystem; + _contentManager = contentManager; + if (Program.PreviewerDetached) + { + Task.Run(LoadTimeZones); + } + } + + public SettingsViewModel() + { + GameDirectories = []; + AutoloadDirectories = []; + TimeZones = []; + AvailableGpus = []; + _validTzRegions = []; + _networkInterfaces = new Dictionary(); + + Task.Run(CheckSoundBackends); + Task.Run(PopulateNetworkInterfaces); + + if (Program.PreviewerDetached) + { + Task.Run(LoadAvailableGpus); + LoadCurrentConfiguration(); + } + } + + public async Task CheckSoundBackends() + { + IsOpenAlEnabled = OpenALHardwareDeviceDriver.IsSupported; + IsSoundIoEnabled = SoundIoHardwareDeviceDriver.IsSupported; + IsSDL2Enabled = SDL2HardwareDeviceDriver.IsSupported; + + await Dispatcher.UIThread.InvokeAsync(() => + { + OnPropertyChanged(nameof(IsOpenAlEnabled)); + OnPropertyChanged(nameof(IsSoundIoEnabled)); + OnPropertyChanged(nameof(IsSDL2Enabled)); + }); + } + + private async Task LoadAvailableGpus() + { + AvailableGpus.Clear(); + + var devices = VulkanRenderer.GetPhysicalDevices(); + + if (devices.Length == 0) + { + IsVulkanAvailable = false; + GraphicsBackendIndex = 1; + } + else + { + foreach (var device in devices) + { + await Dispatcher.UIThread.InvokeAsync(() => + { + _gpuIds.Add(device.Id); + + AvailableGpus.Add(new ComboBoxItem { Content = $"{device.Name} {(device.IsDiscrete ? "(dGPU)" : string.Empty)}" }); + }); + } + } + + // GPU configuration needs to be loaded during the async method or it will always return 0. + PreferredGpuIndex = _gpuIds.Contains(ConfigurationState.Instance.Graphics.PreferredGpu) ? + _gpuIds.IndexOf(ConfigurationState.Instance.Graphics.PreferredGpu) : 0; + + Dispatcher.UIThread.Post(() => OnPropertyChanged(nameof(PreferredGpuIndex))); + } + + public async Task LoadTimeZones() + { + _timeZoneContentManager = new TimeZoneContentManager(); + + _timeZoneContentManager.InitializeInstance(_virtualFileSystem, _contentManager, IntegrityCheckLevel.None); + + foreach ((int offset, string location, string abbr) in _timeZoneContentManager.ParseTzOffsets()) + { + int hours = Math.DivRem(offset, 3600, out int seconds); + int minutes = Math.Abs(seconds) / 60; + + string abbr2 = abbr.StartsWith('+') || abbr.StartsWith('-') ? string.Empty : abbr; + + await Dispatcher.UIThread.InvokeAsync(() => + { + TimeZones.Add(new TimeZone($"UTC{hours:+0#;-0#;+00}:{minutes:D2}", location, abbr2)); + + _validTzRegions.Add(location); + }); + } + + Dispatcher.UIThread.Post(() => OnPropertyChanged(nameof(TimeZone))); + } + + private async Task PopulateNetworkInterfaces() + { + _networkInterfaces.Clear(); + _networkInterfaces.Add(LocaleManager.Instance[LocaleKeys.NetworkInterfaceDefault], "0"); + + foreach (NetworkInterface networkInterface in NetworkInterface.GetAllNetworkInterfaces()) + { + await Dispatcher.UIThread.InvokeAsync(() => + { + _networkInterfaces.Add(networkInterface.Name, networkInterface.Id); + }); + } + + // Network interface index needs to be loaded during the async method or it will always return 0. + NetworkInterfaceIndex = _networkInterfaces.Values.ToList().IndexOf(ConfigurationState.Instance.Multiplayer.LanInterfaceId.Value); + + Dispatcher.UIThread.Post(() => OnPropertyChanged(nameof(NetworkInterfaceIndex))); + } + + private bool ValidateLdnPassphrase(string passphrase) + { + return string.IsNullOrEmpty(passphrase) || (passphrase.Length == 16 && LdnPassphraseRegex().IsMatch(passphrase)); + } + + public void ValidateAndSetTimeZone(string location) + { + if (_validTzRegions.Contains(location)) + { + TimeZone = location; + } + } + + public void LoadCurrentConfiguration() + { + ConfigurationState config = ConfigurationState.Instance; + + // User Interface + EnableDiscordIntegration = config.EnableDiscordIntegration; + CheckUpdatesOnStart = config.CheckUpdatesOnStart; + ShowConfirmExit = config.ShowConfirmExit; + IgnoreApplet = config.IgnoreApplet; + RememberWindowState = config.RememberWindowState; + ShowTitleBar = config.ShowTitleBar; + HideCursor = (int)config.HideCursor.Value; + + GameDirectories.Clear(); + GameDirectories.AddRange(config.UI.GameDirs.Value); + + AutoloadDirectories.Clear(); + AutoloadDirectories.AddRange(config.UI.AutoloadDirs.Value); + + BaseStyleIndex = config.UI.BaseStyle.Value switch + { + "Auto" => 0, + "Light" => 1, + "Dark" => 2, + _ => 0 + }; + + // Input + EnableDockedMode = config.System.EnableDockedMode; + EnableKeyboard = config.Hid.EnableKeyboard; + EnableMouse = config.Hid.EnableMouse; + + // Keyboard Hotkeys + KeyboardHotkey = new HotkeyConfig(config.Hid.Hotkeys.Value); + + // System + Region = (int)config.System.Region.Value; + Language = (int)config.System.Language.Value; + TimeZone = config.System.TimeZone; + + DateTime currentHostDateTime = DateTime.Now; + TimeSpan systemDateTimeOffset = TimeSpan.FromSeconds(config.System.SystemTimeOffset); + DateTime currentDateTime = currentHostDateTime.Add(systemDateTimeOffset); + CurrentDate = currentDateTime.Date; + CurrentTime = currentDateTime.TimeOfDay; + + EnableCustomVSyncInterval = config.Graphics.EnableCustomVSyncInterval.Value; + CustomVSyncInterval = config.Graphics.CustomVSyncInterval; + VSyncMode = config.Graphics.VSyncMode; + EnableFsIntegrityChecks = config.System.EnableFsIntegrityChecks; + DramSize = config.System.DramSize; + IgnoreMissingServices = config.System.IgnoreMissingServices; + + // CPU + EnablePptc = config.System.EnablePtc; + EnableLowPowerPptc = config.System.EnableLowPowerPtc; + MemoryMode = (int)config.System.MemoryManagerMode.Value; + UseHypervisor = config.System.UseHypervisor; + + // Graphics + GraphicsBackendIndex = (int)config.Graphics.GraphicsBackend.Value; + // Physical devices are queried asynchronously hence the preferred index config value is loaded in LoadAvailableGpus(). + EnableShaderCache = config.Graphics.EnableShaderCache; + EnableTextureRecompression = config.Graphics.EnableTextureRecompression; + EnableMacroHLE = config.Graphics.EnableMacroHLE; + EnableColorSpacePassthrough = config.Graphics.EnableColorSpacePassthrough; + ResolutionScale = config.Graphics.ResScale == -1 ? 4 : config.Graphics.ResScale - 1; + CustomResolutionScale = config.Graphics.ResScaleCustom; + MaxAnisotropy = config.Graphics.MaxAnisotropy == -1 ? 0 : (int)MathF.Log2(config.Graphics.MaxAnisotropy); + AspectRatio = (int)config.Graphics.AspectRatio.Value; + GraphicsBackendMultithreadingIndex = (int)config.Graphics.BackendThreading.Value; + ShaderDumpPath = config.Graphics.ShadersDumpPath; + AntiAliasingEffect = (int)config.Graphics.AntiAliasing.Value; + ScalingFilter = (int)config.Graphics.ScalingFilter.Value; + ScalingFilterLevel = config.Graphics.ScalingFilterLevel.Value; + + // Audio + AudioBackend = (int)config.System.AudioBackend.Value; + Volume = config.System.AudioVolume * 100; + + // Network + EnableInternetAccess = config.System.EnableInternetAccess; + // LAN interface index is loaded asynchronously in PopulateNetworkInterfaces() + + // Logging + EnableFileLog = config.Logger.EnableFileLog; + EnableStub = config.Logger.EnableStub; + EnableInfo = config.Logger.EnableInfo; + EnableWarn = config.Logger.EnableWarn; + EnableError = config.Logger.EnableError; + EnableTrace = config.Logger.EnableTrace; + EnableGuest = config.Logger.EnableGuest; + EnableDebug = config.Logger.EnableDebug; + EnableFsAccessLog = config.Logger.EnableFsAccessLog; + FsGlobalAccessLogMode = config.System.FsGlobalAccessLogMode; + OpenglDebugLevel = (int)config.Logger.GraphicsDebugLevel.Value; + + MultiplayerModeIndex = (int)config.Multiplayer.Mode.Value; + DisableP2P = config.Multiplayer.DisableP2p.Value; + LdnPassphrase = config.Multiplayer.LdnPassphrase.Value; + LdnServer = config.Multiplayer.LdnServer.Value; + } + + public void SaveSettings() + { + ConfigurationState config = ConfigurationState.Instance; + + // User Interface + config.EnableDiscordIntegration.Value = EnableDiscordIntegration; + config.CheckUpdatesOnStart.Value = CheckUpdatesOnStart; + config.ShowConfirmExit.Value = ShowConfirmExit; + config.IgnoreApplet.Value = IgnoreApplet; + config.RememberWindowState.Value = RememberWindowState; + config.ShowTitleBar.Value = ShowTitleBar; + config.HideCursor.Value = (HideCursorMode)HideCursor; + + if (_gameDirectoryChanged) + { + List gameDirs = new(GameDirectories); + config.UI.GameDirs.Value = gameDirs; + } + + if (_autoloadDirectoryChanged) + { + List autoloadDirs = new(AutoloadDirectories); + config.UI.AutoloadDirs.Value = autoloadDirs; + } + + config.UI.BaseStyle.Value = BaseStyleIndex switch + { + 0 => "Auto", + 1 => "Light", + 2 => "Dark", + _ => "Auto" + }; + + // Input + config.System.EnableDockedMode.Value = EnableDockedMode; + config.Hid.EnableKeyboard.Value = EnableKeyboard; + config.Hid.EnableMouse.Value = EnableMouse; + + // Keyboard Hotkeys + config.Hid.Hotkeys.Value = KeyboardHotkey.GetConfig(); + + // System + config.System.Region.Value = (Region)Region; + config.System.Language.Value = (Language)Language; + + if (_validTzRegions.Contains(TimeZone)) + { + config.System.TimeZone.Value = TimeZone; + } + + config.System.SystemTimeOffset.Value = Convert.ToInt64((CurrentDate.ToUnixTimeSeconds() + CurrentTime.TotalSeconds) - DateTimeOffset.Now.ToUnixTimeSeconds()); + config.Graphics.VSyncMode.Value = VSyncMode; + config.Graphics.EnableCustomVSyncInterval.Value = EnableCustomVSyncInterval; + config.Graphics.CustomVSyncInterval.Value = CustomVSyncInterval; + config.System.EnableFsIntegrityChecks.Value = EnableFsIntegrityChecks; + config.System.DramSize.Value = DramSize; + config.System.IgnoreMissingServices.Value = IgnoreMissingServices; + + // CPU + config.System.EnablePtc.Value = EnablePptc; + config.System.EnableLowPowerPtc.Value = EnableLowPowerPptc; + config.System.MemoryManagerMode.Value = (MemoryManagerMode)MemoryMode; + config.System.UseHypervisor.Value = UseHypervisor; + + // Graphics + config.Graphics.GraphicsBackend.Value = (GraphicsBackend)GraphicsBackendIndex; + config.Graphics.PreferredGpu.Value = _gpuIds.ElementAtOrDefault(PreferredGpuIndex); + config.Graphics.EnableShaderCache.Value = EnableShaderCache; + config.Graphics.EnableTextureRecompression.Value = EnableTextureRecompression; + config.Graphics.EnableMacroHLE.Value = EnableMacroHLE; + config.Graphics.EnableColorSpacePassthrough.Value = EnableColorSpacePassthrough; + config.Graphics.ResScale.Value = ResolutionScale == 4 ? -1 : ResolutionScale + 1; + config.Graphics.ResScaleCustom.Value = CustomResolutionScale; + config.Graphics.MaxAnisotropy.Value = MaxAnisotropy == 0 ? -1 : MathF.Pow(2, MaxAnisotropy); + config.Graphics.AspectRatio.Value = (AspectRatio)AspectRatio; + config.Graphics.AntiAliasing.Value = (AntiAliasing)AntiAliasingEffect; + config.Graphics.ScalingFilter.Value = (ScalingFilter)ScalingFilter; + config.Graphics.ScalingFilterLevel.Value = ScalingFilterLevel; + + if (ConfigurationState.Instance.Graphics.BackendThreading != (BackendThreading)GraphicsBackendMultithreadingIndex) + { + DriverUtilities.ToggleOGLThreading(GraphicsBackendMultithreadingIndex == (int)BackendThreading.Off); + } + + config.Graphics.BackendThreading.Value = (BackendThreading)GraphicsBackendMultithreadingIndex; + config.Graphics.ShadersDumpPath.Value = ShaderDumpPath; + + // Audio + AudioBackend audioBackend = (AudioBackend)AudioBackend; + if (audioBackend != config.System.AudioBackend.Value) + { + config.System.AudioBackend.Value = audioBackend; + + Logger.Info?.Print(LogClass.Application, $"AudioBackend toggled to: {audioBackend}"); + } + + config.System.AudioVolume.Value = Volume / 100; + + // Network + config.System.EnableInternetAccess.Value = EnableInternetAccess; + + // Logging + config.Logger.EnableFileLog.Value = EnableFileLog; + config.Logger.EnableStub.Value = EnableStub; + config.Logger.EnableInfo.Value = EnableInfo; + config.Logger.EnableWarn.Value = EnableWarn; + config.Logger.EnableError.Value = EnableError; + config.Logger.EnableTrace.Value = EnableTrace; + config.Logger.EnableGuest.Value = EnableGuest; + config.Logger.EnableDebug.Value = EnableDebug; + config.Logger.EnableFsAccessLog.Value = EnableFsAccessLog; + config.System.FsGlobalAccessLogMode.Value = FsGlobalAccessLogMode; + config.Logger.GraphicsDebugLevel.Value = (GraphicsDebugLevel)OpenglDebugLevel; + + config.Multiplayer.LanInterfaceId.Value = _networkInterfaces[NetworkInterfaceList[NetworkInterfaceIndex]]; + config.Multiplayer.Mode.Value = (MultiplayerMode)MultiplayerModeIndex; + config.Multiplayer.DisableP2p.Value = DisableP2P; + config.Multiplayer.LdnPassphrase.Value = LdnPassphrase; + config.Multiplayer.LdnServer.Value = LdnServer; + + config.ToFileFormat().SaveConfig(Program.ConfigurationPath); + + MainWindow.UpdateGraphicsConfig(); + MainWindow.MainWindowViewModel.VSyncModeSettingChanged(); + + SaveSettingsEvent?.Invoke(); + + _gameDirectoryChanged = false; + _autoloadDirectoryChanged = false; + } + + private static void RevertIfNotSaved() + { + Program.ReloadConfig(); + } + + public void ApplyButton() + { + SaveSettings(); + } + + public void OkButton() + { + SaveSettings(); + CloseWindow?.Invoke(); + } + + public void CancelButton() + { + RevertIfNotSaved(); + CloseWindow?.Invoke(); + } + } +} diff --git a/src/Ryujinx/UI/ViewModels/TitleUpdateViewModel.cs b/src/Ryujinx/UI/ViewModels/TitleUpdateViewModel.cs new file mode 100644 index 000000000..dacdc3056 --- /dev/null +++ b/src/Ryujinx/UI/ViewModels/TitleUpdateViewModel.cs @@ -0,0 +1,242 @@ +using Avalonia.Collections; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Platform.Storage; +using Avalonia.Threading; +using FluentAvalonia.UI.Controls; +using Ryujinx.Ava.Common.Locale; +using Ryujinx.Ava.UI.Helpers; +using Ryujinx.HLE.FileSystem; +using Ryujinx.UI.App.Common; +using Ryujinx.UI.Common.Models; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Application = Avalonia.Application; + +namespace Ryujinx.Ava.UI.ViewModels +{ + public record TitleUpdateViewNoUpdateSentinal(); + + public class TitleUpdateViewModel : BaseModel + { + private ApplicationLibrary ApplicationLibrary { get; } + private ApplicationData ApplicationData { get; } + + private AvaloniaList _titleUpdates = new(); + private AvaloniaList _views = new(); + private object _selectedUpdate = new TitleUpdateViewNoUpdateSentinal(); + private bool _showBundledContentNotice = false; + + public AvaloniaList TitleUpdates + { + get => _titleUpdates; + set + { + _titleUpdates = value; + OnPropertyChanged(); + } + } + + public AvaloniaList Views + { + get => _views; + set + { + _views = value; + OnPropertyChanged(); + } + } + + public object SelectedUpdate + { + get => _selectedUpdate; + set + { + _selectedUpdate = value; + OnPropertyChanged(); + } + } + + public bool ShowBundledContentNotice + { + get => _showBundledContentNotice; + set + { + _showBundledContentNotice = value; + OnPropertyChanged(); + } + } + + public IStorageProvider StorageProvider; + + public TitleUpdateViewModel(ApplicationLibrary applicationLibrary, ApplicationData applicationData) + { + ApplicationLibrary = applicationLibrary; + + ApplicationData = applicationData; + + if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + StorageProvider = desktop.MainWindow.StorageProvider; + } + + LoadUpdates(); + } + + private void LoadUpdates() + { + var updates = ApplicationLibrary.TitleUpdates.Items + .Where(it => it.TitleUpdate.TitleIdBase == ApplicationData.IdBase); + + bool hasBundledContent = false; + SelectedUpdate = new TitleUpdateViewNoUpdateSentinal(); + foreach ((TitleUpdateModel update, bool isSelected) in updates) + { + TitleUpdates.Add(update); + hasBundledContent = hasBundledContent || update.IsBundled; + + if (isSelected) + { + SelectedUpdate = update; + } + } + + ShowBundledContentNotice = hasBundledContent; + + SortUpdates(); + } + + public void SortUpdates() + { + var sortedUpdates = TitleUpdates.OrderByDescending(update => update.Version); + + // NOTE(jpr): this works around a bug where calling Views.Clear also clears SelectedUpdate for + // some reason. so we save the item here and restore it after + var selected = SelectedUpdate; + + Views.Clear(); + Views.Add(new TitleUpdateViewNoUpdateSentinal()); + Views.AddRange(sortedUpdates); + + SelectedUpdate = selected; + + if (SelectedUpdate is TitleUpdateViewNoUpdateSentinal) + { + SelectedUpdate = Views[0]; + } + // this is mainly to handle a scenario where the user removes the selected update + else if (!TitleUpdates.Contains((TitleUpdateModel)SelectedUpdate)) + { + SelectedUpdate = Views.Count > 1 ? Views[1] : Views[0]; + } + } + + private bool AddUpdate(string path, out int numUpdatesAdded) + { + numUpdatesAdded = 0; + + if (!File.Exists(path)) + { + return false; + } + + if (!ApplicationLibrary.TryGetTitleUpdatesFromFile(path, out var updates)) + { + return false; + } + + var updatesForThisGame = updates.Where(it => it.TitleIdBase == ApplicationData.Id).ToList(); + if (updatesForThisGame.Count == 0) + { + return false; + } + + foreach (var update in updatesForThisGame) + { + if (!TitleUpdates.Contains(update)) + { + TitleUpdates.Add(update); + SelectedUpdate = update; + + numUpdatesAdded++; + } + } + + if (numUpdatesAdded > 0) + { + SortUpdates(); + } + + return true; + } + + public void RemoveUpdate(TitleUpdateModel update) + { + if (!update.IsBundled) + { + TitleUpdates.Remove(update); + } + else if (update == SelectedUpdate as TitleUpdateModel) + { + SelectedUpdate = new TitleUpdateViewNoUpdateSentinal(); + } + + SortUpdates(); + } + + public async Task Add() + { + var result = await StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions + { + AllowMultiple = true, + FileTypeFilter = new List + { + new(LocaleManager.Instance[LocaleKeys.AllSupportedFormats]) + { + Patterns = new[] { "*.nsp" }, + AppleUniformTypeIdentifiers = new[] { "com.ryujinx.nsp" }, + MimeTypes = new[] { "application/x-nx-nsp" }, + }, + }, + }); + + var totalUpdatesAdded = 0; + foreach (var file in result) + { + if (!AddUpdate(file.Path.LocalPath, out var newUpdatesAdded)) + { + await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogUpdateAddUpdateErrorMessage]); + } + + totalUpdatesAdded += newUpdatesAdded; + } + + if (totalUpdatesAdded > 0) + { + await ShowNewUpdatesAddedDialog(totalUpdatesAdded); + } + } + + public void Save() + { + var updates = TitleUpdates.Select(it => (it, it == SelectedUpdate as TitleUpdateModel)).ToList(); + ApplicationLibrary.SaveTitleUpdatesForGame(ApplicationData, updates); + } + + private Task ShowNewUpdatesAddedDialog(int numAdded) + { + var msg = string.Format(LocaleManager.Instance[LocaleKeys.UpdateWindowUpdateAddedMessage], numAdded); + return Dispatcher.UIThread.InvokeAsync(async () => + await ContentDialogHelper.ShowTextDialog( + LocaleManager.Instance[LocaleKeys.DialogConfirmationTitle], + msg, + string.Empty, + string.Empty, + string.Empty, + LocaleManager.Instance[LocaleKeys.InputDialogOk], + (int)Symbol.Checkmark + )); + } + } +} diff --git a/src/Ryujinx/UI/ViewModels/UserFirmwareAvatarSelectorViewModel.cs b/src/Ryujinx/UI/ViewModels/UserFirmwareAvatarSelectorViewModel.cs new file mode 100644 index 000000000..b07bf78b9 --- /dev/null +++ b/src/Ryujinx/UI/ViewModels/UserFirmwareAvatarSelectorViewModel.cs @@ -0,0 +1,225 @@ +using Avalonia.Media; +using LibHac.Common; +using LibHac.Fs; +using LibHac.Fs.Fsa; +using LibHac.FsSystem; +using LibHac.Ncm; +using LibHac.Tools.Fs; +using LibHac.Tools.FsSystem; +using LibHac.Tools.FsSystem.NcaUtils; +using Ryujinx.Ava.UI.Models; +using Ryujinx.HLE.FileSystem; +using SkiaSharp; +using System; +using System.Buffers.Binary; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.IO; +using Color = Avalonia.Media.Color; +using Image = SkiaSharp.SKImage; + +namespace Ryujinx.Ava.UI.ViewModels +{ + internal class UserFirmwareAvatarSelectorViewModel : BaseModel + { + private static readonly Dictionary _avatarStore = new(); + + private ObservableCollection _images; + private Color _backgroundColor = Colors.White; + + private int _selectedIndex; + + public UserFirmwareAvatarSelectorViewModel() + { + _images = new ObservableCollection(); + + LoadImagesFromStore(); + } + + public Color BackgroundColor + { + get => _backgroundColor; + set + { + _backgroundColor = value; + OnPropertyChanged(); + ChangeImageBackground(); + } + } + + public ObservableCollection Images + { + get => _images; + set + { + _images = value; + OnPropertyChanged(); + } + } + + public int SelectedIndex + { + get => _selectedIndex; + set + { + _selectedIndex = value; + + if (_selectedIndex == -1) + { + SelectedImage = null; + } + else + { + SelectedImage = _images[_selectedIndex].Data; + } + + OnPropertyChanged(); + } + } + + public byte[] SelectedImage { get; private set; } + + private void LoadImagesFromStore() + { + Images.Clear(); + + foreach (var image in _avatarStore) + { + Images.Add(new ProfileImageModel(image.Key, image.Value)); + } + } + + private void ChangeImageBackground() + { + foreach (var image in Images) + { + image.BackgroundColor = new SolidColorBrush(BackgroundColor); + } + } + + public static void PreloadAvatars(ContentManager contentManager, VirtualFileSystem virtualFileSystem) + { + if (_avatarStore.Count > 0) + { + return; + } + + string contentPath = contentManager.GetInstalledContentPath(0x010000000000080A, StorageId.BuiltInSystem, NcaContentType.Data); + string avatarPath = VirtualFileSystem.SwitchPathToSystemPath(contentPath); + + if (!string.IsNullOrWhiteSpace(avatarPath)) + { + using IStorage ncaFileStream = new LocalStorage(avatarPath, FileAccess.Read, FileMode.Open); + + Nca nca = new(virtualFileSystem.KeySet, ncaFileStream); + IFileSystem romfs = nca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.ErrorOnInvalid); + + foreach (DirectoryEntryEx item in romfs.EnumerateEntries()) + { + // TODO: Parse DatabaseInfo.bin and table.bin files for more accuracy. + if (item.Type == DirectoryEntryType.File && item.FullPath.Contains("chara") && item.FullPath.Contains("szs")) + { + using var file = new UniqueRef(); + + romfs.OpenFile(ref file.Ref, ("/" + item.FullPath).ToU8Span(), OpenMode.Read).ThrowIfFailure(); + + using MemoryStream stream = new(); + using MemoryStream streamPng = new(); + + file.Get.AsStream().CopyTo(stream); + + stream.Position = 0; + + Image avatarImage = Image.FromPixelCopy(new SKImageInfo(256, 256, SKColorType.Rgba8888, SKAlphaType.Premul), DecompressYaz0(stream)); + + using (SKData data = avatarImage.Encode(SKEncodedImageFormat.Png, 100)) + { + data.SaveTo(streamPng); + } + + _avatarStore.Add(item.FullPath, streamPng.ToArray()); + } + } + } + } + + private static byte[] DecompressYaz0(Stream stream) + { + using BinaryReader reader = new(stream); + + reader.ReadInt32(); // Magic + + uint decodedLength = BinaryPrimitives.ReverseEndianness(reader.ReadUInt32()); + + reader.ReadInt64(); // Padding + + byte[] input = new byte[stream.Length - stream.Position]; + stream.ReadExactly(input, 0, input.Length); + + uint inputOffset = 0; + + byte[] output = new byte[decodedLength]; + uint outputOffset = 0; + + ushort mask = 0; + byte header = 0; + + while (outputOffset < decodedLength) + { + if ((mask >>= 1) == 0) + { + header = input[inputOffset++]; + mask = 0x80; + } + + if ((header & mask) != 0) + { + if (outputOffset == output.Length) + { + break; + } + + output[outputOffset++] = input[inputOffset++]; + } + else + { + byte byte1 = input[inputOffset++]; + byte byte2 = input[inputOffset++]; + + uint dist = (uint)((byte1 & 0xF) << 8) | byte2; + uint position = outputOffset - (dist + 1); + + uint length = (uint)byte1 >> 4; + if (length == 0) + { + length = (uint)input[inputOffset++] + 0x12; + } + else + { + length += 2; + } + + uint gap = outputOffset - position; + uint nonOverlappingLength = length; + + if (nonOverlappingLength > gap) + { + nonOverlappingLength = gap; + } + + Buffer.BlockCopy(output, (int)position, output, (int)outputOffset, (int)nonOverlappingLength); + outputOffset += nonOverlappingLength; + position += nonOverlappingLength; + length -= nonOverlappingLength; + + while (length-- > 0) + { + output[outputOffset++] = output[position++]; + } + } + } + + return output; + } + } +} diff --git a/src/Ryujinx/UI/ViewModels/UserProfileImageSelectorViewModel.cs b/src/Ryujinx/UI/ViewModels/UserProfileImageSelectorViewModel.cs new file mode 100644 index 000000000..8e7d41a55 --- /dev/null +++ b/src/Ryujinx/UI/ViewModels/UserProfileImageSelectorViewModel.cs @@ -0,0 +1,18 @@ +namespace Ryujinx.Ava.UI.ViewModels +{ + internal class UserProfileImageSelectorViewModel : BaseModel + { + private bool _firmwareFound; + + public bool FirmwareFound + { + get => _firmwareFound; + + set + { + _firmwareFound = value; + OnPropertyChanged(); + } + } + } +} diff --git a/src/Ryujinx/UI/ViewModels/UserProfileViewModel.cs b/src/Ryujinx/UI/ViewModels/UserProfileViewModel.cs new file mode 100644 index 000000000..0b65e6d13 --- /dev/null +++ b/src/Ryujinx/UI/ViewModels/UserProfileViewModel.cs @@ -0,0 +1,28 @@ +using Ryujinx.Ava.UI.Models; +using System; +using System.Collections.ObjectModel; +using System.Linq; + +namespace Ryujinx.Ava.UI.ViewModels +{ + public class UserProfileViewModel : BaseModel, IDisposable + { + public UserProfileViewModel() + { + Profiles = new ObservableCollection(); + LostProfiles = new ObservableCollection(); + IsEmpty = !LostProfiles.Any(); + } + + public ObservableCollection Profiles { get; set; } + + public ObservableCollection LostProfiles { get; set; } + + public bool IsEmpty { get; set; } + + public void Dispose() + { + GC.SuppressFinalize(this); + } + } +} diff --git a/src/Ryujinx/UI/ViewModels/UserSaveManagerViewModel.cs b/src/Ryujinx/UI/ViewModels/UserSaveManagerViewModel.cs new file mode 100644 index 000000000..85adef005 --- /dev/null +++ b/src/Ryujinx/UI/ViewModels/UserSaveManagerViewModel.cs @@ -0,0 +1,117 @@ +using DynamicData; +using DynamicData.Binding; +using Ryujinx.Ava.Common.Locale; +using Ryujinx.Ava.UI.Models; +using Ryujinx.HLE.HOS.Services.Account.Acc; +using System.Collections.Generic; +using System.Collections.ObjectModel; + +namespace Ryujinx.Ava.UI.ViewModels +{ + public class UserSaveManagerViewModel : BaseModel + { + private int _sortIndex; + private int _orderIndex; + private string _search; + private ObservableCollection _saves = new(); + private ObservableCollection _views = new(); + private readonly AccountManager _accountManager; + + public string SaveManagerHeading => LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.SaveManagerHeading, _accountManager.LastOpenedUser.Name, _accountManager.LastOpenedUser.UserId); + + public int SortIndex + { + get => _sortIndex; + set + { + _sortIndex = value; + OnPropertyChanged(); + Sort(); + } + } + + public int OrderIndex + { + get => _orderIndex; + set + { + _orderIndex = value; + OnPropertyChanged(); + Sort(); + } + } + + public string Search + { + get => _search; + set + { + _search = value; + OnPropertyChanged(); + Sort(); + } + } + + public ObservableCollection Saves + { + get => _saves; + set + { + _saves = value; + OnPropertyChanged(); + Sort(); + } + } + + public ObservableCollection Views + { + get => _views; + set + { + _views = value; + OnPropertyChanged(); + } + } + + public UserSaveManagerViewModel(AccountManager accountManager) + { + _accountManager = accountManager; + } + + public void Sort() + { + Saves.AsObservableChangeSet() + .Filter(Filter) + .Sort(GetComparer()) + .Bind(out var view).AsObservableList(); + + _views.Clear(); + _views.AddRange(view); + OnPropertyChanged(nameof(Views)); + } + + private bool Filter(object arg) + { + if (arg is SaveModel save) + { + return string.IsNullOrWhiteSpace(_search) || save.Title.ToLower().Contains(_search.ToLower()); + } + + return false; + } + + private IComparer GetComparer() + { + return SortIndex switch + { + 0 => OrderIndex == 0 + ? SortExpressionComparer.Ascending(save => save.Title) + : SortExpressionComparer.Descending(save => save.Title), + 1 => OrderIndex == 0 + ? SortExpressionComparer.Ascending(save => save.Size) + : SortExpressionComparer.Descending(save => save.Size), + _ => null, + }; + } + } +} diff --git a/src/Ryujinx/UI/ViewModels/XCITrimmerViewModel.cs b/src/Ryujinx/UI/ViewModels/XCITrimmerViewModel.cs new file mode 100644 index 000000000..b582360f1 --- /dev/null +++ b/src/Ryujinx/UI/ViewModels/XCITrimmerViewModel.cs @@ -0,0 +1,541 @@ +using Avalonia.Collections; +using DynamicData; +using Gommon; +using Avalonia.Threading; +using Ryujinx.Ava.Common; +using Ryujinx.Ava.Common.Locale; +using Ryujinx.Ava.UI.Helpers; +using Ryujinx.Common.Utilities; +using Ryujinx.UI.App.Common; +using Ryujinx.UI.Common.Models; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using static Ryujinx.Common.Utilities.XCIFileTrimmer; + +namespace Ryujinx.Ava.UI.ViewModels +{ + public class XCITrimmerViewModel : BaseModel + { + private const long _bytesPerMB = 1024 * 1024; + private enum ProcessingMode + { + Trimming, + Untrimming + } + + public enum SortField + { + Name, + Saved + } + + private const string _FileExtXCI = "XCI"; + + private readonly Ryujinx.Common.Logging.XCIFileTrimmerLog _logger; + private readonly ApplicationLibrary _applicationLibrary; + private Optional _processingApplication = null; + private AvaloniaList _allXCIFiles = new(); + private AvaloniaList _selectedXCIFiles = new(); + private AvaloniaList _displayedXCIFiles = new(); + private MainWindowViewModel _mainWindowViewModel; + private CancellationTokenSource _cancellationTokenSource; + private string _search; + private ProcessingMode _processingMode; + private SortField _sortField = SortField.Name; + private bool _sortAscending = true; + + public XCITrimmerViewModel(MainWindowViewModel mainWindowViewModel) + { + _logger = new XCIFileTrimmerWindowLog(this); + _mainWindowViewModel = mainWindowViewModel; + _applicationLibrary = _mainWindowViewModel.ApplicationLibrary; + LoadXCIApplications(); + } + + private void LoadXCIApplications() + { + var apps = _applicationLibrary.Applications.Items + .Where(app => app.FileExtension == _FileExtXCI); + + foreach (var xciApp in apps) + AddOrUpdateXCITrimmerFile(CreateXCITrimmerFile(xciApp.Path)); + + ApplicationsChanged(); + } + + private XCITrimmerFileModel CreateXCITrimmerFile( + string path, + OperationOutcome operationOutcome = OperationOutcome.Undetermined) + { + var xciApp = _applicationLibrary.Applications.Items.First(app => app.FileExtension == _FileExtXCI && app.Path == path); + return XCITrimmerFileModel.FromApplicationData(xciApp, _logger) with { ProcessingOutcome = operationOutcome }; + } + + private bool AddOrUpdateXCITrimmerFile(XCITrimmerFileModel xci, bool suppressChanged = true, bool autoSelect = true) + { + bool replaced = _allXCIFiles.ReplaceWith(xci); + _displayedXCIFiles.ReplaceWith(xci, Filter(xci)); + _selectedXCIFiles.ReplaceWith(xci, xci.Trimmable && autoSelect); + + if (!suppressChanged) + ApplicationsChanged(); + + return replaced; + } + + private void FilteringChanged() + { + OnPropertyChanged(nameof(Search)); + SortAndFilter(); + } + + private void SortingChanged() + { + OnPropertyChanged(nameof(IsSortedByName)); + OnPropertyChanged(nameof(IsSortedBySaved)); + OnPropertyChanged(nameof(SortingAscending)); + OnPropertyChanged(nameof(SortingField)); + OnPropertyChanged(nameof(SortingFieldName)); + SortAndFilter(); + } + + private void DisplayedChanged() + { + OnPropertyChanged(nameof(Status)); + OnPropertyChanged(nameof(DisplayedXCIFiles)); + OnPropertyChanged(nameof(SelectedDisplayedXCIFiles)); + } + + private void ApplicationsChanged() + { + OnPropertyChanged(nameof(AllXCIFiles)); + OnPropertyChanged(nameof(Status)); + OnPropertyChanged(nameof(PotentialSavings)); + OnPropertyChanged(nameof(ActualSavings)); + OnPropertyChanged(nameof(CanTrim)); + OnPropertyChanged(nameof(CanUntrim)); + DisplayedChanged(); + SortAndFilter(); + } + + private void SelectionChanged(bool displayedChanged = true) + { + OnPropertyChanged(nameof(Status)); + OnPropertyChanged(nameof(CanTrim)); + OnPropertyChanged(nameof(CanUntrim)); + OnPropertyChanged(nameof(SelectedXCIFiles)); + + if (displayedChanged) + OnPropertyChanged(nameof(SelectedDisplayedXCIFiles)); + } + + private void ProcessingChanged() + { + OnPropertyChanged(nameof(Processing)); + OnPropertyChanged(nameof(Cancel)); + OnPropertyChanged(nameof(Status)); + OnPropertyChanged(nameof(CanTrim)); + OnPropertyChanged(nameof(CanUntrim)); + } + + private IEnumerable GetSelectedDisplayedXCIFiles() + { + return _displayedXCIFiles.Where(xci => _selectedXCIFiles.Contains(xci)); + } + + private void PerformOperation(ProcessingMode processingMode) + { + if (Processing) + { + return; + } + + _processingMode = processingMode; + Processing = true; + var cancellationToken = _cancellationTokenSource.Token; + + Thread XCIFileTrimThread = new(() => + { + var toProcess = Sort(SelectedXCIFiles + .Where(xci => + (processingMode == ProcessingMode.Untrimming && xci.Untrimmable) || + (processingMode == ProcessingMode.Trimming && xci.Trimmable) + )).ToList(); + + var viewsSaved = DisplayedXCIFiles.ToList(); + + Dispatcher.UIThread.Post(() => + { + _selectedXCIFiles.Clear(); + _displayedXCIFiles.Clear(); + _displayedXCIFiles.AddRange(toProcess); + }); + + try + { + foreach (var xciApp in toProcess) + { + if (cancellationToken.IsCancellationRequested) + break; + + var trimmer = new XCIFileTrimmer(xciApp.Path, _logger); + + Dispatcher.UIThread.Post(() => + { + ProcessingApplication = xciApp; + }); + + var outcome = OperationOutcome.Undetermined; + + try + { + if (cancellationToken.IsCancellationRequested) + break; + + switch (processingMode) + { + case ProcessingMode.Trimming: + outcome = trimmer.Trim(cancellationToken); + break; + case ProcessingMode.Untrimming: + outcome = trimmer.Untrim(cancellationToken); + break; + } + + if (outcome == OperationOutcome.Cancelled) + outcome = OperationOutcome.Undetermined; + } + finally + { + Dispatcher.UIThread.Post(() => + { + ProcessingApplication = CreateXCITrimmerFile(xciApp.Path); + AddOrUpdateXCITrimmerFile(ProcessingApplication, false, false); + ProcessingApplication = null; + }); + } + } + } + finally + { + Dispatcher.UIThread.Post(() => + { + _displayedXCIFiles.AddOrReplaceMatching(_allXCIFiles, viewsSaved); + _selectedXCIFiles.AddOrReplaceMatching(_allXCIFiles, toProcess); + Processing = false; + ApplicationsChanged(); + }); + } + }) + { + Name = "GUI.XCIFilesTrimmerThread", + IsBackground = true, + }; + + XCIFileTrimThread.Start(); + } + + private bool Filter(T arg) + { + if (arg is XCITrimmerFileModel content) + { + return string.IsNullOrWhiteSpace(_search) + || content.Name.ToLower().Contains(_search.ToLower()) + || content.Path.ToLower().Contains(_search.ToLower()); + } + + return false; + } + + private class CompareXCITrimmerFiles : IComparer + { + private XCITrimmerViewModel _viewModel; + + public CompareXCITrimmerFiles(XCITrimmerViewModel ViewModel) + { + _viewModel = ViewModel; + } + + public int Compare(XCITrimmerFileModel x, XCITrimmerFileModel y) + { + int result = 0; + + switch (_viewModel.SortingField) + { + case SortField.Name: + result = x.Name.CompareTo(y.Name); + break; + case SortField.Saved: + result = x.PotentialSavingsB.CompareTo(y.PotentialSavingsB); + break; + } + + if (!_viewModel.SortingAscending) + result = -result; + + if (result == 0) + result = x.Path.CompareTo(y.Path); + + return result; + } + } + + private IOrderedEnumerable Sort(IEnumerable list) + { + return list + .OrderBy(xci => xci, new CompareXCITrimmerFiles(this)) + .ThenBy(it => it.Path); + } + + public void TrimSelected() + { + PerformOperation(ProcessingMode.Trimming); + } + + public void UntrimSelected() + { + PerformOperation(ProcessingMode.Untrimming); + } + + public void SetProgress(int current, int maximum) + { + if (_processingApplication != null) + { + int percentageProgress = 100 * current / maximum; + if (!ProcessingApplication.HasValue || (ProcessingApplication.Value.PercentageProgress != percentageProgress)) + ProcessingApplication = ProcessingApplication.Value with { PercentageProgress = percentageProgress }; + } + } + + public void SelectDisplayed() + { + SelectedXCIFiles.AddRange(DisplayedXCIFiles); + SelectionChanged(); + } + + public void DeselectDisplayed() + { + SelectedXCIFiles.RemoveMany(DisplayedXCIFiles); + SelectionChanged(); + } + + public void Select(XCITrimmerFileModel model) + { + bool selectionChanged = !SelectedXCIFiles.Contains(model); + bool displayedSelectionChanged = !SelectedDisplayedXCIFiles.Contains(model); + SelectedXCIFiles.ReplaceOrAdd(model, model); + if (selectionChanged) + SelectionChanged(displayedSelectionChanged); + } + + public void Deselect(XCITrimmerFileModel model) + { + bool displayedSelectionChanged = !SelectedDisplayedXCIFiles.Contains(model); + if (SelectedXCIFiles.Remove(model)) + SelectionChanged(displayedSelectionChanged); + } + + public void SortAndFilter() + { + if (Processing) + return; + + Sort(AllXCIFiles) + .AsObservableChangeSet() + .Filter(Filter) + .Bind(out var view).AsObservableList(); + + _displayedXCIFiles.Clear(); + _displayedXCIFiles.AddRange(view); + + DisplayedChanged(); + } + + public Optional ProcessingApplication + { + get => _processingApplication; + set + { + if (!value.HasValue && _processingApplication.HasValue) + value = _processingApplication.Value with { PercentageProgress = null }; + + if (value.HasValue) + _displayedXCIFiles.ReplaceWith(value.Value); + + _processingApplication = value; + OnPropertyChanged(); + } + } + + public bool Processing + { + get => _cancellationTokenSource != null; + private set + { + if (value && !Processing) + { + _cancellationTokenSource = new CancellationTokenSource(); + } + else if (!value && Processing) + { + _cancellationTokenSource.Dispose(); + _cancellationTokenSource = null; + } + + ProcessingChanged(); + } + } + + public bool Cancel + { + get => _cancellationTokenSource != null && _cancellationTokenSource.IsCancellationRequested; + set + { + if (value) + { + if (!Processing) + return; + + _cancellationTokenSource.Cancel(); + } + + ProcessingChanged(); + } + } + + public string Status + { + get + { + if (Processing) + { + return _processingMode switch + { + ProcessingMode.Trimming => string.Format(LocaleManager.Instance[LocaleKeys.XCITrimmerTitleStatusTrimming], DisplayedXCIFiles.Count), + ProcessingMode.Untrimming => string.Format(LocaleManager.Instance[LocaleKeys.XCITrimmerTitleStatusUntrimming], DisplayedXCIFiles.Count), + _ => string.Empty + }; + } + else + { + return string.IsNullOrEmpty(Search) ? + string.Format(LocaleManager.Instance[LocaleKeys.XCITrimmerTitleStatusCount], SelectedXCIFiles.Count, AllXCIFiles.Count) : + string.Format(LocaleManager.Instance[LocaleKeys.XCITrimmerTitleStatusCountWithFilter], SelectedXCIFiles.Count, AllXCIFiles.Count, DisplayedXCIFiles.Count); + } + } + } + + public string Search + { + get => _search; + set + { + _search = value; + FilteringChanged(); + } + } + + public SortField SortingField + { + get => _sortField; + set + { + _sortField = value; + SortingChanged(); + } + } + + public string SortingFieldName + { + get + { + return SortingField switch + { + SortField.Name => LocaleManager.Instance[LocaleKeys.XCITrimmerSortName], + SortField.Saved => LocaleManager.Instance[LocaleKeys.XCITrimmerSortSaved], + _ => string.Empty, + }; + } + } + public bool SortingAscending + { + get => _sortAscending; + set + { + _sortAscending = value; + SortingChanged(); + } + } + + public bool IsSortedByName + { + get => _sortField == SortField.Name; + } + + public bool IsSortedBySaved + { + get => _sortField == SortField.Saved; + } + + public AvaloniaList SelectedXCIFiles + { + get => _selectedXCIFiles; + set + { + _selectedXCIFiles = value; + SelectionChanged(); + } + } + + public AvaloniaList AllXCIFiles + { + get => _allXCIFiles; + } + + public AvaloniaList DisplayedXCIFiles + { + get => _displayedXCIFiles; + } + + public string PotentialSavings + { + get + { + return string.Format(LocaleManager.Instance[LocaleKeys.XCITrimmerSavingsMb], AllXCIFiles.Sum(xci => xci.PotentialSavingsB / _bytesPerMB)); + } + } + + public string ActualSavings + { + get + { + return string.Format(LocaleManager.Instance[LocaleKeys.XCITrimmerSavingsMb], AllXCIFiles.Sum(xci => xci.CurrentSavingsB / _bytesPerMB)); + } + } + + public IEnumerable SelectedDisplayedXCIFiles + { + get + { + return GetSelectedDisplayedXCIFiles().ToList(); + } + } + + public bool CanTrim + { + get + { + return !Processing && _selectedXCIFiles.Any(xci => xci.Trimmable); + } + } + + public bool CanUntrim + { + get + { + return !Processing && _selectedXCIFiles.Any(xci => xci.Untrimmable); + } + } + } +} \ No newline at end of file diff --git a/src/Ryujinx/UI/Views/Input/ControllerInputView.axaml b/src/Ryujinx/UI/Views/Input/ControllerInputView.axaml new file mode 100644 index 000000000..7daf23eb6 --- /dev/null +++ b/src/Ryujinx/UI/Views/Input/ControllerInputView.axaml @@ -0,0 +1,765 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Ryujinx/UI/Views/Input/ControllerInputView.axaml.cs b/src/Ryujinx/UI/Views/Input/ControllerInputView.axaml.cs new file mode 100644 index 000000000..ee84fbc37 --- /dev/null +++ b/src/Ryujinx/UI/Views/Input/ControllerInputView.axaml.cs @@ -0,0 +1,241 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.LogicalTree; +using DiscordRPC; +using Ryujinx.Ava.UI.Helpers; +using Ryujinx.Ava.UI.ViewModels.Input; +using Ryujinx.Common.Configuration.Hid.Controller; +using Ryujinx.Common.Logging; +using Ryujinx.Input; +using Ryujinx.Input.Assigner; +using System; +using StickInputId = Ryujinx.Common.Configuration.Hid.Controller.StickInputId; + +namespace Ryujinx.Ava.UI.Views.Input +{ + public partial class ControllerInputView : UserControl + { + private ButtonKeyAssigner _currentAssigner; + + public ControllerInputView() + { + InitializeComponent(); + + foreach (ILogical visual in SettingButtons.GetLogicalDescendants()) + { + switch (visual) + { + case ToggleButton button and not CheckBox: + button.IsCheckedChanged += Button_IsCheckedChanged; + break; + case CheckBox check: + check.IsCheckedChanged += CheckBox_IsCheckedChanged; + break; + case Slider slider: + slider.PropertyChanged += Slider_ValueChanged; + break; + } + } + } + + protected override void OnPointerReleased(PointerReleasedEventArgs e) + { + base.OnPointerReleased(e); + + if (_currentAssigner is { ToggledButton.IsPointerOver: false }) + { + _currentAssigner.Cancel(); + } + } + + private float _changeSlider = float.NaN; + + private void Slider_ValueChanged(object sender, AvaloniaPropertyChangedEventArgs e) + { + if (sender is Slider check) + { + _changeSlider = check.IsPointerOver switch + { + true when float.IsNaN(_changeSlider) => (float)check.Value, + false => float.NaN, + _ => _changeSlider + }; + + if (!float.IsNaN(_changeSlider) && _changeSlider != (float)check.Value) + { + (DataContext as ControllerInputViewModel)!.ParentModel.IsModified = true; + _changeSlider = (float)check.Value; + } + } + } + + private void CheckBox_IsCheckedChanged(object sender, RoutedEventArgs e) + { + if (sender is CheckBox { IsPointerOver: true }) + { + (DataContext as ControllerInputViewModel)!.ParentModel.IsModified = true; + _currentAssigner?.Cancel(); + _currentAssigner = null; + } + } + + + private void Button_IsCheckedChanged(object sender, RoutedEventArgs e) + { + if (sender is ToggleButton button) + { + if (button.IsChecked is true) + { + if (_currentAssigner != null && button == _currentAssigner.ToggledButton) + { + return; + } + + bool isStick = button.Tag != null && button.Tag.ToString() == "stick"; + + if (_currentAssigner == null) + { + _currentAssigner = new ButtonKeyAssigner(button); + + this.Focus(NavigationMethod.Pointer); + + PointerPressed += MouseClick; + + var viewModel = (DataContext as ControllerInputViewModel); + + IKeyboard keyboard = (IKeyboard)viewModel.ParentModel.AvaloniaKeyboardDriver.GetGamepad("0"); // Open Avalonia keyboard for cancel operations. + IButtonAssigner assigner = CreateButtonAssigner(isStick); + + _currentAssigner.ButtonAssigned += (sender, e) => + { + if (e.ButtonValue.HasValue) + { + var buttonValue = e.ButtonValue.Value; + viewModel.ParentModel.IsModified = true; + + switch (button.Name) + { + case "ButtonZl": + viewModel.Config.ButtonZl = buttonValue.AsHidType(); + break; + case "ButtonL": + viewModel.Config.ButtonL = buttonValue.AsHidType(); + break; + case "ButtonMinus": + viewModel.Config.ButtonMinus = buttonValue.AsHidType(); + break; + case "LeftStickButton": + viewModel.Config.LeftStickButton = buttonValue.AsHidType(); + break; + case "LeftJoystick": + viewModel.Config.LeftJoystick = buttonValue.AsHidType(); + break; + case "DpadUp": + viewModel.Config.DpadUp = buttonValue.AsHidType(); + break; + case "DpadDown": + viewModel.Config.DpadDown = buttonValue.AsHidType(); + break; + case "DpadLeft": + viewModel.Config.DpadLeft = buttonValue.AsHidType(); + break; + case "DpadRight": + viewModel.Config.DpadRight = buttonValue.AsHidType(); + break; + case "LeftButtonSr": + viewModel.Config.LeftButtonSr = buttonValue.AsHidType(); + break; + case "LeftButtonSl": + viewModel.Config.LeftButtonSl = buttonValue.AsHidType(); + break; + case "RightButtonSr": + viewModel.Config.RightButtonSr = buttonValue.AsHidType(); + break; + case "RightButtonSl": + viewModel.Config.RightButtonSl = buttonValue.AsHidType(); + break; + case "ButtonZr": + viewModel.Config.ButtonZr = buttonValue.AsHidType(); + break; + case "ButtonR": + viewModel.Config.ButtonR = buttonValue.AsHidType(); + break; + case "ButtonPlus": + viewModel.Config.ButtonPlus = buttonValue.AsHidType(); + break; + case "ButtonA": + viewModel.Config.ButtonA = buttonValue.AsHidType(); + break; + case "ButtonB": + viewModel.Config.ButtonB = buttonValue.AsHidType(); + break; + case "ButtonX": + viewModel.Config.ButtonX = buttonValue.AsHidType(); + break; + case "ButtonY": + viewModel.Config.ButtonY = buttonValue.AsHidType(); + break; + case "RightStickButton": + viewModel.Config.RightStickButton = buttonValue.AsHidType(); + break; + case "RightJoystick": + viewModel.Config.RightJoystick = buttonValue.AsHidType(); + break; + } + } + }; + + _currentAssigner.GetInputAndAssign(assigner, keyboard); + } + else + { + if (_currentAssigner != null) + { + _currentAssigner.Cancel(); + _currentAssigner = null; + button.IsChecked = false; + } + } + } + else + { + _currentAssigner?.Cancel(); + _currentAssigner = null; + } + } + } + + private void MouseClick(object sender, PointerPressedEventArgs e) + { + bool shouldUnbind = e.GetCurrentPoint(this).Properties.IsMiddleButtonPressed; + + _currentAssigner?.Cancel(shouldUnbind); + + PointerPressed -= MouseClick; + } + + private IButtonAssigner CreateButtonAssigner(bool forStick) + { + IButtonAssigner assigner; + + var controllerInputViewModel = DataContext as ControllerInputViewModel; + + assigner = new GamepadButtonAssigner( + controllerInputViewModel.ParentModel.SelectedGamepad, + (controllerInputViewModel.ParentModel.Config as StandardControllerInputConfig).TriggerThreshold, + forStick); + + return assigner; + } + + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnDetachedFromVisualTree(e); + _currentAssigner?.Cancel(); + _currentAssigner = null; + } + } +} diff --git a/src/Ryujinx/UI/Views/Input/InputView.axaml b/src/Ryujinx/UI/Views/Input/InputView.axaml new file mode 100644 index 000000000..b5bfa666d --- /dev/null +++ b/src/Ryujinx/UI/Views/Input/InputView.axaml @@ -0,0 +1,225 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Ryujinx/UI/Views/Input/InputView.axaml.cs b/src/Ryujinx/UI/Views/Input/InputView.axaml.cs new file mode 100644 index 000000000..3c9d4040f --- /dev/null +++ b/src/Ryujinx/UI/Views/Input/InputView.axaml.cs @@ -0,0 +1,78 @@ +using Avalonia.Controls; +using Ryujinx.Ava.Common.Locale; +using Ryujinx.Ava.UI.Helpers; +using Ryujinx.Ava.UI.Models; +using Ryujinx.Ava.UI.ViewModels.Input; + +namespace Ryujinx.Ava.UI.Views.Input +{ + public partial class InputView : UserControl + { + private bool _dialogOpen; + private InputViewModel ViewModel { get; set; } + + public InputView() + { + DataContext = ViewModel = new InputViewModel(this); + + InitializeComponent(); + } + + public void SaveCurrentProfile() + { + ViewModel.Save(); + } + + private async void PlayerIndexBox_OnSelectionChanged(object sender, SelectionChangedEventArgs e) + { + if (PlayerIndexBox != null) + { + if (PlayerIndexBox.SelectedIndex != (int)ViewModel.PlayerId) + { + PlayerIndexBox.SelectedIndex = (int)ViewModel.PlayerId; + } + } + + if (ViewModel.IsModified && !_dialogOpen) + { + _dialogOpen = true; + + var result = await ContentDialogHelper.CreateDeniableConfirmationDialog( + LocaleManager.Instance[LocaleKeys.DialogControllerSettingsModifiedConfirmMessage], + LocaleManager.Instance[LocaleKeys.DialogControllerSettingsModifiedConfirmSubMessage], + LocaleManager.Instance[LocaleKeys.InputDialogYes], + LocaleManager.Instance[LocaleKeys.InputDialogNo], + LocaleManager.Instance[LocaleKeys.Cancel], + LocaleManager.Instance[LocaleKeys.RyujinxConfirm]); + + + if (result == UserResult.Yes) + { + ViewModel.Save(); + } + + _dialogOpen = false; + + if (result == UserResult.Cancel) + { + if (e.AddedItems.Count > 0) + { + ViewModel.IsModified = true; + ViewModel.PlayerId = ((PlayerModel)e.AddedItems[0])!.Id; + } + return; + } + + ViewModel.PlayerId = ViewModel.PlayerIdChoose; + + ViewModel.IsModified = false; + } + + } + + public void Dispose() + { + ViewModel.Dispose(); + } + } +} diff --git a/src/Ryujinx/UI/Views/Input/KeyboardInputView.axaml b/src/Ryujinx/UI/Views/Input/KeyboardInputView.axaml new file mode 100644 index 000000000..ecb9053f7 --- /dev/null +++ b/src/Ryujinx/UI/Views/Input/KeyboardInputView.axaml @@ -0,0 +1,675 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Ryujinx/UI/Views/Input/KeyboardInputView.axaml.cs b/src/Ryujinx/UI/Views/Input/KeyboardInputView.axaml.cs new file mode 100644 index 000000000..090d0335c --- /dev/null +++ b/src/Ryujinx/UI/Views/Input/KeyboardInputView.axaml.cs @@ -0,0 +1,202 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.LogicalTree; +using Ryujinx.Ava.UI.Helpers; +using Ryujinx.Ava.UI.ViewModels.Input; +using Ryujinx.Input; +using Ryujinx.Input.Assigner; +using Key = Ryujinx.Common.Configuration.Hid.Key; + +namespace Ryujinx.Ava.UI.Views.Input +{ + public partial class KeyboardInputView : UserControl + { + private ButtonKeyAssigner _currentAssigner; + + public KeyboardInputView() + { + InitializeComponent(); + + foreach (ILogical visual in SettingButtons.GetLogicalDescendants()) + { + if (visual is ToggleButton button and not CheckBox) + { + button.IsCheckedChanged += Button_IsCheckedChanged; + } + } + } + + protected override void OnPointerReleased(PointerReleasedEventArgs e) + { + base.OnPointerReleased(e); + + if (_currentAssigner is { ToggledButton.IsPointerOver: false }) + { + _currentAssigner.Cancel(); + } + } + + private void Button_IsCheckedChanged(object sender, RoutedEventArgs e) + { + if (sender is not ToggleButton button) + return; + + if (button.IsChecked is true) + { + if (_currentAssigner != null && button == _currentAssigner.ToggledButton) + { + return; + } + + if (_currentAssigner == null) + { + _currentAssigner = new ButtonKeyAssigner(button); + + Focus(NavigationMethod.Pointer); + + PointerPressed += MouseClick; + + if (DataContext is not KeyboardInputViewModel viewModel) + return; + + IKeyboard keyboard = + (IKeyboard)viewModel.ParentModel.AvaloniaKeyboardDriver.GetGamepad("0"); // Open Avalonia keyboard for cancel operations. + IButtonAssigner assigner = + new KeyboardKeyAssigner((IKeyboard)viewModel.ParentModel.SelectedGamepad); + + _currentAssigner.ButtonAssigned += (_, e) => + { + if (e.ButtonValue.HasValue) + { + var buttonValue = e.ButtonValue.Value; + viewModel.ParentModel.IsModified = true; + + switch (button.Name) + { + case "ButtonZl": + viewModel.Config.ButtonZl = buttonValue.AsHidType(); + break; + case "ButtonL": + viewModel.Config.ButtonL = buttonValue.AsHidType(); + break; + case "ButtonMinus": + viewModel.Config.ButtonMinus = buttonValue.AsHidType(); + break; + case "LeftStickButton": + viewModel.Config.LeftStickButton = buttonValue.AsHidType(); + break; + case "LeftStickUp": + viewModel.Config.LeftStickUp = buttonValue.AsHidType(); + break; + case "LeftStickDown": + viewModel.Config.LeftStickDown = buttonValue.AsHidType(); + break; + case "LeftStickRight": + viewModel.Config.LeftStickRight = buttonValue.AsHidType(); + break; + case "LeftStickLeft": + viewModel.Config.LeftStickLeft = buttonValue.AsHidType(); + break; + case "DpadUp": + viewModel.Config.DpadUp = buttonValue.AsHidType(); + break; + case "DpadDown": + viewModel.Config.DpadDown = buttonValue.AsHidType(); + break; + case "DpadLeft": + viewModel.Config.DpadLeft = buttonValue.AsHidType(); + break; + case "DpadRight": + viewModel.Config.DpadRight = buttonValue.AsHidType(); + break; + case "LeftButtonSr": + viewModel.Config.LeftButtonSr = buttonValue.AsHidType(); + break; + case "LeftButtonSl": + viewModel.Config.LeftButtonSl = buttonValue.AsHidType(); + break; + case "RightButtonSr": + viewModel.Config.RightButtonSr = buttonValue.AsHidType(); + break; + case "RightButtonSl": + viewModel.Config.RightButtonSl = buttonValue.AsHidType(); + break; + case "ButtonZr": + viewModel.Config.ButtonZr = buttonValue.AsHidType(); + break; + case "ButtonR": + viewModel.Config.ButtonR = buttonValue.AsHidType(); + break; + case "ButtonPlus": + viewModel.Config.ButtonPlus = buttonValue.AsHidType(); + break; + case "ButtonA": + viewModel.Config.ButtonA = buttonValue.AsHidType(); + break; + case "ButtonB": + viewModel.Config.ButtonB = buttonValue.AsHidType(); + break; + case "ButtonX": + viewModel.Config.ButtonX = buttonValue.AsHidType(); + break; + case "ButtonY": + viewModel.Config.ButtonY = buttonValue.AsHidType(); + break; + case "RightStickButton": + viewModel.Config.RightStickButton = buttonValue.AsHidType(); + break; + case "RightStickUp": + viewModel.Config.RightStickUp = buttonValue.AsHidType(); + break; + case "RightStickDown": + viewModel.Config.RightStickDown = buttonValue.AsHidType(); + break; + case "RightStickRight": + viewModel.Config.RightStickRight = buttonValue.AsHidType(); + break; + case "RightStickLeft": + viewModel.Config.RightStickLeft = buttonValue.AsHidType(); + break; + } + } + }; + + _currentAssigner.GetInputAndAssign(assigner, keyboard); + } + else + { + if (_currentAssigner != null) + { + _currentAssigner.Cancel(); + _currentAssigner = null; + button.IsChecked = false; + } + } + } + else + { + _currentAssigner?.Cancel(); + _currentAssigner = null; + } + } + + private void MouseClick(object sender, PointerPressedEventArgs e) + { + bool shouldUnbind = e.GetCurrentPoint(this).Properties.IsMiddleButtonPressed; + + _currentAssigner?.Cancel(shouldUnbind); + + PointerPressed -= MouseClick; + } + + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnDetachedFromVisualTree(e); + _currentAssigner?.Cancel(); + _currentAssigner = null; + } + } +} diff --git a/src/Ryujinx/UI/Views/Input/MotionInputView.axaml b/src/Ryujinx/UI/Views/Input/MotionInputView.axaml new file mode 100644 index 000000000..9096a06d1 --- /dev/null +++ b/src/Ryujinx/UI/Views/Input/MotionInputView.axaml @@ -0,0 +1,171 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Ryujinx/UI/Views/Input/MotionInputView.axaml.cs b/src/Ryujinx/UI/Views/Input/MotionInputView.axaml.cs new file mode 100644 index 000000000..ca4a4e1cf --- /dev/null +++ b/src/Ryujinx/UI/Views/Input/MotionInputView.axaml.cs @@ -0,0 +1,66 @@ +using Avalonia.Controls; +using FluentAvalonia.UI.Controls; +using Ryujinx.Ava.Common.Locale; +using Ryujinx.Ava.UI.ViewModels.Input; +using System.Threading.Tasks; + +namespace Ryujinx.Ava.UI.Views.Input +{ + public partial class MotionInputView : UserControl + { + private readonly MotionInputViewModel _viewModel; + + public MotionInputView() + { + InitializeComponent(); + } + + public MotionInputView(ControllerInputViewModel viewModel) + { + var config = viewModel.Config; + + _viewModel = new MotionInputViewModel + { + Slot = config.Slot, + AltSlot = config.AltSlot, + DsuServerHost = config.DsuServerHost, + DsuServerPort = config.DsuServerPort, + MirrorInput = config.MirrorInput, + Sensitivity = config.Sensitivity, + GyroDeadzone = config.GyroDeadzone, + EnableCemuHookMotion = config.EnableCemuHookMotion, + }; + + InitializeComponent(); + DataContext = _viewModel; + } + + public static async Task Show(ControllerInputViewModel viewModel) + { + MotionInputView content = new(viewModel); + + ContentDialog contentDialog = new() + { + Title = LocaleManager.Instance[LocaleKeys.ControllerMotionTitle], + PrimaryButtonText = LocaleManager.Instance[LocaleKeys.ControllerSettingsSave], + SecondaryButtonText = string.Empty, + CloseButtonText = LocaleManager.Instance[LocaleKeys.ControllerSettingsClose], + Content = content, + }; + contentDialog.PrimaryButtonClick += (sender, args) => + { + var config = viewModel.Config; + config.Slot = content._viewModel.Slot; + config.Sensitivity = content._viewModel.Sensitivity; + config.GyroDeadzone = content._viewModel.GyroDeadzone; + config.AltSlot = content._viewModel.AltSlot; + config.DsuServerHost = content._viewModel.DsuServerHost; + config.DsuServerPort = content._viewModel.DsuServerPort; + config.EnableCemuHookMotion = content._viewModel.EnableCemuHookMotion; + config.MirrorInput = content._viewModel.MirrorInput; + }; + + await contentDialog.ShowAsync(); + } + } +} diff --git a/src/Ryujinx/UI/Views/Input/RumbleInputView.axaml b/src/Ryujinx/UI/Views/Input/RumbleInputView.axaml new file mode 100644 index 000000000..5f6cde5b5 --- /dev/null +++ b/src/Ryujinx/UI/Views/Input/RumbleInputView.axaml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/src/Ryujinx/UI/Views/Input/RumbleInputView.axaml.cs b/src/Ryujinx/UI/Views/Input/RumbleInputView.axaml.cs new file mode 100644 index 000000000..86a75e6eb --- /dev/null +++ b/src/Ryujinx/UI/Views/Input/RumbleInputView.axaml.cs @@ -0,0 +1,56 @@ +using Avalonia.Controls; +using FluentAvalonia.UI.Controls; +using Ryujinx.Ava.Common.Locale; +using Ryujinx.Ava.UI.ViewModels.Input; +using System.Threading.Tasks; + +namespace Ryujinx.Ava.UI.Views.Input +{ + public partial class RumbleInputView : UserControl + { + private readonly RumbleInputViewModel _viewModel; + + public RumbleInputView() + { + InitializeComponent(); + } + + public RumbleInputView(ControllerInputViewModel viewModel) + { + var config = viewModel.Config; + + _viewModel = new RumbleInputViewModel + { + StrongRumble = config.StrongRumble, + WeakRumble = config.WeakRumble, + }; + + InitializeComponent(); + + DataContext = _viewModel; + } + + public static async Task Show(ControllerInputViewModel viewModel) + { + RumbleInputView content = new(viewModel); + + ContentDialog contentDialog = new() + { + Title = LocaleManager.Instance[LocaleKeys.ControllerRumbleTitle], + PrimaryButtonText = LocaleManager.Instance[LocaleKeys.ControllerSettingsSave], + SecondaryButtonText = string.Empty, + CloseButtonText = LocaleManager.Instance[LocaleKeys.ControllerSettingsClose], + Content = content, + }; + + contentDialog.PrimaryButtonClick += (sender, args) => + { + var config = viewModel.Config; + config.StrongRumble = content._viewModel.StrongRumble; + config.WeakRumble = content._viewModel.WeakRumble; + }; + + await contentDialog.ShowAsync(); + } + } +} diff --git a/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml new file mode 100644 index 000000000..6cf76cf49 --- /dev/null +++ b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml @@ -0,0 +1,297 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs new file mode 100644 index 000000000..41b27e9c1 --- /dev/null +++ b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs @@ -0,0 +1,222 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Threading; +using Gommon; +using LibHac.Ncm; +using LibHac.Tools.FsSystem.NcaUtils; +using Ryujinx.Ava.Common.Locale; +using Ryujinx.Ava.UI.Helpers; +using Ryujinx.Ava.UI.ViewModels; +using Ryujinx.Ava.UI.Windows; +using Ryujinx.Common; +using Ryujinx.Common.Utilities; +using Ryujinx.UI.App.Common; +using Ryujinx.UI.Common; +using Ryujinx.UI.Common.Configuration; +using Ryujinx.UI.Common.Helper; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace Ryujinx.Ava.UI.Views.Main +{ + public partial class MainMenuBarView : UserControl + { + public MainWindow Window { get; private set; } + public MainWindowViewModel ViewModel { get; private set; } + + public MainMenuBarView() + { + InitializeComponent(); + + RyuLogo.IsVisible = !ConfigurationState.Instance.ShowTitleBar; + + ToggleFileTypesMenuItem.ItemsSource = GenerateToggleFileTypeItems(); + ChangeLanguageMenuItem.ItemsSource = GenerateLanguageMenuItems(); + } + + private CheckBox[] GenerateToggleFileTypeItems() => + Enum.GetValues() + .Select(it => (FileName: Enum.GetName(it)!, FileType: it)) + .Select(it => + new CheckBox + { + Content = $".{it.FileName}", + IsChecked = it.FileType.GetConfigValue(ConfigurationState.Instance.UI.ShownFileTypes), + Command = MiniCommand.Create(() => Window.ToggleFileType(it.FileName)) + } + ).ToArray(); + + private static MenuItem[] GenerateLanguageMenuItems() + { + List menuItems = new(); + + string localePath = "Ryujinx/Assets/Locales"; + string localeExt = ".json"; + + string[] localesPath = EmbeddedResources.GetAllAvailableResources(localePath, localeExt); + + Array.Sort(localesPath); + + foreach (string locale in localesPath) + { + string languageCode = Path.GetFileNameWithoutExtension(locale).Split('.').Last(); + string languageJson = EmbeddedResources.ReadAllText($"{localePath}/{languageCode}{localeExt}"); + var strings = JsonHelper.Deserialize(languageJson, CommonJsonContext.Default.StringDictionary); + + if (!strings.TryGetValue("Language", out string languageName)) + { + languageName = languageCode; + } + + MenuItem menuItem = new() + { + Padding = new Thickness(10, 0, 0, 0), + Header = " " + languageName, + Command = MiniCommand.Create(() => MainWindowViewModel.ChangeLanguage(languageCode)) + }; + + menuItems.Add(menuItem); + } + + return menuItems.ToArray(); + } + + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + + if (VisualRoot is MainWindow window) + { + Window = window; + DataContext = ViewModel = window.ViewModel; + } + } + + private async void StopEmulation_Click(object sender, RoutedEventArgs e) + { + await ViewModel.AppHost?.ShowExitPrompt().OrCompleted()!; + } + + private void PauseEmulation_Click(object sender, RoutedEventArgs e) + { + ViewModel.AppHost?.Pause(); + } + + private void ResumeEmulation_Click(object sender, RoutedEventArgs e) + { + ViewModel.AppHost?.Resume(); + } + + public async void OpenSettings(object sender, RoutedEventArgs e) + { + Window.SettingsWindow = new(Window.VirtualFileSystem, Window.ContentManager); + + await Window.SettingsWindow.ShowDialog(Window); + + Window.SettingsWindow = null; + + ViewModel.LoadConfigurableHotKeys(); + } + + public async void OpenMiiApplet(object sender, RoutedEventArgs e) + { + string contentPath = ViewModel.ContentManager.GetInstalledContentPath(0x0100000000001009, StorageId.BuiltInSystem, NcaContentType.Program); + + if (!string.IsNullOrEmpty(contentPath)) + { + ApplicationData applicationData = new() + { + Name = "miiEdit", + Id = 0x0100000000001009, + Path = contentPath, + }; + + await ViewModel.LoadApplication(applicationData, ViewModel.IsFullScreen || ViewModel.StartGamesInFullscreen); + } + } + + public async void OpenAmiiboWindow(object sender, RoutedEventArgs e) + => await ViewModel.OpenAmiiboWindow(); + + public async void OpenCheatManagerForCurrentApp(object sender, RoutedEventArgs e) + { + if (!ViewModel.IsGameRunning) + return; + + string name = ViewModel.AppHost.Device.Processes.ActiveApplication.ApplicationControlProperties.Title[(int)ViewModel.AppHost.Device.System.State.DesiredTitleLanguage].NameString.ToString(); + + await new CheatWindow( + Window.VirtualFileSystem, + ViewModel.AppHost.Device.Processes.ActiveApplication.ProgramIdText, + name, + ViewModel.SelectedApplication.Path).ShowDialog(Window); + + ViewModel.AppHost.Device.EnableCheats(); + } + + private void ScanAmiiboMenuItem_AttachedToVisualTree(object sender, VisualTreeAttachmentEventArgs e) + { + if (sender is MenuItem) + ViewModel.IsAmiiboRequested = ViewModel.AppHost.Device.System.SearchingForAmiibo(out _); + } + + private async void InstallFileTypes_Click(object sender, RoutedEventArgs e) + { + ViewModel.AreMimeTypesRegistered = FileAssociationHelper.Install(); + if (ViewModel.AreMimeTypesRegistered) + await ContentDialogHelper.CreateInfoDialog(LocaleManager.Instance[LocaleKeys.DialogInstallFileTypesSuccessMessage], string.Empty, LocaleManager.Instance[LocaleKeys.InputDialogOk], string.Empty, string.Empty); + else + await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogInstallFileTypesErrorMessage]); + } + + private async void UninstallFileTypes_Click(object sender, RoutedEventArgs e) + { + ViewModel.AreMimeTypesRegistered = !FileAssociationHelper.Uninstall(); + if (!ViewModel.AreMimeTypesRegistered) + await ContentDialogHelper.CreateInfoDialog(LocaleManager.Instance[LocaleKeys.DialogUninstallFileTypesSuccessMessage], string.Empty, LocaleManager.Instance[LocaleKeys.InputDialogOk], string.Empty, string.Empty); + else + await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogUninstallFileTypesErrorMessage]); + } + + private async void ChangeWindowSize_Click(object sender, RoutedEventArgs e) + { + if (sender is not MenuItem { Tag: string resolution }) + return; + + (int resolutionWidth, int resolutionHeight) = resolution.Split(' ', 2) + .Into(parts => + (int.Parse(parts[0]), int.Parse(parts[1])) + ); + + // Correctly size window when 'TitleBar' is enabled (Nov. 14, 2024) + double barsHeight = ((Window.StatusBarHeight + Window.MenuBarHeight) + + (ConfigurationState.Instance.ShowTitleBar ? (int)Window.TitleBar.Height : 0)); + + double windowWidthScaled = (resolutionWidth * Program.WindowScaleFactor); + double windowHeightScaled = ((resolutionHeight + barsHeight) * Program.WindowScaleFactor); + + await Dispatcher.UIThread.InvokeAsync(() => + { + + ViewModel.WindowState = WindowState.Normal; + + Window.Arrange(new Rect(Window.Position.X, Window.Position.Y, windowWidthScaled, windowHeightScaled)); + }); + } + + public async void CheckForUpdates(object sender, RoutedEventArgs e) + { + if (Updater.CanUpdate(true)) + await Window.BeginUpdateAsync(true); + } + + public async void OpenXCITrimmerWindow(object sender, RoutedEventArgs e) => await XCITrimmerWindow.Show(ViewModel); + + public async void OpenAboutWindow(object sender, RoutedEventArgs e) => await AboutWindow.Show(); + + public void CloseWindow(object sender, RoutedEventArgs e) => Window.Close(); + } +} diff --git a/src/Ryujinx/UI/Views/Main/MainStatusBarView.axaml b/src/Ryujinx/UI/Views/Main/MainStatusBarView.axaml new file mode 100644 index 000000000..597cf10e1 --- /dev/null +++ b/src/Ryujinx/UI/Views/Main/MainStatusBarView.axaml @@ -0,0 +1,345 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Ryujinx/UI/Views/Main/MainStatusBarView.axaml.cs b/src/Ryujinx/UI/Views/Main/MainStatusBarView.axaml.cs new file mode 100644 index 000000000..dd4ed8297 --- /dev/null +++ b/src/Ryujinx/UI/Views/Main/MainStatusBarView.axaml.cs @@ -0,0 +1,75 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Threading; +using Ryujinx.Ava.Common.Locale; +using Ryujinx.Ava.UI.Windows; +using Ryujinx.Common; +using Ryujinx.Common.Configuration; +using Ryujinx.Common.Logging; +using Ryujinx.UI.Common.Configuration; +using System; + +namespace Ryujinx.Ava.UI.Views.Main +{ + public partial class MainStatusBarView : UserControl + { + public MainWindow Window; + + public MainStatusBarView() + { + InitializeComponent(); + } + + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + + if (VisualRoot is MainWindow window) + { + Window = window; + DataContext = window.ViewModel; + LocaleManager.Instance.LocaleChanged += () => Dispatcher.UIThread.Post(() => + { + if (Window.ViewModel.EnableNonGameRunningControls) + Window.LoadApplications(); + }); + } + } + + private void VSyncMode_PointerReleased(object sender, PointerReleasedEventArgs e) + { + Window.ViewModel.ToggleVSyncMode(); + Logger.Info?.Print(LogClass.Application, $"VSync Mode toggled to: {Window.ViewModel.AppHost.Device.VSyncMode}"); + } + + private void DockedStatus_PointerReleased(object sender, PointerReleasedEventArgs e) + { + ConfigurationState.Instance.System.EnableDockedMode.Toggle(); + } + + private void AspectRatioStatus_OnClick(object sender, RoutedEventArgs e) + { + AspectRatio aspectRatio = ConfigurationState.Instance.Graphics.AspectRatio.Value; + ConfigurationState.Instance.Graphics.AspectRatio.Value = (int)aspectRatio + 1 > Enum.GetNames(typeof(AspectRatio)).Length - 1 ? AspectRatio.Fixed4x3 : aspectRatio + 1; + } + + private void Refresh_OnClick(object sender, RoutedEventArgs e) => Window.LoadApplications(); + + private void VolumeStatus_OnPointerWheelChanged(object sender, PointerWheelEventArgs e) + { + // Change the volume by 5% at a time + float newValue = Window.ViewModel.Volume + (float)e.Delta.Y * 0.05f; + + Window.ViewModel.Volume = newValue switch + { + < 0 => 0, + > 1 => 1, + _ => newValue, + }; + + e.Handled = true; + } + } +} diff --git a/src/Ryujinx/UI/Views/Main/MainViewControls.axaml b/src/Ryujinx/UI/Views/Main/MainViewControls.axaml new file mode 100644 index 000000000..1c6895db1 --- /dev/null +++ b/src/Ryujinx/UI/Views/Main/MainViewControls.axaml @@ -0,0 +1,177 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Ryujinx/UI/Views/Main/MainViewControls.axaml.cs b/src/Ryujinx/UI/Views/Main/MainViewControls.axaml.cs new file mode 100644 index 000000000..d5f7fbd1c --- /dev/null +++ b/src/Ryujinx/UI/Views/Main/MainViewControls.axaml.cs @@ -0,0 +1,49 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Interactivity; +using Ryujinx.Ava.Common; +using Ryujinx.Ava.UI.ViewModels; +using Ryujinx.Ava.UI.Windows; +using System; + +namespace Ryujinx.Ava.UI.Views.Main +{ + public partial class MainViewControls : UserControl + { + public MainWindowViewModel ViewModel; + + public MainViewControls() + { + InitializeComponent(); + } + + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + + if (VisualRoot is MainWindow window) + { + DataContext = ViewModel = window.ViewModel; + } + + } + + public void Sort_Checked(object sender, RoutedEventArgs args) + { + if (sender is RadioButton { Tag: string sortStrategy }) + ViewModel.Sort(Enum.Parse(sortStrategy)); + } + + public void Order_Checked(object sender, RoutedEventArgs args) + { + if (sender is RadioButton { Tag: string sortOrder }) + ViewModel.Sort(sortOrder is not "Descending"); + } + + private void SearchBox_OnKeyUp(object sender, KeyEventArgs e) + { + ViewModel.SearchText = SearchBox.Text; + } + } +} diff --git a/src/Ryujinx/UI/Views/Settings/SettingsAudioView.axaml b/src/Ryujinx/UI/Views/Settings/SettingsAudioView.axaml new file mode 100644 index 000000000..2f9ae65a0 --- /dev/null +++ b/src/Ryujinx/UI/Views/Settings/SettingsAudioView.axaml @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Ryujinx/UI/Views/Settings/SettingsAudioView.axaml.cs b/src/Ryujinx/UI/Views/Settings/SettingsAudioView.axaml.cs new file mode 100644 index 000000000..b672a0f29 --- /dev/null +++ b/src/Ryujinx/UI/Views/Settings/SettingsAudioView.axaml.cs @@ -0,0 +1,12 @@ +using Avalonia.Controls; + +namespace Ryujinx.Ava.UI.Views.Settings +{ + public partial class SettingsAudioView : UserControl + { + public SettingsAudioView() + { + InitializeComponent(); + } + } +} diff --git a/src/Ryujinx/UI/Views/Settings/SettingsCPUView.axaml b/src/Ryujinx/UI/Views/Settings/SettingsCPUView.axaml new file mode 100644 index 000000000..c7f03a45d --- /dev/null +++ b/src/Ryujinx/UI/Views/Settings/SettingsCPUView.axaml @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Ryujinx/UI/Views/Settings/SettingsCPUView.axaml.cs b/src/Ryujinx/UI/Views/Settings/SettingsCPUView.axaml.cs new file mode 100644 index 000000000..a475971a1 --- /dev/null +++ b/src/Ryujinx/UI/Views/Settings/SettingsCPUView.axaml.cs @@ -0,0 +1,12 @@ +using Avalonia.Controls; + +namespace Ryujinx.Ava.UI.Views.Settings +{ + public partial class SettingsCPUView : UserControl + { + public SettingsCPUView() + { + InitializeComponent(); + } + } +} diff --git a/src/Ryujinx/UI/Views/Settings/SettingsGraphicsView.axaml b/src/Ryujinx/UI/Views/Settings/SettingsGraphicsView.axaml new file mode 100644 index 000000000..219efcef8 --- /dev/null +++ b/src/Ryujinx/UI/Views/Settings/SettingsGraphicsView.axaml @@ -0,0 +1,304 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Ryujinx/UI/Views/Settings/SettingsGraphicsView.axaml.cs b/src/Ryujinx/UI/Views/Settings/SettingsGraphicsView.axaml.cs new file mode 100644 index 000000000..673413309 --- /dev/null +++ b/src/Ryujinx/UI/Views/Settings/SettingsGraphicsView.axaml.cs @@ -0,0 +1,12 @@ +using Avalonia.Controls; + +namespace Ryujinx.Ava.UI.Views.Settings +{ + public partial class SettingsGraphicsView : UserControl + { + public SettingsGraphicsView() + { + InitializeComponent(); + } + } +} diff --git a/src/Ryujinx/UI/Views/Settings/SettingsHotkeysView.axaml b/src/Ryujinx/UI/Views/Settings/SettingsHotkeysView.axaml new file mode 100644 index 000000000..da0957e02 --- /dev/null +++ b/src/Ryujinx/UI/Views/Settings/SettingsHotkeysView.axaml @@ -0,0 +1,121 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Ryujinx/UI/Views/Settings/SettingsHotkeysView.axaml.cs b/src/Ryujinx/UI/Views/Settings/SettingsHotkeysView.axaml.cs new file mode 100644 index 000000000..609f61633 --- /dev/null +++ b/src/Ryujinx/UI/Views/Settings/SettingsHotkeysView.axaml.cs @@ -0,0 +1,150 @@ +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.LogicalTree; +using Ryujinx.Ava.Input; +using Ryujinx.Ava.UI.Helpers; +using Ryujinx.Ava.UI.ViewModels; +using Ryujinx.Input; +using Ryujinx.Input.Assigner; +using Key = Ryujinx.Common.Configuration.Hid.Key; + +namespace Ryujinx.Ava.UI.Views.Settings +{ + public partial class SettingsHotkeysView : UserControl + { + private ButtonKeyAssigner _currentAssigner; + private readonly IGamepadDriver _avaloniaKeyboardDriver; + + public SettingsHotkeysView() + { + InitializeComponent(); + + foreach (ILogical visual in SettingButtons.GetLogicalDescendants()) + { + if (visual is ToggleButton button and not CheckBox) + { + button.IsCheckedChanged += Button_IsCheckedChanged; + } + } + + _avaloniaKeyboardDriver = new AvaloniaKeyboardDriver(this); + } + + protected override void OnPointerReleased(PointerReleasedEventArgs e) + { + base.OnPointerReleased(e); + + if (!_currentAssigner?.ToggledButton?.IsPointerOver ?? false) + { + _currentAssigner.Cancel(); + } + } + + private void MouseClick(object sender, PointerPressedEventArgs e) + { + bool shouldUnbind = e.GetCurrentPoint(this).Properties.IsMiddleButtonPressed; + + _currentAssigner?.Cancel(shouldUnbind); + + PointerPressed -= MouseClick; + } + + private void Button_IsCheckedChanged(object sender, RoutedEventArgs e) + { + if (sender is ToggleButton button) + { + if ((bool)button.IsChecked) + { + if (_currentAssigner != null && button == _currentAssigner.ToggledButton) + { + return; + } + + if (_currentAssigner == null) + { + _currentAssigner = new ButtonKeyAssigner(button); + + this.Focus(NavigationMethod.Pointer); + + PointerPressed += MouseClick; + + var keyboard = (IKeyboard)_avaloniaKeyboardDriver.GetGamepad("0"); + IButtonAssigner assigner = new KeyboardKeyAssigner(keyboard); + + _currentAssigner.ButtonAssigned += (sender, e) => + { + if (e.ButtonValue.HasValue) + { + var viewModel = (DataContext) as SettingsViewModel; + var buttonValue = e.ButtonValue.Value; + + switch (button.Name) + { + case "ToggleVSyncMode": + viewModel.KeyboardHotkey.ToggleVSyncMode = buttonValue.AsHidType(); + break; + case "Screenshot": + viewModel.KeyboardHotkey.Screenshot = buttonValue.AsHidType(); + break; + case "ShowUI": + viewModel.KeyboardHotkey.ShowUI = buttonValue.AsHidType(); + break; + case "Pause": + viewModel.KeyboardHotkey.Pause = buttonValue.AsHidType(); + break; + case "ToggleMute": + viewModel.KeyboardHotkey.ToggleMute = buttonValue.AsHidType(); + break; + case "ResScaleUp": + viewModel.KeyboardHotkey.ResScaleUp = buttonValue.AsHidType(); + break; + case "ResScaleDown": + viewModel.KeyboardHotkey.ResScaleDown = buttonValue.AsHidType(); + break; + case "VolumeUp": + viewModel.KeyboardHotkey.VolumeUp = buttonValue.AsHidType(); + break; + case "VolumeDown": + viewModel.KeyboardHotkey.VolumeDown = buttonValue.AsHidType(); + break; + case "CustomVSyncIntervalIncrement": + viewModel.KeyboardHotkey.CustomVSyncIntervalIncrement = buttonValue.AsHidType(); + break; + case "CustomVSyncIntervalDecrement": + viewModel.KeyboardHotkey.CustomVSyncIntervalDecrement = buttonValue.AsHidType(); + break; + } + } + }; + + _currentAssigner.GetInputAndAssign(assigner, keyboard); + } + else + { + if (_currentAssigner != null) + { + _currentAssigner.Cancel(); + _currentAssigner = null; + button.IsChecked = false; + } + } + } + else + { + _currentAssigner?.Cancel(); + _currentAssigner = null; + } + } + } + + public void Dispose() + { + _currentAssigner?.Cancel(); + _currentAssigner = null; + + _avaloniaKeyboardDriver.Dispose(); + } + } +} diff --git a/src/Ryujinx/UI/Views/Settings/SettingsInputView.axaml b/src/Ryujinx/UI/Views/Settings/SettingsInputView.axaml new file mode 100644 index 000000000..b0edc51a5 --- /dev/null +++ b/src/Ryujinx/UI/Views/Settings/SettingsInputView.axaml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Ryujinx/UI/Views/Settings/SettingsInputView.axaml.cs b/src/Ryujinx/UI/Views/Settings/SettingsInputView.axaml.cs new file mode 100644 index 000000000..55b69af06 --- /dev/null +++ b/src/Ryujinx/UI/Views/Settings/SettingsInputView.axaml.cs @@ -0,0 +1,17 @@ +using Avalonia.Controls; + +namespace Ryujinx.Ava.UI.Views.Settings +{ + public partial class SettingsInputView : UserControl + { + public SettingsInputView() + { + InitializeComponent(); + } + + public void Dispose() + { + InputView.Dispose(); + } + } +} diff --git a/src/Ryujinx/UI/Views/Settings/SettingsLoggingView.axaml b/src/Ryujinx/UI/Views/Settings/SettingsLoggingView.axaml new file mode 100644 index 000000000..7ab49c0ae --- /dev/null +++ b/src/Ryujinx/UI/Views/Settings/SettingsLoggingView.axaml @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Ryujinx/UI/Views/Settings/SettingsLoggingView.axaml.cs b/src/Ryujinx/UI/Views/Settings/SettingsLoggingView.axaml.cs new file mode 100644 index 000000000..c8df46b38 --- /dev/null +++ b/src/Ryujinx/UI/Views/Settings/SettingsLoggingView.axaml.cs @@ -0,0 +1,12 @@ +using Avalonia.Controls; + +namespace Ryujinx.Ava.UI.Views.Settings +{ + public partial class SettingsLoggingView : UserControl + { + public SettingsLoggingView() + { + InitializeComponent(); + } + } +} diff --git a/src/Ryujinx/UI/Views/Settings/SettingsNetworkView.axaml b/src/Ryujinx/UI/Views/Settings/SettingsNetworkView.axaml new file mode 100644 index 000000000..2fc59f04d --- /dev/null +++ b/src/Ryujinx/UI/Views/Settings/SettingsNetworkView.axaml @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Ryujinx/UI/Views/Settings/SettingsNetworkView.axaml.cs b/src/Ryujinx/UI/Views/Settings/SettingsNetworkView.axaml.cs new file mode 100644 index 000000000..c69307522 --- /dev/null +++ b/src/Ryujinx/UI/Views/Settings/SettingsNetworkView.axaml.cs @@ -0,0 +1,29 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; +using Ryujinx.Ava.UI.ViewModels; +using System; + +namespace Ryujinx.Ava.UI.Views.Settings +{ + public partial class SettingsNetworkView : UserControl + { + public SettingsViewModel ViewModel; + + public SettingsNetworkView() + { + InitializeComponent(); + } + + private void GenLdnPassButton_OnClick(object sender, RoutedEventArgs e) + { + byte[] code = new byte[4]; + new Random().NextBytes(code); + ViewModel.LdnPassphrase = $"Ryujinx-{BitConverter.ToUInt32(code):x8}"; + } + + private void ClearLdnPassButton_OnClick(object sender, RoutedEventArgs e) + { + ViewModel.LdnPassphrase = ""; + } + } +} diff --git a/src/Ryujinx/UI/Views/Settings/SettingsSystemView.axaml b/src/Ryujinx/UI/Views/Settings/SettingsSystemView.axaml new file mode 100644 index 000000000..e04e541c3 --- /dev/null +++ b/src/Ryujinx/UI/Views/Settings/SettingsSystemView.axaml @@ -0,0 +1,314 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Ryujinx/UI/Views/Settings/SettingsSystemView.axaml.cs b/src/Ryujinx/UI/Views/Settings/SettingsSystemView.axaml.cs new file mode 100644 index 000000000..2c9eac28c --- /dev/null +++ b/src/Ryujinx/UI/Views/Settings/SettingsSystemView.axaml.cs @@ -0,0 +1,37 @@ +using Avalonia.Controls; +using Ryujinx.Ava.UI.ViewModels; +using TimeZone = Ryujinx.Ava.UI.Models.TimeZone; + +namespace Ryujinx.Ava.UI.Views.Settings +{ + public partial class SettingsSystemView : UserControl + { + public SettingsViewModel ViewModel; + + public SettingsSystemView() + { + InitializeComponent(); + } + + private void TimeZoneBox_OnSelectionChanged(object sender, SelectionChangedEventArgs e) + { + if (e.AddedItems != null && e.AddedItems.Count > 0) + { + if (e.AddedItems[0] is TimeZone timeZone) + { + e.Handled = true; + + ViewModel.ValidateAndSetTimeZone(timeZone.Location); + } + } + } + + private void TimeZoneBox_OnTextChanged(object sender, TextChangedEventArgs e) + { + if (sender is AutoCompleteBox box && box.SelectedItem is TimeZone timeZone) + { + ViewModel.ValidateAndSetTimeZone(timeZone.Location); + } + } + } +} diff --git a/src/Ryujinx/UI/Views/Settings/SettingsUIView.axaml b/src/Ryujinx/UI/Views/Settings/SettingsUIView.axaml new file mode 100644 index 000000000..3f23a75fd --- /dev/null +++ b/src/Ryujinx/UI/Views/Settings/SettingsUIView.axaml @@ -0,0 +1,191 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Ryujinx/UI/Views/Settings/SettingsUIView.axaml.cs b/src/Ryujinx/UI/Views/Settings/SettingsUIView.axaml.cs new file mode 100644 index 000000000..3532e1855 --- /dev/null +++ b/src/Ryujinx/UI/Views/Settings/SettingsUIView.axaml.cs @@ -0,0 +1,109 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Platform.Storage; +using Avalonia.VisualTree; +using Ryujinx.Ava.UI.ViewModels; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace Ryujinx.Ava.UI.Views.Settings +{ + public partial class SettingsUiView : UserControl + { + public SettingsViewModel ViewModel; + + public SettingsUiView() + { + InitializeComponent(); + ShowTitleBarBox.IsVisible = OperatingSystem.IsWindows(); + } + + private async void AddGameDirButton_OnClick(object sender, RoutedEventArgs e) + { + string path = GameDirPathBox.Text; + + if (!string.IsNullOrWhiteSpace(path) && Directory.Exists(path) && !ViewModel.GameDirectories.Contains(path)) + { + ViewModel.GameDirectories.Add(path); + ViewModel.GameDirectoryChanged = true; + } + else + { + if (this.GetVisualRoot() is Window window) + { + var result = await window.StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions + { + AllowMultiple = false, + }); + + if (result.Count > 0) + { + ViewModel.GameDirectories.Add(result[0].Path.LocalPath); + ViewModel.GameDirectoryChanged = true; + } + } + } + } + + private void RemoveGameDirButton_OnClick(object sender, RoutedEventArgs e) + { + int oldIndex = GameDirsList.SelectedIndex; + + foreach (string path in new List(GameDirsList.SelectedItems.Cast())) + { + ViewModel.GameDirectories.Remove(path); + ViewModel.GameDirectoryChanged = true; + } + + if (GameDirsList.ItemCount > 0) + { + GameDirsList.SelectedIndex = oldIndex < GameDirsList.ItemCount ? oldIndex : 0; + } + } + + private async void AddAutoloadDirButton_OnClick(object sender, RoutedEventArgs e) + { + string path = AutoloadDirPathBox.Text; + + if (!string.IsNullOrWhiteSpace(path) && Directory.Exists(path) && !ViewModel.AutoloadDirectories.Contains(path)) + { + ViewModel.AutoloadDirectories.Add(path); + ViewModel.AutoloadDirectoryChanged = true; + } + else + { + if (this.GetVisualRoot() is Window window) + { + var result = await window.StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions + { + AllowMultiple = false, + }); + + if (result.Count > 0) + { + ViewModel.AutoloadDirectories.Add(result[0].Path.LocalPath); + ViewModel.AutoloadDirectoryChanged = true; + } + } + } + } + + private void RemoveAutoloadDirButton_OnClick(object sender, RoutedEventArgs e) + { + int oldIndex = AutoloadDirsList.SelectedIndex; + + foreach (string path in new List(AutoloadDirsList.SelectedItems.Cast())) + { + ViewModel.AutoloadDirectories.Remove(path); + ViewModel.AutoloadDirectoryChanged = true; + } + + if (AutoloadDirsList.ItemCount > 0) + { + AutoloadDirsList.SelectedIndex = oldIndex < AutoloadDirsList.ItemCount ? oldIndex : 0; + } + } + } +} diff --git a/src/Ryujinx/UI/Views/User/UserEditorView.axaml b/src/Ryujinx/UI/Views/User/UserEditorView.axaml new file mode 100644 index 000000000..7a4af4823 --- /dev/null +++ b/src/Ryujinx/UI/Views/User/UserEditorView.axaml @@ -0,0 +1,122 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Ryujinx/UI/Views/User/UserProfileImageSelectorView.axaml.cs b/src/Ryujinx/UI/Views/User/UserProfileImageSelectorView.axaml.cs new file mode 100644 index 000000000..dba762972 --- /dev/null +++ b/src/Ryujinx/UI/Views/User/UserProfileImageSelectorView.axaml.cs @@ -0,0 +1,119 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Platform.Storage; +using Avalonia.VisualTree; +using FluentAvalonia.UI.Controls; +using FluentAvalonia.UI.Navigation; +using Ryujinx.Ava.Common.Locale; +using Ryujinx.Ava.UI.Controls; +using Ryujinx.Ava.UI.Models; +using Ryujinx.Ava.UI.ViewModels; +using Ryujinx.HLE.FileSystem; +using SkiaSharp; +using System.Collections.Generic; +using System.IO; + +namespace Ryujinx.Ava.UI.Views.User +{ + public partial class UserProfileImageSelectorView : UserControl + { + private ContentManager _contentManager; + private NavigationDialogHost _parent; + private TempProfile _profile; + + internal UserProfileImageSelectorViewModel ViewModel { get; private set; } + + public UserProfileImageSelectorView() + { + InitializeComponent(); + AddHandler(Frame.NavigatedToEvent, (s, e) => + { + NavigatedTo(e); + }, RoutingStrategies.Direct); + } + + private void NavigatedTo(NavigationEventArgs arg) + { + if (Program.PreviewerDetached) + { + switch (arg.NavigationMode) + { + case NavigationMode.New: + (_parent, _profile) = ((NavigationDialogHost, TempProfile))arg.Parameter; + _contentManager = _parent.ContentManager; + + ((ContentDialog)_parent.Parent).Title = $"{LocaleManager.Instance[LocaleKeys.UserProfileWindowTitle]} - {LocaleManager.Instance[LocaleKeys.ProfileImageSelectionHeader]}"; + + if (Program.PreviewerDetached) + { + DataContext = ViewModel = new UserProfileImageSelectorViewModel(); + ViewModel.FirmwareFound = _contentManager.GetCurrentFirmwareVersion() != null; + } + + break; + case NavigationMode.Back: + if (_profile.Image != null) + { + _parent.GoBack(); + } + break; + } + } + } + + private async void Import_OnClick(object sender, RoutedEventArgs e) + { + var result = await ((Window)this.GetVisualRoot()!).StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions + { + AllowMultiple = false, + FileTypeFilter = new List + { + new(LocaleManager.Instance[LocaleKeys.AllSupportedFormats]) + { + Patterns = new[] { "*.jpg", "*.jpeg", "*.png", "*.bmp" }, + AppleUniformTypeIdentifiers = new[] { "public.jpeg", "public.png", "com.microsoft.bmp" }, + MimeTypes = new[] { "image/jpeg", "image/png", "image/bmp" }, + }, + }, + }); + + if (result.Count > 0) + { + _profile.Image = ProcessProfileImage(File.ReadAllBytes(result[0].Path.LocalPath)); + _parent.GoBack(); + } + } + + private void GoBack(object sender, RoutedEventArgs e) + { + _parent.GoBack(); + } + + private void SelectFirmwareImage_OnClick(object sender, RoutedEventArgs e) + { + if (ViewModel.FirmwareFound) + { + _parent.Navigate(typeof(UserFirmwareAvatarSelectorView), (_parent, _profile)); + } + } + + private static byte[] ProcessProfileImage(byte[] buffer) + { + using var bitmap = SKBitmap.Decode(buffer); + + var resizedBitmap = bitmap.Resize(new SKImageInfo(256, 256), SKFilterQuality.High); + + using var streamJpg = new MemoryStream(); + + if (resizedBitmap != null) + { + using var image = SKImage.FromBitmap(resizedBitmap); + using var dataJpeg = image.Encode(SKEncodedImageFormat.Jpeg, 100); + + dataJpeg.SaveTo(streamJpg); + } + + return streamJpg.ToArray(); + } + } +} diff --git a/src/Ryujinx/UI/Views/User/UserRecovererView.axaml b/src/Ryujinx/UI/Views/User/UserRecovererView.axaml new file mode 100644 index 000000000..f49444642 --- /dev/null +++ b/src/Ryujinx/UI/Views/User/UserRecovererView.axaml @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Ryujinx/UI/Views/User/UserRecovererView.axaml.cs b/src/Ryujinx/UI/Views/User/UserRecovererView.axaml.cs new file mode 100644 index 000000000..31934349d --- /dev/null +++ b/src/Ryujinx/UI/Views/User/UserRecovererView.axaml.cs @@ -0,0 +1,51 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; +using FluentAvalonia.UI.Controls; +using FluentAvalonia.UI.Navigation; +using Ryujinx.Ava.Common.Locale; +using Ryujinx.Ava.UI.Controls; + +namespace Ryujinx.Ava.UI.Views.User +{ + public partial class UserRecovererView : UserControl + { + private NavigationDialogHost _parent; + + public UserRecovererView() + { + InitializeComponent(); + AddHandler(Frame.NavigatedToEvent, (s, e) => + { + NavigatedTo(e); + }, RoutingStrategies.Direct); + } + + private void NavigatedTo(NavigationEventArgs arg) + { + if (Program.PreviewerDetached) + { + switch (arg.NavigationMode) + { + case NavigationMode.New: + var parent = (NavigationDialogHost)arg.Parameter; + + _parent = parent; + + ((ContentDialog)_parent.Parent).Title = $"{LocaleManager.Instance[LocaleKeys.UserProfileWindowTitle]} - {LocaleManager.Instance[LocaleKeys.UserProfilesRecoverHeading]}"; + + break; + } + } + } + + private void GoBack(object sender, RoutedEventArgs e) + { + _parent?.GoBack(); + } + + private void Recover(object sender, RoutedEventArgs e) + { + _parent?.RecoverLostAccounts(); + } + } +} diff --git a/src/Ryujinx/UI/Views/User/UserSaveManagerView.axaml b/src/Ryujinx/UI/Views/User/UserSaveManagerView.axaml new file mode 100644 index 000000000..e43c39dfa --- /dev/null +++ b/src/Ryujinx/UI/Views/User/UserSaveManagerView.axaml @@ -0,0 +1,213 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Ryujinx/UI/Views/User/UserSaveManagerView.axaml.cs b/src/Ryujinx/UI/Views/User/UserSaveManagerView.axaml.cs new file mode 100644 index 000000000..69986c014 --- /dev/null +++ b/src/Ryujinx/UI/Views/User/UserSaveManagerView.axaml.cs @@ -0,0 +1,149 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Threading; +using FluentAvalonia.UI.Controls; +using FluentAvalonia.UI.Navigation; +using LibHac; +using LibHac.Common; +using LibHac.Fs; +using LibHac.Fs.Shim; +using Ryujinx.Ava.Common; +using Ryujinx.Ava.Common.Locale; +using Ryujinx.Ava.UI.Controls; +using Ryujinx.Ava.UI.Helpers; +using Ryujinx.Ava.UI.Models; +using Ryujinx.Ava.UI.ViewModels; +using Ryujinx.HLE.FileSystem; +using Ryujinx.HLE.HOS.Services.Account.Acc; +using System; +using System.Collections.ObjectModel; +using System.Threading.Tasks; +using Button = Avalonia.Controls.Button; +using UserId = LibHac.Fs.UserId; + +namespace Ryujinx.Ava.UI.Views.User +{ + public partial class UserSaveManagerView : UserControl + { + internal UserSaveManagerViewModel ViewModel { get; private set; } + + private AccountManager _accountManager; + private HorizonClient _horizonClient; + private VirtualFileSystem _virtualFileSystem; + private NavigationDialogHost _parent; + + public UserSaveManagerView() + { + InitializeComponent(); + AddHandler(Frame.NavigatedToEvent, (s, e) => + { + NavigatedTo(e); + }, RoutingStrategies.Direct); + } + + private void NavigatedTo(NavigationEventArgs arg) + { + if (Program.PreviewerDetached) + { + switch (arg.NavigationMode) + { + case NavigationMode.New: + var (parent, accountManager, client, virtualFileSystem) = ((NavigationDialogHost parent, AccountManager accountManager, HorizonClient client, VirtualFileSystem virtualFileSystem))arg.Parameter; + _accountManager = accountManager; + _horizonClient = client; + _virtualFileSystem = virtualFileSystem; + + _parent = parent; + break; + } + + DataContext = ViewModel = new UserSaveManagerViewModel(_accountManager); + ((ContentDialog)_parent.Parent).Title = $"{LocaleManager.Instance[LocaleKeys.UserProfileWindowTitle]} - {ViewModel.SaveManagerHeading}"; + + Task.Run(LoadSaves); + } + } + + public void LoadSaves() + { + ViewModel.Saves.Clear(); + var saves = new ObservableCollection(); + var saveDataFilter = SaveDataFilter.Make( + programId: default, + saveType: SaveDataType.Account, + new UserId((ulong)_accountManager.LastOpenedUser.UserId.High, (ulong)_accountManager.LastOpenedUser.UserId.Low), + saveDataId: default, + index: default); + + using var saveDataIterator = new UniqueRef(); + + _horizonClient.Fs.OpenSaveDataIterator(ref saveDataIterator.Ref, SaveDataSpaceId.User, in saveDataFilter).ThrowIfFailure(); + + Span saveDataInfo = stackalloc SaveDataInfo[10]; + + while (true) + { + saveDataIterator.Get.ReadSaveDataInfo(out long readCount, saveDataInfo).ThrowIfFailure(); + + if (readCount == 0) + { + break; + } + + for (int i = 0; i < readCount; i++) + { + var save = saveDataInfo[i]; + if (save.ProgramId.Value != 0) + { + var saveModel = new SaveModel(save); + saves.Add(saveModel); + } + } + } + + Dispatcher.UIThread.Post(() => + { + ViewModel.Saves = saves; + ViewModel.Sort(); + }); + } + + private void GoBack(object sender, RoutedEventArgs e) + { + _parent?.GoBack(); + } + + private void OpenLocation(object sender, RoutedEventArgs e) + { + if (sender is Button button) + { + if (button.DataContext is SaveModel saveModel) + { + ApplicationHelper.OpenSaveDir(saveModel.SaveId); + } + } + } + + private async void Delete(object sender, RoutedEventArgs e) + { + if (sender is Button button) + { + if (button.DataContext is SaveModel saveModel) + { + var result = await ContentDialogHelper.CreateConfirmationDialog(LocaleManager.Instance[LocaleKeys.DeleteUserSave], + LocaleManager.Instance[LocaleKeys.IrreversibleActionNote], + LocaleManager.Instance[LocaleKeys.InputDialogYes], + LocaleManager.Instance[LocaleKeys.InputDialogNo], + string.Empty); + + if (result == UserResult.Yes) + { + _horizonClient.Fs.DeleteSaveData(SaveDataSpaceId.User, saveModel.SaveId); + ViewModel.Saves.Remove(saveModel); + ViewModel.Sort(); + } + } + } + } + } +} diff --git a/src/Ryujinx/UI/Views/User/UserSelectorView.axaml b/src/Ryujinx/UI/Views/User/UserSelectorView.axaml new file mode 100644 index 000000000..fd6513ae7 --- /dev/null +++ b/src/Ryujinx/UI/Views/User/UserSelectorView.axaml @@ -0,0 +1,162 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Ryujinx/UI/Windows/AboutWindow.axaml.cs b/src/Ryujinx/UI/Windows/AboutWindow.axaml.cs new file mode 100644 index 000000000..3cf709019 --- /dev/null +++ b/src/Ryujinx/UI/Windows/AboutWindow.axaml.cs @@ -0,0 +1,65 @@ +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Layout; +using Avalonia.Styling; +using FluentAvalonia.UI.Controls; +using Ryujinx.Ava.Common.Locale; +using Ryujinx.Ava.UI.Helpers; +using Ryujinx.Ava.UI.ViewModels; +using Ryujinx.Common; +using Ryujinx.UI.Common.Helper; +using System.Threading.Tasks; +using Button = Avalonia.Controls.Button; + +namespace Ryujinx.Ava.UI.Windows +{ + public partial class AboutWindow : UserControl + { + public AboutWindow() + { + DataContext = new AboutWindowViewModel(); + + InitializeComponent(); + + GitHubRepoButton.Tag = + $"https://github.com/{ReleaseInformation.ReleaseChannelOwner}/{ReleaseInformation.ReleaseChannelRepo}"; + } + + public static async Task Show() + { + ContentDialog contentDialog = new() + { + PrimaryButtonText = string.Empty, + SecondaryButtonText = string.Empty, + CloseButtonText = LocaleManager.Instance[LocaleKeys.UserProfilesClose], + Content = new AboutWindow(), + }; + + Style closeButton = new(x => x.Name("CloseButton")); + closeButton.Setters.Add(new Setter(WidthProperty, 80d)); + + Style closeButtonParent = new(x => x.Name("CommandSpace")); + closeButtonParent.Setters.Add(new Setter(HorizontalAlignmentProperty, HorizontalAlignment.Right)); + + contentDialog.Styles.Add(closeButton); + contentDialog.Styles.Add(closeButtonParent); + + await ContentDialogHelper.ShowAsync(contentDialog); + } + + private void Button_OnClick(object sender, RoutedEventArgs e) + { + if (sender is Button { Tag: string url }) + OpenHelper.OpenUrl(url); + } + + private void AmiiboLabel_OnPointerPressed(object sender, PointerPressedEventArgs e) + { + if (sender is TextBlock) + { + OpenHelper.OpenUrl("https://amiiboapi.com"); + } + } + } +} diff --git a/src/Ryujinx/UI/Windows/AmiiboWindow.axaml b/src/Ryujinx/UI/Windows/AmiiboWindow.axaml new file mode 100644 index 000000000..ce410923d --- /dev/null +++ b/src/Ryujinx/UI/Windows/AmiiboWindow.axaml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Ryujinx/UI/Windows/CheatWindow.axaml.cs b/src/Ryujinx/UI/Windows/CheatWindow.axaml.cs new file mode 100644 index 000000000..8c8d56b34 --- /dev/null +++ b/src/Ryujinx/UI/Windows/CheatWindow.axaml.cs @@ -0,0 +1,115 @@ +using Avalonia.Collections; +using LibHac.Tools.FsSystem; +using Ryujinx.Ava.Common.Locale; +using Ryujinx.Ava.UI.Models; +using Ryujinx.HLE.FileSystem; +using Ryujinx.HLE.HOS; +using Ryujinx.UI.App.Common; +using Ryujinx.UI.Common.Configuration; +using System.Globalization; +using System.IO; +using System.Linq; + +namespace Ryujinx.Ava.UI.Windows +{ + public partial class CheatWindow : StyleableAppWindow + { + private readonly string _enabledCheatsPath; + public bool NoCheatsFound { get; } + + public AvaloniaList LoadedCheats { get; } + + public string Heading { get; } + public string BuildId { get; } + + public CheatWindow() + { + DataContext = this; + + InitializeComponent(); + + Title = App.FormatTitle(LocaleKeys.CheatWindowTitle); + } + + public CheatWindow(VirtualFileSystem virtualFileSystem, string titleId, string titleName, string titlePath) + { + LoadedCheats = new AvaloniaList(); + IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks + ? IntegrityCheckLevel.ErrorOnInvalid + : IntegrityCheckLevel.None; + + Heading = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.CheatWindowHeading, titleName, titleId.ToUpper()); + BuildId = ApplicationData.GetBuildId(virtualFileSystem, checkLevel, titlePath); + + InitializeComponent(); + + string modsBasePath = ModLoader.GetModsBasePath(); + string titleModsPath = ModLoader.GetApplicationDir(modsBasePath, titleId); + ulong titleIdValue = ulong.Parse(titleId, NumberStyles.HexNumber); + + _enabledCheatsPath = Path.Combine(titleModsPath, "cheats", "enabled.txt"); + + string[] enabled = []; + + if (File.Exists(_enabledCheatsPath)) + { + enabled = File.ReadAllLines(_enabledCheatsPath); + } + + int cheatAdded = 0; + + var mods = new ModLoader.ModCache(); + + ModLoader.QueryContentsDir(mods, new DirectoryInfo(Path.Combine(modsBasePath, "contents")), titleIdValue); + + string currentCheatFile = string.Empty; + string buildId = string.Empty; + + CheatNode currentGroup = null; + + foreach (var cheat in mods.Cheats) + { + if (cheat.Path.FullName != currentCheatFile) + { + currentCheatFile = cheat.Path.FullName; + string parentPath = currentCheatFile.Replace(titleModsPath, string.Empty); + + buildId = Path.GetFileNameWithoutExtension(currentCheatFile).ToUpper(); + currentGroup = new CheatNode(string.Empty, buildId, parentPath, true); + + LoadedCheats.Add(currentGroup); + } + + var model = new CheatNode(cheat.Name, buildId, string.Empty, false, enabled.Contains($"{buildId}-{cheat.Name}")); + currentGroup?.SubNodes.Add(model); + + cheatAdded++; + } + + if (cheatAdded == 0) + { + NoCheatsFound = true; + } + + DataContext = this; + + Title = App.FormatTitle(LocaleKeys.CheatWindowTitle); + } + + public void Save() + { + if (NoCheatsFound) + return; + + var enabledCheats = LoadedCheats.SelectMany(it => it.SubNodes) + .Where(it => it.IsEnabled) + .Select(it => it.BuildIdKey); + + Directory.CreateDirectory(Path.GetDirectoryName(_enabledCheatsPath)!); + + File.WriteAllLines(_enabledCheatsPath, enabledCheats); + + Close(); + } + } +} diff --git a/src/Ryujinx/UI/Windows/ContentDialogOverlayWindow.axaml b/src/Ryujinx/UI/Windows/ContentDialogOverlayWindow.axaml new file mode 100644 index 000000000..d56dd2a0d --- /dev/null +++ b/src/Ryujinx/UI/Windows/ContentDialogOverlayWindow.axaml @@ -0,0 +1,25 @@ + + + + + + diff --git a/src/Ryujinx/UI/Windows/ContentDialogOverlayWindow.axaml.cs b/src/Ryujinx/UI/Windows/ContentDialogOverlayWindow.axaml.cs new file mode 100644 index 000000000..c5f6b15eb --- /dev/null +++ b/src/Ryujinx/UI/Windows/ContentDialogOverlayWindow.axaml.cs @@ -0,0 +1,20 @@ +using Avalonia.Controls; +using Avalonia.Media; + +namespace Ryujinx.Ava.UI.Windows +{ + public partial class ContentDialogOverlayWindow : StyleableWindow + { + public ContentDialogOverlayWindow() + { + InitializeComponent(); + + TransparencyLevelHint = [WindowTransparencyLevel.Transparent]; + WindowStartupLocation = WindowStartupLocation.Manual; + SystemDecorations = SystemDecorations.None; + ExtendClientAreaTitleBarHeightHint = 0; + Background = Brushes.Transparent; + CanResize = false; + } + } +} diff --git a/src/Ryujinx/UI/Windows/DownloadableContentManagerWindow.axaml b/src/Ryujinx/UI/Windows/DownloadableContentManagerWindow.axaml new file mode 100644 index 000000000..df70f02eb --- /dev/null +++ b/src/Ryujinx/UI/Windows/DownloadableContentManagerWindow.axaml @@ -0,0 +1,201 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Ryujinx/UI/Windows/DownloadableContentManagerWindow.axaml.cs b/src/Ryujinx/UI/Windows/DownloadableContentManagerWindow.axaml.cs new file mode 100644 index 000000000..340515a5b --- /dev/null +++ b/src/Ryujinx/UI/Windows/DownloadableContentManagerWindow.axaml.cs @@ -0,0 +1,103 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Styling; +using FluentAvalonia.UI.Controls; +using Ryujinx.Ava.Common.Locale; +using Ryujinx.Ava.UI.ViewModels; +using Ryujinx.UI.App.Common; +using Ryujinx.UI.Common.Helper; +using Ryujinx.UI.Common.Models; +using System.Threading.Tasks; + +namespace Ryujinx.Ava.UI.Windows +{ + public partial class DownloadableContentManagerWindow : UserControl + { + public DownloadableContentManagerViewModel ViewModel; + + public DownloadableContentManagerWindow() + { + DataContext = this; + + InitializeComponent(); + } + + public DownloadableContentManagerWindow(ApplicationLibrary applicationLibrary, ApplicationData applicationData) + { + DataContext = ViewModel = new DownloadableContentManagerViewModel(applicationLibrary, applicationData); + + InitializeComponent(); + } + + public static async Task Show(ApplicationLibrary applicationLibrary, ApplicationData applicationData) + { + ContentDialog contentDialog = new() + { + PrimaryButtonText = string.Empty, + SecondaryButtonText = string.Empty, + CloseButtonText = string.Empty, + Content = new DownloadableContentManagerWindow(applicationLibrary, applicationData), + Title = string.Format(LocaleManager.Instance[LocaleKeys.DlcWindowTitle], applicationData.Name, applicationData.IdBaseString), + }; + + Style bottomBorder = new(x => x.OfType().Name("DialogSpace").Child().OfType()); + bottomBorder.Setters.Add(new Setter(IsVisibleProperty, false)); + + contentDialog.Styles.Add(bottomBorder); + + await contentDialog.ShowAsync(); + } + + private void SaveAndClose(object sender, RoutedEventArgs routedEventArgs) + { + ViewModel.Save(); + ((ContentDialog)Parent).Hide(); + } + + private void Close(object sender, RoutedEventArgs e) + { + ((ContentDialog)Parent).Hide(); + } + + private void RemoveDLC(object sender, RoutedEventArgs e) + { + if (sender is Button button) + { + if (button.DataContext is DownloadableContentModel model) + { + ViewModel.Remove(model); + } + } + } + + private void OpenLocation(object sender, RoutedEventArgs e) + { + if (sender is Button button) + { + if (button.DataContext is DownloadableContentModel model) + { + OpenHelper.LocateFile(model.ContainerPath); + } + } + } + + private void OnSelectionChanged(object sender, SelectionChangedEventArgs e) + { + foreach (var content in e.AddedItems) + { + if (content is DownloadableContentModel model) + { + ViewModel.Enable(model); + } + } + + foreach (var content in e.RemovedItems) + { + if (content is DownloadableContentModel model) + { + ViewModel.Disable(model); + } + } + } + } +} diff --git a/src/Ryujinx/UI/Windows/IconColorPicker.cs b/src/Ryujinx/UI/Windows/IconColorPicker.cs new file mode 100644 index 000000000..bfa33eb43 --- /dev/null +++ b/src/Ryujinx/UI/Windows/IconColorPicker.cs @@ -0,0 +1,177 @@ +using SkiaSharp; +using System; +using System.Collections.Generic; + +namespace Ryujinx.Ava.UI.Windows +{ + static class IconColorPicker + { + private const int ColorsPerLine = 64; + private const int TotalColors = ColorsPerLine * ColorsPerLine; + + private const int UvQuantBits = 3; + private const int UvQuantShift = BitsPerComponent - UvQuantBits; + + private const int SatQuantBits = 5; + private const int SatQuantShift = BitsPerComponent - SatQuantBits; + + private const int BitsPerComponent = 8; + + private const int CutOffLuminosity = 64; + + private readonly struct PaletteColor(int qck, byte r, byte g, byte b) + { + public int Qck => qck; + public byte R => r; + public byte G => g; + public byte B => b; + } + + public static SKColor GetFilteredColor(SKBitmap image) + { + var color = GetColor(image); + + + // We don't want colors that are too dark. + // If the color is too dark, make it brighter by reducing the range + // and adding a constant color. + int luminosity = GetColorApproximateLuminosity(color.Red, color.Green, color.Blue); + if (luminosity < CutOffLuminosity) + { + color = new SKColor( + (byte)Math.Min(CutOffLuminosity + color.Red, byte.MaxValue), + (byte)Math.Min(CutOffLuminosity + color.Green, byte.MaxValue), + (byte)Math.Min(CutOffLuminosity + color.Blue, byte.MaxValue)); + } + + return color; + } + + public static SKColor GetColor(SKBitmap image) + { + var colors = new PaletteColor[TotalColors]; + var dominantColorBin = new Dictionary(); + + var buffer = GetBuffer(image); + + int i = 0; + int maxHitCount = 0; + + for (int y = 0; y < image.Height; y++) + { + int yOffset = y * image.Width; + + for (int x = 0; x < image.Width && i < TotalColors; x++) + { + int offset = x + yOffset; + + SKColor pixel = buffer[offset]; + byte cr = pixel.Red; + byte cg = pixel.Green; + byte cb = pixel.Blue; + + var qck = GetQuantizedColorKey(cr, cg, cb); + + if (dominantColorBin.TryGetValue(qck, out int hitCount)) + { + dominantColorBin[qck] = hitCount + 1; + + if (maxHitCount < hitCount) + { + maxHitCount = hitCount; + } + } + else + { + dominantColorBin.Add(qck, 1); + } + + colors[i++] = new PaletteColor(qck, cr, cg, cb); + } + } + + int highScore = -1; + PaletteColor bestCandidate = default; + + for (i = 0; i < TotalColors; i++) + { + var score = GetColorScore(dominantColorBin, maxHitCount, colors[i]); + + if (highScore < score) + { + highScore = score; + bestCandidate = colors[i]; + } + } + + return new SKColor(bestCandidate.R, bestCandidate.G, bestCandidate.B); + } + + public static SKColor[] GetBuffer(SKBitmap image) + { + var pixels = new SKColor[image.Width * image.Height]; + + for (int y = 0; y < image.Height; y++) + { + for (int x = 0; x < image.Width; x++) + { + pixels[x + y * image.Width] = image.GetPixel(x, y); + } + } + + return pixels; + } + + private static int GetColorScore(Dictionary dominantColorBin, int maxHitCount, PaletteColor color) + { + var hitCount = dominantColorBin[color.Qck]; + var balancedHitCount = BalanceHitCount(hitCount, maxHitCount); + var quantSat = (GetColorSaturation(color) >> SatQuantShift) << SatQuantShift; + var value = GetColorValue(color); + + // If the color is rarely used on the image, + // then chances are that there's a better candidate, even if the saturation value + // is high. By multiplying the saturation value with a weight, we can lower + // it if the color is almost never used (hit count is low). + var satWeighted = quantSat; + var satWeight = balancedHitCount << 5; + if (satWeight < 0x100) + { + satWeighted = (satWeighted * satWeight) >> 8; + } + + // Compute score from saturation and dominance of the color. + // We prefer more vivid colors over dominant ones, so give more weight to the saturation. + var score = ((satWeighted << 1) + balancedHitCount) * value; + + return score; + } + + private static int GetColorSaturation(PaletteColor color) + { + int cMax = Math.Max(Math.Max(color.R, color.G), color.B); + + if (cMax == 0) + { + return 0; + } + + int cMin = Math.Min(Math.Min(color.R, color.G), color.B); + int delta = cMax - cMin; + return (delta << 8) / cMax; + } + + private static int GetColorValue(PaletteColor color) => Math.Max(Math.Max(color.R, color.G), color.B); + + private static int BalanceHitCount(int hitCount, int maxHitCount) => (hitCount << 8) / maxHitCount; + + private static int GetColorApproximateLuminosity(byte r, byte g, byte b) => (r + g + b) / 3; + + private static int GetQuantizedColorKey(byte r, byte g, byte b) + { + int u = ((-38 * r - 74 * g + 112 * b + 128) >> 8) + 128; + int v = ((112 * r - 94 * g - 18 * b + 128) >> 8) + 128; + return (v >> UvQuantShift) | ((u >> UvQuantShift) << UvQuantBits); + } + } +} diff --git a/src/Ryujinx/UI/Windows/MainWindow.axaml b/src/Ryujinx/UI/Windows/MainWindow.axaml new file mode 100644 index 000000000..cb2e5936d --- /dev/null +++ b/src/Ryujinx/UI/Windows/MainWindow.axaml @@ -0,0 +1,181 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Ryujinx/UI/Windows/MainWindow.axaml.cs b/src/Ryujinx/UI/Windows/MainWindow.axaml.cs new file mode 100644 index 000000000..059f99a60 --- /dev/null +++ b/src/Ryujinx/UI/Windows/MainWindow.axaml.cs @@ -0,0 +1,728 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Interactivity; +using Avalonia.Platform; +using Avalonia.Threading; +using DynamicData; +using FluentAvalonia.UI.Controls; +using FluentAvalonia.UI.Windowing; +using LibHac.Tools.FsSystem; +using Ryujinx.Ava.Common; +using Ryujinx.Ava.Common.Locale; +using Ryujinx.Ava.Input; +using Ryujinx.Ava.UI.Applet; +using Ryujinx.Ava.UI.Helpers; +using Ryujinx.Ava.UI.ViewModels; +using Ryujinx.Common; +using Ryujinx.Common.Logging; +using Ryujinx.Graphics.Gpu; +using Ryujinx.HLE.FileSystem; +using Ryujinx.HLE.HOS; +using Ryujinx.HLE.HOS.Services.Account.Acc; +using Ryujinx.Input.HLE; +using Ryujinx.Input.SDL2; +using Ryujinx.UI.App.Common; +using Ryujinx.UI.Common; +using Ryujinx.UI.Common.Configuration; +using Ryujinx.UI.Common.Helper; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reactive.Linq; +using System.Runtime.Versioning; +using System.Threading; +using System.Threading.Tasks; + +namespace Ryujinx.Ava.UI.Windows +{ + public partial class MainWindow : StyleableAppWindow + { + internal static MainWindowViewModel MainWindowViewModel { get; private set; } + + public MainWindowViewModel ViewModel { get; } + + internal readonly AvaHostUIHandler UiHandler; + + private bool _isLoading; + private bool _applicationsLoadedOnce; + + private UserChannelPersistence _userChannelPersistence; + private static bool _deferLoad; + private static string _launchPath; + private static string _launchApplicationId; + private static bool _startFullscreen; + private IDisposable _appLibraryAppsSubscription; + + public VirtualFileSystem VirtualFileSystem { get; private set; } + public ContentManager ContentManager { get; private set; } + public AccountManager AccountManager { get; private set; } + + public LibHacHorizonManager LibHacHorizonManager { get; private set; } + + public InputManager InputManager { get; private set; } + + public SettingsWindow SettingsWindow { get; set; } + + public static bool ShowKeyErrorOnLoad { get; set; } + public ApplicationLibrary ApplicationLibrary { get; set; } + + // Correctly size window when 'TitleBar' is enabled (Nov. 14, 2024) + public readonly double TitleBarHeight; + + public readonly double StatusBarHeight; + public readonly double MenuBarHeight; + + public MainWindow() + { + DataContext = ViewModel = MainWindowViewModel = new MainWindowViewModel + { + Window = this + }; + + InitializeComponent(); + Load(); + + UiHandler = new AvaHostUIHandler(this); + + ViewModel.Title = App.FormatTitle(); + + TitleBar.ExtendsContentIntoTitleBar = !ConfigurationState.Instance.ShowTitleBar; + TitleBar.TitleBarHitTestType = (ConfigurationState.Instance.ShowTitleBar) ? TitleBarHitTestType.Simple : TitleBarHitTestType.Complex; + + // Correctly size window when 'TitleBar' is enabled (Nov. 14, 2024) + TitleBarHeight = (ConfigurationState.Instance.ShowTitleBar ? TitleBar.Height : 0); + + // NOTE: Height of MenuBar and StatusBar is not usable here, since it would still be 0 at this point. + StatusBarHeight = StatusBarView.StatusBar.MinHeight; + MenuBarHeight = MenuBar.MinHeight; + + SetWindowSizePosition(); + + if (Program.PreviewerDetached) + { + InputManager = new InputManager(new AvaloniaKeyboardDriver(this), new SDL2GamepadDriver()); + + _ = this.GetObservable(IsActiveProperty).Subscribe(it => ViewModel.IsActive = it); + this.ScalingChanged += OnScalingChanged; + } + } + + /// + /// Event handler for detecting OS theme change when using "Follow OS theme" option + /// + private static void OnPlatformColorValuesChanged(object sender, PlatformColorValues e) + { + if (Application.Current is App app) + app.ApplyConfiguredTheme(ConfigurationState.Instance.UI.BaseStyle); + } + + protected override void OnClosed(EventArgs e) + { + base.OnClosed(e); + if (PlatformSettings != null) + { + PlatformSettings.ColorValuesChanged -= OnPlatformColorValuesChanged; + } + } + + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + base.OnApplyTemplate(e); + + NotificationHelper.SetNotificationManager(this); + } + + private void OnScalingChanged(object sender, EventArgs e) + { + Program.DesktopScaleFactor = this.RenderScaling; + } + + private void ApplicationLibrary_ApplicationCountUpdated(object sender, ApplicationCountUpdatedEventArgs e) + { + LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.StatusBarGamesLoaded, e.NumAppsLoaded, e.NumAppsFound); + + Dispatcher.UIThread.Post(() => + { + ViewModel.StatusBarProgressValue = e.NumAppsLoaded; + ViewModel.StatusBarProgressMaximum = e.NumAppsFound; + + if (e.NumAppsFound == 0) + { + StatusBarView.LoadProgressBar.IsVisible = false; + } + + if (e.NumAppsLoaded == e.NumAppsFound) + { + StatusBarView.LoadProgressBar.IsVisible = false; + } + }); + } + + private void ApplicationLibrary_LdnGameDataReceived(object sender, LdnGameDataReceivedEventArgs e) + { + Dispatcher.UIThread.Post(() => + { + var ldnGameDataArray = e.LdnData; + ViewModel.LastLdnGameData = ldnGameDataArray; + foreach (var application in ViewModel.Applications) + { + UpdateApplicationWithLdnData(application); + } + ViewModel.RefreshView(); + }); + } + + private void UpdateApplicationWithLdnData(ApplicationData application) + { + if (application.ControlHolder.ByteSpan.Length > 0 && ViewModel.LastLdnGameData != null) + { + IEnumerable ldnGameData = ViewModel.LastLdnGameData.Where(game => application.ControlHolder.Value.LocalCommunicationId.Items.Contains(Convert.ToUInt64(game.TitleId, 16))); + + application.PlayerCount = ldnGameData.Sum(game => game.PlayerCount); + application.GameCount = ldnGameData.Count(); + } + else + { + application.PlayerCount = 0; + application.GameCount = 0; + } + } + + public void Application_Opened(object sender, ApplicationOpenedEventArgs args) + { + if (args.Application != null) + { + ViewModel.SelectedIcon = args.Application.Icon; + + ViewModel.LoadApplication(args.Application).Wait(); + } + + args.Handled = true; + } + + internal static void DeferLoadApplication(string launchPathArg, string launchApplicationId, bool startFullscreenArg) + { + _deferLoad = true; + _launchPath = launchPathArg; + _launchApplicationId = launchApplicationId; + _startFullscreen = startFullscreenArg; + } + + public void SwitchToGameControl(bool startFullscreen = false) + { + ViewModel.ShowLoadProgress = false; + ViewModel.ShowContent = true; + ViewModel.IsLoadingIndeterminate = false; + + if (startFullscreen && ViewModel.WindowState is not WindowState.FullScreen) + { + ViewModel.ToggleFullscreen(); + } + } + + public void ShowLoading(bool startFullscreen = false) + { + ViewModel.ShowContent = false; + ViewModel.ShowLoadProgress = true; + ViewModel.IsLoadingIndeterminate = true; + + if (startFullscreen && ViewModel.WindowState is not WindowState.FullScreen) + { + ViewModel.ToggleFullscreen(); + } + } + + private void Initialize() + { + _userChannelPersistence = new UserChannelPersistence(); + VirtualFileSystem = VirtualFileSystem.CreateInstance(); + LibHacHorizonManager = new LibHacHorizonManager(); + ContentManager = new ContentManager(VirtualFileSystem); + + LibHacHorizonManager.InitializeFsServer(VirtualFileSystem); + LibHacHorizonManager.InitializeArpServer(); + LibHacHorizonManager.InitializeBcatServer(); + LibHacHorizonManager.InitializeSystemClients(); + + IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks + ? IntegrityCheckLevel.ErrorOnInvalid + : IntegrityCheckLevel.None; + + ApplicationLibrary = new ApplicationLibrary(VirtualFileSystem, checkLevel) + { + DesiredLanguage = ConfigurationState.Instance.System.Language, + }; + + // Save data created before we supported extra data in directory save data will not work properly if + // given empty extra data. Luckily some of that extra data can be created using the data from the + // save data indexer, which should be enough to check access permissions for user saves. + // Every single save data's extra data will be checked and fixed if needed each time the emulator is opened. + // Consider removing this at some point in the future when we don't need to worry about old saves. + VirtualFileSystem.FixExtraData(LibHacHorizonManager.RyujinxClient); + + AccountManager = new AccountManager(LibHacHorizonManager.RyujinxClient, CommandLineState.Profile); + + VirtualFileSystem.ReloadKeySet(); + + ApplicationHelper.Initialize(VirtualFileSystem, AccountManager, LibHacHorizonManager.RyujinxClient); + } + + [SupportedOSPlatform("linux")] + private static async Task ShowVmMaxMapCountWarning() + { + LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.LinuxVmMaxMapCountWarningTextSecondary, + LinuxHelper.VmMaxMapCount, LinuxHelper.RecommendedVmMaxMapCount); + + await ContentDialogHelper.CreateWarningDialog( + LocaleManager.Instance[LocaleKeys.LinuxVmMaxMapCountWarningTextPrimary], + LocaleManager.Instance[LocaleKeys.LinuxVmMaxMapCountWarningTextSecondary] + ); + } + + [SupportedOSPlatform("linux")] + private static async Task ShowVmMaxMapCountDialog() + { + LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.LinuxVmMaxMapCountDialogTextPrimary, + LinuxHelper.RecommendedVmMaxMapCount); + + UserResult response = await ContentDialogHelper.ShowTextDialog( + $"Ryujinx - {LocaleManager.Instance[LocaleKeys.LinuxVmMaxMapCountDialogTitle]}", + LocaleManager.Instance[LocaleKeys.LinuxVmMaxMapCountDialogTextPrimary], + LocaleManager.Instance[LocaleKeys.LinuxVmMaxMapCountDialogTextSecondary], + LocaleManager.Instance[LocaleKeys.LinuxVmMaxMapCountDialogButtonUntilRestart], + LocaleManager.Instance[LocaleKeys.LinuxVmMaxMapCountDialogButtonPersistent], + LocaleManager.Instance[LocaleKeys.InputDialogNo], + (int)Symbol.Help + ); + + int rc; + + switch (response) + { + case UserResult.Ok: + rc = LinuxHelper.RunPkExec($"echo {LinuxHelper.RecommendedVmMaxMapCount} > {LinuxHelper.VmMaxMapCountPath}"); + if (rc == 0) + { + Logger.Info?.Print(LogClass.Application, $"vm.max_map_count set to {LinuxHelper.VmMaxMapCount} until the next restart."); + } + else + { + Logger.Error?.Print(LogClass.Application, $"Unable to change vm.max_map_count. Process exited with code: {rc}"); + } + break; + case UserResult.No: + rc = LinuxHelper.RunPkExec($"echo \"vm.max_map_count = {LinuxHelper.RecommendedVmMaxMapCount}\" > {LinuxHelper.SysCtlConfigPath} && sysctl -p {LinuxHelper.SysCtlConfigPath}"); + if (rc == 0) + { + Logger.Info?.Print(LogClass.Application, $"vm.max_map_count set to {LinuxHelper.VmMaxMapCount}. Written to config: {LinuxHelper.SysCtlConfigPath}"); + } + else + { + Logger.Error?.Print(LogClass.Application, $"Unable to write new value for vm.max_map_count to config. Process exited with code: {rc}"); + } + break; + } + } + + private async Task CheckLaunchState() + { + if (OperatingSystem.IsLinux() && LinuxHelper.VmMaxMapCount < LinuxHelper.RecommendedVmMaxMapCount) + { + Logger.Warning?.Print(LogClass.Application, $"The value of vm.max_map_count is lower than {LinuxHelper.RecommendedVmMaxMapCount}. ({LinuxHelper.VmMaxMapCount})"); + + if (LinuxHelper.PkExecPath is not null) + { + await Dispatcher.UIThread.InvokeAsync(ShowVmMaxMapCountDialog); + } + else + { + await Dispatcher.UIThread.InvokeAsync(ShowVmMaxMapCountWarning); + } + } + + if (!ShowKeyErrorOnLoad) + { + if (_deferLoad) + { + _deferLoad = false; + + if (ApplicationLibrary.TryGetApplicationsFromFile(_launchPath, out List applications)) + { + ApplicationData applicationData; + + if (_launchApplicationId != null) + { + applicationData = applications.Find(application => application.IdString == _launchApplicationId); + + if (applicationData != null) + { + await ViewModel.LoadApplication(applicationData, _startFullscreen); + } + else + { + Logger.Error?.Print(LogClass.Application, $"Couldn't find requested application id '{_launchApplicationId}' in '{_launchPath}'."); + await Dispatcher.UIThread.InvokeAsync(async () => await UserErrorDialog.ShowUserErrorDialog(UserError.ApplicationNotFound)); + } + } + else + { + applicationData = applications[0]; + await ViewModel.LoadApplication(applicationData, _startFullscreen); + } + } + else + { + Logger.Error?.Print(LogClass.Application, $"Couldn't find any application in '{_launchPath}'."); + await Dispatcher.UIThread.InvokeAsync(async () => await UserErrorDialog.ShowUserErrorDialog(UserError.ApplicationNotFound)); + } + } + } + else + { + ShowKeyErrorOnLoad = false; + + await Dispatcher.UIThread.InvokeAsync(async () => await UserErrorDialog.ShowUserErrorDialog(UserError.NoKeys)); + } + + if (ConfigurationState.Instance.CheckUpdatesOnStart && !CommandLineState.HideAvailableUpdates && Updater.CanUpdate()) + { + await this.BeginUpdateAsync() + .ContinueWith( + task => Logger.Error?.Print(LogClass.Application, $"Updater Error: {task.Exception}"), + TaskContinuationOptions.OnlyOnFaulted); + } + } + + private void Load() + { + StatusBarView.VolumeStatus.Click += VolumeStatus_CheckedChanged; + + ApplicationGrid.ApplicationOpened += Application_Opened; + + ApplicationGrid.DataContext = ViewModel; + + ApplicationList.ApplicationOpened += Application_Opened; + + ApplicationList.DataContext = ViewModel; + } + + private void SetWindowSizePosition() + { + if (!ConfigurationState.Instance.RememberWindowState) + { + // Correctly size window when 'TitleBar' is enabled (Nov. 14, 2024) + ViewModel.WindowHeight = (720 + StatusBarHeight + MenuBarHeight + TitleBarHeight) * Program.WindowScaleFactor; + ViewModel.WindowWidth = 1280 * Program.WindowScaleFactor; + + WindowState = WindowState.Normal; + WindowStartupLocation = WindowStartupLocation.CenterScreen; + + return; + } + + PixelPoint savedPoint = new(ConfigurationState.Instance.UI.WindowStartup.WindowPositionX, + ConfigurationState.Instance.UI.WindowStartup.WindowPositionY); + + ViewModel.WindowHeight = ConfigurationState.Instance.UI.WindowStartup.WindowSizeHeight * Program.WindowScaleFactor; + ViewModel.WindowWidth = ConfigurationState.Instance.UI.WindowStartup.WindowSizeWidth * Program.WindowScaleFactor; + + ViewModel.WindowState = ConfigurationState.Instance.UI.WindowStartup.WindowMaximized.Value ? WindowState.Maximized : WindowState.Normal; + + if (Screens.All.Any(screen => screen.Bounds.Contains(savedPoint))) + { + Position = savedPoint; + } + else + { + Logger.Warning?.Print(LogClass.Application, "Failed to find valid start-up coordinates. Defaulting to primary monitor center."); + WindowStartupLocation = WindowStartupLocation.CenterScreen; + } + } + + private void SaveWindowSizePosition() + { + ConfigurationState.Instance.UI.WindowStartup.WindowMaximized.Value = WindowState == WindowState.Maximized; + + // Only save rectangle properties if the window is not in a maximized state. + if (WindowState != WindowState.Maximized) + { + // Since scaling is being applied to the loaded settings from disk (see SetWindowSizePosition() above), scaling should be removed from width/height before saving out to disk + // as well - otherwise anyone not using a 1.0 scale factor their window will increase in size with every subsequent launch of the program when scaling is applied (Nov. 14, 2024) + ConfigurationState.Instance.UI.WindowStartup.WindowSizeHeight.Value = (int)(Height / Program.WindowScaleFactor); + ConfigurationState.Instance.UI.WindowStartup.WindowSizeWidth.Value = (int)(Width / Program.WindowScaleFactor); + + ConfigurationState.Instance.UI.WindowStartup.WindowPositionX.Value = Position.X; + ConfigurationState.Instance.UI.WindowStartup.WindowPositionY.Value = Position.Y; + } + + MainWindowViewModel.SaveConfig(); + } + + protected override void OnOpened(EventArgs e) + { + base.OnOpened(e); + + Initialize(); + + PlatformSettings!.ColorValuesChanged += OnPlatformColorValuesChanged; + + ViewModel.Initialize( + ContentManager, + StorageProvider, + ApplicationLibrary, + VirtualFileSystem, + AccountManager, + InputManager, + _userChannelPersistence, + LibHacHorizonManager, + UiHandler, + ShowLoading, + SwitchToGameControl, + SetMainContent, + this); + + ApplicationLibrary.ApplicationCountUpdated += ApplicationLibrary_ApplicationCountUpdated; + _appLibraryAppsSubscription?.Dispose(); + _appLibraryAppsSubscription = ApplicationLibrary.Applications + .Connect() + .ObserveOn(SynchronizationContext.Current!) + .Bind(ViewModel.Applications) + .OnItemAdded(UpdateApplicationWithLdnData) + .Subscribe(); + ApplicationLibrary.LdnGameDataReceived += ApplicationLibrary_LdnGameDataReceived; + + ConfigurationState.Instance.Multiplayer.Mode.Event += (sender, evt) => + { + _ = Task.Run(ViewModel.ApplicationLibrary.RefreshLdn); + }; + + ConfigurationState.Instance.Multiplayer.LdnServer.Event += (sender, evt) => + { + _ = Task.Run(ViewModel.ApplicationLibrary.RefreshLdn); + }; + _ = Task.Run(ViewModel.ApplicationLibrary.RefreshLdn); + + ViewModel.RefreshFirmwareStatus(); + + // Load applications if no application was requested by the command line + if (!_deferLoad) + { + LoadApplications(); + } + + _ = CheckLaunchState(); + } + + private void SetMainContent(Control content = null) + { + content ??= GameLibrary; + + if (MainContent.Content != content) + { + // Load applications while switching to the GameLibrary if we haven't done that yet + if (!_applicationsLoadedOnce && content == GameLibrary) + { + LoadApplications(); + } + + MainContent.Content = content; + } + } + + public static void UpdateGraphicsConfig() + { +#pragma warning disable IDE0055 // Disable formatting + GraphicsConfig.ResScale = ConfigurationState.Instance.Graphics.ResScale == -1 + ? ConfigurationState.Instance.Graphics.ResScaleCustom + : ConfigurationState.Instance.Graphics.ResScale; + GraphicsConfig.MaxAnisotropy = ConfigurationState.Instance.Graphics.MaxAnisotropy; + GraphicsConfig.ShadersDumpPath = ConfigurationState.Instance.Graphics.ShadersDumpPath; + GraphicsConfig.EnableShaderCache = ConfigurationState.Instance.Graphics.EnableShaderCache; + GraphicsConfig.EnableTextureRecompression = ConfigurationState.Instance.Graphics.EnableTextureRecompression; + GraphicsConfig.EnableMacroHLE = ConfigurationState.Instance.Graphics.EnableMacroHLE; +#pragma warning restore IDE0055 + } + + private void VolumeStatus_CheckedChanged(object sender, RoutedEventArgs e) + { + if (ViewModel.IsGameRunning && sender is ToggleSplitButton volumeSplitButton) + { + if (!volumeSplitButton.IsChecked) + { + ViewModel.AppHost.Device.SetVolume(ViewModel.VolumeBeforeMute); + } + else + { + ViewModel.VolumeBeforeMute = ViewModel.AppHost.Device.GetVolume(); + ViewModel.AppHost.Device.SetVolume(0); + } + + ViewModel.Volume = ViewModel.AppHost.Device.GetVolume(); + } + } + + protected override void OnClosing(WindowClosingEventArgs e) + { + if (!ViewModel.IsClosing && ViewModel.AppHost != null && ConfigurationState.Instance.ShowConfirmExit) + { + e.Cancel = true; + + ConfirmExit(); + + return; + } + + ViewModel.IsClosing = true; + + if (ViewModel.AppHost != null) + { + ViewModel.AppHost.AppExit -= ViewModel.AppHost_AppExit; + ViewModel.AppHost.AppExit += (_, _) => + { + ViewModel.AppHost = null; + + Dispatcher.UIThread.Post(() => + { + MainContent = null; + + Close(); + }); + }; + ViewModel.AppHost?.Stop(); + + e.Cancel = true; + + return; + } + + if (ConfigurationState.Instance.RememberWindowState) + { + SaveWindowSizePosition(); + } + + ApplicationLibrary.CancelLoading(); + InputManager.Dispose(); + _appLibraryAppsSubscription?.Dispose(); + Program.Exit(); + + base.OnClosing(e); + } + + private void ConfirmExit() + { + Dispatcher.UIThread.InvokeAsync(async () => + { + ViewModel.IsClosing = await ContentDialogHelper.CreateExitDialog(); + + if (ViewModel.IsClosing) + { + Close(); + } + }); + } + + public void LoadApplications() + { + _applicationsLoadedOnce = true; + + StatusBarView.LoadProgressBar.IsVisible = true; + ViewModel.StatusBarProgressMaximum = 0; + ViewModel.StatusBarProgressValue = 0; + + LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.StatusBarGamesLoaded, 0, 0); + + ReloadGameList(); + } + + public void ToggleFileType(string fileType) + { + switch (fileType) + { + case "NSP": + ConfigurationState.Instance.UI.ShownFileTypes.NSP.Toggle(); + break; + case "PFS0": + ConfigurationState.Instance.UI.ShownFileTypes.PFS0.Toggle(); + break; + case "XCI": + ConfigurationState.Instance.UI.ShownFileTypes.XCI.Toggle(); + break; + case "NCA": + ConfigurationState.Instance.UI.ShownFileTypes.NCA.Toggle(); + break; + case "NRO": + ConfigurationState.Instance.UI.ShownFileTypes.NRO.Toggle(); + break; + case "NSO": + ConfigurationState.Instance.UI.ShownFileTypes.NSO.Toggle(); + break; + default: + throw new ArgumentOutOfRangeException(fileType); + } + + ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.ConfigurationPath); + LoadApplications(); + } + + private void ReloadGameList() + { + if (_isLoading) + { + return; + } + + _isLoading = true; + + Thread applicationLibraryThread = new(() => + { + ApplicationLibrary.DesiredLanguage = ConfigurationState.Instance.System.Language; + + ApplicationLibrary.LoadApplications(ConfigurationState.Instance.UI.GameDirs); + + var autoloadDirs = ConfigurationState.Instance.UI.AutoloadDirs.Value; + if (autoloadDirs.Count > 0) + { + var updatesLoaded = ApplicationLibrary.AutoLoadTitleUpdates(autoloadDirs, out int updatesRemoved); + var dlcLoaded = ApplicationLibrary.AutoLoadDownloadableContents(autoloadDirs, out int dlcRemoved); + + ShowNewContentAddedDialog(dlcLoaded, dlcRemoved, updatesLoaded, updatesRemoved); + } + + _isLoading = false; + }) + { + Name = "GUI.ApplicationLibraryThread", + IsBackground = true, + }; + applicationLibraryThread.Start(); + } + + private void ShowNewContentAddedDialog(int numDlcAdded, int numDlcRemoved, int numUpdatesAdded, int numUpdatesRemoved) + { + string[] messages = { + numDlcRemoved > 0 ? string.Format(LocaleManager.Instance[LocaleKeys.AutoloadDlcRemovedMessage], numDlcRemoved): null, + numDlcAdded > 0 ? string.Format(LocaleManager.Instance[LocaleKeys.AutoloadDlcAddedMessage], numDlcAdded): null, + numUpdatesRemoved > 0 ? string.Format(LocaleManager.Instance[LocaleKeys.AutoloadUpdateRemovedMessage], numUpdatesRemoved): null, + numUpdatesAdded > 0 ? string.Format(LocaleManager.Instance[LocaleKeys.AutoloadUpdateAddedMessage], numUpdatesAdded) : null + }; + + string msg = String.Join("\r\n", messages); + + if (String.IsNullOrWhiteSpace(msg)) + return; + + Dispatcher.UIThread.InvokeAsync(async () => + { + await ContentDialogHelper.ShowTextDialog( + LocaleManager.Instance[LocaleKeys.DialogConfirmationTitle], + msg, + string.Empty, + string.Empty, + string.Empty, + LocaleManager.Instance[LocaleKeys.InputDialogOk], + (int)Symbol.Checkmark); + }); + } + } +} diff --git a/src/Ryujinx/UI/Windows/ModManagerWindow.axaml b/src/Ryujinx/UI/Windows/ModManagerWindow.axaml new file mode 100644 index 000000000..3a1c4e6dd --- /dev/null +++ b/src/Ryujinx/UI/Windows/ModManagerWindow.axaml @@ -0,0 +1,169 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Ryujinx/UI/Windows/ModManagerWindow.axaml.cs b/src/Ryujinx/UI/Windows/ModManagerWindow.axaml.cs new file mode 100644 index 000000000..774446cf1 --- /dev/null +++ b/src/Ryujinx/UI/Windows/ModManagerWindow.axaml.cs @@ -0,0 +1,139 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Styling; +using FluentAvalonia.UI.Controls; +using Ryujinx.Ava.Common.Locale; +using Ryujinx.Ava.UI.Helpers; +using Ryujinx.Ava.UI.Models; +using Ryujinx.Ava.UI.ViewModels; +using Ryujinx.UI.Common.Helper; +using System.Threading.Tasks; +using Button = Avalonia.Controls.Button; + +namespace Ryujinx.Ava.UI.Windows +{ + public partial class ModManagerWindow : UserControl + { + public readonly ModManagerViewModel ViewModel; + + public ModManagerWindow() + { + DataContext = this; + + InitializeComponent(); + } + + public ModManagerWindow(ulong titleId) + { + DataContext = ViewModel = new ModManagerViewModel(titleId); + + InitializeComponent(); + } + + public static async Task Show(ulong titleId, string titleName) + { + ContentDialog contentDialog = new() + { + PrimaryButtonText = string.Empty, + SecondaryButtonText = string.Empty, + CloseButtonText = string.Empty, + Content = new ModManagerWindow(titleId), + Title = string.Format(LocaleManager.Instance[LocaleKeys.ModWindowTitle], titleName, titleId.ToString("X16")), + }; + + Style bottomBorder = new(x => x.OfType().Name("DialogSpace").Child().OfType()); + bottomBorder.Setters.Add(new Setter(IsVisibleProperty, false)); + + contentDialog.Styles.Add(bottomBorder); + + await contentDialog.ShowAsync(); + } + + private void SaveAndClose(object sender, RoutedEventArgs e) + { + ViewModel.Save(); + ((ContentDialog)Parent).Hide(); + } + + private void Close(object sender, RoutedEventArgs e) + { + ((ContentDialog)Parent).Hide(); + } + + private async void DeleteMod(object sender, RoutedEventArgs e) + { + if (sender is Button button) + { + if (button.DataContext is ModModel model) + { + var result = await ContentDialogHelper.CreateConfirmationDialog( + LocaleManager.Instance[LocaleKeys.DialogWarning], + LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogModManagerDeletionWarningMessage, model.Name), + LocaleManager.Instance[LocaleKeys.InputDialogYes], + LocaleManager.Instance[LocaleKeys.InputDialogNo], + LocaleManager.Instance[LocaleKeys.RyujinxConfirm]); + + if (result == UserResult.Yes) + { + ViewModel.Delete(model); + } + } + } + } + + private async void DeleteAll(object sender, RoutedEventArgs e) + { + var result = await ContentDialogHelper.CreateConfirmationDialog( + LocaleManager.Instance[LocaleKeys.DialogWarning], + LocaleManager.Instance[LocaleKeys.DialogModManagerDeletionAllWarningMessage], + LocaleManager.Instance[LocaleKeys.InputDialogYes], + LocaleManager.Instance[LocaleKeys.InputDialogNo], + LocaleManager.Instance[LocaleKeys.RyujinxConfirm]); + + if (result == UserResult.Yes) + { + ViewModel.DeleteAll(); + } + } + + private void OpenLocation(object sender, RoutedEventArgs e) + { + if (sender is Button button) + { + if (button.DataContext is ModModel model) + { + OpenHelper.OpenFolder(model.Path); + } + } + } + + private void OnSelectionChanged(object sender, SelectionChangedEventArgs e) + { + foreach (var content in e.AddedItems) + { + if (content is ModModel model) + { + var index = ViewModel.Mods.IndexOf(model); + + if (index != -1) + { + ViewModel.Mods[index].Enabled = true; + } + } + } + + foreach (var content in e.RemovedItems) + { + if (content is ModModel model) + { + var index = ViewModel.Mods.IndexOf(model); + + if (index != -1) + { + ViewModel.Mods[index].Enabled = false; + } + } + } + } + } +} diff --git a/src/Ryujinx/UI/Windows/SettingsWindow.axaml b/src/Ryujinx/UI/Windows/SettingsWindow.axaml new file mode 100644 index 000000000..2bf5b55e7 --- /dev/null +++ b/src/Ryujinx/UI/Windows/SettingsWindow.axaml @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Ryujinx/UI/Windows/TitleUpdateWindow.axaml.cs b/src/Ryujinx/UI/Windows/TitleUpdateWindow.axaml.cs new file mode 100644 index 000000000..a13ad4012 --- /dev/null +++ b/src/Ryujinx/UI/Windows/TitleUpdateWindow.axaml.cs @@ -0,0 +1,82 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Styling; +using FluentAvalonia.UI.Controls; +using Ryujinx.Ava.Common.Locale; +using Ryujinx.Ava.UI.ViewModels; +using Ryujinx.UI.App.Common; +using Ryujinx.UI.Common.Helper; +using Ryujinx.UI.Common.Models; +using System.Threading.Tasks; + +namespace Ryujinx.Ava.UI.Windows +{ + public partial class TitleUpdateWindow : UserControl + { + public readonly TitleUpdateViewModel ViewModel; + + public TitleUpdateWindow() + { + DataContext = this; + + InitializeComponent(); + } + + public TitleUpdateWindow(ApplicationLibrary applicationLibrary, ApplicationData applicationData) + { + DataContext = ViewModel = new TitleUpdateViewModel(applicationLibrary, applicationData); + + InitializeComponent(); + } + + public static async Task Show(ApplicationLibrary applicationLibrary, ApplicationData applicationData) + { + ContentDialog contentDialog = new() + { + PrimaryButtonText = string.Empty, + SecondaryButtonText = string.Empty, + CloseButtonText = string.Empty, + Content = new TitleUpdateWindow(applicationLibrary, applicationData), + Title = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.GameUpdateWindowHeading, applicationData.Name, applicationData.IdBaseString), + }; + + Style bottomBorder = new(x => x.OfType().Name("DialogSpace").Child().OfType()); + bottomBorder.Setters.Add(new Setter(IsVisibleProperty, false)); + + contentDialog.Styles.Add(bottomBorder); + + await contentDialog.ShowAsync(); + } + + private void Close(object sender, RoutedEventArgs e) + { + ((ContentDialog)Parent).Hide(); + } + + public void Save(object sender, RoutedEventArgs e) + { + ViewModel.Save(); + + ((ContentDialog)Parent).Hide(); + } + + private void OpenLocation(object sender, RoutedEventArgs e) + { + if (sender is Button { DataContext: TitleUpdateModel model }) + OpenHelper.LocateFile(model.Path); + } + + private void RemoveUpdate(object sender, RoutedEventArgs e) + { + if (sender is Button { DataContext: TitleUpdateModel model }) + ViewModel.RemoveUpdate(model); + } + + private void RemoveAll(object sender, RoutedEventArgs e) + { + ViewModel.TitleUpdates.Clear(); + + ViewModel.SortUpdates(); + } + } +} diff --git a/src/Ryujinx/UI/Windows/XCITrimmerWindow.axaml b/src/Ryujinx/UI/Windows/XCITrimmerWindow.axaml new file mode 100644 index 000000000..d726f8099 --- /dev/null +++ b/src/Ryujinx/UI/Windows/XCITrimmerWindow.axaml @@ -0,0 +1,354 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + > + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Ryujinx/UI/Windows/XCITrimmerWindow.axaml.cs b/src/Ryujinx/UI/Windows/XCITrimmerWindow.axaml.cs new file mode 100644 index 000000000..6df862283 --- /dev/null +++ b/src/Ryujinx/UI/Windows/XCITrimmerWindow.axaml.cs @@ -0,0 +1,101 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Styling; +using FluentAvalonia.UI.Controls; +using Ryujinx.Ava.Common.Locale; +using Ryujinx.Ava.UI.ViewModels; +using Ryujinx.UI.Common.Models; +using System; +using System.Threading.Tasks; + +namespace Ryujinx.Ava.UI.Windows +{ + public partial class XCITrimmerWindow : UserControl + { + public XCITrimmerViewModel ViewModel; + + public XCITrimmerWindow() + { + DataContext = this; + + InitializeComponent(); + } + + public XCITrimmerWindow(MainWindowViewModel mainWindowViewModel) + { + DataContext = ViewModel = new XCITrimmerViewModel(mainWindowViewModel); + + InitializeComponent(); + } + + public static async Task Show(MainWindowViewModel mainWindowViewModel) + { + ContentDialog contentDialog = new() + { + PrimaryButtonText = string.Empty, + SecondaryButtonText = string.Empty, + CloseButtonText = string.Empty, + Content = new XCITrimmerWindow(mainWindowViewModel), + Title = string.Format(LocaleManager.Instance[LocaleKeys.XCITrimmerWindowTitle]), + }; + + Style bottomBorder = new(x => x.OfType().Name("DialogSpace").Child().OfType()); + bottomBorder.Setters.Add(new Setter(IsVisibleProperty, false)); + + contentDialog.Styles.Add(bottomBorder); + + await contentDialog.ShowAsync(); + } + + private void Trim(object sender, RoutedEventArgs e) + { + ViewModel.TrimSelected(); + } + + private void Untrim(object sender, RoutedEventArgs e) + { + ViewModel.UntrimSelected(); + } + + private void Close(object sender, RoutedEventArgs e) + { + ((ContentDialog)Parent).Hide(); + } + + private void Cancel(Object sender, RoutedEventArgs e) + { + ViewModel.Cancel = true; + } + + public void Sort_Checked(object sender, RoutedEventArgs args) + { + if (sender is RadioButton { Tag: string sortField }) + ViewModel.SortingField = Enum.Parse(sortField); + } + + public void Order_Checked(object sender, RoutedEventArgs args) + { + if (sender is RadioButton { Tag: string sortOrder }) + ViewModel.SortingAscending = sortOrder is "Ascending"; + } + + private void OnSelectionChanged(object sender, SelectionChangedEventArgs e) + { + foreach (var content in e.AddedItems) + { + if (content is XCITrimmerFileModel applicationData) + { + ViewModel.Select(applicationData); + } + } + + foreach (var content in e.RemovedItems) + { + if (content is XCITrimmerFileModel applicationData) + { + ViewModel.Deselect(applicationData); + } + } + } + } +} diff --git a/src/Ryujinx/Ui/Applet/ErrorAppletDialog.cs b/src/Ryujinx/Ui/Applet/ErrorAppletDialog.cs deleted file mode 100644 index c6bcf3f28..000000000 --- a/src/Ryujinx/Ui/Applet/ErrorAppletDialog.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Gtk; -using Ryujinx.Ui.Common.Configuration; -using System.Reflection; - -namespace Ryujinx.Ui.Applet -{ - internal class ErrorAppletDialog : MessageDialog - { - public ErrorAppletDialog(Window parentWindow, DialogFlags dialogFlags, MessageType messageType, string[] buttons) : base(parentWindow, dialogFlags, messageType, ButtonsType.None, null) - { - Icon = new Gdk.Pixbuf(Assembly.GetAssembly(typeof(ConfigurationState)), "Ryujinx.Ui.Common.Resources.Logo_Ryujinx.png"); - - int responseId = 0; - - if (buttons != null) - { - foreach (string buttonText in buttons) - { - AddButton(buttonText, responseId); - responseId++; - } - } - else - { - AddButton("OK", 0); - } - - ShowAll(); - } - } -} diff --git a/src/Ryujinx/Ui/Applet/GtkDynamicTextInputHandler.cs b/src/Ryujinx/Ui/Applet/GtkDynamicTextInputHandler.cs deleted file mode 100644 index 3a3da4650..000000000 --- a/src/Ryujinx/Ui/Applet/GtkDynamicTextInputHandler.cs +++ /dev/null @@ -1,108 +0,0 @@ -using Gtk; -using Ryujinx.HLE.Ui; -using Ryujinx.Input.GTK3; -using Ryujinx.Ui.Widgets; -using System.Threading; - -namespace Ryujinx.Ui.Applet -{ - /// - /// Class that forwards key events to a GTK Entry so they can be processed into text. - /// - internal class GtkDynamicTextInputHandler : IDynamicTextInputHandler - { - private readonly Window _parent; - private readonly OffscreenWindow _inputToTextWindow = new(); - private readonly RawInputToTextEntry _inputToTextEntry = new(); - - private bool _canProcessInput; - - public event DynamicTextChangedHandler TextChangedEvent; - public event KeyPressedHandler KeyPressedEvent; - public event KeyReleasedHandler KeyReleasedEvent; - - public bool TextProcessingEnabled - { - get - { - return Volatile.Read(ref _canProcessInput); - } - - set - { - Volatile.Write(ref _canProcessInput, value); - } - } - - public GtkDynamicTextInputHandler(Window parent) - { - _parent = parent; - _parent.KeyPressEvent += HandleKeyPressEvent; - _parent.KeyReleaseEvent += HandleKeyReleaseEvent; - - _inputToTextWindow.Add(_inputToTextEntry); - - _inputToTextEntry.TruncateMultiline = true; - - // Start with input processing turned off so the text box won't accumulate text - // if the user is playing on the keyboard. - _canProcessInput = false; - } - - [GLib.ConnectBefore()] - private void HandleKeyPressEvent(object o, KeyPressEventArgs args) - { - var key = (Ryujinx.Common.Configuration.Hid.Key)GTK3MappingHelper.ToInputKey(args.Event.Key); - - if (!(KeyPressedEvent?.Invoke(key)).GetValueOrDefault(true)) - { - return; - } - - if (_canProcessInput) - { - _inputToTextEntry.SendKeyPressEvent(o, args); - _inputToTextEntry.GetSelectionBounds(out int selectionStart, out int selectionEnd); - TextChangedEvent?.Invoke(_inputToTextEntry.Text, selectionStart, selectionEnd, _inputToTextEntry.OverwriteMode); - } - } - - [GLib.ConnectBefore()] - private void HandleKeyReleaseEvent(object o, KeyReleaseEventArgs args) - { - var key = (Ryujinx.Common.Configuration.Hid.Key)GTK3MappingHelper.ToInputKey(args.Event.Key); - - if (!(KeyReleasedEvent?.Invoke(key)).GetValueOrDefault(true)) - { - return; - } - - if (_canProcessInput) - { - // TODO (caian): This solution may have problems if the pause is sent after a key press - // and before a key release. But for now GTK Entry does not seem to use release events. - _inputToTextEntry.SendKeyReleaseEvent(o, args); - _inputToTextEntry.GetSelectionBounds(out int selectionStart, out int selectionEnd); - TextChangedEvent?.Invoke(_inputToTextEntry.Text, selectionStart, selectionEnd, _inputToTextEntry.OverwriteMode); - } - } - - public void SetText(string text, int cursorBegin) - { - _inputToTextEntry.Text = text; - _inputToTextEntry.Position = cursorBegin; - } - - public void SetText(string text, int cursorBegin, int cursorEnd) - { - _inputToTextEntry.Text = text; - _inputToTextEntry.SelectRegion(cursorBegin, cursorEnd); - } - - public void Dispose() - { - _parent.KeyPressEvent -= HandleKeyPressEvent; - _parent.KeyReleaseEvent -= HandleKeyReleaseEvent; - } - } -} diff --git a/src/Ryujinx/Ui/Applet/GtkHostUiHandler.cs b/src/Ryujinx/Ui/Applet/GtkHostUiHandler.cs deleted file mode 100644 index 241e5e6cf..000000000 --- a/src/Ryujinx/Ui/Applet/GtkHostUiHandler.cs +++ /dev/null @@ -1,200 +0,0 @@ -using Gtk; -using Ryujinx.HLE.HOS.Applets; -using Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.ApplicationProxy.Types; -using Ryujinx.HLE.Ui; -using Ryujinx.Ui.Widgets; -using System; -using System.Threading; - -namespace Ryujinx.Ui.Applet -{ - internal class GtkHostUiHandler : IHostUiHandler - { - private readonly Window _parent; - - public IHostUiTheme HostUiTheme { get; } - - public GtkHostUiHandler(Window parent) - { - _parent = parent; - - HostUiTheme = new GtkHostUiTheme(parent); - } - - public bool DisplayMessageDialog(ControllerAppletUiArgs args) - { - string playerCount = args.PlayerCountMin == args.PlayerCountMax ? $"exactly {args.PlayerCountMin}" : $"{args.PlayerCountMin}-{args.PlayerCountMax}"; - - string message = $"Application requests {playerCount} player(s) with:\n\n" - + $"TYPES: {args.SupportedStyles}\n\n" - + $"PLAYERS: {string.Join(", ", args.SupportedPlayers)}\n\n" - + (args.IsDocked ? "Docked mode set. Handheld is also invalid.\n\n" : "") - + "Please reconfigure Input now and then press OK."; - - return DisplayMessageDialog("Controller Applet", message); - } - - public bool DisplayMessageDialog(string title, string message) - { - ManualResetEvent dialogCloseEvent = new(false); - - bool okPressed = false; - - Application.Invoke(delegate - { - MessageDialog msgDialog = null; - - try - { - msgDialog = new MessageDialog(_parent, DialogFlags.DestroyWithParent, MessageType.Info, ButtonsType.Ok, null) - { - Title = title, - Text = message, - UseMarkup = true, - }; - - msgDialog.SetDefaultSize(400, 0); - - msgDialog.Response += (object o, ResponseArgs args) => - { - if (args.ResponseId == ResponseType.Ok) - { - okPressed = true; - } - - dialogCloseEvent.Set(); - msgDialog?.Dispose(); - }; - - msgDialog.Show(); - } - catch (Exception ex) - { - GtkDialog.CreateErrorDialog($"Error displaying Message Dialog: {ex}"); - - dialogCloseEvent.Set(); - } - }); - - dialogCloseEvent.WaitOne(); - - return okPressed; - } - - public bool DisplayInputDialog(SoftwareKeyboardUiArgs args, out string userText) - { - ManualResetEvent dialogCloseEvent = new(false); - - bool okPressed = false; - bool error = false; - string inputText = args.InitialText ?? ""; - - Application.Invoke(delegate - { - try - { - var swkbdDialog = new SwkbdAppletDialog(_parent) - { - Title = "Software Keyboard", - Text = args.HeaderText, - SecondaryText = args.SubtitleText, - }; - - swkbdDialog.InputEntry.Text = inputText; - swkbdDialog.InputEntry.PlaceholderText = args.GuideText; - swkbdDialog.OkButton.Label = args.SubmitText; - - swkbdDialog.SetInputLengthValidation(args.StringLengthMin, args.StringLengthMax); - swkbdDialog.SetInputValidation(args.KeyboardMode); - - if (swkbdDialog.Run() == (int)ResponseType.Ok) - { - inputText = swkbdDialog.InputEntry.Text; - okPressed = true; - } - - swkbdDialog.Dispose(); - } - catch (Exception ex) - { - error = true; - - GtkDialog.CreateErrorDialog($"Error displaying Software Keyboard: {ex}"); - } - finally - { - dialogCloseEvent.Set(); - } - }); - - dialogCloseEvent.WaitOne(); - - userText = error ? null : inputText; - - return error || okPressed; - } - - public void ExecuteProgram(HLE.Switch device, ProgramSpecifyKind kind, ulong value) - { - device.Configuration.UserChannelPersistence.ExecuteProgram(kind, value); - ((MainWindow)_parent).RendererWidget?.Exit(); - } - - public bool DisplayErrorAppletDialog(string title, string message, string[] buttons) - { - ManualResetEvent dialogCloseEvent = new(false); - - bool showDetails = false; - - Application.Invoke(delegate - { - try - { - ErrorAppletDialog msgDialog = new(_parent, DialogFlags.DestroyWithParent, MessageType.Error, buttons) - { - Title = title, - Text = message, - UseMarkup = true, - WindowPosition = WindowPosition.CenterAlways, - }; - - msgDialog.SetDefaultSize(400, 0); - - msgDialog.Response += (object o, ResponseArgs args) => - { - if (buttons != null) - { - if (buttons.Length > 1) - { - if (args.ResponseId != (ResponseType)(buttons.Length - 1)) - { - showDetails = true; - } - } - } - - dialogCloseEvent.Set(); - msgDialog?.Dispose(); - }; - - msgDialog.Show(); - } - catch (Exception ex) - { - GtkDialog.CreateErrorDialog($"Error displaying ErrorApplet Dialog: {ex}"); - - dialogCloseEvent.Set(); - } - }); - - dialogCloseEvent.WaitOne(); - - return showDetails; - } - - public IDynamicTextInputHandler CreateDynamicTextInputHandler() - { - return new GtkDynamicTextInputHandler(_parent); - } - } -} diff --git a/src/Ryujinx/Ui/Applet/GtkHostUiTheme.cs b/src/Ryujinx/Ui/Applet/GtkHostUiTheme.cs deleted file mode 100644 index 1bc010591..000000000 --- a/src/Ryujinx/Ui/Applet/GtkHostUiTheme.cs +++ /dev/null @@ -1,90 +0,0 @@ -using Gtk; -using Ryujinx.HLE.Ui; -using System.Diagnostics; - -namespace Ryujinx.Ui.Applet -{ - internal class GtkHostUiTheme : IHostUiTheme - { - private const int RenderSurfaceWidth = 32; - private const int RenderSurfaceHeight = 32; - - public string FontFamily { get; private set; } - - public ThemeColor DefaultBackgroundColor { get; } - public ThemeColor DefaultForegroundColor { get; } - public ThemeColor DefaultBorderColor { get; } - public ThemeColor SelectionBackgroundColor { get; } - public ThemeColor SelectionForegroundColor { get; } - - public GtkHostUiTheme(Window parent) - { - Entry entry = new(); - entry.SetStateFlags(StateFlags.Selected, true); - - // Get the font and some colors directly from GTK. - FontFamily = entry.PangoContext.FontDescription.Family; - - // Get foreground colors from the style context. - - var defaultForegroundColor = entry.StyleContext.GetColor(StateFlags.Normal); - var selectedForegroundColor = entry.StyleContext.GetColor(StateFlags.Selected); - - DefaultForegroundColor = new ThemeColor((float)defaultForegroundColor.Alpha, (float)defaultForegroundColor.Red, (float)defaultForegroundColor.Green, (float)defaultForegroundColor.Blue); - SelectionForegroundColor = new ThemeColor((float)selectedForegroundColor.Alpha, (float)selectedForegroundColor.Red, (float)selectedForegroundColor.Green, (float)selectedForegroundColor.Blue); - - ListBoxRow row = new(); - row.SetStateFlags(StateFlags.Selected, true); - - // Request the main thread to render some UI elements to an image to get an approximation for the color. - // NOTE (caian): This will only take the color of the top-left corner of the background, which may be incorrect - // if someone provides a custom style with a gradient or image. - - using (var surface = new Cairo.ImageSurface(Cairo.Format.Argb32, RenderSurfaceWidth, RenderSurfaceHeight)) - using (var context = new Cairo.Context(surface)) - { - context.SetSourceRGBA(1, 1, 1, 1); - context.Rectangle(0, 0, RenderSurfaceWidth, RenderSurfaceHeight); - context.Fill(); - - // The background color must be from the main Window because entry uses a different color. - parent.StyleContext.RenderBackground(context, 0, 0, RenderSurfaceWidth, RenderSurfaceHeight); - - DefaultBackgroundColor = ToThemeColor(surface.Data); - - context.SetSourceRGBA(1, 1, 1, 1); - context.Rectangle(0, 0, RenderSurfaceWidth, RenderSurfaceHeight); - context.Fill(); - - // Use the background color of the list box row when selected as the text box frame color because they are the - // same in the default theme. - row.StyleContext.RenderBackground(context, 0, 0, RenderSurfaceWidth, RenderSurfaceHeight); - - DefaultBorderColor = ToThemeColor(surface.Data); - } - - // Use the border color as the text selection color. - SelectionBackgroundColor = DefaultBorderColor; - } - - private static ThemeColor ToThemeColor(byte[] data) - { - Debug.Assert(data.Length == 4 * RenderSurfaceWidth * RenderSurfaceHeight); - - // Take the center-bottom pixel of the surface. - int position = 4 * (RenderSurfaceWidth * (RenderSurfaceHeight - 1) + RenderSurfaceWidth / 2); - - if (position + 4 > data.Length) - { - return new ThemeColor(1, 0, 0, 0); - } - - float a = data[position + 3] / 255.0f; - float r = data[position + 2] / 255.0f; - float g = data[position + 1] / 255.0f; - float b = data[position + 0] / 255.0f; - - return new ThemeColor(a, r, g, b); - } - } -} diff --git a/src/Ryujinx/Ui/Applet/SwkbdAppletDialog.cs b/src/Ryujinx/Ui/Applet/SwkbdAppletDialog.cs deleted file mode 100644 index c1f3d77c1..000000000 --- a/src/Ryujinx/Ui/Applet/SwkbdAppletDialog.cs +++ /dev/null @@ -1,127 +0,0 @@ -using Gtk; -using Ryujinx.HLE.HOS.Applets.SoftwareKeyboard; -using System; -using System.Linq; - -namespace Ryujinx.Ui.Applet -{ - public class SwkbdAppletDialog : MessageDialog - { - private int _inputMin; - private int _inputMax; -#pragma warning disable IDE0052 // Remove unread private member - private KeyboardMode _mode; -#pragma warning restore IDE0052 - - private string _validationInfoText = ""; - - private Predicate _checkLength = _ => true; - private Predicate _checkInput = _ => true; - - private readonly Label _validationInfo; - - public Entry InputEntry { get; } - public Button OkButton { get; } - public Button CancelButton { get; } - - public SwkbdAppletDialog(Window parent) : base(parent, DialogFlags.Modal | DialogFlags.DestroyWithParent, MessageType.Question, ButtonsType.None, null) - { - SetDefaultSize(300, 0); - - _validationInfo = new Label() - { - Visible = false, - }; - - InputEntry = new Entry() - { - Visible = true, - }; - - InputEntry.Activated += OnInputActivated; - InputEntry.Changed += OnInputChanged; - - OkButton = (Button)AddButton("OK", ResponseType.Ok); - CancelButton = (Button)AddButton("Cancel", ResponseType.Cancel); - - ((Box)MessageArea).PackEnd(_validationInfo, true, true, 0); - ((Box)MessageArea).PackEnd(InputEntry, true, true, 4); - } - - private void ApplyValidationInfo() - { - _validationInfo.Visible = !string.IsNullOrEmpty(_validationInfoText); - _validationInfo.Markup = _validationInfoText; - } - - public void SetInputLengthValidation(int min, int max) - { - _inputMin = Math.Min(min, max); - _inputMax = Math.Max(min, max); - - _validationInfo.Visible = false; - - if (_inputMin <= 0 && _inputMax == int.MaxValue) // Disable. - { - _validationInfo.Visible = false; - - _checkLength = _ => true; - } - else if (_inputMin > 0 && _inputMax == int.MaxValue) - { - _validationInfoText = $"Must be at least {_inputMin} characters long. "; - - _checkLength = length => _inputMin <= length; - } - else - { - _validationInfoText = $"Must be {_inputMin}-{_inputMax} characters long. "; - - _checkLength = length => _inputMin <= length && length <= _inputMax; - } - - ApplyValidationInfo(); - OnInputChanged(this, EventArgs.Empty); - } - - public void SetInputValidation(KeyboardMode mode) - { - _mode = mode; - - switch (mode) - { - case KeyboardMode.Numeric: - _validationInfoText += "Must be 0-9 or '.' only."; - _checkInput = text => text.All(NumericCharacterValidation.IsNumeric); - break; - case KeyboardMode.Alphabet: - _validationInfoText += "Must be non CJK-characters only."; - _checkInput = text => text.All(value => !CJKCharacterValidation.IsCJK(value)); - break; - case KeyboardMode.ASCII: - _validationInfoText += "Must be ASCII text only."; - _checkInput = text => text.All(char.IsAscii); - break; - default: - _checkInput = _ => true; - break; - } - - ApplyValidationInfo(); - OnInputChanged(this, EventArgs.Empty); - } - - private void OnInputActivated(object sender, EventArgs e) - { - if (OkButton.IsSensitive) - { - Respond(ResponseType.Ok); - } - } - - private void OnInputChanged(object sender, EventArgs e) - { - OkButton.Sensitive = _checkLength(InputEntry.Text.Length) && _checkInput(InputEntry.Text); - } - } -} diff --git a/src/Ryujinx/Ui/Helper/MetalHelper.cs b/src/Ryujinx/Ui/Helper/MetalHelper.cs deleted file mode 100644 index f46a5e36e..000000000 --- a/src/Ryujinx/Ui/Helper/MetalHelper.cs +++ /dev/null @@ -1,136 +0,0 @@ -using Gdk; -using System; -using System.Runtime.InteropServices; -using System.Runtime.Versioning; - -namespace Ryujinx.Ui.Helper -{ - public delegate void UpdateBoundsCallbackDelegate(Window window); - - [SupportedOSPlatform("macos")] - [SupportedOSPlatform("ios")] - static partial class MetalHelper - { - private const string LibObjCImport = "/usr/lib/libobjc.A.dylib"; - - private readonly struct Selector - { - public readonly IntPtr NativePtr; - - public unsafe Selector(string value) - { - int size = System.Text.Encoding.UTF8.GetMaxByteCount(value.Length); - byte* data = stackalloc byte[size]; - - fixed (char* pValue = value) - { - System.Text.Encoding.UTF8.GetBytes(pValue, value.Length, data, size); - } - - NativePtr = sel_registerName(data); - } - - public static implicit operator Selector(string value) => new(value); - } - - private static unsafe IntPtr GetClass(string value) - { - int size = System.Text.Encoding.UTF8.GetMaxByteCount(value.Length); - byte* data = stackalloc byte[size]; - - fixed (char* pValue = value) - { - System.Text.Encoding.UTF8.GetBytes(pValue, value.Length, data, size); - } - - return objc_getClass(data); - } - - private struct NsPoint - { - public double X; - public double Y; - - public NsPoint(double x, double y) - { - X = x; - Y = y; - } - } - - private struct NsRect - { - public NsPoint Pos; - public NsPoint Size; - - public NsRect(double x, double y, double width, double height) - { - Pos = new NsPoint(x, y); - Size = new NsPoint(width, height); - } - } - - public static IntPtr GetMetalLayer(Display display, Window window, out IntPtr nsView, out UpdateBoundsCallbackDelegate updateBounds) - { - nsView = gdk_quartz_window_get_nsview(window.Handle); - - // Create a new CAMetalLayer. - IntPtr layerClass = GetClass("CAMetalLayer"); - IntPtr metalLayer = IntPtr_objc_msgSend(layerClass, "alloc"); - objc_msgSend(metalLayer, "init"); - - // Create a child NSView to render into. - IntPtr nsViewClass = GetClass("NSView"); - IntPtr child = IntPtr_objc_msgSend(nsViewClass, "alloc"); - objc_msgSend(child, "init", new NsRect()); - - // Add it as a child. - objc_msgSend(nsView, "addSubview:", child); - - // Make its renderer our metal layer. - objc_msgSend(child, "setWantsLayer:", (byte)1); - objc_msgSend(child, "setLayer:", metalLayer); - objc_msgSend(metalLayer, "setContentsScale:", (double)display.GetMonitorAtWindow(window).ScaleFactor); - - // Set the frame position/location. - updateBounds = (Window window) => - { - window.GetPosition(out int x, out int y); - int width = window.Width; - int height = window.Height; - objc_msgSend(child, "setFrame:", new NsRect(x, y, width, height)); - }; - - updateBounds(window); - - return metalLayer; - } - - [LibraryImport(LibObjCImport)] - private static unsafe partial IntPtr sel_registerName(byte* data); - - [LibraryImport(LibObjCImport)] - private static unsafe partial IntPtr objc_getClass(byte* data); - - [LibraryImport(LibObjCImport)] - private static partial void objc_msgSend(IntPtr receiver, Selector selector); - - [LibraryImport(LibObjCImport)] - private static partial void objc_msgSend(IntPtr receiver, Selector selector, byte value); - - [LibraryImport(LibObjCImport)] - private static partial void objc_msgSend(IntPtr receiver, Selector selector, IntPtr value); - - [LibraryImport(LibObjCImport)] - private static partial void objc_msgSend(IntPtr receiver, Selector selector, NsRect point); - - [LibraryImport(LibObjCImport)] - private static partial void objc_msgSend(IntPtr receiver, Selector selector, double value); - - [LibraryImport(LibObjCImport, EntryPoint = "objc_msgSend")] - private static partial IntPtr IntPtr_objc_msgSend(IntPtr receiver, Selector selector); - - [LibraryImport("libgdk-3.0.dylib")] - private static partial IntPtr gdk_quartz_window_get_nsview(IntPtr gdkWindow); - } -} diff --git a/src/Ryujinx/Ui/Helper/SortHelper.cs b/src/Ryujinx/Ui/Helper/SortHelper.cs deleted file mode 100644 index 4e625f922..000000000 --- a/src/Ryujinx/Ui/Helper/SortHelper.cs +++ /dev/null @@ -1,33 +0,0 @@ -using Gtk; -using Ryujinx.Ui.Common.Helper; -using System; - -namespace Ryujinx.Ui.Helper -{ - static class SortHelper - { - public static int TimePlayedSort(ITreeModel model, TreeIter a, TreeIter b) - { - TimeSpan aTimeSpan = ValueFormatUtils.ParseTimeSpan(model.GetValue(a, 5).ToString()); - TimeSpan bTimeSpan = ValueFormatUtils.ParseTimeSpan(model.GetValue(b, 5).ToString()); - - return TimeSpan.Compare(aTimeSpan, bTimeSpan); - } - - public static int LastPlayedSort(ITreeModel model, TreeIter a, TreeIter b) - { - DateTime aDateTime = ValueFormatUtils.ParseDateTime(model.GetValue(a, 6).ToString()); - DateTime bDateTime = ValueFormatUtils.ParseDateTime(model.GetValue(b, 6).ToString()); - - return DateTime.Compare(aDateTime, bDateTime); - } - - public static int FileSizeSort(ITreeModel model, TreeIter a, TreeIter b) - { - long aSize = ValueFormatUtils.ParseFileSize(model.GetValue(a, 8).ToString()); - long bSize = ValueFormatUtils.ParseFileSize(model.GetValue(b, 8).ToString()); - - return aSize.CompareTo(bSize); - } - } -} diff --git a/src/Ryujinx/Ui/Helper/ThemeHelper.cs b/src/Ryujinx/Ui/Helper/ThemeHelper.cs deleted file mode 100644 index 67962cb6c..000000000 --- a/src/Ryujinx/Ui/Helper/ThemeHelper.cs +++ /dev/null @@ -1,35 +0,0 @@ -using Gtk; -using Ryujinx.Common.Logging; -using Ryujinx.Ui.Common.Configuration; -using System.IO; - -namespace Ryujinx.Ui.Helper -{ - static class ThemeHelper - { - public static void ApplyTheme() - { - if (!ConfigurationState.Instance.Ui.EnableCustomTheme) - { - return; - } - - if (File.Exists(ConfigurationState.Instance.Ui.CustomThemePath) && (Path.GetExtension(ConfigurationState.Instance.Ui.CustomThemePath) == ".css")) - { - CssProvider cssProvider = new(); - - cssProvider.LoadFromPath(ConfigurationState.Instance.Ui.CustomThemePath); - - StyleContext.AddProviderForScreen(Gdk.Screen.Default, cssProvider, 800); - } - else - { - Logger.Warning?.Print(LogClass.Application, $"The \"custom_theme_path\" section in \"Config.json\" contains an invalid path: \"{ConfigurationState.Instance.Ui.CustomThemePath}\"."); - - ConfigurationState.Instance.Ui.CustomThemePath.Value = ""; - ConfigurationState.Instance.Ui.EnableCustomTheme.Value = false; - ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.ConfigurationPath); - } - } - } -} diff --git a/src/Ryujinx/Ui/MainWindow.cs b/src/Ryujinx/Ui/MainWindow.cs deleted file mode 100644 index 2a088f561..000000000 --- a/src/Ryujinx/Ui/MainWindow.cs +++ /dev/null @@ -1,1944 +0,0 @@ -using ARMeilleure.Translation; -using Gtk; -using LibHac.Common; -using LibHac.Common.Keys; -using LibHac.Ncm; -using LibHac.Ns; -using LibHac.Tools.FsSystem; -using LibHac.Tools.FsSystem.NcaUtils; -using Ryujinx.Audio.Backends.Dummy; -using Ryujinx.Audio.Backends.OpenAL; -using Ryujinx.Audio.Backends.SDL2; -using Ryujinx.Audio.Backends.SoundIo; -using Ryujinx.Audio.Integration; -using Ryujinx.Common; -using Ryujinx.Common.Configuration; -using Ryujinx.Common.Configuration.Multiplayer; -using Ryujinx.Common.Logging; -using Ryujinx.Common.SystemInterop; -using Ryujinx.Cpu; -using Ryujinx.Graphics.GAL; -using Ryujinx.Graphics.GAL.Multithreading; -using Ryujinx.HLE.FileSystem; -using Ryujinx.HLE.HOS; -using Ryujinx.HLE.HOS.Services.Account.Acc; -using Ryujinx.HLE.HOS.SystemState; -using Ryujinx.Input.GTK3; -using Ryujinx.Input.HLE; -using Ryujinx.Input.SDL2; -using Ryujinx.Modules; -using Ryujinx.Ui.App.Common; -using Ryujinx.Ui.Applet; -using Ryujinx.Ui.Common; -using Ryujinx.Ui.Common.Configuration; -using Ryujinx.Ui.Common.Helper; -using Ryujinx.Ui.Helper; -using Ryujinx.Ui.Widgets; -using Ryujinx.Ui.Windows; -using Silk.NET.Vulkan; -using SPB.Graphics.Vulkan; -using System; -using System.Diagnostics; -using System.IO; -using System.Reflection; -using System.Threading; -using System.Threading.Tasks; -using GUI = Gtk.Builder.ObjectAttribute; -using ShaderCacheLoadingState = Ryujinx.Graphics.Gpu.Shader.ShaderCacheState; - -namespace Ryujinx.Ui -{ - public class MainWindow : Window - { - private readonly VirtualFileSystem _virtualFileSystem; - private readonly ContentManager _contentManager; - private readonly AccountManager _accountManager; - private readonly LibHacHorizonManager _libHacHorizonManager; - - private UserChannelPersistence _userChannelPersistence; - - private HLE.Switch _emulationContext; - - private WindowsMultimediaTimerResolution _windowsMultimediaTimerResolution; - - private readonly ApplicationLibrary _applicationLibrary; - private readonly GtkHostUiHandler _uiHandler; - private readonly AutoResetEvent _deviceExitStatus; - private readonly ListStore _tableStore; - - private bool _updatingGameTable; - private bool _gameLoaded; - private bool _ending; - - private string _currentEmulatedGamePath = null; - - private string _lastScannedAmiiboId = ""; - private bool _lastScannedAmiiboShowAll = false; - - public RendererWidgetBase RendererWidget; - public InputManager InputManager; - - public bool IsFocused; - -#pragma warning disable CS0169, CS0649, IDE0044, IDE0051 // Field is never assigned to, Add readonly modifier, Remove unused private member - - [GUI] public MenuItem ExitMenuItem; - [GUI] public MenuItem UpdateMenuItem; - [GUI] MenuBar _menuBar; - [GUI] Box _footerBox; - [GUI] Box _statusBar; - [GUI] MenuItem _optionMenu; - [GUI] MenuItem _manageUserProfiles; - [GUI] MenuItem _fileMenu; - [GUI] MenuItem _loadApplicationFile; - [GUI] MenuItem _loadApplicationFolder; - [GUI] MenuItem _appletMenu; - [GUI] MenuItem _actionMenu; - [GUI] MenuItem _pauseEmulation; - [GUI] MenuItem _resumeEmulation; - [GUI] MenuItem _stopEmulation; - [GUI] MenuItem _simulateWakeUpMessage; - [GUI] MenuItem _scanAmiibo; - [GUI] MenuItem _takeScreenshot; - [GUI] MenuItem _hideUi; - [GUI] MenuItem _fullScreen; - [GUI] CheckMenuItem _startFullScreen; - [GUI] CheckMenuItem _showConsole; - [GUI] CheckMenuItem _favToggle; - [GUI] MenuItem _firmwareInstallDirectory; - [GUI] MenuItem _firmwareInstallFile; - [GUI] MenuItem _fileTypesSubMenu; - [GUI] Label _fifoStatus; - [GUI] CheckMenuItem _iconToggle; - [GUI] CheckMenuItem _developerToggle; - [GUI] CheckMenuItem _appToggle; - [GUI] CheckMenuItem _timePlayedToggle; - [GUI] CheckMenuItem _versionToggle; - [GUI] CheckMenuItem _lastPlayedToggle; - [GUI] CheckMenuItem _fileExtToggle; - [GUI] CheckMenuItem _pathToggle; - [GUI] CheckMenuItem _fileSizeToggle; - [GUI] CheckMenuItem _nspShown; - [GUI] CheckMenuItem _pfs0Shown; - [GUI] CheckMenuItem _xciShown; - [GUI] CheckMenuItem _ncaShown; - [GUI] CheckMenuItem _nroShown; - [GUI] CheckMenuItem _nsoShown; - [GUI] Label _gpuBackend; - [GUI] Label _dockedMode; - [GUI] Label _aspectRatio; - [GUI] Label _gameStatus; - [GUI] TreeView _gameTable; - [GUI] TreeSelection _gameTableSelection; - [GUI] ScrolledWindow _gameTableWindow; - [GUI] Label _gpuName; - [GUI] Label _progressLabel; - [GUI] Label _firmwareVersionLabel; - [GUI] Gtk.ProgressBar _progressBar; - [GUI] Box _viewBox; - [GUI] Label _vSyncStatus; - [GUI] Label _volumeStatus; - [GUI] Box _listStatusBox; - [GUI] Label _loadingStatusLabel; - [GUI] Gtk.ProgressBar _loadingStatusBar; - -#pragma warning restore CS0649, IDE0044, CS0169, IDE0051 - - public MainWindow() : this(new Builder("Ryujinx.Ui.MainWindow.glade")) { } - - private MainWindow(Builder builder) : base(builder.GetRawOwnedObject("_mainWin")) - { - builder.Autoconnect(this); - - // Apply custom theme if needed. - ThemeHelper.ApplyTheme(); - - SetWindowSizePosition(); - - Icon = new Gdk.Pixbuf(Assembly.GetAssembly(typeof(ConfigurationState)), "Ryujinx.Ui.Common.Resources.Logo_Ryujinx.png"); - Title = $"Ryujinx {Program.Version}"; - - // Hide emulation context status bar. - _statusBar.Hide(); - - // Instantiate HLE objects. - _virtualFileSystem = VirtualFileSystem.CreateInstance(); - _libHacHorizonManager = new LibHacHorizonManager(); - - _libHacHorizonManager.InitializeFsServer(_virtualFileSystem); - _libHacHorizonManager.InitializeArpServer(); - _libHacHorizonManager.InitializeBcatServer(); - _libHacHorizonManager.InitializeSystemClients(); - - // Save data created before we supported extra data in directory save data will not work properly if - // given empty extra data. Luckily some of that extra data can be created using the data from the - // save data indexer, which should be enough to check access permissions for user saves. - // Every single save data's extra data will be checked and fixed if needed each time the emulator is opened. - // Consider removing this at some point in the future when we don't need to worry about old saves. - VirtualFileSystem.FixExtraData(_libHacHorizonManager.RyujinxClient); - - _contentManager = new ContentManager(_virtualFileSystem); - _accountManager = new AccountManager(_libHacHorizonManager.RyujinxClient, CommandLineState.Profile); - _userChannelPersistence = new UserChannelPersistence(); - - // Instantiate GUI objects. - _applicationLibrary = new ApplicationLibrary(_virtualFileSystem); - _uiHandler = new GtkHostUiHandler(this); - _deviceExitStatus = new AutoResetEvent(false); - - WindowStateEvent += WindowStateEvent_Changed; - DeleteEvent += Window_Close; - FocusInEvent += MainWindow_FocusInEvent; - FocusOutEvent += MainWindow_FocusOutEvent; - - _applicationLibrary.ApplicationAdded += Application_Added; - _applicationLibrary.ApplicationCountUpdated += ApplicationCount_Updated; - - _fileMenu.StateChanged += FileMenu_StateChanged; - _actionMenu.StateChanged += ActionMenu_StateChanged; - _optionMenu.StateChanged += OptionMenu_StateChanged; - - _gameTable.ButtonReleaseEvent += Row_Clicked; - _fullScreen.Activated += FullScreen_Toggled; - - RendererWidgetBase.StatusUpdatedEvent += Update_StatusBar; - - ConfigurationState.Instance.System.IgnoreMissingServices.Event += UpdateIgnoreMissingServicesState; - ConfigurationState.Instance.Graphics.AspectRatio.Event += UpdateAspectRatioState; - ConfigurationState.Instance.System.EnableDockedMode.Event += UpdateDockedModeState; - ConfigurationState.Instance.System.AudioVolume.Event += UpdateAudioVolumeState; - - ConfigurationState.Instance.Multiplayer.Mode.Event += UpdateMultiplayerMode; - ConfigurationState.Instance.Multiplayer.LanInterfaceId.Event += UpdateMultiplayerLanInterfaceId; - - if (ConfigurationState.Instance.Ui.StartFullscreen) - { - _startFullScreen.Active = true; - } - - _showConsole.Active = ConfigurationState.Instance.Ui.ShowConsole.Value; - _showConsole.Visible = ConsoleHelper.SetConsoleWindowStateSupported; - - _actionMenu.Sensitive = false; - _pauseEmulation.Sensitive = false; - _resumeEmulation.Sensitive = false; - - _nspShown.Active = ConfigurationState.Instance.Ui.ShownFileTypes.NSP.Value; - _pfs0Shown.Active = ConfigurationState.Instance.Ui.ShownFileTypes.PFS0.Value; - _xciShown.Active = ConfigurationState.Instance.Ui.ShownFileTypes.XCI.Value; - _ncaShown.Active = ConfigurationState.Instance.Ui.ShownFileTypes.NCA.Value; - _nroShown.Active = ConfigurationState.Instance.Ui.ShownFileTypes.NRO.Value; - _nsoShown.Active = ConfigurationState.Instance.Ui.ShownFileTypes.NSO.Value; - - _nspShown.Toggled += NSP_Shown_Toggled; - _pfs0Shown.Toggled += PFS0_Shown_Toggled; - _xciShown.Toggled += XCI_Shown_Toggled; - _ncaShown.Toggled += NCA_Shown_Toggled; - _nroShown.Toggled += NRO_Shown_Toggled; - _nsoShown.Toggled += NSO_Shown_Toggled; - - _fileTypesSubMenu.Visible = FileAssociationHelper.IsTypeAssociationSupported; - - if (ConfigurationState.Instance.Ui.GuiColumns.FavColumn) - { - _favToggle.Active = true; - } - if (ConfigurationState.Instance.Ui.GuiColumns.IconColumn) - { - _iconToggle.Active = true; - } - if (ConfigurationState.Instance.Ui.GuiColumns.AppColumn) - { - _appToggle.Active = true; - } - if (ConfigurationState.Instance.Ui.GuiColumns.DevColumn) - { - _developerToggle.Active = true; - } - if (ConfigurationState.Instance.Ui.GuiColumns.VersionColumn) - { - _versionToggle.Active = true; - } - if (ConfigurationState.Instance.Ui.GuiColumns.TimePlayedColumn) - { - _timePlayedToggle.Active = true; - } - if (ConfigurationState.Instance.Ui.GuiColumns.LastPlayedColumn) - { - _lastPlayedToggle.Active = true; - } - if (ConfigurationState.Instance.Ui.GuiColumns.FileExtColumn) - { - _fileExtToggle.Active = true; - } - if (ConfigurationState.Instance.Ui.GuiColumns.FileSizeColumn) - { - _fileSizeToggle.Active = true; - } - if (ConfigurationState.Instance.Ui.GuiColumns.PathColumn) - { - _pathToggle.Active = true; - } - - _favToggle.Toggled += Fav_Toggled; - _iconToggle.Toggled += Icon_Toggled; - _appToggle.Toggled += App_Toggled; - _developerToggle.Toggled += Developer_Toggled; - _versionToggle.Toggled += Version_Toggled; - _timePlayedToggle.Toggled += TimePlayed_Toggled; - _lastPlayedToggle.Toggled += LastPlayed_Toggled; - _fileExtToggle.Toggled += FileExt_Toggled; - _fileSizeToggle.Toggled += FileSize_Toggled; - _pathToggle.Toggled += Path_Toggled; - - _gameTable.Model = _tableStore = new ListStore( - typeof(bool), - typeof(Gdk.Pixbuf), - typeof(string), - typeof(string), - typeof(string), - typeof(string), - typeof(string), - typeof(string), - typeof(string), - typeof(string), - typeof(BlitStruct)); - - _tableStore.SetSortFunc(5, SortHelper.TimePlayedSort); - _tableStore.SetSortFunc(6, SortHelper.LastPlayedSort); - _tableStore.SetSortFunc(8, SortHelper.FileSizeSort); - - int columnId = ConfigurationState.Instance.Ui.ColumnSort.SortColumnId; - bool ascending = ConfigurationState.Instance.Ui.ColumnSort.SortAscending; - - _tableStore.SetSortColumnId(columnId, ascending ? SortType.Ascending : SortType.Descending); - - _gameTable.EnableSearch = true; - _gameTable.SearchColumn = 2; - _gameTable.SearchEqualFunc = (model, col, key, iter) => !((string)model.GetValue(iter, col)).Contains(key, StringComparison.InvariantCultureIgnoreCase); - - _hideUi.Label = _hideUi.Label.Replace("SHOWUIKEY", ConfigurationState.Instance.Hid.Hotkeys.Value.ShowUi.ToString()); - - UpdateColumns(); - UpdateGameTable(); - - ConfigurationState.Instance.Ui.GameDirs.Event += (sender, args) => - { - if (args.OldValue != args.NewValue) - { - UpdateGameTable(); - } - }; - - Task.Run(RefreshFirmwareLabel); - - InputManager = new InputManager(new GTK3KeyboardDriver(this), new SDL2GamepadDriver()); - } - - private void UpdateMultiplayerLanInterfaceId(object sender, ReactiveEventArgs args) - { - if (_emulationContext != null) - { - _emulationContext.Configuration.MultiplayerLanInterfaceId = args.NewValue; - } - } - - private void UpdateMultiplayerMode(object sender, ReactiveEventArgs args) - { - if (_emulationContext != null) - { - _emulationContext.Configuration.MultiplayerMode = args.NewValue; - } - } - - private void UpdateIgnoreMissingServicesState(object sender, ReactiveEventArgs args) - { - if (_emulationContext != null) - { - _emulationContext.Configuration.IgnoreMissingServices = args.NewValue; - } - } - - private void UpdateAspectRatioState(object sender, ReactiveEventArgs args) - { - if (_emulationContext != null) - { - _emulationContext.Configuration.AspectRatio = args.NewValue; - } - } - - private void UpdateDockedModeState(object sender, ReactiveEventArgs e) - { - _emulationContext?.System.ChangeDockedModeState(e.NewValue); - } - - private void UpdateAudioVolumeState(object sender, ReactiveEventArgs e) - { - _emulationContext?.SetVolume(e.NewValue); - } - - private void WindowStateEvent_Changed(object o, WindowStateEventArgs args) - { - _fullScreen.Label = args.Event.NewWindowState.HasFlag(Gdk.WindowState.Fullscreen) ? "Exit Fullscreen" : "Enter Fullscreen"; - } - - private void MainWindow_FocusOutEvent(object o, FocusOutEventArgs args) - { - IsFocused = false; - } - - private void MainWindow_FocusInEvent(object o, FocusInEventArgs args) - { - IsFocused = true; - } - - private void UpdateColumns() - { - foreach (TreeViewColumn column in _gameTable.Columns) - { - _gameTable.RemoveColumn(column); - } - - CellRendererToggle favToggle = new(); - favToggle.Toggled += FavToggle_Toggled; - - if (ConfigurationState.Instance.Ui.GuiColumns.FavColumn) - { - _gameTable.AppendColumn("Fav", favToggle, "active", 0); - } - if (ConfigurationState.Instance.Ui.GuiColumns.IconColumn) - { - _gameTable.AppendColumn("Icon", new CellRendererPixbuf(), "pixbuf", 1); - } - if (ConfigurationState.Instance.Ui.GuiColumns.AppColumn) - { - _gameTable.AppendColumn("Application", new CellRendererText(), "text", 2); - } - if (ConfigurationState.Instance.Ui.GuiColumns.DevColumn) - { - _gameTable.AppendColumn("Developer", new CellRendererText(), "text", 3); - } - if (ConfigurationState.Instance.Ui.GuiColumns.VersionColumn) - { - _gameTable.AppendColumn("Version", new CellRendererText(), "text", 4); - } - if (ConfigurationState.Instance.Ui.GuiColumns.TimePlayedColumn) - { - _gameTable.AppendColumn("Time Played", new CellRendererText(), "text", 5); - } - if (ConfigurationState.Instance.Ui.GuiColumns.LastPlayedColumn) - { - _gameTable.AppendColumn("Last Played", new CellRendererText(), "text", 6); - } - if (ConfigurationState.Instance.Ui.GuiColumns.FileExtColumn) - { - _gameTable.AppendColumn("File Ext", new CellRendererText(), "text", 7); - } - if (ConfigurationState.Instance.Ui.GuiColumns.FileSizeColumn) - { - _gameTable.AppendColumn("File Size", new CellRendererText(), "text", 8); - } - if (ConfigurationState.Instance.Ui.GuiColumns.PathColumn) - { - _gameTable.AppendColumn("Path", new CellRendererText(), "text", 9); - } - - foreach (TreeViewColumn column in _gameTable.Columns) - { - switch (column.Title) - { - case "Fav": - column.SortColumnId = 0; - column.Clicked += Column_Clicked; - break; - case "Application": - column.SortColumnId = 2; - column.Clicked += Column_Clicked; - break; - case "Developer": - column.SortColumnId = 3; - column.Clicked += Column_Clicked; - break; - case "Version": - column.SortColumnId = 4; - column.Clicked += Column_Clicked; - break; - case "Time Played": - column.SortColumnId = 5; - column.Clicked += Column_Clicked; - break; - case "Last Played": - column.SortColumnId = 6; - column.Clicked += Column_Clicked; - break; - case "File Ext": - column.SortColumnId = 7; - column.Clicked += Column_Clicked; - break; - case "File Size": - column.SortColumnId = 8; - column.Clicked += Column_Clicked; - break; - case "Path": - column.SortColumnId = 9; - column.Clicked += Column_Clicked; - break; - } - } - } - - protected override void OnDestroyed() - { - InputManager.Dispose(); - } - - private void InitializeSwitchInstance() - { - _virtualFileSystem.ReloadKeySet(); - - IRenderer renderer; - - if (ConfigurationState.Instance.Graphics.GraphicsBackend == GraphicsBackend.Vulkan) - { - string preferredGpu = ConfigurationState.Instance.Graphics.PreferredGpu.Value; - renderer = new Graphics.Vulkan.VulkanRenderer(Vk.GetApi(), CreateVulkanSurface, VulkanHelper.GetRequiredInstanceExtensions, preferredGpu); - } - else - { - renderer = new Graphics.OpenGL.OpenGLRenderer(); - } - - BackendThreading threadingMode = ConfigurationState.Instance.Graphics.BackendThreading; - - bool threadedGAL = threadingMode == BackendThreading.On || (threadingMode == BackendThreading.Auto && renderer.PreferThreading); - - if (threadedGAL) - { - renderer = new ThreadedRenderer(renderer); - } - - Logger.Info?.PrintMsg(LogClass.Gpu, $"Backend Threading ({threadingMode}): {threadedGAL}"); - - IHardwareDeviceDriver deviceDriver = new DummyHardwareDeviceDriver(); - - if (ConfigurationState.Instance.System.AudioBackend.Value == AudioBackend.SDL2) - { - if (SDL2HardwareDeviceDriver.IsSupported) - { - deviceDriver = new SDL2HardwareDeviceDriver(); - } - else - { - Logger.Warning?.Print(LogClass.Audio, "SDL2 is not supported, trying to fall back to OpenAL."); - - if (OpenALHardwareDeviceDriver.IsSupported) - { - Logger.Warning?.Print(LogClass.Audio, "Found OpenAL, changing configuration."); - - ConfigurationState.Instance.System.AudioBackend.Value = AudioBackend.OpenAl; - SaveConfig(); - - deviceDriver = new OpenALHardwareDeviceDriver(); - } - else - { - Logger.Warning?.Print(LogClass.Audio, "OpenAL is not supported, trying to fall back to SoundIO."); - - if (SoundIoHardwareDeviceDriver.IsSupported) - { - Logger.Warning?.Print(LogClass.Audio, "Found SoundIO, changing configuration."); - - ConfigurationState.Instance.System.AudioBackend.Value = AudioBackend.SoundIo; - SaveConfig(); - - deviceDriver = new SoundIoHardwareDeviceDriver(); - } - else - { - Logger.Warning?.Print(LogClass.Audio, "SoundIO is not supported, falling back to dummy audio out."); - } - } - } - } - else if (ConfigurationState.Instance.System.AudioBackend.Value == AudioBackend.SoundIo) - { - if (SoundIoHardwareDeviceDriver.IsSupported) - { - deviceDriver = new SoundIoHardwareDeviceDriver(); - } - else - { - Logger.Warning?.Print(LogClass.Audio, "SoundIO is not supported, trying to fall back to SDL2."); - - if (SDL2HardwareDeviceDriver.IsSupported) - { - Logger.Warning?.Print(LogClass.Audio, "Found SDL2, changing configuration."); - - ConfigurationState.Instance.System.AudioBackend.Value = AudioBackend.SDL2; - SaveConfig(); - - deviceDriver = new SDL2HardwareDeviceDriver(); - } - else - { - Logger.Warning?.Print(LogClass.Audio, "SDL2 is not supported, trying to fall back to OpenAL."); - - if (OpenALHardwareDeviceDriver.IsSupported) - { - Logger.Warning?.Print(LogClass.Audio, "Found OpenAL, changing configuration."); - - ConfigurationState.Instance.System.AudioBackend.Value = AudioBackend.OpenAl; - SaveConfig(); - - deviceDriver = new OpenALHardwareDeviceDriver(); - } - else - { - Logger.Warning?.Print(LogClass.Audio, "OpenAL is not supported, falling back to dummy audio out."); - } - } - } - } - else if (ConfigurationState.Instance.System.AudioBackend.Value == AudioBackend.OpenAl) - { - if (OpenALHardwareDeviceDriver.IsSupported) - { - deviceDriver = new OpenALHardwareDeviceDriver(); - } - else - { - Logger.Warning?.Print(LogClass.Audio, "OpenAL is not supported, trying to fall back to SDL2."); - - if (SDL2HardwareDeviceDriver.IsSupported) - { - Logger.Warning?.Print(LogClass.Audio, "Found SDL2, changing configuration."); - - ConfigurationState.Instance.System.AudioBackend.Value = AudioBackend.SDL2; - SaveConfig(); - - deviceDriver = new SDL2HardwareDeviceDriver(); - } - else - { - Logger.Warning?.Print(LogClass.Audio, "SDL2 is not supported, trying to fall back to SoundIO."); - - if (SoundIoHardwareDeviceDriver.IsSupported) - { - Logger.Warning?.Print(LogClass.Audio, "Found SoundIO, changing configuration."); - - ConfigurationState.Instance.System.AudioBackend.Value = AudioBackend.SoundIo; - SaveConfig(); - - deviceDriver = new SoundIoHardwareDeviceDriver(); - } - else - { - Logger.Warning?.Print(LogClass.Audio, "SoundIO is not supported, falling back to dummy audio out."); - } - } - } - } - - var memoryConfiguration = ConfigurationState.Instance.System.ExpandRam.Value - ? HLE.MemoryConfiguration.MemoryConfiguration6GiB - : HLE.MemoryConfiguration.MemoryConfiguration4GiB; - - IntegrityCheckLevel fsIntegrityCheckLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks ? IntegrityCheckLevel.ErrorOnInvalid : IntegrityCheckLevel.None; - - HLE.HLEConfiguration configuration = new(_virtualFileSystem, - _libHacHorizonManager, - _contentManager, - _accountManager, - _userChannelPersistence, - renderer, - deviceDriver, - memoryConfiguration, - _uiHandler, - (SystemLanguage)ConfigurationState.Instance.System.Language.Value, - (RegionCode)ConfigurationState.Instance.System.Region.Value, - ConfigurationState.Instance.Graphics.EnableVsync, - ConfigurationState.Instance.System.EnableDockedMode, - ConfigurationState.Instance.System.EnablePtc, - ConfigurationState.Instance.System.EnableInternetAccess, - fsIntegrityCheckLevel, - ConfigurationState.Instance.System.FsGlobalAccessLogMode, - ConfigurationState.Instance.System.SystemTimeOffset, - ConfigurationState.Instance.System.TimeZone, - ConfigurationState.Instance.System.MemoryManagerMode, - ConfigurationState.Instance.System.IgnoreMissingServices, - ConfigurationState.Instance.Graphics.AspectRatio, - ConfigurationState.Instance.System.AudioVolume, - ConfigurationState.Instance.System.UseHypervisor, - ConfigurationState.Instance.Multiplayer.LanInterfaceId.Value, - ConfigurationState.Instance.Multiplayer.Mode); - - _emulationContext = new HLE.Switch(configuration); - } - - private SurfaceKHR CreateVulkanSurface(Instance instance, Vk vk) - { - return new SurfaceKHR((ulong)((VulkanRenderer)RendererWidget).CreateWindowSurface(instance.Handle)); - } - - private void SetupProgressUiHandlers() - { - if (_emulationContext.Processes.ActiveApplication.DiskCacheLoadState != null) - { - _emulationContext.Processes.ActiveApplication.DiskCacheLoadState.StateChanged -= ProgressHandler; - _emulationContext.Processes.ActiveApplication.DiskCacheLoadState.StateChanged += ProgressHandler; - } - - _emulationContext.Gpu.ShaderCacheStateChanged -= ProgressHandler; - _emulationContext.Gpu.ShaderCacheStateChanged += ProgressHandler; - } - - private void ProgressHandler(T state, int current, int total) where T : Enum - { - bool visible; - string label; - - switch (state) - { - case LoadState ptcState: - visible = ptcState != LoadState.Loaded; - label = $"PTC : {current}/{total}"; - break; - case ShaderCacheLoadingState shaderCacheState: - visible = shaderCacheState != ShaderCacheLoadingState.Loaded; - label = $"Shaders : {current}/{total}"; - break; - default: - throw new ArgumentException($"Unknown Progress Handler type {typeof(T)}"); - } - - Application.Invoke(delegate - { - _loadingStatusLabel.Text = label; - _loadingStatusBar.Fraction = total > 0 ? (double)current / total : 0; - _loadingStatusBar.Visible = visible; - _loadingStatusLabel.Visible = visible; - }); - } - - public void UpdateGameTable() - { - if (_updatingGameTable || _gameLoaded) - { - return; - } - - _updatingGameTable = true; - - _tableStore.Clear(); - - Thread applicationLibraryThread = new(() => - { - _applicationLibrary.LoadApplications(ConfigurationState.Instance.Ui.GameDirs, ConfigurationState.Instance.System.Language); - - _updatingGameTable = false; - }) - { - Name = "GUI.ApplicationLibraryThread", - IsBackground = true, - }; - applicationLibraryThread.Start(); - } - - [Conditional("RELEASE")] - public void PerformanceCheck() - { - if (ConfigurationState.Instance.Logger.EnableTrace.Value) - { - MessageDialog debugWarningDialog = new(this, DialogFlags.Modal, MessageType.Warning, ButtonsType.YesNo, null) - { - Title = "Ryujinx - Warning", - Text = "You have trace logging enabled, which is designed to be used by developers only.", - SecondaryText = "For optimal performance, it's recommended to disable trace logging. Would you like to disable trace logging now?", - }; - - if (debugWarningDialog.Run() == (int)ResponseType.Yes) - { - ConfigurationState.Instance.Logger.EnableTrace.Value = false; - SaveConfig(); - } - - debugWarningDialog.Dispose(); - } - - if (!string.IsNullOrWhiteSpace(ConfigurationState.Instance.Graphics.ShadersDumpPath.Value)) - { - MessageDialog shadersDumpWarningDialog = new(this, DialogFlags.Modal, MessageType.Warning, ButtonsType.YesNo, null) - { - Title = "Ryujinx - Warning", - Text = "You have shader dumping enabled, which is designed to be used by developers only.", - SecondaryText = "For optimal performance, it's recommended to disable shader dumping. Would you like to disable shader dumping now?", - }; - - if (shadersDumpWarningDialog.Run() == (int)ResponseType.Yes) - { - ConfigurationState.Instance.Graphics.ShadersDumpPath.Value = ""; - SaveConfig(); - } - - shadersDumpWarningDialog.Dispose(); - } - } - - private bool LoadApplication(string path, bool isFirmwareTitle) - { - SystemVersion firmwareVersion = _contentManager.GetCurrentFirmwareVersion(); - - if (!SetupValidator.CanStartApplication(_contentManager, path, out UserError userError)) - { - if (SetupValidator.CanFixStartApplication(_contentManager, path, userError, out firmwareVersion)) - { - string message = $"Would you like to install the firmware embedded in this game? (Firmware {firmwareVersion.VersionString})"; - - ResponseType responseDialog = (ResponseType)GtkDialog.CreateConfirmationDialog("No Firmware Installed", message).Run(); - - if (responseDialog != ResponseType.Yes || !SetupValidator.TryFixStartApplication(_contentManager, path, userError, out _)) - { - UserErrorDialog.CreateUserErrorDialog(userError); - - return false; - } - - // Tell the user that we installed a firmware for them. - - firmwareVersion = _contentManager.GetCurrentFirmwareVersion(); - - RefreshFirmwareLabel(); - - message = $"No installed firmware was found but Ryujinx was able to install firmware {firmwareVersion.VersionString} from the provided game.\nThe emulator will now start."; - - GtkDialog.CreateInfoDialog($"Firmware {firmwareVersion.VersionString} was installed", message); - } - else - { - UserErrorDialog.CreateUserErrorDialog(userError); - - return false; - } - } - - Logger.Notice.Print(LogClass.Application, $"Using Firmware Version: {firmwareVersion?.VersionString}"); - - if (isFirmwareTitle) - { - Logger.Info?.Print(LogClass.Application, "Loading as Firmware Title (NCA)."); - - return _emulationContext.LoadNca(path); - } - - if (Directory.Exists(path)) - { - string[] romFsFiles = Directory.GetFiles(path, "*.istorage"); - - if (romFsFiles.Length == 0) - { - romFsFiles = Directory.GetFiles(path, "*.romfs"); - } - - if (romFsFiles.Length > 0) - { - Logger.Info?.Print(LogClass.Application, "Loading as cart with RomFS."); - - return _emulationContext.LoadCart(path, romFsFiles[0]); - } - - Logger.Info?.Print(LogClass.Application, "Loading as cart WITHOUT RomFS."); - - return _emulationContext.LoadCart(path); - } - - if (File.Exists(path)) - { - switch (System.IO.Path.GetExtension(path).ToLowerInvariant()) - { - case ".xci": - Logger.Info?.Print(LogClass.Application, "Loading as XCI."); - - return _emulationContext.LoadXci(path); - case ".nca": - Logger.Info?.Print(LogClass.Application, "Loading as NCA."); - - return _emulationContext.LoadNca(path); - case ".nsp": - case ".pfs0": - Logger.Info?.Print(LogClass.Application, "Loading as NSP."); - - return _emulationContext.LoadNsp(path); - default: - Logger.Info?.Print(LogClass.Application, "Loading as Homebrew."); - try - { - return _emulationContext.LoadProgram(path); - } - catch (ArgumentOutOfRangeException) - { - Logger.Error?.Print(LogClass.Application, "The specified file is not supported by Ryujinx."); - - return false; - } - } - } - - Logger.Warning?.Print(LogClass.Application, "Please specify a valid XCI/NCA/NSP/PFS0/NRO file."); - - return false; - } - - public void RunApplication(string path, bool startFullscreen = false) - { - if (_gameLoaded) - { - GtkDialog.CreateInfoDialog("A game has already been loaded", "Please stop emulation or close the emulator before launching another game."); - } - else - { - PerformanceCheck(); - - Logger.RestartTime(); - - RendererWidget = CreateRendererWidget(); - - SwitchToRenderWidget(startFullscreen); - - InitializeSwitchInstance(); - - UpdateGraphicsConfig(); - - bool isFirmwareTitle = false; - - if (path.StartsWith("@SystemContent")) - { - path = VirtualFileSystem.SwitchPathToSystemPath(path); - - isFirmwareTitle = true; - } - - if (!LoadApplication(path, isFirmwareTitle)) - { - _emulationContext.Dispose(); - SwitchToGameTable(); - - return; - } - - SetupProgressUiHandlers(); - - _currentEmulatedGamePath = path; - - _deviceExitStatus.Reset(); - - Translator.IsReadyForTranslation.Reset(); - - Thread windowThread = new(CreateGameWindow) - { - Name = "GUI.WindowThread", - }; - - windowThread.Start(); - - _gameLoaded = true; - _actionMenu.Sensitive = true; - UpdateMenuItem.Sensitive = false; - - _lastScannedAmiiboId = ""; - - _firmwareInstallFile.Sensitive = false; - _firmwareInstallDirectory.Sensitive = false; - - DiscordIntegrationModule.SwitchToPlayingState(_emulationContext.Processes.ActiveApplication.ProgramIdText, - _emulationContext.Processes.ActiveApplication.ApplicationControlProperties.Title[(int)_emulationContext.System.State.DesiredTitleLanguage].NameString.ToString()); - - ApplicationLibrary.LoadAndSaveMetaData(_emulationContext.Processes.ActiveApplication.ProgramIdText, appMetadata => - { - appMetadata.UpdatePreGame(); - }); - } - } - - private RendererWidgetBase CreateRendererWidget() - { - if (ConfigurationState.Instance.Graphics.GraphicsBackend == GraphicsBackend.Vulkan) - { - return new VulkanRenderer(InputManager, ConfigurationState.Instance.Logger.GraphicsDebugLevel); - } - else - { - return new OpenGLRenderer(InputManager, ConfigurationState.Instance.Logger.GraphicsDebugLevel); - } - } - - private void SwitchToRenderWidget(bool startFullscreen = false) - { - _viewBox.Remove(_gameTableWindow); - RendererWidget.Expand = true; - _viewBox.Child = RendererWidget; - - RendererWidget.ShowAll(); - EditFooterForGameRenderer(); - - if (Window.State.HasFlag(Gdk.WindowState.Fullscreen)) - { - ToggleExtraWidgets(false); - } - else if (startFullscreen || ConfigurationState.Instance.Ui.StartFullscreen.Value) - { - FullScreen_Toggled(null, null); - } - } - - private void SwitchToGameTable() - { - if (Window.State.HasFlag(Gdk.WindowState.Fullscreen)) - { - ToggleExtraWidgets(true); - } - - RendererWidget.Exit(); - - if (RendererWidget.Window != Window && RendererWidget.Window != null) - { - RendererWidget.Window.Dispose(); - } - - RendererWidget.Dispose(); - - if (OperatingSystem.IsWindows()) - { - _windowsMultimediaTimerResolution?.Dispose(); - _windowsMultimediaTimerResolution = null; - } - - DisplaySleep.Restore(); - - _viewBox.Remove(RendererWidget); - _viewBox.Add(_gameTableWindow); - - _gameTableWindow.Expand = true; - - Window.Title = $"Ryujinx {Program.Version}"; - - _emulationContext = null; - _gameLoaded = false; - RendererWidget = null; - - DiscordIntegrationModule.SwitchToMainMenu(); - - RecreateFooterForMenu(); - - UpdateColumns(); - UpdateGameTable(); - - RefreshFirmwareLabel(); - HandleRelaunch(); - } - - private void CreateGameWindow() - { - if (OperatingSystem.IsWindows()) - { - _windowsMultimediaTimerResolution = new WindowsMultimediaTimerResolution(1); - } - - DisplaySleep.Prevent(); - - RendererWidget.Initialize(_emulationContext); - - RendererWidget.WaitEvent.WaitOne(); - - RendererWidget.Start(); - - _emulationContext.Dispose(); - _deviceExitStatus.Set(); - - // NOTE: Everything that is here will not be executed when you close the UI. - Application.Invoke(delegate - { - SwitchToGameTable(); - }); - } - - private void RecreateFooterForMenu() - { - _listStatusBox.Show(); - _statusBar.Hide(); - } - - private void EditFooterForGameRenderer() - { - _listStatusBox.Hide(); - _statusBar.Show(); - } - - public void ToggleExtraWidgets(bool show) - { - if (RendererWidget != null) - { - if (show) - { - _menuBar.ShowAll(); - _footerBox.Show(); - _statusBar.Show(); - } - else - { - _menuBar.Hide(); - _footerBox.Hide(); - } - } - } - - private void UpdateGameMetadata(string titleId) - { - if (_gameLoaded) - { - ApplicationLibrary.LoadAndSaveMetaData(titleId, appMetadata => - { - appMetadata.UpdatePostGame(); - }); - } - } - - public static void UpdateGraphicsConfig() - { - int resScale = ConfigurationState.Instance.Graphics.ResScale; - float resScaleCustom = ConfigurationState.Instance.Graphics.ResScaleCustom; - - Graphics.Gpu.GraphicsConfig.ResScale = (resScale == -1) ? resScaleCustom : resScale; - Graphics.Gpu.GraphicsConfig.MaxAnisotropy = ConfigurationState.Instance.Graphics.MaxAnisotropy; - Graphics.Gpu.GraphicsConfig.ShadersDumpPath = ConfigurationState.Instance.Graphics.ShadersDumpPath; - Graphics.Gpu.GraphicsConfig.EnableShaderCache = ConfigurationState.Instance.Graphics.EnableShaderCache; - Graphics.Gpu.GraphicsConfig.EnableTextureRecompression = ConfigurationState.Instance.Graphics.EnableTextureRecompression; - Graphics.Gpu.GraphicsConfig.EnableMacroHLE = ConfigurationState.Instance.Graphics.EnableMacroHLE; - } - - public void UpdateInternetAccess() - { - if (_gameLoaded) - { - _emulationContext.Configuration.EnableInternetAccess = ConfigurationState.Instance.System.EnableInternetAccess.Value; - } - } - - public static void SaveConfig() - { - ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.ConfigurationPath); - } - - private void End() - { - if (_ending) - { - return; - } - - _ending = true; - - if (_emulationContext != null) - { - UpdateGameMetadata(_emulationContext.Processes.ActiveApplication.ProgramIdText); - - if (RendererWidget != null) - { - // We tell the widget that we are exiting. - RendererWidget.Exit(); - - // Wait for the other thread to dispose the HLE context before exiting. - _deviceExitStatus.WaitOne(); - RendererWidget.Dispose(); - } - } - - Dispose(); - - Program.Exit(); - Application.Quit(); - } - - // - // Events - // - private void Application_Added(object sender, ApplicationAddedEventArgs args) - { - Application.Invoke(delegate - { - _tableStore.AppendValues( - args.AppData.Favorite, - new Gdk.Pixbuf(args.AppData.Icon, 75, 75), - $"{args.AppData.TitleName}\n{args.AppData.TitleId.ToUpper()}", - args.AppData.Developer, - args.AppData.Version, - args.AppData.TimePlayedString, - args.AppData.LastPlayedString, - args.AppData.FileExtension, - args.AppData.FileSizeString, - args.AppData.Path, - args.AppData.ControlHolder); - }); - } - - private void ApplicationCount_Updated(object sender, ApplicationCountUpdatedEventArgs args) - { - Application.Invoke(delegate - { - _progressLabel.Text = $"{args.NumAppsLoaded}/{args.NumAppsFound} Games Loaded"; - float barValue = 0; - - if (args.NumAppsFound != 0) - { - barValue = (float)args.NumAppsLoaded / args.NumAppsFound; - } - - _progressBar.Fraction = barValue; - - // Reset the vertical scrollbar to the top when titles finish loading - if (args.NumAppsLoaded == args.NumAppsFound) - { - _gameTableWindow.Vadjustment.Value = 0; - } - }); - } - - private void Update_StatusBar(object sender, StatusUpdatedEventArgs args) - { - Application.Invoke(delegate - { - _gameStatus.Text = args.GameStatus; - _fifoStatus.Text = args.FifoStatus; - _gpuName.Text = args.GpuName; - _dockedMode.Text = args.DockedMode; - _aspectRatio.Text = args.AspectRatio; - _gpuBackend.Text = args.GpuBackend; - _volumeStatus.Text = GetVolumeLabelText(args.Volume); - - if (args.VSyncEnabled) - { - _vSyncStatus.Attributes = new Pango.AttrList(); - _vSyncStatus.Attributes.Insert(new Pango.AttrForeground(11822, 60138, 51657)); - } - else - { - _vSyncStatus.Attributes = new Pango.AttrList(); - _vSyncStatus.Attributes.Insert(new Pango.AttrForeground(ushort.MaxValue, 17733, 21588)); - } - }); - } - - private void FavToggle_Toggled(object sender, ToggledArgs args) - { - _tableStore.GetIter(out TreeIter treeIter, new TreePath(args.Path)); - - string titleId = _tableStore.GetValue(treeIter, 2).ToString().Split("\n")[1].ToLower(); - bool newToggleValue = !(bool)_tableStore.GetValue(treeIter, 0); - - _tableStore.SetValue(treeIter, 0, newToggleValue); - - ApplicationLibrary.LoadAndSaveMetaData(titleId, appMetadata => - { - appMetadata.Favorite = newToggleValue; - }); - } - - private void Column_Clicked(object sender, EventArgs args) - { - TreeViewColumn column = (TreeViewColumn)sender; - - ConfigurationState.Instance.Ui.ColumnSort.SortColumnId.Value = column.SortColumnId; - ConfigurationState.Instance.Ui.ColumnSort.SortAscending.Value = column.SortOrder == SortType.Ascending; - - SaveConfig(); - } - - private void Row_Activated(object sender, RowActivatedArgs args) - { - _gameTableSelection.GetSelected(out TreeIter treeIter); - - string path = (string)_tableStore.GetValue(treeIter, 9); - - RunApplication(path); - } - - private void VSyncStatus_Clicked(object sender, ButtonReleaseEventArgs args) - { - _emulationContext.EnableDeviceVsync = !_emulationContext.EnableDeviceVsync; - - Logger.Info?.Print(LogClass.Application, $"VSync toggled to: {_emulationContext.EnableDeviceVsync}"); - } - - private void DockedMode_Clicked(object sender, ButtonReleaseEventArgs args) - { - ConfigurationState.Instance.System.EnableDockedMode.Value = !ConfigurationState.Instance.System.EnableDockedMode.Value; - } - - private static string GetVolumeLabelText(float volume) - { - string icon = volume == 0 ? "🔇" : "🔊"; - - return $"{icon} {(int)(volume * 100)}%"; - } - - private void VolumeStatus_Clicked(object sender, ButtonReleaseEventArgs args) - { - if (_emulationContext != null) - { - if (_emulationContext.IsAudioMuted()) - { - _emulationContext.SetVolume(ConfigurationState.Instance.System.AudioVolume); - } - else - { - _emulationContext.SetVolume(0); - } - } - } - - private void AspectRatio_Clicked(object sender, ButtonReleaseEventArgs args) - { - AspectRatio aspectRatio = ConfigurationState.Instance.Graphics.AspectRatio.Value; - - ConfigurationState.Instance.Graphics.AspectRatio.Value = ((int)aspectRatio + 1) > Enum.GetNames().Length - 1 ? AspectRatio.Fixed4x3 : aspectRatio + 1; - } - - private void Row_Clicked(object sender, ButtonReleaseEventArgs args) - { - if (args.Event.Button != 3 /* Right Click */) - { - return; - } - - _gameTableSelection.GetSelected(out TreeIter treeIter); - - if (treeIter.UserData == IntPtr.Zero) - { - return; - } - - string titleFilePath = _tableStore.GetValue(treeIter, 9).ToString(); - string titleName = _tableStore.GetValue(treeIter, 2).ToString().Split("\n")[0]; - string titleId = _tableStore.GetValue(treeIter, 2).ToString().Split("\n")[1].ToLower(); - - BlitStruct controlData = (BlitStruct)_tableStore.GetValue(treeIter, 10); - - _ = new GameTableContextMenu(this, _virtualFileSystem, _accountManager, _libHacHorizonManager.RyujinxClient, titleFilePath, titleName, titleId, controlData); - } - - private void Load_Application_File(object sender, EventArgs args) - { - using FileChooserNative fileChooser = new("Choose the file to open", this, FileChooserAction.Open, "Open", "Cancel"); - - FileFilter filter = new() - { - Name = "Switch Executables", - }; - filter.AddPattern("*.xci"); - filter.AddPattern("*.nsp"); - filter.AddPattern("*.pfs0"); - filter.AddPattern("*.nca"); - filter.AddPattern("*.nro"); - filter.AddPattern("*.nso"); - - fileChooser.AddFilter(filter); - - if (fileChooser.Run() == (int)ResponseType.Accept) - { - RunApplication(fileChooser.Filename); - } - } - - private void Load_Application_Folder(object sender, EventArgs args) - { - using FileChooserNative fileChooser = new("Choose the folder to open", this, FileChooserAction.SelectFolder, "Open", "Cancel"); - - if (fileChooser.Run() == (int)ResponseType.Accept) - { - RunApplication(fileChooser.Filename); - } - } - - private void FileMenu_StateChanged(object o, StateChangedArgs args) - { - _appletMenu.Sensitive = _emulationContext == null && _contentManager.GetCurrentFirmwareVersion() != null && _contentManager.GetCurrentFirmwareVersion().Major > 3; - _loadApplicationFile.Sensitive = _emulationContext == null; - _loadApplicationFolder.Sensitive = _emulationContext == null; - } - - private void Load_Mii_Edit_Applet(object sender, EventArgs args) - { - string contentPath = _contentManager.GetInstalledContentPath(0x0100000000001009, StorageId.BuiltInSystem, NcaContentType.Program); - - RunApplication(contentPath); - } - - private void Open_Ryu_Folder(object sender, EventArgs args) - { - OpenHelper.OpenFolder(AppDataManager.BaseDirPath); - } - - private void OpenLogsFolder_Pressed(object sender, EventArgs args) - { - string logPath = System.IO.Path.Combine(ReleaseInformation.GetBaseApplicationDirectory(), "Logs"); - - new DirectoryInfo(logPath).Create(); - - OpenHelper.OpenFolder(logPath); - } - - private void Exit_Pressed(object sender, EventArgs args) - { - if (!_gameLoaded || !ConfigurationState.Instance.ShowConfirmExit || GtkDialog.CreateExitDialog()) - { - SaveWindowSizePosition(); - End(); - } - } - - private void Window_Close(object sender, DeleteEventArgs args) - { - if (!_gameLoaded || !ConfigurationState.Instance.ShowConfirmExit || GtkDialog.CreateExitDialog()) - { - SaveWindowSizePosition(); - End(); - } - else - { - args.RetVal = true; - } - } - - private void SetWindowSizePosition() - { - DefaultWidth = ConfigurationState.Instance.Ui.WindowStartup.WindowSizeWidth; - DefaultHeight = ConfigurationState.Instance.Ui.WindowStartup.WindowSizeHeight; - - Move(ConfigurationState.Instance.Ui.WindowStartup.WindowPositionX, ConfigurationState.Instance.Ui.WindowStartup.WindowPositionY); - - if (ConfigurationState.Instance.Ui.WindowStartup.WindowMaximized) - { - Maximize(); - } - } - - private void SaveWindowSizePosition() - { - GetSize(out int windowWidth, out int windowHeight); - GetPosition(out int windowXPos, out int windowYPos); - - ConfigurationState.Instance.Ui.WindowStartup.WindowMaximized.Value = IsMaximized; - ConfigurationState.Instance.Ui.WindowStartup.WindowSizeWidth.Value = windowWidth; - ConfigurationState.Instance.Ui.WindowStartup.WindowSizeHeight.Value = windowHeight; - ConfigurationState.Instance.Ui.WindowStartup.WindowPositionX.Value = windowXPos; - ConfigurationState.Instance.Ui.WindowStartup.WindowPositionY.Value = windowYPos; - - SaveConfig(); - } - - private void StopEmulation_Pressed(object sender, EventArgs args) - { - if (_emulationContext != null) - { - UpdateGameMetadata(_emulationContext.Processes.ActiveApplication.ProgramIdText); - } - - _pauseEmulation.Sensitive = false; - _resumeEmulation.Sensitive = false; - UpdateMenuItem.Sensitive = true; - RendererWidget?.Exit(); - } - - private void PauseEmulation_Pressed(object sender, EventArgs args) - { - _pauseEmulation.Sensitive = false; - _resumeEmulation.Sensitive = true; - _emulationContext.System.TogglePauseEmulation(true); - Title = TitleHelper.ActiveApplicationTitle(_emulationContext.Processes.ActiveApplication, Program.Version, "Paused"); - Logger.Info?.Print(LogClass.Emulation, "Emulation was paused"); - } - - private void ResumeEmulation_Pressed(object sender, EventArgs args) - { - _pauseEmulation.Sensitive = true; - _resumeEmulation.Sensitive = false; - _emulationContext.System.TogglePauseEmulation(false); - Title = TitleHelper.ActiveApplicationTitle(_emulationContext.Processes.ActiveApplication, Program.Version); - Logger.Info?.Print(LogClass.Emulation, "Emulation was resumed"); - } - - public void ActivatePauseMenu() - { - _pauseEmulation.Sensitive = true; - _resumeEmulation.Sensitive = false; - } - - public void TogglePause() - { - _pauseEmulation.Sensitive ^= true; - _resumeEmulation.Sensitive ^= true; - _emulationContext.System.TogglePauseEmulation(_resumeEmulation.Sensitive); - } - - private void Installer_File_Pressed(object o, EventArgs args) - { - FileChooserNative fileChooser = new("Choose the firmware file to open", this, FileChooserAction.Open, "Open", "Cancel"); - - FileFilter filter = new() - { - Name = "Switch Firmware Files", - }; - filter.AddPattern("*.zip"); - filter.AddPattern("*.xci"); - - fileChooser.AddFilter(filter); - - HandleInstallerDialog(fileChooser); - } - - private void Installer_Directory_Pressed(object o, EventArgs args) - { - FileChooserNative directoryChooser = new("Choose the firmware directory to open", this, FileChooserAction.SelectFolder, "Open", "Cancel"); - - HandleInstallerDialog(directoryChooser); - } - - private void HandleInstallerDialog(FileChooserNative fileChooser) - { - if (fileChooser.Run() == (int)ResponseType.Accept) - { - try - { - string filename = fileChooser.Filename; - - fileChooser.Dispose(); - - SystemVersion firmwareVersion = _contentManager.VerifyFirmwarePackage(filename); - - if (firmwareVersion is null) - { - GtkDialog.CreateErrorDialog($"A valid system firmware was not found in {filename}."); - - return; - } - - string dialogTitle = $"Install Firmware {firmwareVersion.VersionString}"; - - SystemVersion currentVersion = _contentManager.GetCurrentFirmwareVersion(); - - string dialogMessage = $"System version {firmwareVersion.VersionString} will be installed."; - - if (currentVersion != null) - { - dialogMessage += $"\n\nThis will replace the current system version {currentVersion.VersionString}. "; - } - - dialogMessage += "\n\nDo you want to continue?"; - - ResponseType responseInstallDialog = (ResponseType)GtkDialog.CreateConfirmationDialog(dialogTitle, dialogMessage).Run(); - - MessageDialog waitingDialog = GtkDialog.CreateWaitingDialog(dialogTitle, "Installing firmware..."); - - if (responseInstallDialog == ResponseType.Yes) - { - Logger.Info?.Print(LogClass.Application, $"Installing firmware {firmwareVersion.VersionString}"); - - Thread thread = new(() => - { - Application.Invoke(delegate - { - waitingDialog.Run(); - - }); - - try - { - _contentManager.InstallFirmware(filename); - - Application.Invoke(delegate - { - waitingDialog.Dispose(); - - string message = $"System version {firmwareVersion.VersionString} successfully installed."; - - GtkDialog.CreateInfoDialog(dialogTitle, message); - Logger.Info?.Print(LogClass.Application, message); - - // Purge Applet Cache. - - DirectoryInfo miiEditorCacheFolder = new(System.IO.Path.Combine(AppDataManager.GamesDirPath, "0100000000001009", "cache")); - - if (miiEditorCacheFolder.Exists) - { - miiEditorCacheFolder.Delete(true); - } - }); - } - catch (Exception ex) - { - Application.Invoke(delegate - { - waitingDialog.Dispose(); - - GtkDialog.CreateErrorDialog(ex.Message); - }); - } - finally - { - RefreshFirmwareLabel(); - } - }) - { - Name = "GUI.FirmwareInstallerThread", - }; - thread.Start(); - } - } - catch (MissingKeyException ex) - { - Logger.Error?.Print(LogClass.Application, ex.ToString()); - UserErrorDialog.CreateUserErrorDialog(UserError.FirmwareParsingFailed); - } - catch (Exception ex) - { - GtkDialog.CreateErrorDialog(ex.Message); - } - } - else - { - fileChooser.Dispose(); - } - } - - private void RefreshFirmwareLabel() - { - SystemVersion currentFirmware = _contentManager.GetCurrentFirmwareVersion(); - - Application.Invoke(delegate - { - _firmwareVersionLabel.Text = currentFirmware != null ? currentFirmware.VersionString : "0.0.0"; - }); - } - - private void InstallFileTypes_Pressed(object sender, EventArgs e) - { - if (FileAssociationHelper.Install()) - { - GtkDialog.CreateInfoDialog("Install file types", "File types successfully installed!"); - } - else - { - GtkDialog.CreateErrorDialog("Failed to install file types."); - } - } - - private void UninstallFileTypes_Pressed(object sender, EventArgs e) - { - if (FileAssociationHelper.Uninstall()) - { - GtkDialog.CreateInfoDialog("Uninstall file types", "File types successfully uninstalled!"); - } - else - { - GtkDialog.CreateErrorDialog("Failed to uninstall file types."); - } - } - - private void HandleRelaunch() - { - if (_userChannelPersistence.PreviousIndex != -1 && _userChannelPersistence.ShouldRestart) - { - _userChannelPersistence.ShouldRestart = false; - - RunApplication(_currentEmulatedGamePath); - } - else - { - // otherwise, clear state. - _userChannelPersistence = new UserChannelPersistence(); - _currentEmulatedGamePath = null; - _actionMenu.Sensitive = false; - _firmwareInstallFile.Sensitive = true; - _firmwareInstallDirectory.Sensitive = true; - } - } - - private void FullScreen_Toggled(object sender, EventArgs args) - { - if (!Window.State.HasFlag(Gdk.WindowState.Fullscreen)) - { - Fullscreen(); - - ToggleExtraWidgets(false); - } - else - { - Unfullscreen(); - - ToggleExtraWidgets(true); - } - } - - private void StartFullScreen_Toggled(object sender, EventArgs args) - { - ConfigurationState.Instance.Ui.StartFullscreen.Value = _startFullScreen.Active; - - SaveConfig(); - } - - private void ShowConsole_Toggled(object sender, EventArgs args) - { - ConfigurationState.Instance.Ui.ShowConsole.Value = _showConsole.Active; - - SaveConfig(); - } - - private void OptionMenu_StateChanged(object o, StateChangedArgs args) - { - _manageUserProfiles.Sensitive = _emulationContext == null; - } - - private void Settings_Pressed(object sender, EventArgs args) - { - SettingsWindow settingsWindow = new(this, _virtualFileSystem, _contentManager); - - settingsWindow.SetSizeRequest((int)(settingsWindow.DefaultWidth * Program.WindowScaleFactor), (int)(settingsWindow.DefaultHeight * Program.WindowScaleFactor)); - settingsWindow.Show(); - } - - private void HideUi_Pressed(object sender, EventArgs args) - { - ToggleExtraWidgets(false); - } - - private void ManageCheats_Pressed(object sender, EventArgs args) - { - var window = new CheatWindow( - _virtualFileSystem, - _emulationContext.Processes.ActiveApplication.ProgramId, - _emulationContext.Processes.ActiveApplication.ApplicationControlProperties - .Title[(int)_emulationContext.System.State.DesiredTitleLanguage].NameString.ToString(), - _currentEmulatedGamePath); - - window.Destroyed += CheatWindow_Destroyed; - window.Show(); - } - - private void CheatWindow_Destroyed(object sender, EventArgs e) - { - _emulationContext.EnableCheats(); - (sender as CheatWindow).Destroyed -= CheatWindow_Destroyed; - } - - private void ManageUserProfiles_Pressed(object sender, EventArgs args) - { - UserProfilesManagerWindow userProfilesManagerWindow = new(_accountManager, _contentManager, _virtualFileSystem); - - userProfilesManagerWindow.SetSizeRequest((int)(userProfilesManagerWindow.DefaultWidth * Program.WindowScaleFactor), (int)(userProfilesManagerWindow.DefaultHeight * Program.WindowScaleFactor)); - userProfilesManagerWindow.Show(); - } - - private void Simulate_WakeUp_Message_Pressed(object sender, EventArgs args) - { - _emulationContext?.System.SimulateWakeUpMessage(); - } - - private void ActionMenu_StateChanged(object o, StateChangedArgs args) - { - _scanAmiibo.Sensitive = _emulationContext != null && _emulationContext.System.SearchingForAmiibo(out int _); - _takeScreenshot.Sensitive = _emulationContext != null; - } - - private void Scan_Amiibo(object sender, EventArgs args) - { - if (_emulationContext.System.SearchingForAmiibo(out int deviceId)) - { - AmiiboWindow amiiboWindow = new() - { - LastScannedAmiiboShowAll = _lastScannedAmiiboShowAll, - LastScannedAmiiboId = _lastScannedAmiiboId, - DeviceId = deviceId, - TitleId = _emulationContext.Processes.ActiveApplication.ProgramIdText.ToUpper(), - }; - - amiiboWindow.DeleteEvent += AmiiboWindow_DeleteEvent; - - amiiboWindow.Show(); - } - else - { - GtkDialog.CreateInfoDialog($"Amiibo", "The game is currently not ready to receive Amiibo scan data. Ensure that you have an Amiibo-compatible game open and ready to receive Amiibo scan data."); - } - } - - private void Take_Screenshot(object sender, EventArgs args) - { - if (_emulationContext != null && RendererWidget != null) - { - RendererWidget.ScreenshotRequested = true; - } - } - - private void AmiiboWindow_DeleteEvent(object sender, DeleteEventArgs args) - { - if (((AmiiboWindow)sender).AmiiboId != "" && ((AmiiboWindow)sender).Response == ResponseType.Ok) - { - _lastScannedAmiiboId = ((AmiiboWindow)sender).AmiiboId; - _lastScannedAmiiboShowAll = ((AmiiboWindow)sender).LastScannedAmiiboShowAll; - - _emulationContext.System.ScanAmiibo(((AmiiboWindow)sender).DeviceId, ((AmiiboWindow)sender).AmiiboId, ((AmiiboWindow)sender).UseRandomUuid); - } - } - - private void Update_Pressed(object sender, EventArgs args) - { - if (Updater.CanUpdate(true)) - { - Updater.BeginParse(this, true).ContinueWith(task => - { - Logger.Error?.Print(LogClass.Application, $"Updater error: {task.Exception}"); - }, TaskContinuationOptions.OnlyOnFaulted); - } - } - - private void About_Pressed(object sender, EventArgs args) - { - AboutWindow aboutWindow = new(); - - aboutWindow.SetSizeRequest((int)(aboutWindow.DefaultWidth * Program.WindowScaleFactor), (int)(aboutWindow.DefaultHeight * Program.WindowScaleFactor)); - aboutWindow.Show(); - } - - private void Fav_Toggled(object sender, EventArgs args) - { - ConfigurationState.Instance.Ui.GuiColumns.FavColumn.Value = _favToggle.Active; - - SaveConfig(); - UpdateColumns(); - } - - private void Icon_Toggled(object sender, EventArgs args) - { - ConfigurationState.Instance.Ui.GuiColumns.IconColumn.Value = _iconToggle.Active; - - SaveConfig(); - UpdateColumns(); - } - - private void App_Toggled(object sender, EventArgs args) - { - ConfigurationState.Instance.Ui.GuiColumns.AppColumn.Value = _appToggle.Active; - - SaveConfig(); - UpdateColumns(); - } - - private void Developer_Toggled(object sender, EventArgs args) - { - ConfigurationState.Instance.Ui.GuiColumns.DevColumn.Value = _developerToggle.Active; - - SaveConfig(); - UpdateColumns(); - } - - private void Version_Toggled(object sender, EventArgs args) - { - ConfigurationState.Instance.Ui.GuiColumns.VersionColumn.Value = _versionToggle.Active; - - SaveConfig(); - UpdateColumns(); - } - - private void TimePlayed_Toggled(object sender, EventArgs args) - { - ConfigurationState.Instance.Ui.GuiColumns.TimePlayedColumn.Value = _timePlayedToggle.Active; - - SaveConfig(); - UpdateColumns(); - } - - private void LastPlayed_Toggled(object sender, EventArgs args) - { - ConfigurationState.Instance.Ui.GuiColumns.LastPlayedColumn.Value = _lastPlayedToggle.Active; - - SaveConfig(); - UpdateColumns(); - } - - private void FileExt_Toggled(object sender, EventArgs args) - { - ConfigurationState.Instance.Ui.GuiColumns.FileExtColumn.Value = _fileExtToggle.Active; - - SaveConfig(); - UpdateColumns(); - } - - private void FileSize_Toggled(object sender, EventArgs args) - { - ConfigurationState.Instance.Ui.GuiColumns.FileSizeColumn.Value = _fileSizeToggle.Active; - - SaveConfig(); - UpdateColumns(); - } - - private void Path_Toggled(object sender, EventArgs args) - { - ConfigurationState.Instance.Ui.GuiColumns.PathColumn.Value = _pathToggle.Active; - - SaveConfig(); - UpdateColumns(); - } - - private void NSP_Shown_Toggled(object sender, EventArgs args) - { - ConfigurationState.Instance.Ui.ShownFileTypes.NSP.Value = _nspShown.Active; - - SaveConfig(); - UpdateGameTable(); - } - - private void PFS0_Shown_Toggled(object sender, EventArgs args) - { - ConfigurationState.Instance.Ui.ShownFileTypes.PFS0.Value = _pfs0Shown.Active; - - SaveConfig(); - UpdateGameTable(); - } - - private void XCI_Shown_Toggled(object sender, EventArgs args) - { - ConfigurationState.Instance.Ui.ShownFileTypes.XCI.Value = _xciShown.Active; - - SaveConfig(); - UpdateGameTable(); - } - - private void NCA_Shown_Toggled(object sender, EventArgs args) - { - ConfigurationState.Instance.Ui.ShownFileTypes.NCA.Value = _ncaShown.Active; - - SaveConfig(); - UpdateGameTable(); - } - - private void NRO_Shown_Toggled(object sender, EventArgs args) - { - ConfigurationState.Instance.Ui.ShownFileTypes.NRO.Value = _nroShown.Active; - - SaveConfig(); - UpdateGameTable(); - } - - private void NSO_Shown_Toggled(object sender, EventArgs args) - { - ConfigurationState.Instance.Ui.ShownFileTypes.NSO.Value = _nsoShown.Active; - - SaveConfig(); - UpdateGameTable(); - } - - private void RefreshList_Pressed(object sender, ButtonReleaseEventArgs args) - { - UpdateGameTable(); - } - } -} diff --git a/src/Ryujinx/Ui/MainWindow.glade b/src/Ryujinx/Ui/MainWindow.glade deleted file mode 100644 index 58d5d9558..000000000 --- a/src/Ryujinx/Ui/MainWindow.glade +++ /dev/null @@ -1,1006 +0,0 @@ - - - - - - False - Ryujinx - center - - - True - False - vertical - - - True - False - - - True - False - File - True - - - True - False - - - True - False - Open a file explorer to choose a Switch compatible file to load - Load Application from File - True - - - - - - True - False - Open a file explorer to choose a Switch compatible, unpacked application to load - Load Unpacked Game - True - - - - - - True - False - Load Applet - True - - - True - False - - - True - False - Open Mii Editor Applet in Standalone mode - Mii Editor - True - - - - - - - - - - True - False - - - - - True - False - Open Ryujinx filesystem folder - Open Ryujinx Folder - True - - - - - - True - False - Opens the folder where logs are written to. - Open Logs Folder - True - - - - - - True - False - - - - - True - False - Exit Ryujinx - Exit - True - - - - - - - - - - True - False - Options - True - - - True - False - - - True - False - Enter Fullscreen - True - - - - - True - False - Start Games in Fullscreen Mode - True - - - - - - True - False - Show Log Console - True - - - - - - True - False - - - - - True - False - Select which GUI columns to enable - Enable GUI Columns - True - - - True - False - - - True - False - Enable or Disable Favorite Games Column in the game list - Enable Favorite Games Column - True - - - - - True - False - Enable or Disable Icon Column in the game list - Enable Icon Column - True - - - - - True - False - Enable or Disable Title Name/ID Column in the game list - Enable Title Name/ID Column - True - - - - - True - False - Enable or Disable Developer Column in the game list - Enable Developer Column - True - - - - - True - False - Enable or Disable Version Column in the game list - Enable Version Column - True - - - - - True - False - Enable or Disable Time Played Column in the game list - Enable Time Played Column - True - - - - - True - False - Enable or Disable Last Played Column in the game list - Enable Last Played Column - True - - - - - True - False - Enable or Disable file extension column in the game list - Enable File Ext Column - True - - - - - True - False - Enable or Disable File Size Column in the game list - Enable File Size Column - True - - - - - True - False - Enable or Disable Path Column in the game list - Enable Path Column - True - - - - - - - - - True - False - Select which file types to show - Show File Types - True - - - True - False - - - True - False - Shows .NSP files in the games list - .NSP - True - - - - - True - False - Shows .PFS0 files in the games list - .PFS0 - True - - - - - True - False - Shows .XCI files in the games list - .XCI - True - - - - - True - False - Shows .NCA files in the games list - .NCA - True - - - - - True - False - Shows .NRO files in the games list - .NRO - True - - - - - True - False - Shows .NSO files in the games list - .NSO - True - - - - - - - - - True - False - - - - - True - False - Open settings window - Settings - True - - - - - - True - False - Open User Profiles Manager window - Manage User Profiles - True - - - - - - - - - - True - False - Actions - True - - - True - False - - - True - False - Pause emulation - Pause Emulation - True - - - - - - True - False - Resume emulation - Resume Emulation - True - - - - - - True - False - Stop emulation of the current game and return to game selection - Stop Emulation - True - - - - - - True - False - - - - - True - False - Simulate a Wake-up Message - Simulate Wake-up Message - True - - - - - - True - False - Scan an Amiibo - Scan an Amiibo - True - - - - - - True - False - Take a screenshot - Take Screenshot - - - - - - True - False - Hide UI (SHOWUIKEY to show) - True - - - - - - True - False - Manage Cheats - - - - - - - - - - True - False - Tools - True - - - True - False - - - True - False - Install Firmware - True - - - True - False - - - True - False - Install a firmware from XCI or ZIP - True - - - - - - True - False - Install a firmware from a directory - True - - - - - - - - - - True - False - Manage file types - True - - - True - False - - - True - False - Install file types - - - - - - True - False - Uninstall file types - - - - - - - - - - - - - - True - False - Help - True - - - True - False - - - True - False - Check for updates to Ryujinx - Check for Updates - True - - - - - - True - False - - - - - True - False - Open about window - About - True - - - - - - - - - - False - True - 0 - - - - - True - False - vertical - - - True - False - vertical - - - True - True - in - - - True - True - True - True - - - - - - - - - True - True - 0 - - - - - True - True - 0 - - - - - 19 - True - False - - - True - False - - - True - False - 5 - - - - RefreshList - True - False - gtk-refresh - - - - - False - False - 0 - - - - - True - False - 10 - 5 - 2 - 2 - 0/0 Games Loaded - - - False - True - 1 - - - - - 200 - True - False - start - 10 - 5 - 6 - - - True - True - 2 - - - - - True - True - 0 - - - - - True - False - - - True - False - - - - True - False - start - 5 - 5 - VSync - - - - - False - True - 0 - - - - - True - False - - - False - True - 1 - - - - - True - False - - - - True - False - start - 5 - 5 - - - - - False - True - 2 - - - - - True - False - - - False - True - 3 - - - - - True - False - - - - True - False - start - 5 - 5 - - - - - False - True - 4 - - - - - True - False - - - False - True - 5 - - - - - True - False - - - - True - False - start - 5 - 5 - - - - - False - True - 6 - - - - - True - False - - - False - True - 7 - - - - - True - False - start - 5 - 5 - - - False - True - 8 - - - - - True - False - - - False - True - 9 - - - - - True - False - start - 5 - 5 - - - False - True - 10 - - - - - True - False - - - False - True - 11 - - - - - True - False - start - 5 - 5 - - - False - True - 12 - - - - - True - False - - - False - True - 13 - - - - - True - False - start - 5 - 5 - - - True - True - 14 - - - - - True - True - 1 - - - - - True - False - 5 - - - True - False - System Version - - - False - True - 0 - - - - - 50 - True - False - 5 - 5 - - - False - True - end - 1 - - - - - False - True - end - 4 - - - - - False - 5 - 5 - 0/0 - - - False - True - 11 - - - - - 200 - False - 5 - 5 - 6 - - - False - True - 12 - - - - - False - True - 1 - - - - - True - True - 1 - - - - - - diff --git a/src/Ryujinx/Ui/OpenGLRenderer.cs b/src/Ryujinx/Ui/OpenGLRenderer.cs deleted file mode 100644 index d10445b00..000000000 --- a/src/Ryujinx/Ui/OpenGLRenderer.cs +++ /dev/null @@ -1,142 +0,0 @@ -using OpenTK.Graphics.OpenGL; -using Ryujinx.Common.Configuration; -using Ryujinx.Common.Logging; -using Ryujinx.Input.HLE; -using SPB.Graphics; -using SPB.Graphics.Exceptions; -using SPB.Graphics.OpenGL; -using SPB.Platform; -using SPB.Platform.GLX; -using SPB.Platform.WGL; -using SPB.Windowing; -using System; -using System.Runtime.InteropServices; - -namespace Ryujinx.Ui -{ - public partial class OpenGLRenderer : RendererWidgetBase - { - private readonly GraphicsDebugLevel _glLogLevel; - - private bool _initializedOpenGL; - - private OpenGLContextBase _openGLContext; - private SwappableNativeWindowBase _nativeWindow; - - public OpenGLRenderer(InputManager inputManager, GraphicsDebugLevel glLogLevel) : base(inputManager, glLogLevel) - { - _glLogLevel = glLogLevel; - } - - protected override bool OnDrawn(Cairo.Context cr) - { - if (!_initializedOpenGL) - { - IntializeOpenGL(); - } - - return true; - } - - private void IntializeOpenGL() - { - _nativeWindow = RetrieveNativeWindow(); - - Window.EnsureNative(); - - _openGLContext = PlatformHelper.CreateOpenGLContext(GetGraphicsMode(), 3, 3, _glLogLevel == GraphicsDebugLevel.None ? OpenGLContextFlags.Compat : OpenGLContextFlags.Compat | OpenGLContextFlags.Debug); - _openGLContext.Initialize(_nativeWindow); - _openGLContext.MakeCurrent(_nativeWindow); - - // Release the GL exclusivity that SPB gave us as we aren't going to use it in GTK Thread. - _openGLContext.MakeCurrent(null); - - WaitEvent.Set(); - - _initializedOpenGL = true; - } - - private SwappableNativeWindowBase RetrieveNativeWindow() - { - if (OperatingSystem.IsWindows()) - { - IntPtr windowHandle = gdk_win32_window_get_handle(Window.Handle); - - return new WGLWindow(new NativeHandle(windowHandle)); - } - else if (OperatingSystem.IsLinux()) - { - IntPtr displayHandle = gdk_x11_display_get_xdisplay(Display.Handle); - IntPtr windowHandle = gdk_x11_window_get_xid(Window.Handle); - - return new GLXWindow(new NativeHandle(displayHandle), new NativeHandle(windowHandle)); - } - - throw new NotImplementedException(); - } - - [LibraryImport("libgdk-3-0.dll")] - private static partial IntPtr gdk_win32_window_get_handle(IntPtr d); - - [LibraryImport("libgdk-3.so.0")] - private static partial IntPtr gdk_x11_display_get_xdisplay(IntPtr gdkDisplay); - - [LibraryImport("libgdk-3.so.0")] - private static partial IntPtr gdk_x11_window_get_xid(IntPtr gdkWindow); - - private static FramebufferFormat GetGraphicsMode() - { - return Environment.OSVersion.Platform == PlatformID.Unix ? new FramebufferFormat(new ColorFormat(8, 8, 8, 0), 16, 0, ColorFormat.Zero, 0, 2, false) : FramebufferFormat.Default; - } - - public override void InitializeRenderer() - { - // First take exclusivity on the OpenGL context. - ((Graphics.OpenGL.OpenGLRenderer)Renderer).InitializeBackgroundContext(SPBOpenGLContext.CreateBackgroundContext(_openGLContext)); - - _openGLContext.MakeCurrent(_nativeWindow); - - GL.ClearColor(0, 0, 0, 1.0f); - GL.Clear(ClearBufferMask.ColorBufferBit); - SwapBuffers(); - } - - public override void SwapBuffers() - { - _nativeWindow.SwapBuffers(); - } - - protected override string GetGpuBackendName() - { - return "OpenGL"; - } - - protected override void Dispose(bool disposing) - { - // Try to bind the OpenGL context before calling the shutdown event. - try - { - _openGLContext?.MakeCurrent(_nativeWindow); - } - catch (ContextException e) - { - Logger.Warning?.Print(LogClass.Ui, $"Failed to bind OpenGL context: {e}"); - } - - Device?.DisposeGpu(); - NpadManager.Dispose(); - - // Unbind context and destroy everything. - try - { - _openGLContext?.MakeCurrent(null); - } - catch (ContextException e) - { - Logger.Warning?.Print(LogClass.Ui, $"Failed to unbind OpenGL context: {e}"); - } - - _openGLContext?.Dispose(); - } - } -} diff --git a/src/Ryujinx/Ui/OpenToolkitBindingsContext.cs b/src/Ryujinx/Ui/OpenToolkitBindingsContext.cs deleted file mode 100644 index 49dd5da8e..000000000 --- a/src/Ryujinx/Ui/OpenToolkitBindingsContext.cs +++ /dev/null @@ -1,20 +0,0 @@ -using SPB.Graphics; -using System; - -namespace Ryujinx.Ui -{ - public class OpenToolkitBindingsContext : OpenTK.IBindingsContext - { - private readonly IBindingsContext _bindingContext; - - public OpenToolkitBindingsContext(IBindingsContext bindingsContext) - { - _bindingContext = bindingsContext; - } - - public IntPtr GetProcAddress(string procName) - { - return _bindingContext.GetProcAddress(procName); - } - } -} diff --git a/src/Ryujinx/Ui/RendererWidgetBase.cs b/src/Ryujinx/Ui/RendererWidgetBase.cs deleted file mode 100644 index 6ae122a0a..000000000 --- a/src/Ryujinx/Ui/RendererWidgetBase.cs +++ /dev/null @@ -1,805 +0,0 @@ -using ARMeilleure.Translation; -using Gdk; -using Gtk; -using Ryujinx.Common; -using Ryujinx.Common.Configuration; -using Ryujinx.Common.Logging; -using Ryujinx.Graphics.GAL; -using Ryujinx.Graphics.GAL.Multithreading; -using Ryujinx.Graphics.Gpu; -using Ryujinx.Input; -using Ryujinx.Input.GTK3; -using Ryujinx.Input.HLE; -using Ryujinx.Ui.Common.Configuration; -using Ryujinx.Ui.Common.Helper; -using Ryujinx.Ui.Widgets; -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.Formats.Png; -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing; -using System; -using System.Diagnostics; -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using Image = SixLabors.ImageSharp.Image; -using Key = Ryujinx.Input.Key; -using ScalingFilter = Ryujinx.Graphics.GAL.ScalingFilter; -using Switch = Ryujinx.HLE.Switch; - -namespace Ryujinx.Ui -{ - public abstract class RendererWidgetBase : DrawingArea - { - private const int SwitchPanelWidth = 1280; - private const int SwitchPanelHeight = 720; - private const int TargetFps = 60; - private const float MaxResolutionScale = 4.0f; // Max resolution hotkeys can scale to before wrapping. - private const float VolumeDelta = 0.05f; - - public ManualResetEvent WaitEvent { get; set; } - public NpadManager NpadManager { get; } - public TouchScreenManager TouchScreenManager { get; } - public Switch Device { get; private set; } - public IRenderer Renderer { get; private set; } - - public bool ScreenshotRequested { get; set; } - protected int WindowWidth { get; private set; } - protected int WindowHeight { get; private set; } - - public static event EventHandler StatusUpdatedEvent; - - private bool _isActive; - private bool _isStopped; - - private bool _toggleFullscreen; - private bool _toggleDockedMode; - - private readonly long _ticksPerFrame; - - private long _ticks = 0; - private float _newVolume; - - private readonly Stopwatch _chrono; - - private KeyboardHotkeyState _prevHotkeyState; - - private readonly ManualResetEvent _exitEvent; - private readonly ManualResetEvent _gpuDoneEvent; - - private readonly CancellationTokenSource _gpuCancellationTokenSource; - - // Hide Cursor - const int CursorHideIdleTime = 5; // seconds - private static readonly Cursor _invisibleCursor = new(Display.Default, CursorType.BlankCursor); - private long _lastCursorMoveTime; - private HideCursorMode _hideCursorMode; - private readonly InputManager _inputManager; - private readonly IKeyboard _keyboardInterface; - private readonly GraphicsDebugLevel _glLogLevel; - private string _gpuBackendName; - private string _gpuVendorName; - private bool _isMouseInClient; - - public RendererWidgetBase(InputManager inputManager, GraphicsDebugLevel glLogLevel) - { - var mouseDriver = new GTK3MouseDriver(this); - - _inputManager = inputManager; - _inputManager.SetMouseDriver(mouseDriver); - NpadManager = _inputManager.CreateNpadManager(); - TouchScreenManager = _inputManager.CreateTouchScreenManager(); - _keyboardInterface = (IKeyboard)_inputManager.KeyboardDriver.GetGamepad("0"); - - WaitEvent = new ManualResetEvent(false); - - _glLogLevel = glLogLevel; - - Destroyed += Renderer_Destroyed; - - _chrono = new Stopwatch(); - - _ticksPerFrame = Stopwatch.Frequency / TargetFps; - - AddEvents((int)(EventMask.ButtonPressMask - | EventMask.ButtonReleaseMask - | EventMask.PointerMotionMask - | EventMask.ScrollMask - | EventMask.EnterNotifyMask - | EventMask.LeaveNotifyMask - | EventMask.KeyPressMask - | EventMask.KeyReleaseMask)); - - _exitEvent = new ManualResetEvent(false); - _gpuDoneEvent = new ManualResetEvent(false); - - _gpuCancellationTokenSource = new CancellationTokenSource(); - - _hideCursorMode = ConfigurationState.Instance.HideCursor; - _lastCursorMoveTime = Stopwatch.GetTimestamp(); - - ConfigurationState.Instance.HideCursor.Event += HideCursorStateChanged; - ConfigurationState.Instance.Graphics.AntiAliasing.Event += UpdateAnriAliasing; - ConfigurationState.Instance.Graphics.ScalingFilter.Event += UpdateScalingFilter; - ConfigurationState.Instance.Graphics.ScalingFilterLevel.Event += UpdateScalingFilterLevel; - } - - private void UpdateScalingFilterLevel(object sender, ReactiveEventArgs e) - { - Renderer.Window.SetScalingFilter((ScalingFilter)ConfigurationState.Instance.Graphics.ScalingFilter.Value); - Renderer.Window.SetScalingFilterLevel(ConfigurationState.Instance.Graphics.ScalingFilterLevel.Value); - } - - private void UpdateScalingFilter(object sender, ReactiveEventArgs e) - { - Renderer.Window.SetScalingFilter((ScalingFilter)ConfigurationState.Instance.Graphics.ScalingFilter.Value); - Renderer.Window.SetScalingFilterLevel(ConfigurationState.Instance.Graphics.ScalingFilterLevel.Value); - } - - public abstract void InitializeRenderer(); - - public abstract void SwapBuffers(); - - protected abstract string GetGpuBackendName(); - - private string GetGpuVendorName() - { - return Renderer.GetHardwareInfo().GpuVendor; - } - - private void HideCursorStateChanged(object sender, ReactiveEventArgs state) - { - Application.Invoke(delegate - { - _hideCursorMode = state.NewValue; - - switch (_hideCursorMode) - { - case HideCursorMode.Never: - Window.Cursor = null; - break; - case HideCursorMode.OnIdle: - _lastCursorMoveTime = Stopwatch.GetTimestamp(); - break; - case HideCursorMode.Always: - Window.Cursor = _invisibleCursor; - break; - default: - throw new ArgumentOutOfRangeException(nameof(state)); - } - }); - } - - private void Renderer_Destroyed(object sender, EventArgs e) - { - ConfigurationState.Instance.HideCursor.Event -= HideCursorStateChanged; - ConfigurationState.Instance.Graphics.AntiAliasing.Event -= UpdateAnriAliasing; - ConfigurationState.Instance.Graphics.ScalingFilter.Event -= UpdateScalingFilter; - ConfigurationState.Instance.Graphics.ScalingFilterLevel.Event -= UpdateScalingFilterLevel; - - NpadManager.Dispose(); - Dispose(); - } - - private void UpdateAnriAliasing(object sender, ReactiveEventArgs e) - { - Renderer?.Window.SetAntiAliasing((Graphics.GAL.AntiAliasing)e.NewValue); - } - - protected override bool OnMotionNotifyEvent(EventMotion evnt) - { - if (_hideCursorMode == HideCursorMode.OnIdle) - { - _lastCursorMoveTime = Stopwatch.GetTimestamp(); - } - - if (ConfigurationState.Instance.Hid.EnableMouse) - { - Window.Cursor = _invisibleCursor; - } - - _isMouseInClient = true; - - return false; - } - - protected override bool OnEnterNotifyEvent(EventCrossing evnt) - { - Window.Cursor = ConfigurationState.Instance.Hid.EnableMouse ? _invisibleCursor : null; - - _isMouseInClient = true; - - return base.OnEnterNotifyEvent(evnt); - } - - protected override bool OnLeaveNotifyEvent(EventCrossing evnt) - { - Window.Cursor = null; - - _isMouseInClient = false; - - return base.OnLeaveNotifyEvent(evnt); - } - - protected override void OnGetPreferredHeight(out int minimumHeight, out int naturalHeight) - { - Gdk.Monitor monitor = Display.GetMonitorAtWindow(Window); - - // If the monitor is at least 1080p, use the Switch panel size as minimal size. - if (monitor.Geometry.Height >= 1080) - { - minimumHeight = SwitchPanelHeight; - } - // Otherwise, we default minimal size to 480p 16:9. - else - { - minimumHeight = 480; - } - - naturalHeight = minimumHeight; - } - - protected override void OnGetPreferredWidth(out int minimumWidth, out int naturalWidth) - { - Gdk.Monitor monitor = Display.GetMonitorAtWindow(Window); - - // If the monitor is at least 1080p, use the Switch panel size as minimal size. - if (monitor.Geometry.Height >= 1080) - { - minimumWidth = SwitchPanelWidth; - } - // Otherwise, we default minimal size to 480p 16:9. - else - { - minimumWidth = 854; - } - - naturalWidth = minimumWidth; - } - - protected override bool OnConfigureEvent(EventConfigure evnt) - { - bool result = base.OnConfigureEvent(evnt); - - Gdk.Monitor monitor = Display.GetMonitorAtWindow(Window); - - WindowWidth = evnt.Width * monitor.ScaleFactor; - WindowHeight = evnt.Height * monitor.ScaleFactor; - - Renderer?.Window?.SetSize(WindowWidth, WindowHeight); - - return result; - } - - private void HandleScreenState(KeyboardStateSnapshot keyboard) - { - bool toggleFullscreen = keyboard.IsPressed(Key.F11) - || ((keyboard.IsPressed(Key.AltLeft) - || keyboard.IsPressed(Key.AltRight)) - && keyboard.IsPressed(Key.Enter)) - || keyboard.IsPressed(Key.Escape); - - bool fullScreenToggled = ParentWindow.State.HasFlag(WindowState.Fullscreen); - - if (toggleFullscreen != _toggleFullscreen) - { - if (toggleFullscreen) - { - if (fullScreenToggled) - { - ParentWindow.Unfullscreen(); - (Toplevel as MainWindow)?.ToggleExtraWidgets(true); - } - else - { - if (keyboard.IsPressed(Key.Escape)) - { - if (!ConfigurationState.Instance.ShowConfirmExit || GtkDialog.CreateExitDialog()) - { - Exit(); - } - } - else - { - ParentWindow.Fullscreen(); - (Toplevel as MainWindow)?.ToggleExtraWidgets(false); - } - } - } - } - - _toggleFullscreen = toggleFullscreen; - - bool toggleDockedMode = keyboard.IsPressed(Key.F9); - - if (toggleDockedMode != _toggleDockedMode) - { - if (toggleDockedMode) - { - ConfigurationState.Instance.System.EnableDockedMode.Value = - !ConfigurationState.Instance.System.EnableDockedMode.Value; - } - } - - _toggleDockedMode = toggleDockedMode; - - if (_isMouseInClient) - { - if (ConfigurationState.Instance.Hid.EnableMouse.Value) - { - Window.Cursor = _invisibleCursor; - } - else - { - switch (_hideCursorMode) - { - case HideCursorMode.OnIdle: - long cursorMoveDelta = Stopwatch.GetTimestamp() - _lastCursorMoveTime; - Window.Cursor = (cursorMoveDelta >= CursorHideIdleTime * Stopwatch.Frequency) ? _invisibleCursor : null; - break; - case HideCursorMode.Always: - Window.Cursor = _invisibleCursor; - break; - case HideCursorMode.Never: - Window.Cursor = null; - break; - } - } - } - } - - public void Initialize(Switch device) - { - Device = device; - - IRenderer renderer = Device.Gpu.Renderer; - - if (renderer is ThreadedRenderer tr) - { - renderer = tr.BaseRenderer; - } - - Renderer = renderer; - Renderer?.Window?.SetSize(WindowWidth, WindowHeight); - - if (Renderer != null) - { - Renderer.ScreenCaptured += Renderer_ScreenCaptured; - } - - NpadManager.Initialize(device, ConfigurationState.Instance.Hid.InputConfig, ConfigurationState.Instance.Hid.EnableKeyboard, ConfigurationState.Instance.Hid.EnableMouse); - TouchScreenManager.Initialize(device); - } - - private unsafe void Renderer_ScreenCaptured(object sender, ScreenCaptureImageInfo e) - { - if (e.Data.Length > 0 && e.Height > 0 && e.Width > 0) - { - Task.Run(() => - { - lock (this) - { - var currentTime = DateTime.Now; - string filename = $"ryujinx_capture_{currentTime.Year}-{currentTime.Month:D2}-{currentTime.Day:D2}_{currentTime.Hour:D2}-{currentTime.Minute:D2}-{currentTime.Second:D2}.png"; - string directory = AppDataManager.Mode switch - { - AppDataManager.LaunchMode.Portable or AppDataManager.LaunchMode.Custom => System.IO.Path.Combine(AppDataManager.BaseDirPath, "screenshots"), - _ => System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyPictures), "Ryujinx"), - }; - - string path = System.IO.Path.Combine(directory, filename); - - try - { - Directory.CreateDirectory(directory); - } - catch (Exception ex) - { - Logger.Error?.Print(LogClass.Application, $"Failed to create directory at path {directory}. Error : {ex.GetType().Name}", "Screenshot"); - - return; - } - - Image image = e.IsBgra ? Image.LoadPixelData(e.Data, e.Width, e.Height) - : Image.LoadPixelData(e.Data, e.Width, e.Height); - - if (e.FlipX) - { - image.Mutate(x => x.Flip(FlipMode.Horizontal)); - } - - if (e.FlipY) - { - image.Mutate(x => x.Flip(FlipMode.Vertical)); - } - - image.SaveAsPng(path, new PngEncoder() - { - ColorType = PngColorType.Rgb, - }); - - image.Dispose(); - - Logger.Notice.Print(LogClass.Application, $"Screenshot saved to {path}", "Screenshot"); - } - }); - } - else - { - Logger.Error?.Print(LogClass.Application, $"Screenshot is empty. Size : {e.Data.Length} bytes. Resolution : {e.Width}x{e.Height}", "Screenshot"); - } - } - - public void Render() - { - Gtk.Window parent = Toplevel as Gtk.Window; - parent.Present(); - - InitializeRenderer(); - - Device.Gpu.Renderer.Initialize(_glLogLevel); - - Renderer.Window.SetAntiAliasing((Graphics.GAL.AntiAliasing)ConfigurationState.Instance.Graphics.AntiAliasing.Value); - Renderer.Window.SetScalingFilter((Graphics.GAL.ScalingFilter)ConfigurationState.Instance.Graphics.ScalingFilter.Value); - Renderer.Window.SetScalingFilterLevel(ConfigurationState.Instance.Graphics.ScalingFilterLevel.Value); - - _gpuBackendName = GetGpuBackendName(); - _gpuVendorName = GetGpuVendorName(); - - Device.Gpu.Renderer.RunLoop(() => - { - Device.Gpu.SetGpuThread(); - Device.Gpu.InitializeShaderCache(_gpuCancellationTokenSource.Token); - Translator.IsReadyForTranslation.Set(); - - Renderer.Window.ChangeVSyncMode(Device.EnableDeviceVsync); - - (Toplevel as MainWindow)?.ActivatePauseMenu(); - - while (_isActive) - { - if (_isStopped) - { - return; - } - - _ticks += _chrono.ElapsedTicks; - - _chrono.Restart(); - - if (Device.WaitFifo()) - { - Device.Statistics.RecordFifoStart(); - Device.ProcessFrame(); - Device.Statistics.RecordFifoEnd(); - } - - while (Device.ConsumeFrameAvailable()) - { - Device.PresentFrame(SwapBuffers); - } - - if (_ticks >= _ticksPerFrame) - { - string dockedMode = ConfigurationState.Instance.System.EnableDockedMode ? "Docked" : "Handheld"; - float scale = GraphicsConfig.ResScale; - if (scale != 1) - { - dockedMode += $" ({scale}x)"; - } - - StatusUpdatedEvent?.Invoke(this, new StatusUpdatedEventArgs( - Device.EnableDeviceVsync, - Device.GetVolume(), - _gpuBackendName, - dockedMode, - ConfigurationState.Instance.Graphics.AspectRatio.Value.ToText(), - $"Game: {Device.Statistics.GetGameFrameRate():00.00} FPS ({Device.Statistics.GetGameFrameTime():00.00} ms)", - $"FIFO: {Device.Statistics.GetFifoPercent():0.00} %", - $"GPU: {_gpuVendorName}")); - - _ticks = Math.Min(_ticks - _ticksPerFrame, _ticksPerFrame); - } - } - - // Make sure all commands in the run loop are fully executed before leaving the loop. - if (Device.Gpu.Renderer is ThreadedRenderer threaded) - { - threaded.FlushThreadedCommands(); - } - - _gpuDoneEvent.Set(); - }); - } - - public void Start() - { - _chrono.Restart(); - - _isActive = true; - - Gtk.Window parent = Toplevel as Gtk.Window; - - Application.Invoke(delegate - { - parent.Present(); - - var activeProcess = Device.Processes.ActiveApplication; - - parent.Title = TitleHelper.ActiveApplicationTitle(activeProcess, Program.Version); - }); - - Thread renderLoopThread = new(Render) - { - Name = "GUI.RenderLoop", - }; - renderLoopThread.Start(); - - Thread nvidiaStutterWorkaround = null; - if (Renderer is Graphics.OpenGL.OpenGLRenderer) - { - nvidiaStutterWorkaround = new Thread(NvidiaStutterWorkaround) - { - Name = "GUI.NvidiaStutterWorkaround", - }; - nvidiaStutterWorkaround.Start(); - } - - MainLoop(); - - // NOTE: The render loop is allowed to stay alive until the renderer itself is disposed, as it may handle resource dispose. - // We only need to wait for all commands submitted during the main gpu loop to be processed. - _gpuDoneEvent.WaitOne(); - _gpuDoneEvent.Dispose(); - nvidiaStutterWorkaround?.Join(); - - Exit(); - } - - public void Exit() - { - TouchScreenManager?.Dispose(); - NpadManager?.Dispose(); - - if (_isStopped) - { - return; - } - - _gpuCancellationTokenSource.Cancel(); - - _isStopped = true; - - if (_isActive) - { - _isActive = false; - - _exitEvent.WaitOne(); - _exitEvent.Dispose(); - } - } - - private void NvidiaStutterWorkaround() - { - while (_isActive) - { - // When NVIDIA Threaded Optimization is on, the driver will snapshot all threads in the system whenever the application creates any new ones. - // The ThreadPool has something called a "GateThread" which terminates itself after some inactivity. - // However, it immediately starts up again, since the rules regarding when to terminate and when to start differ. - // This creates a new thread every second or so. - // The main problem with this is that the thread snapshot can take 70ms, is on the OpenGL thread and will delay rendering any graphics. - // This is a little over budget on a frame time of 16ms, so creates a large stutter. - // The solution is to keep the ThreadPool active so that it never has a reason to terminate the GateThread. - - // TODO: This should be removed when the issue with the GateThread is resolved. - - ThreadPool.QueueUserWorkItem((state) => { }); - Thread.Sleep(300); - } - } - - public void MainLoop() - { - while (_isActive) - { - UpdateFrame(); - - // Polling becomes expensive if it's not slept - Thread.Sleep(1); - } - - _exitEvent.Set(); - } - - private bool UpdateFrame() - { - if (!_isActive) - { - return true; - } - - if (_isStopped) - { - return false; - } - - if ((Toplevel as MainWindow).IsFocused) - { - Application.Invoke(delegate - { - KeyboardStateSnapshot keyboard = _keyboardInterface.GetKeyboardStateSnapshot(); - - HandleScreenState(keyboard); - - if (keyboard.IsPressed(Key.Delete)) - { - if (!ParentWindow.State.HasFlag(WindowState.Fullscreen)) - { - Device.Processes.ActiveApplication.DiskCacheLoadState?.Cancel(); - } - } - }); - } - - NpadManager.Update(ConfigurationState.Instance.Graphics.AspectRatio.Value.ToFloat()); - - if ((Toplevel as MainWindow).IsFocused) - { - KeyboardHotkeyState currentHotkeyState = GetHotkeyState(); - - if (currentHotkeyState.HasFlag(KeyboardHotkeyState.ToggleVSync) && - !_prevHotkeyState.HasFlag(KeyboardHotkeyState.ToggleVSync)) - { - Device.EnableDeviceVsync = !Device.EnableDeviceVsync; - } - - if ((currentHotkeyState.HasFlag(KeyboardHotkeyState.Screenshot) && - !_prevHotkeyState.HasFlag(KeyboardHotkeyState.Screenshot)) || ScreenshotRequested) - { - ScreenshotRequested = false; - - Renderer.Screenshot(); - } - - if (currentHotkeyState.HasFlag(KeyboardHotkeyState.ShowUi) && - !_prevHotkeyState.HasFlag(KeyboardHotkeyState.ShowUi)) - { - (Toplevel as MainWindow).ToggleExtraWidgets(true); - } - - if (currentHotkeyState.HasFlag(KeyboardHotkeyState.Pause) && - !_prevHotkeyState.HasFlag(KeyboardHotkeyState.Pause)) - { - (Toplevel as MainWindow)?.TogglePause(); - } - - if (currentHotkeyState.HasFlag(KeyboardHotkeyState.ToggleMute) && - !_prevHotkeyState.HasFlag(KeyboardHotkeyState.ToggleMute)) - { - if (Device.IsAudioMuted()) - { - Device.SetVolume(ConfigurationState.Instance.System.AudioVolume); - } - else - { - Device.SetVolume(0); - } - } - - if (currentHotkeyState.HasFlag(KeyboardHotkeyState.ResScaleUp) && - !_prevHotkeyState.HasFlag(KeyboardHotkeyState.ResScaleUp)) - { - GraphicsConfig.ResScale = GraphicsConfig.ResScale % MaxResolutionScale + 1; - } - - if (currentHotkeyState.HasFlag(KeyboardHotkeyState.ResScaleDown) && - !_prevHotkeyState.HasFlag(KeyboardHotkeyState.ResScaleDown)) - { - GraphicsConfig.ResScale = - (MaxResolutionScale + GraphicsConfig.ResScale - 2) % MaxResolutionScale + 1; - } - - if (currentHotkeyState.HasFlag(KeyboardHotkeyState.VolumeUp) && - !_prevHotkeyState.HasFlag(KeyboardHotkeyState.VolumeUp)) - { - _newVolume = MathF.Round((Device.GetVolume() + VolumeDelta), 2); - Device.SetVolume(_newVolume); - } - - if (currentHotkeyState.HasFlag(KeyboardHotkeyState.VolumeDown) && - !_prevHotkeyState.HasFlag(KeyboardHotkeyState.VolumeDown)) - { - _newVolume = MathF.Round((Device.GetVolume() - VolumeDelta), 2); - Device.SetVolume(_newVolume); - } - - _prevHotkeyState = currentHotkeyState; - } - - // Touchscreen - bool hasTouch = false; - - // Get screen touch position - if ((Toplevel as MainWindow).IsFocused && !ConfigurationState.Instance.Hid.EnableMouse) - { - hasTouch = TouchScreenManager.Update(true, (_inputManager.MouseDriver as GTK3MouseDriver).IsButtonPressed(MouseButton.Button1), ConfigurationState.Instance.Graphics.AspectRatio.Value.ToFloat()); - } - - if (!hasTouch) - { - TouchScreenManager.Update(false); - } - - Device.Hid.DebugPad.Update(); - - return true; - } - - [Flags] - private enum KeyboardHotkeyState - { - None = 0, - ToggleVSync = 1 << 0, - Screenshot = 1 << 1, - ShowUi = 1 << 2, - Pause = 1 << 3, - ToggleMute = 1 << 4, - ResScaleUp = 1 << 5, - ResScaleDown = 1 << 6, - VolumeUp = 1 << 7, - VolumeDown = 1 << 8, - } - - private KeyboardHotkeyState GetHotkeyState() - { - KeyboardHotkeyState state = KeyboardHotkeyState.None; - - if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.ToggleVsync)) - { - state |= KeyboardHotkeyState.ToggleVSync; - } - - if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.Screenshot)) - { - state |= KeyboardHotkeyState.Screenshot; - } - - if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.ShowUi)) - { - state |= KeyboardHotkeyState.ShowUi; - } - - if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.Pause)) - { - state |= KeyboardHotkeyState.Pause; - } - - if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.ToggleMute)) - { - state |= KeyboardHotkeyState.ToggleMute; - } - - if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.ResScaleUp)) - { - state |= KeyboardHotkeyState.ResScaleUp; - } - - if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.ResScaleDown)) - { - state |= KeyboardHotkeyState.ResScaleDown; - } - - if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.VolumeUp)) - { - state |= KeyboardHotkeyState.VolumeUp; - } - - if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.VolumeDown)) - { - state |= KeyboardHotkeyState.VolumeDown; - } - - return state; - } - } -} diff --git a/src/Ryujinx/Ui/StatusUpdatedEventArgs.cs b/src/Ryujinx/Ui/StatusUpdatedEventArgs.cs deleted file mode 100644 index 72e7d7f5b..000000000 --- a/src/Ryujinx/Ui/StatusUpdatedEventArgs.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System; - -namespace Ryujinx.Ui -{ - public class StatusUpdatedEventArgs : EventArgs - { - public bool VSyncEnabled; - public float Volume; - public string DockedMode; - public string AspectRatio; - public string GameStatus; - public string FifoStatus; - public string GpuName; - public string GpuBackend; - - public StatusUpdatedEventArgs(bool vSyncEnabled, float volume, string gpuBackend, string dockedMode, string aspectRatio, string gameStatus, string fifoStatus, string gpuName) - { - VSyncEnabled = vSyncEnabled; - Volume = volume; - GpuBackend = gpuBackend; - DockedMode = dockedMode; - AspectRatio = aspectRatio; - GameStatus = gameStatus; - FifoStatus = fifoStatus; - GpuName = gpuName; - } - } -} diff --git a/src/Ryujinx/Ui/VulkanRenderer.cs b/src/Ryujinx/Ui/VulkanRenderer.cs deleted file mode 100644 index e1aae0965..000000000 --- a/src/Ryujinx/Ui/VulkanRenderer.cs +++ /dev/null @@ -1,93 +0,0 @@ -using Gdk; -using Ryujinx.Common.Configuration; -using Ryujinx.Input.HLE; -using Ryujinx.Ui.Helper; -using SPB.Graphics.Vulkan; -using SPB.Platform.Metal; -using SPB.Platform.Win32; -using SPB.Platform.X11; -using SPB.Windowing; -using System; -using System.Runtime.InteropServices; - -namespace Ryujinx.Ui -{ - public partial class VulkanRenderer : RendererWidgetBase - { - public NativeWindowBase NativeWindow { get; private set; } - private UpdateBoundsCallbackDelegate _updateBoundsCallback; - - public VulkanRenderer(InputManager inputManager, GraphicsDebugLevel glLogLevel) : base(inputManager, glLogLevel) { } - - private NativeWindowBase RetrieveNativeWindow() - { - if (OperatingSystem.IsWindows()) - { - IntPtr windowHandle = gdk_win32_window_get_handle(Window.Handle); - - return new SimpleWin32Window(new NativeHandle(windowHandle)); - } - else if (OperatingSystem.IsLinux()) - { - IntPtr displayHandle = gdk_x11_display_get_xdisplay(Display.Handle); - IntPtr windowHandle = gdk_x11_window_get_xid(Window.Handle); - - return new SimpleX11Window(new NativeHandle(displayHandle), new NativeHandle(windowHandle)); - } - else if (OperatingSystem.IsMacOS()) - { - IntPtr metalLayer = MetalHelper.GetMetalLayer(Display, Window, out IntPtr nsView, out _updateBoundsCallback); - - return new SimpleMetalWindow(new NativeHandle(nsView), new NativeHandle(metalLayer)); - } - - throw new NotImplementedException(); - } - - [LibraryImport("libgdk-3-0.dll")] - private static partial IntPtr gdk_win32_window_get_handle(IntPtr d); - - [LibraryImport("libgdk-3.so.0")] - private static partial IntPtr gdk_x11_display_get_xdisplay(IntPtr gdkDisplay); - - [LibraryImport("libgdk-3.so.0")] - private static partial IntPtr gdk_x11_window_get_xid(IntPtr gdkWindow); - - protected override bool OnConfigureEvent(EventConfigure evnt) - { - if (NativeWindow == null) - { - NativeWindow = RetrieveNativeWindow(); - - WaitEvent.Set(); - } - - bool result = base.OnConfigureEvent(evnt); - - _updateBoundsCallback?.Invoke(Window); - - return result; - } - - public unsafe IntPtr CreateWindowSurface(IntPtr instance) - { - return VulkanHelper.CreateWindowSurface(instance, NativeWindow); - } - - public override void InitializeRenderer() { } - - public override void SwapBuffers() { } - - protected override string GetGpuBackendName() - { - return "Vulkan"; - } - - protected override void Dispose(bool disposing) - { - Device?.DisposeGpu(); - - NpadManager.Dispose(); - } - } -} diff --git a/src/Ryujinx/Ui/Widgets/GameTableContextMenu.Designer.cs b/src/Ryujinx/Ui/Widgets/GameTableContextMenu.Designer.cs deleted file mode 100644 index 734437eea..000000000 --- a/src/Ryujinx/Ui/Widgets/GameTableContextMenu.Designer.cs +++ /dev/null @@ -1,232 +0,0 @@ -using Gtk; - -namespace Ryujinx.Ui.Widgets -{ - public partial class GameTableContextMenu : Menu - { - private MenuItem _openSaveUserDirMenuItem; - private MenuItem _openSaveDeviceDirMenuItem; - private MenuItem _openSaveBcatDirMenuItem; - private MenuItem _manageTitleUpdatesMenuItem; - private MenuItem _manageDlcMenuItem; - private MenuItem _manageCheatMenuItem; - private MenuItem _openTitleModDirMenuItem; - private MenuItem _openTitleSdModDirMenuItem; - private Menu _extractSubMenu; - private MenuItem _extractMenuItem; - private MenuItem _extractRomFsMenuItem; - private MenuItem _extractExeFsMenuItem; - private MenuItem _extractLogoMenuItem; - private Menu _manageSubMenu; - private MenuItem _manageCacheMenuItem; - private MenuItem _purgePtcCacheMenuItem; - private MenuItem _purgeShaderCacheMenuItem; - private MenuItem _openPtcDirMenuItem; - private MenuItem _openShaderCacheDirMenuItem; - private MenuItem _createShortcutMenuItem; - - private void InitializeComponent() - { - // - // _openSaveUserDirMenuItem - // - _openSaveUserDirMenuItem = new MenuItem("Open User Save Directory") - { - TooltipText = "Open the directory which contains Application's User Saves.", - }; - _openSaveUserDirMenuItem.Activated += OpenSaveUserDir_Clicked; - - // - // _openSaveDeviceDirMenuItem - // - _openSaveDeviceDirMenuItem = new MenuItem("Open Device Save Directory") - { - TooltipText = "Open the directory which contains Application's Device Saves.", - }; - _openSaveDeviceDirMenuItem.Activated += OpenSaveDeviceDir_Clicked; - - // - // _openSaveBcatDirMenuItem - // - _openSaveBcatDirMenuItem = new MenuItem("Open BCAT Save Directory") - { - TooltipText = "Open the directory which contains Application's BCAT Saves.", - }; - _openSaveBcatDirMenuItem.Activated += OpenSaveBcatDir_Clicked; - - // - // _manageTitleUpdatesMenuItem - // - _manageTitleUpdatesMenuItem = new MenuItem("Manage Title Updates") - { - TooltipText = "Open the Title Update management window", - }; - _manageTitleUpdatesMenuItem.Activated += ManageTitleUpdates_Clicked; - - // - // _manageDlcMenuItem - // - _manageDlcMenuItem = new MenuItem("Manage DLC") - { - TooltipText = "Open the DLC management window", - }; - _manageDlcMenuItem.Activated += ManageDlc_Clicked; - - // - // _manageCheatMenuItem - // - _manageCheatMenuItem = new MenuItem("Manage Cheats") - { - TooltipText = "Open the Cheat management window", - }; - _manageCheatMenuItem.Activated += ManageCheats_Clicked; - - // - // _openTitleModDirMenuItem - // - _openTitleModDirMenuItem = new MenuItem("Open Mods Directory") - { - TooltipText = "Open the directory which contains Application's Mods.", - }; - _openTitleModDirMenuItem.Activated += OpenTitleModDir_Clicked; - - // - // _openTitleSdModDirMenuItem - // - _openTitleSdModDirMenuItem = new MenuItem("Open Atmosphere Mods Directory") - { - TooltipText = "Open the alternative SD card atmosphere directory which contains the Application's Mods.", - }; - _openTitleSdModDirMenuItem.Activated += OpenTitleSdModDir_Clicked; - - // - // _extractSubMenu - // - _extractSubMenu = new Menu(); - - // - // _extractMenuItem - // - _extractMenuItem = new MenuItem("Extract Data") - { - Submenu = _extractSubMenu - }; - - // - // _extractRomFsMenuItem - // - _extractRomFsMenuItem = new MenuItem("RomFS") - { - TooltipText = "Extract the RomFS section from Application's current config (including updates).", - }; - _extractRomFsMenuItem.Activated += ExtractRomFs_Clicked; - - // - // _extractExeFsMenuItem - // - _extractExeFsMenuItem = new MenuItem("ExeFS") - { - TooltipText = "Extract the ExeFS section from Application's current config (including updates).", - }; - _extractExeFsMenuItem.Activated += ExtractExeFs_Clicked; - - // - // _extractLogoMenuItem - // - _extractLogoMenuItem = new MenuItem("Logo") - { - TooltipText = "Extract the Logo section from Application's current config (including updates).", - }; - _extractLogoMenuItem.Activated += ExtractLogo_Clicked; - - // - // _manageSubMenu - // - _manageSubMenu = new Menu(); - - // - // _manageCacheMenuItem - // - _manageCacheMenuItem = new MenuItem("Cache Management") - { - Submenu = _manageSubMenu, - }; - - // - // _purgePtcCacheMenuItem - // - _purgePtcCacheMenuItem = new MenuItem("Queue PPTC Rebuild") - { - TooltipText = "Trigger PPTC to rebuild at boot time on the next game launch.", - }; - _purgePtcCacheMenuItem.Activated += PurgePtcCache_Clicked; - - // - // _purgeShaderCacheMenuItem - // - _purgeShaderCacheMenuItem = new MenuItem("Purge Shader Cache") - { - TooltipText = "Delete the Application's shader cache.", - }; - _purgeShaderCacheMenuItem.Activated += PurgeShaderCache_Clicked; - - // - // _openPtcDirMenuItem - // - _openPtcDirMenuItem = new MenuItem("Open PPTC Directory") - { - TooltipText = "Open the directory which contains the Application's PPTC cache.", - }; - _openPtcDirMenuItem.Activated += OpenPtcDir_Clicked; - - // - // _openShaderCacheDirMenuItem - // - _openShaderCacheDirMenuItem = new MenuItem("Open Shader Cache Directory") - { - TooltipText = "Open the directory which contains the Application's shader cache.", - }; - _openShaderCacheDirMenuItem.Activated += OpenShaderCacheDir_Clicked; - - // - // _createShortcutMenuItem - // - _createShortcutMenuItem = new MenuItem("Create Application Shortcut") - { - TooltipText = "Create a Desktop Shortcut that launches the selected Application." - }; - _createShortcutMenuItem.Activated += CreateShortcut_Clicked; - - ShowComponent(); - } - - private void ShowComponent() - { - _extractSubMenu.Append(_extractExeFsMenuItem); - _extractSubMenu.Append(_extractRomFsMenuItem); - _extractSubMenu.Append(_extractLogoMenuItem); - - _manageSubMenu.Append(_purgePtcCacheMenuItem); - _manageSubMenu.Append(_purgeShaderCacheMenuItem); - _manageSubMenu.Append(_openPtcDirMenuItem); - _manageSubMenu.Append(_openShaderCacheDirMenuItem); - - Add(_createShortcutMenuItem); - Add(new SeparatorMenuItem()); - Add(_openSaveUserDirMenuItem); - Add(_openSaveDeviceDirMenuItem); - Add(_openSaveBcatDirMenuItem); - Add(new SeparatorMenuItem()); - Add(_manageTitleUpdatesMenuItem); - Add(_manageDlcMenuItem); - Add(_manageCheatMenuItem); - Add(_openTitleModDirMenuItem); - Add(_openTitleSdModDirMenuItem); - Add(new SeparatorMenuItem()); - Add(_manageCacheMenuItem); - Add(_extractMenuItem); - - ShowAll(); - } - } -} diff --git a/src/Ryujinx/Ui/Widgets/GameTableContextMenu.cs b/src/Ryujinx/Ui/Widgets/GameTableContextMenu.cs deleted file mode 100644 index eb048b00d..000000000 --- a/src/Ryujinx/Ui/Widgets/GameTableContextMenu.cs +++ /dev/null @@ -1,644 +0,0 @@ -using Gtk; -using LibHac; -using LibHac.Account; -using LibHac.Common; -using LibHac.Fs; -using LibHac.Fs.Fsa; -using LibHac.Fs.Shim; -using LibHac.FsSystem; -using LibHac.Ns; -using LibHac.Tools.Fs; -using LibHac.Tools.FsSystem; -using LibHac.Tools.FsSystem.NcaUtils; -using Ryujinx.Common; -using Ryujinx.Common.Configuration; -using Ryujinx.Common.Logging; -using Ryujinx.HLE.FileSystem; -using Ryujinx.HLE.HOS; -using Ryujinx.HLE.HOS.Services.Account.Acc; -using Ryujinx.Ui.App.Common; -using Ryujinx.Ui.Common.Configuration; -using Ryujinx.Ui.Common.Helper; -using Ryujinx.Ui.Windows; -using System; -using System.Buffers; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Reflection; -using System.Threading; - -namespace Ryujinx.Ui.Widgets -{ - public partial class GameTableContextMenu : Menu - { - private readonly MainWindow _parent; - private readonly VirtualFileSystem _virtualFileSystem; - private readonly AccountManager _accountManager; - private readonly HorizonClient _horizonClient; - private readonly BlitStruct _controlData; - - private readonly string _titleFilePath; - private readonly string _titleName; - private readonly string _titleIdText; - private readonly ulong _titleId; - - private MessageDialog _dialog; - private bool _cancel; - - public GameTableContextMenu(MainWindow parent, VirtualFileSystem virtualFileSystem, AccountManager accountManager, HorizonClient horizonClient, string titleFilePath, string titleName, string titleId, BlitStruct controlData) - { - _parent = parent; - - InitializeComponent(); - - _virtualFileSystem = virtualFileSystem; - _accountManager = accountManager; - _horizonClient = horizonClient; - _titleFilePath = titleFilePath; - _titleName = titleName; - _titleIdText = titleId; - _controlData = controlData; - - if (!ulong.TryParse(_titleIdText, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out _titleId)) - { - GtkDialog.CreateErrorDialog("The selected game did not have a valid Title Id"); - - return; - } - - _openSaveUserDirMenuItem.Sensitive = !Utilities.IsZeros(controlData.ByteSpan) && controlData.Value.UserAccountSaveDataSize > 0; - _openSaveDeviceDirMenuItem.Sensitive = !Utilities.IsZeros(controlData.ByteSpan) && controlData.Value.DeviceSaveDataSize > 0; - _openSaveBcatDirMenuItem.Sensitive = !Utilities.IsZeros(controlData.ByteSpan) && controlData.Value.BcatDeliveryCacheStorageSize > 0; - - string fileExt = System.IO.Path.GetExtension(_titleFilePath).ToLower(); - bool hasNca = fileExt == ".nca" || fileExt == ".nsp" || fileExt == ".pfs0" || fileExt == ".xci"; - - _extractRomFsMenuItem.Sensitive = hasNca; - _extractExeFsMenuItem.Sensitive = hasNca; - _extractLogoMenuItem.Sensitive = hasNca; - - _createShortcutMenuItem.Sensitive = !ReleaseInformation.IsFlatHubBuild(); - - PopupAtPointer(null); - } - - private bool TryFindSaveData(string titleName, ulong titleId, BlitStruct controlHolder, in SaveDataFilter filter, out ulong saveDataId) - { - saveDataId = default; - - Result result = _horizonClient.Fs.FindSaveDataWithFilter(out SaveDataInfo saveDataInfo, SaveDataSpaceId.User, in filter); - - if (ResultFs.TargetNotFound.Includes(result)) - { - ref ApplicationControlProperty control = ref controlHolder.Value; - - Logger.Info?.Print(LogClass.Application, $"Creating save directory for Title: {titleName} [{titleId:x16}]"); - - if (Utilities.IsZeros(controlHolder.ByteSpan)) - { - // If the current application doesn't have a loaded control property, create a dummy one - // and set the savedata sizes so a user savedata will be created. - control = ref new BlitStruct(1).Value; - - // The set sizes don't actually matter as long as they're non-zero because we use directory savedata. - control.UserAccountSaveDataSize = 0x4000; - control.UserAccountSaveDataJournalSize = 0x4000; - - Logger.Warning?.Print(LogClass.Application, "No control file was found for this game. Using a dummy one instead. This may cause inaccuracies in some games."); - } - - Uid user = new((ulong)_accountManager.LastOpenedUser.UserId.High, (ulong)_accountManager.LastOpenedUser.UserId.Low); - - result = _horizonClient.Fs.EnsureApplicationSaveData(out _, new LibHac.Ncm.ApplicationId(titleId), in control, in user); - - if (result.IsFailure()) - { - GtkDialog.CreateErrorDialog($"There was an error creating the specified savedata: {result.ToStringWithName()}"); - - return false; - } - - // Try to find the savedata again after creating it - result = _horizonClient.Fs.FindSaveDataWithFilter(out saveDataInfo, SaveDataSpaceId.User, in filter); - } - - if (result.IsSuccess()) - { - saveDataId = saveDataInfo.SaveDataId; - - return true; - } - - GtkDialog.CreateErrorDialog($"There was an error finding the specified savedata: {result.ToStringWithName()}"); - - return false; - } - - private void OpenSaveDir(in SaveDataFilter saveDataFilter) - { - if (!TryFindSaveData(_titleName, _titleId, _controlData, in saveDataFilter, out ulong saveDataId)) - { - return; - } - - string saveRootPath = System.IO.Path.Combine(VirtualFileSystem.GetNandPath(), $"user/save/{saveDataId:x16}"); - - if (!Directory.Exists(saveRootPath)) - { - // Inconsistent state. Create the directory - Directory.CreateDirectory(saveRootPath); - } - - string committedPath = System.IO.Path.Combine(saveRootPath, "0"); - string workingPath = System.IO.Path.Combine(saveRootPath, "1"); - - // If the committed directory exists, that path will be loaded the next time the savedata is mounted - if (Directory.Exists(committedPath)) - { - OpenHelper.OpenFolder(committedPath); - } - else - { - // If the working directory exists and the committed directory doesn't, - // the working directory will be loaded the next time the savedata is mounted - if (!Directory.Exists(workingPath)) - { - Directory.CreateDirectory(workingPath); - } - - OpenHelper.OpenFolder(workingPath); - } - } - - private void ExtractSection(NcaSectionType ncaSectionType, int programIndex = 0) - { - FileChooserNative fileChooser = new("Choose the folder to extract into", _parent, FileChooserAction.SelectFolder, "Extract", "Cancel"); - - ResponseType response = (ResponseType)fileChooser.Run(); - string destination = fileChooser.Filename; - - fileChooser.Dispose(); - - if (response == ResponseType.Accept) - { - Thread extractorThread = new(() => - { - Gtk.Application.Invoke(delegate - { - _dialog = new MessageDialog(null, DialogFlags.DestroyWithParent, MessageType.Info, ButtonsType.Cancel, null) - { - Title = "Ryujinx - NCA Section Extractor", - Icon = new Gdk.Pixbuf(Assembly.GetAssembly(typeof(ConfigurationState)), "Ryujinx.Ui.Common.Resources.Logo_Ryujinx.png"), - SecondaryText = $"Extracting {ncaSectionType} section from {System.IO.Path.GetFileName(_titleFilePath)}...", - WindowPosition = WindowPosition.Center, - }; - - int dialogResponse = _dialog.Run(); - if (dialogResponse == (int)ResponseType.Cancel || dialogResponse == (int)ResponseType.DeleteEvent) - { - _cancel = true; - _dialog.Dispose(); - } - }); - - using FileStream file = new(_titleFilePath, FileMode.Open, FileAccess.Read); - - Nca mainNca = null; - Nca patchNca = null; - - if ((System.IO.Path.GetExtension(_titleFilePath).ToLower() == ".nsp") || - (System.IO.Path.GetExtension(_titleFilePath).ToLower() == ".pfs0") || - (System.IO.Path.GetExtension(_titleFilePath).ToLower() == ".xci")) - { - IFileSystem pfs; - - if (System.IO.Path.GetExtension(_titleFilePath) == ".xci") - { - Xci xci = new(_virtualFileSystem.KeySet, file.AsStorage()); - - pfs = xci.OpenPartition(XciPartitionType.Secure); - } - else - { - var pfsTemp = new PartitionFileSystem(); - pfsTemp.Initialize(file.AsStorage()).ThrowIfFailure(); - pfs = pfsTemp; - } - - foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca")) - { - using var ncaFile = new UniqueRef(); - - pfs.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); - - Nca nca = new(_virtualFileSystem.KeySet, ncaFile.Release().AsStorage()); - - if (nca.Header.ContentType == NcaContentType.Program) - { - int dataIndex = Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program); - - if (nca.SectionExists(NcaSectionType.Data) && nca.Header.GetFsHeader(dataIndex).IsPatchSection()) - { - patchNca = nca; - } - else - { - mainNca = nca; - } - } - } - } - else if (System.IO.Path.GetExtension(_titleFilePath).ToLower() == ".nca") - { - mainNca = new Nca(_virtualFileSystem.KeySet, file.AsStorage()); - } - - if (mainNca == null) - { - Logger.Error?.Print(LogClass.Application, "Extraction failure. The main NCA is not present in the selected file."); - - Gtk.Application.Invoke(delegate - { - GtkDialog.CreateErrorDialog("Extraction failure. The main NCA is not present in the selected file."); - }); - - return; - } - - (Nca updatePatchNca, _) = ApplicationLibrary.GetGameUpdateData(_virtualFileSystem, mainNca.Header.TitleId.ToString("x16"), programIndex, out _); - - if (updatePatchNca != null) - { - patchNca = updatePatchNca; - } - - int index = Nca.GetSectionIndexFromType(ncaSectionType, mainNca.Header.ContentType); - - bool sectionExistsInPatch = false; - - if (patchNca != null) - { - sectionExistsInPatch = patchNca.CanOpenSection(index); - } - - IFileSystem ncaFileSystem = sectionExistsInPatch ? mainNca.OpenFileSystemWithPatch(patchNca, index, IntegrityCheckLevel.ErrorOnInvalid) - : mainNca.OpenFileSystem(index, IntegrityCheckLevel.ErrorOnInvalid); - - FileSystemClient fsClient = _horizonClient.Fs; - - string source = DateTime.Now.ToFileTime().ToString()[10..]; - string output = DateTime.Now.ToFileTime().ToString()[10..]; - - using var uniqueSourceFs = new UniqueRef(ncaFileSystem); - using var uniqueOutputFs = new UniqueRef(new LocalFileSystem(destination)); - - fsClient.Register(source.ToU8Span(), ref uniqueSourceFs.Ref); - fsClient.Register(output.ToU8Span(), ref uniqueOutputFs.Ref); - - (Result? resultCode, bool canceled) = CopyDirectory(fsClient, $"{source}:/", $"{output}:/"); - - if (!canceled) - { - if (resultCode.Value.IsFailure()) - { - Logger.Error?.Print(LogClass.Application, $"LibHac returned error code: {resultCode.Value.ErrorCode}"); - - Gtk.Application.Invoke(delegate - { - _dialog?.Dispose(); - - GtkDialog.CreateErrorDialog("Extraction failed. Read the log file for further information."); - }); - } - else if (resultCode.Value.IsSuccess()) - { - Gtk.Application.Invoke(delegate - { - _dialog?.Dispose(); - - MessageDialog dialog = new(null, DialogFlags.DestroyWithParent, MessageType.Info, ButtonsType.Ok, null) - { - Title = "Ryujinx - NCA Section Extractor", - Icon = new Gdk.Pixbuf(Assembly.GetAssembly(typeof(ConfigurationState)), "Ryujinx.Ui.Common.Resources.Logo_Ryujinx.png"), - SecondaryText = "Extraction completed successfully.", - WindowPosition = WindowPosition.Center, - }; - - dialog.Run(); - dialog.Dispose(); - }); - } - } - - fsClient.Unmount(source.ToU8Span()); - fsClient.Unmount(output.ToU8Span()); - }) - { - Name = "GUI.NcaSectionExtractorThread", - IsBackground = true, - }; - extractorThread.Start(); - } - } - - private (Result? result, bool canceled) CopyDirectory(FileSystemClient fs, string sourcePath, string destPath) - { - Result rc = fs.OpenDirectory(out DirectoryHandle sourceHandle, sourcePath.ToU8Span(), OpenDirectoryMode.All); - if (rc.IsFailure()) - { - return (rc, false); - } - - using (sourceHandle) - { - foreach (DirectoryEntryEx entry in fs.EnumerateEntries(sourcePath, "*", SearchOptions.Default)) - { - if (_cancel) - { - return (null, true); - } - - string subSrcPath = PathTools.Normalize(PathTools.Combine(sourcePath, entry.Name)); - string subDstPath = PathTools.Normalize(PathTools.Combine(destPath, entry.Name)); - - if (entry.Type == DirectoryEntryType.Directory) - { - fs.EnsureDirectoryExists(subDstPath); - - (Result? result, bool canceled) = CopyDirectory(fs, subSrcPath, subDstPath); - if (canceled || result.Value.IsFailure()) - { - return (result, canceled); - } - } - - if (entry.Type == DirectoryEntryType.File) - { - fs.CreateOrOverwriteFile(subDstPath, entry.Size); - - rc = CopyFile(fs, subSrcPath, subDstPath); - if (rc.IsFailure()) - { - return (rc, false); - } - } - } - } - - return (Result.Success, false); - } - - public static Result CopyFile(FileSystemClient fs, string sourcePath, string destPath) - { - Result rc = fs.OpenFile(out FileHandle sourceHandle, sourcePath.ToU8Span(), OpenMode.Read); - if (rc.IsFailure()) - { - return rc; - } - - using (sourceHandle) - { - rc = fs.OpenFile(out FileHandle destHandle, destPath.ToU8Span(), OpenMode.Write | OpenMode.AllowAppend); - if (rc.IsFailure()) - { - return rc; - } - - using (destHandle) - { - const int MaxBufferSize = 1024 * 1024; - - rc = fs.GetFileSize(out long fileSize, sourceHandle); - if (rc.IsFailure()) - { - return rc; - } - - int bufferSize = (int)Math.Min(MaxBufferSize, fileSize); - - byte[] buffer = ArrayPool.Shared.Rent(bufferSize); - try - { - for (long offset = 0; offset < fileSize; offset += bufferSize) - { - int toRead = (int)Math.Min(fileSize - offset, bufferSize); - Span buf = buffer.AsSpan(0, toRead); - - rc = fs.ReadFile(out long _, sourceHandle, offset, buf); - if (rc.IsFailure()) - { - return rc; - } - - rc = fs.WriteFile(destHandle, offset, buf, WriteOption.None); - if (rc.IsFailure()) - { - return rc; - } - } - } - finally - { - ArrayPool.Shared.Return(buffer); - } - - rc = fs.FlushFile(destHandle); - if (rc.IsFailure()) - { - return rc; - } - } - } - - return Result.Success; - } - - // - // Events - // - private void OpenSaveUserDir_Clicked(object sender, EventArgs args) - { - var userId = new LibHac.Fs.UserId((ulong)_accountManager.LastOpenedUser.UserId.High, (ulong)_accountManager.LastOpenedUser.UserId.Low); - var saveDataFilter = SaveDataFilter.Make(_titleId, saveType: default, userId, saveDataId: default, index: default); - - OpenSaveDir(in saveDataFilter); - } - - private void OpenSaveDeviceDir_Clicked(object sender, EventArgs args) - { - var saveDataFilter = SaveDataFilter.Make(_titleId, SaveDataType.Device, userId: default, saveDataId: default, index: default); - - OpenSaveDir(in saveDataFilter); - } - - private void OpenSaveBcatDir_Clicked(object sender, EventArgs args) - { - var saveDataFilter = SaveDataFilter.Make(_titleId, SaveDataType.Bcat, userId: default, saveDataId: default, index: default); - - OpenSaveDir(in saveDataFilter); - } - - private void ManageTitleUpdates_Clicked(object sender, EventArgs args) - { - new TitleUpdateWindow(_parent, _virtualFileSystem, _titleIdText, _titleName).Show(); - } - - private void ManageDlc_Clicked(object sender, EventArgs args) - { - new DlcWindow(_virtualFileSystem, _titleIdText, _titleName).Show(); - } - - private void ManageCheats_Clicked(object sender, EventArgs args) - { - new CheatWindow(_virtualFileSystem, _titleId, _titleName, _titleFilePath).Show(); - } - - private void OpenTitleModDir_Clicked(object sender, EventArgs args) - { - string modsBasePath = ModLoader.GetModsBasePath(); - string titleModsPath = ModLoader.GetTitleDir(modsBasePath, _titleIdText); - - OpenHelper.OpenFolder(titleModsPath); - } - - private void OpenTitleSdModDir_Clicked(object sender, EventArgs args) - { - string sdModsBasePath = ModLoader.GetSdModsBasePath(); - string titleModsPath = ModLoader.GetTitleDir(sdModsBasePath, _titleIdText); - - OpenHelper.OpenFolder(titleModsPath); - } - - private void ExtractRomFs_Clicked(object sender, EventArgs args) - { - ExtractSection(NcaSectionType.Data); - } - - private void ExtractExeFs_Clicked(object sender, EventArgs args) - { - ExtractSection(NcaSectionType.Code); - } - - private void ExtractLogo_Clicked(object sender, EventArgs args) - { - ExtractSection(NcaSectionType.Logo); - } - - private void OpenPtcDir_Clicked(object sender, EventArgs args) - { - string ptcDir = System.IO.Path.Combine(AppDataManager.GamesDirPath, _titleIdText, "cache", "cpu"); - - string mainPath = System.IO.Path.Combine(ptcDir, "0"); - string backupPath = System.IO.Path.Combine(ptcDir, "1"); - - if (!Directory.Exists(ptcDir)) - { - Directory.CreateDirectory(ptcDir); - Directory.CreateDirectory(mainPath); - Directory.CreateDirectory(backupPath); - } - - OpenHelper.OpenFolder(ptcDir); - } - - private void OpenShaderCacheDir_Clicked(object sender, EventArgs args) - { - string shaderCacheDir = System.IO.Path.Combine(AppDataManager.GamesDirPath, _titleIdText, "cache", "shader"); - - if (!Directory.Exists(shaderCacheDir)) - { - Directory.CreateDirectory(shaderCacheDir); - } - - OpenHelper.OpenFolder(shaderCacheDir); - } - - private void PurgePtcCache_Clicked(object sender, EventArgs args) - { - DirectoryInfo mainDir = new(System.IO.Path.Combine(AppDataManager.GamesDirPath, _titleIdText, "cache", "cpu", "0")); - DirectoryInfo backupDir = new(System.IO.Path.Combine(AppDataManager.GamesDirPath, _titleIdText, "cache", "cpu", "1")); - - MessageDialog warningDialog = GtkDialog.CreateConfirmationDialog("Warning", $"You are about to queue a PPTC rebuild on the next boot of:\n\n{_titleName}\n\nAre you sure you want to proceed?"); - - List cacheFiles = new(); - - if (mainDir.Exists) - { - cacheFiles.AddRange(mainDir.EnumerateFiles("*.cache")); - } - - if (backupDir.Exists) - { - cacheFiles.AddRange(backupDir.EnumerateFiles("*.cache")); - } - - if (cacheFiles.Count > 0 && warningDialog.Run() == (int)ResponseType.Yes) - { - foreach (FileInfo file in cacheFiles) - { - try - { - file.Delete(); - } - catch (Exception e) - { - GtkDialog.CreateErrorDialog($"Error purging PPTC cache {file.Name}: {e}"); - } - } - } - - warningDialog.Dispose(); - } - - private void PurgeShaderCache_Clicked(object sender, EventArgs args) - { - DirectoryInfo shaderCacheDir = new(System.IO.Path.Combine(AppDataManager.GamesDirPath, _titleIdText, "cache", "shader")); - - using MessageDialog warningDialog = GtkDialog.CreateConfirmationDialog("Warning", $"You are about to delete the shader cache for :\n\n{_titleName}\n\nAre you sure you want to proceed?"); - - List oldCacheDirectories = new(); - List newCacheFiles = new(); - - if (shaderCacheDir.Exists) - { - oldCacheDirectories.AddRange(shaderCacheDir.EnumerateDirectories("*")); - newCacheFiles.AddRange(shaderCacheDir.GetFiles("*.toc")); - newCacheFiles.AddRange(shaderCacheDir.GetFiles("*.data")); - } - - if ((oldCacheDirectories.Count > 0 || newCacheFiles.Count > 0) && warningDialog.Run() == (int)ResponseType.Yes) - { - foreach (DirectoryInfo directory in oldCacheDirectories) - { - try - { - directory.Delete(true); - } - catch (Exception e) - { - GtkDialog.CreateErrorDialog($"Error purging shader cache at {directory.Name}: {e}"); - } - } - - foreach (FileInfo file in newCacheFiles) - { - try - { - file.Delete(); - } - catch (Exception e) - { - GtkDialog.CreateErrorDialog($"Error purging shader cache at {file.Name}: {e}"); - } - } - } - } - - private void CreateShortcut_Clicked(object sender, EventArgs args) - { - byte[] appIcon = new ApplicationLibrary(_virtualFileSystem).GetApplicationIcon(_titleFilePath, ConfigurationState.Instance.System.Language); - ShortcutHelper.CreateAppShortcut(_titleFilePath, _titleName, _titleIdText, appIcon); - } - } -} diff --git a/src/Ryujinx/Ui/Widgets/GtkDialog.cs b/src/Ryujinx/Ui/Widgets/GtkDialog.cs deleted file mode 100644 index 51e777fa1..000000000 --- a/src/Ryujinx/Ui/Widgets/GtkDialog.cs +++ /dev/null @@ -1,114 +0,0 @@ -using Gtk; -using Ryujinx.Common.Logging; -using Ryujinx.Ui.Common.Configuration; -using System.Collections.Generic; -using System.Reflection; - -namespace Ryujinx.Ui.Widgets -{ - internal class GtkDialog : MessageDialog - { - private static bool _isChoiceDialogOpen; - - private GtkDialog(string title, string mainText, string secondaryText, MessageType messageType = MessageType.Other, ButtonsType buttonsType = ButtonsType.Ok) - : base(null, DialogFlags.Modal, messageType, buttonsType, null) - { - Title = title; - Icon = new Gdk.Pixbuf(Assembly.GetAssembly(typeof(ConfigurationState)), "Ryujinx.Ui.Common.Resources.Logo_Ryujinx.png"); - Text = mainText; - SecondaryText = secondaryText; - WindowPosition = WindowPosition.Center; - SecondaryUseMarkup = true; - - Response += GtkDialog_Response; - - SetSizeRequest(200, 20); - } - - private void GtkDialog_Response(object sender, ResponseArgs args) - { - Dispose(); - } - - internal static void CreateInfoDialog(string mainText, string secondaryText) - { - new GtkDialog("Ryujinx - Info", mainText, secondaryText, MessageType.Info).Run(); - } - - internal static void CreateUpdaterInfoDialog(string mainText, string secondaryText) - { - new GtkDialog("Ryujinx - Updater", mainText, secondaryText, MessageType.Info).Run(); - } - - internal static MessageDialog CreateWaitingDialog(string mainText, string secondaryText) - { - return new GtkDialog("Ryujinx - Waiting", mainText, secondaryText, MessageType.Info, ButtonsType.None); - } - - internal static void CreateWarningDialog(string mainText, string secondaryText) - { - new GtkDialog("Ryujinx - Warning", mainText, secondaryText, MessageType.Warning).Run(); - } - - internal static void CreateErrorDialog(string errorMessage) - { - Logger.Error?.Print(LogClass.Application, errorMessage); - - new GtkDialog("Ryujinx - Error", "Ryujinx has encountered an error", errorMessage, MessageType.Error).Run(); - } - - internal static MessageDialog CreateConfirmationDialog(string mainText, string secondaryText = "") - { - return new GtkDialog("Ryujinx - Confirmation", mainText, secondaryText, MessageType.Question, ButtonsType.YesNo); - } - - internal static bool CreateChoiceDialog(string title, string mainText, string secondaryText) - { - if (_isChoiceDialogOpen) - { - return false; - } - - _isChoiceDialogOpen = true; - - ResponseType response = (ResponseType)new GtkDialog(title, mainText, secondaryText, MessageType.Question, ButtonsType.YesNo).Run(); - - _isChoiceDialogOpen = false; - - return response == ResponseType.Yes; - } - - internal static ResponseType CreateCustomDialog(string title, string mainText, string secondaryText, Dictionary buttons, MessageType messageType = MessageType.Other) - { - GtkDialog gtkDialog = new(title, mainText, secondaryText, messageType, ButtonsType.None); - - foreach (var button in buttons) - { - gtkDialog.AddButton(button.Value, button.Key); - } - - return (ResponseType)gtkDialog.Run(); - } - - internal static string CreateInputDialog(Window parent, string title, string mainText, uint inputMax) - { - GtkInputDialog gtkDialog = new(parent, title, mainText, inputMax); - ResponseType response = (ResponseType)gtkDialog.Run(); - string responseText = gtkDialog.InputEntry.Text.TrimEnd(); - - gtkDialog.Dispose(); - - if (response == ResponseType.Ok) - { - return responseText; - } - - return ""; - } - - internal static bool CreateExitDialog() - { - return CreateChoiceDialog("Ryujinx - Exit", "Are you sure you want to close Ryujinx?", "All unsaved data will be lost!"); - } - } -} diff --git a/src/Ryujinx/Ui/Widgets/GtkInputDialog.cs b/src/Ryujinx/Ui/Widgets/GtkInputDialog.cs deleted file mode 100644 index 622980921..000000000 --- a/src/Ryujinx/Ui/Widgets/GtkInputDialog.cs +++ /dev/null @@ -1,37 +0,0 @@ -using Gtk; - -namespace Ryujinx.Ui.Widgets -{ - public class GtkInputDialog : MessageDialog - { - public Entry InputEntry { get; } - - public GtkInputDialog(Window parent, string title, string mainText, uint inputMax) : base(parent, DialogFlags.Modal | DialogFlags.DestroyWithParent, MessageType.Question, ButtonsType.OkCancel, null) - { - SetDefaultSize(300, 0); - - Title = title; - - Label mainTextLabel = new() - { - Text = mainText, - }; - - InputEntry = new Entry - { - MaxLength = (int)inputMax, - }; - - Label inputMaxTextLabel = new() - { - Text = $"(Max length: {inputMax})", - }; - - ((Box)MessageArea).PackStart(mainTextLabel, true, true, 0); - ((Box)MessageArea).PackStart(InputEntry, true, true, 5); - ((Box)MessageArea).PackStart(inputMaxTextLabel, true, true, 0); - - ShowAll(); - } - } -} diff --git a/src/Ryujinx/Ui/Widgets/ProfileDialog.cs b/src/Ryujinx/Ui/Widgets/ProfileDialog.cs deleted file mode 100644 index 0731b37a1..000000000 --- a/src/Ryujinx/Ui/Widgets/ProfileDialog.cs +++ /dev/null @@ -1,57 +0,0 @@ -using Gtk; -using Ryujinx.Ui.Common.Configuration; -using System; -using System.Reflection; -using GUI = Gtk.Builder.ObjectAttribute; - -namespace Ryujinx.Ui.Widgets -{ - public class ProfileDialog : Dialog - { - public string FileName { get; private set; } - -#pragma warning disable CS0649, IDE0044 // Field is never assigned to, Add readonly modifier - [GUI] Entry _profileEntry; - [GUI] Label _errorMessage; -#pragma warning restore CS0649, IDE0044 - - public ProfileDialog() : this(new Builder("Ryujinx.Ui.Widgets.ProfileDialog.glade")) { } - - private ProfileDialog(Builder builder) : base(builder.GetRawOwnedObject("_profileDialog")) - { - builder.Autoconnect(this); - Icon = new Gdk.Pixbuf(Assembly.GetAssembly(typeof(ConfigurationState)), "Ryujinx.Ui.Common.Resources.Logo_Ryujinx.png"); - } - - private void OkToggle_Activated(object sender, EventArgs args) - { - ((ToggleButton)sender).SetStateFlags(StateFlags.Normal, true); - - bool validFileName = true; - - foreach (char invalidChar in System.IO.Path.GetInvalidFileNameChars()) - { - if (_profileEntry.Text.Contains(invalidChar)) - { - validFileName = false; - } - } - - if (validFileName && !string.IsNullOrEmpty(_profileEntry.Text)) - { - FileName = $"{_profileEntry.Text}.json"; - - Respond(ResponseType.Ok); - } - else - { - _errorMessage.Text = "The file name contains invalid characters. Please try again."; - } - } - - private void CancelToggle_Activated(object sender, EventArgs args) - { - Respond(ResponseType.Cancel); - } - } -} diff --git a/src/Ryujinx/Ui/Widgets/ProfileDialog.glade b/src/Ryujinx/Ui/Widgets/ProfileDialog.glade deleted file mode 100644 index adaf6608f..000000000 --- a/src/Ryujinx/Ui/Widgets/ProfileDialog.glade +++ /dev/null @@ -1,124 +0,0 @@ - - - - - - False - Ryujinx - Profile Dialog - True - center - 400 - dialog - - - - - - False - vertical - 2 - - - False - end - - - OK - True - True - True - - - - False - True - 0 - - - - - Cancel - True - True - True - - - - False - True - 5 - 1 - - - - - False - False - 0 - - - - - True - False - vertical - - - True - False - 10 - 10 - 20 - 10 - Enter a name for the new profile: - - - True - True - 0 - - - - - True - True - 20 - 20 - 20 - - - True - True - 1 - - - - - True - False - start - 20 - 10 - 10 - 10 - - - - - - False - True - 2 - - - - - True - True - 1 - - - - - - diff --git a/src/Ryujinx/Ui/Widgets/RawInputToTextEntry.cs b/src/Ryujinx/Ui/Widgets/RawInputToTextEntry.cs deleted file mode 100644 index e82a3d492..000000000 --- a/src/Ryujinx/Ui/Widgets/RawInputToTextEntry.cs +++ /dev/null @@ -1,27 +0,0 @@ -using Gtk; - -namespace Ryujinx.Ui.Widgets -{ - public class RawInputToTextEntry : Entry - { - public void SendKeyPressEvent(object o, KeyPressEventArgs args) - { - base.OnKeyPressEvent(args.Event); - } - - public void SendKeyReleaseEvent(object o, KeyReleaseEventArgs args) - { - base.OnKeyReleaseEvent(args.Event); - } - - public void SendButtonPressEvent(object o, ButtonPressEventArgs args) - { - base.OnButtonPressEvent(args.Event); - } - - public void SendButtonReleaseEvent(object o, ButtonReleaseEventArgs args) - { - base.OnButtonReleaseEvent(args.Event); - } - } -} diff --git a/src/Ryujinx/Ui/Widgets/UserErrorDialog.cs b/src/Ryujinx/Ui/Widgets/UserErrorDialog.cs deleted file mode 100644 index 63a280e6d..000000000 --- a/src/Ryujinx/Ui/Widgets/UserErrorDialog.cs +++ /dev/null @@ -1,123 +0,0 @@ -using Gtk; -using Ryujinx.Ui.Common; -using Ryujinx.Ui.Common.Helper; - -namespace Ryujinx.Ui.Widgets -{ - internal class UserErrorDialog : MessageDialog - { - private const string SetupGuideUrl = "https://github.com/Ryujinx/Ryujinx/wiki/Ryujinx-Setup-&-Configuration-Guide"; - private const int OkResponseId = 0; - private const int SetupGuideResponseId = 1; - - private readonly UserError _userError; - - private UserErrorDialog(UserError error) : base(null, DialogFlags.Modal, MessageType.Error, ButtonsType.None, null) - { - _userError = error; - - WindowPosition = WindowPosition.Center; - SecondaryUseMarkup = true; - - Response += UserErrorDialog_Response; - - SetSizeRequest(120, 50); - - AddButton("OK", OkResponseId); - - bool isInSetupGuide = IsCoveredBySetupGuide(error); - - if (isInSetupGuide) - { - AddButton("Open the Setup Guide", SetupGuideResponseId); - } - - string errorCode = GetErrorCode(error); - - SecondaryUseMarkup = true; - - Title = $"Ryujinx error ({errorCode})"; - Text = $"{errorCode}: {GetErrorTitle(error)}"; - SecondaryText = GetErrorDescription(error); - - if (isInSetupGuide) - { - SecondaryText += "\nFor more information on how to fix this error, follow our Setup Guide."; - } - } - - private static string GetErrorCode(UserError error) - { - return $"RYU-{(uint)error:X4}"; - } - - private static string GetErrorTitle(UserError error) - { - return error switch - { - UserError.NoKeys => "Keys not found", - UserError.NoFirmware => "Firmware not found", - UserError.FirmwareParsingFailed => "Firmware parsing error", - UserError.ApplicationNotFound => "Application not found", - UserError.Unknown => "Unknown error", - _ => "Undefined error", - }; - } - - private static string GetErrorDescription(UserError error) - { - return error switch - { - UserError.NoKeys => "Ryujinx was unable to find your 'prod.keys' file", - UserError.NoFirmware => "Ryujinx was unable to find any firmwares installed", - UserError.FirmwareParsingFailed => "Ryujinx was unable to parse the provided firmware. This is usually caused by outdated keys.", - UserError.ApplicationNotFound => "Ryujinx couldn't find a valid application at the given path.", - UserError.Unknown => "An unknown error occured!", - _ => "An undefined error occured! This shouldn't happen, please contact a dev!", - }; - } - - private static bool IsCoveredBySetupGuide(UserError error) - { - return error switch - { - UserError.NoKeys or - UserError.NoFirmware or - UserError.FirmwareParsingFailed => true, - _ => false, - }; - } - - private static string GetSetupGuideUrl(UserError error) - { - if (!IsCoveredBySetupGuide(error)) - { - return null; - } - - return error switch - { - UserError.NoKeys => SetupGuideUrl + "#initial-setup---placement-of-prodkeys", - UserError.NoFirmware => SetupGuideUrl + "#initial-setup-continued---installation-of-firmware", - _ => SetupGuideUrl, - }; - } - - private void UserErrorDialog_Response(object sender, ResponseArgs args) - { - int responseId = (int)args.ResponseId; - - if (responseId == SetupGuideResponseId) - { - OpenHelper.OpenUrl(GetSetupGuideUrl(_userError)); - } - - Dispose(); - } - - public static void CreateUserErrorDialog(UserError error) - { - new UserErrorDialog(error).Run(); - } - } -} diff --git a/src/Ryujinx/Ui/Windows/AboutWindow.Designer.cs b/src/Ryujinx/Ui/Windows/AboutWindow.Designer.cs deleted file mode 100644 index 345026334..000000000 --- a/src/Ryujinx/Ui/Windows/AboutWindow.Designer.cs +++ /dev/null @@ -1,511 +0,0 @@ -using Gtk; -using Pango; -using Ryujinx.Ui.Common.Configuration; -using System.Reflection; - -namespace Ryujinx.Ui.Windows -{ - public partial class AboutWindow : Window - { - private Box _mainBox; - private Box _leftBox; - private Box _logoBox; - private Image _ryujinxLogo; - private Box _logoTextBox; - private Label _ryujinxLabel; - private Label _ryujinxPhoneticLabel; - private EventBox _ryujinxLink; - private Label _ryujinxLinkLabel; - private Label _versionLabel; - private Label _disclaimerLabel; - private EventBox _amiiboApiLink; - private Label _amiiboApiLinkLabel; - private Box _socialBox; - private EventBox _patreonEventBox; - private Box _patreonBox; - private Image _patreonLogo; - private Label _patreonLabel; - private EventBox _githubEventBox; - private Box _githubBox; - private Image _githubLogo; - private Label _githubLabel; - private Box _discordBox; - private EventBox _discordEventBox; - private Image _discordLogo; - private Label _discordLabel; - private EventBox _twitterEventBox; - private Box _twitterBox; - private Image _twitterLogo; - private Label _twitterLabel; - private Separator _separator; - private Box _rightBox; - private Label _aboutLabel; - private Label _aboutDescriptionLabel; - private Label _createdByLabel; - private TextView _createdByText; - private EventBox _contributorsEventBox; - private Label _contributorsLinkLabel; - private Label _patreonNamesLabel; - private ScrolledWindow _patreonNamesScrolled; - private TextView _patreonNamesText; - private EventBox _changelogEventBox; - private Label _changelogLinkLabel; - - private void InitializeComponent() - { - - // - // AboutWindow - // - CanFocus = false; - Resizable = false; - Modal = true; - WindowPosition = WindowPosition.Center; - DefaultWidth = 800; - DefaultHeight = 450; - TypeHint = Gdk.WindowTypeHint.Dialog; - - // - // _mainBox - // - _mainBox = new Box(Orientation.Horizontal, 0); - - // - // _leftBox - // - _leftBox = new Box(Orientation.Vertical, 0) - { - Margin = 15, - MarginStart = 30, - MarginEnd = 0, - }; - - // - // _logoBox - // - _logoBox = new Box(Orientation.Horizontal, 0); - - // - // _ryujinxLogo - // - _ryujinxLogo = new Image(new Gdk.Pixbuf(Assembly.GetAssembly(typeof(ConfigurationState)), "Ryujinx.Ui.Common.Resources.Logo_Ryujinx.png", 100, 100)) - { - Margin = 10, - MarginStart = 15, - }; - - // - // _logoTextBox - // - _logoTextBox = new Box(Orientation.Vertical, 0); - - // - // _ryujinxLabel - // - _ryujinxLabel = new Label("Ryujinx") - { - MarginTop = 15, - Justify = Justification.Center, - Attributes = new AttrList(), - }; - _ryujinxLabel.Attributes.Insert(new Pango.AttrScale(2.7f)); - - // - // _ryujinxPhoneticLabel - // - _ryujinxPhoneticLabel = new Label("(REE-YOU-JINX)") - { - Justify = Justification.Center, - }; - - // - // _ryujinxLink - // - _ryujinxLink = new EventBox() - { - Margin = 5 - }; - _ryujinxLink.ButtonPressEvent += RyujinxButton_Pressed; - - // - // _ryujinxLinkLabel - // - _ryujinxLinkLabel = new Label("www.ryujinx.org") - { - TooltipText = "Click to open the Ryujinx website in your default browser.", - Justify = Justification.Center, - Attributes = new AttrList(), - }; - _ryujinxLinkLabel.Attributes.Insert(new Pango.AttrUnderline(Underline.Single)); - - // - // _versionLabel - // - _versionLabel = new Label(Program.Version) - { - Expand = true, - Justify = Justification.Center, - Margin = 5, - }; - - // - // _changelogEventBox - // - _changelogEventBox = new EventBox(); - _changelogEventBox.ButtonPressEvent += ChangelogButton_Pressed; - - // - // _changelogLinkLabel - // - _changelogLinkLabel = new Label("View Changelog on GitHub") - { - TooltipText = "Click to open the changelog for this version in your default browser.", - Justify = Justification.Center, - Attributes = new AttrList(), - }; - _changelogLinkLabel.Attributes.Insert(new Pango.AttrUnderline(Underline.Single)); - - // - // _disclaimerLabel - // - _disclaimerLabel = new Label("Ryujinx is not affiliated with Nintendo™,\nor any of its partners, in any way.") - { - Expand = true, - Justify = Justification.Center, - Margin = 5, - Attributes = new AttrList(), - }; - _disclaimerLabel.Attributes.Insert(new Pango.AttrScale(0.8f)); - - // - // _amiiboApiLink - // - _amiiboApiLink = new EventBox() - { - Margin = 5, - }; - _amiiboApiLink.ButtonPressEvent += AmiiboApiButton_Pressed; - - // - // _amiiboApiLinkLabel - // - _amiiboApiLinkLabel = new Label("AmiiboAPI (www.amiiboapi.com) is used\nin our Amiibo emulation.") - { - TooltipText = "Click to open the AmiiboAPI website in your default browser.", - Justify = Justification.Center, - Attributes = new AttrList(), - }; - _amiiboApiLinkLabel.Attributes.Insert(new Pango.AttrScale(0.9f)); - - // - // _socialBox - // - _socialBox = new Box(Orientation.Horizontal, 0) - { - Margin = 25, - MarginBottom = 10, - }; - - // - // _patreonEventBox - // - _patreonEventBox = new EventBox() - { - TooltipText = "Click to open the Ryujinx Patreon page in your default browser.", - }; - _patreonEventBox.ButtonPressEvent += PatreonButton_Pressed; - - // - // _patreonBox - // - _patreonBox = new Box(Orientation.Vertical, 0); - - // - // _patreonLogo - // - _patreonLogo = new Image(new Gdk.Pixbuf(Assembly.GetAssembly(typeof(ConfigurationState)), "Ryujinx.Ui.Common.Resources.Logo_Patreon_Light.png", 30, 30)) - { - Margin = 10, - }; - - // - // _patreonLabel - // - _patreonLabel = new Label("Patreon") - { - Justify = Justification.Center, - }; - - // - // _githubEventBox - // - _githubEventBox = new EventBox() - { - TooltipText = "Click to open the Ryujinx GitHub page in your default browser.", - }; - _githubEventBox.ButtonPressEvent += GitHubButton_Pressed; - - // - // _githubBox - // - _githubBox = new Box(Orientation.Vertical, 0); - - // - // _githubLogo - // - _githubLogo = new Image(new Gdk.Pixbuf(Assembly.GetAssembly(typeof(ConfigurationState)), "Ryujinx.Ui.Common.Resources.Logo_GitHub_Light.png", 30, 30)) - { - Margin = 10, - }; - - // - // _githubLabel - // - _githubLabel = new Label("GitHub") - { - Justify = Justification.Center, - }; - - // - // _discordBox - // - _discordBox = new Box(Orientation.Vertical, 0); - - // - // _discordEventBox - // - _discordEventBox = new EventBox() - { - TooltipText = "Click to open an invite to the Ryujinx Discord server in your default browser.", - }; - _discordEventBox.ButtonPressEvent += DiscordButton_Pressed; - - // - // _discordLogo - // - _discordLogo = new Image(new Gdk.Pixbuf(Assembly.GetAssembly(typeof(ConfigurationState)), "Ryujinx.Ui.Common.Resources.Logo_Discord_Light.png", 30, 30)) - { - Margin = 10, - }; - - // - // _discordLabel - // - _discordLabel = new Label("Discord") - { - Justify = Justification.Center, - }; - - // - // _twitterEventBox - // - _twitterEventBox = new EventBox() - { - TooltipText = "Click to open the Ryujinx Twitter page in your default browser.", - }; - _twitterEventBox.ButtonPressEvent += TwitterButton_Pressed; - - // - // _twitterBox - // - _twitterBox = new Box(Orientation.Vertical, 0); - - // - // _twitterLogo - // - _twitterLogo = new Image(new Gdk.Pixbuf(Assembly.GetAssembly(typeof(ConfigurationState)), "Ryujinx.Ui.Common.Resources.Logo_Twitter_Light.png", 30, 30)) - { - Margin = 10, - }; - - // - // _twitterLabel - // - _twitterLabel = new Label("Twitter") - { - Justify = Justification.Center, - }; - - // - // _separator - // - _separator = new Separator(Orientation.Vertical) - { - Margin = 15, - }; - - // - // _rightBox - // - _rightBox = new Box(Orientation.Vertical, 0) - { - Margin = 15, - MarginTop = 40, - }; - - // - // _aboutLabel - // - _aboutLabel = new Label("About :") - { - Halign = Align.Start, - Attributes = new AttrList(), - }; - _aboutLabel.Attributes.Insert(new Pango.AttrWeight(Weight.Bold)); - _aboutLabel.Attributes.Insert(new Pango.AttrUnderline(Underline.Single)); - - // - // _aboutDescriptionLabel - // - _aboutDescriptionLabel = new Label("Ryujinx is an emulator for the Nintendo Switch™.\n" + - "Please support us on Patreon.\n" + - "Get all the latest news on our Twitter or Discord.\n" + - "Developers interested in contributing can find out more on our GitHub or Discord.") - { - Margin = 15, - Halign = Align.Start, - }; - - // - // _createdByLabel - // - _createdByLabel = new Label("Maintained by :") - { - Halign = Align.Start, - Attributes = new AttrList(), - }; - _createdByLabel.Attributes.Insert(new Pango.AttrWeight(Weight.Bold)); - _createdByLabel.Attributes.Insert(new Pango.AttrUnderline(Underline.Single)); - - // - // _createdByText - // - _createdByText = new TextView() - { - WrapMode = Gtk.WrapMode.Word, - Editable = false, - CursorVisible = false, - Margin = 15, - MarginEnd = 30, - }; - _createdByText.Buffer.Text = "gdkchan, Ac_K, Thog, rip in peri peri, LDj3SNuD, emmaus, Thealexbarney, Xpl0itR, GoffyDude, »jD« and more..."; - - // - // _contributorsEventBox - // - _contributorsEventBox = new EventBox(); - _contributorsEventBox.ButtonPressEvent += ContributorsButton_Pressed; - - // - // _contributorsLinkLabel - // - _contributorsLinkLabel = new Label("See All Contributors...") - { - TooltipText = "Click to open the Contributors page in your default browser.", - MarginEnd = 30, - Halign = Align.End, - Attributes = new AttrList(), - }; - _contributorsLinkLabel.Attributes.Insert(new Pango.AttrUnderline(Underline.Single)); - - // - // _patreonNamesLabel - // - _patreonNamesLabel = new Label("Supported on Patreon by :") - { - Halign = Align.Start, - Attributes = new AttrList(), - }; - _patreonNamesLabel.Attributes.Insert(new Pango.AttrWeight(Weight.Bold)); - _patreonNamesLabel.Attributes.Insert(new Pango.AttrUnderline(Underline.Single)); - - // - // _patreonNamesScrolled - // - _patreonNamesScrolled = new ScrolledWindow() - { - Margin = 15, - MarginEnd = 30, - Expand = true, - ShadowType = ShadowType.In, - }; - _patreonNamesScrolled.SetPolicy(PolicyType.Never, PolicyType.Automatic); - - // - // _patreonNamesText - // - _patreonNamesText = new TextView() - { - WrapMode = Gtk.WrapMode.Word, - }; - _patreonNamesText.Buffer.Text = "Loading..."; - _patreonNamesText.SetProperty("editable", new GLib.Value(false)); - - ShowComponent(); - } - - private void ShowComponent() - { - _logoBox.Add(_ryujinxLogo); - - _ryujinxLink.Add(_ryujinxLinkLabel); - - _logoTextBox.Add(_ryujinxLabel); - _logoTextBox.Add(_ryujinxPhoneticLabel); - _logoTextBox.Add(_ryujinxLink); - - _logoBox.Add(_logoTextBox); - - _amiiboApiLink.Add(_amiiboApiLinkLabel); - - _patreonBox.Add(_patreonLogo); - _patreonBox.Add(_patreonLabel); - _patreonEventBox.Add(_patreonBox); - - _githubBox.Add(_githubLogo); - _githubBox.Add(_githubLabel); - _githubEventBox.Add(_githubBox); - - _discordBox.Add(_discordLogo); - _discordBox.Add(_discordLabel); - _discordEventBox.Add(_discordBox); - - _twitterBox.Add(_twitterLogo); - _twitterBox.Add(_twitterLabel); - _twitterEventBox.Add(_twitterBox); - - _socialBox.Add(_patreonEventBox); - _socialBox.Add(_githubEventBox); - _socialBox.Add(_discordEventBox); - _socialBox.Add(_twitterEventBox); - - _changelogEventBox.Add(_changelogLinkLabel); - - _leftBox.Add(_logoBox); - _leftBox.Add(_versionLabel); - _leftBox.Add(_changelogEventBox); - _leftBox.Add(_disclaimerLabel); - _leftBox.Add(_amiiboApiLink); - _leftBox.Add(_socialBox); - - _contributorsEventBox.Add(_contributorsLinkLabel); - _patreonNamesScrolled.Add(_patreonNamesText); - - _rightBox.Add(_aboutLabel); - _rightBox.Add(_aboutDescriptionLabel); - _rightBox.Add(_createdByLabel); - _rightBox.Add(_createdByText); - _rightBox.Add(_contributorsEventBox); - _rightBox.Add(_patreonNamesLabel); - _rightBox.Add(_patreonNamesScrolled); - - _mainBox.Add(_leftBox); - _mainBox.Add(_separator); - _mainBox.Add(_rightBox); - - Add(_mainBox); - - ShowAll(); - } - } -} diff --git a/src/Ryujinx/Ui/Windows/AboutWindow.cs b/src/Ryujinx/Ui/Windows/AboutWindow.cs deleted file mode 100644 index ba12bb68b..000000000 --- a/src/Ryujinx/Ui/Windows/AboutWindow.cs +++ /dev/null @@ -1,85 +0,0 @@ -using Gtk; -using Ryujinx.Common.Utilities; -using Ryujinx.Ui.Common.Helper; -using System.Net.Http; -using System.Net.NetworkInformation; -using System.Reflection; -using System.Threading.Tasks; - -namespace Ryujinx.Ui.Windows -{ - public partial class AboutWindow : Window - { - public AboutWindow() : base($"Ryujinx {Program.Version} - About") - { - Icon = new Gdk.Pixbuf(Assembly.GetAssembly(typeof(OpenHelper)), "Ryujinx.Ui.Common.Resources.Logo_Ryujinx.png"); - InitializeComponent(); - - _ = DownloadPatronsJson(); - } - - private async Task DownloadPatronsJson() - { - if (!NetworkInterface.GetIsNetworkAvailable()) - { - _patreonNamesText.Buffer.Text = "Connection Error."; - } - - HttpClient httpClient = new(); - - try - { - string patreonJsonString = await httpClient.GetStringAsync("https://patreon.ryujinx.org/"); - - _patreonNamesText.Buffer.Text = string.Join(", ", JsonHelper.Deserialize(patreonJsonString, CommonJsonContext.Default.StringArray)); - } - catch - { - _patreonNamesText.Buffer.Text = "API Error."; - } - } - - // - // Events - // - private void RyujinxButton_Pressed(object sender, ButtonPressEventArgs args) - { - OpenHelper.OpenUrl("https://ryujinx.org"); - } - - private void AmiiboApiButton_Pressed(object sender, ButtonPressEventArgs args) - { - OpenHelper.OpenUrl("https://amiiboapi.com"); - } - - private void PatreonButton_Pressed(object sender, ButtonPressEventArgs args) - { - OpenHelper.OpenUrl("https://www.patreon.com/ryujinx"); - } - - private void GitHubButton_Pressed(object sender, ButtonPressEventArgs args) - { - OpenHelper.OpenUrl("https://github.com/Ryujinx/Ryujinx"); - } - - private void DiscordButton_Pressed(object sender, ButtonPressEventArgs args) - { - OpenHelper.OpenUrl("https://discordapp.com/invite/N2FmfVc"); - } - - private void TwitterButton_Pressed(object sender, ButtonPressEventArgs args) - { - OpenHelper.OpenUrl("https://twitter.com/RyujinxEmu"); - } - - private void ContributorsButton_Pressed(object sender, ButtonPressEventArgs args) - { - OpenHelper.OpenUrl("https://github.com/Ryujinx/Ryujinx/graphs/contributors?type=a"); - } - - private void ChangelogButton_Pressed(object sender, ButtonPressEventArgs args) - { - OpenHelper.OpenUrl("https://github.com/Ryujinx/Ryujinx/wiki/Changelog#ryujinx-changelog"); - } - } -} diff --git a/src/Ryujinx/Ui/Windows/AmiiboWindow.Designer.cs b/src/Ryujinx/Ui/Windows/AmiiboWindow.Designer.cs deleted file mode 100644 index cb27c34cb..000000000 --- a/src/Ryujinx/Ui/Windows/AmiiboWindow.Designer.cs +++ /dev/null @@ -1,190 +0,0 @@ -using Gtk; - -namespace Ryujinx.Ui.Windows -{ - public partial class AmiiboWindow : Window - { - private Box _mainBox; - private ButtonBox _buttonBox; - private Button _scanButton; - private Button _cancelButton; - private CheckButton _randomUuidCheckBox; - private Box _amiiboBox; - private Box _amiiboHeadBox; - private Box _amiiboSeriesBox; - private Label _amiiboSeriesLabel; - private ComboBoxText _amiiboSeriesComboBox; - private Box _amiiboCharsBox; - private Label _amiiboCharsLabel; - private ComboBoxText _amiiboCharsComboBox; - private CheckButton _showAllCheckBox; - private Image _amiiboImage; - private Label _gameUsageLabel; - - private void InitializeComponent() - { - // - // AmiiboWindow - // - CanFocus = false; - Resizable = false; - Modal = true; - WindowPosition = WindowPosition.Center; - DefaultWidth = 600; - DefaultHeight = 470; - TypeHint = Gdk.WindowTypeHint.Dialog; - - // - // _mainBox - // - _mainBox = new Box(Orientation.Vertical, 2); - - // - // _buttonBox - // - _buttonBox = new ButtonBox(Orientation.Horizontal) - { - Margin = 20, - LayoutStyle = ButtonBoxStyle.End, - }; - - // - // _scanButton - // - _scanButton = new Button() - { - Label = "Scan It!", - CanFocus = true, - ReceivesDefault = true, - MarginStart = 10, - }; - _scanButton.Clicked += ScanButton_Pressed; - - // - // _randomUuidCheckBox - // - _randomUuidCheckBox = new CheckButton() - { - Label = "Hack: Use Random Tag Uuid", - TooltipText = "This allows multiple scans of a single Amiibo.\n(used in The Legend of Zelda: Breath of the Wild)", - }; - - // - // _cancelButton - // - _cancelButton = new Button() - { - Label = "Cancel", - CanFocus = true, - ReceivesDefault = true, - MarginStart = 10, - }; - _cancelButton.Clicked += CancelButton_Pressed; - - // - // _amiiboBox - // - _amiiboBox = new Box(Orientation.Vertical, 0); - - // - // _amiiboHeadBox - // - _amiiboHeadBox = new Box(Orientation.Horizontal, 0) - { - Margin = 20, - Hexpand = true, - }; - - // - // _amiiboSeriesBox - // - _amiiboSeriesBox = new Box(Orientation.Horizontal, 0) - { - Hexpand = true, - }; - - // - // _amiiboSeriesLabel - // - _amiiboSeriesLabel = new Label("Amiibo Series:"); - - // - // _amiiboSeriesComboBox - // - _amiiboSeriesComboBox = new ComboBoxText(); - - // - // _amiiboCharsBox - // - _amiiboCharsBox = new Box(Orientation.Horizontal, 0) - { - Hexpand = true, - }; - - // - // _amiiboCharsLabel - // - _amiiboCharsLabel = new Label("Character:"); - - // - // _amiiboCharsComboBox - // - _amiiboCharsComboBox = new ComboBoxText(); - - // - // _showAllCheckBox - // - _showAllCheckBox = new CheckButton() - { - Label = "Show All Amiibo", - }; - - // - // _amiiboImage - // - _amiiboImage = new Image() - { - HeightRequest = 350, - WidthRequest = 350, - }; - - // - // _gameUsageLabel - // - _gameUsageLabel = new Label("") - { - MarginTop = 20, - }; - - ShowComponent(); - } - - private void ShowComponent() - { - _buttonBox.Add(_showAllCheckBox); - _buttonBox.Add(_randomUuidCheckBox); - _buttonBox.Add(_scanButton); - _buttonBox.Add(_cancelButton); - - _amiiboSeriesBox.Add(_amiiboSeriesLabel); - _amiiboSeriesBox.Add(_amiiboSeriesComboBox); - - _amiiboCharsBox.Add(_amiiboCharsLabel); - _amiiboCharsBox.Add(_amiiboCharsComboBox); - - _amiiboHeadBox.Add(_amiiboSeriesBox); - _amiiboHeadBox.Add(_amiiboCharsBox); - - _amiiboBox.PackStart(_amiiboHeadBox, true, true, 0); - _amiiboBox.PackEnd(_gameUsageLabel, false, false, 0); - _amiiboBox.PackEnd(_amiiboImage, false, false, 0); - - _mainBox.Add(_amiiboBox); - _mainBox.PackEnd(_buttonBox, false, false, 0); - - Add(_mainBox); - - ShowAll(); - } - } -} diff --git a/src/Ryujinx/Ui/Windows/AmiiboWindow.cs b/src/Ryujinx/Ui/Windows/AmiiboWindow.cs deleted file mode 100644 index 2673f9121..000000000 --- a/src/Ryujinx/Ui/Windows/AmiiboWindow.cs +++ /dev/null @@ -1,398 +0,0 @@ -using Gtk; -using Ryujinx.Common; -using Ryujinx.Common.Configuration; -using Ryujinx.Common.Logging; -using Ryujinx.Common.Utilities; -using Ryujinx.Ui.Common.Configuration; -using Ryujinx.Ui.Common.Models.Amiibo; -using Ryujinx.Ui.Widgets; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net.Http; -using System.Reflection; -using System.Text; -using System.Threading.Tasks; - -namespace Ryujinx.Ui.Windows -{ - public partial class AmiiboWindow : Window - { - private const string DefaultJson = "{ \"amiibo\": [] }"; - - public string AmiiboId { get; private set; } - - public int DeviceId { get; set; } - public string TitleId { get; set; } - public string LastScannedAmiiboId { get; set; } - public bool LastScannedAmiiboShowAll { get; set; } - - public ResponseType Response { get; private set; } - - public bool UseRandomUuid - { - get - { - return _randomUuidCheckBox.Active; - } - } - - private readonly HttpClient _httpClient; - private readonly string _amiiboJsonPath; - - private readonly byte[] _amiiboLogoBytes; - - private List _amiiboList; - - private static readonly AmiiboJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions()); - - public AmiiboWindow() : base($"Ryujinx {Program.Version} - Amiibo") - { - Icon = new Gdk.Pixbuf(Assembly.GetAssembly(typeof(ConfigurationState)), "Ryujinx.Ui.Common.Resources.Logo_Ryujinx.png"); - - InitializeComponent(); - - _httpClient = new HttpClient() - { - Timeout = TimeSpan.FromSeconds(30), - }; - - Directory.CreateDirectory(System.IO.Path.Join(AppDataManager.BaseDirPath, "system", "amiibo")); - - _amiiboJsonPath = System.IO.Path.Join(AppDataManager.BaseDirPath, "system", "amiibo", "Amiibo.json"); - _amiiboList = new List(); - - _amiiboLogoBytes = EmbeddedResources.Read("Ryujinx.Ui.Common/Resources/Logo_Amiibo.png"); - _amiiboImage.Pixbuf = new Gdk.Pixbuf(_amiiboLogoBytes); - - _scanButton.Sensitive = false; - _randomUuidCheckBox.Sensitive = false; - - _ = LoadContentAsync(); - } - - private bool TryGetAmiiboJson(string json, out AmiiboJson amiiboJson) - { - try - { - amiiboJson = JsonHelper.Deserialize(json, _serializerContext.AmiiboJson); - - return true; - } - catch - { - amiiboJson = JsonHelper.Deserialize(DefaultJson, _serializerContext.AmiiboJson); - - return false; - } - } - - private async Task GetMostRecentAmiiboListOrDefaultJson() - { - bool localIsValid = false; - bool remoteIsValid = false; - AmiiboJson amiiboJson = JsonHelper.Deserialize(DefaultJson, _serializerContext.AmiiboJson); - - try - { - localIsValid = TryGetAmiiboJson(File.ReadAllText(_amiiboJsonPath), out amiiboJson); - - if (!localIsValid || await NeedsUpdate(amiiboJson.LastUpdated)) - { - remoteIsValid = TryGetAmiiboJson(await DownloadAmiiboJson(), out amiiboJson); - } - } - catch - { - if (!(localIsValid || remoteIsValid)) - { - // Neither local or remote files are valid JSON, close window. - ShowInfoDialog(); - Close(); - } - else if (!remoteIsValid) - { - // Only the local file is valid, the local one should be used - // but the user should be warned. - ShowInfoDialog(); - } - } - - return amiiboJson; - } - - private async Task LoadContentAsync() - { - AmiiboJson amiiboJson = await GetMostRecentAmiiboListOrDefaultJson(); - - _amiiboList = amiiboJson.Amiibo.OrderBy(amiibo => amiibo.AmiiboSeries).ToList(); - - if (LastScannedAmiiboShowAll) - { - _showAllCheckBox.Click(); - } - - ParseAmiiboData(); - - _showAllCheckBox.Clicked += ShowAllCheckBox_Clicked; - } - - private void ParseAmiiboData() - { - List comboxItemList = new(); - - for (int i = 0; i < _amiiboList.Count; i++) - { - if (!comboxItemList.Contains(_amiiboList[i].AmiiboSeries)) - { - if (!_showAllCheckBox.Active) - { - foreach (var game in _amiiboList[i].GamesSwitch) - { - if (game != null) - { - if (game.GameId.Contains(TitleId)) - { - comboxItemList.Add(_amiiboList[i].AmiiboSeries); - _amiiboSeriesComboBox.Append(_amiiboList[i].AmiiboSeries, _amiiboList[i].AmiiboSeries); - - break; - } - } - } - } - else - { - comboxItemList.Add(_amiiboList[i].AmiiboSeries); - _amiiboSeriesComboBox.Append(_amiiboList[i].AmiiboSeries, _amiiboList[i].AmiiboSeries); - } - } - } - - _amiiboSeriesComboBox.Changed += SeriesComboBox_Changed; - _amiiboCharsComboBox.Changed += CharacterComboBox_Changed; - - if (LastScannedAmiiboId != "") - { - SelectLastScannedAmiibo(); - } - else - { - _amiiboSeriesComboBox.Active = 0; - } - } - - private void SelectLastScannedAmiibo() - { - bool isSet = _amiiboSeriesComboBox.SetActiveId(_amiiboList.Find(amiibo => amiibo.Head + amiibo.Tail == LastScannedAmiiboId).AmiiboSeries); - isSet = _amiiboCharsComboBox.SetActiveId(LastScannedAmiiboId); - - if (isSet == false) - { - _amiiboSeriesComboBox.Active = 0; - } - } - - private async Task NeedsUpdate(DateTime oldLastModified) - { - HttpResponseMessage response = await _httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Head, "https://amiibo.ryujinx.org/")); - - if (response.IsSuccessStatusCode) - { - return response.Content.Headers.LastModified != oldLastModified; - } - - return false; - } - - private async Task DownloadAmiiboJson() - { - HttpResponseMessage response = await _httpClient.GetAsync("https://amiibo.ryujinx.org/"); - - if (response.IsSuccessStatusCode) - { - string amiiboJsonString = await response.Content.ReadAsStringAsync(); - - using (FileStream dlcJsonStream = File.Create(_amiiboJsonPath, 4096, FileOptions.WriteThrough)) - { - dlcJsonStream.Write(Encoding.UTF8.GetBytes(amiiboJsonString)); - } - - return amiiboJsonString; - } - else - { - Logger.Error?.Print(LogClass.Application, $"Failed to download amiibo data. Response status code: {response.StatusCode}"); - - GtkDialog.CreateInfoDialog($"Amiibo API", "An error occured while fetching information from the API."); - - Close(); - } - - return DefaultJson; - } - - private async Task UpdateAmiiboPreview(string imageUrl) - { - HttpResponseMessage response = await _httpClient.GetAsync(imageUrl); - - if (response.IsSuccessStatusCode) - { - byte[] amiiboPreviewBytes = await response.Content.ReadAsByteArrayAsync(); - Gdk.Pixbuf amiiboPreview = new(amiiboPreviewBytes); - - float ratio = Math.Min((float)_amiiboImage.AllocatedWidth / amiiboPreview.Width, - (float)_amiiboImage.AllocatedHeight / amiiboPreview.Height); - - int resizeHeight = (int)(amiiboPreview.Height * ratio); - int resizeWidth = (int)(amiiboPreview.Width * ratio); - - _amiiboImage.Pixbuf = amiiboPreview.ScaleSimple(resizeWidth, resizeHeight, Gdk.InterpType.Bilinear); - } - else - { - Logger.Error?.Print(LogClass.Application, $"Failed to get amiibo preview. Response status code: {response.StatusCode}"); - } - } - - private static void ShowInfoDialog() - { - GtkDialog.CreateInfoDialog($"Amiibo API", "Unable to connect to Amiibo API server. The service may be down or you may need to verify your internet connection is online."); - } - - // - // Events - // - private void SeriesComboBox_Changed(object sender, EventArgs args) - { - _amiiboCharsComboBox.Changed -= CharacterComboBox_Changed; - - _amiiboCharsComboBox.RemoveAll(); - - List amiiboSortedList = _amiiboList.Where(amiibo => amiibo.AmiiboSeries == _amiiboSeriesComboBox.ActiveId).OrderBy(amiibo => amiibo.Name).ToList(); - - List comboxItemList = new(); - - for (int i = 0; i < amiiboSortedList.Count; i++) - { - if (!comboxItemList.Contains(amiiboSortedList[i].Head + amiiboSortedList[i].Tail)) - { - if (!_showAllCheckBox.Active) - { - foreach (var game in amiiboSortedList[i].GamesSwitch) - { - if (game != null) - { - if (game.GameId.Contains(TitleId)) - { - comboxItemList.Add(amiiboSortedList[i].Head + amiiboSortedList[i].Tail); - _amiiboCharsComboBox.Append(amiiboSortedList[i].Head + amiiboSortedList[i].Tail, amiiboSortedList[i].Name); - - break; - } - } - } - } - else - { - comboxItemList.Add(amiiboSortedList[i].Head + amiiboSortedList[i].Tail); - _amiiboCharsComboBox.Append(amiiboSortedList[i].Head + amiiboSortedList[i].Tail, amiiboSortedList[i].Name); - } - } - } - - _amiiboCharsComboBox.Changed += CharacterComboBox_Changed; - - _amiiboCharsComboBox.Active = 0; - - _scanButton.Sensitive = true; - _randomUuidCheckBox.Sensitive = true; - } - - private void CharacterComboBox_Changed(object sender, EventArgs args) - { - AmiiboId = _amiiboCharsComboBox.ActiveId; - - _amiiboImage.Pixbuf = new Gdk.Pixbuf(_amiiboLogoBytes); - - string imageUrl = _amiiboList.Find(amiibo => amiibo.Head + amiibo.Tail == _amiiboCharsComboBox.ActiveId).Image; - - var usageStringBuilder = new StringBuilder(); - - for (int i = 0; i < _amiiboList.Count; i++) - { - if (_amiiboList[i].Head + _amiiboList[i].Tail == _amiiboCharsComboBox.ActiveId) - { - bool writable = false; - - foreach (var item in _amiiboList[i].GamesSwitch) - { - if (item.GameId.Contains(TitleId)) - { - foreach (AmiiboApiUsage usageItem in item.AmiiboUsage) - { - usageStringBuilder.Append(Environment.NewLine); - usageStringBuilder.Append($"- {usageItem.Usage.Replace("/", Environment.NewLine + "-")}"); - - writable = usageItem.Write; - } - } - } - - if (usageStringBuilder.Length == 0) - { - usageStringBuilder.Append("Unknown."); - } - - _gameUsageLabel.Text = $"Usage{(writable ? " (Writable)" : "")} : {usageStringBuilder}"; - } - } - - _ = UpdateAmiiboPreview(imageUrl); - } - - private void ShowAllCheckBox_Clicked(object sender, EventArgs e) - { - _amiiboImage.Pixbuf = new Gdk.Pixbuf(_amiiboLogoBytes); - - _amiiboSeriesComboBox.Changed -= SeriesComboBox_Changed; - _amiiboCharsComboBox.Changed -= CharacterComboBox_Changed; - - _amiiboSeriesComboBox.RemoveAll(); - _amiiboCharsComboBox.RemoveAll(); - - _scanButton.Sensitive = false; - _randomUuidCheckBox.Sensitive = false; - - new Task(() => ParseAmiiboData()).Start(); - } - - private void ScanButton_Pressed(object sender, EventArgs args) - { - LastScannedAmiiboShowAll = _showAllCheckBox.Active; - - Response = ResponseType.Ok; - - Close(); - } - - private void CancelButton_Pressed(object sender, EventArgs args) - { - AmiiboId = ""; - LastScannedAmiiboId = ""; - LastScannedAmiiboShowAll = false; - - Response = ResponseType.Cancel; - - Close(); - } - - protected override void Dispose(bool disposing) - { - _httpClient.Dispose(); - - base.Dispose(disposing); - } - } -} diff --git a/src/Ryujinx/Ui/Windows/AvatarWindow.cs b/src/Ryujinx/Ui/Windows/AvatarWindow.cs deleted file mode 100644 index 3d3ff7c3c..000000000 --- a/src/Ryujinx/Ui/Windows/AvatarWindow.cs +++ /dev/null @@ -1,291 +0,0 @@ -using Gtk; -using LibHac.Common; -using LibHac.Fs; -using LibHac.Fs.Fsa; -using LibHac.FsSystem; -using LibHac.Ncm; -using LibHac.Tools.FsSystem; -using LibHac.Tools.FsSystem.NcaUtils; -using Ryujinx.Common.Memory; -using Ryujinx.HLE.FileSystem; -using Ryujinx.Ui.Common.Configuration; -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.Formats.Png; -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing; -using System; -using System.Buffers.Binary; -using System.Collections.Generic; -using System.IO; -using System.Reflection; -using Image = SixLabors.ImageSharp.Image; - -namespace Ryujinx.Ui.Windows -{ - public class AvatarWindow : Window - { - public byte[] SelectedProfileImage; - public bool NewUser; - - private static readonly Dictionary _avatarDict = new(); - - private readonly ListStore _listStore; - private readonly IconView _iconView; - private readonly Button _setBackgroungColorButton; - private Gdk.RGBA _backgroundColor; - - public AvatarWindow() : base($"Ryujinx {Program.Version} - Manage Accounts - Avatar") - { - Icon = new Gdk.Pixbuf(Assembly.GetAssembly(typeof(ConfigurationState)), "Ryujinx.Ui.Common.Resources.Logo_Ryujinx.png"); - - CanFocus = false; - Resizable = false; - Modal = true; - TypeHint = Gdk.WindowTypeHint.Dialog; - - SetDefaultSize(740, 400); - SetPosition(WindowPosition.Center); - - Box vbox = new(Orientation.Vertical, 0); - Add(vbox); - - ScrolledWindow scrolledWindow = new() - { - ShadowType = ShadowType.EtchedIn, - }; - scrolledWindow.SetPolicy(PolicyType.Automatic, PolicyType.Automatic); - - Box hbox = new(Orientation.Horizontal, 0); - - Button chooseButton = new() - { - Label = "Choose", - CanFocus = true, - ReceivesDefault = true, - }; - chooseButton.Clicked += ChooseButton_Pressed; - - _setBackgroungColorButton = new Button() - { - Label = "Set Background Color", - CanFocus = true, - }; - _setBackgroungColorButton.Clicked += SetBackgroungColorButton_Pressed; - - _backgroundColor.Red = 1; - _backgroundColor.Green = 1; - _backgroundColor.Blue = 1; - _backgroundColor.Alpha = 1; - - Button closeButton = new() - { - Label = "Close", - CanFocus = true, - }; - closeButton.Clicked += CloseButton_Pressed; - - vbox.PackStart(scrolledWindow, true, true, 0); - hbox.PackStart(chooseButton, true, true, 0); - hbox.PackStart(_setBackgroungColorButton, true, true, 0); - hbox.PackStart(closeButton, true, true, 0); - vbox.PackStart(hbox, false, false, 0); - - _listStore = new ListStore(typeof(string), typeof(Gdk.Pixbuf)); - _listStore.SetSortColumnId(0, SortType.Ascending); - - _iconView = new IconView(_listStore) - { - ItemWidth = 64, - ItemPadding = 10, - PixbufColumn = 1, - }; - - _iconView.SelectionChanged += IconView_SelectionChanged; - - scrolledWindow.Add(_iconView); - - _iconView.GrabFocus(); - - ProcessAvatars(); - - ShowAll(); - } - - public static void PreloadAvatars(ContentManager contentManager, VirtualFileSystem virtualFileSystem) - { - if (_avatarDict.Count > 0) - { - return; - } - - string contentPath = contentManager.GetInstalledContentPath(0x010000000000080A, StorageId.BuiltInSystem, NcaContentType.Data); - string avatarPath = VirtualFileSystem.SwitchPathToSystemPath(contentPath); - - if (!string.IsNullOrWhiteSpace(avatarPath)) - { - using IStorage ncaFileStream = new LocalStorage(avatarPath, FileAccess.Read, FileMode.Open); - - Nca nca = new(virtualFileSystem.KeySet, ncaFileStream); - IFileSystem romfs = nca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.ErrorOnInvalid); - - foreach (var item in romfs.EnumerateEntries()) - { - // TODO: Parse DatabaseInfo.bin and table.bin files for more accuracy. - - if (item.Type == DirectoryEntryType.File && item.FullPath.Contains("chara") && item.FullPath.Contains("szs")) - { - using var file = new UniqueRef(); - - romfs.OpenFile(ref file.Ref, ("/" + item.FullPath).ToU8Span(), OpenMode.Read).ThrowIfFailure(); - - using MemoryStream stream = MemoryStreamManager.Shared.GetStream(); - using MemoryStream streamPng = MemoryStreamManager.Shared.GetStream(); - file.Get.AsStream().CopyTo(stream); - - stream.Position = 0; - - Image avatarImage = Image.LoadPixelData(DecompressYaz0(stream), 256, 256); - - avatarImage.SaveAsPng(streamPng); - - _avatarDict.Add(item.FullPath, streamPng.ToArray()); - } - } - } - } - - private void ProcessAvatars() - { - _listStore.Clear(); - - foreach (var avatar in _avatarDict) - { - _listStore.AppendValues(avatar.Key, new Gdk.Pixbuf(ProcessImage(avatar.Value), 96, 96)); - } - - _iconView.SelectPath(new TreePath(new[] { 0 })); - } - - private byte[] ProcessImage(byte[] data) - { - using MemoryStream streamJpg = MemoryStreamManager.Shared.GetStream(); - - Image avatarImage = Image.Load(data, new PngDecoder()); - - avatarImage.Mutate(x => x.BackgroundColor(new Rgba32( - (byte)(_backgroundColor.Red * 255), - (byte)(_backgroundColor.Green * 255), - (byte)(_backgroundColor.Blue * 255), - (byte)(_backgroundColor.Alpha * 255) - ))); - avatarImage.SaveAsJpeg(streamJpg); - - return streamJpg.ToArray(); - } - - private void CloseButton_Pressed(object sender, EventArgs e) - { - SelectedProfileImage = null; - - Close(); - } - - private void IconView_SelectionChanged(object sender, EventArgs e) - { - if (_iconView.SelectedItems.Length > 0) - { - _listStore.GetIter(out TreeIter iter, _iconView.SelectedItems[0]); - - SelectedProfileImage = ProcessImage(_avatarDict[(string)_listStore.GetValue(iter, 0)]); - } - } - - private void SetBackgroungColorButton_Pressed(object sender, EventArgs e) - { - using ColorChooserDialog colorChooserDialog = new("Set Background Color", this); - - colorChooserDialog.UseAlpha = false; - colorChooserDialog.Rgba = _backgroundColor; - - if (colorChooserDialog.Run() == (int)ResponseType.Ok) - { - _backgroundColor = colorChooserDialog.Rgba; - - ProcessAvatars(); - } - - colorChooserDialog.Hide(); - } - - private void ChooseButton_Pressed(object sender, EventArgs e) - { - Close(); - } - - private static byte[] DecompressYaz0(Stream stream) - { - using BinaryReader reader = new(stream); - - reader.ReadInt32(); // Magic - - uint decodedLength = BinaryPrimitives.ReverseEndianness(reader.ReadUInt32()); - - reader.ReadInt64(); // Padding - - byte[] input = new byte[stream.Length - stream.Position]; - stream.Read(input, 0, input.Length); - - long inputOffset = 0; - - byte[] output = new byte[decodedLength]; - long outputOffset = 0; - - ushort mask = 0; - byte header = 0; - - while (outputOffset < decodedLength) - { - if ((mask >>= 1) == 0) - { - header = input[inputOffset++]; - mask = 0x80; - } - - if ((header & mask) > 0) - { - if (outputOffset == output.Length) - { - break; - } - - output[outputOffset++] = input[inputOffset++]; - } - else - { - byte byte1 = input[inputOffset++]; - byte byte2 = input[inputOffset++]; - - int dist = ((byte1 & 0xF) << 8) | byte2; - int position = (int)outputOffset - (dist + 1); - - int length = byte1 >> 4; - if (length == 0) - { - length = input[inputOffset++] + 0x12; - } - else - { - length += 2; - } - - while (length-- > 0) - { - output[outputOffset++] = output[position++]; - } - } - } - - return output; - } - } -} diff --git a/src/Ryujinx/Ui/Windows/CheatWindow.cs b/src/Ryujinx/Ui/Windows/CheatWindow.cs deleted file mode 100644 index 1eca732b2..000000000 --- a/src/Ryujinx/Ui/Windows/CheatWindow.cs +++ /dev/null @@ -1,156 +0,0 @@ -using Gtk; -using Ryujinx.HLE.FileSystem; -using Ryujinx.HLE.HOS; -using Ryujinx.Ui.App.Common; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using GUI = Gtk.Builder.ObjectAttribute; - -namespace Ryujinx.Ui.Windows -{ - public class CheatWindow : Window - { - private readonly string _enabledCheatsPath; - private readonly bool _noCheatsFound; - -#pragma warning disable CS0649, IDE0044 // Field is never assigned to, Add readonly modifier - [GUI] Label _baseTitleInfoLabel; - [GUI] TextView _buildIdTextView; - [GUI] TreeView _cheatTreeView; - [GUI] Button _saveButton; -#pragma warning restore CS0649, IDE0044 - - public CheatWindow(VirtualFileSystem virtualFileSystem, ulong titleId, string titleName, string titlePath) : this(new Builder("Ryujinx.Ui.Windows.CheatWindow.glade"), virtualFileSystem, titleId, titleName, titlePath) { } - - private CheatWindow(Builder builder, VirtualFileSystem virtualFileSystem, ulong titleId, string titleName, string titlePath) : base(builder.GetRawOwnedObject("_cheatWindow")) - { - builder.Autoconnect(this); - _baseTitleInfoLabel.Text = $"Cheats Available for {titleName} [{titleId:X16}]"; - _buildIdTextView.Buffer.Text = $"BuildId: {ApplicationData.GetApplicationBuildId(virtualFileSystem, titlePath)}"; - - string modsBasePath = ModLoader.GetModsBasePath(); - string titleModsPath = ModLoader.GetTitleDir(modsBasePath, titleId.ToString("X16")); - - _enabledCheatsPath = System.IO.Path.Combine(titleModsPath, "cheats", "enabled.txt"); - - _cheatTreeView.Model = new TreeStore(typeof(bool), typeof(string), typeof(string), typeof(string)); - - CellRendererToggle enableToggle = new(); - enableToggle.Toggled += (sender, args) => - { - _cheatTreeView.Model.GetIter(out TreeIter treeIter, new TreePath(args.Path)); - bool newValue = !(bool)_cheatTreeView.Model.GetValue(treeIter, 0); - _cheatTreeView.Model.SetValue(treeIter, 0, newValue); - - if (_cheatTreeView.Model.IterChildren(out TreeIter childIter, treeIter)) - { - do - { - _cheatTreeView.Model.SetValue(childIter, 0, newValue); - } - while (_cheatTreeView.Model.IterNext(ref childIter)); - } - }; - - _cheatTreeView.AppendColumn("Enabled", enableToggle, "active", 0); - _cheatTreeView.AppendColumn("Name", new CellRendererText(), "text", 1); - _cheatTreeView.AppendColumn("Path", new CellRendererText(), "text", 2); - - var buildIdColumn = _cheatTreeView.AppendColumn("Build Id", new CellRendererText(), "text", 3); - buildIdColumn.Visible = false; - - string[] enabled = Array.Empty(); - - if (File.Exists(_enabledCheatsPath)) - { - enabled = File.ReadAllLines(_enabledCheatsPath); - } - - int cheatAdded = 0; - - var mods = new ModLoader.ModCache(); - - ModLoader.QueryContentsDir(mods, new DirectoryInfo(System.IO.Path.Combine(modsBasePath, "contents")), titleId); - - string currentCheatFile = string.Empty; - string buildId = string.Empty; - TreeIter parentIter = default; - - foreach (var cheat in mods.Cheats) - { - if (cheat.Path.FullName != currentCheatFile) - { - currentCheatFile = cheat.Path.FullName; - string parentPath = currentCheatFile.Replace(titleModsPath, ""); - - buildId = System.IO.Path.GetFileNameWithoutExtension(currentCheatFile).ToUpper(); - parentIter = ((TreeStore)_cheatTreeView.Model).AppendValues(false, buildId, parentPath, ""); - } - - string cleanName = cheat.Name[1..^7]; - ((TreeStore)_cheatTreeView.Model).AppendValues(parentIter, enabled.Contains($"{buildId}-{cheat.Name}"), cleanName, "", buildId); - - cheatAdded++; - } - - if (cheatAdded == 0) - { - ((TreeStore)_cheatTreeView.Model).AppendValues(false, "No Cheats Found", "", ""); - _cheatTreeView.GetColumn(0).Visible = false; - - _noCheatsFound = true; - - _saveButton.Visible = false; - } - - _cheatTreeView.ExpandAll(); - } - - private void SaveButton_Clicked(object sender, EventArgs args) - { - if (_noCheatsFound) - { - return; - } - - List enabledCheats = new(); - - if (_cheatTreeView.Model.GetIterFirst(out TreeIter parentIter)) - { - do - { - if (_cheatTreeView.Model.IterChildren(out TreeIter childIter, parentIter)) - { - do - { - var enabled = (bool)_cheatTreeView.Model.GetValue(childIter, 0); - - if (enabled) - { - var name = _cheatTreeView.Model.GetValue(childIter, 1).ToString(); - var buildId = _cheatTreeView.Model.GetValue(childIter, 3).ToString(); - - enabledCheats.Add($"{buildId}-<{name} Cheat>"); - } - } - while (_cheatTreeView.Model.IterNext(ref childIter)); - } - } - while (_cheatTreeView.Model.IterNext(ref parentIter)); - } - - Directory.CreateDirectory(System.IO.Path.GetDirectoryName(_enabledCheatsPath)); - - File.WriteAllLines(_enabledCheatsPath, enabledCheats); - - Dispose(); - } - - private void CancelButton_Clicked(object sender, EventArgs args) - { - Dispose(); - } - } -} diff --git a/src/Ryujinx/Ui/Windows/CheatWindow.glade b/src/Ryujinx/Ui/Windows/CheatWindow.glade deleted file mode 100644 index 9a165f1a8..000000000 --- a/src/Ryujinx/Ui/Windows/CheatWindow.glade +++ /dev/null @@ -1,150 +0,0 @@ - - - - - - False - Ryujinx - Cheat Manager - 440 - 550 - - - True - False - vertical - - - True - False - vertical - - - True - False - 10 - 10 - Available Cheats - - - False - True - 0 - - - - - True - 10 - center - 10 - False - False - - - False - True - 1 - - - - - True - True - 10 - 10 - in - - - True - False - - - True - True - - - - - - - - - - True - True - 2 - - - - - True - True - 0 - - - - - True - False - - - True - False - 10 - 10 - end - - - Save - True - True - True - 10 - 2 - 2 - - - - True - True - 0 - - - - - Cancel - True - True - True - 10 - 2 - 2 - - - - True - True - 1 - - - - - True - True - 1 - - - - - False - True - 1 - - - - - - - - - diff --git a/src/Ryujinx/Ui/Windows/ControllerWindow.cs b/src/Ryujinx/Ui/Windows/ControllerWindow.cs deleted file mode 100644 index ebf22ab60..000000000 --- a/src/Ryujinx/Ui/Windows/ControllerWindow.cs +++ /dev/null @@ -1,1230 +0,0 @@ -using Gtk; -using Ryujinx.Common.Configuration; -using Ryujinx.Common.Configuration.Hid; -using Ryujinx.Common.Configuration.Hid.Controller; -using Ryujinx.Common.Configuration.Hid.Controller.Motion; -using Ryujinx.Common.Configuration.Hid.Keyboard; -using Ryujinx.Common.Logging; -using Ryujinx.Common.Utilities; -using Ryujinx.Input; -using Ryujinx.Input.Assigner; -using Ryujinx.Input.GTK3; -using Ryujinx.Ui.Common.Configuration; -using Ryujinx.Ui.Widgets; -using System; -using System.Collections.Generic; -using System.IO; -using System.Reflection; -using System.Text.Json; -using System.Threading; -using ConfigGamepadInputId = Ryujinx.Common.Configuration.Hid.Controller.GamepadInputId; -using ConfigStickInputId = Ryujinx.Common.Configuration.Hid.Controller.StickInputId; -using GUI = Gtk.Builder.ObjectAttribute; -using Key = Ryujinx.Common.Configuration.Hid.Key; - -namespace Ryujinx.Ui.Windows -{ - public class ControllerWindow : Window - { - private readonly PlayerIndex _playerIndex; - private readonly InputConfig _inputConfig; - - private bool _isWaitingForInput; - -#pragma warning disable CS0649, IDE0044 // Field is never assigned to, Add readonly modifier - [GUI] Adjustment _controllerStrongRumble; - [GUI] Adjustment _controllerWeakRumble; - [GUI] Adjustment _controllerDeadzoneLeft; - [GUI] Adjustment _controllerDeadzoneRight; - [GUI] Adjustment _controllerRangeLeft; - [GUI] Adjustment _controllerRangeRight; - [GUI] Adjustment _controllerTriggerThreshold; - [GUI] Adjustment _slotNumber; - [GUI] Adjustment _altSlotNumber; - [GUI] Adjustment _sensitivity; - [GUI] Adjustment _gyroDeadzone; - [GUI] CheckButton _enableMotion; - [GUI] CheckButton _enableCemuHook; - [GUI] CheckButton _mirrorInput; - [GUI] Entry _dsuServerHost; - [GUI] Entry _dsuServerPort; - [GUI] ComboBoxText _inputDevice; - [GUI] ComboBoxText _profile; - [GUI] Box _settingsBox; - [GUI] Box _motionAltBox; - [GUI] Box _motionBox; - [GUI] Box _dsuServerHostBox; - [GUI] Box _dsuServerPortBox; - [GUI] Box _motionControllerSlot; - [GUI] Grid _leftStickKeyboard; - [GUI] Grid _leftStickController; - [GUI] Box _deadZoneLeftBox; - [GUI] Box _rangeLeftBox; - [GUI] Grid _rightStickKeyboard; - [GUI] Grid _rightStickController; - [GUI] Box _deadZoneRightBox; - [GUI] Box _rangeRightBox; - [GUI] Grid _leftSideTriggerBox; - [GUI] Grid _rightSideTriggerBox; - [GUI] Box _triggerThresholdBox; - [GUI] ComboBoxText _controllerType; - [GUI] ToggleButton _lStick; - [GUI] CheckButton _invertLStickX; - [GUI] CheckButton _invertLStickY; - [GUI] CheckButton _rotateL90CW; - [GUI] ToggleButton _lStickUp; - [GUI] ToggleButton _lStickDown; - [GUI] ToggleButton _lStickLeft; - [GUI] ToggleButton _lStickRight; - [GUI] ToggleButton _lStickButton; - [GUI] ToggleButton _dpadUp; - [GUI] ToggleButton _dpadDown; - [GUI] ToggleButton _dpadLeft; - [GUI] ToggleButton _dpadRight; - [GUI] ToggleButton _minus; - [GUI] ToggleButton _l; - [GUI] ToggleButton _zL; - [GUI] ToggleButton _rStick; - [GUI] CheckButton _invertRStickX; - [GUI] CheckButton _invertRStickY; - [GUI] CheckButton _rotateR90CW; - [GUI] ToggleButton _rStickUp; - [GUI] ToggleButton _rStickDown; - [GUI] ToggleButton _rStickLeft; - [GUI] ToggleButton _rStickRight; - [GUI] ToggleButton _rStickButton; - [GUI] ToggleButton _a; - [GUI] ToggleButton _b; - [GUI] ToggleButton _x; - [GUI] ToggleButton _y; - [GUI] ToggleButton _plus; - [GUI] ToggleButton _r; - [GUI] ToggleButton _zR; - [GUI] ToggleButton _lSl; - [GUI] ToggleButton _lSr; - [GUI] ToggleButton _rSl; - [GUI] ToggleButton _rSr; - [GUI] Image _controllerImage; - [GUI] CheckButton _enableRumble; - [GUI] Box _rumbleBox; -#pragma warning restore CS0649, IDE0044 - - private readonly MainWindow _mainWindow; - private readonly IGamepadDriver _gtk3KeyboardDriver; - private IGamepad _selectedGamepad; - private bool _mousePressed; - private bool _middleMousePressed; - - private static readonly InputConfigJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions()); - - public ControllerWindow(MainWindow mainWindow, PlayerIndex controllerId) : this(mainWindow, new Builder("Ryujinx.Ui.Windows.ControllerWindow.glade"), controllerId) { } - - private ControllerWindow(MainWindow mainWindow, Builder builder, PlayerIndex controllerId) : base(builder.GetRawOwnedObject("_controllerWin")) - { - _mainWindow = mainWindow; - _selectedGamepad = null; - - // NOTE: To get input in this window, we need to bind a custom keyboard driver instead of using the InputManager one as the main window isn't focused... - _gtk3KeyboardDriver = new GTK3KeyboardDriver(this); - - Icon = new Gdk.Pixbuf(Assembly.GetAssembly(typeof(ConfigurationState)), "Ryujinx.Ui.Common.Resources.Logo_Ryujinx.png"); - - builder.Autoconnect(this); - - _playerIndex = controllerId; - _inputConfig = ConfigurationState.Instance.Hid.InputConfig.Value.Find(inputConfig => inputConfig.PlayerIndex == _playerIndex); - - Title = $"Ryujinx - Controller Settings - {_playerIndex}"; - - if (_playerIndex == PlayerIndex.Handheld) - { - _controllerType.Append(ControllerType.Handheld.ToString(), "Handheld"); - _controllerType.Sensitive = false; - } - else - { - _controllerType.Append(ControllerType.ProController.ToString(), "Pro Controller"); - _controllerType.Append(ControllerType.JoyconPair.ToString(), "Joycon Pair"); - _controllerType.Append(ControllerType.JoyconLeft.ToString(), "Joycon Left"); - _controllerType.Append(ControllerType.JoyconRight.ToString(), "Joycon Right"); - } - - _controllerType.Active = 0; // Set initial value to first in list. - - // Bind Events. - _lStick.Clicked += ButtonForStick_Pressed; - _lStickUp.Clicked += Button_Pressed; - _lStickDown.Clicked += Button_Pressed; - _lStickLeft.Clicked += Button_Pressed; - _lStickRight.Clicked += Button_Pressed; - _lStickButton.Clicked += Button_Pressed; - _dpadUp.Clicked += Button_Pressed; - _dpadDown.Clicked += Button_Pressed; - _dpadLeft.Clicked += Button_Pressed; - _dpadRight.Clicked += Button_Pressed; - _minus.Clicked += Button_Pressed; - _l.Clicked += Button_Pressed; - _zL.Clicked += Button_Pressed; - _lSl.Clicked += Button_Pressed; - _lSr.Clicked += Button_Pressed; - _rStick.Clicked += ButtonForStick_Pressed; - _rStickUp.Clicked += Button_Pressed; - _rStickDown.Clicked += Button_Pressed; - _rStickLeft.Clicked += Button_Pressed; - _rStickRight.Clicked += Button_Pressed; - _rStickButton.Clicked += Button_Pressed; - _a.Clicked += Button_Pressed; - _b.Clicked += Button_Pressed; - _x.Clicked += Button_Pressed; - _y.Clicked += Button_Pressed; - _plus.Clicked += Button_Pressed; - _r.Clicked += Button_Pressed; - _zR.Clicked += Button_Pressed; - _rSl.Clicked += Button_Pressed; - _rSr.Clicked += Button_Pressed; - _enableCemuHook.Clicked += CemuHookCheckButtonPressed; - - // Setup current values. - UpdateInputDeviceList(); - SetAvailableOptions(); - - ClearValues(); - if (_inputDevice.ActiveId != null) - { - SetCurrentValues(); - } - - mainWindow.InputManager.GamepadDriver.OnGamepadConnected += HandleOnGamepadConnected; - mainWindow.InputManager.GamepadDriver.OnGamepadDisconnected += HandleOnGamepadDisconnected; - - _mainWindow.RendererWidget?.NpadManager.BlockInputUpdates(); - } - - private void CemuHookCheckButtonPressed(object sender, EventArgs e) - { - UpdateCemuHookSpecificFieldsVisibility(); - } - - private void HandleOnGamepadDisconnected(string id) - { - Application.Invoke(delegate - { - UpdateInputDeviceList(); - }); - } - - private void HandleOnGamepadConnected(string id) - { - Application.Invoke(delegate - { - UpdateInputDeviceList(); - }); - } - - protected override void OnDestroyed() - { - _mainWindow.InputManager.GamepadDriver.OnGamepadConnected -= HandleOnGamepadConnected; - _mainWindow.InputManager.GamepadDriver.OnGamepadDisconnected -= HandleOnGamepadDisconnected; - - _mainWindow.RendererWidget?.NpadManager.UnblockInputUpdates(); - - _selectedGamepad?.Dispose(); - - _gtk3KeyboardDriver.Dispose(); - } - - private static string GetShortGamepadName(string str) - { - const string ShrinkChars = "..."; - const int MaxSize = 50; - - if (str.Length > MaxSize) - { - return $"{str.AsSpan(0, MaxSize - ShrinkChars.Length)}{ShrinkChars}"; - } - - return str; - } - - private void UpdateInputDeviceList() - { - _inputDevice.RemoveAll(); - _inputDevice.Append("disabled", "Disabled"); - _inputDevice.SetActiveId("disabled"); - - foreach (string id in _mainWindow.InputManager.KeyboardDriver.GamepadsIds) - { - IGamepad gamepad = _mainWindow.InputManager.KeyboardDriver.GetGamepad(id); - - if (gamepad != null) - { - _inputDevice.Append($"keyboard/{id}", GetShortGamepadName($"{gamepad.Name} ({id})")); - - gamepad.Dispose(); - } - } - - foreach (string id in _mainWindow.InputManager.GamepadDriver.GamepadsIds) - { - IGamepad gamepad = _mainWindow.InputManager.GamepadDriver.GetGamepad(id); - - if (gamepad != null) - { - _inputDevice.Append($"controller/{id}", GetShortGamepadName($"{gamepad.Name} ({id})")); - - gamepad.Dispose(); - } - } - - switch (_inputConfig) - { - case StandardKeyboardInputConfig keyboard: - _inputDevice.SetActiveId($"keyboard/{keyboard.Id}"); - break; - case StandardControllerInputConfig controller: - _inputDevice.SetActiveId($"controller/{controller.Id}"); - break; - } - } - - private void UpdateCemuHookSpecificFieldsVisibility() - { - if (_enableCemuHook.Active) - { - _dsuServerHostBox.Show(); - _dsuServerPortBox.Show(); - _motionControllerSlot.Show(); - _motionAltBox.Show(); - _mirrorInput.Show(); - } - else - { - _dsuServerHostBox.Hide(); - _dsuServerPortBox.Hide(); - _motionControllerSlot.Hide(); - _motionAltBox.Hide(); - _mirrorInput.Hide(); - } - } - - private void SetAvailableOptions() - { - if (_inputDevice.ActiveId != null && _inputDevice.ActiveId.StartsWith("keyboard")) - { - ShowAll(); - _leftStickController.Hide(); - _rightStickController.Hide(); - _deadZoneLeftBox.Hide(); - _deadZoneRightBox.Hide(); - _rangeLeftBox.Hide(); - _rangeRightBox.Hide(); - _triggerThresholdBox.Hide(); - _motionBox.Hide(); - _rumbleBox.Hide(); - } - else if (_inputDevice.ActiveId != null && _inputDevice.ActiveId.StartsWith("controller")) - { - ShowAll(); - _leftStickKeyboard.Hide(); - _rightStickKeyboard.Hide(); - - UpdateCemuHookSpecificFieldsVisibility(); - } - else - { - _settingsBox.Hide(); - } - - ClearValues(); - } - - private void SetCurrentValues() - { - SetControllerSpecificFields(); - - SetProfiles(); - - if (_inputDevice.ActiveId.StartsWith("keyboard") && _inputConfig is StandardKeyboardInputConfig) - { - SetValues(_inputConfig); - } - else if (_inputDevice.ActiveId.StartsWith("controller") && _inputConfig is StandardControllerInputConfig) - { - SetValues(_inputConfig); - } - } - - private void SetControllerSpecificFields() - { - _leftSideTriggerBox.Hide(); - _rightSideTriggerBox.Hide(); - _motionAltBox.Hide(); - - switch (_controllerType.ActiveId) - { - case "JoyconLeft": - _leftSideTriggerBox.Show(); - break; - case "JoyconRight": - _rightSideTriggerBox.Show(); - break; - case "JoyconPair": - _motionAltBox.Show(); - break; - } - - if (!OperatingSystem.IsMacOS()) - { - _controllerImage.Pixbuf = _controllerType.ActiveId switch - { - "ProController" => new Gdk.Pixbuf(Assembly.GetAssembly(typeof(ConfigurationState)), "Ryujinx.Ui.Common.Resources.Controller_ProCon.svg", 400, 400), - "JoyconLeft" => new Gdk.Pixbuf(Assembly.GetAssembly(typeof(ConfigurationState)), "Ryujinx.Ui.Common.Resources.Controller_JoyConLeft.svg", 400, 500), - "JoyconRight" => new Gdk.Pixbuf(Assembly.GetAssembly(typeof(ConfigurationState)), "Ryujinx.Ui.Common.Resources.Controller_JoyConRight.svg", 400, 500), - _ => new Gdk.Pixbuf(Assembly.GetAssembly(typeof(ConfigurationState)), "Ryujinx.Ui.Common.Resources.Controller_JoyConPair.svg", 400, 500), - }; - } - } - - private void ClearValues() - { - _lStick.Label = "Unbound"; - _lStickUp.Label = "Unbound"; - _lStickDown.Label = "Unbound"; - _lStickLeft.Label = "Unbound"; - _lStickRight.Label = "Unbound"; - _lStickButton.Label = "Unbound"; - _dpadUp.Label = "Unbound"; - _dpadDown.Label = "Unbound"; - _dpadLeft.Label = "Unbound"; - _dpadRight.Label = "Unbound"; - _minus.Label = "Unbound"; - _l.Label = "Unbound"; - _zL.Label = "Unbound"; - _lSl.Label = "Unbound"; - _lSr.Label = "Unbound"; - _rStick.Label = "Unbound"; - _rStickUp.Label = "Unbound"; - _rStickDown.Label = "Unbound"; - _rStickLeft.Label = "Unbound"; - _rStickRight.Label = "Unbound"; - _rStickButton.Label = "Unbound"; - _a.Label = "Unbound"; - _b.Label = "Unbound"; - _x.Label = "Unbound"; - _y.Label = "Unbound"; - _plus.Label = "Unbound"; - _r.Label = "Unbound"; - _zR.Label = "Unbound"; - _rSl.Label = "Unbound"; - _rSr.Label = "Unbound"; - _controllerStrongRumble.Value = 1; - _controllerWeakRumble.Value = 1; - _controllerDeadzoneLeft.Value = 0; - _controllerDeadzoneRight.Value = 0; - _controllerRangeLeft.Value = 1; - _controllerRangeRight.Value = 1; - _controllerTriggerThreshold.Value = 0; - _mirrorInput.Active = false; - _enableMotion.Active = false; - _enableCemuHook.Active = false; - _slotNumber.Value = 0; - _altSlotNumber.Value = 0; - _sensitivity.Value = 100; - _gyroDeadzone.Value = 1; - _dsuServerHost.Buffer.Text = ""; - _dsuServerPort.Buffer.Text = ""; - _enableRumble.Active = false; - } - - private void SetValues(InputConfig config) - { - switch (config) - { - case StandardKeyboardInputConfig keyboardConfig: - if (!_controllerType.SetActiveId(keyboardConfig.ControllerType.ToString())) - { - _controllerType.SetActiveId(_playerIndex == PlayerIndex.Handheld - ? ControllerType.Handheld.ToString() - : ControllerType.ProController.ToString()); - } - - _lStickUp.Label = keyboardConfig.LeftJoyconStick.StickUp.ToString(); - _lStickDown.Label = keyboardConfig.LeftJoyconStick.StickDown.ToString(); - _lStickLeft.Label = keyboardConfig.LeftJoyconStick.StickLeft.ToString(); - _lStickRight.Label = keyboardConfig.LeftJoyconStick.StickRight.ToString(); - _lStickButton.Label = keyboardConfig.LeftJoyconStick.StickButton.ToString(); - _dpadUp.Label = keyboardConfig.LeftJoycon.DpadUp.ToString(); - _dpadDown.Label = keyboardConfig.LeftJoycon.DpadDown.ToString(); - _dpadLeft.Label = keyboardConfig.LeftJoycon.DpadLeft.ToString(); - _dpadRight.Label = keyboardConfig.LeftJoycon.DpadRight.ToString(); - _minus.Label = keyboardConfig.LeftJoycon.ButtonMinus.ToString(); - _l.Label = keyboardConfig.LeftJoycon.ButtonL.ToString(); - _zL.Label = keyboardConfig.LeftJoycon.ButtonZl.ToString(); - _lSl.Label = keyboardConfig.LeftJoycon.ButtonSl.ToString(); - _lSr.Label = keyboardConfig.LeftJoycon.ButtonSr.ToString(); - _rStickUp.Label = keyboardConfig.RightJoyconStick.StickUp.ToString(); - _rStickDown.Label = keyboardConfig.RightJoyconStick.StickDown.ToString(); - _rStickLeft.Label = keyboardConfig.RightJoyconStick.StickLeft.ToString(); - _rStickRight.Label = keyboardConfig.RightJoyconStick.StickRight.ToString(); - _rStickButton.Label = keyboardConfig.RightJoyconStick.StickButton.ToString(); - _a.Label = keyboardConfig.RightJoycon.ButtonA.ToString(); - _b.Label = keyboardConfig.RightJoycon.ButtonB.ToString(); - _x.Label = keyboardConfig.RightJoycon.ButtonX.ToString(); - _y.Label = keyboardConfig.RightJoycon.ButtonY.ToString(); - _plus.Label = keyboardConfig.RightJoycon.ButtonPlus.ToString(); - _r.Label = keyboardConfig.RightJoycon.ButtonR.ToString(); - _zR.Label = keyboardConfig.RightJoycon.ButtonZr.ToString(); - _rSl.Label = keyboardConfig.RightJoycon.ButtonSl.ToString(); - _rSr.Label = keyboardConfig.RightJoycon.ButtonSr.ToString(); - break; - - case StandardControllerInputConfig controllerConfig: - if (!_controllerType.SetActiveId(controllerConfig.ControllerType.ToString())) - { - _controllerType.SetActiveId(_playerIndex == PlayerIndex.Handheld - ? ControllerType.Handheld.ToString() - : ControllerType.ProController.ToString()); - } - - _lStick.Label = controllerConfig.LeftJoyconStick.Joystick.ToString(); - _invertLStickX.Active = controllerConfig.LeftJoyconStick.InvertStickX; - _invertLStickY.Active = controllerConfig.LeftJoyconStick.InvertStickY; - _rotateL90CW.Active = controllerConfig.LeftJoyconStick.Rotate90CW; - _lStickButton.Label = controllerConfig.LeftJoyconStick.StickButton.ToString(); - _dpadUp.Label = controllerConfig.LeftJoycon.DpadUp.ToString(); - _dpadDown.Label = controllerConfig.LeftJoycon.DpadDown.ToString(); - _dpadLeft.Label = controllerConfig.LeftJoycon.DpadLeft.ToString(); - _dpadRight.Label = controllerConfig.LeftJoycon.DpadRight.ToString(); - _minus.Label = controllerConfig.LeftJoycon.ButtonMinus.ToString(); - _l.Label = controllerConfig.LeftJoycon.ButtonL.ToString(); - _zL.Label = controllerConfig.LeftJoycon.ButtonZl.ToString(); - _lSl.Label = controllerConfig.LeftJoycon.ButtonSl.ToString(); - _lSr.Label = controllerConfig.LeftJoycon.ButtonSr.ToString(); - _rStick.Label = controllerConfig.RightJoyconStick.Joystick.ToString(); - _invertRStickX.Active = controllerConfig.RightJoyconStick.InvertStickX; - _invertRStickY.Active = controllerConfig.RightJoyconStick.InvertStickY; - _rotateR90CW.Active = controllerConfig.RightJoyconStick.Rotate90CW; - _rStickButton.Label = controllerConfig.RightJoyconStick.StickButton.ToString(); - _a.Label = controllerConfig.RightJoycon.ButtonA.ToString(); - _b.Label = controllerConfig.RightJoycon.ButtonB.ToString(); - _x.Label = controllerConfig.RightJoycon.ButtonX.ToString(); - _y.Label = controllerConfig.RightJoycon.ButtonY.ToString(); - _plus.Label = controllerConfig.RightJoycon.ButtonPlus.ToString(); - _r.Label = controllerConfig.RightJoycon.ButtonR.ToString(); - _zR.Label = controllerConfig.RightJoycon.ButtonZr.ToString(); - _rSl.Label = controllerConfig.RightJoycon.ButtonSl.ToString(); - _rSr.Label = controllerConfig.RightJoycon.ButtonSr.ToString(); - _controllerStrongRumble.Value = controllerConfig.Rumble.StrongRumble; - _controllerWeakRumble.Value = controllerConfig.Rumble.WeakRumble; - _enableRumble.Active = controllerConfig.Rumble.EnableRumble; - _controllerDeadzoneLeft.Value = controllerConfig.DeadzoneLeft; - _controllerDeadzoneRight.Value = controllerConfig.DeadzoneRight; - _controllerRangeLeft.Value = controllerConfig.RangeLeft; - _controllerRangeRight.Value = controllerConfig.RangeRight; - _controllerTriggerThreshold.Value = controllerConfig.TriggerThreshold; - _sensitivity.Value = controllerConfig.Motion.Sensitivity; - _gyroDeadzone.Value = controllerConfig.Motion.GyroDeadzone; - _enableMotion.Active = controllerConfig.Motion.EnableMotion; - _enableCemuHook.Active = controllerConfig.Motion.MotionBackend == MotionInputBackendType.CemuHook; - - // If both stick ranges are 0 (usually indicative of an outdated profile load) then both sticks will be set to 1.0. - if (_controllerRangeLeft.Value <= 0.0 && _controllerRangeRight.Value <= 0.0) - { - _controllerRangeLeft.Value = 1.0; - _controllerRangeRight.Value = 1.0; - - Logger.Info?.Print(LogClass.Application, $"{config.PlayerIndex} stick range reset. Save the profile now to update your configuration"); - } - - if (controllerConfig.Motion is CemuHookMotionConfigController cemuHookMotionConfig) - { - _slotNumber.Value = cemuHookMotionConfig.Slot; - _altSlotNumber.Value = cemuHookMotionConfig.AltSlot; - _mirrorInput.Active = cemuHookMotionConfig.MirrorInput; - _dsuServerHost.Buffer.Text = cemuHookMotionConfig.DsuServerHost; - _dsuServerPort.Buffer.Text = cemuHookMotionConfig.DsuServerPort.ToString(); - } - - break; - } - } - - private InputConfig GetValues() - { - if (_inputDevice.ActiveId.StartsWith("keyboard")) - { -#pragma warning disable CA1806, IDE0055 // Disable formatting - Enum.TryParse(_lStickUp.Label, out Key lStickUp); - Enum.TryParse(_lStickDown.Label, out Key lStickDown); - Enum.TryParse(_lStickLeft.Label, out Key lStickLeft); - Enum.TryParse(_lStickRight.Label, out Key lStickRight); - Enum.TryParse(_lStickButton.Label, out Key lStickButton); - Enum.TryParse(_dpadUp.Label, out Key lDPadUp); - Enum.TryParse(_dpadDown.Label, out Key lDPadDown); - Enum.TryParse(_dpadLeft.Label, out Key lDPadLeft); - Enum.TryParse(_dpadRight.Label, out Key lDPadRight); - Enum.TryParse(_minus.Label, out Key lButtonMinus); - Enum.TryParse(_l.Label, out Key lButtonL); - Enum.TryParse(_zL.Label, out Key lButtonZl); - Enum.TryParse(_lSl.Label, out Key lButtonSl); - Enum.TryParse(_lSr.Label, out Key lButtonSr); - - Enum.TryParse(_rStickUp.Label, out Key rStickUp); - Enum.TryParse(_rStickDown.Label, out Key rStickDown); - Enum.TryParse(_rStickLeft.Label, out Key rStickLeft); - Enum.TryParse(_rStickRight.Label, out Key rStickRight); - Enum.TryParse(_rStickButton.Label, out Key rStickButton); - Enum.TryParse(_a.Label, out Key rButtonA); - Enum.TryParse(_b.Label, out Key rButtonB); - Enum.TryParse(_x.Label, out Key rButtonX); - Enum.TryParse(_y.Label, out Key rButtonY); - Enum.TryParse(_plus.Label, out Key rButtonPlus); - Enum.TryParse(_r.Label, out Key rButtonR); - Enum.TryParse(_zR.Label, out Key rButtonZr); - Enum.TryParse(_rSl.Label, out Key rButtonSl); - Enum.TryParse(_rSr.Label, out Key rButtonSr); -#pragma warning restore CA1806, IDE0055 - - return new StandardKeyboardInputConfig - { - Backend = InputBackendType.WindowKeyboard, - Version = InputConfig.CurrentVersion, - Id = _inputDevice.ActiveId.Split("/")[1], - ControllerType = Enum.Parse(_controllerType.ActiveId), - PlayerIndex = _playerIndex, - LeftJoycon = new LeftJoyconCommonConfig - { - ButtonMinus = lButtonMinus, - ButtonL = lButtonL, - ButtonZl = lButtonZl, - ButtonSl = lButtonSl, - ButtonSr = lButtonSr, - DpadUp = lDPadUp, - DpadDown = lDPadDown, - DpadLeft = lDPadLeft, - DpadRight = lDPadRight, - }, - LeftJoyconStick = new JoyconConfigKeyboardStick - { - StickUp = lStickUp, - StickDown = lStickDown, - StickLeft = lStickLeft, - StickRight = lStickRight, - StickButton = lStickButton, - }, - RightJoycon = new RightJoyconCommonConfig - { - ButtonA = rButtonA, - ButtonB = rButtonB, - ButtonX = rButtonX, - ButtonY = rButtonY, - ButtonPlus = rButtonPlus, - ButtonR = rButtonR, - ButtonZr = rButtonZr, - ButtonSl = rButtonSl, - ButtonSr = rButtonSr, - }, - RightJoyconStick = new JoyconConfigKeyboardStick - { - StickUp = rStickUp, - StickDown = rStickDown, - StickLeft = rStickLeft, - StickRight = rStickRight, - StickButton = rStickButton, - }, - }; - } - - if (_inputDevice.ActiveId.StartsWith("controller")) - { -#pragma warning disable CA1806, IDE0055 // Disable formatting - Enum.TryParse(_lStick.Label, out ConfigStickInputId lStick); - Enum.TryParse(_lStickButton.Label, out ConfigGamepadInputId lStickButton); - Enum.TryParse(_minus.Label, out ConfigGamepadInputId lButtonMinus); - Enum.TryParse(_l.Label, out ConfigGamepadInputId lButtonL); - Enum.TryParse(_zL.Label, out ConfigGamepadInputId lButtonZl); - Enum.TryParse(_lSl.Label, out ConfigGamepadInputId lButtonSl); - Enum.TryParse(_lSr.Label, out ConfigGamepadInputId lButtonSr); - Enum.TryParse(_dpadUp.Label, out ConfigGamepadInputId lDPadUp); - Enum.TryParse(_dpadDown.Label, out ConfigGamepadInputId lDPadDown); - Enum.TryParse(_dpadLeft.Label, out ConfigGamepadInputId lDPadLeft); - Enum.TryParse(_dpadRight.Label, out ConfigGamepadInputId lDPadRight); - - Enum.TryParse(_rStick.Label, out ConfigStickInputId rStick); - Enum.TryParse(_rStickButton.Label, out ConfigGamepadInputId rStickButton); - Enum.TryParse(_a.Label, out ConfigGamepadInputId rButtonA); - Enum.TryParse(_b.Label, out ConfigGamepadInputId rButtonB); - Enum.TryParse(_x.Label, out ConfigGamepadInputId rButtonX); - Enum.TryParse(_y.Label, out ConfigGamepadInputId rButtonY); - Enum.TryParse(_plus.Label, out ConfigGamepadInputId rButtonPlus); - Enum.TryParse(_r.Label, out ConfigGamepadInputId rButtonR); - Enum.TryParse(_zR.Label, out ConfigGamepadInputId rButtonZr); - Enum.TryParse(_rSl.Label, out ConfigGamepadInputId rButtonSl); - Enum.TryParse(_rSr.Label, out ConfigGamepadInputId rButtonSr); - - int.TryParse(_dsuServerPort.Buffer.Text, out int port); -#pragma warning restore CA1806, IDE0055 - - MotionConfigController motionConfig; - - if (_enableCemuHook.Active) - { - motionConfig = new CemuHookMotionConfigController - { - MotionBackend = MotionInputBackendType.CemuHook, - EnableMotion = _enableMotion.Active, - Sensitivity = (int)_sensitivity.Value, - GyroDeadzone = _gyroDeadzone.Value, - MirrorInput = _mirrorInput.Active, - Slot = (int)_slotNumber.Value, - AltSlot = (int)_altSlotNumber.Value, - DsuServerHost = _dsuServerHost.Buffer.Text, - DsuServerPort = port, - }; - } - else - { - motionConfig = new StandardMotionConfigController - { - MotionBackend = MotionInputBackendType.GamepadDriver, - EnableMotion = _enableMotion.Active, - Sensitivity = (int)_sensitivity.Value, - GyroDeadzone = _gyroDeadzone.Value, - }; - } - - return new StandardControllerInputConfig - { - Backend = InputBackendType.GamepadSDL2, - Version = InputConfig.CurrentVersion, - Id = _inputDevice.ActiveId.Split("/")[1].Split(" ")[0], - ControllerType = Enum.Parse(_controllerType.ActiveId), - PlayerIndex = _playerIndex, - DeadzoneLeft = (float)_controllerDeadzoneLeft.Value, - DeadzoneRight = (float)_controllerDeadzoneRight.Value, - RangeLeft = (float)_controllerRangeLeft.Value, - RangeRight = (float)_controllerRangeRight.Value, - TriggerThreshold = (float)_controllerTriggerThreshold.Value, - LeftJoycon = new LeftJoyconCommonConfig - { - ButtonMinus = lButtonMinus, - ButtonL = lButtonL, - ButtonZl = lButtonZl, - ButtonSl = lButtonSl, - ButtonSr = lButtonSr, - DpadUp = lDPadUp, - DpadDown = lDPadDown, - DpadLeft = lDPadLeft, - DpadRight = lDPadRight, - }, - LeftJoyconStick = new JoyconConfigControllerStick - { - InvertStickX = _invertLStickX.Active, - Joystick = lStick, - InvertStickY = _invertLStickY.Active, - StickButton = lStickButton, - Rotate90CW = _rotateL90CW.Active, - }, - RightJoycon = new RightJoyconCommonConfig - { - ButtonA = rButtonA, - ButtonB = rButtonB, - ButtonX = rButtonX, - ButtonY = rButtonY, - ButtonPlus = rButtonPlus, - ButtonR = rButtonR, - ButtonZr = rButtonZr, - ButtonSl = rButtonSl, - ButtonSr = rButtonSr, - }, - RightJoyconStick = new JoyconConfigControllerStick - { - InvertStickX = _invertRStickX.Active, - Joystick = rStick, - InvertStickY = _invertRStickY.Active, - StickButton = rStickButton, - Rotate90CW = _rotateR90CW.Active, - }, - Motion = motionConfig, - Rumble = new RumbleConfigController - { - StrongRumble = (float)_controllerStrongRumble.Value, - WeakRumble = (float)_controllerWeakRumble.Value, - EnableRumble = _enableRumble.Active, - }, - }; - } - - if (!_inputDevice.ActiveId.StartsWith("disabled")) - { - GtkDialog.CreateErrorDialog("Invalid data detected in one or more fields; the configuration was not saved."); - } - - return null; - } - - private string GetProfileBasePath() - { - if (_inputDevice.ActiveId.StartsWith("keyboard")) - { - return System.IO.Path.Combine(AppDataManager.ProfilesDirPath, "keyboard"); - } - else if (_inputDevice.ActiveId.StartsWith("controller")) - { - return System.IO.Path.Combine(AppDataManager.ProfilesDirPath, "controller"); - } - - return AppDataManager.ProfilesDirPath; - } - - // - // Events - // - private void InputDevice_Changed(object sender, EventArgs args) - { - SetAvailableOptions(); - SetControllerSpecificFields(); - - _selectedGamepad?.Dispose(); - _selectedGamepad = null; - - if (_inputDevice.ActiveId != null) - { - SetProfiles(); - - string id = GetCurrentGamepadId(); - - if (_inputDevice.ActiveId.StartsWith("keyboard")) - { - if (_inputConfig is StandardKeyboardInputConfig) - { - SetValues(_inputConfig); - } - - if (_mainWindow.InputManager.KeyboardDriver is GTK3KeyboardDriver) - { - // NOTE: To get input in this window, we need to bind a custom keyboard driver instead of using the InputManager one as the main window isn't focused... - _selectedGamepad = _gtk3KeyboardDriver.GetGamepad(id); - } - else - { - _selectedGamepad = _mainWindow.InputManager.KeyboardDriver.GetGamepad(id); - } - } - else if (_inputDevice.ActiveId.StartsWith("controller")) - { - if (_inputConfig is StandardControllerInputConfig) - { - SetValues(_inputConfig); - } - - _selectedGamepad = _mainWindow.InputManager.GamepadDriver.GetGamepad(id); - } - } - } - - private string GetCurrentGamepadId() - { - if (_inputDevice.ActiveId == null || _inputDevice.ActiveId == "disabled") - { - return null; - } - - return _inputDevice.ActiveId.Split("/")[1].Split(" ")[0]; - } - - private void Controller_Changed(object sender, EventArgs args) - { - SetControllerSpecificFields(); - } - - private IButtonAssigner CreateButtonAssigner(bool forStick) - { - IButtonAssigner assigner; - - if (_inputDevice.ActiveId.StartsWith("keyboard")) - { - assigner = new KeyboardKeyAssigner((IKeyboard)_selectedGamepad); - } - else if (_inputDevice.ActiveId.StartsWith("controller")) - { - assigner = new GamepadButtonAssigner(_selectedGamepad, (float)_controllerTriggerThreshold.Value, forStick); - } - else - { - throw new Exception("Controller not supported"); - } - - return assigner; - } - - private void HandleButtonPressed(ToggleButton button, bool forStick) - { - if (_isWaitingForInput) - { - button.Active = false; - - return; - } - - _mousePressed = false; - - ButtonPressEvent += MouseClick; - - IButtonAssigner assigner = CreateButtonAssigner(forStick); - - _isWaitingForInput = true; - - // Open GTK3 keyboard for cancel operations - IKeyboard keyboard = (IKeyboard)_gtk3KeyboardDriver.GetGamepad("0"); - - Thread inputThread = new(() => - { - assigner.Initialize(); - - while (true) - { - Thread.Sleep(10); - assigner.ReadInput(); - - if (_mousePressed || keyboard.IsPressed(Ryujinx.Input.Key.Escape) || assigner.HasAnyButtonPressed() || assigner.ShouldCancel()) - { - break; - } - } - - string pressedButton = assigner.GetPressedButton(); - - Application.Invoke(delegate - { - if (_middleMousePressed) - { - button.Label = "Unbound"; - } - else if (pressedButton != "") - { - button.Label = pressedButton; - } - - _middleMousePressed = false; - - ButtonPressEvent -= MouseClick; - keyboard.Dispose(); - - button.Active = false; - _isWaitingForInput = false; - }); - }) - { - Name = "GUI.InputThread", - IsBackground = true, - }; - inputThread.Start(); - } - - private void Button_Pressed(object sender, EventArgs args) - { - HandleButtonPressed((ToggleButton)sender, false); - } - - private void ButtonForStick_Pressed(object sender, EventArgs args) - { - HandleButtonPressed((ToggleButton)sender, true); - } - - private void MouseClick(object sender, ButtonPressEventArgs args) - { - _mousePressed = true; - _middleMousePressed = args.Event.Button == 2; - } - - private void SetProfiles() - { - _profile.RemoveAll(); - - string basePath = GetProfileBasePath(); - - if (!Directory.Exists(basePath)) - { - Directory.CreateDirectory(basePath); - } - - if (_inputDevice.ActiveId == null || _inputDevice.ActiveId.Equals("disabled")) - { - _profile.Append("default", "None"); - } - else - { - _profile.Append("default", "Default"); - - foreach (string profile in Directory.GetFiles(basePath, "*.*", SearchOption.AllDirectories)) - { - _profile.Append(System.IO.Path.GetFileName(profile), System.IO.Path.GetFileNameWithoutExtension(profile)); - } - } - - _profile.SetActiveId("default"); - } - - private void ProfileLoad_Activated(object sender, EventArgs args) - { - ((ToggleButton)sender).SetStateFlags(StateFlags.Normal, true); - - if (_inputDevice.ActiveId == "disabled" || _profile.ActiveId == null) - { - return; - } - - InputConfig config = null; - int pos = _profile.Active; - - if (_profile.ActiveId == "default") - { - if (_inputDevice.ActiveId.StartsWith("keyboard")) - { - config = new StandardKeyboardInputConfig - { - Version = InputConfig.CurrentVersion, - Backend = InputBackendType.WindowKeyboard, - Id = null, - ControllerType = ControllerType.ProController, - LeftJoycon = new LeftJoyconCommonConfig - { - DpadUp = Key.Up, - DpadDown = Key.Down, - DpadLeft = Key.Left, - DpadRight = Key.Right, - ButtonMinus = Key.Minus, - ButtonL = Key.E, - ButtonZl = Key.Q, - ButtonSl = Key.Unbound, - ButtonSr = Key.Unbound, - }, - - LeftJoyconStick = new JoyconConfigKeyboardStick - { - StickUp = Key.W, - StickDown = Key.S, - StickLeft = Key.A, - StickRight = Key.D, - StickButton = Key.F, - }, - - RightJoycon = new RightJoyconCommonConfig - { - ButtonA = Key.Z, - ButtonB = Key.X, - ButtonX = Key.C, - ButtonY = Key.V, - ButtonPlus = Key.Plus, - ButtonR = Key.U, - ButtonZr = Key.O, - ButtonSl = Key.Unbound, - ButtonSr = Key.Unbound, - }, - - RightJoyconStick = new JoyconConfigKeyboardStick - { - StickUp = Key.I, - StickDown = Key.K, - StickLeft = Key.J, - StickRight = Key.L, - StickButton = Key.H, - }, - }; - } - else if (_inputDevice.ActiveId.StartsWith("controller")) - { - bool isNintendoStyle = _inputDevice.ActiveText.Contains("Nintendo"); - - config = new StandardControllerInputConfig - { - Version = InputConfig.CurrentVersion, - Backend = InputBackendType.GamepadSDL2, - Id = null, - ControllerType = ControllerType.JoyconPair, - DeadzoneLeft = 0.1f, - DeadzoneRight = 0.1f, - RangeLeft = 1.0f, - RangeRight = 1.0f, - TriggerThreshold = 0.5f, - LeftJoycon = new LeftJoyconCommonConfig - { - DpadUp = ConfigGamepadInputId.DpadUp, - DpadDown = ConfigGamepadInputId.DpadDown, - DpadLeft = ConfigGamepadInputId.DpadLeft, - DpadRight = ConfigGamepadInputId.DpadRight, - ButtonMinus = ConfigGamepadInputId.Minus, - ButtonL = ConfigGamepadInputId.LeftShoulder, - ButtonZl = ConfigGamepadInputId.LeftTrigger, - ButtonSl = ConfigGamepadInputId.Unbound, - ButtonSr = ConfigGamepadInputId.Unbound, - }, - - LeftJoyconStick = new JoyconConfigControllerStick - { - Joystick = ConfigStickInputId.Left, - StickButton = ConfigGamepadInputId.LeftStick, - InvertStickX = false, - InvertStickY = false, - Rotate90CW = false, - }, - - RightJoycon = new RightJoyconCommonConfig - { - ButtonA = isNintendoStyle ? ConfigGamepadInputId.A : ConfigGamepadInputId.B, - ButtonB = isNintendoStyle ? ConfigGamepadInputId.B : ConfigGamepadInputId.A, - ButtonX = isNintendoStyle ? ConfigGamepadInputId.X : ConfigGamepadInputId.Y, - ButtonY = isNintendoStyle ? ConfigGamepadInputId.Y : ConfigGamepadInputId.X, - ButtonPlus = ConfigGamepadInputId.Plus, - ButtonR = ConfigGamepadInputId.RightShoulder, - ButtonZr = ConfigGamepadInputId.RightTrigger, - ButtonSl = ConfigGamepadInputId.Unbound, - ButtonSr = ConfigGamepadInputId.Unbound, - }, - - RightJoyconStick = new JoyconConfigControllerStick - { - Joystick = ConfigStickInputId.Right, - StickButton = ConfigGamepadInputId.RightStick, - InvertStickX = false, - InvertStickY = false, - Rotate90CW = false, - }, - - Motion = new StandardMotionConfigController - { - MotionBackend = MotionInputBackendType.GamepadDriver, - EnableMotion = true, - Sensitivity = 100, - GyroDeadzone = 1, - }, - Rumble = new RumbleConfigController - { - StrongRumble = 1f, - WeakRumble = 1f, - EnableRumble = false, - }, - }; - } - } - else - { - string path = System.IO.Path.Combine(GetProfileBasePath(), _profile.ActiveId); - - if (!File.Exists(path)) - { - if (pos >= 0) - { - _profile.Remove(pos); - } - - return; - } - - try - { - config = JsonHelper.DeserializeFromFile(path, _serializerContext.InputConfig); - } - catch (JsonException) { } - } - - SetValues(config); - } - - private void ProfileAdd_Activated(object sender, EventArgs args) - { - ((ToggleButton)sender).SetStateFlags(StateFlags.Normal, true); - - if (_inputDevice.ActiveId == "disabled") - { - return; - } - - InputConfig inputConfig = GetValues(); - ProfileDialog profileDialog = new(); - - if (inputConfig == null) - { - return; - } - - if (profileDialog.Run() == (int)ResponseType.Ok) - { - string path = System.IO.Path.Combine(GetProfileBasePath(), profileDialog.FileName); - string jsonString = JsonHelper.Serialize(inputConfig, _serializerContext.InputConfig); - - File.WriteAllText(path, jsonString); - } - - profileDialog.Dispose(); - - SetProfiles(); - } - - private void ProfileRemove_Activated(object sender, EventArgs args) - { - ((ToggleButton)sender).SetStateFlags(StateFlags.Normal, true); - - if (_inputDevice.ActiveId == "disabled" || _profile.ActiveId == "default" || _profile.ActiveId == null) - { - return; - } - - MessageDialog confirmDialog = GtkDialog.CreateConfirmationDialog("Deleting Profile", "This action is irreversible, are you sure you want to continue?"); - - if (confirmDialog.Run() == (int)ResponseType.Yes) - { - string path = System.IO.Path.Combine(GetProfileBasePath(), _profile.ActiveId); - - if (File.Exists(path)) - { - File.Delete(path); - } - - SetProfiles(); - } - } - - private void SaveToggle_Activated(object sender, EventArgs args) - { - InputConfig inputConfig = GetValues(); - - var newConfig = new List(); - newConfig.AddRange(ConfigurationState.Instance.Hid.InputConfig.Value); - - if (_inputConfig == null && inputConfig != null) - { - newConfig.Add(inputConfig); - } - else - { - if (_inputDevice.ActiveId == "disabled") - { - newConfig.Remove(_inputConfig); - } - else if (inputConfig != null) - { - int index = newConfig.IndexOf(_inputConfig); - - newConfig[index] = inputConfig; - } - } - - _mainWindow.RendererWidget?.NpadManager.ReloadConfiguration(newConfig, ConfigurationState.Instance.Hid.EnableKeyboard, ConfigurationState.Instance.Hid.EnableMouse); - - // Atomically replace and signal input change. - // NOTE: Do not modify InputConfig.Value directly as other code depends on the on-change event. - ConfigurationState.Instance.Hid.InputConfig.Value = newConfig; - - ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.ConfigurationPath); - - Dispose(); - } - - private void CloseToggle_Activated(object sender, EventArgs args) - { - Dispose(); - } - } -} diff --git a/src/Ryujinx/Ui/Windows/ControllerWindow.glade b/src/Ryujinx/Ui/Windows/ControllerWindow.glade deleted file mode 100644 index e433f5cc4..000000000 --- a/src/Ryujinx/Ui/Windows/ControllerWindow.glade +++ /dev/null @@ -1,2241 +0,0 @@ - - - - - - 4 - 1 - 4 - - - 0.1 - 10 - 1.0 - 0.1 - 1.0 - - - 0.1 - 10 - 1.0 - 0.1 - 1.0 - - - 1 - 0.050000000000000003 - 0.01 - 0.10000000000000001 - - - 1 - 0.050000000000000003 - 0.01 - 0.10000000000000001 - - - 2 - 1.000000000000000003 - 0.01 - 0.10000000000000001 - - - 2 - 1.000000000000000003 - 0.01 - 0.10000000000000001 - - - 1 - 0.5 - 0.01 - 0.10000000000000001 - - - 100 - 0.01 - 0.01 - 0.10000000000000001 - 0.10000000000000001 - - - 1000 - 100 - 1 - 4 - - - 4 - 1 - 4 - - - False - Ryujinx - Controller Settings - True - center - 1200 - 720 - - - - - - True - False - vertical - - - True - True - in - - - True - False - - - True - False - vertical - - - True - False - 10 - 10 - 10 - - - True - False - - - True - False - 5 - Input Device - - - False - True - 0 - - - - - True - False - 0 - disabled - - Disabled - - - - - True - True - 1 - - - - - False - True - 0 - - - - - True - False - 20 - - - True - False - The controller's type - center - 5 - Controller Type: - - - False - True - 0 - - - - - True - False - The controller's type - 0 - - - - False - True - 1 - - - - - False - True - 1 - - - - - True - False - 20 - - - True - False - 5 - Profile: - - - False - True - 0 - - - - - True - False - 5 - 0 - default - - - False - True - 1 - - - - - Load - 60 - True - True - True - 5 - - - - False - True - 2 - - - - - Add - 60 - True - True - True - 5 - - - - False - True - 3 - - - - - Remove - 60 - True - True - True - - - - False - True - 4 - - - - - False - True - 2 - - - - - False - True - 0 - - - - - True - False - - - True - False - vertical - - - True - False - 10 - 5 - - - 156 - True - False - 10 - vertical - - - True - False - 5 - 5 - Buttons - - - - - - False - True - 0 - - - - - True - False - 3 - 10 - - - 80 - True - False - A - - - 0 - 0 - - - - - 80 - True - False - B - - - 0 - 1 - - - - - 80 - True - False - X - - - 0 - 2 - - - - - 80 - True - False - Y - - - 0 - 3 - - - - - - 70 - True - True - True - - - 1 - 0 - - - - - - 70 - True - True - True - - - 1 - 1 - - - - - - 70 - True - True - True - - - 1 - 2 - - - - - - 70 - True - True - True - - - 1 - 3 - - - - - 80 - True - False - + - - - 0 - 4 - - - - - 80 - True - False - - - - - 0 - 5 - - - - - - 70 - True - True - True - - - 1 - 5 - - - - - - 70 - True - True - True - - - 1 - 4 - - - - - False - True - 1 - - - - - False - True - 0 - - - - - True - False - - - False - True - 1 - - - - - 160 - True - False - 10 - 10 - vertical - - - True - False - 5 - 5 - Left Stick - - - - - - False - True - 0 - - - - - True - False - 5 - 3 - 10 - - - 80 - True - False - LStick Button - 0 - - - 0 - 0 - - - - - - 65 - True - True - True - - - 1 - 0 - - - - - False - True - 1 - - - - - True - False - 3 - 10 - - - - 65 - True - True - True - - - 1 - 1 - - - - - - 65 - True - True - True - - - 1 - 0 - - - - - - 65 - True - True - True - - - 1 - 2 - - - - - - 65 - True - True - True - - - 1 - 3 - - - - - 80 - True - False - LStick Down - 0 - - - 0 - 1 - - - - - 80 - True - False - LStick Up - 0 - - - 0 - 0 - - - - - 80 - True - False - LStick Right - 0 - - - 0 - 3 - - - - - 80 - True - False - LStick Left - 0 - - - 0 - 2 - - - - - False - True - 2 - - - - - True - False - 3 - 10 - - - 80 - True - False - LStick - 0 - - - 0 - 0 - - - - - - 65 - True - True - True - - - 1 - 0 - - - - - Invert Stick X - True - True - False - True - - - 2 - 0 - - - - - Invert Stick Y - True - True - False - True - - - 2 - 1 - - - - - Rotate 90° Clockwise - True - True - False - True - - - 2 - 2 - - - - - False - True - 3 - - - - - True - False - 10 - vertical - - - True - False - start - Deadzone Left - - - False - True - 0 - - - - - True - True - _controllerDeadzoneLeft - 2 - 2 - - - True - True - 1 - - - - - False - True - 4 - - - - - True - False - 10 - vertical - - - True - False - start - Range Left - - - False - True - 0 - - - - - True - True - _controllerRangeLeft - 2 - 2 - - - True - True - 1 - - - - - False - True - 5 - - - - - False - True - 2 - - - - - True - False - - - False - True - 3 - - - - - 150 - True - False - 10 - 10 - vertical - - - True - False - 5 - 5 - Triggers - - - - - - False - True - 0 - - - - - True - False - 3 - 10 - - - 80 - True - False - L - - - 0 - 0 - - - - - 80 - True - False - R - - - 0 - 1 - - - - - - 65 - True - True - True - - - 1 - 0 - - - - - - 65 - True - True - True - - - 1 - 1 - - - - - 80 - True - False - ZL - - - 0 - 2 - - - - - 80 - True - False - ZR - - - 0 - 3 - - - - - - 65 - True - True - True - - - 1 - 2 - - - - - - 65 - True - True - True - - - 1 - 3 - - - - - False - True - 1 - - - - - _sideTriggerBox - True - False - 5 - 3 - 10 - - - 80 - True - False - Left SL - - - 0 - 0 - - - - - 80 - True - False - Left SR - - - 0 - 1 - - - - - - 65 - True - True - True - - - 1 - 0 - - - - - - 65 - True - True - True - - - 1 - 1 - - - - - False - True - 2 - - - - - _sideTriggerBox - True - False - 5 - 3 - 10 - - - 80 - True - False - Right SL - - - 0 - 0 - - - - - 80 - True - False - Right SR - - - 0 - 1 - - - - - - 65 - True - True - True - - - 1 - 0 - - - - - - 65 - True - True - True - - - 1 - 1 - - - - - False - True - 3 - - - - - True - False - 10 - vertical - - - True - False - start - 10 - Trigger Threshold - - - False - True - 0 - - - - - True - True - _controllerTriggerThreshold - 2 - 2 - - - True - True - 1 - - - - - False - True - 4 - - - - - False - True - 4 - - - - - False - True - 0 - - - - - True - False - 10 - - - 156 - True - False - 10 - 10 - vertical - - - True - False - 5 - 5 - Directional Pad - - - - - - False - True - 0 - - - - - True - False - 3 - 10 - - - 80 - True - False - Dpad Up - 0 - - - 0 - 0 - - - - - 80 - True - False - Dpad Down - 0 - - - 0 - 1 - - - - - 80 - True - False - Dpad Left - 0 - - - 0 - 2 - - - - - 80 - True - False - Dpad Right - 0 - - - 0 - 3 - - - - - - 70 - True - True - True - - - 1 - 0 - - - - - - 70 - True - True - True - - - 1 - 1 - - - - - - 70 - True - True - True - - - 1 - 2 - - - - - - 70 - True - True - True - - - 1 - 3 - - - - - False - True - 1 - - - - - True - False - 10 - vertical - - - True - False - 10 - 5 - Rumble - - - - - - False - True - 0 - - - - - Enable - True - True - False - True - - - False - True - 1 - - - - - True - False - 10 - vertical - - - True - False - start - Strong rumble multiplier - - - False - True - 0 - - - - - True - True - _controllerStrongRumble - 1 - 1 - - - True - True - 1 - - - - - False - True - 2 - - - - - True - False - 10 - vertical - - - True - False - start - Weak rumble multiplier - - - False - True - 0 - - - - - True - True - _controllerWeakRumble - 1 - 1 - - - True - True - 1 - - - - - False - True - 3 - - - - - False - True - 2 - - - - - False - True - 0 - - - - - True - False - - - False - True - 1 - - - - - 160 - True - False - 10 - 10 - vertical - - - True - False - 5 - 5 - Right Stick - - - - - - False - True - 0 - - - - - True - False - 5 - 3 - 10 - - - 80 - True - False - RStick Button - 0 - - - 0 - 0 - - - - - - 65 - True - True - True - - - 1 - 0 - - - - - False - True - 1 - - - - - True - False - 3 - 10 - - - 80 - True - False - RStick Up - 0 - - - 0 - 0 - - - - - 80 - True - False - RStick Down - 0 - - - 0 - 1 - - - - - 80 - True - False - RStick Left - 0 - - - 0 - 2 - - - - - 80 - True - False - RStick Right - 0 - - - 0 - 3 - - - - - - 65 - True - True - True - - - 1 - 0 - - - - - - 65 - True - True - True - - - 1 - 1 - - - - - - 65 - True - True - True - - - 1 - 2 - - - - - - 65 - True - True - True - - - 1 - 3 - - - - - False - True - 2 - - - - - True - False - 3 - 10 - - - 80 - True - False - RStick - 0 - - - 0 - 0 - - - - - - 65 - True - True - True - - - 1 - 0 - - - - - Invert Stick X - True - True - False - True - - - 2 - 0 - - - - - Invert Stick Y - True - True - False - True - - - 2 - 1 - - - - - Rotate 90° Clockwise - True - True - False - True - - - 2 - 2 - - - - - False - True - 3 - - - - - True - False - 10 - vertical - - - True - False - start - Deadzone Right - - - False - True - 0 - - - - - True - True - _controllerDeadzoneRight - 2 - 2 - - - True - True - 1 - - - - - False - True - 4 - - - - - True - False - 10 - vertical - - - True - False - start - Range Right - - - False - True - 0 - - - - - True - True - _controllerRangeRight - 2 - 2 - - - True - True - 1 - - - - - False - True - 5 - - - - - False - True - 2 - - - - - True - False - - - False - True - 3 - - - - - True - False - 10 - 10 - vertical - 5 - - - True - False - 5 - 5 - Motion - - - - - - False - True - 0 - - - - - Enable Motion Controls - True - True - False - True - - - False - True - 1 - - - - - Use CemuHook compatible motion - True - True - False - True - - - False - True - 2 - - - - - True - False - 10 - - - True - False - 17 - Controller Slot - - - False - True - 5 - 0 - - - - - True - True - 10 - _slotNumber - 1 - True - True - - - False - True - 1 - - - - - False - True - 5 - 3 - - - - - True - False - 10 - - - True - False - 5 - Gyro Sensitivity % - - - False - True - 5 - 0 - - - - - True - True - 0 - _sensitivity - 1 - True - True - - - False - True - 1 - - - - - False - True - 5 - 4 - - - - - True - False - vertical - - - Mirror Input - True - True - False - True - - - False - True - 0 - - - - - True - False - 10 - - - True - False - Right JoyCon Slot - - - False - True - 5 - 0 - - - - - True - True - 0 - _altSlotNumber - 1 - True - True - - - False - True - 1 - - - - - False - True - 5 - 1 - - - - - False - True - 5 - - - - - True - False - 30 - - - True - False - Server Host - - - False - True - 5 - 0 - - - - - True - True - - - False - True - 1 - - - - - False - True - 5 - 6 - - - - - True - False - 30 - - - True - False - Server Port - - - False - True - 5 - 0 - - - - - True - True - - - False - True - 1 - - - - - False - True - 5 - 7 - - - - - True - False - start - Gyro Deadzone - - - False - True - 8 - - - - - True - True - _gyroDeadzone - 2 - 2 - - - True - True - 9 - - - - - False - True - 4 - - - - - False - True - 1 - - - - - True - True - 0 - - - - - True - False - 10 - 20 - 5 - 5 - - - True - True - 1 - - - - - True - True - 1 - - - - - - - - - True - True - 0 - - - - - True - False - 5 - 3 - 3 - end - - - Save - True - True - True - - - - False - True - 0 - - - - - Close - True - True - True - 4 - - - - False - True - 5 - 1 - - - - - False - False - 1 - - - - - - diff --git a/src/Ryujinx/Ui/Windows/DlcWindow.cs b/src/Ryujinx/Ui/Windows/DlcWindow.cs deleted file mode 100644 index 9f7179467..000000000 --- a/src/Ryujinx/Ui/Windows/DlcWindow.cs +++ /dev/null @@ -1,280 +0,0 @@ -using Gtk; -using LibHac.Common; -using LibHac.Fs; -using LibHac.Fs.Fsa; -using LibHac.FsSystem; -using LibHac.Tools.Fs; -using LibHac.Tools.FsSystem; -using LibHac.Tools.FsSystem.NcaUtils; -using Ryujinx.Common.Configuration; -using Ryujinx.Common.Utilities; -using Ryujinx.HLE.FileSystem; -using Ryujinx.Ui.Widgets; -using System; -using System.Collections.Generic; -using System.IO; -using GUI = Gtk.Builder.ObjectAttribute; - -namespace Ryujinx.Ui.Windows -{ - public class DlcWindow : Window - { - private readonly VirtualFileSystem _virtualFileSystem; - private readonly string _titleId; - private readonly string _dlcJsonPath; - private readonly List _dlcContainerList; - - private static readonly DownloadableContentJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions()); - -#pragma warning disable CS0649, IDE0044 // Field is never assigned to, Add readonly modifier - [GUI] Label _baseTitleInfoLabel; - [GUI] TreeView _dlcTreeView; - [GUI] TreeSelection _dlcTreeSelection; -#pragma warning restore CS0649, IDE0044 - - public DlcWindow(VirtualFileSystem virtualFileSystem, string titleId, string titleName) : this(new Builder("Ryujinx.Ui.Windows.DlcWindow.glade"), virtualFileSystem, titleId, titleName) { } - - private DlcWindow(Builder builder, VirtualFileSystem virtualFileSystem, string titleId, string titleName) : base(builder.GetRawOwnedObject("_dlcWindow")) - { - builder.Autoconnect(this); - - _titleId = titleId; - _virtualFileSystem = virtualFileSystem; - _dlcJsonPath = System.IO.Path.Combine(AppDataManager.GamesDirPath, _titleId, "dlc.json"); - _baseTitleInfoLabel.Text = $"DLC Available for {titleName} [{titleId.ToUpper()}]"; - - try - { - _dlcContainerList = JsonHelper.DeserializeFromFile(_dlcJsonPath, _serializerContext.ListDownloadableContentContainer); - } - catch - { - _dlcContainerList = new List(); - } - - _dlcTreeView.Model = new TreeStore(typeof(bool), typeof(string), typeof(string)); - - CellRendererToggle enableToggle = new(); - enableToggle.Toggled += (sender, args) => - { - _dlcTreeView.Model.GetIter(out TreeIter treeIter, new TreePath(args.Path)); - bool newValue = !(bool)_dlcTreeView.Model.GetValue(treeIter, 0); - _dlcTreeView.Model.SetValue(treeIter, 0, newValue); - - if (_dlcTreeView.Model.IterChildren(out TreeIter childIter, treeIter)) - { - do - { - _dlcTreeView.Model.SetValue(childIter, 0, newValue); - } - while (_dlcTreeView.Model.IterNext(ref childIter)); - } - }; - - _dlcTreeView.AppendColumn("Enabled", enableToggle, "active", 0); - _dlcTreeView.AppendColumn("TitleId", new CellRendererText(), "text", 1); - _dlcTreeView.AppendColumn("Path", new CellRendererText(), "text", 2); - - foreach (DownloadableContentContainer dlcContainer in _dlcContainerList) - { - if (File.Exists(dlcContainer.ContainerPath)) - { - // The parent tree item has its own "enabled" check box, but it's the actual - // nca entries that store the enabled / disabled state. A bit of a UI inconsistency. - // Maybe a tri-state check box would be better, but for now we check the parent - // "enabled" box if all child NCAs are enabled. Usually fine since each nsp has only one nca. - bool areAllContentPacksEnabled = dlcContainer.DownloadableContentNcaList.TrueForAll((nca) => nca.Enabled); - TreeIter parentIter = ((TreeStore)_dlcTreeView.Model).AppendValues(areAllContentPacksEnabled, "", dlcContainer.ContainerPath); - - using FileStream containerFile = File.OpenRead(dlcContainer.ContainerPath); - - PartitionFileSystem pfs = new(); - pfs.Initialize(containerFile.AsStorage()).ThrowIfFailure(); - - _virtualFileSystem.ImportTickets(pfs); - - foreach (DownloadableContentNca dlcNca in dlcContainer.DownloadableContentNcaList) - { - using var ncaFile = new UniqueRef(); - - pfs.OpenFile(ref ncaFile.Ref, dlcNca.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); - Nca nca = TryCreateNca(ncaFile.Get.AsStorage(), dlcContainer.ContainerPath); - - if (nca != null) - { - ((TreeStore)_dlcTreeView.Model).AppendValues(parentIter, dlcNca.Enabled, nca.Header.TitleId.ToString("X16"), dlcNca.FullPath); - } - } - } - else - { - // DLC file moved or renamed. Allow the user to remove it without crashing the whole dialog. - TreeIter parentIter = ((TreeStore)_dlcTreeView.Model).AppendValues(false, "", $"(MISSING) {dlcContainer.ContainerPath}"); - } - } - } - - private Nca TryCreateNca(IStorage ncaStorage, string containerPath) - { - try - { - return new Nca(_virtualFileSystem.KeySet, ncaStorage); - } - catch (Exception exception) - { - GtkDialog.CreateErrorDialog($"{exception.Message}. Errored File: {containerPath}"); - } - - return null; - } - - private void AddButton_Clicked(object sender, EventArgs args) - { - FileChooserNative fileChooser = new("Select DLC files", this, FileChooserAction.Open, "Add", "Cancel") - { - SelectMultiple = true, - }; - - FileFilter filter = new() - { - Name = "Switch Game DLCs", - }; - filter.AddPattern("*.nsp"); - - fileChooser.AddFilter(filter); - - if (fileChooser.Run() == (int)ResponseType.Accept) - { - foreach (string containerPath in fileChooser.Filenames) - { - if (!File.Exists(containerPath)) - { - return; - } - - using FileStream containerFile = File.OpenRead(containerPath); - - PartitionFileSystem pfs = new(); - pfs.Initialize(containerFile.AsStorage()).ThrowIfFailure(); - bool containsDlc = false; - - _virtualFileSystem.ImportTickets(pfs); - - TreeIter? parentIter = null; - - foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca")) - { - using var ncaFile = new UniqueRef(); - - pfs.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); - - Nca nca = TryCreateNca(ncaFile.Get.AsStorage(), containerPath); - - if (nca == null) - { - continue; - } - - if (nca.Header.ContentType == NcaContentType.PublicData) - { - if ((nca.Header.TitleId & 0xFFFFFFFFFFFFE000).ToString("x16") != _titleId) - { - break; - } - - parentIter ??= ((TreeStore)_dlcTreeView.Model).AppendValues(true, "", containerPath); - - ((TreeStore)_dlcTreeView.Model).AppendValues(parentIter.Value, true, nca.Header.TitleId.ToString("X16"), fileEntry.FullPath); - containsDlc = true; - } - } - - if (!containsDlc) - { - GtkDialog.CreateErrorDialog("The specified file does not contain DLC for the selected title!"); - } - } - } - - fileChooser.Dispose(); - } - - private void RemoveButton_Clicked(object sender, EventArgs args) - { - if (_dlcTreeSelection.GetSelected(out ITreeModel treeModel, out TreeIter treeIter)) - { - if (_dlcTreeView.Model.IterParent(out TreeIter parentIter, treeIter) && _dlcTreeView.Model.IterNChildren(parentIter) <= 1) - { - ((TreeStore)treeModel).Remove(ref parentIter); - } - else - { - ((TreeStore)treeModel).Remove(ref treeIter); - } - } - } - - private void RemoveAllButton_Clicked(object sender, EventArgs args) - { - List toRemove = new(); - - if (_dlcTreeView.Model.GetIterFirst(out TreeIter iter)) - { - do - { - toRemove.Add(iter); - } - while (_dlcTreeView.Model.IterNext(ref iter)); - } - - foreach (TreeIter i in toRemove) - { - TreeIter j = i; - ((TreeStore)_dlcTreeView.Model).Remove(ref j); - } - } - - private void SaveButton_Clicked(object sender, EventArgs args) - { - _dlcContainerList.Clear(); - - if (_dlcTreeView.Model.GetIterFirst(out TreeIter parentIter)) - { - do - { - if (_dlcTreeView.Model.IterChildren(out TreeIter childIter, parentIter)) - { - DownloadableContentContainer dlcContainer = new() - { - ContainerPath = (string)_dlcTreeView.Model.GetValue(parentIter, 2), - DownloadableContentNcaList = new List(), - }; - - do - { - dlcContainer.DownloadableContentNcaList.Add(new DownloadableContentNca - { - Enabled = (bool)_dlcTreeView.Model.GetValue(childIter, 0), - TitleId = Convert.ToUInt64(_dlcTreeView.Model.GetValue(childIter, 1).ToString(), 16), - FullPath = (string)_dlcTreeView.Model.GetValue(childIter, 2), - }); - } - while (_dlcTreeView.Model.IterNext(ref childIter)); - - _dlcContainerList.Add(dlcContainer); - } - } - while (_dlcTreeView.Model.IterNext(ref parentIter)); - } - - JsonHelper.SerializeToFile(_dlcJsonPath, _dlcContainerList, _serializerContext.ListDownloadableContentContainer); - - Dispose(); - } - - private void CancelButton_Clicked(object sender, EventArgs args) - { - Dispose(); - } - } -} diff --git a/src/Ryujinx/Ui/Windows/DlcWindow.glade b/src/Ryujinx/Ui/Windows/DlcWindow.glade deleted file mode 100644 index bdb0e647a..000000000 --- a/src/Ryujinx/Ui/Windows/DlcWindow.glade +++ /dev/null @@ -1,202 +0,0 @@ - - - - - - False - Ryujinx - DLC Manager - True - center - 550 - 350 - - - True - False - vertical - - - True - False - vertical - - - True - False - 10 - 10 - 10 - 10 - Available DLC - - - False - True - 0 - - - - - True - True - 10 - 10 - in - - - True - False - - - True - True - False - - - - - - - - - - True - True - 1 - - - - - True - True - 0 - - - - - True - False - - - True - False - 10 - 10 - start - - - Add - True - True - True - Adds a DLC to this list - 10 - - - - True - True - 0 - - - - - Remove - True - True - True - Removes the selected DLC - 10 - - - - True - True - 1 - - - - - Remove All - True - True - True - Removes all DLCs - 10 - - - - True - True - 2 - - - - - True - True - 0 - - - - - True - False - 10 - 10 - end - - - Save - True - True - True - 10 - 2 - 2 - - - - True - True - 0 - - - - - Cancel - True - True - True - 10 - 2 - 2 - - - - True - True - 1 - - - - - True - True - 1 - - - - - False - True - 1 - - - - - - - - - diff --git a/src/Ryujinx/Ui/Windows/SettingsWindow.cs b/src/Ryujinx/Ui/Windows/SettingsWindow.cs deleted file mode 100644 index dabef14dd..000000000 --- a/src/Ryujinx/Ui/Windows/SettingsWindow.cs +++ /dev/null @@ -1,847 +0,0 @@ -using Gtk; -using LibHac.Tools.FsSystem; -using Ryujinx.Audio.Backends.OpenAL; -using Ryujinx.Audio.Backends.SDL2; -using Ryujinx.Audio.Backends.SoundIo; -using Ryujinx.Common.Configuration; -using Ryujinx.Common.Configuration.Hid; -using Ryujinx.Common.Configuration.Multiplayer; -using Ryujinx.Common.GraphicsDriver; -using Ryujinx.HLE.FileSystem; -using Ryujinx.HLE.HOS.Services.Time.TimeZone; -using Ryujinx.Ui.Common.Configuration; -using Ryujinx.Ui.Common.Configuration.System; -using Ryujinx.Ui.Helper; -using Ryujinx.Ui.Widgets; -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Net.NetworkInformation; -using System.Reflection; -using System.Text.RegularExpressions; -using System.Threading.Tasks; -using GUI = Gtk.Builder.ObjectAttribute; - -namespace Ryujinx.Ui.Windows -{ - public class SettingsWindow : Window - { - private readonly MainWindow _parent; - private readonly ListStore _gameDirsBoxStore; - private readonly ListStore _audioBackendStore; - private readonly TimeZoneContentManager _timeZoneContentManager; - private readonly HashSet _validTzRegions; - - private long _systemTimeOffset; - private float _previousVolumeLevel; - private bool _directoryChanged = false; - -#pragma warning disable CS0649, IDE0044 // Field is never assigned to, Add readonly modifier - [GUI] CheckButton _traceLogToggle; - [GUI] CheckButton _errorLogToggle; - [GUI] CheckButton _warningLogToggle; - [GUI] CheckButton _infoLogToggle; - [GUI] CheckButton _stubLogToggle; - [GUI] CheckButton _debugLogToggle; - [GUI] CheckButton _fileLogToggle; - [GUI] CheckButton _guestLogToggle; - [GUI] CheckButton _fsAccessLogToggle; - [GUI] Adjustment _fsLogSpinAdjustment; - [GUI] ComboBoxText _graphicsDebugLevel; - [GUI] CheckButton _dockedModeToggle; - [GUI] CheckButton _discordToggle; - [GUI] CheckButton _checkUpdatesToggle; - [GUI] CheckButton _showConfirmExitToggle; - [GUI] RadioButton _hideCursorNever; - [GUI] RadioButton _hideCursorOnIdle; - [GUI] RadioButton _hideCursorAlways; - [GUI] CheckButton _vSyncToggle; - [GUI] CheckButton _shaderCacheToggle; - [GUI] CheckButton _textureRecompressionToggle; - [GUI] CheckButton _macroHLEToggle; - [GUI] CheckButton _ptcToggle; - [GUI] CheckButton _internetToggle; - [GUI] CheckButton _fsicToggle; - [GUI] RadioButton _mmSoftware; - [GUI] RadioButton _mmHost; - [GUI] RadioButton _mmHostUnsafe; - [GUI] CheckButton _expandRamToggle; - [GUI] CheckButton _ignoreToggle; - [GUI] CheckButton _directKeyboardAccess; - [GUI] CheckButton _directMouseAccess; - [GUI] ComboBoxText _systemLanguageSelect; - [GUI] ComboBoxText _systemRegionSelect; - [GUI] Entry _systemTimeZoneEntry; - [GUI] EntryCompletion _systemTimeZoneCompletion; - [GUI] Box _audioBackendBox; - [GUI] ComboBox _audioBackendSelect; - [GUI] Label _audioVolumeLabel; - [GUI] Scale _audioVolumeSlider; - [GUI] SpinButton _systemTimeYearSpin; - [GUI] SpinButton _systemTimeMonthSpin; - [GUI] SpinButton _systemTimeDaySpin; - [GUI] SpinButton _systemTimeHourSpin; - [GUI] SpinButton _systemTimeMinuteSpin; - [GUI] Adjustment _systemTimeYearSpinAdjustment; - [GUI] Adjustment _systemTimeMonthSpinAdjustment; - [GUI] Adjustment _systemTimeDaySpinAdjustment; - [GUI] Adjustment _systemTimeHourSpinAdjustment; - [GUI] Adjustment _systemTimeMinuteSpinAdjustment; - [GUI] ComboBoxText _multiLanSelect; - [GUI] ComboBoxText _multiModeSelect; - [GUI] CheckButton _custThemeToggle; - [GUI] Entry _custThemePath; - [GUI] ToggleButton _browseThemePath; - [GUI] Label _custThemePathLabel; - [GUI] TreeView _gameDirsBox; - [GUI] Entry _addGameDirBox; - [GUI] ComboBoxText _galThreading; - [GUI] Entry _graphicsShadersDumpPath; - [GUI] ComboBoxText _anisotropy; - [GUI] ComboBoxText _aspectRatio; - [GUI] ComboBoxText _antiAliasing; - [GUI] ComboBoxText _scalingFilter; - [GUI] ComboBoxText _graphicsBackend; - [GUI] ComboBoxText _preferredGpu; - [GUI] ComboBoxText _resScaleCombo; - [GUI] Entry _resScaleText; - [GUI] Adjustment _scalingFilterLevel; - [GUI] Scale _scalingFilterSlider; - [GUI] ToggleButton _configureController1; - [GUI] ToggleButton _configureController2; - [GUI] ToggleButton _configureController3; - [GUI] ToggleButton _configureController4; - [GUI] ToggleButton _configureController5; - [GUI] ToggleButton _configureController6; - [GUI] ToggleButton _configureController7; - [GUI] ToggleButton _configureController8; - [GUI] ToggleButton _configureControllerH; - -#pragma warning restore CS0649, IDE0044 - - public SettingsWindow(MainWindow parent, VirtualFileSystem virtualFileSystem, ContentManager contentManager) : this(parent, new Builder("Ryujinx.Ui.Windows.SettingsWindow.glade"), virtualFileSystem, contentManager) { } - - private SettingsWindow(MainWindow parent, Builder builder, VirtualFileSystem virtualFileSystem, ContentManager contentManager) : base(builder.GetRawOwnedObject("_settingsWin")) - { - Icon = new Gdk.Pixbuf(Assembly.GetAssembly(typeof(ConfigurationState)), "Ryujinx.Ui.Common.Resources.Logo_Ryujinx.png"); - - _parent = parent; - - builder.Autoconnect(this); - - _timeZoneContentManager = new TimeZoneContentManager(); - _timeZoneContentManager.InitializeInstance(virtualFileSystem, contentManager, IntegrityCheckLevel.None); - - _validTzRegions = new HashSet(_timeZoneContentManager.LocationNameCache.Length, StringComparer.Ordinal); // Zone regions are identifiers. Must match exactly. - - // Bind Events. - _configureController1.Pressed += (sender, args) => ConfigureController_Pressed(sender, PlayerIndex.Player1); - _configureController2.Pressed += (sender, args) => ConfigureController_Pressed(sender, PlayerIndex.Player2); - _configureController3.Pressed += (sender, args) => ConfigureController_Pressed(sender, PlayerIndex.Player3); - _configureController4.Pressed += (sender, args) => ConfigureController_Pressed(sender, PlayerIndex.Player4); - _configureController5.Pressed += (sender, args) => ConfigureController_Pressed(sender, PlayerIndex.Player5); - _configureController6.Pressed += (sender, args) => ConfigureController_Pressed(sender, PlayerIndex.Player6); - _configureController7.Pressed += (sender, args) => ConfigureController_Pressed(sender, PlayerIndex.Player7); - _configureController8.Pressed += (sender, args) => ConfigureController_Pressed(sender, PlayerIndex.Player8); - _configureControllerH.Pressed += (sender, args) => ConfigureController_Pressed(sender, PlayerIndex.Handheld); - _systemTimeZoneEntry.FocusOutEvent += TimeZoneEntry_FocusOut; - - _resScaleCombo.Changed += (sender, args) => _resScaleText.Visible = _resScaleCombo.ActiveId == "-1"; - _scalingFilter.Changed += (sender, args) => _scalingFilterSlider.Visible = _scalingFilter.ActiveId == "2"; - _galThreading.Changed += (sender, args) => - { - if (_galThreading.ActiveId != ConfigurationState.Instance.Graphics.BackendThreading.Value.ToString()) - { - GtkDialog.CreateInfoDialog("Warning - Backend Threading", "Ryujinx must be restarted after changing this option for it to apply fully. Depending on your platform, you may need to manually disable your driver's own multithreading when using Ryujinx's."); - } - }; - - // Setup Currents. - if (ConfigurationState.Instance.Logger.EnableTrace) - { - _traceLogToggle.Click(); - } - - if (ConfigurationState.Instance.Logger.EnableFileLog) - { - _fileLogToggle.Click(); - } - - if (ConfigurationState.Instance.Logger.EnableError) - { - _errorLogToggle.Click(); - } - - if (ConfigurationState.Instance.Logger.EnableWarn) - { - _warningLogToggle.Click(); - } - - if (ConfigurationState.Instance.Logger.EnableInfo) - { - _infoLogToggle.Click(); - } - - if (ConfigurationState.Instance.Logger.EnableStub) - { - _stubLogToggle.Click(); - } - - if (ConfigurationState.Instance.Logger.EnableDebug) - { - _debugLogToggle.Click(); - } - - if (ConfigurationState.Instance.Logger.EnableGuest) - { - _guestLogToggle.Click(); - } - - if (ConfigurationState.Instance.Logger.EnableFsAccessLog) - { - _fsAccessLogToggle.Click(); - } - - foreach (GraphicsDebugLevel level in Enum.GetValues()) - { - _graphicsDebugLevel.Append(level.ToString(), level.ToString()); - } - - _graphicsDebugLevel.SetActiveId(ConfigurationState.Instance.Logger.GraphicsDebugLevel.Value.ToString()); - - if (ConfigurationState.Instance.System.EnableDockedMode) - { - _dockedModeToggle.Click(); - } - - if (ConfigurationState.Instance.EnableDiscordIntegration) - { - _discordToggle.Click(); - } - - if (ConfigurationState.Instance.CheckUpdatesOnStart) - { - _checkUpdatesToggle.Click(); - } - - if (ConfigurationState.Instance.ShowConfirmExit) - { - _showConfirmExitToggle.Click(); - } - - switch (ConfigurationState.Instance.HideCursor.Value) - { - case HideCursorMode.Never: - _hideCursorNever.Click(); - break; - case HideCursorMode.OnIdle: - _hideCursorOnIdle.Click(); - break; - case HideCursorMode.Always: - _hideCursorAlways.Click(); - break; - } - - if (ConfigurationState.Instance.Graphics.EnableVsync) - { - _vSyncToggle.Click(); - } - - if (ConfigurationState.Instance.Graphics.EnableShaderCache) - { - _shaderCacheToggle.Click(); - } - - if (ConfigurationState.Instance.Graphics.EnableTextureRecompression) - { - _textureRecompressionToggle.Click(); - } - - if (ConfigurationState.Instance.Graphics.EnableMacroHLE) - { - _macroHLEToggle.Click(); - } - - if (ConfigurationState.Instance.System.EnablePtc) - { - _ptcToggle.Click(); - } - - if (ConfigurationState.Instance.System.EnableInternetAccess) - { - _internetToggle.Click(); - } - - if (ConfigurationState.Instance.System.EnableFsIntegrityChecks) - { - _fsicToggle.Click(); - } - - switch (ConfigurationState.Instance.System.MemoryManagerMode.Value) - { - case MemoryManagerMode.SoftwarePageTable: - _mmSoftware.Click(); - break; - case MemoryManagerMode.HostMapped: - _mmHost.Click(); - break; - case MemoryManagerMode.HostMappedUnsafe: - _mmHostUnsafe.Click(); - break; - } - - if (ConfigurationState.Instance.System.ExpandRam) - { - _expandRamToggle.Click(); - } - - if (ConfigurationState.Instance.System.IgnoreMissingServices) - { - _ignoreToggle.Click(); - } - - if (ConfigurationState.Instance.Hid.EnableKeyboard) - { - _directKeyboardAccess.Click(); - } - - if (ConfigurationState.Instance.Hid.EnableMouse) - { - _directMouseAccess.Click(); - } - - if (ConfigurationState.Instance.Ui.EnableCustomTheme) - { - _custThemeToggle.Click(); - } - - // Custom EntryCompletion Columns. If added to glade, need to override more signals - ListStore tzList = new(typeof(string), typeof(string), typeof(string)); - _systemTimeZoneCompletion.Model = tzList; - - CellRendererText offsetCol = new(); - CellRendererText abbrevCol = new(); - - _systemTimeZoneCompletion.PackStart(offsetCol, false); - _systemTimeZoneCompletion.AddAttribute(offsetCol, "text", 0); - _systemTimeZoneCompletion.TextColumn = 1; // Regions Column - _systemTimeZoneCompletion.PackStart(abbrevCol, false); - _systemTimeZoneCompletion.AddAttribute(abbrevCol, "text", 2); - - int maxLocationLength = 0; - - foreach (var (offset, location, abbr) in _timeZoneContentManager.ParseTzOffsets()) - { - var hours = Math.DivRem(offset, 3600, out int seconds); - var minutes = Math.Abs(seconds) / 60; - - var abbr2 = (abbr.StartsWith('+') || abbr.StartsWith('-')) ? string.Empty : abbr; - - tzList.AppendValues($"UTC{hours:+0#;-0#;+00}:{minutes:D2} ", location, abbr2); - _validTzRegions.Add(location); - - maxLocationLength = Math.Max(maxLocationLength, location.Length); - } - - _systemTimeZoneEntry.WidthChars = Math.Max(20, maxLocationLength + 1); // Ensure minimum Entry width - _systemTimeZoneEntry.Text = _timeZoneContentManager.SanityCheckDeviceLocationName(ConfigurationState.Instance.System.TimeZone); - - _systemTimeZoneCompletion.MatchFunc = TimeZoneMatchFunc; - - _systemLanguageSelect.SetActiveId(ConfigurationState.Instance.System.Language.Value.ToString()); - _systemRegionSelect.SetActiveId(ConfigurationState.Instance.System.Region.Value.ToString()); - _galThreading.SetActiveId(ConfigurationState.Instance.Graphics.BackendThreading.Value.ToString()); - _resScaleCombo.SetActiveId(ConfigurationState.Instance.Graphics.ResScale.Value.ToString()); - _anisotropy.SetActiveId(ConfigurationState.Instance.Graphics.MaxAnisotropy.Value.ToString()); - _aspectRatio.SetActiveId(((int)ConfigurationState.Instance.Graphics.AspectRatio.Value).ToString()); - _graphicsBackend.SetActiveId(((int)ConfigurationState.Instance.Graphics.GraphicsBackend.Value).ToString()); - _antiAliasing.SetActiveId(((int)ConfigurationState.Instance.Graphics.AntiAliasing.Value).ToString()); - _scalingFilter.SetActiveId(((int)ConfigurationState.Instance.Graphics.ScalingFilter.Value).ToString()); - - UpdatePreferredGpuComboBox(); - - _graphicsBackend.Changed += (sender, e) => UpdatePreferredGpuComboBox(); - PopulateNetworkInterfaces(); - _multiLanSelect.SetActiveId(ConfigurationState.Instance.Multiplayer.LanInterfaceId.Value); - _multiModeSelect.SetActiveId(ConfigurationState.Instance.Multiplayer.Mode.Value.ToString()); - - _custThemePath.Buffer.Text = ConfigurationState.Instance.Ui.CustomThemePath; - _resScaleText.Buffer.Text = ConfigurationState.Instance.Graphics.ResScaleCustom.Value.ToString(); - _scalingFilterLevel.Value = ConfigurationState.Instance.Graphics.ScalingFilterLevel.Value; - _resScaleText.Visible = _resScaleCombo.ActiveId == "-1"; - _scalingFilterSlider.Visible = _scalingFilter.ActiveId == "2"; - _graphicsShadersDumpPath.Buffer.Text = ConfigurationState.Instance.Graphics.ShadersDumpPath; - _fsLogSpinAdjustment.Value = ConfigurationState.Instance.System.FsGlobalAccessLogMode; - _systemTimeOffset = ConfigurationState.Instance.System.SystemTimeOffset; - - _gameDirsBox.AppendColumn("", new CellRendererText(), "text", 0); - _gameDirsBoxStore = new ListStore(typeof(string)); - _gameDirsBox.Model = _gameDirsBoxStore; - - foreach (string gameDir in ConfigurationState.Instance.Ui.GameDirs.Value) - { - _gameDirsBoxStore.AppendValues(gameDir); - } - - if (_custThemeToggle.Active == false) - { - _custThemePath.Sensitive = false; - _custThemePathLabel.Sensitive = false; - _browseThemePath.Sensitive = false; - } - - // Setup system time spinners - UpdateSystemTimeSpinners(); - - _audioBackendStore = new ListStore(typeof(string), typeof(AudioBackend)); - - TreeIter openAlIter = _audioBackendStore.AppendValues("OpenAL", AudioBackend.OpenAl); - TreeIter soundIoIter = _audioBackendStore.AppendValues("SoundIO", AudioBackend.SoundIo); - TreeIter sdl2Iter = _audioBackendStore.AppendValues("SDL2", AudioBackend.SDL2); - TreeIter dummyIter = _audioBackendStore.AppendValues("Dummy", AudioBackend.Dummy); - - _audioBackendSelect = ComboBox.NewWithModelAndEntry(_audioBackendStore); - _audioBackendSelect.EntryTextColumn = 0; - _audioBackendSelect.Entry.IsEditable = false; - - switch (ConfigurationState.Instance.System.AudioBackend.Value) - { - case AudioBackend.OpenAl: - _audioBackendSelect.SetActiveIter(openAlIter); - break; - case AudioBackend.SoundIo: - _audioBackendSelect.SetActiveIter(soundIoIter); - break; - case AudioBackend.SDL2: - _audioBackendSelect.SetActiveIter(sdl2Iter); - break; - case AudioBackend.Dummy: - _audioBackendSelect.SetActiveIter(dummyIter); - break; - default: - throw new InvalidOperationException($"{nameof(ConfigurationState.Instance.System.AudioBackend)} contains an invalid value: {ConfigurationState.Instance.System.AudioBackend.Value}"); - } - - _audioBackendBox.Add(_audioBackendSelect); - _audioBackendSelect.Show(); - - _previousVolumeLevel = ConfigurationState.Instance.System.AudioVolume; - _audioVolumeLabel = new Label("Volume: "); - _audioVolumeSlider = new Scale(Orientation.Horizontal, 0, 100, 1); - _audioVolumeLabel.MarginStart = 10; - _audioVolumeSlider.ValuePos = PositionType.Right; - _audioVolumeSlider.WidthRequest = 200; - - _audioVolumeSlider.Value = _previousVolumeLevel * 100; - _audioVolumeSlider.ValueChanged += VolumeSlider_OnChange; - _audioBackendBox.Add(_audioVolumeLabel); - _audioBackendBox.Add(_audioVolumeSlider); - _audioVolumeLabel.Show(); - _audioVolumeSlider.Show(); - - bool openAlIsSupported = false; - bool soundIoIsSupported = false; - bool sdl2IsSupported = false; - - Task.Run(() => - { - openAlIsSupported = OpenALHardwareDeviceDriver.IsSupported; - soundIoIsSupported = !OperatingSystem.IsMacOS() && SoundIoHardwareDeviceDriver.IsSupported; - sdl2IsSupported = SDL2HardwareDeviceDriver.IsSupported; - }); - - // This function runs whenever the dropdown is opened - _audioBackendSelect.SetCellDataFunc(_audioBackendSelect.Cells[0], (layout, cell, model, iter) => - { - cell.Sensitive = ((AudioBackend)_audioBackendStore.GetValue(iter, 1)) switch - { - AudioBackend.OpenAl => openAlIsSupported, - AudioBackend.SoundIo => soundIoIsSupported, - AudioBackend.SDL2 => sdl2IsSupported, - AudioBackend.Dummy => true, - _ => throw new InvalidOperationException($"{nameof(_audioBackendStore)} contains an invalid value for iteration {iter}: {_audioBackendStore.GetValue(iter, 1)}"), - }; - }); - - if (OperatingSystem.IsMacOS()) - { - var store = (_graphicsBackend.Model as ListStore); - store.GetIter(out TreeIter openglIter, new TreePath(new[] { 1 })); - store.Remove(ref openglIter); - - _graphicsBackend.Model = store; - } - } - - private void UpdatePreferredGpuComboBox() - { - _preferredGpu.RemoveAll(); - - if (Enum.Parse(_graphicsBackend.ActiveId) == GraphicsBackend.Vulkan) - { - var devices = Graphics.Vulkan.VulkanRenderer.GetPhysicalDevices(); - string preferredGpuIdFromConfig = ConfigurationState.Instance.Graphics.PreferredGpu.Value; - string preferredGpuId = preferredGpuIdFromConfig; - bool noGpuId = string.IsNullOrEmpty(preferredGpuIdFromConfig); - - foreach (var device in devices) - { - string dGpu = device.IsDiscrete ? " (dGPU)" : ""; - _preferredGpu.Append(device.Id, $"{device.Name}{dGpu}"); - - // If there's no GPU selected yet, we just pick the first GPU. - // If there's a discrete GPU available, we always prefer that over the previous selection, - // as it is likely to have better performance and more features. - // If the configuration file already has a GPU selection, we always prefer that instead. - if (noGpuId && (string.IsNullOrEmpty(preferredGpuId) || device.IsDiscrete)) - { - preferredGpuId = device.Id; - } - } - - if (!string.IsNullOrEmpty(preferredGpuId)) - { - _preferredGpu.SetActiveId(preferredGpuId); - } - } - } - - private void PopulateNetworkInterfaces() - { - NetworkInterface[] interfaces = NetworkInterface.GetAllNetworkInterfaces(); - - foreach (NetworkInterface nif in interfaces) - { - string guid = nif.Id; - string name = nif.Name; - - _multiLanSelect.Append(guid, name); - } - } - - private void UpdateSystemTimeSpinners() - { - //Bind system time events - _systemTimeYearSpin.ValueChanged -= SystemTimeSpin_ValueChanged; - _systemTimeMonthSpin.ValueChanged -= SystemTimeSpin_ValueChanged; - _systemTimeDaySpin.ValueChanged -= SystemTimeSpin_ValueChanged; - _systemTimeHourSpin.ValueChanged -= SystemTimeSpin_ValueChanged; - _systemTimeMinuteSpin.ValueChanged -= SystemTimeSpin_ValueChanged; - - //Apply actual system time + SystemTimeOffset to system time spin buttons - DateTime systemTime = DateTime.Now.AddSeconds(_systemTimeOffset); - - _systemTimeYearSpinAdjustment.Value = systemTime.Year; - _systemTimeMonthSpinAdjustment.Value = systemTime.Month; - _systemTimeDaySpinAdjustment.Value = systemTime.Day; - _systemTimeHourSpinAdjustment.Value = systemTime.Hour; - _systemTimeMinuteSpinAdjustment.Value = systemTime.Minute; - - //Format spin buttons text to include leading zeros - _systemTimeYearSpin.Text = systemTime.Year.ToString("0000"); - _systemTimeMonthSpin.Text = systemTime.Month.ToString("00"); - _systemTimeDaySpin.Text = systemTime.Day.ToString("00"); - _systemTimeHourSpin.Text = systemTime.Hour.ToString("00"); - _systemTimeMinuteSpin.Text = systemTime.Minute.ToString("00"); - - //Bind system time events - _systemTimeYearSpin.ValueChanged += SystemTimeSpin_ValueChanged; - _systemTimeMonthSpin.ValueChanged += SystemTimeSpin_ValueChanged; - _systemTimeDaySpin.ValueChanged += SystemTimeSpin_ValueChanged; - _systemTimeHourSpin.ValueChanged += SystemTimeSpin_ValueChanged; - _systemTimeMinuteSpin.ValueChanged += SystemTimeSpin_ValueChanged; - } - - private void SaveSettings() - { - if (_directoryChanged) - { - List gameDirs = new(); - - _gameDirsBoxStore.GetIterFirst(out TreeIter treeIter); - - for (int i = 0; i < _gameDirsBoxStore.IterNChildren(); i++) - { - gameDirs.Add((string)_gameDirsBoxStore.GetValue(treeIter, 0)); - - _gameDirsBoxStore.IterNext(ref treeIter); - } - - ConfigurationState.Instance.Ui.GameDirs.Value = gameDirs; - - _directoryChanged = false; - } - - HideCursorMode hideCursor = HideCursorMode.Never; - - if (_hideCursorOnIdle.Active) - { - hideCursor = HideCursorMode.OnIdle; - } - - if (_hideCursorAlways.Active) - { - hideCursor = HideCursorMode.Always; - } - - if (!float.TryParse(_resScaleText.Buffer.Text, out float resScaleCustom) || resScaleCustom <= 0.0f) - { - resScaleCustom = 1.0f; - } - - if (_validTzRegions.Contains(_systemTimeZoneEntry.Text)) - { - ConfigurationState.Instance.System.TimeZone.Value = _systemTimeZoneEntry.Text; - } - - MemoryManagerMode memoryMode = MemoryManagerMode.SoftwarePageTable; - - if (_mmHost.Active) - { - memoryMode = MemoryManagerMode.HostMapped; - } - - if (_mmHostUnsafe.Active) - { - memoryMode = MemoryManagerMode.HostMappedUnsafe; - } - - BackendThreading backendThreading = Enum.Parse(_galThreading.ActiveId); - if (ConfigurationState.Instance.Graphics.BackendThreading != backendThreading) - { - DriverUtilities.ToggleOGLThreading(backendThreading == BackendThreading.Off); - } - - ConfigurationState.Instance.Logger.EnableError.Value = _errorLogToggle.Active; - ConfigurationState.Instance.Logger.EnableTrace.Value = _traceLogToggle.Active; - ConfigurationState.Instance.Logger.EnableWarn.Value = _warningLogToggle.Active; - ConfigurationState.Instance.Logger.EnableInfo.Value = _infoLogToggle.Active; - ConfigurationState.Instance.Logger.EnableStub.Value = _stubLogToggle.Active; - ConfigurationState.Instance.Logger.EnableDebug.Value = _debugLogToggle.Active; - ConfigurationState.Instance.Logger.EnableGuest.Value = _guestLogToggle.Active; - ConfigurationState.Instance.Logger.EnableFsAccessLog.Value = _fsAccessLogToggle.Active; - ConfigurationState.Instance.Logger.EnableFileLog.Value = _fileLogToggle.Active; - ConfigurationState.Instance.Logger.GraphicsDebugLevel.Value = Enum.Parse(_graphicsDebugLevel.ActiveId); - ConfigurationState.Instance.System.EnableDockedMode.Value = _dockedModeToggle.Active; - ConfigurationState.Instance.EnableDiscordIntegration.Value = _discordToggle.Active; - ConfigurationState.Instance.CheckUpdatesOnStart.Value = _checkUpdatesToggle.Active; - ConfigurationState.Instance.ShowConfirmExit.Value = _showConfirmExitToggle.Active; - ConfigurationState.Instance.HideCursor.Value = hideCursor; - ConfigurationState.Instance.Graphics.EnableVsync.Value = _vSyncToggle.Active; - ConfigurationState.Instance.Graphics.EnableShaderCache.Value = _shaderCacheToggle.Active; - ConfigurationState.Instance.Graphics.EnableTextureRecompression.Value = _textureRecompressionToggle.Active; - ConfigurationState.Instance.Graphics.EnableMacroHLE.Value = _macroHLEToggle.Active; - ConfigurationState.Instance.System.EnablePtc.Value = _ptcToggle.Active; - ConfigurationState.Instance.System.EnableInternetAccess.Value = _internetToggle.Active; - ConfigurationState.Instance.System.EnableFsIntegrityChecks.Value = _fsicToggle.Active; - ConfigurationState.Instance.System.MemoryManagerMode.Value = memoryMode; - ConfigurationState.Instance.System.ExpandRam.Value = _expandRamToggle.Active; - ConfigurationState.Instance.System.IgnoreMissingServices.Value = _ignoreToggle.Active; - ConfigurationState.Instance.Hid.EnableKeyboard.Value = _directKeyboardAccess.Active; - ConfigurationState.Instance.Hid.EnableMouse.Value = _directMouseAccess.Active; - ConfigurationState.Instance.Ui.EnableCustomTheme.Value = _custThemeToggle.Active; - ConfigurationState.Instance.System.Language.Value = Enum.Parse(_systemLanguageSelect.ActiveId); - ConfigurationState.Instance.System.Region.Value = Enum.Parse(_systemRegionSelect.ActiveId); - ConfigurationState.Instance.System.SystemTimeOffset.Value = _systemTimeOffset; - ConfigurationState.Instance.Ui.CustomThemePath.Value = _custThemePath.Buffer.Text; - ConfigurationState.Instance.Graphics.ShadersDumpPath.Value = _graphicsShadersDumpPath.Buffer.Text; - ConfigurationState.Instance.System.FsGlobalAccessLogMode.Value = (int)_fsLogSpinAdjustment.Value; - ConfigurationState.Instance.Graphics.MaxAnisotropy.Value = float.Parse(_anisotropy.ActiveId, CultureInfo.InvariantCulture); - ConfigurationState.Instance.Graphics.AspectRatio.Value = Enum.Parse(_aspectRatio.ActiveId); - ConfigurationState.Instance.Graphics.BackendThreading.Value = backendThreading; - ConfigurationState.Instance.Graphics.GraphicsBackend.Value = Enum.Parse(_graphicsBackend.ActiveId); - ConfigurationState.Instance.Graphics.PreferredGpu.Value = _preferredGpu.ActiveId; - ConfigurationState.Instance.Graphics.ResScale.Value = int.Parse(_resScaleCombo.ActiveId); - ConfigurationState.Instance.Graphics.ResScaleCustom.Value = resScaleCustom; - ConfigurationState.Instance.System.AudioVolume.Value = (float)_audioVolumeSlider.Value / 100.0f; - ConfigurationState.Instance.Graphics.AntiAliasing.Value = Enum.Parse(_antiAliasing.ActiveId); - ConfigurationState.Instance.Graphics.ScalingFilter.Value = Enum.Parse(_scalingFilter.ActiveId); - ConfigurationState.Instance.Graphics.ScalingFilterLevel.Value = (int)_scalingFilterLevel.Value; - ConfigurationState.Instance.Multiplayer.LanInterfaceId.Value = _multiLanSelect.ActiveId; - - _previousVolumeLevel = ConfigurationState.Instance.System.AudioVolume.Value; - - ConfigurationState.Instance.Multiplayer.Mode.Value = Enum.Parse(_multiModeSelect.ActiveId); - ConfigurationState.Instance.Multiplayer.LanInterfaceId.Value = _multiLanSelect.ActiveId; - - if (_audioBackendSelect.GetActiveIter(out TreeIter activeIter)) - { - ConfigurationState.Instance.System.AudioBackend.Value = (AudioBackend)_audioBackendStore.GetValue(activeIter, 1); - } - - ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.ConfigurationPath); - - _parent.UpdateInternetAccess(); - MainWindow.UpdateGraphicsConfig(); - ThemeHelper.ApplyTheme(); - } - - // - // Events - // - private void TimeZoneEntry_FocusOut(object sender, FocusOutEventArgs e) - { - if (!_validTzRegions.Contains(_systemTimeZoneEntry.Text)) - { - _systemTimeZoneEntry.Text = _timeZoneContentManager.SanityCheckDeviceLocationName(ConfigurationState.Instance.System.TimeZone); - } - } - - private bool TimeZoneMatchFunc(EntryCompletion compl, string key, TreeIter iter) - { - key = key.Trim().Replace(' ', '_'); - - return ((string)compl.Model.GetValue(iter, 1)).Contains(key, StringComparison.OrdinalIgnoreCase) || // region - ((string)compl.Model.GetValue(iter, 2)).StartsWith(key, StringComparison.OrdinalIgnoreCase) || // abbr - ((string)compl.Model.GetValue(iter, 0))[3..].StartsWith(key); // offset - } - - private void SystemTimeSpin_ValueChanged(object sender, EventArgs e) - { - int year = _systemTimeYearSpin.ValueAsInt; - int month = _systemTimeMonthSpin.ValueAsInt; - int day = _systemTimeDaySpin.ValueAsInt; - int hour = _systemTimeHourSpin.ValueAsInt; - int minute = _systemTimeMinuteSpin.ValueAsInt; - - if (!DateTime.TryParse(year + "-" + month + "-" + day + " " + hour + ":" + minute, out DateTime newTime)) - { - UpdateSystemTimeSpinners(); - - return; - } - - newTime = newTime.AddSeconds(DateTime.Now.Second).AddMilliseconds(DateTime.Now.Millisecond); - - long systemTimeOffset = (long)Math.Ceiling((newTime - DateTime.Now).TotalMinutes) * 60L; - - if (_systemTimeOffset != systemTimeOffset) - { - _systemTimeOffset = systemTimeOffset; - UpdateSystemTimeSpinners(); - } - } - - private void AddDir_Pressed(object sender, EventArgs args) - { - if (Directory.Exists(_addGameDirBox.Buffer.Text)) - { - _gameDirsBoxStore.AppendValues(_addGameDirBox.Buffer.Text); - _directoryChanged = true; - } - else - { - FileChooserNative fileChooser = new("Choose the game directory to add to the list", this, FileChooserAction.SelectFolder, "Add", "Cancel") - { - SelectMultiple = true, - }; - - if (fileChooser.Run() == (int)ResponseType.Accept) - { - _directoryChanged = false; - foreach (string directory in fileChooser.Filenames) - { - if (_gameDirsBoxStore.GetIterFirst(out TreeIter treeIter)) - { - do - { - if (directory.Equals((string)_gameDirsBoxStore.GetValue(treeIter, 0))) - { - break; - } - } while (_gameDirsBoxStore.IterNext(ref treeIter)); - } - - if (!_directoryChanged) - { - _gameDirsBoxStore.AppendValues(directory); - } - } - - _directoryChanged = true; - } - - fileChooser.Dispose(); - } - - _addGameDirBox.Buffer.Text = ""; - - ((ToggleButton)sender).SetStateFlags(StateFlags.Normal, true); - } - - private void RemoveDir_Pressed(object sender, EventArgs args) - { - TreeSelection selection = _gameDirsBox.Selection; - - if (selection.GetSelected(out TreeIter treeIter)) - { - _gameDirsBoxStore.Remove(ref treeIter); - - _directoryChanged = true; - } - - ((ToggleButton)sender).SetStateFlags(StateFlags.Normal, true); - } - - private void CustThemeToggle_Activated(object sender, EventArgs args) - { - _custThemePath.Sensitive = _custThemeToggle.Active; - _custThemePathLabel.Sensitive = _custThemeToggle.Active; - _browseThemePath.Sensitive = _custThemeToggle.Active; - } - - private void BrowseThemeDir_Pressed(object sender, EventArgs args) - { - using (FileChooserNative fileChooser = new("Choose the theme to load", this, FileChooserAction.Open, "Select", "Cancel")) - { - FileFilter filter = new() - { - Name = "Theme Files", - }; - filter.AddPattern("*.css"); - - fileChooser.AddFilter(filter); - - if (fileChooser.Run() == (int)ResponseType.Accept) - { - _custThemePath.Buffer.Text = fileChooser.Filename; - } - } - - _browseThemePath.SetStateFlags(StateFlags.Normal, true); - } - - private void ConfigureController_Pressed(object sender, PlayerIndex playerIndex) - { - ((ToggleButton)sender).SetStateFlags(StateFlags.Normal, true); - - ControllerWindow controllerWindow = new(_parent, playerIndex); - - controllerWindow.SetSizeRequest((int)(controllerWindow.DefaultWidth * Program.WindowScaleFactor), (int)(controllerWindow.DefaultHeight * Program.WindowScaleFactor)); - controllerWindow.Show(); - } - - private void VolumeSlider_OnChange(object sender, EventArgs args) - { - ConfigurationState.Instance.System.AudioVolume.Value = (float)(_audioVolumeSlider.Value / 100); - } - - private void SaveToggle_Activated(object sender, EventArgs args) - { - SaveSettings(); - Dispose(); - } - - private void ApplyToggle_Activated(object sender, EventArgs args) - { - SaveSettings(); - } - - private void CloseToggle_Activated(object sender, EventArgs args) - { - ConfigurationState.Instance.System.AudioVolume.Value = _previousVolumeLevel; - Dispose(); - } - } -} diff --git a/src/Ryujinx/Ui/Windows/SettingsWindow.glade b/src/Ryujinx/Ui/Windows/SettingsWindow.glade deleted file mode 100644 index f0dbd6b63..000000000 --- a/src/Ryujinx/Ui/Windows/SettingsWindow.glade +++ /dev/null @@ -1,3221 +0,0 @@ - - - - - - 3 - 1 - 10 - - - 101 - 1 - 5 - 1 - - - 1 - 31 - 1 - 5 - - - 23 - 1 - 5 - - - 59 - 1 - 5 - - - 1 - 12 - 1 - 5 - - - 2000 - 2060 - 1 - 10 - - - 0 - True - True - - - False - Ryujinx - Settings - True - center - 650 - 650 - - - True - False - vertical - - - True - True - in - - - True - False - - - True - True - - - True - False - 5 - 10 - 5 - vertical - - - True - False - 5 - 5 - vertical - - - True - False - start - 5 - General - - - - - - False - True - 0 - - - - - True - False - 10 - 10 - vertical - - - Enable Discord Rich Presence - True - True - False - Choose whether or not to display Ryujinx on your "currently playing" Discord activity - start - True - - - False - True - 5 - 0 - - - - - Check for Updates on Launch - True - True - False - start - True - - - False - True - 5 - 1 - - - - - Show "Confirm Exit" Dialog - True - True - False - start - True - - - False - True - 5 - 2 - - - - - True - False - - - True - False - end - Hide Cursor: - - - False - True - 5 - 2 - - - - - Never - True - True - False - start - 5 - 5 - True - True - - - False - True - 3 - - - - - On Idle - True - True - False - start - 5 - 5 - True - _hideCursorNever - - - False - True - 4 - - - - - Always - True - True - False - start - 5 - 5 - True - _hideCursorNever - - - False - True - 5 - - - - - False - True - 5 - 4 - - - - - True - True - 1 - - - - - False - True - 5 - 1 - - - - - True - False - 5 - 5 - - - False - True - 5 - 2 - - - - - True - False - 5 - 5 - vertical - - - True - False - start - 5 - Game Directories - - - - - - False - True - 0 - - - - - True - False - 10 - 10 - vertical - - - True - True - 10 - in - - - True - True - False - False - - - - - - - - - True - True - 0 - - - - - True - False - - - True - True - Enter a game directory to add to the list - - - True - True - 0 - - - - - Add - 80 - True - True - True - Add a game directory to the list - 5 - - - - False - True - 1 - - - - - Remove - 80 - True - True - True - Remove selected game directory - 5 - - - - False - True - 3 - - - - - False - True - 1 - - - - - True - True - 1 - - - - - True - True - 5 - 4 - - - - - True - False - 5 - 5 - - - False - True - 5 - 5 - - - - - True - False - 5 - 5 - vertical - - - True - False - start - 5 - Themes - - - - - - False - True - 0 - - - - - True - False - 10 - 10 - vertical - - - Use Custom Theme - True - True - False - Enable or disable custom themes in the GUI - start - True - - - - False - True - 5 - 1 - - - - - True - False - - - True - False - Path to custom GUI theme - Custom Theme Path: - - - False - True - 5 - 0 - - - - - True - True - Path to custom GUI theme - center - - - True - True - 1 - - - - - Browse... - 80 - True - True - True - Browse for a custom GUI theme - 5 - - - - False - True - 2 - - - - - False - True - 10 - 2 - - - - - False - True - 1 - - - - - False - True - 5 - 6 - - - - - - - True - False - General - - - False - - - - - True - False - 5 - 10 - 5 - vertical - - - True - False - 5 - 5 - - - Enable Docked Mode - True - True - False - Docked mode makes the emulated system behave as a docked Nintendo Switch. This improves graphical fidelity in most games. Conversely, disabling this will make the emulated system behave as a handheld Nintendo Switch, reducing graphics quality. Configure player 1 controls if planning to use docked mode; configure handheld controls if planning to use handheld mode. Leave ON if unsure. - True - - - False - True - 10 - 0 - - - - - Direct Keyboard Access - True - True - False - Direct keyboard access (HID) support. Provides games access to your keyboard as a text entry device. - True - - - False - False - 10 - 1 - - - - - Direct Mouse Access - True - True - False - Direct mouse access (HID) support. Provides games access to your mouse as a pointing device. - True - - - False - False - 10 - 2 - - - - - False - True - 5 - 0 - - - - - True - False - - - False - True - 1 - - - - - - True - False - center - center - 20 - - - True - False - vertical - - - True - False - 20 - 20 - Player 1 - - - False - True - 0 - - - - - Configure - True - True - True - 20 - 20 - 20 - 20 - - - False - True - 1 - - - - - 0 - 0 - - - - - True - False - vertical - - - True - False - 20 - 20 - Player 3 - - - False - True - 0 - - - - - Configure - True - True - True - 20 - 20 - 20 - 20 - - - False - True - 1 - - - - - 4 - 0 - - - - - True - False - vertical - - - True - False - 20 - 20 - Player 2 - - - False - True - 0 - - - - - Configure - True - True - True - 20 - 20 - 20 - 20 - - - False - True - 1 - - - - - 2 - 0 - - - - - True - False - vertical - - - True - False - 20 - 20 - Handheld - - - False - True - 0 - - - - - Configure - True - True - True - 20 - 20 - 20 - 20 - - - False - True - 1 - - - - - 4 - 4 - - - - - True - False - vertical - - - True - False - 20 - 20 - Player 6 - - - False - True - 0 - - - - - Configure - True - True - True - 20 - 20 - 20 - 20 - - - False - True - 1 - - - - - 4 - 2 - - - - - True - False - vertical - - - True - False - 20 - 20 - Player 5 - - - False - True - 0 - - - - - Configure - True - True - True - 20 - 20 - 20 - 20 - - - False - True - 1 - - - - - 2 - 2 - - - - - True - False - vertical - - - True - False - 20 - 20 - Player 7 - - - False - True - 0 - - - - - Configure - True - True - True - 20 - 20 - 20 - 20 - - - False - True - 1 - - - - - 0 - 4 - - - - - True - False - vertical - - - True - False - 20 - 20 - Player 4 - - - False - True - 0 - - - - - Configure - True - True - True - 20 - 20 - 20 - 20 - - - False - True - 1 - - - - - 0 - 2 - - - - - True - False - vertical - - - True - False - 20 - 20 - Player 8 - - - False - True - 0 - - - - - Configure - True - True - True - 20 - 20 - 20 - 20 - - - False - True - 1 - - - - - 2 - 4 - - - - - True - False - - - 1 - 0 - - - - - True - False - - - 3 - 0 - - - - - True - False - - - 3 - 2 - - - - - True - False - - - 3 - 4 - - - - - True - False - - - 1 - 2 - - - - - True - False - - - 1 - 4 - - - - - True - False - - - 1 - 1 - - - - - True - False - - - 1 - 3 - - - - - True - False - - - 3 - 1 - - - - - True - False - - - 3 - 3 - - - - - True - False - - - 0 - 1 - - - - - True - False - - - 2 - 1 - - - - - True - False - - - 4 - 1 - - - - - True - False - - - 0 - 3 - - - - - True - False - - - 2 - 3 - - - - - True - False - - - 4 - 3 - - - - - True - True - 2 - - - - - True - False - - - False - True - 3 - - - - - 1 - - - - - True - False - Input - - - 1 - False - - - - - True - False - 5 - 10 - 5 - vertical - - - True - False - start - 5 - 5 - vertical - - - True - False - start - 5 - Core - - - - - - False - True - 0 - - - - - True - False - 10 - 10 - vertical - - - True - False - - - True - False - Change System Region - end - System Region: - - - False - True - 5 - 2 - - - - - True - False - Change System Region - 5 - - Japan - USA - Europe - Australia - China - Korea - Taiwan - - - - False - True - 3 - - - - - False - True - 5 - 0 - - - - - True - False - - - True - False - Change System Language - end - System Language: - - - False - True - 5 - 0 - - - - - True - False - Change System Language - - American English - British English - Canadian French - Chinese - Dutch - French - German - Italian - Japanese - Korean - Latin American Spanish - Portuguese - Russian - Simplified Chinese - Spanish - Taiwanese - Traditional Chinese - Brazilian Portuguese - - - - False - True - 1 - - - - - False - True - 5 - 1 - - - - - True - False - - - True - False - Change System TimeZone - end - System TimeZone: - - - False - True - 5 - 1 - - - - - True - True - Change System TimeZone - 5 - _systemTimeZoneCompletion - - - False - True - 2 - - - - - False - True - 5 - 2 - - - - - True - False - - - True - False - Change System Time - end - System Time: - - - False - True - 5 - 0 - - - - - True - True - 2000 - vertical - _systemTimeYearSpinAdjustment - True - 2000 - - - False - True - 1 - - - - - True - False - end - - - - - False - True - 5 - 2 - - - - - True - True - 1 - vertical - _systemTimeMonthSpinAdjustment - True - 1 - - - False - True - 3 - - - - - True - False - end - - - - - False - True - 5 - 4 - - - - - True - True - 1 - vertical - _systemTimeDaySpinAdjustment - True - 1 - - - False - True - 5 - - - - - True - True - 0 - vertical - _systemTimeHourSpinAdjustment - True - - - False - True - 6 - - - - - True - False - end - : - - - False - True - 5 - 7 - - - - - True - True - 0 - vertical - _systemTimeMinuteSpinAdjustment - True - - - False - True - 8 - - - - - False - True - 5 - 3 - - - - - Enable VSync - True - True - False - Emulated console's Vertical Sync. Essentially a frame-limiter for the majority of games; disabling it may cause games to run at higher speed or make loading screens take longer or get stuck. Can be toggled in-game with a hotkey of your preference. We recommend doing this if you plan on disabling it. Leave ON if unsure. - start - 5 - 5 - True - - - False - True - 4 - - - - - Enable PPTC (Profiled Persistent Translation Cache) - True - True - False - Saves translated JIT functions so that they do not need to be translated every time the game loads. Reduces stuttering and significantly speeds up boot times after the first boot of a game. Leave ON if unsure. - start - 5 - 5 - True - - - False - True - 6 - - - - - Enable Guest Internet Access - True - True - False - Allows the emulated application to connect to the Internet. Games with a LAN mode can connect to each other when this is enabled and the systems are connected to the same access point. This includes real consoles as well. Does NOT allow connecting to Nintendo servers. May cause crashing in certain games that try to connect to the Internet. Leave OFF if unsure. - start - 5 - 5 - True - - - False - True - 7 - - - - - Enable FS Integrity Checks - True - True - False - Checks for corrupt files when booting a game, and if corrupt files are detected, displays a hash error in the log. Has no impact on performance and is meant to help troubleshooting. Leave ON if unsure. - start - 5 - 5 - True - - - False - True - 8 - - - - - True - True - 1 - - - - - True - False - - - - - - True - False - Changes the backend used to render audio. SDL2 is the preferred one, while OpenAL and SoundIO are used as fallbacks. Dummy will have no sound. Set to SDL2 if unsure. - end - 5 - Audio Backend: - - - False - True - 5 - 2 - - - - - False - True - 5 - 2 - - - - - True - False - - - - - - True - False - Change how guest memory is mapped and accessed. Greatly affects emulated CPU performance. Set to HOST UNCHECKED if unsure. - end - 5 - Memory Manager Mode: - - - False - True - 5 - 2 - - - - - Software - True - True - False - Use a software page table for address translation. Highest accuracy but slowest performance. - start - 5 - 5 - True - - - False - True - 3 - - - - - Host (fast) - True - True - False - Directly map memory in the host address space. Much faster JIT compilation and execution. - start - 5 - 5 - True - _mmSoftware - - - False - True - 4 - - - - - Host Unchecked (fastest, unsafe) - True - True - False - Directly map memory, but do not mask the address within the guest address space before access. Faster, but at the cost of safety. The guest application can access memory from anywhere in Ryujinx, so only run programs you trust with this mode. - start - 5 - 5 - True - _mmSoftware - - - False - True - 5 - - - - - False - True - 5 - 3 - - - - - False - True - 5 - 0 - - - - - True - False - 5 - 5 - - - False - True - 5 - 1 - - - - - True - False - start - 5 - 5 - vertical - - - True - False - - - True - False - start - 5 - Hacks - - - - - - False - True - 0 - - - - - True - False - start - 5 - (may cause instability) - - - False - True - 1 - - - - - False - True - 1 - - - - - True - False - 10 - 10 - vertical - - - Use alternative memory layout (Developers) - True - True - False - Utilizes an alternative MemoryMode layout to mimic a Switch development model. This is only useful for higher-resolution texture packs or 4k resolution mods. Does NOT improve performance. Leave OFF if unsure. - start - 5 - 5 - True - - - False - True - 0 - - - - - Ignore Missing Services - True - True - False - Ignores unimplemented Horizon OS services. This may help in bypassing crashes when booting certain games. Leave OFF if unsure. - start - 5 - 5 - True - - - False - True - 1 - - - - - True - True - 2 - - - - - False - True - 5 - 4 - - - - - 2 - - - - - True - False - end - System - - - 2 - False - - - - - True - False - 5 - vertical - - - True - False - 5 - 5 - vertical - - - True - False - start - 5 - 5 - 5 - Features - - - - - - False - True - 0 - - - - - True - False - 10 - 10 - vertical - - - True - False - 5 - 5 - - - True - False - Executes graphics backend commands on a second thread. Speeds up shader compilation, reduces stuttering, and improves performance on GPU drivers without multithreading support of their own. Slightly better performance on drivers with multithreading. Set to AUTO if unsure. - Graphics Backend Multithreading: - - - False - True - 5 - 0 - - - - - True - False - Executes graphics backend commands on a second thread. Speeds up shader compilation, reduces stuttering, and improves performance on GPU drivers without multithreading support of their own. Slightly better performance on drivers with multithreading. Set to AUTO if unsure. - -1 - - Auto - Off - On - - - - False - True - 1 - - - - - False - True - 5 - 0 - - - - - True - False - 5 - 5 - - - True - False - Graphics Backend to use - Graphics Backend: - - - False - True - 5 - 0 - - - - - True - False - Graphics Backend to use - -1 - - Vulkan - OpenGL - - - - False - True - 1 - - - - - False - True - 5 - 1 - - - - - True - False - 5 - 5 - - - True - False - Preferred GPU (Vulkan only) - Preferred GPU: - - - False - True - 5 - 0 - - - - - True - False - Preferred GPU (Vulkan only) - -1 - - - False - True - 1 - - - - - False - True - 5 - 2 - - - - - False - True - 2 - - - - - False - True - 5 - 0 - - - - - True - False - 5 - 5 - vertical - - - True - False - start - 5 - 5 - 5 - Enhancements - - - - - - False - True - 0 - - - - - True - False - 10 - 10 - vertical - - - Enable Shader Cache - True - True - False - Saves a disk shader cache which reduces stuttering in subsequent runs. Leave ON if unsure. - start - 5 - 5 - True - - - False - True - 0 - - - - - Enable Texture Recompression - True - True - False - Enables or disables Texture Recompression. Reduces VRAM usage at the cost of texture quality, and may also increase stuttering - start - 5 - 5 - True - - - False - True - 1 - - - - - Enable Macro HLE - True - True - False - Enables or disables high-level emulation of Macro code. Improves performance but may cause graphical glitches in some games - start - 5 - 5 - True - - - False - True - 2 - - - - - True - False - 5 - 5 - - - True - False - Resolution Scale applied to applicable render targets. - Resolution Scale: - - - False - True - 5 - 0 - - - - - True - False - Resolution Scale applied to applicable render targets. - 1 - - Native (720p/1080p) - 2x (1440p/2160p) - 3x (2160p/3240p) - 4x (2880p/4320p) - Custom (not recommended) - - - - False - True - 1 - - - - - True - True - Floating point resolution scale, such as 1.5. Non-integral scales are more likely to cause issues or crash. - center - False - 1.0 - number - - - True - True - 2 - - - - - False - True - 5 - 3 - - - - - True - False - 5 - 5 - - - True - False - Applies a final effect to the game render - Post Processing Effect: - - - False - True - 5 - 0 - - - - - True - False - Applies anti-aliasing to the game render - 1 - - None - FXAA - SMAA Low - SMAA Medium - SMAA High - SMAA Ultra - - - - False - True - 1 - - - - - False - True - 5 - 4 - - - - - 100 - True - False - 5 - 5 - - - True - False - Enables Framebuffer Upscaling - Upscale: - - - False - True - 5 - 0 - - - - - True - False - Enables Framebuffer Upscaling - 1 - - Bilinear - Nearest - FSR - - - - False - True - 1 - - - - - 200 - True - True - 5 - _scalingFilterLevel - 1 - right - - - False - True - 3 - - - - - False - True - 5 - 5 - - - - - True - False - 5 - 5 - - - True - False - Level of Anisotropic Filtering (set to Auto to use the value requested by the game) - Anisotropic Filtering: - - - False - True - 5 - 0 - - - - - True - False - Level of Anisotropic Filtering (set to Auto to use the value requested by the game) - -1 - - Auto - 2x - 4x - 8x - 16x - - - - False - True - 1 - - - - - False - True - 5 - 6 - - - - - True - False - 5 - 5 - - - True - False - Aspect Ratio applied to the renderer window. - Aspect Ratio: - - - False - True - 5 - 0 - - - - - True - False - Aspect Ratio applied to the renderer window. - 1 - - 4:3 - 16:9 - 16:10 - 21:9 - 32:9 - Stretch to Fit Window - - - - False - True - 1 - - - - - False - True - 5 - 7 - - - - - False - True - 2 - - - - - False - True - 5 - 2 - - - - - True - False - - - False - True - 5 - 3 - - - - - True - False - 5 - 5 - vertical - - - True - False - start - 5 - 5 - 5 - Developer Options - - - - - - False - True - 0 - - - - - True - False - 10 - 10 - vertical - - - True - False - 5 - 5 - - - True - False - Graphics Shaders Dump Path - Graphics Shaders Dump Path: - - - False - True - 5 - 0 - - - - - True - True - Graphics Shaders Dump Path - center - False - - - True - True - 1 - - - - - False - True - 5 - 0 - - - - - False - True - 1 - - - - - False - True - 5 - 4 - - - - - 3 - - - - - True - False - Graphics - - - 3 - False - - - - - True - False - 5 - 10 - 5 - vertical - - - True - False - 5 - 5 - vertical - - - True - False - start - 5 - Logging - - - - - - False - True - 0 - - - - - True - False - start - 10 - 10 - vertical - - - Enable Logging to File - True - True - False - Saves console logging to a log file on disk. Does not affect performance. - start - 5 - 5 - True - - - False - True - 0 - - - - - Enable Stub Logs - True - True - False - Prints stub log messages in the console. Does not affect performance. - start - 5 - 5 - True - - - False - True - 3 - - - - - Enable Info Logs - True - True - False - Prints info log messages in the console. Does not affect performance. - start - 5 - 5 - True - - - False - True - 4 - - - - - Enable Warning Logs - True - True - False - Prints warning log messages in the console. Does not affect performance. - start - 5 - 5 - True - - - False - True - 5 - - - - - Enable Error Logs - True - True - False - Prints error log messages in the console. Does not affect performance. - start - 5 - 5 - True - - - False - True - 6 - - - - - Enable Guest Logs - True - True - False - Prints guest log messages in the console. Does not affect performance. - start - 5 - 5 - True - - - False - True - 7 - - - - - Enable Fs Access Logs - True - True - False - Enables FS access log output to the console. Possible modes are 0-3 - start - 5 - 5 - True - - - False - True - 8 - - - - - True - False - - - True - False - Enables FS access log output to the console. Possible modes are 0-3 - Fs Global Access Log Mode: - - - False - True - 5 - 0 - - - - - True - True - Enables FS access log output to the console. Possible modes are 0-3 - 0 - _fsLogSpinAdjustment - - - True - True - 1 - - - - - False - True - 5 - 9 - - - - - True - True - 1 - - - - - False - True - 5 - 0 - - - - - True - False - 5 - 5 - 10 - vertical - - - True - False - Use with care - start - 5 - Developer Options (WARNING: Will reduce performance) - - - - - - False - True - 0 - - - - - True - False - start - 10 - 10 - vertical - - - True - False - 5 - - - True - False - Requires appropriate log levels enabled. - Graphics Backend Log Level - - - False - True - 5 - 22 - - - - - True - False - Requires appropriate log levels enabled. - 5 - - - False - True - 22 - - - - - False - True - 1 - - - - - Enable Debug Logs - True - True - False - Prints debug log messages in the console. Only use this if specifically instructed by a staff member, as it will make logs difficult to read and worsen emulator performance. - start - 5 - 5 - True - - - False - True - 21 - - - - - Enable Trace Logs - True - True - False - Prints trace log messages in the console. Does not affect performance. - start - 5 - 5 - True - - - False - True - 22 - - - - - False - True - 1 - - - - - False - True - 5 - 22 - - - - - 4 - - - - - True - False - Logging - - - 4 - False - - - - - True - False - 5 - 10 - 5 - vertical - - - True - False - start - 5 - 5 - vertical - - - True - False - start - 5 - Multiplayer - - - - - - False - True - 0 - - - - - True - False - start - 10 - 10 - vertical - - - True - False - - - True - False - Change Multiplayer Mode - end - Mode: - - - False - True - 5 - 0 - - - - - True - False - Change Multiplayer Mode - Disabled - - Disabled - ldn_mitm - - - - False - True - 1 - - - - - False - True - 3 - - - - - True - True - 2 - - - - - False - True - 5 - 0 - - - - - True - False - start - 5 - 5 - vertical - - - True - False - start - 5 - LAN Mode - - - - - - False - True - 0 - - - - - True - False - start - 10 - 10 - vertical - - - True - False - - - True - False - The network interface used for LAN/LDN features - end - Network Interface: - - - False - True - 5 - 0 - - - - - True - False - The network interface used for LAN/LDN features - 0 - - Default - - - - False - True - 1 - - - - - False - True - 5 - 1 - - - - - True - False - start - 5 - To use LAN functionality in games, Enable Guest Internet Access must be checked in System. - True - - - False - True - 1 - - - - - True - True - 2 - - - - - False - True - 5 - 1 - - - - - 5 - - - - - True - False - Multiplayer - - - 5 - False - - - - - - - - - True - True - 0 - - - - - True - False - 5 - 3 - 3 - 5 - end - - - Save - True - True - True - - - - False - False - 0 - - - - - Close - True - True - True - - - - False - False - 1 - - - - - Apply - True - True - True - - - - True - True - 2 - - - - - False - False - 1 - - - - - - diff --git a/src/Ryujinx/Ui/Windows/TitleUpdateWindow.cs b/src/Ryujinx/Ui/Windows/TitleUpdateWindow.cs deleted file mode 100644 index 51918eeab..000000000 --- a/src/Ryujinx/Ui/Windows/TitleUpdateWindow.cs +++ /dev/null @@ -1,206 +0,0 @@ -using Gtk; -using LibHac.Common; -using LibHac.Fs; -using LibHac.Fs.Fsa; -using LibHac.FsSystem; -using LibHac.Ns; -using LibHac.Tools.FsSystem; -using LibHac.Tools.FsSystem.NcaUtils; -using Ryujinx.Common.Configuration; -using Ryujinx.Common.Utilities; -using Ryujinx.HLE.FileSystem; -using Ryujinx.Ui.App.Common; -using Ryujinx.Ui.Widgets; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using GUI = Gtk.Builder.ObjectAttribute; -using SpanHelpers = LibHac.Common.SpanHelpers; - -namespace Ryujinx.Ui.Windows -{ - public class TitleUpdateWindow : Window - { - private readonly MainWindow _parent; - private readonly VirtualFileSystem _virtualFileSystem; - private readonly string _titleId; - private readonly string _updateJsonPath; - - private TitleUpdateMetadata _titleUpdateWindowData; - - private readonly Dictionary _radioButtonToPathDictionary; - private static readonly TitleUpdateMetadataJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions()); - -#pragma warning disable CS0649, IDE0044 // Field is never assigned to, Add readonly modifier - [GUI] Label _baseTitleInfoLabel; - [GUI] Box _availableUpdatesBox; - [GUI] RadioButton _noUpdateRadioButton; -#pragma warning restore CS0649, IDE0044 - - public TitleUpdateWindow(MainWindow parent, VirtualFileSystem virtualFileSystem, string titleId, string titleName) : this(new Builder("Ryujinx.Ui.Windows.TitleUpdateWindow.glade"), parent, virtualFileSystem, titleId, titleName) { } - - private TitleUpdateWindow(Builder builder, MainWindow parent, VirtualFileSystem virtualFileSystem, string titleId, string titleName) : base(builder.GetRawOwnedObject("_titleUpdateWindow")) - { - _parent = parent; - - builder.Autoconnect(this); - - _titleId = titleId; - _virtualFileSystem = virtualFileSystem; - _updateJsonPath = System.IO.Path.Combine(AppDataManager.GamesDirPath, _titleId, "updates.json"); - _radioButtonToPathDictionary = new Dictionary(); - - try - { - _titleUpdateWindowData = JsonHelper.DeserializeFromFile(_updateJsonPath, _serializerContext.TitleUpdateMetadata); - } - catch - { - _titleUpdateWindowData = new TitleUpdateMetadata - { - Selected = "", - Paths = new List(), - }; - } - - _baseTitleInfoLabel.Text = $"Updates Available for {titleName} [{titleId.ToUpper()}]"; - - foreach (string path in _titleUpdateWindowData.Paths) - { - AddUpdate(path); - } - - if (_titleUpdateWindowData.Selected == "") - { - _noUpdateRadioButton.Active = true; - } - else - { - foreach ((RadioButton update, var _) in _radioButtonToPathDictionary.Where(keyValuePair => keyValuePair.Value == _titleUpdateWindowData.Selected)) - { - update.Active = true; - } - } - } - - private void AddUpdate(string path) - { - if (File.Exists(path)) - { - using FileStream file = new(path, FileMode.Open, FileAccess.Read); - - PartitionFileSystem nsp = new(); - nsp.Initialize(file.AsStorage()).ThrowIfFailure(); - - try - { - (Nca patchNca, Nca controlNca) = ApplicationLibrary.GetGameUpdateDataFromPartition(_virtualFileSystem, nsp, _titleId, 0); - - if (controlNca != null && patchNca != null) - { - ApplicationControlProperty controlData = new(); - - using var nacpFile = new UniqueRef(); - - controlNca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None).OpenFile(ref nacpFile.Ref, "/control.nacp".ToU8Span(), OpenMode.Read).ThrowIfFailure(); - nacpFile.Get.Read(out _, 0, SpanHelpers.AsByteSpan(ref controlData), ReadOption.None).ThrowIfFailure(); - - RadioButton radioButton = new($"Version {controlData.DisplayVersionString.ToString()} - {path}"); - radioButton.JoinGroup(_noUpdateRadioButton); - - _availableUpdatesBox.Add(radioButton); - _radioButtonToPathDictionary.Add(radioButton, path); - - radioButton.Show(); - radioButton.Active = true; - } - else - { - GtkDialog.CreateErrorDialog("The specified file does not contain an update for the selected title!"); - } - } - catch (Exception exception) - { - GtkDialog.CreateErrorDialog($"{exception.Message}. Errored File: {path}"); - } - } - } - - private void RemoveUpdates(bool removeSelectedOnly = false) - { - foreach (RadioButton radioButton in _noUpdateRadioButton.Group) - { - if (radioButton.Label != "No Update" && (!removeSelectedOnly || radioButton.Active)) - { - _availableUpdatesBox.Remove(radioButton); - _radioButtonToPathDictionary.Remove(radioButton); - radioButton.Dispose(); - } - } - } - - private void AddButton_Clicked(object sender, EventArgs args) - { - using FileChooserNative fileChooser = new("Select update files", this, FileChooserAction.Open, "Add", "Cancel"); - - fileChooser.SelectMultiple = true; - - FileFilter filter = new() - { - Name = "Switch Game Updates", - }; - filter.AddPattern("*.nsp"); - - fileChooser.AddFilter(filter); - - if (fileChooser.Run() == (int)ResponseType.Accept) - { - foreach (string path in fileChooser.Filenames) - { - AddUpdate(path); - } - } - } - - private void RemoveButton_Clicked(object sender, EventArgs args) - { - RemoveUpdates(true); - } - - private void RemoveAllButton_Clicked(object sender, EventArgs args) - { - RemoveUpdates(); - } - - private void SaveButton_Clicked(object sender, EventArgs args) - { - _titleUpdateWindowData.Paths.Clear(); - _titleUpdateWindowData.Selected = ""; - - foreach (string paths in _radioButtonToPathDictionary.Values) - { - _titleUpdateWindowData.Paths.Add(paths); - } - - foreach (RadioButton radioButton in _noUpdateRadioButton.Group) - { - if (radioButton.Active) - { - _titleUpdateWindowData.Selected = _radioButtonToPathDictionary.TryGetValue(radioButton, out string updatePath) ? updatePath : ""; - } - } - - JsonHelper.SerializeToFile(_updateJsonPath, _titleUpdateWindowData, _serializerContext.TitleUpdateMetadata); - - _parent.UpdateGameTable(); - - Dispose(); - } - - private void CancelButton_Clicked(object sender, EventArgs args) - { - Dispose(); - } - } -} diff --git a/src/Ryujinx/Ui/Windows/TitleUpdateWindow.glade b/src/Ryujinx/Ui/Windows/TitleUpdateWindow.glade deleted file mode 100644 index cfbac86dd..000000000 --- a/src/Ryujinx/Ui/Windows/TitleUpdateWindow.glade +++ /dev/null @@ -1,214 +0,0 @@ - - - - - - False - Ryujinx - Title Update Manager - True - center - 550 - 250 - - - True - False - vertical - - - True - False - vertical - - - True - False - 10 - 10 - 10 - 10 - Available Updates - - - False - True - 0 - - - - - True - True - 10 - 10 - in - - - True - False - - - True - False - vertical - - - No Update - True - True - False - True - True - - - False - True - 0 - - - - - - - - - True - True - 1 - - - - - True - True - 0 - - - - - True - False - - - True - False - 10 - 10 - start - - - Add - True - True - True - Adds an update to this list - 10 - - - - True - True - 0 - - - - - Remove - True - True - True - Removes the selected update - 10 - - - - True - True - 1 - - - - - Remove All - True - True - True - Removes all the updates - 10 - - - - True - True - 2 - - - - - True - True - 0 - - - - - True - False - 10 - 10 - end - - - Save - True - True - True - 10 - 2 - 2 - - - - True - True - 0 - - - - - Cancel - True - True - True - 10 - 2 - 2 - - - - True - True - 1 - - - - - True - True - 1 - - - - - False - True - 1 - - - - - - - - - diff --git a/src/Ryujinx/Ui/Windows/UserProfilesManagerWindow.Designer.cs b/src/Ryujinx/Ui/Windows/UserProfilesManagerWindow.Designer.cs deleted file mode 100644 index 804bd3fb0..000000000 --- a/src/Ryujinx/Ui/Windows/UserProfilesManagerWindow.Designer.cs +++ /dev/null @@ -1,255 +0,0 @@ -using Gtk; -using Pango; -using System; - -namespace Ryujinx.Ui.Windows -{ - public partial class UserProfilesManagerWindow : Window - { - private Box _mainBox; - private Label _selectedLabel; - private Box _selectedUserBox; - private Image _selectedUserImage; - private Box _selectedUserInfoBox; - private Entry _selectedUserNameEntry; - private Label _selectedUserIdLabel; - private Box _selectedUserButtonsBox; - private Button _saveProfileNameButton; - private Button _changeProfileImageButton; - private Box _usersTreeViewBox; - private Label _availableUsersLabel; - private ScrolledWindow _usersTreeViewWindow; - private ListStore _tableStore; - private TreeView _usersTreeView; - private Box _bottomBox; - private Button _addButton; - private Button _deleteButton; - private Button _closeButton; - - private void InitializeComponent() - { - // - // UserProfilesManagerWindow - // - CanFocus = false; - Resizable = false; - Modal = true; - WindowPosition = WindowPosition.Center; - DefaultWidth = 620; - DefaultHeight = 548; - TypeHint = Gdk.WindowTypeHint.Dialog; - - // - // _mainBox - // - _mainBox = new Box(Orientation.Vertical, 0); - - // - // _selectedLabel - // - _selectedLabel = new Label("Selected User Profile:") - { - Margin = 15, - Attributes = new AttrList(), - Halign = Align.Start, - }; - _selectedLabel.Attributes.Insert(new Pango.AttrWeight(Weight.Bold)); - - // - // _viewBox - // - _usersTreeViewBox = new Box(Orientation.Vertical, 0); - - // - // _SelectedUserBox - // - _selectedUserBox = new Box(Orientation.Horizontal, 0) - { - MarginStart = 30, - }; - - // - // _selectedUserImage - // - _selectedUserImage = new Image(); - - // - // _selectedUserInfoBox - // - _selectedUserInfoBox = new Box(Orientation.Vertical, 0) - { - Homogeneous = true, - }; - - // - // _selectedUserNameEntry - // - _selectedUserNameEntry = new Entry("") - { - MarginStart = 15, - MaxLength = (int)MaxProfileNameLength, - }; - _selectedUserNameEntry.KeyReleaseEvent += SelectedUserNameEntry_KeyReleaseEvent; - - // - // _selectedUserIdLabel - // - _selectedUserIdLabel = new Label("") - { - MarginTop = 15, - MarginStart = 15, - }; - - // - // _selectedUserButtonsBox - // - _selectedUserButtonsBox = new Box(Orientation.Vertical, 0) - { - MarginEnd = 30, - }; - - // - // _saveProfileNameButton - // - _saveProfileNameButton = new Button() - { - Label = "Save Profile Name", - CanFocus = true, - ReceivesDefault = true, - Sensitive = false, - }; - _saveProfileNameButton.Clicked += EditProfileNameButton_Pressed; - - // - // _changeProfileImageButton - // - _changeProfileImageButton = new Button() - { - Label = "Change Profile Image", - CanFocus = true, - ReceivesDefault = true, - MarginTop = 10, - }; - _changeProfileImageButton.Clicked += ChangeProfileImageButton_Pressed; - - // - // _availableUsersLabel - // - _availableUsersLabel = new Label("Available User Profiles:") - { - Margin = 15, - Attributes = new AttrList(), - Halign = Align.Start, - }; - _availableUsersLabel.Attributes.Insert(new Pango.AttrWeight(Weight.Bold)); - - // - // _usersTreeViewWindow - // - _usersTreeViewWindow = new ScrolledWindow() - { - ShadowType = ShadowType.In, - CanFocus = true, - Expand = true, - MarginStart = 30, - MarginEnd = 30, - MarginBottom = 15, - }; - - // - // _tableStore - // - _tableStore = new ListStore(typeof(bool), typeof(Gdk.Pixbuf), typeof(string), typeof(Gdk.RGBA)); - - // - // _usersTreeView - // - _usersTreeView = new TreeView(_tableStore) - { - HoverSelection = true, - HeadersVisible = false, - }; - _usersTreeView.RowActivated += UsersTreeView_Activated; - - // - // _bottomBox - // - _bottomBox = new Box(Orientation.Horizontal, 0) - { - MarginStart = 30, - MarginEnd = 30, - MarginBottom = 15, - }; - - // - // _addButton - // - _addButton = new Button() - { - Label = "Add New Profile", - CanFocus = true, - ReceivesDefault = true, - HeightRequest = 35, - }; - _addButton.Clicked += AddButton_Pressed; - - // - // _deleteButton - // - _deleteButton = new Button() - { - Label = "Delete Selected Profile", - CanFocus = true, - ReceivesDefault = true, - HeightRequest = 35, - MarginStart = 10, - }; - _deleteButton.Clicked += DeleteButton_Pressed; - - // - // _closeButton - // - _closeButton = new Button() - { - Label = "Close", - CanFocus = true, - ReceivesDefault = true, - HeightRequest = 35, - WidthRequest = 80, - }; - _closeButton.Clicked += CloseButton_Pressed; - - ShowComponent(); - } - - private void ShowComponent() - { - _usersTreeViewWindow.Add(_usersTreeView); - - _usersTreeViewBox.Add(_usersTreeViewWindow); - _bottomBox.PackStart(_addButton, false, false, 0); - _bottomBox.PackStart(_deleteButton, false, false, 0); - _bottomBox.PackEnd(_closeButton, false, false, 0); - - _selectedUserInfoBox.Add(_selectedUserNameEntry); - _selectedUserInfoBox.Add(_selectedUserIdLabel); - - _selectedUserButtonsBox.Add(_saveProfileNameButton); - _selectedUserButtonsBox.Add(_changeProfileImageButton); - - _selectedUserBox.Add(_selectedUserImage); - _selectedUserBox.PackStart(_selectedUserInfoBox, false, false, 0); - _selectedUserBox.PackEnd(_selectedUserButtonsBox, false, false, 0); - - _mainBox.PackStart(_selectedLabel, false, false, 0); - _mainBox.PackStart(_selectedUserBox, false, true, 0); - _mainBox.PackStart(_availableUsersLabel, false, false, 0); - _mainBox.Add(_usersTreeViewBox); - _mainBox.Add(_bottomBox); - - Add(_mainBox); - - ShowAll(); - } - } -} diff --git a/src/Ryujinx/Ui/Windows/UserProfilesManagerWindow.cs b/src/Ryujinx/Ui/Windows/UserProfilesManagerWindow.cs deleted file mode 100644 index 3d503f64e..000000000 --- a/src/Ryujinx/Ui/Windows/UserProfilesManagerWindow.cs +++ /dev/null @@ -1,328 +0,0 @@ -using Gtk; -using Ryujinx.Common.Memory; -using Ryujinx.HLE.FileSystem; -using Ryujinx.HLE.HOS.Services.Account.Acc; -using Ryujinx.Ui.Common.Configuration; -using Ryujinx.Ui.Widgets; -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.Processing; -using System; -using System.Collections.Generic; -using System.IO; -using System.Reflection; -using System.Threading; -using System.Threading.Tasks; -using Image = SixLabors.ImageSharp.Image; - -namespace Ryujinx.Ui.Windows -{ - public partial class UserProfilesManagerWindow : Window - { - private const uint MaxProfileNameLength = 0x20; - - private readonly AccountManager _accountManager; - private readonly ContentManager _contentManager; - - private byte[] _bufferImageProfile; - private string _tempNewProfileName; - - private Gdk.RGBA _selectedColor; - - private readonly ManualResetEvent _avatarsPreloadingEvent = new(false); - - public UserProfilesManagerWindow(AccountManager accountManager, ContentManager contentManager, VirtualFileSystem virtualFileSystem) : base($"Ryujinx {Program.Version} - Manage User Profiles") - { - Icon = new Gdk.Pixbuf(Assembly.GetAssembly(typeof(ConfigurationState)), "Ryujinx.Ui.Common.Resources.Logo_Ryujinx.png"); - - InitializeComponent(); - - _selectedColor.Red = 0.212; - _selectedColor.Green = 0.843; - _selectedColor.Blue = 0.718; - _selectedColor.Alpha = 1; - - _accountManager = accountManager; - _contentManager = contentManager; - - CellRendererToggle userSelectedToggle = new(); - userSelectedToggle.Toggled += UserSelectedToggle_Toggled; - - // NOTE: Uncomment following line when multiple selection of user profiles is supported. - //_usersTreeView.AppendColumn("Selected", userSelectedToggle, "active", 0); - _usersTreeView.AppendColumn("User Icon", new CellRendererPixbuf(), "pixbuf", 1); - _usersTreeView.AppendColumn("User Info", new CellRendererText(), "text", 2, "background-rgba", 3); - - _tableStore.SetSortColumnId(0, SortType.Descending); - - RefreshList(); - - if (_contentManager.GetCurrentFirmwareVersion() != null) - { - Task.Run(() => - { - AvatarWindow.PreloadAvatars(contentManager, virtualFileSystem); - _avatarsPreloadingEvent.Set(); - }); - } - } - - public void RefreshList() - { - _tableStore.Clear(); - - foreach (UserProfile userProfile in _accountManager.GetAllUsers()) - { - _tableStore.AppendValues(userProfile.AccountState == AccountState.Open, new Gdk.Pixbuf(userProfile.Image, 96, 96), $"{userProfile.Name}\n{userProfile.UserId}", Gdk.RGBA.Zero); - - if (userProfile.AccountState == AccountState.Open) - { - _selectedUserImage.Pixbuf = new Gdk.Pixbuf(userProfile.Image, 96, 96); - _selectedUserIdLabel.Text = userProfile.UserId.ToString(); - _selectedUserNameEntry.Text = userProfile.Name; - - _deleteButton.Sensitive = userProfile.UserId != AccountManager.DefaultUserId; - - _usersTreeView.Model.GetIterFirst(out TreeIter firstIter); - _tableStore.SetValue(firstIter, 3, _selectedColor); - } - } - } - - // - // Events - // - - private void UsersTreeView_Activated(object o, RowActivatedArgs args) - { - SelectUserTreeView(); - } - - private void UserSelectedToggle_Toggled(object o, ToggledArgs args) - { - SelectUserTreeView(); - } - - private void SelectUserTreeView() - { - // Get selected item informations. - _usersTreeView.Selection.GetSelected(out TreeIter selectedIter); - - Gdk.Pixbuf userPicture = (Gdk.Pixbuf)_tableStore.GetValue(selectedIter, 1); - - string userName = _tableStore.GetValue(selectedIter, 2).ToString().Split("\n")[0]; - string userId = _tableStore.GetValue(selectedIter, 2).ToString().Split("\n")[1]; - - // Unselect the first user. - _usersTreeView.Model.GetIterFirst(out TreeIter firstIter); - _tableStore.SetValue(firstIter, 0, false); - _tableStore.SetValue(firstIter, 3, Gdk.RGBA.Zero); - - // Set new informations. - _tableStore.SetValue(selectedIter, 0, true); - - _selectedUserImage.Pixbuf = userPicture; - _selectedUserNameEntry.Text = userName; - _selectedUserIdLabel.Text = userId; - _saveProfileNameButton.Sensitive = false; - - // Open the selected one. - _accountManager.OpenUser(new UserId(userId)); - - _deleteButton.Sensitive = userId != AccountManager.DefaultUserId.ToString(); - - _tableStore.SetValue(selectedIter, 3, _selectedColor); - } - - private void SelectedUserNameEntry_KeyReleaseEvent(object o, KeyReleaseEventArgs args) - { - if (_saveProfileNameButton.Sensitive == false) - { - _saveProfileNameButton.Sensitive = true; - } - } - - private void AddButton_Pressed(object sender, EventArgs e) - { - _tempNewProfileName = GtkDialog.CreateInputDialog(this, "Choose the Profile Name", "Please Enter a Profile Name", MaxProfileNameLength); - - if (_tempNewProfileName != "") - { - SelectProfileImage(true); - - if (_bufferImageProfile != null) - { - AddUser(); - } - } - } - - private void DeleteButton_Pressed(object sender, EventArgs e) - { - if (GtkDialog.CreateChoiceDialog("Delete User Profile", "Are you sure you want to delete the profile ?", "Deleting this profile will also delete all associated save data.")) - { - _accountManager.DeleteUser(GetSelectedUserId()); - - RefreshList(); - } - } - - private void EditProfileNameButton_Pressed(object sender, EventArgs e) - { - _saveProfileNameButton.Sensitive = false; - - _accountManager.SetUserName(GetSelectedUserId(), _selectedUserNameEntry.Text); - - RefreshList(); - } - - private void ProcessProfileImage(byte[] buffer) - { - using Image image = Image.Load(buffer); - - image.Mutate(x => x.Resize(256, 256)); - - using MemoryStream streamJpg = MemoryStreamManager.Shared.GetStream(); - - image.SaveAsJpeg(streamJpg); - - _bufferImageProfile = streamJpg.ToArray(); - } - - private void ProfileImageFileChooser() - { - FileChooserNative fileChooser = new("Import Custom Profile Image", this, FileChooserAction.Open, "Import", "Cancel") - { - SelectMultiple = false, - }; - - FileFilter filter = new() - { - Name = "Custom Profile Images", - }; - filter.AddPattern("*.jpg"); - filter.AddPattern("*.jpeg"); - filter.AddPattern("*.png"); - filter.AddPattern("*.bmp"); - - fileChooser.AddFilter(filter); - - if (fileChooser.Run() == (int)ResponseType.Accept) - { - ProcessProfileImage(File.ReadAllBytes(fileChooser.Filename)); - } - - fileChooser.Dispose(); - } - - private void SelectProfileImage(bool newUser = false) - { - if (_contentManager.GetCurrentFirmwareVersion() == null) - { - ProfileImageFileChooser(); - } - else - { - Dictionary buttons = new() - { - { 0, "Import Image File" }, - { 1, "Select Firmware Avatar" }, - }; - - ResponseType responseDialog = GtkDialog.CreateCustomDialog("Profile Image Selection", - "Choose a Profile Image", - "You may import a custom profile image, or select an avatar from the system firmware.", - buttons, MessageType.Question); - - if (responseDialog == 0) - { - ProfileImageFileChooser(); - } - else if (responseDialog == (ResponseType)1) - { - AvatarWindow avatarWindow = new() - { - NewUser = newUser, - }; - - avatarWindow.DeleteEvent += AvatarWindow_DeleteEvent; - - avatarWindow.SetSizeRequest((int)(avatarWindow.DefaultWidth * Program.WindowScaleFactor), (int)(avatarWindow.DefaultHeight * Program.WindowScaleFactor)); - avatarWindow.Show(); - } - } - } - - private void ChangeProfileImageButton_Pressed(object sender, EventArgs e) - { - if (_contentManager.GetCurrentFirmwareVersion() != null) - { - _avatarsPreloadingEvent.WaitOne(); - } - - SelectProfileImage(); - - if (_bufferImageProfile != null) - { - SetUserImage(); - } - } - - private void AvatarWindow_DeleteEvent(object sender, DeleteEventArgs args) - { - _bufferImageProfile = ((AvatarWindow)sender).SelectedProfileImage; - - if (_bufferImageProfile != null) - { - if (((AvatarWindow)sender).NewUser) - { - AddUser(); - } - else - { - SetUserImage(); - } - } - } - - private void AddUser() - { - _accountManager.AddUser(_tempNewProfileName, _bufferImageProfile); - - _bufferImageProfile = null; - _tempNewProfileName = ""; - - RefreshList(); - } - - private void SetUserImage() - { - _accountManager.SetUserImage(GetSelectedUserId(), _bufferImageProfile); - - _bufferImageProfile = null; - - RefreshList(); - } - - private UserId GetSelectedUserId() - { - if (_usersTreeView.Model.GetIterFirst(out TreeIter iter)) - { - do - { - if ((bool)_tableStore.GetValue(iter, 0)) - { - break; - } - } - while (_usersTreeView.Model.IterNext(ref iter)); - } - - return new UserId(_tableStore.GetValue(iter, 2).ToString().Split("\n")[1]); - } - - private void CloseButton_Pressed(object sender, EventArgs e) - { - Close(); - } - } -} diff --git a/src/Ryujinx/Updater.cs b/src/Ryujinx/Updater.cs new file mode 100644 index 000000000..bdb44d668 --- /dev/null +++ b/src/Ryujinx/Updater.cs @@ -0,0 +1,788 @@ +using Avalonia.Controls; +using Avalonia.Threading; +using FluentAvalonia.UI.Controls; +using Gommon; +using ICSharpCode.SharpZipLib.GZip; +using ICSharpCode.SharpZipLib.Tar; +using ICSharpCode.SharpZipLib.Zip; +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.Ava +{ + internal static class Updater + { + private const string GitHubApiUrl = "https://api.github.com"; + private const string LatestReleaseUrl = + $"{GitHubApiUrl}/repos/{ReleaseInformation.ReleaseChannelOwner}/{ReleaseInformation.ReleaseChannelRepo}/releases/latest"; + + 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 = []; + + public static async Task BeginUpdateAsync(this Window mainWindow, bool showVersionUpToDate = false) + { + 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()) + { + var arch = RuntimeInformation.OSArchitecture == Architecture.Arm64 ? "arm64" : "x64"; + _platformExt = $"linux_{arch}.tar.gz"; + } + + Version newVersion; + Version currentVersion; + + try + { + currentVersion = Version.Parse(Program.Version); + } + catch + { + Logger.Error?.Print(LogClass.Application, $"Failed to convert the current {App.FullAppName} 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 fetchedJson = await jsonClient.GetStringAsync(LatestReleaseUrl); + var fetched = JsonHelper.Deserialize(fetchedJson, _serializerContext.GithubReleasesJsonResponse); + _buildVer = fetched.TagName; + + foreach (var asset in fetched.Assets) + { + if (asset.Name.StartsWith("ryujinx") && asset.Name.EndsWith(_platformExt)) + { + _buildUrl = asset.BrowserDownloadUrl; + + if (asset.State != "uploaded") + { + if (showVersionUpToDate) + { + UserResult userResult = await ContentDialogHelper.CreateUpdaterUpToDateInfoDialog( + LocaleManager.Instance[LocaleKeys.DialogUpdaterAlreadyOnLatestVersionMessage], + string.Empty); + + if (userResult is UserResult.Ok) + { + OpenHelper.OpenUrl(ReleaseInformation.GetChangelogForVersion(currentVersion)); + } + } + + _running = false; + + return; + } + + break; + } + } + + // If build not done, assume no new update are available. + if (_buildUrl is null) + { + if (showVersionUpToDate) + { + UserResult userResult = await ContentDialogHelper.CreateUpdaterUpToDateInfoDialog( + LocaleManager.Instance[LocaleKeys.DialogUpdaterAlreadyOnLatestVersionMessage], + string.Empty); + + if (userResult is UserResult.Ok) + { + OpenHelper.OpenUrl(ReleaseInformation.GetChangelogForVersion(currentVersion)); + } + } + + _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 {App.FullAppName} version from GitHub!"); + + await ContentDialogHelper.CreateWarningDialog( + LocaleManager.Instance[LocaleKeys.DialogUpdaterConvertFailedGithubMessage], + LocaleManager.Instance[LocaleKeys.DialogUpdaterCancelUpdateMessage]); + + _running = false; + + return; + } + + if (newVersion <= currentVersion) + { + if (showVersionUpToDate) + { + UserResult userResult = await ContentDialogHelper.CreateUpdaterUpToDateInfoDialog( + LocaleManager.Instance[LocaleKeys.DialogUpdaterAlreadyOnLatestVersionMessage], + string.Empty); + + if (userResult is UserResult.Ok) + { + OpenHelper.OpenUrl(ReleaseInformation.GetChangelogForVersion(currentVersion)); + } + } + + _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 () => + { + string newVersionString = ReleaseInformation.IsCanaryBuild + ? $"Canary {currentVersion} -> Canary {newVersion}" + : $"{currentVersion} -> {newVersion}"; + + RequestUserToUpdate: + // Show a message asking the user if they want to update + UserResult shouldUpdate = await ContentDialogHelper.CreateUpdaterChoiceDialog( + LocaleManager.Instance[LocaleKeys.RyujinxUpdater], + LocaleManager.Instance[LocaleKeys.RyujinxUpdaterMessage], + newVersionString); + + switch (shouldUpdate) + { + case UserResult.Yes: + await UpdateRyujinx(mainWindow, _buildUrl); + break; + // Secondary button maps to no, which in this case is the show changelog button. + case UserResult.No: + OpenHelper.OpenUrl(ReleaseInformation.GetChangelogUrl(currentVersion, newVersion)); + goto RequestUserToUpdate; + default: + _running = false; + break; + } + }); + } + + 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) ?? string.Empty; + + // 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 (ryuName.Length == 0 || !Path.Exists(Path.Combine(executableDirectory, ryuName))) + { + ryuName = OperatingSystem.IsWindows() ? "Ryujinx.exe" : "Ryujinx"; + } + + 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 bytesWritten = 0; + + byte[] buffer = new byte[32 * 1024]; + + while (true) + { + int readSize = remoteFileStream.Read(buffer); + + if (readSize == 0) + { + break; + } + + bytesWritten += readSize; + + taskDialog.SetProgressBarState(GetPercentage(bytesWritten, totalBytes), TaskDialogProgressState.Normal); + App.SetTaskbarProgressValue(bytesWritten, totalBytes); + + 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 = false) + { +#if !DISABLE_UPDATER + 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() => + Directory.GetFiles(_homeDir, "*.ryuold", SearchOption.AllDirectories) + .ForEach(File.Delete); + } +} diff --git a/src/Ryujinx/app.manifest b/src/Ryujinx/app.manifest new file mode 100644 index 000000000..920136fb4 --- /dev/null +++ b/src/Ryujinx/app.manifest @@ -0,0 +1,10 @@ + + + + + + + + + +