mirror of
https://git.ryujinx.app/ryubing/ryujinx.git
synced 2026-05-14 09:15:46 +00:00
## Description ~~Fixes a fatal CLR crash when `caps` screenshot saving receives an input buffer larger than `0x384000`.~~ ~~Resolves a crash in Tomodachi Life: Living the Dream where saving the pictures to the system's album crashes with 0x80131506.~~ Follow up to #18. This PR adjusts the validation and copy behavior to better match real hardware, and adds logging to make invalid screenshot buffer cases easier to diagnose. Real hardware accepts screenshot buffers with a size greater than or equal to `0x384000`, but only `0x384000` bytes are needed for the 1280x720 RGBA image. This changes screenshot saving to: - reject buffers smaller than `0x384000` - accept buffers equal to or larger than `0x384000` - copy only the first `0x384000` bytes into the 1280x720 bitmap ## Testing Tested with a real Switch NRO using `capssuSaveScreenShotEx0`, `capssuSaveScreenShotEx1`, and `capssuSaveScreenShotEx2`. Observed hardware behavior: ```text 0x384000 => OK 0x384000 - 1 => NullInputBuffer 0x384000 + 1 => OK 0x3C0000 => OK // Tomo life picture size ``` Co-authored-by: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com> Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/44
160 lines
5.8 KiB
C#
160 lines
5.8 KiB
C#
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;
|
|
using System.Security.Cryptography;
|
|
|
|
namespace Ryujinx.HLE.HOS.Services.Caps
|
|
{
|
|
internal class CaptureManager
|
|
{
|
|
public CaptureManager(Switch device)
|
|
{
|
|
_ = device;
|
|
}
|
|
private readonly string _sdCardPath = FileSystem.VirtualFileSystem.GetSdCardPath();
|
|
|
|
private uint _shimLibraryVersion;
|
|
|
|
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)
|
|
{
|
|
ulong shimLibraryVersion = context.RequestData.ReadUInt64();
|
|
#pragma warning disable IDE0059 // Remove unnecessary value assignment
|
|
ulong appletResourceUserId = context.RequestData.ReadUInt64();
|
|
#pragma warning restore IDE0059
|
|
|
|
// 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)
|
|
{
|
|
Logger.Stub?.PrintStub(LogClass.ServiceCaps, new
|
|
{
|
|
appletResourceUserId,
|
|
titleId,
|
|
screenshotDataLength = screenshotData?.Length ?? 0,
|
|
});
|
|
|
|
applicationAlbumEntry = default;
|
|
|
|
if (screenshotData == null || screenshotData.Length == 0)
|
|
{
|
|
return ResultCode.NullInputBuffer;
|
|
}
|
|
|
|
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<ApplicationAlbumEntry>(),
|
|
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<byte>(),
|
|
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;
|
|
}
|
|
|
|
Marshal.Copy(screenshotData, 0, pixels, ScreenshotDataSize);
|
|
|
|
using SKData data = bitmap.Encode(SKEncodedImageFormat.Jpeg, 80);
|
|
using FileStream file = File.OpenWrite(filePath);
|
|
data.SaveTo(file);
|
|
|
|
return ResultCode.Success;
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
}
|