diff --git a/Ryujinx.HLE/HOS/Horizon.cs b/Ryujinx.HLE/HOS/Horizon.cs index 39764c5b3..daa8dcc3f 100644 --- a/Ryujinx.HLE/HOS/Horizon.cs +++ b/Ryujinx.HLE/HOS/Horizon.cs @@ -22,6 +22,7 @@ using Ryujinx.HLE.HOS.Services.Am.AppletAE.AllSystemAppletProxiesService.SystemA using Ryujinx.HLE.HOS.Services.Apm; using Ryujinx.HLE.HOS.Services.Arp; using Ryujinx.HLE.HOS.Services.Audio.AudioRenderer; +using Ryujinx.HLE.HOS.Services.Caps; using Ryujinx.HLE.HOS.Services.Mii; using Ryujinx.HLE.HOS.Services.Nfc.Nfp.UserManager; using Ryujinx.HLE.HOS.Services.Nv; @@ -86,6 +87,7 @@ namespace Ryujinx.HLE.HOS internal SharedFontManager Font { get; private set; } internal ContentManager ContentManager { get; private set; } + internal CaptureManager CaptureManager { get; private set; } internal KEvent VsyncEvent { get; private set; } @@ -160,6 +162,7 @@ namespace Ryujinx.HLE.HOS DisplayResolutionChangeEvent = new KEvent(KernelContext); ContentManager = contentManager; + CaptureManager = new CaptureManager(device); // TODO: use set:sys (and get external clock source id from settings) // TODO: use "time!standard_steady_clock_rtc_update_interval_minutes" and implement a worker thread to be accurate. diff --git a/Ryujinx.HLE/HOS/Services/Caps/CaptureManager.cs b/Ryujinx.HLE/HOS/Services/Caps/CaptureManager.cs new file mode 100644 index 000000000..37cc9bdab --- /dev/null +++ b/Ryujinx.HLE/HOS/Services/Caps/CaptureManager.cs @@ -0,0 +1,143 @@ +using Ryujinx.Common.Memory; +using Ryujinx.HLE.HOS.Services.Caps.Types; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats.Jpeg; +using SixLabors.ImageSharp.PixelFormats; +using System; +using System.IO; +using System.Runtime.CompilerServices; +using System.Security.Cryptography; + +namespace Ryujinx.HLE.HOS.Services.Caps +{ + class CaptureManager + { + private string _sdCardPath; + + private uint _shimLibraryVersion; + + public CaptureManager(Switch device) + { + _sdCardPath = device.FileSystem.GetSdCardPath(); + + SixLabors.ImageSharp.Configuration.Default.ImageFormatsManager.SetEncoder(JpegFormat.Instance, new JpegEncoder() + { + Quality = 100 + }); + } + + public ResultCode SetShimLibraryVersion(ServiceCtx context) + { + ulong shimLibraryVersion = context.RequestData.ReadUInt64(); + ulong appletResourceUserId = context.RequestData.ReadUInt64(); + + // TODO: Service checks if the pid is present in an internal list and returns ResultCode.BlacklistedPid if it is. + // The list contents needs to be determined. + + ResultCode resultCode = ResultCode.OutOfRange; + + if (shimLibraryVersion != 0) + { + if (_shimLibraryVersion == shimLibraryVersion) + { + resultCode = ResultCode.Success; + } + else if (_shimLibraryVersion != 0) + { + resultCode = ResultCode.ShimLibraryVersionAlreadySet; + } + else if (shimLibraryVersion == 1) + { + resultCode = ResultCode.Success; + + _shimLibraryVersion = 1; + } + } + + return resultCode; + } + + public ResultCode SaveScreenShot(byte[] screenshotData, ulong appletResourceUserId, ulong titleId, out ApplicationAlbumEntry applicationAlbumEntry) + { + applicationAlbumEntry = default; + + if (screenshotData.Length == 0) + { + return ResultCode.NullInputBuffer; + } + + /* + // NOTE: On our current implementation, appletResourceUserId starts at 0, disable it for now. + if (appletResourceUserId == 0) + { + return ResultCode.InvalidArgument; + } + */ + + /* + // Doesn't occur in our case. + if (applicationAlbumEntry == null) + { + return ResultCode.NullOutputBuffer; + } + */ + + if (screenshotData.Length >= 0x384000) + { + 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 + }; + + using (SHA256 sha256Hash = SHA256.Create()) + { + // 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 = BitConverter.ToString(sha256Hash.ComputeHash(BitConverter.GetBytes(titleId))).Replace("-", "").Remove(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. + Image.LoadPixelData(screenshotData, 1280, 720).SaveAsJpegAsync(filePath); + } + + return ResultCode.Success; + } + + return ResultCode.NullInputBuffer; + } + + private string GenerateFilePath(string folderPath, ApplicationAlbumEntry applicationAlbumEntry, DateTime currentDateTime, string hash) + { + string fileName = $"{currentDateTime:yyyyMMddHHmmss}{applicationAlbumEntry.AlbumFileDateTime.UniqueId:00}-{hash}.jpg"; + + return Path.Combine(folderPath, fileName); + } + } +} \ No newline at end of file diff --git a/Ryujinx.HLE/HOS/Services/Caps/IAlbumApplicationService.cs b/Ryujinx.HLE/HOS/Services/Caps/IAlbumApplicationService.cs index 9b699f602..88803ccc8 100644 --- a/Ryujinx.HLE/HOS/Services/Caps/IAlbumApplicationService.cs +++ b/Ryujinx.HLE/HOS/Services/Caps/IAlbumApplicationService.cs @@ -11,12 +11,7 @@ namespace Ryujinx.HLE.HOS.Services.Caps // SetShimLibraryVersion(pid, u64, nn::applet::AppletResourceUserId) public ResultCode SetShimLibraryVersion(ServiceCtx context) { - ulong shimLibraryVersion = context.RequestData.ReadUInt64(); - ulong appletResourceUserId = context.RequestData.ReadUInt64(); - - Logger.Stub?.PrintStub(LogClass.ServiceCaps, new { shimLibraryVersion, appletResourceUserId }); - - return ResultCode.Success; + return context.Device.System.CaptureManager.SetShimLibraryVersion(context); } } } \ No newline at end of file diff --git a/Ryujinx.HLE/HOS/Services/Caps/IAlbumControlService.cs b/Ryujinx.HLE/HOS/Services/Caps/IAlbumControlService.cs index de880153b..48cb9cab4 100644 --- a/Ryujinx.HLE/HOS/Services/Caps/IAlbumControlService.cs +++ b/Ryujinx.HLE/HOS/Services/Caps/IAlbumControlService.cs @@ -4,5 +4,12 @@ namespace Ryujinx.HLE.HOS.Services.Caps class IAlbumControlService : IpcService { public IAlbumControlService(ServiceCtx context) { } + + [Command(33)] // 7.0.0+ + // SetShimLibraryVersion(pid, u64, nn::applet::AppletResourceUserId) + public ResultCode SetShimLibraryVersion(ServiceCtx context) + { + return context.Device.System.CaptureManager.SetShimLibraryVersion(context); + } } } \ No newline at end of file diff --git a/Ryujinx.HLE/HOS/Services/Caps/IScreenShotApplicationService.cs b/Ryujinx.HLE/HOS/Services/Caps/IScreenShotApplicationService.cs index 36bdb9f56..3824e7a34 100644 --- a/Ryujinx.HLE/HOS/Services/Caps/IScreenShotApplicationService.cs +++ b/Ryujinx.HLE/HOS/Services/Caps/IScreenShotApplicationService.cs @@ -1,4 +1,5 @@ -using Ryujinx.Common.Logging; +using Ryujinx.Common; +using Ryujinx.HLE.HOS.Services.Caps.Types; namespace Ryujinx.HLE.HOS.Services.Caps { @@ -11,12 +12,87 @@ namespace Ryujinx.HLE.HOS.Services.Caps // SetShimLibraryVersion(pid, u64, nn::applet::AppletResourceUserId) public ResultCode SetShimLibraryVersion(ServiceCtx context) { - ulong shimLibraryVersion = context.RequestData.ReadUInt64(); + return context.Device.System.CaptureManager.SetShimLibraryVersion(context); + } + + [Command(203)] + // SaveScreenShotEx0(bytes<0x40> ScreenShotAttribute, u32 unknown, u64 AppletResourceUserId, pid, buffer ScreenshotData) -> bytes<0x20> ApplicationAlbumEntry + public ResultCode SaveScreenShotEx0(ServiceCtx context) + { + // TODO: Use the ScreenShotAttribute. + ScreenShotAttribute screenShotAttribute = context.RequestData.ReadStruct(); + + uint unknown = context.RequestData.ReadUInt32(); + ulong appletResourceUserId = context.RequestData.ReadUInt64(); + ulong pidPlaceholder = context.RequestData.ReadUInt64(); + + long screenshotDataPosition = context.Request.SendBuff[0].Position; + long screenshotDataSize = context.Request.SendBuff[0].Size; + + byte[] screenshotData = context.Memory.GetSpan((ulong)screenshotDataPosition, (int)screenshotDataSize, true).ToArray(); + + ResultCode resultCode = context.Device.System.CaptureManager.SaveScreenShot(screenshotData, appletResourceUserId, context.Device.Application.TitleId, out ApplicationAlbumEntry applicationAlbumEntry); + + context.ResponseData.WriteStruct(applicationAlbumEntry); + + return resultCode; + } + + [Command(205)] // 8.0.0+ + // SaveScreenShotEx1(bytes<0x40> ScreenShotAttribute, u32 unknown, u64 AppletResourceUserId, pid, buffer ApplicationData, buffer ScreenshotData) -> bytes<0x20> ApplicationAlbumEntry + public ResultCode SaveScreenShotEx1(ServiceCtx context) + { + // TODO: Use the ScreenShotAttribute. + ScreenShotAttribute screenShotAttribute = context.RequestData.ReadStruct(); + + uint unknown = context.RequestData.ReadUInt32(); + ulong appletResourceUserId = context.RequestData.ReadUInt64(); + ulong pidPlaceholder = context.RequestData.ReadUInt64(); + + long applicationDataPosition = context.Request.SendBuff[0].Position; + long applicationDataSize = context.Request.SendBuff[0].Size; + + long screenshotDataPosition = context.Request.SendBuff[1].Position; + long screenshotDataSize = context.Request.SendBuff[1].Size; + + // TODO: Parse the application data: At 0x00 it's UserData (Size of 0x400), at 0x404 it's a uint UserDataSize (Always empty for now). + byte[] applicationData = context.Memory.GetSpan((ulong)applicationDataPosition, (int)applicationDataSize).ToArray(); + + byte[] screenshotData = context.Memory.GetSpan((ulong)screenshotDataPosition, (int)screenshotDataSize, true).ToArray(); + + ResultCode resultCode = context.Device.System.CaptureManager.SaveScreenShot(screenshotData, appletResourceUserId, context.Device.Application.TitleId, out ApplicationAlbumEntry applicationAlbumEntry); + + context.ResponseData.WriteStruct(applicationAlbumEntry); + + return resultCode; + } + + [Command(210)] + // SaveScreenShotEx2(bytes<0x40> ScreenShotAttribute, u32 unknown, u64 AppletResourceUserId, buffer UserIdList, buffer ScreenshotData) -> bytes<0x20> ApplicationAlbumEntry + public ResultCode SaveScreenShotEx2(ServiceCtx context) + { + // TODO: Use the ScreenShotAttribute. + ScreenShotAttribute screenShotAttribute = context.RequestData.ReadStruct(); + + uint unknown = context.RequestData.ReadUInt32(); ulong appletResourceUserId = context.RequestData.ReadUInt64(); - Logger.Stub?.PrintStub(LogClass.ServiceCaps, new { shimLibraryVersion, appletResourceUserId }); + long userIdListPosition = context.Request.SendBuff[0].Position; + long userIdListSize = context.Request.SendBuff[0].Size; - return ResultCode.Success; + long screenshotDataPosition = context.Request.SendBuff[1].Position; + long screenshotDataSize = context.Request.SendBuff[1].Size; + + // TODO: Parse the UserIdList. + byte[] userIdList = context.Memory.GetSpan((ulong)userIdListPosition, (int)userIdListSize).ToArray(); + + byte[] screenshotData = context.Memory.GetSpan((ulong)screenshotDataPosition, (int)screenshotDataSize, true).ToArray(); + + ResultCode resultCode = context.Device.System.CaptureManager.SaveScreenShot(screenshotData, appletResourceUserId, context.Device.Application.TitleId, out ApplicationAlbumEntry applicationAlbumEntry); + + context.ResponseData.WriteStruct(applicationAlbumEntry); + + return resultCode; } } } \ No newline at end of file diff --git a/Ryujinx.HLE/HOS/Services/Caps/ResultCode.cs b/Ryujinx.HLE/HOS/Services/Caps/ResultCode.cs new file mode 100644 index 000000000..c3e4c2cd1 --- /dev/null +++ b/Ryujinx.HLE/HOS/Services/Caps/ResultCode.cs @@ -0,0 +1,17 @@ +namespace Ryujinx.HLE.HOS.Services.Caps +{ + enum ResultCode + { + ModuleId = 206, + ErrorCodeShift = 9, + + Success = 0, + + InvalidArgument = (2 << ErrorCodeShift) | ModuleId, + ShimLibraryVersionAlreadySet = (7 << ErrorCodeShift) | ModuleId, + OutOfRange = (8 << ErrorCodeShift) | ModuleId, + NullOutputBuffer = (141 << ErrorCodeShift) | ModuleId, + NullInputBuffer = (142 << ErrorCodeShift) | ModuleId, + BlacklistedPid = (822 << ErrorCodeShift) | ModuleId + } +} \ No newline at end of file diff --git a/Ryujinx.HLE/HOS/Services/Caps/Types/AlbumFileDateTime.cs b/Ryujinx.HLE/HOS/Services/Caps/Types/AlbumFileDateTime.cs new file mode 100644 index 000000000..b9bc799c0 --- /dev/null +++ b/Ryujinx.HLE/HOS/Services/Caps/Types/AlbumFileDateTime.cs @@ -0,0 +1,16 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Caps.Types +{ + [StructLayout(LayoutKind.Sequential, Size = 0x8)] + struct AlbumFileDateTime + { + public ushort Year; + public byte Month; + public byte Day; + public byte Hour; + public byte Minute; + public byte Second; + public byte UniqueId; + } +} \ No newline at end of file diff --git a/Ryujinx.HLE/HOS/Services/Caps/Types/AlbumImageOrientation.cs b/Ryujinx.HLE/HOS/Services/Caps/Types/AlbumImageOrientation.cs new file mode 100644 index 000000000..479675d67 --- /dev/null +++ b/Ryujinx.HLE/HOS/Services/Caps/Types/AlbumImageOrientation.cs @@ -0,0 +1,10 @@ +namespace Ryujinx.HLE.HOS.Services.Caps.Types +{ + enum AlbumImageOrientation : uint + { + Degrees0, + Degrees90, + Degrees180, + Degrees270 + } +} \ No newline at end of file diff --git a/Ryujinx.HLE/HOS/Services/Caps/Types/AlbumStorage.cs b/Ryujinx.HLE/HOS/Services/Caps/Types/AlbumStorage.cs new file mode 100644 index 000000000..cfe6c1e04 --- /dev/null +++ b/Ryujinx.HLE/HOS/Services/Caps/Types/AlbumStorage.cs @@ -0,0 +1,8 @@ +namespace Ryujinx.HLE.HOS.Services.Caps.Types +{ + enum AlbumStorage : byte + { + Nand, + Sd + } +} \ No newline at end of file diff --git a/Ryujinx.HLE/HOS/Services/Caps/Types/ApplicationAlbumEntry.cs b/Ryujinx.HLE/HOS/Services/Caps/Types/ApplicationAlbumEntry.cs new file mode 100644 index 000000000..699bb4180 --- /dev/null +++ b/Ryujinx.HLE/HOS/Services/Caps/Types/ApplicationAlbumEntry.cs @@ -0,0 +1,17 @@ +using Ryujinx.Common.Memory; +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Caps.Types +{ + [StructLayout(LayoutKind.Sequential, Size = 0x20)] + struct ApplicationAlbumEntry + { + public ulong Size; + public ulong TitleId; + public AlbumFileDateTime AlbumFileDateTime; + public AlbumStorage AlbumStorage; + public ContentType ContentType; + public Array5 Padding; + public byte Unknown0x1f; // Always 1 + } +} \ No newline at end of file diff --git a/Ryujinx.HLE/HOS/Services/Caps/Types/ContentType.cs b/Ryujinx.HLE/HOS/Services/Caps/Types/ContentType.cs new file mode 100644 index 000000000..c1e7f0fcc --- /dev/null +++ b/Ryujinx.HLE/HOS/Services/Caps/Types/ContentType.cs @@ -0,0 +1,9 @@ +namespace Ryujinx.HLE.HOS.Services.Caps.Types +{ + enum ContentType : byte + { + Screenshot, + Movie, + ExtraMovie + } +} \ No newline at end of file diff --git a/Ryujinx.HLE/HOS/Services/Caps/Types/ScreenShotAttribute.cs b/Ryujinx.HLE/HOS/Services/Caps/Types/ScreenShotAttribute.cs new file mode 100644 index 000000000..5528379ad --- /dev/null +++ b/Ryujinx.HLE/HOS/Services/Caps/Types/ScreenShotAttribute.cs @@ -0,0 +1,15 @@ +using Ryujinx.Common.Memory; +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Caps.Types +{ + [StructLayout(LayoutKind.Sequential, Size = 0x40)] + struct ScreenShotAttribute + { + public uint Unknown0x00; // Always 0 + public AlbumImageOrientation AlbumImageOrientation; + public uint Unknown0x08; // Always 0 + public uint Unknown0x0C; // Always 1 + public Array30 Unknown0x10; // Always 0 + } +} \ No newline at end of file diff --git a/Ryujinx.HLE/Ryujinx.HLE.csproj b/Ryujinx.HLE/Ryujinx.HLE.csproj index 3d48d893f..c9f30280e 100644 --- a/Ryujinx.HLE/Ryujinx.HLE.csproj +++ b/Ryujinx.HLE/Ryujinx.HLE.csproj @@ -22,6 +22,7 @@ +