From ad34237fc63dadef6b885724ec99f3c5072b7317 Mon Sep 17 00:00:00 2001 From: GreemDev Date: Sun, 10 May 2026 15:56:44 -0500 Subject: [PATCH 01/11] Revert "use new workflow type in conditions" This reverts commit bf083a716c3bc264ebca15e11236d1709c521c12. --- .forgejo/workflows/build.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.forgejo/workflows/build.yml b/.forgejo/workflows/build.yml index e7b961602..04cf4ea32 100644 --- a/.forgejo/workflows/build.yml +++ b/.forgejo/workflows/build.yml @@ -63,7 +63,7 @@ jobs: - name: Change config filename run: sed -r --in-place 's/\%\%RYUJINX_CONFIG_FILE_NAME\%\%/PRConfig\.json/g;' src/Ryujinx.Common/ReleaseInformation.cs shell: bash - if: forgejo.event_name == 'pull_request_target' + if: forgejo.event_name == 'pull_request' - name: 'Cache: ~/.nuget/packages' uses: actions/cache@v5 @@ -85,7 +85,7 @@ jobs: - name: Publish Ryujinx run: dotnet publish -c "${{ matrix.configuration }}" -r "${{ matrix.platform.name }}" -o ./publish -p:Version="${{ steps.version_info.outputs.result }}" -p:DebugType=embedded -p:SourceRevisionId="${{ steps.version_info.outputs.git_short_hash }}" -p:ExtraDefineConstants=DISABLE_UPDATER src/Ryujinx --self-contained - if: forgejo.event_name == 'pull_request_target' + if: forgejo.event_name == 'pull_request' - name: Packing Windows builds if: contains(matrix.platform.name, 'win') @@ -98,10 +98,10 @@ jobs: with: name: ryujinx-${{ matrix.configuration }}-${{ steps.version_info.outputs.result }}+${{ steps.version_info.outputs.git_short_hash }}-${{ matrix.platform.zip_os_name }} path: artifact - if: forgejo.event_name == 'pull_request_target' && contains(matrix.platform.name, 'win') + if: forgejo.event_name == 'pull_request' && contains(matrix.platform.name, 'win') - name: Build AppImage - if: forgejo.event_name == 'pull_request_target' && contains(matrix.platform.name, 'linux') + if: forgejo.event_name == 'pull_request' && contains(matrix.platform.name, 'linux') run: | chmod +x ./publish/Ryujinx ./publish/Ryujinx.sh @@ -134,7 +134,7 @@ jobs: - name: Upload Ryujinx AppImage artifact uses: actions/upload-artifact@v5 - if: forgejo.event_name == 'pull_request_target' && contains(matrix.platform.name, 'linux') + if: forgejo.event_name == 'pull_request' && contains(matrix.platform.name, 'linux') with: name: ryujinx-${{ matrix.configuration }}-${{ steps.version_info.outputs.result }}+${{ steps.version_info.outputs.git_short_hash }}-${{ matrix.platform.zip_os_name }}-AppImage path: publish_appimage @@ -182,7 +182,7 @@ jobs: - name: Change config filename run: sed -r --in-place 's/\%\%RYUJINX_CONFIG_FILE_NAME\%\%/PRConfig\.json/g;' src/Ryujinx.Common/ReleaseInformation.cs shell: bash - if: forgejo.event_name == 'pull_request_target' + if: forgejo.event_name == 'pull_request' - name: 'Cache: ~/.nuget/packages' uses: actions/cache@v5 @@ -201,7 +201,7 @@ jobs: with: name: ryujinx-${{ matrix.configuration }}-${{ steps.version_info.outputs.result }}+${{ steps.version_info.outputs.git_short_hash }}-macos_universal path: "publish/*.tar.gz" - if: forgejo.event_name == 'pull_request_target' + if: forgejo.event_name == 'pull_request' post_comment: name: Post comment linking uploaded artifacts From 708186d8d256521f0ff0d3bb9f1fa3db4f662b94 Mon Sep 17 00:00:00 2001 From: GreemDev Date: Sun, 10 May 2026 15:56:48 -0500 Subject: [PATCH 02/11] Revert "Update .forgejo/workflows/build.yml" This reverts commit 2d2661298c7ef484cf9dfc421e1409efdf137aa1. --- .forgejo/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.forgejo/workflows/build.yml b/.forgejo/workflows/build.yml index 04cf4ea32..03388765a 100644 --- a/.forgejo/workflows/build.yml +++ b/.forgejo/workflows/build.yml @@ -1,7 +1,7 @@ name: Build PR on: - pull_request_target: + pull_request: branches: [ master ] paths: - '**' From 5d8cb3e378e6eafb15f651bf462dcbfcc79f5228 Mon Sep 17 00:00:00 2001 From: greem Date: Sun, 10 May 2026 21:09:26 +0000 Subject: [PATCH 03/11] This stupid bullshit doesn't work, I'm done --- .forgejo/workflows/build.yml | 43 ------------------------------------ 1 file changed, 43 deletions(-) diff --git a/.forgejo/workflows/build.yml b/.forgejo/workflows/build.yml index 03388765a..e7052a6f6 100644 --- a/.forgejo/workflows/build.yml +++ b/.forgejo/workflows/build.yml @@ -202,46 +202,3 @@ jobs: name: ryujinx-${{ matrix.configuration }}-${{ steps.version_info.outputs.result }}+${{ steps.version_info.outputs.git_short_hash }}-macos_universal path: "publish/*.tar.gz" if: forgejo.event_name == 'pull_request' - - post_comment: - name: Post comment linking uploaded artifacts - runs-on: ubuntu-latest - needs: - - build - - build_macos - steps: - - uses: actions/github-script@v9 - env: - COMMENTER_TOKEN: ${{ secrets.COMMENTER_TOKEN }} - with: - github-token: 'n/a' - script: | - const forgejo = getOctokit(process.env.COMMENTER_TOKEN, { - baseUrl: 'https://git.ryujinx.app/api/v1' - }); - - const {owner, repo} = context.repo; - const run_id = ${{ env.FORGEJO_RUN_ID }}; - - const issue_number = ${{ forgejo.event.pull_request.number }}; - core.info(`Using run ID ${run_id} from pull request ${issue_number}`); - - const {data: {artifacts}} = await forgejo.rest.actions.listWorkflowRunArtifacts({owner, repo, run_id}); - if (artifacts == undefined || !artifacts.length) { - return core.error(`No artifacts found for run ID`); - } - let body = `Download the artifacts for this pull request:\n`; - for (const art of artifacts) { - const url = `https://git.ryujinx.app/api/v1/repos/${owner}/${repo}/actions/artifacts/${art.id}/zip`; - body += `\n* [${art.name}](${url})`; - } - - const {data: comments} = await forgejo.rest.issues.listComments({repo, owner, issue_number}); - const existing_comment = comments.find((c) => c.user.login === 'forgejo-actions'); - if (existing_comment) { - core.info(`Updating comment ${existing_comment.id}`); - await forgejo.rest.issues.updateComment({repo, owner, comment_id: existing_comment.id, body}); - } else { - core.info(`Creating a comment`); - await forgejo.rest.issues.createComment({repo, owner, issue_number, body}); - } From 1f9bfab9239402204933dfbb462fbf2c710b3dc0 Mon Sep 17 00:00:00 2001 From: Max Date: Sun, 10 May 2026 23:18:23 +0000 Subject: [PATCH 04/11] Updated OpenGL calls to no longer be deprecated (#83) - updated SharpCompress 0.47.4 -> 0.48.0 (security) - set ProcessResult to be nullable (since it is) Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/83 --- Directory.Packages.props | 4 ++-- .../Image/TextureView.cs | 18 +++++++++--------- .../PersistentBuffers.cs | 4 ++-- src/Ryujinx.Graphics.OpenGL/Pipeline.cs | 10 +++++----- src/Ryujinx.Graphics.OpenGL/Program.cs | 2 +- .../Queries/BufferedQuery.cs | 2 +- .../Loaders/Processes/ProcessLoader.cs | 2 ++ 7 files changed, 22 insertions(+), 20 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 7c94ffe24..4466f2777 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -8,7 +8,7 @@ - + @@ -64,4 +64,4 @@ - + \ No newline at end of file diff --git a/src/Ryujinx.Graphics.OpenGL/Image/TextureView.cs b/src/Ryujinx.Graphics.OpenGL/Image/TextureView.cs index 12ec23c8b..9b1df80dc 100644 --- a/src/Ryujinx.Graphics.OpenGL/Image/TextureView.cs +++ b/src/Ryujinx.Graphics.OpenGL/Image/TextureView.cs @@ -551,7 +551,7 @@ namespace Ryujinx.Graphics.OpenGL.Image level, x, width, - format.PixelFormat, + (InternalFormat) format.PixelFormat, mipSize, data); } @@ -579,7 +579,7 @@ namespace Ryujinx.Graphics.OpenGL.Image layer, width, 1, - format.PixelFormat, + (InternalFormat) format.PixelFormat, mipSize, data); } @@ -609,7 +609,7 @@ namespace Ryujinx.Graphics.OpenGL.Image y, width, height, - format.PixelFormat, + (InternalFormat) format.PixelFormat, mipSize, data); } @@ -643,7 +643,7 @@ namespace Ryujinx.Graphics.OpenGL.Image width, height, 1, - format.PixelFormat, + (InternalFormat) format.PixelFormat, mipSize, data); } @@ -675,7 +675,7 @@ namespace Ryujinx.Graphics.OpenGL.Image y, width, height, - format.PixelFormat, + (InternalFormat) format.PixelFormat, mipSize, data); } @@ -744,7 +744,7 @@ namespace Ryujinx.Graphics.OpenGL.Image level, 0, width, - format.PixelFormat, + (InternalFormat) format.PixelFormat, mipSize, data); } @@ -773,7 +773,7 @@ namespace Ryujinx.Graphics.OpenGL.Image 0, width, height, - format.PixelFormat, + (InternalFormat) format.PixelFormat, mipSize, data); } @@ -807,7 +807,7 @@ namespace Ryujinx.Graphics.OpenGL.Image width, height, depth, - format.PixelFormat, + (InternalFormat) format.PixelFormat, mipSize, data); } @@ -843,7 +843,7 @@ namespace Ryujinx.Graphics.OpenGL.Image 0, width, height, - format.PixelFormat, + (InternalFormat) format.PixelFormat, mipSize / 6, data + faceOffset); } diff --git a/src/Ryujinx.Graphics.OpenGL/PersistentBuffers.cs b/src/Ryujinx.Graphics.OpenGL/PersistentBuffers.cs index d5c02f4df..b77ac5f17 100644 --- a/src/Ryujinx.Graphics.OpenGL/PersistentBuffers.cs +++ b/src/Ryujinx.Graphics.OpenGL/PersistentBuffers.cs @@ -27,7 +27,7 @@ namespace Ryujinx.Graphics.OpenGL public void Map(BufferHandle handle, int size) { GL.BindBuffer(BufferTarget.CopyWriteBuffer, handle); - nint ptr = GL.MapBufferRange(BufferTarget.CopyWriteBuffer, nint.Zero, size, BufferAccessMask.MapReadBit | BufferAccessMask.MapPersistentBit); + nint ptr = GL.MapBufferRange(BufferTarget.CopyWriteBuffer, nint.Zero, size, MapBufferAccessMask.MapReadBit | MapBufferAccessMask.MapPersistentBit); _maps[handle] = ptr; } @@ -75,7 +75,7 @@ namespace Ryujinx.Graphics.OpenGL GL.BindBuffer(BufferTarget.CopyWriteBuffer, _copyBufferHandle); GL.BufferStorage(BufferTarget.CopyWriteBuffer, requiredSize, nint.Zero, BufferStorageFlags.MapReadBit | BufferStorageFlags.MapPersistentBit); - _bufferMap = GL.MapBufferRange(BufferTarget.CopyWriteBuffer, nint.Zero, requiredSize, BufferAccessMask.MapReadBit | BufferAccessMask.MapPersistentBit); + _bufferMap = GL.MapBufferRange(BufferTarget.CopyWriteBuffer, nint.Zero, requiredSize, MapBufferAccessMask.MapReadBit | MapBufferAccessMask.MapPersistentBit); } } diff --git a/src/Ryujinx.Graphics.OpenGL/Pipeline.cs b/src/Ryujinx.Graphics.OpenGL/Pipeline.cs index e58e6f2b9..5b1e63e3b 100644 --- a/src/Ryujinx.Graphics.OpenGL/Pipeline.cs +++ b/src/Ryujinx.Graphics.OpenGL/Pipeline.cs @@ -924,8 +924,8 @@ namespace Ryujinx.Graphics.OpenGL GL.Disable(EnableCap.CullFace); return; } - - GL.CullFace(face.Convert()); + + GL.CullFace((TriangleFace) face.Convert()); GL.Enable(EnableCap.CullFace); } @@ -1085,12 +1085,12 @@ namespace Ryujinx.Graphics.OpenGL { if (frontMode == backMode) { - GL.PolygonMode(MaterialFace.FrontAndBack, frontMode.Convert()); + GL.PolygonMode((TriangleFace) MaterialFace.FrontAndBack, frontMode.Convert()); } else { - GL.PolygonMode(MaterialFace.Front, frontMode.Convert()); - GL.PolygonMode(MaterialFace.Back, backMode.Convert()); + GL.PolygonMode((TriangleFace) MaterialFace.Front, frontMode.Convert()); + GL.PolygonMode((TriangleFace) MaterialFace.Back, backMode.Convert()); } } diff --git a/src/Ryujinx.Graphics.OpenGL/Program.cs b/src/Ryujinx.Graphics.OpenGL/Program.cs index 608a03451..cb9933c10 100644 --- a/src/Ryujinx.Graphics.OpenGL/Program.cs +++ b/src/Ryujinx.Graphics.OpenGL/Program.cs @@ -59,7 +59,7 @@ namespace Ryujinx.Graphics.OpenGL GL.CompileShader(shaderHandle); break; case TargetLanguage.Spirv: - GL.ShaderBinary(1, ref shaderHandle, (BinaryFormat)All.ShaderBinaryFormatSpirVArb, shader.BinaryCode, shader.BinaryCode.Length); + GL.ShaderBinary(1, ref shaderHandle, ShaderBinaryFormat.ShaderBinaryFormatSpirV, shader.BinaryCode, shader.BinaryCode.Length); GL.SpecializeShader(shaderHandle, "main", 0, (int[])null, (int[])null); break; } diff --git a/src/Ryujinx.Graphics.OpenGL/Queries/BufferedQuery.cs b/src/Ryujinx.Graphics.OpenGL/Queries/BufferedQuery.cs index f39829923..c9dbcdcf2 100644 --- a/src/Ryujinx.Graphics.OpenGL/Queries/BufferedQuery.cs +++ b/src/Ryujinx.Graphics.OpenGL/Queries/BufferedQuery.cs @@ -32,7 +32,7 @@ namespace Ryujinx.Graphics.OpenGL.Queries GL.BufferStorage(BufferTarget.QueryBuffer, sizeof(long), (nint)(&defaultValue), BufferStorageFlags.MapReadBit | BufferStorageFlags.MapWriteBit | BufferStorageFlags.MapPersistentBit); } - _bufferMap = GL.MapBufferRange(BufferTarget.QueryBuffer, nint.Zero, sizeof(long), BufferAccessMask.MapReadBit | BufferAccessMask.MapWriteBit | BufferAccessMask.MapPersistentBit); + _bufferMap = GL.MapBufferRange(BufferTarget.QueryBuffer, nint.Zero, sizeof(long), MapBufferAccessMask.MapReadBit | MapBufferAccessMask.MapWriteBit | MapBufferAccessMask.MapPersistentBit); } public void Reset() diff --git a/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs b/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs index e0edd2df5..900703f6e 100644 --- a/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs +++ b/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs @@ -28,6 +28,7 @@ namespace Ryujinx.HLE.Loaders.Processes private ulong _latestPid; +#nullable enable public ProcessResult? ActiveApplication { get @@ -44,6 +45,7 @@ namespace Ryujinx.HLE.Loaders.Processes return value; } } +#nullable disable public ProcessLoader(Switch device) { From bf7f978f9da0d368aae449d798550f2593b412d1 Mon Sep 17 00:00:00 2001 From: Max Date: Sun, 10 May 2026 23:18:53 +0000 Subject: [PATCH 05/11] [HLE] Implemented ILibraryAppletSelfAccessor:1 (#79) Needed for Tomodachi Life: Living the Dream (?) based on [this](https://www.reddit.com/r/Ryubing/comments/1t4lfc9/comment/ok4e7tu/) Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/79 --- .../ILibraryAppletSelfAccessor.cs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/Ryujinx.HLE/HOS/Services/Am/AppletAE/AllSystemAppletProxiesService/LibraryAppletProxy/ILibraryAppletSelfAccessor.cs b/src/Ryujinx.HLE/HOS/Services/Am/AppletAE/AllSystemAppletProxiesService/LibraryAppletProxy/ILibraryAppletSelfAccessor.cs index fc02ea172..44c4d133a 100644 --- a/src/Ryujinx.HLE/HOS/Services/Am/AppletAE/AllSystemAppletProxiesService/LibraryAppletProxy/ILibraryAppletSelfAccessor.cs +++ b/src/Ryujinx.HLE/HOS/Services/Am/AppletAE/AllSystemAppletProxiesService/LibraryAppletProxy/ILibraryAppletSelfAccessor.cs @@ -44,6 +44,22 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletAE.AllSystemAppletProxiesService.Lib return ResultCode.Success; } + + [CommandCmif(1)] + // PushOutData(object) + public ResultCode PushOutData(ServiceCtx context) + { + IStorage appletData = GetObject(context, 0); + + if (appletData == null || appletData.Data.Length == 0) // is this necessary? + { + return ResultCode.NullObject; + } + + _appletStandalone.InputData.Enqueue(appletData.Data); + + return ResultCode.Success; + } [CommandCmif(11)] // GetLibraryAppletInfo() -> nn::am::service::LibraryAppletInfo From 5511ff5686d712e53cf4d6b16d31c8da4dda92c8 Mon Sep 17 00:00:00 2001 From: Frosch Date: Sun, 10 May 2026 23:19:59 +0000 Subject: [PATCH 06/11] fix: gamepads have the same name (#27) When connecting multiple controllers of the same model, the first device's name ends with (0), the second with (1), the third with (1), the fourth with (1), and so on. To ensure these names are truly unique, GetUniqueGamepadName is now called recursively. Before: ![image](/attachments/c27ab407-0945-48d8-92a8-6f1fe7fb2727) After: ![image](/attachments/da7b1427-958c-45d5-8351-6f977d971e1e) Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/27 --- src/Ryujinx/UI/ViewModels/Input/InputViewModel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Ryujinx/UI/ViewModels/Input/InputViewModel.cs b/src/Ryujinx/UI/ViewModels/Input/InputViewModel.cs index e5f085e0f..51229af72 100644 --- a/src/Ryujinx/UI/ViewModels/Input/InputViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/Input/InputViewModel.cs @@ -574,7 +574,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input if (Devices.Any(controller => controller.Name == name)) { controllerNumber++; - name = GetGamepadName(gamepad, controllerNumber); + name = GetUniqueGamepadName(gamepad, ref controllerNumber); } return name; From e9c31bea3b3483a9ceee884be8b8077bf91c657c Mon Sep 17 00:00:00 2001 From: Shyanne Date: Sun, 10 May 2026 23:29:15 +0000 Subject: [PATCH 07/11] [DEBUG] Implemented NetLog logging type (#5) Implemented a new debug log type called NetLog and added more verbose logging to the LDN service. I'd like some feedback on what all should be logged under this category -- I'm likely going to be adding logs for sockets as well, but I want to know specifically if the logging should be more or less verbose and what would be the most helpful things to log. ![image](/attachments/70d5d467-2b57-436b-944f-7bf7a1f609af) Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/5 --- assets/Locales/Root.json | 50 +++++++++ src/Ryujinx.Common/Logging/LogLevel.cs | 1 + src/Ryujinx.Common/Logging/Logger.cs | 2 + .../IUserLocalCommunicationService.cs | 101 +++++++++++++++--- .../LdnRyu/LdnMasterProxyClient.cs | 8 +- .../Ldn/UserServiceCreator/Station.cs | 3 + src/Ryujinx/Headless/HeadlessRyujinx.cs | 1 + src/Ryujinx/Headless/Options.cs | 6 ++ .../Configuration/ConfigurationFileFormat.cs | 7 +- .../ConfigurationState.Migration.cs | 3 +- .../Configuration/ConfigurationState.Model.cs | 6 ++ .../Configuration/ConfigurationState.cs | 2 + .../Systems/Configuration/LoggerModule.cs | 2 + .../UI/ViewModels/SettingsViewModel.cs | 3 + .../Views/Settings/SettingsLoggingView.axaml | 4 + 15 files changed, 179 insertions(+), 20 deletions(-) diff --git a/assets/Locales/Root.json b/assets/Locales/Root.json index 0b1115cdc..eee476519 100644 --- a/assets/Locales/Root.json +++ b/assets/Locales/Root.json @@ -6100,6 +6100,31 @@ "zh_TW": "檔案系統全域存取日誌模式:" } }, + { + "ID": "SettingsTabLoggingEnableNetLogs", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Enable Net Logs", + "es_ES": "Habilitar registros de red.", + "fr_FR": "", + "he_IL": "", + "it_IT": "", + "ja_JP": "", + "ko_KR": "", + "no_NO": "", + "pl_PL": "", + "pt_BR": "", + "ru_RU": "", + "sv_SE": "", + "th_TH": "", + "tr_TR": "", + "uk_UA": "", + "zh_CN": "", + "zh_TW": "" + } + }, { "ID": "SettingsTabLoggingDeveloperOptions", "Translations": { @@ -17075,6 +17100,31 @@ "zh_TW": "啟用檔案系統存取日誌輸出到控制台中。可能的模式為 0 到 3。" } }, + { + "ID": "NetLogTooltip", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Prints network log messages in the console.", + "es_ES": "Imprimir registros de red en la consola.", + "fr_FR": "", + "he_IL": "", + "it_IT": "", + "ja_JP": "", + "ko_KR": "", + "no_NO": "", + "pl_PL": "", + "pt_BR": "", + "ru_RU": "", + "sv_SE": "", + "th_TH": "", + "tr_TR": "", + "uk_UA": "", + "zh_CN": "", + "zh_TW": "" + } + }, { "ID": "DeveloperOptionTooltip", "Translations": { diff --git a/src/Ryujinx.Common/Logging/LogLevel.cs b/src/Ryujinx.Common/Logging/LogLevel.cs index 282b07111..e8778191a 100644 --- a/src/Ryujinx.Common/Logging/LogLevel.cs +++ b/src/Ryujinx.Common/Logging/LogLevel.cs @@ -12,6 +12,7 @@ namespace Ryujinx.Common.Logging Error, Guest, AccessLog, + NetLog, Notice, Trace, } diff --git a/src/Ryujinx.Common/Logging/Logger.cs b/src/Ryujinx.Common/Logging/Logger.cs index b5450c94b..788d2ab3d 100644 --- a/src/Ryujinx.Common/Logging/Logger.cs +++ b/src/Ryujinx.Common/Logging/Logger.cs @@ -119,6 +119,7 @@ namespace Ryujinx.Common.Logging public static Log? Error { get; private set; } public static Log? Guest { get; private set; } public static Log? AccessLog { get; private set; } + public static Log? NetLog { get; private set; } public static Log? Stub { get; private set; } public static Log? Trace { get; private set; } public static Log Notice { get; } // Always enabled @@ -247,6 +248,7 @@ namespace Ryujinx.Common.Logging case LogLevel.Error : Error = enabled ? new Log(LogLevel.Error) : null; break; case LogLevel.Guest : Guest = enabled ? new Log(LogLevel.Guest) : null; break; case LogLevel.AccessLog : AccessLog = enabled ? new Log(LogLevel.AccessLog) : null; break; + case LogLevel.NetLog : NetLog = enabled ? new Log(LogLevel.NetLog) : null; break; case LogLevel.Stub : Stub = enabled ? new Log(LogLevel.Stub) : null; break; case LogLevel.Trace : Trace = enabled ? new Log(LogLevel.Trace) : null; break; case LogLevel.Notice : break; diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/IUserLocalCommunicationService.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/IUserLocalCommunicationService.cs index 71d1623f3..2f764e99f 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/IUserLocalCommunicationService.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/IUserLocalCommunicationService.cs @@ -5,7 +5,6 @@ using Ryujinx.Common.Logging; using Ryujinx.Common.Memory; using Ryujinx.Common.Utilities; using Ryujinx.Cpu; -using Ryujinx.HLE.Exceptions; using Ryujinx.HLE.HOS.Ipc; using Ryujinx.HLE.HOS.Kernel.Threading; using Ryujinx.HLE.HOS.Services.Ldn.Types; @@ -15,7 +14,6 @@ using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types; using Ryujinx.Horizon.Common; using Ryujinx.Memory; using System; -using System.ComponentModel; using System.IO; using System.Net; using System.Net.NetworkInformation; @@ -68,10 +66,11 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator { if (localCommunicationId == localCommunicationIdChecked) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn,"CheckLocalCommumicationIdPermission: Checked!"); return true; } } - + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn,"CheckLocalCommumicationIdPermission: Check failed!"); return false; } @@ -82,7 +81,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator if (_nifmResultCode != ResultCode.Success) { context.ResponseData.Write((int)NetworkState.Error); - + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn,$"GetState: _nifmResultCode = {_nifmResultCode.ToString()}"); return ResultCode.Success; } @@ -114,12 +113,14 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator if (_nifmResultCode != ResultCode.Success) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn,$"GetNetworkInfo: _nifmResultCode = {_nifmResultCode.ToString()}"); return _nifmResultCode; } ResultCode resultCode = GetNetworkInfoImpl(out NetworkInfo networkInfo); if (resultCode != ResultCode.Success) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn,$"GetState: resultCode = {resultCode.ToString()}"); return resultCode; } @@ -135,18 +136,22 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator if (_state == NetworkState.StationConnected) { networkInfo = _station.NetworkInfo; + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn,"GetNetworkInfoImpl: _station"); } else if (_state == NetworkState.AccessPointCreated) { networkInfo = _accessPoint.NetworkInfo; + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn,"GetNetworkInfoImpl: _accessPoint"); } else { networkInfo = new NetworkInfo(); - + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn,"GetNetworkInfoImpl: Invalid state!"); + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn,$"GetNetworkInfoImpl: networkInfo = {networkInfo}"); return ResultCode.InvalidState; } + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn,$"GetNetworkInfoImpl: networkInfo = {networkInfo}"); return ResultCode.Success; } @@ -198,7 +203,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator } else { - Logger.Info?.Print(LogClass.ServiceLdn, $"Console's LDN IP is \"{unicastAddress.Address}\"."); + Logger.NetLog?.Print(LogClass.ServiceLdn, $"Console's LDN IP is \"{unicastAddress.Address}\"."); context.ResponseData.Write(NetworkHelpers.ConvertIpv4Address(unicastAddress.Address)); context.ResponseData.Write(NetworkHelpers.ConvertIpv4Address(unicastAddress.IPv4Mask)); @@ -206,7 +211,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator } else { - Logger.Info?.Print(LogClass.ServiceLdn, $"LDN obtained proxy IP."); + Logger.NetLog?.Print(LogClass.ServiceLdn, $"LDN obtained proxy IP."); context.ResponseData.Write(config.ProxyIp); context.ResponseData.Write(config.ProxySubnetMask); @@ -227,7 +232,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator // NOTE: Returns ResultCode.InvalidArgument if _disconnectReason is null, doesn't occur in our case. context.ResponseData.Write((short)_disconnectReason); - + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"GetDisconnectReason: {_disconnectReason}"); return ResultCode.Success; } @@ -247,12 +252,14 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator { if (_nifmResultCode != ResultCode.Success) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"GetSecurityParameter: _nifmResultCode = {_nifmResultCode}"); return _nifmResultCode; } ResultCode resultCode = GetNetworkInfoImpl(out NetworkInfo networkInfo); if (resultCode != ResultCode.Success) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"GetSecurityParameter: resultCode = {resultCode}"); return resultCode; } @@ -263,7 +270,8 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator }; context.ResponseData.WriteStruct(securityParameter); - + + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"GetSecurityParameter: securityParameter = {securityParameter}"); return ResultCode.Success; } @@ -273,12 +281,14 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator { if (_nifmResultCode != ResultCode.Success) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"GetNetworkConfig: _nifmResultCode = {_nifmResultCode}"); return _nifmResultCode; } ResultCode resultCode = GetNetworkInfoImpl(out NetworkInfo networkInfo); if (resultCode != ResultCode.Success) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"GetNetworkConfig: resultCode = {resultCode}"); return resultCode; } @@ -292,6 +302,8 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator }; context.ResponseData.WriteStruct(networkConfig); + + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"GetNetworkConfig: networkConfig = {networkConfig}"); return ResultCode.Success; } @@ -322,12 +334,14 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator if (_nifmResultCode != ResultCode.Success) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"GetNetworkInfoLatestUpdate: _nifmResultCode = {_nifmResultCode}"); return _nifmResultCode; } ResultCode resultCode = GetNetworkInfoImpl(out NetworkInfo networkInfo); if (resultCode != ResultCode.Success) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"GetNetworkInfoLatestUpdate: resultCode = {resultCode}"); return resultCode; } @@ -378,6 +392,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator if (_nifmResultCode != ResultCode.Success) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"ScanImpl: _nifmResultCode = {_nifmResultCode}"); return _nifmResultCode; } @@ -400,6 +415,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator { if (scanFilter.Ssid.Length <= 31) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"ScanImpl: resultCode = {resultCode}"); return resultCode; } } @@ -408,11 +424,13 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator { if (scanFilterFlag > ScanFilterFlag.All) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"ScanImpl: resultCode = {resultCode}"); return resultCode; } if (_state - 3 >= NetworkState.AccessPoint) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, "ScanImpl: Invalid state!"); resultCode = ResultCode.InvalidState; } else @@ -437,7 +455,8 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator } } } - + + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"ScanImpl: resultCode = {resultCode}"); return resultCode; } @@ -462,6 +481,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator } } + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"ScanInternal: availableGames = {availableGames}"); return ResultCode.Success; } @@ -502,7 +522,8 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator throw new ArgumentException($"{GetType().FullName}: Protocol value is not 1 or 3!! Protocol value: {protocolValue}"); } - Logger.Stub?.PrintStub(LogClass.ServiceLdn, $"Protocol value: {protocolValue}"); + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"SetProtocol: protocolValue = {protocolValue}"); + Logger.Stub?.PrintStub(LogClass.ServiceLdn, new { protocolValue}); return ResultCode.Success; } @@ -512,11 +533,13 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator { if (_nifmResultCode != ResultCode.Success) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"OpenAccessPoint: _nifmResultCode = {_nifmResultCode}"); return _nifmResultCode; } if (_state != NetworkState.Initialized) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, "OpenAccessPoint: Invalid state!"); return ResultCode.InvalidState; } @@ -538,6 +561,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator { if (_nifmResultCode != ResultCode.Success) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"CloseAccessPoint: _nifmResultCode = {_nifmResultCode}"); return _nifmResultCode; } @@ -547,6 +571,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator } else { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, "CloseAccessPoint: Invalid state!"); return ResultCode.InvalidState; } @@ -596,11 +621,13 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator bool isLocalCommunicationIdValid = CheckLocalCommunicationIdPermission(context, (ulong)networkConfig.IntentId.LocalCommunicationId); if (!isLocalCommunicationIdValid && NetworkClient.NeedsRealId) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, "CreateNetworkImpl: Invalid object!"); return ResultCode.InvalidObject; } if (_nifmResultCode != ResultCode.Success) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"CreateNetworkImpl: _nifmResultCode = {_nifmResultCode}"); return _nifmResultCode; } @@ -629,16 +656,22 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator AddressList addressList = MemoryMarshal.Cast(addressListBytes)[0]; _accessPoint.CreateNetworkPrivate(securityConfig, securityParameter, userConfig, networkConfig, addressList); + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"CreateNetworkImpl: Created private network! " + + $"| securityConfig = {securityConfig} | securityParameter = {securityParameter} " + + $"| userConfig = {userConfig} | networkConfig = {networkConfig} | addressList = {addressList}"); } else { _accessPoint.CreateNetwork(securityConfig, userConfig, networkConfig); + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"CreateNetworkImpl: Created network! " + + $"| securityConfig = {securityConfig} | userConfig = {userConfig} | networkConfig = {networkConfig}"); } return ResultCode.Success; } else { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, "CreateNetworkImpl: Invalid state!"); return ResultCode.InvalidState; } } @@ -660,6 +693,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator { if (_nifmResultCode != ResultCode.Success) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"DestroyNetworkImpl: _nifmResultCode = {_nifmResultCode}"); return _nifmResultCode; } @@ -676,9 +710,11 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator CloseAccessPoint(); + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, "DestroyNetworkImpl: Invalid state!"); return ResultCode.InvalidState; } + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, "DestroyNetworkImpl: Invalid argument!"); return ResultCode.InvalidArgument; } @@ -695,14 +731,17 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator { if (_nifmResultCode != ResultCode.Success) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"RejectImpl: _nifmResultCode = {_nifmResultCode}"); return _nifmResultCode; } if (_state != NetworkState.AccessPointCreated) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, "RejectImpl: Invalid state!"); return ResultCode.InvalidState; // Must be network host to reject nodes. } + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"RejectImpl: disconnectReason = {disconnectReason} | nodeId = {nodeId}"); return NetworkClient.Reject(disconnectReason, nodeId); } @@ -714,11 +753,13 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator if (_nifmResultCode != ResultCode.Success) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"SetAdvertiseData: _nifmResultCode = {_nifmResultCode}"); return _nifmResultCode; } if (bufferSize is 0 or > LdnConst.AdvertiseDataSizeMax) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, "SetAdvertiseData: Invalid argument!"); return ResultCode.InvalidArgument; } @@ -727,11 +768,12 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator byte[] advertiseData = new byte[bufferSize]; context.Memory.Read(bufferPosition, advertiseData); - + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"SetAdvertiseData: advertiseData = {advertiseData}"); return _accessPoint.SetAdvertiseData(advertiseData); } else { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, "SetAdvertiseData: Invalid state!"); return ResultCode.InvalidState; } } @@ -744,20 +786,24 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator if (_nifmResultCode != ResultCode.Success) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"SetStationAcceptPolicy: _nifmResultCode = {_nifmResultCode}"); return _nifmResultCode; } if (acceptPolicy > AcceptPolicy.WhiteList) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, "SetStationAcceptPolicy: Invalid argument!"); return ResultCode.InvalidArgument; } if (_state is NetworkState.AccessPoint or NetworkState.AccessPointCreated) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"SetStationAcceptPolicy: acceptPolicy = {acceptPolicy}"); return _accessPoint.SetStationAcceptPolicy(acceptPolicy); } else { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, "SetStationAcceptPolicy: Invalid state!"); return ResultCode.InvalidState; } } @@ -768,6 +814,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator { if (_nifmResultCode != ResultCode.Success) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"AddAcceptFilterEntry: _nifmResultCode = {_nifmResultCode}"); return _nifmResultCode; } @@ -782,6 +829,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator { if (_nifmResultCode != ResultCode.Success) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"ClearAcceptFilter: _nifmResultCode = {_nifmResultCode}"); return _nifmResultCode; } @@ -796,11 +844,13 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator { if (_nifmResultCode != ResultCode.Success) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"OpenStation: _nifmResultCode = {_nifmResultCode}"); return _nifmResultCode; } if (_state != NetworkState.Initialized) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, "OpenStation: Invalid state!"); return ResultCode.InvalidState; } @@ -813,6 +863,8 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator // NOTE: Calls nifm service and returns related result codes. // Since we use our own implementation we can return ResultCode.Success. + + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"OpenStation: _station = {_station}"); return ResultCode.Success; } @@ -823,6 +875,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator { if (_nifmResultCode != ResultCode.Success) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"CloseStation: _nifmResultCode = {_nifmResultCode}"); return _nifmResultCode; } @@ -832,11 +885,13 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator } else { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, "CloseStation: Invalid state!"); return ResultCode.InvalidState; } SetState(NetworkState.Initialized); - + + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, "CloseStation: Closed."); return ResultCode.Success; } @@ -901,11 +956,13 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator bool isLocalCommunicationIdValid = CheckLocalCommunicationIdPermission(context, (ulong)networkInfo.NetworkId.IntentId.LocalCommunicationId); if (!isLocalCommunicationIdValid && NetworkClient.NeedsRealId) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, "ConnectImpl: Invalid object!"); return ResultCode.InvalidObject; } if (_nifmResultCode != ResultCode.Success) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"ConnectImpl: _nifmResultCode = {_nifmResultCode}"); return _nifmResultCode; } @@ -925,6 +982,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator { if (_state != NetworkState.Station) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, "ConnectImpl: Invalid state!"); resultCode = ResultCode.InvalidState; } else @@ -932,10 +990,16 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator if (isPrivate) { resultCode = _station.ConnectPrivate(securityConfig, securityParameter, userConfig, localCommunicationVersion, optionUnknown, networkConfig); + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"ConnectImpl: Private connection established! " + + $"| securityConfig = {securityConfig} | securityParameter = {securityParameter} | userConfig = {userConfig} " + + $"| localCommunicationVersion = {localCommunicationVersion} | optionUnknown = {optionUnknown} | networkConfig = {networkConfig}"); } else { resultCode = _station.Connect(securityConfig, userConfig, localCommunicationVersion, optionUnknown, networkInfo); + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"ConnectImpl: Connection established! " + + $"| securityConfig = {securityConfig} | userConfig = {userConfig} " + + $"| localCommunicationVersion = {localCommunicationVersion} | optionUnknown = {optionUnknown} | networkConfig = {networkConfig}"); } } } @@ -943,6 +1007,8 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator } } + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"ConnectImpl: resultCode = {resultCode}"); + return resultCode; } @@ -957,6 +1023,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator { if (_nifmResultCode != ResultCode.Success) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"DisconnectImpl: _nifmResultCode = {_nifmResultCode}"); return _nifmResultCode; } @@ -970,14 +1037,17 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator _disconnectReason = disconnectReason; + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"DisconnectImpl: _disconnectReason = {_disconnectReason}"); return ResultCode.Success; } CloseStation(); + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, "DisconnectImpl: Invalid state!"); return ResultCode.InvalidState; } + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, "DisconnectImpl: Invalid argument!"); return ResultCode.InvalidArgument; } @@ -994,6 +1064,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator { if (_nifmResultCode != ResultCode.Success) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"Finalize: _disconnectReason = {_disconnectReason}"); return _nifmResultCode; } @@ -1010,11 +1081,13 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator _stateChangeEventHandle = 0; } + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"Finalize: resultCode = {resultCode}"); return resultCode; } private ResultCode FinalizeImpl(bool isCausedBySystem) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, "FinalizeImpl"); DisconnectReason disconnectReason; switch (_state) @@ -1138,7 +1211,6 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator NetworkClient.SetGameVersion(context.Device.Processes.ActiveApplication.ApplicationControlProperties.DisplayVersion); resultCode = ResultCode.Success; - _nifmResultCode = resultCode; SetState(NetworkState.Initialized); @@ -1152,6 +1224,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator } } + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"InitializeImpl: resultCode = {resultCode}"); return resultCode; } diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/LdnMasterProxyClient.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/LdnMasterProxyClient.cs index f93b1c4cc..37fa722b7 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/LdnMasterProxyClient.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/LdnMasterProxyClient.cs @@ -132,7 +132,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu protected override void OnConnected() { - Logger.Info?.PrintMsg(LogClass.ServiceLdn, $"LDN TCP client connected a new session with Id {Id}"); + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"LDN TCP client connected a new session with Id {Id}"); UpdatePassphraseIfNeeded(); @@ -141,7 +141,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu protected override void OnDisconnected() { - Logger.Info?.PrintMsg(LogClass.ServiceLdn, $"LDN TCP client disconnected a session with Id {Id}"); + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"LDN TCP client disconnected a session with Id {Id}"); _passphrase = null; @@ -174,7 +174,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu protected override void OnError(SocketError error) { - Logger.Info?.PrintMsg(LogClass.ServiceLdn, $"LDN TCP client caught an error with code {error}"); + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"LDN TCP client caught an error with code {error}"); _error.Set(); } @@ -428,7 +428,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu } else { - Logger.Info?.Print(LogClass.ServiceLdn, $"Created a wireless P2P network on port {request.ExternalProxyPort}."); + Logger.NetLog?.Print(LogClass.ServiceLdn, $"Created a wireless P2P network on port {request.ExternalProxyPort}."); _hostedProxy.Start(); (_, UnicastIPAddressInformation unicastAddress) = NetworkHelpers.GetLocalInterface(); diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Station.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Station.cs index fa43f789e..b589c56a4 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Station.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Station.cs @@ -1,3 +1,4 @@ +using Ryujinx.Common.Logging; using Ryujinx.Common.Memory; using Ryujinx.HLE.HOS.Services.Ldn.Types; using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types; @@ -36,10 +37,12 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator if (Connected) { _parent.SetState(NetworkState.StationConnected); + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn,$"NetworkChanged: {NetworkInfo}"); } else { _parent.SetDisconnectReason(e.DisconnectReasonOrDefault(DisconnectReason.DestroyedByUser)); + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn,"NetworkChanged: Disconnected (DestroyedByUser)"); } } else diff --git a/src/Ryujinx/Headless/HeadlessRyujinx.cs b/src/Ryujinx/Headless/HeadlessRyujinx.cs index bba505dbb..a6ff5da57 100644 --- a/src/Ryujinx/Headless/HeadlessRyujinx.cs +++ b/src/Ryujinx/Headless/HeadlessRyujinx.cs @@ -254,6 +254,7 @@ namespace Ryujinx.Headless Logger.SetEnable(LogLevel.Trace, option.LoggingEnableTrace); Logger.SetEnable(LogLevel.Guest, !option.LoggingDisableGuest); Logger.SetEnable(LogLevel.AccessLog, option.LoggingEnableFsAccessLog); + Logger.SetEnable(LogLevel.NetLog, option.LoggingEnableFsAccessLog); if (!option.DisableFileLog) { diff --git a/src/Ryujinx/Headless/Options.cs b/src/Ryujinx/Headless/Options.cs index 382294cf7..a2dd6e4d3 100644 --- a/src/Ryujinx/Headless/Options.cs +++ b/src/Ryujinx/Headless/Options.cs @@ -108,6 +108,9 @@ namespace Ryujinx.Headless if (NeedsOverride(nameof(LoggingEnableFsAccessLog))) LoggingEnableFsAccessLog = configurationState.Logger.EnableFsAccessLog; + + if (NeedsOverride(nameof(LoggingEnableNetLog))) + LoggingEnableNetLog = configurationState.Logger.EnableNetLog; if (NeedsOverride(nameof(LoggingGraphicsDebugLevel))) LoggingGraphicsDebugLevel = configurationState.Logger.GraphicsDebugLevel; @@ -370,6 +373,9 @@ namespace Ryujinx.Headless [Option("enable-fs-access-logs", Required = false, Default = false, HelpText = "Enables printing FS access log messages.")] public bool LoggingEnableFsAccessLog { get; set; } + + [Option("enable-net-logs", Required = false, Default = false, HelpText = "Enables printing net log messages.")] + public bool LoggingEnableNetLog { get; set; } [Option("graphics-debug-level", Required = false, Default = GraphicsDebugLevel.None, HelpText = "Change Graphics API debug log level.")] public GraphicsDebugLevel LoggingGraphicsDebugLevel { get; set; } diff --git a/src/Ryujinx/Systems/Configuration/ConfigurationFileFormat.cs b/src/Ryujinx/Systems/Configuration/ConfigurationFileFormat.cs index f0fafb4e0..0b451eacb 100644 --- a/src/Ryujinx/Systems/Configuration/ConfigurationFileFormat.cs +++ b/src/Ryujinx/Systems/Configuration/ConfigurationFileFormat.cs @@ -17,7 +17,7 @@ namespace Ryujinx.Ava.Systems.Configuration /// /// The current version of the file format /// - public const int CurrentVersion = 71; + public const int CurrentVersion = 72; /// /// Version of the configuration file format @@ -113,6 +113,11 @@ namespace Ryujinx.Ava.Systems.Configuration /// Enables printing FS access log messages /// public bool LoggingEnableFsAccessLog { get; set; } + + /// + /// Enables printing network log messages + /// + public bool LoggingEnableNetLog { get; set; } /// /// Enables log messages from Avalonia diff --git a/src/Ryujinx/Systems/Configuration/ConfigurationState.Migration.cs b/src/Ryujinx/Systems/Configuration/ConfigurationState.Migration.cs index 163b7e98f..90a045a67 100644 --- a/src/Ryujinx/Systems/Configuration/ConfigurationState.Migration.cs +++ b/src/Ryujinx/Systems/Configuration/ConfigurationState.Migration.cs @@ -1,4 +1,4 @@ -using Avalonia.Media; + using Avalonia.Media; using Gommon; using Ryujinx.Ava.Systems.Configuration.System; using Ryujinx.Ava.Systems.Configuration.UI; @@ -68,6 +68,7 @@ namespace Ryujinx.Ava.Systems.Configuration Logger.EnableTrace.Value = cff.LoggingEnableTrace; Logger.EnableGuest.Value = cff.LoggingEnableGuest; Logger.EnableFsAccessLog.Value = cff.LoggingEnableFsAccessLog; + Logger.EnableNetLog.Value = cff.LoggingEnableNetLog; Logger.FilteredClasses.Value = cff.LoggingFilteredClasses; Logger.GraphicsDebugLevel.Value = cff.LoggingGraphicsDebugLevel; diff --git a/src/Ryujinx/Systems/Configuration/ConfigurationState.Model.cs b/src/Ryujinx/Systems/Configuration/ConfigurationState.Model.cs index 2b4c8f991..7775125d4 100644 --- a/src/Ryujinx/Systems/Configuration/ConfigurationState.Model.cs +++ b/src/Ryujinx/Systems/Configuration/ConfigurationState.Model.cs @@ -257,6 +257,11 @@ namespace Ryujinx.Ava.Systems.Configuration /// Enables printing FS access log messages /// public ReactiveObject EnableFsAccessLog { get; private set; } + + /// + /// Enables printing network log messages + /// + public ReactiveObject EnableNetLog { get; private set; } /// /// Enables log messages from Avalonia @@ -289,6 +294,7 @@ namespace Ryujinx.Ava.Systems.Configuration EnableTrace = new ReactiveObject(); EnableGuest = new ReactiveObject(); EnableFsAccessLog = new ReactiveObject(); + EnableNetLog = new ReactiveObject(); EnableAvaloniaLog = new ReactiveObject(); FilteredClasses = new ReactiveObject(); EnableFileLog = new ReactiveObject(); diff --git a/src/Ryujinx/Systems/Configuration/ConfigurationState.cs b/src/Ryujinx/Systems/Configuration/ConfigurationState.cs index 0e2f6aaec..e4874963d 100644 --- a/src/Ryujinx/Systems/Configuration/ConfigurationState.cs +++ b/src/Ryujinx/Systems/Configuration/ConfigurationState.cs @@ -47,6 +47,7 @@ namespace Ryujinx.Ava.Systems.Configuration LoggingEnableTrace = Logger.EnableTrace, LoggingEnableGuest = Logger.EnableGuest, LoggingEnableFsAccessLog = Logger.EnableFsAccessLog, + LoggingEnableNetLog = Logger.EnableNetLog, LoggingEnableAvalonia = Logger.EnableAvaloniaLog, LoggingFilteredClasses = Logger.FilteredClasses, LoggingGraphicsDebugLevel = Logger.GraphicsDebugLevel, @@ -176,6 +177,7 @@ namespace Ryujinx.Ava.Systems.Configuration Logger.EnableTrace.Value = false; Logger.EnableGuest.Value = true; Logger.EnableFsAccessLog.Value = false; + Logger.EnableNetLog.Value = false; Logger.EnableAvaloniaLog.Value = false; Logger.FilteredClasses.Value = []; Logger.GraphicsDebugLevel.Value = GraphicsDebugLevel.None; diff --git a/src/Ryujinx/Systems/Configuration/LoggerModule.cs b/src/Ryujinx/Systems/Configuration/LoggerModule.cs index e3d08ab8c..29c38b3d2 100644 --- a/src/Ryujinx/Systems/Configuration/LoggerModule.cs +++ b/src/Ryujinx/Systems/Configuration/LoggerModule.cs @@ -26,6 +26,8 @@ namespace Ryujinx.Ava.Systems.Configuration (_, e) => Logger.SetEnable(LogLevel.Guest, e.NewValue); ConfigurationState.Instance.Logger.EnableFsAccessLog.Event += (_, e) => Logger.SetEnable(LogLevel.AccessLog, e.NewValue); + ConfigurationState.Instance.Logger.EnableNetLog.Event += + (_, e) => Logger.SetEnable(LogLevel.NetLog, e.NewValue); ConfigurationState.Instance.Logger.FilteredClasses.Event += (_, e) => { diff --git a/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs b/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs index abb284960..6904d4ebc 100644 --- a/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs @@ -273,6 +273,7 @@ namespace Ryujinx.Ava.UI.ViewModels public bool EnableTrace { get; set; } public bool EnableGuest { get; set; } public bool EnableFsAccessLog { get; set; } + public bool EnableNetLog { get; set; } public bool EnableAvaloniaLog { get; set; } public bool EnableDebug { get; set; } public bool IsOpenAlEnabled { get; set; } @@ -725,6 +726,7 @@ namespace Ryujinx.Ava.UI.ViewModels EnableGuest = config.Logger.EnableGuest; EnableDebug = config.Logger.EnableDebug; EnableFsAccessLog = config.Logger.EnableFsAccessLog; + EnableNetLog = config.Logger.EnableNetLog; EnableAvaloniaLog = config.Logger.EnableAvaloniaLog; FsGlobalAccessLogMode = config.System.FsGlobalAccessLogMode; OpenglDebugLevel = (int)config.Logger.GraphicsDebugLevel.Value; @@ -848,6 +850,7 @@ namespace Ryujinx.Ava.UI.ViewModels config.Logger.EnableGuest.Value = EnableGuest; config.Logger.EnableDebug.Value = EnableDebug; config.Logger.EnableFsAccessLog.Value = EnableFsAccessLog; + config.Logger.EnableNetLog.Value = EnableNetLog; config.Logger.EnableAvaloniaLog.Value = EnableAvaloniaLog; config.System.FsGlobalAccessLogMode.Value = FsGlobalAccessLogMode; config.Logger.GraphicsDebugLevel.Value = (GraphicsDebugLevel)OpenglDebugLevel; diff --git a/src/Ryujinx/UI/Views/Settings/SettingsLoggingView.axaml b/src/Ryujinx/UI/Views/Settings/SettingsLoggingView.axaml index fa3b4e866..325c4f599 100644 --- a/src/Ryujinx/UI/Views/Settings/SettingsLoggingView.axaml +++ b/src/Ryujinx/UI/Views/Settings/SettingsLoggingView.axaml @@ -70,6 +70,10 @@ ToolTip.Tip="{ext:Locale FileAccessLogTooltip}"> + + + From 8065dec74410d00efce1dbf8813260c7258c6f67 Mon Sep 17 00:00:00 2001 From: Max Date: Sun, 10 May 2026 23:31:45 +0000 Subject: [PATCH 08/11] [HLE] Renamed INotificationServicesForSystem and implemented a few commands and stubs (#6) Should allow Ring Fit and other games that rely heavily on the notification system to get in-game (?), needs testing. if you're wondering what happened to the first branch -- no you're not. (duplicated history somehow??) Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/6 --- src/Ryujinx.Common/Logging/LogClass.cs | 1 + .../Notification/INotificationServices.cs | 24 ++++++++++++++ .../INotificationServicesForApplication.cs | 25 +++++++++++++++ .../INotificationServicesForSystem.cs | 8 ----- .../INotificationSystemEventAccessor.cs | 32 +++++++++++++++++++ 5 files changed, 82 insertions(+), 8 deletions(-) create mode 100644 src/Ryujinx.HLE/HOS/Services/Notification/INotificationServices.cs delete mode 100644 src/Ryujinx.HLE/HOS/Services/Notification/INotificationServicesForSystem.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Notification/INotificationSystemEventAccessor.cs diff --git a/src/Ryujinx.Common/Logging/LogClass.cs b/src/Ryujinx.Common/Logging/LogClass.cs index 7c6810599..0e02cfcc2 100644 --- a/src/Ryujinx.Common/Logging/LogClass.cs +++ b/src/Ryujinx.Common/Logging/LogClass.cs @@ -51,6 +51,7 @@ namespace Ryujinx.Common.Logging ServiceNgct, ServiceNifm, ServiceNim, + ServiceNotification, ServiceNs, ServiceNsd, ServiceNtc, diff --git a/src/Ryujinx.HLE/HOS/Services/Notification/INotificationServices.cs b/src/Ryujinx.HLE/HOS/Services/Notification/INotificationServices.cs new file mode 100644 index 000000000..e44983fce --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Notification/INotificationServices.cs @@ -0,0 +1,24 @@ +namespace Ryujinx.HLE.HOS.Services.Notification +{ + [Service("notif:s")] // 9.0.0+ + class INotificationServices : IpcService + { + public INotificationServices(ServiceCtx context) { } + + [CommandCmif(1000)] // 9.0.0+ + // GetNotificationCount() -> nn::notification::server::INotificationSystemEventAccessor + public ResultCode GetNotificationCount(ServiceCtx context) + { + MakeObject(context, new INotificationSystemEventAccessor(context)); + return ResultCode.Success; + } + + [CommandCmif(1040)] // 9.0.0+ + // GetNotificationSendingNotifier() -> nn::notification::server::INotificationSystemEventAccessor + public ResultCode GetNotificationSendingNotifier(ServiceCtx context) + { + MakeObject(context, new INotificationSystemEventAccessor(context)); + return ResultCode.Success; + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Notification/INotificationServicesForApplication.cs b/src/Ryujinx.HLE/HOS/Services/Notification/INotificationServicesForApplication.cs index 29f8bfa85..498fc52fb 100644 --- a/src/Ryujinx.HLE/HOS/Services/Notification/INotificationServicesForApplication.cs +++ b/src/Ryujinx.HLE/HOS/Services/Notification/INotificationServicesForApplication.cs @@ -1,8 +1,33 @@ +using Ryujinx.Common.Logging; + namespace Ryujinx.HLE.HOS.Services.Notification { [Service("notif:a")] // 9.0.0+ class INotificationServicesForApplication : IpcService { public INotificationServicesForApplication(ServiceCtx context) { } + + // Leaving this here since I can never find it: https://switchbrew.org/wiki/Glue_services + + [CommandCmif(520)] // 9.0.0+ + // ListAlarmSettings(nn::arp::ApplicationCertificate) -> s32 AlarmSettingsCount + public ResultCode ListAlarmSettings(ServiceCtx context) + { + // TO-DO: Currently just returns 0. Should read in an ApplicationCertificate. + int alarmSettingsCount = 0; + context.ResponseData.Write(alarmSettingsCount); + return ResultCode.Success; + } + + [CommandCmif(1000)] // 9.0.0+ + // Initialize(PID-descriptor, u64 pid_reserved) + public ResultCode Intialize(ServiceCtx context) + { + ulong pid = context.Request.HandleDesc.PId; + context.RequestData.ReadUInt64(); // pid placeholder, zero + + Logger.Stub?.PrintStub(LogClass.ServiceNotification, new { pid }); + return ResultCode.Success; + } } } diff --git a/src/Ryujinx.HLE/HOS/Services/Notification/INotificationServicesForSystem.cs b/src/Ryujinx.HLE/HOS/Services/Notification/INotificationServicesForSystem.cs deleted file mode 100644 index c5946be84..000000000 --- a/src/Ryujinx.HLE/HOS/Services/Notification/INotificationServicesForSystem.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Ryujinx.HLE.HOS.Services.Notification -{ - [Service("notif:s")] // 9.0.0+ - class INotificationServicesForSystem : IpcService - { - public INotificationServicesForSystem(ServiceCtx context) { } - } -} diff --git a/src/Ryujinx.HLE/HOS/Services/Notification/INotificationSystemEventAccessor.cs b/src/Ryujinx.HLE/HOS/Services/Notification/INotificationSystemEventAccessor.cs new file mode 100644 index 000000000..8cca7cc6e --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Notification/INotificationSystemEventAccessor.cs @@ -0,0 +1,32 @@ +using Ryujinx.Common.Logging; +using Ryujinx.HLE.HOS.Ipc; +using Ryujinx.HLE.HOS.Kernel.Threading; +using Ryujinx.Horizon.Common; +using System; + +namespace Ryujinx.HLE.HOS.Services.Notification +{ + class INotificationSystemEventAccessor : IpcService + { + + private readonly KEvent _getNotificationSendingNotifierEvent; + private int _getNotificationSendingNotifierEventHandle; + public INotificationSystemEventAccessor(ServiceCtx context) { } + + [CommandCmif(0)] // 9.0.0+ + // GetNotificationSendingNotifier() -> nn::notification::server::INotificationSystemEventAccessor + public ResultCode GetSystemEvent(ServiceCtx context) + { + if (_getNotificationSendingNotifierEventHandle == 0) + { + if (context.Process.HandleTable.GenerateHandle(_getNotificationSendingNotifierEvent.ReadableEvent, out _getNotificationSendingNotifierEventHandle) != Result.Success) + { + throw new InvalidOperationException("Out of handles!"); + } + } + + context.Response.HandleDesc = IpcHandleDesc.MakeCopy(_getNotificationSendingNotifierEventHandle); + return ResultCode.Success; + } + } +} From e8cc252d9a79ec7cb1950d9f6e13cd5091d2bded Mon Sep 17 00:00:00 2001 From: yell0wsuit Date: Sun, 10 May 2026 23:39:19 +0000 Subject: [PATCH 09/11] [HLE] Fix StoreData layout and implement IDatabaseService.Append (#43) - Fixes `StoreData` layout/update handling so `UpdateLatest` returns the stored Mii data correctly. - Implements `IDatabaseService.Append` (https://switchbrew.org/wiki/Shared_Database_services#IDatabaseService) Also adds regression tests for `UpdateLatest` and `Append`. (Might) fix Mario Kart 8 Deluxe crashing on first boot due to failed Mii verification check (due to custom Mii from emulator), and potentially Tomodachi Life: Living the Dream for the "Import Mii from system" option. Co-authored-by: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com> Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/43 --- .../HOS/Services/Mii/DatabaseImpl.cs | 18 ++- .../HOS/Services/Mii/MiiDatabaseManager.cs | 26 ++++ .../Mii/StaticService/DatabaseServiceImpl.cs | 15 ++- .../Mii/StaticService/IDatabaseService.cs | 11 ++ src/Ryujinx.Tests/HLE/MiiDatabaseTests.cs | 122 ++++++++++++++++++ 5 files changed, 184 insertions(+), 8 deletions(-) create mode 100644 src/Ryujinx.Tests/HLE/MiiDatabaseTests.cs diff --git a/src/Ryujinx.HLE/HOS/Services/Mii/DatabaseImpl.cs b/src/Ryujinx.HLE/HOS/Services/Mii/DatabaseImpl.cs index 4bb736992..cd2115132 100644 --- a/src/Ryujinx.HLE/HOS/Services/Mii/DatabaseImpl.cs +++ b/src/Ryujinx.HLE/HOS/Services/Mii/DatabaseImpl.cs @@ -81,8 +81,10 @@ namespace Ryujinx.HLE.HOS.Services.Mii return ResultCode.Success; } - public ResultCode UpdateLatest(DatabaseSessionMetadata metadata, IStoredData oldMiiData, SourceFlag flag, IStoredData newMiiData) where T : unmanaged + public ResultCode UpdateLatest(DatabaseSessionMetadata metadata, T oldMiiData, SourceFlag flag, out T newMiiData) where T : unmanaged, IStoredData { + newMiiData = default; + if (!flag.HasFlag(SourceFlag.Database)) { return ResultCode.NotFound; @@ -106,7 +108,7 @@ namespace Ryujinx.HLE.HOS.Services.Mii newMiiData.SetFromStoreData(storeData); - if (oldMiiData == newMiiData) + if (oldMiiData.Equals(newMiiData)) { return ResultCode.NotUpdated; } @@ -286,6 +288,18 @@ namespace Ryujinx.HLE.HOS.Services.Mii return result; } + public ResultCode Append(DatabaseSessionMetadata metadata, CharInfo charInfo) + { + ResultCode result = _miiDatabase.Append(metadata, _utilityImpl, charInfo); + + if (result == ResultCode.Success) + { + result = _miiDatabase.SaveDatabase(); + } + + return result; + } + public ResultCode ConvertCharInfoToCoreData(CharInfo charInfo, out CoreData coreData) { coreData = new CoreData(); diff --git a/src/Ryujinx.HLE/HOS/Services/Mii/MiiDatabaseManager.cs b/src/Ryujinx.HLE/HOS/Services/Mii/MiiDatabaseManager.cs index 356d42a85..f5a173d8e 100644 --- a/src/Ryujinx.HLE/HOS/Services/Mii/MiiDatabaseManager.cs +++ b/src/Ryujinx.HLE/HOS/Services/Mii/MiiDatabaseManager.cs @@ -449,6 +449,32 @@ namespace Ryujinx.HLE.HOS.Services.Mii return ResultCode.Success; } + public ResultCode Append(DatabaseSessionMetadata metadata, UtilityImpl utilityImpl, CharInfo charInfo) + { + if (!charInfo.IsValid()) + { + return ResultCode.InvalidCharInfo; + } + + if (charInfo.Type == 1) + { + return ResultCode.InvalidOperationOnSpecialMii; + } + + CoreData coreData = new(); + coreData.SetFromCharInfo(charInfo); + + StoreData storeData; + + do + { + storeData = StoreData.BuildFromCoreData(utilityImpl, coreData); + } + while (_database.GetIndexByCreatorId(out _, storeData.CreateId)); + + return AddOrReplace(metadata, storeData); + } + public ResultCode Delete(DatabaseSessionMetadata metadata, CreateId createId) { if (!_database.GetIndexByCreatorId(out int index, createId)) diff --git a/src/Ryujinx.HLE/HOS/Services/Mii/StaticService/DatabaseServiceImpl.cs b/src/Ryujinx.HLE/HOS/Services/Mii/StaticService/DatabaseServiceImpl.cs index fc12e2533..c12517430 100644 --- a/src/Ryujinx.HLE/HOS/Services/Mii/StaticService/DatabaseServiceImpl.cs +++ b/src/Ryujinx.HLE/HOS/Services/Mii/StaticService/DatabaseServiceImpl.cs @@ -54,9 +54,7 @@ namespace Ryujinx.HLE.HOS.Services.Mii.StaticService protected override ResultCode UpdateLatest(CharInfo oldCharInfo, SourceFlag flag, out CharInfo newCharInfo) { - newCharInfo = default; - - return _database.UpdateLatest(_metadata, oldCharInfo, flag, newCharInfo); + return _database.UpdateLatest(_metadata, oldCharInfo, flag, out newCharInfo); } protected override ResultCode BuildRandom(Age age, Gender gender, Race race, out CharInfo charInfo) @@ -113,14 +111,14 @@ namespace Ryujinx.HLE.HOS.Services.Mii.StaticService protected override ResultCode UpdateLatest1(StoreData oldStoreData, SourceFlag flag, out StoreData newStoreData) { - newStoreData = default; - if (!_isSystem) { + newStoreData = default; + return ResultCode.PermissionDenied; } - return _database.UpdateLatest(_metadata, oldStoreData, flag, newStoreData); + return _database.UpdateLatest(_metadata, oldStoreData, flag, out newStoreData); } protected override ResultCode FindIndex(CreateId createId, bool isSpecial, out int index) @@ -262,5 +260,10 @@ namespace Ryujinx.HLE.HOS.Services.Mii.StaticService { return _database.ConvertCharInfoToCoreData(charInfo, out coreData); } + + protected override ResultCode Append(CharInfo charInfo) + { + return _database.Append(_metadata, charInfo); + } } } diff --git a/src/Ryujinx.HLE/HOS/Services/Mii/StaticService/IDatabaseService.cs b/src/Ryujinx.HLE/HOS/Services/Mii/StaticService/IDatabaseService.cs index 1a1c20d6e..3f9fad4fb 100644 --- a/src/Ryujinx.HLE/HOS/Services/Mii/StaticService/IDatabaseService.cs +++ b/src/Ryujinx.HLE/HOS/Services/Mii/StaticService/IDatabaseService.cs @@ -340,6 +340,15 @@ namespace Ryujinx.HLE.HOS.Services.Mii.StaticService return result; } + [CommandCmif(26)] // 10.2.0+ + // Append(nn::mii::CharInfo char_info) + public ResultCode Append(ServiceCtx context) + { + CharInfo charInfo = context.RequestData.ReadStruct(); + + return Append(charInfo); + } + private Span CreateByteSpanFromBuffer(ServiceCtx context, IpcBuffDesc ipcBuff, bool isOutput) { byte[] rawData; @@ -421,5 +430,7 @@ namespace Ryujinx.HLE.HOS.Services.Mii.StaticService protected abstract ResultCode ConvertCoreDataToCharInfo(CoreData coreData, out CharInfo charInfo); protected abstract ResultCode ConvertCharInfoToCoreData(CharInfo charInfo, out CoreData coreData); + + protected abstract ResultCode Append(CharInfo charInfo); } } diff --git a/src/Ryujinx.Tests/HLE/MiiDatabaseTests.cs b/src/Ryujinx.Tests/HLE/MiiDatabaseTests.cs new file mode 100644 index 000000000..b1a20bb93 --- /dev/null +++ b/src/Ryujinx.Tests/HLE/MiiDatabaseTests.cs @@ -0,0 +1,122 @@ +using System.Reflection; + +using NUnit.Framework; + +using Ryujinx.Cpu; +using Ryujinx.HLE.HOS.Services.Mii; +using Ryujinx.HLE.HOS.Services.Mii.StaticService; +using Ryujinx.HLE.HOS.Services.Mii.Types; + +namespace Ryujinx.Tests.HLE +{ + public class MiiDatabaseTests + { + [Test] + public void UpdateLatestReturnsStoredCharInfo() + { + DatabaseImpl database = new(); + StoreData storedData = StoreData.BuildDefault(new UtilityImpl(new TickSource(19200000)), 0); + MiiDatabaseManager databaseManager = GetDatabaseManager(database); + + NintendoFigurineDatabase figurineDatabase = new(); + figurineDatabase.Format(); + figurineDatabase.Add(storedData); + SetFigurineDatabase(databaseManager, figurineDatabase); + + TestDatabaseService service = new(database); + + CharInfo oldCharInfo = new(); + oldCharInfo.SetFromStoreData(storedData); + oldCharInfo.Height--; + + ResultCode result = service.UpdateLatestForTest(oldCharInfo, SourceFlag.Database, out CharInfo newCharInfo); + + Assert.Multiple(() => + { + Assert.That(result, Is.EqualTo(ResultCode.Success)); + Assert.That(newCharInfo.CreateId, Is.EqualTo(oldCharInfo.CreateId)); + Assert.That(newCharInfo.Height, Is.EqualTo(storedData.CoreData.Height)); + Assert.That(newCharInfo.IsValid(), Is.True); + }); + + } + + [Test] + public void AppendAddsRegularCharInfoToDatabase() + { + DatabaseImpl database = new(); + UtilityImpl utilityImpl = new(new TickSource(19200000)); + SetUtilityImpl(database, utilityImpl); + MiiDatabaseManager databaseManager = GetDatabaseManager(database); + SetFigurineDatabase(databaseManager, CreateFormattedDatabase()); + + StoreData defaultStoreData = StoreData.BuildDefault(utilityImpl, 0); + Assert.Multiple(() => + { + Assert.That(defaultStoreData.CoreData.IsValid(), Is.True); + Assert.That(defaultStoreData.IsValidDataCrc(), Is.True); + Assert.That(defaultStoreData.IsValidDeviceCrc(), Is.True); + Assert.That(defaultStoreData.IsValid(), Is.True); + }); + + CharInfo charInfo = new(); + charInfo.SetFromStoreData(defaultStoreData); + + DatabaseSessionMetadata metadata = database.CreateSessionMetadata(new SpecialMiiKeyCode()); + + ResultCode result = databaseManager.Append(metadata, utilityImpl, charInfo); + + int count = databaseManager.GetCount(metadata); + databaseManager.Get(metadata, 0, out StoreData storedData); + + CoreData expectedCoreData = new(); + expectedCoreData.SetFromCharInfo(charInfo); + + Assert.Multiple(() => + { + Assert.That(result, Is.EqualTo(ResultCode.Success)); + Assert.That(count, Is.EqualTo(1)); + Assert.That(storedData.IsValid(), Is.True); + Assert.That(storedData.CreateId, Is.Not.EqualTo(charInfo.CreateId)); + Assert.That(storedData.CoreData, Is.EqualTo(expectedCoreData)); + }); + } + + private sealed class TestDatabaseService(DatabaseImpl database) : DatabaseServiceImpl(database, true, new SpecialMiiKeyCode()) + { + public ResultCode UpdateLatestForTest(CharInfo oldCharInfo, SourceFlag flag, out CharInfo newCharInfo) + { + return UpdateLatest(oldCharInfo, flag, out newCharInfo); + } + } + + private static MiiDatabaseManager GetDatabaseManager(DatabaseImpl database) + { + return (MiiDatabaseManager)typeof(DatabaseImpl) + .GetField("_miiDatabase", BindingFlags.Instance | BindingFlags.NonPublic) + .GetValue(database); + } + + private static void SetFigurineDatabase(MiiDatabaseManager databaseManager, NintendoFigurineDatabase figurineDatabase) + { + typeof(MiiDatabaseManager) + .GetField("_database", BindingFlags.Instance | BindingFlags.NonPublic) + .SetValue(databaseManager, figurineDatabase); + } + + private static NintendoFigurineDatabase CreateFormattedDatabase() + { + NintendoFigurineDatabase figurineDatabase = new(); + figurineDatabase.Format(); + + return figurineDatabase; + } + + private static void SetUtilityImpl(DatabaseImpl database, UtilityImpl utilityImpl) + { + typeof(DatabaseImpl) + .GetField("_utilityImpl", BindingFlags.Instance | BindingFlags.NonPublic) + .SetValue(database, utilityImpl); + } + } +} From bab160d650050efc3b7d703874b587f27fc0fd7b Mon Sep 17 00:00:00 2001 From: Babib3l Date: Sun, 10 May 2026 23:47:39 +0000 Subject: [PATCH 10/11] Fix Windows fullscreen gap when toggling from maximized (#80) This PR aims to Fix the Windows fullscreen gap when toggling from maximized state by using canonical Win32 fullscreen approach. When entering fullscreen, the window style is saved and replaced with WS_POPUP | WS_VISIBLE to remove all window chrome (title bar, borders), then SetWindowPos sizes the window to cover the full screen with SWP_FRAMECHANGED to force Win32 to recalculate the frame. On exit, the original window style is restored. This eliminates the few-pixel gap at the top that occurred because Avalonia's WindowState = FullScreen transition from a maximized state retained the title bar non-client area, leaving visible space at the top of the screen (Fullscreen resolution would be 1920 x 1072p on a 1080p monitor, it now correctly renders at 1920 x 1080p) Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/80 --- src/Ryujinx/UI/Helpers/Win32NativeInterop.cs | 26 +++++++++ .../UI/ViewModels/MainWindowViewModel.cs | 58 ++++++++++++++++++- 2 files changed, 82 insertions(+), 2 deletions(-) diff --git a/src/Ryujinx/UI/Helpers/Win32NativeInterop.cs b/src/Ryujinx/UI/Helpers/Win32NativeInterop.cs index 0ff7f5fde..adfa899ed 100644 --- a/src/Ryujinx/UI/Helpers/Win32NativeInterop.cs +++ b/src/Ryujinx/UI/Helpers/Win32NativeInterop.cs @@ -8,6 +8,12 @@ namespace Ryujinx.Ava.UI.Helpers internal partial class Win32NativeInterop { internal const int GWLP_WNDPROC = -4; + internal const int GWL_STYLE = -16; + internal const int GWL_EXSTYLE = -20; + + internal const uint WS_OVERLAPPEDWINDOW = 0x00CF0000; + internal const uint WS_POPUP = 0x80000000; + internal const uint WS_VISIBLE = 0x10000000; [Flags] public enum ClassStyles : uint @@ -107,9 +113,29 @@ namespace Ryujinx.Ava.UI.Helpers nint hInstance, nint lpParam); + [LibraryImport("user32.dll", SetLastError = true, EntryPoint = "GetWindowLongPtrW")] + public static partial nint GetWindowLongPtrW(nint hWnd, int nIndex); + [LibraryImport("user32.dll", SetLastError = true)] public static partial nint SetWindowLongPtrW(nint hWnd, int nIndex, nint value); + [LibraryImport("user32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + public static partial bool SetWindowPos( + nint hWnd, + nint hWndInsertAfter, + int x, + int y, + int cx, + int cy, + uint uFlags); + + internal const uint SWP_NOZORDER = 0x0004; + internal const uint SWP_NOACTIVATE = 0x0010; + internal const uint SWP_FRAMECHANGED = 0x0020; + internal const uint SWP_NOMOVE = 0x0002; + internal const uint SWP_NOSIZE = 0x0001; + [LibraryImport("user32.dll", SetLastError = true)] public static partial ushort GetAsyncKeyState(int nVirtKey); diff --git a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs index 57fd825b3..e488495d6 100644 --- a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs @@ -5,6 +5,7 @@ using Avalonia.Input; using Avalonia.Media; using Avalonia.Platform.Storage; using Avalonia.Threading; +using System.Runtime.Versioning; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using DynamicData; @@ -2014,7 +2015,7 @@ namespace Ryujinx.Ava.UI.ViewModels LastFullscreenToggle = Environment.TickCount64; - if (WindowState is not WindowState.Normal) + if (WindowState is WindowState.FullScreen) { WindowState = WindowState.Normal; Window.TitleBar.ExtendsContentIntoTitleBar = !ConfigurationState.Instance.ShowOldUI; @@ -2023,21 +2024,74 @@ namespace Ryujinx.Ava.UI.ViewModels { ShowMenuAndStatusBar = true; } + + if (OperatingSystem.IsWindows()) + { + RestoreWindowFromFullscreen(); + } } else { - WindowState = WindowState.FullScreen; Window.TitleBar.ExtendsContentIntoTitleBar = true; if (IsGameRunning) { ShowMenuAndStatusBar = false; } + + if (OperatingSystem.IsWindows()) + { + MakeWindowFullscreen(); + } + else + { + WindowState = WindowState.FullScreen; + } } IsFullScreen = WindowState is WindowState.FullScreen; } + private nint _savedWindowStyle; + + [SupportedOSPlatform("windows")] + private void MakeWindowFullscreen() + { + nint hwnd = Window.TryGetPlatformHandle()?.Handle ?? nint.Zero; + if (hwnd == nint.Zero) return; + + // Save current style and placement + _savedWindowStyle = Win32NativeInterop.GetWindowLongPtrW(hwnd, Win32NativeInterop.GWL_STYLE); + + // Remove window chrome: WS_OVERLAPPEDWINDOW -> WS_POPUP | WS_VISIBLE + Win32NativeInterop.SetWindowLongPtrW(hwnd, Win32NativeInterop.GWL_STYLE, + unchecked((nint)(Win32NativeInterop.WS_POPUP | Win32NativeInterop.WS_VISIBLE))); + + Avalonia.Platform.Screen? screen = Window.Screens.ScreenFromVisual(Window); + int w = screen?.Bounds.Width ?? 0; + int h = screen?.Bounds.Height ?? 0; + + Win32NativeInterop.SetWindowPos(hwnd, nint.Zero, 0, 0, w, h, + Win32NativeInterop.SWP_NOZORDER | Win32NativeInterop.SWP_NOACTIVATE | Win32NativeInterop.SWP_FRAMECHANGED); + + WindowState = WindowState.FullScreen; + } + + [SupportedOSPlatform("windows")] + private void RestoreWindowFromFullscreen() + { + nint hwnd = Window.TryGetPlatformHandle()?.Handle ?? nint.Zero; + if (hwnd == nint.Zero) return; + + // Restore original window style + Win32NativeInterop.SetWindowLongPtrW(hwnd, Win32NativeInterop.GWL_STYLE, _savedWindowStyle); + + Win32NativeInterop.SetWindowPos(hwnd, nint.Zero, 0, 0, 0, 0, + Win32NativeInterop.SWP_NOZORDER | Win32NativeInterop.SWP_NOACTIVATE | + Win32NativeInterop.SWP_FRAMECHANGED | Win32NativeInterop.SWP_NOMOVE | Win32NativeInterop.SWP_NOSIZE); + + } + public static void SaveConfig() { ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.ConfigurationPath); From f8167eb6252438c63f82b2bdb04f1215dc47071b Mon Sep 17 00:00:00 2001 From: yell0wsuit Date: Sun, 10 May 2026 23:57:05 +0000 Subject: [PATCH 11/11] [HLE] Match hardware screenshot buffer size behavior for captures (#44) ## Description ~~Fixes a fatal CLR crash when `caps` screenshot saving receives an input buffer larger than `0x384000`.~~ ~~Resolves a crash in Tomodachi Life: Living the Dream where saving the pictures to the system's album crashes with 0x80131506.~~ Follow up to #18. This PR adjusts the validation and copy behavior to better match real hardware, and adds logging to make invalid screenshot buffer cases easier to diagnose. Real hardware accepts screenshot buffers with a size greater than or equal to `0x384000`, but only `0x384000` bytes are needed for the 1280x720 RGBA image. This changes screenshot saving to: - reject buffers smaller than `0x384000` - accept buffers equal to or larger than `0x384000` - copy only the first `0x384000` bytes into the 1280x720 bitmap ## Testing Tested with a real Switch NRO using `capssuSaveScreenShotEx0`, `capssuSaveScreenShotEx1`, and `capssuSaveScreenShotEx2`. Observed hardware behavior: ```text 0x384000 => OK 0x384000 - 1 => NullInputBuffer 0x384000 + 1 => OK 0x3C0000 => OK // Tomo life picture size ``` Co-authored-by: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com> Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/44 --- .../HOS/Services/Caps/CaptureManager.cs | 158 ++++++++------- .../Caps/IScreenShotApplicationService.cs | 54 ++++- src/Ryujinx.Tests/HLE/CaptureManagerTests.cs | 187 ++++++++++++++++++ 3 files changed, 326 insertions(+), 73 deletions(-) create mode 100644 src/Ryujinx.Tests/HLE/CaptureManagerTests.cs diff --git a/src/Ryujinx.HLE/HOS/Services/Caps/CaptureManager.cs b/src/Ryujinx.HLE/HOS/Services/Caps/CaptureManager.cs index f47c663ed..651f92986 100644 --- a/src/Ryujinx.HLE/HOS/Services/Caps/CaptureManager.cs +++ b/src/Ryujinx.HLE/HOS/Services/Caps/CaptureManager.cs @@ -1,7 +1,9 @@ +using Ryujinx.Common.Logging; using Ryujinx.Common.Memory; using Ryujinx.HLE.HOS.Services.Caps.Types; using SkiaSharp; using System; +using System.Globalization; using System.IO; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -9,16 +11,20 @@ using System.Security.Cryptography; namespace Ryujinx.HLE.HOS.Services.Caps { - class CaptureManager + internal class CaptureManager { - private readonly string _sdCardPath; + public CaptureManager(Switch device) + { + _ = device; + } + private readonly string _sdCardPath = FileSystem.VirtualFileSystem.GetSdCardPath(); private uint _shimLibraryVersion; - public CaptureManager(Switch device) - { - _sdCardPath = FileSystem.VirtualFileSystem.GetSdCardPath(); - } + private const int ScreenshotWidth = 1280; + private const int ScreenshotHeight = 720; + private const int ScreenshotBytesPerPixel = 4; + private const int ScreenshotDataSize = ScreenshotWidth * ScreenshotHeight * ScreenshotBytesPerPixel; // 0x384000 public ResultCode SetShimLibraryVersion(ServiceCtx context) { @@ -53,84 +59,94 @@ namespace Ryujinx.HLE.HOS.Services.Caps return resultCode; } - public ResultCode SaveScreenShot(byte[] screenshotData, ulong appletResourceUserId, ulong titleId, out ApplicationAlbumEntry applicationAlbumEntry) + public ResultCode SaveScreenShot( + byte[] screenshotData, + ulong appletResourceUserId, + ulong titleId, + out ApplicationAlbumEntry applicationAlbumEntry) { + Logger.Stub?.PrintStub(LogClass.ServiceCaps, new + { + appletResourceUserId, + titleId, + screenshotDataLength = screenshotData?.Length ?? 0, + }); + applicationAlbumEntry = default; - if (screenshotData.Length == 0) + if (screenshotData == null || screenshotData.Length == 0) { return ResultCode.NullInputBuffer; } - /* - // NOTE: On our current implementation, appletResourceUserId starts at 0, disable it for now. - if (appletResourceUserId == 0) + if (screenshotData.Length < ScreenshotDataSize) + { + Logger.Warning?.PrintMsg( + LogClass.ServiceCaps, + $"Invalid screenshot buffer size 0x{screenshotData.Length:X}; expected at least 0x{ScreenshotDataSize:X}."); + + return ResultCode.NullInputBuffer; + } + + DateTime currentDateTime = DateTime.Now; + + applicationAlbumEntry = new ApplicationAlbumEntry() + { + Size = (ulong)Unsafe.SizeOf(), + TitleId = titleId, + AlbumFileDateTime = new AlbumFileDateTime() + { + Year = (ushort)currentDateTime.Year, + Month = (byte)currentDateTime.Month, + Day = (byte)currentDateTime.Day, + Hour = (byte)currentDateTime.Hour, + Minute = (byte)currentDateTime.Minute, + Second = (byte)currentDateTime.Second, + UniqueId = 0, + }, + AlbumStorage = AlbumStorage.Sd, + ContentType = ContentType.Screenshot, + Padding = new Array5(), + Unknown0x1f = 1, + }; + + // NOTE: The hex hash is a HMAC-SHA256 (first 32 bytes) using a hardcoded secret key over the titleId, we can simulate it by hashing the titleId instead. + string hash = Convert.ToHexString(SHA256.HashData(BitConverter.GetBytes(titleId)))[..0x20]; + + string folderPath = Path.Combine( + _sdCardPath, + "Nintendo", + "Album", + currentDateTime.Year.ToString("0000", CultureInfo.InvariantCulture), + currentDateTime.Month.ToString("00", CultureInfo.InvariantCulture), + currentDateTime.Day.ToString("00", CultureInfo.InvariantCulture)); + + string filePath = GenerateFilePath(folderPath, applicationAlbumEntry, currentDateTime, hash); + + _ = Directory.CreateDirectory(folderPath); + + while (File.Exists(filePath)) + { + applicationAlbumEntry.AlbumFileDateTime.UniqueId++; + filePath = GenerateFilePath(folderPath, applicationAlbumEntry, currentDateTime, hash); + } + + using SKBitmap bitmap = new(new SKImageInfo(ScreenshotWidth, ScreenshotHeight, SKColorType.Rgba8888)); + + IntPtr pixels = bitmap.GetPixels(); + + if (pixels == IntPtr.Zero) { return ResultCode.InvalidArgument; } - */ - /* - // Doesn't occur in our case. - if (applicationAlbumEntry == null) - { - return ResultCode.NullOutputBuffer; - } - */ + Marshal.Copy(screenshotData, 0, pixels, ScreenshotDataSize); - if (screenshotData.Length >= 0x384000) - { - DateTime currentDateTime = DateTime.Now; + using SKData data = bitmap.Encode(SKEncodedImageFormat.Jpeg, 80); + using FileStream file = File.OpenWrite(filePath); + data.SaveTo(file); - applicationAlbumEntry = new ApplicationAlbumEntry() - { - Size = (ulong)Unsafe.SizeOf(), - TitleId = titleId, - AlbumFileDateTime = new AlbumFileDateTime() - { - Year = (ushort)currentDateTime.Year, - Month = (byte)currentDateTime.Month, - Day = (byte)currentDateTime.Day, - Hour = (byte)currentDateTime.Hour, - Minute = (byte)currentDateTime.Minute, - Second = (byte)currentDateTime.Second, - UniqueId = 0, - }, - AlbumStorage = AlbumStorage.Sd, - ContentType = ContentType.Screenshot, - Padding = new Array5(), - Unknown0x1f = 1, - }; - - // NOTE: The hex hash is a HMAC-SHA256 (first 32 bytes) using a hardcoded secret key over the titleId, we can simulate it by hashing the titleId instead. - string hash = Convert.ToHexString(SHA256.HashData(BitConverter.GetBytes(titleId)))[..0x20]; - string folderPath = Path.Combine(_sdCardPath, "Nintendo", "Album", currentDateTime.Year.ToString("00"), currentDateTime.Month.ToString("00"), currentDateTime.Day.ToString("00")); - string filePath = GenerateFilePath(folderPath, applicationAlbumEntry, currentDateTime, hash); - - // TODO: Handle that using the FS service implementation and return the right error code instead of throwing exceptions. - Directory.CreateDirectory(folderPath); - - while (File.Exists(filePath)) - { - applicationAlbumEntry.AlbumFileDateTime.UniqueId++; - - filePath = GenerateFilePath(folderPath, applicationAlbumEntry, currentDateTime, hash); - } - - // NOTE: The saved JPEG file doesn't have the limitation in the extra EXIF data. - using SKBitmap bitmap = new(new SKImageInfo(1280, 720, SKColorType.Rgba8888, SKAlphaType.Premul)); - int dataLen = screenshotData.Length > bitmap.ByteCount ? bitmap.ByteCount : screenshotData.Length; - - Marshal.Copy(screenshotData, 0, bitmap.GetPixels(), dataLen); - - using SKData data = bitmap.Encode(SKEncodedImageFormat.Jpeg, 80); - using FileStream file = File.OpenWrite(filePath); - data.SaveTo(file); - - return ResultCode.Success; - } - - return ResultCode.NullInputBuffer; + return ResultCode.Success; } private string GenerateFilePath(string folderPath, ApplicationAlbumEntry applicationAlbumEntry, DateTime currentDateTime, string hash) diff --git a/src/Ryujinx.HLE/HOS/Services/Caps/IScreenShotApplicationService.cs b/src/Ryujinx.HLE/HOS/Services/Caps/IScreenShotApplicationService.cs index 0723b57cc..2ccb7c598 100644 --- a/src/Ryujinx.HLE/HOS/Services/Caps/IScreenShotApplicationService.cs +++ b/src/Ryujinx.HLE/HOS/Services/Caps/IScreenShotApplicationService.cs @@ -1,13 +1,19 @@ using Ryujinx.Common; +using Ryujinx.Common.Logging; using Ryujinx.HLE.HOS.Services.Caps.Types; namespace Ryujinx.HLE.HOS.Services.Caps { [Service("caps:su")] // 6.0.0+ - class IScreenShotApplicationService : IpcService + internal class IScreenShotApplicationService : IpcService { - public IScreenShotApplicationService(ServiceCtx context) { } + private const ulong ScreenshotDataSize = 0x384000; + private const ulong ApplicationDataSize = 0x404; + public IScreenShotApplicationService(ServiceCtx context) + { + _ = context; + } [CommandCmif(32)] // 7.0.0+ // SetShimLibraryVersion(pid, u64, nn::applet::AppletResourceUserId) public ResultCode SetShimLibraryVersion(ServiceCtx context) @@ -33,6 +39,15 @@ namespace Ryujinx.HLE.HOS.Services.Caps ulong screenshotDataPosition = context.Request.SendBuff[0].Position; ulong screenshotDataSize = context.Request.SendBuff[0].Size; + if (screenshotDataSize < ScreenshotDataSize) + { + Logger.Warning?.PrintMsg( + LogClass.ServiceCaps, + $"Invalid screenshot buffer size 0x{screenshotDataSize:X}; expected at least 0x{ScreenshotDataSize:X}."); + + return ResultCode.NullInputBuffer; + } + byte[] screenshotData = context.Memory.GetSpan(screenshotDataPosition, (int)screenshotDataSize, true).ToArray(); ResultCode resultCode = context.Device.System.CaptureManager.SaveScreenShot(screenshotData, appletResourceUserId, context.Device.Processes.ActiveApplication.ProgramId, out ApplicationAlbumEntry applicationAlbumEntry); @@ -60,6 +75,24 @@ namespace Ryujinx.HLE.HOS.Services.Caps ulong screenshotDataPosition = context.Request.SendBuff[1].Position; ulong screenshotDataSize = context.Request.SendBuff[1].Size; + if (applicationDataSize != ApplicationDataSize) + { + Logger.Warning?.PrintMsg( + LogClass.ServiceCaps, + $"Invalid ApplicationData size 0x{applicationDataSize:X}; expected 0x{ApplicationDataSize:X}."); + + return ResultCode.InvalidArgument; + } + + if (screenshotDataSize < ScreenshotDataSize) + { + Logger.Warning?.PrintMsg( + LogClass.ServiceCaps, + $"Invalid screenshot buffer size 0x{screenshotDataSize:X}; expected at least 0x{ScreenshotDataSize:X}."); + + return ResultCode.NullInputBuffer; + } + // TODO: Parse the application data: At 0x00 it's UserData (Size of 0x400), at 0x404 it's a uint UserDataSize (Always empty for now). _ = context.Memory.GetSpan(applicationDataPosition, (int)applicationDataSize).ToArray(); @@ -88,6 +121,23 @@ namespace Ryujinx.HLE.HOS.Services.Caps ulong screenshotDataPosition = context.Request.SendBuff[1].Position; ulong screenshotDataSize = context.Request.SendBuff[1].Size; + if (userIdListSize != 0x88) + { + Logger.Warning?.PrintMsg( + LogClass.ServiceCaps, + $"Invalid UserIdList size 0x{userIdListSize:X}; expected 0x88."); + return ResultCode.InvalidArgument; + } + + if (screenshotDataSize < ScreenshotDataSize) + { + Logger.Warning?.PrintMsg( + LogClass.ServiceCaps, + $"Invalid screenshot buffer size 0x{screenshotDataSize:X}; expected at least 0x{ScreenshotDataSize:X}."); + + return ResultCode.NullInputBuffer; + } + // TODO: Parse the UserIdList. _ = context.Memory.GetSpan(userIdListPosition, (int)userIdListSize).ToArray(); diff --git a/src/Ryujinx.Tests/HLE/CaptureManagerTests.cs b/src/Ryujinx.Tests/HLE/CaptureManagerTests.cs new file mode 100644 index 000000000..8dd2b070f --- /dev/null +++ b/src/Ryujinx.Tests/HLE/CaptureManagerTests.cs @@ -0,0 +1,187 @@ +using NUnit.Framework; +using Ryujinx.HLE.HOS.Services.Caps; +using Ryujinx.HLE.HOS.Services.Caps.Types; +using SkiaSharp; +using System; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; + +namespace Ryujinx.Tests.HLE +{ + public class CaptureManagerTests + { + private const int ScreenshotWidth = 1280; + private const int ScreenshotHeight = 720; + private const int BytesPerPixel = 4; + + private const int ScreenshotDataSize = ScreenshotWidth * ScreenshotHeight * BytesPerPixel; // 0x384000 + private const int PaddedScreenshotDataSize = ScreenshotWidth * 768 * BytesPerPixel; // 0x3C0000 + + [Test] + public void SaveScreenShotRejectsBufferSmallerThan720p() + { + using TempSdCard tempSdCard = new(); + + CaptureManager captureManager = CreateCaptureManager(tempSdCard.Path); + byte[] screenshotData = new byte[ScreenshotDataSize - 1]; + + ResultCode result = captureManager.SaveScreenShot( + screenshotData, + appletResourceUserId: 0, + titleId: 0x0100000000001000, + out _); + + Assert.That(result, Is.EqualTo(ResultCode.NullInputBuffer)); + Assert.That(Directory.Exists(Path.Combine(tempSdCard.Path, "Nintendo", "Album")), Is.False); + } + + [Test] + public void SaveScreenShotAcceptsExact720pBuffer() + { + using TempSdCard tempSdCard = new(); + + CaptureManager captureManager = CreateCaptureManager(tempSdCard.Path); + byte[] screenshotData = CreateTestPattern(ScreenshotDataSize); + + ResultCode result = captureManager.SaveScreenShot( + screenshotData, + appletResourceUserId: 0, + titleId: 0x0100000000001000, + out ApplicationAlbumEntry applicationAlbumEntry); + + string filePath = GetSingleAlbumFile(tempSdCard.Path); + + using SKBitmap bitmap = SKBitmap.Decode(filePath); + + Assert.Multiple(() => + { + Assert.That(result, Is.EqualTo(ResultCode.Success)); + Assert.That(bitmap.Width, Is.EqualTo(ScreenshotWidth)); + Assert.That(bitmap.Height, Is.EqualTo(ScreenshotHeight)); + Assert.That(applicationAlbumEntry.TitleId, Is.EqualTo(0x0100000000001000)); + Assert.That(applicationAlbumEntry.AlbumStorage, Is.EqualTo(AlbumStorage.Sd)); + Assert.That(applicationAlbumEntry.ContentType, Is.EqualTo(ContentType.Screenshot)); + Assert.That(applicationAlbumEntry.Unknown0x1f, Is.EqualTo(1)); + }); + } + + [Test] + public void SaveScreenShotAcceptsBufferLargerThan720p() + { + using TempSdCard tempSdCard = new(); + + CaptureManager captureManager = CreateCaptureManager(tempSdCard.Path); + byte[] screenshotData = CreateTestPattern(PaddedScreenshotDataSize); + + ResultCode result = captureManager.SaveScreenShot( + screenshotData, + appletResourceUserId: 0, + titleId: 0x0100000000001000, + out ApplicationAlbumEntry applicationAlbumEntry); + + string filePath = GetSingleAlbumFile(tempSdCard.Path); + + using SKBitmap bitmap = SKBitmap.Decode(filePath); + + Assert.Multiple(() => + { + Assert.That(result, Is.EqualTo(ResultCode.Success)); + Assert.That(bitmap.Width, Is.EqualTo(ScreenshotWidth)); + Assert.That(bitmap.Height, Is.EqualTo(ScreenshotHeight)); + Assert.That(applicationAlbumEntry.TitleId, Is.EqualTo(0x0100000000001000)); + }); + } + + [Test] + public void SaveScreenShotCreatesUniqueFileNamesForRepeatedSaves() + { + using TempSdCard tempSdCard = new(); + + CaptureManager captureManager = CreateCaptureManager(tempSdCard.Path); + byte[] screenshotData = CreateTestPattern(ScreenshotDataSize); + + ResultCode firstResult = captureManager.SaveScreenShot( + screenshotData, + appletResourceUserId: 0, + titleId: 0x0100000000001000, + out _); + + ResultCode secondResult = captureManager.SaveScreenShot( + screenshotData, + appletResourceUserId: 0, + titleId: 0x0100000000001000, + out _); + + string[] files = Directory.GetFiles( + Path.Combine(tempSdCard.Path, "Nintendo", "Album"), + "*.jpg", + SearchOption.AllDirectories); + + Assert.Multiple(() => + { + Assert.That(firstResult, Is.EqualTo(ResultCode.Success)); + Assert.That(secondResult, Is.EqualTo(ResultCode.Success)); + Assert.That(files, Has.Length.EqualTo(2)); + }); + } + + private static CaptureManager CreateCaptureManager(string sdCardPath) + { + CaptureManager captureManager = (CaptureManager)RuntimeHelpers.GetUninitializedObject(typeof(CaptureManager)); + + typeof(CaptureManager) + .GetField("_sdCardPath", BindingFlags.Instance | BindingFlags.NonPublic) + .SetValue(captureManager, sdCardPath); + + return captureManager; + } + + private static string GetSingleAlbumFile(string sdCardPath) + { + string albumPath = Path.Combine(sdCardPath, "Nintendo", "Album"); + + string[] files = Directory.GetFiles(albumPath, "*.jpg", SearchOption.AllDirectories); + + Assert.That(files, Has.Length.EqualTo(1)); + + return files.Single(); + } + + private static byte[] CreateTestPattern(int size) + { + byte[] data = new byte[size]; + + int pixelCount = size / BytesPerPixel; + + for (int i = 0; i < pixelCount; i++) + { + int x = i % ScreenshotWidth; + int y = i / ScreenshotWidth; + + data[(i * 4) + 0] = (byte)(x & 0xff); + data[(i * 4) + 1] = (byte)(y & 0xff); + data[(i * 4) + 2] = 0x80; + data[(i * 4) + 3] = 0xff; + } + + return data; + } + + private sealed class TempSdCard : IDisposable + { + public string Path { get; } = System.IO.Path.Combine( + TestContext.CurrentContext.WorkDirectory, + "sdcard-" + Guid.NewGuid()); + + public void Dispose() + { + if (Directory.Exists(Path)) + { + Directory.Delete(Path, recursive: true); + } + } + } + } +}