CaptureManager.cs
1 using Ryujinx.Common.Memory; 2 using Ryujinx.HLE.HOS.Services.Caps.Types; 3 using SkiaSharp; 4 using System; 5 using System.IO; 6 using System.Runtime.CompilerServices; 7 using System.Runtime.InteropServices; 8 using System.Security.Cryptography; 9 10 namespace Ryujinx.HLE.HOS.Services.Caps 11 { 12 class CaptureManager 13 { 14 private readonly string _sdCardPath; 15 16 private uint _shimLibraryVersion; 17 18 public CaptureManager(Switch device) 19 { 20 _sdCardPath = FileSystem.VirtualFileSystem.GetSdCardPath(); 21 } 22 23 public ResultCode SetShimLibraryVersion(ServiceCtx context) 24 { 25 ulong shimLibraryVersion = context.RequestData.ReadUInt64(); 26 #pragma warning disable IDE0059 // Remove unnecessary value assignment 27 ulong appletResourceUserId = context.RequestData.ReadUInt64(); 28 #pragma warning restore IDE0059 29 30 // TODO: Service checks if the pid is present in an internal list and returns ResultCode.BlacklistedPid if it is. 31 // The list contents needs to be determined. 32 33 ResultCode resultCode = ResultCode.OutOfRange; 34 35 if (shimLibraryVersion != 0) 36 { 37 if (_shimLibraryVersion == shimLibraryVersion) 38 { 39 resultCode = ResultCode.Success; 40 } 41 else if (_shimLibraryVersion != 0) 42 { 43 resultCode = ResultCode.ShimLibraryVersionAlreadySet; 44 } 45 else if (shimLibraryVersion == 1) 46 { 47 resultCode = ResultCode.Success; 48 49 _shimLibraryVersion = 1; 50 } 51 } 52 53 return resultCode; 54 } 55 56 public ResultCode SaveScreenShot(byte[] screenshotData, ulong appletResourceUserId, ulong titleId, out ApplicationAlbumEntry applicationAlbumEntry) 57 { 58 applicationAlbumEntry = default; 59 60 if (screenshotData.Length == 0) 61 { 62 return ResultCode.NullInputBuffer; 63 } 64 65 /* 66 // NOTE: On our current implementation, appletResourceUserId starts at 0, disable it for now. 67 if (appletResourceUserId == 0) 68 { 69 return ResultCode.InvalidArgument; 70 } 71 */ 72 73 /* 74 // Doesn't occur in our case. 75 if (applicationAlbumEntry == null) 76 { 77 return ResultCode.NullOutputBuffer; 78 } 79 */ 80 81 if (screenshotData.Length >= 0x384000) 82 { 83 DateTime currentDateTime = DateTime.Now; 84 85 applicationAlbumEntry = new ApplicationAlbumEntry() 86 { 87 Size = (ulong)Unsafe.SizeOf<ApplicationAlbumEntry>(), 88 TitleId = titleId, 89 AlbumFileDateTime = new AlbumFileDateTime() 90 { 91 Year = (ushort)currentDateTime.Year, 92 Month = (byte)currentDateTime.Month, 93 Day = (byte)currentDateTime.Day, 94 Hour = (byte)currentDateTime.Hour, 95 Minute = (byte)currentDateTime.Minute, 96 Second = (byte)currentDateTime.Second, 97 UniqueId = 0, 98 }, 99 AlbumStorage = AlbumStorage.Sd, 100 ContentType = ContentType.Screenshot, 101 Padding = new Array5<byte>(), 102 Unknown0x1f = 1, 103 }; 104 105 // 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. 106 string hash = Convert.ToHexString(SHA256.HashData(BitConverter.GetBytes(titleId))).Remove(0x20); 107 string folderPath = Path.Combine(_sdCardPath, "Nintendo", "Album", currentDateTime.Year.ToString("00"), currentDateTime.Month.ToString("00"), currentDateTime.Day.ToString("00")); 108 string filePath = GenerateFilePath(folderPath, applicationAlbumEntry, currentDateTime, hash); 109 110 // TODO: Handle that using the FS service implementation and return the right error code instead of throwing exceptions. 111 Directory.CreateDirectory(folderPath); 112 113 while (File.Exists(filePath)) 114 { 115 applicationAlbumEntry.AlbumFileDateTime.UniqueId++; 116 117 filePath = GenerateFilePath(folderPath, applicationAlbumEntry, currentDateTime, hash); 118 } 119 120 // NOTE: The saved JPEG file doesn't have the limitation in the extra EXIF data. 121 using var bitmap = new SKBitmap(new SKImageInfo(1280, 720, SKColorType.Rgba8888)); 122 Marshal.Copy(screenshotData, 0, bitmap.GetPixels(), screenshotData.Length); 123 using var data = bitmap.Encode(SKEncodedImageFormat.Jpeg, 80); 124 using var file = File.OpenWrite(filePath); 125 data.SaveTo(file); 126 127 return ResultCode.Success; 128 } 129 130 return ResultCode.NullInputBuffer; 131 } 132 133 private string GenerateFilePath(string folderPath, ApplicationAlbumEntry applicationAlbumEntry, DateTime currentDateTime, string hash) 134 { 135 string fileName = $"{currentDateTime:yyyyMMddHHmmss}{applicationAlbumEntry.AlbumFileDateTime.UniqueId:00}-{hash}.jpg"; 136 137 return Path.Combine(folderPath, fileName); 138 } 139 } 140 }