From bedee64af5a36a55b82df781b3bc78f9d09844c6 Mon Sep 17 00:00:00 2001 From: TSRBerry <20988865+TSRBerry@users.noreply.github.com> Date: Tue, 1 Aug 2023 00:15:37 +0200 Subject: [PATCH 001/105] Add slightly better workaround for current workflow issues (#5507) * checks: Add retry logic to dotnet format style step as well I can't imagine dotnet format whitespace ever segfaulting, so hopefully it won't be needed there. * checks: Replace bash scripts with unstable-commands action * build: Add unstable-commands action for test step --- .github/workflows/build.yml | 8 ++++++-- .github/workflows/checks.yml | 28 ++++++++++++++-------------- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c93fd0d30..dc3728707 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -54,7 +54,11 @@ jobs: run: dotnet build -c "${{ matrix.configuration }}" -p:Version="${{ env.RYUJINX_BASE_VERSION }}" -p:SourceRevisionId="${{ steps.git_short_hash.outputs.result }}" -p:ExtraDefineConstants=DISABLE_UPDATER - name: Test - run: dotnet test --no-build -c "${{ matrix.configuration }}" + uses: TSRBerry/unstable-commands@v1 + with: + commands: dotnet test --no-build -c "${{ matrix.configuration }}" + timeout-minutes: 10 + retry-codes: 139 - name: Publish Ryujinx run: dotnet publish -c "${{ matrix.configuration }}" -r "${{ matrix.DOTNET_RUNTIME_IDENTIFIER }}" -o ./publish -p:Version="${{ env.RYUJINX_BASE_VERSION }}" -p:DebugType=embedded -p:SourceRevisionId="${{ steps.git_short_hash.outputs.result }}" -p:ExtraDefineConstants=DISABLE_UPDATER src/Ryujinx --self-contained true @@ -141,4 +145,4 @@ jobs: with: name: ava-ryujinx-${{ matrix.configuration }}-${{ env.RYUJINX_BASE_VERSION }}+${{ steps.git_short_hash.outputs.result }}-macos_universal path: "publish_ava/*.tar.gz" - if: github.event_name == 'pull_request' \ No newline at end of file + if: github.event_name == 'pull_request' diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index c34d196f2..94d0a342b 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -40,23 +40,23 @@ jobs: run: | dotnet format whitespace --verify-no-changes --report ./whitespace-report.json -v d + # For some unknown reason this step sometimes fails with exit code 139 (segfault?), + # so in that case we'll try again (3 tries max). - name: Run dotnet format style - run: | - dotnet format style --severity info --verify-no-changes --report ./style-report.json -v d + uses: TSRBerry/unstable-commands@v1 + with: + commands: dotnet format style --severity info --verify-no-changes --report ./style-report.json -v d + timeout-minutes: 5 + retry-codes: 139 - # For some reason this step sometimes fails with exit code 139 (segfault?), - # so should that be the case we'll try again (3 tries max). + # For some unknown reason this step sometimes fails with exit code 139 (segfault?), + # so in that case we'll try again (3 tries max). - name: Run dotnet format analyzers - run: | - attempt=0 - exit_code=139 - until [ $attempt -ge 3 ] || [ $exit_code -ne 139 ]; do - ((attempt+=1)) - exit_code=0 - echo "Attempt: ${attempt}/3" - dotnet format analyzers --severity info --verify-no-changes --report ./analyzers-report.json -v d || exit_code=$? - done - exit $exit_code + uses: TSRBerry/unstable-commands@v1 + with: + commands: dotnet format analyzers --severity info --verify-no-changes --report ./analyzers-report.json -v d + timeout-minutes: 5 + retry-codes: 139 - name: Upload report if: failure() From 93aa40f1fb739f12a6a404353580a9656fe6a52f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 2 Aug 2023 17:19:38 +0200 Subject: [PATCH 002/105] nuget: bump DiscordRichPresence from 1.1.3.18 to 1.2.1.24 (#5515) Bumps [DiscordRichPresence](https://github.com/Lachee/discord-rpc-csharp) from 1.1.3.18 to 1.2.1.24. - [Release notes](https://github.com/Lachee/discord-rpc-csharp/releases) - [Commits](https://github.com/Lachee/discord-rpc-csharp/commits) --- updated-dependencies: - dependency-name: DiscordRichPresence dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index bc740afcf..6191c4984 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -12,7 +12,7 @@ - + From 5a0aa074b661753d8f0202a73d9f6f3ac6e2ab11 Mon Sep 17 00:00:00 2001 From: sunshineinabox Date: Thu, 3 Aug 2023 13:46:23 -0700 Subject: [PATCH 003/105] Enable VK_EXT_4444_formats (#5525) --- src/Ryujinx.Graphics.Vulkan/VulkanInitialization.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Ryujinx.Graphics.Vulkan/VulkanInitialization.cs b/src/Ryujinx.Graphics.Vulkan/VulkanInitialization.cs index 02cfe91d9..6f73397b8 100644 --- a/src/Ryujinx.Graphics.Vulkan/VulkanInitialization.cs +++ b/src/Ryujinx.Graphics.Vulkan/VulkanInitialization.cs @@ -42,6 +42,7 @@ namespace Ryujinx.Graphics.Vulkan "VK_NV_viewport_array2", "VK_EXT_depth_clip_control", "VK_KHR_portability_subset", // As per spec, we should enable this if present. + "VK_EXT_4444_formats", }; private static readonly string[] _requiredExtensions = { From 6e784e0aca240b41c83bd3e77aecf5793fdc238d Mon Sep 17 00:00:00 2001 From: riperiperi Date: Sun, 6 Aug 2023 20:29:20 +0100 Subject: [PATCH 004/105] GPU: Don't sync/bind index buffer when it's not in use (#5526) * GPU: Don't sync/bind index buffer when it's not in use Sometimes draws don't use an index buffer. It's not necessary to check or upload data for the current index buffer binding as it won't be used. This fixes Pokemon: Legends Arceus updating a stale index buffer for every draw during its TFB pass, which was all non-indexed draws. This probably didn't cost much on normal PCs, but it had a large impact on MacOS, which the macos1 release build avoided by mirroring index buffers (the PR currently does not). Needs buffer mirrors still for the rest of the performance. There are additional cases where index buffers are bound or checked with non-indexed draws on the backend, but this one was straightforward to fix and has the largest impact. Testing is welcome to ensure nothing weird broke. * Fix case with _rebind --- .../Engine/Threed/StateUpdater.cs | 2 +- .../Memory/BufferManager.cs | 26 ++++++++++++------- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/src/Ryujinx.Graphics.Gpu/Engine/Threed/StateUpdater.cs b/src/Ryujinx.Graphics.Gpu/Engine/Threed/StateUpdater.cs index c0c2d5b30..b08e7f260 100644 --- a/src/Ryujinx.Graphics.Gpu/Engine/Threed/StateUpdater.cs +++ b/src/Ryujinx.Graphics.Gpu/Engine/Threed/StateUpdater.cs @@ -331,7 +331,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed UpdateShaderState(); } - _channel.BufferManager.CommitGraphicsBindings(); + _channel.BufferManager.CommitGraphicsBindings(_drawState.DrawIndexed); } /// diff --git a/src/Ryujinx.Graphics.Gpu/Memory/BufferManager.cs b/src/Ryujinx.Graphics.Gpu/Memory/BufferManager.cs index 10224a6d4..c656b0f64 100644 --- a/src/Ryujinx.Graphics.Gpu/Memory/BufferManager.cs +++ b/src/Ryujinx.Graphics.Gpu/Memory/BufferManager.cs @@ -515,24 +515,32 @@ namespace Ryujinx.Graphics.Gpu.Memory /// Ensures that the graphics engine bindings are visible to the host GPU. /// Note: this actually performs the binding using the host graphics API. /// - public void CommitGraphicsBindings() + /// True if the index buffer is in use + public void CommitGraphicsBindings(bool indexed) { var bufferCache = _channel.MemoryManager.Physical.BufferCache; - if (_indexBufferDirty || _rebind) + if (indexed) { - _indexBufferDirty = false; - - if (_indexBuffer.Address != 0) + if (_indexBufferDirty || _rebind) { - BufferRange buffer = bufferCache.GetBufferRange(_indexBuffer.Address, _indexBuffer.Size); + _indexBufferDirty = false; - _context.Renderer.Pipeline.SetIndexBuffer(buffer, _indexBuffer.Type); + if (_indexBuffer.Address != 0) + { + BufferRange buffer = bufferCache.GetBufferRange(_indexBuffer.Address, _indexBuffer.Size); + + _context.Renderer.Pipeline.SetIndexBuffer(buffer, _indexBuffer.Type); + } + } + else if (_indexBuffer.Address != 0) + { + bufferCache.SynchronizeBufferRange(_indexBuffer.Address, _indexBuffer.Size); } } - else if (_indexBuffer.Address != 0) + else if (_rebind) { - bufferCache.SynchronizeBufferRange(_indexBuffer.Address, _indexBuffer.Size); + _indexBufferDirty = true; } uint vbEnableMask = _vertexBuffersEnableMask; From 3ab0a71c7bfb60b20008894db3fb6534436753e6 Mon Sep 17 00:00:00 2001 From: TSRBerry <20988865+TSRBerry@users.noreply.github.com> Date: Sun, 6 Aug 2023 23:25:02 +0200 Subject: [PATCH 005/105] Fix PR build concurrency and stop auto assigning reviewers for draft PRs (#5519) * build: Remove concurrency It's called by checks anyway. * Only assign reviewers for PRs that are ready for reviews --- .github/workflows/build.yml | 4 ---- .github/workflows/pr_triage.yml | 1 + 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index dc3728707..bbc2eca80 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -3,10 +3,6 @@ name: Build job on: workflow_call: -concurrency: - group: pr-builds-${{ github.event.number }} - cancel-in-progress: true - env: POWERSHELL_TELEMETRY_OPTOUT: 1 DOTNET_CLI_TELEMETRY_OPTOUT: 1 diff --git a/.github/workflows/pr_triage.yml b/.github/workflows/pr_triage.yml index 86e5084f2..cd90e645a 100644 --- a/.github/workflows/pr_triage.yml +++ b/.github/workflows/pr_triage.yml @@ -28,6 +28,7 @@ jobs: dot: true - name: Assign reviewers + if: ! github.event.pull_request.draft run: | pip3 install PyGithub python3 .github/update_reviewers.py ${{ secrets.GITHUB_TOKEN }} ${{ github.repository }} ${{ github.event.pull_request.number }} .github/reviewers.yml From 42750a74f82ee69cabfaf3c5497af6a8ebc13eca Mon Sep 17 00:00:00 2001 From: gdkchan Date: Mon, 7 Aug 2023 12:20:37 -0300 Subject: [PATCH 006/105] Do not add more code after alpha test discard on fragment shader (#5529) * Do not add more code after alpha test discard on fragment shader * Shader cache version bump --- .../Shader/DiskCache/DiskCacheHostStorage.cs | 2 +- .../Instructions/InstEmitFlowControl.cs | 14 ++++++++++---- .../Translation/EmitterContext.cs | 16 ++++++++++++---- 3 files changed, 23 insertions(+), 9 deletions(-) diff --git a/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheHostStorage.cs b/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheHostStorage.cs index 4bab165da..512ae94b4 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 = 4675; + private const uint CodeGenVersion = 5529; private const string SharedTocFileName = "shared.toc"; private const string SharedDataFileName = "shared.data"; diff --git a/src/Ryujinx.Graphics.Shader/Instructions/InstEmitFlowControl.cs b/src/Ryujinx.Graphics.Shader/Instructions/InstEmitFlowControl.cs index fc1a696fa..7462fc5ad 100644 --- a/src/Ryujinx.Graphics.Shader/Instructions/InstEmitFlowControl.cs +++ b/src/Ryujinx.Graphics.Shader/Instructions/InstEmitFlowControl.cs @@ -162,8 +162,10 @@ namespace Ryujinx.Graphics.Shader.Instructions if (op.Ccc == Ccc.T) { - context.PrepareForReturn(); - context.Return(); + if (context.PrepareForReturn()) + { + context.Return(); + } } else { @@ -174,8 +176,12 @@ namespace Ryujinx.Graphics.Shader.Instructions { Operand lblSkip = Label(); context.BranchIfFalse(lblSkip, cond); - context.PrepareForReturn(); - context.Return(); + + if (context.PrepareForReturn()) + { + context.Return(); + } + context.MarkLabel(lblSkip); } } diff --git a/src/Ryujinx.Graphics.Shader/Translation/EmitterContext.cs b/src/Ryujinx.Graphics.Shader/Translation/EmitterContext.cs index 08d4b9156..614b275ba 100644 --- a/src/Ryujinx.Graphics.Shader/Translation/EmitterContext.cs +++ b/src/Ryujinx.Graphics.Shader/Translation/EmitterContext.cs @@ -304,11 +304,11 @@ namespace Ryujinx.Graphics.Shader.Translation PrepareForVertexReturn(); } - public void PrepareForReturn() + public bool PrepareForReturn() { if (IsNonMain) { - return; + return true; } if (Config.LastInVertexPipeline && @@ -383,13 +383,13 @@ namespace Ryujinx.Graphics.Shader.Translation AlphaTestOp alphaTestOp = Config.GpuAccessor.QueryAlphaTestCompare(); - if (alphaTestOp != AlphaTestOp.Always && (Config.OmapTargets & 8) != 0) + if (alphaTestOp != AlphaTestOp.Always) { if (alphaTestOp == AlphaTestOp.Never) { this.Discard(); } - else + else if ((Config.OmapTargets & 8) != 0) { Instruction comparator = alphaTestOp switch { @@ -415,6 +415,12 @@ namespace Ryujinx.Graphics.Shader.Translation } } + // We don't need to output anything if alpha test always fails. + if (alphaTestOp == AlphaTestOp.Never) + { + return false; + } + int regIndexBase = 0; for (int rtIndex = 0; rtIndex < 8; rtIndex++) @@ -462,6 +468,8 @@ namespace Ryujinx.Graphics.Shader.Translation } } } + + return true; } private void GenerateAlphaToCoverageDitherDiscard() From 773e239db7ceb2c55aa15f9787add4430edcdfcf Mon Sep 17 00:00:00 2001 From: jcm Date: Mon, 7 Aug 2023 11:54:05 -0600 Subject: [PATCH 007/105] Implement color space passthrough option (#5531) Co-authored-by: jcm --- .editorconfig | 7 +++ src/Ryujinx.Ava/AppHost.cs | 8 +++ src/Ryujinx.Ava/Assets/Locales/en_US.json | 2 + .../UI/ViewModels/SettingsViewModel.cs | 3 ++ .../Views/Settings/SettingsGraphicsView.axaml | 4 ++ src/Ryujinx.Graphics.GAL/IWindow.cs | 1 + .../Multithreading/ThreadedWindow.cs | 2 + src/Ryujinx.Graphics.Gpu/GraphicsConfig.cs | 5 ++ src/Ryujinx.Graphics.OpenGL/Window.cs | 2 + src/Ryujinx.Graphics.Vulkan/Window.cs | 49 ++++++++++++++----- src/Ryujinx.Graphics.Vulkan/WindowBase.cs | 1 + .../Configuration/ConfigurationFileFormat.cs | 7 ++- .../Configuration/ConfigurationState.cs | 19 +++++++ 13 files changed, 97 insertions(+), 13 deletions(-) diff --git a/.editorconfig b/.editorconfig index e5a5e6174..9d695c7fb 100644 --- a/.editorconfig +++ b/.editorconfig @@ -14,6 +14,13 @@ tab_width = 4 end_of_line = lf insert_final_newline = true +# JSON files +[*.json] + +# Indentation and spacing +indent_size = 2 +tab_width = 2 + # C# files [*.cs] diff --git a/src/Ryujinx.Ava/AppHost.cs b/src/Ryujinx.Ava/AppHost.cs index 7c1ce542c..786b45070 100644 --- a/src/Ryujinx.Ava/AppHost.cs +++ b/src/Ryujinx.Ava/AppHost.cs @@ -186,6 +186,7 @@ namespace Ryujinx.Ava 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.Multiplayer.LanInterfaceId.Event += UpdateLanInterfaceIdState; @@ -229,6 +230,11 @@ namespace Ryujinx.Ava _renderer.Window?.SetScalingFilterLevel(ConfigurationState.Instance.Graphics.ScalingFilterLevel.Value); } + private void UpdateColorSpacePassthrough(object sender, ReactiveEventArgs e) + { + _renderer.Window?.SetColorSpacePassthrough((bool)ConfigurationState.Instance.Graphics.EnableColorSpacePassthrough.Value); + } + private void ShowCursor() { Dispatcher.UIThread.Post(() => @@ -461,6 +467,7 @@ namespace Ryujinx.Ava 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_PointerEnterOrMoved; _topLevel.PointerEnter -= TopLevel_PointerEnterOrMoved; @@ -887,6 +894,7 @@ namespace Ryujinx.Ava _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; diff --git a/src/Ryujinx.Ava/Assets/Locales/en_US.json b/src/Ryujinx.Ava/Assets/Locales/en_US.json index 4065a1dfb..efd3187ad 100644 --- a/src/Ryujinx.Ava/Assets/Locales/en_US.json +++ b/src/Ryujinx.Ava/Assets/Locales/en_US.json @@ -620,6 +620,8 @@ "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?", diff --git a/src/Ryujinx.Ava/UI/ViewModels/SettingsViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/SettingsViewModel.cs index a9eb9c618..1e6d2734f 100644 --- a/src/Ryujinx.Ava/UI/ViewModels/SettingsViewModel.cs +++ b/src/Ryujinx.Ava/UI/ViewModels/SettingsViewModel.cs @@ -145,6 +145,7 @@ namespace Ryujinx.Ava.UI.ViewModels public bool EnableShaderCache { get; set; } public bool EnableTextureRecompression { get; set; } public bool EnableMacroHLE { get; set; } + public bool EnableColorSpacePassthrough { get; set; } public bool EnableFileLog { get; set; } public bool EnableStub { get; set; } public bool EnableInfo { get; set; } @@ -419,6 +420,7 @@ namespace Ryujinx.Ava.UI.ViewModels 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)); @@ -506,6 +508,7 @@ namespace Ryujinx.Ava.UI.ViewModels 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); diff --git a/src/Ryujinx.Ava/UI/Views/Settings/SettingsGraphicsView.axaml b/src/Ryujinx.Ava/UI/Views/Settings/SettingsGraphicsView.axaml index 8e4122f38..670de69c6 100644 --- a/src/Ryujinx.Ava/UI/Views/Settings/SettingsGraphicsView.axaml +++ b/src/Ryujinx.Ava/UI/Views/Settings/SettingsGraphicsView.axaml @@ -73,6 +73,10 @@ ToolTip.Tip="{locale:Locale SettingsEnableMacroHLETooltip}"> + + + public static bool EnableTextureRecompression = false; + + /// + /// Enables or disables color space passthrough, if available. + /// + public static bool EnableColorSpacePassthrough = false; } #pragma warning restore CA2211 } diff --git a/src/Ryujinx.Graphics.OpenGL/Window.cs b/src/Ryujinx.Graphics.OpenGL/Window.cs index a8e6031b6..6bcfefa4e 100644 --- a/src/Ryujinx.Graphics.OpenGL/Window.cs +++ b/src/Ryujinx.Graphics.OpenGL/Window.cs @@ -307,6 +307,8 @@ namespace Ryujinx.Graphics.OpenGL _updateScalingFilter = true; } + public void SetColorSpacePassthrough(bool colorSpacePassthroughEnabled) { } + private void UpdateEffect() { if (_updateEffect) diff --git a/src/Ryujinx.Graphics.Vulkan/Window.cs b/src/Ryujinx.Graphics.Vulkan/Window.cs index 6027962cf..2d0ad664c 100644 --- a/src/Ryujinx.Graphics.Vulkan/Window.cs +++ b/src/Ryujinx.Graphics.Vulkan/Window.cs @@ -28,7 +28,7 @@ namespace Ryujinx.Graphics.Vulkan private int _width; private int _height; private bool _vsyncEnabled; - private bool _vsyncModeChanged; + private bool _swapchainIsDirty; private VkFormat _format; private AntiAliasing _currentAntiAliasing; private bool _updateEffect; @@ -38,6 +38,7 @@ namespace Ryujinx.Graphics.Vulkan private float _scalingFilterLevel; private bool _updateScalingFilter; private ScalingFilter _currentScalingFilter; + private bool _colorSpacePassthroughEnabled; public unsafe Window(VulkanRenderer gd, SurfaceKHR surface, PhysicalDevice physicalDevice, Device device) { @@ -60,7 +61,7 @@ namespace Ryujinx.Graphics.Vulkan private void RecreateSwapchain() { var oldSwapchain = _swapchain; - _vsyncModeChanged = false; + _swapchainIsDirty = false; for (int i = 0; i < _swapchainImageViews.Length; i++) { @@ -106,7 +107,7 @@ namespace Ryujinx.Graphics.Vulkan imageCount = capabilities.MaxImageCount; } - var surfaceFormat = ChooseSwapSurfaceFormat(surfaceFormats); + var surfaceFormat = ChooseSwapSurfaceFormat(surfaceFormats, _colorSpacePassthroughEnabled); var extent = ChooseSwapExtent(capabilities); @@ -178,22 +179,40 @@ namespace Ryujinx.Graphics.Vulkan return new Auto(new DisposableImageView(_gd.Api, _device, imageView)); } - private static SurfaceFormatKHR ChooseSwapSurfaceFormat(SurfaceFormatKHR[] availableFormats) + private static SurfaceFormatKHR ChooseSwapSurfaceFormat(SurfaceFormatKHR[] availableFormats, bool colorSpacePassthroughEnabled) { if (availableFormats.Length == 1 && availableFormats[0].Format == VkFormat.Undefined) { return new SurfaceFormatKHR(VkFormat.B8G8R8A8Unorm, ColorSpaceKHR.PaceSrgbNonlinearKhr); } - - foreach (var format in availableFormats) + var formatToReturn = availableFormats[0]; + if (colorSpacePassthroughEnabled) { - if (format.Format == VkFormat.B8G8R8A8Unorm && format.ColorSpace == ColorSpaceKHR.PaceSrgbNonlinearKhr) + foreach (var format in availableFormats) { - return format; + if (format.Format == VkFormat.B8G8R8A8Unorm && format.ColorSpace == ColorSpaceKHR.SpacePassThroughExt) + { + formatToReturn = format; + break; + } + else if (format.Format == VkFormat.B8G8R8A8Unorm && format.ColorSpace == ColorSpaceKHR.PaceSrgbNonlinearKhr) + { + formatToReturn = format; + } } } - - return availableFormats[0]; + else + { + foreach (var format in availableFormats) + { + if (format.Format == VkFormat.B8G8R8A8Unorm && format.ColorSpace == ColorSpaceKHR.PaceSrgbNonlinearKhr) + { + formatToReturn = format; + break; + } + } + } + return formatToReturn; } private static CompositeAlphaFlagsKHR ChooseCompositeAlpha(CompositeAlphaFlagsKHR supportedFlags) @@ -259,7 +278,7 @@ namespace Ryujinx.Graphics.Vulkan if (acquireResult == Result.ErrorOutOfDateKhr || acquireResult == Result.SuboptimalKhr || - _vsyncModeChanged) + _swapchainIsDirty) { RecreateSwapchain(); } @@ -443,6 +462,12 @@ namespace Ryujinx.Graphics.Vulkan _updateScalingFilter = true; } + public override void SetColorSpacePassthrough(bool colorSpacePassthroughEnabled) + { + _colorSpacePassthroughEnabled = colorSpacePassthroughEnabled; + _swapchainIsDirty = true; + } + private void UpdateEffect() { if (_updateEffect) @@ -559,7 +584,7 @@ namespace Ryujinx.Graphics.Vulkan public override void ChangeVSyncMode(bool vsyncEnabled) { _vsyncEnabled = vsyncEnabled; - _vsyncModeChanged = true; + _swapchainIsDirty = true; } protected virtual void Dispose(bool disposing) diff --git a/src/Ryujinx.Graphics.Vulkan/WindowBase.cs b/src/Ryujinx.Graphics.Vulkan/WindowBase.cs index 146cf6607..da1613f41 100644 --- a/src/Ryujinx.Graphics.Vulkan/WindowBase.cs +++ b/src/Ryujinx.Graphics.Vulkan/WindowBase.cs @@ -14,5 +14,6 @@ namespace Ryujinx.Graphics.Vulkan public abstract void SetAntiAliasing(AntiAliasing effect); public abstract void SetScalingFilter(ScalingFilter scalerType); public abstract void SetScalingFilterLevel(float scale); + public abstract void SetColorSpacePassthrough(bool colorSpacePassthroughEnabled); } } diff --git a/src/Ryujinx.Ui.Common/Configuration/ConfigurationFileFormat.cs b/src/Ryujinx.Ui.Common/Configuration/ConfigurationFileFormat.cs index 434894328..09e7f570a 100644 --- a/src/Ryujinx.Ui.Common/Configuration/ConfigurationFileFormat.cs +++ b/src/Ryujinx.Ui.Common/Configuration/ConfigurationFileFormat.cs @@ -14,7 +14,7 @@ namespace Ryujinx.Ui.Common.Configuration /// /// The current version of the file format /// - public const int CurrentVersion = 47; + public const int CurrentVersion = 48; /// /// Version of the configuration file format @@ -186,6 +186,11 @@ namespace Ryujinx.Ui.Common.Configuration /// public bool EnableMacroHLE { get; set; } + /// + /// Enables or disables color space passthrough, if available. + /// + public bool EnableColorSpacePassthrough { get; set; } + /// /// Enables or disables profiled translation cache persistency /// diff --git a/src/Ryujinx.Ui.Common/Configuration/ConfigurationState.cs b/src/Ryujinx.Ui.Common/Configuration/ConfigurationState.cs index 7ab20e329..ee898354b 100644 --- a/src/Ryujinx.Ui.Common/Configuration/ConfigurationState.cs +++ b/src/Ryujinx.Ui.Common/Configuration/ConfigurationState.cs @@ -485,6 +485,11 @@ namespace Ryujinx.Ui.Common.Configuration /// public ReactiveObject EnableMacroHLE { get; private set; } + /// + /// Enables or disables color space passthrough, if available. + /// + public ReactiveObject EnableColorSpacePassthrough { get; private set; } + /// /// Graphics backend /// @@ -535,6 +540,8 @@ namespace Ryujinx.Ui.Common.Configuration 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(); @@ -667,6 +674,7 @@ namespace Ryujinx.Ui.Common.Configuration EnableShaderCache = Graphics.EnableShaderCache, EnableTextureRecompression = Graphics.EnableTextureRecompression, EnableMacroHLE = Graphics.EnableMacroHLE, + EnableColorSpacePassthrough = Graphics.EnableColorSpacePassthrough, EnablePtc = System.EnablePtc, EnableInternetAccess = System.EnableInternetAccess, EnableFsIntegrityChecks = System.EnableFsIntegrityChecks, @@ -772,6 +780,7 @@ namespace Ryujinx.Ui.Common.Configuration Graphics.EnableShaderCache.Value = true; Graphics.EnableTextureRecompression.Value = false; Graphics.EnableMacroHLE.Value = true; + Graphics.EnableColorSpacePassthrough.Value = false; Graphics.AntiAliasing.Value = AntiAliasing.None; Graphics.ScalingFilter.Value = ScalingFilter.Bilinear; Graphics.ScalingFilterLevel.Value = 80; @@ -1391,6 +1400,15 @@ namespace Ryujinx.Ui.Common.Configuration 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; @@ -1426,6 +1444,7 @@ namespace Ryujinx.Ui.Common.Configuration 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; From 5e9678c8fad4625026268e457f4c3e23bdc22697 Mon Sep 17 00:00:00 2001 From: TSRBerry <20988865+TSRBerry@users.noreply.github.com> Date: Wed, 9 Aug 2023 23:27:45 +0200 Subject: [PATCH 008/105] Allow access to code memory for exefs mods (#5518) * Allow access to code memory for exefs mods * Add ASLR workaround for Skyline * Hardcode allowCodeMemoryForJit to true --- src/ARMeilleure/Translation/PTC/Ptc.cs | 2 +- .../Translation/PTC/PtcFormatter.cs | 20 ++++++++ .../Translation/PTC/PtcProfiler.cs | 46 ++++++++++++++----- .../Extensions/FileSystemExtensions.cs | 5 +- .../Loaders/Processes/ProcessLoaderHelper.cs | 7 ++- 5 files changed, 63 insertions(+), 17 deletions(-) diff --git a/src/ARMeilleure/Translation/PTC/Ptc.cs b/src/ARMeilleure/Translation/PTC/Ptc.cs index ce653383e..6f6dfcadf 100644 --- a/src/ARMeilleure/Translation/PTC/Ptc.cs +++ b/src/ARMeilleure/Translation/PTC/Ptc.cs @@ -29,7 +29,7 @@ namespace ARMeilleure.Translation.PTC private const string OuterHeaderMagicString = "PTCohd\0\0"; private const string InnerHeaderMagicString = "PTCihd\0\0"; - private const uint InternalVersion = 5502; //! To be incremented manually for each change to the ARMeilleure project. + private const uint InternalVersion = 5518; //! To be incremented manually for each change to the ARMeilleure project. private const string ActualDir = "0"; private const string BackupDir = "1"; diff --git a/src/ARMeilleure/Translation/PTC/PtcFormatter.cs b/src/ARMeilleure/Translation/PTC/PtcFormatter.cs index ddac31338..60953dcd9 100644 --- a/src/ARMeilleure/Translation/PTC/PtcFormatter.cs +++ b/src/ARMeilleure/Translation/PTC/PtcFormatter.cs @@ -27,6 +27,26 @@ namespace ARMeilleure.Translation.PTC return dictionary; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Dictionary DeserializeAndUpdateDictionary(Stream stream, Func valueFunc, Func updateFunc) where TKey : struct + { + Dictionary dictionary = new(); + + int count = DeserializeStructure(stream); + + for (int i = 0; i < count; i++) + { + TKey key = DeserializeStructure(stream); + TValue value = valueFunc(stream); + + (key, value) = updateFunc(key, value); + + dictionary.Add(key, value); + } + + return dictionary; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static List DeserializeList(Stream stream) where T : struct { diff --git a/src/ARMeilleure/Translation/PTC/PtcProfiler.cs b/src/ARMeilleure/Translation/PTC/PtcProfiler.cs index 3a4bfcec6..0fe78edab 100644 --- a/src/ARMeilleure/Translation/PTC/PtcProfiler.cs +++ b/src/ARMeilleure/Translation/PTC/PtcProfiler.cs @@ -9,10 +9,13 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.IO.Compression; +using System.Linq; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Threading; +using System.Timers; using static ARMeilleure.Translation.PTC.PtcFormatter; +using Timer = System.Timers.Timer; namespace ARMeilleure.Translation.PTC { @@ -20,7 +23,11 @@ namespace ARMeilleure.Translation.PTC { private const string OuterHeaderMagicString = "Pohd\0\0\0\0"; - private const uint InternalVersion = 1866; //! Not to be incremented manually for each change to the ARMeilleure project. + private const uint InternalVersion = 5518; //! Not to be incremented manually for each change to the ARMeilleure project. + + private static readonly uint[] _migrateInternalVersions = { + 1866, + }; private const int SaveInterval = 30; // Seconds. @@ -28,7 +35,7 @@ namespace ARMeilleure.Translation.PTC private readonly Ptc _ptc; - private readonly System.Timers.Timer _timer; + private readonly Timer _timer; private readonly ulong _outerHeaderMagic; @@ -51,7 +58,7 @@ namespace ARMeilleure.Translation.PTC { _ptc = ptc; - _timer = new System.Timers.Timer((double)SaveInterval * 1000d); + _timer = new Timer(SaveInterval * 1000d); _timer.Elapsed += PreSave; _outerHeaderMagic = BinaryPrimitives.ReadUInt64LittleEndian(EncodingCache.UTF8NoBOM.GetBytes(OuterHeaderMagicString).AsSpan()); @@ -168,7 +175,7 @@ namespace ARMeilleure.Translation.PTC return false; } - if (outerHeader.InfoFileVersion != InternalVersion) + if (outerHeader.InfoFileVersion != InternalVersion && !_migrateInternalVersions.Contains(outerHeader.InfoFileVersion)) { InvalidateCompressedStream(compressedStream); @@ -211,7 +218,19 @@ namespace ARMeilleure.Translation.PTC return false; } - ProfiledFuncs = Deserialize(stream); + switch (outerHeader.InfoFileVersion) + { + case InternalVersion: + ProfiledFuncs = Deserialize(stream); + break; + case 1866: + ProfiledFuncs = Deserialize(stream, (address, profile) => (address + 0x500000UL, profile)); + break; + default: + Logger.Error?.Print(LogClass.Ptc, $"No migration path for {nameof(outerHeader.InfoFileVersion)} '{outerHeader.InfoFileVersion}'. Discarding cache."); + InvalidateCompressedStream(compressedStream); + return false; + } Debug.Assert(stream.Position == stream.Length); @@ -225,9 +244,14 @@ namespace ARMeilleure.Translation.PTC return true; } - private static Dictionary Deserialize(Stream stream) + private static Dictionary Deserialize(Stream stream, Func migrateEntryFunc = null) { - return DeserializeDictionary(stream, (stream) => DeserializeStructure(stream)); + if (migrateEntryFunc != null) + { + return DeserializeAndUpdateDictionary(stream, DeserializeStructure, migrateEntryFunc); + } + + return DeserializeDictionary(stream, DeserializeStructure); } private static ReadOnlySpan GetReadOnlySpan(MemoryStream memoryStream) @@ -240,7 +264,7 @@ namespace ARMeilleure.Translation.PTC compressedStream.SetLength(0L); } - private void PreSave(object source, System.Timers.ElapsedEventArgs e) + private void PreSave(object source, ElapsedEventArgs e) { _waitEvent.Reset(); @@ -277,7 +301,7 @@ namespace ARMeilleure.Translation.PTC { Debug.Assert(stream.Seek(0L, SeekOrigin.Begin) == 0L && stream.Length == 0L); - stream.Seek((long)Unsafe.SizeOf(), SeekOrigin.Begin); + stream.Seek(Unsafe.SizeOf(), SeekOrigin.Begin); lock (_lock) { @@ -288,7 +312,7 @@ namespace ARMeilleure.Translation.PTC Debug.Assert(stream.Position == stream.Length); - stream.Seek((long)Unsafe.SizeOf(), SeekOrigin.Begin); + stream.Seek(Unsafe.SizeOf(), SeekOrigin.Begin); Hash128 hash = XXHash128.ComputeHash(GetReadOnlySpan(stream)); stream.Seek(0L, SeekOrigin.Begin); @@ -332,7 +356,7 @@ namespace ARMeilleure.Translation.PTC private static void Serialize(Stream stream, Dictionary profiledFuncs) { - SerializeDictionary(stream, profiledFuncs, (stream, structure) => SerializeStructure(stream, structure)); + SerializeDictionary(stream, profiledFuncs, SerializeStructure); } [StructLayout(LayoutKind.Sequential, Pack = 1/*, Size = 29*/)] diff --git a/src/Ryujinx.HLE/Loaders/Processes/Extensions/FileSystemExtensions.cs b/src/Ryujinx.HLE/Loaders/Processes/Extensions/FileSystemExtensions.cs index 040d1143d..07bbaf12b 100644 --- a/src/Ryujinx.HLE/Loaders/Processes/Extensions/FileSystemExtensions.cs +++ b/src/Ryujinx.HLE/Loaders/Processes/Extensions/FileSystemExtensions.cs @@ -89,9 +89,6 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions Logger.Warning?.Print(LogClass.Ptc, "Detected unsupported ExeFs modifications. PTC disabled."); } - // We allow it for nx-hbloader because it can be used to launch homebrew. - bool allowCodeMemoryForJit = programId == 0x010000000000100DUL || isHomebrew; - string programName = ""; if (!isHomebrew && programId > 0x010000000000FFFF) @@ -119,7 +116,7 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions metaLoader, nacpData, enablePtc, - allowCodeMemoryForJit, + true, programName, metaLoader.GetProgramId(), null, diff --git a/src/Ryujinx.HLE/Loaders/Processes/ProcessLoaderHelper.cs b/src/Ryujinx.HLE/Loaders/Processes/ProcessLoaderHelper.cs index d14a013af..292a5c122 100644 --- a/src/Ryujinx.HLE/Loaders/Processes/ProcessLoaderHelper.cs +++ b/src/Ryujinx.HLE/Loaders/Processes/ProcessLoaderHelper.cs @@ -28,6 +28,11 @@ namespace Ryujinx.HLE.Loaders.Processes { static class ProcessLoaderHelper { + // NOTE: If you want to change this value make sure to increment the InternalVersion of Ptc and PtcProfiler. + // You also need to add a new migration path and adjust the existing ones. + // TODO: Remove this workaround when ASLR is implemented. + private const ulong CodeStartOffset = 0x500000UL; + public static LibHac.Result RegisterProgramMapInfo(Switch device, PartitionFileSystem partitionFileSystem) { ulong applicationId = 0; @@ -242,7 +247,7 @@ namespace Ryujinx.HLE.Loaders.Processes ulong argsStart = 0; uint argsSize = 0; - ulong codeStart = (meta.Flags & 1) != 0 ? 0x8000000UL : 0x200000UL; + ulong codeStart = ((meta.Flags & 1) != 0 ? 0x8000000UL : 0x200000UL) + CodeStartOffset; uint codeSize = 0; var buildIds = executables.Select(e => (e switch From fe15c77d30b94a8b720b520dcacf39a0c832d58f Mon Sep 17 00:00:00 2001 From: TSRBerry <20988865+TSRBerry@users.noreply.github.com> Date: Thu, 10 Aug 2023 05:29:15 +0200 Subject: [PATCH 009/105] [Hotfix] hid: Prevent out of bounds array access (#5547) * hid: Prevent out of bounds array access * Cast player to uint * Replace lambda function with local function --- .../Services/Hid/HidDevices/NpadDevices.cs | 5 +++ src/Ryujinx.Tests/Cpu/EnvironmentTests.cs | 33 ++++++++++--------- 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/src/Ryujinx.HLE/HOS/Services/Hid/HidDevices/NpadDevices.cs b/src/Ryujinx.HLE/HOS/Services/Hid/HidDevices/NpadDevices.cs index 240933ada..86c6a825f 100644 --- a/src/Ryujinx.HLE/HOS/Services/Hid/HidDevices/NpadDevices.cs +++ b/src/Ryujinx.HLE/HOS/Services/Hid/HidDevices/NpadDevices.cs @@ -70,6 +70,11 @@ namespace Ryujinx.HLE.HOS.Services.Hid internal void SetSupportedPlayer(PlayerIndex player, bool supported = true) { + if ((uint)player >= _supportedPlayers.Length) + { + return; + } + _supportedPlayers[(int)player] = supported; } diff --git a/src/Ryujinx.Tests/Cpu/EnvironmentTests.cs b/src/Ryujinx.Tests/Cpu/EnvironmentTests.cs index 1ef12de5f..5e6b286bd 100644 --- a/src/Ryujinx.Tests/Cpu/EnvironmentTests.cs +++ b/src/Ryujinx.Tests/Cpu/EnvironmentTests.cs @@ -48,7 +48,22 @@ namespace Ryujinx.Tests.Cpu bool methodCalled = false; bool isFz = false; - var managedMethod = () => + var method = TranslatorTestMethods.GenerateFpFlagsPInvokeTest(); + + // This method sets flush-to-zero and then calls the managed method. + // Before and after setting the flags, it ensures subnormal addition works as expected. + // It returns a positive result if any tests fail, and 0 on success (or if the platform cannot change FP flags) + int result = method(Marshal.GetFunctionPointerForDelegate(ManagedMethod)); + + // Subnormal results are not flushed to zero by default, which we should have returned to exiting the method. + Assert.AreNotEqual(GetDenormal() + GetZero(), 0f); + + Assert.True(result == 0); + Assert.True(methodCalled); + Assert.True(isFz); + return; + + void ManagedMethod() { // Floating point math should not modify fp flags. float test = 2f * 3.5f; @@ -73,21 +88,7 @@ namespace Ryujinx.Tests.Cpu methodCalled = true; } - }; - - var method = TranslatorTestMethods.GenerateFpFlagsPInvokeTest(); - - // This method sets flush-to-zero and then calls the managed method. - // Before and after setting the flags, it ensures subnormal addition works as expected. - // It returns a positive result if any tests fail, and 0 on success (or if the platform cannot change FP flags) - int result = method(Marshal.GetFunctionPointerForDelegate(managedMethod)); - - // Subnormal results are not flushed to zero by default, which we should have returned to exiting the method. - Assert.AreNotEqual(GetDenormal() + GetZero(), 0f); - - Assert.True(result == 0); - Assert.True(methodCalled); - Assert.True(isFz); + } } } } From 7b2225c6b0939b9720c56a89cb0c91311b2e19e5 Mon Sep 17 00:00:00 2001 From: Isaac Marovitz <42140194+IsaacMarovitz@users.noreply.github.com> Date: Sat, 12 Aug 2023 01:47:22 +0100 Subject: [PATCH 010/105] Ava UI: Avalonia 11 & FluentAvalonia 2 Support (#4362) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * It builds (Doesn’t run waiting on FluentAvalonia Preview 5 Release) * Enable CompiledBindings by default * Ignore `PointerPressedEventArgs` Init warning * Define MIME and UTI Types * Update `UserProfileImageSelectorView` to StorageProvider API * PFS0 Magic * Update `MainWindowViewModel` to StorageProvider API * Update `SettingsUIView` to StorageProvider API * Update `ApplicationHelper` to StorageProvider API * Use `IsCheckChanged` * Rename events * Update Fluent Avalonia to Preivew 5 * More package updates * Fix long selection bar * return glyph value directly, instead of using a binding * fix menu item checkboxes * Fix build * Update to Preview 6 Unicorn conflict Fix remaining package oopsie * Fix issues from merge * Fix some warnings * Warnings * Squashed commit of the following: commit 79d1c190dba48e405a833f654691e47509a29792 Author: Mary Date: Sun Apr 16 11:38:07 2023 +0200 chore: Update Silk.NET to 2.17.1 (#4686) commit 2bc88467eb377a0ca1a8b51700300422422c8c37 Author: Ac_K Date: Sun Apr 16 09:37:31 2023 +0000 Update README.md commit baf8752e74488a419074ae1d484e54a00bc01973 Author: Vincenzo Nizza Date: Sun Apr 16 11:19:33 2023 +0200 Ensure the updater doesn't delete hidden or system files (#4626) * Copy desktop.ini to update directory if it exists in HomeDir * EnumerateFilesToDelete() exclude files with "Hidden" and "System" attributes commit d5e4378aea086d9219f890e33cf81d566d96b9ae Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun Apr 16 09:02:06 2023 +0000 nuget: bump DynamicData from 7.13.1 to 7.13.5 (#4654) Bumps [DynamicData](https://github.com/reactiveui/DynamicData) from 7.13.1 to 7.13.5. - [Release notes](https://github.com/reactiveui/DynamicData/releases) - [Changelog](https://github.com/reactivemarbles/DynamicData/blob/main/ReleaseNotes.md) - [Commits](https://github.com/reactiveui/DynamicData/compare/7.13.1...7.13.5) --- updated-dependencies: - dependency-name: DynamicData dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> commit 6dbcdfea47e60aadefd59a75e43549793481f853 Author: TSRBerry <20988865+TSRBerry@users.noreply.github.com> Date: Sun Apr 16 09:09:02 2023 +0200 Ava: Fix nca extraction window never closing & minor cleanup (#4569) * ava: Remove unused doWhileDeferred parameters * ava: Minimally improve swkbd dialog It's currently impossible to get the dialog to redirect focus to the InputBox. * ava: Fix nca extraction dialog never closing Also contains some minor cleanup commit c5258cf082b10f335f81487f22b7eeb86075e09e Author: NitroTears <73270647+NitroTears@users.noreply.github.com> Date: Sun Apr 16 11:03:35 2023 +1000 Ability to hide file types in Game List (#4555) * Added HiddenFileTypes to config state, and check to file enumeration * Added hiddenfiletypes checkboxes to the UI * Added Ava version of HiddenFileTypes * Inverted Hide to Show with file types, minor formatting * all variables with a reference to 'hidden' is now 'shown' * one more variable name changed * review feedback * added FileTypes extension methof to get the correlating config value * moved extension method to new folder and file in Ryujinx.Ui.Common * added default case for ToggleFileType * changed exception type to OutOfRangeException commit 5c89e22bb98072adc240c2eb2d26d25fa119fe7d Author: Daniel Shala Date: Sat Apr 15 18:11:24 2023 +0200 Added check for eventual symlink when displaying game files. (#4526) * Added check for eventual symlink when displaying game files. * Moved symlink check logic * Moved symlink check logic * Fixed prev commit --------- Co-authored-by: Daniel Shala commit 11ecff2ff04633d261b9a43db792f6438df63f40 Author: Alex Barney Date: Fri Apr 14 16:00:34 2023 -0700 Rename Hipc to Cmif where appropriate (#3880) commit 4c3f09644a033dbf70258c4c0e5a848263b16bbd Author: MutantAura <44103205+MutantAura@users.noreply.github.com> Date: Wed Apr 12 20:18:40 2023 +0100 Move swkbd message null check into constructor (#4671) commit e187a8870a6f19ac0a85b08aece3c1a1e196e379 Author: TSRBerry <20988865+TSRBerry@users.noreply.github.com> Date: Wed Apr 12 03:09:47 2023 +0200 HLE: Deal with empty title names properly (#4643) * hle: Deal with empty titleNames in some languages * gui: Fix displaying the wrong title name * Remove unnecessary bounds check * Fix a NRE when getting the version string * Restore empty string logic commit a64fee29dc6b8e523d61abb7e79ceaa95a558c6c Author: riperiperi Date: Tue Apr 11 08:23:41 2023 +0100 Vulkan: add situational "Fast Flush" mode (#4667) * Flush in the middle of long command buffers. * Vulkan: add situational "Fast Flush" mode The AutoFlushCounter class was added to periodically flush Vulkan command buffers throughout a frame, which reduces latency to the GPU as commands are submitted and processed much sooner. This was done by allowing command buffers to flush when framebuffer attachments changed. However, some games have incredibly long render passes with a large number of draws, and really aggressive data access that forces GPU sync. The Vulkan backend could potentially end up building a single command buffer for 4-5ms if a pass has enough draws, such as in BOTW. In the scenario where sync is waited on immediately after submission, this would have to wait for the completion of a much longer command buffer than usual. The solution is to force command buffer submission periodically in a "fast flush" mode. This will end up splitting render passes, but it will only enable if sync is aggressive enough. This should improve performance in GPU limited scenarios, or in games that aggressively wait on synchronization. In some games, it may only kick in when res scaling. It won't trigger in games like SMO where sync is not an issue. Improves performance in Pokemon Scarlet/Violet (res scaled) and BOTW (in general). * Add conversions in milliseconds next to flush timers. commit 9ef94c8292beda825fa76e05ad2e561c6d571c95 Author: riperiperi Date: Tue Apr 11 07:55:04 2023 +0100 ARMeilleure: Move TPIDR_EL0 and TPIDRRO_EL0 to NativeContext (#4661) * ARMeilleure: Move TPIDR_EL0 and TPIDRRO_EL0 to NativeContext Some games access these system registers several tens of thousands of times in a second from many different threads. While this isn't really crippling, it is a lot of wasted time spent in a reverse pinvoke transition. Example games are Pokemon Scarlet/Violet and BOTW. These games have a lot of different potential bottlenecks so it's unlikely you will see a consistent improvement, but it definitely disappears from the cpu profile. * Remove unreachable code. * Add ulong conversion for offsets * Nit commit 915d6d044cbf8c89935f14b8c7e085ad729f0e28 Author: riperiperi Date: Tue Apr 11 07:32:31 2023 +0100 OpenGL: Fix OBS/Overlays again by binding FB before present (#4668) This seems to have been removed by the Post-Processing PR, but it is required for the display in OBS to be the right way up and properly scaled. I've tested this with AA and FSR on MK8D and it seems to behave properly. Testing is welcome. commit a4780ab33b9ca6b698917ded3ef6db6e6716cad1 Author: MutantAura <44103205+MutantAura@users.noreply.github.com> Date: Mon Apr 10 23:04:31 2023 +0100 Force activate parent window before dialog is shown (#4663) * Fix build Extraction dialogue not working * Avalonia Preview 7 Needs Fluent Avalonia update still… * Fix Render Scaling * Update Fluent Avalonia * Remove `pfs0` as runnable file type * Restore Info.plist formatting * Plist Format * Update Avalonia.Svg.Skia * Update theme code (TODO) * swtich to using theme variants for light dark * Fix crashes * Text centering issues * Update `TitleUpdateViewModel` to StorageProvider API * Fixed for new PR (Will crash on launch) * Fixes… * UI: Fix sections extraction (#4820) * UI: Fix sections extraction There is currently an issue when the update NCA doesn't contains the section we want to extract, this is fixed by adding a check. I have fixed the inverted handler of ExeFs/Logo introduced in #4755. Fixes #4521 * Addresses feedback * Fix issues… * Preview 8 * Fix fuck ups * Fixes * More cleanup * Ava 11 RC Maybe there is a god * Update FluentAvalonia * update svg * Second RC (kill me) * It builds * Ava 11 * Remove unnecessary usings * Fix build * Formatting * GAS GAS GAS!!!! * Fix DLC Window Crash * Linux runner try not to crash challenge (impossible) * Add app.manifest * Fix accidental Silk.NET.Vulkan bump * Try fix truncation * Linux fix popup Windows * Fix cutoff text on windows * Status bar styling fixes * Volume Toggle Split Button Fixes * Fix load bar color * Fix shortcuts * Best we're gonna get * Fix spacing Co-authored-by: Exhigh * Formatting * Fix Profile Dropdown * Fix Window Startup Position * Format Fixes * Fix stupid mistake * Fix accidental change * Scaling Handler (peri pls make sure is working) * Remove Locale Reflection Binding Use + Unsued Usings * Fix formatting Code styling Ughhhh Fix interface Make TimeZoneConverter internal * Remove bell workaround (no longer needed) * Disable accent menu * Update to Ava 11.0.2 * Peri suggestions * Formatting * Cleanup a bunch of jank * Dependency update * Berry fixes and suggestions * Final suggestions * Rename assemblyIdentity to Ryujinx.Emulator.Avalonia --------- Co-authored-by: Emmanuel Hansen Co-authored-by: Ac_K Co-authored-by: Exhigh Co-authored-by: TSR Berry <20988865+TSRBerry@users.noreply.github.com> --- Directory.Packages.props | 19 +- distribution/macos/Info.plist | 109 +++++- src/Ryujinx.Ava/App.axaml | 8 + src/Ryujinx.Ava/App.axaml.cs | 39 +- src/Ryujinx.Ava/AppHost.cs | 41 +- src/Ryujinx.Ava/Assets/Styles/BaseDark.xaml | 65 ---- src/Ryujinx.Ava/Assets/Styles/BaseLight.xaml | 57 --- src/Ryujinx.Ava/Assets/Styles/Styles.xaml | 361 +++++++++++------- src/Ryujinx.Ava/Assets/Styles/Themes.xaml | 85 +++++ src/Ryujinx.Ava/Common/ApplicationHelper.cs | 275 ++++++------- .../Input/AvaloniaKeyboardDriver.cs | 7 - src/Ryujinx.Ava/Program.cs | 9 +- src/Ryujinx.Ava/Ryujinx.Ava.csproj | 13 +- .../UI/Applet/ErrorAppletWindow.axaml | 2 +- .../UI/Applet/ErrorAppletWindow.axaml.cs | 10 +- .../UI/Applet/SwkbdAppletDialog.axaml | 6 +- .../UI/Controls/ApplicationContextMenu.axaml | 4 +- .../Controls/ApplicationContextMenu.axaml.cs | 18 +- .../UI/Controls/ApplicationGridView.axaml | 22 +- .../UI/Controls/ApplicationGridView.axaml.cs | 8 +- .../UI/Controls/ApplicationListView.axaml | 18 +- .../UI/Controls/ApplicationListView.axaml.cs | 8 +- .../UI/Helpers/GlyphValueConverter.cs | 10 +- src/Ryujinx.Ava/UI/Helpers/HotKeyControl.cs | 52 --- src/Ryujinx.Ava/UI/Helpers/LoggerAdapter.cs | 15 - .../UI/Helpers/OffscreenTextBox.cs | 1 - .../UI/Helpers/TimeZoneConverter.cs | 28 ++ src/Ryujinx.Ava/UI/Models/UserProfile.cs | 4 +- src/Ryujinx.Ava/UI/Renderer/EmbeddedWindow.cs | 14 +- .../UI/Renderer/RendererHost.axaml.cs | 10 +- .../UI/ViewModels/AboutWindowViewModel.cs | 19 +- .../DownloadableContentManagerViewModel.cs | 36 +- .../UI/ViewModels/MainWindowViewModel.cs | 215 ++++++----- .../UI/ViewModels/TitleUpdateViewModel.cs | 37 +- .../UI/Views/Input/ControllerInputView.axaml | 19 +- .../Views/Input/ControllerInputView.axaml.cs | 83 ++-- .../UI/Views/Input/MotionInputView.axaml | 1 - .../UI/Views/Input/RumbleInputView.axaml | 1 - .../UI/Views/Main/MainMenuBarView.axaml | 89 +++-- .../UI/Views/Main/MainMenuBarView.axaml.cs | 6 +- .../UI/Views/Main/MainStatusBarView.axaml | 39 +- .../UI/Views/Main/MainStatusBarView.axaml.cs | 6 + .../UI/Views/Main/MainViewControls.axaml | 13 +- .../UI/Views/Settings/SettingsAudioView.axaml | 3 +- .../UI/Views/Settings/SettingsCPUView.axaml | 1 - .../Views/Settings/SettingsGraphicsView.axaml | 5 +- .../Views/Settings/SettingsHotkeysView.axaml | 5 +- .../Settings/SettingsHotkeysView.axaml.cs | 2 +- .../UI/Views/Settings/SettingsInputView.axaml | 1 - .../Views/Settings/SettingsLoggingView.axaml | 1 - .../Views/Settings/SettingsNetworkView.axaml | 3 +- .../Views/Settings/SettingsSystemView.axaml | 110 ++++-- .../Settings/SettingsSystemView.axaml.cs | 19 +- .../UI/Views/Settings/SettingsUIView.axaml | 3 +- .../UI/Views/Settings/SettingsUIView.axaml.cs | 42 +- .../UI/Views/User/UserEditorView.axaml | 1 - .../User/UserFirmwareAvatarSelectorView.axaml | 5 +- .../User/UserProfileImageSelectorView.axaml | 1 - .../UserProfileImageSelectorView.axaml.cs | 38 +- .../UI/Views/User/UserRecovererView.axaml | 3 +- .../UI/Views/User/UserSaveManagerView.axaml | 6 +- .../UI/Views/User/UserSelectorView.axaml | 9 +- src/Ryujinx.Ava/UI/Windows/AboutWindow.axaml | 7 +- src/Ryujinx.Ava/UI/Windows/AmiiboWindow.axaml | 15 +- src/Ryujinx.Ava/UI/Windows/CheatWindow.axaml | 12 +- .../ContentDialogOverlayWindow.axaml.cs | 9 +- .../DownloadableContentManagerWindow.axaml | 8 +- src/Ryujinx.Ava/UI/Windows/MainWindow.axaml | 20 +- .../UI/Windows/MainWindow.axaml.cs | 65 ++-- .../UI/Windows/SettingsWindow.axaml | 29 +- .../UI/Windows/SettingsWindow.axaml.cs | 6 +- src/Ryujinx.Ava/UI/Windows/StyleableWindow.cs | 4 +- .../UI/Windows/TitleUpdateWindow.axaml | 6 +- .../UI/Windows/TitleUpdateWindow.axaml.cs | 2 +- src/Ryujinx.Ava/app.manifest | 10 + 75 files changed, 1252 insertions(+), 1081 deletions(-) delete mode 100644 src/Ryujinx.Ava/Assets/Styles/BaseDark.xaml delete mode 100644 src/Ryujinx.Ava/Assets/Styles/BaseLight.xaml create mode 100644 src/Ryujinx.Ava/Assets/Styles/Themes.xaml delete mode 100644 src/Ryujinx.Ava/UI/Helpers/HotKeyControl.cs create mode 100644 src/Ryujinx.Ava/UI/Helpers/TimeZoneConverter.cs create mode 100644 src/Ryujinx.Ava/app.manifest diff --git a/Directory.Packages.props b/Directory.Packages.props index 6191c4984..fbae486c3 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -3,21 +3,21 @@ true - - - - - - - + + + + + + + - + - + @@ -48,6 +48,5 @@ - \ No newline at end of file diff --git a/distribution/macos/Info.plist b/distribution/macos/Info.plist index 968814f94..6e068ba2d 100644 --- a/distribution/macos/Info.plist +++ b/distribution/macos/Info.plist @@ -44,10 +44,115 @@ public.app-category.games LSMinimumSystemVersion 11.0 + UTExportedTypeDeclarations + + + UTTypeDescription + Extensible Application Markup Language + UTTypeConformsTo + + public.xml + + UTTypeIdentifier + com.ryujinx.xaml + UTTypeTagSpecification + + public.filename-extension + + xaml + + + + + UTTypeDescription + Nintendo Submission Package + UTTypeConformsTo + + public.data + + UTTypeIdentifier + com.ryujinx.nsp + UTTypeTagSpecification + + public.filename-extension + + nsp + + + + + UTTypeDescription + Nintendo Switch Cartridge + UTTypeConformsTo + + public.data + + UTTypeIdentifier + com.ryujinx.xci + UTTypeTagSpecification + + public.filename-extension + + xci + + + + + UTTypeDescription + Nintendo Content Archive + UTTypeConformsTo + + public.data + + UTTypeIdentifier + com.ryujinx.nca + UTTypeTagSpecification + + public.filename-extension + + nca + + + + + UTTypeDescription + Nintendo Relocatable Object + UTTypeConformsTo + + public.data + + UTTypeIdentifier + com.ryujinx.nro + UTTypeTagSpecification + + public.filename-extension + + nro + + + + + UTTypeDescription + Nintendo Shared Object + UTTypeConformsTo + + public.data + + UTTypeIdentifier + com.ryujinx.nso + UTTypeTagSpecification + + public.filename-extension + + nso + + + + LSEnvironment - COMPlus_DefaultStackSize + DOTNET_DefaultStackSize 200000 - + \ No newline at end of file diff --git a/src/Ryujinx.Ava/App.axaml b/src/Ryujinx.Ava/App.axaml index 72bc0deee..eab318b7b 100644 --- a/src/Ryujinx.Ava/App.axaml +++ b/src/Ryujinx.Ava/App.axaml @@ -3,7 +3,15 @@ xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:sty="using:FluentAvalonia.Styling"> + + + + + + + + \ No newline at end of file diff --git a/src/Ryujinx.Ava/App.axaml.cs b/src/Ryujinx.Ava/App.axaml.cs index 4ecc424a6..031e7e447 100644 --- a/src/Ryujinx.Ava/App.axaml.cs +++ b/src/Ryujinx.Ava/App.axaml.cs @@ -3,7 +3,6 @@ using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Markup.Xaml; using Avalonia.Styling; using Avalonia.Threading; -using FluentAvalonia.Styling; using Ryujinx.Ava.Common.Locale; using Ryujinx.Ava.UI.Helpers; using Ryujinx.Ava.UI.Windows; @@ -24,6 +23,11 @@ namespace Ryujinx.Ava Name = $"Ryujinx {Program.Version}"; AvaloniaXamlLoader.Load(this); + + if (OperatingSystem.IsMacOS()) + { + Process.Start("/usr/bin/defaults", "write org.ryujinx.Ryujinx ApplePressAndHoldEnabled -bool false"); + } } public override void OnFrameworkInitializationCompleted() @@ -89,8 +93,6 @@ namespace Ryujinx.Ava string themePath = ConfigurationState.Instance.Ui.CustomThemePath; bool enableCustomTheme = ConfigurationState.Instance.Ui.EnableCustomTheme; - const string BaseStyleUrl = "avares://Ryujinx.Ava/Assets/Styles/Base{0}.xaml"; - if (string.IsNullOrWhiteSpace(baseStyle)) { ConfigurationState.Instance.Ui.BaseStyle.Value = "Dark"; @@ -98,31 +100,12 @@ namespace Ryujinx.Ava baseStyle = ConfigurationState.Instance.Ui.BaseStyle; } - var theme = AvaloniaLocator.Current.GetService(); - - theme.RequestedTheme = baseStyle; - - var currentStyles = this.Styles; - - // Remove all styles except the base style. - if (currentStyles.Count > 1) + RequestedThemeVariant = baseStyle switch { - currentStyles.RemoveRange(1, currentStyles.Count - 1); - } - - IStyle newStyles = null; - - // Load requested style, and fallback to Dark theme if loading failed. - try - { - newStyles = (Styles)AvaloniaXamlLoader.Load(new Uri(string.Format(BaseStyleUrl, baseStyle), UriKind.Absolute)); - } - catch (XamlLoadException) - { - newStyles = (Styles)AvaloniaXamlLoader.Load(new Uri(string.Format(BaseStyleUrl, "Dark"), UriKind.Absolute)); - } - - currentStyles.Add(newStyles); + "Light" => ThemeVariant.Light, + "Dark" => ThemeVariant.Dark, + _ => ThemeVariant.Default + }; if (enableCustomTheme) { @@ -133,7 +116,7 @@ namespace Ryujinx.Ava var themeContent = File.ReadAllText(themePath); var customStyle = AvaloniaRuntimeXamlLoader.Parse(themeContent); - currentStyles.Add(customStyle); + Styles.Add(customStyle); } catch (Exception ex) { diff --git a/src/Ryujinx.Ava/AppHost.cs b/src/Ryujinx.Ava/AppHost.cs index 786b45070..a8388e9cd 100644 --- a/src/Ryujinx.Ava/AppHost.cs +++ b/src/Ryujinx.Ava/AppHost.cs @@ -3,6 +3,7 @@ using Avalonia; using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Input; +using Avalonia.Rendering; using Avalonia.Threading; using LibHac.Tools.FsSystem; using Ryujinx.Audio.Backends.Dummy; @@ -54,6 +55,7 @@ using static Ryujinx.Ava.UI.Helpers.Win32NativeInterop; using AntiAliasing = Ryujinx.Common.Configuration.AntiAliasing; using Image = SixLabors.ImageSharp.Image; 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; @@ -167,9 +169,9 @@ namespace Ryujinx.Ava ConfigurationState.Instance.HideCursor.Event += HideCursorState_Changed; - _topLevel.PointerMoved += TopLevel_PointerEnterOrMoved; - _topLevel.PointerEnter += TopLevel_PointerEnterOrMoved; - _topLevel.PointerLeave += TopLevel_PointerLeave; + _topLevel.PointerMoved += TopLevel_PointerEnteredOrMoved; + _topLevel.PointerEntered += TopLevel_PointerEnteredOrMoved; + _topLevel.PointerExited += TopLevel_PointerExited; if (OperatingSystem.IsWindows()) { @@ -194,26 +196,23 @@ namespace Ryujinx.Ava _gpuDoneEvent = new ManualResetEvent(false); } - private void TopLevel_PointerEnterOrMoved(object sender, PointerEventArgs e) + private void TopLevel_PointerEnteredOrMoved(object sender, PointerEventArgs e) { if (sender is MainWindow window) { _lastCursorMoveTime = Stopwatch.GetTimestamp(); - if (RendererHost.EmbeddedWindow.TransformedBounds != null) - { - var point = e.GetCurrentPoint(window).Position; - var bounds = RendererHost.EmbeddedWindow.TransformedBounds.Value.Clip; + var point = e.GetCurrentPoint(window).Position; + var bounds = RendererHost.EmbeddedWindow.Bounds; - _isCursorInRenderer = point.X >= bounds.X && - point.X <= bounds.Width + bounds.X && - point.Y >= bounds.Y && - point.Y <= bounds.Height + bounds.Y; - } + _isCursorInRenderer = point.X >= bounds.X && + point.X <= bounds.Width + bounds.X && + point.Y >= bounds.Y && + point.Y <= bounds.Height + bounds.Y; } } - private void TopLevel_PointerLeave(object sender, PointerEventArgs e) + private void TopLevel_PointerExited(object sender, PointerEventArgs e) { _isCursorInRenderer = false; } @@ -265,7 +264,7 @@ namespace Ryujinx.Ava { if (_renderer != null) { - double scale = _topLevel.PlatformImpl.RenderScaling; + double scale = _topLevel.RenderScaling; _renderer.Window?.SetSize((int)(size.Width * scale), (int)(size.Height * scale)); } @@ -359,7 +358,7 @@ namespace Ryujinx.Ava _viewModel.SetUiProgressHandlers(Device); - RendererHost.SizeChanged += Window_SizeChanged; + RendererHost.BoundsChanged += Window_BoundsChanged; _isActive = true; @@ -469,9 +468,9 @@ namespace Ryujinx.Ava ConfigurationState.Instance.Graphics.AntiAliasing.Event -= UpdateAntiAliasing; ConfigurationState.Instance.Graphics.EnableColorSpacePassthrough.Event -= UpdateColorSpacePassthrough; - _topLevel.PointerMoved -= TopLevel_PointerEnterOrMoved; - _topLevel.PointerEnter -= TopLevel_PointerEnterOrMoved; - _topLevel.PointerLeave -= TopLevel_PointerLeave; + _topLevel.PointerMoved -= TopLevel_PointerEnteredOrMoved; + _topLevel.PointerEntered -= TopLevel_PointerEnteredOrMoved; + _topLevel.PointerExited -= TopLevel_PointerExited; _gpuCancellationTokenSource.Cancel(); _gpuCancellationTokenSource.Dispose(); @@ -849,7 +848,7 @@ namespace Ryujinx.Ava return deviceDriver; } - private void Window_SizeChanged(object sender, Size e) + private void Window_BoundsChanged(object sender, Size e) { Width = (int)e.Width; Height = (int)e.Height; @@ -899,7 +898,7 @@ namespace Ryujinx.Ava Width = (int)RendererHost.Bounds.Width; Height = (int)RendererHost.Bounds.Height; - _renderer.Window.SetSize((int)(Width * _topLevel.PlatformImpl.RenderScaling), (int)(Height * _topLevel.PlatformImpl.RenderScaling)); + _renderer.Window.SetSize((int)(Width * _topLevel.RenderScaling), (int)(Height * _topLevel.RenderScaling)); _chrono.Start(); diff --git a/src/Ryujinx.Ava/Assets/Styles/BaseDark.xaml b/src/Ryujinx.Ava/Assets/Styles/BaseDark.xaml deleted file mode 100644 index c7f6266fb..000000000 --- a/src/Ryujinx.Ava/Assets/Styles/BaseDark.xaml +++ /dev/null @@ -1,65 +0,0 @@ - - - - - - - - - - - - - - - - - - - - @@ -750,7 +749,7 @@ diff --git a/src/Ryujinx.Ava/UI/Views/Input/ControllerInputView.axaml.cs b/src/Ryujinx.Ava/UI/Views/Input/ControllerInputView.axaml.cs index 19009f5f2..351297060 100644 --- a/src/Ryujinx.Ava/UI/Views/Input/ControllerInputView.axaml.cs +++ b/src/Ryujinx.Ava/UI/Views/Input/ControllerInputView.axaml.cs @@ -31,8 +31,7 @@ namespace Ryujinx.Ava.UI.Views.Input { if (visual is ToggleButton button && visual is not CheckBox) { - button.Checked += Button_Checked; - button.Unchecked += Button_Unchecked; + button.IsCheckedChanged += Button_IsCheckedChanged; } } } @@ -47,48 +46,56 @@ namespace Ryujinx.Ava.UI.Views.Input } } - private void Button_Checked(object sender, RoutedEventArgs e) + private void Button_IsCheckedChanged(object sender, RoutedEventArgs e) { if (sender is ToggleButton button) { - if (_currentAssigner != null && button == _currentAssigner.ToggledButton) + if ((bool)button.IsChecked) { - return; - } - - bool isStick = button.Tag != null && button.Tag.ToString() == "stick"; - - if (_currentAssigner == null && (bool)button.IsChecked) - { - _currentAssigner = new ButtonKeyAssigner(button); - - FocusManager.Instance.Focus(this, NavigationMethod.Pointer); - - PointerPressed += MouseClick; - - IKeyboard keyboard = (IKeyboard)ViewModel.AvaloniaKeyboardDriver.GetGamepad("0"); // Open Avalonia keyboard for cancel operations. - IButtonAssigner assigner = CreateButtonAssigner(isStick); - - _currentAssigner.ButtonAssigned += (sender, e) => + if (_currentAssigner != null && button == _currentAssigner.ToggledButton) { - if (e.IsAssigned) - { - ViewModel.IsModified = true; - } - }; + return; + } - _currentAssigner.GetInputAndAssign(assigner, keyboard); + bool isStick = button.Tag != null && button.Tag.ToString() == "stick"; + + if (_currentAssigner == null) + { + _currentAssigner = new ButtonKeyAssigner(button); + + this.Focus(NavigationMethod.Pointer); + + PointerPressed += MouseClick; + + IKeyboard keyboard = (IKeyboard)ViewModel.AvaloniaKeyboardDriver.GetGamepad("0"); // Open Avalonia keyboard for cancel operations. + IButtonAssigner assigner = CreateButtonAssigner(isStick); + + _currentAssigner.ButtonAssigned += (sender, e) => + { + if (e.IsAssigned) + { + ViewModel.IsModified = true; + } + }; + + _currentAssigner.GetInputAndAssign(assigner, keyboard); + } + else + { + if (_currentAssigner != null) + { + ToggleButton oldButton = _currentAssigner.ToggledButton; + + _currentAssigner.Cancel(); + _currentAssigner = null; + button.IsChecked = false; + } + } } else { - if (_currentAssigner != null) - { - ToggleButton oldButton = _currentAssigner.ToggledButton; - - _currentAssigner.Cancel(); - _currentAssigner = null; - button.IsChecked = false; - } + _currentAssigner?.Cancel(); + _currentAssigner = null; } } } @@ -120,12 +127,6 @@ namespace Ryujinx.Ava.UI.Views.Input return assigner; } - private void Button_Unchecked(object sender, RoutedEventArgs e) - { - _currentAssigner?.Cancel(); - _currentAssigner = null; - } - private void MouseClick(object sender, PointerPressedEventArgs e) { bool shouldUnbind = false; diff --git a/src/Ryujinx.Ava/UI/Views/Input/MotionInputView.axaml b/src/Ryujinx.Ava/UI/Views/Input/MotionInputView.axaml index b18324379..71d5d7460 100644 --- a/src/Ryujinx.Ava/UI/Views/Input/MotionInputView.axaml +++ b/src/Ryujinx.Ava/UI/Views/Input/MotionInputView.axaml @@ -8,7 +8,6 @@ xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels" mc:Ignorable="d" x:Class="Ryujinx.Ava.UI.Views.Input.MotionInputView" - x:CompileBindings="True" x:DataType="viewModels:MotionInputViewModel" Focusable="True"> diff --git a/src/Ryujinx.Ava/UI/Views/Input/RumbleInputView.axaml b/src/Ryujinx.Ava/UI/Views/Input/RumbleInputView.axaml index 3882ebe21..16190d391 100644 --- a/src/Ryujinx.Ava/UI/Views/Input/RumbleInputView.axaml +++ b/src/Ryujinx.Ava/UI/Views/Input/RumbleInputView.axaml @@ -8,7 +8,6 @@ mc:Ignorable="d" x:Class="Ryujinx.Ava.UI.Views.Input.RumbleInputView" x:DataType="viewModels:RumbleInputViewModel" - x:CompileBindings="True" Focusable="True"> diff --git a/src/Ryujinx.Ava/UI/Views/Main/MainMenuBarView.axaml b/src/Ryujinx.Ava/UI/Views/Main/MainMenuBarView.axaml index d5b5efcdd..30358adab 100644 --- a/src/Ryujinx.Ava/UI/Views/Main/MainMenuBarView.axaml +++ b/src/Ryujinx.Ava/UI/Views/Main/MainMenuBarView.axaml @@ -7,8 +7,7 @@ mc:Ignorable="d" xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels" x:DataType="viewModels:MainWindowViewModel" - x:Class="Ryujinx.Ava.UI.Views.Main.MainMenuBarView" - x:CompileBindings="True"> + x:Class="Ryujinx.Ava.UI.Views.Main.MainMenuBarView"> @@ -25,12 +24,12 @@ @@ -42,11 +41,11 @@ @@ -57,35 +56,75 @@ - + - - - + + + + + - + - - - + + + + + - - + + @@ -113,7 +152,7 @@ InputGesture="Escape" IsEnabled="{Binding IsGameRunning}" ToolTip.Tip="{locale:Locale StopEmulationTooltip}" /> - + @@ -138,8 +177,8 @@ - - + + diff --git a/src/Ryujinx.Ava/UI/Views/Main/MainMenuBarView.axaml.cs b/src/Ryujinx.Ava/UI/Views/Main/MainMenuBarView.axaml.cs index ae52f0719..af8c4dab9 100644 --- a/src/Ryujinx.Ava/UI/Views/Main/MainMenuBarView.axaml.cs +++ b/src/Ryujinx.Ava/UI/Views/Main/MainMenuBarView.axaml.cs @@ -30,8 +30,8 @@ namespace Ryujinx.Ava.UI.Views.Main { InitializeComponent(); - ToggleFileTypesMenuItem.Items = GenerateToggleFileTypeItems(); - ChangeLanguageMenuItem.Items = GenerateLanguageMenuItems(); + ToggleFileTypesMenuItem.ItemsSource = GenerateToggleFileTypeItems(); + ChangeLanguageMenuItem.ItemsSource = GenerateLanguageMenuItems(); } private CheckBox[] GenerateToggleFileTypeItems() @@ -45,7 +45,7 @@ namespace Ryujinx.Ava.UI.Views.Main { Content = $".{fileName}", IsChecked = ((FileTypes)item).GetConfigValue(ConfigurationState.Instance.Ui.ShownFileTypes), - Command = MiniCommand.Create(() => ViewModel.ToggleFileType(fileName)), + Command = MiniCommand.Create(() => Window.ToggleFileType(fileName)), }); } diff --git a/src/Ryujinx.Ava/UI/Views/Main/MainStatusBarView.axaml b/src/Ryujinx.Ava/UI/Views/Main/MainStatusBarView.axaml index 167056954..58e06a1c2 100644 --- a/src/Ryujinx.Ava/UI/Views/Main/MainStatusBarView.axaml +++ b/src/Ryujinx.Ava/UI/Views/Main/MainStatusBarView.axaml @@ -8,7 +8,6 @@ xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="Ryujinx.Ava.UI.Views.Main.MainStatusBarView" - x:CompileBindings="True" x:DataType="viewModels:MainWindowViewModel"> @@ -46,7 +45,7 @@ Margin="0,0,5,0" VerticalAlignment="Center" Background="Transparent" - Command="{ReflectionBinding LoadApplications}"> + Click="Refresh_OnClick"> @@ -93,6 +92,7 @@ Height="12" Margin="0" BorderBrush="Gray" + Background="Gray" BorderThickness="1" IsVisible="{Binding !ShowLoadProgress}" /> - - + IsVisible="{Binding !ShowLoadProgress}" + Background="Transparent" + BorderThickness="0" + CornerRadius="0"> + + + + + + - - + + - \ No newline at end of file + diff --git a/src/Ryujinx.Ava/UI/Views/Main/MainStatusBarView.axaml.cs b/src/Ryujinx.Ava/UI/Views/Main/MainStatusBarView.axaml.cs index 0640869c1..a0acc2779 100644 --- a/src/Ryujinx.Ava/UI/Views/Main/MainStatusBarView.axaml.cs +++ b/src/Ryujinx.Ava/UI/Views/Main/MainStatusBarView.axaml.cs @@ -1,6 +1,7 @@ using Avalonia; using Avalonia.Controls; using Avalonia.Input; +using Avalonia.Interactivity; using Ryujinx.Ava.UI.Windows; using Ryujinx.Common.Configuration; using Ryujinx.Common.Logging; @@ -48,5 +49,10 @@ namespace Ryujinx.Ava.UI.Views.Main 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(); + } } } diff --git a/src/Ryujinx.Ava/UI/Views/Main/MainViewControls.axaml b/src/Ryujinx.Ava/UI/Views/Main/MainViewControls.axaml index f7dbf2b21..f5a177424 100644 --- a/src/Ryujinx.Ava/UI/Views/Main/MainViewControls.axaml +++ b/src/Ryujinx.Ava/UI/Views/Main/MainViewControls.axaml @@ -9,7 +9,6 @@ xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="Ryujinx.Ava.UI.Views.Main.MainViewControls" - x:CompileBindings="True" x:DataType="viewModels:MainWindowViewModel"> @@ -23,7 +22,7 @@ MinWidth="40" Margin="5,2,0,2" VerticalAlignment="Stretch" - Command="{ReflectionBinding SetListMode}" + Command="{Binding SetListMode}" IsEnabled="{Binding IsGrid}"> - - + - - + + diff --git a/src/Ryujinx.Ava/UI/Views/Settings/SettingsCPUView.axaml b/src/Ryujinx.Ava/UI/Views/Settings/SettingsCPUView.axaml index e98b963c1..c74d3dd57 100644 --- a/src/Ryujinx.Ava/UI/Views/Settings/SettingsCPUView.axaml +++ b/src/Ryujinx.Ava/UI/Views/Settings/SettingsCPUView.axaml @@ -7,7 +7,6 @@ xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale" xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels" mc:Ignorable="d" - x:CompileBindings="True" x:DataType="viewModels:SettingsViewModel"> diff --git a/src/Ryujinx.Ava/UI/Views/Settings/SettingsGraphicsView.axaml b/src/Ryujinx.Ava/UI/Views/Settings/SettingsGraphicsView.axaml index 670de69c6..9dc67dadb 100644 --- a/src/Ryujinx.Ava/UI/Views/Settings/SettingsGraphicsView.axaml +++ b/src/Ryujinx.Ava/UI/Views/Settings/SettingsGraphicsView.axaml @@ -1,4 +1,4 @@ - @@ -54,7 +53,7 @@ HorizontalContentAlignment="Left" ToolTip.Tip="{locale:Locale SettingsTabGraphicsPreferredGpuTooltip}" SelectedIndex="{Binding PreferredGpuIndex}" - Items="{Binding AvailableGpus}"/> + ItemsSource="{Binding AvailableGpus}"/> diff --git a/src/Ryujinx.Ava/UI/Views/Settings/SettingsHotkeysView.axaml b/src/Ryujinx.Ava/UI/Views/Settings/SettingsHotkeysView.axaml index 361125bfe..a53c1dfe4 100644 --- a/src/Ryujinx.Ava/UI/Views/Settings/SettingsHotkeysView.axaml +++ b/src/Ryujinx.Ava/UI/Views/Settings/SettingsHotkeysView.axaml @@ -1,4 +1,4 @@ - @@ -17,7 +16,7 @@ - diff --git a/src/Ryujinx.Ava/UI/Views/Settings/SettingsLoggingView.axaml b/src/Ryujinx.Ava/UI/Views/Settings/SettingsLoggingView.axaml index 948e7181f..0fc9ea1bb 100644 --- a/src/Ryujinx.Ava/UI/Views/Settings/SettingsLoggingView.axaml +++ b/src/Ryujinx.Ava/UI/Views/Settings/SettingsLoggingView.axaml @@ -8,7 +8,6 @@ xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale" xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels" mc:Ignorable="d" - x:CompileBindings="True" x:DataType="viewModels:SettingsViewModel"> diff --git a/src/Ryujinx.Ava/UI/Views/Settings/SettingsNetworkView.axaml b/src/Ryujinx.Ava/UI/Views/Settings/SettingsNetworkView.axaml index ab8a7f6d1..6ce1bb94f 100644 --- a/src/Ryujinx.Ava/UI/Views/Settings/SettingsNetworkView.axaml +++ b/src/Ryujinx.Ava/UI/Views/Settings/SettingsNetworkView.axaml @@ -7,7 +7,6 @@ xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale" xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels" mc:Ignorable="d" - x:CompileBindings="True" x:DataType="viewModels:SettingsViewModel"> @@ -37,7 +36,7 @@ diff --git a/src/Ryujinx.Ava/UI/Views/Settings/SettingsSystemView.axaml b/src/Ryujinx.Ava/UI/Views/Settings/SettingsSystemView.axaml index cc60ef24d..e6f7c6e46 100644 --- a/src/Ryujinx.Ava/UI/Views/Settings/SettingsSystemView.axaml +++ b/src/Ryujinx.Ava/UI/Views/Settings/SettingsSystemView.axaml @@ -3,12 +3,15 @@ xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" - xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale" xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels" - x:CompileBindings="True" - x:DataType="viewModels:SettingsViewModel" - mc:Ignorable="d"> + xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers" + mc:Ignorable="d" + x:DataType="viewModels:SettingsViewModel"> + + + @@ -24,18 +27,24 @@ HorizontalAlignment="Stretch" Orientation="Vertical" Spacing="10"> - - - + + + + Text="{locale:Locale SettingsTabSystemSystemRegion}" + Width="250" /> + ToolTip.Tip="{locale:Locale RegionTooltip}" + HorizontalContentAlignment="Left" + Width="350"> @@ -59,17 +68,19 @@ - + + ToolTip.Tip="{locale:Locale LanguageTooltip}" + Width="250" /> + ToolTip.Tip="{locale:Locale LanguageTooltip}" + HorizontalContentAlignment="Left" + Width="350"> @@ -126,63 +137,84 @@ - + + ToolTip.Tip="{locale:Locale TimezoneTooltip}" + Width="250" /> + ToolTip.Tip="{locale:Locale TimezoneTooltip}" + ValueMemberBinding="{Binding Mode=OneWay, Converter={StaticResource TimeZone}}" /> - + + ToolTip.Tip="{locale:Locale TimeTooltip}" + Width="250"/> + ToolTip.Tip="{locale:Locale TimeTooltip}" + Width="350" /> - + - + - + - - - + + + - + - + diff --git a/src/Ryujinx.Ava/UI/Views/Settings/SettingsSystemView.axaml.cs b/src/Ryujinx.Ava/UI/Views/Settings/SettingsSystemView.axaml.cs index 4acf2f44c..216561dc9 100644 --- a/src/Ryujinx.Ava/UI/Views/Settings/SettingsSystemView.axaml.cs +++ b/src/Ryujinx.Ava/UI/Views/Settings/SettingsSystemView.axaml.cs @@ -1,9 +1,5 @@ using Avalonia.Controls; -using Avalonia.Data; -using Avalonia.Data.Converters; using Ryujinx.Ava.UI.ViewModels; -using System; -using System.Linq; using TimeZone = Ryujinx.Ava.UI.Models.TimeZone; namespace Ryujinx.Ava.UI.Views.Settings @@ -15,15 +11,6 @@ namespace Ryujinx.Ava.UI.Views.Settings public SettingsSystemView() { InitializeComponent(); - - FuncMultiValueConverter converter = new(parts => string.Format("{0} {1} {2}", parts.ToArray()).Trim()); - MultiBinding tzMultiBinding = new() { Converter = converter }; - - tzMultiBinding.Bindings.Add(new Binding("UtcDifference")); - tzMultiBinding.Bindings.Add(new Binding("Location")); - tzMultiBinding.Bindings.Add(new Binding("Abbreviation")); - - TimeZoneBox.ValueMemberBinding = tzMultiBinding; } private void TimeZoneBox_OnSelectionChanged(object sender, SelectionChangedEventArgs e) @@ -39,13 +26,11 @@ namespace Ryujinx.Ava.UI.Views.Settings } } - private void TimeZoneBox_OnTextChanged(object sender, EventArgs e) + private void TimeZoneBox_OnTextChanged(object sender, TextChangedEventArgs e) { if (sender is AutoCompleteBox box && box.SelectedItem is TimeZone timeZone) { - { - ViewModel.ValidateAndSetTimeZone(timeZone.Location); - } + ViewModel.ValidateAndSetTimeZone(timeZone.Location); } } } diff --git a/src/Ryujinx.Ava/UI/Views/Settings/SettingsUIView.axaml b/src/Ryujinx.Ava/UI/Views/Settings/SettingsUIView.axaml index c92d56728..b7471d385 100644 --- a/src/Ryujinx.Ava/UI/Views/Settings/SettingsUIView.axaml +++ b/src/Ryujinx.Ava/UI/Views/Settings/SettingsUIView.axaml @@ -7,7 +7,6 @@ xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale" xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels" mc:Ignorable="d" - x:CompileBindings="True" x:DataType="viewModels:SettingsViewModel"> @@ -66,7 +65,7 @@ + ItemsSource="{Binding GameDirectories}"> - diff --git a/src/Ryujinx.Ava/UI/Views/User/UserProfileImageSelectorView.axaml b/src/Ryujinx.Ava/UI/Views/User/UserProfileImageSelectorView.axaml index b9f51fdc7..65fbd4434 100644 --- a/src/Ryujinx.Ava/UI/Views/User/UserProfileImageSelectorView.axaml +++ b/src/Ryujinx.Ava/UI/Views/User/UserProfileImageSelectorView.axaml @@ -9,7 +9,6 @@ Focusable="True" mc:Ignorable="d" x:Class="Ryujinx.Ava.UI.Views.User.UserProfileImageSelectorView" - x:CompileBindings="True" x:DataType="viewModles:UserProfileImageSelectorViewModel" Width="500" d:DesignWidth="500"> diff --git a/src/Ryujinx.Ava/UI/Views/User/UserProfileImageSelectorView.axaml.cs b/src/Ryujinx.Ava/UI/Views/User/UserProfileImageSelectorView.axaml.cs index 26b77dcdc..e9bf4408c 100644 --- a/src/Ryujinx.Ava/UI/Views/User/UserProfileImageSelectorView.axaml.cs +++ b/src/Ryujinx.Ava/UI/Views/User/UserProfileImageSelectorView.axaml.cs @@ -1,5 +1,6 @@ using Avalonia.Controls; using Avalonia.Interactivity; +using Avalonia.Platform.Storage; using Avalonia.VisualTree; using FluentAvalonia.UI.Controls; using FluentAvalonia.UI.Navigation; @@ -10,6 +11,7 @@ using Ryujinx.Ava.UI.ViewModels; using Ryujinx.HLE.FileSystem; using SixLabors.ImageSharp; using SixLabors.ImageSharp.Processing; +using System.Collections.Generic; using System.IO; using Image = SixLabors.ImageSharp.Image; @@ -63,33 +65,25 @@ namespace Ryujinx.Ava.UI.Views.User private async void Import_OnClick(object sender, RoutedEventArgs e) { - OpenFileDialog dialog = new(); - dialog.Filters.Add(new FileDialogFilter + var window = this.GetVisualRoot() as Window; + var result = await window.StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions { - Name = LocaleManager.Instance[LocaleKeys.AllSupportedFormats], - Extensions = { "jpg", "jpeg", "png", "bmp" }, - }); - dialog.Filters.Add(new FileDialogFilter { Name = "JPEG", Extensions = { "jpg", "jpeg" } }); - dialog.Filters.Add(new FileDialogFilter { Name = "PNG", Extensions = { "png" } }); - dialog.Filters.Add(new FileDialogFilter { Name = "BMP", Extensions = { "bmp" } }); - - dialog.AllowMultiple = false; - - string[] image = await dialog.ShowAsync(((TopLevel)_parent.GetVisualRoot()) as Window); - - if (image != null) - { - if (image.Length > 0) + AllowMultiple = false, + FileTypeFilter = new List { - string imageFile = image[0]; - - _profile.Image = ProcessProfileImage(File.ReadAllBytes(imageFile)); - - if (_profile.Image != null) + new(LocaleManager.Instance[LocaleKeys.AllSupportedFormats]) { - _parent.GoBack(); + 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(); } } diff --git a/src/Ryujinx.Ava/UI/Views/User/UserRecovererView.axaml b/src/Ryujinx.Ava/UI/Views/User/UserRecovererView.axaml index 62b5e1840..debf4b843 100644 --- a/src/Ryujinx.Ava/UI/Views/User/UserRecovererView.axaml +++ b/src/Ryujinx.Ava/UI/Views/User/UserRecovererView.axaml @@ -12,7 +12,6 @@ xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels" x:Class="Ryujinx.Ava.UI.Views.User.UserRecovererView" - x:CompileBindings="True" x:DataType="viewModels:UserProfileViewModel" Focusable="True"> @@ -33,7 +32,7 @@ + ItemsSource="{Binding LostProfiles}"> @@ -107,8 +106,7 @@ VerticalAlignment="Stretch"> @@ -117,7 +115,7 @@ - diff --git a/src/Ryujinx.Ava/UI/Views/User/UserSelectorView.axaml b/src/Ryujinx.Ava/UI/Views/User/UserSelectorView.axaml index 9a6ba054e..818a21d69 100644 --- a/src/Ryujinx.Ava/UI/Views/User/UserSelectorView.axaml +++ b/src/Ryujinx.Ava/UI/Views/User/UserSelectorView.axaml @@ -15,7 +15,6 @@ d:DesignWidth="800" mc:Ignorable="d" Focusable="True" - x:CompileBindings="True" x:DataType="viewModels:UserProfileViewModel"> @@ -38,7 +37,7 @@ VerticalAlignment="Center" SelectionChanged="ProfilesList_SelectionChanged" Background="Transparent" - Items="{Binding Profiles}"> + ItemsSource="{Binding Profiles}"> - @@ -61,8 +60,8 @@ + PointerEntered="Grid_PointerEntered" + PointerExited="Grid_OnPointerExited"> @@ -64,14 +63,14 @@ FontWeight="Bold" Text="Ryujinx" TextAlignment="Center" - Width="100" /> + Width="110" /> + Width="110" /> - \ No newline at end of file + diff --git a/src/Ryujinx.Ava/UI/Windows/AmiiboWindow.axaml b/src/Ryujinx.Ava/UI/Windows/AmiiboWindow.axaml index 90d47b8ed..caf7c1f3f 100644 --- a/src/Ryujinx.Ava/UI/Windows/AmiiboWindow.axaml +++ b/src/Ryujinx.Ava/UI/Windows/AmiiboWindow.axaml @@ -1,4 +1,4 @@ - - + - + diff --git a/src/Ryujinx.Ava/UI/Windows/CheatWindow.axaml b/src/Ryujinx.Ava/UI/Windows/CheatWindow.axaml index 11e86211e..b9cbcb9cc 100644 --- a/src/Ryujinx.Ava/UI/Windows/CheatWindow.axaml +++ b/src/Ryujinx.Ava/UI/Windows/CheatWindow.axaml @@ -40,7 +40,7 @@ HorizontalAlignment="Center" VerticalAlignment="Center" LineHeight="18" - Text="{Binding Heading}" + Text="{ReflectionBinding Heading}" TextAlignment="Center" TextWrapping="Wrap" /> + ItemsSource="{ReflectionBinding LoadedCheats}"> @@ -56,10 +56,10 @@ Margin="10" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" - Classes.huge="{ReflectionBinding $parent[UserControl].DataContext.IsGridHuge}" - Classes.large="{ReflectionBinding $parent[UserControl].DataContext.IsGridLarge}" - Classes.normal="{ReflectionBinding $parent[UserControl].DataContext.IsGridMedium}" - Classes.small="{ReflectionBinding $parent[UserControl].DataContext.IsGridSmall}" + Classes.huge="{Binding $parent[UserControl].((viewModels:MainWindowViewModel)DataContext).IsGridHuge}" + Classes.large="{Binding $parent[UserControl].((viewModels:MainWindowViewModel)DataContext).IsGridLarge}" + Classes.normal="{Binding $parent[UserControl].((viewModels:MainWindowViewModel)DataContext).IsGridMedium}" + Classes.small="{Binding $parent[UserControl].((viewModels:MainWindowViewModel)DataContext).IsGridSmall}" ClipToBounds="True" CornerRadius="4"> @@ -78,7 +78,7 @@ Margin="0,10,0,0" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" - IsVisible="{ReflectionBinding $parent[UserControl].DataContext.ShowNames}"> + IsVisible="{Binding $parent[UserControl].((viewModels:MainWindowViewModel)DataContext).ShowNames}"> - \ No newline at end of file + diff --git a/src/Ryujinx.Ava/UI/Controls/ApplicationListView.axaml b/src/Ryujinx.Ava/UI/Controls/ApplicationListView.axaml index 75bbf9d0d..09011005b 100644 --- a/src/Ryujinx.Ava/UI/Controls/ApplicationListView.axaml +++ b/src/Ryujinx.Ava/UI/Controls/ApplicationListView.axaml @@ -42,7 +42,7 @@ @@ -67,10 +67,10 @@ Grid.RowSpan="3" Grid.Column="0" Margin="0" - Classes.huge="{ReflectionBinding $parent[UserControl].DataContext.IsGridHuge}" - Classes.large="{ReflectionBinding $parent[UserControl].DataContext.IsGridLarge}" - Classes.normal="{ReflectionBinding $parent[UserControl].DataContext.IsGridMedium}" - Classes.small="{ReflectionBinding $parent[UserControl].DataContext.IsGridSmall}" + Classes.huge="{Binding $parent[UserControl].((viewModels:MainWindowViewModel)DataContext).IsGridHuge}" + Classes.large="{Binding $parent[UserControl].((viewModels:MainWindowViewModel)DataContext).IsGridLarge}" + Classes.normal="{Binding $parent[UserControl].((viewModels:MainWindowViewModel)DataContext).IsGridMedium}" + Classes.small="{Binding $parent[UserControl].((viewModels:MainWindowViewModel)DataContext).IsGridSmall}" Source="{Binding Icon, Converter={StaticResource ByteImage}}" /> - \ No newline at end of file + diff --git a/src/Ryujinx.Ava/UI/Windows/CheatWindow.axaml b/src/Ryujinx.Ava/UI/Windows/CheatWindow.axaml index b9cbcb9cc..8a5da5cc2 100644 --- a/src/Ryujinx.Ava/UI/Windows/CheatWindow.axaml +++ b/src/Ryujinx.Ava/UI/Windows/CheatWindow.axaml @@ -11,6 +11,7 @@ Height="500" MinWidth="500" MinHeight="500" + x:DataType="window:CheatWindow" WindowStartupLocation="CenterOwner" mc:Ignorable="d" Focusable="True"> @@ -40,7 +41,7 @@ HorizontalAlignment="Center" VerticalAlignment="Center" LineHeight="18" - Text="{ReflectionBinding Heading}" + Text="{Binding Heading}" TextAlignment="Center" TextWrapping="Wrap" /> + ItemsSource="{Binding LoadedCheats}"> + + + + + + + + + + + + Enum.GetNames(typeof(AspectRatio)).Length - 1 ? AspectRatio.Fixed4x3 : aspectRatio + 1; } From 4e2bb130809c9fe0b8707fb9aac0058217330a49 Mon Sep 17 00:00:00 2001 From: TSRBerry <20988865+TSRBerry@users.noreply.github.com> Date: Mon, 9 Oct 2023 15:47:47 +0200 Subject: [PATCH 088/105] Fix games freezing after initializing LDN 1021 times (#5787) * Close handle for stateChangeEvent on Finalize * Properly dispose NetworkClient before setting it to null --- .../IUserLocalCommunicationService.cs | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/IUserLocalCommunicationService.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/IUserLocalCommunicationService.cs index 6abd2b893..29cc0e1b9 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/IUserLocalCommunicationService.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/IUserLocalCommunicationService.cs @@ -29,6 +29,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator private const bool IsDevelopment = false; private readonly KEvent _stateChangeEvent; + private int _stateChangeEventHandle; private NetworkState _state; private DisconnectReason _disconnectReason; @@ -277,12 +278,12 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator // AttachStateChangeEvent() -> handle public ResultCode AttachStateChangeEvent(ServiceCtx context) { - if (context.Process.HandleTable.GenerateHandle(_stateChangeEvent.ReadableEvent, out int stateChangeEventHandle) != Result.Success) + if (_stateChangeEventHandle == 0 && context.Process.HandleTable.GenerateHandle(_stateChangeEvent.ReadableEvent, out _stateChangeEventHandle) != Result.Success) { throw new InvalidOperationException("Out of handles!"); } - context.Response.HandleDesc = IpcHandleDesc.MakeCopy(stateChangeEventHandle); + context.Response.HandleDesc = IpcHandleDesc.MakeCopy(_stateChangeEventHandle); // Returns ResultCode.InvalidArgument if handle is null, doesn't occur in our case since we already throw an Exception. @@ -964,6 +965,12 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator SetDisconnectReason(DisconnectReason.None); } + if (_stateChangeEventHandle != 0) + { + context.Process.HandleTable.CloseHandle(_stateChangeEventHandle); + _stateChangeEventHandle = 0; + } + return resultCode; } @@ -1021,7 +1028,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator SetState(NetworkState.None); - NetworkClient?.DisconnectAndStop(); + NetworkClient?.Dispose(); NetworkClient = null; return ResultCode.Success; @@ -1072,7 +1079,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator } else { - // NOTE: Service returns differents ResultCode here related to the nifm ResultCode. + // NOTE: Service returns different ResultCode here related to the nifm ResultCode. resultCode = ResultCode.DeviceDisabled; _nifmResultCode = resultCode; } @@ -1084,14 +1091,13 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator public void Dispose() { - if (NetworkClient != null) - { - _station?.Dispose(); - _accessPoint?.Dispose(); + _station?.Dispose(); + _station = null; - NetworkClient.DisconnectAndStop(); - } + _accessPoint?.Dispose(); + _accessPoint = null; + NetworkClient?.Dispose(); NetworkClient = null; } } From e768a54f17b390c3ac10904c7909e3bef020edbd Mon Sep 17 00:00:00 2001 From: sunshineinabox Date: Thu, 12 Oct 2023 09:11:15 -0700 Subject: [PATCH 089/105] Replace ReaderWriterLock with ReaderWriterLockSlim (#5785) * Replace ReaderWriterLock with ReaderWriterLockSlim * Resolve Feedback + Correct typo * Revert some unncessary logic --- .../Translation/TranslatorCache.cs | 36 +++++----- src/Ryujinx.Common/ReactiveObject.cs | 10 +-- src/Ryujinx.Graphics.Vulkan/BufferHolder.cs | 68 ++++++++++--------- 3 files changed, 58 insertions(+), 56 deletions(-) diff --git a/src/ARMeilleure/Translation/TranslatorCache.cs b/src/ARMeilleure/Translation/TranslatorCache.cs index 11286381b..99ca58dc6 100644 --- a/src/ARMeilleure/Translation/TranslatorCache.cs +++ b/src/ARMeilleure/Translation/TranslatorCache.cs @@ -7,14 +7,14 @@ namespace ARMeilleure.Translation internal class TranslatorCache { private readonly IntervalTree _tree; - private readonly ReaderWriterLock _treeLock; + private readonly ReaderWriterLockSlim _treeLock; public int Count => _tree.Count; public TranslatorCache() { _tree = new IntervalTree(); - _treeLock = new ReaderWriterLock(); + _treeLock = new ReaderWriterLockSlim(); } public bool TryAdd(ulong address, ulong size, T value) @@ -24,70 +24,70 @@ namespace ARMeilleure.Translation public bool AddOrUpdate(ulong address, ulong size, T value, Func updateFactoryCallback) { - _treeLock.AcquireWriterLock(Timeout.Infinite); + _treeLock.EnterWriteLock(); bool result = _tree.AddOrUpdate(address, address + size, value, updateFactoryCallback); - _treeLock.ReleaseWriterLock(); + _treeLock.ExitWriteLock(); return result; } public T GetOrAdd(ulong address, ulong size, T value) { - _treeLock.AcquireWriterLock(Timeout.Infinite); + _treeLock.EnterWriteLock(); value = _tree.GetOrAdd(address, address + size, value); - _treeLock.ReleaseWriterLock(); + _treeLock.ExitWriteLock(); return value; } public bool Remove(ulong address) { - _treeLock.AcquireWriterLock(Timeout.Infinite); + _treeLock.EnterWriteLock(); bool removed = _tree.Remove(address) != 0; - _treeLock.ReleaseWriterLock(); + _treeLock.ExitWriteLock(); return removed; } public void Clear() { - _treeLock.AcquireWriterLock(Timeout.Infinite); + _treeLock.EnterWriteLock(); _tree.Clear(); - _treeLock.ReleaseWriterLock(); + _treeLock.ExitWriteLock(); } public bool ContainsKey(ulong address) { - _treeLock.AcquireReaderLock(Timeout.Infinite); + _treeLock.EnterReadLock(); bool result = _tree.ContainsKey(address); - _treeLock.ReleaseReaderLock(); + _treeLock.ExitReadLock(); return result; } public bool TryGetValue(ulong address, out T value) { - _treeLock.AcquireReaderLock(Timeout.Infinite); + _treeLock.EnterReadLock(); bool result = _tree.TryGet(address, out value); - _treeLock.ReleaseReaderLock(); + _treeLock.ExitReadLock(); return result; } public int GetOverlaps(ulong address, ulong size, ref ulong[] overlaps) { - _treeLock.AcquireReaderLock(Timeout.Infinite); + _treeLock.EnterReadLock(); int count = _tree.Get(address, address + size, ref overlaps); - _treeLock.ReleaseReaderLock(); + _treeLock.ExitReadLock(); return count; } public List AsList() { - _treeLock.AcquireReaderLock(Timeout.Infinite); + _treeLock.EnterReadLock(); List list = _tree.AsList(); - _treeLock.ReleaseReaderLock(); + _treeLock.ExitReadLock(); return list; } diff --git a/src/Ryujinx.Common/ReactiveObject.cs b/src/Ryujinx.Common/ReactiveObject.cs index d2624c365..ac7d2c4d8 100644 --- a/src/Ryujinx.Common/ReactiveObject.cs +++ b/src/Ryujinx.Common/ReactiveObject.cs @@ -5,7 +5,7 @@ namespace Ryujinx.Common { public class ReactiveObject { - private readonly ReaderWriterLock _readerWriterLock = new(); + private readonly ReaderWriterLockSlim _readerWriterLock = new(); private bool _isInitialized; private T _value; @@ -15,15 +15,15 @@ namespace Ryujinx.Common { get { - _readerWriterLock.AcquireReaderLock(Timeout.Infinite); + _readerWriterLock.EnterReadLock(); T value = _value; - _readerWriterLock.ReleaseReaderLock(); + _readerWriterLock.ExitReadLock(); return value; } set { - _readerWriterLock.AcquireWriterLock(Timeout.Infinite); + _readerWriterLock.EnterWriteLock(); T oldValue = _value; @@ -32,7 +32,7 @@ namespace Ryujinx.Common _isInitialized = true; _value = value; - _readerWriterLock.ReleaseWriterLock(); + _readerWriterLock.ExitWriteLock(); if (!oldIsInitialized || oldValue == null || !oldValue.Equals(_value)) { diff --git a/src/Ryujinx.Graphics.Vulkan/BufferHolder.cs b/src/Ryujinx.Graphics.Vulkan/BufferHolder.cs index a93ced0e5..d3a3cae11 100644 --- a/src/Ryujinx.Graphics.Vulkan/BufferHolder.cs +++ b/src/Ryujinx.Graphics.Vulkan/BufferHolder.cs @@ -58,7 +58,7 @@ namespace Ryujinx.Graphics.Vulkan private int _flushTemp; private int _lastFlushWrite = -1; - private readonly ReaderWriterLock _flushLock; + private readonly ReaderWriterLockSlim _flushLock; private FenceHolder _flushFence; private int _flushWaiting; @@ -85,7 +85,7 @@ namespace Ryujinx.Graphics.Vulkan _currentType = currentType; DesiredType = currentType; - _flushLock = new ReaderWriterLock(); + _flushLock = new ReaderWriterLockSlim(); _useMirrors = gd.IsTBDR; } @@ -106,7 +106,7 @@ namespace Ryujinx.Graphics.Vulkan _currentType = currentType; DesiredType = currentType; - _flushLock = new ReaderWriterLock(); + _flushLock = new ReaderWriterLockSlim(); } public bool TryBackingSwap(ref CommandBufferScoped? cbs) @@ -116,7 +116,7 @@ namespace Ryujinx.Graphics.Vulkan // 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.IsReaderLockHeld && (_pendingData == null || cbs != null)) + if (!isRented && _gd.CommandBufferPool.OwnedByCurrentThread && !_flushLock.IsReadLockHeld && (_pendingData == null || cbs != null)) { var currentAllocation = _allocationAuto; var currentBuffer = _buffer; @@ -131,7 +131,7 @@ namespace Ryujinx.Graphics.Vulkan ClearMirrors(cbs.Value, 0, Size); } - _flushLock.AcquireWriterLock(Timeout.Infinite); + _flushLock.EnterWriteLock(); ClearFlushFence(); @@ -185,7 +185,7 @@ namespace Ryujinx.Graphics.Vulkan _gd.PipelineInternal.SwapBuffer(currentBuffer, _buffer); - _flushLock.ReleaseWriterLock(); + _flushLock.ExitWriteLock(); } _swapQueued = false; @@ -548,42 +548,44 @@ namespace Ryujinx.Graphics.Vulkan private void WaitForFlushFence() { - // Assumes the _flushLock is held as reader, returns in same state. + if (_flushFence == null) + { + return; + } + + // If storage has changed, make sure the fence has been reached so that the data is in place. + _flushLock.ExitReadLock(); + _flushLock.EnterWriteLock(); if (_flushFence != null) { - // If storage has changed, make sure the fence has been reached so that the data is in place. + var fence = _flushFence; + Interlocked.Increment(ref _flushWaiting); - var cookie = _flushLock.UpgradeToWriterLock(Timeout.Infinite); + // Don't wait in the lock. - if (_flushFence != null) + _flushLock.ExitWriteLock(); + + fence.Wait(); + + _flushLock.EnterWriteLock(); + + if (Interlocked.Decrement(ref _flushWaiting) == 0) { - var fence = _flushFence; - Interlocked.Increment(ref _flushWaiting); - - // Don't wait in the lock. - - var restoreCookie = _flushLock.ReleaseLock(); - - fence.Wait(); - - _flushLock.RestoreLock(ref restoreCookie); - - if (Interlocked.Decrement(ref _flushWaiting) == 0) - { - fence.Put(); - } - - _flushFence = null; + fence.Put(); } - _flushLock.DowngradeFromWriterLock(ref cookie); + _flushFence = null; } + + // Assumes the _flushLock is held as reader, returns in same state. + _flushLock.ExitWriteLock(); + _flushLock.EnterReadLock(); } public PinnedSpan GetData(int offset, int size) { - _flushLock.AcquireReaderLock(Timeout.Infinite); + _flushLock.EnterReadLock(); WaitForFlushFence(); @@ -603,7 +605,7 @@ namespace Ryujinx.Graphics.Vulkan // Need to be careful here, the buffer can't be unmapped while the data is being used. _buffer.IncrementReferenceCount(); - _flushLock.ReleaseReaderLock(); + _flushLock.ExitReadLock(); return PinnedSpan.UnsafeFromSpan(result, _buffer.DecrementReferenceCount); } @@ -621,7 +623,7 @@ namespace Ryujinx.Graphics.Vulkan result = resource.GetFlushBuffer().GetBufferData(resource.GetPool(), this, offset, size); } - _flushLock.ReleaseReaderLock(); + _flushLock.ExitReadLock(); // Flush buffer is pinned until the next GetBufferData on the thread, which is fine for current uses. return PinnedSpan.UnsafeFromSpan(result); @@ -1073,11 +1075,11 @@ namespace Ryujinx.Graphics.Vulkan _allocationAuto.Dispose(); } - _flushLock.AcquireWriterLock(Timeout.Infinite); + _flushLock.EnterWriteLock(); ClearFlushFence(); - _flushLock.ReleaseWriterLock(); + _flushLock.ExitWriteLock(); } } } From 1e06b28b22848706014b18bffcec7553cdab2b2b Mon Sep 17 00:00:00 2001 From: Ac_K Date: Sat, 14 Oct 2023 04:13:15 +0200 Subject: [PATCH 090/105] Horizon: Migrate usb and psc services (#5800) * Horizon: Migrate Usb and Psc services * Fix formatting * Adresses feedback --- .../HOS/Services/Ins/IReceiverManager.cs | 8 --- .../HOS/Services/Ins/ISenderManager.cs | 8 --- .../HOS/Services/Ovln/IReceiverService.cs | 8 --- .../HOS/Services/Ovln/ISenderService.cs | 8 --- .../HOS/Services/Psc/IPmControl.cs | 8 --- .../HOS/Services/Psc/IPmService.cs | 8 --- .../HOS/Services/Psc/IPmUnknown.cs | 8 --- .../HOS/Services/Srepo/ISrepoService.cs | 9 --- .../HOS/Services/Usb/IClientRootSession.cs | 9 --- .../HOS/Services/Usb/IDsService.cs | 8 --- .../HOS/Services/Usb/IPdCradleManager.cs | 8 --- .../HOS/Services/Usb/IPdManager.cs | 8 --- .../HOS/Services/Usb/IPmService.cs | 8 --- src/Ryujinx.HLE/HOS/Services/Usb/IUnknown1.cs | 8 --- src/Ryujinx.HLE/HOS/Services/Usb/IUnknown2.cs | 8 --- .../Hipc/HipcGenerator.cs | 2 +- src/Ryujinx.Horizon/Hshl/HshlIpcServer.cs | 47 ++++++++++++ src/Ryujinx.Horizon/Hshl/HshlMain.cs | 17 +++++ src/Ryujinx.Horizon/Hshl/Ipc/Manager.cs | 8 +++ src/Ryujinx.Horizon/Hshl/Ipc/SetterManager.cs | 8 +++ src/Ryujinx.Horizon/Ins/InsIpcServer.cs | 47 ++++++++++++ src/Ryujinx.Horizon/Ins/InsMain.cs | 17 +++++ .../Ins/Ipc/ReceiverManager.cs | 8 +++ src/Ryujinx.Horizon/Ins/Ipc/SenderManager.cs | 8 +++ .../Ovln/Ipc/ReceiverService.cs | 8 +++ src/Ryujinx.Horizon/Ovln/Ipc/SenderService.cs | 8 +++ src/Ryujinx.Horizon/Ovln/OvlnIpcServer.cs | 48 +++++++++++++ src/Ryujinx.Horizon/Ovln/OvlnMain.cs | 17 +++++ src/Ryujinx.Horizon/Psc/Ipc/PmControl.cs | 8 +++ src/Ryujinx.Horizon/Psc/Ipc/PmService.cs | 8 +++ src/Ryujinx.Horizon/Psc/Ipc/PmStateLock.cs | 8 +++ src/Ryujinx.Horizon/Psc/PscIpcServer.cs | 50 +++++++++++++ src/Ryujinx.Horizon/Psc/PscMain.cs | 17 +++++ src/Ryujinx.Horizon/Sdk/Hshl/IManager.cs | 8 +++ .../Sdk/Hshl/ISetterManager.cs | 8 +++ .../Sdk/Ins/IReceiverManager.cs | 8 +++ src/Ryujinx.Horizon/Sdk/Ins/ISenderManager.cs | 8 +++ .../Sdk/Ovln/IReceiverService.cs | 8 +++ .../Sdk/Ovln/ISenderService.cs | 8 +++ src/Ryujinx.Horizon/Sdk/Psc/IPmControl.cs | 8 +++ src/Ryujinx.Horizon/Sdk/Psc/IPmService.cs | 8 +++ src/Ryujinx.Horizon/Sdk/Psc/IPmStateLock.cs | 8 +++ .../Sdk/Srepo/ISrepoService.cs | 8 +++ .../Sdk/Usb/IClientRootSession.cs | 8 +++ src/Ryujinx.Horizon/Sdk/Usb/IDsRootSession.cs | 8 +++ .../Sdk/Usb/IPdCradleManager.cs | 8 +++ src/Ryujinx.Horizon/Sdk/Usb/IPdManager.cs | 8 +++ .../Sdk/Usb/IPdManufactureManager.cs | 8 +++ .../Sdk/Usb/IPmObserverService.cs | 8 +++ src/Ryujinx.Horizon/Sdk/Usb/IPmService.cs | 8 +++ src/Ryujinx.Horizon/Sdk/Usb/IQdbManager.cs | 8 +++ src/Ryujinx.Horizon/ServiceTable.cs | 16 ++++- src/Ryujinx.Horizon/Srepo/Ipc/SrepoService.cs | 8 +++ src/Ryujinx.Horizon/Srepo/SrepoIpcServer.cs | 46 ++++++++++++ src/Ryujinx.Horizon/Srepo/SrepoMain.cs | 17 +++++ .../Usb/Ipc/ClientRootSession.cs | 8 +++ src/Ryujinx.Horizon/Usb/Ipc/DsRootSession.cs | 8 +++ .../Usb/Ipc/PdCradleManager.cs | 8 +++ src/Ryujinx.Horizon/Usb/Ipc/PdManager.cs | 9 +++ .../Usb/Ipc/PdManufactureManager.cs | 8 +++ .../Usb/Ipc/PmObserverService.cs | 8 +++ src/Ryujinx.Horizon/Usb/Ipc/PmService.cs | 8 +++ src/Ryujinx.Horizon/Usb/Ipc/QdbManager.cs | 8 +++ src/Ryujinx.Horizon/Usb/UsbIpcServer.cs | 71 +++++++++++++++++++ src/Ryujinx.Horizon/Usb/UsbMain.cs | 17 +++++ 65 files changed, 715 insertions(+), 125 deletions(-) delete mode 100644 src/Ryujinx.HLE/HOS/Services/Ins/IReceiverManager.cs delete mode 100644 src/Ryujinx.HLE/HOS/Services/Ins/ISenderManager.cs delete mode 100644 src/Ryujinx.HLE/HOS/Services/Ovln/IReceiverService.cs delete mode 100644 src/Ryujinx.HLE/HOS/Services/Ovln/ISenderService.cs delete mode 100644 src/Ryujinx.HLE/HOS/Services/Psc/IPmControl.cs delete mode 100644 src/Ryujinx.HLE/HOS/Services/Psc/IPmService.cs delete mode 100644 src/Ryujinx.HLE/HOS/Services/Psc/IPmUnknown.cs delete mode 100644 src/Ryujinx.HLE/HOS/Services/Srepo/ISrepoService.cs delete mode 100644 src/Ryujinx.HLE/HOS/Services/Usb/IClientRootSession.cs delete mode 100644 src/Ryujinx.HLE/HOS/Services/Usb/IDsService.cs delete mode 100644 src/Ryujinx.HLE/HOS/Services/Usb/IPdCradleManager.cs delete mode 100644 src/Ryujinx.HLE/HOS/Services/Usb/IPdManager.cs delete mode 100644 src/Ryujinx.HLE/HOS/Services/Usb/IPmService.cs delete mode 100644 src/Ryujinx.HLE/HOS/Services/Usb/IUnknown1.cs delete mode 100644 src/Ryujinx.HLE/HOS/Services/Usb/IUnknown2.cs create mode 100644 src/Ryujinx.Horizon/Hshl/HshlIpcServer.cs create mode 100644 src/Ryujinx.Horizon/Hshl/HshlMain.cs create mode 100644 src/Ryujinx.Horizon/Hshl/Ipc/Manager.cs create mode 100644 src/Ryujinx.Horizon/Hshl/Ipc/SetterManager.cs create mode 100644 src/Ryujinx.Horizon/Ins/InsIpcServer.cs create mode 100644 src/Ryujinx.Horizon/Ins/InsMain.cs create mode 100644 src/Ryujinx.Horizon/Ins/Ipc/ReceiverManager.cs create mode 100644 src/Ryujinx.Horizon/Ins/Ipc/SenderManager.cs create mode 100644 src/Ryujinx.Horizon/Ovln/Ipc/ReceiverService.cs create mode 100644 src/Ryujinx.Horizon/Ovln/Ipc/SenderService.cs create mode 100644 src/Ryujinx.Horizon/Ovln/OvlnIpcServer.cs create mode 100644 src/Ryujinx.Horizon/Ovln/OvlnMain.cs create mode 100644 src/Ryujinx.Horizon/Psc/Ipc/PmControl.cs create mode 100644 src/Ryujinx.Horizon/Psc/Ipc/PmService.cs create mode 100644 src/Ryujinx.Horizon/Psc/Ipc/PmStateLock.cs create mode 100644 src/Ryujinx.Horizon/Psc/PscIpcServer.cs create mode 100644 src/Ryujinx.Horizon/Psc/PscMain.cs create mode 100644 src/Ryujinx.Horizon/Sdk/Hshl/IManager.cs create mode 100644 src/Ryujinx.Horizon/Sdk/Hshl/ISetterManager.cs create mode 100644 src/Ryujinx.Horizon/Sdk/Ins/IReceiverManager.cs create mode 100644 src/Ryujinx.Horizon/Sdk/Ins/ISenderManager.cs create mode 100644 src/Ryujinx.Horizon/Sdk/Ovln/IReceiverService.cs create mode 100644 src/Ryujinx.Horizon/Sdk/Ovln/ISenderService.cs create mode 100644 src/Ryujinx.Horizon/Sdk/Psc/IPmControl.cs create mode 100644 src/Ryujinx.Horizon/Sdk/Psc/IPmService.cs create mode 100644 src/Ryujinx.Horizon/Sdk/Psc/IPmStateLock.cs create mode 100644 src/Ryujinx.Horizon/Sdk/Srepo/ISrepoService.cs create mode 100644 src/Ryujinx.Horizon/Sdk/Usb/IClientRootSession.cs create mode 100644 src/Ryujinx.Horizon/Sdk/Usb/IDsRootSession.cs create mode 100644 src/Ryujinx.Horizon/Sdk/Usb/IPdCradleManager.cs create mode 100644 src/Ryujinx.Horizon/Sdk/Usb/IPdManager.cs create mode 100644 src/Ryujinx.Horizon/Sdk/Usb/IPdManufactureManager.cs create mode 100644 src/Ryujinx.Horizon/Sdk/Usb/IPmObserverService.cs create mode 100644 src/Ryujinx.Horizon/Sdk/Usb/IPmService.cs create mode 100644 src/Ryujinx.Horizon/Sdk/Usb/IQdbManager.cs create mode 100644 src/Ryujinx.Horizon/Srepo/Ipc/SrepoService.cs create mode 100644 src/Ryujinx.Horizon/Srepo/SrepoIpcServer.cs create mode 100644 src/Ryujinx.Horizon/Srepo/SrepoMain.cs create mode 100644 src/Ryujinx.Horizon/Usb/Ipc/ClientRootSession.cs create mode 100644 src/Ryujinx.Horizon/Usb/Ipc/DsRootSession.cs create mode 100644 src/Ryujinx.Horizon/Usb/Ipc/PdCradleManager.cs create mode 100644 src/Ryujinx.Horizon/Usb/Ipc/PdManager.cs create mode 100644 src/Ryujinx.Horizon/Usb/Ipc/PdManufactureManager.cs create mode 100644 src/Ryujinx.Horizon/Usb/Ipc/PmObserverService.cs create mode 100644 src/Ryujinx.Horizon/Usb/Ipc/PmService.cs create mode 100644 src/Ryujinx.Horizon/Usb/Ipc/QdbManager.cs create mode 100644 src/Ryujinx.Horizon/Usb/UsbIpcServer.cs create mode 100644 src/Ryujinx.Horizon/Usb/UsbMain.cs diff --git a/src/Ryujinx.HLE/HOS/Services/Ins/IReceiverManager.cs b/src/Ryujinx.HLE/HOS/Services/Ins/IReceiverManager.cs deleted file mode 100644 index 8ee00d0e9..000000000 --- a/src/Ryujinx.HLE/HOS/Services/Ins/IReceiverManager.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Ryujinx.HLE.HOS.Services.Ins -{ - [Service("ins:r")] - class IReceiverManager : IpcService - { - public IReceiverManager(ServiceCtx context) { } - } -} diff --git a/src/Ryujinx.HLE/HOS/Services/Ins/ISenderManager.cs b/src/Ryujinx.HLE/HOS/Services/Ins/ISenderManager.cs deleted file mode 100644 index 239c4cc83..000000000 --- a/src/Ryujinx.HLE/HOS/Services/Ins/ISenderManager.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Ryujinx.HLE.HOS.Services.Ins -{ - [Service("ins:s")] - class ISenderManager : IpcService - { - public ISenderManager(ServiceCtx context) { } - } -} diff --git a/src/Ryujinx.HLE/HOS/Services/Ovln/IReceiverService.cs b/src/Ryujinx.HLE/HOS/Services/Ovln/IReceiverService.cs deleted file mode 100644 index 99e929a70..000000000 --- a/src/Ryujinx.HLE/HOS/Services/Ovln/IReceiverService.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Ryujinx.HLE.HOS.Services.Ovln -{ - [Service("ovln:rcv")] - class IReceiverService : IpcService - { - public IReceiverService(ServiceCtx context) { } - } -} diff --git a/src/Ryujinx.HLE/HOS/Services/Ovln/ISenderService.cs b/src/Ryujinx.HLE/HOS/Services/Ovln/ISenderService.cs deleted file mode 100644 index e445c16cd..000000000 --- a/src/Ryujinx.HLE/HOS/Services/Ovln/ISenderService.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Ryujinx.HLE.HOS.Services.Ovln -{ - [Service("ovln:snd")] - class ISenderService : IpcService - { - public ISenderService(ServiceCtx context) { } - } -} diff --git a/src/Ryujinx.HLE/HOS/Services/Psc/IPmControl.cs b/src/Ryujinx.HLE/HOS/Services/Psc/IPmControl.cs deleted file mode 100644 index 6682a8481..000000000 --- a/src/Ryujinx.HLE/HOS/Services/Psc/IPmControl.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Ryujinx.HLE.HOS.Services.Psc -{ - [Service("psc:c")] - class IPmControl : IpcService - { - public IPmControl(ServiceCtx context) { } - } -} diff --git a/src/Ryujinx.HLE/HOS/Services/Psc/IPmService.cs b/src/Ryujinx.HLE/HOS/Services/Psc/IPmService.cs deleted file mode 100644 index 1be338660..000000000 --- a/src/Ryujinx.HLE/HOS/Services/Psc/IPmService.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Ryujinx.HLE.HOS.Services.Psc -{ - [Service("psc:m")] - class IPmService : IpcService - { - public IPmService(ServiceCtx context) { } - } -} diff --git a/src/Ryujinx.HLE/HOS/Services/Psc/IPmUnknown.cs b/src/Ryujinx.HLE/HOS/Services/Psc/IPmUnknown.cs deleted file mode 100644 index 95aff9ece..000000000 --- a/src/Ryujinx.HLE/HOS/Services/Psc/IPmUnknown.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Ryujinx.HLE.HOS.Services.Psc -{ - [Service("psc:l")] // 9.0.0+ - class IPmUnknown : IpcService - { - public IPmUnknown(ServiceCtx context) { } - } -} diff --git a/src/Ryujinx.HLE/HOS/Services/Srepo/ISrepoService.cs b/src/Ryujinx.HLE/HOS/Services/Srepo/ISrepoService.cs deleted file mode 100644 index f5467983a..000000000 --- a/src/Ryujinx.HLE/HOS/Services/Srepo/ISrepoService.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Ryujinx.HLE.HOS.Services.Srepo -{ - [Service("srepo:a")] // 5.0.0+ - [Service("srepo:u")] // 5.0.0+ - class ISrepoService : IpcService - { - public ISrepoService(ServiceCtx context) { } - } -} diff --git a/src/Ryujinx.HLE/HOS/Services/Usb/IClientRootSession.cs b/src/Ryujinx.HLE/HOS/Services/Usb/IClientRootSession.cs deleted file mode 100644 index b41b8a48c..000000000 --- a/src/Ryujinx.HLE/HOS/Services/Usb/IClientRootSession.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Ryujinx.HLE.HOS.Services.Usb -{ - [Service("usb:hs")] - [Service("usb:hs:a")] // 7.0.0+ - class IClientRootSession : IpcService - { - public IClientRootSession(ServiceCtx context) { } - } -} diff --git a/src/Ryujinx.HLE/HOS/Services/Usb/IDsService.cs b/src/Ryujinx.HLE/HOS/Services/Usb/IDsService.cs deleted file mode 100644 index ee6c8f070..000000000 --- a/src/Ryujinx.HLE/HOS/Services/Usb/IDsService.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Ryujinx.HLE.HOS.Services.Usb -{ - [Service("usb:ds")] - class IDsService : IpcService - { - public IDsService(ServiceCtx context) { } - } -} diff --git a/src/Ryujinx.HLE/HOS/Services/Usb/IPdCradleManager.cs b/src/Ryujinx.HLE/HOS/Services/Usb/IPdCradleManager.cs deleted file mode 100644 index 18cbce79a..000000000 --- a/src/Ryujinx.HLE/HOS/Services/Usb/IPdCradleManager.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Ryujinx.HLE.HOS.Services.Usb -{ - [Service("usb:pd:c")] - class IPdCradleManager : IpcService - { - public IPdCradleManager(ServiceCtx context) { } - } -} diff --git a/src/Ryujinx.HLE/HOS/Services/Usb/IPdManager.cs b/src/Ryujinx.HLE/HOS/Services/Usb/IPdManager.cs deleted file mode 100644 index 011debafd..000000000 --- a/src/Ryujinx.HLE/HOS/Services/Usb/IPdManager.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Ryujinx.HLE.HOS.Services.Usb -{ - [Service("usb:pd")] - class IPdManager : IpcService - { - public IPdManager(ServiceCtx context) { } - } -} diff --git a/src/Ryujinx.HLE/HOS/Services/Usb/IPmService.cs b/src/Ryujinx.HLE/HOS/Services/Usb/IPmService.cs deleted file mode 100644 index ed6bba694..000000000 --- a/src/Ryujinx.HLE/HOS/Services/Usb/IPmService.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Ryujinx.HLE.HOS.Services.Usb -{ - [Service("usb:pm")] - class IPmService : IpcService - { - public IPmService(ServiceCtx context) { } - } -} diff --git a/src/Ryujinx.HLE/HOS/Services/Usb/IUnknown1.cs b/src/Ryujinx.HLE/HOS/Services/Usb/IUnknown1.cs deleted file mode 100644 index 65bf1c9fa..000000000 --- a/src/Ryujinx.HLE/HOS/Services/Usb/IUnknown1.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Ryujinx.HLE.HOS.Services.Usb -{ - [Service("usb:qdb")] // 7.0.0+ - class IUnknown1 : IpcService - { - public IUnknown1(ServiceCtx context) { } - } -} diff --git a/src/Ryujinx.HLE/HOS/Services/Usb/IUnknown2.cs b/src/Ryujinx.HLE/HOS/Services/Usb/IUnknown2.cs deleted file mode 100644 index e0bf0bf4a..000000000 --- a/src/Ryujinx.HLE/HOS/Services/Usb/IUnknown2.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Ryujinx.HLE.HOS.Services.Usb -{ - [Service("usb:obsv")] // 8.0.0+ - class IUnknown2 : IpcService - { - public IUnknown2(ServiceCtx context) { } - } -} diff --git a/src/Ryujinx.Horizon.Generators/Hipc/HipcGenerator.cs b/src/Ryujinx.Horizon.Generators/Hipc/HipcGenerator.cs index 1b92e9180..4e14f47e9 100644 --- a/src/Ryujinx.Horizon.Generators/Hipc/HipcGenerator.cs +++ b/src/Ryujinx.Horizon.Generators/Hipc/HipcGenerator.cs @@ -93,7 +93,7 @@ namespace Ryujinx.Horizon.Generators.Hipc generator.LeaveScope(); generator.LeaveScope(); - context.AddSource($"{className}.g.cs", generator.ToString()); + context.AddSource($"{GetNamespaceName(commandInterface.ClassDeclarationSyntax)}.{className}.g.cs", generator.ToString()); } } diff --git a/src/Ryujinx.Horizon/Hshl/HshlIpcServer.cs b/src/Ryujinx.Horizon/Hshl/HshlIpcServer.cs new file mode 100644 index 000000000..7182725cb --- /dev/null +++ b/src/Ryujinx.Horizon/Hshl/HshlIpcServer.cs @@ -0,0 +1,47 @@ +using Ryujinx.Horizon.Hshl.Ipc; +using Ryujinx.Horizon.Sdk.Sf.Hipc; +using Ryujinx.Horizon.Sdk.Sm; + +namespace Ryujinx.Horizon.Hshl +{ + class HshlIpcServer + { + private const int HshlMaxSessionsCount = 10; + private const int TotalMaxSessionsCount = HshlMaxSessionsCount * 2; + + private const int PointerBufferSize = 0; + private const int MaxDomains = 0; + private const int MaxDomainObjects = 0; + private const int MaxPortsCount = 2; + + 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, TotalMaxSessionsCount); + +#pragma warning disable IDE0055 // Disable formatting + _serverManager.RegisterObjectForServer(new SetterManager(), ServiceName.Encode("hshl:set"), HshlMaxSessionsCount); // 11.0.0+ + _serverManager.RegisterObjectForServer(new Manager(), ServiceName.Encode("hshl:sys"), HshlMaxSessionsCount); // 11.0.0+ +#pragma warning restore IDE0055 + } + + public void ServiceRequests() + { + _serverManager.ServiceRequests(); + } + + public void Shutdown() + { + _serverManager.Dispose(); + } + } +} diff --git a/src/Ryujinx.Horizon/Hshl/HshlMain.cs b/src/Ryujinx.Horizon/Hshl/HshlMain.cs new file mode 100644 index 000000000..4e894b6f6 --- /dev/null +++ b/src/Ryujinx.Horizon/Hshl/HshlMain.cs @@ -0,0 +1,17 @@ +namespace Ryujinx.Horizon.Hshl +{ + class HshlMain : IService + { + public static void Main(ServiceTable serviceTable) + { + HshlIpcServer ipcServer = new(); + + ipcServer.Initialize(); + + serviceTable.SignalServiceReady(); + + ipcServer.ServiceRequests(); + ipcServer.Shutdown(); + } + } +} diff --git a/src/Ryujinx.Horizon/Hshl/Ipc/Manager.cs b/src/Ryujinx.Horizon/Hshl/Ipc/Manager.cs new file mode 100644 index 000000000..29d9069ac --- /dev/null +++ b/src/Ryujinx.Horizon/Hshl/Ipc/Manager.cs @@ -0,0 +1,8 @@ +using Ryujinx.Horizon.Sdk.Hshl; + +namespace Ryujinx.Horizon.Hshl.Ipc +{ + partial class Manager : IManager + { + } +} diff --git a/src/Ryujinx.Horizon/Hshl/Ipc/SetterManager.cs b/src/Ryujinx.Horizon/Hshl/Ipc/SetterManager.cs new file mode 100644 index 000000000..ac1006f0f --- /dev/null +++ b/src/Ryujinx.Horizon/Hshl/Ipc/SetterManager.cs @@ -0,0 +1,8 @@ +using Ryujinx.Horizon.Sdk.Hshl; + +namespace Ryujinx.Horizon.Hshl.Ipc +{ + partial class SetterManager : ISetterManager + { + } +} diff --git a/src/Ryujinx.Horizon/Ins/InsIpcServer.cs b/src/Ryujinx.Horizon/Ins/InsIpcServer.cs new file mode 100644 index 000000000..68698bf6a --- /dev/null +++ b/src/Ryujinx.Horizon/Ins/InsIpcServer.cs @@ -0,0 +1,47 @@ +using Ryujinx.Horizon.Ins.Ipc; +using Ryujinx.Horizon.Sdk.Sf.Hipc; +using Ryujinx.Horizon.Sdk.Sm; + +namespace Ryujinx.Horizon.Ins +{ + class InsIpcServer + { + private const int InsMaxSessionsCount = 8; + private const int TotalMaxSessionsCount = InsMaxSessionsCount * 2; + + private const int PointerBufferSize = 0x200; + private const int MaxDomains = 0; + private const int MaxDomainObjects = 0; + private const int MaxPortsCount = 2; + + 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, TotalMaxSessionsCount); + +#pragma warning disable IDE0055 // Disable formatting + _serverManager.RegisterObjectForServer(new ReceiverManager(), ServiceName.Encode("ins:r"), InsMaxSessionsCount); // 9.0.0+ + _serverManager.RegisterObjectForServer(new SenderManager(), ServiceName.Encode("ins:s"), InsMaxSessionsCount); // 9.0.0+ +#pragma warning restore IDE0055 + } + + public void ServiceRequests() + { + _serverManager.ServiceRequests(); + } + + public void Shutdown() + { + _serverManager.Dispose(); + } + } +} diff --git a/src/Ryujinx.Horizon/Ins/InsMain.cs b/src/Ryujinx.Horizon/Ins/InsMain.cs new file mode 100644 index 000000000..e428d090a --- /dev/null +++ b/src/Ryujinx.Horizon/Ins/InsMain.cs @@ -0,0 +1,17 @@ +namespace Ryujinx.Horizon.Ins +{ + class InsMain : IService + { + public static void Main(ServiceTable serviceTable) + { + InsIpcServer ipcServer = new(); + + ipcServer.Initialize(); + + serviceTable.SignalServiceReady(); + + ipcServer.ServiceRequests(); + ipcServer.Shutdown(); + } + } +} diff --git a/src/Ryujinx.Horizon/Ins/Ipc/ReceiverManager.cs b/src/Ryujinx.Horizon/Ins/Ipc/ReceiverManager.cs new file mode 100644 index 000000000..6e9b29a99 --- /dev/null +++ b/src/Ryujinx.Horizon/Ins/Ipc/ReceiverManager.cs @@ -0,0 +1,8 @@ +using Ryujinx.Horizon.Sdk.Ins; + +namespace Ryujinx.Horizon.Ins.Ipc +{ + partial class ReceiverManager : IReceiverManager + { + } +} diff --git a/src/Ryujinx.Horizon/Ins/Ipc/SenderManager.cs b/src/Ryujinx.Horizon/Ins/Ipc/SenderManager.cs new file mode 100644 index 000000000..e133014e1 --- /dev/null +++ b/src/Ryujinx.Horizon/Ins/Ipc/SenderManager.cs @@ -0,0 +1,8 @@ +using Ryujinx.Horizon.Sdk.Ins; + +namespace Ryujinx.Horizon.Ins.Ipc +{ + partial class SenderManager : ISenderManager + { + } +} diff --git a/src/Ryujinx.Horizon/Ovln/Ipc/ReceiverService.cs b/src/Ryujinx.Horizon/Ovln/Ipc/ReceiverService.cs new file mode 100644 index 000000000..6cc448e8a --- /dev/null +++ b/src/Ryujinx.Horizon/Ovln/Ipc/ReceiverService.cs @@ -0,0 +1,8 @@ +using Ryujinx.Horizon.Sdk.Ovln; + +namespace Ryujinx.Horizon.Ovln.Ipc +{ + partial class ReceiverService : IReceiverService + { + } +} diff --git a/src/Ryujinx.Horizon/Ovln/Ipc/SenderService.cs b/src/Ryujinx.Horizon/Ovln/Ipc/SenderService.cs new file mode 100644 index 000000000..cab123ecf --- /dev/null +++ b/src/Ryujinx.Horizon/Ovln/Ipc/SenderService.cs @@ -0,0 +1,8 @@ +using Ryujinx.Horizon.Sdk.Ovln; + +namespace Ryujinx.Horizon.Ovln.Ipc +{ + partial class SenderService : ISenderService + { + } +} diff --git a/src/Ryujinx.Horizon/Ovln/OvlnIpcServer.cs b/src/Ryujinx.Horizon/Ovln/OvlnIpcServer.cs new file mode 100644 index 000000000..2c00107fb --- /dev/null +++ b/src/Ryujinx.Horizon/Ovln/OvlnIpcServer.cs @@ -0,0 +1,48 @@ +using Ryujinx.Horizon.Ovln.Ipc; +using Ryujinx.Horizon.Sdk.Sf.Hipc; +using Ryujinx.Horizon.Sdk.Sm; + +namespace Ryujinx.Horizon.Ovln +{ + class OvlnIpcServer + { + private const int OvlnRcvMaxSessionsCount = 2; + private const int OvlnSndMaxSessionsCount = 20; + private const int TotalMaxSessionsCount = OvlnRcvMaxSessionsCount + OvlnSndMaxSessionsCount; + + private const int PointerBufferSize = 0; + private const int MaxDomains = 21; + private const int MaxDomainObjects = 60; + private const int MaxPortsCount = 2; + + 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, TotalMaxSessionsCount); + +#pragma warning disable IDE0055 // Disable formatting + _serverManager.RegisterObjectForServer(new ReceiverService(), ServiceName.Encode("ovln:rcv"), OvlnRcvMaxSessionsCount); // 8.0.0+ + _serverManager.RegisterObjectForServer(new SenderService(), ServiceName.Encode("ovln:snd"), OvlnSndMaxSessionsCount); // 8.0.0+ +#pragma warning restore IDE0055 + } + + public void ServiceRequests() + { + _serverManager.ServiceRequests(); + } + + public void Shutdown() + { + _serverManager.Dispose(); + } + } +} diff --git a/src/Ryujinx.Horizon/Ovln/OvlnMain.cs b/src/Ryujinx.Horizon/Ovln/OvlnMain.cs new file mode 100644 index 000000000..8c6cf84e7 --- /dev/null +++ b/src/Ryujinx.Horizon/Ovln/OvlnMain.cs @@ -0,0 +1,17 @@ +namespace Ryujinx.Horizon.Ovln +{ + class OvlnMain : IService + { + public static void Main(ServiceTable serviceTable) + { + OvlnIpcServer ipcServer = new(); + + ipcServer.Initialize(); + + serviceTable.SignalServiceReady(); + + ipcServer.ServiceRequests(); + ipcServer.Shutdown(); + } + } +} diff --git a/src/Ryujinx.Horizon/Psc/Ipc/PmControl.cs b/src/Ryujinx.Horizon/Psc/Ipc/PmControl.cs new file mode 100644 index 000000000..671472e4e --- /dev/null +++ b/src/Ryujinx.Horizon/Psc/Ipc/PmControl.cs @@ -0,0 +1,8 @@ +using Ryujinx.Horizon.Sdk.Psc; + +namespace Ryujinx.Horizon.Psc.Ipc +{ + partial class PmControl : IPmControl + { + } +} diff --git a/src/Ryujinx.Horizon/Psc/Ipc/PmService.cs b/src/Ryujinx.Horizon/Psc/Ipc/PmService.cs new file mode 100644 index 000000000..c38da8581 --- /dev/null +++ b/src/Ryujinx.Horizon/Psc/Ipc/PmService.cs @@ -0,0 +1,8 @@ +using Ryujinx.Horizon.Sdk.Psc; + +namespace Ryujinx.Horizon.Psc.Ipc +{ + partial class PmService : IPmService + { + } +} diff --git a/src/Ryujinx.Horizon/Psc/Ipc/PmStateLock.cs b/src/Ryujinx.Horizon/Psc/Ipc/PmStateLock.cs new file mode 100644 index 000000000..cef68ac54 --- /dev/null +++ b/src/Ryujinx.Horizon/Psc/Ipc/PmStateLock.cs @@ -0,0 +1,8 @@ +using Ryujinx.Horizon.Sdk.Psc; + +namespace Ryujinx.Horizon.Psc.Ipc +{ + partial class PmStateLock : IPmStateLock + { + } +} diff --git a/src/Ryujinx.Horizon/Psc/PscIpcServer.cs b/src/Ryujinx.Horizon/Psc/PscIpcServer.cs new file mode 100644 index 000000000..f8da56724 --- /dev/null +++ b/src/Ryujinx.Horizon/Psc/PscIpcServer.cs @@ -0,0 +1,50 @@ +using Ryujinx.Horizon.Psc.Ipc; +using Ryujinx.Horizon.Sdk.Sf.Hipc; +using Ryujinx.Horizon.Sdk.Sm; + +namespace Ryujinx.Horizon.Psc +{ + class PscIpcServer + { + private const int PscCMaxSessionsCount = 1; + private const int PscMMaxSessionsCount = 50; + private const int PscLMaxSessionsCount = 5; + private const int TotalMaxSessionsCount = PscCMaxSessionsCount + PscMMaxSessionsCount + PscLMaxSessionsCount; + + private const int PointerBufferSize = 0; + private const int MaxDomains = 0; + private const int MaxDomainObjects = 0; + private const int MaxPortsCount = 3; + + 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, TotalMaxSessionsCount); + +#pragma warning disable IDE0055 // Disable formatting + _serverManager.RegisterObjectForServer(new PmControl(), ServiceName.Encode("psc:c"), PscCMaxSessionsCount); + _serverManager.RegisterObjectForServer(new PmService(), ServiceName.Encode("psc:m"), PscMMaxSessionsCount); + _serverManager.RegisterObjectForServer(new PmStateLock(), ServiceName.Encode("psc:l"), PscLMaxSessionsCount); // 9.0.0+ +#pragma warning restore IDE0055 + } + + public void ServiceRequests() + { + _serverManager.ServiceRequests(); + } + + public void Shutdown() + { + _serverManager.Dispose(); + } + } +} diff --git a/src/Ryujinx.Horizon/Psc/PscMain.cs b/src/Ryujinx.Horizon/Psc/PscMain.cs new file mode 100644 index 000000000..facb6bc08 --- /dev/null +++ b/src/Ryujinx.Horizon/Psc/PscMain.cs @@ -0,0 +1,17 @@ +namespace Ryujinx.Horizon.Psc +{ + class PscMain : IService + { + public static void Main(ServiceTable serviceTable) + { + PscIpcServer ipcServer = new(); + + ipcServer.Initialize(); + + serviceTable.SignalServiceReady(); + + ipcServer.ServiceRequests(); + ipcServer.Shutdown(); + } + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Hshl/IManager.cs b/src/Ryujinx.Horizon/Sdk/Hshl/IManager.cs new file mode 100644 index 000000000..13955c692 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Hshl/IManager.cs @@ -0,0 +1,8 @@ +using Ryujinx.Horizon.Sdk.Sf; + +namespace Ryujinx.Horizon.Sdk.Hshl +{ + interface IManager : IServiceObject + { + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Hshl/ISetterManager.cs b/src/Ryujinx.Horizon/Sdk/Hshl/ISetterManager.cs new file mode 100644 index 000000000..8a4b93dd1 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Hshl/ISetterManager.cs @@ -0,0 +1,8 @@ +using Ryujinx.Horizon.Sdk.Sf; + +namespace Ryujinx.Horizon.Sdk.Hshl +{ + interface ISetterManager : IServiceObject + { + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Ins/IReceiverManager.cs b/src/Ryujinx.Horizon/Sdk/Ins/IReceiverManager.cs new file mode 100644 index 000000000..28fc757e5 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Ins/IReceiverManager.cs @@ -0,0 +1,8 @@ +using Ryujinx.Horizon.Sdk.Sf; + +namespace Ryujinx.Horizon.Sdk.Ins +{ + interface IReceiverManager : IServiceObject + { + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Ins/ISenderManager.cs b/src/Ryujinx.Horizon/Sdk/Ins/ISenderManager.cs new file mode 100644 index 000000000..878dbfb32 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Ins/ISenderManager.cs @@ -0,0 +1,8 @@ +using Ryujinx.Horizon.Sdk.Sf; + +namespace Ryujinx.Horizon.Sdk.Ins +{ + interface ISenderManager : IServiceObject + { + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Ovln/IReceiverService.cs b/src/Ryujinx.Horizon/Sdk/Ovln/IReceiverService.cs new file mode 100644 index 000000000..f59e8002d --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Ovln/IReceiverService.cs @@ -0,0 +1,8 @@ +using Ryujinx.Horizon.Sdk.Sf; + +namespace Ryujinx.Horizon.Sdk.Ovln +{ + interface IReceiverService : IServiceObject + { + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Ovln/ISenderService.cs b/src/Ryujinx.Horizon/Sdk/Ovln/ISenderService.cs new file mode 100644 index 000000000..93323ba50 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Ovln/ISenderService.cs @@ -0,0 +1,8 @@ +using Ryujinx.Horizon.Sdk.Sf; + +namespace Ryujinx.Horizon.Sdk.Ovln +{ + interface ISenderService : IServiceObject + { + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Psc/IPmControl.cs b/src/Ryujinx.Horizon/Sdk/Psc/IPmControl.cs new file mode 100644 index 000000000..6a71d6842 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Psc/IPmControl.cs @@ -0,0 +1,8 @@ +using Ryujinx.Horizon.Sdk.Sf; + +namespace Ryujinx.Horizon.Sdk.Psc +{ + interface IPmControl : IServiceObject + { + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Psc/IPmService.cs b/src/Ryujinx.Horizon/Sdk/Psc/IPmService.cs new file mode 100644 index 000000000..c58665818 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Psc/IPmService.cs @@ -0,0 +1,8 @@ +using Ryujinx.Horizon.Sdk.Sf; + +namespace Ryujinx.Horizon.Sdk.Psc +{ + interface IPmService : IServiceObject + { + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Psc/IPmStateLock.cs b/src/Ryujinx.Horizon/Sdk/Psc/IPmStateLock.cs new file mode 100644 index 000000000..41ead492e --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Psc/IPmStateLock.cs @@ -0,0 +1,8 @@ +using Ryujinx.Horizon.Sdk.Sf; + +namespace Ryujinx.Horizon.Sdk.Psc +{ + interface IPmStateLock : IServiceObject + { + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Srepo/ISrepoService.cs b/src/Ryujinx.Horizon/Sdk/Srepo/ISrepoService.cs new file mode 100644 index 000000000..9a1f4ba4f --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Srepo/ISrepoService.cs @@ -0,0 +1,8 @@ +using Ryujinx.Horizon.Sdk.Sf; + +namespace Ryujinx.Horizon.Sdk.Srepo +{ + interface ISrepoService : IServiceObject + { + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Usb/IClientRootSession.cs b/src/Ryujinx.Horizon/Sdk/Usb/IClientRootSession.cs new file mode 100644 index 000000000..4975ad6b5 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Usb/IClientRootSession.cs @@ -0,0 +1,8 @@ +using Ryujinx.Horizon.Sdk.Sf; + +namespace Ryujinx.Horizon.Sdk.Usb +{ + interface IClientRootSession : IServiceObject + { + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Usb/IDsRootSession.cs b/src/Ryujinx.Horizon/Sdk/Usb/IDsRootSession.cs new file mode 100644 index 000000000..32d7aba6c --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Usb/IDsRootSession.cs @@ -0,0 +1,8 @@ +using Ryujinx.Horizon.Sdk.Sf; + +namespace Ryujinx.Horizon.Sdk.Usb +{ + interface IDsRootSession : IServiceObject + { + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Usb/IPdCradleManager.cs b/src/Ryujinx.Horizon/Sdk/Usb/IPdCradleManager.cs new file mode 100644 index 000000000..0d3865114 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Usb/IPdCradleManager.cs @@ -0,0 +1,8 @@ +using Ryujinx.Horizon.Sdk.Sf; + +namespace Ryujinx.Horizon.Sdk.Usb +{ + interface IPdCradleManager : IServiceObject + { + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Usb/IPdManager.cs b/src/Ryujinx.Horizon/Sdk/Usb/IPdManager.cs new file mode 100644 index 000000000..d63821516 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Usb/IPdManager.cs @@ -0,0 +1,8 @@ +using Ryujinx.Horizon.Sdk.Sf; + +namespace Ryujinx.Horizon.Sdk.Usb +{ + interface IPdManager : IServiceObject + { + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Usb/IPdManufactureManager.cs b/src/Ryujinx.Horizon/Sdk/Usb/IPdManufactureManager.cs new file mode 100644 index 000000000..18bac3ea4 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Usb/IPdManufactureManager.cs @@ -0,0 +1,8 @@ +using Ryujinx.Horizon.Sdk.Sf; + +namespace Ryujinx.Horizon.Sdk.Usb +{ + interface IPdManufactureManager : IServiceObject + { + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Usb/IPmObserverService.cs b/src/Ryujinx.Horizon/Sdk/Usb/IPmObserverService.cs new file mode 100644 index 000000000..ef4cc65af --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Usb/IPmObserverService.cs @@ -0,0 +1,8 @@ +using Ryujinx.Horizon.Sdk.Sf; + +namespace Ryujinx.Horizon.Sdk.Usb +{ + interface IPmObserverService : IServiceObject + { + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Usb/IPmService.cs b/src/Ryujinx.Horizon/Sdk/Usb/IPmService.cs new file mode 100644 index 000000000..b8d177bd0 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Usb/IPmService.cs @@ -0,0 +1,8 @@ +using Ryujinx.Horizon.Sdk.Sf; + +namespace Ryujinx.Horizon.Sdk.Usb +{ + interface IPmService : IServiceObject + { + } +} diff --git a/src/Ryujinx.Horizon/Sdk/Usb/IQdbManager.cs b/src/Ryujinx.Horizon/Sdk/Usb/IQdbManager.cs new file mode 100644 index 000000000..a8f61f058 --- /dev/null +++ b/src/Ryujinx.Horizon/Sdk/Usb/IQdbManager.cs @@ -0,0 +1,8 @@ +using Ryujinx.Horizon.Sdk.Sf; + +namespace Ryujinx.Horizon.Sdk.Usb +{ + interface IQdbManager : IServiceObject + { + } +} diff --git a/src/Ryujinx.Horizon/ServiceTable.cs b/src/Ryujinx.Horizon/ServiceTable.cs index 41da222af..c79328a96 100644 --- a/src/Ryujinx.Horizon/ServiceTable.cs +++ b/src/Ryujinx.Horizon/ServiceTable.cs @@ -1,9 +1,15 @@ using Ryujinx.Horizon.Bcat; +using Ryujinx.Horizon.Hshl; +using Ryujinx.Horizon.Ins; using Ryujinx.Horizon.Lbl; using Ryujinx.Horizon.LogManager; using Ryujinx.Horizon.MmNv; using Ryujinx.Horizon.Ngc; +using Ryujinx.Horizon.Ovln; using Ryujinx.Horizon.Prepo; +using Ryujinx.Horizon.Psc; +using Ryujinx.Horizon.Srepo; +using Ryujinx.Horizon.Usb; using Ryujinx.Horizon.Wlan; using System.Collections.Generic; using System.Threading; @@ -27,12 +33,18 @@ namespace Ryujinx.Horizon } RegisterService(); + RegisterService(); + RegisterService(); RegisterService(); RegisterService(); RegisterService(); - RegisterService(); - RegisterService(); RegisterService(); + RegisterService(); + RegisterService(); + RegisterService(); + RegisterService(); + RegisterService(); + RegisterService(); _totalServices = entries.Count; diff --git a/src/Ryujinx.Horizon/Srepo/Ipc/SrepoService.cs b/src/Ryujinx.Horizon/Srepo/Ipc/SrepoService.cs new file mode 100644 index 000000000..501eb5fed --- /dev/null +++ b/src/Ryujinx.Horizon/Srepo/Ipc/SrepoService.cs @@ -0,0 +1,8 @@ +using Ryujinx.Horizon.Sdk.Srepo; + +namespace Ryujinx.Horizon.Srepo.Ipc +{ + partial class SrepoService : ISrepoService + { + } +} diff --git a/src/Ryujinx.Horizon/Srepo/SrepoIpcServer.cs b/src/Ryujinx.Horizon/Srepo/SrepoIpcServer.cs new file mode 100644 index 000000000..a971f97b8 --- /dev/null +++ b/src/Ryujinx.Horizon/Srepo/SrepoIpcServer.cs @@ -0,0 +1,46 @@ +using Ryujinx.Horizon.Sdk.Sf.Hipc; +using Ryujinx.Horizon.Sdk.Sm; +using Ryujinx.Horizon.Srepo.Ipc; + +namespace Ryujinx.Horizon.Srepo +{ + class SrepoIpcServer + { + private const int SrepoAMaxSessionsCount = 2; + private const int SrepoUMaxSessionsCount = 30; + private const int TotalMaxSessionsCount = SrepoAMaxSessionsCount + SrepoUMaxSessionsCount; + + private const int PointerBufferSize = 0x80; + private const int MaxDomains = 32; + private const int MaxDomainObjects = 192; + private const int MaxPortsCount = 2; + + 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, TotalMaxSessionsCount); + + _serverManager.RegisterObjectForServer(new SrepoService(), ServiceName.Encode("srepo:a"), SrepoAMaxSessionsCount); // 5.0.0+ + _serverManager.RegisterObjectForServer(new SrepoService(), ServiceName.Encode("srepo:u"), SrepoUMaxSessionsCount); // 5.0.0+ + } + + public void ServiceRequests() + { + _serverManager.ServiceRequests(); + } + + public void Shutdown() + { + _serverManager.Dispose(); + } + } +} diff --git a/src/Ryujinx.Horizon/Srepo/SrepoMain.cs b/src/Ryujinx.Horizon/Srepo/SrepoMain.cs new file mode 100644 index 000000000..78d813ac9 --- /dev/null +++ b/src/Ryujinx.Horizon/Srepo/SrepoMain.cs @@ -0,0 +1,17 @@ +namespace Ryujinx.Horizon.Srepo +{ + class SrepoMain : IService + { + public static void Main(ServiceTable serviceTable) + { + SrepoIpcServer ipcServer = new(); + + ipcServer.Initialize(); + + serviceTable.SignalServiceReady(); + + ipcServer.ServiceRequests(); + ipcServer.Shutdown(); + } + } +} diff --git a/src/Ryujinx.Horizon/Usb/Ipc/ClientRootSession.cs b/src/Ryujinx.Horizon/Usb/Ipc/ClientRootSession.cs new file mode 100644 index 000000000..2167ebcad --- /dev/null +++ b/src/Ryujinx.Horizon/Usb/Ipc/ClientRootSession.cs @@ -0,0 +1,8 @@ +using Ryujinx.Horizon.Sdk.Usb; + +namespace Ryujinx.Horizon.Usb.Ipc +{ + partial class ClientRootSession : IClientRootSession + { + } +} diff --git a/src/Ryujinx.Horizon/Usb/Ipc/DsRootSession.cs b/src/Ryujinx.Horizon/Usb/Ipc/DsRootSession.cs new file mode 100644 index 000000000..8a84537f8 --- /dev/null +++ b/src/Ryujinx.Horizon/Usb/Ipc/DsRootSession.cs @@ -0,0 +1,8 @@ +using Ryujinx.Horizon.Sdk.Usb; + +namespace Ryujinx.Horizon.Usb.Ipc +{ + partial class DsRootSession : IDsRootSession + { + } +} diff --git a/src/Ryujinx.Horizon/Usb/Ipc/PdCradleManager.cs b/src/Ryujinx.Horizon/Usb/Ipc/PdCradleManager.cs new file mode 100644 index 000000000..27e1c4e37 --- /dev/null +++ b/src/Ryujinx.Horizon/Usb/Ipc/PdCradleManager.cs @@ -0,0 +1,8 @@ +using Ryujinx.Horizon.Sdk.Usb; + +namespace Ryujinx.Horizon.Usb.Ipc +{ + partial class PdCradleManager : IPdCradleManager + { + } +} diff --git a/src/Ryujinx.Horizon/Usb/Ipc/PdManager.cs b/src/Ryujinx.Horizon/Usb/Ipc/PdManager.cs new file mode 100644 index 000000000..c501e3f20 --- /dev/null +++ b/src/Ryujinx.Horizon/Usb/Ipc/PdManager.cs @@ -0,0 +1,9 @@ +using Ryujinx.Horizon.Sdk.Sf.Hipc; +using Ryujinx.Horizon.Sdk.Usb; + +namespace Ryujinx.Horizon.Usb.Ipc +{ + partial class PdManager : IPdManager + { + } +} diff --git a/src/Ryujinx.Horizon/Usb/Ipc/PdManufactureManager.cs b/src/Ryujinx.Horizon/Usb/Ipc/PdManufactureManager.cs new file mode 100644 index 000000000..04f78b9c2 --- /dev/null +++ b/src/Ryujinx.Horizon/Usb/Ipc/PdManufactureManager.cs @@ -0,0 +1,8 @@ +using Ryujinx.Horizon.Sdk.Usb; + +namespace Ryujinx.Horizon.Usb.Ipc +{ + partial class PdManufactureManager : IPdManufactureManager + { + } +} diff --git a/src/Ryujinx.Horizon/Usb/Ipc/PmObserverService.cs b/src/Ryujinx.Horizon/Usb/Ipc/PmObserverService.cs new file mode 100644 index 000000000..e2edf4cb9 --- /dev/null +++ b/src/Ryujinx.Horizon/Usb/Ipc/PmObserverService.cs @@ -0,0 +1,8 @@ +using Ryujinx.Horizon.Sdk.Usb; + +namespace Ryujinx.Horizon.Usb.Ipc +{ + partial class PmObserverService : IPmObserverService + { + } +} diff --git a/src/Ryujinx.Horizon/Usb/Ipc/PmService.cs b/src/Ryujinx.Horizon/Usb/Ipc/PmService.cs new file mode 100644 index 000000000..625aaa497 --- /dev/null +++ b/src/Ryujinx.Horizon/Usb/Ipc/PmService.cs @@ -0,0 +1,8 @@ +using Ryujinx.Horizon.Sdk.Usb; + +namespace Ryujinx.Horizon.Usb.Ipc +{ + partial class PmService : IPmService + { + } +} diff --git a/src/Ryujinx.Horizon/Usb/Ipc/QdbManager.cs b/src/Ryujinx.Horizon/Usb/Ipc/QdbManager.cs new file mode 100644 index 000000000..1421142fb --- /dev/null +++ b/src/Ryujinx.Horizon/Usb/Ipc/QdbManager.cs @@ -0,0 +1,8 @@ +using Ryujinx.Horizon.Sdk.Usb; + +namespace Ryujinx.Horizon.Usb.Ipc +{ + partial class QdbManager : IQdbManager + { + } +} diff --git a/src/Ryujinx.Horizon/Usb/UsbIpcServer.cs b/src/Ryujinx.Horizon/Usb/UsbIpcServer.cs new file mode 100644 index 000000000..a9158b507 --- /dev/null +++ b/src/Ryujinx.Horizon/Usb/UsbIpcServer.cs @@ -0,0 +1,71 @@ +using Ryujinx.Horizon.Sdk.Sf.Hipc; +using Ryujinx.Horizon.Sdk.Sm; +using Ryujinx.Horizon.Usb.Ipc; + +namespace Ryujinx.Horizon.Usb +{ + class UsbIpcServer + { + private const int UsbDsMaxSessionsCount = 4; + private const int UsbHsMaxSessionsCount = 20; + private const int UsbHsAMaxSessionsCount = 3; + private const int UsbObsvMaxSessionsCount = 2; + private const int UsbPdMaxSessionsCount = 6; + private const int UsbPdCMaxSessionsCount = 4; + private const int UsbPdMMaxSessionsCount = 1; + private const int UsbPmMaxSessionsCount = 5; + private const int UsbQdbMaxSessionsCount = 4; + private const int TotalMaxSessionsCount = + UsbDsMaxSessionsCount + + UsbHsMaxSessionsCount + + UsbHsAMaxSessionsCount + + UsbObsvMaxSessionsCount + + UsbPdMaxSessionsCount + + UsbPdCMaxSessionsCount + + UsbPdMMaxSessionsCount + + UsbPmMaxSessionsCount + + UsbQdbMaxSessionsCount; + + private const int PointerBufferSize = 0; + private const int MaxDomains = 0; + private const int MaxDomainObjects = 0; + private const int MaxPortsCount = 9; + + 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, TotalMaxSessionsCount); + +#pragma warning disable IDE0055 // Disable formatting + _serverManager.RegisterObjectForServer(new DsRootSession(), ServiceName.Encode("usb:ds"), UsbDsMaxSessionsCount); + _serverManager.RegisterObjectForServer(new ClientRootSession(), ServiceName.Encode("usb:hs"), UsbHsMaxSessionsCount); + _serverManager.RegisterObjectForServer(new ClientRootSession(), ServiceName.Encode("usb:hs:a"), UsbHsAMaxSessionsCount); // 7.0.0+ + _serverManager.RegisterObjectForServer(new PmObserverService(), ServiceName.Encode("usb:obsv"), UsbObsvMaxSessionsCount); // 8.0.0+ + _serverManager.RegisterObjectForServer(new PdManager(), ServiceName.Encode("usb:pd"), UsbPdMaxSessionsCount); + _serverManager.RegisterObjectForServer(new PdCradleManager(), ServiceName.Encode("usb:pd:c"), UsbPdCMaxSessionsCount); + _serverManager.RegisterObjectForServer(new PdManufactureManager(), ServiceName.Encode("usb:pd:m"), UsbPdMMaxSessionsCount); // 1.0.0 + _serverManager.RegisterObjectForServer(new PmService(), ServiceName.Encode("usb:pm"), UsbPmMaxSessionsCount); + _serverManager.RegisterObjectForServer(new QdbManager(), ServiceName.Encode("usb:qdb"), UsbQdbMaxSessionsCount); // 7.0.0+ +#pragma warning restore IDE0055 + } + + public void ServiceRequests() + { + _serverManager.ServiceRequests(); + } + + public void Shutdown() + { + _serverManager.Dispose(); + } + } +} diff --git a/src/Ryujinx.Horizon/Usb/UsbMain.cs b/src/Ryujinx.Horizon/Usb/UsbMain.cs new file mode 100644 index 000000000..c54b39a65 --- /dev/null +++ b/src/Ryujinx.Horizon/Usb/UsbMain.cs @@ -0,0 +1,17 @@ +namespace Ryujinx.Horizon.Usb +{ + class UsbMain : IService + { + public static void Main(ServiceTable serviceTable) + { + UsbIpcServer ipcServer = new(); + + ipcServer.Initialize(); + + serviceTable.SignalServiceReady(); + + ipcServer.ServiceRequests(); + ipcServer.Shutdown(); + } + } +} From 28dd7d80af56701887dbb538b56aa58edaf39d91 Mon Sep 17 00:00:00 2001 From: gdkchan Date: Wed, 18 Oct 2023 01:47:22 -0300 Subject: [PATCH 091/105] Enable copy between MS and non-MS textures with different height (#5801) --- src/Ryujinx.Graphics.Gpu/Image/TextureCompatibility.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Ryujinx.Graphics.Gpu/Image/TextureCompatibility.cs b/src/Ryujinx.Graphics.Gpu/Image/TextureCompatibility.cs index eafa50b2e..3a0efcdda 100644 --- a/src/Ryujinx.Graphics.Gpu/Image/TextureCompatibility.cs +++ b/src/Ryujinx.Graphics.Gpu/Image/TextureCompatibility.cs @@ -374,6 +374,13 @@ namespace Ryujinx.Graphics.Gpu.Image return stride == rhs.Stride ? TextureViewCompatibility.CopyOnly : TextureViewCompatibility.LayoutIncompatible; } + else if (lhs.Target.IsMultisample() != rhs.Target.IsMultisample() && alignedWidthMatches && lhsAlignedSize.Height == rhsAlignedSize.Height) + { + // Copy between multisample and non-multisample textures with mismatching size is allowed, + // as long aligned size matches. + + return TextureViewCompatibility.CopyOnly; + } else { return TextureViewCompatibility.LayoutIncompatible; From 76b53e018a2e867899dbce2f3ce5173bbc4eed22 Mon Sep 17 00:00:00 2001 From: riperiperi Date: Fri, 20 Oct 2023 14:05:09 +0100 Subject: [PATCH 092/105] GPU: Add fallback when textureGatherOffsets is not supported (#5792) * GPU: Add fallback when textureGatherOffsets is not supported. This PR adds a fallback for GPUs or APIs that don't support an equivalent to the method `textureGatherOffsets`, where each of the 4 gathered texels has an individual offset. This is done by reusing the existing code to handle non-const offsets for texture instructions, though it has also been corrected as there were a few implementation issues. MoltenVK reports support for this capability, and it didn't error when we initially released the MacOS build, but that has since changed. MVK still reports support, but spirv-cross has been fixed in a way that it _attempts_ to use this capability, but the metal compiler errors since it doesn't exist. Some other fixes: - textureGatherOffsets emulation has been changed significantly. It now uses 4 texture sample instructions (not gather), calculates a base texel (i=0 j=0) and adds the offsets onto it before converting into a tex coord. The final result is offset into a texel center, so it shouldn't be subject to interpolation, though this isn't perfect and could have some error with floating point formats with linear sampling. It is subject to texture wrap mode as it should be, which is why texelFetch was not used. - Maybe gather should be used here with component `w` (i=0, j=0), though this multiplies number of texels fetched by 4... The way it was doing this before _was_ wrong_, but doing it right would avoid issues with texel center precision. - textureGatherOffset (singular) now performs textureGather with the offset applied to the coords, rather than the slower fallback where each texel is fetched individually. * Increment shader cache version, remove unused arg * Use base texture size for gather coord offset. Implicit LOD for gather is not supported. * Use 4 texture gathers for offsets emulation Avoids issues with interpolation at cost of performance (not sure how bad this is) * Address Feedback --- src/Ryujinx.Graphics.GAL/Capabilities.cs | 3 + .../Shader/DiskCache/DiskCacheHostStorage.cs | 2 +- .../Shader/GpuAccessorBase.cs | 2 + src/Ryujinx.Graphics.OpenGL/OpenGLRenderer.cs | 1 + src/Ryujinx.Graphics.Shader/IGpuAccessor.cs | 9 +++ .../Translation/Transforms/TexturePass.cs | 64 ++++++++++++++++--- src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs | 1 + 7 files changed, 71 insertions(+), 11 deletions(-) diff --git a/src/Ryujinx.Graphics.GAL/Capabilities.cs b/src/Ryujinx.Graphics.GAL/Capabilities.cs index 756422049..8959bf93e 100644 --- a/src/Ryujinx.Graphics.GAL/Capabilities.cs +++ b/src/Ryujinx.Graphics.GAL/Capabilities.cs @@ -38,6 +38,7 @@ namespace Ryujinx.Graphics.GAL public readonly bool SupportsShaderBallot; public readonly bool SupportsShaderBarrierDivergence; public readonly bool SupportsShaderFloat64; + public readonly bool SupportsTextureGatherOffsets; public readonly bool SupportsTextureShadowLod; public readonly bool SupportsVertexStoreAndAtomics; public readonly bool SupportsViewportIndexVertexTessellation; @@ -92,6 +93,7 @@ namespace Ryujinx.Graphics.GAL bool supportsShaderBallot, bool supportsShaderBarrierDivergence, bool supportsShaderFloat64, + bool supportsTextureGatherOffsets, bool supportsTextureShadowLod, bool supportsVertexStoreAndAtomics, bool supportsViewportIndexVertexTessellation, @@ -142,6 +144,7 @@ namespace Ryujinx.Graphics.GAL SupportsShaderBallot = supportsShaderBallot; SupportsShaderBarrierDivergence = supportsShaderBarrierDivergence; SupportsShaderFloat64 = supportsShaderFloat64; + SupportsTextureGatherOffsets = supportsTextureGatherOffsets; SupportsTextureShadowLod = supportsTextureShadowLod; SupportsVertexStoreAndAtomics = supportsVertexStoreAndAtomics; SupportsViewportIndexVertexTessellation = supportsViewportIndexVertexTessellation; diff --git a/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheHostStorage.cs b/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheHostStorage.cs index 0f1aa6a96..0dc4b1a72 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 = 5767; + private const uint CodeGenVersion = 5791; private const string SharedTocFileName = "shared.toc"; private const string SharedDataFileName = "shared.data"; diff --git a/src/Ryujinx.Graphics.Gpu/Shader/GpuAccessorBase.cs b/src/Ryujinx.Graphics.Gpu/Shader/GpuAccessorBase.cs index 9d030cd60..a5b31363b 100644 --- a/src/Ryujinx.Graphics.Gpu/Shader/GpuAccessorBase.cs +++ b/src/Ryujinx.Graphics.Gpu/Shader/GpuAccessorBase.cs @@ -186,6 +186,8 @@ namespace Ryujinx.Graphics.Gpu.Shader public bool QueryHostSupportsSnormBufferTextureFormat() => _context.Capabilities.SupportsSnormBufferTextureFormat; + public bool QueryHostSupportsTextureGatherOffsets() => _context.Capabilities.SupportsTextureGatherOffsets; + public bool QueryHostSupportsTextureShadowLod() => _context.Capabilities.SupportsTextureShadowLod; public bool QueryHostSupportsTransformFeedback() => _context.Capabilities.SupportsTransformFeedback; diff --git a/src/Ryujinx.Graphics.OpenGL/OpenGLRenderer.cs b/src/Ryujinx.Graphics.OpenGL/OpenGLRenderer.cs index 3eba15e34..667ea7825 100644 --- a/src/Ryujinx.Graphics.OpenGL/OpenGLRenderer.cs +++ b/src/Ryujinx.Graphics.OpenGL/OpenGLRenderer.cs @@ -163,6 +163,7 @@ namespace Ryujinx.Graphics.OpenGL supportsShaderBallot: HwCapabilities.SupportsShaderBallot, supportsShaderBarrierDivergence: !(intelWindows || intelUnix), supportsShaderFloat64: true, + supportsTextureGatherOffsets: true, supportsTextureShadowLod: HwCapabilities.SupportsTextureShadowLod, supportsVertexStoreAndAtomics: true, supportsViewportIndexVertexTessellation: HwCapabilities.SupportsShaderViewportLayerArray, diff --git a/src/Ryujinx.Graphics.Shader/IGpuAccessor.cs b/src/Ryujinx.Graphics.Shader/IGpuAccessor.cs index 4dc75a3e1..29a5435e3 100644 --- a/src/Ryujinx.Graphics.Shader/IGpuAccessor.cs +++ b/src/Ryujinx.Graphics.Shader/IGpuAccessor.cs @@ -339,6 +339,15 @@ namespace Ryujinx.Graphics.Shader return true; } + /// + /// Queries host GPU texture gather with multiple offsets support. + /// + /// True if the GPU and driver supports texture gather offsets, false otherwise + bool QueryHostSupportsTextureGatherOffsets() + { + return true; + } + /// /// Queries host GPU texture shadow LOD support. /// diff --git a/src/Ryujinx.Graphics.Shader/Translation/Transforms/TexturePass.cs b/src/Ryujinx.Graphics.Shader/Translation/Transforms/TexturePass.cs index dbfe6269e..495ea8a94 100644 --- a/src/Ryujinx.Graphics.Shader/Translation/Transforms/TexturePass.cs +++ b/src/Ryujinx.Graphics.Shader/Translation/Transforms/TexturePass.cs @@ -303,7 +303,9 @@ namespace Ryujinx.Graphics.Shader.Translation.Transforms bool hasOffset = (texOp.Flags & TextureFlags.Offset) != 0; bool hasOffsets = (texOp.Flags & TextureFlags.Offsets) != 0; - bool hasInvalidOffset = (hasOffset || hasOffsets) && !gpuAccessor.QueryHostSupportsNonConstantTextureOffset(); + bool needsOffsetsEmulation = hasOffsets && !gpuAccessor.QueryHostSupportsTextureGatherOffsets(); + + bool hasInvalidOffset = needsOffsetsEmulation || ((hasOffset || hasOffsets) && !gpuAccessor.QueryHostSupportsNonConstantTextureOffset()); bool isBindless = (texOp.Flags & TextureFlags.Bindless) != 0; @@ -402,11 +404,14 @@ namespace Ryujinx.Graphics.Shader.Translation.Transforms offsets[index] = offset; } - hasInvalidOffset &= !areAllOffsetsConstant; - - if (!hasInvalidOffset) + if (!needsOffsetsEmulation) { - return node; + hasInvalidOffset &= !areAllOffsetsConstant; + + if (!hasInvalidOffset) + { + return node; + } } if (hasLodBias) @@ -434,13 +439,13 @@ namespace Ryujinx.Graphics.Shader.Translation.Transforms LinkedListNode oldNode = node; - if (isGather && !isShadow) + if (isGather && !isShadow && hasOffsets) { Operand[] newSources = new Operand[sources.Length]; sources.CopyTo(newSources, 0); - Operand[] texSizes = InsertTextureLod(node, texOp, lodSources, bindlessHandle, coordsCount, stage); + Operand[] texSizes = InsertTextureBaseSize(node, texOp, bindlessHandle, coordsCount); int destIndex = 0; @@ -455,7 +460,7 @@ namespace Ryujinx.Graphics.Shader.Translation.Transforms { Operand offset = Local(); - Operand intOffset = offsets[index + (hasOffsets ? compIndex * coordsCount : 0)]; + Operand intOffset = offsets[index + compIndex * coordsCount]; node.List.AddBefore(node, new Operation( Instruction.FP32 | Instruction.Divide, @@ -478,7 +483,7 @@ namespace Ryujinx.Graphics.Shader.Translation.Transforms texOp.Format, texOp.Flags & ~(TextureFlags.Offset | TextureFlags.Offsets), texOp.Binding, - 1, + 1 << 3, // W component: i=0, j=0 new[] { dests[destIndex++] }, newSources); @@ -502,7 +507,9 @@ namespace Ryujinx.Graphics.Shader.Translation.Transforms } else { - Operand[] texSizes = InsertTextureLod(node, texOp, lodSources, bindlessHandle, coordsCount, stage); + Operand[] texSizes = isGather + ? InsertTextureBaseSize(node, texOp, bindlessHandle, coordsCount) + : InsertTextureLod(node, texOp, lodSources, bindlessHandle, coordsCount, stage); for (int index = 0; index < coordsCount; index++) { @@ -549,6 +556,43 @@ namespace Ryujinx.Graphics.Shader.Translation.Transforms return node; } + private static Operand[] InsertTextureBaseSize( + LinkedListNode node, + TextureOperation texOp, + Operand bindlessHandle, + int coordsCount) + { + Operand[] texSizes = new Operand[coordsCount]; + + for (int index = 0; index < coordsCount; index++) + { + texSizes[index] = Local(); + + Operand[] texSizeSources; + + if (bindlessHandle != null) + { + texSizeSources = new Operand[] { bindlessHandle, Const(0) }; + } + else + { + texSizeSources = new Operand[] { Const(0) }; + } + + node.List.AddBefore(node, new TextureOperation( + Instruction.TextureQuerySize, + texOp.Type, + texOp.Format, + texOp.Flags, + texOp.Binding, + index, + new[] { texSizes[index] }, + texSizeSources)); + } + + return texSizes; + } + private static Operand[] InsertTextureLod( LinkedListNode node, TextureOperation texOp, diff --git a/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs b/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs index a483dc599..ab8e61371 100644 --- a/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs +++ b/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs @@ -605,6 +605,7 @@ namespace Ryujinx.Graphics.Vulkan supportsShaderBallot: false, supportsShaderBarrierDivergence: Vendor != Vendor.Intel, supportsShaderFloat64: Capabilities.SupportsShaderFloat64, + supportsTextureGatherOffsets: features2.Features.ShaderImageGatherExtended && !IsMoltenVk, supportsTextureShadowLod: false, supportsVertexStoreAndAtomics: features2.Features.VertexPipelineStoresAndAtomics, supportsViewportIndexVertexTessellation: featuresVk12.ShaderOutputViewportIndex, From 6fdf7748455b2b71f99885239f8dc31390de2687 Mon Sep 17 00:00:00 2001 From: Isaac Marovitz <42140194+IsaacMarovitz@users.noreply.github.com> Date: Fri, 20 Oct 2023 09:41:50 -0400 Subject: [PATCH 093/105] Ava UI: Update to 11.0.5 (#5815) * Bump bump bump * Missed one --- Directory.Packages.props | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index cde8742f9..b34b882b4 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -3,13 +3,13 @@ true - - - - - - - + + + + + + + From b4bb22ba06f89168c948e6001c51972575ca968b Mon Sep 17 00:00:00 2001 From: Ahmad Tantowi Date: Fri, 20 Oct 2023 21:02:12 +0700 Subject: [PATCH 094/105] Avalonia: Make slider scrollable with mouse wheel (#5760) * Add scrollable custom control based on TickFrequency * Use custom slider to update value when pointer wheel scrolled * Remove extra xaml file * Address formatting issues * Only scroll one element at a time * Add OnPointerWheelChanged event to VolumeStatus button Co-authored-by: Ahmad Tantowi --------- Co-authored-by: TSR Berry <20988865+TSRBerry@users.noreply.github.com> --- .../UI/Controls/SliderScroll.axaml.cs | 31 +++++++++++++++++++ .../UI/Views/Input/ControllerInputView.axaml | 13 ++++---- .../UI/Views/Input/MotionInputView.axaml | 11 ++++--- .../UI/Views/Input/RumbleInputView.axaml | 7 +++-- .../UI/Views/Main/MainStatusBarView.axaml | 4 ++- .../UI/Views/Main/MainStatusBarView.axaml.cs | 15 +++++++++ .../UI/Views/Main/MainViewControls.axaml | 5 +-- .../UI/Views/Settings/SettingsAudioView.axaml | 9 +++--- .../Views/Settings/SettingsGraphicsView.axaml | 3 +- 9 files changed, 76 insertions(+), 22 deletions(-) create mode 100644 src/Ryujinx.Ava/UI/Controls/SliderScroll.axaml.cs diff --git a/src/Ryujinx.Ava/UI/Controls/SliderScroll.axaml.cs b/src/Ryujinx.Ava/UI/Controls/SliderScroll.axaml.cs new file mode 100644 index 000000000..81d3bc303 --- /dev/null +++ b/src/Ryujinx.Ava/UI/Controls/SliderScroll.axaml.cs @@ -0,0 +1,31 @@ +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) + { + var newValue = Value + e.Delta.Y * TickFrequency; + + if (newValue < Minimum) + { + Value = Minimum; + } + else if (newValue > Maximum) + { + Value = Maximum; + } + else + { + Value = newValue; + } + + e.Handled = true; + } + } +} diff --git a/src/Ryujinx.Ava/UI/Views/Input/ControllerInputView.axaml b/src/Ryujinx.Ava/UI/Views/Input/ControllerInputView.axaml index 2ab42e6ee..d636873a3 100644 --- a/src/Ryujinx.Ava/UI/Views/Input/ControllerInputView.axaml +++ b/src/Ryujinx.Ava/UI/Views/Input/ControllerInputView.axaml @@ -5,6 +5,7 @@ xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:controls="clr-namespace:Ryujinx.Ava.UI.Controls" xmlns:models="clr-namespace:Ryujinx.Ava.UI.Models" xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels" xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers" @@ -460,7 +461,7 @@ HorizontalAlignment="Center" VerticalAlignment="Center" Orientation="Horizontal"> - - - - - - \ No newline at end of file + diff --git a/src/Ryujinx.Ava/UI/Views/Input/MotionInputView.axaml b/src/Ryujinx.Ava/UI/Views/Input/MotionInputView.axaml index a98f08825..a6b587f67 100644 --- a/src/Ryujinx.Ava/UI/Views/Input/MotionInputView.axaml +++ b/src/Ryujinx.Ava/UI/Views/Input/MotionInputView.axaml @@ -3,6 +3,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:controls="clr-namespace:Ryujinx.Ava.UI.Controls" xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale" xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels" @@ -23,11 +24,11 @@ Margin="0" HorizontalAlignment="Center" Text="{locale:Locale ControllerSettingsMotionGyroSensitivity}" /> - - - \ No newline at end of file + diff --git a/src/Ryujinx.Ava/UI/Views/Input/RumbleInputView.axaml b/src/Ryujinx.Ava/UI/Views/Input/RumbleInputView.axaml index f633c0ed2..5b7087a47 100644 --- a/src/Ryujinx.Ava/UI/Views/Input/RumbleInputView.axaml +++ b/src/Ryujinx.Ava/UI/Views/Input/RumbleInputView.axaml @@ -1,6 +1,7 @@ - - - \ No newline at end of file + diff --git a/src/Ryujinx.Ava/UI/Views/Main/MainStatusBarView.axaml b/src/Ryujinx.Ava/UI/Views/Main/MainStatusBarView.axaml index 32524740b..01133a4bc 100644 --- a/src/Ryujinx.Ava/UI/Views/Main/MainStatusBarView.axaml +++ b/src/Ryujinx.Ava/UI/Views/Main/MainStatusBarView.axaml @@ -3,6 +3,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:controls="clr-namespace:Ryujinx.Ava.UI.Controls" xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale" xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels" @@ -176,6 +177,7 @@ Content="{Binding VolumeStatusText}" IsChecked="{Binding VolumeMuted}" IsVisible="{Binding !ShowLoadProgress}" + PointerWheelChanged="VolumeStatus_OnPointerWheelChanged" Background="Transparent" BorderThickness="0" CornerRadius="0"> @@ -192,7 +194,7 @@ - 0, + > 1 => 1, + _ => newValue, + }; + + e.Handled = true; + } } } diff --git a/src/Ryujinx.Ava/UI/Views/Main/MainViewControls.axaml b/src/Ryujinx.Ava/UI/Views/Main/MainViewControls.axaml index 34624b222..cc21b5c60 100644 --- a/src/Ryujinx.Ava/UI/Views/Main/MainViewControls.axaml +++ b/src/Ryujinx.Ava/UI/Views/Main/MainViewControls.axaml @@ -3,6 +3,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:controls="clr-namespace:Ryujinx.Ava.UI.Controls" xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers" xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale" @@ -50,7 +51,7 @@ VerticalAlignment="Center" Text="{locale:Locale IconSize}" ToolTip.Tip="{locale:Locale IconSizeTooltip}" /> - - \ No newline at end of file + diff --git a/src/Ryujinx.Ava/UI/Views/Settings/SettingsAudioView.axaml b/src/Ryujinx.Ava/UI/Views/Settings/SettingsAudioView.axaml index 5dc0fef5d..657e07ee7 100644 --- a/src/Ryujinx.Ava/UI/Views/Settings/SettingsAudioView.axaml +++ b/src/Ryujinx.Ava/UI/Views/Settings/SettingsAudioView.axaml @@ -4,6 +4,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:controls="clr-namespace:Ryujinx.Ava.UI.Controls" xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale" xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels" @@ -63,13 +64,13 @@ Maximum="100" /> - @@ -77,4 +78,4 @@ - \ No newline at end of file + diff --git a/src/Ryujinx.Ava/UI/Views/Settings/SettingsGraphicsView.axaml b/src/Ryujinx.Ava/UI/Views/Settings/SettingsGraphicsView.axaml index f6ba0a4c0..224494786 100644 --- a/src/Ryujinx.Ava/UI/Views/Settings/SettingsGraphicsView.axaml +++ b/src/Ryujinx.Ava/UI/Views/Settings/SettingsGraphicsView.axaml @@ -4,6 +4,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:controls="clr-namespace:Ryujinx.Ava.UI.Controls" xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale" xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels" @@ -173,7 +174,7 @@ - Date: Sat, 21 Oct 2023 05:51:15 +1100 Subject: [PATCH 095/105] Add "Create Shortcut" To app context menu (#4734) * Added basic implementation for shortcut creation Currently bitmaps (.bmp) are used as the source file, colours are good (unlike .ico rn) but are scaled poorly on desktop. * Icons display properly in shortcut * code cleanup * Moved shortcut logic to specific file, added Ava UI for shortcuts * Added linux .desktop shortcut creation * fixes to .shortcut data * code issue fixes * Added basic implementation for shortcut creation Currently bitmaps (.bmp) are used as the source file, colours are good (unlike .ico rn) but are scaled poorly on desktop. * Icons display properly in shortcut * code cleanup * Moved shortcut logic to specific file, added Ava UI for shortcuts * Added linux .desktop shortcut creation * fixes to .shortcut data * code issue fixes * added back shortcut to new contextmenu file * Replaced COM reference with ComImport for shortcut functionality * remove specific platform values and regions * Move ShortcutHelper to Ryujinx.Ui.Common.Helpers * Adjust styling and structure * code feedback changes * Added MacOS support using .app folder * Added basic implementation for shortcut creation Currently bitmaps (.bmp) are used as the source file, colours are good (unlike .ico rn) but are scaled poorly on desktop. * Icons display properly in shortcut * code cleanup * Moved shortcut logic to specific file, added Ava UI for shortcuts * Added linux .desktop shortcut creation * fixes to .shortcut data * code issue fixes * Added basic implementation for shortcut creation Currently bitmaps (.bmp) are used as the source file, colours are good (unlike .ico rn) but are scaled poorly on desktop. * Icons display properly in shortcut * code cleanup * Moved shortcut logic to specific file, added Ava UI for shortcuts * Added linux .desktop shortcut creation * fixes to .shortcut data * code issue fixes * Replaced COM reference with ComImport for shortcut functionality * remove specific platform values and regions * Move ShortcutHelper to Ryujinx.Ui.Common.Helpers * Adjust styling and structure * code feedback changes * adjust tooltip message * added shortcut-template.desktop file * set shortcut icon location to .local/share/icons * Linux code feedback changes * change InteropServices to new securifybv.ShellLink Package * added ShellLink to readme, updated shortcut comment * Code feedback changes * Added MacOS Support (As per Jose Estrada's PR) * dotnet format * Small restructuring * Embed template files into Ryujinx.Ui.Common * Disable "CreateShortcut" option for flatpak builds --------- Co-authored-by: TSR Berry <20988865+TSRBerry@users.noreply.github.com> Co-authored-by: Jose Estrada --- Directory.Packages.props | 1 + README.md | 1 + distribution/legal/THIRDPARTY.md | 29 +++ distribution/linux/Ryujinx.desktop | 4 +- distribution/linux/shortcut-template.desktop | 13 ++ distribution/macos/shortcut-template.plist | 35 ++++ src/Ryujinx.Ava/Assets/Locales/en_US.json | 2 + src/Ryujinx.Ava/Ryujinx.Ava.csproj | 2 +- .../UI/Controls/ApplicationContextMenu.axaml | 7 +- .../Controls/ApplicationContextMenu.axaml.cs | 11 ++ .../UI/ViewModels/MainWindowViewModel.cs | 5 +- .../App/ApplicationLibrary.cs | 4 +- .../Helper/ShortcutHelper.cs | 171 ++++++++++++++++++ .../Ryujinx.Ui.Common.csproj | 10 + src/Ryujinx/Ryujinx.csproj | 18 +- .../Widgets/GameTableContextMenu.Designer.cs | 11 ++ .../Ui/Widgets/GameTableContextMenu.cs | 9 + 17 files changed, 316 insertions(+), 17 deletions(-) create mode 100644 distribution/linux/shortcut-template.desktop create mode 100644 distribution/macos/shortcut-template.plist create mode 100644 src/Ryujinx.Ui.Common/Helper/ShortcutHelper.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index b34b882b4..6fdaafddc 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -35,6 +35,7 @@ + diff --git a/README.md b/README.md index 7021abc45..56333278f 100644 --- a/README.md +++ b/README.md @@ -141,3 +141,4 @@ See [LICENSE.txt](LICENSE.txt) and [THIRDPARTY.md](distribution/legal/THIRDPARTY - [LibHac](https://github.com/Thealexbarney/LibHac) is used for our file-system. - [AmiiboAPI](https://www.amiiboapi.com) is used in our Amiibo emulation. +- [ShellLink](https://github.com/securifybv/ShellLink) is used for Windows shortcut generation. diff --git a/distribution/legal/THIRDPARTY.md b/distribution/legal/THIRDPARTY.md index 4cc8b7a45..b0bd5a690 100644 --- a/distribution/legal/THIRDPARTY.md +++ b/distribution/legal/THIRDPARTY.md @@ -681,4 +681,33 @@ END OF TERMS AND CONDITIONS ``` + + +# ShellLink (MIT) +
+ See License + + ``` + MIT License + + Copyright (c) 2017 Yorick Koster, Securify B.V. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + ```
\ No newline at end of file diff --git a/distribution/linux/Ryujinx.desktop b/distribution/linux/Ryujinx.desktop index 19cc5d6cc..a4550d104 100644 --- a/distribution/linux/Ryujinx.desktop +++ b/distribution/linux/Ryujinx.desktop @@ -3,8 +3,8 @@ Version=1.0 Name=Ryujinx Type=Application Icon=Ryujinx -Exec=env DOTNET_EnableAlternateStackCheck=1 Ryujinx %f -Comment=A Nintendo Switch Emulator +Exec=Ryujinx.sh %f +Comment=Plays Nintendo Switch applications GenericName=Nintendo Switch Emulator Terminal=false Categories=Game;Emulator; diff --git a/distribution/linux/shortcut-template.desktop b/distribution/linux/shortcut-template.desktop new file mode 100644 index 000000000..6bee0f8d1 --- /dev/null +++ b/distribution/linux/shortcut-template.desktop @@ -0,0 +1,13 @@ +[Desktop Entry] +Version=1.0 +Name={0} +Type=Application +Icon={1} +Exec={2} %f +Comment=Nintendo Switch application +GenericName=Nintendo Switch Emulator +Terminal=false +Categories=Game;Emulator; +Keywords=Switch;Nintendo;Emulator; +StartupWMClass=Ryujinx +PrefersNonDefaultGPU=true diff --git a/distribution/macos/shortcut-template.plist b/distribution/macos/shortcut-template.plist new file mode 100644 index 000000000..27a9e46a9 --- /dev/null +++ b/distribution/macos/shortcut-template.plist @@ -0,0 +1,35 @@ + + + + + CFBundleDevelopmentRegion + English + CFBundleExecutable + {0} + CFBundleGetInfoString + {1} + CFBundleIconFile + {2} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleVersion + 1.0 + NSHighResolutionCapable + + CSResourcesFileMapped + + NSHumanReadableCopyright + Copyright © 2018 - 2023 Ryujinx Team and Contributors. + LSApplicationCategoryType + public.app-category.games + LSMinimumSystemVersion + 11.0 + UIPrerenderedIcon + + LSEnvironment + + DOTNET_DefaultStackSize + 200000 + + + diff --git a/src/Ryujinx.Ava/Assets/Locales/en_US.json b/src/Ryujinx.Ava/Assets/Locales/en_US.json index 53e277ba9..a67b796bd 100644 --- a/src/Ryujinx.Ava/Assets/Locales/en_US.json +++ b/src/Ryujinx.Ava/Assets/Locales/en_US.json @@ -72,6 +72,8 @@ "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", "StatusBarGamesLoaded": "{0}/{1} Games Loaded", "StatusBarSystemVersion": "System Version: {0}", "LinuxVmMaxMapCountDialogTitle": "Low limit for memory mappings detected", diff --git a/src/Ryujinx.Ava/Ryujinx.Ava.csproj b/src/Ryujinx.Ava/Ryujinx.Ava.csproj index a4c1ebf16..f0e99f427 100644 --- a/src/Ryujinx.Ava/Ryujinx.Ava.csproj +++ b/src/Ryujinx.Ava/Ryujinx.Ava.csproj @@ -145,4 +145,4 @@ - \ No newline at end of file + diff --git a/src/Ryujinx.Ava/UI/Controls/ApplicationContextMenu.axaml b/src/Ryujinx.Ava/UI/Controls/ApplicationContextMenu.axaml index 93638fc53..d81050f83 100644 --- a/src/Ryujinx.Ava/UI/Controls/ApplicationContextMenu.axaml +++ b/src/Ryujinx.Ava/UI/Controls/ApplicationContextMenu.axaml @@ -82,4 +82,9 @@ Header="{locale:Locale GameListContextMenuExtractDataLogo}" ToolTip.Tip="{locale:Locale GameListContextMenuExtractDataLogoToolTip}" /> - \ No newline at end of file + + diff --git a/src/Ryujinx.Ava/UI/Controls/ApplicationContextMenu.axaml.cs b/src/Ryujinx.Ava/UI/Controls/ApplicationContextMenu.axaml.cs index d75572e65..0f0071065 100644 --- a/src/Ryujinx.Ava/UI/Controls/ApplicationContextMenu.axaml.cs +++ b/src/Ryujinx.Ava/UI/Controls/ApplicationContextMenu.axaml.cs @@ -337,6 +337,17 @@ namespace Ryujinx.Ava.UI.Controls } } + public void CreateApplicationShortcut_Click(object sender, RoutedEventArgs args) + { + var viewModel = (sender as MenuItem)?.DataContext as MainWindowViewModel; + + if (viewModel?.SelectedApplication != null) + { + ApplicationData selectedApplication = viewModel.SelectedApplication; + ShortcutHelper.CreateAppShortcut(selectedApplication.Path, selectedApplication.TitleName, selectedApplication.TitleId, selectedApplication.Icon); + } + } + public async void RunApplication_Click(object sender, RoutedEventArgs args) { var viewModel = (sender as MenuItem)?.DataContext as MainWindowViewModel; diff --git a/src/Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs index 7a9e4df14..b14905204 100644 --- a/src/Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs +++ b/src/Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs @@ -356,6 +356,8 @@ namespace Ryujinx.Ava.UI.ViewModels public bool OpenBcatSaveDirectoryEnabled => !SelectedApplication.ControlHolder.ByteSpan.IsZeros() && SelectedApplication.ControlHolder.Value.BcatDeliveryCacheStorageSize > 0; + public bool CreateShortcutEnabled => !ReleaseInformation.IsFlatHubBuild(); + public string LoadHeading { get => _loadHeading; @@ -1488,7 +1490,7 @@ namespace Ryujinx.Ava.UI.ViewModels Logger.RestartTime(); - SelectedIcon ??= ApplicationLibrary.GetApplicationIcon(path); + SelectedIcon ??= ApplicationLibrary.GetApplicationIcon(path, ConfigurationState.Instance.System.Language); PrepareLoadScreen(); @@ -1696,7 +1698,6 @@ namespace Ryujinx.Ava.UI.ViewModels } } } - #endregion } } diff --git a/src/Ryujinx.Ui.Common/App/ApplicationLibrary.cs b/src/Ryujinx.Ui.Common/App/ApplicationLibrary.cs index 33e6c4aad..36b2b727d 100644 --- a/src/Ryujinx.Ui.Common/App/ApplicationLibrary.cs +++ b/src/Ryujinx.Ui.Common/App/ApplicationLibrary.cs @@ -546,7 +546,7 @@ namespace Ryujinx.Ui.App.Common return appMetadata; } - public byte[] GetApplicationIcon(string applicationPath) + public byte[] GetApplicationIcon(string applicationPath, Language desiredTitleLanguage) { byte[] applicationIcon = null; @@ -600,7 +600,7 @@ namespace Ryujinx.Ui.App.Common { using var icon = new UniqueRef(); - controlFs.OpenFile(ref icon.Ref, $"/icon_{_desiredTitleLanguage}.dat".ToU8Span(), OpenMode.Read).ThrowIfFailure(); + controlFs.OpenFile(ref icon.Ref, $"/icon_{desiredTitleLanguage}.dat".ToU8Span(), OpenMode.Read).ThrowIfFailure(); using MemoryStream stream = new(); diff --git a/src/Ryujinx.Ui.Common/Helper/ShortcutHelper.cs b/src/Ryujinx.Ui.Common/Helper/ShortcutHelper.cs new file mode 100644 index 000000000..dab473fa3 --- /dev/null +++ b/src/Ryujinx.Ui.Common/Helper/ShortcutHelper.cs @@ -0,0 +1,171 @@ +using Ryujinx.Common; +using Ryujinx.Common.Configuration; +using ShellLink; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; +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 +{ + public static class ShortcutHelper + { + [SupportedOSPlatform("windows")] + private static void CreateShortcutWindows(string applicationFilePath, 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); + + var shortcut = Shortcut.CreateShortcut(basePath, GetArgsString(basePath, applicationFilePath), 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) + { + string basePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Ryujinx.sh"); + var desktopFile = EmbeddedResources.ReadAllText("Ryujinx.Ui.Common/shortcut-template.desktop"); + iconPath += ".png"; + + var image = SixLabors.ImageSharp.Image.Load(iconData); + image.SaveAsPng(iconPath); + + using StreamWriter outputFile = new(Path.Combine(desktopPath, cleanedAppName + ".desktop")); + outputFile.Write(desktopFile, cleanedAppName, iconPath, GetArgsString(basePath, applicationFilePath)); + } + + [SupportedOSPlatform("macos")] + private static void CreateShortcutMacos(string appFilePath, byte[] iconData, string desktopPath, string cleanedAppName) + { + string basePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, AppDomain.CurrentDomain.FriendlyName); + var plistFile = EmbeddedResources.ReadAllText("Ryujinx.Ui.Common/shortcut-template.plist"); + // Macos .App folder + string contentFolderPath = Path.Combine(desktopPath, cleanedAppName + ".app", "Contents"); + string scriptFolderPath = Path.Combine(contentFolderPath, "MacOS"); + + if (!Directory.Exists(scriptFolderPath)) + { + Directory.CreateDirectory(scriptFolderPath); + } + + // Runner script + const string ScriptName = "runner.sh"; + string scriptPath = Path.Combine(scriptFolderPath, ScriptName); + using StreamWriter scriptFile = new(scriptPath); + + scriptFile.WriteLine("#!/bin/sh"); + scriptFile.WriteLine(GetArgsString(basePath, appFilePath)); + + // Set execute permission + FileInfo fileInfo = new(scriptPath); + fileInfo.UnixFileMode |= UnixFileMode.UserExecute; + + // img + string resourceFolderPath = Path.Combine(contentFolderPath, "Resources"); + if (!Directory.Exists(resourceFolderPath)) + { + Directory.CreateDirectory(resourceFolderPath); + } + + const string IconName = "icon.png"; + var image = SixLabors.ImageSharp.Image.Load(iconData); + image.SaveAsPng(Path.Combine(resourceFolderPath, IconName)); + + // plist file + using StreamWriter outputFile = new(Path.Combine(contentFolderPath, "Info.plist")); + outputFile.Write(plistFile, ScriptName, cleanedAppName, IconName); + } + + public static void CreateAppShortcut(string applicationFilePath, string applicationName, string applicationId, byte[] iconData) + { + string desktopPath = Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory); + string cleanedAppName = string.Join("_", applicationName.Split(Path.GetInvalidFileNameChars())); + + if (OperatingSystem.IsWindows()) + { + string iconPath = Path.Combine(AppDataManager.BaseDirPath, "games", applicationId, "app"); + + CreateShortcutWindows(applicationFilePath, iconData, iconPath, cleanedAppName, desktopPath); + + return; + } + + if (OperatingSystem.IsLinux()) + { + 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); + + return; + } + + if (OperatingSystem.IsMacOS()) + { + CreateShortcutMacos(applicationFilePath, iconData, desktopPath, cleanedAppName); + + return; + } + + throw new NotImplementedException("Shortcut support has not been implemented yet for this OS."); + } + + private static string GetArgsString(string basePath, string appFilePath) + { + // args are first defined as a list, for easier adjustments in the future + var argsList = new List + { + basePath, + }; + + if (!string.IsNullOrEmpty(CommandLineState.BaseDirPathArg)) + { + argsList.Add("--root-data-dir"); + argsList.Add($"\"{CommandLineState.BaseDirPathArg}\""); + } + + argsList.Add($"\"{appFilePath}\""); + + + return String.Join(" ", argsList); + } + + /// + /// Creates a 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) + { + // 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 }; + using FileStream fs = new(filePath, FileMode.Create); + + fs.Write(header); + // Writing actual data + source.Save(fs, ImageFormat.Png); + // 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)); + } + } +} diff --git a/src/Ryujinx.Ui.Common/Ryujinx.Ui.Common.csproj b/src/Ryujinx.Ui.Common/Ryujinx.Ui.Common.csproj index 511a03897..3da47431f 100644 --- a/src/Ryujinx.Ui.Common/Ryujinx.Ui.Common.csproj +++ b/src/Ryujinx.Ui.Common/Ryujinx.Ui.Common.csproj @@ -45,8 +45,18 @@ + + + + + + + + + + diff --git a/src/Ryujinx/Ryujinx.csproj b/src/Ryujinx/Ryujinx.csproj index cf4435e57..5b5ed4637 100644 --- a/src/Ryujinx/Ryujinx.csproj +++ b/src/Ryujinx/Ryujinx.csproj @@ -63,15 +63,15 @@ - - + + Always - - - Always - mime\Ryujinx.xml - - + + + Always + mime\Ryujinx.xml + + @@ -101,4 +101,4 @@ - \ No newline at end of file + diff --git a/src/Ryujinx/Ui/Widgets/GameTableContextMenu.Designer.cs b/src/Ryujinx/Ui/Widgets/GameTableContextMenu.Designer.cs index 0f7b4f22b..75b166136 100644 --- a/src/Ryujinx/Ui/Widgets/GameTableContextMenu.Designer.cs +++ b/src/Ryujinx/Ui/Widgets/GameTableContextMenu.Designer.cs @@ -23,6 +23,7 @@ namespace Ryujinx.Ui.Widgets private MenuItem _purgeShaderCacheMenuItem; private MenuItem _openPtcDirMenuItem; private MenuItem _openShaderCacheDirMenuItem; + private MenuItem _createShortcutMenuItem; private void InitializeComponent() { @@ -187,6 +188,15 @@ namespace Ryujinx.Ui.Widgets }; _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(); } @@ -213,6 +223,7 @@ namespace Ryujinx.Ui.Widgets Add(new SeparatorMenuItem()); Add(_manageCacheMenuItem); Add(_extractMenuItem); + Add(_createShortcutMenuItem); ShowAll(); } diff --git a/src/Ryujinx/Ui/Widgets/GameTableContextMenu.cs b/src/Ryujinx/Ui/Widgets/GameTableContextMenu.cs index c2e0d8ebc..ea60421f8 100644 --- a/src/Ryujinx/Ui/Widgets/GameTableContextMenu.cs +++ b/src/Ryujinx/Ui/Widgets/GameTableContextMenu.cs @@ -10,6 +10,7 @@ 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; @@ -77,6 +78,8 @@ namespace Ryujinx.Ui.Widgets _extractExeFsMenuItem.Sensitive = hasNca; _extractLogoMenuItem.Sensitive = hasNca; + _createShortcutMenuItem.Sensitive = !ReleaseInformation.IsFlatHubBuild(); + PopupAtPointer(null); } @@ -629,5 +632,11 @@ namespace Ryujinx.Ui.Widgets } } } + + 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); + } } } From 49b37550cae6b3c69f59a9c7a44b17e3c12a813b Mon Sep 17 00:00:00 2001 From: Isaac Marovitz <42140194+IsaacMarovitz@users.noreply.github.com> Date: Sat, 21 Oct 2023 07:26:51 -0400 Subject: [PATCH 096/105] Ava UI: Input Menu Refactor (#4998) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * So much boilerplate * Slow and steady * Restructure + Ack suggestions * Restructure + Ack suggestions * Restructure * Clean * Propogate those fields i forgot about * It builds * Progress * Almost there * Fix stupid mistake * Fix more stupid mistakes * Actually fix fuck ups * Start localising * r/therestofthefuckingowl * Localise ButtonKeyAssigner * Are you feeling it now mr krabs * We’re done at last * Crimes against code * Try me in the Hague * Please be quiet * Crimes are here to stay * Dispose stuff * Cleanup a couple things * Visual fixes and improvements One weird bug * Fix rebase errors * Fixes * Ack Suggestions Remaining ack suggestions Update src/Ryujinx.Ava/UI/Models/Input/ControllerInputConfig.cs Co-authored-by: Ac_K Update src/Ryujinx.Ava/UI/Models/Input/ControllerInputConfig.cs Co-authored-by: Ac_K * Formatting and error More Ava 11-ness Whoops * Code style fixes * Style fixes * Analyzer fix * Remove all ReflectionBindings * Remove ambigious object * Remove redundant property * Old man yells at formatter * r e a d o n l y * Fix profiles * Use new Sliders --------- Co-authored-by: Ac_K --- src/Ryujinx.Ava/Assets/Locales/en_US.json | 99 +++ src/Ryujinx.Ava/Assets/Styles/Styles.xaml | 5 +- .../UI/Helpers/ButtonKeyAssigner.cs | 28 +- .../UI/Helpers/KeyValueConverter.cs | 167 ++++- .../UI/Models/Input/ControllerInputConfig.cs | 580 +++++++++++++++ .../UI/Models/Input/KeyboardInputConfig.cs | 422 +++++++++++ .../UI/Models/InputConfiguration.cs | 456 ------------ .../Input/ControllerInputViewModel.cs | 84 +++ .../InputViewModel.cs} | 103 ++- .../Input/KeyboardInputViewModel.cs | 73 ++ .../{ => Input}/MotionInputViewModel.cs | 2 +- .../{ => Input}/RumbleInputViewModel.cs | 2 +- .../UI/Views/Input/ControllerInputView.axaml | 616 ++++------------ .../Views/Input/ControllerInputView.axaml.cs | 160 +++-- .../UI/Views/Input/InputView.axaml | 225 ++++++ .../UI/Views/Input/InputView.axaml.cs | 61 ++ .../UI/Views/Input/KeyboardInputView.axaml | 675 ++++++++++++++++++ .../UI/Views/Input/KeyboardInputView.axaml.cs | 210 ++++++ .../UI/Views/Input/MotionInputView.axaml | 2 +- .../UI/Views/Input/MotionInputView.axaml.cs | 8 +- .../UI/Views/Input/RumbleInputView.axaml | 2 +- .../UI/Views/Input/RumbleInputView.axaml.cs | 8 +- .../UI/Views/Settings/SettingsInputView.axaml | 4 +- .../Views/Settings/SettingsInputView.axaml.cs | 2 +- .../UI/Windows/SettingsWindow.axaml.cs | 2 +- .../Assigner/GamepadButtonAssigner.cs | 6 +- src/Ryujinx.Input/Assigner/IButtonAssigner.cs | 2 +- .../Assigner/KeyboardKeyAssigner.cs | 10 +- src/Ryujinx.Input/ButtonValue.cs | 48 ++ src/Ryujinx/Ui/Windows/ControllerWindow.cs | 2 +- 30 files changed, 2914 insertions(+), 1150 deletions(-) create mode 100644 src/Ryujinx.Ava/UI/Models/Input/ControllerInputConfig.cs create mode 100644 src/Ryujinx.Ava/UI/Models/Input/KeyboardInputConfig.cs delete mode 100644 src/Ryujinx.Ava/UI/Models/InputConfiguration.cs create mode 100644 src/Ryujinx.Ava/UI/ViewModels/Input/ControllerInputViewModel.cs rename src/Ryujinx.Ava/UI/ViewModels/{ControllerInputViewModel.cs => Input/InputViewModel.cs} (92%) create mode 100644 src/Ryujinx.Ava/UI/ViewModels/Input/KeyboardInputViewModel.cs rename src/Ryujinx.Ava/UI/ViewModels/{ => Input}/MotionInputViewModel.cs (97%) rename src/Ryujinx.Ava/UI/ViewModels/{ => Input}/RumbleInputViewModel.cs (92%) create mode 100644 src/Ryujinx.Ava/UI/Views/Input/InputView.axaml create mode 100644 src/Ryujinx.Ava/UI/Views/Input/InputView.axaml.cs create mode 100644 src/Ryujinx.Ava/UI/Views/Input/KeyboardInputView.axaml create mode 100644 src/Ryujinx.Ava/UI/Views/Input/KeyboardInputView.axaml.cs create mode 100644 src/Ryujinx.Input/ButtonValue.cs diff --git a/src/Ryujinx.Ava/Assets/Locales/en_US.json b/src/Ryujinx.Ava/Assets/Locales/en_US.json index a67b796bd..fc65fe4a0 100644 --- a/src/Ryujinx.Ava/Assets/Locales/en_US.json +++ b/src/Ryujinx.Ava/Assets/Locales/en_US.json @@ -263,6 +263,105 @@ "ControllerSettingsMotionGyroDeadzone": "Gyro Deadzone:", "ControllerSettingsSave": "Save", "ControllerSettingsClose": "Close", + "KeyUnknown": "Unknown", + "KeyShiftLeft": "Shift Left", + "KeyShiftRight": "Shift Right", + "KeyControlLeft": "Control Left", + "KeyControlRight": "Control Right", + "KeyAltLeft": "Alt Left", + "KeyAltRight": "Alt Right", + "KeyOptLeft": "⌥ Left", + "KeyOptRight": "⌥ Right", + "KeyWinLeft": "⊞ Left", + "KeyWinRight": "⊞ Right", + "KeyCmdLeft": "⌘ Left", + "KeyCmdRight": "⌘ 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": "Left Stick Button", + "GamepadRightStick": "Right 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", diff --git a/src/Ryujinx.Ava/Assets/Styles/Styles.xaml b/src/Ryujinx.Ava/Assets/Styles/Styles.xaml index f7f64be22..b3a6f59c8 100644 --- a/src/Ryujinx.Ava/Assets/Styles/Styles.xaml +++ b/src/Ryujinx.Ava/Assets/Styles/Styles.xaml @@ -15,8 +15,7 @@ - + @@ -393,4 +392,4 @@ 600 756 - \ No newline at end of file + diff --git a/src/Ryujinx.Ava/UI/Helpers/ButtonKeyAssigner.cs b/src/Ryujinx.Ava/UI/Helpers/ButtonKeyAssigner.cs index 7e8ba7342..54e0918a5 100644 --- a/src/Ryujinx.Ava/UI/Helpers/ButtonKeyAssigner.cs +++ b/src/Ryujinx.Ava/UI/Helpers/ButtonKeyAssigner.cs @@ -1,11 +1,8 @@ -using Avalonia.Controls; using Avalonia.Controls.Primitives; -using Avalonia.LogicalTree; using Avalonia.Threading; using Ryujinx.Input; using Ryujinx.Input.Assigner; using System; -using System.Linq; using System.Threading.Tasks; namespace Ryujinx.Ava.UI.Helpers @@ -15,12 +12,12 @@ namespace Ryujinx.Ava.UI.Helpers internal class ButtonAssignedEventArgs : EventArgs { public ToggleButton Button { get; } - public bool IsAssigned { get; } + public ButtonValue? ButtonValue { get; } - public ButtonAssignedEventArgs(ToggleButton button, bool isAssigned) + public ButtonAssignedEventArgs(ToggleButton button, ButtonValue? buttonValue) { Button = button; - IsAssigned = isAssigned; + ButtonValue = buttonValue; } } @@ -78,15 +75,11 @@ namespace Ryujinx.Ava.UI.Helpers await Dispatcher.UIThread.InvokeAsync(() => { - string pressedButton = assigner.GetPressedButton(); + ButtonValue? pressedButton = assigner.GetPressedButton(); if (_shouldUnbind) { - SetButtonText(ToggledButton, "Unbound"); - } - else if (pressedButton != "") - { - SetButtonText(ToggledButton, pressedButton); + pressedButton = null; } _shouldUnbind = false; @@ -94,17 +87,8 @@ namespace Ryujinx.Ava.UI.Helpers ToggledButton.IsChecked = false; - ButtonAssigned?.Invoke(this, new ButtonAssignedEventArgs(ToggledButton, pressedButton != null)); + ButtonAssigned?.Invoke(this, new ButtonAssignedEventArgs(ToggledButton, pressedButton)); - static void SetButtonText(ToggleButton button, string text) - { - ILogical textBlock = button.GetLogicalDescendants().First(x => x is TextBlock); - - if (textBlock != null && textBlock is TextBlock block) - { - block.Text = text; - } - } }); } diff --git a/src/Ryujinx.Ava/UI/Helpers/KeyValueConverter.cs b/src/Ryujinx.Ava/UI/Helpers/KeyValueConverter.cs index 028ed6bf4..1c4aa7b21 100644 --- a/src/Ryujinx.Ava/UI/Helpers/KeyValueConverter.cs +++ b/src/Ryujinx.Ava/UI/Helpers/KeyValueConverter.cs @@ -1,7 +1,9 @@ using Avalonia.Data.Converters; +using Ryujinx.Ava.Common.Locale; using Ryujinx.Common.Configuration.Hid; using Ryujinx.Common.Configuration.Hid.Controller; using System; +using System.Collections.Generic; using System.Globalization; namespace Ryujinx.Ava.UI.Helpers @@ -10,37 +12,158 @@ namespace Ryujinx.Ava.UI.Helpers { public static KeyValueConverter Instance = new(); + private static readonly Dictionary _keysMap = new() + { + { Key.Unknown, LocaleKeys.KeyUnknown }, + { Key.ShiftLeft, LocaleKeys.KeyShiftLeft }, + { Key.ShiftRight, LocaleKeys.KeyShiftRight }, + { Key.ControlLeft, LocaleKeys.KeyControlLeft }, + { Key.ControlRight, LocaleKeys.KeyControlRight }, + { Key.AltLeft, OperatingSystem.IsMacOS() ? LocaleKeys.KeyOptLeft : LocaleKeys.KeyAltLeft }, + { Key.AltRight, OperatingSystem.IsMacOS() ? LocaleKeys.KeyOptRight : LocaleKeys.KeyAltRight }, + { Key.WinLeft, OperatingSystem.IsMacOS() ? LocaleKeys.KeyCmdLeft : LocaleKeys.KeyWinLeft }, + { Key.WinRight, OperatingSystem.IsMacOS() ? LocaleKeys.KeyCmdRight : LocaleKeys.KeyWinRight }, + { Key.Up, LocaleKeys.KeyUp }, + { Key.Down, LocaleKeys.KeyDown }, + { Key.Left, LocaleKeys.KeyLeft }, + { Key.Right, LocaleKeys.KeyRight }, + { Key.Enter, LocaleKeys.KeyEnter }, + { Key.Escape, LocaleKeys.KeyEscape }, + { Key.Space, LocaleKeys.KeySpace }, + { Key.Tab, LocaleKeys.KeyTab }, + { Key.BackSpace, LocaleKeys.KeyBackSpace }, + { Key.Insert, LocaleKeys.KeyInsert }, + { Key.Delete, LocaleKeys.KeyDelete }, + { Key.PageUp, LocaleKeys.KeyPageUp }, + { Key.PageDown, LocaleKeys.KeyPageDown }, + { Key.Home, LocaleKeys.KeyHome }, + { Key.End, LocaleKeys.KeyEnd }, + { Key.CapsLock, LocaleKeys.KeyCapsLock }, + { Key.ScrollLock, LocaleKeys.KeyScrollLock }, + { Key.PrintScreen, LocaleKeys.KeyPrintScreen }, + { Key.Pause, LocaleKeys.KeyPause }, + { Key.NumLock, LocaleKeys.KeyNumLock }, + { Key.Clear, LocaleKeys.KeyClear }, + { Key.Keypad0, LocaleKeys.KeyKeypad0 }, + { Key.Keypad1, LocaleKeys.KeyKeypad1 }, + { Key.Keypad2, LocaleKeys.KeyKeypad2 }, + { Key.Keypad3, LocaleKeys.KeyKeypad3 }, + { Key.Keypad4, LocaleKeys.KeyKeypad4 }, + { Key.Keypad5, LocaleKeys.KeyKeypad5 }, + { Key.Keypad6, LocaleKeys.KeyKeypad6 }, + { Key.Keypad7, LocaleKeys.KeyKeypad7 }, + { Key.Keypad8, LocaleKeys.KeyKeypad8 }, + { Key.Keypad9, LocaleKeys.KeyKeypad9 }, + { Key.KeypadDivide, LocaleKeys.KeyKeypadDivide }, + { Key.KeypadMultiply, LocaleKeys.KeyKeypadMultiply }, + { Key.KeypadSubtract, LocaleKeys.KeyKeypadSubtract }, + { Key.KeypadAdd, LocaleKeys.KeyKeypadAdd }, + { Key.KeypadDecimal, LocaleKeys.KeyKeypadDecimal }, + { Key.KeypadEnter, LocaleKeys.KeyKeypadEnter }, + { Key.Number0, LocaleKeys.KeyNumber0 }, + { Key.Number1, LocaleKeys.KeyNumber1 }, + { Key.Number2, LocaleKeys.KeyNumber2 }, + { Key.Number3, LocaleKeys.KeyNumber3 }, + { Key.Number4, LocaleKeys.KeyNumber4 }, + { Key.Number5, LocaleKeys.KeyNumber5 }, + { Key.Number6, LocaleKeys.KeyNumber6 }, + { Key.Number7, LocaleKeys.KeyNumber7 }, + { Key.Number8, LocaleKeys.KeyNumber8 }, + { Key.Number9, LocaleKeys.KeyNumber9 }, + { Key.Tilde, LocaleKeys.KeyTilde }, + { Key.Grave, LocaleKeys.KeyGrave }, + { Key.Minus, LocaleKeys.KeyMinus }, + { Key.Plus, LocaleKeys.KeyPlus }, + { Key.BracketLeft, LocaleKeys.KeyBracketLeft }, + { Key.BracketRight, LocaleKeys.KeyBracketRight }, + { Key.Semicolon, LocaleKeys.KeySemicolon }, + { Key.Quote, LocaleKeys.KeyQuote }, + { Key.Comma, LocaleKeys.KeyComma }, + { Key.Period, LocaleKeys.KeyPeriod }, + { Key.Slash, LocaleKeys.KeySlash }, + { Key.BackSlash, LocaleKeys.KeyBackSlash }, + { Key.Unbound, LocaleKeys.KeyUnbound }, + }; + + private static readonly Dictionary _gamepadInputIdMap = new() + { + { GamepadInputId.LeftStick, LocaleKeys.GamepadLeftStick }, + { GamepadInputId.RightStick, LocaleKeys.GamepadRightStick }, + { GamepadInputId.LeftShoulder, LocaleKeys.GamepadLeftShoulder }, + { GamepadInputId.RightShoulder, LocaleKeys.GamepadRightShoulder }, + { GamepadInputId.LeftTrigger, LocaleKeys.GamepadLeftTrigger }, + { GamepadInputId.RightTrigger, LocaleKeys.GamepadRightTrigger }, + { GamepadInputId.DpadUp, LocaleKeys.GamepadDpadUp}, + { GamepadInputId.DpadDown, LocaleKeys.GamepadDpadDown}, + { GamepadInputId.DpadLeft, LocaleKeys.GamepadDpadLeft}, + { GamepadInputId.DpadRight, LocaleKeys.GamepadDpadRight}, + { GamepadInputId.Minus, LocaleKeys.GamepadMinus}, + { GamepadInputId.Plus, LocaleKeys.GamepadPlus}, + { GamepadInputId.Guide, LocaleKeys.GamepadGuide}, + { GamepadInputId.Misc1, LocaleKeys.GamepadMisc1}, + { GamepadInputId.Paddle1, LocaleKeys.GamepadPaddle1}, + { GamepadInputId.Paddle2, LocaleKeys.GamepadPaddle2}, + { GamepadInputId.Paddle3, LocaleKeys.GamepadPaddle3}, + { GamepadInputId.Paddle4, LocaleKeys.GamepadPaddle4}, + { GamepadInputId.Touchpad, LocaleKeys.GamepadTouchpad}, + { GamepadInputId.SingleLeftTrigger0, LocaleKeys.GamepadSingleLeftTrigger0}, + { GamepadInputId.SingleRightTrigger0, LocaleKeys.GamepadSingleRightTrigger0}, + { GamepadInputId.SingleLeftTrigger1, LocaleKeys.GamepadSingleLeftTrigger1}, + { GamepadInputId.SingleRightTrigger1, LocaleKeys.GamepadSingleRightTrigger1}, + { GamepadInputId.Unbound, LocaleKeys.KeyUnbound}, + }; + + private static readonly Dictionary _stickInputIdMap = new() + { + { StickInputId.Left, LocaleKeys.StickLeft}, + { StickInputId.Right, LocaleKeys.StickRight}, + { StickInputId.Unbound, LocaleKeys.KeyUnbound}, + }; + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { - if (value == null) + string keyString = ""; + + if (value is Key key) { - return null; + if (_keysMap.TryGetValue(key, out LocaleKeys localeKey)) + { + keyString = LocaleManager.Instance[localeKey]; + } + else + { + keyString = key.ToString(); + } + } + else if (value is GamepadInputId gamepadInputId) + { + if (_gamepadInputIdMap.TryGetValue(gamepadInputId, out LocaleKeys localeKey)) + { + keyString = LocaleManager.Instance[localeKey]; + } + else + { + keyString = gamepadInputId.ToString(); + } + } + else if (value is StickInputId stickInputId) + { + if (_stickInputIdMap.TryGetValue(stickInputId, out LocaleKeys localeKey)) + { + keyString = LocaleManager.Instance[localeKey]; + } + else + { + keyString = stickInputId.ToString(); + } } - return value.ToString(); + return keyString; } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { - object key = null; - - if (value != null) - { - if (targetType == typeof(Key)) - { - key = Enum.Parse(value.ToString()); - } - else if (targetType == typeof(GamepadInputId)) - { - key = Enum.Parse(value.ToString()); - } - else if (targetType == typeof(StickInputId)) - { - key = Enum.Parse(value.ToString()); - } - } - - return key; + throw new NotSupportedException(); } } } diff --git a/src/Ryujinx.Ava/UI/Models/Input/ControllerInputConfig.cs b/src/Ryujinx.Ava/UI/Models/Input/ControllerInputConfig.cs new file mode 100644 index 000000000..4929e582e --- /dev/null +++ b/src/Ryujinx.Ava/UI/Models/Input/ControllerInputConfig.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 ControllerInputConfig : 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 ControllerInputConfig(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 MotionConfigController + { + EnableMotion = EnableMotion, + MotionBackend = MotionInputBackendType.GamepadDriver, + GyroDeadzone = GyroDeadzone, + Sensitivity = Sensitivity + }; + } + + return config; + } + } +} diff --git a/src/Ryujinx.Ava/UI/Models/Input/KeyboardInputConfig.cs b/src/Ryujinx.Ava/UI/Models/Input/KeyboardInputConfig.cs new file mode 100644 index 000000000..029565210 --- /dev/null +++ b/src/Ryujinx.Ava/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.Ava/UI/Models/InputConfiguration.cs b/src/Ryujinx.Ava/UI/Models/InputConfiguration.cs deleted file mode 100644 index f1352c6d8..000000000 --- a/src/Ryujinx.Ava/UI/Models/InputConfiguration.cs +++ /dev/null @@ -1,456 +0,0 @@ -using Ryujinx.Ava.UI.ViewModels; -using Ryujinx.Common.Configuration.Hid; -using Ryujinx.Common.Configuration.Hid.Controller; -using Ryujinx.Common.Configuration.Hid.Controller.Motion; -using Ryujinx.Common.Configuration.Hid.Keyboard; -using System; - -namespace Ryujinx.Ava.UI.Models -{ - internal class InputConfiguration : BaseModel - { - private float _deadzoneRight; - private float _triggerThreshold; - private float _deadzoneLeft; - private double _gyroDeadzone; - private int _sensitivity; - private bool _enableMotion; - private float _weakRumble; - private float _strongRumble; - private float _rangeLeft; - private float _rangeRight; - - public InputBackendType Backend { get; set; } - - /// - /// Controller id - /// - public string Id { get; set; } - - /// - /// Controller's Type - /// - public ControllerType ControllerType { get; set; } - - /// - /// Player's Index for the controller - /// - public PlayerIndex PlayerIndex { get; set; } - - public TStick LeftJoystick { get; set; } - public bool LeftInvertStickX { get; set; } - public bool LeftInvertStickY { get; set; } - public bool RightRotate90 { get; set; } - public TKey LeftControllerStickButton { get; set; } - - public TStick RightJoystick { get; set; } - public bool RightInvertStickX { get; set; } - public bool RightInvertStickY { get; set; } - public bool LeftRotate90 { get; set; } - public TKey RightControllerStickButton { get; set; } - - public float DeadzoneLeft - { - get => _deadzoneLeft; - set - { - _deadzoneLeft = MathF.Round(value, 3); - - OnPropertyChanged(); - } - } - - public float RangeLeft - { - get => _rangeLeft; - set - { - _rangeLeft = MathF.Round(value, 3); - - OnPropertyChanged(); - } - } - - public float DeadzoneRight - { - get => _deadzoneRight; - set - { - _deadzoneRight = MathF.Round(value, 3); - - OnPropertyChanged(); - } - } - - public float RangeRight - { - get => _rangeRight; - set - { - _rangeRight = MathF.Round(value, 3); - - OnPropertyChanged(); - } - } - - public float TriggerThreshold - { - get => _triggerThreshold; - set - { - _triggerThreshold = MathF.Round(value, 3); - - OnPropertyChanged(); - } - } - - public MotionInputBackendType MotionBackend { get; set; } - - public TKey ButtonMinus { get; set; } - public TKey ButtonL { get; set; } - public TKey ButtonZl { get; set; } - public TKey LeftButtonSl { get; set; } - public TKey LeftButtonSr { get; set; } - public TKey DpadUp { get; set; } - public TKey DpadDown { get; set; } - public TKey DpadLeft { get; set; } - public TKey DpadRight { get; set; } - - public TKey ButtonPlus { get; set; } - public TKey ButtonR { get; set; } - public TKey ButtonZr { get; set; } - public TKey RightButtonSl { get; set; } - public TKey RightButtonSr { get; set; } - public TKey ButtonX { get; set; } - public TKey ButtonB { get; set; } - public TKey ButtonY { get; set; } - public TKey ButtonA { get; set; } - - public TKey LeftStickUp { get; set; } - public TKey LeftStickDown { get; set; } - public TKey LeftStickLeft { get; set; } - public TKey LeftStickRight { get; set; } - public TKey LeftKeyboardStickButton { get; set; } - - public TKey RightStickUp { get; set; } - public TKey RightStickDown { get; set; } - public TKey RightStickLeft { get; set; } - public TKey RightStickRight { get; set; } - public TKey RightKeyboardStickButton { get; set; } - - public int Sensitivity - { - get => _sensitivity; - set - { - _sensitivity = value; - - OnPropertyChanged(); - } - } - - public double GyroDeadzone - { - get => _gyroDeadzone; - set - { - _gyroDeadzone = Math.Round(value, 3); - - OnPropertyChanged(); - } - } - - public bool EnableMotion - { - get => _enableMotion; set - { - _enableMotion = value; - - OnPropertyChanged(); - } - } - - public bool EnableCemuHookMotion { get; set; } - public int Slot { get; set; } - public int AltSlot { get; set; } - public bool MirrorInput { get; set; } - public string DsuServerHost { get; set; } - public int DsuServerPort { get; set; } - - public bool EnableRumble { get; set; } - public float WeakRumble - { - get => _weakRumble; set - { - _weakRumble = value; - - OnPropertyChanged(); - } - } - public float StrongRumble - { - get => _strongRumble; set - { - _strongRumble = value; - - OnPropertyChanged(); - } - } - - public InputConfiguration(InputConfig config) - { - if (config != null) - { - Backend = config.Backend; - Id = config.Id; - ControllerType = config.ControllerType; - PlayerIndex = config.PlayerIndex; - - if (config is StandardKeyboardInputConfig keyboardConfig) - { - LeftStickUp = (TKey)(object)keyboardConfig.LeftJoyconStick.StickUp; - LeftStickDown = (TKey)(object)keyboardConfig.LeftJoyconStick.StickDown; - LeftStickLeft = (TKey)(object)keyboardConfig.LeftJoyconStick.StickLeft; - LeftStickRight = (TKey)(object)keyboardConfig.LeftJoyconStick.StickRight; - LeftKeyboardStickButton = (TKey)(object)keyboardConfig.LeftJoyconStick.StickButton; - - RightStickUp = (TKey)(object)keyboardConfig.RightJoyconStick.StickUp; - RightStickDown = (TKey)(object)keyboardConfig.RightJoyconStick.StickDown; - RightStickLeft = (TKey)(object)keyboardConfig.RightJoyconStick.StickLeft; - RightStickRight = (TKey)(object)keyboardConfig.RightJoyconStick.StickRight; - RightKeyboardStickButton = (TKey)(object)keyboardConfig.RightJoyconStick.StickButton; - - ButtonA = (TKey)(object)keyboardConfig.RightJoycon.ButtonA; - ButtonB = (TKey)(object)keyboardConfig.RightJoycon.ButtonB; - ButtonX = (TKey)(object)keyboardConfig.RightJoycon.ButtonX; - ButtonY = (TKey)(object)keyboardConfig.RightJoycon.ButtonY; - ButtonR = (TKey)(object)keyboardConfig.RightJoycon.ButtonR; - RightButtonSl = (TKey)(object)keyboardConfig.RightJoycon.ButtonSl; - RightButtonSr = (TKey)(object)keyboardConfig.RightJoycon.ButtonSr; - ButtonZr = (TKey)(object)keyboardConfig.RightJoycon.ButtonZr; - ButtonPlus = (TKey)(object)keyboardConfig.RightJoycon.ButtonPlus; - - DpadUp = (TKey)(object)keyboardConfig.LeftJoycon.DpadUp; - DpadDown = (TKey)(object)keyboardConfig.LeftJoycon.DpadDown; - DpadLeft = (TKey)(object)keyboardConfig.LeftJoycon.DpadLeft; - DpadRight = (TKey)(object)keyboardConfig.LeftJoycon.DpadRight; - ButtonMinus = (TKey)(object)keyboardConfig.LeftJoycon.ButtonMinus; - LeftButtonSl = (TKey)(object)keyboardConfig.LeftJoycon.ButtonSl; - LeftButtonSr = (TKey)(object)keyboardConfig.LeftJoycon.ButtonSr; - ButtonZl = (TKey)(object)keyboardConfig.LeftJoycon.ButtonZl; - ButtonL = (TKey)(object)keyboardConfig.LeftJoycon.ButtonL; - } - else if (config is StandardControllerInputConfig controllerConfig) - { - LeftJoystick = (TStick)(object)controllerConfig.LeftJoyconStick.Joystick; - LeftInvertStickX = controllerConfig.LeftJoyconStick.InvertStickX; - LeftInvertStickY = controllerConfig.LeftJoyconStick.InvertStickY; - LeftRotate90 = controllerConfig.LeftJoyconStick.Rotate90CW; - LeftControllerStickButton = (TKey)(object)controllerConfig.LeftJoyconStick.StickButton; - - RightJoystick = (TStick)(object)controllerConfig.RightJoyconStick.Joystick; - RightInvertStickX = controllerConfig.RightJoyconStick.InvertStickX; - RightInvertStickY = controllerConfig.RightJoyconStick.InvertStickY; - RightRotate90 = controllerConfig.RightJoyconStick.Rotate90CW; - RightControllerStickButton = (TKey)(object)controllerConfig.RightJoyconStick.StickButton; - - ButtonA = (TKey)(object)controllerConfig.RightJoycon.ButtonA; - ButtonB = (TKey)(object)controllerConfig.RightJoycon.ButtonB; - ButtonX = (TKey)(object)controllerConfig.RightJoycon.ButtonX; - ButtonY = (TKey)(object)controllerConfig.RightJoycon.ButtonY; - ButtonR = (TKey)(object)controllerConfig.RightJoycon.ButtonR; - RightButtonSl = (TKey)(object)controllerConfig.RightJoycon.ButtonSl; - RightButtonSr = (TKey)(object)controllerConfig.RightJoycon.ButtonSr; - ButtonZr = (TKey)(object)controllerConfig.RightJoycon.ButtonZr; - ButtonPlus = (TKey)(object)controllerConfig.RightJoycon.ButtonPlus; - - DpadUp = (TKey)(object)controllerConfig.LeftJoycon.DpadUp; - DpadDown = (TKey)(object)controllerConfig.LeftJoycon.DpadDown; - DpadLeft = (TKey)(object)controllerConfig.LeftJoycon.DpadLeft; - DpadRight = (TKey)(object)controllerConfig.LeftJoycon.DpadRight; - ButtonMinus = (TKey)(object)controllerConfig.LeftJoycon.ButtonMinus; - LeftButtonSl = (TKey)(object)controllerConfig.LeftJoycon.ButtonSl; - LeftButtonSr = (TKey)(object)controllerConfig.LeftJoycon.ButtonSr; - ButtonZl = (TKey)(object)controllerConfig.LeftJoycon.ButtonZl; - ButtonL = (TKey)(object)controllerConfig.LeftJoycon.ButtonL; - - DeadzoneLeft = controllerConfig.DeadzoneLeft; - DeadzoneRight = controllerConfig.DeadzoneRight; - RangeLeft = controllerConfig.RangeLeft; - RangeRight = controllerConfig.RangeRight; - TriggerThreshold = controllerConfig.TriggerThreshold; - - if (controllerConfig.Motion != null) - { - EnableMotion = controllerConfig.Motion.EnableMotion; - MotionBackend = controllerConfig.Motion.MotionBackend; - GyroDeadzone = controllerConfig.Motion.GyroDeadzone; - Sensitivity = controllerConfig.Motion.Sensitivity; - - if (controllerConfig.Motion is CemuHookMotionConfigController cemuHook) - { - EnableCemuHookMotion = true; - DsuServerHost = cemuHook.DsuServerHost; - DsuServerPort = cemuHook.DsuServerPort; - Slot = cemuHook.Slot; - AltSlot = cemuHook.AltSlot; - MirrorInput = cemuHook.MirrorInput; - } - - if (controllerConfig.Rumble != null) - { - EnableRumble = controllerConfig.Rumble.EnableRumble; - WeakRumble = controllerConfig.Rumble.WeakRumble; - StrongRumble = controllerConfig.Rumble.StrongRumble; - } - } - } - } - } - - public InputConfiguration() - { - } - - public InputConfig GetConfig() - { - if (Backend == InputBackendType.WindowKeyboard) - { - return new StandardKeyboardInputConfig - { - Id = Id, - Backend = Backend, - PlayerIndex = PlayerIndex, - ControllerType = ControllerType, - LeftJoycon = new LeftJoyconCommonConfig - { - DpadUp = (Key)(object)DpadUp, - DpadDown = (Key)(object)DpadDown, - DpadLeft = (Key)(object)DpadLeft, - DpadRight = (Key)(object)DpadRight, - ButtonL = (Key)(object)ButtonL, - ButtonZl = (Key)(object)ButtonZl, - ButtonSl = (Key)(object)LeftButtonSl, - ButtonSr = (Key)(object)LeftButtonSr, - ButtonMinus = (Key)(object)ButtonMinus, - }, - RightJoycon = new RightJoyconCommonConfig - { - ButtonA = (Key)(object)ButtonA, - ButtonB = (Key)(object)ButtonB, - ButtonX = (Key)(object)ButtonX, - ButtonY = (Key)(object)ButtonY, - ButtonPlus = (Key)(object)ButtonPlus, - ButtonSl = (Key)(object)RightButtonSl, - ButtonSr = (Key)(object)RightButtonSr, - ButtonR = (Key)(object)ButtonR, - ButtonZr = (Key)(object)ButtonZr, - }, - LeftJoyconStick = new JoyconConfigKeyboardStick - { - StickUp = (Key)(object)LeftStickUp, - StickDown = (Key)(object)LeftStickDown, - StickRight = (Key)(object)LeftStickRight, - StickLeft = (Key)(object)LeftStickLeft, - StickButton = (Key)(object)LeftKeyboardStickButton, - }, - RightJoyconStick = new JoyconConfigKeyboardStick - { - StickUp = (Key)(object)RightStickUp, - StickDown = (Key)(object)RightStickDown, - StickLeft = (Key)(object)RightStickLeft, - StickRight = (Key)(object)RightStickRight, - StickButton = (Key)(object)RightKeyboardStickButton, - }, - Version = InputConfig.CurrentVersion, - }; - - } - - if (Backend == InputBackendType.GamepadSDL2) - { - var config = new StandardControllerInputConfig - { - Id = Id, - Backend = Backend, - PlayerIndex = PlayerIndex, - ControllerType = ControllerType, - LeftJoycon = new LeftJoyconCommonConfig - { - DpadUp = (GamepadInputId)(object)DpadUp, - DpadDown = (GamepadInputId)(object)DpadDown, - DpadLeft = (GamepadInputId)(object)DpadLeft, - DpadRight = (GamepadInputId)(object)DpadRight, - ButtonL = (GamepadInputId)(object)ButtonL, - ButtonZl = (GamepadInputId)(object)ButtonZl, - ButtonSl = (GamepadInputId)(object)LeftButtonSl, - ButtonSr = (GamepadInputId)(object)LeftButtonSr, - ButtonMinus = (GamepadInputId)(object)ButtonMinus, - }, - RightJoycon = new RightJoyconCommonConfig - { - ButtonA = (GamepadInputId)(object)ButtonA, - ButtonB = (GamepadInputId)(object)ButtonB, - ButtonX = (GamepadInputId)(object)ButtonX, - ButtonY = (GamepadInputId)(object)ButtonY, - ButtonPlus = (GamepadInputId)(object)ButtonPlus, - ButtonSl = (GamepadInputId)(object)RightButtonSl, - ButtonSr = (GamepadInputId)(object)RightButtonSr, - ButtonR = (GamepadInputId)(object)ButtonR, - ButtonZr = (GamepadInputId)(object)ButtonZr, - }, - LeftJoyconStick = new JoyconConfigControllerStick - { - Joystick = (StickInputId)(object)LeftJoystick, - InvertStickX = LeftInvertStickX, - InvertStickY = LeftInvertStickY, - Rotate90CW = LeftRotate90, - StickButton = (GamepadInputId)(object)LeftControllerStickButton, - }, - RightJoyconStick = new JoyconConfigControllerStick - { - Joystick = (StickInputId)(object)RightJoystick, - InvertStickX = RightInvertStickX, - InvertStickY = RightInvertStickY, - Rotate90CW = RightRotate90, - StickButton = (GamepadInputId)(object)RightControllerStickButton, - }, - Rumble = new RumbleConfigController - { - EnableRumble = EnableRumble, - WeakRumble = WeakRumble, - StrongRumble = StrongRumble, - }, - Version = InputConfig.CurrentVersion, - DeadzoneLeft = DeadzoneLeft, - DeadzoneRight = DeadzoneRight, - RangeLeft = RangeLeft, - RangeRight = RangeRight, - TriggerThreshold = TriggerThreshold, - Motion = EnableCemuHookMotion - ? new CemuHookMotionConfigController - { - DsuServerHost = DsuServerHost, - DsuServerPort = DsuServerPort, - Slot = Slot, - AltSlot = AltSlot, - MirrorInput = MirrorInput, - MotionBackend = MotionInputBackendType.CemuHook, - } - : new StandardMotionConfigController - { - MotionBackend = MotionInputBackendType.GamepadDriver, - }, - }; - - config.Motion.Sensitivity = Sensitivity; - config.Motion.EnableMotion = EnableMotion; - config.Motion.GyroDeadzone = GyroDeadzone; - - return config; - } - - return null; - } - } -} diff --git a/src/Ryujinx.Ava/UI/ViewModels/Input/ControllerInputViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/Input/ControllerInputViewModel.cs new file mode 100644 index 000000000..0e23dfa76 --- /dev/null +++ b/src/Ryujinx.Ava/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 ControllerInputConfig _config; + public ControllerInputConfig 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 InputViewModel parentModel; + + public ControllerInputViewModel(InputViewModel model, ControllerInputConfig 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.Ava/UI/ViewModels/ControllerInputViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/Input/InputViewModel.cs similarity index 92% rename from src/Ryujinx.Ava/UI/ViewModels/ControllerInputViewModel.cs rename to src/Ryujinx.Ava/UI/ViewModels/Input/InputViewModel.cs index c0c625321..ef8ffd50d 100644 --- a/src/Ryujinx.Ava/UI/ViewModels/ControllerInputViewModel.cs +++ b/src/Ryujinx.Ava/UI/ViewModels/Input/InputViewModel.cs @@ -8,7 +8,7 @@ using Ryujinx.Ava.Common.Locale; using Ryujinx.Ava.Input; using Ryujinx.Ava.UI.Helpers; using Ryujinx.Ava.UI.Models; -using Ryujinx.Ava.UI.Views.Input; +using Ryujinx.Ava.UI.Models.Input; using Ryujinx.Ava.UI.Windows; using Ryujinx.Common; using Ryujinx.Common.Configuration; @@ -30,9 +30,9 @@ using ConfigGamepadInputId = Ryujinx.Common.Configuration.Hid.Controller.Gamepad using ConfigStickInputId = Ryujinx.Common.Configuration.Hid.Controller.StickInputId; using Key = Ryujinx.Common.Configuration.Hid.Key; -namespace Ryujinx.Ava.UI.ViewModels +namespace Ryujinx.Ava.UI.ViewModels.Input { - public class ControllerInputViewModel : BaseModel, IDisposable + public class InputViewModel : BaseModel, IDisposable { private const string Disabled = "disabled"; private const string ProControllerResource = "Ryujinx.Ui.Common/Resources/Controller_ProCon.svg"; @@ -48,7 +48,7 @@ namespace Ryujinx.Ava.UI.ViewModels private int _controllerNumber; private string _controllerImage; private int _device; - private object _configuration; + private object _configViewModel; private string _profileName; private bool _isLoaded; @@ -71,13 +71,14 @@ namespace Ryujinx.Ava.UI.ViewModels public bool IsLeft { get; set; } public bool IsModified { get; set; } + public event Action NotifyChangesEvent; - public object Configuration + public object ConfigViewModel { - get => _configuration; + get => _configViewModel; set { - _configuration = value; + _configViewModel = value; OnPropertyChanged(); } @@ -232,7 +233,7 @@ namespace Ryujinx.Ava.UI.ViewModels public InputConfig Config { get; set; } - public ControllerInputViewModel(UserControl owner) : this() + public InputViewModel(UserControl owner) : this() { if (Program.PreviewerDetached) { @@ -244,7 +245,6 @@ namespace Ryujinx.Ava.UI.ViewModels _mainWindow.InputManager.GamepadDriver.OnGamepadConnected += HandleOnGamepadConnected; _mainWindow.InputManager.GamepadDriver.OnGamepadDisconnected += HandleOnGamepadDisconnected; - _mainWindow.ViewModel.AppHost?.NpadManager.BlockInputUpdates(); _isLoaded = false; @@ -255,7 +255,7 @@ namespace Ryujinx.Ava.UI.ViewModels } } - public ControllerInputViewModel() + public InputViewModel() { PlayerIndexes = new ObservableCollection(); Controllers = new ObservableCollection(); @@ -282,12 +282,12 @@ namespace Ryujinx.Ava.UI.ViewModels if (Config is StandardKeyboardInputConfig keyboardInputConfig) { - Configuration = new InputConfiguration(keyboardInputConfig); + ConfigViewModel = new KeyboardInputViewModel(this, new KeyboardInputConfig(keyboardInputConfig)); } if (Config is StandardControllerInputConfig controllerInputConfig) { - Configuration = new InputConfiguration(controllerInputConfig); + ConfigViewModel = new ControllerInputViewModel(this, new ControllerInputConfig(controllerInputConfig)); } } @@ -323,16 +323,6 @@ namespace Ryujinx.Ava.UI.ViewModels } } - public async void ShowMotionConfig() - { - await MotionInputView.Show(this); - } - - public async void ShowRumbleConfig() - { - await RumbleInputView.Show(this); - } - private void LoadInputDriver() { if (_device < 0) @@ -740,7 +730,7 @@ namespace Ryujinx.Ava.UI.ViewModels return; } - if (Configuration == null) + if (ConfigViewModel == null) { return; } @@ -751,35 +741,37 @@ namespace Ryujinx.Ava.UI.ViewModels return; } - - bool validFileName = ProfileName.IndexOfAny(Path.GetInvalidFileNameChars()) == -1; - - if (validFileName) - { - string path = Path.Combine(GetProfileBasePath(), ProfileName + ".json"); - - InputConfig config = null; - - if (IsKeyboard) - { - config = (Configuration as InputConfiguration).GetConfig(); - } - else if (IsController) - { - config = (Configuration as InputConfiguration).GetConfig(); - } - - config.ControllerType = Controllers[_controller].Type; - - string jsonString = JsonHelper.Serialize(config, _serializerContext.InputConfig); - - await File.WriteAllTextAsync(path, jsonString); - - LoadProfiles(); - } else { - await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogProfileInvalidProfileNameErrorMessage]); + bool validFileName = ProfileName.IndexOfAny(Path.GetInvalidFileNameChars()) == -1; + + if (validFileName) + { + string path = Path.Combine(GetProfileBasePath(), ProfileName + ".json"); + + InputConfig config = null; + + if (IsKeyboard) + { + config = (ConfigViewModel as KeyboardInputViewModel).Config.GetConfig(); + } + else if (IsController) + { + config = (ConfigViewModel as ControllerInputViewModel).Config.GetConfig(); + } + + config.ControllerType = Controllers[_controller].Type; + + string jsonString = JsonHelper.Serialize(config, _serializerContext.InputConfig); + + await File.WriteAllTextAsync(path, jsonString); + + LoadProfiles(); + } + else + { + await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogProfileInvalidProfileNameErrorMessage]); + } } } @@ -830,18 +822,18 @@ namespace Ryujinx.Ava.UI.ViewModels if (device.Type == DeviceType.Keyboard) { - var inputConfig = Configuration as InputConfiguration; + var inputConfig = (ConfigViewModel as KeyboardInputViewModel).Config; inputConfig.Id = device.Id; } else { - var inputConfig = Configuration as InputConfiguration; + var inputConfig = (ConfigViewModel as ControllerInputViewModel).Config; inputConfig.Id = device.Id.Split(" ")[0]; } var config = !IsController - ? (Configuration as InputConfiguration).GetConfig() - : (Configuration as InputConfiguration).GetConfig(); + ? (ConfigViewModel as KeyboardInputViewModel).Config.GetConfig() + : (ConfigViewModel as ControllerInputViewModel).Config.GetConfig(); config.ControllerType = Controllers[_controller].Type; config.PlayerIndex = _playerId; @@ -872,12 +864,13 @@ namespace Ryujinx.Ava.UI.ViewModels public void NotifyChanges() { - OnPropertyChanged(nameof(Configuration)); + OnPropertyChanged(nameof(ConfigViewModel)); OnPropertyChanged(nameof(IsController)); OnPropertyChanged(nameof(ShowSettings)); OnPropertyChanged(nameof(IsKeyboard)); OnPropertyChanged(nameof(IsRight)); OnPropertyChanged(nameof(IsLeft)); + NotifyChangesEvent?.Invoke(); } public void Dispose() diff --git a/src/Ryujinx.Ava/UI/ViewModels/Input/KeyboardInputViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/Input/KeyboardInputViewModel.cs new file mode 100644 index 000000000..a93873063 --- /dev/null +++ b/src/Ryujinx.Ava/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 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.Ava/UI/ViewModels/MotionInputViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/Input/MotionInputViewModel.cs similarity index 97% rename from src/Ryujinx.Ava/UI/ViewModels/MotionInputViewModel.cs rename to src/Ryujinx.Ava/UI/ViewModels/Input/MotionInputViewModel.cs index 0b12a51f6..c9ed8f2d4 100644 --- a/src/Ryujinx.Ava/UI/ViewModels/MotionInputViewModel.cs +++ b/src/Ryujinx.Ava/UI/ViewModels/Input/MotionInputViewModel.cs @@ -1,4 +1,4 @@ -namespace Ryujinx.Ava.UI.ViewModels +namespace Ryujinx.Ava.UI.ViewModels.Input { public class MotionInputViewModel : BaseModel { diff --git a/src/Ryujinx.Ava/UI/ViewModels/RumbleInputViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/Input/RumbleInputViewModel.cs similarity index 92% rename from src/Ryujinx.Ava/UI/ViewModels/RumbleInputViewModel.cs rename to src/Ryujinx.Ava/UI/ViewModels/Input/RumbleInputViewModel.cs index 49de19937..8ad33cf4c 100644 --- a/src/Ryujinx.Ava/UI/ViewModels/RumbleInputViewModel.cs +++ b/src/Ryujinx.Ava/UI/ViewModels/Input/RumbleInputViewModel.cs @@ -1,4 +1,4 @@ -namespace Ryujinx.Ava.UI.ViewModels +namespace Ryujinx.Ava.UI.ViewModels.Input { public class RumbleInputViewModel : BaseModel { diff --git a/src/Ryujinx.Ava/UI/Views/Input/ControllerInputView.axaml b/src/Ryujinx.Ava/UI/Views/Input/ControllerInputView.axaml index d636873a3..08bdf90f4 100644 --- a/src/Ryujinx.Ava/UI/Views/Input/ControllerInputView.axaml +++ b/src/Ryujinx.Ava/UI/Views/Input/ControllerInputView.axaml @@ -1,13 +1,11 @@ @@ -34,191 +33,10 @@ HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Orientation="Vertical"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + MinHeight="450"> @@ -257,9 +75,9 @@ VerticalAlignment="Center" Text="{locale:Locale ControllerSettingsTriggerZL}" TextAlignment="Center" /> - + @@ -273,9 +91,9 @@ VerticalAlignment="Center" Text="{locale:Locale ControllerSettingsTriggerL}" TextAlignment="Center" /> - + @@ -289,9 +107,9 @@ VerticalAlignment="Center" Text="{locale:Locale ControllerSettingsButtonMinus}" TextAlignment="Center" /> - + @@ -311,100 +129,8 @@ Margin="0,0,0,10" HorizontalAlignment="Center" Text="{locale:Locale ControllerSettingsLStick}" /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + @@ -415,9 +141,9 @@ VerticalAlignment="Center" Text="{locale:Locale ControllerSettingsStickButton}" TextAlignment="Center" /> - + @@ -432,22 +158,22 @@ VerticalAlignment="Center" Text="{locale:Locale ControllerSettingsStickStick}" TextAlignment="Center" /> - + - + - + - + + Value="{Binding Config.DeadzoneLeft, Mode=TwoWay}" /> + Text="{Binding Config.DeadzoneLeft, StringFormat=\{0:0.00\}}" /> + Value="{Binding Config.RangeLeft, Mode=TwoWay}" /> + Text="{Binding Config.RangeLeft, StringFormat=\{0:0.00\}}" /> @@ -525,9 +251,9 @@ VerticalAlignment="Center" Text="{locale:Locale ControllerSettingsDPadUp}" TextAlignment="Center" /> - + @@ -542,9 +268,9 @@ VerticalAlignment="Center" Text="{locale:Locale ControllerSettingsDPadDown}" TextAlignment="Center" /> - + @@ -559,9 +285,9 @@ VerticalAlignment="Center" Text="{locale:Locale ControllerSettingsDPadLeft}" TextAlignment="Center" /> - + @@ -576,9 +302,9 @@ VerticalAlignment="Center" Text="{locale:Locale ControllerSettingsDPadRight}" TextAlignment="Center" /> - + @@ -591,6 +317,13 @@ Grid.Column="1" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"> + + + Value="{Binding Config.TriggerThreshold, Mode=TwoWay}" /> + Text="{Binding Config.TriggerThreshold, StringFormat=\{0:0.00\}}" /> - + - + IsVisible="{Binding IsLeft}" + Orientation="Horizontal"> - - - - + + + + - + IsVisible="{Binding IsLeft}" + Orientation="Horizontal"> - - - - + + + + - + IsVisible="{Binding IsRight}" + Orientation="Horizontal"> - - - - + + + + - + IsVisible="{Binding IsRight}" + Orientation="Horizontal"> - + + + + - - + HorizontalAlignment="Stretch"> @@ -720,7 +449,7 @@ Margin="10" MinWidth="0" Grid.Column="0" - IsChecked="{ReflectionBinding Configuration.EnableMotion, Mode=TwoWay}"> + IsChecked="{Binding Config.EnableMotion, Mode=TwoWay}"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Ryujinx.Ava/UI/Views/Input/InputView.axaml.cs b/src/Ryujinx.Ava/UI/Views/Input/InputView.axaml.cs new file mode 100644 index 000000000..356381a8a --- /dev/null +++ b/src/Ryujinx.Ava/UI/Views/Input/InputView.axaml.cs @@ -0,0 +1,61 @@ +using Avalonia.Controls; +using Ryujinx.Ava.Common.Locale; +using Ryujinx.Ava.UI.Helpers; +using Ryujinx.Ava.UI.Models; +using Ryujinx.Ava.UI.ViewModels.Input; + +namespace Ryujinx.Ava.UI.Views.Input +{ + public partial class InputView : UserControl + { + private bool _dialogOpen; + private InputViewModel ViewModel { get; set; } + + public InputView() + { + DataContext = ViewModel = new InputViewModel(this); + + InitializeComponent(); + } + + public void SaveCurrentProfile() + { + ViewModel.Save(); + } + + private async void PlayerIndexBox_OnSelectionChanged(object sender, SelectionChangedEventArgs e) + { + if (ViewModel.IsModified && !_dialogOpen) + { + _dialogOpen = true; + + var result = await ContentDialogHelper.CreateConfirmationDialog( + LocaleManager.Instance[LocaleKeys.DialogControllerSettingsModifiedConfirmMessage], + LocaleManager.Instance[LocaleKeys.DialogControllerSettingsModifiedConfirmSubMessage], + LocaleManager.Instance[LocaleKeys.InputDialogYes], + LocaleManager.Instance[LocaleKeys.InputDialogNo], + LocaleManager.Instance[LocaleKeys.RyujinxConfirm]); + + if (result == UserResult.Yes) + { + ViewModel.Save(); + } + + _dialogOpen = false; + + ViewModel.IsModified = false; + + if (e.AddedItems.Count > 0) + { + var player = (PlayerModel)e.AddedItems[0]; + ViewModel.PlayerId = player.Id; + } + } + } + + public void Dispose() + { + ViewModel.Dispose(); + } + } +} diff --git a/src/Ryujinx.Ava/UI/Views/Input/KeyboardInputView.axaml b/src/Ryujinx.Ava/UI/Views/Input/KeyboardInputView.axaml new file mode 100644 index 000000000..e4566f463 --- /dev/null +++ b/src/Ryujinx.Ava/UI/Views/Input/KeyboardInputView.axaml @@ -0,0 +1,675 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Ryujinx.Ava/UI/Views/Input/KeyboardInputView.axaml.cs b/src/Ryujinx.Ava/UI/Views/Input/KeyboardInputView.axaml.cs new file mode 100644 index 000000000..f7024c5d1 --- /dev/null +++ b/src/Ryujinx.Ava/UI/Views/Input/KeyboardInputView.axaml.cs @@ -0,0 +1,210 @@ +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; + +namespace Ryujinx.Ava.UI.Views.Input +{ + public partial class KeyboardInputView : UserControl + { + private ButtonKeyAssigner _currentAssigner; + + public KeyboardInputView() + { + InitializeComponent(); + + foreach (ILogical visual in SettingButtons.GetLogicalDescendants()) + { + if (visual is ToggleButton button and not CheckBox) + { + button.IsCheckedChanged += Button_IsCheckedChanged; + } + } + } + + protected override void OnPointerReleased(PointerReleasedEventArgs e) + { + base.OnPointerReleased(e); + + if (_currentAssigner != null && _currentAssigner.ToggledButton != null && !_currentAssigner.ToggledButton.IsPointerOver) + { + _currentAssigner.Cancel(); + } + } + + private void Button_IsCheckedChanged(object sender, RoutedEventArgs e) + { + if (sender is ToggleButton button) + { + if ((bool)button.IsChecked) + { + if (_currentAssigner != null && button == _currentAssigner.ToggledButton) + { + return; + } + + bool isStick = button.Tag != null && button.Tag.ToString() == "stick"; + + if (_currentAssigner == null && (bool)button.IsChecked) + { + _currentAssigner = new ButtonKeyAssigner(button); + + this.Focus(NavigationMethod.Pointer); + + PointerPressed += MouseClick; + + IKeyboard keyboard = (IKeyboard)(DataContext as KeyboardInputViewModel).parentModel.AvaloniaKeyboardDriver.GetGamepad("0"); // Open Avalonia keyboard for cancel operations. + IButtonAssigner assigner = CreateButtonAssigner(isStick); + + _currentAssigner.ButtonAssigned += (sender, e) => + { + if (e.ButtonValue.HasValue) + { + var viewModel = (DataContext as KeyboardInputViewModel); + var buttonValue = e.ButtonValue.Value; + viewModel.parentModel.IsModified = true; + + switch (button.Name) + { + case "ButtonZl": + viewModel.Config.ButtonZl = buttonValue.AsKey(); + break; + case "ButtonL": + viewModel.Config.ButtonL = buttonValue.AsKey(); + break; + case "ButtonMinus": + viewModel.Config.ButtonMinus = buttonValue.AsKey(); + break; + case "LeftStickButton": + viewModel.Config.LeftStickButton = buttonValue.AsKey(); + break; + case "LeftStickUp": + viewModel.Config.LeftStickUp = buttonValue.AsKey(); + break; + case "LeftStickDown": + viewModel.Config.LeftStickDown = buttonValue.AsKey(); + break; + case "LeftStickRight": + viewModel.Config.LeftStickRight = buttonValue.AsKey(); + break; + case "LeftStickLeft": + viewModel.Config.LeftStickLeft = buttonValue.AsKey(); + break; + case "DpadUp": + viewModel.Config.DpadUp = buttonValue.AsKey(); + break; + case "DpadDown": + viewModel.Config.DpadDown = buttonValue.AsKey(); + break; + case "DpadLeft": + viewModel.Config.DpadLeft = buttonValue.AsKey(); + break; + case "DpadRight": + viewModel.Config.DpadRight = buttonValue.AsKey(); + break; + case "LeftButtonSr": + viewModel.Config.LeftButtonSr = buttonValue.AsKey(); + break; + case "LeftButtonSl": + viewModel.Config.LeftButtonSl = buttonValue.AsKey(); + break; + case "RightButtonSr": + viewModel.Config.RightButtonSr = buttonValue.AsKey(); + break; + case "RightButtonSl": + viewModel.Config.RightButtonSl = buttonValue.AsKey(); + break; + case "ButtonZr": + viewModel.Config.ButtonZr = buttonValue.AsKey(); + break; + case "ButtonR": + viewModel.Config.ButtonR = buttonValue.AsKey(); + break; + case "ButtonPlus": + viewModel.Config.ButtonPlus = buttonValue.AsKey(); + break; + case "ButtonA": + viewModel.Config.ButtonA = buttonValue.AsKey(); + break; + case "ButtonB": + viewModel.Config.ButtonB = buttonValue.AsKey(); + break; + case "ButtonX": + viewModel.Config.ButtonX = buttonValue.AsKey(); + break; + case "ButtonY": + viewModel.Config.ButtonY = buttonValue.AsKey(); + break; + case "RightStickButton": + viewModel.Config.RightStickButton = buttonValue.AsKey(); + break; + case "RightStickUp": + viewModel.Config.RightStickUp = buttonValue.AsKey(); + break; + case "RightStickDown": + viewModel.Config.RightStickDown = buttonValue.AsKey(); + break; + case "RightStickRight": + viewModel.Config.RightStickRight = buttonValue.AsKey(); + break; + case "RightStickLeft": + viewModel.Config.RightStickLeft = buttonValue.AsKey(); + break; + } + } + }; + + _currentAssigner.GetInputAndAssign(assigner, keyboard); + } + else + { + if (_currentAssigner != null) + { + ToggleButton oldButton = _currentAssigner.ToggledButton; + + _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; + + assigner = new KeyboardKeyAssigner((IKeyboard)(DataContext as KeyboardInputViewModel).parentModel.SelectedGamepad); + + return assigner; + } + + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnDetachedFromVisualTree(e); + _currentAssigner?.Cancel(); + _currentAssigner = null; + } + } +} diff --git a/src/Ryujinx.Ava/UI/Views/Input/MotionInputView.axaml b/src/Ryujinx.Ava/UI/Views/Input/MotionInputView.axaml index a6b587f67..0d018e297 100644 --- a/src/Ryujinx.Ava/UI/Views/Input/MotionInputView.axaml +++ b/src/Ryujinx.Ava/UI/Views/Input/MotionInputView.axaml @@ -6,7 +6,7 @@ xmlns:controls="clr-namespace:Ryujinx.Ava.UI.Controls" xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale" - xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels" + xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels.Input" mc:Ignorable="d" x:Class="Ryujinx.Ava.UI.Views.Input.MotionInputView" x:DataType="viewModels:MotionInputViewModel" diff --git a/src/Ryujinx.Ava/UI/Views/Input/MotionInputView.axaml.cs b/src/Ryujinx.Ava/UI/Views/Input/MotionInputView.axaml.cs index 1b340752b..2304364b6 100644 --- a/src/Ryujinx.Ava/UI/Views/Input/MotionInputView.axaml.cs +++ b/src/Ryujinx.Ava/UI/Views/Input/MotionInputView.axaml.cs @@ -1,9 +1,7 @@ using Avalonia.Controls; using FluentAvalonia.UI.Controls; using Ryujinx.Ava.Common.Locale; -using Ryujinx.Ava.UI.Models; -using Ryujinx.Ava.UI.ViewModels; -using Ryujinx.Common.Configuration.Hid.Controller; +using Ryujinx.Ava.UI.ViewModels.Input; using System.Threading.Tasks; namespace Ryujinx.Ava.UI.Views.Input @@ -19,7 +17,7 @@ namespace Ryujinx.Ava.UI.Views.Input public MotionInputView(ControllerInputViewModel viewModel) { - var config = viewModel.Configuration as InputConfiguration; + var config = viewModel.Config; _viewModel = new MotionInputViewModel { @@ -51,7 +49,7 @@ namespace Ryujinx.Ava.UI.Views.Input }; contentDialog.PrimaryButtonClick += (sender, args) => { - var config = viewModel.Configuration as InputConfiguration; + var config = viewModel.Config; config.Slot = content._viewModel.Slot; config.Sensitivity = content._viewModel.Sensitivity; config.GyroDeadzone = content._viewModel.GyroDeadzone; diff --git a/src/Ryujinx.Ava/UI/Views/Input/RumbleInputView.axaml b/src/Ryujinx.Ava/UI/Views/Input/RumbleInputView.axaml index 5b7087a47..1beb1f06e 100644 --- a/src/Ryujinx.Ava/UI/Views/Input/RumbleInputView.axaml +++ b/src/Ryujinx.Ava/UI/Views/Input/RumbleInputView.axaml @@ -5,7 +5,7 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale" - xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels" + xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels.Input" mc:Ignorable="d" x:Class="Ryujinx.Ava.UI.Views.Input.RumbleInputView" x:DataType="viewModels:RumbleInputViewModel" diff --git a/src/Ryujinx.Ava/UI/Views/Input/RumbleInputView.axaml.cs b/src/Ryujinx.Ava/UI/Views/Input/RumbleInputView.axaml.cs index 9307f872c..58a4b416b 100644 --- a/src/Ryujinx.Ava/UI/Views/Input/RumbleInputView.axaml.cs +++ b/src/Ryujinx.Ava/UI/Views/Input/RumbleInputView.axaml.cs @@ -1,9 +1,7 @@ using Avalonia.Controls; using FluentAvalonia.UI.Controls; using Ryujinx.Ava.Common.Locale; -using Ryujinx.Ava.UI.Models; -using Ryujinx.Ava.UI.ViewModels; -using Ryujinx.Common.Configuration.Hid.Controller; +using Ryujinx.Ava.UI.ViewModels.Input; using System.Threading.Tasks; namespace Ryujinx.Ava.UI.Views.Input @@ -19,7 +17,7 @@ namespace Ryujinx.Ava.UI.Views.Input public RumbleInputView(ControllerInputViewModel viewModel) { - var config = viewModel.Configuration as InputConfiguration; + var config = viewModel.Config; _viewModel = new RumbleInputViewModel { @@ -47,7 +45,7 @@ namespace Ryujinx.Ava.UI.Views.Input contentDialog.PrimaryButtonClick += (sender, args) => { - var config = viewModel.Configuration as InputConfiguration; + var config = viewModel.Config; config.StrongRumble = content._viewModel.StrongRumble; config.WeakRumble = content._viewModel.WeakRumble; }; diff --git a/src/Ryujinx.Ava/UI/Views/Settings/SettingsInputView.axaml b/src/Ryujinx.Ava/UI/Views/Settings/SettingsInputView.axaml index 81f4b68b7..55c2ed6e3 100644 --- a/src/Ryujinx.Ava/UI/Views/Settings/SettingsInputView.axaml +++ b/src/Ryujinx.Ava/UI/Views/Settings/SettingsInputView.axaml @@ -27,9 +27,9 @@ - + Name="InputView" /> diff --git a/src/Ryujinx.Ava/UI/Views/Settings/SettingsInputView.axaml.cs b/src/Ryujinx.Ava/UI/Views/Settings/SettingsInputView.axaml.cs index 8a0cb8ab9..85ccffccd 100644 --- a/src/Ryujinx.Ava/UI/Views/Settings/SettingsInputView.axaml.cs +++ b/src/Ryujinx.Ava/UI/Views/Settings/SettingsInputView.axaml.cs @@ -11,7 +11,7 @@ namespace Ryujinx.Ava.UI.Views.Settings public void Dispose() { - ControllerSettings.Dispose(); + InputView.Dispose(); } } } diff --git a/src/Ryujinx.Ava/UI/Windows/SettingsWindow.axaml.cs b/src/Ryujinx.Ava/UI/Windows/SettingsWindow.axaml.cs index d7bb0b883..314501c52 100644 --- a/src/Ryujinx.Ava/UI/Windows/SettingsWindow.axaml.cs +++ b/src/Ryujinx.Ava/UI/Windows/SettingsWindow.axaml.cs @@ -37,7 +37,7 @@ namespace Ryujinx.Ava.UI.Windows public void SaveSettings() { - InputPage.ControllerSettings?.SaveCurrentProfile(); + InputPage.InputView?.SaveCurrentProfile(); if (Owner is MainWindow window && ViewModel.DirectoryChanged) { diff --git a/src/Ryujinx.Input/Assigner/GamepadButtonAssigner.cs b/src/Ryujinx.Input/Assigner/GamepadButtonAssigner.cs index 388ebcc07..bf8319a6a 100644 --- a/src/Ryujinx.Input/Assigner/GamepadButtonAssigner.cs +++ b/src/Ryujinx.Input/Assigner/GamepadButtonAssigner.cs @@ -59,16 +59,16 @@ namespace Ryujinx.Input.Assigner return _gamepad == null || !_gamepad.IsConnected; } - public string GetPressedButton() + public ButtonValue? GetPressedButton() { IEnumerable pressedButtons = _detector.GetPressedButtons(); if (pressedButtons.Any()) { - return !_forStick ? pressedButtons.First().ToString() : ((StickInputId)pressedButtons.First()).ToString(); + return !_forStick ? new(pressedButtons.First()) : new(((StickInputId)pressedButtons.First())); } - return ""; + return null; } private void CollectButtonStats() diff --git a/src/Ryujinx.Input/Assigner/IButtonAssigner.cs b/src/Ryujinx.Input/Assigner/IButtonAssigner.cs index 76a9fece4..653717133 100644 --- a/src/Ryujinx.Input/Assigner/IButtonAssigner.cs +++ b/src/Ryujinx.Input/Assigner/IButtonAssigner.cs @@ -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(); + ButtonValue? GetPressedButton(); } } diff --git a/src/Ryujinx.Input/Assigner/KeyboardKeyAssigner.cs b/src/Ryujinx.Input/Assigner/KeyboardKeyAssigner.cs index e52ef4a2c..c66812ba0 100644 --- a/src/Ryujinx.Input/Assigner/KeyboardKeyAssigner.cs +++ b/src/Ryujinx.Input/Assigner/KeyboardKeyAssigner.cs @@ -23,7 +23,7 @@ namespace Ryujinx.Input.Assigner public bool HasAnyButtonPressed() { - 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 ButtonValue? GetPressedButton() { - string keyPressed = ""; + ButtonValue? 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/ButtonValue.cs b/src/Ryujinx.Input/ButtonValue.cs new file mode 100644 index 000000000..f037e6b60 --- /dev/null +++ b/src/Ryujinx.Input/ButtonValue.cs @@ -0,0 +1,48 @@ +using System.Diagnostics; + +namespace Ryujinx.Input +{ + public enum ButtonValueType { Key, GamepadButtonInputId, StickId } + + public readonly struct ButtonValue + { + private readonly ButtonValueType _type; + private readonly uint _rawValue; + + public ButtonValue(Key key) + { + _type = ButtonValueType.Key; + _rawValue = (uint)key; + } + + public ButtonValue(GamepadButtonInputId gamepad) + { + _type = ButtonValueType.GamepadButtonInputId; + _rawValue = (uint)gamepad; + } + + public ButtonValue(StickInputId stick) + { + _type = ButtonValueType.StickId; + _rawValue = (uint)stick; + } + + public Common.Configuration.Hid.Key AsKey() + { + Debug.Assert(_type == ButtonValueType.Key); + return (Common.Configuration.Hid.Key)_rawValue; + } + + public Common.Configuration.Hid.Controller.GamepadInputId AsGamepadButtonInputId() + { + Debug.Assert(_type == ButtonValueType.GamepadButtonInputId); + return (Common.Configuration.Hid.Controller.GamepadInputId)_rawValue; + } + + public Common.Configuration.Hid.Controller.StickInputId AsGamepadStickId() + { + Debug.Assert(_type == ButtonValueType.StickId); + return (Common.Configuration.Hid.Controller.StickInputId)_rawValue; + } + } +} diff --git a/src/Ryujinx/Ui/Windows/ControllerWindow.cs b/src/Ryujinx/Ui/Windows/ControllerWindow.cs index ebf22ab60..52cad5c85 100644 --- a/src/Ryujinx/Ui/Windows/ControllerWindow.cs +++ b/src/Ryujinx/Ui/Windows/ControllerWindow.cs @@ -893,7 +893,7 @@ namespace Ryujinx.Ui.Windows } } - string pressedButton = assigner.GetPressedButton(); + string pressedButton = assigner.GetPressedButton().ToString(); Application.Invoke(delegate { From 638be5f296bf52943da4366699d33f1e8656e00c Mon Sep 17 00:00:00 2001 From: TSR Berry <20988865+TSRBerry@users.noreply.github.com> Date: Sat, 21 Oct 2023 15:19:21 +0200 Subject: [PATCH 097/105] Revert "Ava UI: Input Menu Refactor (#4998)" This reverts commit 49b37550cae6b3c69f59a9c7a44b17e3c12a813b. This currently breaks the GTK GUI. --- src/Ryujinx.Ava/Assets/Locales/en_US.json | 99 --- src/Ryujinx.Ava/Assets/Styles/Styles.xaml | 5 +- .../UI/Helpers/ButtonKeyAssigner.cs | 28 +- .../UI/Helpers/KeyValueConverter.cs | 167 +---- .../UI/Models/Input/ControllerInputConfig.cs | 580 --------------- .../UI/Models/Input/KeyboardInputConfig.cs | 422 ----------- .../UI/Models/InputConfiguration.cs | 456 ++++++++++++ ...ewModel.cs => ControllerInputViewModel.cs} | 103 +-- .../Input/ControllerInputViewModel.cs | 84 --- .../Input/KeyboardInputViewModel.cs | 73 -- .../{Input => }/MotionInputViewModel.cs | 2 +- .../{Input => }/RumbleInputViewModel.cs | 2 +- .../UI/Views/Input/ControllerInputView.axaml | 616 ++++++++++++---- .../Views/Input/ControllerInputView.axaml.cs | 164 ++--- .../UI/Views/Input/InputView.axaml | 225 ------ .../UI/Views/Input/InputView.axaml.cs | 61 -- .../UI/Views/Input/KeyboardInputView.axaml | 675 ------------------ .../UI/Views/Input/KeyboardInputView.axaml.cs | 210 ------ .../UI/Views/Input/MotionInputView.axaml | 2 +- .../UI/Views/Input/MotionInputView.axaml.cs | 8 +- .../UI/Views/Input/RumbleInputView.axaml | 2 +- .../UI/Views/Input/RumbleInputView.axaml.cs | 8 +- .../UI/Views/Settings/SettingsInputView.axaml | 4 +- .../Views/Settings/SettingsInputView.axaml.cs | 2 +- .../UI/Windows/SettingsWindow.axaml.cs | 2 +- .../Assigner/GamepadButtonAssigner.cs | 6 +- src/Ryujinx.Input/Assigner/IButtonAssigner.cs | 2 +- .../Assigner/KeyboardKeyAssigner.cs | 10 +- src/Ryujinx.Input/ButtonValue.cs | 48 -- src/Ryujinx/Ui/Windows/ControllerWindow.cs | 2 +- 30 files changed, 1152 insertions(+), 2916 deletions(-) delete mode 100644 src/Ryujinx.Ava/UI/Models/Input/ControllerInputConfig.cs delete mode 100644 src/Ryujinx.Ava/UI/Models/Input/KeyboardInputConfig.cs create mode 100644 src/Ryujinx.Ava/UI/Models/InputConfiguration.cs rename src/Ryujinx.Ava/UI/ViewModels/{Input/InputViewModel.cs => ControllerInputViewModel.cs} (92%) delete mode 100644 src/Ryujinx.Ava/UI/ViewModels/Input/ControllerInputViewModel.cs delete mode 100644 src/Ryujinx.Ava/UI/ViewModels/Input/KeyboardInputViewModel.cs rename src/Ryujinx.Ava/UI/ViewModels/{Input => }/MotionInputViewModel.cs (97%) rename src/Ryujinx.Ava/UI/ViewModels/{Input => }/RumbleInputViewModel.cs (92%) delete mode 100644 src/Ryujinx.Ava/UI/Views/Input/InputView.axaml delete mode 100644 src/Ryujinx.Ava/UI/Views/Input/InputView.axaml.cs delete mode 100644 src/Ryujinx.Ava/UI/Views/Input/KeyboardInputView.axaml delete mode 100644 src/Ryujinx.Ava/UI/Views/Input/KeyboardInputView.axaml.cs delete mode 100644 src/Ryujinx.Input/ButtonValue.cs diff --git a/src/Ryujinx.Ava/Assets/Locales/en_US.json b/src/Ryujinx.Ava/Assets/Locales/en_US.json index fc65fe4a0..a67b796bd 100644 --- a/src/Ryujinx.Ava/Assets/Locales/en_US.json +++ b/src/Ryujinx.Ava/Assets/Locales/en_US.json @@ -263,105 +263,6 @@ "ControllerSettingsMotionGyroDeadzone": "Gyro Deadzone:", "ControllerSettingsSave": "Save", "ControllerSettingsClose": "Close", - "KeyUnknown": "Unknown", - "KeyShiftLeft": "Shift Left", - "KeyShiftRight": "Shift Right", - "KeyControlLeft": "Control Left", - "KeyControlRight": "Control Right", - "KeyAltLeft": "Alt Left", - "KeyAltRight": "Alt Right", - "KeyOptLeft": "⌥ Left", - "KeyOptRight": "⌥ Right", - "KeyWinLeft": "⊞ Left", - "KeyWinRight": "⊞ Right", - "KeyCmdLeft": "⌘ Left", - "KeyCmdRight": "⌘ 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": "Left Stick Button", - "GamepadRightStick": "Right 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", diff --git a/src/Ryujinx.Ava/Assets/Styles/Styles.xaml b/src/Ryujinx.Ava/Assets/Styles/Styles.xaml index b3a6f59c8..f7f64be22 100644 --- a/src/Ryujinx.Ava/Assets/Styles/Styles.xaml +++ b/src/Ryujinx.Ava/Assets/Styles/Styles.xaml @@ -15,7 +15,8 @@ - + @@ -392,4 +393,4 @@ 600 756 - + \ No newline at end of file diff --git a/src/Ryujinx.Ava/UI/Helpers/ButtonKeyAssigner.cs b/src/Ryujinx.Ava/UI/Helpers/ButtonKeyAssigner.cs index 54e0918a5..7e8ba7342 100644 --- a/src/Ryujinx.Ava/UI/Helpers/ButtonKeyAssigner.cs +++ b/src/Ryujinx.Ava/UI/Helpers/ButtonKeyAssigner.cs @@ -1,8 +1,11 @@ +using Avalonia.Controls; using Avalonia.Controls.Primitives; +using Avalonia.LogicalTree; using Avalonia.Threading; using Ryujinx.Input; using Ryujinx.Input.Assigner; using System; +using System.Linq; using System.Threading.Tasks; namespace Ryujinx.Ava.UI.Helpers @@ -12,12 +15,12 @@ namespace Ryujinx.Ava.UI.Helpers internal class ButtonAssignedEventArgs : EventArgs { public ToggleButton Button { get; } - public ButtonValue? ButtonValue { get; } + public bool IsAssigned { get; } - public ButtonAssignedEventArgs(ToggleButton button, ButtonValue? buttonValue) + public ButtonAssignedEventArgs(ToggleButton button, bool isAssigned) { Button = button; - ButtonValue = buttonValue; + IsAssigned = isAssigned; } } @@ -75,11 +78,15 @@ namespace Ryujinx.Ava.UI.Helpers await Dispatcher.UIThread.InvokeAsync(() => { - ButtonValue? pressedButton = assigner.GetPressedButton(); + string pressedButton = assigner.GetPressedButton(); if (_shouldUnbind) { - pressedButton = null; + SetButtonText(ToggledButton, "Unbound"); + } + else if (pressedButton != "") + { + SetButtonText(ToggledButton, pressedButton); } _shouldUnbind = false; @@ -87,8 +94,17 @@ namespace Ryujinx.Ava.UI.Helpers ToggledButton.IsChecked = false; - ButtonAssigned?.Invoke(this, new ButtonAssignedEventArgs(ToggledButton, pressedButton)); + ButtonAssigned?.Invoke(this, new ButtonAssignedEventArgs(ToggledButton, pressedButton != null)); + static void SetButtonText(ToggleButton button, string text) + { + ILogical textBlock = button.GetLogicalDescendants().First(x => x is TextBlock); + + if (textBlock != null && textBlock is TextBlock block) + { + block.Text = text; + } + } }); } diff --git a/src/Ryujinx.Ava/UI/Helpers/KeyValueConverter.cs b/src/Ryujinx.Ava/UI/Helpers/KeyValueConverter.cs index 1c4aa7b21..028ed6bf4 100644 --- a/src/Ryujinx.Ava/UI/Helpers/KeyValueConverter.cs +++ b/src/Ryujinx.Ava/UI/Helpers/KeyValueConverter.cs @@ -1,9 +1,7 @@ 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 @@ -12,158 +10,37 @@ namespace Ryujinx.Ava.UI.Helpers { public static KeyValueConverter Instance = new(); - private static readonly Dictionary _keysMap = new() - { - { Key.Unknown, LocaleKeys.KeyUnknown }, - { Key.ShiftLeft, LocaleKeys.KeyShiftLeft }, - { Key.ShiftRight, LocaleKeys.KeyShiftRight }, - { Key.ControlLeft, LocaleKeys.KeyControlLeft }, - { Key.ControlRight, LocaleKeys.KeyControlRight }, - { Key.AltLeft, OperatingSystem.IsMacOS() ? LocaleKeys.KeyOptLeft : LocaleKeys.KeyAltLeft }, - { Key.AltRight, OperatingSystem.IsMacOS() ? LocaleKeys.KeyOptRight : LocaleKeys.KeyAltRight }, - { Key.WinLeft, OperatingSystem.IsMacOS() ? LocaleKeys.KeyCmdLeft : LocaleKeys.KeyWinLeft }, - { Key.WinRight, OperatingSystem.IsMacOS() ? LocaleKeys.KeyCmdRight : 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 = ""; - - if (value is Key key) + if (value == null) { - if (_keysMap.TryGetValue(key, out LocaleKeys localeKey)) - { - keyString = LocaleManager.Instance[localeKey]; - } - else - { - keyString = key.ToString(); - } - } - else if (value is GamepadInputId gamepadInputId) - { - if (_gamepadInputIdMap.TryGetValue(gamepadInputId, out LocaleKeys localeKey)) - { - keyString = LocaleManager.Instance[localeKey]; - } - else - { - keyString = gamepadInputId.ToString(); - } - } - else if (value is StickInputId stickInputId) - { - if (_stickInputIdMap.TryGetValue(stickInputId, out LocaleKeys localeKey)) - { - keyString = LocaleManager.Instance[localeKey]; - } - else - { - keyString = stickInputId.ToString(); - } + return null; } - return keyString; + return value.ToString(); } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { - throw new NotSupportedException(); + object key = null; + + if (value != null) + { + if (targetType == typeof(Key)) + { + key = Enum.Parse(value.ToString()); + } + else if (targetType == typeof(GamepadInputId)) + { + key = Enum.Parse(value.ToString()); + } + else if (targetType == typeof(StickInputId)) + { + key = Enum.Parse(value.ToString()); + } + } + + return key; } } } diff --git a/src/Ryujinx.Ava/UI/Models/Input/ControllerInputConfig.cs b/src/Ryujinx.Ava/UI/Models/Input/ControllerInputConfig.cs deleted file mode 100644 index 4929e582e..000000000 --- a/src/Ryujinx.Ava/UI/Models/Input/ControllerInputConfig.cs +++ /dev/null @@ -1,580 +0,0 @@ -using Ryujinx.Ava.UI.ViewModels; -using Ryujinx.Common.Configuration.Hid; -using Ryujinx.Common.Configuration.Hid.Controller; -using Ryujinx.Common.Configuration.Hid.Controller.Motion; -using System; - -namespace Ryujinx.Ava.UI.Models.Input -{ - public class ControllerInputConfig : 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 ControllerInputConfig(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 MotionConfigController - { - EnableMotion = EnableMotion, - MotionBackend = MotionInputBackendType.GamepadDriver, - GyroDeadzone = GyroDeadzone, - Sensitivity = Sensitivity - }; - } - - return config; - } - } -} diff --git a/src/Ryujinx.Ava/UI/Models/Input/KeyboardInputConfig.cs b/src/Ryujinx.Ava/UI/Models/Input/KeyboardInputConfig.cs deleted file mode 100644 index 029565210..000000000 --- a/src/Ryujinx.Ava/UI/Models/Input/KeyboardInputConfig.cs +++ /dev/null @@ -1,422 +0,0 @@ -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.Ava/UI/Models/InputConfiguration.cs b/src/Ryujinx.Ava/UI/Models/InputConfiguration.cs new file mode 100644 index 000000000..f1352c6d8 --- /dev/null +++ b/src/Ryujinx.Ava/UI/Models/InputConfiguration.cs @@ -0,0 +1,456 @@ +using Ryujinx.Ava.UI.ViewModels; +using Ryujinx.Common.Configuration.Hid; +using Ryujinx.Common.Configuration.Hid.Controller; +using Ryujinx.Common.Configuration.Hid.Controller.Motion; +using Ryujinx.Common.Configuration.Hid.Keyboard; +using System; + +namespace Ryujinx.Ava.UI.Models +{ + internal class InputConfiguration : BaseModel + { + private float _deadzoneRight; + private float _triggerThreshold; + private float _deadzoneLeft; + private double _gyroDeadzone; + private int _sensitivity; + private bool _enableMotion; + private float _weakRumble; + private float _strongRumble; + private float _rangeLeft; + private float _rangeRight; + + public InputBackendType Backend { get; set; } + + /// + /// Controller id + /// + public string Id { get; set; } + + /// + /// Controller's Type + /// + public ControllerType ControllerType { get; set; } + + /// + /// Player's Index for the controller + /// + public PlayerIndex PlayerIndex { get; set; } + + public TStick LeftJoystick { get; set; } + public bool LeftInvertStickX { get; set; } + public bool LeftInvertStickY { get; set; } + public bool RightRotate90 { get; set; } + public TKey LeftControllerStickButton { get; set; } + + public TStick RightJoystick { get; set; } + public bool RightInvertStickX { get; set; } + public bool RightInvertStickY { get; set; } + public bool LeftRotate90 { get; set; } + public TKey RightControllerStickButton { get; set; } + + public float DeadzoneLeft + { + get => _deadzoneLeft; + set + { + _deadzoneLeft = MathF.Round(value, 3); + + OnPropertyChanged(); + } + } + + public float RangeLeft + { + get => _rangeLeft; + set + { + _rangeLeft = MathF.Round(value, 3); + + OnPropertyChanged(); + } + } + + public float DeadzoneRight + { + get => _deadzoneRight; + set + { + _deadzoneRight = MathF.Round(value, 3); + + OnPropertyChanged(); + } + } + + public float RangeRight + { + get => _rangeRight; + set + { + _rangeRight = MathF.Round(value, 3); + + OnPropertyChanged(); + } + } + + public float TriggerThreshold + { + get => _triggerThreshold; + set + { + _triggerThreshold = MathF.Round(value, 3); + + OnPropertyChanged(); + } + } + + public MotionInputBackendType MotionBackend { get; set; } + + public TKey ButtonMinus { get; set; } + public TKey ButtonL { get; set; } + public TKey ButtonZl { get; set; } + public TKey LeftButtonSl { get; set; } + public TKey LeftButtonSr { get; set; } + public TKey DpadUp { get; set; } + public TKey DpadDown { get; set; } + public TKey DpadLeft { get; set; } + public TKey DpadRight { get; set; } + + public TKey ButtonPlus { get; set; } + public TKey ButtonR { get; set; } + public TKey ButtonZr { get; set; } + public TKey RightButtonSl { get; set; } + public TKey RightButtonSr { get; set; } + public TKey ButtonX { get; set; } + public TKey ButtonB { get; set; } + public TKey ButtonY { get; set; } + public TKey ButtonA { get; set; } + + public TKey LeftStickUp { get; set; } + public TKey LeftStickDown { get; set; } + public TKey LeftStickLeft { get; set; } + public TKey LeftStickRight { get; set; } + public TKey LeftKeyboardStickButton { get; set; } + + public TKey RightStickUp { get; set; } + public TKey RightStickDown { get; set; } + public TKey RightStickLeft { get; set; } + public TKey RightStickRight { get; set; } + public TKey RightKeyboardStickButton { get; set; } + + public int Sensitivity + { + get => _sensitivity; + set + { + _sensitivity = value; + + OnPropertyChanged(); + } + } + + public double GyroDeadzone + { + get => _gyroDeadzone; + set + { + _gyroDeadzone = Math.Round(value, 3); + + OnPropertyChanged(); + } + } + + public bool EnableMotion + { + get => _enableMotion; set + { + _enableMotion = value; + + OnPropertyChanged(); + } + } + + public bool EnableCemuHookMotion { get; set; } + public int Slot { get; set; } + public int AltSlot { get; set; } + public bool MirrorInput { get; set; } + public string DsuServerHost { get; set; } + public int DsuServerPort { get; set; } + + public bool EnableRumble { get; set; } + public float WeakRumble + { + get => _weakRumble; set + { + _weakRumble = value; + + OnPropertyChanged(); + } + } + public float StrongRumble + { + get => _strongRumble; set + { + _strongRumble = value; + + OnPropertyChanged(); + } + } + + public InputConfiguration(InputConfig config) + { + if (config != null) + { + Backend = config.Backend; + Id = config.Id; + ControllerType = config.ControllerType; + PlayerIndex = config.PlayerIndex; + + if (config is StandardKeyboardInputConfig keyboardConfig) + { + LeftStickUp = (TKey)(object)keyboardConfig.LeftJoyconStick.StickUp; + LeftStickDown = (TKey)(object)keyboardConfig.LeftJoyconStick.StickDown; + LeftStickLeft = (TKey)(object)keyboardConfig.LeftJoyconStick.StickLeft; + LeftStickRight = (TKey)(object)keyboardConfig.LeftJoyconStick.StickRight; + LeftKeyboardStickButton = (TKey)(object)keyboardConfig.LeftJoyconStick.StickButton; + + RightStickUp = (TKey)(object)keyboardConfig.RightJoyconStick.StickUp; + RightStickDown = (TKey)(object)keyboardConfig.RightJoyconStick.StickDown; + RightStickLeft = (TKey)(object)keyboardConfig.RightJoyconStick.StickLeft; + RightStickRight = (TKey)(object)keyboardConfig.RightJoyconStick.StickRight; + RightKeyboardStickButton = (TKey)(object)keyboardConfig.RightJoyconStick.StickButton; + + ButtonA = (TKey)(object)keyboardConfig.RightJoycon.ButtonA; + ButtonB = (TKey)(object)keyboardConfig.RightJoycon.ButtonB; + ButtonX = (TKey)(object)keyboardConfig.RightJoycon.ButtonX; + ButtonY = (TKey)(object)keyboardConfig.RightJoycon.ButtonY; + ButtonR = (TKey)(object)keyboardConfig.RightJoycon.ButtonR; + RightButtonSl = (TKey)(object)keyboardConfig.RightJoycon.ButtonSl; + RightButtonSr = (TKey)(object)keyboardConfig.RightJoycon.ButtonSr; + ButtonZr = (TKey)(object)keyboardConfig.RightJoycon.ButtonZr; + ButtonPlus = (TKey)(object)keyboardConfig.RightJoycon.ButtonPlus; + + DpadUp = (TKey)(object)keyboardConfig.LeftJoycon.DpadUp; + DpadDown = (TKey)(object)keyboardConfig.LeftJoycon.DpadDown; + DpadLeft = (TKey)(object)keyboardConfig.LeftJoycon.DpadLeft; + DpadRight = (TKey)(object)keyboardConfig.LeftJoycon.DpadRight; + ButtonMinus = (TKey)(object)keyboardConfig.LeftJoycon.ButtonMinus; + LeftButtonSl = (TKey)(object)keyboardConfig.LeftJoycon.ButtonSl; + LeftButtonSr = (TKey)(object)keyboardConfig.LeftJoycon.ButtonSr; + ButtonZl = (TKey)(object)keyboardConfig.LeftJoycon.ButtonZl; + ButtonL = (TKey)(object)keyboardConfig.LeftJoycon.ButtonL; + } + else if (config is StandardControllerInputConfig controllerConfig) + { + LeftJoystick = (TStick)(object)controllerConfig.LeftJoyconStick.Joystick; + LeftInvertStickX = controllerConfig.LeftJoyconStick.InvertStickX; + LeftInvertStickY = controllerConfig.LeftJoyconStick.InvertStickY; + LeftRotate90 = controllerConfig.LeftJoyconStick.Rotate90CW; + LeftControllerStickButton = (TKey)(object)controllerConfig.LeftJoyconStick.StickButton; + + RightJoystick = (TStick)(object)controllerConfig.RightJoyconStick.Joystick; + RightInvertStickX = controllerConfig.RightJoyconStick.InvertStickX; + RightInvertStickY = controllerConfig.RightJoyconStick.InvertStickY; + RightRotate90 = controllerConfig.RightJoyconStick.Rotate90CW; + RightControllerStickButton = (TKey)(object)controllerConfig.RightJoyconStick.StickButton; + + ButtonA = (TKey)(object)controllerConfig.RightJoycon.ButtonA; + ButtonB = (TKey)(object)controllerConfig.RightJoycon.ButtonB; + ButtonX = (TKey)(object)controllerConfig.RightJoycon.ButtonX; + ButtonY = (TKey)(object)controllerConfig.RightJoycon.ButtonY; + ButtonR = (TKey)(object)controllerConfig.RightJoycon.ButtonR; + RightButtonSl = (TKey)(object)controllerConfig.RightJoycon.ButtonSl; + RightButtonSr = (TKey)(object)controllerConfig.RightJoycon.ButtonSr; + ButtonZr = (TKey)(object)controllerConfig.RightJoycon.ButtonZr; + ButtonPlus = (TKey)(object)controllerConfig.RightJoycon.ButtonPlus; + + DpadUp = (TKey)(object)controllerConfig.LeftJoycon.DpadUp; + DpadDown = (TKey)(object)controllerConfig.LeftJoycon.DpadDown; + DpadLeft = (TKey)(object)controllerConfig.LeftJoycon.DpadLeft; + DpadRight = (TKey)(object)controllerConfig.LeftJoycon.DpadRight; + ButtonMinus = (TKey)(object)controllerConfig.LeftJoycon.ButtonMinus; + LeftButtonSl = (TKey)(object)controllerConfig.LeftJoycon.ButtonSl; + LeftButtonSr = (TKey)(object)controllerConfig.LeftJoycon.ButtonSr; + ButtonZl = (TKey)(object)controllerConfig.LeftJoycon.ButtonZl; + ButtonL = (TKey)(object)controllerConfig.LeftJoycon.ButtonL; + + DeadzoneLeft = controllerConfig.DeadzoneLeft; + DeadzoneRight = controllerConfig.DeadzoneRight; + RangeLeft = controllerConfig.RangeLeft; + RangeRight = controllerConfig.RangeRight; + TriggerThreshold = controllerConfig.TriggerThreshold; + + if (controllerConfig.Motion != null) + { + EnableMotion = controllerConfig.Motion.EnableMotion; + MotionBackend = controllerConfig.Motion.MotionBackend; + GyroDeadzone = controllerConfig.Motion.GyroDeadzone; + Sensitivity = controllerConfig.Motion.Sensitivity; + + if (controllerConfig.Motion is CemuHookMotionConfigController cemuHook) + { + EnableCemuHookMotion = true; + DsuServerHost = cemuHook.DsuServerHost; + DsuServerPort = cemuHook.DsuServerPort; + Slot = cemuHook.Slot; + AltSlot = cemuHook.AltSlot; + MirrorInput = cemuHook.MirrorInput; + } + + if (controllerConfig.Rumble != null) + { + EnableRumble = controllerConfig.Rumble.EnableRumble; + WeakRumble = controllerConfig.Rumble.WeakRumble; + StrongRumble = controllerConfig.Rumble.StrongRumble; + } + } + } + } + } + + public InputConfiguration() + { + } + + public InputConfig GetConfig() + { + if (Backend == InputBackendType.WindowKeyboard) + { + return new StandardKeyboardInputConfig + { + Id = Id, + Backend = Backend, + PlayerIndex = PlayerIndex, + ControllerType = ControllerType, + LeftJoycon = new LeftJoyconCommonConfig + { + DpadUp = (Key)(object)DpadUp, + DpadDown = (Key)(object)DpadDown, + DpadLeft = (Key)(object)DpadLeft, + DpadRight = (Key)(object)DpadRight, + ButtonL = (Key)(object)ButtonL, + ButtonZl = (Key)(object)ButtonZl, + ButtonSl = (Key)(object)LeftButtonSl, + ButtonSr = (Key)(object)LeftButtonSr, + ButtonMinus = (Key)(object)ButtonMinus, + }, + RightJoycon = new RightJoyconCommonConfig + { + ButtonA = (Key)(object)ButtonA, + ButtonB = (Key)(object)ButtonB, + ButtonX = (Key)(object)ButtonX, + ButtonY = (Key)(object)ButtonY, + ButtonPlus = (Key)(object)ButtonPlus, + ButtonSl = (Key)(object)RightButtonSl, + ButtonSr = (Key)(object)RightButtonSr, + ButtonR = (Key)(object)ButtonR, + ButtonZr = (Key)(object)ButtonZr, + }, + LeftJoyconStick = new JoyconConfigKeyboardStick + { + StickUp = (Key)(object)LeftStickUp, + StickDown = (Key)(object)LeftStickDown, + StickRight = (Key)(object)LeftStickRight, + StickLeft = (Key)(object)LeftStickLeft, + StickButton = (Key)(object)LeftKeyboardStickButton, + }, + RightJoyconStick = new JoyconConfigKeyboardStick + { + StickUp = (Key)(object)RightStickUp, + StickDown = (Key)(object)RightStickDown, + StickLeft = (Key)(object)RightStickLeft, + StickRight = (Key)(object)RightStickRight, + StickButton = (Key)(object)RightKeyboardStickButton, + }, + Version = InputConfig.CurrentVersion, + }; + + } + + if (Backend == InputBackendType.GamepadSDL2) + { + var config = new StandardControllerInputConfig + { + Id = Id, + Backend = Backend, + PlayerIndex = PlayerIndex, + ControllerType = ControllerType, + LeftJoycon = new LeftJoyconCommonConfig + { + DpadUp = (GamepadInputId)(object)DpadUp, + DpadDown = (GamepadInputId)(object)DpadDown, + DpadLeft = (GamepadInputId)(object)DpadLeft, + DpadRight = (GamepadInputId)(object)DpadRight, + ButtonL = (GamepadInputId)(object)ButtonL, + ButtonZl = (GamepadInputId)(object)ButtonZl, + ButtonSl = (GamepadInputId)(object)LeftButtonSl, + ButtonSr = (GamepadInputId)(object)LeftButtonSr, + ButtonMinus = (GamepadInputId)(object)ButtonMinus, + }, + RightJoycon = new RightJoyconCommonConfig + { + ButtonA = (GamepadInputId)(object)ButtonA, + ButtonB = (GamepadInputId)(object)ButtonB, + ButtonX = (GamepadInputId)(object)ButtonX, + ButtonY = (GamepadInputId)(object)ButtonY, + ButtonPlus = (GamepadInputId)(object)ButtonPlus, + ButtonSl = (GamepadInputId)(object)RightButtonSl, + ButtonSr = (GamepadInputId)(object)RightButtonSr, + ButtonR = (GamepadInputId)(object)ButtonR, + ButtonZr = (GamepadInputId)(object)ButtonZr, + }, + LeftJoyconStick = new JoyconConfigControllerStick + { + Joystick = (StickInputId)(object)LeftJoystick, + InvertStickX = LeftInvertStickX, + InvertStickY = LeftInvertStickY, + Rotate90CW = LeftRotate90, + StickButton = (GamepadInputId)(object)LeftControllerStickButton, + }, + RightJoyconStick = new JoyconConfigControllerStick + { + Joystick = (StickInputId)(object)RightJoystick, + InvertStickX = RightInvertStickX, + InvertStickY = RightInvertStickY, + Rotate90CW = RightRotate90, + StickButton = (GamepadInputId)(object)RightControllerStickButton, + }, + Rumble = new RumbleConfigController + { + EnableRumble = EnableRumble, + WeakRumble = WeakRumble, + StrongRumble = StrongRumble, + }, + Version = InputConfig.CurrentVersion, + DeadzoneLeft = DeadzoneLeft, + DeadzoneRight = DeadzoneRight, + RangeLeft = RangeLeft, + RangeRight = RangeRight, + TriggerThreshold = TriggerThreshold, + Motion = EnableCemuHookMotion + ? new CemuHookMotionConfigController + { + DsuServerHost = DsuServerHost, + DsuServerPort = DsuServerPort, + Slot = Slot, + AltSlot = AltSlot, + MirrorInput = MirrorInput, + MotionBackend = MotionInputBackendType.CemuHook, + } + : new StandardMotionConfigController + { + MotionBackend = MotionInputBackendType.GamepadDriver, + }, + }; + + config.Motion.Sensitivity = Sensitivity; + config.Motion.EnableMotion = EnableMotion; + config.Motion.GyroDeadzone = GyroDeadzone; + + return config; + } + + return null; + } + } +} diff --git a/src/Ryujinx.Ava/UI/ViewModels/Input/InputViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/ControllerInputViewModel.cs similarity index 92% rename from src/Ryujinx.Ava/UI/ViewModels/Input/InputViewModel.cs rename to src/Ryujinx.Ava/UI/ViewModels/ControllerInputViewModel.cs index ef8ffd50d..c0c625321 100644 --- a/src/Ryujinx.Ava/UI/ViewModels/Input/InputViewModel.cs +++ b/src/Ryujinx.Ava/UI/ViewModels/ControllerInputViewModel.cs @@ -8,7 +8,7 @@ using Ryujinx.Ava.Common.Locale; using Ryujinx.Ava.Input; using Ryujinx.Ava.UI.Helpers; using Ryujinx.Ava.UI.Models; -using Ryujinx.Ava.UI.Models.Input; +using Ryujinx.Ava.UI.Views.Input; using Ryujinx.Ava.UI.Windows; using Ryujinx.Common; using Ryujinx.Common.Configuration; @@ -30,9 +30,9 @@ using ConfigGamepadInputId = Ryujinx.Common.Configuration.Hid.Controller.Gamepad using ConfigStickInputId = Ryujinx.Common.Configuration.Hid.Controller.StickInputId; using Key = Ryujinx.Common.Configuration.Hid.Key; -namespace Ryujinx.Ava.UI.ViewModels.Input +namespace Ryujinx.Ava.UI.ViewModels { - public class InputViewModel : BaseModel, IDisposable + public class ControllerInputViewModel : BaseModel, IDisposable { private const string Disabled = "disabled"; private const string ProControllerResource = "Ryujinx.Ui.Common/Resources/Controller_ProCon.svg"; @@ -48,7 +48,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input private int _controllerNumber; private string _controllerImage; private int _device; - private object _configViewModel; + private object _configuration; private string _profileName; private bool _isLoaded; @@ -71,14 +71,13 @@ namespace Ryujinx.Ava.UI.ViewModels.Input public bool IsLeft { get; set; } public bool IsModified { get; set; } - public event Action NotifyChangesEvent; - public object ConfigViewModel + public object Configuration { - get => _configViewModel; + get => _configuration; set { - _configViewModel = value; + _configuration = value; OnPropertyChanged(); } @@ -233,7 +232,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input public InputConfig Config { get; set; } - public InputViewModel(UserControl owner) : this() + public ControllerInputViewModel(UserControl owner) : this() { if (Program.PreviewerDetached) { @@ -245,6 +244,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input _mainWindow.InputManager.GamepadDriver.OnGamepadConnected += HandleOnGamepadConnected; _mainWindow.InputManager.GamepadDriver.OnGamepadDisconnected += HandleOnGamepadDisconnected; + _mainWindow.ViewModel.AppHost?.NpadManager.BlockInputUpdates(); _isLoaded = false; @@ -255,7 +255,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input } } - public InputViewModel() + public ControllerInputViewModel() { PlayerIndexes = new ObservableCollection(); Controllers = new ObservableCollection(); @@ -282,12 +282,12 @@ namespace Ryujinx.Ava.UI.ViewModels.Input if (Config is StandardKeyboardInputConfig keyboardInputConfig) { - ConfigViewModel = new KeyboardInputViewModel(this, new KeyboardInputConfig(keyboardInputConfig)); + Configuration = new InputConfiguration(keyboardInputConfig); } if (Config is StandardControllerInputConfig controllerInputConfig) { - ConfigViewModel = new ControllerInputViewModel(this, new ControllerInputConfig(controllerInputConfig)); + Configuration = new InputConfiguration(controllerInputConfig); } } @@ -323,6 +323,16 @@ namespace Ryujinx.Ava.UI.ViewModels.Input } } + public async void ShowMotionConfig() + { + await MotionInputView.Show(this); + } + + public async void ShowRumbleConfig() + { + await RumbleInputView.Show(this); + } + private void LoadInputDriver() { if (_device < 0) @@ -730,7 +740,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input return; } - if (ConfigViewModel == null) + if (Configuration == null) { return; } @@ -741,37 +751,35 @@ namespace Ryujinx.Ava.UI.ViewModels.Input return; } + + bool validFileName = ProfileName.IndexOfAny(Path.GetInvalidFileNameChars()) == -1; + + if (validFileName) + { + string path = Path.Combine(GetProfileBasePath(), ProfileName + ".json"); + + InputConfig config = null; + + if (IsKeyboard) + { + config = (Configuration as InputConfiguration).GetConfig(); + } + else if (IsController) + { + config = (Configuration as InputConfiguration).GetConfig(); + } + + config.ControllerType = Controllers[_controller].Type; + + string jsonString = JsonHelper.Serialize(config, _serializerContext.InputConfig); + + await File.WriteAllTextAsync(path, jsonString); + + LoadProfiles(); + } else { - 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]); - } + await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogProfileInvalidProfileNameErrorMessage]); } } @@ -822,18 +830,18 @@ namespace Ryujinx.Ava.UI.ViewModels.Input if (device.Type == DeviceType.Keyboard) { - var inputConfig = (ConfigViewModel as KeyboardInputViewModel).Config; + var inputConfig = Configuration as InputConfiguration; inputConfig.Id = device.Id; } else { - var inputConfig = (ConfigViewModel as ControllerInputViewModel).Config; + var inputConfig = Configuration as InputConfiguration; inputConfig.Id = device.Id.Split(" ")[0]; } var config = !IsController - ? (ConfigViewModel as KeyboardInputViewModel).Config.GetConfig() - : (ConfigViewModel as ControllerInputViewModel).Config.GetConfig(); + ? (Configuration as InputConfiguration).GetConfig() + : (Configuration as InputConfiguration).GetConfig(); config.ControllerType = Controllers[_controller].Type; config.PlayerIndex = _playerId; @@ -864,13 +872,12 @@ namespace Ryujinx.Ava.UI.ViewModels.Input public void NotifyChanges() { - OnPropertyChanged(nameof(ConfigViewModel)); + OnPropertyChanged(nameof(Configuration)); OnPropertyChanged(nameof(IsController)); OnPropertyChanged(nameof(ShowSettings)); OnPropertyChanged(nameof(IsKeyboard)); OnPropertyChanged(nameof(IsRight)); OnPropertyChanged(nameof(IsLeft)); - NotifyChangesEvent?.Invoke(); } public void Dispose() diff --git a/src/Ryujinx.Ava/UI/ViewModels/Input/ControllerInputViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/Input/ControllerInputViewModel.cs deleted file mode 100644 index 0e23dfa76..000000000 --- a/src/Ryujinx.Ava/UI/ViewModels/Input/ControllerInputViewModel.cs +++ /dev/null @@ -1,84 +0,0 @@ -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 ControllerInputConfig _config; - public ControllerInputConfig 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 InputViewModel parentModel; - - public ControllerInputViewModel(InputViewModel model, ControllerInputConfig 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.Ava/UI/ViewModels/Input/KeyboardInputViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/Input/KeyboardInputViewModel.cs deleted file mode 100644 index a93873063..000000000 --- a/src/Ryujinx.Ava/UI/ViewModels/Input/KeyboardInputViewModel.cs +++ /dev/null @@ -1,73 +0,0 @@ -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 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.Ava/UI/ViewModels/Input/MotionInputViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/MotionInputViewModel.cs similarity index 97% rename from src/Ryujinx.Ava/UI/ViewModels/Input/MotionInputViewModel.cs rename to src/Ryujinx.Ava/UI/ViewModels/MotionInputViewModel.cs index c9ed8f2d4..0b12a51f6 100644 --- a/src/Ryujinx.Ava/UI/ViewModels/Input/MotionInputViewModel.cs +++ b/src/Ryujinx.Ava/UI/ViewModels/MotionInputViewModel.cs @@ -1,4 +1,4 @@ -namespace Ryujinx.Ava.UI.ViewModels.Input +namespace Ryujinx.Ava.UI.ViewModels { public class MotionInputViewModel : BaseModel { diff --git a/src/Ryujinx.Ava/UI/ViewModels/Input/RumbleInputViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/RumbleInputViewModel.cs similarity index 92% rename from src/Ryujinx.Ava/UI/ViewModels/Input/RumbleInputViewModel.cs rename to src/Ryujinx.Ava/UI/ViewModels/RumbleInputViewModel.cs index 8ad33cf4c..49de19937 100644 --- a/src/Ryujinx.Ava/UI/ViewModels/Input/RumbleInputViewModel.cs +++ b/src/Ryujinx.Ava/UI/ViewModels/RumbleInputViewModel.cs @@ -1,4 +1,4 @@ -namespace Ryujinx.Ava.UI.ViewModels.Input +namespace Ryujinx.Ava.UI.ViewModels { public class RumbleInputViewModel : BaseModel { diff --git a/src/Ryujinx.Ava/UI/Views/Input/ControllerInputView.axaml b/src/Ryujinx.Ava/UI/Views/Input/ControllerInputView.axaml index 08bdf90f4..d636873a3 100644 --- a/src/Ryujinx.Ava/UI/Views/Input/ControllerInputView.axaml +++ b/src/Ryujinx.Ava/UI/Views/Input/ControllerInputView.axaml @@ -1,11 +1,13 @@ @@ -33,10 +34,191 @@ HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Orientation="Vertical"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + MinHeight="450" + IsVisible="{Binding ShowSettings}"> @@ -75,9 +257,9 @@ VerticalAlignment="Center" Text="{locale:Locale ControllerSettingsTriggerZL}" TextAlignment="Center" /> - + @@ -91,9 +273,9 @@ VerticalAlignment="Center" Text="{locale:Locale ControllerSettingsTriggerL}" TextAlignment="Center" /> - + @@ -107,9 +289,9 @@ VerticalAlignment="Center" Text="{locale:Locale ControllerSettingsButtonMinus}" TextAlignment="Center" /> - + @@ -129,8 +311,100 @@ Margin="0,0,0,10" HorizontalAlignment="Center" Text="{locale:Locale ControllerSettingsLStick}" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + @@ -141,9 +415,9 @@ VerticalAlignment="Center" Text="{locale:Locale ControllerSettingsStickButton}" TextAlignment="Center" /> - + @@ -158,22 +432,22 @@ VerticalAlignment="Center" Text="{locale:Locale ControllerSettingsStickStick}" TextAlignment="Center" /> - + - + - + - + + Value="{ReflectionBinding Configuration.DeadzoneLeft, Mode=TwoWay}" /> + Text="{ReflectionBinding Configuration.DeadzoneLeft, StringFormat=\{0:0.00\}}" /> + Value="{ReflectionBinding Configuration.RangeLeft, Mode=TwoWay}" /> + Text="{ReflectionBinding Configuration.RangeLeft, StringFormat=\{0:0.00\}}" /> @@ -251,9 +525,9 @@ VerticalAlignment="Center" Text="{locale:Locale ControllerSettingsDPadUp}" TextAlignment="Center" /> - + @@ -268,9 +542,9 @@ VerticalAlignment="Center" Text="{locale:Locale ControllerSettingsDPadDown}" TextAlignment="Center" /> - + @@ -285,9 +559,9 @@ VerticalAlignment="Center" Text="{locale:Locale ControllerSettingsDPadLeft}" TextAlignment="Center" /> - + @@ -302,9 +576,9 @@ VerticalAlignment="Center" Text="{locale:Locale ControllerSettingsDPadRight}" TextAlignment="Center" /> - + @@ -317,13 +591,6 @@ Grid.Column="1" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"> - - + Value="{ReflectionBinding Configuration.TriggerThreshold, Mode=TwoWay}" /> + Text="{ReflectionBinding Configuration.TriggerThreshold, StringFormat=\{0:0.00\}}" /> - + + Text="{locale:Locale ControllerSettingsLeftSR}" + TextAlignment="Center" /> + - - - - - + + + + Text="{locale:Locale ControllerSettingsLeftSL}" + TextAlignment="Center" /> + - - - - - + + + + Text="{locale:Locale ControllerSettingsRightSR}" + TextAlignment="Center" /> + - - - - - + + + + Text="{locale:Locale ControllerSettingsRightSL}" + TextAlignment="Center" /> + - - - - + + + + HorizontalAlignment="Stretch" + IsVisible="{Binding IsController}"> @@ -449,7 +720,7 @@ Margin="10" MinWidth="0" Grid.Column="0" - IsChecked="{Binding Config.EnableMotion, Mode=TwoWay}"> + IsChecked="{ReflectionBinding Configuration.EnableMotion, Mode=TwoWay}"> - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Ryujinx.Ava/UI/Views/Input/InputView.axaml.cs b/src/Ryujinx.Ava/UI/Views/Input/InputView.axaml.cs deleted file mode 100644 index 356381a8a..000000000 --- a/src/Ryujinx.Ava/UI/Views/Input/InputView.axaml.cs +++ /dev/null @@ -1,61 +0,0 @@ -using Avalonia.Controls; -using Ryujinx.Ava.Common.Locale; -using Ryujinx.Ava.UI.Helpers; -using Ryujinx.Ava.UI.Models; -using Ryujinx.Ava.UI.ViewModels.Input; - -namespace Ryujinx.Ava.UI.Views.Input -{ - public partial class InputView : UserControl - { - private bool _dialogOpen; - private InputViewModel ViewModel { get; set; } - - public InputView() - { - DataContext = ViewModel = new InputViewModel(this); - - InitializeComponent(); - } - - public void SaveCurrentProfile() - { - ViewModel.Save(); - } - - private async void PlayerIndexBox_OnSelectionChanged(object sender, SelectionChangedEventArgs e) - { - if (ViewModel.IsModified && !_dialogOpen) - { - _dialogOpen = true; - - var result = await ContentDialogHelper.CreateConfirmationDialog( - LocaleManager.Instance[LocaleKeys.DialogControllerSettingsModifiedConfirmMessage], - LocaleManager.Instance[LocaleKeys.DialogControllerSettingsModifiedConfirmSubMessage], - LocaleManager.Instance[LocaleKeys.InputDialogYes], - LocaleManager.Instance[LocaleKeys.InputDialogNo], - LocaleManager.Instance[LocaleKeys.RyujinxConfirm]); - - if (result == UserResult.Yes) - { - ViewModel.Save(); - } - - _dialogOpen = false; - - ViewModel.IsModified = false; - - if (e.AddedItems.Count > 0) - { - var player = (PlayerModel)e.AddedItems[0]; - ViewModel.PlayerId = player.Id; - } - } - } - - public void Dispose() - { - ViewModel.Dispose(); - } - } -} diff --git a/src/Ryujinx.Ava/UI/Views/Input/KeyboardInputView.axaml b/src/Ryujinx.Ava/UI/Views/Input/KeyboardInputView.axaml deleted file mode 100644 index e4566f463..000000000 --- a/src/Ryujinx.Ava/UI/Views/Input/KeyboardInputView.axaml +++ /dev/null @@ -1,675 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/Ryujinx.Ava/UI/Views/Input/KeyboardInputView.axaml.cs b/src/Ryujinx.Ava/UI/Views/Input/KeyboardInputView.axaml.cs deleted file mode 100644 index f7024c5d1..000000000 --- a/src/Ryujinx.Ava/UI/Views/Input/KeyboardInputView.axaml.cs +++ /dev/null @@ -1,210 +0,0 @@ -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; - -namespace Ryujinx.Ava.UI.Views.Input -{ - public partial class KeyboardInputView : UserControl - { - private ButtonKeyAssigner _currentAssigner; - - public KeyboardInputView() - { - InitializeComponent(); - - foreach (ILogical visual in SettingButtons.GetLogicalDescendants()) - { - if (visual is ToggleButton button and not CheckBox) - { - button.IsCheckedChanged += Button_IsCheckedChanged; - } - } - } - - protected override void OnPointerReleased(PointerReleasedEventArgs e) - { - base.OnPointerReleased(e); - - if (_currentAssigner != null && _currentAssigner.ToggledButton != null && !_currentAssigner.ToggledButton.IsPointerOver) - { - _currentAssigner.Cancel(); - } - } - - private void Button_IsCheckedChanged(object sender, RoutedEventArgs e) - { - if (sender is ToggleButton button) - { - if ((bool)button.IsChecked) - { - if (_currentAssigner != null && button == _currentAssigner.ToggledButton) - { - return; - } - - bool isStick = button.Tag != null && button.Tag.ToString() == "stick"; - - if (_currentAssigner == null && (bool)button.IsChecked) - { - _currentAssigner = new ButtonKeyAssigner(button); - - this.Focus(NavigationMethod.Pointer); - - PointerPressed += MouseClick; - - IKeyboard keyboard = (IKeyboard)(DataContext as KeyboardInputViewModel).parentModel.AvaloniaKeyboardDriver.GetGamepad("0"); // Open Avalonia keyboard for cancel operations. - IButtonAssigner assigner = CreateButtonAssigner(isStick); - - _currentAssigner.ButtonAssigned += (sender, e) => - { - if (e.ButtonValue.HasValue) - { - var viewModel = (DataContext as KeyboardInputViewModel); - var buttonValue = e.ButtonValue.Value; - viewModel.parentModel.IsModified = true; - - switch (button.Name) - { - case "ButtonZl": - viewModel.Config.ButtonZl = buttonValue.AsKey(); - break; - case "ButtonL": - viewModel.Config.ButtonL = buttonValue.AsKey(); - break; - case "ButtonMinus": - viewModel.Config.ButtonMinus = buttonValue.AsKey(); - break; - case "LeftStickButton": - viewModel.Config.LeftStickButton = buttonValue.AsKey(); - break; - case "LeftStickUp": - viewModel.Config.LeftStickUp = buttonValue.AsKey(); - break; - case "LeftStickDown": - viewModel.Config.LeftStickDown = buttonValue.AsKey(); - break; - case "LeftStickRight": - viewModel.Config.LeftStickRight = buttonValue.AsKey(); - break; - case "LeftStickLeft": - viewModel.Config.LeftStickLeft = buttonValue.AsKey(); - break; - case "DpadUp": - viewModel.Config.DpadUp = buttonValue.AsKey(); - break; - case "DpadDown": - viewModel.Config.DpadDown = buttonValue.AsKey(); - break; - case "DpadLeft": - viewModel.Config.DpadLeft = buttonValue.AsKey(); - break; - case "DpadRight": - viewModel.Config.DpadRight = buttonValue.AsKey(); - break; - case "LeftButtonSr": - viewModel.Config.LeftButtonSr = buttonValue.AsKey(); - break; - case "LeftButtonSl": - viewModel.Config.LeftButtonSl = buttonValue.AsKey(); - break; - case "RightButtonSr": - viewModel.Config.RightButtonSr = buttonValue.AsKey(); - break; - case "RightButtonSl": - viewModel.Config.RightButtonSl = buttonValue.AsKey(); - break; - case "ButtonZr": - viewModel.Config.ButtonZr = buttonValue.AsKey(); - break; - case "ButtonR": - viewModel.Config.ButtonR = buttonValue.AsKey(); - break; - case "ButtonPlus": - viewModel.Config.ButtonPlus = buttonValue.AsKey(); - break; - case "ButtonA": - viewModel.Config.ButtonA = buttonValue.AsKey(); - break; - case "ButtonB": - viewModel.Config.ButtonB = buttonValue.AsKey(); - break; - case "ButtonX": - viewModel.Config.ButtonX = buttonValue.AsKey(); - break; - case "ButtonY": - viewModel.Config.ButtonY = buttonValue.AsKey(); - break; - case "RightStickButton": - viewModel.Config.RightStickButton = buttonValue.AsKey(); - break; - case "RightStickUp": - viewModel.Config.RightStickUp = buttonValue.AsKey(); - break; - case "RightStickDown": - viewModel.Config.RightStickDown = buttonValue.AsKey(); - break; - case "RightStickRight": - viewModel.Config.RightStickRight = buttonValue.AsKey(); - break; - case "RightStickLeft": - viewModel.Config.RightStickLeft = buttonValue.AsKey(); - break; - } - } - }; - - _currentAssigner.GetInputAndAssign(assigner, keyboard); - } - else - { - if (_currentAssigner != null) - { - ToggleButton oldButton = _currentAssigner.ToggledButton; - - _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; - - assigner = new KeyboardKeyAssigner((IKeyboard)(DataContext as KeyboardInputViewModel).parentModel.SelectedGamepad); - - return assigner; - } - - protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) - { - base.OnDetachedFromVisualTree(e); - _currentAssigner?.Cancel(); - _currentAssigner = null; - } - } -} diff --git a/src/Ryujinx.Ava/UI/Views/Input/MotionInputView.axaml b/src/Ryujinx.Ava/UI/Views/Input/MotionInputView.axaml index 0d018e297..a6b587f67 100644 --- a/src/Ryujinx.Ava/UI/Views/Input/MotionInputView.axaml +++ b/src/Ryujinx.Ava/UI/Views/Input/MotionInputView.axaml @@ -6,7 +6,7 @@ xmlns:controls="clr-namespace:Ryujinx.Ava.UI.Controls" xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale" - xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels.Input" + xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels" mc:Ignorable="d" x:Class="Ryujinx.Ava.UI.Views.Input.MotionInputView" x:DataType="viewModels:MotionInputViewModel" diff --git a/src/Ryujinx.Ava/UI/Views/Input/MotionInputView.axaml.cs b/src/Ryujinx.Ava/UI/Views/Input/MotionInputView.axaml.cs index 2304364b6..1b340752b 100644 --- a/src/Ryujinx.Ava/UI/Views/Input/MotionInputView.axaml.cs +++ b/src/Ryujinx.Ava/UI/Views/Input/MotionInputView.axaml.cs @@ -1,7 +1,9 @@ using Avalonia.Controls; using FluentAvalonia.UI.Controls; using Ryujinx.Ava.Common.Locale; -using Ryujinx.Ava.UI.ViewModels.Input; +using Ryujinx.Ava.UI.Models; +using Ryujinx.Ava.UI.ViewModels; +using Ryujinx.Common.Configuration.Hid.Controller; using System.Threading.Tasks; namespace Ryujinx.Ava.UI.Views.Input @@ -17,7 +19,7 @@ namespace Ryujinx.Ava.UI.Views.Input public MotionInputView(ControllerInputViewModel viewModel) { - var config = viewModel.Config; + var config = viewModel.Configuration as InputConfiguration; _viewModel = new MotionInputViewModel { @@ -49,7 +51,7 @@ namespace Ryujinx.Ava.UI.Views.Input }; contentDialog.PrimaryButtonClick += (sender, args) => { - var config = viewModel.Config; + var config = viewModel.Configuration as InputConfiguration; config.Slot = content._viewModel.Slot; config.Sensitivity = content._viewModel.Sensitivity; config.GyroDeadzone = content._viewModel.GyroDeadzone; diff --git a/src/Ryujinx.Ava/UI/Views/Input/RumbleInputView.axaml b/src/Ryujinx.Ava/UI/Views/Input/RumbleInputView.axaml index 1beb1f06e..5b7087a47 100644 --- a/src/Ryujinx.Ava/UI/Views/Input/RumbleInputView.axaml +++ b/src/Ryujinx.Ava/UI/Views/Input/RumbleInputView.axaml @@ -5,7 +5,7 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale" - xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels.Input" + xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels" mc:Ignorable="d" x:Class="Ryujinx.Ava.UI.Views.Input.RumbleInputView" x:DataType="viewModels:RumbleInputViewModel" diff --git a/src/Ryujinx.Ava/UI/Views/Input/RumbleInputView.axaml.cs b/src/Ryujinx.Ava/UI/Views/Input/RumbleInputView.axaml.cs index 58a4b416b..9307f872c 100644 --- a/src/Ryujinx.Ava/UI/Views/Input/RumbleInputView.axaml.cs +++ b/src/Ryujinx.Ava/UI/Views/Input/RumbleInputView.axaml.cs @@ -1,7 +1,9 @@ using Avalonia.Controls; using FluentAvalonia.UI.Controls; using Ryujinx.Ava.Common.Locale; -using Ryujinx.Ava.UI.ViewModels.Input; +using Ryujinx.Ava.UI.Models; +using Ryujinx.Ava.UI.ViewModels; +using Ryujinx.Common.Configuration.Hid.Controller; using System.Threading.Tasks; namespace Ryujinx.Ava.UI.Views.Input @@ -17,7 +19,7 @@ namespace Ryujinx.Ava.UI.Views.Input public RumbleInputView(ControllerInputViewModel viewModel) { - var config = viewModel.Config; + var config = viewModel.Configuration as InputConfiguration; _viewModel = new RumbleInputViewModel { @@ -45,7 +47,7 @@ namespace Ryujinx.Ava.UI.Views.Input contentDialog.PrimaryButtonClick += (sender, args) => { - var config = viewModel.Config; + var config = viewModel.Configuration as InputConfiguration; config.StrongRumble = content._viewModel.StrongRumble; config.WeakRumble = content._viewModel.WeakRumble; }; diff --git a/src/Ryujinx.Ava/UI/Views/Settings/SettingsInputView.axaml b/src/Ryujinx.Ava/UI/Views/Settings/SettingsInputView.axaml index 55c2ed6e3..81f4b68b7 100644 --- a/src/Ryujinx.Ava/UI/Views/Settings/SettingsInputView.axaml +++ b/src/Ryujinx.Ava/UI/Views/Settings/SettingsInputView.axaml @@ -27,9 +27,9 @@ - + Name="ControllerSettings" /> diff --git a/src/Ryujinx.Ava/UI/Views/Settings/SettingsInputView.axaml.cs b/src/Ryujinx.Ava/UI/Views/Settings/SettingsInputView.axaml.cs index 85ccffccd..8a0cb8ab9 100644 --- a/src/Ryujinx.Ava/UI/Views/Settings/SettingsInputView.axaml.cs +++ b/src/Ryujinx.Ava/UI/Views/Settings/SettingsInputView.axaml.cs @@ -11,7 +11,7 @@ namespace Ryujinx.Ava.UI.Views.Settings public void Dispose() { - InputView.Dispose(); + ControllerSettings.Dispose(); } } } diff --git a/src/Ryujinx.Ava/UI/Windows/SettingsWindow.axaml.cs b/src/Ryujinx.Ava/UI/Windows/SettingsWindow.axaml.cs index 314501c52..d7bb0b883 100644 --- a/src/Ryujinx.Ava/UI/Windows/SettingsWindow.axaml.cs +++ b/src/Ryujinx.Ava/UI/Windows/SettingsWindow.axaml.cs @@ -37,7 +37,7 @@ namespace Ryujinx.Ava.UI.Windows public void SaveSettings() { - InputPage.InputView?.SaveCurrentProfile(); + InputPage.ControllerSettings?.SaveCurrentProfile(); if (Owner is MainWindow window && ViewModel.DirectoryChanged) { diff --git a/src/Ryujinx.Input/Assigner/GamepadButtonAssigner.cs b/src/Ryujinx.Input/Assigner/GamepadButtonAssigner.cs index bf8319a6a..388ebcc07 100644 --- a/src/Ryujinx.Input/Assigner/GamepadButtonAssigner.cs +++ b/src/Ryujinx.Input/Assigner/GamepadButtonAssigner.cs @@ -59,16 +59,16 @@ namespace Ryujinx.Input.Assigner return _gamepad == null || !_gamepad.IsConnected; } - public ButtonValue? GetPressedButton() + public string GetPressedButton() { IEnumerable pressedButtons = _detector.GetPressedButtons(); if (pressedButtons.Any()) { - return !_forStick ? new(pressedButtons.First()) : new(((StickInputId)pressedButtons.First())); + return !_forStick ? pressedButtons.First().ToString() : ((StickInputId)pressedButtons.First()).ToString(); } - return null; + return ""; } private void CollectButtonStats() diff --git a/src/Ryujinx.Input/Assigner/IButtonAssigner.cs b/src/Ryujinx.Input/Assigner/IButtonAssigner.cs index 653717133..76a9fece4 100644 --- a/src/Ryujinx.Input/Assigner/IButtonAssigner.cs +++ b/src/Ryujinx.Input/Assigner/IButtonAssigner.cs @@ -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 - ButtonValue? GetPressedButton(); + string GetPressedButton(); } } diff --git a/src/Ryujinx.Input/Assigner/KeyboardKeyAssigner.cs b/src/Ryujinx.Input/Assigner/KeyboardKeyAssigner.cs index c66812ba0..e52ef4a2c 100644 --- a/src/Ryujinx.Input/Assigner/KeyboardKeyAssigner.cs +++ b/src/Ryujinx.Input/Assigner/KeyboardKeyAssigner.cs @@ -23,7 +23,7 @@ namespace Ryujinx.Input.Assigner public bool HasAnyButtonPressed() { - return GetPressedButton() is not null; + return GetPressedButton().Length != 0; } public bool ShouldCancel() @@ -31,20 +31,20 @@ namespace Ryujinx.Input.Assigner return _keyboardState.IsPressed(Key.Escape); } - public ButtonValue? GetPressedButton() + public string GetPressedButton() { - ButtonValue? keyPressed = null; + string keyPressed = ""; for (Key key = Key.Unknown; key < Key.Count; key++) { if (_keyboardState.IsPressed(key)) { - keyPressed = new(key); + keyPressed = key.ToString(); break; } } - return !ShouldCancel() ? keyPressed : null; + return !ShouldCancel() ? keyPressed : ""; } } } diff --git a/src/Ryujinx.Input/ButtonValue.cs b/src/Ryujinx.Input/ButtonValue.cs deleted file mode 100644 index f037e6b60..000000000 --- a/src/Ryujinx.Input/ButtonValue.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System.Diagnostics; - -namespace Ryujinx.Input -{ - public enum ButtonValueType { Key, GamepadButtonInputId, StickId } - - public readonly struct ButtonValue - { - private readonly ButtonValueType _type; - private readonly uint _rawValue; - - public ButtonValue(Key key) - { - _type = ButtonValueType.Key; - _rawValue = (uint)key; - } - - public ButtonValue(GamepadButtonInputId gamepad) - { - _type = ButtonValueType.GamepadButtonInputId; - _rawValue = (uint)gamepad; - } - - public ButtonValue(StickInputId stick) - { - _type = ButtonValueType.StickId; - _rawValue = (uint)stick; - } - - public Common.Configuration.Hid.Key AsKey() - { - Debug.Assert(_type == ButtonValueType.Key); - return (Common.Configuration.Hid.Key)_rawValue; - } - - public Common.Configuration.Hid.Controller.GamepadInputId AsGamepadButtonInputId() - { - Debug.Assert(_type == ButtonValueType.GamepadButtonInputId); - return (Common.Configuration.Hid.Controller.GamepadInputId)_rawValue; - } - - public Common.Configuration.Hid.Controller.StickInputId AsGamepadStickId() - { - Debug.Assert(_type == ButtonValueType.StickId); - return (Common.Configuration.Hid.Controller.StickInputId)_rawValue; - } - } -} diff --git a/src/Ryujinx/Ui/Windows/ControllerWindow.cs b/src/Ryujinx/Ui/Windows/ControllerWindow.cs index 52cad5c85..ebf22ab60 100644 --- a/src/Ryujinx/Ui/Windows/ControllerWindow.cs +++ b/src/Ryujinx/Ui/Windows/ControllerWindow.cs @@ -893,7 +893,7 @@ namespace Ryujinx.Ui.Windows } } - string pressedButton = assigner.GetPressedButton().ToString(); + string pressedButton = assigner.GetPressedButton(); Application.Invoke(delegate { From 33ba1703158564c2c3564fa329fd2e630f8a8e95 Mon Sep 17 00:00:00 2001 From: gdkchan Date: Sun, 22 Oct 2023 15:31:36 -0300 Subject: [PATCH 098/105] Fix NRE on gather operations with depth compare on macOS (#5832) --- src/Ryujinx.Graphics.Shader/Instructions/InstEmitTexture.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Ryujinx.Graphics.Shader/Instructions/InstEmitTexture.cs b/src/Ryujinx.Graphics.Shader/Instructions/InstEmitTexture.cs index 5a231079a..55f7d5778 100644 --- a/src/Ryujinx.Graphics.Shader/Instructions/InstEmitTexture.cs +++ b/src/Ryujinx.Graphics.Shader/Instructions/InstEmitTexture.cs @@ -766,7 +766,10 @@ namespace Ryujinx.Graphics.Shader.Instructions flags |= offset == TexOffset.Ptp ? TextureFlags.Offsets : TextureFlags.Offset; } - sourcesList.Add(Const((int)component)); + if (!hasDepthCompare) + { + sourcesList.Add(Const((int)component)); + } Operand[] sources = sourcesList.ToArray(); Operand[] dests = new Operand[BitOperations.PopCount((uint)componentMask)]; From d773d5152e685a164a6eb9f419873ef1908364f7 Mon Sep 17 00:00:00 2001 From: Alex Barney Date: Sun, 22 Oct 2023 16:30:46 -0700 Subject: [PATCH 099/105] Update to LibHac 0.19.0 (#5831) * Update to LibHac v0.19.0 - PartitionFileSystem classes now fully match Nintendo's implementation. Current code creating a PartitionFileSystem now need to use the Initialize method. - Implementing nn::gcsrv and nn::sdmmcsrv now means the FS server now uses that abstraction instead of the old one where we passed in an IDeviceOperator. * Add GetFileSystemAttribute --- Directory.Packages.props | 2 +- src/Ryujinx.Ava/Common/ApplicationHelper.cs | 6 ++++-- .../DownloadableContentManagerViewModel.cs | 6 ++++-- .../UI/ViewModels/TitleUpdateViewModel.cs | 4 +++- src/Ryujinx.HLE/FileSystem/ContentManager.cs | 11 ++++++----- .../FileSystem/VirtualFileSystem.cs | 9 +++++---- src/Ryujinx.HLE/HOS/ModLoader.cs | 4 +++- .../FileSystemProxy/FileSystemProxyHelper.cs | 7 +++++-- .../Fs/FileSystemProxy/IFileSystem.cs | 11 +++++++++++ .../HOS/Services/Fs/IFileSystemProxy.cs | 5 ++++- .../PartitionFileSystemExtensions.cs | 9 +++++++-- .../Loaders/Processes/ProcessLoader.cs | 3 ++- .../Loaders/Processes/ProcessLoaderHelper.cs | 4 ++-- src/Ryujinx.Ui.Common/App/ApplicationData.cs | 6 ++++-- .../App/ApplicationLibrary.cs | 19 ++++++++++++------- .../Ui/Widgets/GameTableContextMenu.cs | 6 ++++-- src/Ryujinx/Ui/Windows/DlcWindow.cs | 6 ++++-- src/Ryujinx/Ui/Windows/TitleUpdateWindow.cs | 3 ++- 18 files changed, 83 insertions(+), 38 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 6fdaafddc..4fd079af6 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -18,7 +18,7 @@ - + diff --git a/src/Ryujinx.Ava/Common/ApplicationHelper.cs b/src/Ryujinx.Ava/Common/ApplicationHelper.cs index b8cd06f3d..91ca8f4d5 100644 --- a/src/Ryujinx.Ava/Common/ApplicationHelper.cs +++ b/src/Ryujinx.Ava/Common/ApplicationHelper.cs @@ -173,7 +173,7 @@ namespace Ryujinx.Ava.Common string extension = Path.GetExtension(titleFilePath).ToLower(); if (extension == ".nsp" || extension == ".pfs0" || extension == ".xci") { - PartitionFileSystem pfs; + IFileSystem pfs; if (extension == ".xci") { @@ -181,7 +181,9 @@ namespace Ryujinx.Ava.Common } else { - pfs = new PartitionFileSystem(file.AsStorage()); + var pfsTemp = new PartitionFileSystem(); + pfsTemp.Initialize(file.AsStorage()).ThrowIfFailure(); + pfs = pfsTemp; } foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca")) diff --git a/src/Ryujinx.Ava/UI/ViewModels/DownloadableContentManagerViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/DownloadableContentManagerViewModel.cs index b88bd3d9c..cdecae77d 100644 --- a/src/Ryujinx.Ava/UI/ViewModels/DownloadableContentManagerViewModel.cs +++ b/src/Ryujinx.Ava/UI/ViewModels/DownloadableContentManagerViewModel.cs @@ -126,7 +126,8 @@ namespace Ryujinx.Ava.UI.ViewModels { using FileStream containerFile = File.OpenRead(downloadableContentContainer.ContainerPath); - PartitionFileSystem partitionFileSystem = new(containerFile.AsStorage()); + PartitionFileSystem partitionFileSystem = new(); + partitionFileSystem.Initialize(containerFile.AsStorage()).ThrowIfFailure(); _virtualFileSystem.ImportTickets(partitionFileSystem); @@ -232,7 +233,8 @@ namespace Ryujinx.Ava.UI.ViewModels using FileStream containerFile = File.OpenRead(path); - PartitionFileSystem partitionFileSystem = new(containerFile.AsStorage()); + PartitionFileSystem partitionFileSystem = new(); + partitionFileSystem.Initialize(containerFile.AsStorage()).ThrowIfFailure(); bool containsDownloadableContent = false; _virtualFileSystem.ImportTickets(partitionFileSystem); diff --git a/src/Ryujinx.Ava/UI/ViewModels/TitleUpdateViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/TitleUpdateViewModel.cs index dd0b92a51..5090a8c70 100644 --- a/src/Ryujinx.Ava/UI/ViewModels/TitleUpdateViewModel.cs +++ b/src/Ryujinx.Ava/UI/ViewModels/TitleUpdateViewModel.cs @@ -170,7 +170,9 @@ namespace Ryujinx.Ava.UI.ViewModels try { - (Nca patchNca, Nca controlNca) = ApplicationLibrary.GetGameUpdateDataFromPartition(VirtualFileSystem, new PartitionFileSystem(file.AsStorage()), TitleId.ToString("x16"), 0); + var pfs = new PartitionFileSystem(); + pfs.Initialize(file.AsStorage()).ThrowIfFailure(); + (Nca patchNca, Nca controlNca) = ApplicationLibrary.GetGameUpdateDataFromPartition(VirtualFileSystem, pfs, TitleId.ToString("x16"), 0); if (controlNca != null && patchNca != null) { diff --git a/src/Ryujinx.HLE/FileSystem/ContentManager.cs b/src/Ryujinx.HLE/FileSystem/ContentManager.cs index 646808e78..8ade34a8b 100644 --- a/src/Ryujinx.HLE/FileSystem/ContentManager.cs +++ b/src/Ryujinx.HLE/FileSystem/ContentManager.cs @@ -238,7 +238,8 @@ namespace Ryujinx.HLE.FileSystem if (!mergedToContainer) { using FileStream fileStream = File.OpenRead(containerPath); - using PartitionFileSystem partitionFileSystem = new(fileStream.AsStorage()); + using PartitionFileSystem partitionFileSystem = new(); + partitionFileSystem.Initialize(fileStream.AsStorage()).ThrowIfFailure(); _virtualFileSystem.ImportTickets(partitionFileSystem); } @@ -259,16 +260,16 @@ namespace Ryujinx.HLE.FileSystem { var file = new FileStream(aoc.ContainerPath, FileMode.Open, FileAccess.Read); using var ncaFile = new UniqueRef(); - PartitionFileSystem pfs; switch (Path.GetExtension(aoc.ContainerPath)) { case ".xci": - pfs = new Xci(_virtualFileSystem.KeySet, file.AsStorage()).OpenPartition(XciPartitionType.Secure); - pfs.OpenFile(ref ncaFile.Ref, aoc.NcaPath.ToU8Span(), OpenMode.Read); + var xci = new Xci(_virtualFileSystem.KeySet, file.AsStorage()).OpenPartition(XciPartitionType.Secure); + xci.OpenFile(ref ncaFile.Ref, aoc.NcaPath.ToU8Span(), OpenMode.Read); break; case ".nsp": - pfs = new PartitionFileSystem(file.AsStorage()); + var pfs = new PartitionFileSystem(); + pfs.Initialize(file.AsStorage()); pfs.OpenFile(ref ncaFile.Ref, aoc.NcaPath.ToU8Span(), OpenMode.Read); break; default: diff --git a/src/Ryujinx.HLE/FileSystem/VirtualFileSystem.cs b/src/Ryujinx.HLE/FileSystem/VirtualFileSystem.cs index 807020c60..eaf481dd7 100644 --- a/src/Ryujinx.HLE/FileSystem/VirtualFileSystem.cs +++ b/src/Ryujinx.HLE/FileSystem/VirtualFileSystem.cs @@ -7,6 +7,7 @@ using LibHac.Fs.Shim; using LibHac.FsSrv; using LibHac.FsSystem; using LibHac.Ncm; +using LibHac.Sdmmc; using LibHac.Spl; using LibHac.Tools.Es; using LibHac.Tools.Fs; @@ -32,7 +33,7 @@ namespace Ryujinx.HLE.FileSystem public KeySet KeySet { get; private set; } public EmulatedGameCard GameCard { get; private set; } - public EmulatedSdCard SdCard { get; private set; } + public SdmmcApi SdCard { get; private set; } public ModLoader ModLoader { get; private set; } private readonly ConcurrentDictionary _romFsByPid; @@ -198,15 +199,15 @@ namespace Ryujinx.HLE.FileSystem fsServerObjects.FsCreators.EncryptedFileSystemCreator = new EncryptedFileSystemCreator(); GameCard = fsServerObjects.GameCard; - SdCard = fsServerObjects.SdCard; + SdCard = fsServerObjects.Sdmmc; - SdCard.SetSdCardInsertionStatus(true); + SdCard.SetSdCardInserted(true); var fsServerConfig = new FileSystemServerConfig { - DeviceOperator = fsServerObjects.DeviceOperator, ExternalKeySet = KeySet.ExternalKeySet, FsCreators = fsServerObjects.FsCreators, + StorageDeviceManagerFactory = fsServerObjects.StorageDeviceManagerFactory, RandomGenerator = randomGenerator, }; diff --git a/src/Ryujinx.HLE/HOS/ModLoader.cs b/src/Ryujinx.HLE/HOS/ModLoader.cs index 6706006c3..834bc0595 100644 --- a/src/Ryujinx.HLE/HOS/ModLoader.cs +++ b/src/Ryujinx.HLE/HOS/ModLoader.cs @@ -533,7 +533,9 @@ namespace Ryujinx.HLE.HOS Logger.Info?.Print(LogClass.ModLoader, "Using replacement ExeFS partition"); - exefs = new PartitionFileSystem(mods.ExefsContainers[0].Path.OpenRead().AsStorage()); + var pfs = new PartitionFileSystem(); + pfs.Initialize(mods.ExefsContainers[0].Path.OpenRead().AsStorage()).ThrowIfFailure(); + exefs = pfs; return true; } diff --git a/src/Ryujinx.HLE/HOS/Services/Fs/FileSystemProxy/FileSystemProxyHelper.cs b/src/Ryujinx.HLE/HOS/Services/Fs/FileSystemProxy/FileSystemProxyHelper.cs index 599025e3b..1ef52a00d 100644 --- a/src/Ryujinx.HLE/HOS/Services/Fs/FileSystemProxy/FileSystemProxyHelper.cs +++ b/src/Ryujinx.HLE/HOS/Services/Fs/FileSystemProxy/FileSystemProxyHelper.cs @@ -26,7 +26,9 @@ namespace Ryujinx.HLE.HOS.Services.Fs.FileSystemProxy try { LocalStorage storage = new(pfsPath, FileAccess.Read, FileMode.Open); - using SharedRef nsp = new(new PartitionFileSystem(storage)); + var pfs = new PartitionFileSystem(); + using SharedRef nsp = new(pfs); + pfs.Initialize(storage).ThrowIfFailure(); ImportTitleKeysFromNsp(nsp.Get, context.Device.System.KeySet); @@ -90,7 +92,8 @@ namespace Ryujinx.HLE.HOS.Services.Fs.FileSystemProxy try { - PartitionFileSystem nsp = new(pfsFile.AsStorage()); + PartitionFileSystem nsp = new(); + nsp.Initialize(pfsFile.AsStorage()).ThrowIfFailure(); ImportTitleKeysFromNsp(nsp, context.Device.System.KeySet); diff --git a/src/Ryujinx.HLE/HOS/Services/Fs/FileSystemProxy/IFileSystem.cs b/src/Ryujinx.HLE/HOS/Services/Fs/FileSystemProxy/IFileSystem.cs index 4c5c56240..66020d57b 100644 --- a/src/Ryujinx.HLE/HOS/Services/Fs/FileSystemProxy/IFileSystem.cs +++ b/src/Ryujinx.HLE/HOS/Services/Fs/FileSystemProxy/IFileSystem.cs @@ -1,6 +1,7 @@ using LibHac; using LibHac.Common; using LibHac.Fs; +using LibHac.Fs.Fsa; using Path = LibHac.FsSrv.Sf.Path; namespace Ryujinx.HLE.HOS.Services.Fs.FileSystemProxy @@ -202,6 +203,16 @@ namespace Ryujinx.HLE.HOS.Services.Fs.FileSystemProxy return (ResultCode)result.Value; } + [CommandCmif(16)] + public ResultCode GetFileSystemAttribute(ServiceCtx context) + { + Result result = _fileSystem.Get.GetFileSystemAttribute(out FileSystemAttribute attribute); + + context.ResponseData.Write(SpanHelpers.AsReadOnlyByteSpan(in attribute)); + + return (ResultCode)result.Value; + } + protected override void Dispose(bool isDisposing) { if (isDisposing) diff --git a/src/Ryujinx.HLE/HOS/Services/Fs/IFileSystemProxy.cs b/src/Ryujinx.HLE/HOS/Services/Fs/IFileSystemProxy.cs index 644e1a17a..24dd1e9be 100644 --- a/src/Ryujinx.HLE/HOS/Services/Fs/IFileSystemProxy.cs +++ b/src/Ryujinx.HLE/HOS/Services/Fs/IFileSystemProxy.cs @@ -1380,7 +1380,10 @@ namespace Ryujinx.HLE.HOS.Services.Fs [CommandCmif(1016)] public ResultCode FlushAccessLogOnSdCard(ServiceCtx context) { - return (ResultCode)_baseFileSystemProxy.Get.FlushAccessLogOnSdCard().Value; + // Logging the access log to the SD card isn't implemented, meaning this function will be a no-op since + // there's nothing to flush. Return success until it's implemented. + // return (ResultCode)_baseFileSystemProxy.Get.FlushAccessLogOnSdCard().Value; + return ResultCode.Success; } [CommandCmif(1017)] diff --git a/src/Ryujinx.HLE/Loaders/Processes/Extensions/PartitionFileSystemExtensions.cs b/src/Ryujinx.HLE/Loaders/Processes/Extensions/PartitionFileSystemExtensions.cs index 6de99131e..50f7d5853 100644 --- a/src/Ryujinx.HLE/Loaders/Processes/Extensions/PartitionFileSystemExtensions.cs +++ b/src/Ryujinx.HLE/Loaders/Processes/Extensions/PartitionFileSystemExtensions.cs @@ -20,7 +20,11 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions private static readonly DownloadableContentJsonSerializerContext _contentSerializerContext = new(JsonHelper.GetDefaultSerializerOptions()); private static readonly TitleUpdateMetadataJsonSerializerContext _titleSerializerContext = new(JsonHelper.GetDefaultSerializerOptions()); - internal static (bool, ProcessResult) TryLoad(this PartitionFileSystem partitionFileSystem, Switch device, string path, out string errorMessage) + internal static (bool, ProcessResult) TryLoad(this PartitionFileSystemCore partitionFileSystem, Switch device, string path, out string errorMessage) + where TMetaData : PartitionFileSystemMetaCore, new() + where TFormat : IPartitionFileSystemFormat + where THeader : unmanaged, IPartitionFileSystemHeader + where TEntry : unmanaged, IPartitionFileSystemEntry { errorMessage = null; @@ -91,7 +95,8 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions string updatePath = JsonHelper.DeserializeFromFile(titleUpdateMetadataPath, _titleSerializerContext.TitleUpdateMetadata).Selected; if (File.Exists(updatePath)) { - PartitionFileSystem updatePartitionFileSystem = new(new FileStream(updatePath, FileMode.Open, FileAccess.Read).AsStorage()); + PartitionFileSystem updatePartitionFileSystem = new(); + updatePartitionFileSystem.Initialize(new FileStream(updatePath, FileMode.Open, FileAccess.Read).AsStorage()).ThrowIfFailure(); device.Configuration.VirtualFileSystem.ImportTickets(updatePartitionFileSystem); diff --git a/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs b/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs index 51cbb6f99..220b868db 100644 --- a/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs +++ b/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs @@ -69,7 +69,8 @@ namespace Ryujinx.HLE.Loaders.Processes public bool LoadNsp(string path) { FileStream file = new(path, FileMode.Open, FileAccess.Read); - PartitionFileSystem partitionFileSystem = new(file.AsStorage()); + PartitionFileSystem partitionFileSystem = new(); + partitionFileSystem.Initialize(file.AsStorage()).ThrowIfFailure(); (bool success, ProcessResult processResult) = partitionFileSystem.TryLoad(_device, path, out string errorMessage); diff --git a/src/Ryujinx.HLE/Loaders/Processes/ProcessLoaderHelper.cs b/src/Ryujinx.HLE/Loaders/Processes/ProcessLoaderHelper.cs index 292a5c122..c229b1742 100644 --- a/src/Ryujinx.HLE/Loaders/Processes/ProcessLoaderHelper.cs +++ b/src/Ryujinx.HLE/Loaders/Processes/ProcessLoaderHelper.cs @@ -1,8 +1,8 @@ using LibHac.Account; using LibHac.Common; using LibHac.Fs; +using LibHac.Fs.Fsa; using LibHac.Fs.Shim; -using LibHac.FsSystem; using LibHac.Loader; using LibHac.Ncm; using LibHac.Ns; @@ -33,7 +33,7 @@ namespace Ryujinx.HLE.Loaders.Processes // TODO: Remove this workaround when ASLR is implemented. private const ulong CodeStartOffset = 0x500000UL; - public static LibHac.Result RegisterProgramMapInfo(Switch device, PartitionFileSystem partitionFileSystem) + public static LibHac.Result RegisterProgramMapInfo(Switch device, IFileSystem partitionFileSystem) { ulong applicationId = 0; int programCount = 0; diff --git a/src/Ryujinx.Ui.Common/App/ApplicationData.cs b/src/Ryujinx.Ui.Common/App/ApplicationData.cs index e6130bdac..1be883ee1 100644 --- a/src/Ryujinx.Ui.Common/App/ApplicationData.cs +++ b/src/Ryujinx.Ui.Common/App/ApplicationData.cs @@ -65,7 +65,7 @@ namespace Ryujinx.Ui.App.Common if (extension is ".nsp" or ".xci") { - PartitionFileSystem pfs; + IFileSystem pfs; if (extension == ".xci") { @@ -75,7 +75,9 @@ namespace Ryujinx.Ui.App.Common } else { - pfs = new PartitionFileSystem(file.AsStorage()); + var pfsTemp = new PartitionFileSystem(); + pfsTemp.Initialize(file.AsStorage()).ThrowIfFailure(); + pfs = pfsTemp; } foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca")) diff --git a/src/Ryujinx.Ui.Common/App/ApplicationLibrary.cs b/src/Ryujinx.Ui.Common/App/ApplicationLibrary.cs index 36b2b727d..2f688126a 100644 --- a/src/Ryujinx.Ui.Common/App/ApplicationLibrary.cs +++ b/src/Ryujinx.Ui.Common/App/ApplicationLibrary.cs @@ -174,7 +174,7 @@ namespace Ryujinx.Ui.App.Common { try { - PartitionFileSystem pfs; + IFileSystem pfs; bool isExeFs = false; @@ -186,7 +186,9 @@ namespace Ryujinx.Ui.App.Common } else { - pfs = new PartitionFileSystem(file.AsStorage()); + 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; @@ -500,7 +502,7 @@ namespace Ryujinx.Ui.App.Common ApplicationCountUpdated?.Invoke(null, e); } - private void GetControlFsAndTitleId(PartitionFileSystem pfs, out IFileSystem controlFs, out string titleId) + private void GetControlFsAndTitleId(IFileSystem pfs, out IFileSystem controlFs, out string titleId) { (_, _, Nca controlNca) = GetGameData(_virtualFileSystem, pfs, 0); @@ -563,7 +565,7 @@ namespace Ryujinx.Ui.App.Common { try { - PartitionFileSystem pfs; + IFileSystem pfs; bool isExeFs = false; @@ -575,7 +577,9 @@ namespace Ryujinx.Ui.App.Common } else { - pfs = new PartitionFileSystem(file.AsStorage()); + var pfsTemp = new PartitionFileSystem(); + pfsTemp.Initialize(file.AsStorage()).ThrowIfFailure(); + pfs = pfsTemp; foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*")) { @@ -827,7 +831,7 @@ namespace Ryujinx.Ui.App.Common return false; } - public static (Nca main, Nca patch, Nca control) GetGameData(VirtualFileSystem fileSystem, PartitionFileSystem pfs, int programIndex) + public static (Nca main, Nca patch, Nca control) GetGameData(VirtualFileSystem fileSystem, IFileSystem pfs, int programIndex) { Nca mainNca = null; Nca patchNca = null; @@ -931,7 +935,8 @@ namespace Ryujinx.Ui.App.Common if (File.Exists(updatePath)) { FileStream file = new(updatePath, FileMode.Open, FileAccess.Read); - PartitionFileSystem nsp = new(file.AsStorage()); + PartitionFileSystem nsp = new(); + nsp.Initialize(file.AsStorage()).ThrowIfFailure(); return GetGameUpdateDataFromPartition(fileSystem, nsp, titleIdBase.ToString("x16"), programIndex); } diff --git a/src/Ryujinx/Ui/Widgets/GameTableContextMenu.cs b/src/Ryujinx/Ui/Widgets/GameTableContextMenu.cs index ea60421f8..5af181b08 100644 --- a/src/Ryujinx/Ui/Widgets/GameTableContextMenu.cs +++ b/src/Ryujinx/Ui/Widgets/GameTableContextMenu.cs @@ -211,7 +211,7 @@ namespace Ryujinx.Ui.Widgets (System.IO.Path.GetExtension(_titleFilePath).ToLower() == ".pfs0") || (System.IO.Path.GetExtension(_titleFilePath).ToLower() == ".xci")) { - PartitionFileSystem pfs; + IFileSystem pfs; if (System.IO.Path.GetExtension(_titleFilePath) == ".xci") { @@ -221,7 +221,9 @@ namespace Ryujinx.Ui.Widgets } else { - pfs = new PartitionFileSystem(file.AsStorage()); + var pfsTemp = new PartitionFileSystem(); + pfsTemp.Initialize(file.AsStorage()).ThrowIfFailure(); + pfs = pfsTemp; } foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca")) diff --git a/src/Ryujinx/Ui/Windows/DlcWindow.cs b/src/Ryujinx/Ui/Windows/DlcWindow.cs index 74aef00f4..9f7179467 100644 --- a/src/Ryujinx/Ui/Windows/DlcWindow.cs +++ b/src/Ryujinx/Ui/Windows/DlcWindow.cs @@ -88,7 +88,8 @@ namespace Ryujinx.Ui.Windows using FileStream containerFile = File.OpenRead(dlcContainer.ContainerPath); - PartitionFileSystem pfs = new(containerFile.AsStorage()); + PartitionFileSystem pfs = new(); + pfs.Initialize(containerFile.AsStorage()).ThrowIfFailure(); _virtualFileSystem.ImportTickets(pfs); @@ -153,7 +154,8 @@ namespace Ryujinx.Ui.Windows using FileStream containerFile = File.OpenRead(containerPath); - PartitionFileSystem pfs = new(containerFile.AsStorage()); + PartitionFileSystem pfs = new(); + pfs.Initialize(containerFile.AsStorage()).ThrowIfFailure(); bool containsDlc = false; _virtualFileSystem.ImportTickets(pfs); diff --git a/src/Ryujinx/Ui/Windows/TitleUpdateWindow.cs b/src/Ryujinx/Ui/Windows/TitleUpdateWindow.cs index 044f7e95a..51918eeab 100644 --- a/src/Ryujinx/Ui/Windows/TitleUpdateWindow.cs +++ b/src/Ryujinx/Ui/Windows/TitleUpdateWindow.cs @@ -90,7 +90,8 @@ namespace Ryujinx.Ui.Windows { using FileStream file = new(path, FileMode.Open, FileAccess.Read); - PartitionFileSystem nsp = new(file.AsStorage()); + PartitionFileSystem nsp = new(); + nsp.Initialize(file.AsStorage()).ThrowIfFailure(); try { From b1f8f868f6fdec87bd3342ac379594bd695cbbfd Mon Sep 17 00:00:00 2001 From: Alex Barney Date: Mon, 23 Oct 2023 10:34:31 -0700 Subject: [PATCH 100/105] Fix the AOC manager using incorrect paths (#5840) * Fix the content manager using incorrect path for some AOC NCAs * Check Results in a few more places in the content manager --- src/Ryujinx.HLE/FileSystem/ContentManager.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Ryujinx.HLE/FileSystem/ContentManager.cs b/src/Ryujinx.HLE/FileSystem/ContentManager.cs index 8ade34a8b..724cb675c 100644 --- a/src/Ryujinx.HLE/FileSystem/ContentManager.cs +++ b/src/Ryujinx.HLE/FileSystem/ContentManager.cs @@ -198,7 +198,7 @@ namespace Ryujinx.HLE.FileSystem { using var ncaFile = new UniqueRef(); - fs.OpenFile(ref ncaFile.Ref, ncaPath.FullPath.ToU8Span(), OpenMode.Read); + 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) { @@ -210,7 +210,7 @@ namespace Ryujinx.HLE.FileSystem using var pfs0 = nca.OpenFileSystem(0, integrityCheckLevel); using var cnmtFile = new UniqueRef(); - pfs0.OpenFile(ref cnmtFile.Ref, pfs0.EnumerateEntries().Single().FullPath.ToU8Span(), OpenMode.Read); + 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) @@ -220,7 +220,7 @@ namespace Ryujinx.HLE.FileSystem string ncaId = Convert.ToHexString(cnmt.ContentEntries[0].NcaId).ToLower(); - AddAocItem(cnmt.TitleId, containerPath, $"{ncaId}.nca", true); + AddAocItem(cnmt.TitleId, containerPath, $"/{ncaId}.nca", true); } } @@ -265,12 +265,12 @@ namespace Ryujinx.HLE.FileSystem { case ".xci": var xci = new Xci(_virtualFileSystem.KeySet, file.AsStorage()).OpenPartition(XciPartitionType.Secure); - xci.OpenFile(ref ncaFile.Ref, aoc.NcaPath.ToU8Span(), OpenMode.Read); + xci.OpenFile(ref ncaFile.Ref, aoc.NcaPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); break; case ".nsp": var pfs = new PartitionFileSystem(); pfs.Initialize(file.AsStorage()); - pfs.OpenFile(ref ncaFile.Ref, aoc.NcaPath.ToU8Span(), OpenMode.Read); + pfs.OpenFile(ref ncaFile.Ref, aoc.NcaPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); break; default: return false; // Print error? @@ -607,11 +607,11 @@ namespace Ryujinx.HLE.FileSystem if (filesystem.FileExists($"{path}/00")) { - filesystem.OpenFile(ref file.Ref, $"{path}/00".ToU8Span(), mode); + filesystem.OpenFile(ref file.Ref, $"{path}/00".ToU8Span(), mode).ThrowIfFailure(); } else { - filesystem.OpenFile(ref file.Ref, path.ToU8Span(), mode); + filesystem.OpenFile(ref file.Ref, path.ToU8Span(), mode).ThrowIfFailure(); } return file.Release(); From 56fe2ff535560a5b7e6461f5925479848b9a9994 Mon Sep 17 00:00:00 2001 From: Alex Barney Date: Tue, 24 Oct 2023 09:26:25 -0700 Subject: [PATCH 101/105] Fix loading tickets from a Sha256PartitionFileSystem (#5844) --- src/Ryujinx.HLE/FileSystem/VirtualFileSystem.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/Ryujinx.HLE/FileSystem/VirtualFileSystem.cs b/src/Ryujinx.HLE/FileSystem/VirtualFileSystem.cs index eaf481dd7..43bd27761 100644 --- a/src/Ryujinx.HLE/FileSystem/VirtualFileSystem.cs +++ b/src/Ryujinx.HLE/FileSystem/VirtualFileSystem.cs @@ -264,7 +264,16 @@ namespace Ryujinx.HLE.FileSystem if (result.IsSuccess()) { - Ticket ticket = new(ticketFile.Get.AsStream()); + // When reading a file from a Sha256PartitionFileSystem, you can't start a read in the middle + // of the hashed portion (usually the first 0x200 bytes) of the file and end the read after + // the end of the hashed portion, so we read the ticket file using a single read. + byte[] ticketData = new byte[0x2C0]; + result = ticketFile.Get.Read(out long bytesRead, 0, ticketData); + + if (result.IsFailure() || bytesRead != ticketData.Length) + continue; + + Ticket ticket = new(new MemoryStream(ticketData)); var titleKey = ticket.GetTitleKey(KeySet); if (titleKey != null) From 171b46ef49f479a5eb30b9769eab3af452092641 Mon Sep 17 00:00:00 2001 From: jcm Date: Tue, 24 Oct 2023 17:37:13 -0500 Subject: [PATCH 102/105] macOS: Use user-friendly macOS version string (#5838) * use user-friendly macOS version string rather than kernel version * add build identifier string --------- Co-authored-by: jcm --- src/Ryujinx.Common/SystemInfo/MacOSSystemInfo.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Ryujinx.Common/SystemInfo/MacOSSystemInfo.cs b/src/Ryujinx.Common/SystemInfo/MacOSSystemInfo.cs index 98a0d8abf..a968ad17b 100644 --- a/src/Ryujinx.Common/SystemInfo/MacOSSystemInfo.cs +++ b/src/Ryujinx.Common/SystemInfo/MacOSSystemInfo.cs @@ -12,6 +12,13 @@ namespace Ryujinx.Common.SystemInfo { internal MacOSSystemInfo() { + if (SysctlByName("kern.osversion", out string buildRevision) != 0) + { + buildRevision = "Unknown Build"; + } + + OsDescription = $"macOS {Environment.OSVersion.Version} ({buildRevision}) ({RuntimeInformation.OSArchitecture})"; + string cpuName = GetCpuidCpuName(); if (cpuName == null && SysctlByName("machdep.cpu.brand_string", out cpuName) != 0) From c14ce4d2a5c9b373fb454906a6dc142c028d7be2 Mon Sep 17 00:00:00 2001 From: TSRBerry <20988865+TSRBerry@users.noreply.github.com> Date: Thu, 26 Oct 2023 00:32:13 +0200 Subject: [PATCH 103/105] Add ldn_mitm as a network client for LDN (#5656) * Add relevant files from private repo Hopefully I didn't miss anything. JsonHelper.cs is a debug only change I only added line 810-812 in IUserLocalCommunicationService.cs for the new Spacemeowx2Ldn case. * Add a small README.md just for fun * Add note about NetCoreServer update to 5.1.0 * Fix a few issues Fix usage of wrong broadcast address Log warning if empty userstring was received and don't add them to outNetworkInfo * Add warning about incompatibility with public LDN version * Add missing changes from old_master * Adjust ldn_mitm for Ryujinx/Ryujinx#3805 * ldn: Adapt to changes from #4582 * ldn_mitm: First cleanup iteration * ldn_mitm: Second cleanup iteration * Credit spacemeowx2 in README.md * Address first review comments by AcK Adhere to Ryujinx coding style Remove leftover log calls Change category of a few log calls Remove leftover debug notes * Replace return type with void for methods always returning true * Address first review comments by riperiperi Purely stylistic changes: - Adhere to naming style for internal fields - Improve code formatting * Throw InvalidOperationException when calling wrong ldn proxy methods * Add missing newlines in LanDiscovery.Scan() * Fix Linux not receiving broadcast packets * Remove ILdnUdpSocket It's very unlikely that we will ever need a udp client. Thus we should simplify LanDiscovery initialization and remove the parameter of InitUdp(). * ldn_mitm: Improve formatting * fixup! Fix Linux not receiving broadcast packets By opening the udp server on 'LocalBroadcastAddr' Linux refused to answer packets going to LocalAddr. So in order to fix this problem, Linux now opens two LdnProxyUdpServers. * ldn_mitm: Fix assigning incorrect NodeIds This just made connecting a lot more reliable! Thanks @riperiperi * Fix node ids when leaving/joining * Change NodeId behaviour to work like RyuLdn * Change timing for accept and network info being reported. * Wait for connection before sending anything. * Remove ConnectAsync() from ILdnTcpSocket * Only broadcast scan responses if we're hosting a network. * Fix some filters, scan network duplication. * Fix silly mistake * Don't die on duplicates, just replace. * Lock around node updates These can happen from multiple threads. * ldn_mitm: Fix namespaces for Types Improve formatting Add warning if compression failed * Add quicker scan, forgetting networks that disappear. * Always force a network sync when updating AdvertiseData * Fix TCP frame size being too large for compressed frames * Allow ldn_mitm to pass -1 id for room localcommunicationids. * ldn_mitm: Match server socket options * ldn_mitm: Use correct socket options * ldn_mitm: Remove TCP broadcast socket options * config: Rename Spacemeowx2Ldn to LdnMitm * ldn_mitm: Generate random fake SSID * ldn_mitm: Adjust logging statements/levels * ldn_mitm: Add missing Stop() call for udp2 * ldn_mitm: Adjust formatting * ldn_mitm: Add stub comments and adjust existing ones * ldn: Add LdnConst class & set tx/rx buffer sizes correctly * Move LdnConst out of UserServiceCreator Replace a few values with LdnConsts * ldn: Adjust namespaces and client names * ldn_mitm: Adjust formatting * ldn: Rename RyuLdn to LdnRyu * Replace LanProtocol.Read() refs with scoped refs * Add MIT license for ldn_mitm * Clarify that network interface is also used for LDN Although it's currently only used by ldn_mitm, it would probably be more confusing to exclude RyuLdn there. * Fix giving a station node id 0 * Update Nuget packages * Remove LdnHelper * Add update functions for EnableInternetAccess setting * ldn: Log MultiplayerMode and DisableP2P * ldn: Adjust namespaces * Apply formatting * Conform to Ryujinx code style * Remove ldn_mitm from THIRDPARTY.md It shouldn't have been there in the first place. * Improve formatting --------- Co-authored-by: riperiperi Co-authored-by: Ac_K --- Directory.Packages.props | 1 + README.md | 1 + distribution/legal/THIRDPARTY.md | 2 +- src/Ryujinx.Ava/AppHost.cs | 6 + src/Ryujinx.Ava/Assets/Locales/en_US.json | 2 +- .../Multiplayer/MultiplayerMode.cs | 1 + .../Utilities/NetworkHelpers.cs | 5 + src/Ryujinx.HLE/HLEConfiguration.cs | 2 +- src/Ryujinx.HLE/HOS/Services/Ldn/LdnConst.cs | 12 + .../HOS/Services/Ldn/Types/LdnNetworkInfo.cs | 2 +- .../HOS/Services/Ldn/Types/NodeInfo.cs | 2 +- .../Services/Ldn/Types/NodeLatestUpdate.cs | 4 +- .../HOS/Services/Ldn/Types/SecurityConfig.cs | 2 +- .../HOS/Services/Ldn/Types/Ssid.cs | 2 +- .../HOS/Services/Ldn/Types/UserConfig.cs | 2 +- .../Ldn/UserServiceCreator/AccessPoint.cs | 5 +- .../{RyuLdn => }/INetworkClient.cs | 7 +- .../IUserLocalCommunicationService.cs | 28 +- ...abledLdnClient.cs => LdnDisabledClient.cs} | 9 +- .../LdnMitm/LanDiscovery.cs | 611 ++++++++++++++++++ .../UserServiceCreator/LdnMitm/LanProtocol.cs | 314 +++++++++ .../LdnMitm/LdnMitmClient.cs | 104 +++ .../LdnMitm/Proxy/ILdnSocket.cs | 12 + .../LdnMitm/Proxy/ILdnTcpSocket.cs | 8 + .../LdnMitm/Proxy/LdnProxyTcpClient.cs | 99 +++ .../LdnMitm/Proxy/LdnProxyTcpServer.cs | 54 ++ .../LdnMitm/Proxy/LdnProxyTcpSession.cs | 83 +++ .../LdnMitm/Proxy/LdnProxyUdpServer.cs | 157 +++++ .../LdnMitm/Types/LanPacketHeader.cs | 16 + .../LdnMitm/Types/LanPacketType.cs | 10 + .../{RyuLdn => }/NetworkChangeEventArgs.cs | 2 +- .../Ldn/UserServiceCreator/Station.cs | 5 +- .../Types/ConnectPrivateRequest.cs | 2 +- .../{Network => }/Types/ConnectRequest.cs | 2 +- .../Types/CreateAccessPointPrivateRequest.cs | 2 +- .../Types/CreateAccessPointRequest.cs | 2 +- .../{RyuLdn => }/Types/NetworkError.cs | 2 +- .../{RyuLdn => }/Types/NetworkErrorMessage.cs | 2 +- src/Ryujinx.HLE/Ryujinx.HLE.csproj | 1 + .../Configuration/ConfigurationState.cs | 1 + src/Ryujinx/Ui/MainWindow.cs | 8 + src/Ryujinx/Ui/Windows/SettingsWindow.cs | 2 + src/Ryujinx/Ui/Windows/SettingsWindow.glade | 5 +- 43 files changed, 1556 insertions(+), 43 deletions(-) create mode 100644 src/Ryujinx.HLE/HOS/Services/Ldn/LdnConst.cs rename src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/{RyuLdn => }/INetworkClient.cs (81%) rename src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/{RyuLdn/DisabledLdnClient.cs => LdnDisabledClient.cs} (87%) create mode 100644 src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/LanDiscovery.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/LanProtocol.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/LdnMitmClient.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/ILdnSocket.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/ILdnTcpSocket.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/LdnProxyTcpClient.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/LdnProxyTcpServer.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/LdnProxyTcpSession.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/LdnProxyUdpServer.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Types/LanPacketHeader.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Types/LanPacketType.cs rename src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/{RyuLdn => }/NetworkChangeEventArgs.cs (91%) rename src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/{RyuLdn => }/Types/ConnectPrivateRequest.cs (86%) rename src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/{Network => }/Types/ConnectRequest.cs (84%) rename src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/{RyuLdn => }/Types/CreateAccessPointPrivateRequest.cs (88%) rename src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/{Network => }/Types/CreateAccessPointRequest.cs (86%) rename src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/{RyuLdn => }/Types/NetworkError.cs (80%) rename src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/{RyuLdn => }/Types/NetworkErrorMessage.cs (71%) diff --git a/Directory.Packages.props b/Directory.Packages.props index 4fd079af6..009430f92 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -24,6 +24,7 @@ + diff --git a/README.md b/README.md index 56333278f..b2a6646f5 100644 --- a/README.md +++ b/README.md @@ -141,4 +141,5 @@ See [LICENSE.txt](LICENSE.txt) and [THIRDPARTY.md](distribution/legal/THIRDPARTY - [LibHac](https://github.com/Thealexbarney/LibHac) is used for our file-system. - [AmiiboAPI](https://www.amiiboapi.com) is used in our Amiibo emulation. +- [ldn_mitm](https://github.com/spacemeowx2/ldn_mitm) is used for one of our available multiplayer modes. - [ShellLink](https://github.com/securifybv/ShellLink) is used for Windows shortcut generation. diff --git a/distribution/legal/THIRDPARTY.md b/distribution/legal/THIRDPARTY.md index b0bd5a690..5caa03771 100644 --- a/distribution/legal/THIRDPARTY.md +++ b/distribution/legal/THIRDPARTY.md @@ -710,4 +710,4 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ``` - \ No newline at end of file + diff --git a/src/Ryujinx.Ava/AppHost.cs b/src/Ryujinx.Ava/AppHost.cs index c473cf562..cd066efba 100644 --- a/src/Ryujinx.Ava/AppHost.cs +++ b/src/Ryujinx.Ava/AppHost.cs @@ -190,6 +190,7 @@ namespace Ryujinx.Ava ConfigurationState.Instance.Graphics.ScalingFilterLevel.Event += UpdateScalingFilterLevel; ConfigurationState.Instance.Graphics.EnableColorSpacePassthrough.Event += UpdateColorSpacePassthrough; + ConfigurationState.Instance.System.EnableInternetAccess.Event += UpdateEnableInternetAccessState; ConfigurationState.Instance.Multiplayer.LanInterfaceId.Event += UpdateLanInterfaceIdState; ConfigurationState.Instance.Multiplayer.Mode.Event += UpdateMultiplayerModeState; @@ -408,6 +409,11 @@ namespace Ryujinx.Ava }); } + private void UpdateEnableInternetAccessState(object sender, ReactiveEventArgs e) + { + Device.Configuration.EnableInternetAccess = e.NewValue; + } + private void UpdateLanInterfaceIdState(object sender, ReactiveEventArgs e) { Device.Configuration.MultiplayerLanInterfaceId = e.NewValue; diff --git a/src/Ryujinx.Ava/Assets/Locales/en_US.json b/src/Ryujinx.Ava/Assets/Locales/en_US.json index a67b796bd..62aac1227 100644 --- a/src/Ryujinx.Ava/Assets/Locales/en_US.json +++ b/src/Ryujinx.Ava/Assets/Locales/en_US.json @@ -650,7 +650,7 @@ "UserEditorTitle": "Edit User", "UserEditorTitleCreate": "Create User", "SettingsTabNetworkInterface": "Network Interface:", - "NetworkInterfaceTooltip": "The network interface used for LAN features", + "NetworkInterfaceTooltip": "The network interface used for LAN/LDN features", "NetworkInterfaceDefault": "Default", "PackagingShaders": "Packaging Shaders", "AboutChangelogButton": "View Changelog on GitHub", diff --git a/src/Ryujinx.Common/Configuration/Multiplayer/MultiplayerMode.cs b/src/Ryujinx.Common/Configuration/Multiplayer/MultiplayerMode.cs index 167429433..05108716d 100644 --- a/src/Ryujinx.Common/Configuration/Multiplayer/MultiplayerMode.cs +++ b/src/Ryujinx.Common/Configuration/Multiplayer/MultiplayerMode.cs @@ -3,5 +3,6 @@ public enum MultiplayerMode { Disabled, + LdnMitm, } } diff --git a/src/Ryujinx.Common/Utilities/NetworkHelpers.cs b/src/Ryujinx.Common/Utilities/NetworkHelpers.cs index 78fb342b1..3b64a28f5 100644 --- a/src/Ryujinx.Common/Utilities/NetworkHelpers.cs +++ b/src/Ryujinx.Common/Utilities/NetworkHelpers.cs @@ -74,5 +74,10 @@ namespace Ryujinx.Common.Utilities { return ConvertIpv4Address(IPAddress.Parse(ipAddress)); } + + 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) }); + } } } diff --git a/src/Ryujinx.HLE/HLEConfiguration.cs b/src/Ryujinx.HLE/HLEConfiguration.cs index b1ba11b59..d52f1815a 100644 --- a/src/Ryujinx.HLE/HLEConfiguration.cs +++ b/src/Ryujinx.HLE/HLEConfiguration.cs @@ -101,7 +101,7 @@ namespace Ryujinx.HLE /// /// Control if the guest application should be told that there is a Internet connection available. /// - internal readonly bool EnableInternetAccess; + public bool EnableInternetAccess { internal get; set; } /// /// Control LibHac's integrity check level. diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/LdnConst.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/LdnConst.cs new file mode 100644 index 000000000..80ea2c9d7 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/LdnConst.cs @@ -0,0 +1,12 @@ +namespace Ryujinx.HLE.HOS.Services.Ldn +{ + static class LdnConst + { + public const int SsidLengthMax = 0x20; + public const int AdvertiseDataSizeMax = 0x180; + public const int UserNameBytesMax = 0x20; + public const int NodeCountMax = 8; + public const int StationCountMax = NodeCountMax - 1; + public const int PassphraseLengthMax = 0x40; + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/LdnNetworkInfo.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/LdnNetworkInfo.cs index 4b7241c43..5fb2aca05 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/LdnNetworkInfo.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/LdnNetworkInfo.cs @@ -1,4 +1,4 @@ -using Ryujinx.Common.Memory; +using Ryujinx.Common.Memory; using System.Runtime.InteropServices; namespace Ryujinx.HLE.HOS.Services.Ldn.Types diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NodeInfo.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NodeInfo.cs index c57a7dc45..9d5477931 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NodeInfo.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NodeInfo.cs @@ -1,4 +1,4 @@ -using Ryujinx.Common.Memory; +using Ryujinx.Common.Memory; using System.Runtime.InteropServices; namespace Ryujinx.HLE.HOS.Services.Ldn.Types diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NodeLatestUpdate.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NodeLatestUpdate.cs index f33ceaebe..0461e783e 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NodeLatestUpdate.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NodeLatestUpdate.cs @@ -1,4 +1,4 @@ -using Ryujinx.Common.Memory; +using Ryujinx.Common.Memory; using System.Runtime.InteropServices; namespace Ryujinx.HLE.HOS.Services.Ldn.Types @@ -48,7 +48,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.Types { result[i].Reserved = new Array7(); - if (i < 8) + if (i < LdnConst.NodeCountMax) { result[i].State = array[i].State; array[i].State = NodeLatestUpdateFlags.None; diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/SecurityConfig.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/SecurityConfig.cs index 85a19a875..5939a1394 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/SecurityConfig.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/SecurityConfig.cs @@ -1,4 +1,4 @@ -using Ryujinx.Common.Memory; +using Ryujinx.Common.Memory; using System.Runtime.InteropServices; namespace Ryujinx.HLE.HOS.Services.Ldn.Types diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/Ssid.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/Ssid.cs index 72db4d41a..764862508 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/Ssid.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/Ssid.cs @@ -1,4 +1,4 @@ -using Ryujinx.Common.Memory; +using Ryujinx.Common.Memory; using System.Runtime.InteropServices; namespace Ryujinx.HLE.HOS.Services.Ldn.Types diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/UserConfig.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/UserConfig.cs index 1401f5214..3820f936e 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/UserConfig.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/UserConfig.cs @@ -1,4 +1,4 @@ -using Ryujinx.Common.Memory; +using Ryujinx.Common.Memory; using System.Runtime.InteropServices; namespace Ryujinx.HLE.HOS.Services.Ldn.Types diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/AccessPoint.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/AccessPoint.cs index 07bbbeda3..78ebcac82 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/AccessPoint.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/AccessPoint.cs @@ -1,7 +1,6 @@ using Ryujinx.Common.Memory; using Ryujinx.HLE.HOS.Services.Ldn.Types; -using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Network.Types; -using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn.Types; +using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types; using System; namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator @@ -30,7 +29,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator _parent.NetworkClient.NetworkChange -= NetworkChanged; } - private void NetworkChanged(object sender, RyuLdn.NetworkChangeEventArgs e) + private void NetworkChanged(object sender, NetworkChangeEventArgs e) { LatestUpdates.CalculateLatestUpdate(NetworkInfo.Ldn.Nodes, e.Info.Ldn.Nodes); diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/INetworkClient.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/INetworkClient.cs similarity index 81% rename from src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/INetworkClient.cs rename to src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/INetworkClient.cs index ff342d27c..81825e977 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/INetworkClient.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/INetworkClient.cs @@ -1,12 +1,13 @@ using Ryujinx.HLE.HOS.Services.Ldn.Types; -using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Network.Types; -using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn.Types; +using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types; using System; -namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator { interface INetworkClient : IDisposable { + bool NeedsRealId { get; } + event EventHandler NetworkChange; void DisconnectNetwork(); diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/IUserLocalCommunicationService.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/IUserLocalCommunicationService.cs index 29cc0e1b9..8c6ea66f7 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/IUserLocalCommunicationService.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/IUserLocalCommunicationService.cs @@ -8,7 +8,7 @@ using Ryujinx.Cpu; 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.RyuLdn; +using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm; using Ryujinx.Horizon.Common; using Ryujinx.Memory; using System; @@ -395,7 +395,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator } else { - if (scanFilter.NetworkId.IntentId.LocalCommunicationId == -1) + if (scanFilter.NetworkId.IntentId.LocalCommunicationId == -1 && NetworkClient.NeedsRealId) { // TODO: Call nn::arp::GetApplicationControlProperty here when implemented. ApplicationControlProperty controlProperty = context.Device.Processes.ActiveApplication.ApplicationControlProperties; @@ -546,7 +546,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator context.RequestData.BaseStream.Seek(4, SeekOrigin.Current); // Alignment? NetworkConfig networkConfig = context.RequestData.ReadStruct(); - if (networkConfig.IntentId.LocalCommunicationId == -1) + if (networkConfig.IntentId.LocalCommunicationId == -1 && NetworkClient.NeedsRealId) { // TODO: Call nn::arp::GetApplicationControlProperty here when implemented. ApplicationControlProperty controlProperty = context.Device.Processes.ActiveApplication.ApplicationControlProperties; @@ -555,7 +555,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator } bool isLocalCommunicationIdValid = CheckLocalCommunicationIdPermission(context, (ulong)networkConfig.IntentId.LocalCommunicationId); - if (!isLocalCommunicationIdValid) + if (!isLocalCommunicationIdValid && NetworkClient.NeedsRealId) { return ResultCode.InvalidObject; } @@ -568,13 +568,13 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator networkConfig.Channel = CheckDevelopmentChannel(networkConfig.Channel); securityConfig.SecurityMode = CheckDevelopmentSecurityMode(securityConfig.SecurityMode); - if (networkConfig.NodeCountMax <= 8) + if (networkConfig.NodeCountMax <= LdnConst.NodeCountMax) { if ((((ulong)networkConfig.LocalCommunicationVersion) & 0x80000000) == 0) { if (securityConfig.SecurityMode <= SecurityMode.Retail) { - if (securityConfig.Passphrase.Length <= 0x40) + if (securityConfig.Passphrase.Length <= LdnConst.PassphraseLengthMax) { if (_state == NetworkState.AccessPoint) { @@ -678,7 +678,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator return _nifmResultCode; } - if (bufferSize == 0 || bufferSize > 0x180) + if (bufferSize == 0 || bufferSize > LdnConst.AdvertiseDataSizeMax) { return ResultCode.InvalidArgument; } @@ -848,10 +848,10 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator context.Memory.Read(bufferPosition, networkInfoBytes); - networkInfo = MemoryMarshal.Cast(networkInfoBytes)[0]; + networkInfo = MemoryMarshal.Read(networkInfoBytes); } - if (networkInfo.NetworkId.IntentId.LocalCommunicationId == -1) + if (networkInfo.NetworkId.IntentId.LocalCommunicationId == -1 && NetworkClient.NeedsRealId) { // TODO: Call nn::arp::GetApplicationControlProperty here when implemented. ApplicationControlProperty controlProperty = context.Device.Processes.ActiveApplication.ApplicationControlProperties; @@ -860,7 +860,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator } bool isLocalCommunicationIdValid = CheckLocalCommunicationIdPermission(context, (ulong)networkInfo.NetworkId.IntentId.LocalCommunicationId); - if (!isLocalCommunicationIdValid) + if (!isLocalCommunicationIdValid && NetworkClient.NeedsRealId) { return ResultCode.InvalidObject; } @@ -1061,10 +1061,16 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator if (System.Net.NetworkInformation.NetworkInterface.GetIsNetworkAvailable()) { MultiplayerMode mode = context.Device.Configuration.MultiplayerMode; + + Logger.Info?.PrintMsg(LogClass.ServiceLdn, $"Initializing with multiplayer mode: {mode}"); + switch (mode) { + case MultiplayerMode.LdnMitm: + NetworkClient = new LdnMitmClient(context.Device.Configuration); + break; case MultiplayerMode.Disabled: - NetworkClient = new DisabledLdnClient(); + NetworkClient = new LdnDisabledClient(); break; } diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/DisabledLdnClient.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnDisabledClient.cs similarity index 87% rename from src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/DisabledLdnClient.cs rename to src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnDisabledClient.cs index 75a1e35ff..e5340b4e9 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/DisabledLdnClient.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnDisabledClient.cs @@ -1,12 +1,13 @@ using Ryujinx.HLE.HOS.Services.Ldn.Types; -using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Network.Types; -using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn.Types; +using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types; using System; -namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator { - class DisabledLdnClient : INetworkClient + class LdnDisabledClient : INetworkClient { + public bool NeedsRealId => true; + public event EventHandler NetworkChange; public NetworkError Connect(ConnectRequest request) diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/LanDiscovery.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/LanDiscovery.cs new file mode 100644 index 000000000..8cfd77acb --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/LanDiscovery.cs @@ -0,0 +1,611 @@ +using Ryujinx.Common.Logging; +using Ryujinx.Common.Memory; +using Ryujinx.Common.Utilities; +using Ryujinx.HLE.HOS.Services.Ldn.Types; +using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm.Proxy; +using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm.Types; +using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Threading; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm +{ + internal class LanDiscovery : IDisposable + { + private const int DefaultPort = 11452; + private const ushort CommonChannel = 6; + private const byte CommonLinkLevel = 3; + private const byte CommonNetworkType = 2; + + private const int FailureTimeout = 4000; + + private readonly LdnMitmClient _parent; + private readonly LanProtocol _protocol; + private bool _initialized; + private readonly Ssid _fakeSsid; + private ILdnTcpSocket _tcp; + private LdnProxyUdpServer _udp, _udp2; + private readonly List _stations = new(); + private readonly object _lock = new(); + + private readonly AutoResetEvent _apConnected = new(false); + + internal readonly IPAddress LocalAddr; + internal readonly IPAddress LocalBroadcastAddr; + internal NetworkInfo NetworkInfo; + + public bool IsHost => _tcp is LdnProxyTcpServer; + + private readonly Random _random = new(); + + // NOTE: Credit to https://stackoverflow.com/a/39338188 + private static IPAddress GetBroadcastAddress(IPAddress address, IPAddress mask) + { + uint ipAddress = BitConverter.ToUInt32(address.GetAddressBytes(), 0); + uint ipMaskV4 = BitConverter.ToUInt32(mask.GetAddressBytes(), 0); + uint broadCastIpAddress = ipAddress | ~ipMaskV4; + + return new IPAddress(BitConverter.GetBytes(broadCastIpAddress)); + } + + private static NetworkInfo GetEmptyNetworkInfo() + { + NetworkInfo networkInfo = new() + { + NetworkId = new NetworkId + { + SessionId = new Array16(), + }, + Common = new CommonNetworkInfo + { + MacAddress = new Array6(), + Ssid = new Ssid + { + Name = new Array33(), + }, + }, + Ldn = new LdnNetworkInfo + { + NodeCountMax = LdnConst.NodeCountMax, + SecurityParameter = new Array16(), + Nodes = new Array8(), + AdvertiseData = new Array384(), + Reserved4 = new Array140(), + }, + }; + + for (int i = 0; i < LdnConst.NodeCountMax; i++) + { + networkInfo.Ldn.Nodes[i] = new NodeInfo + { + MacAddress = new Array6(), + UserName = new Array33(), + Reserved2 = new Array16(), + }; + } + + return networkInfo; + } + + public LanDiscovery(LdnMitmClient parent, IPAddress ipAddress, IPAddress ipv4Mask) + { + Logger.Info?.PrintMsg(LogClass.ServiceLdn, $"Initialize LanDiscovery using IP: {ipAddress}"); + + _parent = parent; + LocalAddr = ipAddress; + LocalBroadcastAddr = GetBroadcastAddress(ipAddress, ipv4Mask); + + _fakeSsid = new Ssid + { + Length = LdnConst.SsidLengthMax, + }; + _random.NextBytes(_fakeSsid.Name.AsSpan()[..32]); + + _protocol = new LanProtocol(this); + _protocol.Accept += OnConnect; + _protocol.SyncNetwork += OnSyncNetwork; + _protocol.DisconnectStation += DisconnectStation; + + NetworkInfo = GetEmptyNetworkInfo(); + + ResetStations(); + + if (!InitUdp()) + { + Logger.Error?.PrintMsg(LogClass.ServiceLdn, "LanDiscovery Initialize: InitUdp failed."); + + return; + } + + _initialized = true; + } + + protected void OnSyncNetwork(NetworkInfo info) + { + bool updated = false; + + lock (_lock) + { + if (!NetworkInfo.Equals(info)) + { + NetworkInfo = info; + updated = true; + + Logger.Debug?.PrintMsg(LogClass.ServiceLdn, $"Host IP: {NetworkHelpers.ConvertUint(info.Ldn.Nodes[0].Ipv4Address)}"); + } + } + + if (updated) + { + _parent.InvokeNetworkChange(info, true); + } + + _apConnected.Set(); + } + + protected void OnConnect(LdnProxyTcpSession station) + { + lock (_lock) + { + station.NodeId = LocateEmptyNode(); + + if (_stations.Count > LdnConst.StationCountMax || station.NodeId == -1) + { + station.Disconnect(); + station.Dispose(); + + return; + } + + _stations.Add(station); + + UpdateNodes(); + } + } + + public void DisconnectStation(LdnProxyTcpSession station) + { + if (!station.IsDisposed) + { + if (station.IsConnected) + { + station.Disconnect(); + } + + station.Dispose(); + } + + lock (_lock) + { + if (_stations.Remove(station)) + { + NetworkInfo.Ldn.Nodes[station.NodeId] = new NodeInfo() + { + MacAddress = new Array6(), + UserName = new Array33(), + Reserved2 = new Array16(), + }; + + UpdateNodes(); + } + } + } + + public bool SetAdvertiseData(byte[] data) + { + if (data.Length > LdnConst.AdvertiseDataSizeMax) + { + Logger.Error?.PrintMsg(LogClass.ServiceLdn, "AdvertiseData exceeds size limit."); + + return false; + } + + data.CopyTo(NetworkInfo.Ldn.AdvertiseData.AsSpan()); + NetworkInfo.Ldn.AdvertiseDataSize = (ushort)data.Length; + + // NOTE: Otherwise this results in SessionKeepFailed or MasterDisconnected + lock (_lock) + { + if (NetworkInfo.Ldn.Nodes[0].IsConnected == 1) + { + UpdateNodes(true); + } + } + + return true; + } + + public void InitNetworkInfo() + { + lock (_lock) + { + NetworkInfo.Common.MacAddress = GetFakeMac(); + NetworkInfo.Common.Channel = CommonChannel; + NetworkInfo.Common.LinkLevel = CommonLinkLevel; + NetworkInfo.Common.NetworkType = CommonNetworkType; + NetworkInfo.Common.Ssid = _fakeSsid; + + NetworkInfo.Ldn.Nodes = new Array8(); + + for (int i = 0; i < LdnConst.NodeCountMax; i++) + { + NetworkInfo.Ldn.Nodes[i].NodeId = (byte)i; + NetworkInfo.Ldn.Nodes[i].IsConnected = 0; + } + } + } + + protected Array6 GetFakeMac(IPAddress address = null) + { + address ??= LocalAddr; + + byte[] ip = address.GetAddressBytes(); + + var macAddress = new Array6(); + new byte[] { 0x02, 0x00, ip[0], ip[1], ip[2], ip[3] }.CopyTo(macAddress.AsSpan()); + + return macAddress; + } + + public bool InitTcp(bool listening, IPAddress address = null, int port = DefaultPort) + { + Logger.Debug?.PrintMsg(LogClass.ServiceLdn, $"LanDiscovery InitTcp: IP: {address}, listening: {listening}"); + + if (_tcp != null) + { + _tcp.DisconnectAndStop(); + _tcp.Dispose(); + _tcp = null; + } + + ILdnTcpSocket tcpSocket; + + if (listening) + { + try + { + address ??= LocalAddr; + + tcpSocket = new LdnProxyTcpServer(_protocol, address, port); + } + catch (Exception ex) + { + Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"Failed to create LdnProxyTcpServer: {ex}"); + + return false; + } + + if (!tcpSocket.Start()) + { + return false; + } + } + else + { + if (address == null) + { + return false; + } + + try + { + tcpSocket = new LdnProxyTcpClient(_protocol, address, port); + } + catch (Exception ex) + { + Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"Failed to create LdnProxyTcpClient: {ex}"); + + return false; + } + } + + _tcp = tcpSocket; + + return true; + } + + public bool InitUdp() + { + _udp?.Stop(); + _udp2?.Stop(); + + try + { + // NOTE: Linux won't receive any broadcast packets if the socket is not bound to the broadcast address. + // Windows only works if bound to localhost or the local address. + // See this discussion: https://stackoverflow.com/questions/13666789/receiving-udp-broadcast-packets-on-linux + if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS()) + { + _udp2 = new LdnProxyUdpServer(_protocol, LocalBroadcastAddr, DefaultPort); + } + + _udp = new LdnProxyUdpServer(_protocol, LocalAddr, DefaultPort); + } + catch (Exception ex) + { + Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"Failed to create LdnProxyUdpServer: {ex}"); + + return false; + } + + return true; + } + + public NetworkInfo[] Scan(ushort channel, ScanFilter filter) + { + _udp.ClearScanResults(); + + if (_protocol.SendBroadcast(_udp, LanPacketType.Scan, DefaultPort) < 0) + { + return Array.Empty(); + } + + List outNetworkInfo = new(); + + foreach (KeyValuePair item in _udp.GetScanResults()) + { + bool copy = true; + + if (filter.Flag.HasFlag(ScanFilterFlag.LocalCommunicationId)) + { + copy &= filter.NetworkId.IntentId.LocalCommunicationId == item.Value.NetworkId.IntentId.LocalCommunicationId; + } + + if (filter.Flag.HasFlag(ScanFilterFlag.SessionId)) + { + copy &= filter.NetworkId.SessionId.AsSpan().SequenceEqual(item.Value.NetworkId.SessionId.AsSpan()); + } + + if (filter.Flag.HasFlag(ScanFilterFlag.NetworkType)) + { + copy &= filter.NetworkType == (NetworkType)item.Value.Common.NetworkType; + } + + if (filter.Flag.HasFlag(ScanFilterFlag.Ssid)) + { + Span gameSsid = item.Value.Common.Ssid.Name.AsSpan()[item.Value.Common.Ssid.Length..]; + Span scanSsid = filter.Ssid.Name.AsSpan()[filter.Ssid.Length..]; + copy &= gameSsid.SequenceEqual(scanSsid); + } + + if (filter.Flag.HasFlag(ScanFilterFlag.SceneId)) + { + copy &= filter.NetworkId.IntentId.SceneId == item.Value.NetworkId.IntentId.SceneId; + } + + if (copy) + { + if (item.Value.Ldn.Nodes[0].UserName[0] != 0) + { + outNetworkInfo.Add(item.Value); + } + else + { + Logger.Warning?.PrintMsg(LogClass.ServiceLdn, "LanDiscovery Scan: Got empty Username. There might be a timing issue somewhere..."); + } + } + } + + return outNetworkInfo.ToArray(); + } + + protected void ResetStations() + { + lock (_lock) + { + foreach (LdnProxyTcpSession station in _stations) + { + station.Disconnect(); + station.Dispose(); + } + + _stations.Clear(); + } + } + + private int LocateEmptyNode() + { + Array8 nodes = NetworkInfo.Ldn.Nodes; + + for (int i = 1; i < nodes.Length; i++) + { + if (nodes[i].IsConnected == 0) + { + return i; + } + } + + return -1; + } + + protected void UpdateNodes(bool forceUpdate = false) + { + int countConnected = 1; + + foreach (LdnProxyTcpSession station in _stations.Where(station => station.IsConnected)) + { + countConnected++; + + station.OverrideInfo(); + + // NOTE: This is not part of the original implementation. + NetworkInfo.Ldn.Nodes[station.NodeId] = station.NodeInfo; + } + + byte nodeCount = (byte)countConnected; + + bool networkInfoChanged = forceUpdate || NetworkInfo.Ldn.NodeCount != nodeCount; + + NetworkInfo.Ldn.NodeCount = nodeCount; + + foreach (LdnProxyTcpSession station in _stations) + { + if (station.IsConnected) + { + if (_protocol.SendPacket(station, LanPacketType.SyncNetwork, SpanHelpers.AsSpan(ref NetworkInfo).ToArray()) < 0) + { + Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"Failed to send {LanPacketType.SyncNetwork} to station {station.NodeId}"); + } + } + } + + if (networkInfoChanged) + { + _parent.InvokeNetworkChange(NetworkInfo, true); + } + } + + protected NodeInfo GetNodeInfo(NodeInfo node, UserConfig userConfig, ushort localCommunicationVersion) + { + uint ipAddress = NetworkHelpers.ConvertIpv4Address(LocalAddr); + + node.MacAddress = GetFakeMac(); + node.IsConnected = 1; + node.UserName = userConfig.UserName; + node.LocalCommunicationVersion = localCommunicationVersion; + node.Ipv4Address = ipAddress; + + return node; + } + + public bool CreateNetwork(SecurityConfig securityConfig, UserConfig userConfig, NetworkConfig networkConfig) + { + if (!InitTcp(true)) + { + return false; + } + + InitNetworkInfo(); + + NetworkInfo.Ldn.NodeCountMax = networkConfig.NodeCountMax; + NetworkInfo.Ldn.SecurityMode = (ushort)securityConfig.SecurityMode; + + NetworkInfo.Common.Channel = networkConfig.Channel == 0 ? (ushort)6 : networkConfig.Channel; + + NetworkInfo.NetworkId.SessionId = new Array16(); + _random.NextBytes(NetworkInfo.NetworkId.SessionId.AsSpan()); + NetworkInfo.NetworkId.IntentId = networkConfig.IntentId; + + NetworkInfo.Ldn.Nodes[0] = GetNodeInfo(NetworkInfo.Ldn.Nodes[0], userConfig, networkConfig.LocalCommunicationVersion); + NetworkInfo.Ldn.Nodes[0].IsConnected = 1; + NetworkInfo.Ldn.NodeCount++; + + _parent.InvokeNetworkChange(NetworkInfo, true); + + return true; + } + + public void DestroyNetwork() + { + if (_tcp != null) + { + try + { + _tcp.DisconnectAndStop(); + } + finally + { + _tcp.Dispose(); + _tcp = null; + } + } + + ResetStations(); + } + + public NetworkError Connect(NetworkInfo networkInfo, UserConfig userConfig, uint localCommunicationVersion) + { + _apConnected.Reset(); + + if (networkInfo.Ldn.NodeCount == 0) + { + return NetworkError.Unknown; + } + + IPAddress address = NetworkHelpers.ConvertUint(networkInfo.Ldn.Nodes[0].Ipv4Address); + + Logger.Info?.PrintMsg(LogClass.ServiceLdn, $"Connecting to host: {address}"); + + if (!InitTcp(false, address)) + { + Logger.Error?.PrintMsg(LogClass.ServiceLdn, "Could not initialize TCPClient"); + + return NetworkError.ConnectNotFound; + } + + if (!_tcp.Connect()) + { + Logger.Error?.PrintMsg(LogClass.ServiceLdn, "Failed to connect."); + + return NetworkError.ConnectFailure; + } + + NodeInfo myNode = GetNodeInfo(new NodeInfo(), userConfig, (ushort)localCommunicationVersion); + if (_protocol.SendPacket(_tcp, LanPacketType.Connect, SpanHelpers.AsSpan(ref myNode).ToArray()) < 0) + { + return NetworkError.Unknown; + } + + return _apConnected.WaitOne(FailureTimeout) ? NetworkError.None : NetworkError.ConnectTimeout; + } + + public void Dispose() + { + if (_initialized) + { + DisconnectAndStop(); + ResetStations(); + _initialized = false; + } + + _protocol.Accept -= OnConnect; + _protocol.SyncNetwork -= OnSyncNetwork; + _protocol.DisconnectStation -= DisconnectStation; + } + + public void DisconnectAndStop() + { + if (_udp != null) + { + try + { + _udp.Stop(); + } + finally + { + _udp.Dispose(); + _udp = null; + } + } + + if (_udp2 != null) + { + try + { + _udp2.Stop(); + } + finally + { + _udp2.Dispose(); + _udp2 = null; + } + } + + if (_tcp != null) + { + try + { + _tcp.DisconnectAndStop(); + } + finally + { + _tcp.Dispose(); + _tcp = null; + } + } + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/LanProtocol.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/LanProtocol.cs new file mode 100644 index 000000000..f22e430bd --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/LanProtocol.cs @@ -0,0 +1,314 @@ +using Ryujinx.Common.Logging; +using Ryujinx.Common.Memory; +using Ryujinx.Common.Utilities; +using Ryujinx.HLE.HOS.Services.Ldn.Types; +using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm.Proxy; +using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm.Types; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm +{ + internal class LanProtocol + { + private const uint LanMagic = 0x11451400; + + public const int BufferSize = 2048; + public const int TcpTxBufferSize = 0x800; + public const int TcpRxBufferSize = 0x1000; + public const int TxBufferSizeMax = 0x2000; + public const int RxBufferSizeMax = 0x2000; + + private readonly int _headerSize = Marshal.SizeOf(); + + private readonly LanDiscovery _discovery; + + public event Action Accept; + public event Action Scan; + public event Action ScanResponse; + public event Action SyncNetwork; + public event Action Connect; + public event Action DisconnectStation; + + public LanProtocol(LanDiscovery parent) + { + _discovery = parent; + } + + public void InvokeAccept(LdnProxyTcpSession session) + { + Accept?.Invoke(session); + } + + public void InvokeDisconnectStation(LdnProxyTcpSession session) + { + DisconnectStation?.Invoke(session); + } + + private void DecodeAndHandle(LanPacketHeader header, byte[] data, EndPoint endPoint = null) + { + switch (header.Type) + { + case LanPacketType.Scan: + // UDP + if (_discovery.IsHost) + { + Scan?.Invoke(endPoint, LanPacketType.ScanResponse, SpanHelpers.AsSpan(ref _discovery.NetworkInfo).ToArray()); + } + break; + case LanPacketType.ScanResponse: + // UDP + ScanResponse?.Invoke(MemoryMarshal.Cast(data)[0]); + break; + case LanPacketType.SyncNetwork: + // TCP + SyncNetwork?.Invoke(MemoryMarshal.Cast(data)[0]); + break; + case LanPacketType.Connect: + // TCP Session / Station + Connect?.Invoke(MemoryMarshal.Cast(data)[0], endPoint); + break; + default: + Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"Decode error: Unhandled type {header.Type}"); + break; + } + } + + public void Read(scoped ref byte[] buffer, scoped ref int bufferEnd, byte[] data, int offset, int size, EndPoint endPoint = null) + { + if (endPoint != null && _discovery.LocalAddr.Equals(((IPEndPoint)endPoint).Address)) + { + return; + } + + int index = 0; + while (index < size) + { + if (bufferEnd < _headerSize) + { + int copyable2 = Math.Min(size - index, Math.Min(size, _headerSize - bufferEnd)); + + Array.Copy(data, index + offset, buffer, bufferEnd, copyable2); + + index += copyable2; + bufferEnd += copyable2; + } + + if (bufferEnd >= _headerSize) + { + LanPacketHeader header = MemoryMarshal.Cast(buffer)[0]; + if (header.Magic != LanMagic) + { + bufferEnd = 0; + + Logger.Warning?.PrintMsg(LogClass.ServiceLdn, $"Invalid magic number in received packet. [magic: {header.Magic}] [EP: {endPoint}]"); + + return; + } + + int totalSize = _headerSize + header.Length; + if (totalSize > BufferSize) + { + bufferEnd = 0; + + Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"Max packet size {BufferSize} exceeded."); + + return; + } + + int copyable = Math.Min(size - index, Math.Min(size, totalSize - bufferEnd)); + + Array.Copy(data, index + offset, buffer, bufferEnd, copyable); + + index += copyable; + bufferEnd += copyable; + + if (totalSize == bufferEnd) + { + byte[] ldnData = new byte[totalSize - _headerSize]; + Array.Copy(buffer, _headerSize, ldnData, 0, ldnData.Length); + + if (header.Compressed == 1) + { + if (Decompress(ldnData, out byte[] decompressedLdnData) != 0) + { + Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"Decompress error:\n {header}, {_headerSize}\n {ldnData}, {ldnData.Length}"); + + return; + } + + 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())}'"); + + return; + } + + ldnData = decompressedLdnData; + } + + DecodeAndHandle(header, ldnData, endPoint); + + bufferEnd = 0; + } + } + } + } + + public int SendBroadcast(ILdnSocket s, LanPacketType type, int port) + { + return SendPacket(s, type, Array.Empty(), new IPEndPoint(_discovery.LocalBroadcastAddr, port)); + } + + public int SendPacket(ILdnSocket s, LanPacketType type, byte[] data, EndPoint endPoint = null) + { + byte[] buf = PreparePacket(type, data); + + return s.SendPacketAsync(endPoint, buf) ? 0 : -1; + } + + public int SendPacket(LdnProxyTcpSession s, LanPacketType type, byte[] data) + { + byte[] buf = PreparePacket(type, data); + + return s.SendAsync(buf) ? 0 : -1; + } + + private LanPacketHeader PrepareHeader(LanPacketHeader header, LanPacketType type) + { + header.Magic = LanMagic; + header.Type = type; + header.Compressed = 0; + header.Length = 0; + header.DecompressLength = 0; + header.Reserved = new Array2(); + + return header; + } + + private byte[] PreparePacket(LanPacketType type, byte[] data) + { + LanPacketHeader header = PrepareHeader(new LanPacketHeader(), type); + header.Length = (ushort)data.Length; + + byte[] buf; + if (data.Length > 0) + { + if (Compress(data, out byte[] compressed) == 0) + { + header.DecompressLength = header.Length; + header.Length = (ushort)compressed.Length; + header.Compressed = 1; + + buf = new byte[compressed.Length + _headerSize]; + + SpanHelpers.AsSpan(ref header).ToArray().CopyTo(buf, 0); + compressed.CopyTo(buf, _headerSize); + } + else + { + buf = new byte[data.Length + _headerSize]; + + Logger.Error?.PrintMsg(LogClass.ServiceLdn, "Compressing packet data failed."); + + SpanHelpers.AsSpan(ref header).ToArray().CopyTo(buf, 0); + data.CopyTo(buf, _headerSize); + } + } + else + { + buf = new byte[_headerSize]; + SpanHelpers.AsSpan(ref header).ToArray().CopyTo(buf, 0); + } + + return buf; + } + + private int Compress(byte[] input, out byte[] output) + { + List outputList = new(); + int i = 0; + int maxCount = 0xFF; + + while (i < input.Length) + { + byte inputByte = input[i++]; + int count = 0; + + if (inputByte == 0) + { + while (i < input.Length && input[i] == 0 && count < maxCount) + { + count += 1; + i++; + } + } + + if (inputByte == 0) + { + outputList.Add(0); + + if (outputList.Count == BufferSize) + { + output = null; + + return -1; + } + + outputList.Add((byte)count); + } + else + { + outputList.Add(inputByte); + } + } + + output = outputList.ToArray(); + + return i == input.Length ? 0 : -1; + } + + private int Decompress(byte[] input, out byte[] output) + { + List outputList = new(); + int i = 0; + + while (i < input.Length && outputList.Count < BufferSize) + { + byte inputByte = input[i++]; + + outputList.Add(inputByte); + + if (inputByte == 0) + { + if (i == input.Length) + { + output = null; + + return -1; + } + + int count = input[i++]; + + for (int j = 0; j < count; j++) + { + if (outputList.Count == BufferSize) + { + break; + } + + outputList.Add(inputByte); + } + } + } + + output = outputList.ToArray(); + + return i == input.Length ? 0 : -1; + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/LdnMitmClient.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/LdnMitmClient.cs new file mode 100644 index 000000000..068013053 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/LdnMitmClient.cs @@ -0,0 +1,104 @@ +using Ryujinx.Common.Logging; +using Ryujinx.Common.Utilities; +using Ryujinx.HLE.HOS.Services.Ldn.Types; +using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types; +using System; +using System.Net.NetworkInformation; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm +{ + /// + /// Client implementation for ldn_mitm + /// + internal class LdnMitmClient : INetworkClient + { + public bool NeedsRealId => false; + + public event EventHandler NetworkChange; + + private readonly LanDiscovery _lanDiscovery; + + public LdnMitmClient(HLEConfiguration config) + { + UnicastIPAddressInformation localIpInterface = NetworkHelpers.GetLocalInterface(config.MultiplayerLanInterfaceId).Item2; + + _lanDiscovery = new LanDiscovery(this, localIpInterface.Address, localIpInterface.IPv4Mask); + } + + internal void InvokeNetworkChange(NetworkInfo info, bool connected, DisconnectReason reason = DisconnectReason.None) + { + NetworkChange?.Invoke(this, new NetworkChangeEventArgs(info, connected: connected, disconnectReason: reason)); + } + + public NetworkError Connect(ConnectRequest request) + { + return _lanDiscovery.Connect(request.NetworkInfo, request.UserConfig, request.LocalCommunicationVersion); + } + + public NetworkError ConnectPrivate(ConnectPrivateRequest request) + { + // NOTE: This method is not implemented in ldn_mitm + Logger.Stub?.PrintMsg(LogClass.ServiceLdn, "LdnMitmClient ConnectPrivate"); + + return NetworkError.None; + } + + public bool CreateNetwork(CreateAccessPointRequest request, byte[] advertiseData) + { + return _lanDiscovery.CreateNetwork(request.SecurityConfig, request.UserConfig, request.NetworkConfig); + } + + public bool CreateNetworkPrivate(CreateAccessPointPrivateRequest request, byte[] advertiseData) + { + // NOTE: This method is not implemented in ldn_mitm + Logger.Stub?.PrintMsg(LogClass.ServiceLdn, "LdnMitmClient CreateNetworkPrivate"); + + return true; + } + + public void DisconnectAndStop() + { + _lanDiscovery.DisconnectAndStop(); + } + + public void DisconnectNetwork() + { + _lanDiscovery.DestroyNetwork(); + } + + public ResultCode Reject(DisconnectReason disconnectReason, uint nodeId) + { + // NOTE: This method is not implemented in ldn_mitm + Logger.Stub?.PrintMsg(LogClass.ServiceLdn, "LdnMitmClient Reject"); + + return ResultCode.Success; + } + + public NetworkInfo[] Scan(ushort channel, ScanFilter scanFilter) + { + return _lanDiscovery.Scan(channel, scanFilter); + } + + public void SetAdvertiseData(byte[] data) + { + _lanDiscovery.SetAdvertiseData(data); + } + + public void SetGameVersion(byte[] versionString) + { + // NOTE: This method is not implemented in ldn_mitm + Logger.Stub?.PrintMsg(LogClass.ServiceLdn, "LdnMitmClient SetGameVersion"); + } + + public void SetStationAcceptPolicy(AcceptPolicy acceptPolicy) + { + // NOTE: This method is not implemented in ldn_mitm + Logger.Stub?.PrintMsg(LogClass.ServiceLdn, "LdnMitmClient SetStationAcceptPolicy"); + } + + public void Dispose() + { + _lanDiscovery.Dispose(); + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/ILdnSocket.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/ILdnSocket.cs new file mode 100644 index 000000000..b6e6cea9e --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/ILdnSocket.cs @@ -0,0 +1,12 @@ +using System; +using System.Net; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm.Proxy +{ + internal interface ILdnSocket : IDisposable + { + bool SendPacketAsync(EndPoint endpoint, byte[] buffer); + bool Start(); + bool Stop(); + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/ILdnTcpSocket.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/ILdnTcpSocket.cs new file mode 100644 index 000000000..97e3bd627 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/ILdnTcpSocket.cs @@ -0,0 +1,8 @@ +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm.Proxy +{ + internal interface ILdnTcpSocket : ILdnSocket + { + bool Connect(); + void DisconnectAndStop(); + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/LdnProxyTcpClient.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/LdnProxyTcpClient.cs new file mode 100644 index 000000000..cfe9a8aae --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/LdnProxyTcpClient.cs @@ -0,0 +1,99 @@ +using Ryujinx.Common.Logging; +using System; +using System.Net; +using System.Net.Sockets; +using System.Threading; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm.Proxy +{ + internal class LdnProxyTcpClient : NetCoreServer.TcpClient, ILdnTcpSocket + { + private readonly LanProtocol _protocol; + private byte[] _buffer; + private int _bufferEnd; + + public LdnProxyTcpClient(LanProtocol protocol, IPAddress address, int port) : base(address, port) + { + _protocol = protocol; + _buffer = new byte[LanProtocol.BufferSize]; + OptionSendBufferSize = LanProtocol.TcpTxBufferSize; + OptionReceiveBufferSize = LanProtocol.TcpRxBufferSize; + OptionSendBufferLimit = LanProtocol.TxBufferSizeMax; + OptionReceiveBufferLimit = LanProtocol.RxBufferSizeMax; + } + + protected override void OnConnected() + { + Logger.Info?.PrintMsg(LogClass.ServiceLdn, $"LdnProxyTCPClient connected!"); + } + + protected override void OnReceived(byte[] buffer, long offset, long size) + { + _protocol.Read(ref _buffer, ref _bufferEnd, buffer, (int)offset, (int)size); + } + + public void DisconnectAndStop() + { + DisconnectAsync(); + + while (IsConnected) + { + Thread.Yield(); + } + } + + public bool SendPacketAsync(EndPoint endPoint, byte[] data) + { + if (endPoint != null) + { + Logger.Warning?.PrintMsg(LogClass.ServiceLdn, "LdnProxyTcpClient is sending a packet but endpoint is not null."); + } + + if (IsConnecting && !IsConnected) + { + Logger.Info?.PrintMsg(LogClass.ServiceLdn, "LdnProxyTCPClient needs to connect before sending packets. Waiting..."); + + while (IsConnecting && !IsConnected) + { + Thread.Yield(); + } + } + + return SendAsync(data); + } + + protected override void OnError(SocketError error) + { + Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"LdnProxyTCPClient caught an error with code {error}"); + } + + protected override void Dispose(bool disposingManagedResources) + { + DisconnectAndStop(); + base.Dispose(disposingManagedResources); + } + + public override bool Connect() + { + // TODO: NetCoreServer has a Connect() method, but it currently leads to weird issues. + base.ConnectAsync(); + + while (IsConnecting) + { + Thread.Sleep(1); + } + + return IsConnected; + } + + public bool Start() + { + throw new InvalidOperationException("Start was called."); + } + + public bool Stop() + { + throw new InvalidOperationException("Stop was called."); + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/LdnProxyTcpServer.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/LdnProxyTcpServer.cs new file mode 100644 index 000000000..0ca12b9f6 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/LdnProxyTcpServer.cs @@ -0,0 +1,54 @@ +using NetCoreServer; +using Ryujinx.Common.Logging; +using System; +using System.Net; +using System.Net.Sockets; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm.Proxy +{ + internal class LdnProxyTcpServer : TcpServer, ILdnTcpSocket + { + private readonly LanProtocol _protocol; + + public LdnProxyTcpServer(LanProtocol protocol, IPAddress address, int port) : base(address, port) + { + _protocol = protocol; + OptionReuseAddress = true; + OptionSendBufferSize = LanProtocol.TcpTxBufferSize; + OptionReceiveBufferSize = LanProtocol.TcpRxBufferSize; + + Logger.Info?.PrintMsg(LogClass.ServiceLdn, $"LdnProxyTCPServer created a server for this address: {address}:{port}"); + } + + protected override TcpSession CreateSession() + { + return new LdnProxyTcpSession(this, _protocol); + } + + protected override void OnError(SocketError error) + { + Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"LdnProxyTCPServer caught an error with code {error}"); + } + + protected override void Dispose(bool disposingManagedResources) + { + Stop(); + base.Dispose(disposingManagedResources); + } + + public bool Connect() + { + throw new InvalidOperationException("Connect was called."); + } + + public void DisconnectAndStop() + { + Stop(); + } + + public bool SendPacketAsync(EndPoint endpoint, byte[] buffer) + { + throw new InvalidOperationException("SendPacketAsync was called."); + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/LdnProxyTcpSession.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/LdnProxyTcpSession.cs new file mode 100644 index 000000000..f30c4b011 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/LdnProxyTcpSession.cs @@ -0,0 +1,83 @@ +using Ryujinx.Common.Logging; +using Ryujinx.HLE.HOS.Services.Ldn.Types; +using System.Net; +using System.Net.Sockets; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm.Proxy +{ + internal class LdnProxyTcpSession : NetCoreServer.TcpSession + { + private readonly LanProtocol _protocol; + + internal int NodeId; + internal NodeInfo NodeInfo; + + private byte[] _buffer; + private int _bufferEnd; + + public LdnProxyTcpSession(LdnProxyTcpServer server, LanProtocol protocol) : base(server) + { + _protocol = protocol; + _protocol.Connect += OnConnect; + _buffer = new byte[LanProtocol.BufferSize]; + OptionSendBufferSize = LanProtocol.TcpTxBufferSize; + OptionReceiveBufferSize = LanProtocol.TcpRxBufferSize; + OptionSendBufferLimit = LanProtocol.TxBufferSizeMax; + OptionReceiveBufferLimit = LanProtocol.RxBufferSizeMax; + } + + public void OverrideInfo() + { + NodeInfo.NodeId = (byte)NodeId; + NodeInfo.IsConnected = (byte)(IsConnected ? 1 : 0); + } + + protected override void OnConnected() + { + Logger.Info?.PrintMsg(LogClass.ServiceLdn, "LdnProxyTCPSession connected!"); + } + + protected override void OnDisconnected() + { + Logger.Info?.PrintMsg(LogClass.ServiceLdn, "LdnProxyTCPSession disconnected!"); + + _protocol.InvokeDisconnectStation(this); + } + + protected override void OnReceived(byte[] buffer, long offset, long size) + { + _protocol.Read(ref _buffer, ref _bufferEnd, buffer, (int)offset, (int)size, this.Socket.RemoteEndPoint); + } + + protected override void OnError(SocketError error) + { + Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"LdnProxyTCPSession caught an error with code {error}"); + + Dispose(); + } + + protected override void Dispose(bool disposingManagedResources) + { + _protocol.Connect -= OnConnect; + base.Dispose(disposingManagedResources); + } + + private void OnConnect(NodeInfo info, EndPoint endPoint) + { + try + { + if (endPoint.Equals(this.Socket.RemoteEndPoint)) + { + NodeInfo = info; + _protocol.InvokeAccept(this); + } + } + catch (System.ObjectDisposedException) + { + Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"LdnProxyTCPSession was disposed. [IP: {NodeInfo.Ipv4Address}]"); + + _protocol.InvokeDisconnectStation(this); + } + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/LdnProxyUdpServer.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/LdnProxyUdpServer.cs new file mode 100644 index 000000000..b1519d1ff --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/LdnProxyUdpServer.cs @@ -0,0 +1,157 @@ +using Ryujinx.Common.Logging; +using Ryujinx.HLE.HOS.Services.Ldn.Types; +using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm.Types; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Net; +using System.Net.Sockets; +using System.Threading; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm.Proxy +{ + internal class LdnProxyUdpServer : NetCoreServer.UdpServer, ILdnSocket + { + private const long ScanFrequency = 1000; + + private readonly LanProtocol _protocol; + private byte[] _buffer; + private int _bufferEnd; + + private readonly object _scanLock = new(); + + private Dictionary _scanResultsLast = new(); + private Dictionary _scanResults = new(); + private readonly AutoResetEvent _scanResponse = new(false); + private long _lastScanTime; + + public LdnProxyUdpServer(LanProtocol protocol, IPAddress address, int port) : base(address, port) + { + _protocol = protocol; + _protocol.Scan += HandleScan; + _protocol.ScanResponse += HandleScanResponse; + _buffer = new byte[LanProtocol.BufferSize]; + OptionReuseAddress = true; + OptionReceiveBufferSize = LanProtocol.RxBufferSizeMax; + OptionSendBufferSize = LanProtocol.TxBufferSizeMax; + + Start(); + } + + protected override Socket CreateSocket() + { + return new Socket(Endpoint.AddressFamily, SocketType.Dgram, ProtocolType.Udp) + { + EnableBroadcast = true, + }; + } + + protected override void OnStarted() + { + ReceiveAsync(); + } + + protected override void OnReceived(EndPoint endpoint, byte[] buffer, long offset, long size) + { + _protocol.Read(ref _buffer, ref _bufferEnd, buffer, (int)offset, (int)size, endpoint); + ReceiveAsync(); + } + + protected override void OnError(SocketError error) + { + Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"LdnProxyUdpServer caught an error with code {error}"); + } + + protected override void Dispose(bool disposingManagedResources) + { + _protocol.Scan -= HandleScan; + _protocol.ScanResponse -= HandleScanResponse; + + _scanResponse.Dispose(); + + base.Dispose(disposingManagedResources); + } + + public bool SendPacketAsync(EndPoint endpoint, byte[] data) + { + return SendAsync(endpoint, data); + } + + private void HandleScan(EndPoint endpoint, LanPacketType type, byte[] data) + { + _protocol.SendPacket(this, type, data, endpoint); + } + + private void HandleScanResponse(NetworkInfo info) + { + Span mac = stackalloc byte[8]; + + info.Common.MacAddress.AsSpan().CopyTo(mac); + + lock (_scanLock) + { + _scanResults[BitConverter.ToUInt64(mac)] = info; + + _scanResponse.Set(); + } + } + + public void ClearScanResults() + { + // Rate limit scans. + + long timeMs = Stopwatch.GetTimestamp() / (Stopwatch.Frequency / 1000); + long delay = ScanFrequency - (timeMs - _lastScanTime); + + if (delay > 0) + { + Thread.Sleep((int)delay); + } + + _lastScanTime = timeMs; + + lock (_scanLock) + { + var newResults = _scanResultsLast; + newResults.Clear(); + + _scanResultsLast = _scanResults; + _scanResults = newResults; + + _scanResponse.Reset(); + } + } + + public Dictionary GetScanResults() + { + // NOTE: Try to minimize waiting time for scan results. + // After we receive the first response, wait a short time for follow-ups and return. + // Responses that were too late to catch will appear in the next scan. + + // ldn_mitm does not do this, but this improves latency for games that expect it to be low (it is on console). + + if (_scanResponse.WaitOne(1000)) + { + // Wait a short while longer in case there are some other responses. + Thread.Sleep(33); + } + + lock (_scanLock) + { + var results = new Dictionary(); + + foreach (KeyValuePair last in _scanResultsLast) + { + results[last.Key] = last.Value; + } + + foreach (KeyValuePair scan in _scanResults) + { + results[scan.Key] = scan.Value; + } + + return results; + } + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Types/LanPacketHeader.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Types/LanPacketHeader.cs new file mode 100644 index 000000000..4cebe414d --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Types/LanPacketHeader.cs @@ -0,0 +1,16 @@ +using Ryujinx.Common.Memory; +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm.Types +{ + [StructLayout(LayoutKind.Sequential, Size = 12)] + internal struct LanPacketHeader + { + public uint Magic; + public LanPacketType Type; + public byte Compressed; + public ushort Length; + public ushort DecompressLength; + public Array2 Reserved; + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Types/LanPacketType.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Types/LanPacketType.cs new file mode 100644 index 000000000..901f00b00 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Types/LanPacketType.cs @@ -0,0 +1,10 @@ +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm.Types +{ + internal enum LanPacketType : byte + { + Scan, + ScanResponse, + Connect, + SyncNetwork, + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/NetworkChangeEventArgs.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/NetworkChangeEventArgs.cs similarity index 91% rename from src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/NetworkChangeEventArgs.cs rename to src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/NetworkChangeEventArgs.cs index 1cc09c00d..b379d2680 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/NetworkChangeEventArgs.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/NetworkChangeEventArgs.cs @@ -1,7 +1,7 @@ using Ryujinx.HLE.HOS.Services.Ldn.Types; using System; -namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator { class NetworkChangeEventArgs : EventArgs { diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Station.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Station.cs index c190d6ed1..e39c01978 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Station.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Station.cs @@ -1,7 +1,6 @@ using Ryujinx.Common.Memory; using Ryujinx.HLE.HOS.Services.Ldn.Types; -using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Network.Types; -using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn.Types; +using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types; using System; namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator @@ -22,7 +21,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator _parent.NetworkClient.NetworkChange += NetworkChanged; } - private void NetworkChanged(object sender, RyuLdn.NetworkChangeEventArgs e) + private void NetworkChanged(object sender, NetworkChangeEventArgs e) { LatestUpdates.CalculateLatestUpdate(NetworkInfo.Ldn.Nodes, e.Info.Ldn.Nodes); diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/Types/ConnectPrivateRequest.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/ConnectPrivateRequest.cs similarity index 86% rename from src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/Types/ConnectPrivateRequest.cs rename to src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/ConnectPrivateRequest.cs index 47e48d0a1..058ce62d0 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/Types/ConnectPrivateRequest.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/ConnectPrivateRequest.cs @@ -1,7 +1,7 @@ using Ryujinx.HLE.HOS.Services.Ldn.Types; using System.Runtime.InteropServices; -namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn.Types +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types { [StructLayout(LayoutKind.Sequential, Size = 0xBC)] struct ConnectPrivateRequest diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Network/Types/ConnectRequest.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/ConnectRequest.cs similarity index 84% rename from src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Network/Types/ConnectRequest.cs rename to src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/ConnectRequest.cs index 9ff46cccb..136589b2a 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Network/Types/ConnectRequest.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/ConnectRequest.cs @@ -1,7 +1,7 @@ using Ryujinx.HLE.HOS.Services.Ldn.Types; using System.Runtime.InteropServices; -namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Network.Types +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types { [StructLayout(LayoutKind.Sequential, Size = 0x4FC)] struct ConnectRequest diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/Types/CreateAccessPointPrivateRequest.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/CreateAccessPointPrivateRequest.cs similarity index 88% rename from src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/Types/CreateAccessPointPrivateRequest.cs rename to src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/CreateAccessPointPrivateRequest.cs index 6e890618c..ec0668884 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/Types/CreateAccessPointPrivateRequest.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/CreateAccessPointPrivateRequest.cs @@ -1,7 +1,7 @@ using Ryujinx.HLE.HOS.Services.Ldn.Types; using System.Runtime.InteropServices; -namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn.Types +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types { /// /// Advertise data is appended separately (remaining data in the buffer). diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Network/Types/CreateAccessPointRequest.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/CreateAccessPointRequest.cs similarity index 86% rename from src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Network/Types/CreateAccessPointRequest.cs rename to src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/CreateAccessPointRequest.cs index 4efe9165a..eecea5eb0 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Network/Types/CreateAccessPointRequest.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/CreateAccessPointRequest.cs @@ -1,7 +1,7 @@ using Ryujinx.HLE.HOS.Services.Ldn.Types; using System.Runtime.InteropServices; -namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Network.Types +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types { /// /// Advertise data is appended separately (remaining data in the buffer). diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/Types/NetworkError.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/NetworkError.cs similarity index 80% rename from src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/Types/NetworkError.cs rename to src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/NetworkError.cs index 70ebf7e38..cd576e055 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/Types/NetworkError.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/NetworkError.cs @@ -1,4 +1,4 @@ -namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn.Types +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types { enum NetworkError : int { diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/Types/NetworkErrorMessage.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/NetworkErrorMessage.cs similarity index 71% rename from src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/Types/NetworkErrorMessage.cs rename to src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/NetworkErrorMessage.cs index acb0b36ac..7e0c2a43f 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/Types/NetworkErrorMessage.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/NetworkErrorMessage.cs @@ -1,6 +1,6 @@ using System.Runtime.InteropServices; -namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn.Types +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types { [StructLayout(LayoutKind.Sequential, Size = 0x4)] struct NetworkErrorMessage diff --git a/src/Ryujinx.HLE/Ryujinx.HLE.csproj b/src/Ryujinx.HLE/Ryujinx.HLE.csproj index 5e3aa0eac..f3439cc8f 100644 --- a/src/Ryujinx.HLE/Ryujinx.HLE.csproj +++ b/src/Ryujinx.HLE/Ryujinx.HLE.csproj @@ -27,6 +27,7 @@ + diff --git a/src/Ryujinx.Ui.Common/Configuration/ConfigurationState.cs b/src/Ryujinx.Ui.Common/Configuration/ConfigurationState.cs index 9d2df5f03..9ed8fd8cc 100644 --- a/src/Ryujinx.Ui.Common/Configuration/ConfigurationState.cs +++ b/src/Ryujinx.Ui.Common/Configuration/ConfigurationState.cs @@ -571,6 +571,7 @@ namespace Ryujinx.Ui.Common.Configuration { LanInterfaceId = new ReactiveObject(); Mode = new ReactiveObject(); + Mode.Event += static (_, e) => LogValueChange(e, nameof(MultiplayerMode)); } } diff --git a/src/Ryujinx/Ui/MainWindow.cs b/src/Ryujinx/Ui/MainWindow.cs index f4817277d..a9d4be109 100644 --- a/src/Ryujinx/Ui/MainWindow.cs +++ b/src/Ryujinx/Ui/MainWindow.cs @@ -1121,6 +1121,14 @@ namespace Ryujinx.Ui 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); diff --git a/src/Ryujinx/Ui/Windows/SettingsWindow.cs b/src/Ryujinx/Ui/Windows/SettingsWindow.cs index f5186d5c1..dabef14dd 100644 --- a/src/Ryujinx/Ui/Windows/SettingsWindow.cs +++ b/src/Ryujinx/Ui/Windows/SettingsWindow.cs @@ -671,6 +671,8 @@ namespace Ryujinx.Ui.Windows } ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.ConfigurationPath); + + _parent.UpdateInternetAccess(); MainWindow.UpdateGraphicsConfig(); ThemeHelper.ApplyTheme(); } diff --git a/src/Ryujinx/Ui/Windows/SettingsWindow.glade b/src/Ryujinx/Ui/Windows/SettingsWindow.glade index fcc8c1d19..f0dbd6b63 100644 --- a/src/Ryujinx/Ui/Windows/SettingsWindow.glade +++ b/src/Ryujinx/Ui/Windows/SettingsWindow.glade @@ -2993,6 +2993,7 @@ Disabled Disabled + ldn_mitm @@ -3064,7 +3065,7 @@ True False - The network interface used for LAN features + The network interface used for LAN/LDN features end Network Interface: @@ -3079,7 +3080,7 @@ True False - The network interface used for LAN features + The network interface used for LAN/LDN features 0 Default From 9ef0be477bd6ea4c2c9aada53d94386824a87f00 Mon Sep 17 00:00:00 2001 From: gdkchan Date: Mon, 30 Oct 2023 19:18:28 -0300 Subject: [PATCH 104/105] Skip some invalid texture flushes (#5755) --- src/Ryujinx.Graphics.Gpu/Image/Texture.cs | 15 ++++++++++++++- src/Ryujinx.Graphics.Gpu/Image/TextureCache.cs | 2 -- src/Ryujinx.Graphics.Gpu/Image/TextureGroup.cs | 8 ++++++++ 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/src/Ryujinx.Graphics.Gpu/Image/Texture.cs b/src/Ryujinx.Graphics.Gpu/Image/Texture.cs index 022a3839f..dca6263aa 100644 --- a/src/Ryujinx.Graphics.Gpu/Image/Texture.cs +++ b/src/Ryujinx.Graphics.Gpu/Image/Texture.cs @@ -101,6 +101,11 @@ namespace Ryujinx.Graphics.Gpu.Image /// public bool AlwaysFlushOnOverlap { get; private set; } + /// + /// Indicates that the texture was fully unmapped since the modified flag was set, and flushes should be ignored until it is modified again. + /// + public bool FlushStale { get; private set; } + /// /// Increments when the host texture is swapped, or when the texture is removed from all pools. /// @@ -149,6 +154,7 @@ namespace Ryujinx.Graphics.Gpu.Image /// public bool HadPoolOwner { get; private set; } + /// /// Physical memory ranges where the texture data is located. /// public MultiRange Range { get; private set; } @@ -1411,6 +1417,7 @@ namespace Ryujinx.Graphics.Gpu.Image /// public void SignalModified() { + FlushStale = false; _scaledSetScore = Math.Max(0, _scaledSetScore - 1); if (_modifiedStale || Group.HasCopyDependencies) @@ -1431,6 +1438,7 @@ namespace Ryujinx.Graphics.Gpu.Image { if (bound) { + FlushStale = false; _scaledSetScore = Math.Max(0, _scaledSetScore - 1); } @@ -1695,12 +1703,17 @@ namespace Ryujinx.Graphics.Gpu.Image /// The range of memory being unmapped public void Unmapped(MultiRange unmapRange) { + if (unmapRange.Contains(Range)) + { + // If this is a full unmap, prevent flushes until the texture is mapped again. + FlushStale = true; + } + ChangedMapping = true; if (Group.Storage == this) { Group.Unmapped(); - Group.ClearModified(unmapRange); } } diff --git a/src/Ryujinx.Graphics.Gpu/Image/TextureCache.cs b/src/Ryujinx.Graphics.Gpu/Image/TextureCache.cs index 5048ccca4..432b10853 100644 --- a/src/Ryujinx.Graphics.Gpu/Image/TextureCache.cs +++ b/src/Ryujinx.Graphics.Gpu/Image/TextureCache.cs @@ -107,8 +107,6 @@ namespace Ryujinx.Graphics.Gpu.Image // Any texture that has been unmapped at any point or is partially unmapped // should update their pool references after the remap completes. - MultiRange unmapped = ((MemoryManager)sender).GetPhysicalRegions(e.Address, e.Size); - foreach (var texture in _partiallyMappedTextures) { texture.UpdatePoolMappings(); diff --git a/src/Ryujinx.Graphics.Gpu/Image/TextureGroup.cs b/src/Ryujinx.Graphics.Gpu/Image/TextureGroup.cs index 746a95ffc..21d7939ad 100644 --- a/src/Ryujinx.Graphics.Gpu/Image/TextureGroup.cs +++ b/src/Ryujinx.Graphics.Gpu/Image/TextureGroup.cs @@ -1659,6 +1659,14 @@ namespace Ryujinx.Graphics.Gpu.Image return; } + // If size is zero, we have nothing to flush. + // If the flush is stale, we should ignore it because the texture was unmapped since the modified + // flag was set, and flushing it is not safe anymore as the GPU might no longer own the memory. + if (size == 0 || Storage.FlushStale) + { + 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. From a16d582a105a6f9218e5f50fafd2670c64c1244c Mon Sep 17 00:00:00 2001 From: riperiperi Date: Mon, 30 Oct 2023 22:26:31 +0000 Subject: [PATCH 105/105] [HLE] Remove ServerBase 1ms polling (#5855) Added a KEvent for each ServerBase which signals whenever a session is added to the _sessions list, which allows it to rerun the ReplyAndReceive with the new session handle. This greatly reduces the presence of ServerBase on profiles, especially of games that aren't particularly busy. It should also increase responsiveness when adding session objects, as it doesn't take at most 1ms for them to start working. It also reduces the load on KTimeManager, which could allow it to spin less often. I have noticed that a bunch of games still do 1ms waits (they actually request a bit less than 1ms), so they still end up spinning to the next millisecond. Maybe for waits like this, it could attempt to nudge the timepoints to snap to each other when they're close enough, and also snap to whole millisecond waits when close enough. --- src/Ryujinx.HLE/HOS/Services/ServerBase.cs | 55 ++++++++++++++-------- 1 file changed, 36 insertions(+), 19 deletions(-) diff --git a/src/Ryujinx.HLE/HOS/Services/ServerBase.cs b/src/Ryujinx.HLE/HOS/Services/ServerBase.cs index 9d7e4d4c5..145680594 100644 --- a/src/Ryujinx.HLE/HOS/Services/ServerBase.cs +++ b/src/Ryujinx.HLE/HOS/Services/ServerBase.cs @@ -39,6 +39,8 @@ namespace Ryujinx.HLE.HOS.Services private readonly KernelContext _context; private KProcess _selfProcess; private KThread _selfThread; + private KEvent _wakeEvent; + private int _wakeHandle = 0; private readonly ReaderWriterLockSlim _handleLock = new(); private readonly Dictionary _sessions = new(); @@ -125,6 +127,8 @@ namespace Ryujinx.HLE.HOS.Services _handleLock.ExitWriteLock(); } } + + _wakeEvent.WritableEvent.Signal(); } private IpcService GetSessionObj(int serverSessionHandle) @@ -195,9 +199,11 @@ namespace Ryujinx.HLE.HOS.Services _selfProcess.CpuMemory.Write(messagePtr + 0x0, 0); _selfProcess.CpuMemory.Write(messagePtr + 0x4, 2 << 10); _selfProcess.CpuMemory.Write(messagePtr + 0x8, heapAddr | ((ulong)PointerBufferSize << 48)); - int replyTargetHandle = 0; + _wakeEvent = new KEvent(_context); + Result result = _selfProcess.HandleTable.GenerateHandle(_wakeEvent.ReadableEvent, out _wakeHandle); + while (true) { int portHandleCount; @@ -211,13 +217,15 @@ namespace Ryujinx.HLE.HOS.Services portHandleCount = _ports.Count; - handleCount = portHandleCount + _sessions.Count; + handleCount = portHandleCount + _sessions.Count + 1; handles = ArrayPool.Shared.Rent(handleCount); - _ports.Keys.CopyTo(handles, 0); + handles[0] = _wakeHandle; - _sessions.Keys.CopyTo(handles, portHandleCount); + _ports.Keys.CopyTo(handles, 1); + + _sessions.Keys.CopyTo(handles, portHandleCount + 1); } finally { @@ -227,8 +235,7 @@ namespace Ryujinx.HLE.HOS.Services } } - // We still need a timeout here to allow the service to pick up and listen new sessions... - var rc = _context.Syscall.ReplyAndReceive(out int signaledIndex, handles.AsSpan(0, handleCount), replyTargetHandle, 1000000L); + var rc = _context.Syscall.ReplyAndReceive(out int signaledIndex, handles.AsSpan(0, handleCount), replyTargetHandle, -1); _selfThread.HandlePostSyscall(); @@ -239,7 +246,7 @@ namespace Ryujinx.HLE.HOS.Services replyTargetHandle = 0; - if (rc == Result.Success && signaledIndex >= portHandleCount) + if (rc == Result.Success && signaledIndex >= portHandleCount + 1) { // We got a IPC request, process it, pass to the appropriate service if needed. int signaledHandle = handles[signaledIndex]; @@ -253,24 +260,32 @@ namespace Ryujinx.HLE.HOS.Services { if (rc == Result.Success) { - // We got a new connection, accept the session to allow servicing future requests. - if (_context.Syscall.AcceptSession(out int serverSessionHandle, handles[signaledIndex]) == Result.Success) + if (signaledIndex > 0) { - bool handleWriteLockTaken = false; - try + // We got a new connection, accept the session to allow servicing future requests. + if (_context.Syscall.AcceptSession(out int serverSessionHandle, handles[signaledIndex]) == Result.Success) { - handleWriteLockTaken = _handleLock.TryEnterWriteLock(Timeout.Infinite); - IpcService obj = _ports[handles[signaledIndex]].Invoke(); - _sessions.Add(serverSessionHandle, obj); - } - finally - { - if (handleWriteLockTaken) + bool handleWriteLockTaken = false; + try { - _handleLock.ExitWriteLock(); + handleWriteLockTaken = _handleLock.TryEnterWriteLock(Timeout.Infinite); + IpcService obj = _ports[handles[signaledIndex]].Invoke(); + _sessions.Add(serverSessionHandle, obj); + } + finally + { + if (handleWriteLockTaken) + { + _handleLock.ExitWriteLock(); + } } } } + else + { + // The _wakeEvent signalled, which means we have a new session. + _wakeEvent.WritableEvent.Clear(); + } } _selfProcess.CpuMemory.Write(messagePtr + 0x0, 0); @@ -499,6 +514,8 @@ namespace Ryujinx.HLE.HOS.Services if (Interlocked.Exchange(ref _isDisposed, 1) == 0) { + _selfProcess.HandleTable.CloseHandle(_wakeHandle); + foreach (IpcService service in _sessions.Values) { (service as IDisposable)?.Dispose();