diff --git a/.forgejo/workflows/build.yml b/.forgejo/workflows/build.yml index e7b961602..e7052a6f6 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: - '**' @@ -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,47 +201,4 @@ 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' - - 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}); - } + if: forgejo.event_name == 'pull_request' 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/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/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.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.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/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 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.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.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.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; + } + } +} 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) { 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); + } + } + } + } +} 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); + } + } +} 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/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/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; 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); 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}"> + + +