caps: Implement SaveScreenShot calls and cleanup (#2140)

* caps: Implement SaveScreenShot calls and cleanup

This PR implement:
- caps:u IAlbumApplicationService (32) SetShimLibraryVersion
- caps:c IAlbumControlService (33) SetShimLibraryVersion
- caps:su IScreenShotApplicationService (32) SetShimLibraryVersion
- caps:su IScreenShotApplicationService (203/205/210) SaveScreenShotEx0/SaveScreenShotEx1/SaveScreenShotEx2

ImageSharp is used to save the raw screenshot data as a JPG file following what the service does.
All screenshots are save in: `%AppData%\Ryujinx\sdcard\Nintendo\Album` folder. (as example a screenshot file path will be `%AppData%\Ryujinx\sdcard\Nintendo\Album\2021\03\26\2021032601020300-0123456789ABCDEF0123456789ABCDEF.jpg`

This is needed by Animal Crossing: New Horizon where screenshots looks like this:

And this is needed in Monster Hunter Rise but screenshots are currently empty due to another issue.

* remove useless comment

* Addresses gdkchan feedback

* Addresses gdkchan feedback 2

* remove useless comment 2

* Fix nits
This commit is contained in:
Ac_K 2021-03-26 01:16:08 +01:00 committed by GitHub
parent 4bd1ad16f9
commit 32be8caa9d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 327 additions and 10 deletions

View File

@ -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.

View File

@ -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<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
};
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<Rgba32>(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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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<bytes, 0x45> ScreenshotData) -> bytes<0x20> ApplicationAlbumEntry
public ResultCode SaveScreenShotEx0(ServiceCtx context)
{
// TODO: Use the ScreenShotAttribute.
ScreenShotAttribute screenShotAttribute = context.RequestData.ReadStruct<ScreenShotAttribute>();
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<bytes, 0x15> ApplicationData, buffer<bytes, 0x45> ScreenshotData) -> bytes<0x20> ApplicationAlbumEntry
public ResultCode SaveScreenShotEx1(ServiceCtx context)
{
// TODO: Use the ScreenShotAttribute.
ScreenShotAttribute screenShotAttribute = context.RequestData.ReadStruct<ScreenShotAttribute>();
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<bytes, 0x15> UserIdList, buffer<bytes, 0x45> ScreenshotData) -> bytes<0x20> ApplicationAlbumEntry
public ResultCode SaveScreenShotEx2(ServiceCtx context)
{
// TODO: Use the ScreenShotAttribute.
ScreenShotAttribute screenShotAttribute = context.RequestData.ReadStruct<ScreenShotAttribute>();
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;
}
}
}

View File

@ -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
}
}

View File

@ -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;
}
}

View File

@ -0,0 +1,10 @@
namespace Ryujinx.HLE.HOS.Services.Caps.Types
{
enum AlbumImageOrientation : uint
{
Degrees0,
Degrees90,
Degrees180,
Degrees270
}
}

View File

@ -0,0 +1,8 @@
namespace Ryujinx.HLE.HOS.Services.Caps.Types
{
enum AlbumStorage : byte
{
Nand,
Sd
}
}

View File

@ -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<byte> Padding;
public byte Unknown0x1f; // Always 1
}
}

View File

@ -0,0 +1,9 @@
namespace Ryujinx.HLE.HOS.Services.Caps.Types
{
enum ContentType : byte
{
Screenshot,
Movie,
ExtraMovie
}
}

View File

@ -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<byte> Unknown0x10; // Always 0
}
}

View File

@ -22,6 +22,7 @@
<PackageReference Include="Concentus" Version="1.1.7" />
<PackageReference Include="LibHac" Version="0.12.0" />
<PackageReference Include="MsgPack.Cli" Version="1.0.1" />
<PackageReference Include="SixLabors.ImageSharp.Drawing" Version="1.0.0-beta11" />
</ItemGroup>
<!-- Due to Concentus. -->